From 4317c65c458f615495fe681d71d1b6582cebf6e5 Mon Sep 17 00:00:00 2001
From: Philipp Stadler <a51820432@unet.univie.ac.at>
Date: Mon, 15 Jul 2024 11:04:31 +0000
Subject: [PATCH] ci: add automatic releases

---
 .gitignore                                    |   4 +-
 .gitlab-ci.yml                                |   8 +-
 src/templates/bijective/.template-spec.yaml   |  16 +
 src/templates/bijective/backward/back.html    |  23 ++
 src/templates/bijective/backward/front.html   |  29 ++
 src/templates/bijective/forward/back.html     |  23 ++
 src/templates/bijective/forward/front.html    |  29 ++
 src/templates/facts/.template-spec.yaml       |  14 +
 src/templates/facts/q_a/back.html             |  23 ++
 src/templates/facts/q_a/front.html            |  29 ++
 src/templates/hanzi/.template-spec.yaml       |  26 ++
 src/templates/hanzi/write/back.html           |  28 ++
 src/templates/hanzi/write/front.html          |  36 ++
 src/templates/index.html                      |  90 +++++
 src/templates/molaoshi/.template-spec.yaml    |  45 +++
 src/templates/molaoshi/hear/back.html         |  68 ++++
 src/templates/molaoshi/hear/front.html        |  45 +++
 .../molaoshi/identify_radical/back.html       |  35 ++
 .../molaoshi/identify_radical/front.html      |  39 ++
 .../identify_radical_traditional/back.html    |  35 ++
 .../identify_radical_traditional/front.html   |  39 ++
 src/templates/molaoshi/read_hanzi/back.html   |  76 ++++
 src/templates/molaoshi/read_hanzi/front.html  |  39 ++
 .../molaoshi/read_hanzi_traditional/back.html |  76 ++++
 .../read_hanzi_traditional/front.html         |  39 ++
 src/templates/molaoshi/read_pinyin/back.html  |  81 +++++
 src/templates/molaoshi/read_pinyin/front.html |  34 ++
 src/templates/molaoshi/speak/back.html        |  81 +++++
 src/templates/molaoshi/speak/front.html       |  34 ++
 src/templates/molaoshi/write/back.html        |  81 +++++
 src/templates/molaoshi/write/front.html       |  34 ++
 test/build/fixtures/anki/.apkg-spec.yaml      |   8 -
 test/build/fixtures/anki/Test.apkg            | Bin 55648 -> 0 bytes
 test/build/fixtures/csv/.apkg-spec.yaml       |  18 -
 test/build/fixtures/csv/content.csv           |   1 -
 .../csv_updated_content/.apkg-spec.yaml       |  18 -
 .../fixtures/csv_updated_content/content.csv  |   1 -
 .../templates_updated/q_a/.template-spec.yaml |  14 -
 .../templates_updated/q_a/q_a/back.html       |   1 -
 .../templates_updated/q_a/q_a/front.html      |   1 -
 test/build/test_export_apkgs.py               | 337 ------------------
 test/templates/hanzi-data.test.ts             | 202 +++++++++++
 test/templates/is-hanzi.test.ts               |  29 ++
 test/templates/lut.test.ts                    |  41 +++
 44 files changed, 1526 insertions(+), 404 deletions(-)
 create mode 100644 src/templates/bijective/.template-spec.yaml
 create mode 100644 src/templates/bijective/backward/back.html
 create mode 100644 src/templates/bijective/backward/front.html
 create mode 100644 src/templates/bijective/forward/back.html
 create mode 100644 src/templates/bijective/forward/front.html
 create mode 100644 src/templates/facts/.template-spec.yaml
 create mode 100644 src/templates/facts/q_a/back.html
 create mode 100644 src/templates/facts/q_a/front.html
 create mode 100644 src/templates/hanzi/.template-spec.yaml
 create mode 100644 src/templates/hanzi/write/back.html
 create mode 100644 src/templates/hanzi/write/front.html
 create mode 100644 src/templates/index.html
 create mode 100644 src/templates/molaoshi/.template-spec.yaml
 create mode 100644 src/templates/molaoshi/hear/back.html
 create mode 100644 src/templates/molaoshi/hear/front.html
 create mode 100644 src/templates/molaoshi/identify_radical/back.html
 create mode 100644 src/templates/molaoshi/identify_radical/front.html
 create mode 100644 src/templates/molaoshi/identify_radical_traditional/back.html
 create mode 100644 src/templates/molaoshi/identify_radical_traditional/front.html
 create mode 100644 src/templates/molaoshi/read_hanzi/back.html
 create mode 100644 src/templates/molaoshi/read_hanzi/front.html
 create mode 100644 src/templates/molaoshi/read_hanzi_traditional/back.html
 create mode 100644 src/templates/molaoshi/read_hanzi_traditional/front.html
 create mode 100644 src/templates/molaoshi/read_pinyin/back.html
 create mode 100644 src/templates/molaoshi/read_pinyin/front.html
 create mode 100644 src/templates/molaoshi/speak/back.html
 create mode 100644 src/templates/molaoshi/speak/front.html
 create mode 100644 src/templates/molaoshi/write/back.html
 create mode 100644 src/templates/molaoshi/write/front.html
 delete mode 100644 test/build/fixtures/anki/.apkg-spec.yaml
 delete mode 100644 test/build/fixtures/anki/Test.apkg
 delete mode 100644 test/build/fixtures/csv/.apkg-spec.yaml
 delete mode 100644 test/build/fixtures/csv/content.csv
 delete mode 100644 test/build/fixtures/csv_updated_content/.apkg-spec.yaml
 delete mode 100644 test/build/fixtures/csv_updated_content/content.csv
 delete mode 100644 test/build/fixtures/templates_updated/q_a/.template-spec.yaml
 delete mode 100644 test/build/fixtures/templates_updated/q_a/q_a/back.html
 delete mode 100644 test/build/fixtures/templates_updated/q_a/q_a/front.html
 delete mode 100644 test/build/test_export_apkgs.py
 create mode 100644 test/templates/hanzi-data.test.ts
 create mode 100644 test/templates/is-hanzi.test.ts
 create mode 100644 test/templates/lut.test.ts

diff --git a/.gitignore b/.gitignore
index 8edad87..406c7f3 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 6865d46..4e3030b 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 0000000..ed45670
--- /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 0000000..84534fb
--- /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 0000000..8fb070c
--- /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 0000000..afbae1a
--- /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 0000000..6552410
--- /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 0000000..fe384c1
--- /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 0000000..1ac8a07
--- /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 0000000..9dce46e
--- /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 0000000..0908f30
--- /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 0000000..b176010
--- /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 0000000..f58e910
--- /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 0000000..700a612
--- /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 0000000..5f982c7
--- /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 0000000..a32ec6f
--- /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 0000000..6d7b05d
--- /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 0000000..0d1a17e
--- /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 0000000..e0302ce
--- /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 0000000..7a642be
--- /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 0000000..ea10b1c
--- /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 0000000..a16bcfd
--- /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 0000000..2238436
--- /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 0000000..e95f0c0
--- /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 0000000..99cfaf7
--- /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 0000000..71d7131
--- /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 0000000..7674257
--- /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 0000000..b80aea9
--- /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 0000000..a93e52e
--- /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 0000000..6417876
--- /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 0000000..ed82b42
--- /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 92ef748..0000000
--- 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
GIT binary patch
literal 0
HcmV?d00001

literal 55648
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

diff --git a/test/build/fixtures/csv/.apkg-spec.yaml b/test/build/fixtures/csv/.apkg-spec.yaml
deleted file mode 100644
index 274a5ed..0000000
--- 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 3c98112..0000000
--- 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 c20f61a..0000000
--- 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 d1f9267..0000000
--- 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 02ef56a..0000000
--- 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 5cc0404..0000000
--- 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 51c00f1..0000000
--- 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 22b46f4..0000000
--- 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 0000000..cadfc82
--- /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 0000000..78338ed
--- /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 0000000..894b4ba
--- /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)
+    })
+  }
+})
-- 
GitLab