feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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>
2026-10-09 01:33:12 +02:00
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
feat: a dictionary's corrections reach the cards made from it, by offer
Every dictionary entry now has a version: 16 hex digits of the SHA-256 of
what cards take of it (headword, reading, entry), made by the loader and
carried on every hit. A card keeps the version it took (card schema 8).
DictionaryEndpoint.changed returns, of the entries named with the
versions cards keep (1,000 at most a call; the app asks 500 at a time),
those whose version is another now.
The card browser says how many cards are behind their dictionary and takes
the corrections at a tap (decided 2026-10-09: by offer, never by
themselves). Taking one writes the entry as it is now over the revision
in which the card last took it, beside whatever came after, so the
revisions merge per side as two devices' edits do (ADR 0003): a side only
the dictionary changed takes the correction, a side the person changed
too is a conflict settled in the composer, whose words now name that
case. Cards made before versions are never offered one.
The server rereads its dictionary folder hourly now, not daily: the
dictionaries service fills its volume after an instance's first start,
and an hour with nothing changed costs a hash of each file.
Migration 20261009145434785-dictionary-entry-version: adds
dictionary_entry.version, text NOT NULL DEFAULT '' (rows loaded before
have none and are never reported as corrected), and by hand empties
dictionary_source."fileHash", so every source loads once more and its
entries get their versions; the schema stays as definition.sql says. The
old server goes on working until the switch.
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>
2026-10-09 17:32:00 +02:00
|
|
|
|
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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>
2026-10-09 01:33:12 +02:00
|
|
|
|
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/dictionary/data/dictionary_adder.dart';
|
|
|
|
|
|
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/dictionary_repository.dart';
|
feat: a dictionary's corrections reach the cards made from it, by offer
Every dictionary entry now has a version: 16 hex digits of the SHA-256 of
what cards take of it (headword, reading, entry), made by the loader and
carried on every hit. A card keeps the version it took (card schema 8).
DictionaryEndpoint.changed returns, of the entries named with the
versions cards keep (1,000 at most a call; the app asks 500 at a time),
those whose version is another now.
The card browser says how many cards are behind their dictionary and takes
the corrections at a tap (decided 2026-10-09: by offer, never by
themselves). Taking one writes the entry as it is now over the revision
in which the card last took it, beside whatever came after, so the
revisions merge per side as two devices' edits do (ADR 0003): a side only
the dictionary changed takes the correction, a side the person changed
too is a conflict settled in the composer, whose words now name that
case. Cards made before versions are never offered one.
The server rereads its dictionary folder hourly now, not daily: the
dictionaries service fills its volume after an instance's first start,
and an hour with nothing changed costs a hash of each file.
Migration 20261009145434785-dictionary-entry-version: adds
dictionary_entry.version, text NOT NULL DEFAULT '' (rows loaded before
have none and are never reported as corrected), and by hand empties
dictionary_source."fileHash", so every source loads once more and its
entries get their versions; the schema stays as definition.sql says. The
old server goes on working until the switch.
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>
2026-10-09 17:32:00 +02:00
|
|
|
|
import 'package:halkyon_learn_flutter/src/feature/dictionary/data/dictionary_updates.dart';
|
feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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>
2026-10-09 01:33:12 +02:00
|
|
|
|
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.dart';
|
|
|
|
|
|
import 'package:halkyon_learn_flutter/src/feature/tag/data/tag_repository.dart';
|
|
|
|
|
|
|
|
|
|
|
|
import '../../support/fake_card_gateway.dart';
|
|
|
|
|
|
import '../../support/fake_dictionary_gateway.dart';
|
|
|
|
|
|
import '../../support/fake_tag_gateway.dart';
|
|
|
|
|
|
|
|
|
|
|
|
final taberu = word(
|
|
|
|
|
|
'1358280',
|
|
|
|
|
|
'食べる',
|
|
|
|
|
|
reading: 'たべる',
|
|
|
|
|
|
glosses: ['to eat'],
|
|
|
|
|
|
labels: ['common', 'jlpt:n5'],
|
|
|
|
|
|
pos: ['v1', 'vt'],
|
|
|
|
|
|
);
|
|
|
|
|
|
final terebi = word(
|
|
|
|
|
|
'1080510',
|
|
|
|
|
|
'テレビ',
|
|
|
|
|
|
glosses: ['television', 'TV'],
|
|
|
|
|
|
labels: ['common', 'jlpt:n5'],
|
|
|
|
|
|
pos: ['n'],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
void main() {
|
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>
2026-10-09 15:19:22 +02:00
|
|
|
|
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);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
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>
2026-10-14 12:35:17 +02:00
|
|
|
|
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, '학꾜');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
feat: a dictionary's corrections reach the cards made from it, by offer
Every dictionary entry now has a version: 16 hex digits of the SHA-256 of
what cards take of it (headword, reading, entry), made by the loader and
carried on every hit. A card keeps the version it took (card schema 8).
DictionaryEndpoint.changed returns, of the entries named with the
versions cards keep (1,000 at most a call; the app asks 500 at a time),
those whose version is another now.
The card browser says how many cards are behind their dictionary and takes
the corrections at a tap (decided 2026-10-09: by offer, never by
themselves). Taking one writes the entry as it is now over the revision
in which the card last took it, beside whatever came after, so the
revisions merge per side as two devices' edits do (ADR 0003): a side only
the dictionary changed takes the correction, a side the person changed
too is a conflict settled in the composer, whose words now name that
case. Cards made before versions are never offered one.
The server rereads its dictionary folder hourly now, not daily: the
dictionaries service fills its volume after an instance's first start,
and an hour with nothing changed costs a hash of each file.
Migration 20261009145434785-dictionary-entry-version: adds
dictionary_entry.version, text NOT NULL DEFAULT '' (rows loaded before
have none and are never reported as corrected), and by hand empties
dictionary_source."fileHash", so every source loads once more and its
entries get their versions; the schema stays as definition.sql says. The
old server goes on working until the switch.
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>
2026-10-09 17:32:00 +02:00
|
|
|
|
group('Cards behind the dictionary', () {
|
|
|
|
|
|
late FakeCardGateway cardGateway;
|
|
|
|
|
|
late CardRepository cards;
|
|
|
|
|
|
setUp(() {
|
|
|
|
|
|
cardGateway = FakeCardGateway();
|
|
|
|
|
|
cards = CardRepository(cardGateway);
|
|
|
|
|
|
});
|
|
|
|
|
|
tearDown(() => cards.dispose());
|
|
|
|
|
|
|
|
|
|
|
|
test('are those whose entry the source corrected since, with the entry '
|
|
|
|
|
|
'as it is now; cards of their own and made before versions are '
|
|
|
|
|
|
'never asked about', () async {
|
|
|
|
|
|
final gateway = FakeDictionaryGateway(
|
|
|
|
|
|
words: [
|
|
|
|
|
|
word('1358280', '食べる', glosses: ['to eat'], version: 'v2'),
|
|
|
|
|
|
word('1080510', 'テレビ', glosses: ['TV'], version: 'v1'),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
final updates = DictionaryUpdates(DictionaryRepository(gateway), cards);
|
|
|
|
|
|
CardContent made(String id, String? version) => CardContent(
|
|
|
|
|
|
type: CardType.vocabulary,
|
|
|
|
|
|
front: RichContent.plain(id),
|
|
|
|
|
|
back: RichContent.empty(),
|
|
|
|
|
|
meanings: const ['x'],
|
|
|
|
|
|
entry: (source: 'jmdict', id: id),
|
|
|
|
|
|
entryVersion: version,
|
|
|
|
|
|
);
|
|
|
|
|
|
final behind = await cards.create('japanese', made('1358280', 'v1'));
|
|
|
|
|
|
await cards.create('japanese', made('1080510', 'v1'));
|
|
|
|
|
|
await cards.create('japanese', made('1080510', null));
|
|
|
|
|
|
await cards.create('japanese', CardContent.empty());
|
|
|
|
|
|
|
|
|
|
|
|
final found = await updates.behind(
|
|
|
|
|
|
await cards.cardsOf('japanese'),
|
|
|
|
|
|
(front: 'ja', back: 'en'),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
expect(found.keys, [behind]);
|
|
|
|
|
|
expect(found[behind]!.version, 'v2');
|
|
|
|
|
|
expect(gateway.changedAsked, [2]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('of one entry keeping different versions are each compared', () async {
|
|
|
|
|
|
final gateway = FakeDictionaryGateway(
|
|
|
|
|
|
words: [
|
|
|
|
|
|
word('1358280', '食べる', glosses: ['to eat'], version: 'v2'),
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
final updates = DictionaryUpdates(DictionaryRepository(gateway), cards);
|
|
|
|
|
|
CardContent made(String version) => CardContent(
|
|
|
|
|
|
type: CardType.vocabulary,
|
|
|
|
|
|
front: RichContent.plain('食べる'),
|
|
|
|
|
|
back: RichContent.empty(),
|
|
|
|
|
|
meanings: const ['to eat'],
|
|
|
|
|
|
entry: (source: 'jmdict', id: '1358280'),
|
|
|
|
|
|
entryVersion: version,
|
|
|
|
|
|
);
|
|
|
|
|
|
await cards.create('japanese', made('v2'));
|
|
|
|
|
|
final old = await cards.create('japanese', made('v1'));
|
|
|
|
|
|
|
|
|
|
|
|
final found = await updates.behind(
|
|
|
|
|
|
await cards.cardsOf('japanese'),
|
|
|
|
|
|
(front: 'ja', back: 'en'),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
expect(found.keys, [old]);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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>
2026-10-09 01:33:12 +02:00
|
|
|
|
group('a word', () {
|
|
|
|
|
|
test('reads its senses from the entry, the codes with them', () {
|
|
|
|
|
|
final read = DictionaryWord.fromEntry(
|
|
|
|
|
|
ref: (source: 'jmdict', id: '1358280'),
|
|
|
|
|
|
headword: '食べる',
|
|
|
|
|
|
reading: 'たべる',
|
|
|
|
|
|
labels: ['common', 'jlpt:n5'],
|
|
|
|
|
|
entry:
|
|
|
|
|
|
'{"forms":[{"text":"食べる"}],"senses":[{"glosses":["to eat"],'
|
|
|
|
|
|
'"pos":["v1","vt"]},{"glosses":["to live on"],"misc":["uk"]}]}',
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
expect(read.senses, const [
|
|
|
|
|
|
VocabularySense(glosses: ['to eat'], pos: ['v1', 'vt']),
|
|
|
|
|
|
VocabularySense(glosses: ['to live on'], misc: ['uk']),
|
|
|
|
|
|
]);
|
|
|
|
|
|
expect(read.jlpt, 5);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('becomes a vocabulary card that keeps where it is from; a word in '
|
|
|
|
|
|
'kana has no reading', () {
|
|
|
|
|
|
final card = taberu.card;
|
|
|
|
|
|
expect(card.type, CardType.vocabulary);
|
|
|
|
|
|
expect(card.front.plainText.trim(), '食べる');
|
|
|
|
|
|
expect(card.reading, 'たべる');
|
|
|
|
|
|
expect(card.meanings, ['to eat']);
|
|
|
|
|
|
expect(card.entry, (source: 'jmdict', id: '1358280'));
|
|
|
|
|
|
|
|
|
|
|
|
expect(terebi.card.reading, '');
|
|
|
|
|
|
});
|
2026-10-09 13:36:16 +02:00
|
|
|
|
|
|
|
|
|
|
test('its first three example sentences become hints, the word in bold '
|
|
|
|
|
|
'and the English below', () {
|
|
|
|
|
|
String example(int n) =>
|
|
|
|
|
|
'{"text":"食べ","ja":"$n: 魚を食べた。","en":"Sentence $n."}';
|
|
|
|
|
|
final word = DictionaryWord.fromEntry(
|
|
|
|
|
|
ref: (source: 'jmdict', id: '1358280'),
|
|
|
|
|
|
headword: '食べる',
|
|
|
|
|
|
reading: 'たべる',
|
|
|
|
|
|
labels: const [],
|
|
|
|
|
|
entry:
|
|
|
|
|
|
'{"senses":[{"glosses":["to eat"],"examples":[${example(1)},'
|
|
|
|
|
|
'${example(2)}]},{"glosses":["to live on"],"examples":['
|
|
|
|
|
|
'${example(3)},${example(4)}]}]}',
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
final hints = word.card.hints;
|
|
|
|
|
|
expect(hints, hasLength(3));
|
|
|
|
|
|
expect(hints.first.plainText, '1: 魚を食べた。\nSentence 1.');
|
|
|
|
|
|
final document = hints.first.editorJson['document']! as Map;
|
|
|
|
|
|
final first = (document['children']! as List).first as Map;
|
|
|
|
|
|
expect((first['data']! as Map)['delta'], [
|
|
|
|
|
|
{'insert': '1: 魚を'},
|
|
|
|
|
|
{
|
|
|
|
|
|
'insert': '食べ',
|
|
|
|
|
|
'attributes': {'bold': true},
|
|
|
|
|
|
},
|
|
|
|
|
|
{'insert': 'た。'},
|
|
|
|
|
|
]);
|
|
|
|
|
|
});
|
feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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>
2026-10-09 01:33:12 +02:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
group('the repository', () {
|
|
|
|
|
|
test('asks for the dictionaries once, again after a failure', () async {
|
|
|
|
|
|
final gateway = FakeDictionaryGateway()..fail = Exception('offline');
|
|
|
|
|
|
final repository = DictionaryRepository(gateway);
|
|
|
|
|
|
|
|
|
|
|
|
await expectLater(repository.dictionaries(), throwsException);
|
|
|
|
|
|
gateway.fail = null;
|
|
|
|
|
|
expect(await repository.dictionaries(), [jmdict]);
|
|
|
|
|
|
await repository.dictionaries();
|
|
|
|
|
|
expect(gateway.asked, 2);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("finds a studyset's dictionary by its front's language, with "
|
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>
2026-10-09 14:47:46 +02:00
|
|
|
|
"meanings in its back's, else in English; one of words unless a "
|
|
|
|
|
|
'kind is asked for', () async {
|
feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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>
2026-10-09 01:33:12 +02:00
|
|
|
|
const german = Dictionary(
|
|
|
|
|
|
source: 'jmdict',
|
|
|
|
|
|
sourceVersion: '1',
|
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>
2026-10-09 14:47:46 +02:00
|
|
|
|
kind: DictionaryKind.words,
|
feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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>
2026-10-09 01:33:12 +02:00
|
|
|
|
language: 'ja',
|
|
|
|
|
|
glossLanguage: 'de',
|
|
|
|
|
|
license: 'CC-BY-SA-4.0',
|
|
|
|
|
|
credits: [],
|
|
|
|
|
|
entryCount: 1,
|
|
|
|
|
|
);
|
|
|
|
|
|
final repository = DictionaryRepository(
|
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>
2026-10-09 14:47:46 +02:00
|
|
|
|
FakeDictionaryGateway(available: [kanjidic, jmdict, german]),
|
feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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>
2026-10-09 01:33:12 +02:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
expect(await repository.dictionaryFor((front: 'ja', back: 'de')), german);
|
|
|
|
|
|
expect(await repository.dictionaryFor((front: 'ja', back: 'fr')), jmdict);
|
|
|
|
|
|
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);
|
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>
2026-10-09 14:47:46 +02:00
|
|
|
|
expect(
|
|
|
|
|
|
await repository.dictionaryFor((
|
|
|
|
|
|
front: 'ja',
|
|
|
|
|
|
back: 'en',
|
|
|
|
|
|
), kind: DictionaryKind.characters),
|
|
|
|
|
|
kanjidic,
|
|
|
|
|
|
);
|
feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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>
2026-10-09 01:33:12 +02:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('brings every word of a level, a page at a time', () async {
|
|
|
|
|
|
final words = [
|
|
|
|
|
|
for (var i = 0; i < DictionaryRepository.page + 3; i++)
|
|
|
|
|
|
word('$i', 'w$i', labels: ['jlpt:n5']),
|
|
|
|
|
|
];
|
|
|
|
|
|
final counts = <int>[];
|
|
|
|
|
|
|
|
|
|
|
|
final all = await DictionaryRepository(
|
|
|
|
|
|
FakeDictionaryGateway(words: words),
|
|
|
|
|
|
).allLabelled(jmdict, 'jlpt:n5', onProgress: counts.add);
|
|
|
|
|
|
|
|
|
|
|
|
expect(all, words);
|
|
|
|
|
|
expect(counts, [
|
|
|
|
|
|
DictionaryRepository.page,
|
|
|
|
|
|
DictionaryRepository.page + 3,
|
|
|
|
|
|
]);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
group('adding words', () {
|
|
|
|
|
|
late CardRepository cards;
|
|
|
|
|
|
late TagRepository tags;
|
|
|
|
|
|
late DictionaryAdder adder;
|
|
|
|
|
|
|
|
|
|
|
|
setUp(() {
|
|
|
|
|
|
cards = CardRepository(FakeCardGateway());
|
|
|
|
|
|
tags = TagRepository(FakeTagGateway());
|
|
|
|
|
|
adder = DictionaryAdder(cards, tags);
|
|
|
|
|
|
});
|
|
|
|
|
|
tearDown(() async {
|
|
|
|
|
|
await cards.dispose();
|
|
|
|
|
|
await tags.dispose();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('makes each word a card once, tagged with its level and word '
|
|
|
|
|
|
'class, marked as from the dictionary', () async {
|
|
|
|
|
|
expect(await adder.add('japanese', [taberu]), (added: 1, skipped: 0));
|
|
|
|
|
|
expect(
|
|
|
|
|
|
await adder.add('japanese', [taberu, terebi]),
|
|
|
|
|
|
(added: 1, skipped: 1),
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
final made = await cards.cardsOf('japanese');
|
|
|
|
|
|
expect(
|
|
|
|
|
|
[for (final card in made) card.content.entry?.id],
|
|
|
|
|
|
[
|
|
|
|
|
|
'1358280',
|
|
|
|
|
|
'1080510',
|
|
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
final tagging = await tags.read();
|
|
|
|
|
|
expect(
|
|
|
|
|
|
[for (final tag in tagging.of(made.first.id)) tag.name],
|
|
|
|
|
|
[
|
feat(app): tags keep their case; dictionary tags read JLPT-N5, Noun
A tag is still its name, but no longer in lowercase: the name keeps the
case it was written in, and names that differ only in case are one tag
(ADR 0004, rule 2, amended). Two such tags from two devices fold into the
one with the smaller id, its case with it. Tags stored before stay
lowercase until renamed.
Words from the dictionary are tagged JLPT-N5 to JLPT-N1 and with their
word class (Noun, Verb, i-Adjective, na-Adjective, ...). Anki tags and
PDF chapter tags arrive as written.
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>
2026-10-09 10:48:06 +02:00
|
|
|
|
'JLPT-N5',
|
|
|
|
|
|
'Verb',
|
feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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>
2026-10-09 01:33:12 +02:00
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(
|
|
|
|
|
|
[for (final tag in tagging.of(made.last.id)) tag.name],
|
|
|
|
|
|
[
|
feat(app): tags keep their case; dictionary tags read JLPT-N5, Noun
A tag is still its name, but no longer in lowercase: the name keeps the
case it was written in, and names that differ only in case are one tag
(ADR 0004, rule 2, amended). Two such tags from two devices fold into the
one with the smaller id, its case with it. Tags stored before stay
lowercase until renamed.
Words from the dictionary are tagged JLPT-N5 to JLPT-N1 and with their
word class (Noun, Verb, i-Adjective, na-Adjective, ...). Anki tags and
PDF chapter tags arrive as written.
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>
2026-10-09 10:48:06 +02:00
|
|
|
|
'JLPT-N5',
|
|
|
|
|
|
'Noun',
|
feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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>
2026-10-09 01:33:12 +02:00
|
|
|
|
],
|
|
|
|
|
|
);
|
|
|
|
|
|
expect((await cards.allCards()).every((card) => card.imported), isTrue);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
fix(app): the dictionary adds each word once, says when it can't, and Look up tags hold
- A word tapped twice, or added while a level is being added, makes one
card: adds run one at a time, and the word counts as in the studyset
at once. The adder reads the studyset's cards once rather than for
every word.
- Adding that fails says so, and a level shows "Making the cards…" once
its words have come.
- Finding a studyset's dictionary tells finding, none and unreachable
apart: the dictionary screen says there is none or offers Retry, and
an instance unreachable at start is asked again once a sync goes
through (Look up and From the dictionary appear then).
- Look up's tags wait on the card by name and are made when it is saved:
leaving the card at once no longer saves it untagged, a word replaced
leaves no tag behind, and a new word replaces any level or word class
on the card, pinned ones too. Look up marks words already made into
cards of the studyset.
- The tag picker's Create keeps the case typed.
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>
2026-10-09 12:45:18 +02:00
|
|
|
|
test('makes one card of a word added twice at once, or twice in one '
|
|
|
|
|
|
'list', () async {
|
|
|
|
|
|
final both = await Future.wait([
|
|
|
|
|
|
adder.add('japanese', [taberu]),
|
|
|
|
|
|
adder.add('japanese', [taberu]),
|
|
|
|
|
|
]);
|
|
|
|
|
|
expect(both, [(added: 1, skipped: 0), (added: 0, skipped: 1)]);
|
|
|
|
|
|
expect(
|
|
|
|
|
|
await adder.add('japanese', [terebi, terebi]),
|
|
|
|
|
|
(added: 1, skipped: 1),
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(await cards.cardsOf('japanese'), hasLength(2));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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>
2026-10-09 01:33:12 +02:00
|
|
|
|
test('names the tags by level and word class', () {
|
2026-10-09 11:40:03 +02:00
|
|
|
|
expect(taberu.tagNames, ['JLPT-N5', 'Verb']);
|
feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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>
2026-10-09 01:33:12 +02:00
|
|
|
|
expect(
|
2026-10-09 11:40:03 +02:00
|
|
|
|
word('1', 'x', pos: ['adj-na', 'n']).tagNames,
|
feat(app): tags keep their case; dictionary tags read JLPT-N5, Noun
A tag is still its name, but no longer in lowercase: the name keeps the
case it was written in, and names that differ only in case are one tag
(ADR 0004, rule 2, amended). Two such tags from two devices fold into the
one with the smaller id, its case with it. Tags stored before stay
lowercase until renamed.
Words from the dictionary are tagged JLPT-N5 to JLPT-N1 and with their
word class (Noun, Verb, i-Adjective, na-Adjective, ...). Anki tags and
PDF chapter tags arrive as written.
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>
2026-10-09 10:48:06 +02:00
|
|
|
|
['na-Adjective', 'Noun'],
|
feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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>
2026-10-09 01:33:12 +02:00
|
|
|
|
);
|
2026-10-09 11:40:03 +02:00
|
|
|
|
expect(word('2', 'y', pos: ['vt']).tagNames, isEmpty);
|
feat(app): the dictionary in the app: look words up, add them as cards
A studyset's dictionary follows from its language, as decided: the
instance's dictionary of its front's language, with meanings in its
back's (else English). Nothing to attach or detach; nothing of it shows
where there is none.
- Add material › From the dictionary: search a word (kanji, kana,
romaji, meaning) and add it as a flashcard, or add a whole level
(JLPT N5…N1, common words) at once, asked first above 500 words, its
progress shown. Words already in the studyset show as such and are
left out. Each card is a vocabulary card (headword, reading, senses
with their codes), tagged `jlpt-n5` and with its word class, and
marked as imported from the dictionary.
- Look up beside a vocabulary card's front fills the card with the word
chosen, keeping its hints, explanation and tags.
- A card keeps the dictionary entry it was made from (card schema 6:
`dictionary` {source, id}), to be found again and not added twice.
- The dictionary's credits show at its foot and in About, as its licence
asks.
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>
2026-10-09 01:33:12 +02:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|