feat(curator): curate update keeps a server's dictionaries current
EDRDG's licence asks of servers showing its dictionaries that they update
them at least monthly. `curate update --into <folder>` downloads EDRDG's
files of the day (JMdict with Tatoeba's sentences, KANJIDIC2), converts
them with the JLPT levels the curator now brings along (yomitan-jlpt-vocab's
lists, CC BY-SA 4.0, and the kanji levels of kanji-data's kanji.json: both
static, in data/ with their origin), and puts each file in place at once
(written beside it as .part, then renamed over it), so the server never
reads half a file; a failure leaves the files as they were. With --every 7
it keeps running and updates whenever the files are a week old, as their
dates say, so a restart doesn't download again; after a failure it tries
again in six hours. A real update takes some 50 seconds and 400 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-09 16:40:19 +02:00
|
|
|
import 'dart:convert';
|
|
|
|
|
import 'dart:io';
|
|
|
|
|
|
feat(curator): Tatoeba's sentences as examples in every new language
JMdict brings Tatoeba's sentences for Japanese; the other languages'
dictionaries had none (CC-CEDICT) or Wiktionary's own, often
quotations. Each file now takes Tatoeba's sentences of the week (its
per-language exports, CC BY 2.0 FR) as its entries' first examples,
which become a card's hints: each sentence with a translation into the
dictionary's meaning language goes to the entries its words find, cut
as the app cuts a line of subtitles (at spaces, Korean's particles off,
Chinese by its longest words), up to three for each, those using the
word as its headword writes it first (Haus rather than zu Hause; 学习
rather than 學習 in the simplified dictionary), then those nearest seven
words long. German's common words nearly all get some (8,849 of
9,292), in ten seconds and under 300 MB; a dictionary whose sentences
can't be had keeps its words. `curate tatoeba` does the same for a file
made by hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:49:34 +02:00
|
|
|
import 'package:archive/archive.dart';
|
|
|
|
|
|
feat(curator): characters of Chinese and Korean, the Greek alphabet, Korean words from KRDict
For parity with Japanese's KANJIDIC2 and kana:
- Unihan (Unicode License v3) makes characters' dictionaries: the 8,105
standard simplified characters (unihan-hans) and the traditional ones in
use (unihan-hant), told apart and ranked by CC-CEDICT's words (Unihan
calls 这 its own traditional variant; 後 ranks as the 后 of 以后), with
pinyin, meanings, strokes, Kangxi radical, HSK 2025 character levels,
the 3,500 common ones and Hong Kong's grades; and Korean Hanja
(unihan-ko) with their sounds, the 1,800 of education, and their native
readings from Wiktionary (물 수), with the 40 Hangul letters as kana are.
- The Greek alphabet, the curator's own (CC0): letters with names,
transliteration and sound.
- KRDict, the Basic Korean Dictionary for learners (CC BY-SA 2.0 KR,
downloaded without an account), replaces Wiktionary for Korean's words:
learners' levels, pronunciation where it differs from the spelling
(같이 [가치]), Hanja, English senses; Wiktionary's update keeps the
words' forms (가다's 99) and the Hanja's readings beside it for both.
- The update converts KRDict when its page offers another export and the
characters weekly, and removes the files of languages no longer kept
or of sources replaced.
Measured on the real data: Unihan 3 s; KRDict 54,870 entries in 24 s
and under 600 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 12:08:10 +02:00
|
|
|
import 'package:halkyon_learn_curator/src/sources/krdict.dart';
|
|
|
|
|
import 'package:halkyon_learn_curator/src/sources/wiktionary.dart';
|
feat(curator): curate update keeps a server's dictionaries current
EDRDG's licence asks of servers showing its dictionaries that they update
them at least monthly. `curate update --into <folder>` downloads EDRDG's
files of the day (JMdict with Tatoeba's sentences, KANJIDIC2), converts
them with the JLPT levels the curator now brings along (yomitan-jlpt-vocab's
lists, CC BY-SA 4.0, and the kanji levels of kanji-data's kanji.json: both
static, in data/ with their origin), and puts each file in place at once
(written beside it as .part, then renamed over it), so the server never
reads half a file; a failure leaves the files as they were. With --every 7
it keeps running and updates whenever the files are a week old, as their
dates say, so a restart doesn't download again; after a failure it tries
again in six hours. A real update takes some 50 seconds and 400 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-09 16:40:19 +02:00
|
|
|
import 'package:halkyon_learn_curator/src/update.dart';
|
|
|
|
|
import 'package:test/test.dart';
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
late Directory into;
|
|
|
|
|
|
|
|
|
|
setUp(() => into = Directory.systemTemp.createTempSync('dictionaries'));
|
|
|
|
|
tearDown(() => into.deleteSync(recursive: true));
|
|
|
|
|
|
|
|
|
|
/// Fetches the samples in place of EDRDG's files of the day.
|
|
|
|
|
Future<void> samples(Uri uri, File file) async {
|
|
|
|
|
final sample = uri == jmdictSource
|
|
|
|
|
? 'test/fixtures/jmdict-e-examp-sample.xml'
|
|
|
|
|
: 'test/fixtures/kanjidic2-sample.xml';
|
|
|
|
|
await file.writeAsBytes(gzip.encode(await File(sample).readAsBytes()));
|
|
|
|
|
}
|
|
|
|
|
|
feat(curator): Tatoeba's sentences as examples in every new language
JMdict brings Tatoeba's sentences for Japanese; the other languages'
dictionaries had none (CC-CEDICT) or Wiktionary's own, often
quotations. Each file now takes Tatoeba's sentences of the week (its
per-language exports, CC BY 2.0 FR) as its entries' first examples,
which become a card's hints: each sentence with a translation into the
dictionary's meaning language goes to the entries its words find, cut
as the app cuts a line of subtitles (at spaces, Korean's particles off,
Chinese by its longest words), up to three for each, those using the
word as its headword writes it first (Haus rather than zu Hause; 学习
rather than 學習 in the simplified dictionary), then those nearest seven
words long. German's common words nearly all get some (8,849 of
9,292), in ten seconds and under 300 MB; a dictionary whose sentences
can't be had keeps its words. `curate tatoeba` does the same for a file
made by hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:49:34 +02:00
|
|
|
/// Tatoeba's exports, as the fixtures hold a few of their lines, packed
|
|
|
|
|
/// as Tatoeba packs them; null for any other address.
|
|
|
|
|
List<int>? tatoeba(Uri uri) {
|
|
|
|
|
final name = uri.pathSegments.last.replaceAll('.bz2', '');
|
|
|
|
|
final fixture = File('test/fixtures/tatoeba-$name');
|
|
|
|
|
return fixture.existsSync()
|
|
|
|
|
? BZip2Encoder().encodeBytes(fixture.readAsBytesSync())
|
|
|
|
|
: null;
|
|
|
|
|
}
|
|
|
|
|
|
feat(curator): curate update keeps a server's dictionaries current
EDRDG's licence asks of servers showing its dictionaries that they update
them at least monthly. `curate update --into <folder>` downloads EDRDG's
files of the day (JMdict with Tatoeba's sentences, KANJIDIC2), converts
them with the JLPT levels the curator now brings along (yomitan-jlpt-vocab's
lists, CC BY-SA 4.0, and the kanji levels of kanji-data's kanji.json: both
static, in data/ with their origin), and puts each file in place at once
(written beside it as .part, then renamed over it), so the server never
reads half a file; a failure leaves the files as they were. With --every 7
it keeps running and updates whenever the files are a week old, as their
dates say, so a restart doesn't download again; after a failure it tries
again in six hours. A real update takes some 50 seconds and 400 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-09 16:40:19 +02:00
|
|
|
/// The header of the dictionary file [name] in [into].
|
|
|
|
|
Map<String, Object?> header(String name) => jsonDecode(
|
|
|
|
|
utf8
|
|
|
|
|
.decode(gzip.decode(File('${into.path}/$name').readAsBytesSync()))
|
|
|
|
|
.split('\n')
|
|
|
|
|
.first,
|
|
|
|
|
) as Map<String, Object?>;
|
|
|
|
|
|
|
|
|
|
test("writes JMdict and KANJIDIC2 from EDRDG's files, the JLPT levels from "
|
|
|
|
|
"the curator's data", () async {
|
|
|
|
|
final written = await updateDictionaries(
|
|
|
|
|
into: into,
|
|
|
|
|
data: Directory('data'),
|
|
|
|
|
fetch: samples,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
[for (final file in written) file.uri.pathSegments.last],
|
|
|
|
|
['jmdict-en.jsonl.gz', 'kanjidic-en.jsonl.gz'],
|
|
|
|
|
);
|
|
|
|
|
expect(header('jmdict-en.jsonl.gz')['sourceVersion'], '2026-10-09');
|
|
|
|
|
expect(
|
|
|
|
|
[
|
|
|
|
|
for (final credit in header('jmdict-en.jsonl.gz')['credits']! as List)
|
|
|
|
|
(credit as Map)['name'],
|
|
|
|
|
],
|
|
|
|
|
['JMdict', 'JLPT vocabulary lists', 'Tatoeba'],
|
|
|
|
|
);
|
|
|
|
|
expect(header('kanjidic-en.jsonl.gz')['kind'], 'characters');
|
feat(dictionary): the radicals, each listing the kanji written with it
Beside Words and Kanji & kana, a Radicals tab lists the 214 Kangxi
radicals by their strokes, with the forms Japanese writes them as inside
kanji (亻, 氵, 艹, …). One chosen lists the kanji KANJIDIC2 files under
it, in the dictionary's order, to add or to see how one is written; the
tab shares the characters' bloc, so a kanji added there shows as held in
a search too.
The curator labels each kanji with its classical radical (`radical:N`),
so the server's existing label queries serve the tab unchanged. Its
files now say which conversion made them (`.conversion`): an instance
running a curator that converts differently converts again at once,
rather than at its weekly update, so the radicals appear right after a
deploy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 02:13:06 +02:00
|
|
|
// Nothing half-written left behind; which conversion made them.
|
feat(curator): curate update keeps a server's dictionaries current
EDRDG's licence asks of servers showing its dictionaries that they update
them at least monthly. `curate update --into <folder>` downloads EDRDG's
files of the day (JMdict with Tatoeba's sentences, KANJIDIC2), converts
them with the JLPT levels the curator now brings along (yomitan-jlpt-vocab's
lists, CC BY-SA 4.0, and the kanji levels of kanji-data's kanji.json: both
static, in data/ with their origin), and puts each file in place at once
(written beside it as .part, then renamed over it), so the server never
reads half a file; a failure leaves the files as they were. With --every 7
it keeps running and updates whenever the files are a week old, as their
dates say, so a restart doesn't download again; after a failure it tries
again in six hours. A real update takes some 50 seconds and 400 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-09 16:40:19 +02:00
|
|
|
expect(
|
|
|
|
|
[for (final entity in into.listSync()) entity.uri.pathSegments.last]
|
|
|
|
|
..sort(),
|
feat(dictionary): the radicals, each listing the kanji written with it
Beside Words and Kanji & kana, a Radicals tab lists the 214 Kangxi
radicals by their strokes, with the forms Japanese writes them as inside
kanji (亻, 氵, 艹, …). One chosen lists the kanji KANJIDIC2 files under
it, in the dictionary's order, to add or to see how one is written; the
tab shares the characters' bloc, so a kanji added there shows as held in
a search too.
The curator labels each kanji with its classical radical (`radical:N`),
so the server's existing label queries serve the tab unchanged. Its
files now say which conversion made them (`.conversion`): an instance
running a curator that converts differently converts again at once,
rather than at its weekly update, so the radicals appear right after a
deploy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 02:13:06 +02:00
|
|
|
['.conversion', 'jmdict-en.jsonl.gz', 'kanjidic-en.jsonl.gz'],
|
feat(curator): curate update keeps a server's dictionaries current
EDRDG's licence asks of servers showing its dictionaries that they update
them at least monthly. `curate update --into <folder>` downloads EDRDG's
files of the day (JMdict with Tatoeba's sentences, KANJIDIC2), converts
them with the JLPT levels the curator now brings along (yomitan-jlpt-vocab's
lists, CC BY-SA 4.0, and the kanji levels of kanji-data's kanji.json: both
static, in data/ with their origin), and puts each file in place at once
(written beside it as .part, then renamed over it), so the server never
reads half a file; a failure leaves the files as they were. With --every 7
it keeps running and updates whenever the files are a week old, as their
dates say, so a restart doesn't download again; after a failure it tries
again in six hours. A real update takes some 50 seconds and 400 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-09 16:40:19 +02:00
|
|
|
);
|
feat(dictionary): the radicals, each listing the kanji written with it
Beside Words and Kanji & kana, a Radicals tab lists the 214 Kangxi
radicals by their strokes, with the forms Japanese writes them as inside
kanji (亻, 氵, 艹, …). One chosen lists the kanji KANJIDIC2 files under
it, in the dictionary's order, to add or to see how one is written; the
tab shares the characters' bloc, so a kanji added there shows as held in
a search too.
The curator labels each kanji with its classical radical (`radical:N`),
so the server's existing label queries serve the tab unchanged. Its
files now say which conversion made them (`.conversion`): an instance
running a curator that converts differently converts again at once,
rather than at its weekly update, so the radicals appear right after a
deploy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 02:13:06 +02:00
|
|
|
expect(File('${into.path}/.conversion').readAsStringSync(), '$conversion');
|
feat(curator): curate update keeps a server's dictionaries current
EDRDG's licence asks of servers showing its dictionaries that they update
them at least monthly. `curate update --into <folder>` downloads EDRDG's
files of the day (JMdict with Tatoeba's sentences, KANJIDIC2), converts
them with the JLPT levels the curator now brings along (yomitan-jlpt-vocab's
lists, CC BY-SA 4.0, and the kanji levels of kanji-data's kanji.json: both
static, in data/ with their origin), and puts each file in place at once
(written beside it as .part, then renamed over it), so the server never
reads half a file; a failure leaves the files as they were. With --every 7
it keeps running and updates whenever the files are a week old, as their
dates say, so a restart doesn't download again; after a failure it tries
again in six hours. A real update takes some 50 seconds and 400 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-09 16:40:19 +02:00
|
|
|
expect(lastUpdated(into), isNotNull);
|
|
|
|
|
});
|
|
|
|
|
|
feat(dictionary): the radicals, each listing the kanji written with it
Beside Words and Kanji & kana, a Radicals tab lists the 214 Kangxi
radicals by their strokes, with the forms Japanese writes them as inside
kanji (亻, 氵, 艹, …). One chosen lists the kanji KANJIDIC2 files under
it, in the dictionary's order, to add or to see how one is written; the
tab shares the characters' bloc, so a kanji added there shows as held in
a search too.
The curator labels each kanji with its classical radical (`radical:N`),
so the server's existing label queries serve the tab unchanged. Its
files now say which conversion made them (`.conversion`): an instance
running a curator that converts differently converts again at once,
rather than at its weekly update, so the radicals appear right after a
deploy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-10 02:13:06 +02:00
|
|
|
test('files another conversion made, or one from before the curator said, '
|
|
|
|
|
'are converted again at once', () async {
|
|
|
|
|
await updateDictionaries(
|
|
|
|
|
into: into,
|
|
|
|
|
data: Directory('data'),
|
|
|
|
|
fetch: samples,
|
|
|
|
|
);
|
|
|
|
|
final made = File('${into.path}/$conversionFile')
|
|
|
|
|
..writeAsStringSync('${conversion - 1}');
|
|
|
|
|
expect(lastUpdated(into), isNull);
|
|
|
|
|
|
|
|
|
|
made.deleteSync();
|
|
|
|
|
expect(lastUpdated(into), isNull);
|
|
|
|
|
});
|
|
|
|
|
|
feat(curator): curate update keeps a server's dictionaries current
EDRDG's licence asks of servers showing its dictionaries that they update
them at least monthly. `curate update --into <folder>` downloads EDRDG's
files of the day (JMdict with Tatoeba's sentences, KANJIDIC2), converts
them with the JLPT levels the curator now brings along (yomitan-jlpt-vocab's
lists, CC BY-SA 4.0, and the kanji levels of kanji-data's kanji.json: both
static, in data/ with their origin), and puts each file in place at once
(written beside it as .part, then renamed over it), so the server never
reads half a file; a failure leaves the files as they were. With --every 7
it keeps running and updates whenever the files are a week old, as their
dates say, so a restart doesn't download again; after a failure it tries
again in six hours. A real update takes some 50 seconds and 400 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-09 16:40:19 +02:00
|
|
|
test('a download that fails leaves the files there as they were', () async {
|
|
|
|
|
await updateDictionaries(
|
|
|
|
|
into: into,
|
|
|
|
|
data: Directory('data'),
|
|
|
|
|
fetch: samples,
|
|
|
|
|
);
|
|
|
|
|
final before = File('${into.path}/jmdict-en.jsonl.gz').readAsBytesSync();
|
|
|
|
|
|
|
|
|
|
await expectLater(
|
|
|
|
|
updateDictionaries(
|
|
|
|
|
into: into,
|
|
|
|
|
data: Directory('data'),
|
|
|
|
|
fetch: (uri, file) => uri == kanjidicSource
|
|
|
|
|
? Future.error(const HttpException('EDRDG answered 503.'))
|
|
|
|
|
: samples(uri, file),
|
|
|
|
|
),
|
|
|
|
|
throwsA(isA<HttpException>()),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(File('${into.path}/jmdict-en.jsonl.gz').readAsBytesSync(), before);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('a folder missing a file was never brought up to date', () {
|
|
|
|
|
expect(lastUpdated(into), isNull);
|
|
|
|
|
});
|
feat(curator): dictionaries of German, Greek and English from Wiktionary
The languages besides Japanese come from the English Wiktionary as
Wiktextract reads it (decided 2026-10-14: German, Greek and English
first, then Chinese, then Korean): kaikki.org's raw dump, every language
in one file, as its per-language files are being withdrawn.
`curate wiktionary --languages de,el,en` reads it once, streamed, and
writes German and Greek with Wiktionary's English meanings and English
with what its translation tables give in German, the app's other
language (`wiktionary-de-en`, `-el-en`, `-en-de`; sources
`wiktionary-<language>`, as the server holds one source per meaning
language). Inflected forms, and Wiktionary's entries for forms alone
("Häusern: dative plural of Haus"), find their word instead of being
entries; names and affixes are left out, as are articles that tables
give cells of their own, abbreviations and rare spellings, which would
match common words. Entries keep their part of speech, genders, IPA,
principal forms and examples keyed by language as JMdict's.
wordfreq's small lists (CC BY-SA 4.0, crediting SUBTLEX) in data/ rank
the words by how often they are used and label the 15,000 most used
`common`. Typing without accents or umlauts finds a word (`strasse`,
`haeuser`, `spiti`).
The format gains `textKeys`: what finds an entry in running text, its
own forms, never its meanings, which would match words of the meaning
language in a subtitle. `curate update --wiktionary de,el,en` keeps the
languages current: it asks kaikki.org when the dump last changed and
converts it again only when it is newer, or the languages or the
conversion changed. The whole dump converts in about ten minutes in
under 300 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:19:16 +02:00
|
|
|
|
|
|
|
|
group("Wiktionary's dump", () {
|
|
|
|
|
var dumped = DateTime.utc(2026, 10, 3, 8, 24);
|
|
|
|
|
var opened = 0;
|
|
|
|
|
|
|
|
|
|
setUp(() {
|
|
|
|
|
dumped = DateTime.utc(2026, 10, 3, 8, 24);
|
|
|
|
|
opened = 0;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Future<DateTime> modified(Uri uri) async => dumped;
|
|
|
|
|
|
|
|
|
|
/// The sample in place of kaikki.org's dump.
|
|
|
|
|
Future<Download> open(Uri uri) async {
|
|
|
|
|
opened++;
|
|
|
|
|
return (
|
|
|
|
|
modified: dumped,
|
|
|
|
|
bytes: Stream.value(
|
|
|
|
|
gzip.encode(
|
|
|
|
|
File('test/fixtures/wiktionary-sample.jsonl').readAsBytesSync(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
feat(curator): characters of Chinese and Korean, the Greek alphabet, Korean words from KRDict
For parity with Japanese's KANJIDIC2 and kana:
- Unihan (Unicode License v3) makes characters' dictionaries: the 8,105
standard simplified characters (unihan-hans) and the traditional ones in
use (unihan-hant), told apart and ranked by CC-CEDICT's words (Unihan
calls 这 its own traditional variant; 後 ranks as the 后 of 以后), with
pinyin, meanings, strokes, Kangxi radical, HSK 2025 character levels,
the 3,500 common ones and Hong Kong's grades; and Korean Hanja
(unihan-ko) with their sounds, the 1,800 of education, and their native
readings from Wiktionary (물 수), with the 40 Hangul letters as kana are.
- The Greek alphabet, the curator's own (CC0): letters with names,
transliteration and sound.
- KRDict, the Basic Korean Dictionary for learners (CC BY-SA 2.0 KR,
downloaded without an account), replaces Wiktionary for Korean's words:
learners' levels, pronunciation where it differs from the spelling
(같이 [가치]), Hanja, English senses; Wiktionary's update keeps the
words' forms (가다's 99) and the Hanja's readings beside it for both.
- The update converts KRDict when its page offers another export and the
characters weekly, and removes the files of languages no longer kept
or of sources replaced.
Measured on the real data: Unihan 3 s; KRDict 54,870 entries in 24 s
and under 600 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 12:08:10 +02:00
|
|
|
Future<List<File>> update(Set<String> languages, {bool korean = false}) =>
|
|
|
|
|
updateWiktionary(
|
|
|
|
|
into: into,
|
|
|
|
|
data: Directory('data'),
|
|
|
|
|
languages: languages,
|
|
|
|
|
korean: korean,
|
|
|
|
|
modified: modified,
|
|
|
|
|
open: open,
|
|
|
|
|
fetch: (uri, file) => file.writeAsBytes(
|
|
|
|
|
tatoeba(uri) ?? (throw HttpException('$uri', uri: uri)),
|
|
|
|
|
),
|
|
|
|
|
);
|
feat(curator): dictionaries of German, Greek and English from Wiktionary
The languages besides Japanese come from the English Wiktionary as
Wiktextract reads it (decided 2026-10-14: German, Greek and English
first, then Chinese, then Korean): kaikki.org's raw dump, every language
in one file, as its per-language files are being withdrawn.
`curate wiktionary --languages de,el,en` reads it once, streamed, and
writes German and Greek with Wiktionary's English meanings and English
with what its translation tables give in German, the app's other
language (`wiktionary-de-en`, `-el-en`, `-en-de`; sources
`wiktionary-<language>`, as the server holds one source per meaning
language). Inflected forms, and Wiktionary's entries for forms alone
("Häusern: dative plural of Haus"), find their word instead of being
entries; names and affixes are left out, as are articles that tables
give cells of their own, abbreviations and rare spellings, which would
match common words. Entries keep their part of speech, genders, IPA,
principal forms and examples keyed by language as JMdict's.
wordfreq's small lists (CC BY-SA 4.0, crediting SUBTLEX) in data/ rank
the words by how often they are used and label the 15,000 most used
`common`. Typing without accents or umlauts finds a word (`strasse`,
`haeuser`, `spiti`).
The format gains `textKeys`: what finds an entry in running text, its
own forms, never its meanings, which would match words of the meaning
language in a subtitle. `curate update --wiktionary de,el,en` keeps the
languages current: it asks kaikki.org when the dump last changed and
converts it again only when it is newer, or the languages or the
conversion changed. The whole dump converts in about ten minutes in
under 300 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:19:16 +02:00
|
|
|
|
|
|
|
|
test(
|
|
|
|
|
'is converted for the languages named, German and Greek with English '
|
|
|
|
|
'meanings, English with German ones, and ranked by frequency',
|
|
|
|
|
() async {
|
|
|
|
|
final written = await update({'de', 'el', 'en'});
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
[for (final file in written) file.uri.pathSegments.last]..sort(),
|
|
|
|
|
[
|
|
|
|
|
'wiktionary-de-en.jsonl.gz',
|
|
|
|
|
'wiktionary-el-en.jsonl.gz',
|
|
|
|
|
'wiktionary-en-de.jsonl.gz',
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
expect(
|
|
|
|
|
header('wiktionary-de-en.jsonl.gz')['sourceVersion'],
|
|
|
|
|
'2026-10-03',
|
|
|
|
|
);
|
|
|
|
|
expect([
|
|
|
|
|
for (final credit
|
|
|
|
|
in header('wiktionary-de-en.jsonl.gz')['credits']! as List)
|
|
|
|
|
(credit as Map)['name'],
|
|
|
|
|
], contains('wordfreq'));
|
|
|
|
|
expect(File('${into.path}/$wiktionaryStateFile').existsSync(), isTrue);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
test(
|
|
|
|
|
'is downloaded again only once it changed, or the languages did',
|
|
|
|
|
() async {
|
|
|
|
|
await update({'de'});
|
|
|
|
|
expect(await update({'de'}), isEmpty);
|
|
|
|
|
expect(opened, 1);
|
|
|
|
|
|
|
|
|
|
dumped = DateTime.utc(2026, 11, 2);
|
|
|
|
|
expect(await update({'de'}), hasLength(1));
|
|
|
|
|
expect(opened, 2);
|
|
|
|
|
|
|
|
|
|
expect(await update({'de', 'el'}), hasLength(2));
|
|
|
|
|
expect(opened, 3);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
test('nothing is asked for without languages', () async {
|
|
|
|
|
expect(await update({}), isEmpty);
|
|
|
|
|
expect(opened, 0);
|
|
|
|
|
});
|
feat(curator): characters of Chinese and Korean, the Greek alphabet, Korean words from KRDict
For parity with Japanese's KANJIDIC2 and kana:
- Unihan (Unicode License v3) makes characters' dictionaries: the 8,105
standard simplified characters (unihan-hans) and the traditional ones in
use (unihan-hant), told apart and ranked by CC-CEDICT's words (Unihan
calls 这 its own traditional variant; 後 ranks as the 后 of 以后), with
pinyin, meanings, strokes, Kangxi radical, HSK 2025 character levels,
the 3,500 common ones and Hong Kong's grades; and Korean Hanja
(unihan-ko) with their sounds, the 1,800 of education, and their native
readings from Wiktionary (물 수), with the 40 Hangul letters as kana are.
- The Greek alphabet, the curator's own (CC0): letters with names,
transliteration and sound.
- KRDict, the Basic Korean Dictionary for learners (CC BY-SA 2.0 KR,
downloaded without an account), replaces Wiktionary for Korean's words:
learners' levels, pronunciation where it differs from the spelling
(같이 [가치]), Hanja, English senses; Wiktionary's update keeps the
words' forms (가다's 99) and the Hanja's readings beside it for both.
- The update converts KRDict when its page offers another export and the
characters weekly, and removes the files of languages no longer kept
or of sources replaced.
Measured on the real data: Unihan 3 s; KRDict 54,870 entries in 24 s
and under 600 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 12:08:10 +02:00
|
|
|
|
|
|
|
|
test("keeps what Korean's dictionaries take from it, even without "
|
|
|
|
|
'languages of its words', () async {
|
|
|
|
|
expect(await update({}, korean: true), isEmpty);
|
|
|
|
|
expect(opened, 1);
|
|
|
|
|
|
|
|
|
|
final kept = WiktionaryKorean.fromJson(
|
|
|
|
|
utf8.decode(
|
|
|
|
|
gzip.decode(
|
|
|
|
|
File('${into.path}/$wiktionaryKoreanFile').readAsBytesSync(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(kept.hanja['水'], [(hun: '물', eum: '수')]);
|
|
|
|
|
expect(await update({}, korean: true), isEmpty);
|
|
|
|
|
expect(opened, 1);
|
|
|
|
|
});
|
feat(curator): dictionaries of German, Greek and English from Wiktionary
The languages besides Japanese come from the English Wiktionary as
Wiktextract reads it (decided 2026-10-14: German, Greek and English
first, then Chinese, then Korean): kaikki.org's raw dump, every language
in one file, as its per-language files are being withdrawn.
`curate wiktionary --languages de,el,en` reads it once, streamed, and
writes German and Greek with Wiktionary's English meanings and English
with what its translation tables give in German, the app's other
language (`wiktionary-de-en`, `-el-en`, `-en-de`; sources
`wiktionary-<language>`, as the server holds one source per meaning
language). Inflected forms, and Wiktionary's entries for forms alone
("Häusern: dative plural of Haus"), find their word instead of being
entries; names and affixes are left out, as are articles that tables
give cells of their own, abbreviations and rare spellings, which would
match common words. Entries keep their part of speech, genders, IPA,
principal forms and examples keyed by language as JMdict's.
wordfreq's small lists (CC BY-SA 4.0, crediting SUBTLEX) in data/ rank
the words by how often they are used and label the 15,000 most used
`common`. Typing without accents or umlauts finds a word (`strasse`,
`haeuser`, `spiti`).
The format gains `textKeys`: what finds an entry in running text, its
own forms, never its meanings, which would match words of the meaning
language in a subtitle. `curate update --wiktionary de,el,en` keeps the
languages current: it asks kaikki.org when the dump last changed and
converts it again only when it is newer, or the languages or the
conversion changed. The whole dump converts in about ten minutes in
under 300 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:19:16 +02:00
|
|
|
});
|
feat(curator): Chinese from CC-CEDICT, with the HSK levels of 2025
Chinese comes from CC-CEDICT (CC BY-SA 4.0), made for learners of
Chinese: `curate cedict` writes two dictionaries of its entries,
`cc-cedict-hans-en` (zh-Hans) and `cc-cedict-hant-en` (zh-Hant), so a
studyset finds its words as it writes them, each finding a word by
either script, by pinyin with or without tones and by its meanings.
Readings are pinyin with tone marks, a syllable to a character; measure
words are a remark beside the meanings.
The HSK levels are those of the 2025 examination syllabus, which HSK
3.0's examination follows since July 2026 (decided 2026-10-14: level
tags, credited), as hsk30 extracted them (MIT): `hsk:1` … `hsk:6`, and
`hsk:7` for 7 to 9, each given to the entry read as the syllabus reads
the word (的 de, not 的 dì; 书 shū, not the Book of Documents, Shū),
ranking the words, HSK 1 first.
`curate update --languages de,el,en,zh` takes over from `--wiktionary`:
Chinese from MDBG's file of the day as often as EDRDG's files, the rest
from Wiktionary's dump.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:27:02 +02:00
|
|
|
|
|
|
|
|
test('CC-CEDICT is converted in both scripts with the HSK levels, and '
|
|
|
|
|
'said to be up to date by its files', () async {
|
|
|
|
|
expect(cedictUpdated(into), isNull);
|
|
|
|
|
|
|
|
|
|
final written = await updateCedict(
|
|
|
|
|
into: into,
|
|
|
|
|
data: Directory('data'),
|
feat(curator): Tatoeba's sentences as examples in every new language
JMdict brings Tatoeba's sentences for Japanese; the other languages'
dictionaries had none (CC-CEDICT) or Wiktionary's own, often
quotations. Each file now takes Tatoeba's sentences of the week (its
per-language exports, CC BY 2.0 FR) as its entries' first examples,
which become a card's hints: each sentence with a translation into the
dictionary's meaning language goes to the entries its words find, cut
as the app cuts a line of subtitles (at spaces, Korean's particles off,
Chinese by its longest words), up to three for each, those using the
word as its headword writes it first (Haus rather than zu Hause; 学习
rather than 學習 in the simplified dictionary), then those nearest seven
words long. German's common words nearly all get some (8,849 of
9,292), in ten seconds and under 300 MB; a dictionary whose sentences
can't be had keeps its words. `curate tatoeba` does the same for a file
made by hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:49:34 +02:00
|
|
|
fetch: (uri, file) => file.writeAsBytes(
|
|
|
|
|
uri == cedictSource
|
|
|
|
|
? gzip.encode(
|
|
|
|
|
File('test/fixtures/cedict-sample.txt').readAsBytesSync(),
|
|
|
|
|
)
|
|
|
|
|
: tatoeba(uri) ?? (throw HttpException('$uri', uri: uri)),
|
|
|
|
|
),
|
feat(curator): Chinese from CC-CEDICT, with the HSK levels of 2025
Chinese comes from CC-CEDICT (CC BY-SA 4.0), made for learners of
Chinese: `curate cedict` writes two dictionaries of its entries,
`cc-cedict-hans-en` (zh-Hans) and `cc-cedict-hant-en` (zh-Hant), so a
studyset finds its words as it writes them, each finding a word by
either script, by pinyin with or without tones and by its meanings.
Readings are pinyin with tone marks, a syllable to a character; measure
words are a remark beside the meanings.
The HSK levels are those of the 2025 examination syllabus, which HSK
3.0's examination follows since July 2026 (decided 2026-10-14: level
tags, credited), as hsk30 extracted them (MIT): `hsk:1` … `hsk:6`, and
`hsk:7` for 7 to 9, each given to the entry read as the syllabus reads
the word (的 de, not 的 dì; 书 shū, not the Book of Documents, Shū),
ranking the words, HSK 1 first.
`curate update --languages de,el,en,zh` takes over from `--wiktionary`:
Chinese from MDBG's file of the day as often as EDRDG's files, the rest
from Wiktionary's dump.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:27:02 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
[for (final file in written) file.uri.pathSegments.last],
|
|
|
|
|
['cc-cedict-hans-en.jsonl.gz', 'cc-cedict-hant-en.jsonl.gz'],
|
|
|
|
|
);
|
|
|
|
|
expect(header('cc-cedict-hant-en.jsonl.gz')['language'], 'zh-Hant');
|
feat(curator): Tatoeba's sentences as examples in every new language
JMdict brings Tatoeba's sentences for Japanese; the other languages'
dictionaries had none (CC-CEDICT) or Wiktionary's own, often
quotations. Each file now takes Tatoeba's sentences of the week (its
per-language exports, CC BY 2.0 FR) as its entries' first examples,
which become a card's hints: each sentence with a translation into the
dictionary's meaning language goes to the entries its words find, cut
as the app cuts a line of subtitles (at spaces, Korean's particles off,
Chinese by its longest words), up to three for each, those using the
word as its headword writes it first (Haus rather than zu Hause; 学习
rather than 學習 in the simplified dictionary), then those nearest seven
words long. German's common words nearly all get some (8,849 of
9,292), in ten seconds and under 300 MB; a dictionary whose sentences
can't be had keeps its words. `curate tatoeba` does the same for a file
made by hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:49:34 +02:00
|
|
|
expect([
|
|
|
|
|
for (final credit
|
|
|
|
|
in header('cc-cedict-hans-en.jsonl.gz')['credits']! as List)
|
|
|
|
|
(credit as Map)['name'],
|
|
|
|
|
], contains('Tatoeba'));
|
feat(curator): Chinese from CC-CEDICT, with the HSK levels of 2025
Chinese comes from CC-CEDICT (CC BY-SA 4.0), made for learners of
Chinese: `curate cedict` writes two dictionaries of its entries,
`cc-cedict-hans-en` (zh-Hans) and `cc-cedict-hant-en` (zh-Hant), so a
studyset finds its words as it writes them, each finding a word by
either script, by pinyin with or without tones and by its meanings.
Readings are pinyin with tone marks, a syllable to a character; measure
words are a remark beside the meanings.
The HSK levels are those of the 2025 examination syllabus, which HSK
3.0's examination follows since July 2026 (decided 2026-10-14: level
tags, credited), as hsk30 extracted them (MIT): `hsk:1` … `hsk:6`, and
`hsk:7` for 7 to 9, each given to the entry read as the syllabus reads
the word (的 de, not 的 dì; 书 shū, not the Book of Documents, Shū),
ranking the words, HSK 1 first.
`curate update --languages de,el,en,zh` takes over from `--wiktionary`:
Chinese from MDBG's file of the day as often as EDRDG's files, the rest
from Wiktionary's dump.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:27:02 +02:00
|
|
|
expect(cedictUpdated(into), isNotNull);
|
feat(curator): Tatoeba's sentences as examples in every new language
JMdict brings Tatoeba's sentences for Japanese; the other languages'
dictionaries had none (CC-CEDICT) or Wiktionary's own, often
quotations. Each file now takes Tatoeba's sentences of the week (its
per-language exports, CC BY 2.0 FR) as its entries' first examples,
which become a card's hints: each sentence with a translation into the
dictionary's meaning language goes to the entries its words find, cut
as the app cuts a line of subtitles (at spaces, Korean's particles off,
Chinese by its longest words), up to three for each, those using the
word as its headword writes it first (Haus rather than zu Hause; 学习
rather than 學習 in the simplified dictionary), then those nearest seven
words long. German's common words nearly all get some (8,849 of
9,292), in ten seconds and under 300 MB; a dictionary whose sentences
can't be had keeps its words. `curate tatoeba` does the same for a file
made by hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:49:34 +02:00
|
|
|
// Nothing of the downloads left beside the files.
|
|
|
|
|
expect(
|
|
|
|
|
[for (final entity in into.listSync()) entity.uri.pathSegments.last]
|
|
|
|
|
..sort(),
|
|
|
|
|
['cc-cedict-hans-en.jsonl.gz', 'cc-cedict-hant-en.jsonl.gz'],
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("a dictionary keeps its words without Tatoeba's sentences when they "
|
|
|
|
|
"can't be had", () async {
|
|
|
|
|
final written = await updateCedict(
|
|
|
|
|
into: into,
|
|
|
|
|
data: Directory('data'),
|
|
|
|
|
fetch: (uri, file) => uri == cedictSource
|
|
|
|
|
? file.writeAsBytes(
|
|
|
|
|
gzip.encode(
|
|
|
|
|
File('test/fixtures/cedict-sample.txt').readAsBytesSync(),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
: throw HttpException('$uri answered 503.', uri: uri),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(written, hasLength(2));
|
|
|
|
|
expect([
|
|
|
|
|
for (final credit
|
|
|
|
|
in header('cc-cedict-hans-en.jsonl.gz')['credits']! as List)
|
|
|
|
|
(credit as Map)['name'],
|
|
|
|
|
], isNot(contains('Tatoeba')));
|
feat(curator): Chinese from CC-CEDICT, with the HSK levels of 2025
Chinese comes from CC-CEDICT (CC BY-SA 4.0), made for learners of
Chinese: `curate cedict` writes two dictionaries of its entries,
`cc-cedict-hans-en` (zh-Hans) and `cc-cedict-hant-en` (zh-Hant), so a
studyset finds its words as it writes them, each finding a word by
either script, by pinyin with or without tones and by its meanings.
Readings are pinyin with tone marks, a syllable to a character; measure
words are a remark beside the meanings.
The HSK levels are those of the 2025 examination syllabus, which HSK
3.0's examination follows since July 2026 (decided 2026-10-14: level
tags, credited), as hsk30 extracted them (MIT): `hsk:1` … `hsk:6`, and
`hsk:7` for 7 to 9, each given to the entry read as the syllabus reads
the word (的 de, not 的 dì; 书 shū, not the Book of Documents, Shū),
ranking the words, HSK 1 first.
`curate update --languages de,el,en,zh` takes over from `--wiktionary`:
Chinese from MDBG's file of the day as often as EDRDG's files, the rest
from Wiktionary's dump.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:27:02 +02:00
|
|
|
});
|
|
|
|
|
|
feat(curator): characters of Chinese and Korean, the Greek alphabet, Korean words from KRDict
For parity with Japanese's KANJIDIC2 and kana:
- Unihan (Unicode License v3) makes characters' dictionaries: the 8,105
standard simplified characters (unihan-hans) and the traditional ones in
use (unihan-hant), told apart and ranked by CC-CEDICT's words (Unihan
calls 这 its own traditional variant; 後 ranks as the 后 of 以后), with
pinyin, meanings, strokes, Kangxi radical, HSK 2025 character levels,
the 3,500 common ones and Hong Kong's grades; and Korean Hanja
(unihan-ko) with their sounds, the 1,800 of education, and their native
readings from Wiktionary (물 수), with the 40 Hangul letters as kana are.
- The Greek alphabet, the curator's own (CC0): letters with names,
transliteration and sound.
- KRDict, the Basic Korean Dictionary for learners (CC BY-SA 2.0 KR,
downloaded without an account), replaces Wiktionary for Korean's words:
learners' levels, pronunciation where it differs from the spelling
(같이 [가치]), Hanja, English senses; Wiktionary's update keeps the
words' forms (가다's 99) and the Hanja's readings beside it for both.
- The update converts KRDict when its page offers another export and the
characters weekly, and removes the files of languages no longer kept
or of sources replaced.
Measured on the real data: Unihan 3 s; KRDict 54,870 entries in 24 s
and under 600 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 12:08:10 +02:00
|
|
|
test('Chinese comes from CC-CEDICT, Korean from KRDict, the other '
|
|
|
|
|
"languages' words from Wiktionary", () {
|
|
|
|
|
final all = dictionarySources({'de', 'el', 'en', 'ko', 'zh'});
|
feat(curator): Chinese from CC-CEDICT, with the HSK levels of 2025
Chinese comes from CC-CEDICT (CC BY-SA 4.0), made for learners of
Chinese: `curate cedict` writes two dictionaries of its entries,
`cc-cedict-hans-en` (zh-Hans) and `cc-cedict-hant-en` (zh-Hant), so a
studyset finds its words as it writes them, each finding a word by
either script, by pinyin with or without tones and by its meanings.
Readings are pinyin with tone marks, a syllable to a character; measure
words are a remark beside the meanings.
The HSK levels are those of the 2025 examination syllabus, which HSK
3.0's examination follows since July 2026 (decided 2026-10-14: level
tags, credited), as hsk30 extracted them (MIT): `hsk:1` … `hsk:6`, and
`hsk:7` for 7 to 9, each given to the entry read as the syllabus reads
the word (的 de, not 的 dì; 书 shū, not the Book of Documents, Shū),
ranking the words, HSK 1 first.
`curate update --languages de,el,en,zh` takes over from `--wiktionary`:
Chinese from MDBG's file of the day as often as EDRDG's files, the rest
from Wiktionary's dump.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:27:02 +02:00
|
|
|
expect(all.wiktionary, {'de', 'el', 'en'});
|
|
|
|
|
expect(all.chinese, isTrue);
|
feat(curator): characters of Chinese and Korean, the Greek alphabet, Korean words from KRDict
For parity with Japanese's KANJIDIC2 and kana:
- Unihan (Unicode License v3) makes characters' dictionaries: the 8,105
standard simplified characters (unihan-hans) and the traditional ones in
use (unihan-hant), told apart and ranked by CC-CEDICT's words (Unihan
calls 这 its own traditional variant; 後 ranks as the 后 of 以后), with
pinyin, meanings, strokes, Kangxi radical, HSK 2025 character levels,
the 3,500 common ones and Hong Kong's grades; and Korean Hanja
(unihan-ko) with their sounds, the 1,800 of education, and their native
readings from Wiktionary (물 수), with the 40 Hangul letters as kana are.
- The Greek alphabet, the curator's own (CC0): letters with names,
transliteration and sound.
- KRDict, the Basic Korean Dictionary for learners (CC BY-SA 2.0 KR,
downloaded without an account), replaces Wiktionary for Korean's words:
learners' levels, pronunciation where it differs from the spelling
(같이 [가치]), Hanja, English senses; Wiktionary's update keeps the
words' forms (가다's 99) and the Hanja's readings beside it for both.
- The update converts KRDict when its page offers another export and the
characters weekly, and removes the files of languages no longer kept
or of sources replaced.
Measured on the real data: Unihan 3 s; KRDict 54,870 entries in 24 s
and under 600 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 12:08:10 +02:00
|
|
|
expect(all.korean, isTrue);
|
|
|
|
|
expect(all.greek, isTrue);
|
feat(curator): Chinese from CC-CEDICT, with the HSK levels of 2025
Chinese comes from CC-CEDICT (CC BY-SA 4.0), made for learners of
Chinese: `curate cedict` writes two dictionaries of its entries,
`cc-cedict-hans-en` (zh-Hans) and `cc-cedict-hant-en` (zh-Hant), so a
studyset finds its words as it writes them, each finding a word by
either script, by pinyin with or without tones and by its meanings.
Readings are pinyin with tone marks, a syllable to a character; measure
words are a remark beside the meanings.
The HSK levels are those of the 2025 examination syllabus, which HSK
3.0's examination follows since July 2026 (decided 2026-10-14: level
tags, credited), as hsk30 extracted them (MIT): `hsk:1` … `hsk:6`, and
`hsk:7` for 7 to 9, each given to the entry read as the syllabus reads
the word (的 de, not 的 dì; 书 shū, not the Book of Documents, Shū),
ranking the words, HSK 1 first.
`curate update --languages de,el,en,zh` takes over from `--wiktionary`:
Chinese from MDBG's file of the day as often as EDRDG's files, the rest
from Wiktionary's dump.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:27:02 +02:00
|
|
|
final traditional = dictionarySources({'zh-Hant'});
|
|
|
|
|
expect(traditional.wiktionary, isEmpty);
|
|
|
|
|
expect(traditional.chinese, isTrue);
|
feat(curator): characters of Chinese and Korean, the Greek alphabet, Korean words from KRDict
For parity with Japanese's KANJIDIC2 and kana:
- Unihan (Unicode License v3) makes characters' dictionaries: the 8,105
standard simplified characters (unihan-hans) and the traditional ones in
use (unihan-hant), told apart and ranked by CC-CEDICT's words (Unihan
calls 这 its own traditional variant; 後 ranks as the 后 of 以后), with
pinyin, meanings, strokes, Kangxi radical, HSK 2025 character levels,
the 3,500 common ones and Hong Kong's grades; and Korean Hanja
(unihan-ko) with their sounds, the 1,800 of education, and their native
readings from Wiktionary (물 수), with the 40 Hangul letters as kana are.
- The Greek alphabet, the curator's own (CC0): letters with names,
transliteration and sound.
- KRDict, the Basic Korean Dictionary for learners (CC BY-SA 2.0 KR,
downloaded without an account), replaces Wiktionary for Korean's words:
learners' levels, pronunciation where it differs from the spelling
(같이 [가치]), Hanja, English senses; Wiktionary's update keeps the
words' forms (가다's 99) and the Hanja's readings beside it for both.
- The update converts KRDict when its page offers another export and the
characters weekly, and removes the files of languages no longer kept
or of sources replaced.
Measured on the real data: Unihan 3 s; KRDict 54,870 entries in 24 s
and under 600 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 12:08:10 +02:00
|
|
|
expect(traditional.korean, isFalse);
|
feat(curator): Chinese from CC-CEDICT, with the HSK levels of 2025
Chinese comes from CC-CEDICT (CC BY-SA 4.0), made for learners of
Chinese: `curate cedict` writes two dictionaries of its entries,
`cc-cedict-hans-en` (zh-Hans) and `cc-cedict-hant-en` (zh-Hant), so a
studyset finds its words as it writes them, each finding a word by
either script, by pinyin with or without tones and by its meanings.
Readings are pinyin with tone marks, a syllable to a character; measure
words are a remark beside the meanings.
The HSK levels are those of the 2025 examination syllabus, which HSK
3.0's examination follows since July 2026 (decided 2026-10-14: level
tags, credited), as hsk30 extracted them (MIT): `hsk:1` … `hsk:6`, and
`hsk:7` for 7 to 9, each given to the entry read as the syllabus reads
the word (的 de, not 的 dì; 书 shū, not the Book of Documents, Shū),
ranking the words, HSK 1 first.
`curate update --languages de,el,en,zh` takes over from `--wiktionary`:
Chinese from MDBG's file of the day as often as EDRDG's files, the rest
from Wiktionary's dump.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 07:27:02 +02:00
|
|
|
expect(dictionarySources({}).chinese, isFalse);
|
|
|
|
|
});
|
feat(curator): characters of Chinese and Korean, the Greek alphabet, Korean words from KRDict
For parity with Japanese's KANJIDIC2 and kana:
- Unihan (Unicode License v3) makes characters' dictionaries: the 8,105
standard simplified characters (unihan-hans) and the traditional ones in
use (unihan-hant), told apart and ranked by CC-CEDICT's words (Unihan
calls 这 its own traditional variant; 後 ranks as the 后 of 以后), with
pinyin, meanings, strokes, Kangxi radical, HSK 2025 character levels,
the 3,500 common ones and Hong Kong's grades; and Korean Hanja
(unihan-ko) with their sounds, the 1,800 of education, and their native
readings from Wiktionary (물 수), with the 40 Hangul letters as kana are.
- The Greek alphabet, the curator's own (CC0): letters with names,
transliteration and sound.
- KRDict, the Basic Korean Dictionary for learners (CC BY-SA 2.0 KR,
downloaded without an account), replaces Wiktionary for Korean's words:
learners' levels, pronunciation where it differs from the spelling
(같이 [가치]), Hanja, English senses; Wiktionary's update keeps the
words' forms (가다's 99) and the Hanja's readings beside it for both.
- The update converts KRDict when its page offers another export and the
characters weekly, and removes the files of languages no longer kept
or of sources replaced.
Measured on the real data: Unihan 3 s; KRDict 54,870 entries in 24 s
and under 600 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 12:08:10 +02:00
|
|
|
|
|
|
|
|
test('files of languages no longer kept, or of a source replaced, go; an '
|
|
|
|
|
"operator's own stay", () {
|
|
|
|
|
for (final name in [
|
|
|
|
|
'jmdict-en.jsonl.gz',
|
|
|
|
|
'wiktionary-de-en.jsonl.gz',
|
|
|
|
|
'wiktionary-ko-en.jsonl.gz',
|
|
|
|
|
'wiktionary-el-en.jsonl.gz',
|
|
|
|
|
'krdict-ko-en.jsonl.gz',
|
|
|
|
|
'unihan-ko-en.jsonl.gz',
|
|
|
|
|
'greek-alphabet-en.jsonl.gz',
|
|
|
|
|
'my-own.jsonl.gz',
|
|
|
|
|
]) {
|
|
|
|
|
File('${into.path}/$name').writeAsStringSync('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
final removed = removeUnwanted(into, {'de', 'ko'});
|
|
|
|
|
|
|
|
|
|
expect([for (final file in removed) file.uri.pathSegments.last]..sort(), [
|
|
|
|
|
'greek-alphabet-en.jsonl.gz',
|
|
|
|
|
'wiktionary-el-en.jsonl.gz',
|
|
|
|
|
'wiktionary-ko-en.jsonl.gz',
|
|
|
|
|
]);
|
|
|
|
|
expect(
|
|
|
|
|
[for (final entity in into.listSync()) entity.uri.pathSegments.last]
|
|
|
|
|
..sort(),
|
|
|
|
|
[
|
|
|
|
|
'jmdict-en.jsonl.gz',
|
|
|
|
|
'krdict-ko-en.jsonl.gz',
|
|
|
|
|
'my-own.jsonl.gz',
|
|
|
|
|
'unihan-ko-en.jsonl.gz',
|
|
|
|
|
'wiktionary-de-en.jsonl.gz',
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-14 17:36:55 +02:00
|
|
|
test("a replaced source stays until what replaces it is there: Korean's "
|
2026-10-14 18:02:57 +02:00
|
|
|
"words from Wiktionary until KRDict's are", () {
|
2026-10-14 17:36:55 +02:00
|
|
|
File('${into.path}/wiktionary-ko-en.jsonl.gz').writeAsStringSync('');
|
|
|
|
|
|
|
|
|
|
expect(removeUnwanted(into, {'ko'}), isEmpty);
|
|
|
|
|
|
|
|
|
|
File('${into.path}/krdict-ko-en.jsonl.gz').writeAsStringSync('');
|
|
|
|
|
expect(
|
|
|
|
|
[
|
|
|
|
|
for (final file in removeUnwanted(into, {'ko'}))
|
|
|
|
|
file.uri.pathSegments.last,
|
|
|
|
|
],
|
|
|
|
|
['wiktionary-ko-en.jsonl.gz'],
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
feat(curator): characters of Chinese and Korean, the Greek alphabet, Korean words from KRDict
For parity with Japanese's KANJIDIC2 and kana:
- Unihan (Unicode License v3) makes characters' dictionaries: the 8,105
standard simplified characters (unihan-hans) and the traditional ones in
use (unihan-hant), told apart and ranked by CC-CEDICT's words (Unihan
calls 这 its own traditional variant; 後 ranks as the 后 of 以后), with
pinyin, meanings, strokes, Kangxi radical, HSK 2025 character levels,
the 3,500 common ones and Hong Kong's grades; and Korean Hanja
(unihan-ko) with their sounds, the 1,800 of education, and their native
readings from Wiktionary (물 수), with the 40 Hangul letters as kana are.
- The Greek alphabet, the curator's own (CC0): letters with names,
transliteration and sound.
- KRDict, the Basic Korean Dictionary for learners (CC BY-SA 2.0 KR,
downloaded without an account), replaces Wiktionary for Korean's words:
learners' levels, pronunciation where it differs from the spelling
(같이 [가치]), Hanja, English senses; Wiktionary's update keeps the
words' forms (가다's 99) and the Hanja's readings beside it for both.
- The update converts KRDict when its page offers another export and the
characters weekly, and removes the files of languages no longer kept
or of sources replaced.
Measured on the real data: Unihan 3 s; KRDict 54,870 entries in 24 s
and under 600 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-14 12:08:10 +02:00
|
|
|
group("KRDict's export", () {
|
|
|
|
|
var number = '217';
|
|
|
|
|
var downloads = 0;
|
|
|
|
|
|
|
|
|
|
setUp(() {
|
|
|
|
|
number = '217';
|
|
|
|
|
downloads = 0;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/// KRDict's page and the fixture packed as its export; Tatoeba's.
|
|
|
|
|
Future<void> fetch(Uri uri, File file) async {
|
|
|
|
|
if (uri == krdictPage) {
|
|
|
|
|
await file.writeAsString(
|
|
|
|
|
"<button onclick=\"batchDownload('215')\"></button>"
|
|
|
|
|
"<button onclick=\"batchDownload('216')\"></button>"
|
|
|
|
|
"<button onclick=\"batchDownload('$number')\"></button>",
|
|
|
|
|
);
|
|
|
|
|
} else if (uri == krdictDownloadOf(number)) {
|
|
|
|
|
downloads++;
|
|
|
|
|
await file.writeAsBytes(
|
|
|
|
|
ZipEncoder().encodeBytes(
|
|
|
|
|
Archive()..add(
|
|
|
|
|
ArchiveFile.bytes(
|
|
|
|
|
'1_5000_20260919.json',
|
|
|
|
|
File('test/fixtures/krdict-sample.json').readAsBytesSync(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
await file.writeAsBytes(
|
|
|
|
|
tatoeba(uri) ?? (throw HttpException('$uri', uri: uri)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('is converted with the forms Wiktionary kept, and again only '
|
|
|
|
|
'once its page offers another', () async {
|
|
|
|
|
final kept = WiktionaryKorean()..forms['가다'] = {'갔어요'};
|
|
|
|
|
File('${into.path}/$wiktionaryKoreanFile')
|
|
|
|
|
.writeAsBytesSync(gzip.encode(utf8.encode(kept.toJson())));
|
|
|
|
|
|
|
|
|
|
final written = await updateKrdict(
|
|
|
|
|
into: into,
|
|
|
|
|
data: Directory('data'),
|
|
|
|
|
fetch: fetch,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
[for (final file in written) file.uri.pathSegments.last],
|
|
|
|
|
[krdictFileName],
|
|
|
|
|
);
|
|
|
|
|
expect(header(krdictFileName)['sourceVersion'], '2026-09-19');
|
|
|
|
|
expect([
|
|
|
|
|
for (final credit in header(krdictFileName)['credits']! as List)
|
|
|
|
|
(credit as Map)['name'],
|
|
|
|
|
], contains('Wiktionary'));
|
|
|
|
|
expect(
|
|
|
|
|
await updateKrdict(into: into, data: Directory('data'), fetch: fetch),
|
|
|
|
|
isEmpty,
|
|
|
|
|
);
|
|
|
|
|
expect(downloads, 1);
|
|
|
|
|
|
|
|
|
|
number = '230';
|
|
|
|
|
expect(
|
|
|
|
|
await updateKrdict(into: into, data: Directory('data'), fetch: fetch),
|
|
|
|
|
hasLength(1),
|
|
|
|
|
);
|
|
|
|
|
expect(downloads, 2);
|
|
|
|
|
expect(
|
|
|
|
|
[for (final entity in into.listSync()) entity.uri.pathSegments.last]
|
|
|
|
|
..sort(),
|
|
|
|
|
['.krdict', wiktionaryKoreanFile, krdictFileName]..sort(),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('the characters of Chinese in both scripts, of Korean with the Hangul '
|
|
|
|
|
'letters and the Greek alphabet, up to date until a Wiktionary update '
|
|
|
|
|
'keeps new Hanja readings', () async {
|
|
|
|
|
Future<void> fetch(Uri uri, File file) async => await file.writeAsBytes(
|
|
|
|
|
uri == unihanSource
|
|
|
|
|
? ZipEncoder().encodeBytes(
|
|
|
|
|
Archive()..add(
|
|
|
|
|
ArchiveFile.bytes(
|
|
|
|
|
'Unihan_Readings.txt',
|
|
|
|
|
File('test/fixtures/unihan-sample.txt').readAsBytesSync(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
: gzip.encode(
|
|
|
|
|
File('test/fixtures/cedict-characters-sample.txt')
|
|
|
|
|
.readAsBytesSync(),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
expect(
|
|
|
|
|
charactersUpdated(into, chinese: true, korean: true, greek: true),
|
|
|
|
|
isNull,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
final written = await updateCharacters(
|
|
|
|
|
into: into,
|
|
|
|
|
data: Directory('data'),
|
|
|
|
|
chinese: true,
|
|
|
|
|
korean: true,
|
|
|
|
|
greek: true,
|
|
|
|
|
fetch: fetch,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(
|
|
|
|
|
[for (final file in written) file.uri.pathSegments.last],
|
|
|
|
|
[
|
|
|
|
|
'unihan-hans-en.jsonl.gz',
|
|
|
|
|
'unihan-hant-en.jsonl.gz',
|
|
|
|
|
'unihan-ko-en.jsonl.gz',
|
|
|
|
|
'greek-alphabet-en.jsonl.gz',
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
expect(header('unihan-hans-en.jsonl.gz')['sourceVersion'], '18.0.0');
|
|
|
|
|
expect([
|
|
|
|
|
for (final credit
|
|
|
|
|
in header('unihan-hans-en.jsonl.gz')['credits']! as List)
|
|
|
|
|
(credit as Map)['name'],
|
|
|
|
|
], containsAll(['Unihan Database', 'CC-CEDICT', 'wordfreq']));
|
|
|
|
|
final updated = charactersUpdated(
|
|
|
|
|
into,
|
|
|
|
|
chinese: true,
|
|
|
|
|
korean: true,
|
|
|
|
|
greek: true,
|
|
|
|
|
);
|
|
|
|
|
expect(updated, isNotNull);
|
|
|
|
|
|
|
|
|
|
File('${into.path}/$wiktionaryKoreanFile')
|
|
|
|
|
..writeAsBytesSync(gzip.encode(utf8.encode(WiktionaryKorean().toJson())))
|
|
|
|
|
..setLastModifiedSync(updated!.add(const Duration(minutes: 1)));
|
|
|
|
|
expect(charactersUpdated(into, korean: true), isNull);
|
|
|
|
|
expect(charactersUpdated(into, chinese: true), isNotNull);
|
|
|
|
|
});
|
feat(curator): curate update keeps a server's dictionaries current
EDRDG's licence asks of servers showing its dictionaries that they update
them at least monthly. `curate update --into <folder>` downloads EDRDG's
files of the day (JMdict with Tatoeba's sentences, KANJIDIC2), converts
them with the JLPT levels the curator now brings along (yomitan-jlpt-vocab's
lists, CC BY-SA 4.0, and the kanji levels of kanji-data's kanji.json: both
static, in data/ with their origin), and puts each file in place at once
(written beside it as .part, then renamed over it), so the server never
reads half a file; a failure leaves the files as they were. With --every 7
it keeps running and updates whenever the files are a week old, as their
dates say, so a restart doesn't download again; after a failure it tries
again in six hours. A real update takes some 50 seconds and 400 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-09 16:40:19 +02:00
|
|
|
}
|