add nihongo grammar app

This commit is contained in:
HDW
2026-07-08 22:24:07 +02:00
parent 4dbddd93d3
commit eac16b35da
5 changed files with 1008 additions and 0 deletions

920
apps/nihongo/configmap.yaml Normal file
View File

@@ -0,0 +1,920 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: nihongo-html
namespace: nihongo
data:
index.html: |
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kody Sensei — CM_7-5 Übung</title>
<style>
:root{
--orange:#f2c58a; --orange-d:#e6a94f; --ink:#4a4a4a; --ink-soft:#6d6d6d;
--blue:#2f7fd6; --wo:#c0392b; --green:#2e8b57; --bg:#fbf7f0; --card:#fff;
--line:#ece3d5; --shadow:0 4px 18px rgba(0,0,0,.08);
}
*{box-sizing:border-box}
body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans JP",sans-serif;
background:var(--bg);color:var(--ink);line-height:1.55}
header{background:var(--orange);padding:14px 20px;display:flex;align-items:center;gap:12px;
box-shadow:var(--shadow);position:sticky;top:0;z-index:10}
header .dog{font-size:28px}
header h1{font-size:17px;margin:0;font-weight:800;color:#555}
header .sub{font-size:12px;color:#7a5b1e;margin-top:2px}
.wrap{max-width:660px;margin:0 auto;padding:18px}
nav{display:flex;gap:8px;margin-bottom:18px;flex-wrap:wrap}
.lessonbar{display:flex;align-items:center;gap:10px;margin-bottom:12px;background:#fff;
border-radius:12px;box-shadow:var(--shadow);padding:10px 14px}
.lessonbar label{font-size:13px;font-weight:700;color:var(--ink-soft);white-space:nowrap}
.lessonbar select{flex:1}
nav button{flex:1;min-width:84px;border:none;border-radius:12px;padding:10px 6px;font-size:13px;
font-weight:700;cursor:pointer;background:#fff;color:var(--ink-soft);box-shadow:var(--shadow);
transition:.15s}
nav button.active{background:var(--orange-d);color:#fff}
.card{background:var(--card);border-radius:18px;box-shadow:var(--shadow);padding:26px 22px;margin-bottom:16px}
.prompt-label{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--orange-d);
font-weight:800;margin-bottom:8px}
.de{font-size:21px;font-weight:600}
.en{font-size:14px;color:var(--ink-soft);margin-top:4px;font-style:italic}
.q{font-size:13px;color:var(--orange-d);font-weight:700;margin-top:8px}
.jp{font-size:30px;margin:16px 0 6px;font-weight:600}
.jp ruby rt{font-size:.5em;color:var(--ink-soft);font-weight:400}
.reading{font-size:16px;color:var(--ink-soft)}
.hidden{display:none}
.btn{display:inline-block;border:none;border-radius:12px;padding:13px 20px;font-size:15px;font-weight:700;
cursor:pointer;transition:.12s}
.btn:active{transform:translateY(1px)}
.btn-primary{background:var(--orange-d);color:#fff;width:100%}
.btn-row{display:flex;gap:10px;margin-top:16px}
.btn-ok{background:var(--green);color:#fff;flex:1}
.btn-no{background:var(--wo);color:#fff;flex:1}
.p-ni{color:var(--blue);font-weight:800}
.p-wo{color:var(--wo);font-weight:800}
.p-de{color:#8e44ad;font-weight:800}
.p-ga{color:#2e8b57;font-weight:800}
.p-wa{color:#b8860b;font-weight:800}
.grammarnote{background:#fff;border-radius:12px;box-shadow:var(--shadow);padding:12px 16px;margin-bottom:14px;
font-size:14px;border-left:4px solid var(--orange-d)}
.grammarnote b{color:var(--orange-d)}
.progress{height:8px;background:#eee;border-radius:99px;overflow:hidden;margin-bottom:14px}
.progress > div{height:100%;background:var(--orange-d);width:0;transition:.3s}
.meta{display:flex;justify-content:space-between;font-size:13px;color:var(--ink-soft);margin-bottom:14px}
.note{font-size:13px;color:var(--ink-soft);background:#fbf3e4;border-left:4px solid var(--orange-d);
padding:10px 14px;border-radius:8px;margin-top:14px;line-height:1.5}
.blanks{font-size:26px;line-height:2.1;margin:14px 0}
.slot{display:inline-block;min-width:46px;border-bottom:3px solid var(--orange-d);text-align:center;
cursor:pointer;color:#bbb}
.slot.filled{color:var(--ink);border-color:var(--green)}
.choices{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
.choices button{border:2px solid var(--line);background:#fff;border-radius:10px;padding:8px 16px;
font-size:20px;cursor:pointer;font-weight:700}
.feedback{margin-top:14px;font-weight:700}
.done{text-align:center;padding:30px}
.done .big{font-size:44px}
.barctl{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;gap:10px}
select.sel{border:2px solid var(--line);border-radius:10px;padding:6px 10px;font-size:13px;background:#fff;color:var(--ink)}
.tip{font-size:12px;color:var(--ink-soft);text-align:center;margin-top:8px}
input.type{width:100%;font-size:22px;padding:12px 14px;border:2px solid var(--line);border-radius:12px;
margin-top:14px;text-align:center;font-family:inherit}
input.type:focus{outline:none;border-color:var(--orange-d)}
input.type.ok{border-color:var(--green);background:#f0fbf4}
input.type.bad{border-color:var(--wo);background:#fdf0ee}
.boxes{display:inline-flex;gap:3px;vertical-align:middle;margin-left:6px}
.boxes i{width:8px;height:8px;border-radius:2px;background:#e2d8c8}
.boxes i.on{background:var(--orange-d)}
.phrase{background:#fff;border-radius:14px;box-shadow:var(--shadow);padding:14px 16px;margin-bottom:10px}
.phrase .jp{font-size:22px;margin:0 0 2px}
.phrase .reading{font-size:13px}
.phrase .de{font-size:15px;color:var(--ink);margin-top:4px;font-weight:500}
h2.section{font-size:16px;color:var(--orange-d);margin:22px 0 10px}
ul.beh{padding-left:18px;margin:0}
ul.beh li{margin-bottom:8px;font-size:14px}
.tiles{display:flex;gap:10px;margin-bottom:6px}
.tile{flex:1;background:#fff;border-radius:14px;box-shadow:var(--shadow);padding:16px 10px;text-align:center}
.tile b{display:block;font-size:26px;color:var(--orange-d)}
.tile span{font-size:12px;color:var(--ink-soft)}
.row{display:flex;align-items:center;justify-content:space-between;background:#fff;border-radius:12px;
box-shadow:var(--shadow);padding:10px 14px;margin-bottom:8px;gap:10px}
.row .lbl{font-size:17px}
.row .rt{font-size:13px;color:var(--ink-soft);white-space:nowrap}
.row .rt .ok{color:var(--green);font-weight:700}
.row .rt .bad{color:var(--wo);font-weight:700}
.btn-reset{background:#f0ece4;color:var(--ink-soft);width:100%;margin-top:8px}
.empty{text-align:center;color:var(--ink-soft);padding:26px}
</style>
</head>
<body>
<header>
<span class="dog">🐕</span>
<div><h1>CM_7-5 · The Purpose of Movement</h1>
<div class="sub">Kody Sensei — Übung für Dominik · ~に ~を ~に いきます</div></div>
</header>
<div class="wrap">
<div class="lessonbar">
<label for="lessonSel">📚 Lektion</label>
<select id="lessonSel" class="sel"></select>
</div>
<nav>
<button data-mode="review" class="active">🔁 Wiederholen</button>
<button data-mode="stats">📊 Fortschritt</button>
<button data-mode="unterricht">🗣️ Unterricht</button>
<button data-mode="vocab">📇 Vokabeln</button>
<button data-mode="verbstem">🔀 ますStamm</button>
<button data-mode="sentence">✍️ Satzbau</button>
<button data-mode="particle">🧩 Partikel</button>
</nav>
<div id="grammarNote" class="grammarnote" style="display:none"></div>
<div class="progress"><div id="bar"></div></div>
<div class="meta"><span id="counter"></span><span id="score"></span></div>
<div id="app"></div>
</div>
<script>
/* ================= DATEN ================= */
const VOCAB = [
{jpr:'<ruby>食<rt>た</rt></ruby>べる', read:"たべる(たべます)", de:"essen", en:"to eat", kana:"たべる"},
{jpr:'<ruby>飲<rt>の</rt></ruby>む', read:"のむ(のみます)", de:"trinken", en:"to drink", kana:"のむ"},
{jpr:'<ruby>作<rt>つく</rt></ruby>る', read:"つくる(つくります)", de:"machen / kochen", en:"to make", kana:"つくる"},
{jpr:'<ruby>行<rt>い</rt></ruby>く', read:"いく(いきます)", de:"gehen", en:"to go", kana:"いく"},
{jpr:'<ruby>働<rt>はたら</rt></ruby>く', read:"はたらく(はたらきます)", de:"arbeiten", en:"to work", kana:"はたらく"},
{jpr:'<ruby>掃除<rt>そうじ</rt></ruby>する', read:"そうじする(そうじします)", de:"putzen / saubermachen", en:"to clean", kana:"そうじする"},
{jpr:'<ruby>仕事<rt>しごと</rt></ruby>', read:"しごと", de:"Arbeit / Job", en:"work, job", kana:"しごと"},
{jpr:'<ruby>地下室<rt>ちかしつ</rt></ruby>', read:"ちかしつ", de:"Keller", en:"basement", kana:"ちかしつ"},
{jpr:'<ruby>晩<rt>ばん</rt></ruby>ご<ruby>飯<rt>はん</rt></ruby>', read:"ばんごはん", de:"Abendessen", en:"dinner", kana:"ばんごはん"},
{jpr:'ラーメン', read:"らーめん", de:"Ramen", en:"ramen", kana:"らーめん"},
{jpr:'<ruby>抹茶<rt>まっちゃ</rt></ruby>', read:"まっちゃ", de:"Matcha", en:"matcha", kana:"まっちゃ"},
{jpr:'コーヒー', read:"こーひー", de:"Kaffee", en:"coffee", kana:"こーひー"},
{jpr:'カフェ', read:"かふぇ", de:"Café", en:"café", kana:"かふぇ"},
{jpr:'レストラン', read:"れすとらん", de:"Restaurant", en:"restaurant", kana:"れすとらん"},
{jpr:'<ruby>家<rt>うち</rt></ruby>', read:"うち", de:"Zuhause", en:"home", kana:"うち"},
{jpr:'<ruby>昨日<rt>きのう</rt></ruby>', read:"きのう", de:"gestern", en:"yesterday", kana:"きのう"},
{jpr:'<ruby>時々<rt>ときどき</rt></ruby>', read:"ときどき", de:"manchmal", en:"sometimes", kana:"ときどき"},
{jpr:'<ruby>毎朝<rt>まいあさ</rt></ruby>', read:"まいあさ", de:"jeden Morgen", en:"every morning", kana:"まいあさ"},
{jpr:'<ruby>毎晩<rt>まいばん</rt></ruby>', read:"まいばん", de:"jeden Abend", en:"every night", kana:"まいばん"},
{jpr:'ちなみに', read:"ちなみに", de:"übrigens", en:"by the way", kana:"ちなみに"},
{jpr:'きれい', read:"きれい", de:"sauber / schön", en:"clean, pretty", kana:"きれい"},
];
const VERBS = [
{jpr:'<ruby>食<rt>た</rt></ruby>べる', de:"essen", group:"Gruppe 2 (ru-Verb)", stemr:'<ruby>食<rt>た</rt></ruby>べ', stemKana:"たべ", masur:'<ruby>食<rt>た</rt></ruby>べます', rule:"ru-Verb: einfach る weg → たべ"},
{jpr:'<ruby>見<rt>み</rt></ruby>る', de:"sehen", group:"Gruppe 2 (ru-Verb)", stemr:'<ruby>見<rt>み</rt></ruby>', stemKana:"み", masur:'<ruby>見<rt>み</rt></ruby>ます', rule:"ru-Verb: る weg → み"},
{jpr:'<ruby>飲<rt>の</rt></ruby>む', de:"trinken", group:"Gruppe 1 (u-Verb)", stemr:'<ruby>飲<rt>の</rt></ruby>み', stemKana:"のみ", masur:'<ruby>飲<rt>の</rt></ruby>みます', rule:"u-Verb: む → み"},
{jpr:'<ruby>作<rt>つく</rt></ruby>る', de:"machen", group:"Gruppe 1 (u-Verb)", stemr:'<ruby>作<rt>つく</rt></ruby>り', stemKana:"つくり", masur:'<ruby>作<rt>つく</rt></ruby>ります', rule:"u-Verb: る → り"},
{jpr:'<ruby>行<rt>い</rt></ruby>く', de:"gehen", group:"Gruppe 1 (u-Verb)", stemr:'<ruby>行<rt>い</rt></ruby>き', stemKana:"いき", masur:'<ruby>行<rt>い</rt></ruby>きます', rule:"u-Verb: く → き"},
{jpr:'<ruby>働<rt>はたら</rt></ruby>く', de:"arbeiten", group:"Gruppe 1 (u-Verb)", stemr:'<ruby>働<rt>はたら</rt></ruby>き', stemKana:"はたらき", masur:'<ruby>働<rt>はたら</rt></ruby>きます', rule:"u-Verb: く → き"},
{jpr:'<ruby>話<rt>はな</rt></ruby>す', de:"sprechen", group:"Gruppe 1 (u-Verb)", stemr:'<ruby>話<rt>はな</rt></ruby>し', stemKana:"はなし", masur:'<ruby>話<rt>はな</rt></ruby>します', rule:"u-Verb: す → し"},
{jpr:'<ruby>買<rt>か</rt></ruby>う', de:"kaufen", group:"Gruppe 1 (u-Verb)", stemr:'<ruby>買<rt>か</rt></ruby>い', stemKana:"かい", masur:'<ruby>買<rt>か</rt></ruby>います', rule:"u-Verb: う → い"},
{jpr:'<ruby>帰<rt>かえ</rt></ruby>る', de:"heimgehen", group:"Gruppe 1 (Ausnahme!)", stemr:'<ruby>帰<rt>かえ</rt></ruby>り', stemKana:"かえり", masur:'<ruby>帰<rt>かえ</rt></ruby>ります', rule:"Sieht aus wie ru-Verb, ist aber u-Verb: る → り"},
{jpr:'<ruby>掃除<rt>そうじ</rt></ruby>する', de:"putzen", group:"Gruppe 3 (suru)", stemr:'<ruby>掃除<rt>そうじ</rt></ruby>し', stemKana:"そうじし", masur:'<ruby>掃除<rt>そうじ</rt></ruby>します', rule:"する-Verb: する → し"},
];
const SENTENCES = [
{de:"Ich gehe nach Yokohama, um Ramen zu essen.", en:"I'll go to Yokohama to eat ramen.",
r:'<ruby>横浜<rt>よこはま</rt></ruby><span class="p-ni">に</span> ラーメン<span class="p-wo">を</span> <ruby>食<rt>た</rt></ruby>べ<span class="p-ni">に</span> <ruby>行<rt>い</rt></ruby>きます。',
read:"よこはまに らーめんを たべに いきます。"},
{de:"Ich gehe nach Kyoto, um Matcha zu trinken.", en:"I'll go to Kyoto to drink matcha.",
r:'<ruby>京都<rt>きょうと</rt></ruby><span class="p-ni">に</span> <ruby>抹茶<rt>まっちゃ</rt></ruby><span class="p-wo">を</span> <ruby>飲<rt>の</rt></ruby>み<span class="p-ni">に</span> <ruby>行<rt>い</rt></ruby>きます。',
read:"きょうとに まっちゃを のみに いきます。"},
{de:"Gehst du (manchmal) ins Café, um Kaffee zu trinken?", en:"Do you go to a café to drink coffee?",
r:'カフェ<span class="p-ni">に</span> コーヒー<span class="p-wo">を</span> <ruby>飲<rt>の</rt></ruby>み<span class="p-ni">に</span> いきますか。',
read:"かふぇに こーひーを のみに いきますか。"},
{de:"Gehst du (manchmal) ins Restaurant, um Abendessen zu essen?", en:"Do you go to a restaurant to eat dinner?",
r:'レストラン<span class="p-ni">に</span> <ruby>晩<rt>ばん</rt></ruby>ごはん<span class="p-wo">を</span> <ruby>食<rt>た</rt></ruby>べ<span class="p-ni">に</span> いきますか。',
read:"れすとらんに ばんごはんを たべに いきますか。"},
{de:"Aber jeden Morgen trinke ich zu Hause Kaffee.", en:"But every morning I drink coffee at home.",
r:'でも <ruby>毎朝<rt>まいあさ</rt></ruby> うち<span class="p-de">で</span> コーヒー<span class="p-wo">を</span> <ruby>飲<rt>の</rt></ruby>みます。',
read:"でも まいあさ うちで こーひーを のみます。"},
{de:"Aber jeden Abend koche ich zu Hause das Abendessen.", en:"But every night I make dinner at home.",
r:'でも <ruby>毎晩<rt>まいばん</rt></ruby> うち<span class="p-de">で</span> <ruby>晩<rt>ばん</rt></ruby>ごはん<span class="p-wo">を</span> <ruby>作<rt>つく</rt></ruby>ります。',
read:"でも まいばん うちで ばんごはんを つくります。"},
{de:"Was hast du gestern gemacht?", en:"What did you do yesterday?",
r:'<ruby>昨日<rt>きのう</rt></ruby> <ruby>何<rt>なに</rt></ruby><span class="p-wo">を</span> しましたか。',
read:"きのう なにを しましたか。"},
{de:"Ich bin zur Arbeit gegangen und habe gearbeitet.", en:"I went to work and worked.",
r:'<ruby>仕事<rt>しごと</rt></ruby><span class="p-ni">に</span> <ruby>行<rt>い</rt></ruby>って <ruby>働<rt>はたら</rt></ruby>きました。',
read:"しごとに いって はたらきました。"},
{de:"Dann habe ich den Keller sauber geputzt.", en:"Then I cleaned the basement.",
r:'そして <ruby>地下室<rt>ちかしつ</rt></ruby><span class="p-wo">を</span> そうじしました。',
read:"そして ちかしつを そうじしました。"},
];
const PHRASES = [
{jpr:'お<ruby>願<rt>ねが</rt></ruby>いします。', read:"おねがいします", de:"Bitte. (allgemeine Bitte)"},
{jpr:'よろしくお<ruby>願<rt>ねが</rt></ruby>いします。', read:"よろしくおねがいします", de:"Bitte um gute Zusammenarbeit (zu Beginn)."},
{jpr:'もう<ruby>一度<rt>いちど</rt></ruby>お<ruby>願<rt>ねが</rt></ruby>いします。', read:"もういちどおねがいします", de:"Noch einmal, bitte."},
{jpr:'ゆっくりお<ruby>願<rt>ねが</rt></ruby>いします。', read:"ゆっくりおねがいします", de:"Langsamer, bitte."},
{jpr:'もう<ruby>少<rt>すこ</rt></ruby>しゆっくり<ruby>話<rt>はな</rt></ruby>してください。', read:"もうすこしゆっくりはなしてください", de:"Bitte sprich etwas langsamer."},
{jpr:'すみません、わかりません。', read:"すみません、わかりません", de:"Entschuldigung, ich verstehe nicht."},
{jpr:'わかりました。', read:"わかりました", de:"Ich habe verstanden."},
{jpr:'〜は<ruby>日本語<rt>にほんご</rt></ruby>で<ruby>何<rt>なん</rt></ruby>ですか。', read:"〜はにほんごでなんですか", de:"Wie heißt … auf Japanisch?"},
{jpr:'〜はどういう<ruby>意味<rt>いみ</rt></ruby>ですか。', read:"〜はどういういみですか", de:"Was bedeutet …?"},
{jpr:'これはどう<ruby>読<rt>よ</rt></ruby>みますか。', read:"これはどうよみますか", de:"Wie liest man das?"},
{jpr:'<ruby>質問<rt>しつもん</rt></ruby>があります。', read:"しつもんがあります", de:"Ich habe eine Frage."},
{jpr:'もう<ruby>一度<rt>いちど</rt></ruby><ruby>言<rt>い</rt></ruby>ってください。', read:"もういちどいってください", de:"Bitte sag es noch einmal."},
{jpr:'<ruby>例<rt>れい</rt></ruby>をお<ruby>願<rt>ねが</rt></ruby>いします。', read:"れいをおねがいします", de:"Ein Beispiel, bitte."},
{jpr:'ちょっと<ruby>待<rt>ま</rt></ruby>ってください。', read:"ちょっとまってください", de:"Einen Moment, bitte."},
{jpr:'<ruby>大丈夫<rt>だいじょうぶ</rt></ruby>です。', read:"だいじょうぶです", de:"Alles gut / passt."},
{jpr:'<ruby>宿題<rt>しゅくだい</rt></ruby>は<ruby>何<rt>なん</rt></ruby>ですか。', read:"しゅくだいはなんですか", de:"Was ist die Hausaufgabe?"},
{jpr:'ありがとうございました。', read:"ありがとうございました", de:"Vielen Dank! (am Ende)"},
{jpr:'また<ruby>来週<rt>らいしゅう</rt></ruby>。', read:"またらいしゅう", de:"Bis nächste Woche."},
{jpr:'ヒントおねがいします。', read:"ひんとおねがいします", de:"Bitte gib mir einen Tipp."},
{jpr:'〜は<ruby>何<rt>なん</rt></ruby>でしたっけ。', read:"〜はなんでしたっけ", de:"Wie war … noch mal?"},
];
const PARTICLES = [
{parts:['<ruby>横浜<rt>よこはま</rt></ruby>',"___","ラーメン","___",'<ruby>食<rt>た</rt></ruby>べ',"___",'<ruby>行<rt>い</rt></ruby>きます。'], ans:["に","を","に"], hint:"nach Yokohama, um Ramen zu essen"},
{parts:["カフェ","___","コーヒー","___",'<ruby>飲<rt>の</rt></ruby>み',"___","いきます。"], ans:["に","を","に"], hint:"ins Café, um Kaffee zu trinken"},
{parts:['まいあさ うち',"___","コーヒー","___",'<ruby>飲<rt>の</rt></ruby>みます。'], ans:["で","を"], hint:"Ort der Handlung, nicht Ziel!"},
{parts:['<ruby>京都<rt>きょうと</rt></ruby>',"___",'<ruby>抹茶<rt>まっちゃ</rt></ruby>',"___",'<ruby>飲<rt>の</rt></ruby>み',"___",'<ruby>行<rt>い</rt></ruby>きます。'], ans:["に","を","に"], hint:"nach Kyoto, um Matcha zu trinken"},
{parts:['<ruby>仕事<rt>しごと</rt></ruby>',"___",'<ruby>行<rt>い</rt></ruby>って <ruby>働<rt>はたら</rt></ruby>きました。'], ans:["に"], hint:"zur Arbeit gehen"},
{parts:['<ruby>地下室<rt>ちかしつ</rt></ruby>',"___","そうじしました。"], ans:["を"], hint:"das Objekt, das geputzt wird"},
];
const CHOICES = ["に","を","で","は","か"];
/* ================= LEKTIONEN (hier hängt Kody-san neue Lektionen an) ================= */
const LESSONS = [
/* ---------- CM_1-2 Question Sentences ---------- */
{id:'cm1-2', code:'CM_1-2', title:'Question Sentences',
grammar:'Fragen mit <b>~ですか</b> beantworten: <b>はい、そうです。</b> (ja) / <b>いいえ、ちがいます。</b> (nein).',
vocab:[
{jpr:'そうです', read:"そうです", de:"genau / so ist es", en:"that's right", kana:"そうです"},
{jpr:'ちがう', read:"ちがう(ちがいます)", de:"falsch / anders sein", en:"to be wrong", kana:"ちがう"},
{jpr:'<ruby>学生<rt>がくせい</rt></ruby>', read:"がくせい", de:"Student", en:"student", kana:"がくせい"},
{jpr:'エンジニア', read:"えんじにあ", de:"Ingenieur", en:"engineer", kana:"えんじにあ"},
{jpr:'はい', read:"はい", de:"ja", en:"yes", kana:"はい"},
{jpr:'いいえ', read:"いいえ", de:"nein", en:"no", kana:"いいえ"},
],
verbs:[],
sentences:[
{de:"Bist du DevOps-Engineer?", en:"Are you a DevOps engineer?", r:'Dさん<span class="p-wa">は</span> DevOps エンジニアですか。', read:"Dさんは DevOps えんじにあですか。"},
{de:"Ja, das stimmt.", en:"Yes, it is.", r:'はい、そうです。', read:"はい、そうです。"},
{de:"Bist du Student?", en:"Are you a student?", r:'Dさん<span class="p-wa">は</span> <ruby>学生<rt>がくせい</rt></ruby>ですか。', read:"Dさんは がくせいですか。"},
{de:"Nein, ich bin kein Student.", en:"No, I'm not a student.", r:'いいえ、<ruby>学生<rt>がくせい</rt></ruby>じゃないです。', read:"いいえ、がくせいじゃないです。"},
{de:"Nein, das stimmt nicht.", en:"No, that's not right.", r:'いいえ、ちがいます。', read:"いいえ、ちがいます。"},
],
particles:[
{parts:['Dさん',"___",'<ruby>学生<rt>がくせい</rt></ruby>ですか。'], ans:["は"], hint:"Themenpartikel"},
]},
/* ---------- CM_3-6 Frequency Adverbs ---------- */
{id:'cm3-6', code:'CM_3-6', title:'Frequency Adverbs',
grammar:'Häufigkeit: <b>ときどき</b> (manchmal) + V<b>ます</b> · <b>あんまり/あまり</b> (nicht sehr) + V<b>ません</b>.',
vocab:[
{jpr:'ときどき', read:"ときどき", de:"manchmal", en:"sometimes", kana:"ときどき"},
{jpr:'あんまり', read:"あんまり(あまり)", de:"nicht besonders", en:"not very", kana:"あんまり"},
{jpr:'ジム', read:"じむ", de:"Fitnessstudio", en:"gym", kana:"じむ"},
{jpr:'カレー', read:"かれー", de:"Curry", en:"curry", kana:"かれー"},
{jpr:'からい', read:"からい", de:"scharf", en:"spicy", kana:"からい"},
{jpr:'<ruby>食<rt>た</rt></ruby>べ<ruby>物<rt>もの</rt></ruby>', read:"たべもの", de:"Essen / Speise", en:"food", kana:"たべもの"},
{jpr:'<ruby>卵<rt>たまご</rt></ruby>', read:"たまご", de:"Ei", en:"egg", kana:"たまご"},
{jpr:'じゃがいも', read:"じゃがいも", de:"Kartoffel", en:"potato", kana:"じゃがいも"},
{jpr:'<ruby>塩<rt>しお</rt></ruby>', read:"しお", de:"Salz", en:"salt", kana:"しお"},
{jpr:'<ruby>料理<rt>りょうり</rt></ruby>', read:"りょうり", de:"Gericht / Küche", en:"dish, cooking", kana:"りょうり"},
{jpr:'ビール', read:"びーる", de:"Bier", en:"beer", kana:"びーる"},
{jpr:'<ruby>高<rt>たか</rt></ruby>い', read:"たかい", de:"teuer / hoch", en:"expensive", kana:"たかい"},
{jpr:'<ruby>好<rt>す</rt></ruby>き', read:"すき", de:"mögen", en:"to like", kana:"すき"},
],
verbs:[],
sentences:[
{de:"Ich gehe manchmal ins Fitnessstudio.", en:"I sometimes go to the gym.", r:'(わたし<span class="p-wa">は</span>)ときどき ジム<span class="p-ni">に</span> いきます。', read:"わたしは ときどき じむに いきます。"},
{de:"Isst du manchmal japanisches Essen?", en:"Do you sometimes eat Japanese food?", r:'Dさん<span class="p-wa">は</span> ときどき <ruby>日本<rt>にほん</rt></ruby>の <ruby>食<rt>た</rt></ruby>べ<ruby>物<rt>もの</rt></ruby><span class="p-wo">を</span> <ruby>食<rt>た</rt></ruby>べますか。', read:"Dさんは ときどき にほんの たべものを たべますか。"},
{de:"Ja, manchmal esse ich Curry.", en:"Yes, I sometimes eat curry.", r:'はい。ときどき カレー<span class="p-wo">を</span> <ruby>食<rt>た</rt></ruby>べます。', read:"はい。ときどき かれーを たべます。"},
{de:"Magst du scharfes Essen?", en:"Do you like spicy food?", r:'からい <ruby>食<rt>た</rt></ruby>べ<ruby>物<rt>もの</rt></ruby><span class="p-ga">が</span> <ruby>好<rt>す</rt></ruby>きですか。', read:"からい たべものが すきですか。"},
{de:"Ich gehe nicht oft ins Fitnessstudio.", en:"I don't go to the gym very often.", r:'(わたし<span class="p-wa">は</span>)あんまり ジム<span class="p-ni">に</span> いきません。', read:"わたしは あんまり じむに いきません。"},
{de:"Ich mag Bier, aber ich trinke nicht viel.", en:"I like beer, but I don't drink much.", r:'ビール<span class="p-ga">が</span> <ruby>好<rt>す</rt></ruby>きです。でも あんまり <ruby>飲<rt>の</rt></ruby>みません。', read:"びーるが すきです。でも あんまり のみません。"},
],
particles:[
{parts:['ときどき ジム',"___",'いきます。'], ans:["に"], hint:"Bewegungsziel"},
{parts:['カレー',"___",'<ruby>食<rt>た</rt></ruby>べます。'], ans:["を"], hint:"Objekt"},
{parts:['からい <ruby>食<rt>た</rt></ruby>べ<ruby>物<rt>もの</rt></ruby>',"___",'<ruby>好<rt>す</rt></ruby>きです。'], ans:["が"], hint:"bei 好き steht が"},
{parts:['あんまり ジム',"___",'いきません。'], ans:["に"], hint:"Bewegungsziel"},
]},
/* ---------- CM_4-1 X があります ---------- */
{id:'cm4-1', code:'CM_4-1', title:'X があります (There is X)',
grammar:'<b>{Ort に}{Nomen が}{あります}</b> für Dinge · <b>{います}</b> für Lebewesen.',
vocab:[
{jpr:'<ruby>屋<rt>や</rt></ruby>', read:"や", de:"…-Laden (Suffix)", en:"shop", kana:"や"},
{jpr:'ラーメン<ruby>屋<rt>や</rt></ruby>', read:"らーめんや", de:"Ramen-Laden", en:"ramen shop", kana:"らーめんや"},
{jpr:'お<ruby>城<rt>しろ</rt></ruby>', read:"おしろ", de:"Burg / Schloss", en:"castle", kana:"おしろ"},
{jpr:'<ruby>町<rt>まち</rt></ruby>', read:"まち", de:"Stadt", en:"town", kana:"まち"},
{jpr:'<ruby>日本人<rt>にほんじん</rt></ruby>', read:"にほんじん", de:"Japaner", en:"Japanese person", kana:"にほんじん"},
{jpr:'たくさん', read:"たくさん", de:"viele / viel", en:"a lot", kana:"たくさん"},
{jpr:'ある', read:"ある(あります)", de:"es gibt (Dinge)", en:"there is (things)", kana:"ある"},
{jpr:'いる', read:"いる(います)", de:"es gibt (Lebewesen)", en:"there is (living)", kana:"いる"},
],
verbs:[],
sentences:[
{de:"In Yokohama gibt es einen Ramen-Laden.", en:"There is a ramen shop in Yokohama.", r:'<ruby>横浜<rt>よこはま</rt></ruby><span class="p-ni">に</span> ラーメン<ruby>屋<rt>や</rt></ruby><span class="p-ga">が</span> あります。', read:"よこはまに らーめんやが あります。"},
{de:"In Osaka gibt es eine Burg.", en:"There is a castle in Osaka.", r:'<ruby>大阪<rt>おおさか</rt></ruby><span class="p-ni">に</span> お<ruby>城<rt>しろ</rt></ruby><span class="p-ga">が</span> あります。', read:"おおさかに おしろが あります。"},
{de:"Auch in Deutschland gibt es Burgen.", en:"There are castles in Germany too.", r:'ドイツ<span class="p-ni">に</span>も お<ruby>城<rt>しろ</rt></ruby><span class="p-ga">が</span> あります。', read:"どいつにも おしろが あります。"},
{de:"Ja, es gibt viele Burgen.", en:"Yes, there are a lot of castles.", r:'はい。たくさん お<ruby>城<rt>しろ</rt></ruby><span class="p-ga">が</span> あります。', read:"はい。たくさん おしろが あります。"},
{de:"In meiner Stadt gibt es Japaner.", en:"There are Japanese people in my city.", r:'<ruby>私<rt>わたし</rt></ruby>の<ruby>町<rt>まち</rt></ruby><span class="p-ni">に</span> <ruby>日本人<rt>にほんじん</rt></ruby><span class="p-ga">が</span> います。', read:"わたしのまちに にほんじんが います。"},
],
particles:[
{parts:['<ruby>横浜<rt>よこはま</rt></ruby>',"___",'ラーメン<ruby>屋<rt>や</rt></ruby>',"___",'あります。'], ans:["に","が"], hint:"Ort に … が あります"},
{parts:['<ruby>大阪<rt>おおさか</rt></ruby>',"___",'お<ruby>城<rt>しろ</rt></ruby>',"___",'あります。'], ans:["に","が"], hint:"Ort に … が あります"},
{parts:['<ruby>私<rt>わたし</rt></ruby>の<ruby>町<rt>まち</rt></ruby>',"___",'<ruby>日本人<rt>にほんじん</rt></ruby>',"___",'います。'], ans:["に","が"], hint:"Lebewesen → います"},
]},
/* ---------- CM_7-3 noun1 は noun2 が adj です ---------- */
{id:'cm7-3', code:'CM_7-3', title:'noun1 は noun2 が adj です',
grammar:'<b>{Nomen1 は}{Nomen2 が}{Adjektiv です}</b> — etwas anhand eines Merkmals beschreiben (Ort/Können/Eigenschaft).',
vocab:[
{jpr:'おいしい', read:"おいしい", de:"lecker", en:"delicious", kana:"おいしい"},
{jpr:'<ruby>有名<rt>ゆうめい</rt></ruby>', read:"ゆうめい", de:"berühmt", en:"famous", kana:"ゆうめい"},
{jpr:'お<ruby>寺<rt>てら</rt></ruby>', read:"おてら", de:"Tempel", en:"temple", kana:"おてら"},
{jpr:'<ruby>上手<rt>じょうず</rt></ruby>', read:"じょうず", de:"gut können", en:"good at", kana:"じょうず"},
{jpr:'<ruby>友達<rt>ともだち</rt></ruby>', read:"ともだち", de:"Freund", en:"friend", kana:"ともだち"},
{jpr:'<ruby>日本語<rt>にほんご</rt></ruby>', read:"にほんご", de:"Japanisch", en:"Japanese (language)", kana:"にほんご"},
{jpr:'みそラーメン', read:"みそらーめん", de:"Miso-Ramen", en:"miso ramen", kana:"みそらーめん"},
{jpr:'<ruby>船<rt>ふね</rt></ruby>', read:"ふね", de:"Schiff", en:"ship", kana:"ふね"},
{jpr:'<ruby>妹<rt>いもうと</rt></ruby>', read:"いもうと", de:"jüngere Schwester", en:"younger sister", kana:"いもうと"},
{jpr:'ゲーム', read:"げーむ", de:"Spiel", en:"game", kana:"げーむ"},
],
verbs:[],
sentences:[
{de:"Bei Ippudo ist der Miso-Ramen lecker.", en:"As for Ippudo, its miso ramen is delicious.", r:'<ruby>一風堂<rt>いっぷうどう</rt></ruby><span class="p-wa">は</span> みそラーメン<span class="p-ga">が</span> おいしいです。', read:"いっぷうどうは みそらーめんが おいしいです。"},
{de:"Bei Coco Ichi ist das Curry lecker.", en:"As for CoCo Ichi, its curry is delicious.", r:'ココイチ<span class="p-wa">は</span> カレー<span class="p-ga">が</span> おいしいです。', read:"ここいちは かれーが おいしいです。"},
{de:"Kyoto ist berühmt für seine Tempel.", en:"As for Kyoto, its temples are famous.", r:'<ruby>京都<rt>きょうと</rt></ruby><span class="p-wa">は</span> お<ruby>寺<rt>てら</rt></ruby><span class="p-ga">が</span> <ruby>有名<rt>ゆうめい</rt></ruby>です。', read:"きょうとは おてらが ゆうめいです。"},
{de:"Flensburg ist berühmt für seine Schiffe.", en:"As for Flensburg, its ships are famous.", r:'フレンスブルク<span class="p-wa">は</span> <ruby>船<rt>ふね</rt></ruby><span class="p-ga">が</span> <ruby>有名<rt>ゆうめい</rt></ruby>です。', read:"ふれんすぶるくは ふねが ゆうめいです。"},
{de:"Mein Freund ist gut in Japanisch.", en:"As for my friend, they're good at Japanese.", r:'<ruby>私<rt>わたし</rt></ruby>の<ruby>友達<rt>ともだち</rt></ruby><span class="p-wa">は</span> <ruby>日本語<rt>にほんご</rt></ruby><span class="p-ga">が</span> <ruby>上手<rt>じょうず</rt></ruby>です。', read:"わたしのともだちは にほんごが じょうずです。"},
{de:"Meine Schwester ist gut in Videospielen.", en:"My sister is good at games.", r:'<ruby>私<rt>わたし</rt></ruby>の<ruby>妹<rt>いもうと</rt></ruby><span class="p-wa">は</span> ゲーム<span class="p-ga">が</span> <ruby>上手<rt>じょうず</rt></ruby>です。', read:"わたしのいもうとは げーむが じょうずです。"},
],
particles:[
{parts:['<ruby>一風堂<rt>いっぷうどう</rt></ruby>',"___",'みそラーメン',"___",'おいしいです。'], ans:["は","が"], hint:"Thema は … Merkmal が"},
{parts:['<ruby>京都<rt>きょうと</rt></ruby>',"___",'お<ruby>寺<rt>てら</rt></ruby>',"___",'<ruby>有名<rt>ゆうめい</rt></ruby>です。'], ans:["は","が"], hint:"Thema は … Merkmal が"},
{parts:['<ruby>友達<rt>ともだち</rt></ruby>',"___",'<ruby>日本語<rt>にほんご</rt></ruby>',"___",'<ruby>上手<rt>じょうず</rt></ruby>です。'], ans:["は","が"], hint:"bei 上手 steht が"},
]},
/* ---------- CM_7-5 The Purpose of Movement (bestehend IDs unverändert) ---------- */
{id:'cm7-5', code:'CM_7-5', title:'The Purpose of Movement',
grammar:'<b>{Ziel に}{Nomen を}{V ますStamm に}{いきます}</b> — irgendwohin gehen, <b>um</b> etwas zu tun. Gegensatz: <b>~で</b> = Ort einer Handlung.',
vocab:VOCAB, verbs:VERBS, sentences:SENTENCES, particles:PARTICLES},
/* ---------- CM_11-3 ~ことがあります ---------- */
{id:'cm11-3', code:'CM_11-3', title:'~ことがあります (Erfahrung)',
grammar:'<b>{V た-Form}{ことが}{あります}</b> — „schon mal … getan haben". <b>こと</b> macht aus dem Verb ein Nomen; た-Form nötig (いく→いった).',
vocab:[
{jpr:'こと', read:"こと", de:"(Nominalisierer)", en:"noun-maker", kana:"こと"},
{jpr:'<ruby>北海道<rt>ほっかいどう</rt></ruby>', read:"ほっかいどう", de:"Hokkaido", en:"Hokkaido", kana:"ほっかいどう"},
{jpr:'そば', read:"そば", de:"Soba(-Nudeln)", en:"soba", kana:"そば"},
{jpr:'<ruby>金沢<rt>かなざわ</rt></ruby>', read:"かなざわ", de:"Kanazawa", en:"Kanazawa", kana:"かなざわ"},
{jpr:'<ruby>行<rt>い</rt></ruby>った', read:"いった", de:"gegangen (た-Form)", en:"went", kana:"いった"},
{jpr:'<ruby>食<rt>た</rt></ruby>べた', read:"たべた", de:"gegessen (た-Form)", en:"ate", kana:"たべた"},
{jpr:'デンマーク', read:"でんまーく", de:"Dänemark", en:"Denmark", kana:"でんまーく"},
],
verbs:[],
sentences:[
{de:"Ich war schon mal in Japan.", en:"I have been to Japan.", r:'<ruby>日本<rt>にほん</rt></ruby><span class="p-ni">に</span> <ruby>行<rt>い</rt></ruby>った こと<span class="p-ga">が</span> あります。', read:"にほんに いった ことが あります。"},
{de:"Ich war schon mal in Osaka.", en:"I have been to Osaka.", r:'<ruby>大阪<rt>おおさか</rt></ruby><span class="p-ni">に</span> <ruby>行<rt>い</rt></ruby>った こと<span class="p-ga">が</span> あります。', read:"おおさかに いった ことが あります。"},
{de:"Warst du schon mal in Hokkaido? — Nein, noch nie.", en:"Have you been to Hokkaido? — No, I haven't.", r:'<ruby>北海道<rt>ほっかいどう</rt></ruby><span class="p-ni">に</span> <ruby>行<rt>い</rt></ruby>った こと<span class="p-ga">が</span> ありますか。いえ。ありません。', read:"ほっかいどうに いった ことが ありますか。いえ。ありません。"},
{de:"Ich habe schon mal Ramen gegessen.", en:"I have eaten ramen.", r:'ラーメン<span class="p-wo">を</span> <ruby>食<rt>た</rt></ruby>べた こと<span class="p-ga">が</span> あります。', read:"らーめんを たべた ことが あります。"},
{de:"Wo hast du es gegessen? — In Kanazawa.", en:"Where did you eat it? — In Kanazawa.", r:'どこ<span class="p-de">で</span> <ruby>食<rt>た</rt></ruby>べましたか。かなざわ<span class="p-de">で</span> <ruby>食<rt>た</rt></ruby>べました。', read:"どこで たべましたか。かなざわで たべました。"},
],
particles:[
{parts:['<ruby>日本<rt>にほん</rt></ruby>',"___",'<ruby>行<rt>い</rt></ruby>った こと',"___",'あります。'], ans:["に","が"], hint:"Ziel に … こと が"},
{parts:['ラーメン',"___",'<ruby>食<rt>た</rt></ruby>べた こと',"___",'あります。'], ans:["を","が"], hint:"Objekt を … こと が"},
{parts:['どこ',"___",'<ruby>食<rt>た</rt></ruby>べましたか。'], ans:["で"], hint:"Ort der Handlung"},
]},
/* ---------- CM_20-5 Name という Item ---------- */
{id:'cm20-5', code:'CM_20-5', title:'Name という Item',
grammar:'<b>{Nomen}{という}{Nomen2}</b> — „ein …(Nomen2) <b>namens</b> …(Nomen)".',
vocab:[
{jpr:'という', read:"という", de:"genannt / namens", en:"called, named", kana:"という"},
{jpr:'<ruby>一風堂<rt>いっぷうどう</rt></ruby>', read:"いっぷうどう", de:"Ippudo (Ramen-Kette)", en:"Ippudo", kana:"いっぷうどう"},
{jpr:'カレー<ruby>屋<rt>や</rt></ruby>', read:"かれーや", de:"Curry-Laden", en:"curry shop", kana:"かれーや"},
{jpr:'たこ<ruby>焼<rt>や</rt></ruby>き', read:"たこやき", de:"Takoyaki", en:"takoyaki", kana:"たこやき"},
{jpr:'ラーメン<ruby>屋<rt>や</rt></ruby>', read:"らーめんや", de:"Ramen-Laden", en:"ramen shop", kana:"らーめんや"},
],
verbs:[],
sentences:[
{de:"Ich war in einem Ramen-Laden namens Ippudo.", en:"I went to a ramen shop called Ippudo.", r:'<ruby>一風堂<rt>いっぷうどう</rt></ruby>という ラーメン<ruby>屋<rt>や</rt></ruby><span class="p-ni">に</span> <ruby>行<rt>い</rt></ruby>きました。', read:"いっぷうどうという らーめんやに いきました。"},
{de:"Ich war in einem Curry-Laden namens Cocos Curry.", en:"I went to a curry shop called Cocos Curry.", r:'ココスカレーという カレー<ruby>屋<rt>や</rt></ruby><span class="p-ni">に</span> <ruby>行<rt>い</rt></ruby>きました。', read:"ここすかれーという かれーやに いきました。"},
{de:"Ich habe ein Gericht namens Takoyaki gegessen.", en:"I ate a food called takoyaki.", r:'たこ<ruby>焼<rt>や</rt></ruby>きという <ruby>食<rt>た</rt></ruby>べ<ruby>物<rt>もの</rt></ruby><span class="p-wo">を</span> <ruby>食<rt>た</rt></ruby>べました。', read:"たこやきという たべものを たべました。"},
],
particles:[
{parts:['<ruby>一風堂<rt>いっぷうどう</rt></ruby>という ラーメン<ruby>屋<rt>や</rt></ruby>',"___",'<ruby>行<rt>い</rt></ruby>きました。'], ans:["に"], hint:"Bewegungsziel"},
{parts:['たこ<ruby>焼<rt>や</rt></ruby>きという <ruby>食<rt>た</rt></ruby>べ<ruby>物<rt>もの</rt></ruby>',"___",'<ruby>食<rt>た</rt></ruby>べました。'], ans:["を"], hint:"Objekt"},
]},
/* ---------- SP_Degree Adverb (crazy) めっちゃ ---------- */
{id:'sp-meccha', code:'SP_めっちゃ', title:'Degree Adverb (crazy)',
grammar:'<b>{Nomen は}{めっちゃ}{Adjektiv です}</b> — めっちゃ = „extrem/voll" (locker, gesprochen; stärker/emotionaler als とても).',
vocab:[
{jpr:'めっちゃ', read:"めっちゃ", de:"extrem / voll (locker)", en:"extremely (casual)", kana:"めっちゃ"},
{jpr:'<ruby>甘<rt>あま</rt></ruby>い', read:"あまい", de:"süß", en:"sweet", kana:"あまい"},
{jpr:'アイスクリーム', read:"あいすくりーむ", de:"Eis", en:"ice cream", kana:"あいすくりーむ"},
{jpr:'いちご', read:"いちご", de:"Erdbeere", en:"strawberry", kana:"いちご"},
{jpr:'とても', read:"とても", de:"sehr (neutral)", en:"very", kana:"とても"},
],
verbs:[],
sentences:[
{de:"Nakamoto ist extrem scharf!", en:"Nakamoto is crazy spicy!", r:'<ruby>仲本<rt>なかもと</rt></ruby><span class="p-wa">は</span> めっちゃ からいです!', read:"なかもとは めっちゃ からいです!"},
{de:"Erdbeer-Eis ist extrem süß!", en:"Strawberry ice cream is crazy sweet!", r:'いちごの アイスクリーム<span class="p-wa">は</span> めっちゃ <ruby>甘<rt>あま</rt></ruby>いです!', read:"いちごの あいすくりーむは めっちゃ あまいです!"},
],
particles:[
{parts:['<ruby>仲本<rt>なかもと</rt></ruby>',"___",'めっちゃ からいです。'], ans:["は"], hint:"Themenpartikel"},
]},
/* ---------- SP_Degree Adverb (fairly) まあまあ ---------- */
{id:'sp-maamaa', code:'SP_まあまあ', title:'Degree Adverb (fairly)',
grammar:'<b>{Nomen は}{まあまあ}{Adjektiv です}</b> — „einigermaßen/mittelmäßig". <b>まあまあ</b> allein = „geht so". Bescheiden auf Lob: <b>まだまだです</b>.',
vocab:[
{jpr:'まあまあ', read:"まあまあ", de:"einigermaßen / geht so", en:"fairly, so-so", kana:"まあまあ"},
{jpr:'おもしろい', read:"おもしろい", de:"interessant", en:"interesting", kana:"おもしろい"},
{jpr:'アニメ', read:"あにめ", de:"Anime", en:"anime", kana:"あにめ"},
{jpr:'レストラン', read:"れすとらん", de:"Restaurant", en:"restaurant", kana:"れすとらん"},
{jpr:'まだまだです', read:"まだまだです", de:"noch ein weiter Weg (bescheiden)", en:"I still have a long way to go", kana:"まだまだです"},
],
verbs:[],
sentences:[
{de:"Dieser Ramen ist einigermaßen scharf.", en:"This ramen is fairly spicy.", r:'このラーメン<span class="p-wa">は</span> まあまあ からいです。', read:"このらーめんは まあまあ からいです。"},
{de:"Dieser Anime ist einigermaßen interessant.", en:"This anime is fairly interesting.", r:'このアニメ<span class="p-wa">は</span> まあまあ おもしろいです。', read:"このあにめは まあまあ おもしろいです。"},
{de:"Dieser Anime ist okay.", en:"This anime is okay.", r:'このアニメ<span class="p-wa">は</span> まあまあです。', read:"このあにめは まあまあです。"},
{de:"Dieses Restaurant ist okay.", en:"This restaurant is okay.", r:'このレストラン<span class="p-wa">は</span> まあまあです。', read:"このれすとらんは まあまあです。"},
{de:"Bescheidene Antwort auf Lob (Ach, noch lange nicht.').", en:"Response to a compliment: I still have a long way to go.", r:'まだまだです。', read:"まだまだです。"},
],
particles:[
{parts:['このラーメン',"___",'まあまあ からいです。'], ans:["は"], hint:"Themenpartikel"},
{parts:['このアニメ',"___",'まあまあです。'], ans:["は"], hint:"Themenpartikel"},
]},
/* ---------- Verbgruppen (ますStamm) ---------- */
{id:'verbs', code:'Verben', title:'Verbgruppen & ます',
grammar:'3 Gruppen: ① <b>Ru-Verben</b> (る weg) ② <b>U-Verben</b> (u→i) ③ <b>unregelmäßig</b> (する→し, くる→き). Hier: höfliche ます-Form.',
vocab:[
{jpr:'<ruby>寝<rt>ね</rt></ruby>る', read:"ねる(ねます)", de:"schlafen", en:"to sleep", kana:"ねる"},
{jpr:'<ruby>起<rt>お</rt></ruby>きる', read:"おきる(おきます)", de:"aufstehen", en:"to get up", kana:"おきる"},
{jpr:'<ruby>読<rt>よ</rt></ruby>む', read:"よむ(よみます)", de:"lesen", en:"to read", kana:"よむ"},
{jpr:'<ruby>聞<rt>き</rt></ruby>く', read:"きく(ききます)", de:"hören / zuhören", en:"to listen", kana:"きく"},
{jpr:'<ruby>来<rt>く</rt></ruby>る', read:"くる(きます)", de:"kommen", en:"to come", kana:"くる"},
{jpr:'<ruby>勉強<rt>べんきょう</rt></ruby>する', read:"べんきょうする", de:"lernen / studieren", en:"to study", kana:"べんきょうする"},
],
verbs:[
{jpr:'<ruby>食<rt>た</rt></ruby>べる', de:"essen", group:"Gruppe 2 (ru-Verb)", stemr:'<ruby>食<rt>た</rt></ruby>べ', stemKana:"たべ", masur:'<ruby>食<rt>た</rt></ruby>べます', rule:"ru-Verb: る weg → たべ"},
{jpr:'<ruby>寝<rt>ね</rt></ruby>る', de:"schlafen", group:"Gruppe 2 (ru-Verb)", stemr:'<ruby>寝<rt>ね</rt></ruby>', stemKana:"ね", masur:'<ruby>寝<rt>ね</rt></ruby>ます', rule:"ru-Verb: る weg → ね"},
{jpr:'<ruby>見<rt>み</rt></ruby>る', de:"sehen", group:"Gruppe 2 (ru-Verb)", stemr:'<ruby>見<rt>み</rt></ruby>', stemKana:"み", masur:'<ruby>見<rt>み</rt></ruby>ます', rule:"ru-Verb: る weg → み"},
{jpr:'<ruby>起<rt>お</rt></ruby>きる', de:"aufstehen", group:"Gruppe 2 (ru-Verb)", stemr:'<ruby>起<rt>お</rt></ruby>き', stemKana:"おき", masur:'<ruby>起<rt>お</rt></ruby>きます', rule:"ru-Verb: る weg → おき"},
{jpr:'<ruby>飲<rt>の</rt></ruby>む', de:"trinken", group:"Gruppe 1 (u-Verb)", stemr:'<ruby>飲<rt>の</rt></ruby>み', stemKana:"のみ", masur:'<ruby>飲<rt>の</rt></ruby>みます', rule:"u-Verb: む → み"},
{jpr:'<ruby>読<rt>よ</rt></ruby>む', de:"lesen", group:"Gruppe 1 (u-Verb)", stemr:'<ruby>読<rt>よ</rt></ruby>み', stemKana:"よみ", masur:'<ruby>読<rt>よ</rt></ruby>みます', rule:"u-Verb: む → み"},
{jpr:'<ruby>話<rt>はな</rt></ruby>す', de:"sprechen", group:"Gruppe 1 (u-Verb)", stemr:'<ruby>話<rt>はな</rt></ruby>し', stemKana:"はなし", masur:'<ruby>話<rt>はな</rt></ruby>します', rule:"u-Verb: す → し"},
{jpr:'<ruby>聞<rt>き</rt></ruby>く', de:"hören", group:"Gruppe 1 (u-Verb)", stemr:'<ruby>聞<rt>き</rt></ruby>き', stemKana:"きき", masur:'<ruby>聞<rt>き</rt></ruby>きます', rule:"u-Verb: く → き"},
{jpr:'<ruby>行<rt>い</rt></ruby>く', de:"gehen", group:"Gruppe 1 (u-Verb)", stemr:'<ruby>行<rt>い</rt></ruby>き', stemKana:"いき", masur:'<ruby>行<rt>い</rt></ruby>きます', rule:"u-Verb: く → き"},
{jpr:'<ruby>帰<rt>かえ</rt></ruby>る', de:"heimgehen", group:"Gruppe 1 (Ausnahme!)", stemr:'<ruby>帰<rt>かえ</rt></ruby>り', stemKana:"かえり", masur:'<ruby>帰<rt>かえ</rt></ruby>ります', rule:"sieht aus wie ru-Verb, ist u-Verb: る → り"},
{jpr:'する', de:"machen / tun", group:"Gruppe 3 (unregelmäßig)", stemr:'し', stemKana:"し", masur:'します', rule:"unregelmäßig: する → し"},
{jpr:'<ruby>勉強<rt>べんきょう</rt></ruby>する', de:"lernen", group:"Gruppe 3 (unregelmäßig)", stemr:'<ruby>勉強<rt>べんきょう</rt></ruby>し', stemKana:"べんきょうし", masur:'<ruby>勉強<rt>べんきょう</rt></ruby>します', rule:"する-Verb: する → し"},
{jpr:'<ruby>来<rt>く</rt></ruby>る', de:"kommen", group:"Gruppe 3 (unregelmäßig)", stemr:'<ruby>来<rt>き</rt></ruby>', stemKana:"き", masur:'<ruby>来<rt>き</rt></ruby>ます', rule:"unregelmäßig: くる → き"},
],
sentences:[
{de:"Ich esse Sushi. (höflich)", en:"I eat sushi.", r:'すし<span class="p-wo">を</span> <ruby>食<rt>た</rt></ruby>べます。', read:"すしを たべます。"},
{de:"Ich sehe einen Anime. (höflich)", en:"I watch anime.", r:'アニメ<span class="p-wo">を</span> <ruby>見<rt>み</rt></ruby>ます。', read:"あにめを みます。"},
{de:"Ich spiele ein Spiel. (höflich)", en:"I play a game.", r:'ゲーム<span class="p-wo">を</span> します。', read:"げーむを します。"},
{de:"Mein Freund kommt nach Japan.", en:"My friend comes to Japan.", r:'<ruby>友達<rt>ともだち</rt></ruby><span class="p-ga">が</span> <ruby>日本<rt>にほん</rt></ruby><span class="p-ni">に</span> <ruby>来<rt>き</rt></ruby>ます。', read:"ともだちが にほんに きます。"},
],
particles:[
{parts:['すし',"___",'<ruby>食<rt>た</rt></ruby>べます。'], ans:["を"], hint:"Objekt"},
{parts:['ゲーム',"___",'します。'], ans:["を"], hint:"Objekt"},
{parts:['<ruby>友達<rt>ともだち</rt></ruby>',"___",'<ruby>日本<rt>にほん</rt></ruby>',"___",'<ruby>来<rt>き</rt></ruby>ます。'], ans:["が","に"], hint:"Subjekt が … Ziel に"},
]},
/* ---------- て-Form ---------- */
{id:'teform', code:'て-Form', title:'te-form conjugations',
grammar:'<b>Ru</b>→て · <b>う/つ/る</b>→って · <b>む/ぶ/ぬ</b>→んで · <b>く</b>→いて (<b>行く→行って</b>!) · <b>ぐ</b>→いで · <b>す</b>→して · する→して, くる→きて.',
vocab:[
{jpr:'<ruby>会<rt>あ</rt></ruby>う', read:"あう", de:"(sich) treffen", en:"to meet", kana:"あう"},
{jpr:'<ruby>待<rt>ま</rt></ruby>つ', read:"まつ", de:"warten", en:"to wait", kana:"まつ"},
{jpr:'<ruby>遊<rt>あそ</rt></ruby>ぶ', read:"あそぶ", de:"spielen / sich vergnügen", en:"to play", kana:"あそぶ"},
{jpr:'<ruby>泳<rt>およ</rt></ruby>ぐ', read:"およぐ", de:"schwimmen", en:"to swim", kana:"およぐ"},
{jpr:'<ruby>書<rt>か</rt></ruby>く', read:"かく", de:"schreiben", en:"to write", kana:"かく"},
],
verbs:[],
sentences:[
{de:"て-Form von 食べる (essen)? — Ru-Verb: る→て", en:"", r:'<ruby>食<rt>た</rt></ruby>べて', read:"たべて"},
{de:"て-Form von 見る (sehen)? — Ru-Verb: る→て", en:"", r:'<ruby>見<rt>み</rt></ruby>て', read:"みて"},
{de:"て-Form von 会う (treffen)? — う→って", en:"", r:'<ruby>会<rt>あ</rt></ruby>って', read:"あって"},
{de:"て-Form von 待つ (warten)? — つ→って", en:"", r:'<ruby>待<rt>ま</rt></ruby>って', read:"まって"},
{de:"て-Form von 帰る (heimgehen)? — る→って", en:"", r:'<ruby>帰<rt>かえ</rt></ruby>って', read:"かえって"},
{de:"て-Form von 読む (lesen)? — む→んで", en:"", r:'<ruby>読<rt>よ</rt></ruby>んで', read:"よんで"},
{de:"て-Form von 遊ぶ (spielen)? — ぶ→んで", en:"", r:'<ruby>遊<rt>あそ</rt></ruby>んで', read:"あそんで"},
{de:"て-Form von 書く (schreiben)? — く→いて", en:"", r:'<ruby>書<rt>か</rt></ruby>いて', read:"かいて"},
{de:"て-Form von 行く (gehen)? — AUSNAHME: →行って", en:"", r:'<ruby>行<rt>い</rt></ruby>って', read:"いって"},
{de:"て-Form von 泳ぐ (schwimmen)? — ぐ→いで", en:"", r:'<ruby>泳<rt>およ</rt></ruby>いで', read:"およいで"},
{de:"て-Form von 話す (sprechen)? — す→して", en:"", r:'<ruby>話<rt>はな</rt></ruby>して', read:"はなして"},
{de:"て-Form von する (machen)? — unregelmäßig", en:"", r:'して', read:"して"},
{de:"て-Form von 来る (kommen)? — unregelmäßig", en:"", r:'<ruby>来<rt>き</rt></ruby>て', read:"きて"},
],
particles:[]},
];
/* ================= RŌMAJI → KANA ================= */
const RMAP={a:'あ',i:'い',u:'う',e:'え',o:'お',
ka:'か',ki:'き',ku:'く',ke:'け',ko:'こ',kya:'きゃ',kyu:'きゅ',kyo:'きょ',
ga:'が',gi:'ぎ',gu:'ぐ',ge:'げ',go:'ご',gya:'ぎゃ',gyu:'ぎゅ',gyo:'ぎょ',
sa:'さ',shi:'し',si:'し',su:'す',se:'せ',so:'そ',sha:'しゃ',shu:'しゅ',sho:'しょ',sya:'しゃ',syu:'しゅ',syo:'しょ',
za:'ざ',ji:'じ',zi:'じ',zu:'ず',ze:'ぜ',zo:'ぞ',ja:'じゃ',ju:'じゅ',jo:'じょ',jya:'じゃ',jyu:'じゅ',jyo:'じょ',
ta:'た',chi:'ち',ti:'ち',tsu:'つ',tu:'つ',te:'て',to:'と',cha:'ちゃ',chu:'ちゅ',cho:'ちょ',tya:'ちゃ',tyu:'ちゅ',tyo:'ちょ',
da:'だ',di:'ぢ',du:'づ',de:'で',do:'ど',
na:'な',ni:'に',nu:'ぬ',ne:'ね',no:'の',nya:'にゃ',nyu:'にゅ',nyo:'にょ',
ha:'は',hi:'ひ',fu:'ふ',hu:'ふ',he:'へ',ho:'ほ',hya:'ひゃ',hyu:'ひゅ',hyo:'ひょ',
ba:'ば',bi:'び',bu:'ぶ',be:'べ',bo:'ぼ',bya:'びゃ',byu:'びゅ',byo:'びょ',
pa:'ぱ',pi:'ぴ',pu:'ぷ',pe:'ぺ',po:'ぽ',pya:'ぴゃ',pyu:'ぴゅ',pyo:'ぴょ',
ma:'ま',mi:'み',mu:'む',me:'め',mo:'も',mya:'みゃ',myu:'みゅ',myo:'みょ',
ya:'や',yu:'ゆ',yo:'よ',
ra:'ら',ri:'り',ru:'る',re:'れ',ro:'ろ',rya:'りゃ',ryu:'りゅ',ryo:'りょ',
wa:'わ',wo:'を',
fa:'ふぁ',fi:'ふぃ',fe:'ふぇ',fo:'ふぉ',
la:'ぁ',li:'ぃ',lu:'ぅ',le:'ぇ',lo:'ぉ',xa:'ぁ',xi:'ぃ',xu:'ぅ',xe:'ぇ',xo:'ぉ',
lya:'ゃ',lyu:'ゅ',lyo:'ょ',xya:'ゃ',xyu:'ゅ',xyo:'ょ',
ltu:'っ',xtu:'っ',ltsu:'っ',lwa:'ゎ',xwa:'ゎ',
'-':'ー'};
function toKana(s){
s=(s||'').toLowerCase().replace(/\s+/g,'');
let o='',i=0;
while(i<s.length){
const c=s[i];
if((c>='぀'&&c<='ヿ')||c==='ー'){o+=c;i++;continue;} // schon Kana
if(c==='t' && s[i+1]==='c' && s[i+2]==='h'){o+='っ';i++;continue;} // "tch" (matcha → まっちゃ)
if(c===s[i+1] && 'bcdfghjkmprstwyz'.includes(c)){o+='っ';i++;continue;} // Doppelkonsonant
if(c==='n' && s[i+1] && !'aiueoy'.includes(s[i+1])){o+='ん';i++;continue;}
if(c==='n' && !s[i+1]){o+='ん';i++;continue;}
let m=false;
for(let L=3;L>=1;L--){const ch=s.substr(i,L); if(RMAP[ch]){o+=RMAP[ch];i+=L;m=true;break;}}
if(!m){o+=c;i++;}
}
return o;
}
const norm = s => toKana(s)
.replace(/[・、。「」!?.,\s]/g,'') // Satzzeichen weg (ー bleibt!)
.replace(/[-ヺ]/g,ch=>String.fromCharCode(ch.charCodeAt(0)-96)); // Katakana → Hiragana, ー unberührt
// Streng: kleines ぇ ≠ großes え, ー muss getippt werden (mit "-"), っ muss stehen — alles wie im Original
/* ================= STATISTIK / PERSISTENZ ================= */
// Pro Karte: {box 0-5, right, wrong, last(Zeitstempel)}
let STATS = JSON.parse(localStorage.getItem('kody_stats')||'{}');
(function migrate(){
// 1) ganz alten Leitner-Stand (nur Zahlen) übernehmen
if(!Object.keys(STATS).length){
const old=JSON.parse(localStorage.getItem('kody_leitner')||'{}');
for(const k in old) STATS[k]={box:old[k]||0,right:0,wrong:0,last:0};
}
// 2) alte IDs ohne Lektions-Präfix der ersten Lektion zuordnen
let ch=false;
for(const k of Object.keys(STATS)){
if(/^(v|vb|s)\d+$/.test(k)){ STATS['cm7-5:'+k]=STATS[k]; delete STATS[k]; ch=true; }
}
if(ch||Object.keys(STATS).length) saveStats();
})();
function saveStats(){ localStorage.setItem('kody_stats',JSON.stringify(STATS)); }
function rec(id){ return STATS[id]||(STATS[id]={box:0,right:0,wrong:0,last:0}); }
const boxOf = id => (STATS[id]&&STATS[id].box)||0;
function bump(id,ok){
if(!id)return; const r=rec(id);
if(ok){ r.right++; r.box=Math.min(5,r.box+1); }
else { r.wrong++; r.box=0; } // was schlecht lief, kommt sofort wieder nach vorne
r.last=Date.now(); saveStats();
}
function boxesHtml(id){let h='<span class="boxes">';for(let k=0;k<5;k++)h+=`<i class="${k<boxOf(id)?'on':''}"></i>`;return h+'</span>';}
function isToday(ts){ if(!ts)return false; const d=new Date(ts),n=new Date();
return d.getFullYear()===n.getFullYear()&&d.getMonth()===n.getMonth()&&d.getDate()===n.getDate(); }
function doneToday(id){ const r=STATS[id]; return !!(r&&isToday(r.last)); }
// Metadaten der Karten (für Fortschritts-Seite) folgt der Lektionsauswahl
function allMeta(){
const o=[];
lessonsActive().forEach(L=>{
L.vocab.forEach((v,i)=>o.push({id:L.id+':v'+i, label:v.jpr+' · '+v.de, deck:'Vokabel'}));
L.verbs.forEach((v,i)=>o.push({id:L.id+':vb'+i, label:v.jpr+' → '+v.stemr, deck:'ますStamm'}));
L.sentences.forEach((s,i)=>o.push({id:L.id+':s'+i, label:s.de, deck:'Satz'}));
});
PHRASES.forEach((p,i)=>o.push({id:'p'+i, label:p.jpr, deck:'Unterricht'}));
return o;
}
// Index über ALLE Lektionen (damit schwache Karten jeder Lektion gefunden werden)
function cardIndex(){
const m={};
const savedLesson=lesson; lesson='all';
[...vocabReview(),...verbItems(),...sentenceItems(),...phraseItems()].forEach(it=>m[it.id]=it);
lesson=savedLesson;
return m;
}
function weakList(){ // geübte Karten, die noch wackeln schwächste zuerst
return allMeta().filter(m=>{const r=STATS[m.id]; return r&&(r.right+r.wrong>0)&&(r.wrong>0||r.box<4);})
.sort((a,b)=>{const ra=STATS[a.id],rb=STATS[b.id]; return (rb.wrong-ra.wrong)||(ra.box-rb.box);});
}
function weakDeck(){ const ci=cardIndex(); return weakList().map(m=>ci[m.id]).filter(Boolean); }
/* ================= DECK-BUILDER ================= */
function lessonsActive(){ return lesson==='all' ? LESSONS : LESSONS.filter(l=>l.id===lesson); }
function vocabItems(){
const out=[];
lessonsActive().forEach(L=>L.vocab.forEach((v,i)=>{ const id=L.id+':v'+i;
out.push(dir==='deJp'
? {id, front:`<div class="de">${v.de}</div><div class="en">${v.en}</div>`,
back:`<div class="jp">${v.jpr}</div><div class="reading">${v.read}</div>`, typeTarget:v.kana}
: {id, front:`<div class="jp">${v.jpr}</div>`,
back:`<div class="de">${v.de}</div><div class="reading">${v.read}</div>`, typeTarget:null});
}));
return out;
}
function vocabReview(){ // fürs Wiederholen immer DE→JP (Produktion)
const out=[];
lessonsActive().forEach(L=>L.vocab.forEach((v,i)=>out.push({id:L.id+':v'+i,
front:`<div class="de">${v.de}</div><div class="en">${v.en}</div>`,
back:`<div class="jp">${v.jpr}</div><div class="reading">${v.read}</div>`, typeTarget:v.kana})));
return out;
}
function verbItems(){
const out=[];
lessonsActive().forEach(L=>L.verbs.forEach((v,i)=>out.push({id:L.id+':vb'+i,
front:`<div class="jp">${v.jpr}</div><div class="en">${v.de} · ${v.group}</div><div class="q">ますStamm? (Form vor に / ます)</div>`,
back:`<div class="jp">${v.stemr}</div><div class="reading">→ ${v.masur}</div><div class="note">${v.rule}</div>`,
typeTarget:v.stemKana})));
return out;
}
function sentenceItems(){
const out=[];
lessonsActive().forEach(L=>L.sentences.forEach((s,i)=>out.push({id:L.id+':s'+i,
front:`<div class="de">${s.de}</div><div class="en">${s.en}</div>`,
back:`<div class="jp">${s.r}</div><div class="reading">${s.read}</div>`,
typeTarget:s.read.replace(/\s/g,'')})));
return out;
}
function particleItems(){
const out=[];
lessonsActive().forEach(L=>L.particles.forEach((p,i)=>out.push({...p, id:L.id+':pt'+i})));
return out;
}
function phraseItems(){
return PHRASES.map((p,i)=>({id:'p'+i,
front:`<div class="de">${p.de}</div>`,
back:`<div class="jp">${p.jpr}</div><div class="reading">${p.read}</div>`,
typeTarget:p.read}));
}
function reviewDeck(){
const items=[...vocabReview(), ...verbItems(), ...phraseItems()];
return items.sort((a,b)=>boxOf(a.id)-boxOf(b.id)); // schwache zuerst
}
/* ================= STATE ================= */
let mode="review", queue=[], idx=0, correct=0, total=0, sessionAll=false;
let dir = localStorage.getItem('kody_dir')||'deJp';
let method = localStorage.getItem('kody_method')||'self'; // 'self' | 'type'
let lesson = localStorage.getItem('kody_lesson')||LESSONS[0].id; // Lektions-Filter oder 'all'
if(lesson!=='all' && !LESSONS.some(l=>l.id===lesson)) lesson=LESSONS[0].id;
const GENERIC=['review','weak','vocab','verbstem','sentence','phrases'];
const app=document.getElementById("app");
const bar=document.getElementById("bar");
const counter=document.getElementById("counter");
const scoreEl=document.getElementById("score");
function shuffle(a){a=a.slice();for(let i=a.length-1;i>0;i--){const j=Math.random()*(i+1)|0;[a[i],a[j]]=[a[j],a[i]]}return a}
function deckFor(m){
if(m==="review") return reviewDeck();
if(m==="weak") return weakDeck();
if(m==="vocab") return shuffle(vocabItems());
if(m==="verbstem") return shuffle(verbItems());
if(m==="sentence") return shuffle(sentenceItems());
if(m==="phrases") return shuffle(phraseItems());
if(m==="particle") return shuffle(particleItems());
return [];
}
function start(m, all=false){
mode=m; idx=0; correct=0; total=0; sessionAll=all;
document.querySelectorAll("nav button").forEach(b=>b.classList.toggle("active",b.dataset.mode===m));
queue=deckFor(m);
// heute schon Erledigtes ausblenden aber NICHT im "schwache Karten"-Modus (der soll die heute verpatzten nochmal bringen)
if(!all && m!=='weak') queue=queue.filter(it=>!(it.id && doneToday(it.id)));
updateGrammarNote();
render();
}
function updateGrammarNote(){
const el=document.getElementById('grammarNote'); if(!el)return;
const ls=lessonsActive();
if(ls.length===1 && ls[0].grammar && ['review','vocab','verbstem','sentence','particle'].includes(mode)){
el.style.display='block'; el.innerHTML='<b>'+ls[0].code+'</b> · '+ls[0].grammar;
} else el.style.display='none';
}
function updateBar(){
const len=queue.length||1;
bar.style.width=(Math.min(idx,len)/len*100)+"%";
counter.textContent = (mode==='unterricht'||mode==='stats')?'' : `${Math.min(idx+1,queue.length)} / ${queue.length}`;
scoreEl.textContent = total>0 ? `✓ ${correct}/${total}` : "";
}
function next(ok){
total++; if(ok)correct++;
const it=queue[idx];
if(it && it.id) bump(it.id, ok);
if(!ok && it) queue.push(it);
idx++; render();
}
function render(){
updateBar();
if(mode==='stats'){ renderStats(); return; }
if(mode==='unterricht'){ renderUnterricht(); return; }
if((GENERIC.includes(mode)||mode==='particle') && queue.length===0){
let msg;
if(mode==='weak')
msg=`<div style="font-size:40px">💪</div><h2>Keine schwachen Karten!</h2>
<p>Alles sitzt gerade gut. Mach eine Runde <b>🔁 Wiederholen</b>, dann füllt sich hier wieder was.</p>
<button class="btn btn-primary" onclick="start('review')">Zum Wiederholen</button>`;
else if(deckFor(mode).length===0)
msg=`<div style="font-size:40px">📭</div><h2>Keine Aufgaben hier</h2>
<p>Diese Lektion hat für diesen Modus keine Karten. Wähle oben eine andere Lektion oder einen anderen Reiter.</p>`;
else if(!sessionAll)
msg=`<div style="font-size:40px">✅</div><h2>Für heute erledigt!</h2>
<p>Diese Übung hast du heute schon komplett durch. Morgen kommen die Karten automatisch wieder.</p>
<button class="btn btn-primary" onclick="start('${mode}',true)">Trotzdem nochmal (alle)</button>`;
else
msg=`<div style="font-size:40px">🎉</div><h2>Nichts mehr zu üben.</h2>`;
app.innerHTML=`<div class="card empty">${msg}</div>`; bar.style.width='100%'; return;
}
if(idx>=queue.length){ done(); return; }
if(mode==='particle'){ renderParticle(); return; }
renderCard();
}
function done(){
const pct=total?Math.round(correct/total*100):0;
app.innerHTML=`<div class="card done">
<div class="big">${pct>=80?"🎉":pct>=50?"👍":"💪"}</div>
<h2>Fertig!</h2><p>${correct} von ${total} richtig · ${pct}%</p>
<button class="btn btn-primary" onclick="start('${mode}',true)">Nochmal</button></div>`;
bar.style.width="100%";
}
/* ========== GENERISCHE KARTE (Selbstkontrolle ODER Tippen) ========== */
function methodToggle(hasType){
const dirSel = (mode==='vocab')
? `<select class="sel" id="dir">
<option value="deJp"${dir==='deJp'?' selected':''}>DE → JP</option>
<option value="jpDe"${dir==='jpDe'?' selected':''}>JP → DE</option></select>` : '';
const mSel = hasType
? `<select class="sel" id="method">
<option value="self"${method==='self'?' selected':''}>Selbstkontrolle</option>
<option value="type"${method==='type'?' selected':''}>Tippen</option></select>` : '';
return `<div class="barctl"><span class="prompt-label">${modeLabel()}</span><span>${dirSel} ${mSel}</span></div>`;
}
function modeLabel(){
return {review:'Wiederholen',vocab:dir==='deJp'?'Deutsch → 日本語':'日本語 → Deutsch',
verbstem:'Verbstamm bilden',sentence:'Satz bauen',phrases:'Unterrichts-Satz'}[mode]||'';
}
function renderCard(){
const it=queue[idx];
const hasType = !!it.typeTarget;
const useType = hasType && method==='type';
app.innerHTML=`<div class="card">
${methodToggle(hasType)}
${it.front}
<div id="ans" class="hidden">${it.back}${it.id?('<div class="reading" style="margin-top:8px">Level: '+boxesHtml(it.id)+'</div>'):''}</div>
<div id="io"></div>
<div class="tip">${useType?'Tippe Kana <b>oder</b> Rōmaji (z.B. „tabe" → たべ). Enter prüft.':'Sag die Antwort <b>laut</b>, bevor du aufdeckst.'}</div>
</div>`;
const d=document.getElementById('dir'); if(d) d.onchange=e=>{dir=e.target.value;localStorage.setItem('kody_dir',dir);start('vocab');};
const mm=document.getElementById('method'); if(mm) mm.onchange=e=>{method=e.target.value;localStorage.setItem('kody_method',method);render();};
const io=document.getElementById('io');
if(useType){
io.innerHTML=`<input class="type" id="inp" autocomplete="off" autocapitalize="off" spellcheck="false" placeholder="…">
<div id="fb" class="feedback"></div>
<button class="btn btn-primary" id="chk" style="margin-top:14px">Prüfen</button>`;
const inp=document.getElementById('inp'); inp.focus();
const check=()=>{
const ok = norm(inp.value)===norm(it.typeTarget);
document.getElementById('ans').classList.remove('hidden');
inp.classList.add(ok?'ok':'bad');
document.getElementById('fb').innerHTML = ok
? '<span style="color:var(--green)">✓ Richtig!</span>'
: `<span style="color:var(--wo)">Du: ${toKana(inp.value)||'—'} · Richtig: ${it.typeTarget}</span>`;
document.getElementById('io').querySelector('#chk').outerHTML =
ok ? `<button class="btn btn-primary" style="margin-top:14px" onclick="next(true)">Weiter →</button>`
: `<div class="btn-row"><button class="btn btn-no" onclick="next(false)">Weiter (falsch)</button>
<button class="btn btn-ok" onclick="next(true)">War doch richtig ✓</button></div>`;
};
document.getElementById('chk').onclick=check;
inp.addEventListener('keydown',e=>{if(e.key==='Enter')check();});
} else {
io.innerHTML=`<button class="btn btn-primary" style="margin-top:16px" onclick="reveal()">Antwort zeigen</button>`;
window.reveal=()=>{
document.getElementById('ans').classList.remove('hidden');
io.innerHTML=`<div class="btn-row">
<button class="btn btn-no" onclick="next(false)">✗ Nochmal</button>
<button class="btn btn-ok" onclick="next(true)">✓ Gewusst</button></div>`;
};
}
}
/* ========== PARTIKEL ========== */
let pFill=[], activeSlot=0;
function renderParticle(){
const q=queue[idx]; pFill=q.ans.map(()=>null); let slot=-1;
const html=q.parts.map(p=>{ if(p==="___"){slot++;const s=slot;return `<span class="slot" id="slot${s}" onclick="selSlot(${s})">_</span>`;} return p;}).join(" ");
app.innerHTML=`<div class="card">
<span class="prompt-label">Setz die Partikel ein</span>
<div class="en" style="margin-bottom:6px">${q.hint}</div>
<div class="blanks">${html}</div>
<div class="choices">${CHOICES.map(c=>`<button onclick="pick('${c}')">${c}</button>`).join("")}</div>
<div id="fb" class="feedback"></div>
<div id="ctl"><button class="btn btn-primary" style="margin-top:16px" onclick="checkP()">Prüfen</button></div>
<div class="tip"><span class="p-ni">に</span> Ziel/Zweck · <span class="p-wo">を</span> Objekt · <span class="p-de">で</span> Ort der Handlung</div>
</div>`;
activeSlot=0; hi();
}
function hi(){document.querySelectorAll(".slot").forEach((s,i)=>s.style.background=i===activeSlot?"#fef6e8":"");}
window.selSlot=i=>{activeSlot=i;hi();};
window.pick=c=>{ pFill[activeSlot]=c; const el=document.getElementById("slot"+activeSlot); el.textContent=c; el.classList.add("filled"); if(activeSlot<pFill.length-1){activeSlot++;hi();}};
window.checkP=()=>{
const q=queue[idx]; const fb=document.getElementById("fb");
if(pFill.includes(null)){fb.innerHTML='<span style="color:var(--wo)">Erst alle Lücken füllen.</span>';return;}
const ok=pFill.every((p,i)=>p===q.ans[i]);
if(ok){ fb.innerHTML='<span style="color:var(--green)">✓ Perfekt!</span>'; document.querySelectorAll(".slot").forEach(s=>s.style.color="var(--green)");
document.getElementById("ctl").innerHTML=`<button class="btn btn-primary" style="margin-top:16px" onclick="next(true)">Weiter →</button>`;
}else{ fb.innerHTML='<span style="color:var(--wo)">Richtig: '+q.ans.join(" · ")+'</span>';
document.getElementById("ctl").innerHTML=`<button class="btn btn-primary" style="margin-top:16px" onclick="next(false)">Weiter →</button>`; }
};
/* ========== FORTSCHRITT / STATISTIK ========== */
function renderStats(){
bar.style.width="100%"; counter.textContent=''; scoreEl.textContent='';
const meta=allMeta();
let att=0, mastered=0, right=0, wrong=0, last=0, newCount=0;
meta.forEach(m=>{const r=STATS[m.id];
if(r&&(r.right+r.wrong>0)){att++; right+=r.right; wrong+=r.wrong; if(r.box>=4)mastered++; if(r.last>last)last=r.last;}
else newCount++;});
const acc = (right+wrong)? Math.round(right/(right+wrong)*100) : 0;
const weak=weakList().slice(0,12);
const strong=meta.filter(m=>{const r=STATS[m.id]; return r&&r.box>=4;}).slice(0,12);
const lastTxt = last? new Date(last).toLocaleDateString('de-DE',{day:'2-digit',month:'2-digit',year:'2-digit',hour:'2-digit',minute:'2-digit'}) : '—';
const rowHtml=(m)=>{const r=STATS[m.id]||{right:0,wrong:0};
return `<div class="row"><span class="lbl">${m.label}</span>
<span class="rt">${boxesHtml(m.id)} &nbsp;<span class="ok">✓${r.right}</span> <span class="bad">✗${r.wrong}</span></span></div>`;};
app.innerHTML=`
<div class="tiles">
<div class="tile"><b>${att}</b><span>geübt</span></div>
<div class="tile"><b>${mastered}</b><span>gemeistert</span></div>
<div class="tile"><b>${acc}%</b><span>Genauigkeit</span></div>
</div>
<div class="note">Von ${meta.length} Karten sind ${newCount} noch neu. Zuletzt geübt: ${lastTxt}.</div>
<h2 class="section">⚠️ Schwach — üben!</h2>
${weak.length? `<button class="btn btn-primary" style="margin-bottom:10px" onclick="start('weak')">🎯 Nur diese ${weakList().length} schwachen Karten üben</button>
${weak.map(rowHtml).join("")}` : `<div class="empty">Noch keine Schwachstellen — oder noch nichts geübt. 🙂</div>`}
<h2 class="section">💪 Stark</h2>
${strong.length? strong.map(rowHtml).join("") : `<div class="empty">Noch keine gemeisterten Karten. Üben lohnt sich!</div>`}
<button class="btn btn-reset" onclick="resetStats()">🗑️ Fortschritt zurücksetzen</button>`;
}
window.resetStats=()=>{ if(confirm('Wirklich den gesamten Lernfortschritt löschen?')){ STATS={}; saveStats(); localStorage.removeItem('kody_leitner'); renderStats(); } };
/* ========== UNTERRICHT (Verhalten + Sätze) ========== */
function renderUnterricht(){
bar.style.width="100%";
app.innerHTML=`<div class="card">
<span class="prompt-label">Verhalten im Unterricht</span>
<ul class="beh">
<li><b>Aktiv sprechen.</b> Antworte in ganzen Sätzen — dein Ziel ist Produktion, nicht nur Verstehen.</li>
<li><b>Nicht schweigen.</b> Wenn du hängst, sag es auf Japanisch: „ちょっと待ってください" statt Pause.</li>
<li><b>Nachfragen auf Japanisch.</b> Nutze die Sätze unten, statt ins Deutsche/Englische zu wechseln.</li>
<li><b>Laut & deutlich.</b> Lieber falsch und laut als richtig und leise.</li>
<li><b>Mitschreiben.</b> Neue Wörter sofort in die App / Notizen.</li>
<li><b>Begrüßung:</b> zu Beginn „よろしくお願いします", am Ende „ありがとうございました".</li>
</ul>
<button class="btn btn-primary" style="margin-top:18px" onclick="start('phrases')">🎯 Diese Sätze üben (DE → JP)</button>
</div>
<h2 class="section">Nützliche Sätze im Unterricht</h2>
${PHRASES.map(p=>`<div class="phrase"><div class="jp">${p.jpr}</div><div class="reading">${p.read}</div><div class="de">${p.de}</div></div>`).join("")}`;
counter.textContent=''; scoreEl.textContent='';
}
/* ========== INIT ========== */
function fillLessonSel(){
const sel=document.getElementById('lessonSel');
let html=LESSONS.map(l=>`<option value="${l.id}">${l.code} · ${l.title}</option>`).join('');
if(LESSONS.length>1) html+=`<option value="all">🔀 Alle Lektionen (gemischt)</option>`;
sel.innerHTML=html; sel.value=lesson;
sel.onchange=e=>{ lesson=e.target.value; localStorage.setItem('kody_lesson',lesson); start(mode); };
}
fillLessonSel();
document.querySelectorAll("nav button").forEach(b=>b.onclick=()=>start(b.dataset.mode));
start("review");
</script>
</body>
</html>

View File

@@ -0,0 +1,47 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: nihongo
namespace: nihongo
spec:
replicas: 1
selector:
matchLabels:
app: nihongo
template:
metadata:
labels:
app: nihongo
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
volumeMounts:
- name: html
mountPath: /usr/share/nginx/html
readOnly: true
resources:
requests:
cpu: 10m
memory: 16Mi
limits:
cpu: 100m
memory: 64Mi
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 15
periodSeconds: 30
volumes:
- name: html
configMap:
name: nihongo-html

25
apps/nihongo/ingress.yaml Normal file
View File

@@ -0,0 +1,25 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nihongo-ingress
namespace: nihongo
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
traefik.ingress.kubernetes.io/router.entrypoints: web, websecure
spec:
ingressClassName: traefik
rules:
- host: nihongo.unreg-hdw.de
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nihongo-service
port:
number: 80
tls:
- hosts:
- nihongo.unreg-hdw.de
secretName: nihongo-tls-cert

View File

@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: nihongo

12
apps/nihongo/service.yaml Normal file
View File

@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: nihongo-service
namespace: nihongo
spec:
selector:
app: nihongo
ports:
- protocol: TCP
port: 80
targetPort: 80