feat(dictionary): characters of Chinese, Korean and Greek, written by hand; pinyin typed

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>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-14 12:35:17 +02:00
commit 5479b973fc
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 2559 additions and 230 deletions

View file

@ -91,6 +91,91 @@ void main() {
});
});
group('a character of another language', () {
test("Chinese: its pinyin as its readings, its HSK level and Hong Kong's "
'grade from its labels', () {
final say = DictionaryCharacter.fromEntry(
ref: (source: 'unihan-hant', id: '說'),
headword: '說',
labels: const ['hsk:1', 'common', 'grade:1', 'radical:149'],
entry:
'{"meanings": ["speak", "say"], "pinyin": ["shuō", "shuì"], '
'"strokes": 14, "simplified": ["说"]}',
language: 'zh-Hant',
);
expect(say.onReadings, ['shuō', 'shuì']);
expect(say.kunReadings, isEmpty);
expect((say.hsk, say.grade, say.strokes), (1, 1, 14));
expect(say.tagNames, ['HSK-1', 'Grade 1']);
expect(say.card.onReadings, ['shuō', 'shuì']);
});
test('Korean: a Hanja its sounds and native meanings, the 1,800 of '
'education tagged; a letter its sound and name', () {
final water = DictionaryCharacter.fromEntry(
ref: (source: 'unihan-ko', id: '水'),
headword: '水',
labels: const ['education', 'radical:85'],
entry:
'{"meanings": ["water"], "eum": ["수"], "hun": ["물"], '
'"eumhun": ["물 수"], "strokes": 4}',
language: 'ko',
);
expect(water.onReadings, ['수']);
expect(water.kunReadings, ['물']);
expect(water.isEducationHanja, isTrue);
expect(water.tagNames, ['Education Hanja']);
final giyeok = DictionaryCharacter.fromEntry(
ref: (source: 'unihan-ko', id: 'ㄱ'),
headword: 'ㄱ',
labels: const ['hangul'],
entry: '{"romaji": "g, k", "script": "hangul", "name": "기역"}',
language: 'ko',
);
expect(giyeok.isKana, isTrue);
expect(giyeok.meaningsShown, ['g, k', '기역']);
expect(giyeok.tagNames, ['Hangul']);
});
test('Greek: a letter with its capital, its transliteration and its '
'names', () {
final beta = DictionaryCharacter.fromEntry(
ref: (source: 'greek-alphabet', id: 'β'),
headword: 'Β β',
labels: const ['greek'],
entry:
'{"romaji": "v", "script": "greek", "name": "βήτα", '
'"englishName": "beta", "sound": "v"}',
language: 'el',
);
expect(beta.card.front.plainText.trim(), 'Β β');
expect(beta.card.meanings, ['v', 'βήτα (beta)']);
expect(beta.tagNames, ['Greek alphabet']);
expect(isDictionaryTagName('Greek alphabet'), isTrue);
});
test("a Korean word's KRDict level names its tag as KRDict does", () {
final school = DictionaryWord.fromEntry(
ref: (source: 'krdict', id: '73276'),
headword: '학교',
reading: '학꾜',
labels: const ['level:beginner'],
entry:
'{"pos": "noun", "hanja": ["學校"], '
'"senses": [{"glosses": ["school"]}]}',
language: 'ko',
);
expect(school.koreanLevel, 1);
expect(school.tagNames, ['초급', 'Noun']);
expect(isDictionaryTagName('초급'), isTrue);
expect(school.card.reading, '학꾜');
});
});
group('Cards behind the dictionary', () {
late FakeCardGateway cardGateway;
late CardRepository cards;

View file

@ -72,8 +72,9 @@ void main() {
await tester.pumpAndSettle();
}
/// Opens the studyset [id]: `japanese` (Japanese with English meanings)
/// or `german`, on an instance with JMdict ([dictionaries]), at
/// Opens the studyset [id]: `japanese` (Japanese with English meanings),
/// `german`, `greek` or `chinese`, on an instance with JMdict
/// ([dictionaries]), at
/// [location] (the studyset's screen unless given).
Future<void> show(
WidgetTester tester, {
@ -107,6 +108,28 @@ void main() {
backLanguage: 'en',
),
),
)
..receiveRemoteChange(
'greek',
encodeStudysetPayload(
StudysetContent(
title: 'Greek',
description: RichContent.empty(),
frontLanguage: 'el',
backLanguage: 'en',
),
),
)
..receiveRemoteChange(
'chinese',
encodeStudysetPayload(
StudysetContent(
title: 'Chinese',
description: RichContent.empty(),
frontLanguage: 'zh-Hans',
backLanguage: 'en',
),
),
);
await pumpApp(
tester,
@ -259,6 +282,76 @@ void main() {
expect(made.content.entry, (source: 'kanjidic', id: '食'));
});
testWidgets("Greek's letters in a tab of their own, without radicals, "
'added whole as character cards', (tester) async {
await show(
tester,
id: 'greek',
dictionaries: FakeDictionaryGateway(
available: [wiktionaryEl, greekAlphabet],
words: [
character(
'Β β',
labels: ['greek'],
romaji: 'v',
name: 'βήτα (beta)',
of: greekAlphabet,
),
],
),
);
await addMaterial(tester, 'From the dictionary');
expect(find.text('Radicals'), findsNothing);
await tester.tap(find.text('Alphabet'));
await settle(tester);
expect(find.text('Letters'), findsOneWidget);
await tester.tap(find.text('Greek alphabet').hitTestable());
await tester.pumpAndSettle();
await tester.tap(find.text('Add 1 character'));
await tester.pumpAndSettle();
final made = (await cards.cardsOf('greek')).single;
expect(made.content.front.plainText.trim(), 'Β β');
expect(made.content.meanings, ['v', 'βήτα (beta)']);
});
testWidgets('Chinese characters by HSK level and in common use, beside '
'the radicals in their simplified forms', (tester) async {
await show(
tester,
id: 'chinese',
dictionaries: FakeDictionaryGateway(
available: [ccCedictHans, unihanHans],
words: [
character(
'说',
meanings: ['speak'],
on: ['shuō'],
labels: ['hsk:1', 'common', 'radical:149'],
of: unihanHans,
),
],
),
);
await addMaterial(tester, 'From the dictionary');
await tester.tap(find.text('Hanzi'));
await settle(tester);
expect(find.text('HSK levels'), findsOneWidget);
expect(find.text('In common use'), findsOneWidget);
expect(find.text('HSK 1'), findsOneWidget);
await tester.tap(find.text('Radicals'));
await settle(tester);
// 言 as simplified Chinese writes it at a character's side.
await tester.scrollUntilVisible(
find.text('讠'),
400,
scrollable: find.byType(Scrollable).last,
);
expect(find.text('讠'), findsOneWidget);
});
testWidgets('a character found shows its readings and strokes, and is '
'added as a card', (tester) async {
await show(tester, dictionaries: withKanji());