diff --git a/.gitignore b/.gitignore
index 8edad8791ad08a32febc5e3dd167557e737164b9..406c7f3af33e3247873f2458a5c1ff7cbbc16f9c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,9 +7,9 @@ __pycache__
 .vscode
 .build-cache
 .venv
-artifacts
+/artifacts
 *.d.mk
-templates
+/templates
 /*.apkg
 hanzi-data
 .data-test-ok
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 6865d465db36274abbb85caec3c22ed7ddfa0ac3..4e3030bf89d1a8652ff83e40d757a9331c2c43c0 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -27,8 +27,7 @@ build:
     - echo RELEASE_TAR_URL=$RELEASE_TAR_URL >> build.env
   artifacts:
     paths:
-      - sinologie-anki-pack-*
-      - ANNOUNCEMENT
+      - card-templates-*.tar.gz
     reports:
       dotenv: build.env
 
@@ -48,7 +47,7 @@ release-package-json-version-as-git-tag:
     - if [ -z "$NPM_VERSION_GIT_TAG" ]; then
     -     echo adding git tag for first commit on main with NPM version ${NPM_VERSION}
     -     git remote remove origin
-    -     git remote add origin https://oauth:${REPOSITORY_ACCESS_TOKEN}@gitlab.phaidra.org/kartenaale/sinologie-anki-pack.git
+    -     git remote add origin https://oauth:${REPOSITORY_ACCESS_TOKEN}@gitlab.phaidra.org/kartenaale/card-templates.git
     -     git config user.email Cao Cao
     -     git config user.name cao.cao@ci.kartenaale
     -     git tag -a $NPM_VERSION -m "Release $NPM_VERSION"
@@ -70,9 +69,12 @@ create-gitlab-release:
     GIT_STRATEGY: none
   # we don't need anything in node_modules or python, so don't fetch the cache
   cache: []
+  script:
+    - echo Creating GitLab release…
   release:
     tag_name: '$CI_COMMIT_TAG'
     name: 'Card Templates $CI_COMMIT_TAG'
+    description: '$RELEASE_TAR is ready for download.' 
     assets:
       links:
         - name: '$RELEASE_TAR'
diff --git a/src/templates/bijective/.template-spec.yaml b/src/templates/bijective/.template-spec.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..ed45670a2d8a25a4528788b12ec8e6095e18541a
--- /dev/null
+++ b/src/templates/bijective/.template-spec.yaml
@@ -0,0 +1,16 @@
+template_version: 2024-02-28 22:00:00+00:00
+
+note_type:
+  id: 2024-01-05 03:00:00+00:00
+  name: Bijection
+  fields:
+  - A
+  - B
+
+card_types:
+- name: Forward
+  template: forward
+- name: Backward
+  template: backward
+
+resource_paths: []
diff --git a/src/templates/bijective/backward/back.html b/src/templates/bijective/backward/back.html
new file mode 100644
index 0000000000000000000000000000000000000000..84534fbef4bf17dc733bf2504a2833cf24a1529d
--- /dev/null
+++ b/src/templates/bijective/backward/back.html
@@ -0,0 +1,23 @@
+<div class="front-side-on-back">
+  {{FrontSide}}
+</div>
+
+<hr id="answer">
+
+<div class="exercise back bijective-backward-back">
+  <div class="bijective-answer">
+    {{A}}
+  </div>
+
+  <include src="src/components/notice/notice.html"></include>
+</div>
+
+<!-- Suppress speech output on AnkiDroid if globally enabled -->
+<tts style="display: none" service="android" voice="zh_CN"></tts>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/back'
+</script>
\ No newline at end of file
diff --git a/src/templates/bijective/backward/front.html b/src/templates/bijective/backward/front.html
new file mode 100644
index 0000000000000000000000000000000000000000..8fb070ca68fe2b6782da088c8d6be88c61c0c954
--- /dev/null
+++ b/src/templates/bijective/backward/front.html
@@ -0,0 +1,29 @@
+<style>
+  @import url(../../../components/global.css);
+  @import url(../../../components/facts.css);
+</style>
+
+<div class="exercise front bijective-backward-front">
+  <header class="card-info">
+    <aside class="exercise-category">
+      {{Subdeck}}
+    </aside>
+    <aside class="exercise-kind">
+      {{Card}}
+    </aside>
+  </header>
+
+  <div class="prompt bijective-question">
+    {{B}}
+  </div>
+</div>
+
+<!-- Suppress speech output on AnkiDroid if globally enabled -->
+<tts style="display: none" service="android" voice="zh_CN"></tts>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/front'
+</script>
\ No newline at end of file
diff --git a/src/templates/bijective/forward/back.html b/src/templates/bijective/forward/back.html
new file mode 100644
index 0000000000000000000000000000000000000000..afbae1a920ec242f9d2fed054e70b5a6d895e16a
--- /dev/null
+++ b/src/templates/bijective/forward/back.html
@@ -0,0 +1,23 @@
+<div class="front-side-on-back">
+  {{FrontSide}}
+</div>
+
+<hr id="answer">
+
+<div class="exercise back bijective-forward-back">
+  <div class="bijective-answer">
+    {{B}}
+  </div>
+
+  <include src="src/components/notice/notice.html"></include>
+</div>
+
+<!-- Suppress speech output on AnkiDroid if globally enabled -->
+<tts style="display: none" service="android" voice="zh_CN"></tts>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/back'
+</script>
\ No newline at end of file
diff --git a/src/templates/bijective/forward/front.html b/src/templates/bijective/forward/front.html
new file mode 100644
index 0000000000000000000000000000000000000000..6552410c728b94ee4edcb7f7bbbe9a20fd07e714
--- /dev/null
+++ b/src/templates/bijective/forward/front.html
@@ -0,0 +1,29 @@
+<style>
+  @import url(../../../components/global.css);
+  @import url(../../../components/facts.css);
+</style>
+
+<div class="exercise front bijective-forward-front">
+  <header class="card-info">
+    <aside class="exercise-category">
+      {{Subdeck}}
+    </aside>
+    <aside class="exercise-kind">
+      {{Card}}
+    </aside>
+  </header>
+
+  <div class="prompt bijective-question">
+    {{A}}
+  </div>
+</div>
+
+<!-- Suppress speech output on AnkiDroid if globally enabled -->
+<tts style="display: none" service="android" voice="zh_CN"></tts>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/front'
+</script>
\ No newline at end of file
diff --git a/src/templates/facts/.template-spec.yaml b/src/templates/facts/.template-spec.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..fe384c1e79bf439cf2299d42295600564245698c
--- /dev/null
+++ b/src/templates/facts/.template-spec.yaml
@@ -0,0 +1,14 @@
+template_version: 2024-02-28 22:00:00+00:00
+
+note_type:
+  id: 2024-01-04 03:00:00+00:00
+  name: Facts
+  fields:
+  - Front
+  - Back
+
+card_types:
+- name: Q/A
+  template: q_a
+
+resource_paths: []
diff --git a/src/templates/facts/q_a/back.html b/src/templates/facts/q_a/back.html
new file mode 100644
index 0000000000000000000000000000000000000000..1ac8a0788f6fcbe64b2bd300a560d7e6648180a2
--- /dev/null
+++ b/src/templates/facts/q_a/back.html
@@ -0,0 +1,23 @@
+<div class="front-side-on-back">
+  {{FrontSide}}
+</div>
+
+<hr id="answer">
+
+<div class="exercise back facts-q-a-back">
+  <div class="facts-answer">
+    {{Back}}
+  </div>
+
+  <include src="src/components/notice/notice.html"></include>
+</div>
+
+<!-- Suppress speech output on AnkiDroid if globally enabled -->
+<tts style="display: none" service="android" voice="zh_CN"></tts>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/back'
+</script>
\ No newline at end of file
diff --git a/src/templates/facts/q_a/front.html b/src/templates/facts/q_a/front.html
new file mode 100644
index 0000000000000000000000000000000000000000..9dce46e5f30d7eca54411614df3c55003fc2b7d2
--- /dev/null
+++ b/src/templates/facts/q_a/front.html
@@ -0,0 +1,29 @@
+<style>
+  @import url(../../../components/global.css);
+  @import url(../../../components/facts.css);
+</style>
+
+<div class="exercise front facts-q-a-front">
+  <header class="card-info">
+    <aside class="exercise-category">
+      {{Subdeck}}
+    </aside>
+    <aside class="exercise-kind">
+      {{Card}}
+    </aside>
+  </header>
+
+  <div class="prompt facts-question">
+    {{Front}}
+  </div>
+</div>
+
+<!-- Suppress speech output on AnkiDroid if globally enabled -->
+<tts style="display: none" service="android" voice="zh_CN"></tts>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/front'
+</script>
\ No newline at end of file
diff --git a/src/templates/hanzi/.template-spec.yaml b/src/templates/hanzi/.template-spec.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..0908f3094ee9c9766d2a2330c2fa076da85480a9
--- /dev/null
+++ b/src/templates/hanzi/.template-spec.yaml
@@ -0,0 +1,26 @@
+template_version: 2024-03-20 10:00:00+00:00
+
+note_type:
+  id: 2024-02-21 12:00:00+00:00
+  name: Hanzi
+  fields:
+  - Keyword
+  - Keyword type
+  - Notes (Front)
+  - Hanzi
+  - Book
+  - Lesson
+  - Frame
+  - Order (Sequential)
+  - Order (Parallel)
+  - resources
+  - Notes (Back)
+  - Traditional
+  - Simplified
+
+card_types:
+- name: Schreiben
+  template: write
+
+resource_paths:
+- '{{BUILD_PREFIX}}hanzi-data'
diff --git a/src/templates/hanzi/write/back.html b/src/templates/hanzi/write/back.html
new file mode 100644
index 0000000000000000000000000000000000000000..b176010e465ffba032bb66d8f77781403597df87
--- /dev/null
+++ b/src/templates/hanzi/write/back.html
@@ -0,0 +1,28 @@
+<div class="front-side-on-back">
+  {{FrontSide}}
+</div>
+
+<hr id="answer">
+
+<div class="exercise back hanzi-write-back">
+  <dl class="translations">
+    <dt>Stroke order</dt>
+    <dd class="strichfolge-animation is-large{{#Traditional}} is-traditional{{/Traditional}}">{{text:Hanzi}}</dd>
+    <dt>Notes</dt>
+    <dd>{{Notes (Back)}}</dd>
+    <dt>Print form</dt>
+    <dd><span class="hanzi-print is-large">{{Hanzi}}</span></dd>
+  </dl>
+
+  <include src="src/components/notice/notice.html"></include>
+</div>
+
+<!-- Suppress speech output on AnkiDroid if globally enabled -->
+<tts style="display: none" service="android" voice="zh_CN"></tts>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/back'
+</script>
\ No newline at end of file
diff --git a/src/templates/hanzi/write/front.html b/src/templates/hanzi/write/front.html
new file mode 100644
index 0000000000000000000000000000000000000000..f58e910788efc3e0ca0a7c49d4d243ffac4cb141
--- /dev/null
+++ b/src/templates/hanzi/write/front.html
@@ -0,0 +1,36 @@
+{{#Hanzi}}{{#Keyword}}
+<style>
+  @import url(../../../components/global.css);
+  @import url(../../../components/heisig.css);
+</style>
+
+<div class="exercise front hanzi-write-front">
+  <header class="card-info">
+    <aside class="exercise-kind">
+      {{Frame}}
+    </aside>
+  </header>
+
+  <dl class="prompt translations">
+    <dt>Key word</dt>
+    <dd>
+      <span class="hanzi-keyword">{{Keyword}}</span>
+      {{#Keyword type}}
+      <span class="hanzi-keyword-type">({{Keyword type}})</span>
+      {{/Keyword type}}
+    </dd>
+    <dt>Notes</dt>
+    <dd>{{Notes (Front)}}</dd>
+  </dl>
+</div>
+
+<!-- Suppress speech output on AnkiDroid if globally enabled -->
+<tts style="display: none" service="android" voice="zh_CN"></tts>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/front'
+</script>
+{{/Keyword}}{{/Hanzi}}
\ No newline at end of file
diff --git a/src/templates/index.html b/src/templates/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..700a6125d0dd74ff316a4999a83d204295f79391
--- /dev/null
+++ b/src/templates/index.html
@@ -0,0 +1,90 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+  <meta charset="UTF-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Anki Template Hanyu Overview</title>
+</head>
+
+<body>
+  <h1>Overview</h1>
+
+  <h2>templates/bijective</h2>
+  <h3>Forward</h3>
+  <h4>Front</h4>
+  <a href="bijective/backward/front.html">forward/front.html</a>
+  <h4>Back</h4>
+  <a href="bijective/backward/back.html">forward/back.html</a>
+  <h3>Backward</h3>
+  <h4>Front</h4>
+  <a href="bijective/forward/front.html">backward/front.html</a>
+  <h4>Back</h4>
+  <a href="bijective/forward/back.html">backward/back.html</a>
+
+  <h2>templates/facts</h2>
+  <h3>Q/A</h3>
+  <h4>Front</h4>
+  <a href="facts/q_a/front.html">q_a/front.html</a>
+  <h4>Back</h4>
+  <a href="facts/q_a/back.html"> q_a/back.html</a>
+
+  <h2>templates/hanzi</h2>
+  <h3>Write</h3>
+  <h4>Front</h4>
+  <a href="hanzi/write/front.html">write/front.html</a>
+  <h4>Back</h4>
+  <a href="hanzi/write/back.html">write/back.html</a>
+
+  <h2>templates/molaoshi</h2>
+  <h3>Hear</h3>
+  <h4>Front</h4>
+  <a href="molaoshi/hear/front.html">hear/front.html</a>
+  <h4>Back</h4>
+  <a href="molaoshi/hear/back.html">hear/back.html</a>
+
+  <h3>Read Hanzi</h3>
+  <h4>Front</h4>
+  <a href="molaoshi/read_hanzi/front.html">read_hanzi/front.html</a>
+  <h4>Back</h4>
+  <a href="molaoshi/read_hanzi/back.html">read_hanzi/back.html</a>
+
+  <h3>Read Hanzi (traditional)</h3>
+  <h4>Front</h4>
+  <a href="molaoshi/read_hanzi_traditional/front.html">read_hanzi_traditional/front.html</a>
+  <h4>Back</h4>
+  <a href="molaoshi/read_hanzi_traditional/back.html">read_hanzi_traditional/back.html</a>
+
+  <h3>Read Pinyin</h3>
+  <h4>Front</h4>
+  <a href="molaoshi/read_pinyin/front.html">read_pinyin/front.html</a>
+  <h4>Back</h4>
+  <a href="molaoshi/read_pinyin/back.html">read_pinyin/back.html</a>
+
+  <h3>Speak</h3>
+  <h4>Front</h4>
+  <a href="molaoshi/speak/front.html">speak/front.html</a>
+  <h4>Back</h4>
+  <a href="molaoshi/speak/back.html">speak/back.html</a>
+
+  <h3>Write</h3>
+  <h4>Front</h4>
+  <a href="molaoshi/write/front.html">write/front.html</a>
+  <h4>Back</h4>
+  <a href="molaoshi/write/back.html">write/back.html</a>
+
+  <h3>Identify radical</h3>
+  <h4>Front</h4>
+  <a href="molaoshi/identify_radical/front.html">identify_radical/front.html</a>
+  <h4>Back</h4>
+  <a href="molaoshi/identify_radical/back.html">identify_radical/back.html</a>
+
+  <h3>Identify radical (traditional)</h3>
+  <h4>Front</h4>
+  <a href="molaoshi/identify_radical_traditional/front.html">identify_radical_traditional/front.html</a>
+  <h4>Back</h4>
+  <a href="molaoshi/identify_radical_traditional/back.html">identify_radical_traditional/back.html</a>
+</body>
+
+</html>
\ No newline at end of file
diff --git a/src/templates/molaoshi/.template-spec.yaml b/src/templates/molaoshi/.template-spec.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5f982c729ae2c0b918880490d4f06b90c13f0568
--- /dev/null
+++ b/src/templates/molaoshi/.template-spec.yaml
@@ -0,0 +1,45 @@
+template_version: 2024-06-14 12:00:00+00:00
+
+note_type:
+  id: 2024-02-20 12:00:00+00:00
+  name: Vokabeln
+  fields:
+  - Deutsch
+  - 简体字
+  - 繁體字
+  - Pīnyīn
+  - Bemerkungen
+  - Beispiele
+  - Lektion
+  # this is only necessary to make explicit to keep the later added fields
+  # id contents
+  - resources
+  - Bemerkungen (Vorderseite)
+  - Standardaussprache
+  - Audioaufnahme
+  - Radikal finden anlegen
+  - Radikal finden (繁體字) anlegen
+  - Schreiben anlegen
+  - Zhuyin
+  - Lesen (繁體字) anlegen
+
+card_types:
+- name: Hören
+  template: hear
+- name: Lesen (Pīnyīn)
+  template: read_pinyin
+- name: Lesen (简体字)
+  template: read_hanzi
+- name: Lesen (繁體字)
+  template: read_hanzi_traditional
+- name: Schreiben
+  template: write
+- name: Sprechen
+  template: speak
+- name: Radikal finden (简体字)
+  template: identify_radical
+- name: Radikal finden (繁體字)
+  template: identify_radical_traditional
+
+resource_paths:
+- '{{BUILD_PREFIX}}hanzi-data'
diff --git a/src/templates/molaoshi/hear/back.html b/src/templates/molaoshi/hear/back.html
new file mode 100644
index 0000000000000000000000000000000000000000..a32ec6f63bf33e981ed055582dc2699bd5198b39
--- /dev/null
+++ b/src/templates/molaoshi/hear/back.html
@@ -0,0 +1,68 @@
+<div class="front-side-on-back">
+  {{FrontSide}}
+</div>
+
+<hr id="answer">
+
+<div class="exercise back anki-template-hanyu-hear-back">
+  <dl class="translations">
+    <dt>Pīnyīn</dt>
+    <dd>{{Pīnyīn}}</dd>
+    <dt>Deutsch</dt>
+    <dd>{{Deutsch}}</dd>
+    <dt>简体字</dt>
+    <dd>
+      <span class="hanzi-print">{{简体字}}</span>
+      <div class="strichfolge-animation" no-animate="true" highlight-radical="true">
+        {{text:简体字}}
+      </div>
+    </dd>
+    <dt>Bemerkungen</dt>
+    <dd>{{Bemerkungen}}</dd>
+    <dt>Beispiele</dt>
+    <dd>{{Beispiele}}</dd>
+    <details class="answer-details">
+      <summary>
+        <span class="answer-details-more">Mehr…</span>
+        <span class="answer-details-less">Weniger…</span>
+      </summary>
+      {{#繁體字}}
+      <dt>繁體字</dt>
+      <dd>
+        <span class="hanzi-print">{{繁體字}}</span>
+        <div class="strichfolge-animation is-traditional" no-animate="true" highlight-radical="true">
+          {{text:繁體字}}
+        </div>
+      </dd>
+      {{/繁體字}}
+      <dt>Radikal (简体字)</dt>
+      <dd>
+        <span hanzi-data="{{text:简体字}}" hanzi-prop="radical" class="hanzi-print"></span>
+        <div class="strichfolge-animation" no-animate="true">
+          <span hanzi-data="{{text:简体字}}" hanzi-prop="radical"></span>
+        </div>
+      </dd>
+      {{#繁體字}}
+      <dt>Radikal (繁體字)</dt>
+      <dd>
+        <span hanzi-data="{{text:繁體字}}" hanzi-kind="traditional" hanzi-prop="radical" class="hanzi-print"></span>
+        <div class="strichfolge-animation is-traditional" no-animate="true">
+          <span hanzi-data="{{text:繁體字}}" hanzi-kind="traditional" hanzi-prop="radical"></span>
+        </div>
+      </dd>
+      {{/繁體字}}
+    </details>
+  </dl>
+
+  <include src="src/components/notice/notice.html"></include>
+</div>
+
+<!-- Repeat only relevant text on the back side on AnkiDroid, not all of it -->
+<tts style="display: none" service="android" voice="zh_CN">{{text:简体字}}</tts>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/back'
+</script>
\ No newline at end of file
diff --git a/src/templates/molaoshi/hear/front.html b/src/templates/molaoshi/hear/front.html
new file mode 100644
index 0000000000000000000000000000000000000000..6d7b05d3ad1767c89f7ba114455ea4b6f99682d1
--- /dev/null
+++ b/src/templates/molaoshi/hear/front.html
@@ -0,0 +1,45 @@
+{{#Standardaussprache}}
+<style>
+  @import url(../../../components/global.css);
+</style>
+
+<div class="exercise front anki-template-hanyu-hear-front">
+  <header class="card-info">
+    <aside class="exercise-category">
+      {{Subdeck}}
+    </aside>
+    <aside class="exercise-kind">
+      {{Card}}
+    </aside>
+  </header>
+  </header>
+
+  <dl class="prompt translations">
+    <dt>汉语</dt>
+    <dd id="t2s-player-container" class="t2s-player-container">
+      <div class="anki-droid-player">
+        <tts style="display: none" service="android" voice="zh_CN">
+          {{text:Standardaussprache}}
+        </tts>
+      </div>
+      <div class="anki-web-player">
+        {{text:Standardaussprache}}
+      </div>
+      <div class="anki-builtin-player">
+        {{tts zh_CN:Standardaussprache}}
+      </div>
+    </dd>
+    <dt>Aufnahme</dt>
+    <dd>{{Audioaufnahme}}</dd>
+    <dt>Bemerkungen</dt>
+    <dd>{{Bemerkungen (Vorderseite)}}</dd>
+  </dl>
+</div>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/front'
+</script>
+{{/Standardaussprache}}
\ No newline at end of file
diff --git a/src/templates/molaoshi/identify_radical/back.html b/src/templates/molaoshi/identify_radical/back.html
new file mode 100644
index 0000000000000000000000000000000000000000..0d1a17ecb421718485288e2dd8bfa750e56f1128
--- /dev/null
+++ b/src/templates/molaoshi/identify_radical/back.html
@@ -0,0 +1,35 @@
+<div class="front-side-on-back">
+  {{FrontSide}}
+</div>
+
+<hr id="answer">
+
+<div class="exercise back radicals-identify-back">
+  <dl class="translations">
+    <dt>Radikal (简体字)</dt>
+    <dd>
+      <span hanzi-data="{{text:简体字}}" hanzi-prop="radical" class="hanzi-print"></span>
+      <div class="strichfolge-animation" no-animate="true">
+        <span hanzi-data="{{text:简体字}}" hanzi-prop="radical"></span>
+      </div>
+    </dd>
+    <dt>Name (中文)</dt>
+    <dd hanzi-data="{{text:简体字}}" hanzi-prop="radicalMeaningZh"></dd>
+    <dt>Name (Deutsch)</dt>
+    <dd hanzi-data="{{text:简体字}}" hanzi-prop="radicalMeaningDe"></dd>
+    <dt>Strichzahl</dt>
+    <dd hanzi-data="{{text:简体字}}" hanzi-prop="count"></dd>
+  </dl>
+
+  <include src="src/components/notice/notice.html"></include>
+</div>
+
+<!-- Suppress speech output on AnkiDroid if globally enabled -->
+<tts style="display: none" service="android" voice="zh_CN"></tts>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/back'
+</script>
\ No newline at end of file
diff --git a/src/templates/molaoshi/identify_radical/front.html b/src/templates/molaoshi/identify_radical/front.html
new file mode 100644
index 0000000000000000000000000000000000000000..e0302ce0df617461cbbfc73f7d96eaf1e4de6d4f
--- /dev/null
+++ b/src/templates/molaoshi/identify_radical/front.html
@@ -0,0 +1,39 @@
+{{#简体字}}{{#Radikal finden anlegen}}
+<style>
+  @import url(../../../components/global.css);
+</style>
+
+<div class="exercise front radicals-identify-front">
+  <header class="card-info">
+    <aside class="exercise-category">
+      {{Subdeck}}
+    </aside>
+    <aside class="exercise-kind">
+      {{Card}}
+    </aside>
+  </header>
+  </header>
+
+  <dl class="prompt translations">
+    <dt>简体字</dt>
+    <dd>
+      <span class="hanzi-print">{{简体字}}</span>
+      <div class="strichfolge-animation" no-animate="true" highlight-radical="answer">
+        {{text:简体字}}
+      </div>
+    </dd>
+    <dt>Bemerkungen</dt>
+    <dd>{{Bemerkungen (Vorderseite)}}</dd>
+  </dl>
+</div>
+
+<!-- Suppress speech output on AnkiDroid if globally enabled -->
+<tts style="display: none" service="android" voice="zh_CN"></tts>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/front'
+</script>
+{{/Radikal finden anlegen}}{{/简体字}}
\ No newline at end of file
diff --git a/src/templates/molaoshi/identify_radical_traditional/back.html b/src/templates/molaoshi/identify_radical_traditional/back.html
new file mode 100644
index 0000000000000000000000000000000000000000..7a642befa6dfc17535bd8a3ccd20f3cb490a1449
--- /dev/null
+++ b/src/templates/molaoshi/identify_radical_traditional/back.html
@@ -0,0 +1,35 @@
+<div class="front-side-on-back">
+  {{FrontSide}}
+</div>
+
+<hr id="answer">
+
+<div class="exercise back radicals-identify-back">
+  <dl class="translations">
+    <dt>Radikal (繁體字)</dt>
+    <dd>
+      <span hanzi-data="{{text:繁體字}}" hanzi-kind="traditional" hanzi-prop="radical" class="hanzi-print"></span>
+      <div class="strichfolge-animation is-traditional" no-animate="true">
+        <span hanzi-data="{{text:繁體字}}" hanzi-kind="traditional" hanzi-prop="radical"></span>
+      </div>
+    </dd>
+    <dt>Name (中文)</dt>
+    <dd hanzi-data="{{text:繁體字}}" hanzi-kind="traditional" hanzi-prop="radicalMeaningZh"></dd>
+    <dt>Name (Deutsch)</dt>
+    <dd hanzi-data="{{text:繁體字}}" hanzi-kind="traditional" hanzi-prop="radicalMeaningDe"></dd>
+    <dt>Strichzahl</dt>
+    <dd hanzi-data="{{text:繁體字}}" hanzi-kind="traditional" hanzi-prop="count"></dd>
+  </dl>
+
+  <include src="src/components/notice/notice.html"></include>
+</div>
+
+<!-- Suppress speech output on AnkiDroid if globally enabled -->
+<tts style="display: none" service="android" voice="zh_CN"></tts>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/back'
+</script>
\ No newline at end of file
diff --git a/src/templates/molaoshi/identify_radical_traditional/front.html b/src/templates/molaoshi/identify_radical_traditional/front.html
new file mode 100644
index 0000000000000000000000000000000000000000..ea10b1cae3adc893ae9c63295f815b1e35ea3647
--- /dev/null
+++ b/src/templates/molaoshi/identify_radical_traditional/front.html
@@ -0,0 +1,39 @@
+{{#繁體字}}{{#Radikal finden (繁體字) anlegen}}
+<style>
+  @import url(../../../components/global.css);
+</style>
+
+<div class="exercise front radicals-identify-front">
+  <header class="card-info">
+    <aside class="exercise-category">
+      {{Subdeck}}
+    </aside>
+    <aside class="exercise-kind">
+      {{Card}}
+    </aside>
+  </header>
+  </header>
+
+  <dl class="prompt translations">
+    <dt>繁體字</dt>
+    <dd>
+      <span class="hanzi-print">{{繁體字}}</span>
+      <div class="strichfolge-animation is-traditional" no-animate="true" highlight-radical="answer">
+        {{text:繁體字}}
+      </div>
+    </dd>
+    <dt>Bemerkungen</dt>
+    <dd>{{Bemerkungen (Vorderseite)}}</dd>
+  </dl>
+</div>
+
+<!-- Suppress speech output on AnkiDroid if globally enabled -->
+<tts style="display: none" service="android" voice="zh_CN"></tts>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/front'
+</script>
+{{/Radikal finden (繁體字) anlegen}}{{/繁體字}}
\ No newline at end of file
diff --git a/src/templates/molaoshi/read_hanzi/back.html b/src/templates/molaoshi/read_hanzi/back.html
new file mode 100644
index 0000000000000000000000000000000000000000..a16bcfd82015db00e0c4a68db99b5cd33cca6372
--- /dev/null
+++ b/src/templates/molaoshi/read_hanzi/back.html
@@ -0,0 +1,76 @@
+<div class="front-side-on-back">
+  {{FrontSide}}
+</div>
+
+<hr id="answer">
+
+<div class="exercise back anki-template-hanyu-read-hanzi-back">
+  <dl class="translations">
+    <dt>Pīnyīn</dt>
+    <dd>{{Pīnyīn}}</dd>
+    <dt>Deutsch</dt>
+    <dd>{{Deutsch}}</dd>
+    {{#Standardaussprache}}
+    <dt class="t2s-player-heading">汉语</dt>
+    <dd id="t2s-player-container" class="t2s-player-container">
+      <div class="anki-droid-player">
+        <tts style="display: none" service="android" voice="zh_CN">
+          {{text:Standardaussprache}}
+        </tts>
+      </div>
+      <div class="anki-web-player">
+        {{text:Standardaussprache}}
+      </div>
+      <div class="anki-builtin-player">
+        {{tts zh_CN:Standardaussprache}}
+      </div>
+    </dd>
+    {{/Standardaussprache}}
+    <dt>Aufnahme</dt>
+    <dd>{{Audioaufnahme}}</dd>
+    <dt>Bemerkungen</dt>
+    <dd>{{Bemerkungen}}</dd>
+    <dt>Beispiele</dt>
+    <dd>{{Beispiele}}</dd>
+    <details class="answer-details">
+      <summary>
+        <span class="answer-details-more">Mehr…</span>
+        <span class="answer-details-less">Weniger…</span>
+      </summary>
+      {{#繁體字}}
+      <dt>繁體字</dt>
+      <dd>
+        <span class="hanzi-print">{{繁體字}}</span>
+        <div class="strichfolge-animation is-traditional" no-animate="true" highlight-radical="true">
+          {{text:繁體字}}
+        </div>
+      </dd>
+      {{/繁體字}}
+      <dt>Radikal (简体字)</dt>
+      <dd>
+        <span hanzi-data="{{text:简体字}}" hanzi-prop="radical" class="hanzi-print"></span>
+        <div class="strichfolge-animation" no-animate="true">
+          <span hanzi-data="{{text:简体字}}" hanzi-prop="radical"></span>
+        </div>
+      </dd>
+      {{#繁體字}}
+      <dt>Radikal (繁體字)</dt>
+      <dd>
+        <span hanzi-data="{{text:繁體字}}" hanzi-kind="traditional" hanzi-prop="radical" class="hanzi-print"></span>
+        <div class="strichfolge-animation is-traditional" no-animate="true">
+          <span hanzi-data="{{text:繁體字}}" hanzi-kind="traditional" hanzi-prop="radical"></span>
+        </div>
+      </dd>
+      {{/繁體字}}
+    </details>
+  </dl>
+
+  <include src="src/components/notice/notice.html"></include>
+</div>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/back'
+</script>
\ No newline at end of file
diff --git a/src/templates/molaoshi/read_hanzi/front.html b/src/templates/molaoshi/read_hanzi/front.html
new file mode 100644
index 0000000000000000000000000000000000000000..2238436e1bfafb3dacff27a7a17ffe88131c441c
--- /dev/null
+++ b/src/templates/molaoshi/read_hanzi/front.html
@@ -0,0 +1,39 @@
+{{#简体字}}
+<style>
+  @import url(../../../components/global.css);
+</style>
+
+<div class="exercise front anki-template-hanyu-read-hanzi-front">
+  <header class="card-info">
+    <aside class="exercise-category">
+      {{Subdeck}}
+    </aside>
+    <aside class="exercise-kind">
+      {{Card}}
+    </aside>
+  </header>
+  </header>
+
+  <dl class="prompt translations">
+    <dt>简体字</dt>
+    <dd>
+      <span class="hanzi-print">{{简体字}}</span>
+      <div class="strichfolge-animation" no-animate="true" highlight-radical="answer">
+        {{text:简体字}}
+      </div>
+    </dd>
+    <dt>Bemerkungen</dt>
+    <dd>{{Bemerkungen (Vorderseite)}}</dd>
+  </dl>
+</div>
+
+<!-- Suppress speech output on AnkiDroid until card flipped -->
+<tts style="display: none" service="android" voice="zh_CN"></tts>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/front'
+</script>
+{{/简体字}}
\ No newline at end of file
diff --git a/src/templates/molaoshi/read_hanzi_traditional/back.html b/src/templates/molaoshi/read_hanzi_traditional/back.html
new file mode 100644
index 0000000000000000000000000000000000000000..e95f0c09f4724f97995ec50a8f97f1cb7687c350
--- /dev/null
+++ b/src/templates/molaoshi/read_hanzi_traditional/back.html
@@ -0,0 +1,76 @@
+<div class="front-side-on-back">
+  {{FrontSide}}
+</div>
+
+<hr id="answer">
+
+<div class="exercise back anki-template-hanyu-read-hanzi-back">
+  <dl class="translations">
+    <dt>Pīnyīn</dt>
+    <dd>{{Pīnyīn}}</dd>
+    <dt>Deutsch</dt>
+    <dd>{{Deutsch}}</dd>
+    {{#Standardaussprache}}
+    <dt class="t2s-player-heading">汉语</dt>
+    <dd id="t2s-player-container" class="t2s-player-container">
+      <div class="anki-droid-player">
+        <tts style="display: none" service="android" voice="zh_CN">
+          {{text:Standardaussprache}}
+        </tts>
+      </div>
+      <div class="anki-web-player">
+        {{text:Standardaussprache}}
+      </div>
+      <div class="anki-builtin-player">
+        {{tts zh_CN:Standardaussprache}}
+      </div>
+    </dd>
+    {{/Standardaussprache}}
+    <dt>Aufnahme</dt>
+    <dd>{{Audioaufnahme}}</dd>
+    <dt>Bemerkungen</dt>
+    <dd>{{Bemerkungen}}</dd>
+    <dt>Beispiele</dt>
+    <dd>{{Beispiele}}</dd>
+    <details class="answer-details">
+      <summary>
+        <span class="answer-details-more">Mehr…</span>
+        <span class="answer-details-less">Weniger…</span>
+      </summary>
+      {{#简体字}}
+      <dt>简体字</dt>
+      <dd>
+        <span class="hanzi-print">{{简体字}}</span>
+        <div class="strichfolge-animation" no-animate="true">
+          {{text:简体字}}
+        </div>
+      </dd>
+      {{/简体字}}
+      <dt>Radikal (繁體字)</dt>
+      <dd>
+        <span hanzi-data="{{text:繁體字}}" hanzi-kind="traditional" hanzi-prop="radical" class="hanzi-print"></span>
+        <div class="strichfolge-animation is-traditional" no-animate="true">
+          <span hanzi-data="{{text:繁體字}}" hanzi-kind="traditional" hanzi-prop="radical"></span>
+        </div>
+      </dd>
+      {{#简体字}}
+      <dt>Radikal (简体字)</dt>
+      <dd>
+        <span hanzi-data="{{text:简体字}}" hanzi-prop="radical" class="hanzi-print"></span>
+        <div class="strichfolge-animation" no-animate="true">
+          <span hanzi-data="{{text:简体字}}" hanzi-prop="radical"></span>
+        </div>
+      </dd>
+      {{/简体字}}
+    </details>
+  </dl>
+
+  <include src="src/components/notice/notice.html"></include>
+</div>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/back'
+</script>
\ No newline at end of file
diff --git a/src/templates/molaoshi/read_hanzi_traditional/front.html b/src/templates/molaoshi/read_hanzi_traditional/front.html
new file mode 100644
index 0000000000000000000000000000000000000000..99cfaf7746d092c5e7b7e54a9746cf83d98820bc
--- /dev/null
+++ b/src/templates/molaoshi/read_hanzi_traditional/front.html
@@ -0,0 +1,39 @@
+{{#Lesen (繁體字) anlegen}}{{#繁體字}}
+<style>
+  @import url(../../../components/global.css);
+</style>
+
+<div class="exercise front anki-template-hanyu-read-hanzi-front">
+  <header class="card-info">
+    <aside class="exercise-category">
+      {{Subdeck}}
+    </aside>
+    <aside class="exercise-kind">
+      {{Card}}
+    </aside>
+  </header>
+  </header>
+
+  <dl class="prompt translations">
+    <dt>繁體字</dt>
+    <dd>
+      <span class="hanzi-print">{{繁體字}}</span>
+      <div class="strichfolge-animation is-traditional" no-animate="true" highlight-radical="answer">
+        {{text:繁體字}}
+      </div>
+    </dd>
+    <dt>Bemerkungen</dt>
+    <dd>{{Bemerkungen (Vorderseite)}}</dd>
+  </dl>
+</div>
+
+<!-- Suppress speech output on AnkiDroid until card flipped -->
+<tts style="display: none" service="android" voice="zh_CN"></tts>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/front'
+</script>
+{{/繁體字}}{{/Lesen (繁體字) anlegen}}
\ No newline at end of file
diff --git a/src/templates/molaoshi/read_pinyin/back.html b/src/templates/molaoshi/read_pinyin/back.html
new file mode 100644
index 0000000000000000000000000000000000000000..71d713190f6f8922fb89900810f4e786aeaa8caf
--- /dev/null
+++ b/src/templates/molaoshi/read_pinyin/back.html
@@ -0,0 +1,81 @@
+<div class="front-side-on-back">
+  {{FrontSide}}
+</div>
+
+<hr id="answer">
+
+<div class="exercise back anki-template-hanyu-read-pinyin-back">
+  <dl class="translations">
+    <dt>Deutsch</dt>
+    <dd>{{Deutsch}}</dd>
+    <dt>简体字</dt>
+    <dd>
+      <span class="hanzi-print">{{简体字}}</span>
+      <div class="strichfolge-animation" no-animate="true" highlight-radical="true">
+        {{text:简体字}}
+      </div>
+    </dd>
+    {{#Standardaussprache}}
+    <dt class="t2s-player-heading">汉语</dt>
+    <dd id="t2s-player-container" class="t2s-player-container">
+      <div class="anki-droid-player">
+        <tts style="display: none" service="android" voice="zh_CN">
+          {{text:Standardaussprache}}
+        </tts>
+      </div>
+      <div class="anki-web-player">
+        {{text:Standardaussprache}}
+      </div>
+      <div class="anki-builtin-player">
+        {{tts zh_CN:Standardaussprache}}
+      </div>
+    </dd>
+    {{/Standardaussprache}}
+    <dt>Aufnahme</dt>
+    <dd>{{Audioaufnahme}}</dd>
+    <dt>Bemerkungen</dt>
+    <dd>{{Bemerkungen}}</dd>
+    <dt>Beispiele</dt>
+    <dd>{{Beispiele}}</dd>
+    <details class="answer-details">
+      <summary>
+        <span class="answer-details-more">Mehr…</span>
+        <span class="answer-details-less">Weniger…</span>
+      </summary>
+      {{#繁體字}}
+      <dt>繁體字</dt>
+      <dd>
+        <span class="hanzi-print">{{繁體字}}</span>
+        <div class="strichfolge-animation is-traditional" no-animate="true" highlight-radical="true">
+          {{text:繁體字}}
+        </div>
+      </dd>
+      {{/繁體字}}
+      <dt>Radikal (简体字)</dt>
+      <dd>
+        <span hanzi-data="{{text:简体字}}" hanzi-prop="radical" class="hanzi-print"></span>
+        <div class="strichfolge-animation" no-animate="true">
+          <span hanzi-data="{{text:简体字}}" hanzi-prop="radical"></span>
+        </div>
+      </dd>
+      {{#繁體字}}
+      <dt>Radikal (繁體字)</dt>
+      <dd>
+        <span hanzi-data="{{text:繁體字}}" hanzi-kind="traditional" hanzi-prop="radical" class="hanzi-print"></span>
+        <div class="strichfolge-animation is-traditional" no-animate="true">
+          <span hanzi-data="{{text:繁體字}}" hanzi-kind="traditional" hanzi-prop="radical"></span>
+        </div>
+      </dd>
+      {{/繁體字}}
+    </details>
+  </dl>
+
+  <include src="src/components/notice/notice.html"></include>
+</div>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/back'
+</script>
\ No newline at end of file
diff --git a/src/templates/molaoshi/read_pinyin/front.html b/src/templates/molaoshi/read_pinyin/front.html
new file mode 100644
index 0000000000000000000000000000000000000000..767425794c210bc5a0c31a65f396e0d0e2d579d0
--- /dev/null
+++ b/src/templates/molaoshi/read_pinyin/front.html
@@ -0,0 +1,34 @@
+{{#Pīnyīn}}
+<style>
+  @import url(../../../components/global.css);
+</style>
+
+<div class="exercise front anki-template-hanyu-read-pinyin-front">
+  <header class="card-info">
+    <aside class="exercise-category">
+      {{Subdeck}}
+    </aside>
+    <aside class="exercise-kind">
+      {{Card}}
+    </aside>
+  </header>
+  </header>
+
+  <dl class="prompt translations">
+    <dt>Pīnyīn</dt>
+    <dd>{{Pīnyīn}}</dd>
+    <dt>Bemerkungen</dt>
+    <dd>{{Bemerkungen (Vorderseite)}}</dd>
+  </dl>
+</div>
+
+<!-- Suppress speech output on AnkiDroid until card flipped -->
+<tts style="display: none" service="android" voice="zh_CN"></tts>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/front'
+</script>
+{{/Pīnyīn}}
\ No newline at end of file
diff --git a/src/templates/molaoshi/speak/back.html b/src/templates/molaoshi/speak/back.html
new file mode 100644
index 0000000000000000000000000000000000000000..b80aea9a7b0f06770e7a3c6d5944612bb0a5e33a
--- /dev/null
+++ b/src/templates/molaoshi/speak/back.html
@@ -0,0 +1,81 @@
+<div class="front-side-on-back">
+  {{FrontSide}}
+</div>
+
+<hr id="answer">
+
+<div class="exercise back anki-template-hanyu-speak-back">
+  <dl class="translations">
+    <dt>Pīnyīn</dt>
+    <dd>{{Pīnyīn}}</dd>
+    <dt>简体字</dt>
+    <dd>
+      <span class="hanzi-print">{{简体字}}</span>
+      <div class="strichfolge-animation" no-animate="true" highlight-radical="true">
+        {{text:简体字}}
+      </div>
+    </dd>
+    {{#Standardaussprache}}
+    <dt class="t2s-player-heading">汉语</dt>
+    <dd id="t2s-player-container" class="t2s-player-container">
+      <div class="anki-droid-player">
+        <tts style="display: none" service="android" voice="zh_CN">
+          {{text:Standardaussprache}}
+        </tts>
+      </div>
+      <div class="anki-web-player">
+        {{text:Standardaussprache}}
+      </div>
+      <div class="anki-builtin-player">
+        {{tts zh_CN:Standardaussprache}}
+      </div>
+    </dd>
+    {{/Standardaussprache}}
+    <dt>Aufnahme</dt>
+    <dd>{{Audioaufnahme}}</dd>
+    <dt>Bemerkungen</dt>
+    <dd>{{Bemerkungen}}</dd>
+    <dt>Beispiele</dt>
+    <dd>{{Beispiele}}</dd>
+    <details class="answer-details">
+      <summary>
+        <span class="answer-details-more">Mehr…</span>
+        <span class="answer-details-less">Weniger…</span>
+      </summary>
+      {{#繁體字}}
+      <dt>繁體字</dt>
+      <dd>
+        <span class="hanzi-print">{{繁體字}}</span>
+        <div class="strichfolge-animation is-traditional" no-animate="true" highlight-radical="true">
+          {{text:繁體字}}
+        </div>
+      </dd>
+      {{/繁體字}}
+      <dt>Radikal (简体字)</dt>
+      <dd>
+        <span hanzi-data="{{text:简体字}}" hanzi-prop="radical" class="hanzi-print"></span>
+        <div class="strichfolge-animation" no-animate="true">
+          <span hanzi-data="{{text:简体字}}" hanzi-prop="radical"></span>
+        </div>
+      </dd>
+      {{#繁體字}}
+      <dt>Radikal (繁體字)</dt>
+      <dd>
+        <span hanzi-data="{{text:繁體字}}" hanzi-kind="traditional" hanzi-prop="radical" class="hanzi-print"></span>
+        <div class="strichfolge-animation is-traditional" no-animate="true">
+          <span hanzi-data="{{text:繁體字}}" hanzi-kind="traditional" hanzi-prop="radical"></span>
+        </div>
+      </dd>
+      {{/繁體字}}
+    </details>
+  </dl>
+
+  <include src="src/components/notice/notice.html"></include>
+</div>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/back'
+</script>
\ No newline at end of file
diff --git a/src/templates/molaoshi/speak/front.html b/src/templates/molaoshi/speak/front.html
new file mode 100644
index 0000000000000000000000000000000000000000..a93e52ec72911078a60d4a58a91fa1f82cacb669
--- /dev/null
+++ b/src/templates/molaoshi/speak/front.html
@@ -0,0 +1,34 @@
+{{#Deutsch}}
+<style>
+  @import url(../../../components/global.css);
+</style>
+
+<div class="exercise front anki-template-hanyu-speak-front">
+  <header class="card-info">
+    <aside class="exercise-category">
+      {{Subdeck}}
+    </aside>
+    <aside class="exercise-kind">
+      {{Card}}
+    </aside>
+  </header>
+  </header>
+
+  <dl class="prompt translations">
+    <dt>Deutsch</dt>
+    <dd>{{Deutsch}}</dd>
+    <dt>Bemerkungen</dt>
+    <dd>{{Bemerkungen (Vorderseite)}}</dd>
+  </dl>
+</div>
+
+<!-- Suppress speech output on AnkiDroid until card flipped -->
+<tts style="display: none" service="android" voice="zh_CN"></tts>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/front'
+</script>
+{{/Deutsch}}
\ No newline at end of file
diff --git a/src/templates/molaoshi/write/back.html b/src/templates/molaoshi/write/back.html
new file mode 100644
index 0000000000000000000000000000000000000000..6417876fcdb6ba973116beaa957fc184cc5d7695
--- /dev/null
+++ b/src/templates/molaoshi/write/back.html
@@ -0,0 +1,81 @@
+<div class="front-side-on-back">
+  {{FrontSide}}
+</div>
+
+<hr id="answer">
+
+<div class="exercise back anki-template-hanyu-write-back">
+  <dl class="translations">
+    <dt>简体字</dt>
+    <dd>
+      <span class="hanzi-print">{{简体字}}</span>
+      <div class="strichfolge-animation" highlight-radical="true">
+        {{text:简体字}}
+      </div>
+    </dd>
+    <dt>Pīnyīn</dt>
+    <dd>{{Pīnyīn}}</dd>
+    {{#Standardaussprache}}
+    <dt class="t2s-player-heading">汉语</dt>
+    <dd id="t2s-player-container" class="t2s-player-container">
+      <div class="anki-droid-player">
+        <tts style="display: none" service="android" voice="zh_CN">
+          {{text:Standardaussprache}}
+        </tts>
+      </div>
+      <div class="anki-web-player">
+        {{text:Standardaussprache}}
+      </div>
+      <div class="anki-builtin-player">
+        {{tts zh_CN:Standardaussprache}}
+      </div>
+    </dd>
+    {{/Standardaussprache}}
+    <dt>Aufnahme</dt>
+    <dd>{{Audioaufnahme}}</dd>
+    <dt>Bemerkungen</dt>
+    <dd>{{Bemerkungen}}</dd>
+    <dt>Beispiele</dt>
+    <dd>{{Beispiele}}</dd>
+    <details class="answer-details">
+      <summary>
+        <span class="answer-details-more">Mehr…</span>
+        <span class="answer-details-less">Weniger…</span>
+      </summary>
+      {{#繁體字}}
+      <dt>繁體字</dt>
+      <dd>
+        <span class="hanzi-print">{{繁體字}}</span>
+        <div class="strichfolge-animation is-traditional" highlight-radical="true">
+          {{text:繁體字}}
+        </div>
+      </dd>
+      {{/繁體字}}
+      <dt>Radikal (简体字)</dt>
+      <dd>
+        <span hanzi-data="{{text:简体字}}" hanzi-prop="radical" class="hanzi-print"></span>
+        <div class="strichfolge-animation" no-animate="true">
+          <span hanzi-data="{{text:简体字}}" hanzi-prop="radical"></span>
+        </div>
+      </dd>
+      {{#繁體字}}
+      <dt>Radikal (繁體字)</dt>
+      <dd>
+        <span hanzi-data="{{text:繁體字}}" hanzi-kind="traditional" hanzi-prop="radical" class="hanzi-print"></span>
+        <div class="strichfolge-animation is-traditional" no-animate="true">
+          <span hanzi-data="{{text:繁體字}}" hanzi-kind="traditional" hanzi-prop="radical"></span>
+        </div>
+      </dd>
+      {{/繁體字}}
+    </details>
+  </dl>
+
+  <include src="src/components/notice/notice.html"></include>
+</div>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/back'
+</script>
\ No newline at end of file
diff --git a/src/templates/molaoshi/write/front.html b/src/templates/molaoshi/write/front.html
new file mode 100644
index 0000000000000000000000000000000000000000..ed82b423a4db16d8ea4255cba9428a23ddca0da7
--- /dev/null
+++ b/src/templates/molaoshi/write/front.html
@@ -0,0 +1,34 @@
+{{#Deutsch}}{{#Schreiben anlegen}}
+<style>
+  @import url(../../../components/global.css);
+</style>
+
+<div class="exercise front anki-template-hanyu-write-front">
+  <header class="card-info">
+    <aside class="exercise-category">
+      {{Subdeck}}
+    </aside>
+    <aside class="exercise-kind">
+      {{Card}}
+    </aside>
+  </header>
+  </header>
+
+  <dl class="prompt translations">
+    <dt>Deutsch</dt>
+    <dd>{{Deutsch}}</dd>
+    <dt>Bemerkungen</dt>
+    <dd>{{Bemerkungen (Vorderseite)}}</dd>
+  </dl>
+</div>
+
+<!-- Suppress default speech output and do that from script instead -->
+<tts style="display: none" service="android" voice="zh_CN"></tts>
+
+<script type="module">
+  import '../../../components/debug'
+</script>
+<script type="module">
+  import '../../../components/front'
+</script>
+{{/Schreiben anlegen}}{{/Deutsch}}
\ No newline at end of file
diff --git a/test/build/fixtures/anki/.apkg-spec.yaml b/test/build/fixtures/anki/.apkg-spec.yaml
deleted file mode 100644
index 92ef7488e7e8155afcb935ef23c3473690740a23..0000000000000000000000000000000000000000
--- a/test/build/fixtures/anki/.apkg-spec.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-content_version: 0.0.1
-
-templates:
-- q_a
-
-content:
-- import_apkg:
-    note_type: Q/A Testnotetype
\ No newline at end of file
diff --git a/test/build/fixtures/anki/Test.apkg b/test/build/fixtures/anki/Test.apkg
deleted file mode 100644
index 28a3614f2b228aa8ae1ba806c9914101a758cdcd..0000000000000000000000000000000000000000
Binary files a/test/build/fixtures/anki/Test.apkg and /dev/null differ
diff --git a/test/build/fixtures/csv/.apkg-spec.yaml b/test/build/fixtures/csv/.apkg-spec.yaml
deleted file mode 100644
index 274a5ede4ea93c1613898b87eac36aba91f2cb35..0000000000000000000000000000000000000000
--- a/test/build/fixtures/csv/.apkg-spec.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-content_version: 1.0.0
-
-templates:
-- q_a
-
-content:
-- import_csv:
-    content_version: 2024-01-19 19:00:00+00:00
-    note_type: Q/A Testnotetype
-    file_patterns:
-    - '*.csv'
-    # and to making one deck per card type
-    deck_name_pattern: '{{card_type}}'
-    fields_mapping:
-    - guid
-    - Question
-    - Answer
-    tags: []
\ No newline at end of file
diff --git a/test/build/fixtures/csv/content.csv b/test/build/fixtures/csv/content.csv
deleted file mode 100644
index 3c98112adacf4a22058eda6fb0e3b4646fe61b1f..0000000000000000000000000000000000000000
--- a/test/build/fixtures/csv/content.csv
+++ /dev/null
@@ -1 +0,0 @@
-test-csv-note-1;What is the scientific name of the only tapir living in Asia?;Tapirus indicus
\ No newline at end of file
diff --git a/test/build/fixtures/csv_updated_content/.apkg-spec.yaml b/test/build/fixtures/csv_updated_content/.apkg-spec.yaml
deleted file mode 100644
index c20f61a2701c946e588aed80a1a36349bb9fd78e..0000000000000000000000000000000000000000
--- a/test/build/fixtures/csv_updated_content/.apkg-spec.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-content_version: 1.0.0
-
-templates:
-- q_a
-
-content:
-- import_csv:
-    content_version: 2024-01-19 20:00:00+00:00
-    note_type: Q/A Testnotetype
-    file_patterns:
-    - '*.csv'
-    # and to making one deck per card type
-    deck_name_pattern: '{{card_type}}'
-    fields_mapping:
-    - guid
-    - Question
-    - Answer
-    tags: []
\ No newline at end of file
diff --git a/test/build/fixtures/csv_updated_content/content.csv b/test/build/fixtures/csv_updated_content/content.csv
deleted file mode 100644
index d1f926732965ce42331922094e9d0e09843759b8..0000000000000000000000000000000000000000
--- a/test/build/fixtures/csv_updated_content/content.csv
+++ /dev/null
@@ -1 +0,0 @@
-test-csv-note-1;What is the scientific name of the only tapir living in Asia?;Tapirus indicus, Acrocodia indica being an outdated synonym.
\ No newline at end of file
diff --git a/test/build/fixtures/templates_updated/q_a/.template-spec.yaml b/test/build/fixtures/templates_updated/q_a/.template-spec.yaml
deleted file mode 100644
index 02ef56a8c9e806366f130c7bf6b46a211305aa4d..0000000000000000000000000000000000000000
--- a/test/build/fixtures/templates_updated/q_a/.template-spec.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-template_version: 2024-01-20 19:00:00+00:00
-
-note_type:
-  id: 2024-01-20 01:00:00+00:00
-  name: Q/A Testnotetype
-  fields:
-  - Question
-  - Answer
-
-card_types:
-- name: Q/A Testcardtype
-  template: q_a
-
-resource_paths: []
diff --git a/test/build/fixtures/templates_updated/q_a/q_a/back.html b/test/build/fixtures/templates_updated/q_a/q_a/back.html
deleted file mode 100644
index 5cc04047c87ec3899621a9dd2ef1783bc121cee6..0000000000000000000000000000000000000000
--- a/test/build/fixtures/templates_updated/q_a/q_a/back.html
+++ /dev/null
@@ -1 +0,0 @@
-Back: {{Answer}}
\ No newline at end of file
diff --git a/test/build/fixtures/templates_updated/q_a/q_a/front.html b/test/build/fixtures/templates_updated/q_a/q_a/front.html
deleted file mode 100644
index 51c00f17bd74b3f03674daf131ee9919167ac2fb..0000000000000000000000000000000000000000
--- a/test/build/fixtures/templates_updated/q_a/q_a/front.html
+++ /dev/null
@@ -1 +0,0 @@
-Front: {{Question}}
\ No newline at end of file
diff --git a/test/build/test_export_apkgs.py b/test/build/test_export_apkgs.py
deleted file mode 100644
index 22b46f425ff7b1a204bae4922bef7f7ec1a88a4c..0000000000000000000000000000000000000000
--- a/test/build/test_export_apkgs.py
+++ /dev/null
@@ -1,337 +0,0 @@
-from anki.collection import Collection, ImportAnkiPackageRequest, ImportAnkiPackageOptions
-from anki.import_export_pb2 import ImportAnkiPackageUpdateCondition
-from build.export_apkgs import export_package_from_spec
-from dataclasses import dataclass
-from pathlib import Path
-from tempfile import TemporaryDirectory
-import unittest
-
-@dataclass
-class MockArgs:
-    content: str
-    templates_dir: str
-    output_dir: str
-    dry_run: bool
-
-CONTENT_PATH_ANKI = 'test/build/fixtures/anki'
-CONTENT_PATH_CSV = 'test/build/fixtures/csv'
-CONTENT_PATH_CSV_UPDATED_CONTENT = 'test/build/fixtures/csv_updated_content'
-TEMPLATES_PATH = 'test/build/fixtures/templates'
-TEMPLATES_PATH_UPDATED = 'test/build/fixtures/templates_updated'
-
-class TestExportApkgs(unittest.TestCase):
-    def test_generate_once_and_reimport(self):
-        """
-        Basic sanity check: if the exported package is imported twice,
-        everything is a duplicate and nothing should change.
-        """
-        with TemporaryDirectory() as temp_collection_dir:
-            col = Collection(str(Path(temp_collection_dir) / "test.anki2"))
-            with TemporaryDirectory() as first_export_dir:
-                args_first = MockArgs(
-                    content=CONTENT_PATH_CSV,
-                    templates_dir=TEMPLATES_PATH,
-                    output_dir=first_export_dir,
-                    dry_run=False)
-                package = export_package_from_spec(
-                    Path(CONTENT_PATH_CSV) / '.apkg-spec.yaml',
-                    args_first)
-
-                # import the first time, everything is new
-                result1 = col.import_anki_package(ImportAnkiPackageRequest(
-                    package_path=str(package),
-                    options=ImportAnkiPackageOptions(
-                        merge_notetypes=True,
-                        update_notes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER,
-                        update_notetypes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER,
-                        with_scheduling=False,
-                        with_deck_configs=False,
-                    )
-                ))
-                self.assertEqual(len(result1.log.new), 1)
-                self.assertEqual(len(result1.log.duplicate), 0)
-
-                # now import again, nothing should change
-                result2 = col.import_anki_package(ImportAnkiPackageRequest(
-                    package_path=str(package),
-                    options=ImportAnkiPackageOptions(
-                        merge_notetypes=True,
-                        update_notes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER,
-                        update_notetypes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER,
-                        with_scheduling=False,
-                        with_deck_configs=False,
-                    )
-                ))
-                self.assertEqual(len(result2.log.duplicate) == 1 and len(result2.log.new), 0,
-                    f'Expected the note to be recognized as duplicate.\nLog1:\n{result1.log}\nLog2:\n{result2.log}')
-
-    def test_generate_twice_and_reimport(self):
-        """
-        This checks that if we generate the package twice and import it twice,
-        then the second import will not change anything because the content is
-        supposed to be the same.
-
-        If we introduce errors that lead to different note and card IDs being
-        generated, then this test will fail.
-        """
-        with TemporaryDirectory() as temp_collection_dir:
-            col = Collection(str(Path(temp_collection_dir) / "test.anki2"))
-            with TemporaryDirectory() as first_export_dir:
-                args_first = MockArgs(
-                    content=CONTENT_PATH_CSV,
-                    templates_dir=TEMPLATES_PATH,
-                    output_dir=first_export_dir,
-                    dry_run=False)
-                with TemporaryDirectory() as second_export_dir:
-                    args_second = MockArgs(
-                        content=CONTENT_PATH_CSV,
-                        templates_dir=TEMPLATES_PATH,
-                        output_dir=second_export_dir,
-                        dry_run=False)
-                    spec_path = Path(CONTENT_PATH_CSV) / '.apkg-spec.yaml'
-                    first_package = export_package_from_spec(spec_path, args_first)
-                    second_package = export_package_from_spec(spec_path, args_second)
-                    
-                    # debug: uncomment to view for testing
-                    # shutil.copy(first_package, './test-export-first.apkg.zip')
-                    # shutil.copy(second_package, './test-export-second.apkg.zip')
-
-                    # import the first time, everything is new
-                    result1 = col.import_anki_package(ImportAnkiPackageRequest(
-                        package_path=str(first_package),
-                        options=ImportAnkiPackageOptions(
-                            merge_notetypes=True,
-                            update_notes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER,
-                            update_notetypes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER,
-                            with_scheduling=False,
-                            with_deck_configs=False,
-                        )
-                    ))
-                    self.assertEqual(len(result1.log.new), 1)
-                    self.assertEqual(len(result1.log.duplicate), 0)
-
-                    # now import again, nothing should change
-                    result2 = col.import_anki_package(ImportAnkiPackageRequest(
-                        package_path=str(second_package),
-                        options=ImportAnkiPackageOptions(
-                            merge_notetypes=True,
-                            update_notes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER,
-                            update_notetypes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER,
-                            with_scheduling=False,
-                            with_deck_configs=False,
-                        )
-                    ))
-                    self.assertTrue(
-                        len(result2.log.duplicate) == 1 and len(result2.log.new) == 0,
-                        f'Expected second import to be a duplicate, but something else happened\nfirst log: {result1.log}\nsecond log:\n{result2.log}')
-                
-    def test_content_update_overwrites_previous_note(self):
-        """
-        Tests that bumping the modification time will update notes from previous versions.
-        """
-        with TemporaryDirectory() as temp_collection_dir:
-            col = Collection(str(Path(temp_collection_dir) / "test.anki2"))
-            with TemporaryDirectory() as first_export_dir:
-                args_first = MockArgs(
-                    content=CONTENT_PATH_CSV,
-                    templates_dir=TEMPLATES_PATH,
-                    output_dir=first_export_dir,
-                    dry_run=False)
-                with TemporaryDirectory() as second_export_dir:
-                    args_second = MockArgs(
-                        content=CONTENT_PATH_CSV_UPDATED_CONTENT,
-                        templates_dir=TEMPLATES_PATH,
-                        output_dir=second_export_dir,
-                        dry_run=False)
-                    spec_path_old = Path(f'{CONTENT_PATH_CSV}/.apkg-spec.yaml')
-                    spec_path_new = Path(f'{CONTENT_PATH_CSV_UPDATED_CONTENT}/.apkg-spec.yaml')
-                    first_package = export_package_from_spec(spec_path_old, args_first)
-                    second_package = export_package_from_spec(spec_path_new, args_second)
-
-                    # import the old version version
-                    result1 = col.import_anki_package(ImportAnkiPackageRequest(
-                        package_path=str(first_package),
-                        options=ImportAnkiPackageOptions(
-                            merge_notetypes=True,
-                            update_notes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER,
-                            update_notetypes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER,
-                            with_scheduling=False,
-                            with_deck_configs=False,
-                        )
-                    ))
-                    self.assertEqual(len(result1.log.updated), 0)
-                    self.assertEqual(len(result1.log.new), 1)
-                    self.assertEqual(len(result1.log.duplicate), 0)
-
-                    # now the update
-                    result2 = col.import_anki_package(ImportAnkiPackageRequest(
-                        package_path=str(second_package),
-                        options=ImportAnkiPackageOptions(
-                            merge_notetypes=True,
-                            update_notes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER,
-                            update_notetypes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER,
-                            with_scheduling=False,
-                            with_deck_configs=False,
-                        )
-                    ))
-                    self.assertTrue(len(result2.log.updated) == 1 and len(result2.log.new) == 0 and len(result2.log.duplicate) == 0,
-                                    f'Expected the note to be recognized as update.\nLog1:\n{result1.log}\nLog2:\n{result2.log}')
-                    
-
-    def test_template_update_overwrites_previous_template_csv(self):
-        """
-        Tests that bumping the modification time will update notes from previous versions
-        of CSV content.
-        """
-        with TemporaryDirectory() as temp_collection_dir:
-            col = Collection(str(Path(temp_collection_dir) / "test.anki2"))
-            with TemporaryDirectory() as first_export_dir:
-                args_first = MockArgs(
-                    content=CONTENT_PATH_CSV,
-                    templates_dir=TEMPLATES_PATH,
-                    output_dir=first_export_dir,
-                    dry_run=False)
-                with TemporaryDirectory() as second_export_dir:
-                    args_second = MockArgs(
-                        content=CONTENT_PATH_CSV,
-                        templates_dir=TEMPLATES_PATH_UPDATED,
-                        output_dir=second_export_dir,
-                        dry_run=False)
-                    spec_path = Path(f'{CONTENT_PATH_CSV}/.apkg-spec.yaml')
-                    first_package = export_package_from_spec(spec_path, args_first)
-                    second_package = export_package_from_spec(spec_path, args_second)
-
-                    # debug: uncomment to view for testing
-                    #shutil.copy(first_package, './test-export-first.apkg.zip')
-                    #shutil.copy(second_package, './test-export-second.apkg.zip')
-
-                    # import the old version version
-                    col.import_anki_package(ImportAnkiPackageRequest(
-                        package_path=str(first_package),
-                        options=ImportAnkiPackageOptions(
-                            merge_notetypes=True,
-                            update_notes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER,
-                            update_notetypes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER,
-                            with_scheduling=False,
-                            with_deck_configs=False,
-                        )
-                    ))
-
-                    template = find_template(col, 'Q/A Testnotetype')
-                    self.assertIsNotNone(template, 'template not found in collection')
-                    self.assertEqual(
-                        template['qfmt'],
-                        '{{Question}}')
-                    self.assertEqual(
-                        template['afmt'],
-                        '{{Answer}}')
-
-                    # now the update
-                    result = col.import_anki_package(ImportAnkiPackageRequest(
-                        package_path=str(second_package),
-                        options=ImportAnkiPackageOptions(
-                            merge_notetypes=True,
-                            update_notes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER,
-                            update_notetypes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER,
-                            with_scheduling=False,
-                            with_deck_configs=False,
-                        )
-                    ))
-
-                    # for some reason the change is only visible if we reload the collection
-                    col.close()
-                    col = Collection(str(Path(temp_collection_dir) / "test.anki2"))
-
-                    template = find_template(col, 'Q/A Testnotetype')
-                    self.assertIsNotNone(template, 'template not found in collection')
-                    self.assertEqual(
-                        template['qfmt'],
-                        'Front: {{Question}}',
-                        f'Template not updated successfully, log:\n{result.log}')
-                    self.assertEqual(
-                        template['afmt'],
-                        'Back: {{Answer}}')
-
-    def test_template_update_overwrites_previous_template_anki(self):
-        """
-        Tests that bumping the modification time will update notes from previous versions
-        of an imported anki package.
-        """
-        with TemporaryDirectory() as temp_collection_dir:
-            col = Collection(str(Path(temp_collection_dir) / "test.anki2"))
-            with TemporaryDirectory() as first_export_dir:
-                args_first = MockArgs(
-                    content=CONTENT_PATH_ANKI,
-                    templates_dir=TEMPLATES_PATH,
-                    output_dir=first_export_dir,
-                    dry_run=False)
-                with TemporaryDirectory() as second_export_dir:
-                    args_second = MockArgs(
-                        content=CONTENT_PATH_ANKI,
-                        templates_dir=TEMPLATES_PATH_UPDATED,
-                        output_dir=second_export_dir,
-                        dry_run=False)
-                    spec_path = Path(f'{CONTENT_PATH_ANKI}/.apkg-spec.yaml')
-                    first_package = export_package_from_spec(spec_path, args_first)
-                    second_package = export_package_from_spec(spec_path, args_second)
-
-                    # debug: uncomment to view for testing
-                    #shutil.copy(first_package, './test-export-first.apkg.zip')
-                    #shutil.copy(second_package, './test-export-second.apkg.zip')
-
-                    # import the old version version
-                    col.import_anki_package(ImportAnkiPackageRequest(
-                        package_path=str(first_package),
-                        options=ImportAnkiPackageOptions(
-                            merge_notetypes=True,
-                            update_notes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER,
-                            update_notetypes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER,
-                            with_scheduling=False,
-                            with_deck_configs=False,
-                        )
-                    ))
-
-                    template = find_template(col, 'Q/A Testnotetype')
-                    self.assertIsNotNone(template, 'template not found in collection')
-                    self.assertEqual(
-                        template['qfmt'],
-                        '{{Question}}')
-                    self.assertEqual(
-                        template['afmt'],
-                        '{{Answer}}')
-
-                    # now the update
-                    result = col.import_anki_package(ImportAnkiPackageRequest(
-                        package_path=str(second_package),
-                        options=ImportAnkiPackageOptions(
-                            merge_notetypes=True,
-                            update_notes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER,
-                            update_notetypes=ImportAnkiPackageUpdateCondition.IMPORT_ANKI_PACKAGE_UPDATE_CONDITION_IF_NEWER,
-                            with_scheduling=False,
-                            with_deck_configs=False,
-                        )
-                    ))
-
-                    # for some reason the change is only visible if we reload the collection
-                    col.close()
-                    col = Collection(str(Path(temp_collection_dir) / "test.anki2"))
-
-                    template = find_template(col, 'Q/A Testnotetype')
-                    self.assertIsNotNone(template, 'template not found in collection')
-                    self.assertEqual(
-                        template['qfmt'],
-                        'Front: {{Question}}',
-                        f'Template not updated successfully, log:\n{result.log}')
-                    self.assertEqual(
-                        template['afmt'],
-                        'Back: {{Answer}}')
-
-def find_template(col: Collection, name: str):
-    template = None
-    for name_and_id in col.models.all_names_and_ids():
-        if name_and_id.name == name:
-            if template is None:
-                template = col.models.get(name_and_id.id)['tmpls'][0]
-            else:
-                raise Exception(f'Found more than one template with name {name}')
-    return template
\ No newline at end of file
diff --git a/test/templates/hanzi-data.test.ts b/test/templates/hanzi-data.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cadfc82bfdbe31f3ff20276d95974de6499c6127
--- /dev/null
+++ b/test/templates/hanzi-data.test.ts
@@ -0,0 +1,202 @@
+import { StrokeType } from '../../src-common/stroke-encodings'
+import {
+  GetHanziDataKind,
+  getHanziData
+} from '../../src/components/hanzi-data'
+
+describe('stroke counts', () => {
+  describe('default kind', () => {
+    const kind = GetHanziDataKind.DEFAULT
+    const testCases = [
+      { character: '错', expectedCount: '5+8' },
+      { character: '你', expectedCount: '2+5' },
+      { character: '笔', expectedCount: '6+4' }
+    ]
+    for (const testCase of testCases) {
+      test(`stroke count of ${testCase.character}`, async () => {
+        const data = await getHanziData({
+          char: testCase.character,
+          kind
+        })
+        const actualCount = data?.count
+        expect(actualCount).toBe(testCase.expectedCount)
+      })
+    }
+  })
+  describe('traditional kind', () => {
+    const kind = GetHanziDataKind.TRADITIONAL
+    const testCases = [
+      { character: '紫', expectedCount: '6+6' },
+      { character: '幫', expectedCount: '3+14' },
+      { character: '處', expectedCount: '6+5' },
+      { character: '夏', expectedCount: '3+7' },
+      { character: '鄰', expectedCount: '7+12' },
+      { character: '圜', expectedCount: '3+13' },
+      { character: '璮', expectedCount: '5+13' },
+      { character: '刷', expectedCount: '2+6' },
+      { character: '戀', expectedCount: '4+19' },
+      { character: '矮', expectedCount: '5+8' },
+      { character: '驚', expectedCount: '10+13' },
+      { character: '雞', expectedCount: '8+10' },
+      { character: '顱', expectedCount: '9+16' },
+      { character: '襪', expectedCount: '6+15' },
+      { character: '勵', expectedCount: '2+15' },
+      { character: '藝', expectedCount: '6+15' },
+      { character: '歡', expectedCount: '4+18' },
+      { character: '戰', expectedCount: '4+12' },
+      { character: '翻', expectedCount: '6+12' },
+      { character: '敵', expectedCount: '4+11' },
+      { character: '彎', expectedCount: '3+19' },
+      { character: '籍', expectedCount: '6+14' },
+      { character: '餓', expectedCount: '9+7' },
+      { character: '隨', expectedCount: '8+13' },
+      { character: '翼', expectedCount: '6+11' },
+      { character: '響', expectedCount: '9+11' },
+      { character: '矊', expectedCount: '5+14' },
+      { character: '聲', expectedCount: '6+11' },
+      { character: '磬', expectedCount: '5+11' },
+      { character: '獸', expectedCount: '4+15' },
+      { character: '義', expectedCount: '6+7' },
+      { character: '登', expectedCount: '5+7' },
+      { character: '灣', expectedCount: '4+22' },
+      { character: '驢', expectedCount: '10+16' },
+      { character: '壓', expectedCount: '3+14' },
+      { character: '避', expectedCount: '7+13' },
+      { character: '甩', expectedCount: '5+0' },
+      { character: '懈', expectedCount: '4+13' },
+      { character: '靈', expectedCount: '8+16' },
+      { character: '攤', expectedCount: '4+19' },
+      { character: '氧', expectedCount: '4+6' },
+      { character: '參', expectedCount: '2+9' },
+      { character: '熱', expectedCount: '4+11' },
+      { character: '餐', expectedCount: '9+7' },
+      { character: '虝', expectedCount: '6+6' },
+      { character: '聽', expectedCount: '6+16' }
+    ]
+    for (const testCase of testCases) {
+      test(`stroke count of ${testCase.character}`, async () => {
+        const data = await getHanziData({
+          char: testCase.character,
+          kind
+        })
+        const actualCount = data?.count
+        expect(actualCount).toBe(testCase.expectedCount)
+      })
+    }
+  })
+})
+
+describe('stroke types', () => {
+  const testCases = [
+    {
+      hanzi: '艸',
+      expected: [
+        StrokeType.SHUZHE,
+        StrokeType.SHU,
+        StrokeType.PIE,
+        StrokeType.SHUZHE,
+        StrokeType.SHU,
+        StrokeType.SHU
+      ]
+    }
+  ]
+  for (const { hanzi, expected } of testCases) {
+    test(hanzi, async () => {
+      const { strokeTypes } = await getHanziData({ char: hanzi })
+      expect(strokeTypes).toEqual(expected)
+    })
+  }
+})
+
+describe('radicals', () => {
+  describe('default kind', () => {
+    const kind = GetHanziDataKind.DEFAULT
+    const testCases = [
+      ['王', '王'],
+      ['了', '乙'],
+      ['草', '艹'],
+      ['笔', '竹']
+    ]
+    for (const [hanzi, expectedRadical] of testCases) {
+      test(hanzi, async () => {
+        const data = await getHanziData({ char: hanzi, kind })
+        const radical = data?.radical
+        expect(radical).toBe(expectedRadical)
+      })
+    }
+  })
+  describe('omitted kind', () => {
+    test('了', async () => {
+      // no kind also means default
+      const data = await getHanziData({ char: '了' })
+      const radical = data?.radical
+      expect(radical).toBe('乙')
+    })
+  })
+  describe('traditional kind', () => {
+    const kind = GetHanziDataKind.TRADITIONAL
+    const testCases = [
+      ['王', '玉'],
+      ['了', '亅'],
+      ['蘭', '艸'],
+      ['聽', '耳'],
+      ['笔', '竹'],
+      ['齣', '齒']
+    ]
+    for (const [hanzi, expectedRadical] of testCases) {
+      test(hanzi, async () => {
+        const data = await getHanziData({ char: hanzi, kind })
+        const radical = data?.radical
+        expect(radical).toBe(expectedRadical)
+      })
+    }
+  })
+})
+
+describe('SVG data', () => {
+  const mustHave = [
+    '凵',
+    '季',
+    '木',
+    '纔',
+    '裏',
+    '這',
+    '阝',
+    '餵',
+    '鼕',
+    '齒'
+  ]
+  for (const hanzi of mustHave) {
+    test(hanzi, async () => {
+      const data = await getHanziData({ char: hanzi })
+      expect(data?.strokes?.length).toBeGreaterThan(0)
+    })
+  }
+})
+
+describe('SVG data for grass radicals default vs. traditional', () => {
+  const grassChars = [
+    '若',
+    '草',
+    '花',
+    '苦',
+    '莫',
+    '苗'
+  ]
+  for (const grassChar of grassChars) {
+    test(grassChar, async () => {
+      const defaultData = await getHanziData({ char: grassChar })
+      const tradData = await getHanziData({
+        char: grassChar,
+        kind: GetHanziDataKind.TRADITIONAL
+      })
+      expect(tradData.strokes.length).toBe(defaultData.strokes.length + 1)
+      expect(tradData.strokeTypes.slice(0, 4)).toEqual([
+        StrokeType.SHU,
+        StrokeType.HENG,
+        StrokeType.SHU,
+        StrokeType.HENG
+      ])
+    })
+  }
+})
diff --git a/test/templates/is-hanzi.test.ts b/test/templates/is-hanzi.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..78338eda57d9d3ed1cba9468a8e5278f01ccb412
--- /dev/null
+++ b/test/templates/is-hanzi.test.ts
@@ -0,0 +1,29 @@
+import { isMaybeHanzi } from '../../src/components/is-hanzi'
+
+describe('accepts Hanzi', () => {
+  const samples = [
+    '⽱',
+    '⻊',
+    '貘',
+    '貘⻊⽱'
+  ]
+  for (const sample of samples) {
+    test(sample, () => {
+      expect(isMaybeHanzi(sample)).toBe(true)
+    })
+  }
+})
+
+describe('rejects Latin characters, umlauts and punctuation', () => {
+  const samples = [
+    '', // empty string is also not hanzi
+    'asdfÄÜ?…',
+    '…',
+    '-–—'
+  ]
+  for (const sample of samples) {
+    test(sample, () => {
+      expect(isMaybeHanzi(sample)).toBe(false)
+    })
+  }
+})
diff --git a/test/templates/lut.test.ts b/test/templates/lut.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..894b4bad90955d541cfc6380ed8e59168381030b
--- /dev/null
+++ b/test/templates/lut.test.ts
@@ -0,0 +1,41 @@
+import { getHanziData } from '../../src/components/hanzi-data'
+import { lookup } from '../../src/components/hanzi-data/lut'
+
+describe('table properties', () => {
+  const testCases = [
+    { hanzi: '草', prop: 'radicalMeaningDe', expected: 'Gras' },
+    { hanzi: '母', prop: 'radicalMeaningDe', expected: 'Mutter' },
+    {
+      hanzi: '木',
+      prop: 'strokeTypeNumbers',
+      expected: ['①', '②', '③', '④']
+    },
+    {
+      hanzi: '道',
+      prop: 'strokeTypeNumbers',
+      expected: [
+        '④',
+        '③',
+        '①',
+        '③',
+        '②',
+        '⑤',
+        '①',
+        '①',
+        '①',
+        '④',
+        '⑤',
+        '④'
+      ]
+    }
+  ]
+  for (const { hanzi, prop, expected } of testCases) {
+    test(`${prop} of ${hanzi}`, async () => {
+      const actual = lookup(
+        await getHanziData({ char: hanzi }),
+        prop
+      )
+      expect(actual).toEqual(expected)
+    })
+  }
+})