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 glosses = const [], List genders = const [], List forms = const [], List 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 glosses = const [], List labels = const [], List 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 meanings = const [], List on = const [], List kun = const [], List 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 available = const [jmdict], final List 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> dictionaries() async { asked++; if (fail case final error?) { throw error; } return available; } /// The entries of [dictionary]. Iterable _of(Dictionary dictionary) => words.where((word) => word.ref.source == dictionary.source); @override Future> 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> entries( Dictionary dictionary, List 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 = []; /// 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>> withKeys( Dictionary dictionary, List keys, ) async { if (fail case final error?) { throw error; } keysAsked.add(keys.length); final asked = keys.toSet(); final byKey = >{}; 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 = []; @override Future> changed( Dictionary dictionary, Map 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 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> 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(); } }