Parity with Japanese's kanji and kana: - The characters' tab is named by its language (Hanzi, Hangul & Hanja, Alphabet), its groups too: HSK levels and the characters in common use (Hong Kong's grades for traditional ones); Hangul and the Hanja taught in schools; Greek's letters. Radicals for Chinese and Korean in their own forms (讠 for 言 in simplified Chinese), none for Greek. - A character's readings are named as its language names them: On and Kun, Pinyin, Eum and Hun (음, 훈); a letter means its sound and name. - How characters are written comes by language: Chinese from Make Me a Hanzi (its medians in KanjiVG's square, Arphic Public License, a data file beside the app, never in cards), the Hangul letters drawn by the curator; a Korean word is written letter by letter (닭: ㄷ ㅏ ㄺ). Words of alphabets (German, Greek) aren't written by hand. - A Chinese word's pinyin is typed in a session, tone numbers turning into marks (ni3 → nǐ); KRDict's levels (초급, 중급, 고급) tag Korean words; a Korean word's pronunciation shows in brackets ([가치]). - Smaller: Look up in the composer keeps a noun's article; the Watch filter names its level list; hints and placeholders by language. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
356 lines
9.5 KiB
Dart
356 lines
9.5 KiB
Dart
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/dictionary_gateway.dart';
|
|
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.dart';
|
|
|
|
/// JMdict in English, as an instance that loaded it lists it.
|
|
const jmdict = Dictionary(
|
|
source: 'jmdict',
|
|
sourceVersion: '3.6.2 (2026-10-05)',
|
|
kind: DictionaryKind.words,
|
|
language: 'ja',
|
|
glossLanguage: 'en',
|
|
license: 'CC-BY-SA-4.0',
|
|
credits: [
|
|
(
|
|
name: 'JMdict',
|
|
by: 'Electronic Dictionary Research and Development Group (EDRDG)',
|
|
license: 'CC-BY-SA-4.0',
|
|
url: 'https://www.edrdg.org/edrdg/licence.html',
|
|
),
|
|
],
|
|
entryCount: 3,
|
|
);
|
|
|
|
/// KANJIDIC2 in English, with the kana, as an instance that loaded it lists
|
|
/// it.
|
|
const kanjidic = Dictionary(
|
|
source: 'kanjidic',
|
|
sourceVersion: '2026-282',
|
|
kind: DictionaryKind.characters,
|
|
language: 'ja',
|
|
glossLanguage: 'en',
|
|
license: 'CC-BY-SA-4.0',
|
|
credits: [
|
|
(
|
|
name: 'KANJIDIC2',
|
|
by: 'Electronic Dictionary Research and Development Group (EDRDG)',
|
|
license: 'CC-BY-SA-4.0',
|
|
url: 'https://www.edrdg.org/edrdg/licence.html',
|
|
),
|
|
],
|
|
entryCount: 2,
|
|
);
|
|
|
|
/// German words with English meanings from Wiktionary, as an instance that
|
|
/// loaded them lists them.
|
|
const wiktionaryDe = Dictionary(
|
|
source: 'wiktionary-de',
|
|
sourceVersion: '2026-10-03',
|
|
kind: DictionaryKind.words,
|
|
language: 'de',
|
|
glossLanguage: 'en',
|
|
license: 'CC-BY-SA-4.0',
|
|
credits: [
|
|
(
|
|
name: 'Wiktionary',
|
|
by: 'Wiktionary contributors',
|
|
license: 'CC-BY-SA-4.0',
|
|
url: 'https://en.wiktionary.org/wiki/Wiktionary:Copyrights',
|
|
),
|
|
],
|
|
entryCount: 4,
|
|
);
|
|
|
|
/// Chinese words of CC-CEDICT in simplified characters.
|
|
const ccCedictHans = Dictionary(
|
|
source: 'cc-cedict-hans',
|
|
sourceVersion: '2026-10-14',
|
|
kind: DictionaryKind.words,
|
|
language: 'zh-Hans',
|
|
glossLanguage: 'en',
|
|
license: 'CC-BY-SA-4.0',
|
|
credits: [
|
|
(
|
|
name: 'CC-CEDICT',
|
|
by: 'MDBG',
|
|
license: 'CC-BY-SA-4.0',
|
|
url: 'https://www.mdbg.net/chinese/dictionary?page=cc-cedict',
|
|
),
|
|
],
|
|
entryCount: 0,
|
|
);
|
|
|
|
/// Chinese characters of Unihan in simplified writing.
|
|
const unihanHans = Dictionary(
|
|
source: 'unihan-hans',
|
|
sourceVersion: '18.0.0',
|
|
kind: DictionaryKind.characters,
|
|
language: 'zh-Hans',
|
|
glossLanguage: 'en',
|
|
license: 'Unicode-3.0',
|
|
credits: [
|
|
(
|
|
name: 'Unihan Database',
|
|
by: 'Unicode, Inc.',
|
|
license: 'Unicode-3.0',
|
|
url: 'https://www.unicode.org/reports/tr38/',
|
|
),
|
|
],
|
|
entryCount: 1,
|
|
);
|
|
|
|
/// Greek words with English meanings from Wiktionary.
|
|
const wiktionaryEl = Dictionary(
|
|
source: 'wiktionary-el',
|
|
sourceVersion: '2026-10-03',
|
|
kind: DictionaryKind.words,
|
|
language: 'el',
|
|
glossLanguage: 'en',
|
|
license: 'CC-BY-SA-4.0',
|
|
credits: [
|
|
(
|
|
name: 'Wiktionary',
|
|
by: 'Wiktionary contributors',
|
|
license: 'CC-BY-SA-4.0',
|
|
url: 'https://en.wiktionary.org/wiki/Wiktionary:Copyrights',
|
|
),
|
|
],
|
|
entryCount: 0,
|
|
);
|
|
|
|
/// The Greek alphabet the curator writes.
|
|
const greekAlphabet = Dictionary(
|
|
source: 'greek-alphabet',
|
|
sourceVersion: '1',
|
|
kind: DictionaryKind.characters,
|
|
language: 'el',
|
|
glossLanguage: 'en',
|
|
license: 'CC0-1.0',
|
|
credits: [
|
|
(
|
|
name: 'Alphabets (Hangul, Greek)',
|
|
by: 'Halkyon Learn contributors',
|
|
license: 'CC0-1.0',
|
|
url: 'https://code.halkyon.app/project-halkyon/halkyon_learn',
|
|
),
|
|
],
|
|
entryCount: 1,
|
|
);
|
|
|
|
/// A German word of Wiktionary: [headword], its part of speech [pos] in
|
|
/// full, meaning [glosses], its [genders] and the [forms] that find it.
|
|
DictionaryWord germanWord(
|
|
String headword, {
|
|
required String pos,
|
|
List<String> glosses = const [],
|
|
List<String> genders = const [],
|
|
List<String> forms = const [],
|
|
List<String> labels = const [],
|
|
}) => DictionaryWord(
|
|
ref: (source: 'wiktionary-de', id: '$headword/$pos'),
|
|
headword: headword,
|
|
reading: null,
|
|
labels: labels,
|
|
senses: [
|
|
VocabularySense(glosses: glosses, pos: [pos]),
|
|
],
|
|
partOfSpeech: pos,
|
|
genders: genders,
|
|
inflections: [for (final form in forms) WordInflection(form, const [])],
|
|
language: 'de',
|
|
);
|
|
|
|
/// A JMdict word: [headword] read [reading], meaning [glosses], [labels]
|
|
/// and the parts of speech [pos] on its one sense.
|
|
DictionaryWord word(
|
|
String id,
|
|
String headword, {
|
|
String? reading,
|
|
List<String> glosses = const [],
|
|
List<String> labels = const [],
|
|
List<String> pos = const [],
|
|
String? version,
|
|
}) => DictionaryWord(
|
|
ref: (source: 'jmdict', id: id),
|
|
headword: headword,
|
|
reading: reading,
|
|
labels: labels,
|
|
senses: [VocabularySense(glosses: glosses, pos: pos)],
|
|
version: version,
|
|
);
|
|
|
|
/// A KANJIDIC2 character: [headword] meaning [meanings], read [on] and
|
|
/// [kun], with [labels]; a kana with its [romaji]; or a character [of]
|
|
/// another dictionary of characters.
|
|
DictionaryCharacter character(
|
|
String headword, {
|
|
List<String> meanings = const [],
|
|
List<String> on = const [],
|
|
List<String> kun = const [],
|
|
List<String> labels = const [],
|
|
String? romaji,
|
|
String? name,
|
|
int? grade,
|
|
int? strokes,
|
|
String? version,
|
|
Dictionary of = kanjidic,
|
|
}) => DictionaryCharacter(
|
|
ref: (source: of.source, id: headword),
|
|
name: name,
|
|
language: of.language,
|
|
headword: headword,
|
|
labels: labels,
|
|
meanings: meanings,
|
|
onReadings: on,
|
|
kunReadings: kun,
|
|
romaji: romaji,
|
|
grade: grade,
|
|
strokes: strokes,
|
|
version: version,
|
|
);
|
|
|
|
/// A [DictionaryGateway] in memory: an instance with the dictionaries
|
|
/// [available], holding the [words] (and characters) of each source in
|
|
/// their order. A search finds the entries whose headword, reading, a
|
|
/// meaning or a reading starts with the query.
|
|
class FakeDictionaryGateway({
|
|
final List<Dictionary> available = const [jmdict],
|
|
final List<DictionaryItem> words = const [],
|
|
}) implements DictionaryGateway {
|
|
/// When set, every request fails with it.
|
|
Exception? fail;
|
|
|
|
/// How often the dictionaries were asked for.
|
|
int asked = 0;
|
|
|
|
@override
|
|
Future<List<Dictionary>> dictionaries() async {
|
|
asked++;
|
|
if (fail case final error?) {
|
|
throw error;
|
|
}
|
|
return available;
|
|
}
|
|
|
|
/// The entries of [dictionary].
|
|
Iterable<DictionaryItem> _of(Dictionary dictionary) =>
|
|
words.where((word) => word.ref.source == dictionary.source);
|
|
|
|
@override
|
|
Future<List<DictionaryItem>> search(
|
|
Dictionary dictionary,
|
|
String query,
|
|
) async {
|
|
if (fail case final error?) {
|
|
throw error;
|
|
}
|
|
final typed = query.trim().toLowerCase();
|
|
return [
|
|
for (final item in _of(dictionary))
|
|
if (switch (item) {
|
|
DictionaryWord() => [
|
|
item.headword,
|
|
?item.reading,
|
|
for (final sense in item.senses) ...sense.glosses,
|
|
],
|
|
DictionaryCharacter() => [
|
|
item.headword,
|
|
...item.meaningsShown,
|
|
...item.onReadings,
|
|
for (final reading in item.kunReadings) reading.replaceAll('.', ''),
|
|
],
|
|
}.any((key) => key.toLowerCase().startsWith(typed)))
|
|
item,
|
|
];
|
|
}
|
|
|
|
@override
|
|
Future<List<DictionaryItem>> entries(
|
|
Dictionary dictionary,
|
|
List<String> ids,
|
|
) async {
|
|
if (fail case final error?) {
|
|
throw error;
|
|
}
|
|
final byId = {for (final item in _of(dictionary)) item.ref.id: item};
|
|
return [for (final id in ids) ?byId[id]];
|
|
}
|
|
|
|
/// How many keys [withKeys] was asked for each time.
|
|
final keysAsked = <int>[];
|
|
|
|
/// By its headword and its reading, as typed: the curator's keys hold
|
|
/// both, the reading also in hiragana; a word of Wiktionary also by its
|
|
/// headword and inflections in lower case, as its text keys hold them.
|
|
@override
|
|
Future<Map<String, List<DictionaryItem>>> withKeys(
|
|
Dictionary dictionary,
|
|
List<String> keys,
|
|
) async {
|
|
if (fail case final error?) {
|
|
throw error;
|
|
}
|
|
keysAsked.add(keys.length);
|
|
final asked = keys.toSet();
|
|
final byKey = <String, List<DictionaryItem>>{};
|
|
for (final item in _of(dictionary)) {
|
|
for (final key in {
|
|
item.headword,
|
|
if (item case DictionaryWord(:final reading?)) reading,
|
|
if (item case DictionaryWord(partOfSpeech: _?, :final inflections)) ...[
|
|
item.headword.toLowerCase(),
|
|
for (final form in inflections) form.text.toLowerCase(),
|
|
],
|
|
}) {
|
|
if (asked.contains(key)) {
|
|
(byKey[key] ??= []).add(item);
|
|
}
|
|
}
|
|
}
|
|
return byKey;
|
|
}
|
|
|
|
/// How often [changed] was asked, and with how many entries each time.
|
|
final changedAsked = <int>[];
|
|
|
|
@override
|
|
Future<List<DictionaryItem>> changed(
|
|
Dictionary dictionary,
|
|
Map<String, String> versions,
|
|
) async {
|
|
if (fail case final error?) {
|
|
throw error;
|
|
}
|
|
changedAsked.add(versions.length);
|
|
return [
|
|
for (final item in _of(dictionary))
|
|
if (versions[item.ref.id] case final kept?)
|
|
if (item.version != null && item.version != kept) item,
|
|
];
|
|
}
|
|
|
|
@override
|
|
Future<int> countLabelled(Dictionary dictionary, String label) async {
|
|
if (fail case final error?) {
|
|
throw error;
|
|
}
|
|
return _of(dictionary).where((word) => word.labels.contains(label)).length;
|
|
}
|
|
|
|
@override
|
|
Future<List<DictionaryItem>> labelled(
|
|
Dictionary dictionary,
|
|
String label, {
|
|
required int offset,
|
|
required int limit,
|
|
}) async {
|
|
if (fail case final error?) {
|
|
throw error;
|
|
}
|
|
return _of(dictionary)
|
|
.where((word) => word.labels.contains(label))
|
|
.skip(offset)
|
|
.take(limit)
|
|
.toList();
|
|
}
|
|
}
|