Chapter 10
Adding and fixing words
Words are added a month at a time, corrected at the source and checked by CI. Never edit data/words/ or data/reference/ by hand: fix the source or the parser, then regenerate.
Where things live
The generated evidence under data/reference/ is listed, with its sources, in Data integrity. The files you write, correct or regenerate are these:
| Piece | What it is |
|---|---|
data/word-plan/YYYY-MM.json | The only hand-written content: { date, term } per day, and a headline for a word with an Etymology section (kana when a spelling has several pool words). |
data/words/YYYY-MM.json | Generated entries. Never edit by hand. |
shared/meanings.ts | Where to correct or enrich what a pool word says (VOCAB_FORM_CORRECTIONS, VOCAB_MEANING_ENRICHMENTS), keyed by term kana and applied by servedVocab() when a snapshot is built. The few rows the source list itself has wrong are fixed in scripts/lib/word-list.mjs (VOCAB_MEANING_OVERRIDES, VOCAB_READING_OVERRIDES). |
Commands
| Command | Does |
|---|---|
pnpm data:reference | Rebuilds data/reference/n5-reference.json from pinned sources (the checksum-verified JMdict and KANJIDIC2 files in the repo root and a word list at a fixed commit). |
pnpm data:reference:jlpt | Rebuilds the N4–N1 files, reusing the N5 builder's helpers. |
pnpm data:etymology | Reads Wiktionary Etymology sections from the pinned dump, offline, for every term in the entries and plans plus --terms a,b,c. --prune drops unused pins; --skip-missing skips a word the dump has no Etymology for; --dump <path> names the file. |
pnpm data:sentences | Picks every entry's example sentences from the pinned Tatoeba export, offline (--dir <path> names the extracted files, --check fails if the snapshot is out of date). |
pnpm data:kanji | Copies KANJIDIC2's record of every kanji the entries use out of the level snapshots into data/reference/kanji.json. Fetches nothing; --check fails if it is out of date. |
pnpm data:pitch | Picks each word's pitch accent from the pinned Kanjium list, keeps those UniDic gives too, and writes them and the words that lost one into data/reference/pitch.json (--file <path> names the list, --unidic <path> the dictionary file, --check fails if the snapshot is out of date). |
pnpm data:components | Copies the shapes KRADFILE lists for every kanji in data/reference/kanji.json into data/reference/components.json (--file <path> names the archive, --check fails if the snapshot is out of date). |
pnpm data:strokes | Copies KanjiVG's strokes for every kanji in data/reference/kanji.json into data/reference/strokes.json, keeping those whose count matches KANJIDIC2's (--file <path> names the release, --check fails if the snapshot is out of date). |
pnpm assets:og-font | Rebuilds the fonts the share images are drawn with in server/assets/og/: a subset of Zen Old Mincho Bold cut to the characters of the entries, Outfit copied from @fontsource/outfit, and glyphs.json, the list of characters they can draw. The upstream font is pinned by commit and checksum in scripts/build-og-font.mjs; download it into the repo root (git-ignored) or pass --font <path>. |
pnpm data:words | Generates data/words/ from the plan and the committed sources (the sentences and pitch accents too). Fetches nothing. --check fails if a file is out of date; --keep-going writes every entry that built and lists the failures. |
data:etymology is offline and takes seconds: it reads the Wiktionary dump from Kaikki.org, a 385 MB file that is never committed. Put it in the repo root (it is git-ignored) or point --dump at it; its name, date and checksum are pinned in scripts/lib/wiktionary-dump.mjs and any other file is refused. The reference builders read JMdict and KANJIDIC2 from two files in the repo root, JMdict_e.gz and kanjidic2.xml.gz (git-ignored). EDRDG overwrites both every day, so their size and checksum are pinned in scripts/lib/jmdict.mjs: a missing file is downloaded from the address there, any other copy is refused, and you should keep the two files if you need to rebuild. The word lists are fetched from GitHub.
data:sentences is offline too: it also reads the two pinned EDRDG files the reference builders share, to check the furigana against JMdict and KANJIDIC2, and runs kuromoji and UniDic from node_modules. It reads five files of a Tatoeba export, about a minute's work on about 175 MB that is never committed. Download them from the paths listed in scripts/lib/tatoeba-export.mjs (each is a bz2; the indices come in a tar), extract them into a tatoeba directory in the repo root (it is git-ignored), and the script verifies their size and checksum before it reads anything. Tatoeba overwrites its exports every week, so keep the files if you need to rebuild: the committed snapshot, not the export, is what CI checks. It reads the word plans, so it runs before data:words, which copies the sentences into each entry; data:kanji runs after it, because it reads the entries' spellings.
data:pitch and data:strokes download their pinned files the first time (Kanjium's accent list at a fixed commit, UniDic's release, whose compiled dictionary checks the accents, and KanjiVG's release; size and checksum are in scripts/build-pitch-reference.mjs, scripts/lib/unidic-accent.mjs and scripts/build-strokes-reference.mjs), cache them under node_modules/.cache/, and are offline after that. The UniDic archive is about half a gigabyte; only its dictionary file is kept. Like data:sentences, data:pitch reads the word plans and the level snapshots, so it runs before data:words; data:strokes reads data/reference/kanji.json, so it runs after data:kanji. Neither guesses: a word the accent list does not give for that exact spelling and reading shows no pitch, an accent UniDic does not give for it too is not shown (UniDic is silent on some words, and the snapshot's heldBack names each one with what the two said), and a kanji whose stroke count KanjiVG and KANJIDIC2 disagree on shows no strokes. data:components downloads and caches its pinned archive (EDRDG's KRADFILE, pinned in scripts/build-components-reference.mjs) the same way, reads data/reference/kanji.json, and so also runs after data:kanji; a kanji the file has no line for shows no shapes.
Adding a month
- Choose the words (a person or a model may do this). They must be pool words (N5–N1) that no plan holds yet. Every pool word is planned, spelling and reading, so a further month means a new word list.
- Write the plan
data/word-plan/YYYY-MM.json: one{ "date", "term" }per day, every day of the month. Add"kana"only when a spelling has several pool words (明日, 梅雨). Add a"headline"only to a word that has an Etymology section: one sentence that earns the click and keeps to what the quoted evidence says, which may be the entry's own section or a section of another reading that the entry shows beside it. A word with a headline promises an origin, so the generator refuses it if the dump has no section for its reading; a word without one is built with whatever the sources hold. A new month continues directly after the last day, with no gap; any day already past opens at once. - Pin the evidence:
pnpm data:etymology, andpnpm data:sentencesfor the example sentences andpnpm data:pitchfor the pitch accent (a word with none of either is fine). Every term in the plans is included; add--terms 電話,友達,…for words with no plan yet. Most pool words have no usable Etymology section, so pin a batch with--skip-missing: a word without one is skipped and its entry says the origin is missing. - Generate:
pnpm data:words, thenpnpm data:kanji,pnpm data:strokesandpnpm data:componentsfor any kanji the new words add. It prints every entry it could not build and why, and writes nothing until they are fixed (or use--keep-going). - Read the result once. Entries with no breakdown are normal (the source gave no clean split, or there is no source), as are entries with no layer (irregular spellings). Skim each headline against the quoted lines: it must not claim more than they do.
- Register the month in
shared/words.ts(one import line plus theMONTHSarray). Keepshared/words.tsout ofapp/(the import rule). - Rebuild the share-image fonts if the new words use a kanji they lack:
pnpm assets:og-font. The test fails and names the characters until you do. pnpm test:run. The range and count the docs quote are read live fromdata/words/, so there is nothing to update.
If the generator refuses an entry
| Message | What to do |
|---|---|
| “none for <reading>” or “its only section is for …” | A word with a headline needs an Etymology section for its reading. Drop the headline (the entry then says the origin is missing) or replace the word; do not borrow another reading's section. |
| “2 pool words with that spelling” | Add kana to the plan entry. |
| “no pinned Wiktionary page” | Pin it first (step 3). |
| “KANJIDIC2 reads … as native but its section says it is from Middle Chinese” | The dump attached another reading's etymology (it did for 道 みち). Replace the word; do not edit the snapshot. |
Correcting a word's form, reading or meaning
- Confirm against the JMdict evidence in
data/reference/<level>-reference.json. - Add to
VOCAB_FORM_CORRECTIONS(wrong form or reading) orVOCAB_MEANING_ENRICHMENTS(fuller gloss) inshared/meanings.ts, keyed byterm kana, with areasonciting the JMdict entry id. The word'sidstays unchanged. - Rebuild:
pnpm data:reference(N5) orpnpm data:reference:jlpt(N4–N1), thenpnpm data:words, and commit the diffs.
Refreshing the sources
- JMdict, KANJIDIC2 and word lists: bump
WORD_LIST_SOURCESand/orJMDICT_SOURCE(put the new files in the repo root and record their date, size and checksum), then runpnpm data:referenceandpnpm data:reference:jlpttogether, thenpnpm data:sentences,pnpm data:wordsandpnpm data:kanji, and review every diff. A newer JMdict can reword a tag or a gloss, and then a test or a correction inshared/meanings.tssays so. - Wiktionary: point
WIKTIONARY_DUMPinscripts/lib/wiktionary-dump.mjsat the new file (date, size, checksum), runpnpm data:etymology, review the text diff, then runpnpm data:words: a changed etymology changes the entry, and the diff shows it. - Tatoeba: download the five files named in
scripts/lib/tatoeba-export.mjs, extract them into atatoebadirectory, and put their date, sizes and checksums inTATOEBA_EXPORT. Runpnpm data:sentences, review the diff (sentences change, and some words gain or lose one), thenpnpm data:words. - Kanjium, UniDic and KanjiVG: change the commit or release, size and checksum in
PITCH_SOURCE,UNIDIC_ACCENT_SOURCEorSTROKES_SOURCE, runpnpm data:pitchandpnpm data:words, orpnpm data:strokes, and review the diff. For KRADFILE, change the size and checksum inCOMPONENTS_SOURCEand runpnpm data:components. - Dropped a word?
pnpm data:etymology --pruneremoves pins nothing uses, andpnpm data:sentences,pnpm data:pitch,pnpm data:kanji,pnpm data:strokesandpnpm data:componentsdrop what the word alone used. pnpm test:run: new gaps show up as failing entries.
When a check fails
| Failure | Fix |
|---|---|
| Word not in JMdict | The source list is wrong. Add a VOCAB_FORM_CORRECTIONS entry with a reason citing the JMdict entry id, then rebuild the reference. |
| Meaning not backed | Reword to match JMdict or drop the sense. Don't widen the checker. |
| Entry differs from the generator | Run pnpm data:words and review the diff. Fix the source or scripts/lib/word-entry.mjs, never the entry. |
| Quote not found | The snapshot changed: re-run pnpm data:etymology, then pnpm data:words. Never loosen a quote. |
| Morpheme or tag wrong | It is derived: fix the source (shared/meanings.ts, a re-pin) or the parser, then pnpm data:words. |
| Headline mentions Japanese no evidence contains | Reword it in data/word-plan/. |
| Share-image font missing a character | A new word uses a character the subset lacks. Run pnpm assets:og-font and commit the fonts. A character none of the fonts have (an IPA symbol) is not an error: the card leaves that headline out. |
| Stale reference | Run the reference builder for that level. |
| Sentences or kanji out of step with the entries | Run pnpm data:sentences (with the Tatoeba export in place), pnpm data:pitch, pnpm data:words, then pnpm data:kanji, pnpm data:strokes and pnpm data:components. |
| Entry disagrees with JMdict | The generator takes JMdict's loan source, wasei and ateji notes itself, so a failure means the entry is stale: run pnpm data:words. A word whose text and JMdict truly disagree is a case for a parser change, never a loosened test. |
What the checks cover, and what they cannot, is in Data integrity.