feat(learn): furigana and pinyin over hints, from Tatoeba's transcriptions
The curator puts Tatoeba's readings on a dictionary's examples by their Tatoeba ID (`withTatoebaReadings`, `curate readings`, and in `update`): furigana over JMdict's Japanese from its jpn transcriptions, pinyin with tone marks over Chinese from its cmn ones, each word over as many Han characters as it has syllables. An example gets `ruby`, `[start, end, reading]` each, and `rubyUnreviewed` where no one wrote or checked them (MeCab's furigana, the automatic pinyin). JMdict's conversion goes to 3, so instances convert again at once. A word's example hints carry them as ruby marks, the word still in bold. The hints sheet shows them from the start with a switch above (Furigana, Pinyin), kept on the device (`DeviceSettings.hintReadings`), and below a hint whose readings no one reviewed says so. Whats-New: Example sentences in a word's hints now show furigana or pinyin over them, with a switch to hide them. Whats-New-De: Beispielsätze in den Hinweisen eines Worts zeigen jetzt Furigana oder Pinyin darüber, mit einem Schalter zum Ausblenden. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
bed39dc2a2
commit
3257905536
4 changed files with 1083 additions and 78 deletions
|
|
@ -77,6 +77,18 @@ void main() {
|
|||
expect(read.withLanguage('de').librarySubject, 'languages');
|
||||
});
|
||||
|
||||
test("shows readings over hints' sentences until told not to (decided "
|
||||
'10 Nov 2026)', () async {
|
||||
expect((await store.read()).hintReadings, isTrue);
|
||||
|
||||
await store.write(const DeviceSettings(hintReadings: false));
|
||||
|
||||
final read = await FileDeviceSettingsStore(file).read();
|
||||
expect(read.hintReadings, isFalse);
|
||||
expect(read.withHintReadings(shown: true).hintReadings, isTrue);
|
||||
expect(read.withLanguage('de').hintReadings, isFalse);
|
||||
});
|
||||
|
||||
test('shows the Pomodoro timer while studying until told not to', () async {
|
||||
expect((await store.read()).pomodoroWhileStudying, isTrue);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'dart:typed_data';
|
|||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/ruby_marks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
|
|
@ -324,6 +325,53 @@ void main() {
|
|||
{'insert': 'た。'},
|
||||
]);
|
||||
});
|
||||
|
||||
test("an example's readings from Tatoeba go over its sentence, the word "
|
||||
'still in bold; those no one reviewed are marked so', () {
|
||||
final word = DictionaryWord.fromEntry(
|
||||
ref: (source: 'jmdict', id: '1358280'),
|
||||
headword: '食べる',
|
||||
reading: 'たべる',
|
||||
labels: const [],
|
||||
entry:
|
||||
'{"senses":[{"glosses":["to eat"],"examples":['
|
||||
'{"text":"食べ","ja":"果物を食べた。","en":"I ate fruit.",'
|
||||
'"ruby":[[0,1,"くだ"],[1,2,"もの"],[3,4,"た"]],'
|
||||
'"rubyUnreviewed":true},'
|
||||
'{"text":"食べ","ja":"食べた。","ruby":[[0,1,"た"]]}]}]}',
|
||||
);
|
||||
|
||||
final [unreviewed, reviewed] = word.card.hints;
|
||||
final document = unreviewed.editorJson['document']! as Map;
|
||||
final [sentence, english] = document['children']! as List;
|
||||
expect(((sentence as Map)['data']! as Map)['delta'], [
|
||||
{
|
||||
'insert': '果',
|
||||
'attributes': {'ruby': 'くだ'},
|
||||
},
|
||||
{
|
||||
'insert': '物',
|
||||
'attributes': {'ruby': 'もの'},
|
||||
},
|
||||
{'insert': 'を'},
|
||||
{
|
||||
'insert': '食',
|
||||
'attributes': {'bold': true, 'ruby': 'た'},
|
||||
},
|
||||
{
|
||||
'insert': 'べ',
|
||||
'attributes': {'bold': true},
|
||||
},
|
||||
{'insert': 'た。'},
|
||||
]);
|
||||
expect(readingsUnreviewed(unreviewed), isTrue);
|
||||
expect(((english as Map)['data']! as Map)['delta'], [
|
||||
{'insert': 'I ate fruit.'},
|
||||
]);
|
||||
expect(readingsUnreviewed(reviewed), isFalse);
|
||||
expect(hasReadings(reviewed), isTrue);
|
||||
expect(hasReadings(RichContent.plain('食べた。')), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('the repository', () {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_editor/halkyon_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/ruby.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/card/widget/card_extras.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/composer/card_composer_screen.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/dictionary.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/geo_map/data/geo_map_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/geo_map/widget/region_map.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/kanji/data/stroke_repository.dart';
|
||||
|
|
@ -33,6 +35,7 @@ import '../../../support/fake_studyset_gateway.dart';
|
|||
import '../../../support/fake_tag_gateway.dart';
|
||||
import '../../../support/loaded_maps.dart';
|
||||
import '../../../support/loaded_strokes.dart';
|
||||
import '../../../support/memory_device_settings_store.dart';
|
||||
import '../../../support/pump_app.dart';
|
||||
import '../../../support/test_dependencies.dart';
|
||||
|
||||
|
|
@ -82,6 +85,7 @@ void main() {
|
|||
StrokeRepository? strokes,
|
||||
String? frontLanguage,
|
||||
double width = 1200,
|
||||
MemoryDeviceSettingsStore? deviceSettings,
|
||||
}) async {
|
||||
tester.view.physicalSize = Size(width, 1600);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
@ -106,6 +110,7 @@ void main() {
|
|||
reviewGateway: reviews,
|
||||
maps: maps,
|
||||
strokes: strokes,
|
||||
deviceSettingsStore: deviceSettings,
|
||||
),
|
||||
location: RoutePaths.study('japanese'),
|
||||
);
|
||||
|
|
@ -411,6 +416,60 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
testWidgets("a hint's readings show over its sentence until the "
|
||||
"sheet's switch hides them, which the device keeps; those no one "
|
||||
'reviewed say so, and hints without readings have no switch', (
|
||||
tester,
|
||||
) async {
|
||||
final word = DictionaryWord.fromEntry(
|
||||
ref: (source: 'jmdict', id: '1358280'),
|
||||
headword: '食べる',
|
||||
reading: 'たべる',
|
||||
labels: const [],
|
||||
entry:
|
||||
'{"senses":[{"glosses":["to eat"],"examples":['
|
||||
'{"text":"食べ","ja":"果物を食べた。","en":"I ate fruit.",'
|
||||
'"ruby":[[0,2,"くだもの"],[3,4,"た"]],"rubyUnreviewed":true}]}]}',
|
||||
);
|
||||
await cards.create(
|
||||
'japanese',
|
||||
CardContent(
|
||||
front: paragraph('食べる'),
|
||||
back: paragraph('to eat'),
|
||||
hints: [...word.card.hints, paragraph('a verb')],
|
||||
),
|
||||
);
|
||||
final store = MemoryDeviceSettingsStore();
|
||||
await study(tester, deviceSettings: store);
|
||||
|
||||
await tester.tap(find.text('Hints'));
|
||||
await tester.pumpAndSettle();
|
||||
RubyText ruby(String reading) => tester.widget<RubyText>(
|
||||
find.byWidgetPredicate(
|
||||
(widget) => widget is RubyText && widget.reading == reading,
|
||||
),
|
||||
);
|
||||
expect(ruby('くだもの').hidden, isFalse);
|
||||
expect(ruby('た').hidden, isFalse);
|
||||
expect(
|
||||
find.text('Some readings were made automatically and not reviewed.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.tap(find.widgetWithText(SwitchListTile, 'Furigana'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(ruby('くだもの').hidden, isTrue);
|
||||
expect(
|
||||
find.text('Some readings were made automatically and not reviewed.'),
|
||||
findsNothing,
|
||||
);
|
||||
expect(store.writes.last.hintReadings, isFalse);
|
||||
|
||||
await tester.tap(find.byTooltip('Next hint'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.widgetWithText(SwitchListTile, 'Furigana'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('with tags chosen and reviews due beyond them, asks before '
|
||||
'learning new cards; reviewing first studies those', (tester) async {
|
||||
final verb = await tags.create('verb');
|
||||
|
|
|
|||
|
|
@ -133,6 +133,8 @@ final sampleWords = [
|
|||
form: '会いましょう',
|
||||
sentence: 'また会いましょう。',
|
||||
translation: "Let's meet again.",
|
||||
ruby: const [],
|
||||
rubyUnreviewed: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in a new issue