feat(app): kanji and kana in the dictionary, beside the words

Where the instance holds the characters of a studyset's language, the
dictionary screen has a tab for them, Kanji & kana: search by character,
reading (kana or romaji) or meaning, and whole levels to add, under their
names: the kana (hiragana, katakana), the JLPT levels and the school
grades. A character becomes a character card, its meanings (a kana's
romaji) and readings, tagged with its level (JLPT-N5), school grade
(Grade 1 … Grade 6, Secondary school) and script (Hiragana, Katakana). A
character card's Look up searches the characters, and an entry looked up
again on a card swaps the tags the one before brought, as words do.

A dictionary's entries are now words or characters (DictionaryItem), as
its kind says.

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:
Tizian 「ティツィアーン」 2026-10-09 15:19:22 +02:00
commit cec518350a
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 1034 additions and 158 deletions

View file

@ -27,6 +27,68 @@ final terebi = word(
);
void main() {
group('a character', () {
test('reads its meanings, readings, grade, strokes and frequency from '
'the entry', () {
final read = DictionaryCharacter.fromEntry(
ref: (source: 'kanjidic', id: '食'),
headword: '食',
labels: ['jlpt:n5', 'grade:2'],
entry:
'{"meanings":["eat","food"],"on":["ショク","ジキ"],'
'"kun":["く.う","た.べる"],"grade":2,"strokes":9,'
'"frequency":328,"jlpt":5}',
);
expect(read.meanings, ['eat', 'food']);
expect(read.onReadings, ['ショク', 'ジキ']);
expect(read.kunReadings, ['く.う', 'た.べる']);
expect((read.grade, read.strokes, read.frequency), (2, 9, 328));
expect(read.jlpt, 5);
expect(read.isKana, isFalse);
});
test('becomes a character card that keeps where it is from; a kana '
'means its romaji', () {
final eat = character(
'食',
meanings: ['eat'],
on: ['ショク'],
kun: ['た.べる'],
).card;
expect(eat.type, CardType.character);
expect(eat.front.plainText.trim(), '食');
expect(eat.meanings, ['eat']);
expect(eat.onReadings, ['ショク']);
expect(eat.kunReadings, ['た.べる']);
expect(eat.entry, (source: 'kanjidic', id: '食'));
expect(eat.isComplete, isTrue);
final a = character('あ', labels: ['hiragana'], romaji: 'a').card;
expect(a.meanings, ['a']);
expect(a.isComplete, isTrue);
});
test('names the tags by JLPT level, school grade and script', () {
expect(
character('食', labels: ['jlpt:n5', 'grade:2'], grade: 2).tagNames,
['JLPT-N5', 'Grade 2'],
);
expect(
character(
'亜',
labels: ['jlpt:n1', 'grade:secondary'],
grade: 8,
).tagNames,
['JLPT-N1', 'Secondary school'],
);
expect(character('ア', labels: ['katakana']).tagNames, ['Katakana']);
expect(isDictionaryTagName('grade 2'), isTrue);
expect(isDictionaryTagName('Hiragana'), isTrue);
expect(isDictionaryTagName('Kanji I like'), isFalse);
});
});
group('a word', () {
test('reads its senses from the entry, the codes with them', () {
final read = DictionaryWord.fromEntry(

View file

@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
import 'package:material_ui/material_ui.dart';
@ -36,6 +37,23 @@ final toukyou = word(
pos: ['n'],
);
final shoku = character(
'食',
meanings: ['eat', 'food'],
on: ['ショク'],
kun: ['た.べる'],
labels: ['jlpt:n5', 'grade:2'],
grade: 2,
strokes: 9,
);
final a = character('あ', labels: ['hiragana'], romaji: 'a');
/// An instance with JMdict and KANJIDIC2.
FakeDictionaryGateway withKanji() => FakeDictionaryGateway(
available: [jmdict, kanjidic],
words: [taberu, terebi, shoku, a],
);
void main() {
late FakeCardGateway cardGateway;
late CardRepository cards;
@ -203,6 +221,72 @@ void main() {
);
});
testWidgets('beside the words, kanji and kana: their levels in groups, '
'one added as character cards tagged with level and grade', (
tester,
) async {
await show(tester, dictionaries: withKanji());
await addMaterial(tester, 'From the dictionary');
expect(find.text('Words'), findsOneWidget);
await tester.tap(find.text('Kanji & kana'));
await settle(tester);
expect(find.text('Kana'), findsOneWidget);
expect(find.text('JLPT levels'), findsOneWidget);
expect(find.text('School grades'), findsOneWidget);
expect(find.text('Grade 2'), findsOneWidget);
await tester.tap(find.text('N5').hitTestable());
await tester.pumpAndSettle();
await tester.tap(find.text('Add 1 character'));
await tester.pumpAndSettle();
expect(find.text('Added 1 character.'), findsOneWidget);
final made = (await cards.cardsOf('japanese')).single;
expect(made.content.type, CardType.character);
expect(made.content.onReadings, ['ショク']);
expect(made.content.entry, (source: 'kanjidic', id: '食'));
});
testWidgets('a character found shows its readings and strokes, and is '
'added as a card', (tester) async {
await show(tester, dictionaries: withKanji());
await addMaterial(tester, 'From the dictionary');
await tester.tap(find.text('Kanji & kana'));
await settle(tester);
await tester.enterText(find.byType(TextField).hitTestable(), 'ショク');
await tester.pump(const Duration(milliseconds: 300));
await tester.pumpAndSettle();
expect(find.text('ショク · た.べる'), findsOneWidget);
expect(find.text('9 strokes'), findsOneWidget);
await tester.tap(find.text('Add').hitTestable());
await tester.pumpAndSettle();
expect(find.text('Added “食”.'), findsOneWidget);
});
testWidgets('Look up fills a character card with its meanings, readings '
'and tags', (tester) async {
await show(tester, dictionaries: withKanji());
await addMaterial(tester, 'Create flashcards');
await tester.tap(find.text('Character'));
await settle(tester);
await tester.tap(find.byTooltip('Look up in the dictionary'));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField).last, 'eat');
await tester.pump(const Duration(milliseconds: 300));
await tester.pumpAndSettle();
await tester.tap(find.text('eat; food'));
await settle(tester);
expect(find.widgetWithText(TextField, 'eat'), findsOneWidget);
expect(find.widgetWithText(TextField, 'ショク'), findsOneWidget);
expect(find.widgetWithText(TextField, 'た.べる'), findsOneWidget);
expect(find.text('JLPT-N5'), findsOneWidget);
expect(find.text('Grade 2'), findsOneWidget);
});
testWidgets('Look up fills a vocabulary card with a word of the '
'dictionary', (tester) async {
await show(tester);