fix(app): a path import that fails says why in the app's language
The importer said what was wrong with a file in English sentences built in the data layer, the last English left in a German app. It now names the problem, the step it is in and what the problem names (a field, a type, a number), and the studyset screen puts it into words: "Step 2: unknown "type": quiz." or "Schritt 2: unbekannter „type“: quiz.". The format's own field names stay as they are in the file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
918e10bb2b
commit
1921043925
11 changed files with 670 additions and 68 deletions
|
|
@ -832,5 +832,32 @@
|
|||
"homeWidgetPomodoro": "Pomodoro-Timer",
|
||||
"pomodoroAlerts": "Pomodoro-Meldungen",
|
||||
"pomodoroUntil": "Bis {time}",
|
||||
"pomodoroLeft": "Noch {left}"
|
||||
"pomodoroLeft": "Noch {left}",
|
||||
"pathImportAtStep": "Schritt {step}: {problem}",
|
||||
"pathImportNotJson": "Das ist kein JSON: {detail}",
|
||||
"pathImportNoPath": "ein „path“-Objekt fehlt.",
|
||||
"pathImportNoTitle": "der Pfad hat keinen „title“.",
|
||||
"pathImportDescriptionNotText": "„description“ des Pfads ist kein Text.",
|
||||
"pathImportNotAnObject": "kein Objekt.",
|
||||
"pathImportUnknownStepType": "unbekannter „type“: {detail}.",
|
||||
"pathImportRightOptionMissing": "die richtige Option {detail} fehlt unter den Optionen.",
|
||||
"pathImportRightOptionsMissing": "eine genannte richtige Option fehlt unter den Optionen.",
|
||||
"pathImportNoPairing": "kein „correct_pairing“.",
|
||||
"pathImportPairingTargetMissing": "Elemente werden einem fehlenden Ziel zugeordnet: {detail}.",
|
||||
"pathImportPairingIncomplete": "jedes Element braucht genau ein Ziel.",
|
||||
"pathImportNoPassage": "keine „passage“.",
|
||||
"pathImportBlanksMismatch": "für jedes ___ in der Passage braucht es ein Wort in „blanks“.",
|
||||
"pathImportBlockWithoutType": "ein Block ohne „type“.",
|
||||
"pathImportMalformedLevel": "eine Überschrift mit fehlerhafter Ebene.",
|
||||
"pathImportMalformedListItem": "ein fehlerhafter Listenpunkt.",
|
||||
"pathImportMalformedMath": "ein fehlerhafter Formelblock.",
|
||||
"pathImportMalformedCode": "ein fehlerhafter Codeblock.",
|
||||
"pathImportUnknownBlock": "ein unbekannter Block: {detail}.",
|
||||
"pathImportTextlessBlock": "ein „{detail}“ ohne „text“ oder „runs“.",
|
||||
"pathImportMalformedRun": "ein fehlerhafter Textabschnitt.",
|
||||
"pathImportListExpected": "eine Liste wurde erwartet.",
|
||||
"pathImportNotText": "„{detail}“ muss Text sein.",
|
||||
"pathImportTooFew": "es braucht mindestens zwei „{detail}“.",
|
||||
"pathImportMalformedNumber": "eine fehlerhafte Zahl.",
|
||||
"pathImportTitleNotText": "ein Titel muss Text sein."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -997,5 +997,41 @@
|
|||
"pomodoroUntil": "Until {time}",
|
||||
"@pomodoroUntil": {"placeholders": {"time": {"type": "String"}}},
|
||||
"pomodoroLeft": "{left} left",
|
||||
"@pomodoroLeft": {"placeholders": {"left": {"type": "String"}}}
|
||||
"@pomodoroLeft": {"placeholders": {"left": {"type": "String"}}},
|
||||
"pathImportAtStep": "Step {step}: {problem}",
|
||||
"@pathImportAtStep": {"placeholders": {"step": {"type": "int"}, "problem": {"type": "String"}}},
|
||||
"pathImportNotJson": "This is no JSON: {detail}",
|
||||
"@pathImportNotJson": {"placeholders": {"detail": {"type": "String"}}},
|
||||
"pathImportNoPath": "a \"path\" object is missing.",
|
||||
"pathImportNoTitle": "the path has no \"title\".",
|
||||
"pathImportDescriptionNotText": "the path's \"description\" is no text.",
|
||||
"pathImportNotAnObject": "no object.",
|
||||
"pathImportUnknownStepType": "unknown \"type\": {detail}.",
|
||||
"@pathImportUnknownStepType": {"placeholders": {"detail": {"type": "String"}}},
|
||||
"pathImportRightOptionMissing": "its right option {detail} is not among its options.",
|
||||
"@pathImportRightOptionMissing": {"placeholders": {"detail": {"type": "String"}}},
|
||||
"pathImportRightOptionsMissing": "it names a right option that is not among its options.",
|
||||
"pathImportNoPairing": "no \"correct_pairing\".",
|
||||
"pathImportPairingTargetMissing": "it pairs items with a target that isn't there: {detail}.",
|
||||
"@pathImportPairingTargetMissing": {"placeholders": {"detail": {"type": "String"}}},
|
||||
"pathImportPairingIncomplete": "every item must be paired with one target.",
|
||||
"pathImportNoPassage": "no \"passage\".",
|
||||
"pathImportBlanksMismatch": "it needs one word in \"blanks\" for each ___ in its passage.",
|
||||
"pathImportBlockWithoutType": "a block without \"type\".",
|
||||
"pathImportMalformedLevel": "a heading with a malformed level.",
|
||||
"pathImportMalformedListItem": "a malformed list item.",
|
||||
"pathImportMalformedMath": "a malformed math block.",
|
||||
"pathImportMalformedCode": "a malformed code block.",
|
||||
"pathImportUnknownBlock": "an unknown block: {detail}.",
|
||||
"@pathImportUnknownBlock": {"placeholders": {"detail": {"type": "String"}}},
|
||||
"pathImportTextlessBlock": "a {detail} without \"text\" or \"runs\".",
|
||||
"@pathImportTextlessBlock": {"placeholders": {"detail": {"type": "String"}}},
|
||||
"pathImportMalformedRun": "a malformed run of text.",
|
||||
"pathImportListExpected": "a list was expected.",
|
||||
"pathImportNotText": "\"{detail}\" must be text.",
|
||||
"@pathImportNotText": {"placeholders": {"detail": {"type": "String"}}},
|
||||
"pathImportTooFew": "it needs at least two \"{detail}\".",
|
||||
"@pathImportTooFew": {"placeholders": {"detail": {"type": "String"}}},
|
||||
"pathImportMalformedNumber": "a malformed number.",
|
||||
"pathImportTitleNotText": "a title must be text."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4988,6 +4988,168 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'{left} left'**
|
||||
String pomodoroLeft(String left);
|
||||
|
||||
/// No description provided for @pathImportAtStep.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Step {step}: {problem}'**
|
||||
String pathImportAtStep(int step, String problem);
|
||||
|
||||
/// No description provided for @pathImportNotJson.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This is no JSON: {detail}'**
|
||||
String pathImportNotJson(String detail);
|
||||
|
||||
/// No description provided for @pathImportNoPath.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'a \"path\" object is missing.'**
|
||||
String get pathImportNoPath;
|
||||
|
||||
/// No description provided for @pathImportNoTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'the path has no \"title\".'**
|
||||
String get pathImportNoTitle;
|
||||
|
||||
/// No description provided for @pathImportDescriptionNotText.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'the path\'s \"description\" is no text.'**
|
||||
String get pathImportDescriptionNotText;
|
||||
|
||||
/// No description provided for @pathImportNotAnObject.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'no object.'**
|
||||
String get pathImportNotAnObject;
|
||||
|
||||
/// No description provided for @pathImportUnknownStepType.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'unknown \"type\": {detail}.'**
|
||||
String pathImportUnknownStepType(String detail);
|
||||
|
||||
/// No description provided for @pathImportRightOptionMissing.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'its right option {detail} is not among its options.'**
|
||||
String pathImportRightOptionMissing(String detail);
|
||||
|
||||
/// No description provided for @pathImportRightOptionsMissing.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'it names a right option that is not among its options.'**
|
||||
String get pathImportRightOptionsMissing;
|
||||
|
||||
/// No description provided for @pathImportNoPairing.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'no \"correct_pairing\".'**
|
||||
String get pathImportNoPairing;
|
||||
|
||||
/// No description provided for @pathImportPairingTargetMissing.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'it pairs items with a target that isn\'t there: {detail}.'**
|
||||
String pathImportPairingTargetMissing(String detail);
|
||||
|
||||
/// No description provided for @pathImportPairingIncomplete.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'every item must be paired with one target.'**
|
||||
String get pathImportPairingIncomplete;
|
||||
|
||||
/// No description provided for @pathImportNoPassage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'no \"passage\".'**
|
||||
String get pathImportNoPassage;
|
||||
|
||||
/// No description provided for @pathImportBlanksMismatch.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'it needs one word in \"blanks\" for each ___ in its passage.'**
|
||||
String get pathImportBlanksMismatch;
|
||||
|
||||
/// No description provided for @pathImportBlockWithoutType.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'a block without \"type\".'**
|
||||
String get pathImportBlockWithoutType;
|
||||
|
||||
/// No description provided for @pathImportMalformedLevel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'a heading with a malformed level.'**
|
||||
String get pathImportMalformedLevel;
|
||||
|
||||
/// No description provided for @pathImportMalformedListItem.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'a malformed list item.'**
|
||||
String get pathImportMalformedListItem;
|
||||
|
||||
/// No description provided for @pathImportMalformedMath.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'a malformed math block.'**
|
||||
String get pathImportMalformedMath;
|
||||
|
||||
/// No description provided for @pathImportMalformedCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'a malformed code block.'**
|
||||
String get pathImportMalformedCode;
|
||||
|
||||
/// No description provided for @pathImportUnknownBlock.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'an unknown block: {detail}.'**
|
||||
String pathImportUnknownBlock(String detail);
|
||||
|
||||
/// No description provided for @pathImportTextlessBlock.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'a {detail} without \"text\" or \"runs\".'**
|
||||
String pathImportTextlessBlock(String detail);
|
||||
|
||||
/// No description provided for @pathImportMalformedRun.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'a malformed run of text.'**
|
||||
String get pathImportMalformedRun;
|
||||
|
||||
/// No description provided for @pathImportListExpected.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'a list was expected.'**
|
||||
String get pathImportListExpected;
|
||||
|
||||
/// No description provided for @pathImportNotText.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'\"{detail}\" must be text.'**
|
||||
String pathImportNotText(String detail);
|
||||
|
||||
/// No description provided for @pathImportTooFew.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'it needs at least two \"{detail}\".'**
|
||||
String pathImportTooFew(String detail);
|
||||
|
||||
/// No description provided for @pathImportMalformedNumber.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'a malformed number.'**
|
||||
String get pathImportMalformedNumber;
|
||||
|
||||
/// No description provided for @pathImportTitleNotText.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'a title must be text.'**
|
||||
String get pathImportTitleNotText;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -3017,4 +3017,108 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String pomodoroLeft(String left) {
|
||||
return 'Noch $left';
|
||||
}
|
||||
|
||||
@override
|
||||
String pathImportAtStep(int step, String problem) {
|
||||
return 'Schritt $step: $problem';
|
||||
}
|
||||
|
||||
@override
|
||||
String pathImportNotJson(String detail) {
|
||||
return 'Das ist kein JSON: $detail';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathImportNoPath => 'ein „path“-Objekt fehlt.';
|
||||
|
||||
@override
|
||||
String get pathImportNoTitle => 'der Pfad hat keinen „title“.';
|
||||
|
||||
@override
|
||||
String get pathImportDescriptionNotText =>
|
||||
'„description“ des Pfads ist kein Text.';
|
||||
|
||||
@override
|
||||
String get pathImportNotAnObject => 'kein Objekt.';
|
||||
|
||||
@override
|
||||
String pathImportUnknownStepType(String detail) {
|
||||
return 'unbekannter „type“: $detail.';
|
||||
}
|
||||
|
||||
@override
|
||||
String pathImportRightOptionMissing(String detail) {
|
||||
return 'die richtige Option $detail fehlt unter den Optionen.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathImportRightOptionsMissing =>
|
||||
'eine genannte richtige Option fehlt unter den Optionen.';
|
||||
|
||||
@override
|
||||
String get pathImportNoPairing => 'kein „correct_pairing“.';
|
||||
|
||||
@override
|
||||
String pathImportPairingTargetMissing(String detail) {
|
||||
return 'Elemente werden einem fehlenden Ziel zugeordnet: $detail.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathImportPairingIncomplete =>
|
||||
'jedes Element braucht genau ein Ziel.';
|
||||
|
||||
@override
|
||||
String get pathImportNoPassage => 'keine „passage“.';
|
||||
|
||||
@override
|
||||
String get pathImportBlanksMismatch =>
|
||||
'für jedes ___ in der Passage braucht es ein Wort in „blanks“.';
|
||||
|
||||
@override
|
||||
String get pathImportBlockWithoutType => 'ein Block ohne „type“.';
|
||||
|
||||
@override
|
||||
String get pathImportMalformedLevel =>
|
||||
'eine Überschrift mit fehlerhafter Ebene.';
|
||||
|
||||
@override
|
||||
String get pathImportMalformedListItem => 'ein fehlerhafter Listenpunkt.';
|
||||
|
||||
@override
|
||||
String get pathImportMalformedMath => 'ein fehlerhafter Formelblock.';
|
||||
|
||||
@override
|
||||
String get pathImportMalformedCode => 'ein fehlerhafter Codeblock.';
|
||||
|
||||
@override
|
||||
String pathImportUnknownBlock(String detail) {
|
||||
return 'ein unbekannter Block: $detail.';
|
||||
}
|
||||
|
||||
@override
|
||||
String pathImportTextlessBlock(String detail) {
|
||||
return 'ein „$detail“ ohne „text“ oder „runs“.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathImportMalformedRun => 'ein fehlerhafter Textabschnitt.';
|
||||
|
||||
@override
|
||||
String get pathImportListExpected => 'eine Liste wurde erwartet.';
|
||||
|
||||
@override
|
||||
String pathImportNotText(String detail) {
|
||||
return '„$detail“ muss Text sein.';
|
||||
}
|
||||
|
||||
@override
|
||||
String pathImportTooFew(String detail) {
|
||||
return 'es braucht mindestens zwei „$detail“.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathImportMalformedNumber => 'eine fehlerhafte Zahl.';
|
||||
|
||||
@override
|
||||
String get pathImportTitleNotText => 'ein Titel muss Text sein.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2987,6 +2987,109 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String pomodoroLeft(String left) {
|
||||
return '$left left';
|
||||
}
|
||||
|
||||
@override
|
||||
String pathImportAtStep(int step, String problem) {
|
||||
return 'Step $step: $problem';
|
||||
}
|
||||
|
||||
@override
|
||||
String pathImportNotJson(String detail) {
|
||||
return 'This is no JSON: $detail';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathImportNoPath => 'a \"path\" object is missing.';
|
||||
|
||||
@override
|
||||
String get pathImportNoTitle => 'the path has no \"title\".';
|
||||
|
||||
@override
|
||||
String get pathImportDescriptionNotText =>
|
||||
'the path\'s \"description\" is no text.';
|
||||
|
||||
@override
|
||||
String get pathImportNotAnObject => 'no object.';
|
||||
|
||||
@override
|
||||
String pathImportUnknownStepType(String detail) {
|
||||
return 'unknown \"type\": $detail.';
|
||||
}
|
||||
|
||||
@override
|
||||
String pathImportRightOptionMissing(String detail) {
|
||||
return 'its right option $detail is not among its options.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathImportRightOptionsMissing =>
|
||||
'it names a right option that is not among its options.';
|
||||
|
||||
@override
|
||||
String get pathImportNoPairing => 'no \"correct_pairing\".';
|
||||
|
||||
@override
|
||||
String pathImportPairingTargetMissing(String detail) {
|
||||
return 'it pairs items with a target that isn\'t there: $detail.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathImportPairingIncomplete =>
|
||||
'every item must be paired with one target.';
|
||||
|
||||
@override
|
||||
String get pathImportNoPassage => 'no \"passage\".';
|
||||
|
||||
@override
|
||||
String get pathImportBlanksMismatch =>
|
||||
'it needs one word in \"blanks\" for each ___ in its passage.';
|
||||
|
||||
@override
|
||||
String get pathImportBlockWithoutType => 'a block without \"type\".';
|
||||
|
||||
@override
|
||||
String get pathImportMalformedLevel => 'a heading with a malformed level.';
|
||||
|
||||
@override
|
||||
String get pathImportMalformedListItem => 'a malformed list item.';
|
||||
|
||||
@override
|
||||
String get pathImportMalformedMath => 'a malformed math block.';
|
||||
|
||||
@override
|
||||
String get pathImportMalformedCode => 'a malformed code block.';
|
||||
|
||||
@override
|
||||
String pathImportUnknownBlock(String detail) {
|
||||
return 'an unknown block: $detail.';
|
||||
}
|
||||
|
||||
@override
|
||||
String pathImportTextlessBlock(String detail) {
|
||||
return 'a $detail without \"text\" or \"runs\".';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathImportMalformedRun => 'a malformed run of text.';
|
||||
|
||||
@override
|
||||
String get pathImportListExpected => 'a list was expected.';
|
||||
|
||||
@override
|
||||
String pathImportNotText(String detail) {
|
||||
return '\"$detail\" must be text.';
|
||||
}
|
||||
|
||||
@override
|
||||
String pathImportTooFew(String detail) {
|
||||
return 'it needs at least two \"$detail\".';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathImportMalformedNumber => 'a malformed number.';
|
||||
|
||||
@override
|
||||
String get pathImportTitleNotText => 'a title must be text.';
|
||||
}
|
||||
|
||||
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class PathListBloc(
|
|||
title: '',
|
||||
steps: 0,
|
||||
skippedBlocks: 0,
|
||||
failure: error.message,
|
||||
failure: error,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
@ -154,7 +154,7 @@ typedef ImportResult = ({
|
|||
String title,
|
||||
int steps,
|
||||
int skippedBlocks,
|
||||
String? failure,
|
||||
PathImportException? failure,
|
||||
});
|
||||
|
||||
/// A studyset's paths, as the list shows them.
|
||||
|
|
|
|||
|
|
@ -35,13 +35,52 @@ import 'package:halkyon_learn_flutter/src/feature/learning_path/model/step_conte
|
|||
// "image" and "lottie" are left out, keeping an "alt" caption: their files
|
||||
// would have to be fetched from the web, which the app never does.
|
||||
|
||||
/// A file in the import format that couldn't be read: [message] says why.
|
||||
/// What is wrong with a file in the import format.
|
||||
enum PathImportProblem() {
|
||||
notJson,
|
||||
noPath,
|
||||
noTitle,
|
||||
descriptionNotText,
|
||||
notAnObject,
|
||||
unknownStepType,
|
||||
rightOptionMissing,
|
||||
rightOptionsMissing,
|
||||
noPairing,
|
||||
pairingTargetMissing,
|
||||
pairingIncomplete,
|
||||
noPassage,
|
||||
blanksMismatch,
|
||||
blockWithoutType,
|
||||
malformedLevel,
|
||||
malformedListItem,
|
||||
malformedMath,
|
||||
malformedCode,
|
||||
unknownBlock,
|
||||
textlessBlock,
|
||||
malformedRun,
|
||||
listExpected,
|
||||
notText,
|
||||
tooFew,
|
||||
malformedNumber,
|
||||
titleNotText,
|
||||
}
|
||||
|
||||
/// A file in the import format that couldn't be read: the [problem], the
|
||||
/// [step] it is in, and what it names ([detail]: a field, a type, a
|
||||
/// number); the app says it in its language.
|
||||
final class const PathImportException(
|
||||
/// What is wrong, and where.
|
||||
final String message,
|
||||
) implements Exception {
|
||||
/// What is wrong.
|
||||
final PathImportProblem problem, {
|
||||
|
||||
/// The step it is in, counting from 1; null for the path itself.
|
||||
final int? step,
|
||||
|
||||
/// What the problem names, as written in the file.
|
||||
final String detail = '',
|
||||
}) implements Exception {
|
||||
@override
|
||||
String toString() => 'PathImportException: $message';
|
||||
String toString() =>
|
||||
'PathImportException: ${problem.name} in ${step ?? 'the path'}: $detail';
|
||||
}
|
||||
|
||||
/// A path read from the import format, not stored yet.
|
||||
|
|
@ -81,33 +120,33 @@ ParsedPath parsePathImport(String source) {
|
|||
try {
|
||||
json = jsonDecode(source);
|
||||
} on FormatException catch (error) {
|
||||
throw PathImportException('This is no JSON: ${error.message}.');
|
||||
throw PathImportException(PathImportProblem.notJson, detail: error.message);
|
||||
}
|
||||
final path = switch (json) {
|
||||
{'path': final Map<String, Object?> path} => path,
|
||||
_ => throw const PathImportException('A "path" object is missing.'),
|
||||
_ => throw const PathImportException(PathImportProblem.noPath),
|
||||
};
|
||||
final title = switch (path['title']) {
|
||||
final String title when title.trim().isNotEmpty => title,
|
||||
_ => throw const PathImportException('The path has no "title".'),
|
||||
_ => throw const PathImportException(PathImportProblem.noTitle),
|
||||
};
|
||||
final blocks = _Blocks();
|
||||
final steps = [
|
||||
for (final (index, step) in _list(
|
||||
path['steps'],
|
||||
'the path\'s "steps"',
|
||||
null,
|
||||
).indexed)
|
||||
_step(step, index + 1, blocks),
|
||||
];
|
||||
return (
|
||||
content: LearningPathContent(
|
||||
title: title,
|
||||
secondaryTitle: _optionalString(path['secondary_title'], 'the path'),
|
||||
secondaryTitle: _optionalString(path['secondary_title'], null),
|
||||
description: switch (path['description']) {
|
||||
null => RichContent.empty(),
|
||||
final String text => RichContent.plain(text),
|
||||
_ => throw const PathImportException(
|
||||
'The path\'s "description" is no text.',
|
||||
PathImportProblem.descriptionNotText,
|
||||
),
|
||||
},
|
||||
).normalized,
|
||||
|
|
@ -121,10 +160,10 @@ ParsedPath parsePathImport(String source) {
|
|||
int number,
|
||||
_Blocks blocks,
|
||||
) {
|
||||
final where = 'step $number';
|
||||
final where = number;
|
||||
final step = switch (json) {
|
||||
final Map<String, Object?> step => step,
|
||||
_ => throw PathImportException('The $where is no object.'),
|
||||
_ => throw PathImportException(PathImportProblem.notAnObject, step: where),
|
||||
};
|
||||
final kind = switch (step['type']) {
|
||||
'info' => StepKind.info,
|
||||
|
|
@ -133,7 +172,9 @@ ParsedPath parsePathImport(String source) {
|
|||
'drag_and_drop' => StepKind.dragAndDrop,
|
||||
'fill_in_the_blank' => StepKind.fillInTheBlank,
|
||||
final type => throw PathImportException(
|
||||
'The $where has an unknown "type": $type.',
|
||||
PathImportProblem.unknownStepType,
|
||||
step: where,
|
||||
detail: '$type',
|
||||
),
|
||||
};
|
||||
final exercise = switch (kind) {
|
||||
|
|
@ -143,7 +184,9 @@ ParsedPath parsePathImport(String source) {
|
|||
final correct = _int(step['correct_option_index'], where);
|
||||
if (correct >= options.length) {
|
||||
throw PathImportException(
|
||||
"The $where's right option $correct is not among its options.",
|
||||
PathImportProblem.rightOptionMissing,
|
||||
step: where,
|
||||
detail: '$correct',
|
||||
);
|
||||
}
|
||||
return SingleChoice(options: options, correct: correct);
|
||||
|
|
@ -156,7 +199,8 @@ ParsedPath parsePathImport(String source) {
|
|||
};
|
||||
if (correct.any((index) => index >= options.length)) {
|
||||
throw PathImportException(
|
||||
'The $where names a right option that is not among its options.',
|
||||
PathImportProblem.rightOptionsMissing,
|
||||
step: where,
|
||||
);
|
||||
}
|
||||
return MultipleChoice(options: options, correct: correct);
|
||||
|
|
@ -168,14 +212,17 @@ ParsedPath parsePathImport(String source) {
|
|||
final pairing = switch (step['correct_pairing']) {
|
||||
final Map<String, Object?> pairing => pairing,
|
||||
_ => throw PathImportException(
|
||||
'The $where has no "correct_pairing".',
|
||||
PathImportProblem.noPairing,
|
||||
step: where,
|
||||
),
|
||||
};
|
||||
for (final MapEntry(key: target, value: placed) in pairing.entries) {
|
||||
final at = int.tryParse(target);
|
||||
if (at == null || at >= targets.length) {
|
||||
throw PathImportException(
|
||||
"The $where pairs items with a target that isn't there: $target.",
|
||||
PathImportProblem.pairingTargetMissing,
|
||||
step: where,
|
||||
detail: target,
|
||||
);
|
||||
}
|
||||
for (final item in _list(placed, where)) {
|
||||
|
|
@ -185,7 +232,8 @@ ParsedPath parsePathImport(String source) {
|
|||
if (targetOf.length != items.length ||
|
||||
targetOf.keys.any((item) => item >= items.length)) {
|
||||
throw PathImportException(
|
||||
'The $where must pair every item with one target.',
|
||||
PathImportProblem.pairingIncomplete,
|
||||
step: where,
|
||||
);
|
||||
}
|
||||
return DragAndDrop(
|
||||
|
|
@ -199,7 +247,10 @@ ParsedPath parsePathImport(String source) {
|
|||
StepKind.fillInTheBlank => () {
|
||||
final passage = switch (step['passage']) {
|
||||
final String passage => passage,
|
||||
_ => throw PathImportException('The $where has no "passage".'),
|
||||
_ => throw PathImportException(
|
||||
PathImportProblem.noPassage,
|
||||
step: where,
|
||||
),
|
||||
};
|
||||
final exercise = FillInTheBlank(
|
||||
passage: passage,
|
||||
|
|
@ -207,7 +258,8 @@ ParsedPath parsePathImport(String source) {
|
|||
);
|
||||
if (!exercise.isComplete) {
|
||||
throw PathImportException(
|
||||
'The $where needs one word in "blanks" for each ___ in its passage.',
|
||||
PathImportProblem.blanksMismatch,
|
||||
step: where,
|
||||
);
|
||||
}
|
||||
return exercise;
|
||||
|
|
@ -230,7 +282,7 @@ class _Blocks() {
|
|||
/// How many blocks were left out.
|
||||
int skipped = 0;
|
||||
|
||||
RichContent read(Object? json, String where) {
|
||||
RichContent read(Object? json, int? where) {
|
||||
if (json == null) {
|
||||
return RichContent.empty();
|
||||
}
|
||||
|
|
@ -245,10 +297,13 @@ class _Blocks() {
|
|||
}
|
||||
|
||||
/// The editor's nodes for [block]; null for a block left out.
|
||||
List<Map<String, Object?>>? _nodes(Object? block, String where) {
|
||||
List<Map<String, Object?>>? _nodes(Object? block, int? where) {
|
||||
final type = switch (block) {
|
||||
{'type': final String type} => type,
|
||||
_ => throw PathImportException('The $where has a block without "type".'),
|
||||
_ => throw PathImportException(
|
||||
PathImportProblem.blockWithoutType,
|
||||
step: where,
|
||||
),
|
||||
};
|
||||
final json = block as Map<String, Object?>;
|
||||
switch (type) {
|
||||
|
|
@ -259,7 +314,10 @@ class _Blocks() {
|
|||
final level = switch (json['level']) {
|
||||
final int level when level >= 1 && level <= 6 => level,
|
||||
null => 2,
|
||||
_ => throw PathImportException('The $where has a malformed level.'),
|
||||
_ => throw PathImportException(
|
||||
PathImportProblem.malformedLevel,
|
||||
step: where,
|
||||
),
|
||||
};
|
||||
return [
|
||||
{
|
||||
|
|
@ -280,7 +338,8 @@ class _Blocks() {
|
|||
final String text => _plain(text),
|
||||
{'runs': final Object? runs} => _runs(runs, where),
|
||||
_ => throw PathImportException(
|
||||
'The $where has a malformed list item.',
|
||||
PathImportProblem.malformedListItem,
|
||||
step: where,
|
||||
),
|
||||
},
|
||||
},
|
||||
|
|
@ -289,7 +348,10 @@ class _Blocks() {
|
|||
case 'math':
|
||||
final formula = json['formula'];
|
||||
if (formula is! String) {
|
||||
throw PathImportException('The $where has a malformed math block.');
|
||||
throw PathImportException(
|
||||
PathImportProblem.malformedMath,
|
||||
step: where,
|
||||
);
|
||||
}
|
||||
return [
|
||||
{
|
||||
|
|
@ -300,7 +362,10 @@ class _Blocks() {
|
|||
case 'code':
|
||||
final source = json['code'];
|
||||
if (source is! String) {
|
||||
throw PathImportException('The $where has a malformed code block.');
|
||||
throw PathImportException(
|
||||
PathImportProblem.malformedCode,
|
||||
step: where,
|
||||
);
|
||||
}
|
||||
return [
|
||||
{
|
||||
|
|
@ -334,14 +399,18 @@ class _Blocks() {
|
|||
_ => null,
|
||||
};
|
||||
default:
|
||||
throw PathImportException('The $where has an unknown block: $type.');
|
||||
throw PathImportException(
|
||||
PathImportProblem.unknownBlock,
|
||||
step: where,
|
||||
detail: type,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object?> _text(
|
||||
String type,
|
||||
Map<String, Object?> json,
|
||||
String where,
|
||||
int? where,
|
||||
) => {
|
||||
'type': type,
|
||||
'data': {
|
||||
|
|
@ -349,7 +418,9 @@ class _Blocks() {
|
|||
{'text': final String text} => _plain(text),
|
||||
{'runs': final Object? runs} => _runs(runs, where),
|
||||
_ => throw PathImportException(
|
||||
'The $where has a $type without "text" or "runs".',
|
||||
PathImportProblem.textlessBlock,
|
||||
step: where,
|
||||
detail: type,
|
||||
),
|
||||
},
|
||||
},
|
||||
|
|
@ -359,7 +430,7 @@ class _Blocks() {
|
|||
if (text.isNotEmpty) {'insert': text},
|
||||
];
|
||||
|
||||
List<Map<String, Object?>> _runs(Object? json, String where) => [
|
||||
List<Map<String, Object?>> _runs(Object? json, int? where) => [
|
||||
for (final run in _list(json, where))
|
||||
switch (run) {
|
||||
{'math': final String formula} => {
|
||||
|
|
@ -376,38 +447,52 @@ class _Blocks() {
|
|||
case final attributes when attributes.isNotEmpty)
|
||||
'attributes': attributes,
|
||||
},
|
||||
_ => throw PathImportException('The $where has a malformed run.'),
|
||||
_ => throw PathImportException(
|
||||
PathImportProblem.malformedRun,
|
||||
step: where,
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
List<Object?> _list(Object? json, String where) => switch (json) {
|
||||
List<Object?> _list(Object? json, int? where) => switch (json) {
|
||||
final List<Object?> list => list,
|
||||
null => const [],
|
||||
_ => throw PathImportException('In $where, a list was expected.'),
|
||||
_ => throw PathImportException(PathImportProblem.listExpected, step: where),
|
||||
};
|
||||
|
||||
List<String> _strings(Object? json, String where, String field) {
|
||||
List<String> _strings(Object? json, int? where, String field) {
|
||||
final strings = [
|
||||
for (final value in _list(json, where))
|
||||
switch (value) {
|
||||
final String text => text,
|
||||
_ => throw PathImportException('The $where\'s "$field" must be text.'),
|
||||
_ => throw PathImportException(
|
||||
PathImportProblem.notText,
|
||||
step: where,
|
||||
detail: field,
|
||||
),
|
||||
},
|
||||
];
|
||||
if (strings.length < 2 && field != 'blanks' && field != 'items') {
|
||||
throw PathImportException('The $where needs at least two "$field".');
|
||||
throw PathImportException(
|
||||
PathImportProblem.tooFew,
|
||||
step: where,
|
||||
detail: field,
|
||||
);
|
||||
}
|
||||
return strings;
|
||||
}
|
||||
|
||||
int _int(Object? json, String where) => switch (json) {
|
||||
int _int(Object? json, int? where) => switch (json) {
|
||||
final int number when number >= 0 => number,
|
||||
_ => throw PathImportException('The $where has a malformed number.'),
|
||||
_ => throw PathImportException(
|
||||
PathImportProblem.malformedNumber,
|
||||
step: where,
|
||||
),
|
||||
};
|
||||
|
||||
String? _optionalString(Object? json, String where) => switch (json) {
|
||||
String? _optionalString(Object? json, int? where) => switch (json) {
|
||||
null => null,
|
||||
final String text => text,
|
||||
_ => throw PathImportException('In $where, a title must be text.'),
|
||||
_ => throw PathImportException(PathImportProblem.titleNotText, step: where),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/path_import.dart';
|
||||
|
||||
/// What is wrong with an import file, in the app's language.
|
||||
extension PathImportTexts on AppLocalizations {
|
||||
/// What [error] says is wrong, with the step it is in.
|
||||
String pathImportProblemText(PathImportException error) {
|
||||
final detail = error.detail;
|
||||
final problem = switch (error.problem) {
|
||||
PathImportProblem.notJson => pathImportNotJson(detail),
|
||||
PathImportProblem.noPath => pathImportNoPath,
|
||||
PathImportProblem.noTitle => pathImportNoTitle,
|
||||
PathImportProblem.descriptionNotText => pathImportDescriptionNotText,
|
||||
PathImportProblem.notAnObject => pathImportNotAnObject,
|
||||
PathImportProblem.unknownStepType => pathImportUnknownStepType(detail),
|
||||
PathImportProblem.rightOptionMissing => pathImportRightOptionMissing(
|
||||
detail,
|
||||
),
|
||||
PathImportProblem.rightOptionsMissing => pathImportRightOptionsMissing,
|
||||
PathImportProblem.noPairing => pathImportNoPairing,
|
||||
PathImportProblem.pairingTargetMissing => pathImportPairingTargetMissing(
|
||||
detail,
|
||||
),
|
||||
PathImportProblem.pairingIncomplete => pathImportPairingIncomplete,
|
||||
PathImportProblem.noPassage => pathImportNoPassage,
|
||||
PathImportProblem.blanksMismatch => pathImportBlanksMismatch,
|
||||
PathImportProblem.blockWithoutType => pathImportBlockWithoutType,
|
||||
PathImportProblem.malformedLevel => pathImportMalformedLevel,
|
||||
PathImportProblem.malformedListItem => pathImportMalformedListItem,
|
||||
PathImportProblem.malformedMath => pathImportMalformedMath,
|
||||
PathImportProblem.malformedCode => pathImportMalformedCode,
|
||||
PathImportProblem.unknownBlock => pathImportUnknownBlock(detail),
|
||||
PathImportProblem.textlessBlock => pathImportTextlessBlock(detail),
|
||||
PathImportProblem.malformedRun => pathImportMalformedRun,
|
||||
PathImportProblem.listExpected => pathImportListExpected,
|
||||
PathImportProblem.notText => pathImportNotText(detail),
|
||||
PathImportProblem.tooFew => pathImportTooFew(detail),
|
||||
PathImportProblem.malformedNumber => pathImportMalformedNumber,
|
||||
PathImportProblem.titleNotText => pathImportTitleNotText,
|
||||
};
|
||||
final sentence = switch (error.step) {
|
||||
final step? => pathImportAtStep(step, problem),
|
||||
null => problem,
|
||||
};
|
||||
// The problems read as the rest of a sentence; standing alone, the
|
||||
// first letter goes up.
|
||||
return sentence.isEmpty
|
||||
? sentence
|
||||
: sentence[0].toUpperCase() + sentence.substring(1);
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import 'package:halkyon_learn_flutter/src/feature/anki/data/anki_importer.dart';
|
|||
import 'package:halkyon_learn_flutter/src/feature/anki/widget/anki_import_sheet.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/card_list.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/bloc/path_list_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/widget/path_import_texts.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/widget/path_list.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/note/widget/note_list.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/bloc/pdf_list_bloc.dart';
|
||||
|
|
@ -120,7 +121,9 @@ class const StudysetScreen({
|
|||
context.read<ContentTabCubit>().show(StudysetTab.paths);
|
||||
}
|
||||
final message = switch (result.failure) {
|
||||
final failure? => l10n.pathImportFailed(failure),
|
||||
final failure? => l10n.pathImportFailed(
|
||||
l10n.pathImportProblemText(failure),
|
||||
),
|
||||
null => [
|
||||
l10n.pathImported(result.title, result.steps),
|
||||
if (result.skippedBlocks > 0)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart';
|
|||
import 'package:halkyon_learn_flutter/src/feature/learning_path/bloc/path_list_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_deletions.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/path_import.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/path_progress_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/exercise.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/learning_path.dart';
|
||||
|
|
@ -116,7 +117,7 @@ void main() {
|
|||
|
||||
bloc.add(const PathListEventImportRequested('{'));
|
||||
final failed = await until((s) => s.importResult?.failure != null);
|
||||
expect(failed.importResult!.failure, contains('JSON'));
|
||||
expect(failed.importResult!.failure?.problem, PathImportProblem.notJson);
|
||||
});
|
||||
|
||||
test('creating a path hands back its id, and lists it', () async {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/content_blocks.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/learning_path_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/path_import.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/model/exercise.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/widget/path_import_texts.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
import '../../../support/fake_learning_path_gateway.dart';
|
||||
|
||||
|
|
@ -158,49 +161,76 @@ void main() {
|
|||
});
|
||||
|
||||
group('refuses, saying why,', () {
|
||||
void refuses(String source, String reason) => expect(
|
||||
() => parsePathImport(source),
|
||||
throwsA(
|
||||
isA<PathImportException>().having(
|
||||
(e) => e.message,
|
||||
'message',
|
||||
contains(reason),
|
||||
),
|
||||
),
|
||||
);
|
||||
void refuses(String source, PathImportProblem problem, {int? step}) =>
|
||||
expect(
|
||||
() => parsePathImport(source),
|
||||
throwsA(
|
||||
isA<PathImportException>()
|
||||
.having((e) => e.problem, 'problem', problem)
|
||||
.having((e) => e.step, 'step', step),
|
||||
),
|
||||
);
|
||||
|
||||
test('what is no JSON', () => refuses('{', 'JSON'));
|
||||
test('what is no JSON', () => refuses('{', PathImportProblem.notJson));
|
||||
|
||||
test('a path without a title', () {
|
||||
refuses('{"path": {"steps": []}}', 'title');
|
||||
refuses('{"path": {"steps": []}}', PathImportProblem.noTitle);
|
||||
});
|
||||
|
||||
test('a step of an unknown type', () {
|
||||
refuses(
|
||||
'{"path": {"title": "x", "steps": [{"title": "a", "type": "quiz"}]}}',
|
||||
'step 1',
|
||||
PathImportProblem.unknownStepType,
|
||||
step: 1,
|
||||
);
|
||||
});
|
||||
|
||||
test('a right option that is not there', () {
|
||||
refuses(
|
||||
'{"path": {"title": "x", "steps": [{"title": "a", '
|
||||
'"type": "single_choice", "options": ["a", "b"], '
|
||||
'"correct_option_index": 5}]}}',
|
||||
'step 1',
|
||||
'"type": "single_choice", "options": ["a", "b"], '
|
||||
'"correct_option_index": 5}]}}',
|
||||
PathImportProblem.rightOptionMissing,
|
||||
step: 1,
|
||||
);
|
||||
});
|
||||
|
||||
test('blanks that do not fit the gaps', () {
|
||||
refuses(
|
||||
'{"path": {"title": "x", "steps": [{"title": "a", '
|
||||
'"type": "fill_in_the_blank", "passage": "a ___ b ___", '
|
||||
'"blanks": ["x"]}]}}',
|
||||
'step 1',
|
||||
'"type": "fill_in_the_blank", "passage": "a ___ b ___", '
|
||||
'"blanks": ["x"]}]}}',
|
||||
PathImportProblem.blanksMismatch,
|
||||
step: 1,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("says what is wrong in the app's language", () {
|
||||
const atStep = PathImportException(
|
||||
PathImportProblem.unknownStepType,
|
||||
step: 2,
|
||||
detail: 'quiz',
|
||||
);
|
||||
const ofPath = PathImportException(PathImportProblem.noTitle);
|
||||
|
||||
final english = lookupAppLocalizations(const Locale('en', 'GB'));
|
||||
final german = lookupAppLocalizations(const Locale('de'));
|
||||
expect(
|
||||
english.pathImportProblemText(atStep),
|
||||
'Step 2: unknown "type": quiz.',
|
||||
);
|
||||
expect(english.pathImportProblemText(ofPath), 'The path has no "title".');
|
||||
expect(
|
||||
german.pathImportProblemText(atStep),
|
||||
'Schritt 2: unbekannter „type“: quiz.',
|
||||
);
|
||||
expect(
|
||||
german.pathImportProblemText(ofPath),
|
||||
'Der Pfad hat keinen „title“.',
|
||||
);
|
||||
});
|
||||
|
||||
test('imports a path with its steps into a studyset', () async {
|
||||
final paths = LearningPathRepository(FakeLearningPathGateway());
|
||||
addTearDown(paths.dispose);
|
||||
|
|
|
|||
Loading…
Reference in a new issue