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 'dart:convert';
|
|
|
|
|
import 'dart:io';
|
|
|
|
|
|
|
|
|
|
import 'package:archive/archive.dart';
|
|
|
|
|
import 'package:halkyon_learn_curator/src/sources/krdict.dart';
|
|
|
|
|
import 'package:halkyon_learn_curator/src/sources/wiktionary.dart';
|
|
|
|
|
import 'package:halkyon_learn_curator/src/wordfreq.dart';
|
|
|
|
|
import 'package:test/test.dart';
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
/// The fixture packed as KRDict packs its export: its first file.
|
|
|
|
|
List<int> export() => ZipEncoder().encodeBytes(
|
|
|
|
|
Archive()..add(
|
|
|
|
|
ArchiveFile.bytes(
|
|
|
|
|
'1_5000_20260919.json',
|
|
|
|
|
File('test/fixtures/krdict-sample.json').readAsBytesSync(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Future<List<Map<String, Object?>>> converted({
|
|
|
|
|
WordFrequencies? frequencies,
|
|
|
|
|
WiktionaryKorean? korean,
|
|
|
|
|
}) async {
|
|
|
|
|
final (:version, :entries) = krdictExport(export());
|
|
|
|
|
return [
|
|
|
|
|
for (final line in await krdictDictionary(
|
|
|
|
|
entries,
|
|
|
|
|
sourceVersion: version,
|
|
|
|
|
frequencies: frequencies,
|
|
|
|
|
korean: korean,
|
|
|
|
|
).toList())
|
|
|
|
|
jsonDecode(line) as Map<String, Object?>,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-14 17:36:55 +02:00
|
|
|
test('an export of another shape is refused rather than written as a '
|
|
|
|
|
'dictionary of no words', () async {
|
|
|
|
|
for (final files in [
|
|
|
|
|
<ArchiveFile>[],
|
|
|
|
|
[ArchiveFile.bytes('1_5000_20260919.xml', utf8.encode('<xml/>'))],
|
|
|
|
|
[
|
|
|
|
|
ArchiveFile.bytes(
|
|
|
|
|
'1_5000_20260919.json',
|
|
|
|
|
utf8.encode(jsonEncode({'LexicalResource': <String, Object?>{}})),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
]) {
|
|
|
|
|
final archive = Archive();
|
|
|
|
|
files.forEach(archive.add);
|
|
|
|
|
final zip = ZipEncoder().encodeBytes(archive);
|
|
|
|
|
final (version: _, :entries) = krdictExport(zip);
|
|
|
|
|
|
|
|
|
|
await expectLater(entries.toList(), throwsFormatException);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
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("KRDict's words with their level, pronunciation where it differs from "
|
|
|
|
|
'the spelling, Hanja, forms and English senses', () async {
|
|
|
|
|
final [header, ...entries] = await converted();
|
|
|
|
|
|
|
|
|
|
expect(header['source'], 'krdict');
|
|
|
|
|
expect(header['language'], 'ko');
|
|
|
|
|
expect(header['sourceVersion'], '2026-09-19');
|
|
|
|
|
expect(
|
|
|
|
|
[
|
|
|
|
|
for (final credit in header['credits']! as List)
|
|
|
|
|
(credit as Map)['name'],
|
|
|
|
|
],
|
|
|
|
|
['한국어기초사전 (Basic Korean Dictionary)'],
|
|
|
|
|
);
|
|
|
|
|
final school = entries.singleWhere((entry) => entry['headword'] == '학교');
|
|
|
|
|
expect(school['id'], '73276');
|
|
|
|
|
expect(school['reading'], '학꾜');
|
|
|
|
|
expect(school['labels'], ['level:beginner']);
|
|
|
|
|
expect(school['keys'], containsAll(['학교', '學校', 'school']));
|
|
|
|
|
const definition =
|
|
|
|
|
'An institution that educates students according to a specific '
|
|
|
|
|
'purpose, curriculum, system, etc.';
|
|
|
|
|
expect(school['entry'], {
|
|
|
|
|
'pos': 'noun',
|
|
|
|
|
'hanja': ['學校'],
|
|
|
|
|
'senses': [
|
|
|
|
|
{
|
|
|
|
|
'glosses': ['school'],
|
|
|
|
|
'info': [definition],
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
});
|
|
|
|
|
final read = entries.singleWhere((entry) => entry['headword'] == '읽다');
|
|
|
|
|
expect(read['reading'], '익따');
|
|
|
|
|
expect(read['textKeys'], containsAll(['읽다', '읽어', '읽고']));
|
|
|
|
|
expect(
|
|
|
|
|
(read['entry']! as Map)['inflections'],
|
|
|
|
|
contains(equals({'text': '읽어'})),
|
|
|
|
|
);
|
|
|
|
|
final together = entries.where((entry) => entry['headword'] == '같이');
|
|
|
|
|
expect(
|
|
|
|
|
[for (final entry in together) (entry['entry']! as Map)['pos']],
|
|
|
|
|
['particle', 'adv'],
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('an idiom or a proverb is an entry of its own, never found in running '
|
|
|
|
|
'text, with an id told apart from its word', () async {
|
|
|
|
|
final [_, ...entries] = await converted();
|
|
|
|
|
|
|
|
|
|
final proverb = entries.singleWhere(
|
|
|
|
|
(entry) => entry['headword'] == '가는 날이 장날',
|
|
|
|
|
);
|
|
|
|
|
expect(proverb['id'], '27500:가는 날이 장날');
|
|
|
|
|
expect((proverb['entry']! as Map)['pos'], 'proverb');
|
|
|
|
|
expect(proverb.containsKey('textKeys'), isFalse);
|
|
|
|
|
expect(
|
|
|
|
|
entries.singleWhere((entry) => entry['headword'] == '가다')['id'],
|
|
|
|
|
'27500',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("Wiktionary's forms find a word in text; a verb rarely written as "
|
|
|
|
|
'the dictionary writes it ranks by its most used form', () async {
|
|
|
|
|
final korean = WiktionaryKorean()..forms['가다'] = {'가요', '갔어요', '갑니다'};
|
|
|
|
|
final [_, ...entries] = await converted(
|
|
|
|
|
korean: korean,
|
|
|
|
|
frequencies: WordFrequencies(const {'갔어요': 50}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
final go = entries.singleWhere((entry) => entry['headword'] == '가다');
|
|
|
|
|
expect(go['textKeys'], containsAll(['가다', '가요', '갔어요']));
|
|
|
|
|
expect(go['labels'], contains('common'));
|
|
|
|
|
expect(go['rank'], 100000 + 100);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("the export's JSON download is the third its page offers", () {
|
|
|
|
|
expect(
|
|
|
|
|
krdictJsonDownloadOf('''
|
|
|
|
|
<button onclick="batchDownload('215')">엑셀</button>
|
|
|
|
|
<button onclick="batchDownload('216')">XML</button>
|
|
|
|
|
<button onclick="batchDownload('217')">Json</button>'''),
|
|
|
|
|
'217',
|
|
|
|
|
);
|
|
|
|
|
expect(krdictJsonDownloadOf("batchDownload('0')"), isNull);
|
|
|
|
|
});
|
|
|
|
|
}
|