feat: dictionaries hold words or characters, and are asked by their source
A dictionary source now says what it holds (`kind`: `words`, the default for files from before, or `characters`), a string so that an app older than a new kind leaves it out. Search, countLabelled and labelled take the dictionary by its source and meaning language, as the unique index names it, rather than by the language: with KANJIDIC2 beside JMdict, a search for words would otherwise bring kanji, and JLPT N5 would hold both. The search reads the language for its query variants from the source's row; a dictionary the instance doesn't hold finds nothing. The rank index goes by source too. Migration: drops and recreates dictionary_entry_rank_idx on (source, glossLanguage, rank), and adds dictionary_source.kind, text NOT NULL DEFAULT 'words', so a JMdict loaded before is a dictionary of words. Both tables are reference data, never synced; the old server keeps working until the switch (its queries by language just lose the index). In the app, a studyset's dictionary is one of words unless a kind is asked for. 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>
This commit is contained in:
parent
e42cdfbfcb
commit
a6699b30cb
5 changed files with 11756 additions and 84 deletions
|
|
@ -16,17 +16,21 @@ class DictionaryRepository(final DictionaryGateway _gateway) {
|
|||
Future<List<Dictionary>> dictionaries() async =>
|
||||
_known ??= await _gateway.dictionaries();
|
||||
|
||||
/// The dictionary for a studyset in [languages]: of its front's
|
||||
/// language, with meanings in its back's, else in English, else in any;
|
||||
/// null when there is none, or no front language.
|
||||
Future<Dictionary?> dictionaryFor(StudysetLanguages languages) async {
|
||||
/// The dictionary of [kind] (words unless given) for a studyset in
|
||||
/// [languages]: of its front's language, with meanings in its back's,
|
||||
/// else in English, else in any; null when there is none, or no front
|
||||
/// language.
|
||||
Future<Dictionary?> dictionaryFor(
|
||||
StudysetLanguages languages, {
|
||||
DictionaryKind kind = DictionaryKind.words,
|
||||
}) async {
|
||||
final front = languages.front;
|
||||
if (front == null) {
|
||||
return null;
|
||||
}
|
||||
final ofLanguage = [
|
||||
for (final dictionary in await dictionaries())
|
||||
if (dictionary.language == front) dictionary,
|
||||
if (dictionary.language == front && dictionary.kind == kind) dictionary,
|
||||
];
|
||||
return ofLanguage
|
||||
.where((dictionary) => dictionary.glossLanguage == languages.back)
|
||||
|
|
|
|||
|
|
@ -5,26 +5,30 @@ import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.da
|
|||
/// [DictionaryGateway] asking the instance through the Serverpod client.
|
||||
class ServerpodDictionaryGateway(final protocol.Client _client)
|
||||
implements DictionaryGateway {
|
||||
/// The dictionaries, but those of a kind this app doesn't know (a newer
|
||||
/// instance's).
|
||||
@override
|
||||
Future<List<Dictionary>> dictionaries() async => [
|
||||
for (final source in await _client.dictionary.dictionaries())
|
||||
Dictionary(
|
||||
source: source.source,
|
||||
sourceVersion: source.sourceVersion,
|
||||
language: source.language,
|
||||
glossLanguage: source.glossLanguage,
|
||||
license: source.license,
|
||||
credits: [
|
||||
for (final credit in source.credits)
|
||||
(
|
||||
name: credit.name,
|
||||
by: credit.by,
|
||||
license: credit.license,
|
||||
url: credit.url,
|
||||
),
|
||||
],
|
||||
entryCount: source.entryCount,
|
||||
),
|
||||
if (DictionaryKind.values.asNameMap()[source.kind] case final kind?)
|
||||
Dictionary(
|
||||
source: source.source,
|
||||
sourceVersion: source.sourceVersion,
|
||||
kind: kind,
|
||||
language: source.language,
|
||||
glossLanguage: source.glossLanguage,
|
||||
license: source.license,
|
||||
credits: [
|
||||
for (final credit in source.credits)
|
||||
(
|
||||
name: credit.name,
|
||||
by: credit.by,
|
||||
license: credit.license,
|
||||
url: credit.url,
|
||||
),
|
||||
],
|
||||
entryCount: source.entryCount,
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
|
|
@ -33,7 +37,7 @@ class ServerpodDictionaryGateway(final protocol.Client _client)
|
|||
String query,
|
||||
) async => [
|
||||
for (final hit in await _client.dictionary.search(
|
||||
dictionary.language,
|
||||
dictionary.source,
|
||||
dictionary.glossLanguage,
|
||||
query,
|
||||
))
|
||||
|
|
@ -43,7 +47,7 @@ class ServerpodDictionaryGateway(final protocol.Client _client)
|
|||
@override
|
||||
Future<int> countLabelled(Dictionary dictionary, String label) =>
|
||||
_client.dictionary.countLabelled(
|
||||
dictionary.language,
|
||||
dictionary.source,
|
||||
dictionary.glossLanguage,
|
||||
label,
|
||||
);
|
||||
|
|
@ -56,7 +60,7 @@ class ServerpodDictionaryGateway(final protocol.Client _client)
|
|||
required int limit,
|
||||
}) async => [
|
||||
for (final hit in await _client.dictionary.labelled(
|
||||
dictionary.language,
|
||||
dictionary.source,
|
||||
dictionary.glossLanguage,
|
||||
label,
|
||||
offset: offset,
|
||||
|
|
|
|||
|
|
@ -13,11 +13,22 @@ typedef DictionaryCredit = ({
|
|||
String url,
|
||||
});
|
||||
|
||||
/// A dictionary the instance holds: words of [language] with meanings in
|
||||
/// [glossLanguage] (BCP 47 tags), from [source] at [sourceVersion].
|
||||
/// What a dictionary holds.
|
||||
enum DictionaryKind() {
|
||||
/// Words (JMdict).
|
||||
words,
|
||||
|
||||
/// The characters words are written in (KANJIDIC2, with the kana).
|
||||
characters,
|
||||
}
|
||||
|
||||
/// A dictionary the instance holds: words or characters ([kind]) of
|
||||
/// [language] with meanings in [glossLanguage] (BCP 47 tags), from [source]
|
||||
/// at [sourceVersion].
|
||||
final class const Dictionary({
|
||||
required final String source,
|
||||
required final String sourceVersion,
|
||||
required final DictionaryKind kind,
|
||||
required final String language,
|
||||
required final String glossLanguage,
|
||||
required final String license,
|
||||
|
|
@ -28,6 +39,7 @@ final class const Dictionary({
|
|||
List<Object?> get props => [
|
||||
source,
|
||||
sourceVersion,
|
||||
kind,
|
||||
language,
|
||||
glossLanguage,
|
||||
license,
|
||||
|
|
|
|||
|
|
@ -102,10 +102,12 @@ void main() {
|
|||
});
|
||||
|
||||
test("finds a studyset's dictionary by its front's language, with "
|
||||
"meanings in its back's, else in English", () async {
|
||||
"meanings in its back's, else in English; one of words unless a "
|
||||
'kind is asked for', () async {
|
||||
const german = Dictionary(
|
||||
source: 'jmdict',
|
||||
sourceVersion: '1',
|
||||
kind: DictionaryKind.words,
|
||||
language: 'ja',
|
||||
glossLanguage: 'de',
|
||||
license: 'CC-BY-SA-4.0',
|
||||
|
|
@ -113,7 +115,7 @@ void main() {
|
|||
entryCount: 1,
|
||||
);
|
||||
final repository = DictionaryRepository(
|
||||
FakeDictionaryGateway(available: [jmdict, german]),
|
||||
FakeDictionaryGateway(available: [kanjidic, jmdict, german]),
|
||||
);
|
||||
|
||||
expect(await repository.dictionaryFor((front: 'ja', back: 'de')), german);
|
||||
|
|
@ -121,6 +123,13 @@ void main() {
|
|||
expect(await repository.dictionaryFor((front: 'ja', back: null)), jmdict);
|
||||
expect(await repository.dictionaryFor((front: 'ko', back: 'en')), isNull);
|
||||
expect(await repository.dictionaryFor((front: null, back: 'en')), isNull);
|
||||
expect(
|
||||
await repository.dictionaryFor((
|
||||
front: 'ja',
|
||||
back: 'en',
|
||||
), kind: DictionaryKind.characters),
|
||||
kanjidic,
|
||||
);
|
||||
});
|
||||
|
||||
test('brings every word of a level, a page at a time', () async {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.da
|
|||
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',
|
||||
|
|
@ -20,6 +21,26 @@ const jmdict = Dictionary(
|
|||
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,
|
||||
);
|
||||
|
||||
/// A JMdict word: [headword] read [reading], meaning [glosses], [labels]
|
||||
/// and the parts of speech [pos] on its one sense.
|
||||
DictionaryWord word(
|
||||
|
|
|
|||
Loading…
Reference in a new issue