diff --git a/.github/actions/install-qt-deps/action.yml b/.github/actions/install-qt-deps/action.yml index b26b204..3d90aed 100644 --- a/.github/actions/install-qt-deps/action.yml +++ b/.github/actions/install-qt-deps/action.yml @@ -15,6 +15,8 @@ runs: ccache \ qt6-base-dev \ qt6-base-private-dev \ + qt6-tools-dev \ + qt6-tools-dev-tools \ libgl1-mesa-dev \ libx11-dev \ libxrandr-dev \ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d9e9c4..ba465e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,61 @@ jobs: fi echo "✅ Code formatting check passed" + i18n-check: + name: Translation Completeness + runs-on: ubuntu-24.04 + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Install system dependencies + uses: ./.github/actions/install-qt-deps + + - name: Install Conan + run: | + pip3 install conan + conan profile detect --force + + - name: Install dependencies + run: conan install . --build=missing -s build_type=Release + + - name: Configure CMake + run: cmake --preset release + + - name: Run lupdate and check for drift + run: | + cmake --build --preset release --target sudoku_lupdate + if ! git diff --exit-code resources/translations/; then + echo "❌ Translation drift detected." + echo "Run 'cmake --build build/Release --target sudoku_lupdate' locally," + echo "translate any new entries" + echo "using Qt Linguist, and commit." + exit 1 + fi + # Check every non-English locale for unfinished entries. + # English is the source language — its entries are always 'unfinished' + # and Qt's QTranslator falls back to the text. + fail=0 + for ts in resources/translations/sudoku_*.ts; do + [ "$ts" = "resources/translations/sudoku_en.ts" ] && continue + if grep -q 'type="unfinished"' "$ts"; then + echo "❌ $ts has unfinished translations:" + grep 'type="unfinished"' "$ts" | head + fail=1 + fi + done + [ "$fail" -eq 1 ] && exit 1 + echo "✅ Translations are in sync and complete" + + - name: Validate fmt-style placeholders in translations + run: | + # Catch translator-introduced placeholder mismatches (e.g. dropping + # {0} from a "Score: {0}" source) that lupdate does not validate. + # See scripts/check_translation_placeholders.py for the rules. + python3 scripts/check_translation_placeholders.py + python3 scripts/tests/test_check_translation_placeholders.py + test-and-coverage: name: Test & Coverage runs-on: ubuntu-24.04 diff --git a/.github/workflows/packaging.yml b/.github/workflows/packaging.yml index acc0e77..3339103 100644 --- a/.github/workflows/packaging.yml +++ b/.github/workflows/packaging.yml @@ -60,7 +60,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y \ - qt6-base-dev libgl1-mesa-dev \ + qt6-base-dev qt6-tools-dev qt6-tools-dev-tools libgl1-mesa-dev \ ninja-build ccache \ libfuse2 diff --git a/.gitignore b/.gitignore index 37f3421..22cb2f4 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,6 @@ Sudoku-*-win64.exe .appimage-tools/ AppDir/ Sudoku-*.AppImage +# Python bytecode caches +__pycache__/ +*.pyc diff --git a/CMakeLists.txt b/CMakeLists.txt index 780ff7c..7f02562 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,8 +87,8 @@ find_package(ZLIB REQUIRED) find_package(libsodium REQUIRED) # Qt6 integration (system-installed) -find_package(Qt6 REQUIRED COMPONENTS Widgets Test) -qt_standard_project_setup() +find_package(Qt6 REQUIRED COMPONENTS Widgets Test LinguistTools) +qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES en de) # Testing framework (optional) find_package(Catch2 QUIET) @@ -178,12 +178,32 @@ set_target_properties(${PROJECT_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" ) -# Copy locale files to build output directory (next to executable) +# Qt Linguist translations: compile .ts -> .qm and copy alongside the executable. +# Refresh .ts files from source after editing tr() call sites: +# cmake --build build/Debug --target sudoku_lupdate +set(SUDOKU_TS_FILES + "${CMAKE_CURRENT_SOURCE_DIR}/resources/translations/sudoku_en.ts" + "${CMAKE_CURRENT_SOURCE_DIR}/resources/translations/sudoku_de.ts" +) +qt_add_lupdate(${PROJECT_NAME} + TS_FILES ${SUDOKU_TS_FILES} + SOURCES ${PROJECT_SOURCES} + OPTIONS + -tr-function-alias translate+=loc + -no-obsolete + -locations none +) +qt_add_lrelease(${PROJECT_NAME} + TS_FILES ${SUDOKU_TS_FILES} + QM_FILES_OUTPUT_VARIABLE SUDOKU_QM_FILES +) add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory - "${CMAKE_CURRENT_SOURCE_DIR}/resources/locales" - "$/locales" - COMMENT "Copying locale files to build directory" + COMMAND ${CMAKE_COMMAND} -E make_directory + "$/translations" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${SUDOKU_QM_FILES} + "$/translations/" + COMMENT "Copying Qt translation .qm files to build directory" ) # Testing @@ -197,7 +217,7 @@ endif() if(WIN32) # Windows: flat layout for NSIS installer install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ".") - install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/resources/locales" DESTINATION ".") + install(FILES ${SUDOKU_QM_FILES} DESTINATION "translations") # Qt runtime via windeployqt # QT6_DIR is set as an environment variable by the build scripts; read it with $ENV{} @@ -221,8 +241,8 @@ else() # Linux: FHS-compliant layout include(GNUInstallDirs) install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") - install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/resources/locales" - DESTINATION "${CMAKE_INSTALL_DATADIR}/sudoku") + install(FILES ${SUDOKU_QM_FILES} + DESTINATION "${CMAKE_INSTALL_DATADIR}/sudoku/translations") install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/resources/linux/org.sudoku_cpp.Sudoku.desktop" DESTINATION "${CMAKE_INSTALL_DATADIR}/applications") install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/resources/linux/org.sudoku_cpp.Sudoku.metainfo.xml" diff --git a/docs/TRANSLATIONS.md b/docs/TRANSLATIONS.md new file mode 100644 index 0000000..6dfbfac --- /dev/null +++ b/docs/TRANSLATIONS.md @@ -0,0 +1,154 @@ +# Translations + +This project uses Qt's `.ts` translation files at runtime (loaded via +`QTranslator`) and Qt's standard `lupdate` tool to extract translatable +strings from C++ source. + +## Source-code conventions + +Two free helper functions in `src/core/i18n_helpers.h` wrap Qt's translation +API so non-`QObject` code (Model, ViewModel, free functions) can stay +non-`QObject`: + +```cpp +#include "core/i18n_helpers.h" + +// Simple lookup — use anywhere: +std::string s = sudoku::core::loc("Sudoku", "Open"); + +// Format with positional args (fmt-style {0}, {1}): +std::string fmt = sudoku::core::locFormat( + sudoku::core::loc("Sudoku", "Score: {0}"), + score +); +``` + +`core::loc(context, source)` has the same signature as +`QCoreApplication::translate(context, source)` — Qt's `lupdate` is told +about this equivalence via `-tr-function-alias translate+=loc` in +[CMakeLists.txt](../CMakeLists.txt), so it extracts every `core::loc("Sudoku", "...")` +call site as a translatable string with context `Sudoku`. + +`core::locFormat` takes an already-translated string (typically the result +of `core::loc(...)`) plus format arguments. The split exists so the +literal source goes through `core::loc` where `lupdate` can see it. **The +first argument to `core::loc` must be the literal `"Sudoku"`** — every +call site uses the same context. + +## Files + +- **Source-of-truth `.ts` files**: `resources/translations/sudoku_.ts` + - One `Sudoku` for the whole app. + - English text is the message ID (no synthetic-key indirection). + - Currently shipping: `en`, `de`. +- **Compiled `.qm` files**: produced at build time by `qt_add_lrelease`. + - Linux install path: `${CMAKE_INSTALL_DATADIR}/sudoku/translations/`. + - Flatpak: `/translations/`. + - Windows: `translations/` subfolder next to the executable. + +## Translator workflow + +Open the `.ts` file in **Qt Linguist** (the GUI editor that ships with +Qt 6), translate any entry with empty `` or +`type="unfinished"`, save. No `lupdate` needed for translation work — +that runs on the developer side when source code adds new strings. + +```sh +# Linux: install Qt Linguist (GUI editor) +sudo dnf install qt6-linguist # Fedora +sudo apt install linguist-qt6 # Debian/Ubuntu + +# Open the German translation +linguist-qt6 resources/translations/sudoku_de.ts +``` + +After saving, rebuild — `qt_add_lrelease` compiles `.ts` → `.qm` +automatically and the next launch picks up the new translations. + +To submit a translation: open a PR with the `.ts` diff. The CI +`i18n-check` job verifies completeness (no `type="unfinished"` entries, +no drift between source and `.ts`). + +## Adding a new locale + +1. Copy `sudoku_de.ts` to `sudoku_.ts` (e.g. `sudoku_ru.ts`). +2. Update the `` attribute to the new language code. +3. Translate every `` element. +4. Add the language to `qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES ...)` + in [CMakeLists.txt](../CMakeLists.txt). +5. Add the language code to the `kLocales` map in + [src/view/main_window.cpp](../src/view/main_window.cpp) (Settings → Language dropdown). + +## Packager workflow + +Distribution packagers building from a release tarball don't need any Qt +translation tooling beyond what's already pulled in by `find_package(Qt6 ... LinguistTools)` +— `cmake --build` runs `lrelease` automatically and produces the `.qm` +files in the build directory. + +## Contributor workflow + +When you add a new user-visible string in C++ source via +`core::loc("Sudoku", "...")` or +`core::locFormat(core::loc("Sudoku", "..."), args...)`: + +1. Run `cmake --build build/Release --target sudoku_lupdate`. + This invokes `lupdate` and updates `sudoku_en.ts` / `sudoku_de.ts` + with the new entry, marked `type="unfinished"`. +2. Open `sudoku_de.ts` in Qt Linguist and translate the new entry (or + coordinate with a translator). +3. Commit both the source change and the `.ts` files in the same PR. + +CI fails if: + +- Running `lupdate` would change the `.ts` files (drift between source and + translations), **or** +- `sudoku_de.ts` contains any `` entries. + +The `i18n-check` GitHub Actions job at +[.github/workflows/ci.yml](../.github/workflows/ci.yml) enforces both. + +### A note on the source language + +`sudoku_en.ts` intentionally has every entry as +`` — English is the source +language and Qt's `QTranslator` falls back to the `` text when +the matching translation is empty. The CI's `unfinished` check runs only +against non-English locales (`sudoku_de.ts` and any future +`sudoku_.ts` files). + +### A note on removed call sites (`-no-obsolete`) + +`qt_add_lupdate` is configured with `-no-obsolete` in +[CMakeLists.txt](../CMakeLists.txt). When a `core::loc(...)` call site is +deleted from the source, the matching `` block is **stripped +from every `.ts` file** the next time `lupdate` runs — including +translations a contributor has invested time in. The CI drift check +catches the removal (the `.ts` diff fails `git diff --exit-code`), so +the removal lands deliberately rather than silently, but the +translation text is gone from the working tree once the change is +committed. If you decide later that the string should come back, recover +the translation via `git log -p resources/translations/sudoku_.ts` +and paste it manually — there is no `` +fallback the way default `lupdate` provides. + +The trade-off is intentional: keeping obsolete markers around indefinitely +clutters the file as the codebase evolves, and Qt Linguist surfaces them +under a separate "Obsolete" section that translators tend to ignore. If +you find yourself recovering translations from git history regularly, +consider dropping `-no-obsolete` from the `qt_add_lupdate` `OPTIONS` +block. + +## Local verification + +```sh +# Rebuild + run lupdate + diff +cmake --build build/Release --target sudoku_lupdate +git diff resources/translations/ + +# Check for unfinished German translations +grep 'type="unfinished"' resources/translations/sudoku_de.ts +``` + +Empty diff and zero unfinished entries means the source and translations +are in sync. diff --git a/resources/locales/de.yaml b/resources/locales/de.yaml deleted file mode 100644 index c6b41d5..0000000 --- a/resources/locales/de.yaml +++ /dev/null @@ -1,599 +0,0 @@ -locale: de -name: Deutsch - -strings: - # Anwendung - app.title: "Sudoku" - - # Menü - menu.game: "Spiel" - menu.new_game: "Neues Spiel" - menu.reset_puzzle: "Rätsel zurücksetzen" - menu.save: "Speichern" - menu.load: "Laden" - menu.statistics: "Statistiken" - menu.export_aggregate: "Gesamtstatistiken als CSV exportieren" - menu.export_sessions: "Spielsitzungen als CSV exportieren" - menu.exit: "Beenden" - menu.edit: "Bearbeiten" - menu.undo: "Rückgängig" - menu.redo: "Wiederherstellen" - menu.clear_cell: "Zelle leeren" - menu.help: "Hilfe" - menu.get_hint: "Hinweis anzeigen" - menu.about: "Über" - menu.training_mode: "Trainingsmodus" - menu.analyze_position: "Position analysieren" - menu.resume_game: "Spiel fortsetzen" - menu.settings: "Einstellungen..." - menu.third_party_licenses: "Drittanbieter-Lizenzen" - - # Werkzeugleiste - toolbar.new_game: "\u25b6 Neues Spiel" - toolbar.difficulty: "Schwierigkeit:" - toolbar.hints: "Hinweise:" - toolbar.rating: "Bewertung:" - toolbar.help_icon: "[?]" - - # Schwierigkeitsgrade - difficulty.easy: "Leicht" - difficulty.medium: "Mittel" - difficulty.hard: "Schwer" - difficulty.expert: "Experte" - difficulty.master: "Meister" - difficulty.unknown: "Unbekannt" - - # Aktionsschaltflächen - button.check_solution: "Lösung prüfen" - button.reset_puzzle: "Rätsel zurücksetzen" - button.undo: "Rückgängig" - button.redo: "Wiederherstellen" - button.undo_to_valid: "Bis gültig rückgängig" - button.auto_notes_on: "Auto-Notizen: EIN" - button.auto_notes_off: "Auto-Notizen: AUS" - button.fill_notes: "Notizen füllen" - button.clear_notes: "Notizen löschen" - button.undo_until_valid: "Bis gültig rückgängig" - - # Eingabemodi - mode.normal: "Normal" - mode.notes: "Notizen" - mode.color: "Farbe" - - # Trainingsmodus - training.title: "Trainingsmodus" - training.analyze_title: "Position analysieren" - training.select_technique: "Wähle eine Technik zum Üben:" - training.back_to_game: "Zurück zum Spiel" - training.group.foundations: "Grundlagen" - training.group.subset_basics: "Teilmengen-Grundlagen" - training.group.intersections: "Überschneidungen & Quartette" - training.group.basic_fish: "Basis-Fische & Flügel" - training.group.links: "Verbindungen & Rechtecke" - training.group.advanced_fish: "Fortgeschrittene Fische & Flügel" - training.group.finned_fish: "Fortgeschrittene Fische (Flosse)" - training.group.chains: "Ketten & Mengenlogik" - training.group.inference: "Schlussfolgerungsmotoren" - training.what_it_is: "Was ist das:" - training.what_to_look_for: "Worauf achten:" - training.start_exercises: "Übungen starten" - training.back: "Zurück" - training.difficulty_points: "{0} Schwierigkeitspunkte" - training.prerequisites: "Voraussetzungen: " - training.exercise_header: "Übung {0} / {1} - {2}" - training.color: "Farbe:" - training.submit: "Absenden" - training.hint: "Hinweis" - training.skip: "Überspringen" - training.quit_lesson: "Lektion beenden" - training.next_exercise: "Nächste Übung" - training.retry: "Wiederholen" - training.show_solution: "Lösung zeigen" - training.score: "Ergebnis: {0} / {1}" - training.correct: "Richtig!" - training.partially_correct: "Teilweise richtig" - training.incorrect: "Falsch" - training.lesson_complete: "Lektion abgeschlossen!" - training.try_again: "Nochmal versuchen" - training.pick_technique: "Technik wählen" - training.return_to_game: "Zurück zum Spiel" - training.technique: "Technik: {0}" - training.hints_used: "Hinweise verwendet: {0}" - training.mastery: "Beherrschung: {0}" - training.points_fmt: "{0} ({1} Pkt.)" - training.prereq_not_met: "Voraussetzungen nicht erfüllt" - training.recommended: "Empfohlene nächste Technik" - training.applicable: "Anwendbar an aktueller Position" - training.excellent: "Ausgezeichnet! Du beherrschst diese Technik." - training.good_progress: "Guter Fortschritt. Versuche es noch einmal für ein besseres Ergebnis." - training.keep_practicing: "Weiter üben! Sieh dir die Theorie an und versuche es noch einmal." - training.error_backtracking: "Backtracking kann nicht geübt werden — es ist keine logische Technik." - training.error_no_step: "Kein anwendbarer Schritt für diese Technik gefunden." - training.correct_continue: "Richtig! {0} Finde den nächsten." - training.feedback_correct: "Richtig! {0}" - training.feedback_partial: "Teilweise richtig. {0}" - training.feedback_incorrect: "Nicht ganz. {0}" - training.feedback_unknown: "Unbekanntes Ergebnis." - mastery.beginner: "Anfänger" - mastery.intermediate: "Fortgeschrittener" - mastery.proficient: "Geübt" - mastery.mastered: "Gemeistert" - - # Statusleiste - status.completed: "Abgeschlossen!" - status.playing: "Spielen" - status.ready: "Bereit" - status.press_f1: "F1 drücken für Menü" - status.in_progress: "In Bearbeitung" - - # Spielfeld - board.no_game_loaded: "Kein Spiel geladen. Starte ein neues Spiel!" - - # Dialoge — Neues Spiel - dialog.new_game: "Neues Spiel" - dialog.select_difficulty: "Schwierigkeit auswählen:" - dialog.start_game: "Spiel starten" - dialog.cancel: "Abbrechen" - dialog.new_game_confirm: "Neues Spiel mit Schwierigkeit {0} starten?\nDer aktuelle Fortschritt geht verloren." - - # Dialoge — Zurücksetzen - dialog.reset_puzzle: "Rätsel zurücksetzen" - dialog.reset_warning: "Aller Fortschritt bei diesem Rätsel geht verloren, einschließlich eingetragener Zahlen, Notizen und Hinweise. Der Timer wird neu gestartet." - dialog.reset: "Zurücksetzen" - - # Dialoge — Speichern - dialog.save_game: "Spiel speichern" - dialog.enter_save_name: "Speichername eingeben:" - dialog.save: "Speichern" - save.preview_title: "Aktuelles Spiel" - save.preview_difficulty: "Schwierigkeit" - save.preview_time: "Zeit" - save.preview_moves: "Züge" - save.preview_mistakes: "Fehler" - save.name_placeholder: "Speichername eingeben..." - save.name_empty: "Bitte einen Speichernamen eingeben." - save.overwrite_confirm: "Ein Spielstand mit dem Namen \"{0}\" existiert bereits. Überschreiben?" - - # Dialoge — Laden - dialog.load_game: "Spiel laden" - dialog.recent_saves: "Letzte Speicherstände:" - load.col_name: "Name" - load.col_difficulty: "Schwierigkeit" - load.col_date: "Zuletzt geändert" - load.col_time: "Spielzeit" - load.col_rating: "Bewertung" - - # Dialoge — Statistiken (parametrisiert mit fmt-Stil {0}) - dialog.statistics: "Statistiken" - dialog.close: "Schließen" - stats.games_played: "Gespielte Spiele: {0}" - stats.games_completed: "Abgeschlossene Spiele: {0}" - stats.completion_rate: "Abschlussrate: {0:.1f}%" - stats.best_time: "Beste Zeit: {0}" - stats.average_time: "Durchschnittliche Zeit: {0}" - stats.current_streak: "Aktuelle Serie: {0}" - stats.best_streak: "Beste Serie: {0}" - stats.time_na: "k. A." - stats.tab_overview: "Übersicht" - stats.tab_per_difficulty: "Nach Schwierigkeit" - stats.tab_recent_games: "Letzte Spiele" - stats.total_moves: "Züge gesamt" - stats.total_hints: "Hinweise gesamt" - stats.total_mistakes: "Fehler gesamt" - stats.total_time: "Gesamtspielzeit" - stats.row_played: "Gespielt" - stats.row_completed: "Abgeschlossen" - stats.row_best_time: "Beste Zeit" - stats.row_avg_time: "Ø Zeit" - stats.row_avg_rating: "Ø SE-Bewertung" - stats.col_date: "Datum" - stats.col_difficulty: "Schwierigkeit" - stats.col_time: "Zeit" - stats.col_rating: "Bewertung" - stats.col_moves: "Züge" - stats.col_mistakes: "Fehler" - - # Dialoge — Über - dialog.about: "Über" - about.sudoku_game: "Sudoku-Spiel" - about.built_with: "Erstellt mit:" - about.description: "Eine funktionsreiche Offline-Sudoku-Anwendung." - rating.format: "SE {0}" - - # Dialoge — Einstellungen - dialog.settings: "Einstellungen" - dialog.third_party_licenses: "Drittanbieter-Lizenzen" - settings.tab_gameplay: "Spielmechanik" - settings.tab_display: "Anzeige" - settings.max_hints: "Maximale Hinweise:" - settings.auto_save_interval: "Automatisches Speichern:" - settings.default_difficulty: "Standard-Schwierigkeit:" - settings.seconds_suffix: " Sekunden" - settings.highlight_conflicts: "Konflikte hervorheben" - settings.show_hints: "Hinweise anzeigen" - settings.collect_detailed_stats: "Detaillierte Spielstatistiken erfassen" - settings.encrypt_detailed_stats: "Sitzungsdaten verschlüsseln" - settings.encrypt_detailed_stats_tooltip: "Sitzungsdaten werden standardmäßig zum Schutz der Privatsphäre verschlüsselt. Deaktivieren, um die Rohdaten selbst einzusehen." - settings.tab_statistics: "Statistiken" - stats.delete_prompt: "Das Deaktivieren beendet die Aufzeichnung von Einzelspielstatistiken. Möchten Sie den vorhandenen Sitzungsverlauf löschen?" - stats.sessions_deleted: "Sitzungsverlauf gelöscht." - - # Dialoge — Rätsel-Schwierigkeit - dialog.puzzle_difficulty: "Rätsel-Schwierigkeit" - dialog.puzzle_rating: "Rätsel-Bewertung: SE {0}" - dialog.techniques_required: "Benötigte Lösungstechniken:" - dialog.no_technique_details: "Keine Technikdetails verfügbar." - - # Werkzeugleiste — Bewertungsformat - toolbar.rating_with_techniques: "SE {0} ({1} Techniken)" - toolbar.rating_simple: "SE {0}" - - # Tooltips — Bewertungsskala - tooltip.rating_scale: "Sudoku-Explainer-Bewertungsskala:" - tooltip.techniques_required: "Benötigte Techniken:" - tooltip.input_mode: "Eingabemodus (Leertaste zum Wechseln, N für Notizen)" - tooltip.place_digit: "{0} in ausgewählte Zelle setzen" - tooltip.eliminate_digit: "{0} aus ausgewählter Zelle eliminieren" - - # Toast-Benachrichtigungen - toast.game_saved: "Spiel erfolgreich gespeichert" - toast.aggregate_exported: "Gesamtstatistiken als CSV exportiert" - toast.sessions_exported: "Spielsitzungen als CSV exportiert" - toast.export_failed: "Export fehlgeschlagen: {0}" - toast.no_strategies: "Keine logischen Strategien an dieser Position gefunden." - - # Seitenleiste - sidebar.difficulty: "Schwierigkeit: {0}" - sidebar.rating: "Bewertung: {0}" - sidebar.language: "Sprache" - - # ViewModel — Fehlermeldungen - error.generate_puzzle: "Rätsel konnte nicht generiert werden" - error.load_game: "Spiel konnte nicht geladen werden" - error.no_active_game: "Kein aktives Spiel zum Speichern" - error.save_game: "Spiel konnte nicht gespeichert werden" - error.export_stats: "Statistiken konnten nicht exportiert werden" - error.export_aggregate: "Gesamtstatistiken konnten nicht exportiert werden" - error.export_sessions: "Spielsitzungen konnten nicht exportiert werden" - error.file_access: "Dateizugriffsfehler" - error.serialization: "Serialisierungsfehler" - error.unknown: "Unbekannter Fehler" - - # ViewModel — Statusmeldungen - status.no_valid_state: "Kein gültiger Zustand im Verlauf" - status.board_valid: "Spielfeld ist bereits gültig" - status.undone_to_valid: "Zum letzten gültigen Zustand zurückgesetzt" - status.puzzle_completed: "Rätsel gelöst in {0}:{1}! Neues Spiel gestartet." - status.solution_errors: "Lösung enthält Fehler. Weiter versuchen!" - - # ViewModel — Hinweismeldungen - hint.no_remaining: "Keine Hinweise übrig (0/10 verwendet)" - hint.select_cell: "Bitte wähle zuerst eine Zelle aus" - hint.cannot_reveal_given: "Kein Hinweis für vorgegebene Zellen möglich" - hint.cell_has_value: "Zelle hat bereits einen Wert" - hint.no_technique: "Keine logische Technik für dieses Rätsel gefunden" - hint.suggestion_place: "Vorschlag: Setze {0} bei R{1}C{2}" - - # ViewModel — Coaching-Hinweise - coaching.no_remaining: "Keine Coaching-Hinweise mehr verfügbar" - coaching.no_technique: "Keine logische Technik gefunden" - coaching.max_level: "Maximale Coaching-Stufe erreicht" - coaching.level_header: "Stufe {0}/3" - coaching.try_it: "Versuche diesen Schritt selbst anzuwenden, dann drücke Prüfen." - coaching.check_correct: "Richtig! Du hast alle {0}/{1} gefunden." - coaching.check_partial: "{0}/{1} richtig, {2} übersehen." - coaching.check_wrong: "Einige Aktionen waren falsch. {0}/{1} richtig, {2} falsch." - coaching.applied: "Schritt angewendet." - coaching.button_check: "Prüfen" - coaching.button_apply: "Anwenden" - coaching.try_it_label: "Probiere es!" - coaching.what_to_look_for: "Worauf achten: " - coaching.check_zero: "0/{0} richtig — versuche zunächst einige Änderungen." - - # Trainingshinweise — progressive Hinweistexte - hint.singles.l1: "Schau dir Zelle {0} an." - hint.singles.l2_region: "Konzentriere dich auf {0} — zähle die Kandidaten." - hint.singles.l2_no_region: "Zähle die Kandidaten in Zelle {0}." - hint.singles.l3: "Der Wert ist {0}." - hint.subsets.l1_region: "Konzentriere dich auf {0}." - hint.subsets.l1_no_region: "Suche nach Zellen, die dieselben Kandidaten in einer Einheit teilen." - hint.subsets.l2_values: "Diese Zellen bilden eine [{0}]-Teilmenge. Die Werte der Teilmenge können nur in diese Zellen — eliminiere sie aus anderen Zellen der Region." - hint.subsets.l2_no_values: "Diese Zellen bilden die Teilmenge. Die Werte können nur in diese Zellen — eliminiere sie aus anderen Zellen der Region." - hint.subsets.l3: "Eliminiere Kandidaten aus Zellen, die alle Teilmengenzellen sehen." - hint.intersections.l1_value: "Suche nach Wert {0}, der auf eine Schnittmenge beschränkt ist." - hint.intersections.l1_no_value: "Suche nach einem Kandidaten, der auf die Schnittmenge von Box und Linie beschränkt ist." - hint.intersections.l2: "Die Schnittmengenzellen. Der Kandidat ist auf diese Zellen beschränkt — eliminiere ihn aus anderen Zellen der Linie oder Box außerhalb dieser Schnittmenge." - hint.intersections.l3: "Eliminiere den Kandidaten aus Zellen außerhalb der Schnittmenge." - hint.fish.l1_value: "Suche nach einem Fischmuster für Wert {0}." - hint.fish.l1_no_value: "Suche nach einem Fischmuster (Zeilen/Spalten mit eingeschränkten Kandidatenpositionen)." - hint.fish.l2: "Basis- und Abdeckungsmengen. Blaue Zellen sind die Basismenge. Grüne Zellen sind die Abdeckungsmenge. Eliminiere den Kandidaten aus Abdeckungszellen, die nicht in der Basismenge sind." - hint.fish.l3: "Eliminiere den Kandidaten aus Abdeckungszellen außerhalb der Basismenge." - hint.wings.l1: "Finde die Pivotzelle bei {0}." - hint.wings.l2: "Pivot- und Flügelzellen. Der orange Pivot verbindet die grünen Flügel. Gemeinsame Kandidaten können aus Zellen eliminiert werden, die alle Flügelendpunkte sehen." - hint.wings.l3: "Eliminiere den gemeinsamen Kandidaten aus Zellen, die alle Flügelendpunkte sehen." - hint.single_digit.l1_value: "Suche nach konjugierten Paaren für Wert {0}." - hint.single_digit.l1_no_value: "Suche nach konjugierten Paaren (Zellen, in denen eine Ziffer genau zweimal in einer Einheit vorkommt)." - hint.single_digit.l2: "Die Kettenzellen. Diese Zellen bilden konjugierte Paare. Folge dem alternierenden Muster, um Eliminierungen zu finden." - hint.single_digit.l3: "Zellen, die beide Endpunkte des Musters sehen, können eliminiert werden." - hint.coloring.l1_value: "Baue eine Färbungskette für Wert {0}." - hint.coloring.l1_no_value: "Beginne konjugierte Paare mit zwei alternierenden Farben zu färben." - hint.coloring.l2: "Die Färbungskette. Blau und Grün sind zwei alternierende Farben — eine muss wahr sein, eine falsch. Zellen, die beide Farben sehen, können den Kandidaten eliminieren." - hint.coloring.l3: "Eine Farbe muss falsch sein — eliminiere aus Zellen, die beide Farben sehen." - hint.unique_rect.l1: "Suche nach einem tödlichen Muster — vier Zellen, die ein Rechteck über zwei Boxen bilden." - hint.unique_rect.l2: "Die Rechteckecken. Diese vier Zellen über zwei Boxen bilden ein potenziell tödliches Muster. Um das Rätsel eindeutig zu halten, eliminiere den Kandidaten, der das Rechteck vervollständigen würde." - hint.unique_rect.l3: "Um das tödliche Muster zu vermeiden, eliminiere den Kandidaten, der es vervollständigen würde." - hint.chains.l1_pos: "Starte die Kette von Zelle {0}." - hint.chains.l1_no_pos: "Suche nach einer Kette verknüpfter Zellen mit alternierenden starken/schwachen Verbindungen." - hint.chains.l2: "Der Kettenpfad. Folge den alternierenden starken (blauen) und schwachen (grünen) Verbindungen. Die Kettenlogik erzwingt eine Schlussfolgerung an den Endpunkten." - hint.chains.l3_placement: "Alle Ketten führen zu Wert {0} bei {1}." - hint.chains.l3_elimination: "Eliminiere Kandidaten, die der Kettenlogik widersprechen." - hint.set_logic.l1: "Suche nach einem Fast Locked Set (eine Gruppe von N Zellen mit N+1 Kandidaten)." - hint.set_logic.l2: "Die ALS-Zellen und der eingeschränkte Gemeinsame. Ein ALS besteht aus N Zellen mit N+1 Kandidaten. Der eingeschränkte gemeinsame Kandidat verbindet die Mengen — Eliminierungen gelten für Zellen, die alle relevanten ALS-Mitglieder sehen." - hint.set_logic.l3: "Eliminiere Kandidaten aus Zellen, die alle relevanten ALS-Mitglieder sehen." - hint.special.l1: "Suche nach der Zelle mit drei Kandidaten (der einzigen Nicht-Biwert-Zelle)." - hint.special.l2: "Die Schlüsselzelle ist {0}." - - # Technikbeschreibungen — what_it_is und what_to_look_for - tech.desc.naked_single.what_it_is: "Eine Zelle, in der nur noch ein einziger Kandidat möglich ist." - tech.desc.naked_single.what_to_look_for: "Suche nach Zellen, die nur einen Kandidaten haben. Dies geschieht, wenn alle anderen Ziffern bereits in derselben Zeile, Spalte oder Box vorkommen." - tech.desc.hidden_single.what_it_is: "Ein Kandidat, der innerhalb einer Einheit (Zeile, Spalte oder Box) nur in einer einzigen Zelle vorkommen kann." - tech.desc.hidden_single.what_to_look_for: "Suche in jeder Einheit nach einem Kandidaten, der nur an einer Stelle möglich ist, auch wenn die Zelle mehrere Kandidaten hat." - tech.desc.naked_pair.what_it_is: "Zwei Zellen in derselben Einheit, die genau dieselben zwei Kandidaten enthalten." - tech.desc.naked_pair.what_to_look_for: "Suche nach zwei Zellen in einer Einheit, die exakt dasselbe Paar von Kandidaten {A,B} haben. Diese beiden Werte können aus allen anderen Zellen der Einheit eliminiert werden." - tech.desc.naked_triple.what_it_is: "Drei Zellen in derselben Einheit, deren Kandidaten insgesamt höchstens drei verschiedene Werte umfassen." - tech.desc.naked_triple.what_to_look_for: "Suche nach drei Zellen in einer Einheit, deren gemeinsame Kandidatenmenge genau drei Werte umfasst. Jede Zelle muss eine Teilmenge dieser drei Werte enthalten." - tech.desc.hidden_pair.what_it_is: "Zwei Kandidaten, die innerhalb einer Einheit nur in genau zwei Zellen vorkommen." - tech.desc.hidden_pair.what_to_look_for: "Suche nach zwei Kandidaten, die in einer Einheit auf dieselben zwei Zellen beschränkt sind. Alle anderen Kandidaten können aus diesen beiden Zellen eliminiert werden." - tech.desc.hidden_triple.what_it_is: "Drei Kandidaten, die innerhalb einer Einheit nur in genau drei Zellen vorkommen." - tech.desc.hidden_triple.what_to_look_for: "Suche nach drei Kandidaten, die in einer Einheit auf dieselben drei Zellen beschränkt sind. Alle anderen Kandidaten können aus diesen drei Zellen eliminiert werden." - tech.desc.pointing_pair.what_it_is: "Ein Kandidat in einer Box, der auf eine einzige Zeile oder Spalte beschränkt ist." - tech.desc.pointing_pair.what_to_look_for: "Wenn ein Kandidat innerhalb einer Box nur in einer Zeile oder Spalte vorkommt, kann er aus den übrigen Zellen dieser Zeile oder Spalte außerhalb der Box eliminiert werden." - tech.desc.box_line_reduction.what_it_is: "Ein Kandidat in einer Zeile oder Spalte, der auf eine einzige Box beschränkt ist." - tech.desc.box_line_reduction.what_to_look_for: "Wenn ein Kandidat innerhalb einer Zeile oder Spalte nur in einer Box vorkommt, kann er aus den übrigen Zellen dieser Box eliminiert werden." - tech.desc.naked_quad.what_it_is: "Vier Zellen in derselben Einheit, deren Kandidaten insgesamt höchstens vier verschiedene Werte umfassen." - tech.desc.naked_quad.what_to_look_for: "Suche nach vier Zellen in einer Einheit, deren gemeinsame Kandidatenmenge genau vier Werte umfasst. Diese Werte können aus allen anderen Zellen der Einheit eliminiert werden." - tech.desc.hidden_quad.what_it_is: "Vier Kandidaten, die innerhalb einer Einheit nur in genau vier Zellen vorkommen." - tech.desc.hidden_quad.what_to_look_for: "Suche nach vier Kandidaten, die in einer Einheit auf dieselben vier Zellen beschränkt sind. Alle anderen Kandidaten können aus diesen vier Zellen eliminiert werden." - tech.desc.x_wing.what_it_is: "Ein Fisch-Muster, bei dem ein Kandidat in zwei Zeilen jeweils auf genau zwei Spalten beschränkt ist (oder umgekehrt)." - tech.desc.x_wing.what_to_look_for: "Suche nach einem Kandidaten, der in zwei Zeilen jeweils nur in denselben zwei Spalten vorkommt. Der Kandidat kann aus den übrigen Zellen dieser Spalten eliminiert werden." - tech.desc.xy_wing.what_it_is: "Drei Biwert-Zellen, die ein Y-Muster bilden: ein Pivot mit {A,B} verbunden mit zwei Flügeln {A,C} und {B,C}." - tech.desc.xy_wing.what_to_look_for: "Suche nach einer Biwert-Zelle (Pivot), die zwei andere Biwert-Zellen (Flügel) sieht. Wenn die Flügel einen gemeinsamen Kandidaten C teilen, kann C aus Zellen eliminiert werden, die beide Flügel sehen." - tech.desc.swordfish.what_it_is: "Ein Fisch-Muster der Größe 3: ein Kandidat in drei Zeilen auf höchstens drei Spalten beschränkt (oder umgekehrt)." - tech.desc.swordfish.what_to_look_for: "Suche nach einem Kandidaten, der in drei Zeilen jeweils nur in denselben drei (oder weniger) Spalten vorkommt. Der Kandidat kann aus den übrigen Zellen dieser Spalten eliminiert werden." - tech.desc.skyscraper.what_it_is: "Zwei konjugierte Paare desselben Kandidaten, die sich einen Endpunkt teilen." - tech.desc.skyscraper.what_to_look_for: "Suche nach zwei Zeilen (oder Spalten) mit je genau zwei Positionen für denselben Kandidaten, wobei eine Position geteilt wird. Der Kandidat kann aus Zellen eliminiert werden, die beide nicht geteilten Endpunkte sehen." - tech.desc.two_string_kite.what_it_is: "Ein Muster aus einem konjugierten Paar in einer Zeile und einem in einer Spalte, verbunden durch eine gemeinsame Box." - tech.desc.two_string_kite.what_to_look_for: "Suche nach einem Kandidaten mit einem konjugierten Paar in einer Zeile und einem in einer Spalte, die über eine Box verbunden sind. Der Kandidat kann aus der Zelle eliminiert werden, die beide äußeren Endpunkte sieht." - tech.desc.xyz_wing.what_it_is: "Ein Pivot mit {A,B,C} verbunden mit zwei Flügeln {A,B} und {A,C}." - tech.desc.xyz_wing.what_to_look_for: "Suche nach einer Zelle mit drei Kandidaten {A,B,C}, die zwei Biwert-Zellen sieht, die jeweils einen Kandidaten mit dem Pivot teilen. Der gemeinsame Kandidat A kann aus Zellen eliminiert werden, die alle drei sehen." - tech.desc.unique_rectangle.what_it_is: "Vier Zellen, die ein Rechteck über zwei Boxen bilden und ein tödliches Muster erzeugen würden." - tech.desc.unique_rectangle.what_to_look_for: "Suche nach vier Zellen in einem Rechteck über zwei Boxen mit zwei gemeinsamen Kandidaten. Um die Eindeutigkeit des Rätsels zu bewahren, muss der Kandidat eliminiert werden, der das Muster vervollständigen würde." - tech.desc.w_wing.what_it_is: "Zwei Biwert-Zellen {A,B}, die durch eine starke Verbindung auf Kandidat A verbunden sind." - tech.desc.w_wing.what_to_look_for: "Suche nach zwei Biwert-Zellen mit denselben Kandidaten {A,B}, die durch eine starke Verbindung auf einem der Werte verbunden sind. Der andere Wert B kann aus Zellen eliminiert werden, die beide Zellen sehen." - tech.desc.simple_coloring.what_it_is: "Konjugierte Ketten auf einem einzelnen Kandidaten mit zwei alternierenden Farben." - tech.desc.simple_coloring.what_to_look_for: "Färbe konjugierte Paare eines Kandidaten mit zwei Farben abwechselnd ein. Wenn zwei gleichfarbige Zellen sich sehen, ist diese Farbe falsch. Zellen, die beide Farben sehen, können den Kandidaten nicht enthalten." - tech.desc.finned_x_wing.what_it_is: "Ein X-Wing-Muster mit einer zusätzlichen Zelle (Flosse), die das perfekte Muster stört." - tech.desc.finned_x_wing.what_to_look_for: "Suche nach einem fast perfekten X-Wing, bei dem eine Zeile eine zusätzliche Position hat (die Flosse). Eliminierungen sind auf Zellen beschränkt, die sowohl im Deckungsset als auch in der Box der Flosse liegen." - tech.desc.remote_pairs.what_it_is: "Eine Kette von Biwert-Zellen mit denselben zwei Kandidaten {A,B}, die sich über mindestens vier Zellen erstreckt." - tech.desc.remote_pairs.what_to_look_for: "Suche nach einer Kette von Biwert-Zellen mit identischen Kandidaten {A,B}, die sich gegenseitig sehen. Bei gerader Kettenlänge können die beiden Kandidaten aus Zellen eliminiert werden, die beide Endpunkte sehen." - tech.desc.bug.what_it_is: "BUG (Bivalue Universal Grave): Ein Zustand, in dem alle Zellen Biwert-Zellen wären — bis auf eine." - tech.desc.bug.what_to_look_for: "Wenn alle leeren Zellen genau zwei Kandidaten haben bis auf eine Zelle mit drei, muss in dieser Zelle der Kandidat platziert werden, der in ihrer Zeile, Spalte und Box dreimal vorkommt." - tech.desc.jellyfish.what_it_is: "Ein Fisch-Muster der Größe 4: ein Kandidat in vier Zeilen auf höchstens vier Spalten beschränkt (oder umgekehrt)." - tech.desc.jellyfish.what_to_look_for: "Suche nach einem Kandidaten, der in vier Zeilen jeweils nur in denselben vier (oder weniger) Spalten vorkommt. Der Kandidat kann aus den übrigen Zellen dieser Spalten eliminiert werden." - tech.desc.finned_swordfish.what_it_is: "Ein Swordfish-Muster mit einer zusätzlichen Flossenposition." - tech.desc.finned_swordfish.what_to_look_for: "Suche nach einem fast perfekten Swordfish, bei dem eine Basis-Einheit eine zusätzliche Position hat. Eliminierungen sind auf Zellen beschränkt, die sowohl im Deckungsset als auch in der Box der Flosse liegen." - tech.desc.empty_rectangle.what_it_is: "Ein Muster, bei dem die Positionen eines Kandidaten in einer Box ein leeres Rechteck bilden und eine konjugierte Verbindung nutzen." - tech.desc.empty_rectangle.what_to_look_for: "Suche nach einer Box, in der ein Kandidat in einer Zeile und einer Spalte vorkommt, aber nicht an deren Schnittpunkt. Kombiniert mit einem konjugierten Paar außerhalb der Box ermöglicht dies eine Eliminierung." - tech.desc.wxyz_wing.what_it_is: "Ein Wing-Muster mit einem Pivot und drei Flügelzellen, die zusammen vier Kandidaten abdecken." - tech.desc.wxyz_wing.what_to_look_for: "Suche nach einer Zelle (Pivot) mit Flügeln, deren gemeinsame Kandidatenmenge vier Werte umfasst. Der eingeschränkte gemeinsame Kandidat kann aus Zellen eliminiert werden, die alle vier sehen." - tech.desc.finned_jellyfish.what_it_is: "Ein Jellyfish-Muster mit einer zusätzlichen Flossenposition." - tech.desc.finned_jellyfish.what_to_look_for: "Suche nach einem fast perfekten Jellyfish, bei dem eine Basis-Einheit eine zusätzliche Position hat. Eliminierungen sind auf Zellen beschränkt, die sowohl im Deckungsset als auch in der Box der Flosse liegen." - tech.desc.xy_chain.what_it_is: "Eine Kette von Biwert-Zellen, die durch gemeinsame Kandidaten verknüpft sind." - tech.desc.xy_chain.what_to_look_for: "Suche nach einer Kette von Biwert-Zellen, bei der jede Zelle einen Kandidaten mit der nächsten teilt. Der Kandidat, der an beiden Endpunkten vorkommt, kann aus Zellen eliminiert werden, die beide Endpunkte sehen." - tech.desc.multi_coloring.what_it_is: "Mehrere Färbungsketten auf einem Kandidaten, die miteinander interagieren." - tech.desc.multi_coloring.what_to_look_for: "Erstelle mehrere Färbungscluster auf einem Kandidaten. Wenn eine Farbe eines Clusters beide Farben eines anderen sieht, muss diese Farbe falsch sein. Zellen, die komplementäre Farben verschiedener Cluster sehen, können eliminiert werden." - tech.desc.als_xz.what_it_is: "Zwei Almost Locked Sets (ALS), verbunden durch einen eingeschränkten gemeinsamen Kandidaten." - tech.desc.als_xz.what_to_look_for: "Suche nach zwei ALS (je N Zellen mit N+1 Kandidaten), die einen eingeschränkten gemeinsamen Kandidaten X teilen. Ein weiterer gemeinsamer Kandidat Z kann aus Zellen eliminiert werden, die beide ALS sehen." - tech.desc.sue_de_coq.what_it_is: "Ein Muster am Schnittpunkt einer Zeile/Spalte und einer Box, kombiniert mit Almost Locked Sets." - tech.desc.sue_de_coq.what_to_look_for: "Suche nach 2-3 Zellen am Schnittpunkt einer Zeile und einer Box, deren Kandidaten mit je einem ALS in der Restzeile und der Restbox abgedeckt werden. Kandidaten können aus den Restzellen eliminiert werden." - tech.desc.forcing_chain.what_it_is: "Für jeden Kandidaten einer Zelle wird eine Kette verfolgt — alle führen zum selben Ergebnis." - tech.desc.forcing_chain.what_to_look_for: "Wähle eine Zelle mit wenigen Kandidaten. Verfolge die logischen Konsequenzen jedes Kandidaten. Wenn alle Ketten dasselbe Ergebnis liefern, muss dieses Ergebnis wahr sein." - tech.desc.nice_loop.what_it_is: "Eine alternierende Schlusskette (AIC), die zwei Zellen durch starke und schwache Verbindungen verknüpft." - tech.desc.nice_loop.what_to_look_for: "Suche nach einer Kette alternierend starker und schwacher Verbindungen. Wenn die Kette mit zwei starken Verbindungen endet, kann der Endkandidat platziert werden; bei zwei schwachen wird er eliminiert." - tech.desc.x_cycles.what_it_is: "Zyklische Ketten auf einem einzelnen Kandidaten mit alternierenden starken und schwachen Verbindungen." - tech.desc.x_cycles.what_to_look_for: "Baue eine Kette auf einem Kandidaten mit alternierenden Verbindungen. Ein geschlossener Zyklus ermöglicht Eliminierungen; bei Diskontinuität wird je nach Typ platziert oder eliminiert." - tech.desc.three_d_medusa.what_it_is: "Erweiterung der einfachen Färbung auf mehrere Kandidaten innerhalb derselben Zellen." - tech.desc.three_d_medusa.what_to_look_for: "Färbe konjugierte Paare verschiedener Kandidaten mit zwei Farben ein. Wenn ein Zellen-Kandidat-Paar beiden Farben zugeordnet wird, ist eine Farbe falsch. Nutze die resultierenden Widersprüche für Eliminierungen." - tech.desc.hidden_unique_rectangle.what_it_is: "Ein Unique Rectangle, bei dem das tödliche Muster durch versteckte Kandidaten aufgedeckt wird." - tech.desc.hidden_unique_rectangle.what_to_look_for: "Suche nach einem Unique Rectangle, bei dem ein konjugiertes Paar auf einem der Werte bestätigt, dass der andere Wert an einer bestimmten Stelle eliminiert werden muss." - tech.desc.avoidable_rectangle.what_it_is: "Ein Unique-Rectangle-Muster mit bereits gelösten Zellen, die ein tödliches Muster erzeugen würden." - tech.desc.avoidable_rectangle.what_to_look_for: "Suche nach einem Rechteck mit zwei gelösten Zellen gleicher Werte und zwei leeren Zellen mit diesen Werten als Kandidaten. Ein Kandidat muss eliminiert werden, um Mehrdeutigkeit zu vermeiden." - tech.desc.als_xy_wing.what_it_is: "Drei ALS, die paarweise durch eingeschränkte gemeinsame Kandidaten X und Y verbunden sind." - tech.desc.als_xy_wing.what_to_look_for: "Suche nach drei ALS: {A,B} verbunden durch X und {A,C} verbunden durch Y. Kandidat Z, der in A und C vorkommt, kann aus Zellen eliminiert werden, die die Z-Zellen beider ALS sehen." - tech.desc.death_blossom.what_it_is: "Eine Stammzelle, deren Kandidaten jeweils auf ein eigenes ALS (Blütenblatt) zeigen." - tech.desc.death_blossom.what_to_look_for: "Suche nach einer Zelle (Stamm), bei der jeder Kandidat über einen eingeschränkten Gemeinsamen mit einem eigenen ALS verbunden ist. Gemeinsame Kandidaten der Blütenblätter können aus Zellen eliminiert werden, die alle sehen." - tech.desc.vwxyz_wing.what_it_is: "Ein Wing-Muster mit einem Pivot und vier Flügelzellen, die zusammen fünf Kandidaten abdecken." - tech.desc.vwxyz_wing.what_to_look_for: "Suche nach einer Zelle (Pivot) mit Flügeln, deren gemeinsame Kandidatenmenge fünf Werte umfasst. Der eingeschränkte gemeinsame Kandidat kann aus Zellen eliminiert werden, die alle Z-Zellen sehen." - tech.desc.franken_fish.what_it_is: "Ein Fisch-Muster, bei dem die Basismengen aus Zeilen/Spalten und Boxen gemischt bestehen." - tech.desc.franken_fish.what_to_look_for: "Suche nach einem Fisch, dessen Basis- und Deckungsmengen Zeilen/Spalten mit Boxen kombinieren. Der Kandidat kann aus Deckungszellen außerhalb der Basismenge eliminiert werden." - tech.desc.grouped_x_cycles.what_it_is: "X-Cycles mit gruppierten Knoten, bei denen mehrere Zellen in einer Box als einzelner Knoten behandelt werden." - tech.desc.grouped_x_cycles.what_to_look_for: "Wie X-Cycles, aber Gruppen von Zellen innerhalb einer Box werden als ein Knoten zusammengefasst. Dies ermöglicht komplexere Kettenmuster und zusätzliche Eliminierungen." - tech.desc.sashimi_x_wing.what_it_is: "Ein Finned X-Wing, bei dem eine Basisposition fehlt — die Flosse ist der einzige Überrest." - tech.desc.sashimi_x_wing.what_to_look_for: "Suche nach einem fast vollständigen X-Wing, bei dem eine erwartete Position fehlt und durch eine Flosse ersetzt wird. Eliminierungen gelten für Zellen in der Box der Flosse, die auch im Deckungsset liegen." - tech.desc.sashimi_swordfish.what_it_is: "Ein Finned Swordfish, bei dem eine Basisposition fehlt — die Flosse kompensiert die Lücke." - tech.desc.sashimi_swordfish.what_to_look_for: "Suche nach einem fast vollständigen Swordfish mit fehlender Basisposition und einer Flosse. Eliminierungen sind auf die Schnittmenge von Flossenbox und Deckungsset beschränkt." - tech.desc.sashimi_jellyfish.what_it_is: "Ein Finned Jellyfish, bei dem eine Basisposition fehlt — die Flosse kompensiert die Lücke." - tech.desc.sashimi_jellyfish.what_to_look_for: "Suche nach einem fast vollständigen Jellyfish mit fehlender Basisposition und einer Flosse. Eliminierungen sind auf die Schnittmenge von Flossenbox und Deckungsset beschränkt." - tech.desc.unit_forcing_chain.what_it_is: "Alle Positionen eines Kandidaten in einer Einheit werden als Startpunkte verfolgt — alle führen zum selben Ergebnis." - tech.desc.unit_forcing_chain.what_to_look_for: "Wähle eine Einheit (Zeile, Spalte oder Box) mit wenigen Positionen für einen Kandidaten. Wenn das Setzen des Kandidaten an jeder Position zum selben Ergebnis führt, muss dieses wahr sein." - tech.desc.region_forcing_chain.what_it_is: "Alle Positionen eines Kandidaten in einer Region werden verfolgt — alle Ketten konvergieren." - tech.desc.region_forcing_chain.what_to_look_for: "Ähnlich wie Unit Forcing Chain, aber der Fokus liegt auf einer Region. Wenn jede mögliche Platzierung eines Kandidaten dasselbe Ergebnis erzwingt, ist das Ergebnis gesichert." - tech.desc.mutant_fish.what_it_is: "Ein Fisch-Muster mit völlig gemischten Basis- und Deckungsmengen aus Zeilen, Spalten und Boxen." - tech.desc.mutant_fish.what_to_look_for: "Suche nach einem Fisch, dessen Basis- und Deckungsmengen beliebig aus Zeilen, Spalten und Boxen zusammengesetzt sind. Kandidaten werden aus Deckungszellen eliminiert, die nicht zur Basismenge gehören." - tech.desc.kraken_fish.what_it_is: "Ein Finned Fish, bei dem die Flosse durch Forcing Chains verifiziert wird." - tech.desc.kraken_fish.what_to_look_for: "Suche nach einem Finned Fish, bei dem die Eliminierungen durch Ketten von der Flosse aus bestätigt werden. Die Ketten beweisen, dass die Eliminierung unabhängig von der Flosse gültig ist." - tech.desc.als_chain.what_it_is: "Eine Kette von ALS, die paarweise durch eingeschränkte gemeinsame Kandidaten verbunden sind." - tech.desc.als_chain.what_to_look_for: "Suche nach einer Folge von ALS, bei denen benachbarte Paare eingeschränkte gemeinsame Kandidaten teilen. Kandidaten des ersten und letzten ALS können aus Zellen eliminiert werden, die die Z-Zellen beider sehen." - tech.desc.unique_loop.what_it_is: "Eine Schleife von Zellen mit zwei gemeinsamen Kandidaten, die ein erweitertes tödliches Muster bilden." - tech.desc.unique_loop.what_to_look_for: "Suche nach einer geschlossenen Schleife von Zellen mit zwei gemeinsamen Kandidaten, wobei jede Zelle die nächste in der Kette sieht. Zusätzliche Kandidaten müssen vorhanden sein, um die Eindeutigkeit zu gewährleisten." - tech.desc.junior_exocet.what_it_is: "Ein Muster mit zwei Basiszellen und zwei Zielzellen, die durch Kandidatenbeschränkungen verknüpft sind." - tech.desc.junior_exocet.what_to_look_for: "Suche nach zwei Basiszellen in einer Zeile mit Zielzellen in verschiedenen Boxen. Die Zielzellen können nur die Kandidaten der Basiszellen enthalten — alle anderen werden eliminiert." - tech.desc.continuous_nice_loop.what_it_is: "Eine geschlossene alternierende Schlusskette, bei der die Schleife zu sich selbst zurückkehrt." - tech.desc.continuous_nice_loop.what_to_look_for: "Suche nach einer geschlossenen Kette alternierend starker und schwacher Verbindungen. Schwache Verbindungen innerhalb der Schleife ermöglichen die Eliminierung der verbundenen Kandidaten aus externen Zellen." - tech.desc.grouped_nice_loop.what_it_is: "Ein Nice Loop mit gruppierten Knoten, bei dem mehrere Zellen einer Box als ein Knoten gelten." - tech.desc.grouped_nice_loop.what_to_look_for: "Wie ein Nice Loop, aber Zellgruppen innerhalb einer Box fungieren als einzelner Knoten. Dies ermöglicht die Erkennung komplexerer Muster und zusätzliche Eliminierungen." - tech.desc.backtracking.what_it_is: "Systematisches Ausprobieren von Kandidaten mit Zurücksetzen bei Widersprüchen." - tech.desc.backtracking.what_to_look_for: "Backtracking ist keine logische Technik. Es wird als letztes Mittel eingesetzt, wenn keine logischen Strategien mehr anwendbar sind." - tech.desc.unknown.what_it_is: "Unbekannte Lösungstechnik." - tech.desc.unknown.what_to_look_for: "Keine Beschreibung verfügbar." - - # ViewModel — Technikformatierung - technique.points_fmt: "{0} (SE {1})" - technique.backtracking: "Backtracking (Versuch und Irrtum)" - - # ViewModel — Statistikfehler - stats_err.invalid_data: "Ungültige Spieldaten" - stats_err.file_access: "Dateizugriffsfehler" - stats_err.serialization: "Serialisierungsfehler" - stats_err.invalid_difficulty: "Ungültige Schwierigkeit" - stats_err.game_not_started: "Spiel nicht gestartet" - stats_err.game_already_ended: "Spiel bereits beendet" - stats_err.unknown: "Unbekannter Statistikfehler" - - # Techniknamen - tech.naked_single: "Nackter Einzelner" - tech.hidden_single: "Versteckter Einzelner" - tech.naked_pair: "Nacktes Paar" - tech.naked_triple: "Nacktes Tripel" - tech.hidden_pair: "Verstecktes Paar" - tech.hidden_triple: "Verstecktes Tripel" - tech.pointing_pair: "Zeigepaar" - tech.box_line_reduction: "Block-Zeilen-Reduktion" - tech.naked_quad: "Nacktes Quartett" - tech.hidden_quad: "Verstecktes Quartett" - tech.x_wing: "X-Wing" - tech.xy_wing: "XY-Wing" - tech.swordfish: "Schwertfisch" - tech.skyscraper: "Wolkenkratzer" - tech.two_string_kite: "Zweidraht-Drachen" - tech.xyz_wing: "XYZ-Wing" - tech.unique_rectangle: "Eindeutiges Rechteck" - tech.w_wing: "W-Wing" - tech.simple_coloring: "Einfache Färbung" - tech.finned_x_wing: "X-Wing mit Flosse" - tech.remote_pairs: "Entfernte Paare" - tech.bug: "BUG" - tech.jellyfish: "Qualle" - tech.finned_swordfish: "Schwertfisch mit Flosse" - tech.empty_rectangle: "Leeres Rechteck" - tech.wxyz_wing: "WXYZ-Wing" - tech.finned_jellyfish: "Qualle mit Flosse" - tech.xy_chain: "XY-Kette" - tech.multi_coloring: "Mehrfach-Färbung" - tech.als_xz: "ALS-XZ" - tech.sue_de_coq: "Sue de Coq" - tech.forcing_chain: "Forcing Chain" - tech.nice_loop: "Nice Loop" - tech.x_cycles: "X-Zyklen" - tech.three_d_medusa: "3D Medusa" - tech.hidden_unique_rectangle: "Verstecktes Eindeutiges Rechteck" - tech.avoidable_rectangle: "Vermeidbares Rechteck" - tech.als_xy_wing: "ALS-XY-Wing" - tech.death_blossom: "Todesblüte" - tech.vwxyz_wing: "VWXYZ-Wing" - tech.franken_fish: "Franken-Fisch" - tech.grouped_x_cycles: "Gruppierte X-Zyklen" - tech.sashimi_x_wing: "Sashimi X-Wing" - tech.sashimi_swordfish: "Sashimi Schwertfisch" - tech.sashimi_jellyfish: "Sashimi Qualle" - tech.unit_forcing_chain: "Einheits-Forcing-Chain" - tech.region_forcing_chain: "Regions-Forcing-Chain" - tech.mutant_fish: "Mutanten-Fisch" - tech.kraken_fish: "Kraken-Fisch" - tech.als_chain: "ALS-Kette" - tech.junior_exocet: "Junior Exocet" - tech.unique_loop: "Eindeutige Schleife" - tech.continuous_nice_loop: "Kontinuierlicher Nice Loop" - tech.grouped_nice_loop: "Gruppierter Nice Loop" - tech.backtracking_name: "Backtracking" - tech.unknown: "Unbekannte Technik" - - # Regionsnamen - region.row: "Zeile" - region.column: "Spalte" - region.box: "Block" - region.unknown: "Unbekannte Region" - - # Positionsformat (Z=Zeile, S=Spalte, 1-indiziert) - position.fmt: "Z{0}S{1}" - - # Erklärungsvorlagen - explain.naked_single: "Nackter Einzelner bei {0}: nur Wert {1} ist möglich" - explain.hidden_single: "Versteckter Einzelner bei {0}: Wert {1} kann nur in dieser Zelle innerhalb seiner Region vorkommen" - explain.naked_pair: "Nacktes Paar [{0}] bei {1} in {2} eliminiert Kandidaten aus anderen Zellen" - explain.naked_triple: "Nacktes Tripel [{0}] bei {1} in {2} eliminiert Kandidaten aus anderen Zellen" - explain.hidden_pair: "Verstecktes Paar [{0}] bei {1} in {2} eliminiert andere Kandidaten aus diesen Zellen" - explain.hidden_triple: "Verstecktes Tripel [{0}] bei {1} in {2} eliminiert andere Kandidaten aus diesen Zellen" - explain.pointing_pair: "Zeigepaar: {0} in Block {1} beschränkt auf {2} {3} eliminiert {0} aus anderen Zellen in {2} {3}" - explain.box_line_reduction: "Block-Zeilen-Reduktion: {0} in {1} {2} beschränkt auf Block {3} eliminiert {0} aus anderen Zellen in Block {3}" - explain.naked_quad: "Nacktes Quartett [{0}] bei {1} in {2} eliminiert Kandidaten aus anderen Zellen" - explain.hidden_quad: "Verstecktes Quartett [{0}] bei {1} in {2} eliminiert andere Kandidaten aus diesen Zellen" - explain.x_wing_row: "X-Wing auf Wert {0} in Zeilen {1} und {2}, Spalten {3} und {4} eliminiert {0} aus anderen Zellen in diesen Spalten" - explain.x_wing_col: "X-Wing auf Wert {0} in Spalten {1} und {2}, Zeilen {3} und {4} eliminiert {0} aus anderen Zellen in diesen Zeilen" - explain.xy_wing: "XY-Wing: Pivot {0} {{{1},{2}}}, Flügel {3} {{{1},{4}}}, Flügel {5} {{{2},{4}}} eliminiert {4} aus Zellen, die beide Flügel sehen" - explain.swordfish_row: "Schwertfisch auf Wert {0} in Zeilen {1}, {2}, {3} und Spalten {4}, {5}, {6} eliminiert {0} aus anderen Zellen in diesen Spalten" - explain.swordfish_col: "Schwertfisch auf Wert {0} in Spalten {1}, {2}, {3} und Zeilen {4}, {5}, {6} eliminiert {0} aus anderen Zellen in diesen Zeilen" - explain.skyscraper: "Wolkenkratzer auf Wert {0}: konjugierte Paare in {1} und {2} teilen Endpunkt {3} — eliminiert {0} aus Zellen, die beide {4} und {5} sehen" - explain.two_string_kite: "Zweidraht-Drachen auf Wert {0}: Zeilenpaar {1},{2} und Spaltenpaar {3},{4} durch gemeinsamen Block verbunden — eliminiert {0} aus Zellen, die beide Endpunkte sehen" - explain.xyz_wing: "XYZ-Wing: Pivot {0} {{{1},{2},{3}}}, Flügel {4} und Flügel {5} eliminieren {3} aus Zellen, die alle drei sehen" - explain.unique_rectangle: "Eindeutiges Rechteck: Zellen {0} mit Werten {{{1},{2}}} — eliminiert {1},{2} aus {3} um tödliches Muster zu vermeiden" - explain.w_wing: "W-Wing: Zellen {0} und {1} {{{2},{3}}} durch starke Verbindung auf {2} verbunden — eliminiert {3} aus Zellen, die beide sehen" - explain.simple_coloring_contradiction: "Einfache Färbung auf {0}: gleichfarbige Zellen sehen einander — eliminiert {0} aus allen Zellen dieser Farbe" - explain.simple_coloring_exclusion: "Einfache Färbung auf {0}: Zelle {1} sieht beide Farben — eliminiert {0} aus {1}" - explain.unique_rectangle_type2: "Eindeutiges Rechteck Typ 2: Zellen {0} mit Werten {{{1},{2}}} — zusätzlicher Kandidat {3} aus Zellen eliminiert, die beide Bodenzellen in gemeinsamer {4} sehen" - explain.unique_rectangle_type3: "Eindeutiges Rechteck Typ 3: Zellen {0} mit Werten {{{1},{2}}} — Bodenextras bilden nackte Teilmenge in {3}, eliminiert aus anderen Zellen" - explain.unique_rectangle_type4: "Eindeutiges Rechteck Typ 4: Zellen {0} mit Werten {{{1},{2}}} — starke Verbindung auf {3} in {4} eliminiert {5} aus Bodenzellen" - explain.unique_rectangle_type6: "Eindeutiges Rechteck Typ 6: Zellen {0} mit Werten {{{1},{2}}} — {3} ist konjugiert in beiden parallelen Linien des Rechtecks, sperrt das Muster — eliminiert Extras aus Bodenzellen" - explain.finned_x_wing_row: "X-Wing mit Flosse auf Wert {0} in Zeilen {1} und {2}, Spalten {3} und {4} mit Flosse bei {5} — eliminiert {0} aus Zellen im Block der Flosse" - explain.finned_x_wing_col: "X-Wing mit Flosse auf Wert {0} in Spalten {1} und {2}, Zeilen {3} und {4} mit Flosse bei {5} — eliminiert {0} aus Zellen im Block der Flosse" - explain.sashimi_x_wing_row: "Sashimi X-Wing auf Wert {0} in Zeilen {1} und {2}, Spalten {3} und {4} mit Flosse bei {5} — eliminiert {0} aus Zellen im Block der Flosse" - explain.sashimi_x_wing_col: "Sashimi X-Wing auf Wert {0} in Spalten {1} und {2}, Zeilen {3} und {4} mit Flosse bei {5} — eliminiert {0} aus Zellen im Block der Flosse" - explain.remote_pairs: "Entfernte Paare: Kette von {{{0},{1}}}-Zellen von {2} bis {3} (Länge {4}) — eliminiert {0},{1} aus Zellen, die beide Endpunkte sehen" - explain.bug: "BUG: alle Zellen bivalent außer {0} — Wert {1} muss gesetzt werden, um tödliches Muster zu vermeiden" - explain.jellyfish_row: "Qualle auf Wert {0} in Zeilen {1}, {2}, {3}, {4} und Spalten {5}, {6}, {7}, {8} eliminiert {0} aus anderen Zellen in diesen Spalten" - explain.jellyfish_col: "Qualle auf Wert {0} in Spalten {1}, {2}, {3}, {4} und Zeilen {5}, {6}, {7}, {8} eliminiert {0} aus anderen Zellen in diesen Zeilen" - explain.finned_swordfish_row: "Schwertfisch mit Flosse auf Wert {0} in Zeilen {1}, {2}, {3} mit Flosse bei {4} — eliminiert {0} aus Zellen im Block der Flosse" - explain.finned_swordfish_col: "Schwertfisch mit Flosse auf Wert {0} in Spalten {1}, {2}, {3} mit Flosse bei {4} — eliminiert {0} aus Zellen im Block der Flosse" - explain.sashimi_swordfish_row: "Sashimi Schwertfisch auf Wert {0} in Zeilen {1}, {2}, {3} mit Flosse bei {4} — eliminiert {0} aus Zellen im Block der Flosse" - explain.sashimi_swordfish_col: "Sashimi Schwertfisch auf Wert {0} in Spalten {1}, {2}, {3} mit Flosse bei {4} — eliminiert {0} aus Zellen im Block der Flosse" - explain.empty_rectangle: "Leeres Rechteck auf Wert {0}: ER in Block {1} mit konjugiertem Paar in {2} — eliminiert {0} aus {3}" - explain.wxyz_wing: "WXYZ-Wing: Pivot {0} mit Flügeln {1}, {2}, {3} — eliminiert {4} aus Zellen, die alle vier sehen" - explain.finned_jellyfish_row: "Qualle mit Flosse auf Wert {0} in Zeilen {1}, {2}, {3}, {4} mit Flosse bei {5} — eliminiert {0} aus Zellen im Block der Flosse" - explain.finned_jellyfish_col: "Qualle mit Flosse auf Wert {0} in Spalten {1}, {2}, {3}, {4} mit Flosse bei {5} — eliminiert {0} aus Zellen im Block der Flosse" - explain.sashimi_jellyfish_row: "Sashimi Qualle auf Wert {0} in Zeilen {1}, {2}, {3}, {4} mit Flosse bei {5} — eliminiert {0} aus Zellen im Block der Flosse" - explain.sashimi_jellyfish_col: "Sashimi Qualle auf Wert {0} in Spalten {1}, {2}, {3}, {4} mit Flosse bei {5} — eliminiert {0} aus Zellen im Block der Flosse" - explain.xy_chain: "XY-Kette: Kette von {0} bivalenten Zellen von {1} bis {2} — eliminiert {3} aus Zellen, die beide Endpunkte sehen" - explain.multi_coloring_wrap: "Mehrfach-Färbung auf {0}: Farbe sieht beide Farben eines anderen Clusters — eliminiert {0} aus allen Zellen dieser Farbe" - explain.multi_coloring_trap: "Mehrfach-Färbung auf {0}: Zelle {1} sieht komplementäre Farben aus zwei Clustern — eliminiert {0}" - explain.als_xz: "ALS-XZ: ALS {0} und ALS {1} durch eingeschränkten gemeinsamen Kandidaten {2} verbunden — eliminiert {3} aus Zellen, die beide ALS sehen" - explain.sue_de_coq: "Sue de Coq: Kreuzung von {0} und Block {1} — eliminiert Kandidaten aus Rest der Zeile und Block" - explain.forcing_chain: "Forcing Chain: Annahme jedes Kandidaten in {0} führt zum gleichen Ergebnis — {1}" - explain.nice_loop: "Nice Loop: Alternierende Schlussfolgerungskette von {0} nach {1} — eliminiert {2}" - explain.x_cycles_type1: "X-Zyklen auf Wert {0}: geschlossene Schleife — eliminiert {0} aus Zellen, die Endpunkte schwacher Verbindungen sehen" - explain.x_cycles_type2: "X-Zyklen auf Wert {0}: stark-stark-Diskontinuität bei {1} — setzt {0}" - explain.x_cycles_type3: "X-Zyklen auf Wert {0}: schwach-schwach-Diskontinuität bei {1} — eliminiert {0} aus {1}" - explain.three_d_medusa: "3D Medusa: Mehrfach-Ziffer-Färbung — {0}" - explain.hidden_unique_rectangle: "Verstecktes Eindeutiges Rechteck: Zellen {0} mit Werten {{{1},{2}}} — eliminiert {3} aus {4} um tödliches Muster zu vermeiden" - explain.avoidable_rectangle: "Vermeidbares Rechteck: Zellen {0} mit gelösten Werten {{{1},{2}}} — eliminiert {3} aus {4} um tödliches Muster zu vermeiden" - explain.als_xy_wing: "ALS-XY-Wing: ALS {0}, ALS {1}, ALS {2} verbunden durch X={3} und Y={4} — eliminiert {5} aus Zellen, die Z-Zellen in A und C sehen" - explain.death_blossom: "Todesblüte: Stammzelle {0} mit Blütenblättern {1} — eliminiert {2} aus Zellen, die alle Blütenblatt-Z-Zellen sehen" - explain.vwxyz_wing: "VWXYZ-Wing: Pivot {0} mit Flügeln {1}, {2}, {3}, {4} — eliminiert {5} aus Zellen, die alle Z-Zellen sehen" - explain.franken_fish: "Franken-{0} auf Wert {1}: Basis {2}, Abdeckung {3} — eliminiert {1} aus Abdeckungszellen außerhalb der Basis" - explain.mutant_fish: "Mutanten-Fisch auf Wert {0}: Basis {1}, Abdeckung {2} — eliminiert {0} aus {3} Abdeckungszelle(n) außerhalb der Basis" - explain.grouped_x_cycles: "Gruppierte X-Zyklen auf Wert {0}: Kette mit gruppierten Knoten — {1}" - explain.kraken_fish: "Kraken-Fisch auf Wert {0}: Fisch mit Flosse und kettenverifizierten Eliminierungen von {1}" - explain.als_chain: "ALS-Kette ({0} ALS): eliminiert {1} aus Zellen, die Z-Zellen in erstem und letztem ALS sehen bei {2}" - explain.junior_exocet: "Junior Exocet: Basiszellen {0} und {1} mit Kandidaten {{{2}}} — Zielzellen {3} und {4} dürfen nur Basiskandidaten enthalten" - explain.unique_loop: "Eindeutige Schleife: Zellen {0} mit Werten {{{1},{2}}} — eliminiert {1},{2} aus {3} um tödliches Muster zu vermeiden" - explain.continuous_nice_loop: "Kontinuierlicher Nice Loop: Schleife mit {0} Knoten — eliminiert {1} Kandidat(en) durch schwache Verbindungslogik" - explain.grouped_nice_loop: "Gruppierter Nice Loop: Alternierende Schlussfolgerungskette von {0} nach {1} — eliminiert {2}" diff --git a/resources/locales/en.yaml b/resources/locales/en.yaml deleted file mode 100644 index d641ccf..0000000 --- a/resources/locales/en.yaml +++ /dev/null @@ -1,599 +0,0 @@ -locale: en -name: English - -strings: - # Application - app.title: "Sudoku" - - # Menu items - menu.game: "Game" - menu.new_game: "New Game" - menu.reset_puzzle: "Reset Puzzle" - menu.save: "Save" - menu.load: "Load" - menu.statistics: "Statistics" - menu.export_aggregate: "Export Aggregate Stats to CSV" - menu.export_sessions: "Export Game Sessions to CSV" - menu.exit: "Exit" - menu.edit: "Edit" - menu.undo: "Undo" - menu.redo: "Redo" - menu.clear_cell: "Clear Cell" - menu.help: "Help" - menu.get_hint: "Get Hint" - menu.about: "About" - menu.training_mode: "Training Mode" - menu.analyze_position: "Analyze Position" - menu.resume_game: "Resume Game" - menu.settings: "Settings..." - menu.third_party_licenses: "Third-Party Licenses" - - # Toolbar - toolbar.new_game: "\u25b6 New Game" - toolbar.difficulty: "Difficulty:" - toolbar.hints: "Hints:" - toolbar.rating: "Rating:" - toolbar.help_icon: "[?]" - - # Difficulty names - difficulty.easy: "Easy" - difficulty.medium: "Medium" - difficulty.hard: "Hard" - difficulty.expert: "Expert" - difficulty.master: "Master" - difficulty.unknown: "Unknown" - - # Action buttons - button.check_solution: "Check Solution" - button.reset_puzzle: "Reset Puzzle" - button.undo: "Undo" - button.redo: "Redo" - button.undo_to_valid: "Undo to Valid" - button.auto_notes_on: "Auto Notes: ON" - button.auto_notes_off: "Auto Notes: OFF" - button.fill_notes: "Fill Notes" - button.clear_notes: "Clear Notes" - button.undo_until_valid: "Undo Until Valid" - - # Input modes - mode.normal: "Normal" - mode.notes: "Notes" - mode.color: "Color" - - # Training mode - training.title: "Training Mode" - training.analyze_title: "Analyze Position" - training.select_technique: "Select a technique to practice:" - training.back_to_game: "Back to Game" - training.group.foundations: "Foundations" - training.group.subset_basics: "Subset Basics" - training.group.intersections: "Intersections & Quads" - training.group.basic_fish: "Basic Fish & Wings" - training.group.links: "Links & Rectangles" - training.group.advanced_fish: "Advanced Fish & Wings" - training.group.finned_fish: "Advanced Fish (Finned)" - training.group.chains: "Chains & Set Logic" - training.group.inference: "Inference Engines" - training.what_it_is: "What It Is:" - training.what_to_look_for: "What to Look For:" - training.start_exercises: "Start Exercises" - training.back: "Back" - training.difficulty_points: "{0} difficulty points" - training.prerequisites: "Prerequisites: " - training.exercise_header: "Exercise {0} / {1} - {2}" - training.color: "Color:" - training.submit: "Submit" - training.hint: "Hint" - training.skip: "Skip" - training.quit_lesson: "Quit Lesson" - training.next_exercise: "Next Exercise" - training.retry: "Retry" - training.show_solution: "Show Solution" - training.score: "Score: {0} / {1}" - training.correct: "Correct!" - training.partially_correct: "Partially Correct" - training.incorrect: "Incorrect" - training.lesson_complete: "Lesson Complete!" - training.try_again: "Try Again" - training.pick_technique: "Pick Technique" - training.return_to_game: "Return to Game" - training.technique: "Technique: {0}" - training.hints_used: "Hints used: {0}" - training.mastery: "Mastery: {0}" - training.points_fmt: "{0} ({1} pts)" - training.prereq_not_met: "Prerequisites not met" - training.recommended: "Recommended next technique" - training.applicable: "Applicable at current position" - training.excellent: "Excellent! You've mastered this technique." - training.good_progress: "Good progress. Try again for a higher score." - training.keep_practicing: "Keep practicing! Review the theory and try again." - training.error_backtracking: "Cannot practice Backtracking — it is not a logical technique." - training.error_no_step: "No applicable step found for this technique." - training.correct_continue: "Correct! {0} Find the next one." - training.feedback_correct: "Correct! {0}" - training.feedback_partial: "Partially correct. {0}" - training.feedback_incorrect: "Not quite. {0}" - training.feedback_unknown: "Unknown result." - mastery.beginner: "Beginner" - mastery.intermediate: "Intermediate" - mastery.proficient: "Proficient" - mastery.mastered: "Mastered" - - # Status bar - status.completed: "Completed!" - status.playing: "Playing" - status.ready: "Ready" - status.press_f1: "Press F1 to show menu" - status.in_progress: "In Progress" - - # Game board - board.no_game_loaded: "No game loaded. Start a new game!" - - # Dialogs — New Game - dialog.new_game: "New Game" - dialog.select_difficulty: "Select difficulty:" - dialog.start_game: "Start Game" - dialog.cancel: "Cancel" - dialog.new_game_confirm: "Start a new {0} game?\nCurrent progress will be lost." - - # Dialogs — Reset - dialog.reset_puzzle: "Reset Puzzle" - dialog.reset_warning: "All progress on this puzzle will be lost, including placed numbers, notes, and hints. The timer will restart." - dialog.reset: "Reset" - - # Dialogs — Save - dialog.save_game: "Save Game" - dialog.enter_save_name: "Enter save name:" - dialog.save: "Save" - save.preview_title: "Current Game" - save.preview_difficulty: "Difficulty" - save.preview_time: "Time" - save.preview_moves: "Moves" - save.preview_mistakes: "Mistakes" - save.name_placeholder: "Enter save name..." - save.name_empty: "Please enter a save name." - save.overwrite_confirm: "A save named \"{0}\" already exists. Overwrite it?" - - # Dialogs — Load - dialog.load_game: "Load Game" - dialog.recent_saves: "Recent saves:" - load.col_name: "Name" - load.col_difficulty: "Difficulty" - load.col_date: "Last Modified" - load.col_time: "Elapsed" - load.col_rating: "Rating" - - # Dialogs — Statistics (parameterized with fmt-style {0}) - dialog.statistics: "Statistics" - dialog.close: "Close" - stats.games_played: "Games Played: {0}" - stats.games_completed: "Games Completed: {0}" - stats.completion_rate: "Completion Rate: {0:.1f}%" - stats.best_time: "Best Time: {0}" - stats.average_time: "Average Time: {0}" - stats.current_streak: "Current Streak: {0}" - stats.best_streak: "Best Streak: {0}" - stats.time_na: "N/A" - stats.tab_overview: "Overview" - stats.tab_per_difficulty: "Per Difficulty" - stats.tab_recent_games: "Recent Games" - stats.total_moves: "Total Moves" - stats.total_hints: "Total Hints Used" - stats.total_mistakes: "Total Mistakes" - stats.total_time: "Total Time Played" - stats.row_played: "Played" - stats.row_completed: "Completed" - stats.row_best_time: "Best Time" - stats.row_avg_time: "Avg Time" - stats.row_avg_rating: "Avg SE Rating" - stats.col_date: "Date" - stats.col_difficulty: "Difficulty" - stats.col_time: "Time" - stats.col_rating: "Rating" - stats.col_moves: "Moves" - stats.col_mistakes: "Mistakes" - - # Dialogs — About - dialog.about: "About" - about.sudoku_game: "Sudoku Game" - about.built_with: "Built with:" - about.description: "A feature-rich offline Sudoku application." - rating.format: "SE {0}" - - # Dialogs — Settings - dialog.settings: "Settings" - dialog.third_party_licenses: "Third-Party Licenses" - settings.tab_gameplay: "Gameplay" - settings.tab_display: "Display" - settings.max_hints: "Maximum Hints:" - settings.auto_save_interval: "Auto-save Interval:" - settings.default_difficulty: "Default Difficulty:" - settings.seconds_suffix: " seconds" - settings.highlight_conflicts: "Highlight Conflicts" - settings.show_hints: "Show Hints" - settings.collect_detailed_stats: "Collect detailed match statistics" - settings.encrypt_detailed_stats: "Encrypt session data" - settings.encrypt_detailed_stats_tooltip: "Session data is encrypted by default for privacy. Disable to inspect the raw data file yourself." - settings.tab_statistics: "Statistics" - stats.delete_prompt: "Disabling session tracking will stop recording per-game statistics. Would you like to delete existing session history?" - stats.sessions_deleted: "Session history deleted." - - # Dialogs — Puzzle Difficulty - dialog.puzzle_difficulty: "Puzzle Difficulty" - dialog.puzzle_rating: "Puzzle Rating: SE {0}" - dialog.techniques_required: "Techniques required to solve:" - dialog.no_technique_details: "No technique details available." - - # Toolbar — Rating button format - toolbar.rating_with_techniques: "SE {0} ({1} techniques)" - toolbar.rating_simple: "SE {0}" - - # Tooltips — Rating scale - tooltip.rating_scale: "Sudoku Explainer Rating Scale:" - tooltip.techniques_required: "Techniques Required:" - tooltip.input_mode: "Input mode (Space to cycle, N for Notes)" - tooltip.place_digit: "Place {0} in selected cell" - tooltip.eliminate_digit: "Eliminate {0} from selected cell" - - # Toast notifications - toast.game_saved: "Game saved successfully" - toast.aggregate_exported: "Aggregate stats exported to CSV" - toast.sessions_exported: "Game sessions exported to CSV" - toast.export_failed: "Export failed: {0}" - toast.no_strategies: "No logical strategies found at this position." - - # Sidebar - sidebar.difficulty: "Difficulty: {0}" - sidebar.rating: "Rating: {0}" - sidebar.language: "Language" - - # ViewModel — Error messages - error.generate_puzzle: "Failed to generate puzzle" - error.load_game: "Failed to load game" - error.no_active_game: "No active game to save" - error.save_game: "Failed to save game" - error.export_stats: "Failed to export statistics" - error.export_aggregate: "Failed to export aggregate stats" - error.export_sessions: "Failed to export game sessions" - error.file_access: "File access error" - error.serialization: "Serialization error" - error.unknown: "Unknown error" - - # ViewModel — Status messages - status.no_valid_state: "No valid state in history" - status.board_valid: "Board is already valid" - status.undone_to_valid: "Undone to last valid state" - status.puzzle_completed: "Puzzle completed in {0}:{1}! New game started." - status.solution_errors: "Solution has errors. Keep trying!" - - # ViewModel — Hint messages - hint.no_remaining: "No hints remaining (0/10 used)" - hint.select_cell: "Please select a cell first" - hint.cannot_reveal_given: "Cannot reveal hint for given cells" - hint.cell_has_value: "Cell already has a value" - hint.no_technique: "No logical technique found for this puzzle" - hint.suggestion_place: "Suggestion: Place {0} at R{1}C{2}" - - # ViewModel — Coaching hint messages - coaching.no_remaining: "No coaching hints remaining" - coaching.no_technique: "No logical technique found" - coaching.max_level: "Maximum coaching level reached" - coaching.level_header: "Level {0}/3" - coaching.try_it: "Try applying this step yourself, then press Check to verify." - coaching.check_correct: "Correct! You found all {0}/{1}." - coaching.check_partial: "{0}/{1} correct, {2} missed." - coaching.check_wrong: "Some actions were incorrect. {0}/{1} correct, {2} wrong." - coaching.applied: "Step applied." - coaching.button_check: "Check" - coaching.button_apply: "Apply" - coaching.try_it_label: "Try it!" - coaching.what_to_look_for: "What to look for: " - coaching.check_zero: "0/{0} correct — try making some changes first." - - # Training hints — progressive hint text - hint.singles.l1: "Look at cell {0}." - hint.singles.l2_region: "Focus on {0} — count the candidates." - hint.singles.l2_no_region: "Count the candidates in cell {0}." - hint.singles.l3: "The value is {0}." - hint.subsets.l1_region: "Focus on {0}." - hint.subsets.l1_no_region: "Look for cells that share the same candidates in a unit." - hint.subsets.l2_values: "These cells form a [{0}] subset. Values in the subset can only go in these cells — eliminate them from other cells in the region." - hint.subsets.l2_no_values: "These cells form the subset. Values in the subset can only go in these cells — eliminate them from other cells in the region." - hint.subsets.l3: "Eliminate candidates from cells that see all subset cells." - hint.intersections.l1_value: "Look for value {0} confined to an intersection." - hint.intersections.l1_no_value: "Look for a candidate confined to the intersection of a box and a line." - hint.intersections.l2: "The intersection cells. The candidate is confined to these cells — eliminate it from other cells in the line or box outside this intersection." - hint.intersections.l3: "Eliminate the candidate from cells outside the intersection." - hint.fish.l1_value: "Look for a fish pattern on value {0}." - hint.fish.l1_no_value: "Look for a fish pattern (rows/columns with restricted candidate positions)." - hint.fish.l2: "Base and cover sets. Blue cells are the base set (rows/columns where the candidate is restricted). Green cells are the cover set. Eliminate the candidate from cover set cells that aren't in the base set." - hint.fish.l3: "Eliminate the candidate from cover set cells outside the base set." - hint.wings.l1: "Find the pivot cell at {0}." - hint.wings.l2: "Pivot and wing cells. The orange pivot connects to the green wings. Candidates shared by both wings can be eliminated from cells that see all wing endpoints." - hint.wings.l3: "Eliminate the shared candidate from cells that see all wing endpoints." - hint.single_digit.l1_value: "Look for conjugate pairs on value {0}." - hint.single_digit.l1_no_value: "Look for conjugate pairs (cells where a digit appears exactly twice in a unit)." - hint.single_digit.l2: "The chain cells. These cells form conjugate pairs (a digit appears exactly twice in a unit). Follow the alternating pattern to find eliminations." - hint.single_digit.l3: "Cells that see both endpoints of the pattern can be eliminated." - hint.coloring.l1_value: "Build a coloring chain on value {0}." - hint.coloring.l1_no_value: "Start coloring conjugate pairs with two alternating colors." - hint.coloring.l2: "The coloring chain. Blue and green are two alternating colors — one must be true, one false. Cells that see both colors can have the candidate eliminated." - hint.coloring.l3: "One color must be false — eliminate from cells that see both colors." - hint.unique_rect.l1: "Look for a deadly pattern — four cells forming a rectangle across two boxes." - hint.unique_rect.l2: "The rectangle corners. These four cells across two boxes form a potential deadly pattern. To keep the puzzle unique, eliminate the candidate that would complete the rectangle." - hint.unique_rect.l3: "To avoid the deadly pattern, eliminate the candidate that would complete it." - hint.chains.l1_pos: "Start the chain from cell {0}." - hint.chains.l1_no_pos: "Look for a chain of linked cells with alternating strong/weak links." - hint.chains.l2: "The chain path. Follow the alternating strong (blue) and weak (green) links. The chain's logic forces a conclusion at the endpoints." - hint.chains.l3_placement: "All chains lead to value {0} at {1}." - hint.chains.l3_elimination: "Eliminate candidates that contradict the chain logic." - hint.set_logic.l1: "Look for an Almost Locked Set (a group of N cells with N+1 candidates)." - hint.set_logic.l2: "The ALS cells and restricted common. An ALS is N cells with N+1 candidates. The restricted common candidate links the sets — eliminations apply to cells that see all relevant ALS members." - hint.set_logic.l3: "Eliminate candidates from cells that see all relevant ALS members." - hint.special.l1: "Look for the cell with three candidates (the only non-bivalue cell)." - hint.special.l2: "The key cell is {0}." - - # Technique descriptions — what_it_is and what_to_look_for - tech.desc.naked_single.what_it_is: "A cell has only one possible candidate left. All other values are eliminated by row, column, and box constraints." - tech.desc.naked_single.what_to_look_for: "Look for cells where 8 of the 9 values are already present in the cell's row, column, or box." - tech.desc.hidden_single.what_it_is: "A value can only go in one cell within a row, column, or box. Even though the cell may have multiple candidates, only this value has no other place in the region." - tech.desc.hidden_single.what_to_look_for: "For each region, check if any value has only one possible cell." - tech.desc.naked_pair.what_it_is: "Two cells in the same region each contain exactly the same two candidates. Those two values must go in those two cells, so they can be eliminated from all other cells in the region." - tech.desc.naked_pair.what_to_look_for: "Find two cells in a row, column, or box that share the same pair of candidates." - tech.desc.naked_triple.what_it_is: "Three cells in a region collectively contain exactly three candidates. Each cell has a subset of those three values. Those values can be eliminated from other cells in the region." - tech.desc.naked_triple.what_to_look_for: "Find three cells in a region whose combined candidates form a set of exactly three values." - tech.desc.hidden_pair.what_it_is: "Two values in a region appear as candidates in exactly the same two cells. Other candidates in those two cells can be eliminated." - tech.desc.hidden_pair.what_to_look_for: "For each region, find two values that appear only in the same two cells." - tech.desc.hidden_triple.what_it_is: "Three values in a region appear as candidates in exactly three cells. Other candidates in those cells can be eliminated." - tech.desc.hidden_triple.what_to_look_for: "For each region, find three values confined to exactly three cells." - tech.desc.pointing_pair.what_it_is: "A candidate in a box is confined to a single row or column. That candidate can be eliminated from the rest of that row or column outside the box." - tech.desc.pointing_pair.what_to_look_for: "In each box, check if a candidate appears only in one row or one column." - tech.desc.box_line_reduction.what_it_is: "A candidate in a row or column is confined to a single box. That candidate can be eliminated from the rest of the box outside that row or column." - tech.desc.box_line_reduction.what_to_look_for: "In each row/column, check if a candidate appears only within one box." - tech.desc.naked_quad.what_it_is: "Four cells in a region collectively contain exactly four candidates. Those values can be eliminated from other cells in the region." - tech.desc.naked_quad.what_to_look_for: "Find four cells in a region whose combined candidates form a set of exactly four values." - tech.desc.hidden_quad.what_it_is: "Four values in a region appear as candidates in exactly four cells. Other candidates in those cells can be eliminated." - tech.desc.hidden_quad.what_to_look_for: "For each region, find four values confined to exactly four cells." - tech.desc.x_wing.what_it_is: "A candidate appears in exactly two cells in each of two rows, and those cells are in the same two columns. The candidate can be eliminated from other cells in those columns." - tech.desc.x_wing.what_to_look_for: "Find a candidate forming a rectangle pattern: two rows, two columns, four cells." - tech.desc.xy_wing.what_it_is: "A pivot cell with candidates {A,B} sees two wing cells: one with {A,C} and one with {B,C}. Value C can be eliminated from any cell that sees both wings." - tech.desc.xy_wing.what_to_look_for: "Find a bivalue cell (pivot) that sees two other bivalue cells sharing one candidate each." - tech.desc.swordfish.what_it_is: "A candidate appears in 2-3 cells in each of three rows, and those cells fall in exactly three columns. The candidate can be eliminated from other cells in those columns." - tech.desc.swordfish.what_to_look_for: "Extend the X-Wing pattern to three rows and three columns." - tech.desc.skyscraper.what_it_is: "Two conjugate pairs for a digit share one endpoint in the same row or column. The digit can be eliminated from cells that see both non-shared endpoints." - tech.desc.skyscraper.what_to_look_for: "Find two rows (or columns) each with exactly two cells for a digit, sharing one column (or row)." - tech.desc.two_string_kite.what_it_is: "A conjugate pair in a row and a conjugate pair in a column are connected through a box. The digit can be eliminated from the cell that sees both unconnected endpoints." - tech.desc.two_string_kite.what_to_look_for: "Find a row pair and column pair for the same digit connected via a shared box." - tech.desc.xyz_wing.what_it_is: "A pivot cell with candidates {A,B,C} sees a wing with {A,B} and a wing with {A,C}. Value A can be eliminated from cells that see all three cells." - tech.desc.xyz_wing.what_to_look_for: "Find a trivalue pivot seeing two bivalue wings that each share two candidates with the pivot." - tech.desc.unique_rectangle.what_it_is: "Four cells forming a rectangle across two boxes would create a deadly pattern (two solutions) if they all had the same two candidates. Extra candidates in some cells can force eliminations to avoid this ambiguity." - tech.desc.unique_rectangle.what_to_look_for: "Find four cells in a rectangle across two boxes sharing the same two candidates." - tech.desc.w_wing.what_it_is: "Two cells with the same pair of candidates {A,B} are connected by a strong link on value A. Value B can be eliminated from cells that see both endpoints." - tech.desc.w_wing.what_to_look_for: "Find two identical bivalue cells connected by a conjugate pair on one of their values." - tech.desc.simple_coloring.what_it_is: "For a single digit, build chains of conjugate pairs and assign two colors. If both colors appear in the same region, one color is false and its candidates are eliminated." - tech.desc.simple_coloring.what_to_look_for: "Pick a digit, trace conjugate pairs, color alternately. Check for color conflicts." - tech.desc.finned_x_wing.what_it_is: "An X-Wing pattern with one extra candidate cell (the fin) in the same box as a corner. Eliminations are restricted to cells that see both the fin and the X-Wing column." - tech.desc.finned_x_wing.what_to_look_for: "Find an X-Wing where one row has an extra candidate cell in the same box." - tech.desc.remote_pairs.what_it_is: "A chain of bivalue cells all containing the same pair {A,B}, where each adjacent pair shares a region. Cells seeing both endpoints of an even-length chain lose both values." - tech.desc.remote_pairs.what_to_look_for: "Find a chain of identical bivalue cells connected through shared regions." - tech.desc.bug.what_it_is: "If all unsolved cells have exactly two candidates except one cell with three, the puzzle would have multiple solutions unless the trivalue cell is set to the value that appears three times in its row, column, or box." - tech.desc.bug.what_to_look_for: "Check if only one cell has more than two candidates. If so, find its odd-count value." - tech.desc.jellyfish.what_it_is: "A candidate appears in 2-4 cells in each of four rows, and those cells fall in exactly four columns. The candidate can be eliminated from other cells in those columns." - tech.desc.jellyfish.what_to_look_for: "Extend the Swordfish pattern to four rows and four columns." - tech.desc.finned_swordfish.what_it_is: "A Swordfish pattern with extra candidate cells (fins) in the same box. Eliminations are restricted to cells seeing both the fin box and the Swordfish columns." - tech.desc.finned_swordfish.what_to_look_for: "Find a Swordfish where one row has extra candidates in the same box." - tech.desc.empty_rectangle.what_it_is: "A digit's candidates in a box form an L-shape or cross, leaving an empty rectangle. Combined with a conjugate pair outside the box, this eliminates the digit from a target cell." - tech.desc.empty_rectangle.what_to_look_for: "Find a box where a digit's candidates leave an empty rectangle, connected to a conjugate pair." - tech.desc.wxyz_wing.what_it_is: "A four-cell wing pattern: a pivot and three wings collectively contain four candidates, and a shared candidate Z can be eliminated from cells seeing all cells containing Z." - tech.desc.wxyz_wing.what_to_look_for: "Find a group of four cells with exactly four combined candidates sharing a common value." - tech.desc.finned_jellyfish.what_it_is: "A Jellyfish pattern with extra fin cells in the same box. Eliminations are restricted to cells seeing both the fin box and the Jellyfish columns." - tech.desc.finned_jellyfish.what_to_look_for: "Find a Jellyfish where one row has extra candidates forming a fin." - tech.desc.xy_chain.what_it_is: "A chain of bivalue cells where consecutive cells share a candidate value, alternating between the two candidates. The value shared by the chain's endpoints can be eliminated from cells that see both endpoints." - tech.desc.xy_chain.what_to_look_for: "Build a chain of bivalue cells connected by shared candidates. Check the endpoints." - tech.desc.multi_coloring.what_it_is: "Build separate conjugate pair chains (clusters) for a digit and color each. When two clusters interact (cells in different clusters see each other), eliminations can be made from cells that see conflicting colors across clusters." - tech.desc.multi_coloring.what_to_look_for: "Color multiple conjugate chains for one digit, then check cross-cluster interactions." - tech.desc.als_xz.what_it_is: "Two Almost Locked Sets (each has N cells with N+1 candidates) share a restricted common candidate X. A second common candidate Z can be eliminated from cells that see all Z-cells in both sets." - tech.desc.als_xz.what_to_look_for: "Find two groups of cells that are almost locked, sharing a restricted common candidate." - tech.desc.sue_de_coq.what_it_is: "An intersection of a line and box contains 2-3 cells whose candidates can be covered by two Almost Locked Sets (one from the line remainder, one from the box remainder). Extra ALS candidates can be eliminated from their respective remainders." - tech.desc.sue_de_coq.what_to_look_for: "Find an intersection where candidates can be partitioned into two covering ALS." - tech.desc.forcing_chain.what_it_is: "For a cell with 2-3 candidates, assume each candidate is true and propagate the consequences. If all assumptions lead to the same conclusion (a placement or elimination), that conclusion must be true." - tech.desc.forcing_chain.what_to_look_for: "Pick a cell with few candidates. Try each value and propagate. Look for common outcomes." - tech.desc.nice_loop.what_it_is: "Build a chain of alternating strong and weak links between (cell, digit) pairs. If the chain forms a loop or its endpoints share a digit, eliminations can be derived from the alternating inference chain rules." - tech.desc.nice_loop.what_to_look_for: "Trace alternating strong/weak links. Check if endpoints share a digit for eliminations." - tech.desc.x_cycles.what_it_is: "For a single digit, build a chain of alternating strong and weak links. Type 1 (continuous loop) eliminates the digit from cells seeing weak link endpoints. Type 2 places the digit at a strong-strong discontinuity. Type 3 eliminates at a weak-weak discontinuity." - tech.desc.x_cycles.what_to_look_for: "For each digit, trace alternating strong/weak links and look for cycles or discontinuities." - tech.desc.three_d_medusa.what_it_is: "Multi-digit coloring: build a graph of (cell, digit) pairs connected by strong links (conjugate pairs and bivalue cells). Color with two alternating colors. Apply six rules to find contradictions or trap eliminations." - tech.desc.three_d_medusa.what_to_look_for: "Extend single-digit coloring to multiple digits via bivalue cell connections." - tech.desc.hidden_unique_rectangle.what_it_is: "A deadly rectangle pattern where one or more corners have the UR values hidden among other candidates. Strong links on UR values in shared units force eliminations to avoid the deadly pattern." - tech.desc.hidden_unique_rectangle.what_to_look_for: "Find a rectangle across two boxes where the UR values are present but hidden by extras." - tech.desc.avoidable_rectangle.what_it_is: "Like Unique Rectangle but using the distinction between given clues and solver-placed values. If three solver-placed corners of a rectangle have values {A,B}, the fourth unsolved corner cannot complete the deadly pattern." - tech.desc.avoidable_rectangle.what_to_look_for: "Find rectangles where three corners are solver-placed (not givens) with two values." - tech.desc.als_xy_wing.what_it_is: "Three non-overlapping Almost Locked Sets linked by restricted commons: A-B linked by X, B-C linked by Y (Y != X). Common value Z in both A and C can be eliminated from cells seeing all Z-cells in both A and C." - tech.desc.als_xy_wing.what_to_look_for: "Find three ALSs forming a chain with two restricted common candidates." - tech.desc.death_blossom.what_it_is: "A stem cell with 2-3 candidates, each linked to a petal ALS via restricted common. A value Z common across all petals (but not in the stem) can be eliminated from cells seeing all Z-cells in all petals." - tech.desc.death_blossom.what_to_look_for: "Find a cell whose candidates each connect to an ALS via restricted common." - tech.desc.vwxyz_wing.what_it_is: "A five-cell wing pattern: a pivot and four wings collectively contain five candidates. The non-restricted shared value Z can be eliminated from cells seeing all Z-cells." - tech.desc.vwxyz_wing.what_to_look_for: "Find five cells with exactly five combined candidates and a restricted elimination value." - tech.desc.franken_fish.what_it_is: "A fish pattern (X-Wing/Swordfish/Jellyfish) where base and cover sets are mixed rows/columns and boxes. At least one base set must be a box. Eliminates from cover cells outside the base." - tech.desc.franken_fish.what_to_look_for: "Look for fish patterns that include boxes as base or cover sets." - tech.desc.grouped_x_cycles.what_it_is: "Extends X-Cycles by allowing grouped nodes: 2-3 cells in the same box on the same row or column that all have a candidate digit. Same Type 1/2/3 rules apply." - tech.desc.grouped_x_cycles.what_to_look_for: "Build X-Cycle chains using grouped box nodes alongside individual cells." - tech.desc.sashimi_x_wing.what_it_is: "A fish pattern where one base row has only one candidate position instead of two. The missing position is compensated by a fin cell, restricting eliminations to the fin's box." - tech.desc.sashimi_x_wing.what_to_look_for: "Look for an X-Wing-like pattern where one row is incomplete — it only has the candidate in one of the two expected columns, plus an extra fin cell." - tech.desc.sashimi_swordfish.what_it_is: "A 3-row fish pattern where at least one base row has fewer candidate positions than expected. The missing position creates a fin that restricts eliminations." - tech.desc.sashimi_swordfish.what_to_look_for: "Find a Swordfish shape where one row only covers 1 of the 3 base columns, plus a fin." - tech.desc.sashimi_jellyfish.what_it_is: "A 4-row fish pattern where at least one base row has fewer candidate positions than expected. The missing position creates a fin restricting eliminations." - tech.desc.sashimi_jellyfish.what_to_look_for: "Find a Jellyfish shape where one row only covers 1 of the 4 base columns, plus a fin." - tech.desc.unit_forcing_chain.what_it_is: "For a digit in a unit with 2-3 positions, assume the digit goes in each position and propagate. If all branches lead to the same conclusion, that conclusion is true." - tech.desc.unit_forcing_chain.what_to_look_for: "Find a unit where a digit appears in few cells, then try each placement." - tech.desc.region_forcing_chain.what_it_is: "For a digit in a box with 2-3 positions, assume the digit goes in each position and propagate. If all branches lead to the same conclusion, that conclusion is true." - tech.desc.region_forcing_chain.what_to_look_for: "Find a box where a digit appears in few cells, then try each placement." - tech.desc.mutant_fish.what_it_is: "A fish pattern where BOTH base and cover sets freely mix rows, columns, and boxes. Unlike Franken Fish (one mixed side), Mutant Fish requires both sides to contain at least 2 different unit types. Eliminates from cover cells outside the base." - tech.desc.mutant_fish.what_to_look_for: "Look for fish patterns where both the base set and cover set mix rows, columns, and boxes." - tech.desc.kraken_fish.what_it_is: "Extends finned fish by using chain propagation to verify eliminations outside the fin's box. For each candidate that a standard finned fish would reject (outside the fin's box), place the digit at the fin cell and propagate. If the target still loses the candidate, the elimination is valid regardless of whether the fin is true or false." - tech.desc.kraken_fish.what_to_look_for: "Find a finned fish pattern, then check if chain propagation from the fin cell eliminates the digit from cells outside the fin's box." - tech.desc.als_chain.what_it_is: "A generalized chain of 4-6 Almost Locked Sets linked by distinct restricted commons. Values common across the chain endpoints can be eliminated from cells that see all relevant ALS members." - tech.desc.als_chain.what_to_look_for: "Find a chain of ALSs where each adjacent pair shares a restricted common." - tech.desc.unique_loop.what_it_is: "A deadly pattern where 4-6 cells form a loop, each consecutive pair sharing a unit (row, column, or box). All cells contain the same candidate pair {A,B}. If all cells had only {A,B}, two solutions would exist. Cells with extra candidates must keep them." - tech.desc.unique_loop.what_to_look_for: "Find a loop of 4-6 cells across at least 2 boxes where each cell has candidates {A,B} and each consecutive pair shares a row, column, or box. If exactly one cell has extras, eliminate A and B from it." - tech.desc.junior_exocet.what_it_is: "A base pair of cells in a box whose candidates must appear in specific target cells in other boxes along cross-lines. Candidates not matching the base pair pattern can be eliminated from the target cells." - tech.desc.junior_exocet.what_to_look_for: "Find a base pair in a box with target cells in aligned boxes along cross-lines." - tech.desc.continuous_nice_loop.what_it_is: "A continuous alternating inference chain (AIC) that forms a complete loop. Every weak link in the loop produces eliminations: the digit can be removed from any cell outside the loop that sees both endpoints of a weak link." - tech.desc.continuous_nice_loop.what_to_look_for: "Build an AIC where the chain closes back on itself with consistent alternating links. Every weak link segment yields eliminations from external cells seeing both endpoints." - tech.desc.grouped_nice_loop.what_it_is: "Extends Nice Loop (AIC) with grouped nodes: 2-3 cells in the same box on the same row or column that all share a candidate digit. Combines multi-digit cell-based links (bivalue strong links) with single-digit grouped unit links for stronger chains." - tech.desc.grouped_nice_loop.what_to_look_for: "Build AIC chains using grouped box nodes alongside individual cells. Look for discontinuous Type 2 chains where both endpoints assert the same digit." - tech.desc.backtracking.what_it_is: "A brute-force trial-and-error method. Not a logical technique — used as a fallback when no logical strategy can make progress." - tech.desc.backtracking.what_to_look_for: "This technique is not used in training exercises." - tech.desc.unknown.what_it_is: "Unknown technique." - tech.desc.unknown.what_to_look_for: "No identification tips available." - - # ViewModel — Technique formatting - technique.points_fmt: "{0} (SE {1})" - technique.backtracking: "Backtracking (trial & error)" - - # ViewModel — Statistics error strings - stats_err.invalid_data: "Invalid game data" - stats_err.file_access: "File access error" - stats_err.serialization: "Serialization error" - stats_err.invalid_difficulty: "Invalid difficulty" - stats_err.game_not_started: "Game not started" - stats_err.game_already_ended: "Game already ended" - stats_err.unknown: "Unknown statistics error" - - # Technique names - tech.naked_single: "Naked Single" - tech.hidden_single: "Hidden Single" - tech.naked_pair: "Naked Pair" - tech.naked_triple: "Naked Triple" - tech.hidden_pair: "Hidden Pair" - tech.hidden_triple: "Hidden Triple" - tech.pointing_pair: "Pointing Pair" - tech.box_line_reduction: "Box/Line Reduction" - tech.naked_quad: "Naked Quad" - tech.hidden_quad: "Hidden Quad" - tech.x_wing: "X-Wing" - tech.xy_wing: "XY-Wing" - tech.swordfish: "Swordfish" - tech.skyscraper: "Skyscraper" - tech.two_string_kite: "2-String Kite" - tech.xyz_wing: "XYZ-Wing" - tech.unique_rectangle: "Unique Rectangle" - tech.w_wing: "W-Wing" - tech.simple_coloring: "Simple Coloring" - tech.finned_x_wing: "Finned X-Wing" - tech.remote_pairs: "Remote Pairs" - tech.bug: "BUG" - tech.jellyfish: "Jellyfish" - tech.finned_swordfish: "Finned Swordfish" - tech.empty_rectangle: "Empty Rectangle" - tech.wxyz_wing: "WXYZ-Wing" - tech.finned_jellyfish: "Finned Jellyfish" - tech.xy_chain: "XY-Chain" - tech.multi_coloring: "Multi-Coloring" - tech.als_xz: "ALS-XZ" - tech.sue_de_coq: "Sue de Coq" - tech.forcing_chain: "Forcing Chain" - tech.nice_loop: "Nice Loop" - tech.x_cycles: "X-Cycles" - tech.three_d_medusa: "3D Medusa" - tech.hidden_unique_rectangle: "Hidden Unique Rectangle" - tech.avoidable_rectangle: "Avoidable Rectangle" - tech.als_xy_wing: "ALS-XY-Wing" - tech.death_blossom: "Death Blossom" - tech.vwxyz_wing: "VWXYZ-Wing" - tech.franken_fish: "Franken Fish" - tech.grouped_x_cycles: "Grouped X-Cycles" - tech.sashimi_x_wing: "Sashimi X-Wing" - tech.sashimi_swordfish: "Sashimi Swordfish" - tech.sashimi_jellyfish: "Sashimi Jellyfish" - tech.unit_forcing_chain: "Unit Forcing Chain" - tech.region_forcing_chain: "Region Forcing Chain" - tech.mutant_fish: "Mutant Fish" - tech.kraken_fish: "Kraken Fish" - tech.als_chain: "ALS Chain" - tech.junior_exocet: "Junior Exocet" - tech.unique_loop: "Unique Loop" - tech.continuous_nice_loop: "Continuous Nice Loop" - tech.grouped_nice_loop: "Grouped Nice Loop" - tech.backtracking_name: "Backtracking" - tech.unknown: "Unknown Technique" - - # Region names - region.row: "Row" - region.column: "Column" - region.box: "Box" - region.unknown: "Unknown Region" - - # Position format (R=Row, C=Column, 1-indexed) - position.fmt: "R{0}C{1}" - - # Explanation templates - explain.naked_single: "Naked Single at {0}: only value {1} is possible" - explain.hidden_single: "Hidden Single at {0}: value {1} can only appear in this cell within its region" - explain.naked_pair: "Naked Pair [{0}] at {1} in {2} eliminates candidates from other cells" - explain.naked_triple: "Naked Triple [{0}] at {1} in {2} eliminates candidates from other cells" - explain.hidden_pair: "Hidden Pair [{0}] at {1} in {2} eliminates other candidates from these cells" - explain.hidden_triple: "Hidden Triple [{0}] at {1} in {2} eliminates other candidates from these cells" - explain.pointing_pair: "Pointing Pair: {0} in Box {1} confined to {2} {3} eliminates {0} from other cells in {2} {3}" - explain.box_line_reduction: "Box/Line Reduction: {0} in {1} {2} confined to Box {3} eliminates {0} from other cells in Box {3}" - explain.naked_quad: "Naked Quad [{0}] at {1} in {2} eliminates candidates from other cells" - explain.hidden_quad: "Hidden Quad [{0}] at {1} in {2} eliminates other candidates from these cells" - explain.x_wing_row: "X-Wing on value {0} in Rows {1} and {2}, Columns {3} and {4} eliminates {0} from other cells in those columns" - explain.x_wing_col: "X-Wing on value {0} in Columns {1} and {2}, Rows {3} and {4} eliminates {0} from other cells in those rows" - explain.xy_wing: "XY-Wing: pivot {0} {{{1},{2}}}, wing {3} {{{1},{4}}}, wing {5} {{{2},{4}}} eliminates {4} from cells seeing both wings" - explain.swordfish_row: "Swordfish on value {0} in Rows {1}, {2}, {3} and Columns {4}, {5}, {6} eliminates {0} from other cells in those columns" - explain.swordfish_col: "Swordfish on value {0} in Columns {1}, {2}, {3} and Rows {4}, {5}, {6} eliminates {0} from other cells in those rows" - explain.skyscraper: "Skyscraper on value {0}: conjugate pairs in {1} and {2} share endpoint {3} — eliminates {0} from cells seeing both {4} and {5}" - explain.two_string_kite: "2-String Kite on value {0}: row pair {1},{2} and column pair {3},{4} connected through shared box — eliminates {0} from cells seeing both endpoints" - explain.xyz_wing: "XYZ-Wing: pivot {0} {{{1},{2},{3}}}, wing {4} and wing {5} eliminate {3} from cells seeing all three" - explain.unique_rectangle: "Unique Rectangle: cells {0} with values {{{1},{2}}} — eliminates {1},{2} from {3} to avoid deadly pattern" - explain.w_wing: "W-Wing: cells {0} and {1} {{{2},{3}}} connected by strong link on {2} — eliminates {3} from cells seeing both" - explain.simple_coloring_contradiction: "Simple Coloring on {0}: same-color cells see each other — eliminates {0} from all cells of that color" - explain.simple_coloring_exclusion: "Simple Coloring on {0}: cell {1} sees both colors — eliminates {0} from {1}" - explain.unique_rectangle_type2: "Unique Rectangle Type 2: cells {0} with values {{{1},{2}}} — extra candidate {3} eliminated from cells seeing both floor cells in shared {4}" - explain.unique_rectangle_type3: "Unique Rectangle Type 3: cells {0} with values {{{1},{2}}} — floor extras form naked subset in {3}, eliminating from other cells" - explain.unique_rectangle_type4: "Unique Rectangle Type 4: cells {0} with values {{{1},{2}}} — strong link on {3} in {4} eliminates {5} from floor cells" - explain.unique_rectangle_type6: "Unique Rectangle Type 6: cells {0} with values {{{1},{2}}} — {3} is conjugate in both parallel lines of the rectangle, locking the pattern — eliminates extras from floor cells" - explain.finned_x_wing_row: "Finned X-Wing on value {0} in Rows {1} and {2}, Columns {3} and {4} with fin at {5} — eliminates {0} from cells in fin's box" - explain.finned_x_wing_col: "Finned X-Wing on value {0} in Columns {1} and {2}, Rows {3} and {4} with fin at {5} — eliminates {0} from cells in fin's box" - explain.sashimi_x_wing_row: "Sashimi X-Wing on value {0} in Rows {1} and {2}, Columns {3} and {4} with fin at {5} — eliminates {0} from cells in fin's box" - explain.sashimi_x_wing_col: "Sashimi X-Wing on value {0} in Columns {1} and {2}, Rows {3} and {4} with fin at {5} — eliminates {0} from cells in fin's box" - explain.remote_pairs: "Remote Pairs: chain of {{{0},{1}}} cells from {2} to {3} (length {4}) — eliminates {0},{1} from cells seeing both endpoints" - explain.bug: "BUG: all cells bivalue except {0} — value {1} must be placed to avoid deadly pattern" - explain.jellyfish_row: "Jellyfish on value {0} in Rows {1}, {2}, {3}, {4} and Columns {5}, {6}, {7}, {8} eliminates {0} from other cells in those columns" - explain.jellyfish_col: "Jellyfish on value {0} in Columns {1}, {2}, {3}, {4} and Rows {5}, {6}, {7}, {8} eliminates {0} from other cells in those rows" - explain.finned_swordfish_row: "Finned Swordfish on value {0} in Rows {1}, {2}, {3} with fin at {4} — eliminates {0} from cells in fin's box" - explain.finned_swordfish_col: "Finned Swordfish on value {0} in Columns {1}, {2}, {3} with fin at {4} — eliminates {0} from cells in fin's box" - explain.sashimi_swordfish_row: "Sashimi Swordfish on value {0} in Rows {1}, {2}, {3} with fin at {4} — eliminates {0} from cells in fin's box" - explain.sashimi_swordfish_col: "Sashimi Swordfish on value {0} in Columns {1}, {2}, {3} with fin at {4} — eliminates {0} from cells in fin's box" - explain.empty_rectangle: "Empty Rectangle on value {0}: ER in Box {1} with conjugate pair in {2} — eliminates {0} from {3}" - explain.wxyz_wing: "WXYZ-Wing: pivot {0} with wings {1}, {2}, {3} — eliminates {4} from cells seeing all four" - explain.finned_jellyfish_row: "Finned Jellyfish on value {0} in Rows {1}, {2}, {3}, {4} with fin at {5} — eliminates {0} from cells in fin's box" - explain.finned_jellyfish_col: "Finned Jellyfish on value {0} in Columns {1}, {2}, {3}, {4} with fin at {5} — eliminates {0} from cells in fin's box" - explain.sashimi_jellyfish_row: "Sashimi Jellyfish on value {0} in Rows {1}, {2}, {3}, {4} with fin at {5} — eliminates {0} from cells in fin's box" - explain.sashimi_jellyfish_col: "Sashimi Jellyfish on value {0} in Columns {1}, {2}, {3}, {4} with fin at {5} — eliminates {0} from cells in fin's box" - explain.xy_chain: "XY-Chain: chain of {0} bivalue cells from {1} to {2} — eliminates {3} from cells seeing both endpoints" - explain.multi_coloring_wrap: "Multi-Coloring on {0}: color sees both colors of another cluster — eliminates {0} from all cells of that color" - explain.multi_coloring_trap: "Multi-Coloring on {0}: cell {1} sees complementary colors from two clusters — eliminates {0}" - explain.als_xz: "ALS-XZ: ALS {0} and ALS {1} linked by restricted common {2} — eliminates {3} from cells seeing both ALSs" - explain.sue_de_coq: "Sue de Coq: intersection of {0} and Box {1} — eliminates candidates from rest of line and box" - explain.forcing_chain: "Forcing Chain: assuming each candidate in {0} leads to the same conclusion — {1}" - explain.nice_loop: "Nice Loop: alternating inference chain from {0} to {1} — eliminates {2}" - explain.x_cycles_type1: "X-Cycles on value {0}: continuous loop — eliminates {0} from cells seeing weak link endpoints" - explain.x_cycles_type2: "X-Cycles on value {0}: strong-strong discontinuity at {1} — places {0}" - explain.x_cycles_type3: "X-Cycles on value {0}: weak-weak discontinuity at {1} — eliminates {0} from {1}" - explain.three_d_medusa: "3D Medusa: multi-digit coloring — {0}" - explain.hidden_unique_rectangle: "Hidden Unique Rectangle: cells {0} with values {{{1},{2}}} — eliminates {3} from {4} to avoid deadly pattern" - explain.avoidable_rectangle: "Avoidable Rectangle: cells {0} with solved values {{{1},{2}}} — eliminates {3} from {4} to avoid deadly pattern" - explain.als_xy_wing: "ALS-XY-Wing: ALS {0}, ALS {1}, ALS {2} linked by X={3} and Y={4} — eliminates {5} from cells seeing Z-cells in A and C" - explain.death_blossom: "Death Blossom: stem {0} with petals {1} — eliminates {2} from cells seeing all petal Z-cells" - explain.vwxyz_wing: "VWXYZ-Wing: pivot {0} with wings {1}, {2}, {3}, {4} — eliminates {5} from cells seeing all Z-cells" - explain.franken_fish: "Franken {0} on value {1}: base {2}, cover {3} — eliminates {1} from cover cells outside base" - explain.mutant_fish: "Mutant Fish on value {0}: base {1}, cover {2} — eliminates {0} from {3} cover cell(s) outside base" - explain.grouped_x_cycles: "Grouped X-Cycles on value {0}: chain with grouped nodes — {1}" - explain.kraken_fish: "Kraken Fish on value {0}: finned fish with chain-verified eliminations from {1}" - explain.als_chain: "ALS Chain ({0} ALSs): eliminates {1} from cells seeing Z-cells in first and last ALS at {2}" - explain.junior_exocet: "Junior Exocet: base cells {0} and {1} with candidates {{{2}}} — targets {3} and {4} can only contain base candidates" - explain.unique_loop: "Unique Loop: cells {0} with values {{{1},{2}}} — eliminates {1},{2} from {3} to avoid deadly pattern" - explain.continuous_nice_loop: "Continuous Nice Loop: loop of {0} nodes — eliminates {1} candidate(s) via weak link logic" - explain.grouped_nice_loop: "Grouped Nice Loop: alternating inference chain from {0} to {1} — eliminates {2}" diff --git a/resources/translations/sudoku_de.ts b/resources/translations/sudoku_de.ts new file mode 100644 index 0000000..d7c2557 --- /dev/null +++ b/resources/translations/sudoku_de.ts @@ -0,0 +1,1961 @@ + + + + + Sudoku + + Sudoku + Sudoku + + + Game + Spiel + + + New Game + Neues Spiel + + + Reset Puzzle + Rätsel zurücksetzen + + + Save + Speichern + + + Load + Laden + + + Statistics + Statistiken + + + Export Aggregate Stats to CSV + Gesamtstatistiken als CSV exportieren + + + Export Game Sessions to CSV + Spielsitzungen als CSV exportieren + + + Exit + Beenden + + + Edit + Bearbeiten + + + Undo + Rückgängig + + + Redo + Wiederherstellen + + + Clear Cell + Zelle leeren + + + Help + Hilfe + + + Get Hint + Hinweis anzeigen + + + Get Coaching Hint + Coaching-Tipp + + + About + Über + + + Training Mode + Trainingsmodus + + + Analyze Position + Position analysieren + + + Resume Game + Spiel fortsetzen + + + Settings... + Einstellungen... + + + Third-Party Licenses + Drittanbieter-Lizenzen + + + ▶ New Game + ▶ Neues Spiel + + + Difficulty: + Schwierigkeit: + + + Hints: + Hinweise: + + + Easy + Leicht + + + Medium + Mittel + + + Hard + Schwer + + + Expert + Experte + + + Master + Meister + + + Unknown + Unbekannt + + + Fill Notes + Notizen füllen + + + Clear Notes + Notizen löschen + + + Undo Until Valid + Bis gültig rückgängig + + + Normal + Normal + + + Notes + Notizen + + + Color + Farbe + + + Select a technique to practice: + Wähle eine Technik zum Üben: + + + Back to Game + Zurück zum Spiel + + + Foundations + Grundlagen + + + Subset Basics + Teilmengen-Grundlagen + + + Intersections & Quads + Überschneidungen & Quartette + + + Basic Fish & Wings + Basis-Fische & Flügel + + + Links & Rectangles + Verbindungen & Rechtecke + + + Advanced Fish & Wings + Fortgeschrittene Fische & Flügel + + + Advanced Fish (Finned) + Fortgeschrittene Fische (Flosse) + + + Chains & Set Logic + Ketten & Mengenlogik + + + Inference Engines + Schlussfolgerungsmotoren + + + What It Is: + Was ist das: + + + What to Look For: + Worauf achten: + + + Start Exercises + Übungen starten + + + Back + Zurück + + + {0} difficulty points + {0} Schwierigkeitspunkte + + + Prerequisites: + Voraussetzungen: + + + Exercise {0} / {1} - {2} + Übung {0} / {1} - {2} + + + Color: + Farbe: + + + Submit + Absenden + + + Hint + Hinweis + + + Skip + Überspringen + + + Quit Lesson + Lektion beenden + + + Next Exercise + Nächste Übung + + + Retry + Wiederholen + + + Show Solution + Lösung zeigen + + + Score: {0} / {1} + Ergebnis: {0} / {1} + + + Correct! + Richtig! + + + Partially Correct + Teilweise richtig + + + Incorrect + Falsch + + + Lesson Complete! + Lektion abgeschlossen! + + + Try Again + Nochmal versuchen + + + Pick Technique + Technik wählen + + + Return to Game + Zurück zum Spiel + + + Technique: {0} + Technik: {0} + + + Hints used: {0} + Hinweise verwendet: {0} + + + Mastery: {0} + Beherrschung: {0} + + + {0} ({1} pts) + {0} ({1} Pkt.) + + + Prerequisites not met + Voraussetzungen nicht erfüllt + + + Recommended next technique + Empfohlene nächste Technik + + + Applicable at current position + Anwendbar an aktueller Position + + + Excellent! You've mastered this technique. + Ausgezeichnet! Du beherrschst diese Technik. + + + Good progress. Try again for a higher score. + Guter Fortschritt. Versuche es noch einmal für ein besseres Ergebnis. + + + Keep practicing! Review the theory and try again. + Weiter üben! Sieh dir die Theorie an und versuche es noch einmal. + + + Cannot practice Backtracking — it is not a logical technique. + Backtracking kann nicht geübt werden — es ist keine logische Technik. + + + No applicable step found for this technique. + Kein anwendbarer Schritt für diese Technik gefunden. + + + Correct! {0} Find the next one. + Richtig! {0} Finde den nächsten. + + + Correct! {0} + Richtig! {0} + + + Partially correct. {0} + Teilweise richtig. {0} + + + Not quite. {0} + Nicht ganz. {0} + + + Unknown result. + Unbekanntes Ergebnis. + + + Beginner + Anfänger + + + Intermediate + Fortgeschrittener + + + Proficient + Geübt + + + Mastered + Gemeistert + + + Completed! + Abgeschlossen! + + + Playing + Spielen + + + Ready + Bereit + + + No game loaded. Start a new game! + Kein Spiel geladen. Starte ein neues Spiel! + + + Start a new {0} game? +Current progress will be lost. + Neues Spiel mit Schwierigkeit {0} starten? +Der aktuelle Fortschritt geht verloren. + + + All progress on this puzzle will be lost, including placed numbers, notes, and hints. The timer will restart. + Aller Fortschritt bei diesem Rätsel geht verloren, einschließlich eingetragener Zahlen, Notizen und Hinweise. Der Timer wird neu gestartet. + + + Save Game + Spiel speichern + + + Enter save name: + Speichername eingeben: + + + Current Game + Aktuelles Spiel + + + Difficulty + Schwierigkeit + + + Time + Zeit + + + Moves + Züge + + + Mistakes + Fehler + + + Enter save name... + Speichername eingeben... + + + Please enter a save name. + Bitte einen Speichernamen eingeben. + + + A save named "{0}" already exists. Overwrite it? + Ein Spielstand mit dem Namen "{0}" existiert bereits. Überschreiben? + + + Load Game + Spiel laden + + + Name + Name + + + Last Modified + Zuletzt geändert + + + Elapsed + Spielzeit + + + Rating + Bewertung + + + Games Played: {0} + Gespielte Spiele: {0} + + + Games Completed: {0} + Abgeschlossene Spiele: {0} + + + Completion Rate: {0:.1f}% + Abschlussrate: {0:.1f}% + + + Best Time: {0} + Beste Zeit: {0} + + + Average Time: {0} + Durchschnittliche Zeit: {0} + + + Current Streak: {0} + Aktuelle Serie: {0} + + + Best Streak: {0} + Beste Serie: {0} + + + N/A + k. A. + + + Overview + Übersicht + + + Per Difficulty + Nach Schwierigkeit + + + Recent Games + Letzte Spiele + + + Total Moves + Züge gesamt + + + Total Hints Used + Hinweise gesamt + + + Total Mistakes + Fehler gesamt + + + Total Time Played + Gesamtspielzeit + + + Played + Gespielt + + + Completed + Abgeschlossen + + + Best Time + Beste Zeit + + + Avg Time + Ø Zeit + + + Avg SE Rating + Ø SE-Bewertung + + + Date + Datum + + + Sudoku Game + Sudoku-Spiel + + + Built with: + Erstellt mit: + + + A feature-rich offline Sudoku application. + Eine funktionsreiche Offline-Sudoku-Anwendung. + + + SE {0} + SE {0} + + + Settings + Einstellungen + + + Gameplay + Spielmechanik + + + Display + Anzeige + + + Maximum Hints: + Maximale Hinweise: + + + Auto-save Interval: + Automatisches Speichern: + + + Default Difficulty: + Standard-Schwierigkeit: + + + seconds + Sekunden + + + Highlight Conflicts + Konflikte hervorheben + + + Show Hints + Hinweise anzeigen + + + Collect detailed match statistics + Detaillierte Spielstatistiken erfassen + + + Encrypt session data + Sitzungsdaten verschlüsseln + + + Session data is encrypted by default for privacy. Disable to inspect the raw data file yourself. + Sitzungsdaten werden standardmäßig zum Schutz der Privatsphäre verschlüsselt. Deaktivieren, um die Rohdaten selbst einzusehen. + + + Disabling session tracking will stop recording per-game statistics. Would you like to delete existing session history? + Das Deaktivieren beendet die Aufzeichnung von Einzelspielstatistiken. Möchten Sie den vorhandenen Sitzungsverlauf löschen? + + + Input mode (Space to cycle, N for Notes) + Eingabemodus (Leertaste zum Wechseln, N für Notizen) + + + Place {0} in selected cell + {0} in ausgewählte Zelle setzen + + + Eliminate {0} from selected cell + {0} aus ausgewählter Zelle eliminieren + + + Puzzle Difficulty + Rätsel-Schwierigkeit + + + Puzzle Rating: SE {0} + Rätsel-Bewertung: SE {0} + + + Techniques required to solve: + Benötigte Lösungstechniken: + + + No technique details available. + Keine Technikdetails verfügbar. + + + SE {0} ({1} techniques) + SE {0} ({1} Techniken) + + + Game saved successfully + Spiel erfolgreich gespeichert + + + Aggregate stats exported to CSV + Gesamtstatistiken als CSV exportiert + + + Game sessions exported to CSV + Spielsitzungen als CSV exportiert + + + Export failed: {0} + Export fehlgeschlagen: {0} + + + No logical strategies found at this position. + Keine logischen Strategien an dieser Position gefunden. + + + Language + Sprache + + + Failed to generate puzzle + Rätsel konnte nicht generiert werden + + + Failed to load game + Spiel konnte nicht geladen werden + + + No active game to save + Kein aktives Spiel zum Speichern + + + Failed to save game + Spiel konnte nicht gespeichert werden + + + Failed to export statistics + Statistiken konnten nicht exportiert werden + + + Failed to export aggregate stats + Gesamtstatistiken konnten nicht exportiert werden + + + Failed to export game sessions + Spielsitzungen konnten nicht exportiert werden + + + File access error + Dateizugriffsfehler + + + Serialization error + Serialisierungsfehler + + + No valid state in history + Kein gültiger Zustand im Verlauf + + + Board is already valid + Spielfeld ist bereits gültig + + + Undone to last valid state + Zum letzten gültigen Zustand zurückgesetzt + + + Puzzle completed in {0}:{1}! New game started. + Rätsel gelöst in {0}:{1}! Neues Spiel gestartet. + + + Solution has errors. Keep trying! + Lösung enthält Fehler. Weiter versuchen! + + + No hints remaining (0/10 used) + Keine Hinweise übrig (0/10 verwendet) + + + Please select a cell first + Bitte wähle zuerst eine Zelle aus + + + Cannot reveal hint for given cells + Kein Hinweis für vorgegebene Zellen möglich + + + Cell already has a value + Zelle hat bereits einen Wert + + + No logical technique found for this puzzle + Keine logische Technik für dieses Rätsel gefunden + + + Suggestion: Place {0} at R{1}C{2} + Vorschlag: Setze {0} bei R{1}C{2} + + + No coaching hints remaining + Keine Coaching-Hinweise mehr verfügbar + + + No logical technique found + Keine logische Technik gefunden + + + Level {0}/3 + Stufe {0}/3 + + + Try applying this step yourself, then press Check to verify. + Versuche diesen Schritt selbst anzuwenden, dann drücke Prüfen. + + + Correct! You found all {0}/{1}. + Richtig! Du hast alle {0}/{1} gefunden. + + + {0}/{1} correct, {2} missed. + {0}/{1} richtig, {2} übersehen. + + + Some actions were incorrect. {0}/{1} correct, {2} wrong. + Einige Aktionen waren falsch. {0}/{1} richtig, {2} falsch. + + + Check + Prüfen + + + Apply + Anwenden + + + Try it! + Probiere es! + + + What to look for: + Worauf achten: + + + 0/{0} correct — try making some changes first. + 0/{0} richtig — versuche zunächst einige Änderungen. + + + Look at cell {0}. + Schau dir Zelle {0} an. + + + Focus on {0} — count the candidates. + Konzentriere dich auf {0} — zähle die Kandidaten. + + + Count the candidates in cell {0}. + Zähle die Kandidaten in Zelle {0}. + + + The value is {0}. + Der Wert ist {0}. + + + Focus on {0}. + Konzentriere dich auf {0}. + + + Look for cells that share the same candidates in a unit. + Suche nach Zellen, die dieselben Kandidaten in einer Einheit teilen. + + + These cells form a [{0}] subset. Values in the subset can only go in these cells — eliminate them from other cells in the region. + Diese Zellen bilden eine [{0}]-Teilmenge. Die Werte der Teilmenge können nur in diese Zellen — eliminiere sie aus anderen Zellen der Region. + + + These cells form the subset. Values in the subset can only go in these cells — eliminate them from other cells in the region. + Diese Zellen bilden die Teilmenge. Die Werte können nur in diese Zellen — eliminiere sie aus anderen Zellen der Region. + + + Eliminate candidates from cells that see all subset cells. + Eliminiere Kandidaten aus Zellen, die alle Teilmengenzellen sehen. + + + Look for value {0} confined to an intersection. + Suche nach Wert {0}, der auf eine Schnittmenge beschränkt ist. + + + Look for a candidate confined to the intersection of a box and a line. + Suche nach einem Kandidaten, der auf die Schnittmenge von Box und Linie beschränkt ist. + + + The intersection cells. The candidate is confined to these cells — eliminate it from other cells in the line or box outside this intersection. + Die Schnittmengenzellen. Der Kandidat ist auf diese Zellen beschränkt — eliminiere ihn aus anderen Zellen der Linie oder Box außerhalb dieser Schnittmenge. + + + Eliminate the candidate from cells outside the intersection. + Eliminiere den Kandidaten aus Zellen außerhalb der Schnittmenge. + + + Look for a fish pattern on value {0}. + Suche nach einem Fischmuster für Wert {0}. + + + Look for a fish pattern (rows/columns with restricted candidate positions). + Suche nach einem Fischmuster (Zeilen/Spalten mit eingeschränkten Kandidatenpositionen). + + + Base and cover sets. Blue cells are the base set (rows/columns where the candidate is restricted). Green cells are the cover set. Eliminate the candidate from cover set cells that aren't in the base set. + Basis- und Abdeckungsmengen. Blaue Zellen sind die Basismenge. Grüne Zellen sind die Abdeckungsmenge. Eliminiere den Kandidaten aus Abdeckungszellen, die nicht in der Basismenge sind. + + + Eliminate the candidate from cover set cells outside the base set. + Eliminiere den Kandidaten aus Abdeckungszellen außerhalb der Basismenge. + + + Find the pivot cell at {0}. + Finde die Pivotzelle bei {0}. + + + Pivot and wing cells. The orange pivot connects to the green wings. Candidates shared by both wings can be eliminated from cells that see all wing endpoints. + Pivot- und Flügelzellen. Der orange Pivot verbindet die grünen Flügel. Gemeinsame Kandidaten können aus Zellen eliminiert werden, die alle Flügelendpunkte sehen. + + + Eliminate the shared candidate from cells that see all wing endpoints. + Eliminiere den gemeinsamen Kandidaten aus Zellen, die alle Flügelendpunkte sehen. + + + Look for conjugate pairs on value {0}. + Suche nach konjugierten Paaren für Wert {0}. + + + Look for conjugate pairs (cells where a digit appears exactly twice in a unit). + Suche nach konjugierten Paaren (Zellen, in denen eine Ziffer genau zweimal in einer Einheit vorkommt). + + + The chain cells. These cells form conjugate pairs (a digit appears exactly twice in a unit). Follow the alternating pattern to find eliminations. + Die Kettenzellen. Diese Zellen bilden konjugierte Paare. Folge dem alternierenden Muster, um Eliminierungen zu finden. + + + Cells that see both endpoints of the pattern can be eliminated. + Zellen, die beide Endpunkte des Musters sehen, können eliminiert werden. + + + Build a coloring chain on value {0}. + Baue eine Färbungskette für Wert {0}. + + + Start coloring conjugate pairs with two alternating colors. + Beginne konjugierte Paare mit zwei alternierenden Farben zu färben. + + + The coloring chain. Blue and green are two alternating colors — one must be true, one false. Cells that see both colors can have the candidate eliminated. + Die Färbungskette. Blau und Grün sind zwei alternierende Farben — eine muss wahr sein, eine falsch. Zellen, die beide Farben sehen, können den Kandidaten eliminieren. + + + One color must be false — eliminate from cells that see both colors. + Eine Farbe muss falsch sein — eliminiere aus Zellen, die beide Farben sehen. + + + Look for a deadly pattern — four cells forming a rectangle across two boxes. + Suche nach einem tödlichen Muster — vier Zellen, die ein Rechteck über zwei Boxen bilden. + + + The rectangle corners. These four cells across two boxes form a potential deadly pattern. To keep the puzzle unique, eliminate the candidate that would complete the rectangle. + Die Rechteckecken. Diese vier Zellen über zwei Boxen bilden ein potenziell tödliches Muster. Um das Rätsel eindeutig zu halten, eliminiere den Kandidaten, der das Rechteck vervollständigen würde. + + + To avoid the deadly pattern, eliminate the candidate that would complete it. + Um das tödliche Muster zu vermeiden, eliminiere den Kandidaten, der es vervollständigen würde. + + + Start the chain from cell {0}. + Starte die Kette von Zelle {0}. + + + Look for a chain of linked cells with alternating strong/weak links. + Suche nach einer Kette verknüpfter Zellen mit alternierenden starken/schwachen Verbindungen. + + + The chain path. Follow the alternating strong (blue) and weak (green) links. The chain's logic forces a conclusion at the endpoints. + Der Kettenpfad. Folge den alternierenden starken (blauen) und schwachen (grünen) Verbindungen. Die Kettenlogik erzwingt eine Schlussfolgerung an den Endpunkten. + + + All chains lead to value {0} at {1}. + Alle Ketten führen zu Wert {0} bei {1}. + + + Eliminate candidates that contradict the chain logic. + Eliminiere Kandidaten, die der Kettenlogik widersprechen. + + + Look for an Almost Locked Set (a group of N cells with N+1 candidates). + Suche nach einem Fast Locked Set (eine Gruppe von N Zellen mit N+1 Kandidaten). + + + The ALS cells and restricted common. An ALS is N cells with N+1 candidates. The restricted common candidate links the sets — eliminations apply to cells that see all relevant ALS members. + Die ALS-Zellen und der eingeschränkte Gemeinsame. Ein ALS besteht aus N Zellen mit N+1 Kandidaten. Der eingeschränkte gemeinsame Kandidat verbindet die Mengen — Eliminierungen gelten für Zellen, die alle relevanten ALS-Mitglieder sehen. + + + Eliminate candidates from cells that see all relevant ALS members. + Eliminiere Kandidaten aus Zellen, die alle relevanten ALS-Mitglieder sehen. + + + Look for the cell with three candidates (the only non-bivalue cell). + Suche nach der Zelle mit drei Kandidaten (der einzigen Nicht-Biwert-Zelle). + + + The key cell is {0}. + Die Schlüsselzelle ist {0}. + + + A cell has only one possible candidate left. All other values are eliminated by row, column, and box constraints. + Eine Zelle, in der nur noch ein einziger Kandidat möglich ist. + + + Look for cells where 8 of the 9 values are already present in the cell's row, column, or box. + Suche nach Zellen, die nur einen Kandidaten haben. Dies geschieht, wenn alle anderen Ziffern bereits in derselben Zeile, Spalte oder Box vorkommen. + + + A value can only go in one cell within a row, column, or box. Even though the cell may have multiple candidates, only this value has no other place in the region. + Ein Kandidat, der innerhalb einer Einheit (Zeile, Spalte oder Box) nur in einer einzigen Zelle vorkommen kann. + + + For each region, check if any value has only one possible cell. + Suche in jeder Einheit nach einem Kandidaten, der nur an einer Stelle möglich ist, auch wenn die Zelle mehrere Kandidaten hat. + + + Two cells in the same region each contain exactly the same two candidates. Those two values must go in those two cells, so they can be eliminated from all other cells in the region. + Zwei Zellen in derselben Einheit, die genau dieselben zwei Kandidaten enthalten. + + + Find two cells in a row, column, or box that share the same pair of candidates. + Suche nach zwei Zellen in einer Einheit, die exakt dasselbe Paar von Kandidaten {A,B} haben. Diese beiden Werte können aus allen anderen Zellen der Einheit eliminiert werden. + + + Three cells in a region collectively contain exactly three candidates. Each cell has a subset of those three values. Those values can be eliminated from other cells in the region. + Drei Zellen in derselben Einheit, deren Kandidaten insgesamt höchstens drei verschiedene Werte umfassen. + + + Find three cells in a region whose combined candidates form a set of exactly three values. + Suche nach drei Zellen in einer Einheit, deren gemeinsame Kandidatenmenge genau drei Werte umfasst. Jede Zelle muss eine Teilmenge dieser drei Werte enthalten. + + + Two values in a region appear as candidates in exactly the same two cells. Other candidates in those two cells can be eliminated. + Zwei Kandidaten, die innerhalb einer Einheit nur in genau zwei Zellen vorkommen. + + + For each region, find two values that appear only in the same two cells. + Suche nach zwei Kandidaten, die in einer Einheit auf dieselben zwei Zellen beschränkt sind. Alle anderen Kandidaten können aus diesen beiden Zellen eliminiert werden. + + + Three values in a region appear as candidates in exactly three cells. Other candidates in those cells can be eliminated. + Drei Kandidaten, die innerhalb einer Einheit nur in genau drei Zellen vorkommen. + + + For each region, find three values confined to exactly three cells. + Suche nach drei Kandidaten, die in einer Einheit auf dieselben drei Zellen beschränkt sind. Alle anderen Kandidaten können aus diesen drei Zellen eliminiert werden. + + + A candidate in a box is confined to a single row or column. That candidate can be eliminated from the rest of that row or column outside the box. + Ein Kandidat in einer Box, der auf eine einzige Zeile oder Spalte beschränkt ist. + + + In each box, check if a candidate appears only in one row or one column. + Wenn ein Kandidat innerhalb einer Box nur in einer Zeile oder Spalte vorkommt, kann er aus den übrigen Zellen dieser Zeile oder Spalte außerhalb der Box eliminiert werden. + + + A candidate in a row or column is confined to a single box. That candidate can be eliminated from the rest of the box outside that row or column. + Ein Kandidat in einer Zeile oder Spalte, der auf eine einzige Box beschränkt ist. + + + In each row/column, check if a candidate appears only within one box. + Wenn ein Kandidat innerhalb einer Zeile oder Spalte nur in einer Box vorkommt, kann er aus den übrigen Zellen dieser Box eliminiert werden. + + + Four cells in a region collectively contain exactly four candidates. Those values can be eliminated from other cells in the region. + Vier Zellen in derselben Einheit, deren Kandidaten insgesamt höchstens vier verschiedene Werte umfassen. + + + Find four cells in a region whose combined candidates form a set of exactly four values. + Suche nach vier Zellen in einer Einheit, deren gemeinsame Kandidatenmenge genau vier Werte umfasst. Diese Werte können aus allen anderen Zellen der Einheit eliminiert werden. + + + Four values in a region appear as candidates in exactly four cells. Other candidates in those cells can be eliminated. + Vier Kandidaten, die innerhalb einer Einheit nur in genau vier Zellen vorkommen. + + + For each region, find four values confined to exactly four cells. + Suche nach vier Kandidaten, die in einer Einheit auf dieselben vier Zellen beschränkt sind. Alle anderen Kandidaten können aus diesen vier Zellen eliminiert werden. + + + A candidate appears in exactly two cells in each of two rows, and those cells are in the same two columns. The candidate can be eliminated from other cells in those columns. + Ein Fisch-Muster, bei dem ein Kandidat in zwei Zeilen jeweils auf genau zwei Spalten beschränkt ist (oder umgekehrt). + + + Find a candidate forming a rectangle pattern: two rows, two columns, four cells. + Suche nach einem Kandidaten, der in zwei Zeilen jeweils nur in denselben zwei Spalten vorkommt. Der Kandidat kann aus den übrigen Zellen dieser Spalten eliminiert werden. + + + A pivot cell with candidates {A,B} sees two wing cells: one with {A,C} and one with {B,C}. Value C can be eliminated from any cell that sees both wings. + Drei Biwert-Zellen, die ein Y-Muster bilden: ein Pivot mit {A,B} verbunden mit zwei Flügeln {A,C} und {B,C}. + + + Find a bivalue cell (pivot) that sees two other bivalue cells sharing one candidate each. + Suche nach einer Biwert-Zelle (Pivot), die zwei andere Biwert-Zellen (Flügel) sieht. Wenn die Flügel einen gemeinsamen Kandidaten C teilen, kann C aus Zellen eliminiert werden, die beide Flügel sehen. + + + A candidate appears in 2-3 cells in each of three rows, and those cells fall in exactly three columns. The candidate can be eliminated from other cells in those columns. + Ein Fisch-Muster der Größe 3: ein Kandidat in drei Zeilen auf höchstens drei Spalten beschränkt (oder umgekehrt). + + + Extend the X-Wing pattern to three rows and three columns. + Suche nach einem Kandidaten, der in drei Zeilen jeweils nur in denselben drei (oder weniger) Spalten vorkommt. Der Kandidat kann aus den übrigen Zellen dieser Spalten eliminiert werden. + + + Two conjugate pairs for a digit share one endpoint in the same row or column. The digit can be eliminated from cells that see both non-shared endpoints. + Zwei konjugierte Paare desselben Kandidaten, die sich einen Endpunkt teilen. + + + Find two rows (or columns) each with exactly two cells for a digit, sharing one column (or row). + Suche nach zwei Zeilen (oder Spalten) mit je genau zwei Positionen für denselben Kandidaten, wobei eine Position geteilt wird. Der Kandidat kann aus Zellen eliminiert werden, die beide nicht geteilten Endpunkte sehen. + + + A conjugate pair in a row and a conjugate pair in a column are connected through a box. The digit can be eliminated from the cell that sees both unconnected endpoints. + Ein Muster aus einem konjugierten Paar in einer Zeile und einem in einer Spalte, verbunden durch eine gemeinsame Box. + + + Find a row pair and column pair for the same digit connected via a shared box. + Suche nach einem Kandidaten mit einem konjugierten Paar in einer Zeile und einem in einer Spalte, die über eine Box verbunden sind. Der Kandidat kann aus der Zelle eliminiert werden, die beide äußeren Endpunkte sieht. + + + A pivot cell with candidates {A,B,C} sees a wing with {A,B} and a wing with {A,C}. Value A can be eliminated from cells that see all three cells. + Ein Pivot mit {A,B,C} verbunden mit zwei Flügeln {A,B} und {A,C}. + + + Find a trivalue pivot seeing two bivalue wings that each share two candidates with the pivot. + Suche nach einer Zelle mit drei Kandidaten {A,B,C}, die zwei Biwert-Zellen sieht, die jeweils einen Kandidaten mit dem Pivot teilen. Der gemeinsame Kandidat A kann aus Zellen eliminiert werden, die alle drei sehen. + + + Four cells forming a rectangle across two boxes would create a deadly pattern (two solutions) if they all had the same two candidates. Extra candidates in some cells can force eliminations to avoid this ambiguity. + Vier Zellen, die ein Rechteck über zwei Boxen bilden und ein tödliches Muster erzeugen würden. + + + Find four cells in a rectangle across two boxes sharing the same two candidates. + Suche nach vier Zellen in einem Rechteck über zwei Boxen mit zwei gemeinsamen Kandidaten. Um die Eindeutigkeit des Rätsels zu bewahren, muss der Kandidat eliminiert werden, der das Muster vervollständigen würde. + + + Two cells with the same pair of candidates {A,B} are connected by a strong link on value A. Value B can be eliminated from cells that see both endpoints. + Zwei Biwert-Zellen {A,B}, die durch eine starke Verbindung auf Kandidat A verbunden sind. + + + Find two identical bivalue cells connected by a conjugate pair on one of their values. + Suche nach zwei Biwert-Zellen mit denselben Kandidaten {A,B}, die durch eine starke Verbindung auf einem der Werte verbunden sind. Der andere Wert B kann aus Zellen eliminiert werden, die beide Zellen sehen. + + + For a single digit, build chains of conjugate pairs and assign two colors. If both colors appear in the same region, one color is false and its candidates are eliminated. + Konjugierte Ketten auf einem einzelnen Kandidaten mit zwei alternierenden Farben. + + + Pick a digit, trace conjugate pairs, color alternately. Check for color conflicts. + Färbe konjugierte Paare eines Kandidaten mit zwei Farben abwechselnd ein. Wenn zwei gleichfarbige Zellen sich sehen, ist diese Farbe falsch. Zellen, die beide Farben sehen, können den Kandidaten nicht enthalten. + + + An X-Wing pattern with one extra candidate cell (the fin) in the same box as a corner. Eliminations are restricted to cells that see both the fin and the X-Wing column. + Ein X-Wing-Muster mit einer zusätzlichen Zelle (Flosse), die das perfekte Muster stört. + + + Find an X-Wing where one row has an extra candidate cell in the same box. + Suche nach einem fast perfekten X-Wing, bei dem eine Zeile eine zusätzliche Position hat (die Flosse). Eliminierungen sind auf Zellen beschränkt, die sowohl im Deckungsset als auch in der Box der Flosse liegen. + + + A chain of bivalue cells all containing the same pair {A,B}, where each adjacent pair shares a region. Cells seeing both endpoints of an even-length chain lose both values. + Eine Kette von Biwert-Zellen mit denselben zwei Kandidaten {A,B}, die sich über mindestens vier Zellen erstreckt. + + + Find a chain of identical bivalue cells connected through shared regions. + Suche nach einer Kette von Biwert-Zellen mit identischen Kandidaten {A,B}, die sich gegenseitig sehen. Bei gerader Kettenlänge können die beiden Kandidaten aus Zellen eliminiert werden, die beide Endpunkte sehen. + + + If all unsolved cells have exactly two candidates except one cell with three, the puzzle would have multiple solutions unless the trivalue cell is set to the value that appears three times in its row, column, or box. + BUG (Bivalue Universal Grave): Ein Zustand, in dem alle Zellen Biwert-Zellen wären — bis auf eine. + + + Check if only one cell has more than two candidates. If so, find its odd-count value. + Wenn alle leeren Zellen genau zwei Kandidaten haben bis auf eine Zelle mit drei, muss in dieser Zelle der Kandidat platziert werden, der in ihrer Zeile, Spalte und Box dreimal vorkommt. + + + A candidate appears in 2-4 cells in each of four rows, and those cells fall in exactly four columns. The candidate can be eliminated from other cells in those columns. + Ein Fisch-Muster der Größe 4: ein Kandidat in vier Zeilen auf höchstens vier Spalten beschränkt (oder umgekehrt). + + + Extend the Swordfish pattern to four rows and four columns. + Suche nach einem Kandidaten, der in vier Zeilen jeweils nur in denselben vier (oder weniger) Spalten vorkommt. Der Kandidat kann aus den übrigen Zellen dieser Spalten eliminiert werden. + + + A Swordfish pattern with extra candidate cells (fins) in the same box. Eliminations are restricted to cells seeing both the fin box and the Swordfish columns. + Ein Swordfish-Muster mit einer zusätzlichen Flossenposition. + + + Find a Swordfish where one row has extra candidates in the same box. + Suche nach einem fast perfekten Swordfish, bei dem eine Basis-Einheit eine zusätzliche Position hat. Eliminierungen sind auf Zellen beschränkt, die sowohl im Deckungsset als auch in der Box der Flosse liegen. + + + A digit's candidates in a box form an L-shape or cross, leaving an empty rectangle. Combined with a conjugate pair outside the box, this eliminates the digit from a target cell. + Ein Muster, bei dem die Positionen eines Kandidaten in einer Box ein leeres Rechteck bilden und eine konjugierte Verbindung nutzen. + + + Find a box where a digit's candidates leave an empty rectangle, connected to a conjugate pair. + Suche nach einer Box, in der ein Kandidat in einer Zeile und einer Spalte vorkommt, aber nicht an deren Schnittpunkt. Kombiniert mit einem konjugierten Paar außerhalb der Box ermöglicht dies eine Eliminierung. + + + A four-cell wing pattern: a pivot and three wings collectively contain four candidates, and a shared candidate Z can be eliminated from cells seeing all cells containing Z. + Ein Wing-Muster mit einem Pivot und drei Flügelzellen, die zusammen vier Kandidaten abdecken. + + + Find a group of four cells with exactly four combined candidates sharing a common value. + Suche nach einer Zelle (Pivot) mit Flügeln, deren gemeinsame Kandidatenmenge vier Werte umfasst. Der eingeschränkte gemeinsame Kandidat kann aus Zellen eliminiert werden, die alle vier sehen. + + + A Jellyfish pattern with extra fin cells in the same box. Eliminations are restricted to cells seeing both the fin box and the Jellyfish columns. + Ein Jellyfish-Muster mit einer zusätzlichen Flossenposition. + + + Find a Jellyfish where one row has extra candidates forming a fin. + Suche nach einem fast perfekten Jellyfish, bei dem eine Basis-Einheit eine zusätzliche Position hat. Eliminierungen sind auf Zellen beschränkt, die sowohl im Deckungsset als auch in der Box der Flosse liegen. + + + A chain of bivalue cells where consecutive cells share a candidate value, alternating between the two candidates. The value shared by the chain's endpoints can be eliminated from cells that see both endpoints. + Eine Kette von Biwert-Zellen, die durch gemeinsame Kandidaten verknüpft sind. + + + Build a chain of bivalue cells connected by shared candidates. Check the endpoints. + Suche nach einer Kette von Biwert-Zellen, bei der jede Zelle einen Kandidaten mit der nächsten teilt. Der Kandidat, der an beiden Endpunkten vorkommt, kann aus Zellen eliminiert werden, die beide Endpunkte sehen. + + + Build separate conjugate pair chains (clusters) for a digit and color each. When two clusters interact (cells in different clusters see each other), eliminations can be made from cells that see conflicting colors across clusters. + Mehrere Färbungsketten auf einem Kandidaten, die miteinander interagieren. + + + Color multiple conjugate chains for one digit, then check cross-cluster interactions. + Erstelle mehrere Färbungscluster auf einem Kandidaten. Wenn eine Farbe eines Clusters beide Farben eines anderen sieht, muss diese Farbe falsch sein. Zellen, die komplementäre Farben verschiedener Cluster sehen, können eliminiert werden. + + + Two Almost Locked Sets (each has N cells with N+1 candidates) share a restricted common candidate X. A second common candidate Z can be eliminated from cells that see all Z-cells in both sets. + Zwei Almost Locked Sets (ALS), verbunden durch einen eingeschränkten gemeinsamen Kandidaten. + + + Find two groups of cells that are almost locked, sharing a restricted common candidate. + Suche nach zwei ALS (je N Zellen mit N+1 Kandidaten), die einen eingeschränkten gemeinsamen Kandidaten X teilen. Ein weiterer gemeinsamer Kandidat Z kann aus Zellen eliminiert werden, die beide ALS sehen. + + + An intersection of a line and box contains 2-3 cells whose candidates can be covered by two Almost Locked Sets (one from the line remainder, one from the box remainder). Extra ALS candidates can be eliminated from their respective remainders. + Ein Muster am Schnittpunkt einer Zeile/Spalte und einer Box, kombiniert mit Almost Locked Sets. + + + Find an intersection where candidates can be partitioned into two covering ALS. + Suche nach 2-3 Zellen am Schnittpunkt einer Zeile und einer Box, deren Kandidaten mit je einem ALS in der Restzeile und der Restbox abgedeckt werden. Kandidaten können aus den Restzellen eliminiert werden. + + + For a cell with 2-3 candidates, assume each candidate is true and propagate the consequences. If all assumptions lead to the same conclusion (a placement or elimination), that conclusion must be true. + Für jeden Kandidaten einer Zelle wird eine Kette verfolgt — alle führen zum selben Ergebnis. + + + Pick a cell with few candidates. Try each value and propagate. Look for common outcomes. + Wähle eine Zelle mit wenigen Kandidaten. Verfolge die logischen Konsequenzen jedes Kandidaten. Wenn alle Ketten dasselbe Ergebnis liefern, muss dieses Ergebnis wahr sein. + + + Build a chain of alternating strong and weak links between (cell, digit) pairs. If the chain forms a loop or its endpoints share a digit, eliminations can be derived from the alternating inference chain rules. + Eine alternierende Schlusskette (AIC), die zwei Zellen durch starke und schwache Verbindungen verknüpft. + + + Trace alternating strong/weak links. Check if endpoints share a digit for eliminations. + Suche nach einer Kette alternierend starker und schwacher Verbindungen. Wenn die Kette mit zwei starken Verbindungen endet, kann der Endkandidat platziert werden; bei zwei schwachen wird er eliminiert. + + + For a single digit, build a chain of alternating strong and weak links. Type 1 (continuous loop) eliminates the digit from cells seeing weak link endpoints. Type 2 places the digit at a strong-strong discontinuity. Type 3 eliminates at a weak-weak discontinuity. + Zyklische Ketten auf einem einzelnen Kandidaten mit alternierenden starken und schwachen Verbindungen. + + + For each digit, trace alternating strong/weak links and look for cycles or discontinuities. + Baue eine Kette auf einem Kandidaten mit alternierenden Verbindungen. Ein geschlossener Zyklus ermöglicht Eliminierungen; bei Diskontinuität wird je nach Typ platziert oder eliminiert. + + + Multi-digit coloring: build a graph of (cell, digit) pairs connected by strong links (conjugate pairs and bivalue cells). Color with two alternating colors. Apply six rules to find contradictions or trap eliminations. + Erweiterung der einfachen Färbung auf mehrere Kandidaten innerhalb derselben Zellen. + + + Extend single-digit coloring to multiple digits via bivalue cell connections. + Färbe konjugierte Paare verschiedener Kandidaten mit zwei Farben ein. Wenn ein Zellen-Kandidat-Paar beiden Farben zugeordnet wird, ist eine Farbe falsch. Nutze die resultierenden Widersprüche für Eliminierungen. + + + A deadly rectangle pattern where one or more corners have the UR values hidden among other candidates. Strong links on UR values in shared units force eliminations to avoid the deadly pattern. + Ein Unique Rectangle, bei dem das tödliche Muster durch versteckte Kandidaten aufgedeckt wird. + + + Find a rectangle across two boxes where the UR values are present but hidden by extras. + Suche nach einem Unique Rectangle, bei dem ein konjugiertes Paar auf einem der Werte bestätigt, dass der andere Wert an einer bestimmten Stelle eliminiert werden muss. + + + Like Unique Rectangle but using the distinction between given clues and solver-placed values. If three solver-placed corners of a rectangle have values {A,B}, the fourth unsolved corner cannot complete the deadly pattern. + Ein Unique-Rectangle-Muster mit bereits gelösten Zellen, die ein tödliches Muster erzeugen würden. + + + Find rectangles where three corners are solver-placed (not givens) with two values. + Suche nach einem Rechteck mit zwei gelösten Zellen gleicher Werte und zwei leeren Zellen mit diesen Werten als Kandidaten. Ein Kandidat muss eliminiert werden, um Mehrdeutigkeit zu vermeiden. + + + Three non-overlapping Almost Locked Sets linked by restricted commons: A-B linked by X, B-C linked by Y (Y != X). Common value Z in both A and C can be eliminated from cells seeing all Z-cells in both A and C. + Drei ALS, die paarweise durch eingeschränkte gemeinsame Kandidaten X und Y verbunden sind. + + + Find three ALSs forming a chain with two restricted common candidates. + Suche nach drei ALS: {A,B} verbunden durch X und {A,C} verbunden durch Y. Kandidat Z, der in A und C vorkommt, kann aus Zellen eliminiert werden, die die Z-Zellen beider ALS sehen. + + + A stem cell with 2-3 candidates, each linked to a petal ALS via restricted common. A value Z common across all petals (but not in the stem) can be eliminated from cells seeing all Z-cells in all petals. + Eine Stammzelle, deren Kandidaten jeweils auf ein eigenes ALS (Blütenblatt) zeigen. + + + Find a cell whose candidates each connect to an ALS via restricted common. + Suche nach einer Zelle (Stamm), bei der jeder Kandidat über einen eingeschränkten Gemeinsamen mit einem eigenen ALS verbunden ist. Gemeinsame Kandidaten der Blütenblätter können aus Zellen eliminiert werden, die alle sehen. + + + A five-cell wing pattern: a pivot and four wings collectively contain five candidates. The non-restricted shared value Z can be eliminated from cells seeing all Z-cells. + Ein Wing-Muster mit einem Pivot und vier Flügelzellen, die zusammen fünf Kandidaten abdecken. + + + Find five cells with exactly five combined candidates and a restricted elimination value. + Suche nach einer Zelle (Pivot) mit Flügeln, deren gemeinsame Kandidatenmenge fünf Werte umfasst. Der eingeschränkte gemeinsame Kandidat kann aus Zellen eliminiert werden, die alle Z-Zellen sehen. + + + A fish pattern (X-Wing/Swordfish/Jellyfish) where base and cover sets are mixed rows/columns and boxes. At least one base set must be a box. Eliminates from cover cells outside the base. + Ein Fisch-Muster, bei dem die Basismengen aus Zeilen/Spalten und Boxen gemischt bestehen. + + + Look for fish patterns that include boxes as base or cover sets. + Suche nach einem Fisch, dessen Basis- und Deckungsmengen Zeilen/Spalten mit Boxen kombinieren. Der Kandidat kann aus Deckungszellen außerhalb der Basismenge eliminiert werden. + + + Extends X-Cycles by allowing grouped nodes: 2-3 cells in the same box on the same row or column that all have a candidate digit. Same Type 1/2/3 rules apply. + X-Cycles mit gruppierten Knoten, bei denen mehrere Zellen in einer Box als einzelner Knoten behandelt werden. + + + Build X-Cycle chains using grouped box nodes alongside individual cells. + Wie X-Cycles, aber Gruppen von Zellen innerhalb einer Box werden als ein Knoten zusammengefasst. Dies ermöglicht komplexere Kettenmuster und zusätzliche Eliminierungen. + + + A fish pattern where one base row has only one candidate position instead of two. The missing position is compensated by a fin cell, restricting eliminations to the fin's box. + Ein Finned X-Wing, bei dem eine Basisposition fehlt — die Flosse ist der einzige Überrest. + + + Look for an X-Wing-like pattern where one row is incomplete — it only has the candidate in one of the two expected columns, plus an extra fin cell. + Suche nach einem fast vollständigen X-Wing, bei dem eine erwartete Position fehlt und durch eine Flosse ersetzt wird. Eliminierungen gelten für Zellen in der Box der Flosse, die auch im Deckungsset liegen. + + + A 3-row fish pattern where at least one base row has fewer candidate positions than expected. The missing position creates a fin that restricts eliminations. + Ein Finned Swordfish, bei dem eine Basisposition fehlt — die Flosse kompensiert die Lücke. + + + Find a Swordfish shape where one row only covers 1 of the 3 base columns, plus a fin. + Suche nach einem fast vollständigen Swordfish mit fehlender Basisposition und einer Flosse. Eliminierungen sind auf die Schnittmenge von Flossenbox und Deckungsset beschränkt. + + + A 4-row fish pattern where at least one base row has fewer candidate positions than expected. The missing position creates a fin restricting eliminations. + Ein Finned Jellyfish, bei dem eine Basisposition fehlt — die Flosse kompensiert die Lücke. + + + Find a Jellyfish shape where one row only covers 1 of the 4 base columns, plus a fin. + Suche nach einem fast vollständigen Jellyfish mit fehlender Basisposition und einer Flosse. Eliminierungen sind auf die Schnittmenge von Flossenbox und Deckungsset beschränkt. + + + For a digit in a unit with 2-3 positions, assume the digit goes in each position and propagate. If all branches lead to the same conclusion, that conclusion is true. + Alle Positionen eines Kandidaten in einer Einheit werden als Startpunkte verfolgt — alle führen zum selben Ergebnis. + + + Find a unit where a digit appears in few cells, then try each placement. + Wähle eine Einheit (Zeile, Spalte oder Box) mit wenigen Positionen für einen Kandidaten. Wenn das Setzen des Kandidaten an jeder Position zum selben Ergebnis führt, muss dieses wahr sein. + + + For a digit in a box with 2-3 positions, assume the digit goes in each position and propagate. If all branches lead to the same conclusion, that conclusion is true. + Alle Positionen eines Kandidaten in einer Region werden verfolgt — alle Ketten konvergieren. + + + Find a box where a digit appears in few cells, then try each placement. + Ähnlich wie Unit Forcing Chain, aber der Fokus liegt auf einer Region. Wenn jede mögliche Platzierung eines Kandidaten dasselbe Ergebnis erzwingt, ist das Ergebnis gesichert. + + + A fish pattern where BOTH base and cover sets freely mix rows, columns, and boxes. Unlike Franken Fish (one mixed side), Mutant Fish requires both sides to contain at least 2 different unit types. Eliminates from cover cells outside the base. + Ein Fisch-Muster mit völlig gemischten Basis- und Deckungsmengen aus Zeilen, Spalten und Boxen. + + + Look for fish patterns where both the base set and cover set mix rows, columns, and boxes. + Suche nach einem Fisch, dessen Basis- und Deckungsmengen beliebig aus Zeilen, Spalten und Boxen zusammengesetzt sind. Kandidaten werden aus Deckungszellen eliminiert, die nicht zur Basismenge gehören. + + + Extends finned fish by using chain propagation to verify eliminations outside the fin's box. For each candidate that a standard finned fish would reject (outside the fin's box), place the digit at the fin cell and propagate. If the target still loses the candidate, the elimination is valid regardless of whether the fin is true or false. + Ein Finned Fish, bei dem die Flosse durch Forcing Chains verifiziert wird. + + + Find a finned fish pattern, then check if chain propagation from the fin cell eliminates the digit from cells outside the fin's box. + Suche nach einem Finned Fish, bei dem die Eliminierungen durch Ketten von der Flosse aus bestätigt werden. Die Ketten beweisen, dass die Eliminierung unabhängig von der Flosse gültig ist. + + + A generalized chain of 4-6 Almost Locked Sets linked by distinct restricted commons. Values common across the chain endpoints can be eliminated from cells that see all relevant ALS members. + Eine Kette von ALS, die paarweise durch eingeschränkte gemeinsame Kandidaten verbunden sind. + + + Find a chain of ALSs where each adjacent pair shares a restricted common. + Suche nach einer Folge von ALS, bei denen benachbarte Paare eingeschränkte gemeinsame Kandidaten teilen. Kandidaten des ersten und letzten ALS können aus Zellen eliminiert werden, die die Z-Zellen beider sehen. + + + A deadly pattern where 4-6 cells form a loop, each consecutive pair sharing a unit (row, column, or box). All cells contain the same candidate pair {A,B}. If all cells had only {A,B}, two solutions would exist. Cells with extra candidates must keep them. + Eine Schleife von Zellen mit zwei gemeinsamen Kandidaten, die ein erweitertes tödliches Muster bilden. + + + Find a loop of 4-6 cells across at least 2 boxes where each cell has candidates {A,B} and each consecutive pair shares a row, column, or box. If exactly one cell has extras, eliminate A and B from it. + Suche nach einer geschlossenen Schleife von Zellen mit zwei gemeinsamen Kandidaten, wobei jede Zelle die nächste in der Kette sieht. Zusätzliche Kandidaten müssen vorhanden sein, um die Eindeutigkeit zu gewährleisten. + + + A base pair of cells in a box whose candidates must appear in specific target cells in other boxes along cross-lines. Candidates not matching the base pair pattern can be eliminated from the target cells. + Ein Muster mit zwei Basiszellen und zwei Zielzellen, die durch Kandidatenbeschränkungen verknüpft sind. + + + Find a base pair in a box with target cells in aligned boxes along cross-lines. + Suche nach zwei Basiszellen in einer Zeile mit Zielzellen in verschiedenen Boxen. Die Zielzellen können nur die Kandidaten der Basiszellen enthalten — alle anderen werden eliminiert. + + + A continuous alternating inference chain (AIC) that forms a complete loop. Every weak link in the loop produces eliminations: the digit can be removed from any cell outside the loop that sees both endpoints of a weak link. + Eine geschlossene alternierende Schlusskette, bei der die Schleife zu sich selbst zurückkehrt. + + + Build an AIC where the chain closes back on itself with consistent alternating links. Every weak link segment yields eliminations from external cells seeing both endpoints. + Suche nach einer geschlossenen Kette alternierend starker und schwacher Verbindungen. Schwache Verbindungen innerhalb der Schleife ermöglichen die Eliminierung der verbundenen Kandidaten aus externen Zellen. + + + Extends Nice Loop (AIC) with grouped nodes: 2-3 cells in the same box on the same row or column that all share a candidate digit. Combines multi-digit cell-based links (bivalue strong links) with single-digit grouped unit links for stronger chains. + Ein Nice Loop mit gruppierten Knoten, bei dem mehrere Zellen einer Box als ein Knoten gelten. + + + Build AIC chains using grouped box nodes alongside individual cells. Look for discontinuous Type 2 chains where both endpoints assert the same digit. + Wie ein Nice Loop, aber Zellgruppen innerhalb einer Box fungieren als einzelner Knoten. Dies ermöglicht die Erkennung komplexerer Muster und zusätzliche Eliminierungen. + + + A brute-force trial-and-error method. Not a logical technique — used as a fallback when no logical strategy can make progress. + Systematisches Ausprobieren von Kandidaten mit Zurücksetzen bei Widersprüchen. + + + This technique is not used in training exercises. + Backtracking ist keine logische Technik. Es wird als letztes Mittel eingesetzt, wenn keine logischen Strategien mehr anwendbar sind. + + + Unknown technique. + Unbekannte Lösungstechnik. + + + No identification tips available. + Keine Beschreibung verfügbar. + + + {0} (SE {1}) + {0} (SE {1}) + + + Backtracking (trial & error) + Backtracking (Versuch und Irrtum) + + + Invalid game data + Ungültige Spieldaten + + + Invalid difficulty + Ungültige Schwierigkeit + + + Game not started + Spiel nicht gestartet + + + Game already ended + Spiel bereits beendet + + + Unknown statistics error + Unbekannter Statistikfehler + + + Naked Single + Nackter Einzelner + + + Hidden Single + Versteckter Einzelner + + + Naked Pair + Nacktes Paar + + + Naked Triple + Nacktes Tripel + + + Hidden Pair + Verstecktes Paar + + + Hidden Triple + Verstecktes Tripel + + + Pointing Pair + Zeigepaar + + + Box/Line Reduction + Block-Zeilen-Reduktion + + + Naked Quad + Nacktes Quartett + + + Hidden Quad + Verstecktes Quartett + + + X-Wing + X-Wing + + + XY-Wing + XY-Wing + + + Swordfish + Schwertfisch + + + Skyscraper + Wolkenkratzer + + + 2-String Kite + Zweidraht-Drachen + + + XYZ-Wing + XYZ-Wing + + + Unique Rectangle + Eindeutiges Rechteck + + + W-Wing + W-Wing + + + Simple Coloring + Einfache Färbung + + + Finned X-Wing + X-Wing mit Flosse + + + Remote Pairs + Entfernte Paare + + + BUG + BUG + + + Jellyfish + Qualle + + + Finned Swordfish + Schwertfisch mit Flosse + + + Empty Rectangle + Leeres Rechteck + + + WXYZ-Wing + WXYZ-Wing + + + Finned Jellyfish + Qualle mit Flosse + + + XY-Chain + XY-Kette + + + Multi-Coloring + Mehrfach-Färbung + + + ALS-XZ + ALS-XZ + + + Sue de Coq + Sue de Coq + + + Forcing Chain + Forcing Chain + + + Nice Loop + Nice Loop + + + X-Cycles + X-Zyklen + + + 3D Medusa + 3D Medusa + + + Hidden Unique Rectangle + Verstecktes Eindeutiges Rechteck + + + Avoidable Rectangle + Vermeidbares Rechteck + + + ALS-XY-Wing + ALS-XY-Wing + + + Death Blossom + Todesblüte + + + VWXYZ-Wing + VWXYZ-Wing + + + Franken Fish + Franken-Fisch + + + Grouped X-Cycles + Gruppierte X-Zyklen + + + Sashimi X-Wing + Sashimi X-Wing + + + Sashimi Swordfish + Sashimi Schwertfisch + + + Sashimi Jellyfish + Sashimi Qualle + + + Unit Forcing Chain + Einheits-Forcing-Chain + + + Region Forcing Chain + Regions-Forcing-Chain + + + Mutant Fish + Mutanten-Fisch + + + Kraken Fish + Kraken-Fisch + + + ALS Chain + ALS-Kette + + + Junior Exocet + Junior Exocet + + + Unique Loop + Eindeutige Schleife + + + Continuous Nice Loop + Kontinuierlicher Nice Loop + + + Grouped Nice Loop + Gruppierter Nice Loop + + + Backtracking + Backtracking + + + Unknown Technique + Unbekannte Technik + + + Row + Zeile + + + Column + Spalte + + + Box + Block + + + Unknown Region + Unbekannte Region + + + R{0}C{1} + Z{0}S{1} + + + Naked Single at {0}: only value {1} is possible + Nackter Einzelner bei {0}: nur Wert {1} ist möglich + + + Hidden Single at {0}: value {1} can only appear in this cell within its region + Versteckter Einzelner bei {0}: Wert {1} kann nur in dieser Zelle innerhalb seiner Region vorkommen + + + Naked Pair [{0}] at {1} in {2} eliminates candidates from other cells + Nacktes Paar [{0}] bei {1} in {2} eliminiert Kandidaten aus anderen Zellen + + + Naked Triple [{0}] at {1} in {2} eliminates candidates from other cells + Nacktes Tripel [{0}] bei {1} in {2} eliminiert Kandidaten aus anderen Zellen + + + Hidden Pair [{0}] at {1} in {2} eliminates other candidates from these cells + Verstecktes Paar [{0}] bei {1} in {2} eliminiert andere Kandidaten aus diesen Zellen + + + Hidden Triple [{0}] at {1} in {2} eliminates other candidates from these cells + Verstecktes Tripel [{0}] bei {1} in {2} eliminiert andere Kandidaten aus diesen Zellen + + + Pointing Pair: {0} in Box {1} confined to {2} {3} eliminates {0} from other cells in {2} {3} + Zeigepaar: {0} in Block {1} beschränkt auf {2} {3} eliminiert {0} aus anderen Zellen in {2} {3} + + + Box/Line Reduction: {0} in {1} {2} confined to Box {3} eliminates {0} from other cells in Box {3} + Block-Zeilen-Reduktion: {0} in {1} {2} beschränkt auf Block {3} eliminiert {0} aus anderen Zellen in Block {3} + + + Naked Quad [{0}] at {1} in {2} eliminates candidates from other cells + Nacktes Quartett [{0}] bei {1} in {2} eliminiert Kandidaten aus anderen Zellen + + + Hidden Quad [{0}] at {1} in {2} eliminates other candidates from these cells + Verstecktes Quartett [{0}] bei {1} in {2} eliminiert andere Kandidaten aus diesen Zellen + + + X-Wing on value {0} in Rows {1} and {2}, Columns {3} and {4} eliminates {0} from other cells in those columns + X-Wing auf Wert {0} in Zeilen {1} und {2}, Spalten {3} und {4} eliminiert {0} aus anderen Zellen in diesen Spalten + + + X-Wing on value {0} in Columns {1} and {2}, Rows {3} and {4} eliminates {0} from other cells in those rows + X-Wing auf Wert {0} in Spalten {1} und {2}, Zeilen {3} und {4} eliminiert {0} aus anderen Zellen in diesen Zeilen + + + XY-Wing: pivot {0} {{{1},{2}}}, wing {3} {{{1},{4}}}, wing {5} {{{2},{4}}} eliminates {4} from cells seeing both wings + XY-Wing: Pivot {0} {{{1},{2}}}, Flügel {3} {{{1},{4}}}, Flügel {5} {{{2},{4}}} eliminiert {4} aus Zellen, die beide Flügel sehen + + + Swordfish on value {0} in Rows {1}, {2}, {3} and Columns {4}, {5}, {6} eliminates {0} from other cells in those columns + Schwertfisch auf Wert {0} in Zeilen {1}, {2}, {3} und Spalten {4}, {5}, {6} eliminiert {0} aus anderen Zellen in diesen Spalten + + + Swordfish on value {0} in Columns {1}, {2}, {3} and Rows {4}, {5}, {6} eliminates {0} from other cells in those rows + Schwertfisch auf Wert {0} in Spalten {1}, {2}, {3} und Zeilen {4}, {5}, {6} eliminiert {0} aus anderen Zellen in diesen Zeilen + + + Skyscraper on value {0}: conjugate pairs in {1} and {2} share endpoint {3} — eliminates {0} from cells seeing both {4} and {5} + Wolkenkratzer auf Wert {0}: konjugierte Paare in {1} und {2} teilen Endpunkt {3} — eliminiert {0} aus Zellen, die beide {4} und {5} sehen + + + 2-String Kite on value {0}: row pair {1},{2} and column pair {3},{4} connected through shared box — eliminates {0} from cells seeing both endpoints + Zweidraht-Drachen auf Wert {0}: Zeilenpaar {1},{2} und Spaltenpaar {3},{4} durch gemeinsamen Block verbunden — eliminiert {0} aus Zellen, die beide Endpunkte sehen + + + XYZ-Wing: pivot {0} {{{1},{2},{3}}}, wing {4} and wing {5} eliminate {3} from cells seeing all three + XYZ-Wing: Pivot {0} {{{1},{2},{3}}}, Flügel {4} und Flügel {5} eliminieren {3} aus Zellen, die alle drei sehen + + + Unique Rectangle: cells {0} with values {{{1},{2}}} — eliminates {1},{2} from {3} to avoid deadly pattern + Eindeutiges Rechteck: Zellen {0} mit Werten {{{1},{2}}} — eliminiert {1},{2} aus {3} um tödliches Muster zu vermeiden + + + W-Wing: cells {0} and {1} {{{2},{3}}} connected by strong link on {2} — eliminates {3} from cells seeing both + W-Wing: Zellen {0} und {1} {{{2},{3}}} durch starke Verbindung auf {2} verbunden — eliminiert {3} aus Zellen, die beide sehen + + + Simple Coloring on {0}: same-color cells see each other — eliminates {0} from all cells of that color + Einfache Färbung auf {0}: gleichfarbige Zellen sehen einander — eliminiert {0} aus allen Zellen dieser Farbe + + + Simple Coloring on {0}: cell {1} sees both colors — eliminates {0} from {1} + Einfache Färbung auf {0}: Zelle {1} sieht beide Farben — eliminiert {0} aus {1} + + + Unique Rectangle Type 2: cells {0} with values {{{1},{2}}} — extra candidate {3} eliminated from cells seeing both floor cells in shared {4} + Eindeutiges Rechteck Typ 2: Zellen {0} mit Werten {{{1},{2}}} — zusätzlicher Kandidat {3} aus Zellen eliminiert, die beide Bodenzellen in gemeinsamer {4} sehen + + + Unique Rectangle Type 3: cells {0} with values {{{1},{2}}} — floor extras form naked subset in {3}, eliminating from other cells + Eindeutiges Rechteck Typ 3: Zellen {0} mit Werten {{{1},{2}}} — Bodenextras bilden nackte Teilmenge in {3}, eliminiert aus anderen Zellen + + + Unique Rectangle Type 4: cells {0} with values {{{1},{2}}} — strong link on {3} in {4} eliminates {5} from floor cells + Eindeutiges Rechteck Typ 4: Zellen {0} mit Werten {{{1},{2}}} — starke Verbindung auf {3} in {4} eliminiert {5} aus Bodenzellen + + + Unique Rectangle Type 6: cells {0} with values {{{1},{2}}} — {3} is conjugate in both parallel lines of the rectangle, locking the pattern — eliminates extras from floor cells + Eindeutiges Rechteck Typ 6: Zellen {0} mit Werten {{{1},{2}}} — {3} ist konjugiert in beiden parallelen Linien des Rechtecks, sperrt das Muster — eliminiert Extras aus Bodenzellen + + + Finned X-Wing on value {0} in Rows {1} and {2}, Columns {3} and {4} with fin at {5} — eliminates {0} from cells in fin's box + X-Wing mit Flosse auf Wert {0} in Zeilen {1} und {2}, Spalten {3} und {4} mit Flosse bei {5} — eliminiert {0} aus Zellen im Block der Flosse + + + Finned X-Wing on value {0} in Columns {1} and {2}, Rows {3} and {4} with fin at {5} — eliminates {0} from cells in fin's box + X-Wing mit Flosse auf Wert {0} in Spalten {1} und {2}, Zeilen {3} und {4} mit Flosse bei {5} — eliminiert {0} aus Zellen im Block der Flosse + + + Sashimi X-Wing on value {0} in Rows {1} and {2}, Columns {3} and {4} with fin at {5} — eliminates {0} from cells in fin's box + Sashimi X-Wing auf Wert {0} in Zeilen {1} und {2}, Spalten {3} und {4} mit Flosse bei {5} — eliminiert {0} aus Zellen im Block der Flosse + + + Sashimi X-Wing on value {0} in Columns {1} and {2}, Rows {3} and {4} with fin at {5} — eliminates {0} from cells in fin's box + Sashimi X-Wing auf Wert {0} in Spalten {1} und {2}, Zeilen {3} und {4} mit Flosse bei {5} — eliminiert {0} aus Zellen im Block der Flosse + + + Sashimi Swordfish on value {0} in Rows {1}, {2}, {3} with fin at {4} — eliminates {0} from cells in fin's box + Sashimi Schwertfisch auf Wert {0} in Zeilen {1}, {2}, {3} mit Flosse bei {4} — eliminiert {0} aus Zellen im Block der Flosse + + + Sashimi Swordfish on value {0} in Columns {1}, {2}, {3} with fin at {4} — eliminates {0} from cells in fin's box + Sashimi Schwertfisch auf Wert {0} in Spalten {1}, {2}, {3} mit Flosse bei {4} — eliminiert {0} aus Zellen im Block der Flosse + + + Sashimi Jellyfish on value {0} in Rows {1}, {2}, {3}, {4} with fin at {5} — eliminates {0} from cells in fin's box + Sashimi Qualle auf Wert {0} in Zeilen {1}, {2}, {3}, {4} mit Flosse bei {5} — eliminiert {0} aus Zellen im Block der Flosse + + + Sashimi Jellyfish on value {0} in Columns {1}, {2}, {3}, {4} with fin at {5} — eliminates {0} from cells in fin's box + Sashimi Qualle auf Wert {0} in Spalten {1}, {2}, {3}, {4} mit Flosse bei {5} — eliminiert {0} aus Zellen im Block der Flosse + + + Remote Pairs: chain of {{{0},{1}}} cells from {2} to {3} (length {4}) — eliminates {0},{1} from cells seeing both endpoints + Entfernte Paare: Kette von {{{0},{1}}}-Zellen von {2} bis {3} (Länge {4}) — eliminiert {0},{1} aus Zellen, die beide Endpunkte sehen + + + BUG: all cells bivalue except {0} — value {1} must be placed to avoid deadly pattern + BUG: alle Zellen bivalent außer {0} — Wert {1} muss gesetzt werden, um tödliches Muster zu vermeiden + + + Jellyfish on value {0} in Rows {1}, {2}, {3}, {4} and Columns {5}, {6}, {7}, {8} eliminates {0} from other cells in those columns + Qualle auf Wert {0} in Zeilen {1}, {2}, {3}, {4} und Spalten {5}, {6}, {7}, {8} eliminiert {0} aus anderen Zellen in diesen Spalten + + + Jellyfish on value {0} in Columns {1}, {2}, {3}, {4} and Rows {5}, {6}, {7}, {8} eliminates {0} from other cells in those rows + Qualle auf Wert {0} in Spalten {1}, {2}, {3}, {4} und Zeilen {5}, {6}, {7}, {8} eliminiert {0} aus anderen Zellen in diesen Zeilen + + + Finned Swordfish on value {0} in Rows {1}, {2}, {3} with fin at {4} — eliminates {0} from cells in fin's box + Schwertfisch mit Flosse auf Wert {0} in Zeilen {1}, {2}, {3} mit Flosse bei {4} — eliminiert {0} aus Zellen im Block der Flosse + + + Finned Swordfish on value {0} in Columns {1}, {2}, {3} with fin at {4} — eliminates {0} from cells in fin's box + Schwertfisch mit Flosse auf Wert {0} in Spalten {1}, {2}, {3} mit Flosse bei {4} — eliminiert {0} aus Zellen im Block der Flosse + + + Empty Rectangle on value {0}: ER in Box {1} with conjugate pair in {2} — eliminates {0} from {3} + Leeres Rechteck auf Wert {0}: ER in Block {1} mit konjugiertem Paar in {2} — eliminiert {0} aus {3} + + + WXYZ-Wing: pivot {0} with wings {1}, {2}, {3} — eliminates {4} from cells seeing all four + WXYZ-Wing: Pivot {0} mit Flügeln {1}, {2}, {3} — eliminiert {4} aus Zellen, die alle vier sehen + + + Finned Jellyfish on value {0} in Rows {1}, {2}, {3}, {4} with fin at {5} — eliminates {0} from cells in fin's box + Qualle mit Flosse auf Wert {0} in Zeilen {1}, {2}, {3}, {4} mit Flosse bei {5} — eliminiert {0} aus Zellen im Block der Flosse + + + Finned Jellyfish on value {0} in Columns {1}, {2}, {3}, {4} with fin at {5} — eliminates {0} from cells in fin's box + Qualle mit Flosse auf Wert {0} in Spalten {1}, {2}, {3}, {4} mit Flosse bei {5} — eliminiert {0} aus Zellen im Block der Flosse + + + XY-Chain: chain of {0} bivalue cells from {1} to {2} — eliminates {3} from cells seeing both endpoints + XY-Kette: Kette von {0} bivalenten Zellen von {1} bis {2} — eliminiert {3} aus Zellen, die beide Endpunkte sehen + + + Multi-Coloring on {0}: color sees both colors of another cluster — eliminates {0} from all cells of that color + Mehrfach-Färbung auf {0}: Farbe sieht beide Farben eines anderen Clusters — eliminiert {0} aus allen Zellen dieser Farbe + + + Multi-Coloring on {0}: cell {1} sees complementary colors from two clusters — eliminates {0} + Mehrfach-Färbung auf {0}: Zelle {1} sieht komplementäre Farben aus zwei Clustern — eliminiert {0} + + + ALS-XZ: ALS {0} and ALS {1} linked by restricted common {2} — eliminates {3} from cells seeing both ALSs + ALS-XZ: ALS {0} und ALS {1} durch eingeschränkten gemeinsamen Kandidaten {2} verbunden — eliminiert {3} aus Zellen, die beide ALS sehen + + + Sue de Coq: intersection of {0} and Box {1} — eliminates candidates from rest of line and box + Sue de Coq: Kreuzung von {0} und Block {1} — eliminiert Kandidaten aus Rest der Zeile und Block + + + Forcing Chain: assuming each candidate in {0} leads to the same conclusion — {1} + Forcing Chain: Annahme jedes Kandidaten in {0} führt zum gleichen Ergebnis — {1} + + + Nice Loop: alternating inference chain from {0} to {1} — eliminates {2} + Nice Loop: Alternierende Schlussfolgerungskette von {0} nach {1} — eliminiert {2} + + + X-Cycles on value {0}: continuous loop — eliminates {0} from cells seeing weak link endpoints + X-Zyklen auf Wert {0}: geschlossene Schleife — eliminiert {0} aus Zellen, die Endpunkte schwacher Verbindungen sehen + + + X-Cycles on value {0}: strong-strong discontinuity at {1} — places {0} + X-Zyklen auf Wert {0}: stark-stark-Diskontinuität bei {1} — setzt {0} + + + X-Cycles on value {0}: weak-weak discontinuity at {1} — eliminates {0} from {1} + X-Zyklen auf Wert {0}: schwach-schwach-Diskontinuität bei {1} — eliminiert {0} aus {1} + + + 3D Medusa: multi-digit coloring — {0} + 3D Medusa: Mehrfach-Ziffer-Färbung — {0} + + + Hidden Unique Rectangle: cells {0} with values {{{1},{2}}} — eliminates {3} from {4} to avoid deadly pattern + Verstecktes Eindeutiges Rechteck: Zellen {0} mit Werten {{{1},{2}}} — eliminiert {3} aus {4} um tödliches Muster zu vermeiden + + + Avoidable Rectangle: cells {0} with solved values {{{1},{2}}} — eliminates {3} from {4} to avoid deadly pattern + Vermeidbares Rechteck: Zellen {0} mit gelösten Werten {{{1},{2}}} — eliminiert {3} aus {4} um tödliches Muster zu vermeiden + + + ALS-XY-Wing: ALS {0}, ALS {1}, ALS {2} linked by X={3} and Y={4} — eliminates {5} from cells seeing Z-cells in A and C + ALS-XY-Wing: ALS {0}, ALS {1}, ALS {2} verbunden durch X={3} und Y={4} — eliminiert {5} aus Zellen, die Z-Zellen in A und C sehen + + + Death Blossom: stem {0} with petals {1} — eliminates {2} from cells seeing all petal Z-cells + Todesblüte: Stammzelle {0} mit Blütenblättern {1} — eliminiert {2} aus Zellen, die alle Blütenblatt-Z-Zellen sehen + + + VWXYZ-Wing: pivot {0} with wings {1}, {2}, {3}, {4} — eliminates {5} from cells seeing all Z-cells + VWXYZ-Wing: Pivot {0} mit Flügeln {1}, {2}, {3}, {4} — eliminiert {5} aus Zellen, die alle Z-Zellen sehen + + + Franken {0} on value {1}: base {2}, cover {3} — eliminates {1} from cover cells outside base + Franken-{0} auf Wert {1}: Basis {2}, Abdeckung {3} — eliminiert {1} aus Abdeckungszellen außerhalb der Basis + + + Mutant Fish on value {0}: base {1}, cover {2} — eliminates {0} from {3} cover cell(s) outside base + Mutanten-Fisch auf Wert {0}: Basis {1}, Abdeckung {2} — eliminiert {0} aus {3} Abdeckungszelle(n) außerhalb der Basis + + + Grouped X-Cycles on value {0}: chain with grouped nodes — {1} + Gruppierte X-Zyklen auf Wert {0}: Kette mit gruppierten Knoten — {1} + + + Kraken Fish on value {0}: finned fish with chain-verified eliminations from {1} + Kraken-Fisch auf Wert {0}: Fisch mit Flosse und kettenverifizierten Eliminierungen von {1} + + + ALS Chain ({0} ALSs): eliminates {1} from cells seeing Z-cells in first and last ALS at {2} + ALS-Kette ({0} ALS): eliminiert {1} aus Zellen, die Z-Zellen in erstem und letztem ALS sehen bei {2} + + + Junior Exocet: base cells {0} and {1} with candidates {{{2}}} — targets {3} and {4} can only contain base candidates + Junior Exocet: Basiszellen {0} und {1} mit Kandidaten {{{2}}} — Zielzellen {3} und {4} dürfen nur Basiskandidaten enthalten + + + Unique Loop: cells {0} with values {{{1},{2}}} — eliminates {1},{2} from {3} to avoid deadly pattern + Eindeutige Schleife: Zellen {0} mit Werten {{{1},{2}}} — eliminiert {1},{2} aus {3} um tödliches Muster zu vermeiden + + + Continuous Nice Loop: loop of {0} nodes — eliminates {1} candidate(s) via weak link logic + Kontinuierlicher Nice Loop: Schleife mit {0} Knoten — eliminiert {1} Kandidat(en) durch schwache Verbindungslogik + + + Grouped Nice Loop: alternating inference chain from {0} to {1} — eliminates {2} + Gruppierter Nice Loop: Alternierende Schlussfolgerungskette von {0} nach {1} — eliminiert {2} + + + diff --git a/resources/translations/sudoku_en.ts b/resources/translations/sudoku_en.ts new file mode 100644 index 0000000..870b7f6 --- /dev/null +++ b/resources/translations/sudoku_en.ts @@ -0,0 +1,1960 @@ + + + + + Sudoku + + Sudoku + + + + Game + + + + New Game + + + + Reset Puzzle + + + + Save + + + + Load + + + + Statistics + + + + Export Aggregate Stats to CSV + + + + Export Game Sessions to CSV + + + + Exit + + + + Edit + + + + Undo + + + + Redo + + + + Clear Cell + + + + Help + + + + Get Hint + + + + Get Coaching Hint + + + + About + + + + Training Mode + + + + Analyze Position + + + + Resume Game + + + + Settings... + + + + Third-Party Licenses + + + + ▶ New Game + + + + Difficulty: + + + + Hints: + + + + Easy + + + + Medium + + + + Hard + + + + Expert + + + + Master + + + + Unknown + + + + Fill Notes + + + + Clear Notes + + + + Undo Until Valid + + + + Normal + + + + Notes + + + + Color + + + + Select a technique to practice: + + + + Back to Game + + + + Foundations + + + + Subset Basics + + + + Intersections & Quads + + + + Basic Fish & Wings + + + + Links & Rectangles + + + + Advanced Fish & Wings + + + + Advanced Fish (Finned) + + + + Chains & Set Logic + + + + Inference Engines + + + + What It Is: + + + + What to Look For: + + + + Start Exercises + + + + Back + + + + {0} difficulty points + + + + Prerequisites: + + + + Exercise {0} / {1} - {2} + + + + Color: + + + + Submit + + + + Hint + + + + Skip + + + + Quit Lesson + + + + Next Exercise + + + + Retry + + + + Show Solution + + + + Score: {0} / {1} + + + + Correct! + + + + Partially Correct + + + + Incorrect + + + + Lesson Complete! + + + + Try Again + + + + Pick Technique + + + + Return to Game + + + + Technique: {0} + + + + Hints used: {0} + + + + Mastery: {0} + + + + {0} ({1} pts) + + + + Prerequisites not met + + + + Recommended next technique + + + + Applicable at current position + + + + Excellent! You've mastered this technique. + + + + Good progress. Try again for a higher score. + + + + Keep practicing! Review the theory and try again. + + + + Cannot practice Backtracking — it is not a logical technique. + + + + No applicable step found for this technique. + + + + Correct! {0} Find the next one. + + + + Correct! {0} + + + + Partially correct. {0} + + + + Not quite. {0} + + + + Unknown result. + + + + Beginner + + + + Intermediate + + + + Proficient + + + + Mastered + + + + Completed! + + + + Playing + + + + Ready + + + + No game loaded. Start a new game! + + + + Start a new {0} game? +Current progress will be lost. + + + + All progress on this puzzle will be lost, including placed numbers, notes, and hints. The timer will restart. + + + + Save Game + + + + Enter save name: + + + + Current Game + + + + Difficulty + + + + Time + + + + Moves + + + + Mistakes + + + + Enter save name... + + + + Please enter a save name. + + + + A save named "{0}" already exists. Overwrite it? + + + + Load Game + + + + Name + + + + Last Modified + + + + Elapsed + + + + Rating + + + + Games Played: {0} + + + + Games Completed: {0} + + + + Completion Rate: {0:.1f}% + + + + Best Time: {0} + + + + Average Time: {0} + + + + Current Streak: {0} + + + + Best Streak: {0} + + + + N/A + + + + Overview + + + + Per Difficulty + + + + Recent Games + + + + Total Moves + + + + Total Hints Used + + + + Total Mistakes + + + + Total Time Played + + + + Played + + + + Completed + + + + Best Time + + + + Avg Time + + + + Avg SE Rating + + + + Date + + + + Sudoku Game + + + + Built with: + + + + A feature-rich offline Sudoku application. + + + + SE {0} + + + + Settings + + + + Gameplay + + + + Display + + + + Maximum Hints: + + + + Auto-save Interval: + + + + Default Difficulty: + + + + seconds + + + + Highlight Conflicts + + + + Show Hints + + + + Collect detailed match statistics + + + + Encrypt session data + + + + Session data is encrypted by default for privacy. Disable to inspect the raw data file yourself. + + + + Disabling session tracking will stop recording per-game statistics. Would you like to delete existing session history? + + + + Input mode (Space to cycle, N for Notes) + + + + Place {0} in selected cell + + + + Eliminate {0} from selected cell + + + + Puzzle Difficulty + + + + Puzzle Rating: SE {0} + + + + Techniques required to solve: + + + + No technique details available. + + + + SE {0} ({1} techniques) + + + + Game saved successfully + + + + Aggregate stats exported to CSV + + + + Game sessions exported to CSV + + + + Export failed: {0} + + + + No logical strategies found at this position. + + + + Language + + + + Failed to generate puzzle + + + + Failed to load game + + + + No active game to save + + + + Failed to save game + + + + Failed to export statistics + + + + Failed to export aggregate stats + + + + Failed to export game sessions + + + + File access error + + + + Serialization error + + + + No valid state in history + + + + Board is already valid + + + + Undone to last valid state + + + + Puzzle completed in {0}:{1}! New game started. + + + + Solution has errors. Keep trying! + + + + No hints remaining (0/10 used) + + + + Please select a cell first + + + + Cannot reveal hint for given cells + + + + Cell already has a value + + + + No logical technique found for this puzzle + + + + Suggestion: Place {0} at R{1}C{2} + + + + No coaching hints remaining + + + + No logical technique found + + + + Level {0}/3 + + + + Try applying this step yourself, then press Check to verify. + + + + Correct! You found all {0}/{1}. + + + + {0}/{1} correct, {2} missed. + + + + Some actions were incorrect. {0}/{1} correct, {2} wrong. + + + + Check + + + + Apply + + + + Try it! + + + + What to look for: + + + + 0/{0} correct — try making some changes first. + + + + Look at cell {0}. + + + + Focus on {0} — count the candidates. + + + + Count the candidates in cell {0}. + + + + The value is {0}. + + + + Focus on {0}. + + + + Look for cells that share the same candidates in a unit. + + + + These cells form a [{0}] subset. Values in the subset can only go in these cells — eliminate them from other cells in the region. + + + + These cells form the subset. Values in the subset can only go in these cells — eliminate them from other cells in the region. + + + + Eliminate candidates from cells that see all subset cells. + + + + Look for value {0} confined to an intersection. + + + + Look for a candidate confined to the intersection of a box and a line. + + + + The intersection cells. The candidate is confined to these cells — eliminate it from other cells in the line or box outside this intersection. + + + + Eliminate the candidate from cells outside the intersection. + + + + Look for a fish pattern on value {0}. + + + + Look for a fish pattern (rows/columns with restricted candidate positions). + + + + Base and cover sets. Blue cells are the base set (rows/columns where the candidate is restricted). Green cells are the cover set. Eliminate the candidate from cover set cells that aren't in the base set. + + + + Eliminate the candidate from cover set cells outside the base set. + + + + Find the pivot cell at {0}. + + + + Pivot and wing cells. The orange pivot connects to the green wings. Candidates shared by both wings can be eliminated from cells that see all wing endpoints. + + + + Eliminate the shared candidate from cells that see all wing endpoints. + + + + Look for conjugate pairs on value {0}. + + + + Look for conjugate pairs (cells where a digit appears exactly twice in a unit). + + + + The chain cells. These cells form conjugate pairs (a digit appears exactly twice in a unit). Follow the alternating pattern to find eliminations. + + + + Cells that see both endpoints of the pattern can be eliminated. + + + + Build a coloring chain on value {0}. + + + + Start coloring conjugate pairs with two alternating colors. + + + + The coloring chain. Blue and green are two alternating colors — one must be true, one false. Cells that see both colors can have the candidate eliminated. + + + + One color must be false — eliminate from cells that see both colors. + + + + Look for a deadly pattern — four cells forming a rectangle across two boxes. + + + + The rectangle corners. These four cells across two boxes form a potential deadly pattern. To keep the puzzle unique, eliminate the candidate that would complete the rectangle. + + + + To avoid the deadly pattern, eliminate the candidate that would complete it. + + + + Start the chain from cell {0}. + + + + Look for a chain of linked cells with alternating strong/weak links. + + + + The chain path. Follow the alternating strong (blue) and weak (green) links. The chain's logic forces a conclusion at the endpoints. + + + + All chains lead to value {0} at {1}. + + + + Eliminate candidates that contradict the chain logic. + + + + Look for an Almost Locked Set (a group of N cells with N+1 candidates). + + + + The ALS cells and restricted common. An ALS is N cells with N+1 candidates. The restricted common candidate links the sets — eliminations apply to cells that see all relevant ALS members. + + + + Eliminate candidates from cells that see all relevant ALS members. + + + + Look for the cell with three candidates (the only non-bivalue cell). + + + + The key cell is {0}. + + + + A cell has only one possible candidate left. All other values are eliminated by row, column, and box constraints. + + + + Look for cells where 8 of the 9 values are already present in the cell's row, column, or box. + + + + A value can only go in one cell within a row, column, or box. Even though the cell may have multiple candidates, only this value has no other place in the region. + + + + For each region, check if any value has only one possible cell. + + + + Two cells in the same region each contain exactly the same two candidates. Those two values must go in those two cells, so they can be eliminated from all other cells in the region. + + + + Find two cells in a row, column, or box that share the same pair of candidates. + + + + Three cells in a region collectively contain exactly three candidates. Each cell has a subset of those three values. Those values can be eliminated from other cells in the region. + + + + Find three cells in a region whose combined candidates form a set of exactly three values. + + + + Two values in a region appear as candidates in exactly the same two cells. Other candidates in those two cells can be eliminated. + + + + For each region, find two values that appear only in the same two cells. + + + + Three values in a region appear as candidates in exactly three cells. Other candidates in those cells can be eliminated. + + + + For each region, find three values confined to exactly three cells. + + + + A candidate in a box is confined to a single row or column. That candidate can be eliminated from the rest of that row or column outside the box. + + + + In each box, check if a candidate appears only in one row or one column. + + + + A candidate in a row or column is confined to a single box. That candidate can be eliminated from the rest of the box outside that row or column. + + + + In each row/column, check if a candidate appears only within one box. + + + + Four cells in a region collectively contain exactly four candidates. Those values can be eliminated from other cells in the region. + + + + Find four cells in a region whose combined candidates form a set of exactly four values. + + + + Four values in a region appear as candidates in exactly four cells. Other candidates in those cells can be eliminated. + + + + For each region, find four values confined to exactly four cells. + + + + A candidate appears in exactly two cells in each of two rows, and those cells are in the same two columns. The candidate can be eliminated from other cells in those columns. + + + + Find a candidate forming a rectangle pattern: two rows, two columns, four cells. + + + + A pivot cell with candidates {A,B} sees two wing cells: one with {A,C} and one with {B,C}. Value C can be eliminated from any cell that sees both wings. + + + + Find a bivalue cell (pivot) that sees two other bivalue cells sharing one candidate each. + + + + A candidate appears in 2-3 cells in each of three rows, and those cells fall in exactly three columns. The candidate can be eliminated from other cells in those columns. + + + + Extend the X-Wing pattern to three rows and three columns. + + + + Two conjugate pairs for a digit share one endpoint in the same row or column. The digit can be eliminated from cells that see both non-shared endpoints. + + + + Find two rows (or columns) each with exactly two cells for a digit, sharing one column (or row). + + + + A conjugate pair in a row and a conjugate pair in a column are connected through a box. The digit can be eliminated from the cell that sees both unconnected endpoints. + + + + Find a row pair and column pair for the same digit connected via a shared box. + + + + A pivot cell with candidates {A,B,C} sees a wing with {A,B} and a wing with {A,C}. Value A can be eliminated from cells that see all three cells. + + + + Find a trivalue pivot seeing two bivalue wings that each share two candidates with the pivot. + + + + Four cells forming a rectangle across two boxes would create a deadly pattern (two solutions) if they all had the same two candidates. Extra candidates in some cells can force eliminations to avoid this ambiguity. + + + + Find four cells in a rectangle across two boxes sharing the same two candidates. + + + + Two cells with the same pair of candidates {A,B} are connected by a strong link on value A. Value B can be eliminated from cells that see both endpoints. + + + + Find two identical bivalue cells connected by a conjugate pair on one of their values. + + + + For a single digit, build chains of conjugate pairs and assign two colors. If both colors appear in the same region, one color is false and its candidates are eliminated. + + + + Pick a digit, trace conjugate pairs, color alternately. Check for color conflicts. + + + + An X-Wing pattern with one extra candidate cell (the fin) in the same box as a corner. Eliminations are restricted to cells that see both the fin and the X-Wing column. + + + + Find an X-Wing where one row has an extra candidate cell in the same box. + + + + A chain of bivalue cells all containing the same pair {A,B}, where each adjacent pair shares a region. Cells seeing both endpoints of an even-length chain lose both values. + + + + Find a chain of identical bivalue cells connected through shared regions. + + + + If all unsolved cells have exactly two candidates except one cell with three, the puzzle would have multiple solutions unless the trivalue cell is set to the value that appears three times in its row, column, or box. + + + + Check if only one cell has more than two candidates. If so, find its odd-count value. + + + + A candidate appears in 2-4 cells in each of four rows, and those cells fall in exactly four columns. The candidate can be eliminated from other cells in those columns. + + + + Extend the Swordfish pattern to four rows and four columns. + + + + A Swordfish pattern with extra candidate cells (fins) in the same box. Eliminations are restricted to cells seeing both the fin box and the Swordfish columns. + + + + Find a Swordfish where one row has extra candidates in the same box. + + + + A digit's candidates in a box form an L-shape or cross, leaving an empty rectangle. Combined with a conjugate pair outside the box, this eliminates the digit from a target cell. + + + + Find a box where a digit's candidates leave an empty rectangle, connected to a conjugate pair. + + + + A four-cell wing pattern: a pivot and three wings collectively contain four candidates, and a shared candidate Z can be eliminated from cells seeing all cells containing Z. + + + + Find a group of four cells with exactly four combined candidates sharing a common value. + + + + A Jellyfish pattern with extra fin cells in the same box. Eliminations are restricted to cells seeing both the fin box and the Jellyfish columns. + + + + Find a Jellyfish where one row has extra candidates forming a fin. + + + + A chain of bivalue cells where consecutive cells share a candidate value, alternating between the two candidates. The value shared by the chain's endpoints can be eliminated from cells that see both endpoints. + + + + Build a chain of bivalue cells connected by shared candidates. Check the endpoints. + + + + Build separate conjugate pair chains (clusters) for a digit and color each. When two clusters interact (cells in different clusters see each other), eliminations can be made from cells that see conflicting colors across clusters. + + + + Color multiple conjugate chains for one digit, then check cross-cluster interactions. + + + + Two Almost Locked Sets (each has N cells with N+1 candidates) share a restricted common candidate X. A second common candidate Z can be eliminated from cells that see all Z-cells in both sets. + + + + Find two groups of cells that are almost locked, sharing a restricted common candidate. + + + + An intersection of a line and box contains 2-3 cells whose candidates can be covered by two Almost Locked Sets (one from the line remainder, one from the box remainder). Extra ALS candidates can be eliminated from their respective remainders. + + + + Find an intersection where candidates can be partitioned into two covering ALS. + + + + For a cell with 2-3 candidates, assume each candidate is true and propagate the consequences. If all assumptions lead to the same conclusion (a placement or elimination), that conclusion must be true. + + + + Pick a cell with few candidates. Try each value and propagate. Look for common outcomes. + + + + Build a chain of alternating strong and weak links between (cell, digit) pairs. If the chain forms a loop or its endpoints share a digit, eliminations can be derived from the alternating inference chain rules. + + + + Trace alternating strong/weak links. Check if endpoints share a digit for eliminations. + + + + For a single digit, build a chain of alternating strong and weak links. Type 1 (continuous loop) eliminates the digit from cells seeing weak link endpoints. Type 2 places the digit at a strong-strong discontinuity. Type 3 eliminates at a weak-weak discontinuity. + + + + For each digit, trace alternating strong/weak links and look for cycles or discontinuities. + + + + Multi-digit coloring: build a graph of (cell, digit) pairs connected by strong links (conjugate pairs and bivalue cells). Color with two alternating colors. Apply six rules to find contradictions or trap eliminations. + + + + Extend single-digit coloring to multiple digits via bivalue cell connections. + + + + A deadly rectangle pattern where one or more corners have the UR values hidden among other candidates. Strong links on UR values in shared units force eliminations to avoid the deadly pattern. + + + + Find a rectangle across two boxes where the UR values are present but hidden by extras. + + + + Like Unique Rectangle but using the distinction between given clues and solver-placed values. If three solver-placed corners of a rectangle have values {A,B}, the fourth unsolved corner cannot complete the deadly pattern. + + + + Find rectangles where three corners are solver-placed (not givens) with two values. + + + + Three non-overlapping Almost Locked Sets linked by restricted commons: A-B linked by X, B-C linked by Y (Y != X). Common value Z in both A and C can be eliminated from cells seeing all Z-cells in both A and C. + + + + Find three ALSs forming a chain with two restricted common candidates. + + + + A stem cell with 2-3 candidates, each linked to a petal ALS via restricted common. A value Z common across all petals (but not in the stem) can be eliminated from cells seeing all Z-cells in all petals. + + + + Find a cell whose candidates each connect to an ALS via restricted common. + + + + A five-cell wing pattern: a pivot and four wings collectively contain five candidates. The non-restricted shared value Z can be eliminated from cells seeing all Z-cells. + + + + Find five cells with exactly five combined candidates and a restricted elimination value. + + + + A fish pattern (X-Wing/Swordfish/Jellyfish) where base and cover sets are mixed rows/columns and boxes. At least one base set must be a box. Eliminates from cover cells outside the base. + + + + Look for fish patterns that include boxes as base or cover sets. + + + + Extends X-Cycles by allowing grouped nodes: 2-3 cells in the same box on the same row or column that all have a candidate digit. Same Type 1/2/3 rules apply. + + + + Build X-Cycle chains using grouped box nodes alongside individual cells. + + + + A fish pattern where one base row has only one candidate position instead of two. The missing position is compensated by a fin cell, restricting eliminations to the fin's box. + + + + Look for an X-Wing-like pattern where one row is incomplete — it only has the candidate in one of the two expected columns, plus an extra fin cell. + + + + A 3-row fish pattern where at least one base row has fewer candidate positions than expected. The missing position creates a fin that restricts eliminations. + + + + Find a Swordfish shape where one row only covers 1 of the 3 base columns, plus a fin. + + + + A 4-row fish pattern where at least one base row has fewer candidate positions than expected. The missing position creates a fin restricting eliminations. + + + + Find a Jellyfish shape where one row only covers 1 of the 4 base columns, plus a fin. + + + + For a digit in a unit with 2-3 positions, assume the digit goes in each position and propagate. If all branches lead to the same conclusion, that conclusion is true. + + + + Find a unit where a digit appears in few cells, then try each placement. + + + + For a digit in a box with 2-3 positions, assume the digit goes in each position and propagate. If all branches lead to the same conclusion, that conclusion is true. + + + + Find a box where a digit appears in few cells, then try each placement. + + + + A fish pattern where BOTH base and cover sets freely mix rows, columns, and boxes. Unlike Franken Fish (one mixed side), Mutant Fish requires both sides to contain at least 2 different unit types. Eliminates from cover cells outside the base. + + + + Look for fish patterns where both the base set and cover set mix rows, columns, and boxes. + + + + Extends finned fish by using chain propagation to verify eliminations outside the fin's box. For each candidate that a standard finned fish would reject (outside the fin's box), place the digit at the fin cell and propagate. If the target still loses the candidate, the elimination is valid regardless of whether the fin is true or false. + + + + Find a finned fish pattern, then check if chain propagation from the fin cell eliminates the digit from cells outside the fin's box. + + + + A generalized chain of 4-6 Almost Locked Sets linked by distinct restricted commons. Values common across the chain endpoints can be eliminated from cells that see all relevant ALS members. + + + + Find a chain of ALSs where each adjacent pair shares a restricted common. + + + + A deadly pattern where 4-6 cells form a loop, each consecutive pair sharing a unit (row, column, or box). All cells contain the same candidate pair {A,B}. If all cells had only {A,B}, two solutions would exist. Cells with extra candidates must keep them. + + + + Find a loop of 4-6 cells across at least 2 boxes where each cell has candidates {A,B} and each consecutive pair shares a row, column, or box. If exactly one cell has extras, eliminate A and B from it. + + + + A base pair of cells in a box whose candidates must appear in specific target cells in other boxes along cross-lines. Candidates not matching the base pair pattern can be eliminated from the target cells. + + + + Find a base pair in a box with target cells in aligned boxes along cross-lines. + + + + A continuous alternating inference chain (AIC) that forms a complete loop. Every weak link in the loop produces eliminations: the digit can be removed from any cell outside the loop that sees both endpoints of a weak link. + + + + Build an AIC where the chain closes back on itself with consistent alternating links. Every weak link segment yields eliminations from external cells seeing both endpoints. + + + + Extends Nice Loop (AIC) with grouped nodes: 2-3 cells in the same box on the same row or column that all share a candidate digit. Combines multi-digit cell-based links (bivalue strong links) with single-digit grouped unit links for stronger chains. + + + + Build AIC chains using grouped box nodes alongside individual cells. Look for discontinuous Type 2 chains where both endpoints assert the same digit. + + + + A brute-force trial-and-error method. Not a logical technique — used as a fallback when no logical strategy can make progress. + + + + This technique is not used in training exercises. + + + + Unknown technique. + + + + No identification tips available. + + + + {0} (SE {1}) + + + + Backtracking (trial & error) + + + + Invalid game data + + + + Invalid difficulty + + + + Game not started + + + + Game already ended + + + + Unknown statistics error + + + + Naked Single + + + + Hidden Single + + + + Naked Pair + + + + Naked Triple + + + + Hidden Pair + + + + Hidden Triple + + + + Pointing Pair + + + + Box/Line Reduction + + + + Naked Quad + + + + Hidden Quad + + + + X-Wing + + + + XY-Wing + + + + Swordfish + + + + Skyscraper + + + + 2-String Kite + + + + XYZ-Wing + + + + Unique Rectangle + + + + W-Wing + + + + Simple Coloring + + + + Finned X-Wing + + + + Remote Pairs + + + + BUG + + + + Jellyfish + + + + Finned Swordfish + + + + Empty Rectangle + + + + WXYZ-Wing + + + + Finned Jellyfish + + + + XY-Chain + + + + Multi-Coloring + + + + ALS-XZ + + + + Sue de Coq + + + + Forcing Chain + + + + Nice Loop + + + + X-Cycles + + + + 3D Medusa + + + + Hidden Unique Rectangle + + + + Avoidable Rectangle + + + + ALS-XY-Wing + + + + Death Blossom + + + + VWXYZ-Wing + + + + Franken Fish + + + + Grouped X-Cycles + + + + Sashimi X-Wing + + + + Sashimi Swordfish + + + + Sashimi Jellyfish + + + + Unit Forcing Chain + + + + Region Forcing Chain + + + + Mutant Fish + + + + Kraken Fish + + + + ALS Chain + + + + Junior Exocet + + + + Unique Loop + + + + Continuous Nice Loop + + + + Grouped Nice Loop + + + + Backtracking + + + + Unknown Technique + + + + Row + + + + Column + + + + Box + + + + Unknown Region + + + + R{0}C{1} + + + + Naked Single at {0}: only value {1} is possible + + + + Hidden Single at {0}: value {1} can only appear in this cell within its region + + + + Naked Pair [{0}] at {1} in {2} eliminates candidates from other cells + + + + Naked Triple [{0}] at {1} in {2} eliminates candidates from other cells + + + + Hidden Pair [{0}] at {1} in {2} eliminates other candidates from these cells + + + + Hidden Triple [{0}] at {1} in {2} eliminates other candidates from these cells + + + + Pointing Pair: {0} in Box {1} confined to {2} {3} eliminates {0} from other cells in {2} {3} + + + + Box/Line Reduction: {0} in {1} {2} confined to Box {3} eliminates {0} from other cells in Box {3} + + + + Naked Quad [{0}] at {1} in {2} eliminates candidates from other cells + + + + Hidden Quad [{0}] at {1} in {2} eliminates other candidates from these cells + + + + X-Wing on value {0} in Rows {1} and {2}, Columns {3} and {4} eliminates {0} from other cells in those columns + + + + X-Wing on value {0} in Columns {1} and {2}, Rows {3} and {4} eliminates {0} from other cells in those rows + + + + XY-Wing: pivot {0} {{{1},{2}}}, wing {3} {{{1},{4}}}, wing {5} {{{2},{4}}} eliminates {4} from cells seeing both wings + + + + Swordfish on value {0} in Rows {1}, {2}, {3} and Columns {4}, {5}, {6} eliminates {0} from other cells in those columns + + + + Swordfish on value {0} in Columns {1}, {2}, {3} and Rows {4}, {5}, {6} eliminates {0} from other cells in those rows + + + + Skyscraper on value {0}: conjugate pairs in {1} and {2} share endpoint {3} — eliminates {0} from cells seeing both {4} and {5} + + + + 2-String Kite on value {0}: row pair {1},{2} and column pair {3},{4} connected through shared box — eliminates {0} from cells seeing both endpoints + + + + XYZ-Wing: pivot {0} {{{1},{2},{3}}}, wing {4} and wing {5} eliminate {3} from cells seeing all three + + + + Unique Rectangle: cells {0} with values {{{1},{2}}} — eliminates {1},{2} from {3} to avoid deadly pattern + + + + W-Wing: cells {0} and {1} {{{2},{3}}} connected by strong link on {2} — eliminates {3} from cells seeing both + + + + Simple Coloring on {0}: same-color cells see each other — eliminates {0} from all cells of that color + + + + Simple Coloring on {0}: cell {1} sees both colors — eliminates {0} from {1} + + + + Unique Rectangle Type 2: cells {0} with values {{{1},{2}}} — extra candidate {3} eliminated from cells seeing both floor cells in shared {4} + + + + Unique Rectangle Type 3: cells {0} with values {{{1},{2}}} — floor extras form naked subset in {3}, eliminating from other cells + + + + Unique Rectangle Type 4: cells {0} with values {{{1},{2}}} — strong link on {3} in {4} eliminates {5} from floor cells + + + + Unique Rectangle Type 6: cells {0} with values {{{1},{2}}} — {3} is conjugate in both parallel lines of the rectangle, locking the pattern — eliminates extras from floor cells + + + + Finned X-Wing on value {0} in Rows {1} and {2}, Columns {3} and {4} with fin at {5} — eliminates {0} from cells in fin's box + + + + Finned X-Wing on value {0} in Columns {1} and {2}, Rows {3} and {4} with fin at {5} — eliminates {0} from cells in fin's box + + + + Sashimi X-Wing on value {0} in Rows {1} and {2}, Columns {3} and {4} with fin at {5} — eliminates {0} from cells in fin's box + + + + Sashimi X-Wing on value {0} in Columns {1} and {2}, Rows {3} and {4} with fin at {5} — eliminates {0} from cells in fin's box + + + + Sashimi Swordfish on value {0} in Rows {1}, {2}, {3} with fin at {4} — eliminates {0} from cells in fin's box + + + + Sashimi Swordfish on value {0} in Columns {1}, {2}, {3} with fin at {4} — eliminates {0} from cells in fin's box + + + + Sashimi Jellyfish on value {0} in Rows {1}, {2}, {3}, {4} with fin at {5} — eliminates {0} from cells in fin's box + + + + Sashimi Jellyfish on value {0} in Columns {1}, {2}, {3}, {4} with fin at {5} — eliminates {0} from cells in fin's box + + + + Remote Pairs: chain of {{{0},{1}}} cells from {2} to {3} (length {4}) — eliminates {0},{1} from cells seeing both endpoints + + + + BUG: all cells bivalue except {0} — value {1} must be placed to avoid deadly pattern + + + + Jellyfish on value {0} in Rows {1}, {2}, {3}, {4} and Columns {5}, {6}, {7}, {8} eliminates {0} from other cells in those columns + + + + Jellyfish on value {0} in Columns {1}, {2}, {3}, {4} and Rows {5}, {6}, {7}, {8} eliminates {0} from other cells in those rows + + + + Finned Swordfish on value {0} in Rows {1}, {2}, {3} with fin at {4} — eliminates {0} from cells in fin's box + + + + Finned Swordfish on value {0} in Columns {1}, {2}, {3} with fin at {4} — eliminates {0} from cells in fin's box + + + + Empty Rectangle on value {0}: ER in Box {1} with conjugate pair in {2} — eliminates {0} from {3} + + + + WXYZ-Wing: pivot {0} with wings {1}, {2}, {3} — eliminates {4} from cells seeing all four + + + + Finned Jellyfish on value {0} in Rows {1}, {2}, {3}, {4} with fin at {5} — eliminates {0} from cells in fin's box + + + + Finned Jellyfish on value {0} in Columns {1}, {2}, {3}, {4} with fin at {5} — eliminates {0} from cells in fin's box + + + + XY-Chain: chain of {0} bivalue cells from {1} to {2} — eliminates {3} from cells seeing both endpoints + + + + Multi-Coloring on {0}: color sees both colors of another cluster — eliminates {0} from all cells of that color + + + + Multi-Coloring on {0}: cell {1} sees complementary colors from two clusters — eliminates {0} + + + + ALS-XZ: ALS {0} and ALS {1} linked by restricted common {2} — eliminates {3} from cells seeing both ALSs + + + + Sue de Coq: intersection of {0} and Box {1} — eliminates candidates from rest of line and box + + + + Forcing Chain: assuming each candidate in {0} leads to the same conclusion — {1} + + + + Nice Loop: alternating inference chain from {0} to {1} — eliminates {2} + + + + X-Cycles on value {0}: continuous loop — eliminates {0} from cells seeing weak link endpoints + + + + X-Cycles on value {0}: strong-strong discontinuity at {1} — places {0} + + + + X-Cycles on value {0}: weak-weak discontinuity at {1} — eliminates {0} from {1} + + + + 3D Medusa: multi-digit coloring — {0} + + + + Hidden Unique Rectangle: cells {0} with values {{{1},{2}}} — eliminates {3} from {4} to avoid deadly pattern + + + + Avoidable Rectangle: cells {0} with solved values {{{1},{2}}} — eliminates {3} from {4} to avoid deadly pattern + + + + ALS-XY-Wing: ALS {0}, ALS {1}, ALS {2} linked by X={3} and Y={4} — eliminates {5} from cells seeing Z-cells in A and C + + + + Death Blossom: stem {0} with petals {1} — eliminates {2} from cells seeing all petal Z-cells + + + + VWXYZ-Wing: pivot {0} with wings {1}, {2}, {3}, {4} — eliminates {5} from cells seeing all Z-cells + + + + Franken {0} on value {1}: base {2}, cover {3} — eliminates {1} from cover cells outside base + + + + Mutant Fish on value {0}: base {1}, cover {2} — eliminates {0} from {3} cover cell(s) outside base + + + + Grouped X-Cycles on value {0}: chain with grouped nodes — {1} + + + + Kraken Fish on value {0}: finned fish with chain-verified eliminations from {1} + + + + ALS Chain ({0} ALSs): eliminates {1} from cells seeing Z-cells in first and last ALS at {2} + + + + Junior Exocet: base cells {0} and {1} with candidates {{{2}}} — targets {3} and {4} can only contain base candidates + + + + Unique Loop: cells {0} with values {{{1},{2}}} — eliminates {1},{2} from {3} to avoid deadly pattern + + + + Continuous Nice Loop: loop of {0} nodes — eliminates {1} candidate(s) via weak link logic + + + + Grouped Nice Loop: alternating inference chain from {0} to {1} — eliminates {2} + + + + diff --git a/scripts/check_translation_placeholders.py b/scripts/check_translation_placeholders.py new file mode 100755 index 0000000..f438548 --- /dev/null +++ b/scripts/check_translation_placeholders.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Validate fmt-style placeholders in Qt Linguist .ts translations. + +The runtime feeds translated strings to ``fmt::format(fmt::runtime(...), args)``, +so a translator who drops, renumbers, or mangles a ``{0}``-style placeholder +introduces a ``fmt::format_error`` that fires only on the affected locale. +``lupdate`` does not validate fmt syntax (it knows about Qt's ``%1`` instead). + +This script parses every in each non-source-language .ts file and +fails if the multiset of placeholder field names in differs +from , or if either string is malformed. + +Exit codes: + 0 every translation has matching placeholders. + 1 at least one mismatch or malformed string. + 2 argument error. +""" +from __future__ import annotations + +import argparse +import string +import sys +import xml.etree.ElementTree as ET +from collections import Counter +from pathlib import Path + + +def extract_placeholders(s: str) -> tuple[Counter[str], str | None]: + """Return (multiset of field names, error message or None). + + Uses Python's ``string.Formatter`` to parse the string the same way fmt does + for positional/named substitutions. Field names like ``0`` or ``1:.1f`` are + captured as ``"0"`` / ``"1"``; literal ``{{`` / ``}}`` are skipped. + """ + counts: Counter[str] = Counter() + try: + for _literal, field_name, _spec, _conv in string.Formatter().parse(s): + if field_name is not None: + counts[field_name] += 1 + except ValueError as e: + return counts, f"malformed format string: {e}" + return counts, None + + +def has_positional_placeholder(counts: Counter[str]) -> bool: + """True if any field name is digit-only (positional, e.g. {0}).""" + return any(name.isdigit() for name in counts) + + +def check_ts_file(path: Path) -> list[str]: + """Return a list of human-readable error messages for the given .ts file.""" + errors: list[str] = [] + try: + tree = ET.parse(path) + except ET.ParseError as e: + return [f"{path}: XML parse error: {e}"] + + for msg in tree.getroot().iter("message"): + source_el = msg.find("source") + translation_el = msg.find("translation") + if source_el is None or source_el.text is None: + continue + if translation_el is None: + continue + # Skip unfinished entries — Qt falls back to at runtime so the + # placeholder set is whatever has, by definition consistent. + if translation_el.get("type") == "unfinished": + continue + translated = translation_el.text or "" + if not translated: + continue + + src_counts, src_err = extract_placeholders(source_el.text) + + # Only validate strings the developer intended to fmt::format. The + # signal is a positional placeholder ({0}, {1}, …) in the source, + # since that's what every locFormat call site in the codebase uses. + # Strings without positional placeholders are rendered via plain + # core::loc, so literal braces (e.g. Sudoku notation like "{A,B}") + # in translations are harmless and intentional. + if src_err: + errors.append(f"{path}: {src_err} for: {source_el.text!r}") + continue + if not has_positional_placeholder(src_counts): + continue + + tr_counts, tr_err = extract_placeholders(translated) + if tr_err: + errors.append(f"{path}: {tr_err} for source {source_el.text!r}") + continue + + if src_counts != tr_counts: + errors.append( + f"{path}: placeholder mismatch for source {source_el.text!r}\n" + f" source placeholders: {dict(src_counts)}\n" + f" translation placeholders: {dict(tr_counts)}\n" + f" translation: {translated!r}" + ) + + return errors + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("paths", nargs="*", type=Path, + help=".ts files to check (default: resources/translations/sudoku_*.ts " + "excluding the source-language sudoku_en.ts)") + p.add_argument("--source-locale", default="en", + help="locale code to skip as the source language (default: en)") + args = p.parse_args(argv) + + if args.paths: + targets = list(args.paths) + else: + repo_root = Path(__file__).resolve().parent.parent + targets = sorted((repo_root / "resources" / "translations").glob("sudoku_*.ts")) + skip = f"sudoku_{args.source_locale}.ts" + targets = [t for t in targets if t.name != skip] + + if not targets: + print("No .ts files to check.", file=sys.stderr) + return 0 + + all_errors: list[str] = [] + for ts in targets: + all_errors.extend(check_ts_file(ts)) + + if all_errors: + print("Translation placeholder validation FAILED:\n", file=sys.stderr) + for err in all_errors: + print(f" {err}", file=sys.stderr) + print(f"\n{len(all_errors)} issue(s) across {len(targets)} file(s).", + file=sys.stderr) + return 1 + + print(f"OK — placeholders match in {len(targets)} file(s).") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/tests/test_check_translation_placeholders.py b/scripts/tests/test_check_translation_placeholders.py new file mode 100644 index 0000000..41a68be --- /dev/null +++ b/scripts/tests/test_check_translation_placeholders.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +"""Unit tests for scripts/check_translation_placeholders.py. + +Run via `python3 -m scripts.tests.test_check_translation_placeholders` from +the project root, or as a CI step. No third-party dependencies. +""" +from __future__ import annotations + +import sys +import unittest +from pathlib import Path + +# Allow `python3 scripts/tests/test_check_translation_placeholders.py` from +# the repo root by adding scripts/ to sys.path. +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from check_translation_placeholders import ( # noqa: E402 + check_ts_file, + extract_placeholders, + main, +) + + +def write_ts(path: Path, body: str) -> None: + path.write_text( + '\n' + '\n' + '\n' + '\n' + ' Sudoku\n' + f'{body}\n' + '\n' + '\n' + ) + + +def message(source: str, translation: str, *, unfinished: bool = False) -> str: + attr = ' type="unfinished"' if unfinished else "" + return ( + ' \n' + f' {source}\n' + f' {translation}\n' + ' ' + ) + + +class ExtractPlaceholdersTest(unittest.TestCase): + def test_no_placeholders(self): + counts, err = extract_placeholders("Hello world") + self.assertEqual(counts, {}) + self.assertIsNone(err) + + def test_positional_indices(self): + counts, err = extract_placeholders("Score: {0} of {1}") + self.assertEqual(counts, {"0": 1, "1": 1}) + self.assertIsNone(err) + + def test_format_spec_does_not_change_field_name(self): + counts, err = extract_placeholders("Rate: {0:.1f}%") + self.assertEqual(counts, {"0": 1}) + self.assertIsNone(err) + + def test_repeated_index(self): + counts, err = extract_placeholders("{0} and {0} again") + self.assertEqual(counts, {"0": 2}) + self.assertIsNone(err) + + def test_double_brace_escaped(self): + counts, err = extract_placeholders("Use {{0}} as a literal") + self.assertEqual(counts, {}) + self.assertIsNone(err) + + def test_unmatched_brace_is_error(self): + _counts, err = extract_placeholders("Score: {0") + self.assertIsNotNone(err) + + +class CheckTsFileTest(unittest.TestCase): + def setUp(self): + self.tmp = Path(self.id() + ".ts") + + def tearDown(self): + if self.tmp.exists(): + self.tmp.unlink() + + def test_matching_placeholders_passes(self): + write_ts(self.tmp, message("Score: {0}", "Punkte: {0}")) + self.assertEqual(check_ts_file(self.tmp), []) + + def test_missing_placeholder_fails(self): + write_ts(self.tmp, message("Score: {0} of {1}", "Punkte: {0}")) + errs = check_ts_file(self.tmp) + self.assertEqual(len(errs), 1) + self.assertIn("placeholder mismatch", errs[0]) + + def test_extra_placeholder_fails(self): + write_ts(self.tmp, message("Score: {0}", "Punkte: {0} {1}")) + errs = check_ts_file(self.tmp) + self.assertEqual(len(errs), 1) + self.assertIn("placeholder mismatch", errs[0]) + + def test_renumbered_placeholder_fails(self): + write_ts(self.tmp, message("Got {0} from {1}", "{1} hat {0}")) + # Multisets are equal here — both have one {0} and one {1} — + # so this *passes*. That's the intended trade-off: positional + # reordering for grammar reasons is legitimate. + self.assertEqual(check_ts_file(self.tmp), []) + + def test_format_spec_preserved(self): + write_ts(self.tmp, message("Rate: {0:.1f}%", "Quote: {0:.1f}%")) + self.assertEqual(check_ts_file(self.tmp), []) + + def test_format_spec_lost_passes(self): + # We only check field-name multisets; losing the format spec is bad + # but does not cause a fmt::format_error, so we don't flag it. + write_ts(self.tmp, message("Rate: {0:.1f}%", "Quote: {0}%")) + self.assertEqual(check_ts_file(self.tmp), []) + + def test_malformed_translation_fails(self): + write_ts(self.tmp, message("Score: {0}", "Punkte: {0")) + errs = check_ts_file(self.tmp) + self.assertEqual(len(errs), 1) + self.assertIn("malformed", errs[0]) + + def test_unfinished_entries_skipped(self): + write_ts(self.tmp, message("Score: {0}", "", unfinished=True)) + self.assertEqual(check_ts_file(self.tmp), []) + + def test_empty_translation_skipped(self): + write_ts(self.tmp, message("Score: {0}", "")) + self.assertEqual(check_ts_file(self.tmp), []) + + def test_named_field_mismatch_fails(self): + # Translator changed {0} → {name}. + write_ts(self.tmp, message("Score: {0}", "Punkte: {name}")) + errs = check_ts_file(self.tmp) + self.assertEqual(len(errs), 1) + self.assertIn("placeholder mismatch", errs[0]) + + def test_non_positional_source_skipped(self): + # Source is plain prose containing literal Sudoku notation in braces. + # The call site is core::loc (not locFormat), so the {A,B} in the + # translation is harmless display text — must not be flagged. + write_ts(self.tmp, message( + "Find cells sharing a candidate pair.", + "Suche nach Zellen mit dem Kandidatenpaar {A,B}.")) + self.assertEqual(check_ts_file(self.tmp), []) + + def test_malformed_source_reported(self): + # If the source itself is malformed, that's a code bug — flag it + # regardless of placeholder presence so it surfaces in CI. + write_ts(self.tmp, message("Score: {0", "Punkte: {0")) + errs = check_ts_file(self.tmp) + self.assertEqual(len(errs), 1) + self.assertIn("", errs[0]) + + +class CliTest(unittest.TestCase): + def setUp(self): + self.tmp = Path(self.id() + ".ts") + + def tearDown(self): + if self.tmp.exists(): + self.tmp.unlink() + + def test_passing_file_returns_zero(self): + write_ts(self.tmp, message("Score: {0}", "Punkte: {0}")) + self.assertEqual(main([str(self.tmp)]), 0) + + def test_failing_file_returns_one(self): + write_ts(self.tmp, message("Score: {0}", "Punkte: {0} {1}")) + self.assertEqual(main([str(self.tmp)]), 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/core/i18n_helpers.h b/src/core/i18n_helpers.h new file mode 100644 index 0000000..c9216ad --- /dev/null +++ b/src/core/i18n_helpers.h @@ -0,0 +1,51 @@ +// sudoku-cpp - Offline Sudoku Game +// Copyright (C) 2025-2026 Alexander Bendlin (darkstar79) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include +#include + +#include +#include + +namespace sudoku::core { + +[[nodiscard]] inline std::string loc(const char* context, const char* source) { + return QCoreApplication::translate(context, source).toStdString(); +} + +// locFormat takes an already-translated string (typically from core::loc) and +// runs it through fmt::format. The split exists so lupdate sees translatable +// literals through the 2-arg `core::loc(...)` calls — which match its +// `translate(ctx, src)` alias — rather than buried inside a variadic template +// it can't reliably destructure. +// +// core::locFormat(core::loc("Sudoku", "Score: {0}"), score) +template +[[nodiscard]] std::string locFormat(const std::string& translated, Args&&... args) { + return fmt::format(fmt::runtime(translated), std::forward(args)...); +} + +// Block the legacy 3-arg form `locFormat("Sudoku", "fmt {0}", v)`. Without this +// deleted overload, `const char* "Sudoku"` would implicitly convert to the +// std::string parameter above, fmt::format would run on "Sudoku" (zero format +// specifiers), and the rest of the args would be silently discarded — a hidden +// runtime regression with no compile-time warning. +template +std::string locFormat(const char*, Args&&...) = delete; + +} // namespace sudoku::core diff --git a/src/core/i_localization_manager.h b/src/core/i_localization_manager.h deleted file mode 100644 index bf76cac..0000000 --- a/src/core/i_localization_manager.h +++ /dev/null @@ -1,64 +0,0 @@ -// sudoku-cpp - Offline Sudoku Game -// Copyright (C) 2025-2026 Alexander Bendlin (darkstar79) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#pragma once - -#include -#include -#include -#include -#include - -namespace sudoku::core { - -/// Interface for localization services, enabling testable internationalization. -/// -/// Production code uses LocalizationManager (loads YAML string files). -/// Test code uses MockLocalizationManager (returns keys as-is). -/// -/// All getString() return values are std::string_view backed by stable storage. -class ILocalizationManager { -public: - virtual ~ILocalizationManager() = default; - - /// Get a localized string by its key. - /// @param key String key (e.g., "menu.game") - /// @return Localized string, or the key itself if not found - [[nodiscard]] virtual std::string_view getString(std::string_view key) const = 0; - - /// Switch the active locale by loading its YAML file. - /// @param locale_code Locale code (e.g., "en", "de") - /// @return void on success, error message on failure - [[nodiscard]] virtual std::expected setLocale(std::string_view locale_code) = 0; - - /// Get the currently active locale code. - /// @return Locale code (e.g., "en") - [[nodiscard]] virtual std::string_view getCurrentLocale() const = 0; - - /// Get all available locales discovered in the locales directory. - /// @return Vector of (locale_code, display_name) pairs - [[nodiscard]] virtual std::vector> getAvailableLocales() const = 0; - -protected: - // Protected special member functions to prevent slicing while allowing derived classes - ILocalizationManager() = default; - ILocalizationManager(const ILocalizationManager&) = default; - ILocalizationManager& operator=(const ILocalizationManager&) = default; - ILocalizationManager(ILocalizationManager&&) = default; - ILocalizationManager& operator=(ILocalizationManager&&) = default; -}; - -} // namespace sudoku::core diff --git a/src/core/localization_manager.cpp b/src/core/localization_manager.cpp deleted file mode 100644 index 5c8f53e..0000000 --- a/src/core/localization_manager.cpp +++ /dev/null @@ -1,154 +0,0 @@ -// sudoku-cpp - Offline Sudoku Game -// Copyright (C) 2025-2026 Alexander Bendlin (darkstar79) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#include "localization_manager.h" - -#include - -#include -#include -#include -#include - -namespace sudoku::core { - -LocalizationManager::LocalizationManager(std::filesystem::path locales_dir) : locales_dir_(std::move(locales_dir)) { - discoverLocales(); -} - -std::string_view LocalizationManager::getString(std::string_view key) const { - // Look up in active locale first - auto it = strings_.find(std::string(key)); - if (it != strings_.end()) { - return it->second; - } - - // Fall back to English - auto fallback_it = fallback_strings_.find(std::string(key)); - if (fallback_it != fallback_strings_.end()) { - return fallback_it->second; - } - - // Key not found in any locale — warn once and return the key itself - std::string key_str(key); - if (!warned_missing_keys_.contains(key_str)) { - spdlog::warn("Localization: missing key '{}' in locale '{}' and fallback", key_str, current_locale_); - warned_missing_keys_.insert(key_str); - } - - // Cache the key string so we can return a stable string_view - auto [cache_it, inserted] = missing_key_cache_.try_emplace(key_str, key_str); - return cache_it->second; -} - -std::expected LocalizationManager::setLocale(std::string_view locale_code) { - auto locale_file = locales_dir_ / (std::string(locale_code) + ".yaml"); - - if (!std::filesystem::exists(locale_file)) { - return std::unexpected("Locale file not found: " + locale_file.string()); - } - - // Load fallback (English) if not already loaded and we're switching to non-English - if (fallback_strings_.empty() && locale_code != "en") { - auto en_file = locales_dir_ / "en.yaml"; - if (std::filesystem::exists(en_file)) { - auto result = loadYamlFile(en_file, fallback_strings_); - if (!result) { - spdlog::warn("Failed to load English fallback: {}", result.error()); - } - } - } - - // Load the requested locale - std::unordered_map new_strings; - auto result = loadYamlFile(locale_file, new_strings); - if (!result) { - return result; - } - - strings_ = std::move(new_strings); - current_locale_ = std::string(locale_code); - - // If we just loaded English, also use it as fallback - if (locale_code == "en") { - fallback_strings_ = strings_; - } - - // Clear warning caches since locale changed - warned_missing_keys_.clear(); - missing_key_cache_.clear(); - - spdlog::info("Locale set to '{}' ({} strings loaded)", current_locale_, strings_.size()); - return {}; -} - -std::string_view LocalizationManager::getCurrentLocale() const { - return current_locale_; -} - -std::vector> LocalizationManager::getAvailableLocales() const { - return available_locales_; -} - -std::expected -LocalizationManager::loadYamlFile(const std::filesystem::path& file_path, - std::unordered_map& target) { - try { - YAML::Node root = YAML::LoadFile(file_path.string()); - - if (!root["strings"]) { - return std::unexpected("YAML file missing 'strings' section: " + file_path.string()); - } - - target.clear(); - for (const auto& pair : root["strings"]) { - target[pair.first.as()] = pair.second.as(); - } - - return {}; - } catch (const YAML::Exception& e) { - return std::unexpected("Failed to parse YAML file '" + file_path.string() + "': " + e.what()); - } -} - -void LocalizationManager::discoverLocales() { - available_locales_.clear(); - - if (!std::filesystem::exists(locales_dir_) || !std::filesystem::is_directory(locales_dir_)) { - spdlog::warn("Locales directory not found: {}", locales_dir_.string()); - return; - } - - for (const auto& entry : std::filesystem::directory_iterator(locales_dir_)) { - if (entry.path().extension() == ".yaml") { - try { - YAML::Node root = YAML::LoadFile(entry.path().string()); - auto code = root["locale"].as(); - auto name = root["name"].as(); - available_locales_.emplace_back(std::move(code), std::move(name)); - } catch (const YAML::Exception& e) { - spdlog::warn("Failed to read locale file '{}': {}", entry.path().string(), e.what()); - } - } - } - - // Sort by locale code for consistent ordering - std::ranges::sort(available_locales_, [](const auto& a, const auto& b) { return a.first < b.first; }); - - spdlog::info("Discovered {} locale(s) in {}", available_locales_.size(), locales_dir_.string()); -} - -} // namespace sudoku::core diff --git a/src/core/localization_manager.h b/src/core/localization_manager.h deleted file mode 100644 index 82ea88b..0000000 --- a/src/core/localization_manager.h +++ /dev/null @@ -1,92 +0,0 @@ -// sudoku-cpp - Offline Sudoku Game -// Copyright (C) 2025-2026 Alexander Bendlin (darkstar79) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#pragma once - -#include "i_localization_manager.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace sudoku::core { - -/// Production implementation of ILocalizationManager. -/// -/// Loads localized strings from YAML files in a locales directory. -/// Falls back to English for missing keys, then to the key itself. -/// -/// YAML file format: -/// locale: en -/// name: English -/// strings: -/// menu.game: "Game" -/// stats.games_played: "Games Played: {0}" -class LocalizationManager final : public ILocalizationManager { -public: - /// Construct with path to locales directory. - /// @param locales_dir Directory containing locale YAML files (e.g., en.yaml, de.yaml) - explicit LocalizationManager(std::filesystem::path locales_dir); - - ~LocalizationManager() override = default; - - // Non-copyable, non-movable (singleton) - LocalizationManager(const LocalizationManager&) = delete; - LocalizationManager& operator=(const LocalizationManager&) = delete; - LocalizationManager(LocalizationManager&&) = delete; - LocalizationManager& operator=(LocalizationManager&&) = delete; - - [[nodiscard]] std::string_view getString(std::string_view key) const override; - - [[nodiscard]] std::expected setLocale(std::string_view locale_code) override; - - [[nodiscard]] std::string_view getCurrentLocale() const override; - - [[nodiscard]] std::vector> getAvailableLocales() const override; - -private: - /// Load strings from a YAML file into the target map. - /// @param file_path Path to the YAML locale file - /// @param target Map to populate with key-value string pairs - /// @return void on success, error message on failure - [[nodiscard]] static std::expected - loadYamlFile(const std::filesystem::path& file_path, std::unordered_map& target); - - /// Scan locales directory for available YAML files and populate available_locales_. - void discoverLocales(); - - std::filesystem::path locales_dir_; - std::string current_locale_; - std::unordered_map strings_; ///< Active locale strings - std::unordered_map fallback_strings_; ///< English fallback strings - - /// Tracks missing keys to avoid repeated log warnings (mutable for const getString) - mutable std::unordered_set warned_missing_keys_; - - /// Cache for missing key strings (returns const char* to stored copies) - mutable std::unordered_map missing_key_cache_; - - /// Available locales discovered in the locales directory: (code, display_name) - std::vector> available_locales_; -}; - -} // namespace sudoku::core diff --git a/src/core/localized_explanations.h b/src/core/localized_explanations.h index c521ab7..1d1cafb 100644 --- a/src/core/localized_explanations.h +++ b/src/core/localized_explanations.h @@ -16,9 +16,8 @@ #pragma once -#include "i_localization_manager.h" +#include "core/i18n_helpers.h" #include "solve_step.h" -#include "string_keys.h" #include @@ -28,25 +27,25 @@ namespace sudoku::core { /// Format a position using the localized template (e.g., "R3C5" in English) -[[nodiscard]] inline std::string localizedPosition(const ILocalizationManager& loc, const Position& pos) { - return fmt::format(fmt::runtime(loc.getString(StringKeys::PositionFmt)), pos.row + 1, pos.col + 1); +[[nodiscard]] inline std::string localizedPosition(const Position& pos) { + return fmt::format(fmt::runtime(core::loc("Sudoku", "R{0}C{1}")), pos.row + 1, pos.col + 1); } /// Format a region name with 1-indexed number (e.g., "Row 3" in English) -[[nodiscard]] inline std::string localizedRegion(const ILocalizationManager& loc, RegionType type, size_t idx) { - std::string_view name; +[[nodiscard]] inline std::string localizedRegion(RegionType type, size_t idx) { + std::string name; switch (type) { case RegionType::Row: - name = loc.getString(StringKeys::RegionRow); + name = core::loc("Sudoku", "Row"); break; case RegionType::Col: - name = loc.getString(StringKeys::RegionColumn); + name = core::loc("Sudoku", "Column"); break; case RegionType::Box: - name = loc.getString(StringKeys::RegionBox); + name = core::loc("Sudoku", "Box"); break; default: - name = loc.getString(StringKeys::RegionUnknown); + name = core::loc("Sudoku", "Unknown Region"); break; } return fmt::format("{} {}", name, idx + 1); @@ -58,12 +57,11 @@ namespace sudoku::core { } /// Format a comma-separated list of positions using localized format -[[nodiscard]] inline std::string formatPositionList(const ILocalizationManager& loc, - const std::vector& positions) { +[[nodiscard]] inline std::string formatPositionList(const std::vector& positions) { std::vector strs; strs.reserve(positions.size()); for (const auto& pos : positions) { - strs.push_back(localizedPosition(loc, pos)); + strs.push_back(localizedPosition(pos)); } return fmt::format("{}", fmt::join(strs, ", ")); } @@ -73,7 +71,7 @@ namespace sudoku::core { /// dynamic data (positions, values, regions) from the step's explanation_data. /// Falls back to the raw English explanation if data is insufficient. // NOLINTNEXTLINE(readability-function-cognitive-complexity,readability-function-size) — dispatch table over SolvingTechnique enum; each case is independent; switch-over-enum complexity is not meaningful here -[[nodiscard]] inline std::string getLocalizedExplanation(const ILocalizationManager& loc, const SolveStep& step) { +[[nodiscard]] inline std::string getLocalizedExplanation(const SolveStep& step) { const auto& data = step.explanation_data; switch (step.technique) { @@ -81,47 +79,56 @@ namespace sudoku::core { if (data.positions.empty() || data.values.empty()) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainNakedSingle)), - localizedPosition(loc, data.positions[0]), data.values[0]); + return fmt::format(fmt::runtime(core::loc("Sudoku", "Naked Single at {0}: only value {1} is possible")), + localizedPosition(data.positions[0]), data.values[0]); } case SolvingTechnique::HiddenSingle: { if (data.positions.empty() || data.values.empty()) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainHiddenSingle)), - localizedPosition(loc, data.positions[0]), data.values[0]); + return fmt::format( + fmt::runtime(core::loc( + "Sudoku", "Hidden Single at {0}: value {1} can only appear in this cell within its region")), + localizedPosition(data.positions[0]), data.values[0]); } case SolvingTechnique::NakedPair: { if (data.positions.size() < 2 || data.values.size() < 2 || data.region_type == RegionType::None) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainNakedPair)), formatValueList(data.values), - formatPositionList(loc, data.positions), - localizedRegion(loc, data.region_type, data.region_index)); + return fmt::format(fmt::runtime(core::loc( + "Sudoku", "Naked Pair [{0}] at {1} in {2} eliminates candidates from other cells")), + formatValueList(data.values), formatPositionList(data.positions), + localizedRegion(data.region_type, data.region_index)); } case SolvingTechnique::NakedTriple: { if (data.positions.size() < 3 || data.values.size() < 3 || data.region_type == RegionType::None) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainNakedTriple)), - formatValueList(data.values), formatPositionList(loc, data.positions), - localizedRegion(loc, data.region_type, data.region_index)); + return fmt::format( + fmt::runtime( + core::loc("Sudoku", "Naked Triple [{0}] at {1} in {2} eliminates candidates from other cells")), + formatValueList(data.values), formatPositionList(data.positions), + localizedRegion(data.region_type, data.region_index)); } case SolvingTechnique::HiddenPair: { if (data.positions.size() < 2 || data.values.size() < 2 || data.region_type == RegionType::None) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainHiddenPair)), formatValueList(data.values), - formatPositionList(loc, data.positions), - localizedRegion(loc, data.region_type, data.region_index)); + return fmt::format( + fmt::runtime(core::loc("Sudoku", + "Hidden Pair [{0}] at {1} in {2} eliminates other candidates from these cells")), + formatValueList(data.values), formatPositionList(data.positions), + localizedRegion(data.region_type, data.region_index)); } case SolvingTechnique::HiddenTriple: { if (data.positions.size() < 3 || data.values.size() < 3 || data.region_type == RegionType::None) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainHiddenTriple)), - formatValueList(data.values), formatPositionList(loc, data.positions), - localizedRegion(loc, data.region_type, data.region_index)); + return fmt::format( + fmt::runtime(core::loc( + "Sudoku", "Hidden Triple [{0}] at {1} in {2} eliminates other candidates from these cells")), + formatValueList(data.values), formatPositionList(data.positions), + localizedRegion(data.region_type, data.region_index)); } case SolvingTechnique::PointingPair: { if (data.values.empty() || data.region_type == RegionType::None || @@ -129,20 +136,23 @@ namespace sudoku::core { return step.explanation; } // Template: "Pointing Pair: {0} in Box {1} confined to {2} {3} eliminates {0} from other cells in {2} {3}" - std::string_view sec_region_name; + std::string sec_region_name; switch (data.secondary_region_type) { case RegionType::Row: - sec_region_name = loc.getString(StringKeys::RegionRow); + sec_region_name = core::loc("Sudoku", "Row"); break; case RegionType::Col: - sec_region_name = loc.getString(StringKeys::RegionColumn); + sec_region_name = core::loc("Sudoku", "Column"); break; default: - sec_region_name = loc.getString(StringKeys::RegionUnknown); + sec_region_name = core::loc("Sudoku", "Unknown Region"); break; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainPointingPair)), data.values[0], - data.region_index + 1, sec_region_name, data.secondary_region_index + 1); + return fmt::format( + fmt::runtime(core::loc( + "Sudoku", + "Pointing Pair: {0} in Box {1} confined to {2} {3} eliminates {0} from other cells in {2} {3}")), + data.values[0], data.region_index + 1, sec_region_name, data.secondary_region_index + 1); } case SolvingTechnique::BoxLineReduction: { if (data.values.empty() || data.region_type == RegionType::None || @@ -151,36 +161,41 @@ namespace sudoku::core { } // Template: "Box/Line Reduction: {0} in {1} {2} confined to Box {3} eliminates {0} from other cells in Box // {3}" - std::string_view region_name; + std::string region_name; switch (data.region_type) { case RegionType::Row: - region_name = loc.getString(StringKeys::RegionRow); + region_name = core::loc("Sudoku", "Row"); break; case RegionType::Col: - region_name = loc.getString(StringKeys::RegionColumn); + region_name = core::loc("Sudoku", "Column"); break; default: - region_name = loc.getString(StringKeys::RegionUnknown); + region_name = core::loc("Sudoku", "Unknown Region"); break; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainBoxLineReduction)), data.values[0], - region_name, data.region_index + 1, data.secondary_region_index + 1); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "Box/Line Reduction: {0} in {1} {2} confined to Box {3} " + "eliminates {0} from other cells in Box {3}")), + data.values[0], region_name, data.region_index + 1, data.secondary_region_index + 1); } case SolvingTechnique::NakedQuad: { if (data.positions.size() < 4 || data.values.size() < 4 || data.region_type == RegionType::None) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainNakedQuad)), formatValueList(data.values), - formatPositionList(loc, data.positions), - localizedRegion(loc, data.region_type, data.region_index)); + return fmt::format(fmt::runtime(core::loc( + "Sudoku", "Naked Quad [{0}] at {1} in {2} eliminates candidates from other cells")), + formatValueList(data.values), formatPositionList(data.positions), + localizedRegion(data.region_type, data.region_index)); } case SolvingTechnique::HiddenQuad: { if (data.positions.size() < 4 || data.values.size() < 4 || data.region_type == RegionType::None) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainHiddenQuad)), formatValueList(data.values), - formatPositionList(loc, data.positions), - localizedRegion(loc, data.region_type, data.region_index)); + return fmt::format( + fmt::runtime(core::loc("Sudoku", + "Hidden Quad [{0}] at {1} in {2} eliminates other candidates from these cells")), + formatValueList(data.values), formatPositionList(data.positions), + localizedRegion(data.region_type, data.region_index)); } case SolvingTechnique::XWing: { if (data.values.empty() || data.region_type == RegionType::None) { @@ -193,17 +208,21 @@ namespace sudoku::core { if (data.positions.size() < 4) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainXWingRow)), data.values[0], - data.positions[0].row + 1, data.positions[2].row + 1, data.positions[0].col + 1, - data.positions[1].col + 1); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "X-Wing on value {0} in Rows {1} and {2}, Columns {3} and " + "{4} eliminates {0} from other cells in those columns")), + data.values[0], data.positions[0].row + 1, data.positions[2].row + 1, data.positions[0].col + 1, + data.positions[1].col + 1); } // Col-based if (data.positions.size() < 4) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainXWingCol)), data.values[0], - data.positions[0].col + 1, data.positions[1].col + 1, data.positions[0].row + 1, - data.positions[2].row + 1); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "X-Wing on value {0} in Columns {1} and {2}, Rows {3} and {4} " + "eliminates {0} from other cells in those rows")), + data.values[0], data.positions[0].col + 1, data.positions[1].col + 1, data.positions[0].row + 1, + data.positions[2].row + 1); } case SolvingTechnique::XYWing: { if (data.positions.size() < 3 || data.values.size() < 3) { @@ -211,10 +230,11 @@ namespace sudoku::core { } // Template: "XY-Wing: pivot {0} {{{1},{2}}}, wing {3} {{{1},{4}}}, wing {5} {{{2},{4}}} eliminates {4} from // cells seeing both wings" - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainXYWing)), - localizedPosition(loc, data.positions[0]), data.values[0], data.values[1], - localizedPosition(loc, data.positions[1]), data.values[2], - localizedPosition(loc, data.positions[2])); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "XY-Wing: pivot {0} {{{1},{2}}}, wing {3} {{{1},{4}}}, wing {5} " + "{{{2},{4}}} eliminates {4} from cells seeing both wings")), + localizedPosition(data.positions[0]), data.values[0], data.values[1], + localizedPosition(data.positions[1]), data.values[2], localizedPosition(data.positions[2])); } case SolvingTechnique::Swordfish: { // values = {candidate, r1, r2, r3, c1, c2, c3} (1-indexed from strategy) @@ -223,14 +243,19 @@ namespace sudoku::core { } if (data.region_type == RegionType::Row) { // Row-based: rows then cols - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainSwordfishRow)), data.values[0], - data.values[1], data.values[2], data.values[3], data.values[4], data.values[5], - data.values[6]); + return fmt::format( + fmt::runtime(core::loc("Sudoku", + "Swordfish on value {0} in Rows {1}, {2}, {3} and Columns {4}, {5}, {6} " + "eliminates {0} from other cells in those columns")), + data.values[0], data.values[1], data.values[2], data.values[3], data.values[4], data.values[5], + data.values[6]); } // Col-based: cols then rows - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainSwordfishCol)), data.values[0], - data.values[1], data.values[2], data.values[3], data.values[4], data.values[5], - data.values[6]); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "Swordfish on value {0} in Columns {1}, {2}, {3} and Rows {4}, " + "{5}, {6} eliminates {0} from other cells in those rows")), + data.values[0], data.values[1], data.values[2], data.values[3], data.values[4], data.values[5], + data.values[6]); } case SolvingTechnique::Skyscraper: { if (data.positions.size() < 4 || data.values.empty()) { @@ -239,20 +264,26 @@ namespace sudoku::core { // positions: [pair1_shared, pair1_non_shared, pair2_shared, pair2_non_shared] // Template: "Skyscraper on value {0}: conjugate pairs in {1} and {2} share endpoint {3} // — eliminates {0} from cells seeing both {4} and {5}" - std::string region1 = localizedRegion(loc, data.region_type, data.region_index); - std::string region2 = localizedRegion(loc, data.secondary_region_type, data.secondary_region_index); - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainSkyscraper)), data.values[0], region1, - region2, localizedPosition(loc, data.positions[0]), - localizedPosition(loc, data.positions[1]), localizedPosition(loc, data.positions[3])); + std::string region1 = localizedRegion(data.region_type, data.region_index); + std::string region2 = localizedRegion(data.secondary_region_type, data.secondary_region_index); + return fmt::format( + fmt::runtime(core::loc("Sudoku", + "Skyscraper on value {0}: conjugate pairs in {1} and {2} share endpoint {3} — " + "eliminates {0} from cells seeing both {4} and {5}")), + data.values[0], region1, region2, localizedPosition(data.positions[0]), + localizedPosition(data.positions[1]), localizedPosition(data.positions[3])); } case SolvingTechnique::TwoStringKite: { if (data.positions.size() < 4 || data.values.empty()) { return step.explanation; } // positions: [row_end1, row_end2, col_end1, col_end2] - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainTwoStringKite)), data.values[0], - localizedPosition(loc, data.positions[0]), localizedPosition(loc, data.positions[1]), - localizedPosition(loc, data.positions[2]), localizedPosition(loc, data.positions[3])); + return fmt::format( + fmt::runtime(core::loc("Sudoku", + "2-String Kite on value {0}: row pair {1},{2} and column pair {3},{4} connected " + "through shared box — eliminates {0} from cells seeing both endpoints")), + data.values[0], localizedPosition(data.positions[0]), localizedPosition(data.positions[1]), + localizedPosition(data.positions[2]), localizedPosition(data.positions[3])); } case SolvingTechnique::XYZWing: { if (data.positions.size() < 3 || data.values.size() < 3) { @@ -260,10 +291,11 @@ namespace sudoku::core { } // Template: "XYZ-Wing: pivot {0} {{{1},{2},{3}}}, wing {4} and wing {5} eliminate {3} from cells seeing // all three" - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainXYZWing)), - localizedPosition(loc, data.positions[0]), data.values[0], data.values[1], - data.values[2], localizedPosition(loc, data.positions[1]), - localizedPosition(loc, data.positions[2])); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "XYZ-Wing: pivot {0} {{{1},{2},{3}}}, wing {4} and wing {5} " + "eliminate {3} from cells seeing all three")), + localizedPosition(data.positions[0]), data.values[0], data.values[1], data.values[2], + localizedPosition(data.positions[1]), localizedPosition(data.positions[2])); } case SolvingTechnique::UniqueRectangle: { if (data.positions.size() < 4 || data.values.size() < 2) { @@ -272,34 +304,45 @@ namespace sudoku::core { // technique_subtype distinguishes UR sub-types: 0=Type1, 1=Type2, 2=Type3, 3=Type4 if (data.technique_subtype == 1 && data.values.size() >= 3) { // Type 2: extra candidate {3} eliminated from shared {4} - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainUniqueRectangleType2)), - formatPositionList(loc, data.positions), data.values[0], data.values[1], - data.values[2], - localizedRegion(loc, data.secondary_region_type, data.secondary_region_index)); + return fmt::format( + fmt::runtime(core::loc( + "Sudoku", "Unique Rectangle Type 2: cells {0} with values {{{1},{2}}} — extra candidate " + "{3} eliminated from cells seeing both floor cells in shared {4}")), + formatPositionList(data.positions), data.values[0], data.values[1], data.values[2], + localizedRegion(data.secondary_region_type, data.secondary_region_index)); } if (data.technique_subtype == 2 && data.values.size() >= 2) { // Type 3: floor extras form naked subset in {3} - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainUniqueRectangleType3)), - formatPositionList(loc, data.positions), data.values[0], data.values[1], - localizedRegion(loc, data.secondary_region_type, data.secondary_region_index)); + return fmt::format( + fmt::runtime(core::loc("Sudoku", + "Unique Rectangle Type 3: cells {0} with values {{{1},{2}}} — floor extras " + "form naked subset in {3}, eliminating from other cells")), + formatPositionList(data.positions), data.values[0], data.values[1], + localizedRegion(data.secondary_region_type, data.secondary_region_index)); } if (data.technique_subtype == 3 && data.values.size() >= 4) { // Type 4: strong link on {3} in {4} eliminates {5} return fmt::format( - fmt::runtime(loc.getString(StringKeys::ExplainUniqueRectangleType4)), - formatPositionList(loc, data.positions), data.values[0], data.values[1], data.values[2], - localizedRegion(loc, data.secondary_region_type, data.secondary_region_index), data.values[3]); + fmt::runtime(core::loc("Sudoku", + "Unique Rectangle Type 4: cells {0} with values {{{1},{2}}} — strong link " + "on {3} in {4} eliminates {5} from floor cells")), + formatPositionList(data.positions), data.values[0], data.values[1], data.values[2], + localizedRegion(data.secondary_region_type, data.secondary_region_index), data.values[3]); } if (data.technique_subtype == 5 && data.values.size() >= 3) { // Type 6: digit {3} conjugate in both parallel lines → eliminates extras - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainUniqueRectangleType6)), - formatPositionList(loc, data.positions), data.values[0], data.values[1], - data.values[2]); + return fmt::format( + fmt::runtime(core::loc( + "Sudoku", + "Unique Rectangle Type 6: cells {0} with values {{{1},{2}}} — {3} is conjugate in both " + "parallel lines of the rectangle, locking the pattern — eliminates extras from floor cells")), + formatPositionList(data.positions), data.values[0], data.values[1], data.values[2]); } // Type 1 (default): eliminates {1},{2} from {3} - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainUniqueRectangle)), - formatPositionList(loc, data.positions), data.values[0], data.values[1], - localizedPosition(loc, data.positions[3])); + return fmt::format(fmt::runtime(core::loc("Sudoku", "Unique Rectangle: cells {0} with values {{{1},{2}}} — " + "eliminates {1},{2} from {3} to avoid deadly pattern")), + formatPositionList(data.positions), data.values[0], data.values[1], + localizedPosition(data.positions[3])); } case SolvingTechnique::WWing: { if (data.positions.size() < 4 || data.values.size() < 2) { @@ -307,9 +350,11 @@ namespace sudoku::core { } // Template: "W-Wing: cells {0} and {1} {{{2},{3}}} connected by strong link on {2} — eliminates {3} from // cells seeing both" - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainWWing)), - localizedPosition(loc, data.positions[0]), localizedPosition(loc, data.positions[1]), - data.values[0], data.values[1]); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "W-Wing: cells {0} and {1} {{{2},{3}}} connected by strong link " + "on {2} — eliminates {3} from cells seeing both")), + localizedPosition(data.positions[0]), localizedPosition(data.positions[1]), data.values[0], + data.values[1]); } case SolvingTechnique::SimpleColoring: { if (data.values.empty()) { @@ -317,14 +362,18 @@ namespace sudoku::core { } // technique_subtype: 0 = contradiction, 1 = exclusion if (data.technique_subtype == 0) { - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainSimpleColoringContradiction)), - data.values[0]); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "Simple Coloring on {0}: same-color cells see each other — " + "eliminates {0} from all cells of that color")), + data.values[0]); } if (data.positions.empty()) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainSimpleColoringExclusion)), data.values[0], - localizedPosition(loc, data.positions[0])); + return fmt::format( + fmt::runtime( + core::loc("Sudoku", "Simple Coloring on {0}: cell {1} sees both colors — eliminates {0} from {1}")), + data.values[0], localizedPosition(data.positions[0])); } case SolvingTechnique::FinnedXWing: { if (data.values.empty() || data.region_type == RegionType::None) { @@ -334,29 +383,38 @@ namespace sudoku::core { if (data.positions.empty() || data.values.size() < 5) { return step.explanation; } - auto fin_pos = localizedPosition(loc, data.positions.back()); + auto fin_pos = localizedPosition(data.positions.back()); if (data.region_type == RegionType::Row) { - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainFinnedXWingRow)), data.values[0], - data.values[1], data.values[2], data.values[3], data.values[4], fin_pos); + return fmt::format( + fmt::runtime(core::loc("Sudoku", + "Finned X-Wing on value {0} in Rows {1} and {2}, Columns {3} and {4} with " + "fin at {5} — eliminates {0} from cells in fin's box")), + data.values[0], data.values[1], data.values[2], data.values[3], data.values[4], fin_pos); } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainFinnedXWingCol)), data.values[0], - data.values[1], data.values[2], data.values[3], data.values[4], fin_pos); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "Finned X-Wing on value {0} in Columns {1} and {2}, Rows {3} and " + "{4} with fin at {5} — eliminates {0} from cells in fin's box")), + data.values[0], data.values[1], data.values[2], data.values[3], data.values[4], fin_pos); } case SolvingTechnique::RemotePairs: { if (data.positions.size() < 2 || data.values.size() < 3) { return step.explanation; } // values = {A, B, chain_length} - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainRemotePairs)), data.values[0], - data.values[1], localizedPosition(loc, data.positions[0]), - localizedPosition(loc, data.positions[1]), data.values[2]); + return fmt::format(fmt::runtime(core::loc( + "Sudoku", "Remote Pairs: chain of {{{0},{1}}} cells from {2} to {3} (length {4}) — " + "eliminates {0},{1} from cells seeing both endpoints")), + data.values[0], data.values[1], localizedPosition(data.positions[0]), + localizedPosition(data.positions[1]), data.values[2]); } case SolvingTechnique::BUG: { if (data.positions.empty() || data.values.empty()) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainBUG)), - localizedPosition(loc, data.positions[0]), data.values[0]); + return fmt::format( + fmt::runtime(core::loc( + "Sudoku", "BUG: all cells bivalue except {0} — value {1} must be placed to avoid deadly pattern")), + localizedPosition(data.positions[0]), data.values[0]); } case SolvingTechnique::Jellyfish: { // values = {candidate, r1, r2, r3, r4, c1, c2, c3, c4} (1-indexed) @@ -364,67 +422,87 @@ namespace sudoku::core { return step.explanation; } if (data.region_type == RegionType::Row) { - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainJellyfishRow)), data.values[0], - data.values[1], data.values[2], data.values[3], data.values[4], data.values[5], - data.values[6], data.values[7], data.values[8]); - } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainJellyfishCol)), data.values[0], - data.values[1], data.values[2], data.values[3], data.values[4], data.values[5], - data.values[6], data.values[7], data.values[8]); + return fmt::format( + fmt::runtime(core::loc("Sudoku", + "Jellyfish on value {0} in Rows {1}, {2}, {3}, {4} and Columns {5}, {6}, " + "{7}, {8} eliminates {0} from other cells in those columns")), + data.values[0], data.values[1], data.values[2], data.values[3], data.values[4], data.values[5], + data.values[6], data.values[7], data.values[8]); + } + return fmt::format( + fmt::runtime(core::loc("Sudoku", + "Jellyfish on value {0} in Columns {1}, {2}, {3}, {4} and Rows {5}, {6}, {7}, " + "{8} eliminates {0} from other cells in those rows")), + data.values[0], data.values[1], data.values[2], data.values[3], data.values[4], data.values[5], + data.values[6], data.values[7], data.values[8]); } case SolvingTechnique::FinnedSwordfish: { // values = {candidate, row/col1, row/col2, row/col3}, positions includes fin at back if (data.positions.empty() || data.values.size() < 4 || data.region_type == RegionType::None) { return step.explanation; } - auto fin_pos = localizedPosition(loc, data.positions.back()); + auto fin_pos = localizedPosition(data.positions.back()); if (data.region_type == RegionType::Row) { - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainFinnedSwordfishRow)), data.values[0], - data.values[1], data.values[2], data.values[3], fin_pos); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "Finned Swordfish on value {0} in Rows {1}, {2}, {3} with " + "fin at {4} — eliminates {0} from cells in fin's box")), + data.values[0], data.values[1], data.values[2], data.values[3], fin_pos); } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainFinnedSwordfishCol)), data.values[0], - data.values[1], data.values[2], data.values[3], fin_pos); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "Finned Swordfish on value {0} in Columns {1}, {2}, {3} with fin " + "at {4} — eliminates {0} from cells in fin's box")), + data.values[0], data.values[1], data.values[2], data.values[3], fin_pos); } case SolvingTechnique::EmptyRectangle: { if (data.positions.empty() || data.values.size() < 2 || data.region_type == RegionType::None) { return step.explanation; } // values = {candidate, box+1}, positions.back() = elimination target - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainEmptyRectangle)), data.values[0], - data.values[1], localizedRegion(loc, data.region_type, data.region_index), - localizedPosition(loc, data.positions.back())); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "Empty Rectangle on value {0}: ER in Box {1} with conjugate pair " + "in {2} — eliminates {0} from {3}")), + data.values[0], data.values[1], localizedRegion(data.region_type, data.region_index), + localizedPosition(data.positions.back())); } case SolvingTechnique::WXYZWing: { if (data.positions.size() < 4 || data.values.empty()) { return step.explanation; } // positions: [pivot, w1, w2, w3], values: [Z] - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainWXYZWing)), - localizedPosition(loc, data.positions[0]), localizedPosition(loc, data.positions[1]), - localizedPosition(loc, data.positions[2]), localizedPosition(loc, data.positions[3]), - data.values[0]); + return fmt::format( + fmt::runtime(core::loc( + "Sudoku", + "WXYZ-Wing: pivot {0} with wings {1}, {2}, {3} — eliminates {4} from cells seeing all four")), + localizedPosition(data.positions[0]), localizedPosition(data.positions[1]), + localizedPosition(data.positions[2]), localizedPosition(data.positions[3]), data.values[0]); } case SolvingTechnique::FinnedJellyfish: { // values = {candidate, row/col1..4}, positions includes fin at back if (data.positions.empty() || data.values.size() < 5 || data.region_type == RegionType::None) { return step.explanation; } - auto fin_pos = localizedPosition(loc, data.positions.back()); + auto fin_pos = localizedPosition(data.positions.back()); if (data.region_type == RegionType::Row) { - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainFinnedJellyfishRow)), data.values[0], - data.values[1], data.values[2], data.values[3], data.values[4], fin_pos); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "Finned Jellyfish on value {0} in Rows {1}, {2}, {3}, {4} " + "with fin at {5} — eliminates {0} from cells in fin's box")), + data.values[0], data.values[1], data.values[2], data.values[3], data.values[4], fin_pos); } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainFinnedJellyfishCol)), data.values[0], - data.values[1], data.values[2], data.values[3], data.values[4], fin_pos); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "Finned Jellyfish on value {0} in Columns {1}, {2}, {3}, {4} " + "with fin at {5} — eliminates {0} from cells in fin's box")), + data.values[0], data.values[1], data.values[2], data.values[3], data.values[4], fin_pos); } case SolvingTechnique::XYChain: { if (data.positions.size() < 2 || data.values.size() < 2) { return step.explanation; } // values = {X, chain_length} - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainXYChain)), data.values[1], - localizedPosition(loc, data.positions[0]), localizedPosition(loc, data.positions[1]), - data.values[0]); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "XY-Chain: chain of {0} bivalue cells from {1} to {2} — " + "eliminates {3} from cells seeing both endpoints")), + data.values[1], localizedPosition(data.positions[0]), localizedPosition(data.positions[1]), + data.values[0]); } case SolvingTechnique::MultiColoring: { if (data.values.empty()) { @@ -432,13 +510,19 @@ namespace sudoku::core { } // technique_subtype: 0 = wrap, 1 = trap if (data.technique_subtype == 0) { - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainMultiColoringWrap)), data.values[0]); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "Multi-Coloring on {0}: color sees both colors of another " + "cluster — eliminates {0} from all cells of that color")), + data.values[0]); } if (data.positions.empty()) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainMultiColoringTrap)), data.values[0], - localizedPosition(loc, data.positions[0])); + return fmt::format( + fmt::runtime(core::loc( + "Sudoku", + "Multi-Coloring on {0}: cell {1} sees complementary colors from two clusters — eliminates {0}")), + data.values[0], localizedPosition(data.positions[0])); } case SolvingTechnique::ALSxZ: { if (data.values.size() < 4 || data.positions.empty()) { @@ -452,33 +536,39 @@ namespace sudoku::core { data.positions.begin() + static_cast(als_a_size)); std::vector als_b_pos(data.positions.begin() + static_cast(als_b_start), data.positions.end()); - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainALSxZ)), - formatPositionList(loc, als_a_pos), formatPositionList(loc, als_b_pos), data.values[0], - data.values[1]); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "ALS-XZ: ALS {0} and ALS {1} linked by restricted common {2} — " + "eliminates {3} from cells seeing both ALSs")), + formatPositionList(als_a_pos), formatPositionList(als_b_pos), data.values[0], data.values[1]); } case SolvingTechnique::SueDeCoq: { if (data.values.empty() || data.region_type == RegionType::None) { return step.explanation; } // region_type: Row/Col; region_index = line index; secondary_region_index = box index - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainSueDeCoq)), - localizedRegion(loc, data.region_type, data.region_index), - data.secondary_region_index + 1); + return fmt::format( + fmt::runtime(core::loc( + "Sudoku", + "Sue de Coq: intersection of {0} and Box {1} — eliminates candidates from rest of line and box")), + localizedRegion(data.region_type, data.region_index), data.secondary_region_index + 1); } case SolvingTechnique::ForcingChain: { if (data.positions.empty() || data.values.empty()) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainForcingChain)), - localizedPosition(loc, data.positions[0]), data.values[0]); + return fmt::format( + fmt::runtime(core::loc( + "Sudoku", "Forcing Chain: assuming each candidate in {0} leads to the same conclusion — {1}")), + localizedPosition(data.positions[0]), data.values[0]); } case SolvingTechnique::NiceLoop: { if (data.positions.size() < 2 || data.values.empty()) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainNiceLoop)), - localizedPosition(loc, data.positions[0]), localizedPosition(loc, data.positions[1]), - data.values[0]); + return fmt::format( + fmt::runtime( + core::loc("Sudoku", "Nice Loop: alternating inference chain from {0} to {1} — eliminates {2}")), + localizedPosition(data.positions[0]), localizedPosition(data.positions[1]), data.values[0]); } case SolvingTechnique::XCycles: { if (data.values.empty()) { @@ -486,36 +576,49 @@ namespace sudoku::core { } // technique_subtype: 0=Type1, 1=Type2, 2=Type3 if (data.technique_subtype == 1 && !data.positions.empty()) { - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainXCyclesType2)), data.values[0], - localizedPosition(loc, data.positions[0])); + return fmt::format( + fmt::runtime( + core::loc("Sudoku", "X-Cycles on value {0}: strong-strong discontinuity at {1} — places {0}")), + data.values[0], localizedPosition(data.positions[0])); } if (data.technique_subtype == 2 && !data.positions.empty()) { - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainXCyclesType3)), data.values[0], - localizedPosition(loc, data.positions[0])); + return fmt::format( + fmt::runtime(core::loc( + "Sudoku", "X-Cycles on value {0}: weak-weak discontinuity at {1} — eliminates {0} from {1}")), + data.values[0], localizedPosition(data.positions[0])); } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainXCyclesType1)), data.values[0]); + return fmt::format( + fmt::runtime(core::loc( + "Sudoku", + "X-Cycles on value {0}: continuous loop — eliminates {0} from cells seeing weak link endpoints")), + data.values[0]); } case SolvingTechnique::ThreeDMedusa: { if (data.values.empty()) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainThreeDMedusa)), data.values[0]); + return fmt::format(fmt::runtime(core::loc("Sudoku", "3D Medusa: multi-digit coloring — {0}")), + data.values[0]); } case SolvingTechnique::HiddenUniqueRectangle: { if (data.positions.size() < 4 || data.values.size() < 4) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainHiddenUniqueRectangle)), - formatPositionList(loc, data.positions), data.values[0], data.values[1], data.values[2], - localizedPosition(loc, data.positions[static_cast(data.values[3])])); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "Hidden Unique Rectangle: cells {0} with values {{{1},{2}}} — " + "eliminates {3} from {4} to avoid deadly pattern")), + formatPositionList(data.positions), data.values[0], data.values[1], data.values[2], + localizedPosition(data.positions[static_cast(data.values[3])])); } case SolvingTechnique::AvoidableRectangle: { if (data.positions.size() < 4 || data.values.size() < 4) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainAvoidableRectangle)), - formatPositionList(loc, data.positions), data.values[0], data.values[1], data.values[2], - localizedPosition(loc, data.positions[static_cast(data.values[3])])); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "Avoidable Rectangle: cells {0} with solved values {{{1},{2}}} — " + "eliminates {3} from {4} to avoid deadly pattern")), + formatPositionList(data.positions), data.values[0], data.values[1], data.values[2], + localizedPosition(data.positions[static_cast(data.values[3])])); } case SolvingTechnique::ALSXYWing: { if (data.values.size() < 3 || data.positions.empty()) { @@ -534,52 +637,64 @@ namespace sudoku::core { std::vector als_b(data.positions.begin() + static_cast(b_start), data.positions.begin() + static_cast(c_start)); std::vector als_c(data.positions.begin() + static_cast(c_start), data.positions.end()); - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainALSXYWing)), - formatPositionList(loc, als_a), formatPositionList(loc, als_b), - formatPositionList(loc, als_c), data.values[0], data.values[1], data.values[2]); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "ALS-XY-Wing: ALS {0}, ALS {1}, ALS {2} linked by X={3} and " + "Y={4} — eliminates {5} from cells seeing Z-cells in A and C")), + formatPositionList(als_a), formatPositionList(als_b), formatPositionList(als_c), data.values[0], + data.values[1], data.values[2]); } case SolvingTechnique::DeathBlossom: { if (data.positions.empty() || data.values.empty()) { return step.explanation; } // positions[0] = stem, values[0] = Z description - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainDeathBlossom)), - localizedPosition(loc, data.positions[0]), data.values[0], data.values[1]); + return fmt::format( + fmt::runtime(core::loc( + "Sudoku", + "Death Blossom: stem {0} with petals {1} — eliminates {2} from cells seeing all petal Z-cells")), + localizedPosition(data.positions[0]), data.values[0], data.values[1]); } case SolvingTechnique::VWXYZWing: { if (data.positions.size() < 5 || data.values.empty()) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainVWXYZWing)), - localizedPosition(loc, data.positions[0]), localizedPosition(loc, data.positions[1]), - localizedPosition(loc, data.positions[2]), localizedPosition(loc, data.positions[3]), - localizedPosition(loc, data.positions[4]), data.values[0]); + return fmt::format(fmt::runtime(core::loc("Sudoku", "VWXYZ-Wing: pivot {0} with wings {1}, {2}, {3}, {4} — " + "eliminates {5} from cells seeing all Z-cells")), + localizedPosition(data.positions[0]), localizedPosition(data.positions[1]), + localizedPosition(data.positions[2]), localizedPosition(data.positions[3]), + localizedPosition(data.positions[4]), data.values[0]); } case SolvingTechnique::FrankenFish: { if (data.values.size() < 2) { return step.explanation; } // values = {fish_name_placeholder, digit, ...} - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainFrankenFish)), data.values[0], - data.values[1], data.positions.empty() ? "" : formatPositionList(loc, data.positions), - data.values.size() >= 3 ? std::to_string(data.values[2]) : ""); + return fmt::format( + fmt::runtime(core::loc( + "Sudoku", + "Franken {0} on value {1}: base {2}, cover {3} — eliminates {1} from cover cells outside base")), + data.values[0], data.values[1], data.positions.empty() ? "" : formatPositionList(data.positions), + data.values.size() >= 3 ? std::to_string(data.values[2]) : ""); } case SolvingTechnique::MutantFish: { if (data.values.empty()) { return step.explanation; } // values = {digit}, positions = base pattern cells - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainMutantFish)), data.values[0], - data.positions.empty() ? "" : formatPositionList(loc, data.positions), - "", // cover description not stored in explanation_data - static_cast(step.eliminations.size())); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "Mutant Fish on value {0}: base {1}, cover {2} — eliminates {0} " + "from {3} cover cell(s) outside base")), + data.values[0], data.positions.empty() ? "" : formatPositionList(data.positions), + "", // cover description not stored in explanation_data + static_cast(step.eliminations.size())); } case SolvingTechnique::GroupedXCycles: { if (data.values.empty()) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainGroupedXCycles)), data.values[0], - data.values.size() >= 2 ? std::to_string(data.values[1]) : ""); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "Grouped X-Cycles on value {0}: chain with grouped nodes — {1}")), + data.values[0], data.values.size() >= 2 ? std::to_string(data.values[1]) : ""); } case SolvingTechnique::SashimiXWing: { if (data.values.empty() || data.positions.empty() || data.region_type == RegionType::None) { @@ -589,45 +704,61 @@ namespace sudoku::core { if (data.values.size() < 3) { return step.explanation; } - auto fin_pos = localizedPosition(loc, data.positions.back()); + auto fin_pos = localizedPosition(data.positions.back()); if (data.region_type == RegionType::Row) { - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainSashimiXWingRow)), data.values[0], - data.values[1], data.values[2], fin_pos); + return fmt::format( + fmt::runtime(core::loc("Sudoku", + "Sashimi X-Wing on value {0} in Rows {1} and {2}, Columns {3} and {4} with " + "fin at {5} — eliminates {0} from cells in fin's box")), + data.values[0], data.values[1], data.values[2], fin_pos); } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainSashimiXWingCol)), data.values[0], - data.values[1], data.values[2], fin_pos); + return fmt::format( + fmt::runtime(core::loc("Sudoku", + "Sashimi X-Wing on value {0} in Columns {1} and {2}, Rows {3} and {4} with fin " + "at {5} — eliminates {0} from cells in fin's box")), + data.values[0], data.values[1], data.values[2], fin_pos); } case SolvingTechnique::SashimiSwordfish: { if (data.positions.empty() || data.values.size() < 4 || data.region_type == RegionType::None) { return step.explanation; } - auto fin_pos = localizedPosition(loc, data.positions.back()); + auto fin_pos = localizedPosition(data.positions.back()); if (data.region_type == RegionType::Row) { - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainSashimiSwordfishRow)), data.values[0], - data.values[1], data.values[2], data.values[3], fin_pos); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "Sashimi Swordfish on value {0} in Rows {1}, {2}, {3} with " + "fin at {4} — eliminates {0} from cells in fin's box")), + data.values[0], data.values[1], data.values[2], data.values[3], fin_pos); } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainSashimiSwordfishCol)), data.values[0], - data.values[1], data.values[2], data.values[3], fin_pos); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "Sashimi Swordfish on value {0} in Columns {1}, {2}, {3} with " + "fin at {4} — eliminates {0} from cells in fin's box")), + data.values[0], data.values[1], data.values[2], data.values[3], fin_pos); } case SolvingTechnique::SashimiJellyfish: { if (data.positions.empty() || data.values.size() < 5 || data.region_type == RegionType::None) { return step.explanation; } - auto fin_pos = localizedPosition(loc, data.positions.back()); + auto fin_pos = localizedPosition(data.positions.back()); if (data.region_type == RegionType::Row) { - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainSashimiJellyfishRow)), data.values[0], - data.values[1], data.values[2], data.values[3], data.values[4], fin_pos); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "Sashimi Jellyfish on value {0} in Rows {1}, {2}, {3}, {4} " + "with fin at {5} — eliminates {0} from cells in fin's box")), + data.values[0], data.values[1], data.values[2], data.values[3], data.values[4], fin_pos); } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainSashimiJellyfishCol)), data.values[0], - data.values[1], data.values[2], data.values[3], data.values[4], fin_pos); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "Sashimi Jellyfish on value {0} in Columns {1}, {2}, {3}, {4} " + "with fin at {5} — eliminates {0} from cells in fin's box")), + data.values[0], data.values[1], data.values[2], data.values[3], data.values[4], fin_pos); } case SolvingTechnique::KrakenFish: { if (data.values.empty() || data.positions.empty()) { return step.explanation; } // Template: "Kraken Fish on value {0}: finned fish with chain-verified eliminations from {1}" - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainKrakenFish)), data.values[0], - formatPositionList(loc, data.positions)); + return fmt::format( + fmt::runtime(core::loc( + "Sudoku", "Kraken Fish on value {0}: finned fish with chain-verified eliminations from {1}")), + data.values[0], formatPositionList(data.positions)); } case SolvingTechnique::ALSChain: { if (data.values.empty() || data.positions.empty()) { @@ -636,43 +767,54 @@ namespace sudoku::core { // values = [rc1, rc2, ..., z, chain_length]; positions = all ALS cells auto chain_len = data.values.back(); auto val_z = data.values.size() >= 2 ? data.values[static_cast(data.values.size() - 2)] : 0; - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainALSChain)), chain_len, val_z, - formatPositionList(loc, data.positions)); + return fmt::format( + fmt::runtime(core::loc( + "Sudoku", + "ALS Chain ({0} ALSs): eliminates {1} from cells seeing Z-cells in first and last ALS at {2}")), + chain_len, val_z, formatPositionList(data.positions)); } case SolvingTechnique::JuniorExocet: { if (data.positions.size() < 4 || data.values.empty()) { return step.explanation; } // positions = {base1, base2, target1, target2}, values = base candidates - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainJuniorExocet)), - localizedPosition(loc, data.positions[0]), localizedPosition(loc, data.positions[1]), - formatValueList(data.values), localizedPosition(loc, data.positions[2]), - localizedPosition(loc, data.positions[3])); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "Junior Exocet: base cells {0} and {1} with candidates {{{2}}} — " + "targets {3} and {4} can only contain base candidates")), + localizedPosition(data.positions[0]), localizedPosition(data.positions[1]), + formatValueList(data.values), localizedPosition(data.positions[2]), + localizedPosition(data.positions[3])); } case SolvingTechnique::UniqueLoop: { if (data.positions.size() < 4 || data.values.size() < 2) { return step.explanation; } // Template: "Unique Loop: cells {0} with values {{{1},{2}}} — eliminates {1},{2} from {3}" - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainUniqueLoop)), - formatPositionList(loc, data.positions), data.values[0], data.values[1], - localizedPosition(loc, data.positions.back())); + return fmt::format( + fmt::runtime(core::loc("Sudoku", "Unique Loop: cells {0} with values {{{1},{2}}} — eliminates " + "{1},{2} from {3} to avoid deadly pattern")), + formatPositionList(data.positions), data.values[0], data.values[1], + localizedPosition(data.positions.back())); } case SolvingTechnique::ContinuousNiceLoop: { if (data.values.empty()) { return step.explanation; } // Template: "Continuous Nice Loop: loop of {0} nodes — eliminates {1} candidate(s)" - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainContinuousNiceLoop)), data.values[0], - data.values.size() >= 2 ? data.values[1] : 0); + return fmt::format( + fmt::runtime(core::loc( + "Sudoku", + "Continuous Nice Loop: loop of {0} nodes — eliminates {1} candidate(s) via weak link logic")), + data.values[0], data.values.size() >= 2 ? data.values[1] : 0); } case SolvingTechnique::GroupedNiceLoop: { if (data.positions.size() < 2 || data.values.empty()) { return step.explanation; } - return fmt::format(fmt::runtime(loc.getString(StringKeys::ExplainGroupedNiceLoop)), - localizedPosition(loc, data.positions[0]), localizedPosition(loc, data.positions[1]), - data.values[0]); + return fmt::format( + fmt::runtime(core::loc( + "Sudoku", "Grouped Nice Loop: alternating inference chain from {0} to {1} — eliminates {2}")), + localizedPosition(data.positions[0]), localizedPosition(data.positions[1]), data.values[0]); } case SolvingTechnique::UnitForcingChain: case SolvingTechnique::RegionForcingChain: diff --git a/src/core/solving_technique.h b/src/core/solving_technique.h index 825b786..afc87e8 100644 --- a/src/core/solving_technique.h +++ b/src/core/solving_technique.h @@ -16,10 +16,10 @@ #pragma once -#include "i_localization_manager.h" -#include "string_keys.h" +#include "core/i18n_helpers.h" #include +#include #include namespace sudoku::core { @@ -310,123 +310,121 @@ enum class SolvingTechnique : uint8_t { /// @param technique The solving technique /// @return Localized technique name as string_view (e.g., "Naked Single" in English) // NOLINTNEXTLINE(readability-function-size) — exhaustive switch over 54 SolvingTechnique enum values; inherently large -[[nodiscard]] inline std::string_view getLocalizedTechniqueName(const ILocalizationManager& loc, - SolvingTechnique technique) { +[[nodiscard]] inline std::string getLocalizedTechniqueName(SolvingTechnique technique) { using enum SolvingTechnique; - using namespace StringKeys; switch (technique) { case NakedSingle: - return loc.getString(TechNakedSingle); + return core::loc("Sudoku", "Naked Single"); case HiddenSingle: - return loc.getString(TechHiddenSingle); + return core::loc("Sudoku", "Hidden Single"); case NakedPair: - return loc.getString(TechNakedPair); + return core::loc("Sudoku", "Naked Pair"); case NakedTriple: - return loc.getString(TechNakedTriple); + return core::loc("Sudoku", "Naked Triple"); case HiddenPair: - return loc.getString(TechHiddenPair); + return core::loc("Sudoku", "Hidden Pair"); case HiddenTriple: - return loc.getString(TechHiddenTriple); + return core::loc("Sudoku", "Hidden Triple"); case PointingPair: - return loc.getString(TechPointingPair); + return core::loc("Sudoku", "Pointing Pair"); case BoxLineReduction: - return loc.getString(TechBoxLineReduction); + return core::loc("Sudoku", "Box/Line Reduction"); case NakedQuad: - return loc.getString(TechNakedQuad); + return core::loc("Sudoku", "Naked Quad"); case HiddenQuad: - return loc.getString(TechHiddenQuad); + return core::loc("Sudoku", "Hidden Quad"); case XWing: - return loc.getString(TechXWing); + return core::loc("Sudoku", "X-Wing"); case XYWing: - return loc.getString(TechXYWing); + return core::loc("Sudoku", "XY-Wing"); case Swordfish: - return loc.getString(TechSwordfish); + return core::loc("Sudoku", "Swordfish"); case Skyscraper: - return loc.getString(TechSkyscraper); + return core::loc("Sudoku", "Skyscraper"); case TwoStringKite: - return loc.getString(TechTwoStringKite); + return core::loc("Sudoku", "2-String Kite"); case XYZWing: - return loc.getString(TechXYZWing); + return core::loc("Sudoku", "XYZ-Wing"); case UniqueRectangle: - return loc.getString(TechUniqueRectangle); + return core::loc("Sudoku", "Unique Rectangle"); case WWing: - return loc.getString(TechWWing); + return core::loc("Sudoku", "W-Wing"); case SimpleColoring: - return loc.getString(TechSimpleColoring); + return core::loc("Sudoku", "Simple Coloring"); case FinnedXWing: - return loc.getString(TechFinnedXWing); + return core::loc("Sudoku", "Finned X-Wing"); case RemotePairs: - return loc.getString(TechRemotePairs); + return core::loc("Sudoku", "Remote Pairs"); case BUG: - return loc.getString(TechBUG); + return core::loc("Sudoku", "BUG"); case Jellyfish: - return loc.getString(TechJellyfish); + return core::loc("Sudoku", "Jellyfish"); case FinnedSwordfish: - return loc.getString(TechFinnedSwordfish); + return core::loc("Sudoku", "Finned Swordfish"); case EmptyRectangle: - return loc.getString(TechEmptyRectangle); + return core::loc("Sudoku", "Empty Rectangle"); case WXYZWing: - return loc.getString(TechWXYZWing); + return core::loc("Sudoku", "WXYZ-Wing"); case FinnedJellyfish: - return loc.getString(TechFinnedJellyfish); + return core::loc("Sudoku", "Finned Jellyfish"); case XYChain: - return loc.getString(TechXYChain); + return core::loc("Sudoku", "XY-Chain"); case MultiColoring: - return loc.getString(TechMultiColoring); + return core::loc("Sudoku", "Multi-Coloring"); case ALSxZ: - return loc.getString(TechALSxZ); + return core::loc("Sudoku", "ALS-XZ"); case SueDeCoq: - return loc.getString(TechSueDeCoq); + return core::loc("Sudoku", "Sue de Coq"); case ForcingChain: - return loc.getString(TechForcingChain); + return core::loc("Sudoku", "Forcing Chain"); case NiceLoop: - return loc.getString(TechNiceLoop); + return core::loc("Sudoku", "Nice Loop"); case XCycles: - return loc.getString(TechXCycles); + return core::loc("Sudoku", "X-Cycles"); case ThreeDMedusa: - return loc.getString(TechThreeDMedusa); + return core::loc("Sudoku", "3D Medusa"); case HiddenUniqueRectangle: - return loc.getString(TechHiddenUniqueRectangle); + return core::loc("Sudoku", "Hidden Unique Rectangle"); case AvoidableRectangle: - return loc.getString(TechAvoidableRectangle); + return core::loc("Sudoku", "Avoidable Rectangle"); case ALSXYWing: - return loc.getString(TechALSXYWing); + return core::loc("Sudoku", "ALS-XY-Wing"); case DeathBlossom: - return loc.getString(TechDeathBlossom); + return core::loc("Sudoku", "Death Blossom"); case VWXYZWing: - return loc.getString(TechVWXYZWing); + return core::loc("Sudoku", "VWXYZ-Wing"); case FrankenFish: - return loc.getString(TechFrankenFish); + return core::loc("Sudoku", "Franken Fish"); case GroupedXCycles: - return loc.getString(TechGroupedXCycles); + return core::loc("Sudoku", "Grouped X-Cycles"); case SashimiXWing: - return loc.getString(TechSashimiXWing); + return core::loc("Sudoku", "Sashimi X-Wing"); case SashimiSwordfish: - return loc.getString(TechSashimiSwordfish); + return core::loc("Sudoku", "Sashimi Swordfish"); case SashimiJellyfish: - return loc.getString(TechSashimiJellyfish); + return core::loc("Sudoku", "Sashimi Jellyfish"); case UnitForcingChain: - return loc.getString(TechUnitForcingChain); + return core::loc("Sudoku", "Unit Forcing Chain"); case RegionForcingChain: - return loc.getString(TechRegionForcingChain); + return core::loc("Sudoku", "Region Forcing Chain"); case MutantFish: - return loc.getString(TechMutantFish); + return core::loc("Sudoku", "Mutant Fish"); case KrakenFish: - return loc.getString(TechKrakenFish); + return core::loc("Sudoku", "Kraken Fish"); case ALSChain: - return loc.getString(TechALSChain); + return core::loc("Sudoku", "ALS Chain"); case JuniorExocet: - return loc.getString(TechJuniorExocet); + return core::loc("Sudoku", "Junior Exocet"); case UniqueLoop: - return loc.getString(TechUniqueLoop); + return core::loc("Sudoku", "Unique Loop"); case ContinuousNiceLoop: - return loc.getString(TechContinuousNiceLoop); + return core::loc("Sudoku", "Continuous Nice Loop"); case GroupedNiceLoop: - return loc.getString(TechGroupedNiceLoop); + return core::loc("Sudoku", "Grouped Nice Loop"); case Backtracking: - return loc.getString(TechBacktrackingName); + return core::loc("Sudoku", "Backtracking"); } - return loc.getString(TechUnknown); + return core::loc("Sudoku", "Unknown Technique"); } } // namespace sudoku::core diff --git a/src/core/string_keys.h b/src/core/string_keys.h deleted file mode 100644 index 8b1de09..0000000 --- a/src/core/string_keys.h +++ /dev/null @@ -1,703 +0,0 @@ -// sudoku-cpp - Offline Sudoku Game -// Copyright (C) 2025-2026 Alexander Bendlin (darkstar79) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#pragma once - -#include - -namespace sudoku::core::StringKeys { - -// ========================================================================= -// Application -// ========================================================================= -inline constexpr std::string_view AppTitle = "app.title"; - -// ========================================================================= -// Menu items -// ========================================================================= -inline constexpr std::string_view MenuGame = "menu.game"; -inline constexpr std::string_view MenuNewGame = "menu.new_game"; -inline constexpr std::string_view MenuResetPuzzle = "menu.reset_puzzle"; -inline constexpr std::string_view MenuSave = "menu.save"; -inline constexpr std::string_view MenuLoad = "menu.load"; -inline constexpr std::string_view MenuStatistics = "menu.statistics"; -inline constexpr std::string_view MenuExportAggregate = "menu.export_aggregate"; -inline constexpr std::string_view MenuExportSessions = "menu.export_sessions"; -inline constexpr std::string_view MenuExit = "menu.exit"; -inline constexpr std::string_view MenuEdit = "menu.edit"; -inline constexpr std::string_view MenuUndo = "menu.undo"; -inline constexpr std::string_view MenuRedo = "menu.redo"; -inline constexpr std::string_view MenuClearCell = "menu.clear_cell"; -inline constexpr std::string_view MenuHelp = "menu.help"; -inline constexpr std::string_view MenuGetHint = "menu.get_hint"; -inline constexpr std::string_view MenuGetCoachingHint = "menu.get_coaching_hint"; -inline constexpr std::string_view MenuAbout = "menu.about"; -inline constexpr std::string_view MenuTrainingMode = "menu.training_mode"; -inline constexpr std::string_view MenuAnalyzePosition = "menu.analyze_position"; -inline constexpr std::string_view MenuResumeGame = "menu.resume_game"; -inline constexpr std::string_view MenuSettings = "menu.settings"; -inline constexpr std::string_view MenuThirdPartyLicenses = "menu.third_party_licenses"; - -// ========================================================================= -// Toolbar -// ========================================================================= -inline constexpr std::string_view ToolbarNewGame = "toolbar.new_game"; -inline constexpr std::string_view ToolbarDifficulty = "toolbar.difficulty"; -inline constexpr std::string_view ToolbarHints = "toolbar.hints"; -inline constexpr std::string_view ToolbarRating = "toolbar.rating"; -inline constexpr std::string_view ToolbarHelpIcon = "toolbar.help_icon"; - -// ========================================================================= -// Difficulty names (shared across toolbar combo, dialogs, sidebar) -// ========================================================================= -inline constexpr std::string_view DifficultyEasy = "difficulty.easy"; -inline constexpr std::string_view DifficultyMedium = "difficulty.medium"; -inline constexpr std::string_view DifficultyHard = "difficulty.hard"; -inline constexpr std::string_view DifficultyExpert = "difficulty.expert"; -inline constexpr std::string_view DifficultyMaster = "difficulty.master"; -inline constexpr std::string_view DifficultyUnknown = "difficulty.unknown"; - -// ========================================================================= -// Action buttons -// ========================================================================= -inline constexpr std::string_view ButtonCheckSolution = "button.check_solution"; -inline constexpr std::string_view ButtonResetPuzzle = "button.reset_puzzle"; -inline constexpr std::string_view ButtonUndo = "button.undo"; -inline constexpr std::string_view ButtonRedo = "button.redo"; -inline constexpr std::string_view ButtonUndoToValid = "button.undo_to_valid"; -inline constexpr std::string_view ButtonAutoNotesOn = "button.auto_notes_on"; -inline constexpr std::string_view ButtonAutoNotesOff = "button.auto_notes_off"; -inline constexpr std::string_view ButtonFillNotes = "button.fill_notes"; -inline constexpr std::string_view ButtonClearNotes = "button.clear_notes"; -inline constexpr std::string_view ButtonUndoUntilValid = "button.undo_until_valid"; - -// ========================================================================= -// Input modes -// ========================================================================= -inline constexpr std::string_view ModeNormal = "mode.normal"; -inline constexpr std::string_view ModeNotes = "mode.notes"; -inline constexpr std::string_view ModeColor = "mode.color"; - -// ========================================================================= -// Training mode -// ========================================================================= -inline constexpr std::string_view TrainingTitle = "training.title"; -inline constexpr std::string_view TrainingAnalyzeTitle = "training.analyze_title"; -inline constexpr std::string_view TrainingSelectTechnique = "training.select_technique"; -inline constexpr std::string_view TrainingBackToGame = "training.back_to_game"; -inline constexpr std::string_view TrainingGroupFoundations = "training.group.foundations"; -inline constexpr std::string_view TrainingGroupSubsetBasics = "training.group.subset_basics"; -inline constexpr std::string_view TrainingGroupIntersections = "training.group.intersections"; -inline constexpr std::string_view TrainingGroupBasicFish = "training.group.basic_fish"; -inline constexpr std::string_view TrainingGroupLinks = "training.group.links"; -inline constexpr std::string_view TrainingGroupAdvancedFish = "training.group.advanced_fish"; -inline constexpr std::string_view TrainingGroupFinnedFish = "training.group.finned_fish"; -inline constexpr std::string_view TrainingGroupChains = "training.group.chains"; -inline constexpr std::string_view TrainingGroupInference = "training.group.inference"; -inline constexpr std::string_view TrainingWhatItIs = "training.what_it_is"; -inline constexpr std::string_view TrainingWhatToLookFor = "training.what_to_look_for"; -inline constexpr std::string_view TrainingStartExercises = "training.start_exercises"; -inline constexpr std::string_view TrainingBack = "training.back"; -inline constexpr std::string_view TrainingDifficultyPoints = "training.difficulty_points"; -inline constexpr std::string_view TrainingPrerequisites = "training.prerequisites"; -inline constexpr std::string_view TrainingExerciseHeader = "training.exercise_header"; -inline constexpr std::string_view TrainingColor = "training.color"; -inline constexpr std::string_view TrainingSubmit = "training.submit"; -inline constexpr std::string_view TrainingHint = "training.hint"; -inline constexpr std::string_view TrainingSkip = "training.skip"; -inline constexpr std::string_view TrainingQuitLesson = "training.quit_lesson"; -inline constexpr std::string_view TrainingNextExercise = "training.next_exercise"; -inline constexpr std::string_view TrainingRetry = "training.retry"; -inline constexpr std::string_view TrainingShowSolution = "training.show_solution"; -inline constexpr std::string_view TrainingScore = "training.score"; -inline constexpr std::string_view TrainingCorrect = "training.correct"; -inline constexpr std::string_view TrainingPartiallyCorrect = "training.partially_correct"; -inline constexpr std::string_view TrainingIncorrect = "training.incorrect"; -inline constexpr std::string_view TrainingLessonComplete = "training.lesson_complete"; -inline constexpr std::string_view TrainingTryAgain = "training.try_again"; -inline constexpr std::string_view TrainingPickTechnique = "training.pick_technique"; -inline constexpr std::string_view TrainingReturnToGame = "training.return_to_game"; -inline constexpr std::string_view TrainingTechnique = "training.technique"; -inline constexpr std::string_view TrainingHintsUsed = "training.hints_used"; -inline constexpr std::string_view TrainingMastery = "training.mastery"; -inline constexpr std::string_view TrainingPointsFmt = "training.points_fmt"; -inline constexpr std::string_view TrainingPrereqNotMet = "training.prereq_not_met"; -inline constexpr std::string_view TrainingRecommended = "training.recommended"; -inline constexpr std::string_view TrainingApplicable = "training.applicable"; -inline constexpr std::string_view TrainingExcellent = "training.excellent"; -inline constexpr std::string_view TrainingGoodProgress = "training.good_progress"; -inline constexpr std::string_view TrainingKeepPracticing = "training.keep_practicing"; -inline constexpr std::string_view TrainingErrorBacktracking = "training.error_backtracking"; -inline constexpr std::string_view TrainingErrorNoStep = "training.error_no_step"; -inline constexpr std::string_view TrainingCorrectContinue = "training.correct_continue"; -inline constexpr std::string_view TrainingFeedbackCorrect = "training.feedback_correct"; -inline constexpr std::string_view TrainingFeedbackPartial = "training.feedback_partial"; -inline constexpr std::string_view TrainingFeedbackIncorrect = "training.feedback_incorrect"; -inline constexpr std::string_view TrainingFeedbackUnknown = "training.feedback_unknown"; -inline constexpr std::string_view MasteryBeginner = "mastery.beginner"; -inline constexpr std::string_view MasteryIntermediate = "mastery.intermediate"; -inline constexpr std::string_view MasteryProficient = "mastery.proficient"; -inline constexpr std::string_view MasteryMastered = "mastery.mastered"; - -// ========================================================================= -// Status bar -// ========================================================================= -inline constexpr std::string_view StatusCompleted = "status.completed"; -inline constexpr std::string_view StatusPlaying = "status.playing"; -inline constexpr std::string_view StatusReady = "status.ready"; -inline constexpr std::string_view StatusPressF1 = "status.press_f1"; -inline constexpr std::string_view StatusInProgress = "status.in_progress"; - -// ========================================================================= -// Game board -// ========================================================================= -inline constexpr std::string_view BoardNoGameLoaded = "board.no_game_loaded"; - -// ========================================================================= -// Dialogs — New Game -// ========================================================================= -inline constexpr std::string_view DialogNewGame = "dialog.new_game"; -inline constexpr std::string_view DialogSelectDifficulty = "dialog.select_difficulty"; -inline constexpr std::string_view DialogStartGame = "dialog.start_game"; -inline constexpr std::string_view DialogCancel = "dialog.cancel"; -inline constexpr std::string_view DialogNewGameConfirm = "dialog.new_game_confirm"; - -// ========================================================================= -// Dialogs — Reset -// ========================================================================= -inline constexpr std::string_view DialogResetPuzzle = "dialog.reset_puzzle"; -inline constexpr std::string_view DialogResetWarning = "dialog.reset_warning"; -inline constexpr std::string_view DialogReset = "dialog.reset"; - -// ========================================================================= -// Dialogs — Save -// ========================================================================= -inline constexpr std::string_view DialogSaveGame = "dialog.save_game"; -inline constexpr std::string_view DialogEnterSaveName = "dialog.enter_save_name"; -inline constexpr std::string_view DialogSave = "dialog.save"; -inline constexpr std::string_view SavePreviewTitle = "save.preview_title"; -inline constexpr std::string_view SavePreviewDifficulty = "save.preview_difficulty"; -inline constexpr std::string_view SavePreviewTime = "save.preview_time"; -inline constexpr std::string_view SavePreviewMoves = "save.preview_moves"; -inline constexpr std::string_view SavePreviewMistakes = "save.preview_mistakes"; -inline constexpr std::string_view SaveNamePlaceholder = "save.name_placeholder"; -inline constexpr std::string_view SaveNameEmpty = "save.name_empty"; -inline constexpr std::string_view SaveOverwriteConfirm = "save.overwrite_confirm"; - -// ========================================================================= -// Dialogs — Load -// ========================================================================= -inline constexpr std::string_view DialogLoadGame = "dialog.load_game"; -inline constexpr std::string_view DialogRecentSaves = "dialog.recent_saves"; -inline constexpr std::string_view LoadColName = "load.col_name"; -inline constexpr std::string_view LoadColDifficulty = "load.col_difficulty"; -inline constexpr std::string_view LoadColDate = "load.col_date"; -inline constexpr std::string_view LoadColTime = "load.col_time"; -inline constexpr std::string_view LoadColRating = "load.col_rating"; - -// ========================================================================= -// Dialogs — Statistics (parameterized with fmt-style {0}) -// ========================================================================= -inline constexpr std::string_view DialogStatistics = "dialog.statistics"; -inline constexpr std::string_view DialogClose = "dialog.close"; -inline constexpr std::string_view StatsGamesPlayed = "stats.games_played"; -inline constexpr std::string_view StatsGamesCompleted = "stats.games_completed"; -inline constexpr std::string_view StatsCompletionRate = "stats.completion_rate"; -inline constexpr std::string_view StatsBestTime = "stats.best_time"; -inline constexpr std::string_view StatsAverageTime = "stats.average_time"; -inline constexpr std::string_view StatsCurrentStreak = "stats.current_streak"; -inline constexpr std::string_view StatsBestStreak = "stats.best_streak"; -inline constexpr std::string_view StatsTimeNa = "stats.time_na"; - -// Statistics dialog — tabs -inline constexpr std::string_view StatsTabOverview = "stats.tab_overview"; -inline constexpr std::string_view StatsTabPerDifficulty = "stats.tab_per_difficulty"; -inline constexpr std::string_view StatsTabRecentGames = "stats.tab_recent_games"; - -// Statistics dialog — totals -inline constexpr std::string_view StatsTotalMoves = "stats.total_moves"; -inline constexpr std::string_view StatsTotalHints = "stats.total_hints"; -inline constexpr std::string_view StatsTotalMistakes = "stats.total_mistakes"; -inline constexpr std::string_view StatsTotalTime = "stats.total_time"; - -// Statistics dialog — per-difficulty table rows -inline constexpr std::string_view StatsRowPlayed = "stats.row_played"; -inline constexpr std::string_view StatsRowCompleted = "stats.row_completed"; -inline constexpr std::string_view StatsRowBestTime = "stats.row_best_time"; -inline constexpr std::string_view StatsRowAvgTime = "stats.row_avg_time"; -inline constexpr std::string_view StatsRowAvgRating = "stats.row_avg_rating"; - -// Statistics dialog — recent games columns -inline constexpr std::string_view StatsColDate = "stats.col_date"; -inline constexpr std::string_view StatsColDifficulty = "stats.col_difficulty"; -inline constexpr std::string_view StatsColTime = "stats.col_time"; -inline constexpr std::string_view StatsColRating = "stats.col_rating"; -inline constexpr std::string_view StatsColMoves = "stats.col_moves"; -inline constexpr std::string_view StatsColMistakes = "stats.col_mistakes"; - -// ========================================================================= -// Dialogs — About -// ========================================================================= -inline constexpr std::string_view DialogAbout = "dialog.about"; -inline constexpr std::string_view AboutSudokuGame = "about.sudoku_game"; -inline constexpr std::string_view AboutBuiltWith = "about.built_with"; -inline constexpr std::string_view AboutDescription = "about.description"; -inline constexpr std::string_view RatingFormat = "rating.format"; - -// ========================================================================= -// Dialogs — Settings -// ========================================================================= -inline constexpr std::string_view DialogSettings = "dialog.settings"; -inline constexpr std::string_view DialogThirdPartyLicenses = "dialog.third_party_licenses"; -inline constexpr std::string_view SettingsTabGameplay = "settings.tab_gameplay"; -inline constexpr std::string_view SettingsTabDisplay = "settings.tab_display"; -inline constexpr std::string_view SettingsMaxHints = "settings.max_hints"; -inline constexpr std::string_view SettingsAutoSaveInterval = "settings.auto_save_interval"; -inline constexpr std::string_view SettingsDefaultDifficulty = "settings.default_difficulty"; -inline constexpr std::string_view SettingsSecondsSuffix = "settings.seconds_suffix"; -inline constexpr std::string_view SettingsHighlightConflicts = "settings.highlight_conflicts"; -inline constexpr std::string_view SettingsShowHints = "settings.show_hints"; -inline constexpr std::string_view SettingsCollectDetailedStats = "settings.collect_detailed_stats"; -inline constexpr std::string_view SettingsEncryptDetailedStats = "settings.encrypt_detailed_stats"; -inline constexpr std::string_view SettingsEncryptDetailedStatsTooltip = "settings.encrypt_detailed_stats_tooltip"; -inline constexpr std::string_view SettingsTabStatistics = "settings.tab_statistics"; -inline constexpr std::string_view StatsDeletePrompt = "stats.delete_prompt"; -inline constexpr std::string_view StatsSessionsDeleted = "stats.sessions_deleted"; - -// ========================================================================= -// Tooltips — Rating scale -// ========================================================================= -inline constexpr std::string_view TooltipRatingScale = "tooltip.rating_scale"; -inline constexpr std::string_view TooltipTechniquesRequired = "tooltip.techniques_required"; -inline constexpr std::string_view TooltipInputMode = "tooltip.input_mode"; -inline constexpr std::string_view TooltipPlaceDigit = "tooltip.place_digit"; -inline constexpr std::string_view TooltipEliminateDigit = "tooltip.eliminate_digit"; - -// ========================================================================= -// Dialogs — Puzzle Difficulty -// ========================================================================= -inline constexpr std::string_view DialogPuzzleDifficulty = "dialog.puzzle_difficulty"; -inline constexpr std::string_view DialogPuzzleRating = "dialog.puzzle_rating"; -inline constexpr std::string_view DialogTechniquesRequired = "dialog.techniques_required"; -inline constexpr std::string_view DialogNoTechniqueDetails = "dialog.no_technique_details"; - -// ========================================================================= -// Toolbar — Rating button format -// ========================================================================= -inline constexpr std::string_view ToolbarRatingWithTechniques = "toolbar.rating_with_techniques"; -inline constexpr std::string_view ToolbarRatingSimple = "toolbar.rating_simple"; - -// ========================================================================= -// Toast notifications -// ========================================================================= -inline constexpr std::string_view ToastGameSaved = "toast.game_saved"; -inline constexpr std::string_view ToastAggregateExported = "toast.aggregate_exported"; -inline constexpr std::string_view ToastSessionsExported = "toast.sessions_exported"; -inline constexpr std::string_view ToastExportFailed = "toast.export_failed"; -inline constexpr std::string_view ToastNoStrategies = "toast.no_strategies"; - -// ========================================================================= -// Sidebar -// ========================================================================= -inline constexpr std::string_view SidebarDifficulty = "sidebar.difficulty"; -inline constexpr std::string_view SidebarRating = "sidebar.rating"; -inline constexpr std::string_view SidebarLanguage = "sidebar.language"; - -// ========================================================================= -// ViewModel — Error messages -// ========================================================================= -inline constexpr std::string_view ErrorGeneratePuzzle = "error.generate_puzzle"; -inline constexpr std::string_view ErrorLoadGame = "error.load_game"; -inline constexpr std::string_view ErrorNoActiveGame = "error.no_active_game"; -inline constexpr std::string_view ErrorSaveGame = "error.save_game"; -inline constexpr std::string_view ErrorExportStats = "error.export_stats"; -inline constexpr std::string_view ErrorExportAggregate = "error.export_aggregate"; -inline constexpr std::string_view ErrorExportSessions = "error.export_sessions"; -inline constexpr std::string_view ErrorFileAccess = "error.file_access"; -inline constexpr std::string_view ErrorSerialization = "error.serialization"; -inline constexpr std::string_view ErrorUnknown = "error.unknown"; - -// ========================================================================= -// ViewModel — Status messages -// ========================================================================= -inline constexpr std::string_view StatusNoValidState = "status.no_valid_state"; -inline constexpr std::string_view StatusBoardValid = "status.board_valid"; -inline constexpr std::string_view StatusUndoneToValid = "status.undone_to_valid"; -inline constexpr std::string_view StatusPuzzleCompleted = "status.puzzle_completed"; -inline constexpr std::string_view StatusSolutionErrors = "status.solution_errors"; - -// ========================================================================= -// ViewModel — Hint messages -// ========================================================================= -inline constexpr std::string_view HintNoRemaining = "hint.no_remaining"; -inline constexpr std::string_view HintSelectCell = "hint.select_cell"; -inline constexpr std::string_view HintCannotRevealGiven = "hint.cannot_reveal_given"; -inline constexpr std::string_view HintCellHasValue = "hint.cell_has_value"; -inline constexpr std::string_view HintNoTechnique = "hint.no_technique"; -inline constexpr std::string_view HintSuggestionPlace = "hint.suggestion_place"; - -// ========================================================================= -// ViewModel — Coaching hint messages -// ========================================================================= -inline constexpr std::string_view CoachingNoRemaining = "coaching.no_remaining"; -inline constexpr std::string_view CoachingNoTechnique = "coaching.no_technique"; -inline constexpr std::string_view CoachingMaxLevel = "coaching.max_level"; -inline constexpr std::string_view CoachingLevelHeader = "coaching.level_header"; -inline constexpr std::string_view CoachingTryIt = "coaching.try_it"; -inline constexpr std::string_view CoachingCheckCorrect = "coaching.check_correct"; -inline constexpr std::string_view CoachingCheckPartial = "coaching.check_partial"; -inline constexpr std::string_view CoachingCheckWrong = "coaching.check_wrong"; -inline constexpr std::string_view CoachingApplied = "coaching.applied"; -inline constexpr std::string_view CoachingButtonCheck = "coaching.button_check"; -inline constexpr std::string_view CoachingButtonApply = "coaching.button_apply"; -inline constexpr std::string_view CoachingTryItLabel = "coaching.try_it_label"; -inline constexpr std::string_view CoachingWhatToLookFor = "coaching.what_to_look_for"; -inline constexpr std::string_view CoachingCheckZero = "coaching.check_zero"; - -// ========================================================================= -// Training hints — progressive hint text per category/level -// ========================================================================= -inline constexpr std::string_view HintSinglesL1 = "hint.singles.l1"; -inline constexpr std::string_view HintSinglesL2Region = "hint.singles.l2_region"; -inline constexpr std::string_view HintSinglesL2NoRegion = "hint.singles.l2_no_region"; -inline constexpr std::string_view HintSinglesL3 = "hint.singles.l3"; -inline constexpr std::string_view HintSubsetsL1Region = "hint.subsets.l1_region"; -inline constexpr std::string_view HintSubsetsL1NoRegion = "hint.subsets.l1_no_region"; -inline constexpr std::string_view HintSubsetsL2Values = "hint.subsets.l2_values"; -inline constexpr std::string_view HintSubsetsL2NoValues = "hint.subsets.l2_no_values"; -inline constexpr std::string_view HintSubsetsL3 = "hint.subsets.l3"; -inline constexpr std::string_view HintIntersectionsL1Value = "hint.intersections.l1_value"; -inline constexpr std::string_view HintIntersectionsL1NoValue = "hint.intersections.l1_no_value"; -inline constexpr std::string_view HintIntersectionsL2 = "hint.intersections.l2"; -inline constexpr std::string_view HintIntersectionsL3 = "hint.intersections.l3"; -inline constexpr std::string_view HintFishL1Value = "hint.fish.l1_value"; -inline constexpr std::string_view HintFishL1NoValue = "hint.fish.l1_no_value"; -inline constexpr std::string_view HintFishL2 = "hint.fish.l2"; -inline constexpr std::string_view HintFishL3 = "hint.fish.l3"; -inline constexpr std::string_view HintWingsL1 = "hint.wings.l1"; -inline constexpr std::string_view HintWingsL2 = "hint.wings.l2"; -inline constexpr std::string_view HintWingsL3 = "hint.wings.l3"; -inline constexpr std::string_view HintSingleDigitL1Value = "hint.single_digit.l1_value"; -inline constexpr std::string_view HintSingleDigitL1NoValue = "hint.single_digit.l1_no_value"; -inline constexpr std::string_view HintSingleDigitL2 = "hint.single_digit.l2"; -inline constexpr std::string_view HintSingleDigitL3 = "hint.single_digit.l3"; -inline constexpr std::string_view HintColoringL1Value = "hint.coloring.l1_value"; -inline constexpr std::string_view HintColoringL1NoValue = "hint.coloring.l1_no_value"; -inline constexpr std::string_view HintColoringL2 = "hint.coloring.l2"; -inline constexpr std::string_view HintColoringL3 = "hint.coloring.l3"; -inline constexpr std::string_view HintUniqueRectL1 = "hint.unique_rect.l1"; -inline constexpr std::string_view HintUniqueRectL2 = "hint.unique_rect.l2"; -inline constexpr std::string_view HintUniqueRectL3 = "hint.unique_rect.l3"; -inline constexpr std::string_view HintChainsL1Pos = "hint.chains.l1_pos"; -inline constexpr std::string_view HintChainsL1NoPos = "hint.chains.l1_no_pos"; -inline constexpr std::string_view HintChainsL2 = "hint.chains.l2"; -inline constexpr std::string_view HintChainsL3Placement = "hint.chains.l3_placement"; -inline constexpr std::string_view HintChainsL3Elimination = "hint.chains.l3_elimination"; -inline constexpr std::string_view HintSetLogicL1 = "hint.set_logic.l1"; -inline constexpr std::string_view HintSetLogicL2 = "hint.set_logic.l2"; -inline constexpr std::string_view HintSetLogicL3 = "hint.set_logic.l3"; -inline constexpr std::string_view HintSpecialL1 = "hint.special.l1"; -inline constexpr std::string_view HintSpecialL2 = "hint.special.l2"; - -// ========================================================================= -// Technique descriptions — what_it_is and what_to_look_for per technique -// ========================================================================= -inline constexpr std::string_view TechDescNakedSingleWhatItIs = "tech.desc.naked_single.what_it_is"; -inline constexpr std::string_view TechDescNakedSingleWhatToLookFor = "tech.desc.naked_single.what_to_look_for"; -inline constexpr std::string_view TechDescHiddenSingleWhatItIs = "tech.desc.hidden_single.what_it_is"; -inline constexpr std::string_view TechDescHiddenSingleWhatToLookFor = "tech.desc.hidden_single.what_to_look_for"; -inline constexpr std::string_view TechDescNakedPairWhatItIs = "tech.desc.naked_pair.what_it_is"; -inline constexpr std::string_view TechDescNakedPairWhatToLookFor = "tech.desc.naked_pair.what_to_look_for"; -inline constexpr std::string_view TechDescNakedTripleWhatItIs = "tech.desc.naked_triple.what_it_is"; -inline constexpr std::string_view TechDescNakedTripleWhatToLookFor = "tech.desc.naked_triple.what_to_look_for"; -inline constexpr std::string_view TechDescHiddenPairWhatItIs = "tech.desc.hidden_pair.what_it_is"; -inline constexpr std::string_view TechDescHiddenPairWhatToLookFor = "tech.desc.hidden_pair.what_to_look_for"; -inline constexpr std::string_view TechDescHiddenTripleWhatItIs = "tech.desc.hidden_triple.what_it_is"; -inline constexpr std::string_view TechDescHiddenTripleWhatToLookFor = "tech.desc.hidden_triple.what_to_look_for"; -inline constexpr std::string_view TechDescPointingPairWhatItIs = "tech.desc.pointing_pair.what_it_is"; -inline constexpr std::string_view TechDescPointingPairWhatToLookFor = "tech.desc.pointing_pair.what_to_look_for"; -inline constexpr std::string_view TechDescBoxLineReductionWhatItIs = "tech.desc.box_line_reduction.what_it_is"; -inline constexpr std::string_view TechDescBoxLineReductionWhatToLookFor = - "tech.desc.box_line_reduction.what_to_look_for"; -inline constexpr std::string_view TechDescNakedQuadWhatItIs = "tech.desc.naked_quad.what_it_is"; -inline constexpr std::string_view TechDescNakedQuadWhatToLookFor = "tech.desc.naked_quad.what_to_look_for"; -inline constexpr std::string_view TechDescHiddenQuadWhatItIs = "tech.desc.hidden_quad.what_it_is"; -inline constexpr std::string_view TechDescHiddenQuadWhatToLookFor = "tech.desc.hidden_quad.what_to_look_for"; -inline constexpr std::string_view TechDescXWingWhatItIs = "tech.desc.x_wing.what_it_is"; -inline constexpr std::string_view TechDescXWingWhatToLookFor = "tech.desc.x_wing.what_to_look_for"; -inline constexpr std::string_view TechDescXYWingWhatItIs = "tech.desc.xy_wing.what_it_is"; -inline constexpr std::string_view TechDescXYWingWhatToLookFor = "tech.desc.xy_wing.what_to_look_for"; -inline constexpr std::string_view TechDescSwordfishWhatItIs = "tech.desc.swordfish.what_it_is"; -inline constexpr std::string_view TechDescSwordfishWhatToLookFor = "tech.desc.swordfish.what_to_look_for"; -inline constexpr std::string_view TechDescSkyscraperWhatItIs = "tech.desc.skyscraper.what_it_is"; -inline constexpr std::string_view TechDescSkyscraperWhatToLookFor = "tech.desc.skyscraper.what_to_look_for"; -inline constexpr std::string_view TechDescTwoStringKiteWhatItIs = "tech.desc.two_string_kite.what_it_is"; -inline constexpr std::string_view TechDescTwoStringKiteWhatToLookFor = "tech.desc.two_string_kite.what_to_look_for"; -inline constexpr std::string_view TechDescXYZWingWhatItIs = "tech.desc.xyz_wing.what_it_is"; -inline constexpr std::string_view TechDescXYZWingWhatToLookFor = "tech.desc.xyz_wing.what_to_look_for"; -inline constexpr std::string_view TechDescUniqueRectangleWhatItIs = "tech.desc.unique_rectangle.what_it_is"; -inline constexpr std::string_view TechDescUniqueRectangleWhatToLookFor = "tech.desc.unique_rectangle.what_to_look_for"; -inline constexpr std::string_view TechDescWWingWhatItIs = "tech.desc.w_wing.what_it_is"; -inline constexpr std::string_view TechDescWWingWhatToLookFor = "tech.desc.w_wing.what_to_look_for"; -inline constexpr std::string_view TechDescSimpleColoringWhatItIs = "tech.desc.simple_coloring.what_it_is"; -inline constexpr std::string_view TechDescSimpleColoringWhatToLookFor = "tech.desc.simple_coloring.what_to_look_for"; -inline constexpr std::string_view TechDescFinnedXWingWhatItIs = "tech.desc.finned_x_wing.what_it_is"; -inline constexpr std::string_view TechDescFinnedXWingWhatToLookFor = "tech.desc.finned_x_wing.what_to_look_for"; -inline constexpr std::string_view TechDescRemotePairsWhatItIs = "tech.desc.remote_pairs.what_it_is"; -inline constexpr std::string_view TechDescRemotePairsWhatToLookFor = "tech.desc.remote_pairs.what_to_look_for"; -inline constexpr std::string_view TechDescBUGWhatItIs = "tech.desc.bug.what_it_is"; -inline constexpr std::string_view TechDescBUGWhatToLookFor = "tech.desc.bug.what_to_look_for"; -inline constexpr std::string_view TechDescJellyfishWhatItIs = "tech.desc.jellyfish.what_it_is"; -inline constexpr std::string_view TechDescJellyfishWhatToLookFor = "tech.desc.jellyfish.what_to_look_for"; -inline constexpr std::string_view TechDescFinnedSwordfishWhatItIs = "tech.desc.finned_swordfish.what_it_is"; -inline constexpr std::string_view TechDescFinnedSwordfishWhatToLookFor = "tech.desc.finned_swordfish.what_to_look_for"; -inline constexpr std::string_view TechDescEmptyRectangleWhatItIs = "tech.desc.empty_rectangle.what_it_is"; -inline constexpr std::string_view TechDescEmptyRectangleWhatToLookFor = "tech.desc.empty_rectangle.what_to_look_for"; -inline constexpr std::string_view TechDescWXYZWingWhatItIs = "tech.desc.wxyz_wing.what_it_is"; -inline constexpr std::string_view TechDescWXYZWingWhatToLookFor = "tech.desc.wxyz_wing.what_to_look_for"; -inline constexpr std::string_view TechDescFinnedJellyfishWhatItIs = "tech.desc.finned_jellyfish.what_it_is"; -inline constexpr std::string_view TechDescFinnedJellyfishWhatToLookFor = "tech.desc.finned_jellyfish.what_to_look_for"; -inline constexpr std::string_view TechDescXYChainWhatItIs = "tech.desc.xy_chain.what_it_is"; -inline constexpr std::string_view TechDescXYChainWhatToLookFor = "tech.desc.xy_chain.what_to_look_for"; -inline constexpr std::string_view TechDescMultiColoringWhatItIs = "tech.desc.multi_coloring.what_it_is"; -inline constexpr std::string_view TechDescMultiColoringWhatToLookFor = "tech.desc.multi_coloring.what_to_look_for"; -inline constexpr std::string_view TechDescALSxZWhatItIs = "tech.desc.als_xz.what_it_is"; -inline constexpr std::string_view TechDescALSxZWhatToLookFor = "tech.desc.als_xz.what_to_look_for"; -inline constexpr std::string_view TechDescSueDeCoqWhatItIs = "tech.desc.sue_de_coq.what_it_is"; -inline constexpr std::string_view TechDescSueDeCoqWhatToLookFor = "tech.desc.sue_de_coq.what_to_look_for"; -inline constexpr std::string_view TechDescForcingChainWhatItIs = "tech.desc.forcing_chain.what_it_is"; -inline constexpr std::string_view TechDescForcingChainWhatToLookFor = "tech.desc.forcing_chain.what_to_look_for"; -inline constexpr std::string_view TechDescNiceLoopWhatItIs = "tech.desc.nice_loop.what_it_is"; -inline constexpr std::string_view TechDescNiceLoopWhatToLookFor = "tech.desc.nice_loop.what_to_look_for"; -inline constexpr std::string_view TechDescXCyclesWhatItIs = "tech.desc.x_cycles.what_it_is"; -inline constexpr std::string_view TechDescXCyclesWhatToLookFor = "tech.desc.x_cycles.what_to_look_for"; -inline constexpr std::string_view TechDescThreeDMedusaWhatItIs = "tech.desc.three_d_medusa.what_it_is"; -inline constexpr std::string_view TechDescThreeDMedusaWhatToLookFor = "tech.desc.three_d_medusa.what_to_look_for"; -inline constexpr std::string_view TechDescHiddenUniqueRectangleWhatItIs = - "tech.desc.hidden_unique_rectangle.what_it_is"; -inline constexpr std::string_view TechDescHiddenUniqueRectangleWhatToLookFor = - "tech.desc.hidden_unique_rectangle.what_to_look_for"; -inline constexpr std::string_view TechDescAvoidableRectangleWhatItIs = "tech.desc.avoidable_rectangle.what_it_is"; -inline constexpr std::string_view TechDescAvoidableRectangleWhatToLookFor = - "tech.desc.avoidable_rectangle.what_to_look_for"; -inline constexpr std::string_view TechDescALSXYWingWhatItIs = "tech.desc.als_xy_wing.what_it_is"; -inline constexpr std::string_view TechDescALSXYWingWhatToLookFor = "tech.desc.als_xy_wing.what_to_look_for"; -inline constexpr std::string_view TechDescDeathBlossomWhatItIs = "tech.desc.death_blossom.what_it_is"; -inline constexpr std::string_view TechDescDeathBlossomWhatToLookFor = "tech.desc.death_blossom.what_to_look_for"; -inline constexpr std::string_view TechDescVWXYZWingWhatItIs = "tech.desc.vwxyz_wing.what_it_is"; -inline constexpr std::string_view TechDescVWXYZWingWhatToLookFor = "tech.desc.vwxyz_wing.what_to_look_for"; -inline constexpr std::string_view TechDescFrankenFishWhatItIs = "tech.desc.franken_fish.what_it_is"; -inline constexpr std::string_view TechDescFrankenFishWhatToLookFor = "tech.desc.franken_fish.what_to_look_for"; -inline constexpr std::string_view TechDescGroupedXCyclesWhatItIs = "tech.desc.grouped_x_cycles.what_it_is"; -inline constexpr std::string_view TechDescGroupedXCyclesWhatToLookFor = "tech.desc.grouped_x_cycles.what_to_look_for"; -inline constexpr std::string_view TechDescSashimiXWingWhatItIs = "tech.desc.sashimi_x_wing.what_it_is"; -inline constexpr std::string_view TechDescSashimiXWingWhatToLookFor = "tech.desc.sashimi_x_wing.what_to_look_for"; -inline constexpr std::string_view TechDescSashimiSwordfishWhatItIs = "tech.desc.sashimi_swordfish.what_it_is"; -inline constexpr std::string_view TechDescSashimiSwordfishWhatToLookFor = - "tech.desc.sashimi_swordfish.what_to_look_for"; -inline constexpr std::string_view TechDescSashimiJellyfishWhatItIs = "tech.desc.sashimi_jellyfish.what_it_is"; -inline constexpr std::string_view TechDescSashimiJellyfishWhatToLookFor = - "tech.desc.sashimi_jellyfish.what_to_look_for"; -inline constexpr std::string_view TechDescUnitForcingChainWhatItIs = "tech.desc.unit_forcing_chain.what_it_is"; -inline constexpr std::string_view TechDescUnitForcingChainWhatToLookFor = - "tech.desc.unit_forcing_chain.what_to_look_for"; -inline constexpr std::string_view TechDescRegionForcingChainWhatItIs = "tech.desc.region_forcing_chain.what_it_is"; -inline constexpr std::string_view TechDescRegionForcingChainWhatToLookFor = - "tech.desc.region_forcing_chain.what_to_look_for"; -inline constexpr std::string_view TechDescMutantFishWhatItIs = "tech.desc.mutant_fish.what_it_is"; -inline constexpr std::string_view TechDescMutantFishWhatToLookFor = "tech.desc.mutant_fish.what_to_look_for"; -inline constexpr std::string_view TechDescKrakenFishWhatItIs = "tech.desc.kraken_fish.what_it_is"; -inline constexpr std::string_view TechDescKrakenFishWhatToLookFor = "tech.desc.kraken_fish.what_to_look_for"; -inline constexpr std::string_view TechDescALSChainWhatItIs = "tech.desc.als_chain.what_it_is"; -inline constexpr std::string_view TechDescALSChainWhatToLookFor = "tech.desc.als_chain.what_to_look_for"; -inline constexpr std::string_view TechDescUniqueLoopWhatItIs = "tech.desc.unique_loop.what_it_is"; -inline constexpr std::string_view TechDescUniqueLoopWhatToLookFor = "tech.desc.unique_loop.what_to_look_for"; -inline constexpr std::string_view TechDescJuniorExocetWhatItIs = "tech.desc.junior_exocet.what_it_is"; -inline constexpr std::string_view TechDescJuniorExocetWhatToLookFor = "tech.desc.junior_exocet.what_to_look_for"; -inline constexpr std::string_view TechDescContinuousNiceLoopWhatItIs = "tech.desc.continuous_nice_loop.what_it_is"; -inline constexpr std::string_view TechDescContinuousNiceLoopWhatToLookFor = - "tech.desc.continuous_nice_loop.what_to_look_for"; -inline constexpr std::string_view TechDescGroupedNiceLoopWhatItIs = "tech.desc.grouped_nice_loop.what_it_is"; -inline constexpr std::string_view TechDescGroupedNiceLoopWhatToLookFor = "tech.desc.grouped_nice_loop.what_to_look_for"; -inline constexpr std::string_view TechDescBacktrackingWhatItIs = "tech.desc.backtracking.what_it_is"; -inline constexpr std::string_view TechDescBacktrackingWhatToLookFor = "tech.desc.backtracking.what_to_look_for"; -inline constexpr std::string_view TechDescUnknownWhatItIs = "tech.desc.unknown.what_it_is"; -inline constexpr std::string_view TechDescUnknownWhatToLookFor = "tech.desc.unknown.what_to_look_for"; - -// ========================================================================= -// ViewModel — Technique formatting -// ========================================================================= -inline constexpr std::string_view TechniquePointsFmt = "technique.points_fmt"; -inline constexpr std::string_view TechniqueBacktracking = "technique.backtracking"; - -// ========================================================================= -// ViewModel — Statistics error strings -// ========================================================================= -inline constexpr std::string_view StatsErrInvalidData = "stats_err.invalid_data"; -inline constexpr std::string_view StatsErrFileAccess = "stats_err.file_access"; -inline constexpr std::string_view StatsErrSerialization = "stats_err.serialization"; -inline constexpr std::string_view StatsErrInvalidDifficulty = "stats_err.invalid_difficulty"; -inline constexpr std::string_view StatsErrGameNotStarted = "stats_err.game_not_started"; -inline constexpr std::string_view StatsErrGameAlreadyEnded = "stats_err.game_already_ended"; -inline constexpr std::string_view StatsErrUnknown = "stats_err.unknown"; - -// ========================================================================= -// Technique names -// ========================================================================= -inline constexpr std::string_view TechNakedSingle = "tech.naked_single"; -inline constexpr std::string_view TechHiddenSingle = "tech.hidden_single"; -inline constexpr std::string_view TechNakedPair = "tech.naked_pair"; -inline constexpr std::string_view TechNakedTriple = "tech.naked_triple"; -inline constexpr std::string_view TechHiddenPair = "tech.hidden_pair"; -inline constexpr std::string_view TechHiddenTriple = "tech.hidden_triple"; -inline constexpr std::string_view TechPointingPair = "tech.pointing_pair"; -inline constexpr std::string_view TechBoxLineReduction = "tech.box_line_reduction"; -inline constexpr std::string_view TechNakedQuad = "tech.naked_quad"; -inline constexpr std::string_view TechHiddenQuad = "tech.hidden_quad"; -inline constexpr std::string_view TechXWing = "tech.x_wing"; -inline constexpr std::string_view TechXYWing = "tech.xy_wing"; -inline constexpr std::string_view TechSwordfish = "tech.swordfish"; -inline constexpr std::string_view TechSkyscraper = "tech.skyscraper"; -inline constexpr std::string_view TechTwoStringKite = "tech.two_string_kite"; -inline constexpr std::string_view TechXYZWing = "tech.xyz_wing"; -inline constexpr std::string_view TechUniqueRectangle = "tech.unique_rectangle"; -inline constexpr std::string_view TechWWing = "tech.w_wing"; -inline constexpr std::string_view TechSimpleColoring = "tech.simple_coloring"; -inline constexpr std::string_view TechFinnedXWing = "tech.finned_x_wing"; -inline constexpr std::string_view TechRemotePairs = "tech.remote_pairs"; -inline constexpr std::string_view TechBUG = "tech.bug"; -inline constexpr std::string_view TechJellyfish = "tech.jellyfish"; -inline constexpr std::string_view TechFinnedSwordfish = "tech.finned_swordfish"; -inline constexpr std::string_view TechEmptyRectangle = "tech.empty_rectangle"; -inline constexpr std::string_view TechWXYZWing = "tech.wxyz_wing"; -inline constexpr std::string_view TechFinnedJellyfish = "tech.finned_jellyfish"; -inline constexpr std::string_view TechXYChain = "tech.xy_chain"; -inline constexpr std::string_view TechMultiColoring = "tech.multi_coloring"; -inline constexpr std::string_view TechALSxZ = "tech.als_xz"; -inline constexpr std::string_view TechSueDeCoq = "tech.sue_de_coq"; -inline constexpr std::string_view TechForcingChain = "tech.forcing_chain"; -inline constexpr std::string_view TechNiceLoop = "tech.nice_loop"; -inline constexpr std::string_view TechXCycles = "tech.x_cycles"; -inline constexpr std::string_view TechThreeDMedusa = "tech.three_d_medusa"; -inline constexpr std::string_view TechHiddenUniqueRectangle = "tech.hidden_unique_rectangle"; -inline constexpr std::string_view TechAvoidableRectangle = "tech.avoidable_rectangle"; -inline constexpr std::string_view TechALSXYWing = "tech.als_xy_wing"; -inline constexpr std::string_view TechDeathBlossom = "tech.death_blossom"; -inline constexpr std::string_view TechVWXYZWing = "tech.vwxyz_wing"; -inline constexpr std::string_view TechFrankenFish = "tech.franken_fish"; -inline constexpr std::string_view TechGroupedXCycles = "tech.grouped_x_cycles"; -inline constexpr std::string_view TechSashimiXWing = "tech.sashimi_x_wing"; -inline constexpr std::string_view TechSashimiSwordfish = "tech.sashimi_swordfish"; -inline constexpr std::string_view TechSashimiJellyfish = "tech.sashimi_jellyfish"; -inline constexpr std::string_view TechUnitForcingChain = "tech.unit_forcing_chain"; -inline constexpr std::string_view TechRegionForcingChain = "tech.region_forcing_chain"; -inline constexpr std::string_view TechMutantFish = "tech.mutant_fish"; -inline constexpr std::string_view TechKrakenFish = "tech.kraken_fish"; -inline constexpr std::string_view TechALSChain = "tech.als_chain"; -inline constexpr std::string_view TechJuniorExocet = "tech.junior_exocet"; -inline constexpr std::string_view TechUniqueLoop = "tech.unique_loop"; -inline constexpr std::string_view TechContinuousNiceLoop = "tech.continuous_nice_loop"; -inline constexpr std::string_view TechGroupedNiceLoop = "tech.grouped_nice_loop"; -inline constexpr std::string_view TechBacktrackingName = "tech.backtracking_name"; -inline constexpr std::string_view TechUnknown = "tech.unknown"; - -// ========================================================================= -// Region names -// ========================================================================= -inline constexpr std::string_view RegionRow = "region.row"; -inline constexpr std::string_view RegionColumn = "region.column"; -inline constexpr std::string_view RegionBox = "region.box"; -inline constexpr std::string_view RegionUnknown = "region.unknown"; - -// ========================================================================= -// Position format -// ========================================================================= -inline constexpr std::string_view PositionFmt = "position.fmt"; - -// ========================================================================= -// Explanation templates (one per technique variant) -// ========================================================================= -inline constexpr std::string_view ExplainNakedSingle = "explain.naked_single"; -inline constexpr std::string_view ExplainHiddenSingle = "explain.hidden_single"; -inline constexpr std::string_view ExplainNakedPair = "explain.naked_pair"; -inline constexpr std::string_view ExplainNakedTriple = "explain.naked_triple"; -inline constexpr std::string_view ExplainHiddenPair = "explain.hidden_pair"; -inline constexpr std::string_view ExplainHiddenTriple = "explain.hidden_triple"; -inline constexpr std::string_view ExplainPointingPair = "explain.pointing_pair"; -inline constexpr std::string_view ExplainBoxLineReduction = "explain.box_line_reduction"; -inline constexpr std::string_view ExplainNakedQuad = "explain.naked_quad"; -inline constexpr std::string_view ExplainHiddenQuad = "explain.hidden_quad"; -inline constexpr std::string_view ExplainXWingRow = "explain.x_wing_row"; -inline constexpr std::string_view ExplainXWingCol = "explain.x_wing_col"; -inline constexpr std::string_view ExplainXYWing = "explain.xy_wing"; -inline constexpr std::string_view ExplainSwordfishRow = "explain.swordfish_row"; -inline constexpr std::string_view ExplainSwordfishCol = "explain.swordfish_col"; -inline constexpr std::string_view ExplainSkyscraper = "explain.skyscraper"; -inline constexpr std::string_view ExplainTwoStringKite = "explain.two_string_kite"; -inline constexpr std::string_view ExplainXYZWing = "explain.xyz_wing"; -inline constexpr std::string_view ExplainUniqueRectangle = "explain.unique_rectangle"; -inline constexpr std::string_view ExplainWWing = "explain.w_wing"; -inline constexpr std::string_view ExplainSimpleColoringContradiction = "explain.simple_coloring_contradiction"; -inline constexpr std::string_view ExplainSimpleColoringExclusion = "explain.simple_coloring_exclusion"; -inline constexpr std::string_view ExplainUniqueRectangleType2 = "explain.unique_rectangle_type2"; -inline constexpr std::string_view ExplainUniqueRectangleType3 = "explain.unique_rectangle_type3"; -inline constexpr std::string_view ExplainUniqueRectangleType4 = "explain.unique_rectangle_type4"; -inline constexpr std::string_view ExplainUniqueRectangleType6 = "explain.unique_rectangle_type6"; -inline constexpr std::string_view ExplainFinnedXWingRow = "explain.finned_x_wing_row"; -inline constexpr std::string_view ExplainFinnedXWingCol = "explain.finned_x_wing_col"; -inline constexpr std::string_view ExplainSashimiXWingRow = "explain.sashimi_x_wing_row"; -inline constexpr std::string_view ExplainSashimiXWingCol = "explain.sashimi_x_wing_col"; -inline constexpr std::string_view ExplainSashimiSwordfishRow = "explain.sashimi_swordfish_row"; -inline constexpr std::string_view ExplainSashimiSwordfishCol = "explain.sashimi_swordfish_col"; -inline constexpr std::string_view ExplainSashimiJellyfishRow = "explain.sashimi_jellyfish_row"; -inline constexpr std::string_view ExplainSashimiJellyfishCol = "explain.sashimi_jellyfish_col"; -inline constexpr std::string_view ExplainRemotePairs = "explain.remote_pairs"; -inline constexpr std::string_view ExplainBUG = "explain.bug"; -inline constexpr std::string_view ExplainJellyfishRow = "explain.jellyfish_row"; -inline constexpr std::string_view ExplainJellyfishCol = "explain.jellyfish_col"; -inline constexpr std::string_view ExplainFinnedSwordfishRow = "explain.finned_swordfish_row"; -inline constexpr std::string_view ExplainFinnedSwordfishCol = "explain.finned_swordfish_col"; -inline constexpr std::string_view ExplainEmptyRectangle = "explain.empty_rectangle"; -inline constexpr std::string_view ExplainWXYZWing = "explain.wxyz_wing"; -inline constexpr std::string_view ExplainFinnedJellyfishRow = "explain.finned_jellyfish_row"; -inline constexpr std::string_view ExplainFinnedJellyfishCol = "explain.finned_jellyfish_col"; -inline constexpr std::string_view ExplainXYChain = "explain.xy_chain"; -inline constexpr std::string_view ExplainMultiColoringWrap = "explain.multi_coloring_wrap"; -inline constexpr std::string_view ExplainMultiColoringTrap = "explain.multi_coloring_trap"; -inline constexpr std::string_view ExplainALSxZ = "explain.als_xz"; -inline constexpr std::string_view ExplainSueDeCoq = "explain.sue_de_coq"; -inline constexpr std::string_view ExplainForcingChain = "explain.forcing_chain"; -inline constexpr std::string_view ExplainNiceLoop = "explain.nice_loop"; -inline constexpr std::string_view ExplainXCyclesType1 = "explain.x_cycles_type1"; -inline constexpr std::string_view ExplainXCyclesType2 = "explain.x_cycles_type2"; -inline constexpr std::string_view ExplainXCyclesType3 = "explain.x_cycles_type3"; -inline constexpr std::string_view ExplainThreeDMedusa = "explain.three_d_medusa"; -inline constexpr std::string_view ExplainHiddenUniqueRectangle = "explain.hidden_unique_rectangle"; -inline constexpr std::string_view ExplainAvoidableRectangle = "explain.avoidable_rectangle"; -inline constexpr std::string_view ExplainALSXYWing = "explain.als_xy_wing"; -inline constexpr std::string_view ExplainDeathBlossom = "explain.death_blossom"; -inline constexpr std::string_view ExplainVWXYZWing = "explain.vwxyz_wing"; -inline constexpr std::string_view ExplainFrankenFish = "explain.franken_fish"; -inline constexpr std::string_view ExplainMutantFish = "explain.mutant_fish"; -inline constexpr std::string_view ExplainGroupedXCycles = "explain.grouped_x_cycles"; -inline constexpr std::string_view ExplainKrakenFish = "explain.kraken_fish"; -inline constexpr std::string_view ExplainALSChain = "explain.als_chain"; -inline constexpr std::string_view ExplainJuniorExocet = "explain.junior_exocet"; -inline constexpr std::string_view ExplainUniqueLoop = "explain.unique_loop"; -inline constexpr std::string_view ExplainContinuousNiceLoop = "explain.continuous_nice_loop"; -inline constexpr std::string_view ExplainGroupedNiceLoop = "explain.grouped_nice_loop"; - -} // namespace sudoku::core::StringKeys diff --git a/src/core/technique_descriptions.h b/src/core/technique_descriptions.h index 6786bde..9040f61 100644 --- a/src/core/technique_descriptions.h +++ b/src/core/technique_descriptions.h @@ -16,255 +16,492 @@ #pragma once -#include "i_localization_manager.h" +#include "core/i18n_helpers.h" #include "solving_technique.h" -#include "string_keys.h" -#include +#include namespace sudoku::core { /// Localized theory text for a solving technique struct TechniqueDescription { - std::string_view title; - std::string_view what_it_is; - std::string_view what_to_look_for; + std::string title; + std::string what_it_is; + std::string what_to_look_for; }; /// Get the localized description for a solving technique -/// @param loc Localization manager for translating description text /// @param technique The technique to describe /// @return Description with localized title, explanation, and identification tips // NOLINTNEXTLINE(readability-function-size) — dispatch table over all SolvingTechnique values; each case returns one constant struct; cannot meaningfully split -[[nodiscard]] inline TechniqueDescription getTechniqueDescription(const ILocalizationManager& loc, - SolvingTechnique technique) { +[[nodiscard]] inline TechniqueDescription getTechniqueDescription(SolvingTechnique technique) { using enum SolvingTechnique; - using namespace StringKeys; switch (technique) { case NakedSingle: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescNakedSingleWhatItIs), - .what_to_look_for = loc.getString(TechDescNakedSingleWhatToLookFor)}; + return { + .title = getLocalizedTechniqueName(technique), + .what_it_is = + core::loc("Sudoku", "A cell has only one possible candidate left. All other values are eliminated " + "by row, column, and box constraints."), + .what_to_look_for = core::loc( + "Sudoku", + "Look for cells where 8 of the 9 values are already present in the cell's row, column, or box.")}; case HiddenSingle: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescHiddenSingleWhatItIs), - .what_to_look_for = loc.getString(TechDescHiddenSingleWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", "A value can only go in one cell within a row, column, or box. Even though the cell " + "may have multiple candidates, only this value has no other place in the region."), + .what_to_look_for = + core::loc("Sudoku", "For each region, check if any value has only one possible cell.")}; case NakedPair: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescNakedPairWhatItIs), - .what_to_look_for = loc.getString(TechDescNakedPairWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", + "Two cells in the same region each contain exactly the same two candidates. Those two values " + "must go in those two cells, so they can be eliminated from all other cells in the region."), + .what_to_look_for = core::loc( + "Sudoku", "Find two cells in a row, column, or box that share the same pair of candidates.")}; case NakedTriple: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescNakedTripleWhatItIs), - .what_to_look_for = loc.getString(TechDescNakedTripleWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", + "Three cells in a region collectively contain exactly three candidates. Each cell has a subset " + "of those three values. Those values can be eliminated from other cells in the region."), + .what_to_look_for = core::loc( + "Sudoku", + "Find three cells in a region whose combined candidates form a set of exactly three values.")}; case HiddenPair: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescHiddenPairWhatItIs), - .what_to_look_for = loc.getString(TechDescHiddenPairWhatToLookFor)}; + return { + .title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc("Sudoku", "Two values in a region appear as candidates in exactly the same two " + "cells. Other candidates in those two cells can be eliminated."), + .what_to_look_for = + core::loc("Sudoku", "For each region, find two values that appear only in the same two cells.")}; case HiddenTriple: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescHiddenTripleWhatItIs), - .what_to_look_for = loc.getString(TechDescHiddenTripleWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = + core::loc("Sudoku", "Three values in a region appear as candidates in exactly three cells. " + "Other candidates in those cells can be eliminated."), + .what_to_look_for = + core::loc("Sudoku", "For each region, find three values confined to exactly three cells.")}; case PointingPair: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescPointingPairWhatItIs), - .what_to_look_for = loc.getString(TechDescPointingPairWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", "A candidate in a box is confined to a single row or column. That candidate can be " + "eliminated from the rest of that row or column outside the box."), + .what_to_look_for = core::loc( + "Sudoku", "In each box, check if a candidate appears only in one row or one column.")}; case BoxLineReduction: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescBoxLineReductionWhatItIs), - .what_to_look_for = loc.getString(TechDescBoxLineReductionWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", "A candidate in a row or column is confined to a single box. That candidate " + "can be eliminated from the rest of the box outside that row or column."), + .what_to_look_for = + core::loc("Sudoku", "In each row/column, check if a candidate appears only within one box.")}; case NakedQuad: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescNakedQuadWhatItIs), - .what_to_look_for = loc.getString(TechDescNakedQuadWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = + core::loc("Sudoku", "Four cells in a region collectively contain exactly four candidates. " + "Those values can be eliminated from other cells in the region."), + .what_to_look_for = core::loc( + "Sudoku", + "Find four cells in a region whose combined candidates form a set of exactly four values.")}; case HiddenQuad: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescHiddenQuadWhatItIs), - .what_to_look_for = loc.getString(TechDescHiddenQuadWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = + core::loc("Sudoku", "Four values in a region appear as candidates in exactly four cells. Other " + "candidates in those cells can be eliminated."), + .what_to_look_for = + core::loc("Sudoku", "For each region, find four values confined to exactly four cells.")}; case XWing: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescXWingWhatItIs), - .what_to_look_for = loc.getString(TechDescXWingWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", + "A candidate appears in exactly two cells in each of two rows, and those cells are in the same " + "two columns. The candidate can be eliminated from other cells in those columns."), + .what_to_look_for = core::loc( + "Sudoku", "Find a candidate forming a rectangle pattern: two rows, two columns, four cells.")}; case XYWing: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescXYWingWhatItIs), - .what_to_look_for = loc.getString(TechDescXYWingWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", "A pivot cell with candidates {A,B} sees two wing cells: one with {A,C} and one with " + "{B,C}. Value C can be eliminated from any cell that sees both wings."), + .what_to_look_for = core::loc( + "Sudoku", + "Find a bivalue cell (pivot) that sees two other bivalue cells sharing one candidate each.")}; case Swordfish: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescSwordfishWhatItIs), - .what_to_look_for = loc.getString(TechDescSwordfishWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", + "A candidate appears in 2-3 cells in each of three rows, and those cells fall in exactly three " + "columns. The candidate can be eliminated from other cells in those columns."), + .what_to_look_for = + core::loc("Sudoku", "Extend the X-Wing pattern to three rows and three columns.")}; case Skyscraper: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescSkyscraperWhatItIs), - .what_to_look_for = loc.getString(TechDescSkyscraperWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", "Two conjugate pairs for a digit share one endpoint in the same row or column. The " + "digit can be eliminated from cells that see both non-shared endpoints."), + .what_to_look_for = + core::loc("Sudoku", "Find two rows (or columns) each with exactly two cells for a digit, " + "sharing one column (or row).")}; case TwoStringKite: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescTwoStringKiteWhatItIs), - .what_to_look_for = loc.getString(TechDescTwoStringKiteWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", + "A conjugate pair in a row and a conjugate pair in a column are connected through a box. The " + "digit can be eliminated from the cell that sees both unconnected endpoints."), + .what_to_look_for = core::loc( + "Sudoku", "Find a row pair and column pair for the same digit connected via a shared box.")}; case XYZWing: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescXYZWingWhatItIs), - .what_to_look_for = loc.getString(TechDescXYZWingWhatToLookFor)}; + return { + .title = getLocalizedTechniqueName(technique), + .what_it_is = + core::loc("Sudoku", "A pivot cell with candidates {A,B,C} sees a wing with {A,B} and a wing with " + "{A,C}. Value A can be eliminated from cells that see all three cells."), + .what_to_look_for = core::loc( + "Sudoku", + "Find a trivalue pivot seeing two bivalue wings that each share two candidates with the pivot.")}; case UniqueRectangle: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescUniqueRectangleWhatItIs), - .what_to_look_for = loc.getString(TechDescUniqueRectangleWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = + core::loc("Sudoku", "Four cells forming a rectangle across two boxes would create a deadly " + "pattern (two solutions) if they all had the same two candidates. Extra " + "candidates in some cells can force eliminations to avoid this ambiguity."), + .what_to_look_for = core::loc( + "Sudoku", "Find four cells in a rectangle across two boxes sharing the same two candidates.")}; case WWing: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescWWingWhatItIs), - .what_to_look_for = loc.getString(TechDescWWingWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", "Two cells with the same pair of candidates {A,B} are connected by a strong link on " + "value A. Value B can be eliminated from cells that see both endpoints."), + .what_to_look_for = core::loc( + "Sudoku", + "Find two identical bivalue cells connected by a conjugate pair on one of their values.")}; case SimpleColoring: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescSimpleColoringWhatItIs), - .what_to_look_for = loc.getString(TechDescSimpleColoringWhatToLookFor)}; + return { + .title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", + "For a single digit, build chains of conjugate pairs and assign two colors. If both colors " + "appear in the same region, one color is false and its candidates are eliminated."), + .what_to_look_for = core::loc( + "Sudoku", "Pick a digit, trace conjugate pairs, color alternately. Check for color conflicts.")}; case FinnedXWing: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescFinnedXWingWhatItIs), - .what_to_look_for = loc.getString(TechDescFinnedXWingWhatToLookFor)}; + return { + .title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", "An X-Wing pattern with one extra candidate cell (the fin) in the same box as a corner. " + "Eliminations are restricted to cells that see both the fin and the X-Wing column."), + .what_to_look_for = + core::loc("Sudoku", "Find an X-Wing where one row has an extra candidate cell in the same box.")}; case RemotePairs: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescRemotePairsWhatItIs), - .what_to_look_for = loc.getString(TechDescRemotePairsWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", + "A chain of bivalue cells all containing the same pair {A,B}, where each adjacent pair shares " + "a region. Cells seeing both endpoints of an even-length chain lose both values."), + .what_to_look_for = core::loc( + "Sudoku", "Find a chain of identical bivalue cells connected through shared regions.")}; case BUG: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescBUGWhatItIs), - .what_to_look_for = loc.getString(TechDescBUGWhatToLookFor)}; + return { + .title = getLocalizedTechniqueName(technique), + .what_it_is = + core::loc("Sudoku", "If all unsolved cells have exactly two candidates except one cell with " + "three, the puzzle would have multiple solutions unless the trivalue cell " + "is set to the value that appears three times in its row, column, or box."), + .what_to_look_for = core::loc( + "Sudoku", "Check if only one cell has more than two candidates. If so, find its odd-count value.")}; case Jellyfish: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescJellyfishWhatItIs), - .what_to_look_for = loc.getString(TechDescJellyfishWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", + "A candidate appears in 2-4 cells in each of four rows, and those cells fall in exactly four " + "columns. The candidate can be eliminated from other cells in those columns."), + .what_to_look_for = + core::loc("Sudoku", "Extend the Swordfish pattern to four rows and four columns.")}; case FinnedSwordfish: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescFinnedSwordfishWhatItIs), - .what_to_look_for = loc.getString(TechDescFinnedSwordfishWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", "A Swordfish pattern with extra candidate cells (fins) in the same box. Eliminations " + "are restricted to cells seeing both the fin box and the Swordfish columns."), + .what_to_look_for = + core::loc("Sudoku", "Find a Swordfish where one row has extra candidates in the same box.")}; case EmptyRectangle: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescEmptyRectangleWhatItIs), - .what_to_look_for = loc.getString(TechDescEmptyRectangleWhatToLookFor)}; + return { + .title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", + "A digit's candidates in a box form an L-shape or cross, leaving an empty rectangle. Combined with " + "a conjugate pair outside the box, this eliminates the digit from a target cell."), + .what_to_look_for = core::loc( + "Sudoku", + "Find a box where a digit's candidates leave an empty rectangle, connected to a conjugate pair.")}; case WXYZWing: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescWXYZWingWhatItIs), - .what_to_look_for = loc.getString(TechDescWXYZWingWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", + "A four-cell wing pattern: a pivot and three wings collectively contain four candidates, and a " + "shared candidate Z can be eliminated from cells seeing all cells containing Z."), + .what_to_look_for = core::loc( + "Sudoku", + "Find a group of four cells with exactly four combined candidates sharing a common value.")}; case FinnedJellyfish: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescFinnedJellyfishWhatItIs), - .what_to_look_for = loc.getString(TechDescFinnedJellyfishWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", "A Jellyfish pattern with extra fin cells in the same box. Eliminations are " + "restricted to cells seeing both the fin box and the Jellyfish columns."), + .what_to_look_for = + core::loc("Sudoku", "Find a Jellyfish where one row has extra candidates forming a fin.")}; case XYChain: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescXYChainWhatItIs), - .what_to_look_for = loc.getString(TechDescXYChainWhatToLookFor)}; + return { + .title = getLocalizedTechniqueName(technique), + .what_it_is = + core::loc("Sudoku", "A chain of bivalue cells where consecutive cells share a candidate value, " + "alternating between the two candidates. The value shared by the chain's " + "endpoints can be eliminated from cells that see both endpoints."), + .what_to_look_for = core::loc( + "Sudoku", "Build a chain of bivalue cells connected by shared candidates. Check the endpoints.")}; case MultiColoring: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescMultiColoringWhatItIs), - .what_to_look_for = loc.getString(TechDescMultiColoringWhatToLookFor)}; + return { + .title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", "Build separate conjugate pair chains (clusters) for a digit and color each. When " + "two clusters interact (cells in different clusters see each other), eliminations " + "can be made from cells that see conflicting colors across clusters."), + .what_to_look_for = core::loc( + "Sudoku", "Color multiple conjugate chains for one digit, then check cross-cluster interactions.")}; case ALSxZ: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescALSxZWhatItIs), - .what_to_look_for = loc.getString(TechDescALSxZWhatToLookFor)}; + return { + .title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", + "Two Almost Locked Sets (each has N cells with N+1 candidates) share a restricted common candidate " + "X. A second common candidate Z can be eliminated from cells that see all Z-cells in both sets."), + .what_to_look_for = core::loc( + "Sudoku", + "Find two groups of cells that are almost locked, sharing a restricted common candidate.")}; case SueDeCoq: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescSueDeCoqWhatItIs), - .what_to_look_for = loc.getString(TechDescSueDeCoqWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", + "An intersection of a line and box contains 2-3 cells whose candidates can be covered by two " + "Almost Locked Sets (one from the line remainder, one from the box remainder). Extra ALS " + "candidates can be eliminated from their respective remainders."), + .what_to_look_for = core::loc( + "Sudoku", "Find an intersection where candidates can be partitioned into two covering ALS.")}; case ForcingChain: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescForcingChainWhatItIs), - .what_to_look_for = loc.getString(TechDescForcingChainWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = + core::loc("Sudoku", "For a cell with 2-3 candidates, assume each candidate is true and " + "propagate the consequences. If all assumptions lead to the same " + "conclusion (a placement or elimination), that conclusion must be true."), + .what_to_look_for = core::loc( + "Sudoku", + "Pick a cell with few candidates. Try each value and propagate. Look for common outcomes.")}; case NiceLoop: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescNiceLoopWhatItIs), - .what_to_look_for = loc.getString(TechDescNiceLoopWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = + core::loc("Sudoku", "Build a chain of alternating strong and weak links between (cell, digit) " + "pairs. If the chain forms a loop or its endpoints share a digit, " + "eliminations can be derived from the alternating inference chain rules."), + .what_to_look_for = core::loc( + "Sudoku", + "Trace alternating strong/weak links. Check if endpoints share a digit for eliminations.")}; case XCycles: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescXCyclesWhatItIs), - .what_to_look_for = loc.getString(TechDescXCyclesWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", + "For a single digit, build a chain of alternating strong and weak links. Type 1 (continuous " + "loop) eliminates the digit from cells seeing weak link endpoints. Type 2 places the digit at " + "a strong-strong discontinuity. Type 3 eliminates at a weak-weak discontinuity."), + .what_to_look_for = core::loc( + "Sudoku", + "For each digit, trace alternating strong/weak links and look for cycles or discontinuities.")}; case ThreeDMedusa: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescThreeDMedusaWhatItIs), - .what_to_look_for = loc.getString(TechDescThreeDMedusaWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", "Multi-digit coloring: build a graph of (cell, digit) pairs connected by strong " + "links (conjugate pairs and bivalue cells). Color with two alternating colors. Apply " + "six rules to find contradictions or trap eliminations."), + .what_to_look_for = core::loc( + "Sudoku", "Extend single-digit coloring to multiple digits via bivalue cell connections.")}; case HiddenUniqueRectangle: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescHiddenUniqueRectangleWhatItIs), - .what_to_look_for = loc.getString(TechDescHiddenUniqueRectangleWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = + core::loc("Sudoku", "A deadly rectangle pattern where one or more corners have the UR values " + "hidden among other candidates. Strong links on UR values in shared units " + "force eliminations to avoid the deadly pattern."), + .what_to_look_for = core::loc( + "Sudoku", + "Find a rectangle across two boxes where the UR values are present but hidden by extras.")}; case AvoidableRectangle: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescAvoidableRectangleWhatItIs), - .what_to_look_for = loc.getString(TechDescAvoidableRectangleWhatToLookFor)}; + return { + .title = getLocalizedTechniqueName(technique), + .what_it_is = + core::loc("Sudoku", "Like Unique Rectangle but using the distinction between given clues and " + "solver-placed values. If three solver-placed corners of a rectangle have " + "values {A,B}, the fourth unsolved corner cannot complete the deadly pattern."), + .what_to_look_for = core::loc( + "Sudoku", "Find rectangles where three corners are solver-placed (not givens) with two values.")}; case ALSXYWing: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescALSXYWingWhatItIs), - .what_to_look_for = loc.getString(TechDescALSXYWingWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = + core::loc("Sudoku", "Three non-overlapping Almost Locked Sets linked by restricted commons: " + "A-B linked by X, B-C linked by Y (Y != X). Common value Z in both A and C " + "can be eliminated from cells seeing all Z-cells in both A and C."), + .what_to_look_for = + core::loc("Sudoku", "Find three ALSs forming a chain with two restricted common candidates.")}; case DeathBlossom: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescDeathBlossomWhatItIs), - .what_to_look_for = loc.getString(TechDescDeathBlossomWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = + core::loc("Sudoku", "A stem cell with 2-3 candidates, each linked to a petal ALS via " + "restricted common. A value Z common across all petals (but not in the " + "stem) can be eliminated from cells seeing all Z-cells in all petals."), + .what_to_look_for = core::loc( + "Sudoku", "Find a cell whose candidates each connect to an ALS via restricted common.")}; case VWXYZWing: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescVWXYZWingWhatItIs), - .what_to_look_for = loc.getString(TechDescVWXYZWingWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", + "A five-cell wing pattern: a pivot and four wings collectively contain five candidates. The " + "non-restricted shared value Z can be eliminated from cells seeing all Z-cells."), + .what_to_look_for = core::loc( + "Sudoku", + "Find five cells with exactly five combined candidates and a restricted elimination value.")}; case FrankenFish: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescFrankenFishWhatItIs), - .what_to_look_for = loc.getString(TechDescFrankenFishWhatToLookFor)}; + return { + .title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", + "A fish pattern (X-Wing/Swordfish/Jellyfish) where base and cover sets are mixed rows/columns and " + "boxes. At least one base set must be a box. Eliminates from cover cells outside the base."), + .what_to_look_for = + core::loc("Sudoku", "Look for fish patterns that include boxes as base or cover sets.")}; case GroupedXCycles: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescGroupedXCyclesWhatItIs), - .what_to_look_for = loc.getString(TechDescGroupedXCyclesWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", "Extends X-Cycles by allowing grouped nodes: 2-3 cells in the same box on the same " + "row or column that all have a candidate digit. Same Type 1/2/3 rules apply."), + .what_to_look_for = core::loc( + "Sudoku", "Build X-Cycle chains using grouped box nodes alongside individual cells.")}; case SashimiXWing: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescSashimiXWingWhatItIs), - .what_to_look_for = loc.getString(TechDescSashimiXWingWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", + "A fish pattern where one base row has only one candidate position instead of two. The missing " + "position is compensated by a fin cell, restricting eliminations to the fin's box."), + .what_to_look_for = core::loc( + "Sudoku", "Look for an X-Wing-like pattern where one row is incomplete — it only has the " + "candidate in one of the two expected columns, plus an extra fin cell.")}; case SashimiSwordfish: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescSashimiSwordfishWhatItIs), - .what_to_look_for = loc.getString(TechDescSashimiSwordfishWhatToLookFor)}; + return { + .title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", "A 3-row fish pattern where at least one base row has fewer candidate positions than " + "expected. The missing position creates a fin that restricts eliminations."), + .what_to_look_for = core::loc( + "Sudoku", "Find a Swordfish shape where one row only covers 1 of the 3 base columns, plus a fin.")}; case SashimiJellyfish: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescSashimiJellyfishWhatItIs), - .what_to_look_for = loc.getString(TechDescSashimiJellyfishWhatToLookFor)}; + return { + .title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", "A 4-row fish pattern where at least one base row has fewer candidate positions than " + "expected. The missing position creates a fin restricting eliminations."), + .what_to_look_for = core::loc( + "Sudoku", "Find a Jellyfish shape where one row only covers 1 of the 4 base columns, plus a fin.")}; case UnitForcingChain: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescUnitForcingChainWhatItIs), - .what_to_look_for = loc.getString(TechDescUnitForcingChainWhatToLookFor)}; + return { + .title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", "For a digit in a unit with 2-3 positions, assume the digit goes in each position and " + "propagate. If all branches lead to the same conclusion, that conclusion is true."), + .what_to_look_for = + core::loc("Sudoku", "Find a unit where a digit appears in few cells, then try each placement.")}; case RegionForcingChain: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescRegionForcingChainWhatItIs), - .what_to_look_for = loc.getString(TechDescRegionForcingChainWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", "For a digit in a box with 2-3 positions, assume the digit goes in each position and " + "propagate. If all branches lead to the same conclusion, that conclusion is true."), + .what_to_look_for = + core::loc("Sudoku", "Find a box where a digit appears in few cells, then try each placement.")}; case MutantFish: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescMutantFishWhatItIs), - .what_to_look_for = loc.getString(TechDescMutantFishWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", "A fish pattern where BOTH base and cover sets freely mix rows, columns, and boxes. " + "Unlike Franken Fish (one mixed side), Mutant Fish requires both sides to contain at " + "least 2 different unit types. Eliminates from cover cells outside the base."), + .what_to_look_for = core::loc( + "Sudoku", + "Look for fish patterns where both the base set and cover set mix rows, columns, and boxes.")}; case KrakenFish: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescKrakenFishWhatItIs), - .what_to_look_for = loc.getString(TechDescKrakenFishWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", + "Extends finned fish by using chain propagation to verify eliminations outside the fin's " + "box. For each candidate that a standard finned fish would reject (outside the fin's " + "box), place the digit at the fin cell and propagate. If the target still loses the " + "candidate, the elimination is valid regardless of whether the fin is true or false."), + .what_to_look_for = + core::loc("Sudoku", "Find a finned fish pattern, then check if chain propagation from the " + "fin cell eliminates the digit from cells outside the fin's box.")}; case ALSChain: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescALSChainWhatItIs), - .what_to_look_for = loc.getString(TechDescALSChainWhatToLookFor)}; + return { + .title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc("Sudoku", "A generalized chain of 4-6 Almost Locked Sets linked by distinct " + "restricted commons. Values common across the chain endpoints can be " + "eliminated from cells that see all relevant ALS members."), + .what_to_look_for = + core::loc("Sudoku", "Find a chain of ALSs where each adjacent pair shares a restricted common.")}; case UniqueLoop: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescUniqueLoopWhatItIs), - .what_to_look_for = loc.getString(TechDescUniqueLoopWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", + "A deadly pattern where 4-6 cells form a loop, each consecutive pair sharing a unit " + "(row, column, or box). All cells contain the same candidate pair {A,B}. If all cells " + "had only {A,B}, two solutions would exist. Cells with extra candidates must keep them."), + .what_to_look_for = + core::loc("Sudoku", "Find a loop of 4-6 cells across at least 2 boxes where each cell has " + "candidates {A,B} and each consecutive pair shares a row, column, or " + "box. If exactly one cell has extras, eliminate A and B from it.")}; case JuniorExocet: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescJuniorExocetWhatItIs), - .what_to_look_for = loc.getString(TechDescJuniorExocetWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = + core::loc("Sudoku", "A base pair of cells in a box whose candidates must appear in specific " + "target cells in other boxes along cross-lines. Candidates not matching " + "the base pair pattern can be eliminated from the target cells."), + .what_to_look_for = core::loc( + "Sudoku", "Find a base pair in a box with target cells in aligned boxes along cross-lines.")}; case ContinuousNiceLoop: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescContinuousNiceLoopWhatItIs), - .what_to_look_for = loc.getString(TechDescContinuousNiceLoopWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", "A continuous alternating inference chain (AIC) that forms a complete loop. Every " + "weak link in the loop produces eliminations: the digit can be removed from any cell " + "outside the loop that sees both endpoints of a weak link."), + .what_to_look_for = core::loc( + "Sudoku", + "Build an AIC where the chain closes back on itself with consistent alternating links. Every " + "weak link segment yields eliminations from external cells seeing both endpoints.")}; case GroupedNiceLoop: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescGroupedNiceLoopWhatItIs), - .what_to_look_for = loc.getString(TechDescGroupedNiceLoopWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc( + "Sudoku", + "Extends Nice Loop (AIC) with grouped nodes: 2-3 cells in the same box on the same row or " + "column that all share a candidate digit. Combines multi-digit cell-based links (bivalue " + "strong links) with single-digit grouped unit links for stronger chains."), + .what_to_look_for = core::loc( + "Sudoku", "Build AIC chains using grouped box nodes alongside individual cells. Look for " + "discontinuous Type 2 chains where both endpoints assert the same digit.")}; case Backtracking: - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescBacktrackingWhatItIs), - .what_to_look_for = loc.getString(TechDescBacktrackingWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = + core::loc("Sudoku", "A brute-force trial-and-error method. Not a logical technique — used as a " + "fallback when no logical strategy can make progress."), + .what_to_look_for = core::loc("Sudoku", "This technique is not used in training exercises.")}; } - return {.title = getLocalizedTechniqueName(loc, technique), - .what_it_is = loc.getString(TechDescUnknownWhatItIs), - .what_to_look_for = loc.getString(TechDescUnknownWhatToLookFor)}; + return {.title = getLocalizedTechniqueName(technique), + .what_it_is = core::loc("Sudoku", "Unknown technique."), + .what_to_look_for = core::loc("Sudoku", "No identification tips available.")}; } } // namespace sudoku::core diff --git a/src/core/training_hints.cpp b/src/core/training_hints.cpp index eda042e..b0bbf2c 100644 --- a/src/core/training_hints.cpp +++ b/src/core/training_hints.cpp @@ -16,27 +16,14 @@ #include "training_hints.h" +#include "core/i18n_helpers.h" #include "localized_explanations.h" -#include "string_keys.h" #include -#include #include -#include - namespace sudoku::core { -namespace { - -/// Helper to format a localized string from a key with arguments -template -[[nodiscard]] std::string locHint(const ILocalizationManager& loc, std::string_view key, Args&&... args) { - return fmt::format(fmt::runtime(loc.getString(key)), std::forward(args)...); -} - -} // namespace - void appendDataHighlights(TrainingHint& hint, const ExplanationData& data, CellRole default_role) { for (size_t i = 0; i < data.positions.size(); ++i) { auto role = (i < data.position_roles.size()) ? data.position_roles[i] : default_role; @@ -51,8 +38,7 @@ void appendEliminationHighlights(TrainingHint& hint, const SolveStep& expected) } // NOLINTNEXTLINE(readability-function-cognitive-complexity,readability-function-size) — per-category hint dispatch with 3 levels each; inherent branching -TrainingHint getTrainingHint(const ILocalizationManager& loc, SolvingTechnique technique, int level, - const SolveStep& expected) { +TrainingHint getTrainingHint(SolvingTechnique technique, int level, const SolveStep& expected) { auto category = getTechniqueCategory(technique); const auto& data = expected.explanation_data; @@ -61,17 +47,18 @@ TrainingHint getTrainingHint(const ILocalizationManager& loc, SolvingTechnique t switch (category) { case TechniqueCategory::Singles: { if (level == 1) { - hint.text = locHint(loc, StringKeys::HintSinglesL1, localizedPosition(loc, expected.position)); + hint.text = + core::locFormat(core::loc("Sudoku", "Look at cell {0}."), localizedPosition(expected.position)); } else if (level == 2) { if (data.region_type != RegionType::None) { - hint.text = locHint(loc, StringKeys::HintSinglesL2Region, - localizedRegion(loc, data.region_type, data.region_index)); + hint.text = core::locFormat(core::loc("Sudoku", "Focus on {0} — count the candidates."), + localizedRegion(data.region_type, data.region_index)); } else { - hint.text = - locHint(loc, StringKeys::HintSinglesL2NoRegion, localizedPosition(loc, expected.position)); + hint.text = core::locFormat(core::loc("Sudoku", "Count the candidates in cell {0}."), + localizedPosition(expected.position)); } } else { - hint.text = locHint(loc, StringKeys::HintSinglesL3, expected.value); + hint.text = core::locFormat(core::loc("Sudoku", "The value is {0}."), expected.value); } hint.highlights = {{.position = expected.position, .role = CellRole::Pattern}}; break; @@ -80,20 +67,27 @@ TrainingHint getTrainingHint(const ILocalizationManager& loc, SolvingTechnique t case TechniqueCategory::Subsets: { if (level == 1) { if (data.region_type != RegionType::None) { - hint.text = locHint(loc, StringKeys::HintSubsetsL1Region, - localizedRegion(loc, data.region_type, data.region_index)); + hint.text = core::locFormat(core::loc("Sudoku", "Focus on {0}."), + localizedRegion(data.region_type, data.region_index)); } else { - hint.text = std::string(loc.getString(StringKeys::HintSubsetsL1NoRegion)); + hint.text = + std::string(core::loc("Sudoku", "Look for cells that share the same candidates in a unit.")); } } else if (level == 2) { if (!data.values.empty()) { - hint.text = locHint(loc, StringKeys::HintSubsetsL2Values, formatValueList(data.values)); + hint.text = core::locFormat( + core::loc("Sudoku", "These cells form a [{0}] subset. Values in the subset can only go in " + "these cells — eliminate them from other cells in the region."), + formatValueList(data.values)); } else { - hint.text = std::string(loc.getString(StringKeys::HintSubsetsL2NoValues)); + hint.text = std::string( + core::loc("Sudoku", "These cells form the subset. Values in the subset can only go in these " + "cells — eliminate them from other cells in the region.")); } appendDataHighlights(hint, data, CellRole::Pattern); } else { - hint.text = std::string(loc.getString(StringKeys::HintSubsetsL3)); + hint.text = + std::string(core::loc("Sudoku", "Eliminate candidates from cells that see all subset cells.")); appendEliminationHighlights(hint, expected); } break; @@ -102,15 +96,20 @@ TrainingHint getTrainingHint(const ILocalizationManager& loc, SolvingTechnique t case TechniqueCategory::Intersections: { if (level == 1) { if (!data.values.empty()) { - hint.text = locHint(loc, StringKeys::HintIntersectionsL1Value, data.values[0]); + hint.text = core::locFormat(core::loc("Sudoku", "Look for value {0} confined to an intersection."), + data.values[0]); } else { - hint.text = std::string(loc.getString(StringKeys::HintIntersectionsL1NoValue)); + hint.text = std::string( + core::loc("Sudoku", "Look for a candidate confined to the intersection of a box and a line.")); } } else if (level == 2) { - hint.text = std::string(loc.getString(StringKeys::HintIntersectionsL2)); + hint.text = std::string(core::loc( + "Sudoku", "The intersection cells. The candidate is confined to these cells — eliminate it from " + "other cells in the line or box outside this intersection.")); appendDataHighlights(hint, data, CellRole::Pattern); } else { - hint.text = std::string(loc.getString(StringKeys::HintIntersectionsL3)); + hint.text = + std::string(core::loc("Sudoku", "Eliminate the candidate from cells outside the intersection.")); appendEliminationHighlights(hint, expected); } break; @@ -119,15 +118,21 @@ TrainingHint getTrainingHint(const ILocalizationManager& loc, SolvingTechnique t case TechniqueCategory::Fish: { if (level == 1) { if (!data.values.empty()) { - hint.text = locHint(loc, StringKeys::HintFishL1Value, data.values[0]); + hint.text = + core::locFormat(core::loc("Sudoku", "Look for a fish pattern on value {0}."), data.values[0]); } else { - hint.text = std::string(loc.getString(StringKeys::HintFishL1NoValue)); + hint.text = std::string(core::loc( + "Sudoku", "Look for a fish pattern (rows/columns with restricted candidate positions).")); } } else if (level == 2) { - hint.text = std::string(loc.getString(StringKeys::HintFishL2)); + hint.text = std::string( + core::loc("Sudoku", "Base and cover sets. Blue cells are the base set (rows/columns where the " + "candidate is restricted). Green cells are the cover set. Eliminate the " + "candidate from cover set cells that aren't in the base set.")); appendDataHighlights(hint, data, CellRole::Pattern); } else { - hint.text = std::string(loc.getString(StringKeys::HintFishL3)); + hint.text = std::string( + core::loc("Sudoku", "Eliminate the candidate from cover set cells outside the base set.")); appendEliminationHighlights(hint, expected); } break; @@ -143,13 +148,17 @@ TrainingHint getTrainingHint(const ILocalizationManager& loc, SolvingTechnique t break; } } - hint.text = locHint(loc, StringKeys::HintWingsL1, localizedPosition(loc, pivot)); + hint.text = + core::locFormat(core::loc("Sudoku", "Find the pivot cell at {0}."), localizedPosition(pivot)); hint.highlights = {{.position = pivot, .role = CellRole::Pivot}}; } else if (level == 2) { - hint.text = std::string(loc.getString(StringKeys::HintWingsL2)); + hint.text = std::string(core::loc( + "Sudoku", "Pivot and wing cells. The orange pivot connects to the green wings. Candidates shared " + "by both wings can be eliminated from cells that see all wing endpoints.")); appendDataHighlights(hint, data, CellRole::Wing); } else { - hint.text = std::string(loc.getString(StringKeys::HintWingsL3)); + hint.text = std::string( + core::loc("Sudoku", "Eliminate the shared candidate from cells that see all wing endpoints.")); appendEliminationHighlights(hint, expected); } break; @@ -158,15 +167,20 @@ TrainingHint getTrainingHint(const ILocalizationManager& loc, SolvingTechnique t case TechniqueCategory::SingleDigit: { if (level == 1) { if (!data.values.empty()) { - hint.text = locHint(loc, StringKeys::HintSingleDigitL1Value, data.values[0]); + hint.text = + core::locFormat(core::loc("Sudoku", "Look for conjugate pairs on value {0}."), data.values[0]); } else { - hint.text = std::string(loc.getString(StringKeys::HintSingleDigitL1NoValue)); + hint.text = std::string(core::loc( + "Sudoku", "Look for conjugate pairs (cells where a digit appears exactly twice in a unit).")); } } else if (level == 2) { - hint.text = std::string(loc.getString(StringKeys::HintSingleDigitL2)); + hint.text = std::string( + core::loc("Sudoku", "The chain cells. These cells form conjugate pairs (a digit appears exactly " + "twice in a unit). Follow the alternating pattern to find eliminations.")); appendDataHighlights(hint, data, CellRole::ChainA); } else { - hint.text = std::string(loc.getString(StringKeys::HintSingleDigitL3)); + hint.text = + std::string(core::loc("Sudoku", "Cells that see both endpoints of the pattern can be eliminated.")); appendEliminationHighlights(hint, expected); } break; @@ -175,15 +189,20 @@ TrainingHint getTrainingHint(const ILocalizationManager& loc, SolvingTechnique t case TechniqueCategory::Coloring: { if (level == 1) { if (!data.values.empty()) { - hint.text = locHint(loc, StringKeys::HintColoringL1Value, data.values[0]); + hint.text = + core::locFormat(core::loc("Sudoku", "Build a coloring chain on value {0}."), data.values[0]); } else { - hint.text = std::string(loc.getString(StringKeys::HintColoringL1NoValue)); + hint.text = + std::string(core::loc("Sudoku", "Start coloring conjugate pairs with two alternating colors.")); } } else if (level == 2) { - hint.text = std::string(loc.getString(StringKeys::HintColoringL2)); + hint.text = std::string(core::loc( + "Sudoku", "The coloring chain. Blue and green are two alternating colors — one must be true, one " + "false. Cells that see both colors can have the candidate eliminated.")); appendDataHighlights(hint, data, CellRole::ChainA); } else { - hint.text = std::string(loc.getString(StringKeys::HintColoringL3)); + hint.text = std::string( + core::loc("Sudoku", "One color must be false — eliminate from cells that see both colors.")); appendEliminationHighlights(hint, expected); } break; @@ -191,12 +210,17 @@ TrainingHint getTrainingHint(const ILocalizationManager& loc, SolvingTechnique t case TechniqueCategory::UniqueRect: { if (level == 1) { - hint.text = std::string(loc.getString(StringKeys::HintUniqueRectL1)); + hint.text = std::string(core::loc( + "Sudoku", "Look for a deadly pattern — four cells forming a rectangle across two boxes.")); } else if (level == 2) { - hint.text = std::string(loc.getString(StringKeys::HintUniqueRectL2)); + hint.text = std::string(core::loc( + "Sudoku", + "The rectangle corners. These four cells across two boxes form a potential deadly pattern. To keep " + "the puzzle unique, eliminate the candidate that would complete the rectangle.")); appendDataHighlights(hint, data, CellRole::Roof); } else { - hint.text = std::string(loc.getString(StringKeys::HintUniqueRectL3)); + hint.text = std::string(core::loc( + "Sudoku", "To avoid the deadly pattern, eliminate the candidate that would complete it.")); appendEliminationHighlights(hint, expected); } break; @@ -205,21 +229,26 @@ TrainingHint getTrainingHint(const ILocalizationManager& loc, SolvingTechnique t case TechniqueCategory::Chains: { if (level == 1) { if (!data.positions.empty()) { - hint.text = locHint(loc, StringKeys::HintChainsL1Pos, localizedPosition(loc, data.positions[0])); + hint.text = core::locFormat(core::loc("Sudoku", "Start the chain from cell {0}."), + localizedPosition(data.positions[0])); hint.highlights = {{.position = data.positions[0], .role = CellRole::ChainA}}; } else { - hint.text = std::string(loc.getString(StringKeys::HintChainsL1NoPos)); + hint.text = std::string( + core::loc("Sudoku", "Look for a chain of linked cells with alternating strong/weak links.")); } } else if (level == 2) { - hint.text = std::string(loc.getString(StringKeys::HintChainsL2)); + hint.text = std::string( + core::loc("Sudoku", "The chain path. Follow the alternating strong (blue) and weak (green) " + "links. The chain's logic forces a conclusion at the endpoints.")); appendDataHighlights(hint, data, CellRole::ChainA); } else { if (expected.type == SolveStepType::Placement) { - hint.text = locHint(loc, StringKeys::HintChainsL3Placement, expected.value, - localizedPosition(loc, expected.position)); + hint.text = core::locFormat(core::loc("Sudoku", "All chains lead to value {0} at {1}."), + expected.value, localizedPosition(expected.position)); hint.highlights = {{.position = expected.position, .role = CellRole::Pattern}}; } else { - hint.text = std::string(loc.getString(StringKeys::HintChainsL3Elimination)); + hint.text = + std::string(core::loc("Sudoku", "Eliminate candidates that contradict the chain logic.")); appendEliminationHighlights(hint, expected); } } @@ -228,12 +257,17 @@ TrainingHint getTrainingHint(const ILocalizationManager& loc, SolvingTechnique t case TechniqueCategory::SetLogic: { if (level == 1) { - hint.text = std::string(loc.getString(StringKeys::HintSetLogicL1)); + hint.text = std::string( + core::loc("Sudoku", "Look for an Almost Locked Set (a group of N cells with N+1 candidates).")); } else if (level == 2) { - hint.text = std::string(loc.getString(StringKeys::HintSetLogicL2)); + hint.text = std::string(core::loc( + "Sudoku", + "The ALS cells and restricted common. An ALS is N cells with N+1 candidates. The restricted common " + "candidate links the sets — eliminations apply to cells that see all relevant ALS members.")); appendDataHighlights(hint, data, CellRole::SetA); } else { - hint.text = std::string(loc.getString(StringKeys::HintSetLogicL3)); + hint.text = std::string( + core::loc("Sudoku", "Eliminate candidates from cells that see all relevant ALS members.")); appendEliminationHighlights(hint, expected); } break; @@ -241,10 +275,12 @@ TrainingHint getTrainingHint(const ILocalizationManager& loc, SolvingTechnique t case TechniqueCategory::Special: { if (level == 1) { - hint.text = std::string(loc.getString(StringKeys::HintSpecialL1)); + hint.text = std::string( + core::loc("Sudoku", "Look for the cell with three candidates (the only non-bivalue cell).")); } else if (level == 2) { if (!data.positions.empty()) { - hint.text = locHint(loc, StringKeys::HintSpecialL2, localizedPosition(loc, data.positions[0])); + hint.text = core::locFormat(core::loc("Sudoku", "The key cell is {0}."), + localizedPosition(data.positions[0])); hint.highlights = {{.position = data.positions[0], .role = CellRole::Pattern}}; } else { hint.text = expected.explanation; diff --git a/src/core/training_hints.h b/src/core/training_hints.h index bf9a98c..135bcf2 100644 --- a/src/core/training_hints.h +++ b/src/core/training_hints.h @@ -17,7 +17,6 @@ #pragma once #include "i_game_validator.h" -#include "i_localization_manager.h" #include "solve_step.h" #include "solving_technique.h" @@ -128,12 +127,10 @@ void appendDataHighlights(TrainingHint& hint, const ExplanationData& data, CellR void appendEliminationHighlights(TrainingHint& hint, const SolveStep& expected); /// Get a per-technique progressive hint for training exercises. -/// @param loc Localization manager for translating hint text /// @param technique The technique being practiced /// @param level Hint level (1-3) /// @param expected The expected solving step (contains positions, values, explanation data) /// @return TrainingHint with text and cells to highlight -[[nodiscard]] TrainingHint getTrainingHint(const ILocalizationManager& loc, SolvingTechnique technique, int level, - const SolveStep& expected); +[[nodiscard]] TrainingHint getTrainingHint(SolvingTechnique technique, int level, const SolveStep& expected); } // namespace sudoku::core diff --git a/src/main.cpp b/src/main.cpp index 9dc8240..fb13685 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,7 +17,6 @@ #include "core/di_container.h" #include "core/game_validator.h" #include "core/i_game_validator.h" -#include "core/i_localization_manager.h" #include "core/i_puzzle_generator.h" #include "core/i_puzzle_rater.h" #include "core/i_save_manager.h" @@ -27,7 +26,6 @@ #include "core/i_time_provider.h" #include "core/i_training_exercise_generator.h" #include "core/i_training_statistics_manager.h" -#include "core/localization_manager.h" #include "core/puzzle_generator.h" #include "core/puzzle_rater.h" #include "core/save_manager.h" @@ -44,7 +42,6 @@ #include #include #include -#include #include #include @@ -93,27 +90,6 @@ void setupDependencies() { container.registerSingleton( []() { return std::make_unique(); }); - container.registerSingleton([&container]() { - auto exe_dir = std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()); - // Try dev/Windows layout first (locales next to executable), then FHS layout - auto locales_dir = exe_dir / "locales"; - if (!std::filesystem::exists(locales_dir)) { - locales_dir = exe_dir / ".." / "share" / "sudoku" / "locales"; - } - auto manager = std::make_unique(locales_dir); - // Use language from settings if available - auto settings = container.resolve(); - auto locale = settings ? settings->getSettings().language : "en"; - auto result = manager->setLocale(locale); - if (!result) { - spdlog::warn("Failed to load locale '{}': {}", locale, result.error()); - if (locale != "en") { - [[maybe_unused]] auto fallback = manager->setLocale("en"); - } - } - return manager; - }); - container.registerSingleton( []() { return std::make_unique(); }); @@ -134,11 +110,10 @@ std::shared_ptr createViewModel() { auto solver = container.resolve(); auto stats_manager = container.resolve(); auto save_manager = container.resolve(); - auto loc_manager = container.resolve(); auto settings_manager = container.resolve(); return std::make_shared(validator, generator, solver, stats_manager, save_manager, - loc_manager, settings_manager); + settings_manager); } } // namespace @@ -152,8 +127,9 @@ int main(int argc, char* argv[]) { auto console_sink = std::make_shared(); auto exe_dir = std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()); auto log_path = exe_dir / "sudoku_debug.log"; - // In sandboxed environments (Flatpak), exe_dir is read-only; use data directory instead - if (!std::filesystem::exists(exe_dir / "locales")) { + // In sandboxed environments (Flatpak), exe_dir is read-only; use data directory instead. + // We detect by absence of writable bundled translations next to the executable. + if (!std::filesystem::exists(exe_dir / "translations")) { auto log_dir = sudoku::infrastructure::AppDirectoryManager::getDefaultDirectory( sudoku::infrastructure::DirectoryType::Logs); std::filesystem::create_directories(log_dir); @@ -170,19 +146,19 @@ int main(int argc, char* argv[]) { setupDependencies(); - auto view_model = createViewModel(); + // The Qt translator is owned by MainWindow and installed when + // setSettingsManager() is called below. This lets the user switch + // language at runtime via the Settings dialog. auto& container = sudoku::core::getContainer(); - auto loc_manager = container.resolve(); + auto view_model = createViewModel(); auto exercise_gen = container.resolve(); auto training_stats = container.resolve(); - auto training_vm = - std::make_shared(exercise_gen, loc_manager, training_stats); + auto training_vm = std::make_shared(exercise_gen, training_stats); auto settings_manager = container.resolve(); sudoku::view::MainWindow main_window; main_window.setViewModel(view_model); - main_window.setLocalizationManager(loc_manager); main_window.setSettingsManager(settings_manager); main_window.setTrainingViewModel(training_vm); diff --git a/src/view/main_window.cpp b/src/view/main_window.cpp index d2d7731..34071e3 100644 --- a/src/view/main_window.cpp +++ b/src/view/main_window.cpp @@ -16,10 +16,9 @@ #include "main_window.h" -#include "../core/string_keys.h" #include "core/constants.h" +#include "core/i18n_helpers.h" #include "core/i_game_validator.h" -#include "core/i_localization_manager.h" #include "core/observable.h" #include "infrastructure/app_directory_manager.h" #include "model/game_state.h" @@ -30,20 +29,24 @@ #include "view_model/game_view_model.h" #include "view_model/training_view_model.h" +#include #include #include #include #include #include +#include #include #include #include #include #include +#include #include #include #include +#include #include #include #include @@ -74,10 +77,8 @@ namespace sudoku::view { -using namespace core::StringKeys; - MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { - setWindowTitle(qstr(loc(AppTitle))); + setWindowTitle(qstr(core::loc("Sudoku", "Sudoku"))); resize(800, 900); // Newspaper-like background @@ -104,8 +105,8 @@ void MainWindow::setupCoachingPanel() { coaching_prev_btn_ = new QPushButton(QStringLiteral("\u25C0")); // ◀ coaching_next_btn_ = new QPushButton(QStringLiteral("\u25B6")); // ▶ - coaching_check_btn_ = new QPushButton(qstr(loc(CoachingButtonCheck))); - coaching_apply_btn_ = new QPushButton(qstr(loc(CoachingButtonApply))); + coaching_check_btn_ = new QPushButton(qstr(core::loc("Sudoku", "Check"))); + coaching_apply_btn_ = new QPushButton(qstr(core::loc("Sudoku", "Apply"))); coaching_dismiss_btn_ = new QPushButton(QStringLiteral("\u2715")); // ✕ static const auto COACHING_BTN_STYLE = @@ -182,13 +183,13 @@ void MainWindow::setupButtonPanel(QVBoxLayout* game_layout) { .arg(StyleColors::BTN_BG, StyleColors::BTN_TEXT, StyleColors::BTN_BORDER, StyleColors::BTN_DISABLED_TEXT, StyleColors::BTN_DISABLED_BG, StyleColors::PRIMARY, StyleColors::PRIMARY_DARK); - undo_btn_ = new QPushButton(qstr(loc(ButtonUndo))); - redo_btn_ = new QPushButton(qstr(loc(ButtonRedo))); - undo_valid_btn_ = new QPushButton(qstr(loc(ButtonUndoUntilValid))); - auto_notes_btn_ = new QPushButton(qstr(loc(ButtonFillNotes))); + undo_btn_ = new QPushButton(qstr(core::loc("Sudoku", "Undo"))); + redo_btn_ = new QPushButton(qstr(core::loc("Sudoku", "Redo"))); + undo_valid_btn_ = new QPushButton(qstr(core::loc("Sudoku", "Undo Until Valid"))); + auto_notes_btn_ = new QPushButton(qstr(core::loc("Sudoku", "Fill Notes"))); auto_notes_btn_->setCheckable(true); - mode_btn_ = new QPushButton(qstr(loc(ModeNormal))); - mode_btn_->setToolTip(qstr(loc(TooltipInputMode))); + mode_btn_ = new QPushButton(qstr(core::loc("Sudoku", "Normal"))); + mode_btn_->setToolTip(qstr(core::loc("Sudoku", "Input mode (Space to cycle, N for Notes)"))); undo_btn_->setStyleSheet(BTN_STYLE); redo_btn_->setStyleSheet(BTN_STYLE); @@ -268,94 +269,101 @@ void MainWindow::setupCentralWidget() { } void MainWindow::setupMenuBar() { - auto* game_menu = menuBar()->addMenu(QString("&%1").arg(qstr(loc(MenuGame)))); + auto* game_menu = menuBar()->addMenu(QString("&%1").arg(qstr(core::loc("Sudoku", "Game")))); - game_menu->addAction(QString("&%1").arg(qstr(loc(MenuNewGame))), QKeySequence("Ctrl+N"), this, + game_menu->addAction(QString("&%1").arg(qstr(core::loc("Sudoku", "New Game"))), QKeySequence("Ctrl+N"), this, &MainWindow::showNewGameDialog); - auto* reset_action = game_menu->addAction(QString("&%1").arg(qstr(loc(MenuResetPuzzle))), QKeySequence("Ctrl+R"), - this, &MainWindow::showResetDialog); + auto* reset_action = game_menu->addAction(QString("&%1").arg(qstr(core::loc("Sudoku", "Reset Puzzle"))), + QKeySequence("Ctrl+R"), this, &MainWindow::showResetDialog); reset_action->setEnabled(false); game_menu->addSeparator(); - game_menu->addAction(QString("&%1").arg(qstr(loc(MenuSave))), QKeySequence("Ctrl+S"), this, + game_menu->addAction(QString("&%1").arg(qstr(core::loc("Sudoku", "Save"))), QKeySequence("Ctrl+S"), this, &MainWindow::showSaveDialog); - game_menu->addAction(QString("&%1").arg(qstr(loc(MenuLoad))), QKeySequence("Ctrl+O"), this, + game_menu->addAction(QString("&%1").arg(qstr(core::loc("Sudoku", "Load"))), QKeySequence("Ctrl+O"), this, &MainWindow::showLoadDialog); game_menu->addSeparator(); - game_menu->addAction(QString("&%1").arg(qstr(loc(MenuTrainingMode))), this, + game_menu->addAction(QString("&%1").arg(qstr(core::loc("Sudoku", "Training Mode"))), this, [this]() { central_stack_->setCurrentIndex(1); }); - game_menu->addAction(QString("&%1").arg(qstr(loc(MenuAnalyzePosition))), QKeySequence("F2"), this, [this]() { - if (!view_model_ || !training_vm_) { - return; - } - auto result = view_model_->analyzePosition(); - if (!result.has_value()) { - if (toast_widget_) { - toast_widget_->show(qstr(loc(ToastNoStrategies))); + game_menu->addAction( + QString("&%1").arg(qstr(core::loc("Sudoku", "Analyze Position"))), QKeySequence("F2"), this, [this]() { + if (!view_model_ || !training_vm_) { + return; } - return; - } - training_vm_->analyzePosition(result->board, result->given_board, result->candidate_masks, - result->applicable_steps); - central_stack_->setCurrentIndex(1); - }); + auto result = view_model_->analyzePosition(); + if (!result.has_value()) { + if (toast_widget_) { + toast_widget_->show(qstr(core::loc("Sudoku", "No logical strategies found at this position."))); + } + return; + } + training_vm_->analyzePosition(result->board, result->given_board, result->candidate_masks, + result->applicable_steps); + central_stack_->setCurrentIndex(1); + }); - game_menu->addAction(QString("&%1").arg(qstr(loc(MenuResumeGame))), this, + game_menu->addAction(QString("&%1").arg(qstr(core::loc("Sudoku", "Resume Game"))), this, [this]() { central_stack_->setCurrentIndex(0); }); game_menu->addSeparator(); - game_menu->addAction(qstr(loc(MenuStatistics)), this, &MainWindow::showStatisticsDialog); - game_menu->addAction(qstr(loc(MenuExportAggregate)), this, &MainWindow::exportAggregateStatsCsv); - game_menu->addAction(qstr(loc(MenuExportSessions)), this, &MainWindow::exportGameSessionsCsv); + game_menu->addAction(qstr(core::loc("Sudoku", "Statistics")), this, &MainWindow::showStatisticsDialog); + game_menu->addAction(qstr(core::loc("Sudoku", "Export Aggregate Stats to CSV")), this, + &MainWindow::exportAggregateStatsCsv); + game_menu->addAction(qstr(core::loc("Sudoku", "Export Game Sessions to CSV")), this, + &MainWindow::exportGameSessionsCsv); game_menu->addSeparator(); - game_menu->addAction(qstr(loc(MenuSettings)), QKeySequence("Ctrl+,"), this, &MainWindow::showSettingsDialog); + game_menu->addAction(qstr(core::loc("Sudoku", "Settings...")), QKeySequence("Ctrl+,"), this, + &MainWindow::showSettingsDialog); game_menu->addSeparator(); - game_menu->addAction(QString("&%1").arg(qstr(loc(MenuExit))), QKeySequence("Alt+F4"), this, &QWidget::close); + game_menu->addAction(QString("&%1").arg(qstr(core::loc("Sudoku", "Exit"))), QKeySequence("Alt+F4"), this, + &QWidget::close); - auto* edit_menu = menuBar()->addMenu(QString("&%1").arg(qstr(loc(MenuEdit)))); - edit_menu->addAction(QString("&%1").arg(qstr(loc(MenuUndo))), QKeySequence("Ctrl+Z"), this, [this]() { + auto* edit_menu = menuBar()->addMenu(QString("&%1").arg(qstr(core::loc("Sudoku", "Edit")))); + edit_menu->addAction(QString("&%1").arg(qstr(core::loc("Sudoku", "Undo"))), QKeySequence("Ctrl+Z"), this, [this]() { if (view_model_) { view_model_->undo(); } }); - edit_menu->addAction(QString("&%1").arg(qstr(loc(MenuRedo))), QKeySequence("Ctrl+Y"), this, [this]() { + edit_menu->addAction(QString("&%1").arg(qstr(core::loc("Sudoku", "Redo"))), QKeySequence("Ctrl+Y"), this, [this]() { if (view_model_) { view_model_->redo(); } }); edit_menu->addSeparator(); - edit_menu->addAction(QString("&%1").arg(qstr(loc(MenuClearCell))), QKeySequence("Delete"), this, [this]() { - if (view_model_) { - auto pos = board_widget_->selectedCell(); - if (pos.has_value()) { - view_model_->clearCell(pos.value()); - } - } - }); - - auto* help_menu = menuBar()->addMenu(QString("&%1").arg(qstr(loc(MenuHelp)))); - help_menu->addAction(qstr(loc(MenuGetHint)), QKeySequence("H"), this, [this]() { + edit_menu->addAction(QString("&%1").arg(qstr(core::loc("Sudoku", "Clear Cell"))), QKeySequence("Delete"), this, + [this]() { + if (view_model_) { + auto pos = board_widget_->selectedCell(); + if (pos.has_value()) { + view_model_->clearCell(pos.value()); + } + } + }); + + auto* help_menu = menuBar()->addMenu(QString("&%1").arg(qstr(core::loc("Sudoku", "Help")))); + help_menu->addAction(qstr(core::loc("Sudoku", "Get Hint")), QKeySequence("H"), this, [this]() { if (view_model_ && view_model_->getHintCount() > 0) { view_model_->getHint(board_widget_->selectedCell()); } }); - help_menu->addAction(qstr(loc(MenuGetCoachingHint)), QKeySequence("Shift+H"), this, [this]() { + help_menu->addAction(qstr(core::loc("Sudoku", "Get Coaching Hint")), QKeySequence("Shift+H"), this, [this]() { if (view_model_) { view_model_->requestCoachingHint(); } }); help_menu->addSeparator(); - help_menu->addAction(QString("&%1").arg(qstr(loc(MenuAbout))), this, &MainWindow::showAboutDialog); - help_menu->addAction(qstr(loc(MenuThirdPartyLicenses)), this, &MainWindow::showThirdPartyLicensesDialog); + help_menu->addAction(QString("&%1").arg(qstr(core::loc("Sudoku", "About"))), this, &MainWindow::showAboutDialog); + help_menu->addAction(qstr(core::loc("Sudoku", "Third-Party Licenses")), this, + &MainWindow::showThirdPartyLicensesDialog); } void MainWindow::setupToolBar() { @@ -365,7 +373,7 @@ void MainWindow::setupToolBar() { "padding: 4px; spacing: 8px; }") .arg(StyleColors::SURFACE, StyleColors::DIVIDER)); - new_game_btn_ = new QPushButton(qstr(loc(ToolbarNewGame))); + new_game_btn_ = new QPushButton(qstr(core::loc("Sudoku", "▶ New Game"))); new_game_btn_->setStyleSheet( QString("QPushButton { background-color: %1; color: white; padding: 6px 16px; border-radius: 4px; }" "QPushButton:hover { background-color: %2; }") @@ -375,11 +383,12 @@ void MainWindow::setupToolBar() { toolbar->addSeparator(); - difficulty_label_ = new QLabel(QString(" %1 ").arg(qstr(loc(ToolbarDifficulty)))); + difficulty_label_ = new QLabel(QString(" %1 ").arg(qstr(core::loc("Sudoku", "Difficulty:")))); toolbar->addWidget(difficulty_label_); difficulty_combo_ = new QComboBox; - difficulty_combo_->addItems({qstr(loc(DifficultyEasy)), qstr(loc(DifficultyMedium)), qstr(loc(DifficultyHard)), - qstr(loc(DifficultyExpert)), qstr(loc(DifficultyMaster))}); + difficulty_combo_->addItems({qstr(core::loc("Sudoku", "Easy")), qstr(core::loc("Sudoku", "Medium")), + qstr(core::loc("Sudoku", "Hard")), qstr(core::loc("Sudoku", "Expert")), + qstr(core::loc("Sudoku", "Master"))}); difficulty_combo_->setCurrentIndex(1); // Medium toolbar->addWidget(difficulty_combo_); @@ -387,9 +396,11 @@ void MainWindow::setupToolBar() { if (!view_model_) { return; } - auto result = QMessageBox::question( - this, qstr(loc(DialogNewGame)), - QString::fromStdString(locFormat(DialogNewGameConfirm, difficulty_combo_->currentText().toStdString()))); + auto result = + QMessageBox::question(this, qstr(core::loc("Sudoku", "New Game")), + QString::fromStdString(core::locFormat( + core::loc("Sudoku", "Start a new {0} game?\nCurrent progress will be lost."), + difficulty_combo_->currentText().toStdString()))); if (result == QMessageBox::Yes) { view_model_->startNewGame(static_cast(index)); board_widget_->setSelectedCell(core::Position{.row = 0, .col = 0}); @@ -404,7 +415,7 @@ void MainWindow::setupToolBar() { toolbar->addSeparator(); - hints_text_label_ = new QLabel(QString(" %1 ").arg(qstr(loc(ToolbarHints)))); + hints_text_label_ = new QLabel(QString(" %1 ").arg(qstr(core::loc("Sudoku", "Hints:")))); toolbar->addWidget(hints_text_label_); hints_label_ = new QLabel("10"); hints_label_->setStyleSheet(QString("background-color: %1; color: white; padding: 2px 12px; border-radius: 12px;") @@ -427,7 +438,7 @@ void MainWindow::setupToolBar() { void MainWindow::setupStatusBar() { timer_label_ = new QLabel(); statusBar()->addWidget(timer_label_); - status_label_ = new QLabel(qstr(loc(StatusReady))); + status_label_ = new QLabel(qstr(core::loc("Sudoku", "Ready"))); statusBar()->addWidget(status_label_, 1); statusBar()->setStyleSheet(QString("QStatusBar { background-color: %1; border-top: 1px solid %2; color: %3; }") .arg(StyleColors::SURFACE_STATUS, StyleColors::DIVIDER, StyleColors::TEXT_MUTED)); @@ -499,9 +510,10 @@ void MainWindow::onCoachingStateChanged(const viewmodel::CoachingState& coaching const bool is_tryit = (coaching.phase == viewmodel::CoachingPhase::TryIt); if (is_tryit) { - coaching_level_label_->setText(qstr(loc(CoachingTryItLabel))); + coaching_level_label_->setText(qstr(core::loc("Sudoku", "Try it!"))); } else { - coaching_level_label_->setText(QString::fromStdString(locFormat(CoachingLevelHeader, coaching.level))); + coaching_level_label_->setText( + QString::fromStdString(core::locFormat(core::loc("Sudoku", "Level {0}/3"), coaching.level))); } coaching_level_label_->setVisible(true); @@ -539,22 +551,6 @@ void MainWindow::setTrainingViewModel(std::shared_ptr loc_manager) { - loc_manager_ = std::move(loc_manager); - spdlog::debug("LocalizationManager bound to MainWindow (locale: {})", loc_manager_->getCurrentLocale()); - if (board_widget_) { - board_widget_->setLocalizationManager(loc_manager_); - } - if (training_widget_) { - training_widget_->setLocalizationManager(loc_manager_); - // Re-bind training VM since rebuildPages() destroyed the old page widgets - if (training_vm_) { - training_widget_->setTrainingViewModel(training_vm_); - } - } - retranslateUi(); -} - void MainWindow::setSettingsManager(std::shared_ptr settings_manager) { settings_manager_ = std::move(settings_manager); @@ -564,15 +560,20 @@ void MainWindow::setSettingsManager(std::shared_ptr sett auto_save_timer_->setInterval(settings_manager_->getSettings().auto_save_interval_ms); } - // Subscribe to settings changes + // Load the configured locale; Qt posts QEvent::LanguageChange to all + // top-level widgets, which retranslates the UI via changeEvent. + applyLocale(settings_manager_->getSettings().language); + + // React to language changes from the Settings dialog. Compare against + // the previous locale so we only reload the translator when it + // actually changes (other settings — auto-save interval, hint + // visibility, etc. — also fire this observer). settings_manager_->settingsObservable().subscribe([this](const core::Settings& s) { if (auto_save_timer_) { auto_save_timer_->setInterval(s.auto_save_interval_ms); } - // Retranslate UI if language changed - if (loc_manager_ && s.language != std::string(loc_manager_->getCurrentLocale())) { - [[maybe_unused]] auto result = loc_manager_->setLocale(s.language); - retranslateUi(); + if (s.language != current_locale_) { + applyLocale(s.language); } }); } @@ -580,6 +581,41 @@ void MainWindow::setSettingsManager(std::shared_ptr sett spdlog::debug("SettingsManager bound to MainWindow"); } +void MainWindow::applyLocale(const std::string& locale_code) { + // Locate sudoku_.qm next to the executable (dev/Windows layout) + // or under ../share/sudoku/translations (FHS install). + auto exe_dir = std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()); + std::filesystem::path translations_dir; + for (const auto& candidate : {exe_dir / "translations", exe_dir / ".." / "share" / "sudoku" / "translations"}) { + if (std::filesystem::exists(candidate)) { + translations_dir = candidate; + break; + } + } + + QCoreApplication::removeTranslator(&translator_); + + if (translations_dir.empty()) { + spdlog::warn("Qt translations directory not found; UI will use source-language strings"); + current_locale_ = locale_code; + return; + } + + auto qm_name = QString::fromStdString(fmt::format("sudoku_{}", locale_code)); + if (!translator_.load(qm_name, QString::fromStdString(translations_dir.string()))) { + spdlog::warn("Failed to load Qt translation '{}.qm' from {}", qm_name.toStdString(), translations_dir.string()); + current_locale_ = locale_code; + return; + } + if (!QCoreApplication::installTranslator(&translator_)) { + spdlog::warn("QCoreApplication::installTranslator returned false for locale '{}'", locale_code); + current_locale_ = locale_code; + return; + } + current_locale_ = locale_code; + spdlog::info("Qt translator installed: {} from {}", qm_name.toStdString(), translations_dir.string()); +} + void MainWindow::closeEvent(QCloseEvent* event) { if (view_model_) { spdlog::info("Window close requested, saving game state..."); @@ -593,6 +629,13 @@ bool MainWindow::event(QEvent* event) { return QMainWindow::event(event); } +void MainWindow::changeEvent(QEvent* event) { + if (event->type() == QEvent::LanguageChange) { + retranslateUi(); + } + QMainWindow::changeEvent(event); +} + void MainWindow::keyPressEvent(QKeyEvent* event) { if (!view_model_) { QMainWindow::keyPressEvent(event); @@ -660,20 +703,21 @@ void MainWindow::keyPressEvent(QKeyEvent* event) { void MainWindow::updateStatusBar() { if (!view_model_) { timer_label_->clear(); - status_label_->setText(qstr(loc(StatusReady))); + status_label_->setText(qstr(core::loc("Sudoku", "Ready"))); return; } const auto& game_state = view_model_->gameState.get(); if (game_state.isComplete()) { timer_label_->setText(QString::fromStdString(view_model_->getFormattedTime())); - status_label_->setText(QString("%1").arg(qstr(loc(StatusCompleted)))); + status_label_->setText( + QString("%1").arg(qstr(core::loc("Sudoku", "Completed!")))); } else if (game_state.isTimerRunning()) { timer_label_->setText(QString::fromStdString(view_model_->getFormattedTime())); - status_label_->setText(qstr(loc(StatusPlaying))); + status_label_->setText(qstr(core::loc("Sudoku", "Playing"))); } else { timer_label_->clear(); - status_label_->setText(qstr(loc(StatusReady))); + status_label_->setText(qstr(core::loc("Sudoku", "Ready"))); } } @@ -690,10 +734,10 @@ void MainWindow::updateToolBar() { const auto& techniques = ui_state.puzzle_techniques; auto rating_str = QString::number(ui_state.puzzle_rating, 'f', 1).toStdString(); if (!techniques.empty()) { - rating_btn_->setText( - QString::fromStdString(locFormat(ToolbarRatingWithTechniques, rating_str, techniques.size()))); + rating_btn_->setText(QString::fromStdString( + core::locFormat(core::loc("Sudoku", "SE {0} ({1} techniques)"), rating_str, techniques.size()))); } else { - rating_btn_->setText(QString::fromStdString(locFormat(ToolbarRatingSimple, rating_str))); + rating_btn_->setText(QString::fromStdString(core::locFormat(core::loc("Sudoku", "SE {0}"), rating_str))); } rating_action_->setVisible(true); } else { @@ -712,20 +756,21 @@ void MainWindow::updateButtonPanel() { // Update input mode indicator switch (view_model_->getInputMode()) { case viewmodel::InputMode::Normal: - mode_btn_->setText(qstr(loc(ModeNormal))); + mode_btn_->setText(qstr(core::loc("Sudoku", "Normal"))); break; case viewmodel::InputMode::Notes: - mode_btn_->setText(qstr(loc(ModeNotes))); + mode_btn_->setText(qstr(core::loc("Sudoku", "Notes"))); break; case viewmodel::InputMode::Color: - mode_btn_->setText(qstr(loc(ModeColor))); + mode_btn_->setText(qstr(core::loc("Sudoku", "Color"))); break; } // Update fill notes toggle state const auto& ui = view_model_->uiState.get(); auto_notes_btn_->setChecked(ui.notes_filled); - auto_notes_btn_->setText(qstr(loc(ui.notes_filled ? ButtonClearNotes : ButtonFillNotes))); + auto_notes_btn_->setText(ui.notes_filled ? qstr(core::loc("Sudoku", "Clear Notes")) + : qstr(core::loc("Sudoku", "Fill Notes"))); } // Dialog methods @@ -736,11 +781,12 @@ void MainWindow::showNewGameDialog() { } int selected = difficulty_combo_ ? difficulty_combo_->currentIndex() : 1; - QString diff_name = difficulty_combo_ ? difficulty_combo_->currentText() : qstr(loc(DifficultyMedium)); + QString diff_name = difficulty_combo_ ? difficulty_combo_->currentText() : qstr(core::loc("Sudoku", "Medium")); - auto result = - QMessageBox::question(this, qstr(loc(DialogNewGame)), - QString::fromStdString(locFormat(DialogNewGameConfirm, diff_name.toStdString()))); + auto result = QMessageBox::question( + this, qstr(core::loc("Sudoku", "New Game")), + QString::fromStdString(core::locFormat( + core::loc("Sudoku", "Start a new {0} game?\nCurrent progress will be lost."), diff_name.toStdString()))); if (result == QMessageBox::Yes) { view_model_->startNewGame(static_cast(selected)); @@ -749,8 +795,11 @@ void MainWindow::showNewGameDialog() { } void MainWindow::showResetDialog() { - auto result = QMessageBox::warning(this, qstr(loc(DialogResetPuzzle)), qstr(loc(DialogResetWarning)), - QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel); + auto result = + QMessageBox::warning(this, qstr(core::loc("Sudoku", "Reset Puzzle")), + qstr(core::loc("Sudoku", "All progress on this puzzle will be lost, including placed " + "numbers, notes, and hints. The timer will restart.")), + QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel); if (result == QMessageBox::Yes && view_model_) { view_model_->executeCommand(viewmodel::GameCommand::ResetGame); } @@ -762,26 +811,27 @@ void MainWindow::showSaveDialog() { } QDialog dialog(this); - dialog.setWindowTitle(qstr(loc(DialogSaveGame))); + dialog.setWindowTitle(qstr(core::loc("Sudoku", "Save Game"))); dialog.setMinimumWidth(380); auto* layout = new QVBoxLayout(&dialog); // Current game info preview - auto* info_group = new QGroupBox(qstr(loc(SavePreviewTitle)), &dialog); + auto* info_group = new QGroupBox(qstr(core::loc("Sudoku", "Current Game")), &dialog); auto* info_layout = new QFormLayout(info_group); - info_layout->addRow(qstr(loc(SavePreviewDifficulty)), + info_layout->addRow(qstr(core::loc("Sudoku", "Difficulty")), new QLabel(qstr(difficultyString(view_model_->gameState.get().getDifficulty())))); - info_layout->addRow(qstr(loc(SavePreviewTime)), + info_layout->addRow(qstr(core::loc("Sudoku", "Time")), new QLabel(QString::fromStdString(view_model_->getFormattedTime()))); - info_layout->addRow(qstr(loc(SavePreviewMoves)), new QLabel(QString::number(view_model_->getMoveCount()))); - info_layout->addRow(qstr(loc(SavePreviewMistakes)), new QLabel(QString::number(view_model_->getMistakeCount()))); + info_layout->addRow(qstr(core::loc("Sudoku", "Moves")), new QLabel(QString::number(view_model_->getMoveCount()))); + info_layout->addRow(qstr(core::loc("Sudoku", "Mistakes")), + new QLabel(QString::number(view_model_->getMistakeCount()))); layout->addWidget(info_group); // Name input - layout->addWidget(new QLabel(qstr(loc(DialogEnterSaveName)))); + layout->addWidget(new QLabel(qstr(core::loc("Sudoku", "Enter save name:")))); auto* name_edit = new QLineEdit(&dialog); - name_edit->setPlaceholderText(qstr(loc(SaveNamePlaceholder))); + name_edit->setPlaceholderText(qstr(core::loc("Sudoku", "Enter save name..."))); layout->addWidget(name_edit); auto* buttons = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel, &dialog); @@ -790,7 +840,8 @@ void MainWindow::showSaveDialog() { connect(buttons, &QDialogButtonBox::accepted, &dialog, [&]() { QString name = name_edit->text().trimmed(); if (name.isEmpty()) { - QMessageBox::warning(&dialog, qstr(loc(DialogSaveGame)), qstr(loc(SaveNameEmpty))); + QMessageBox::warning(&dialog, qstr(core::loc("Sudoku", "Save Game")), + qstr(core::loc("Sudoku", "Please enter a save name."))); return; } @@ -799,9 +850,10 @@ void MainWindow::showSaveDialog() { bool name_exists = std::ranges::any_of(existing, [&](const auto& s) { return s.display_name == name.toStdString(); }); if (name_exists) { - auto confirm = - QMessageBox::question(&dialog, qstr(loc(DialogSaveGame)), - QString::fromStdString(locFormat(SaveOverwriteConfirm, name.toStdString()))); + auto confirm = QMessageBox::question( + &dialog, qstr(core::loc("Sudoku", "Save Game")), + QString::fromStdString(core::locFormat( + core::loc("Sudoku", "A save named \"{0}\" already exists. Overwrite it?"), name.toStdString()))); if (confirm != QMessageBox::Yes) { return; } @@ -815,7 +867,7 @@ void MainWindow::showSaveDialog() { QString name = name_edit->text().trimmed(); bool success = view_model_->saveCurrentGame(name.toStdString()); if (success && toast_widget_) { - toast_widget_->show(qstr(loc(ToastGameSaved))); + toast_widget_->show(qstr(core::loc("Sudoku", "Game saved successfully"))); } } } @@ -828,15 +880,16 @@ void MainWindow::showLoadDialog() { auto saves = view_model_->getSaveList(); QDialog dialog(this); - dialog.setWindowTitle(qstr(loc(DialogLoadGame))); + dialog.setWindowTitle(qstr(core::loc("Sudoku", "Load Game"))); dialog.setMinimumSize(600, 350); auto* layout = new QVBoxLayout(&dialog); static constexpr int LOAD_COLS = 5; auto* table = new QTableWidget(static_cast(saves.size()), LOAD_COLS, &dialog); - table->setHorizontalHeaderLabels({qstr(loc(LoadColName)), qstr(loc(LoadColDifficulty)), qstr(loc(LoadColDate)), - qstr(loc(LoadColTime)), qstr(loc(LoadColRating))}); + table->setHorizontalHeaderLabels({qstr(core::loc("Sudoku", "Name")), qstr(core::loc("Sudoku", "Difficulty")), + qstr(core::loc("Sudoku", "Last Modified")), qstr(core::loc("Sudoku", "Elapsed")), + qstr(core::loc("Sudoku", "Rating"))}); table->setSelectionBehavior(QAbstractItemView::SelectRows); table->setSelectionMode(QAbstractItemView::SingleSelection); table->setEditTriggers(QAbstractItemView::NoEditTriggers); @@ -857,10 +910,10 @@ void MainWindow::showLoadDialog() { row, 3, new QTableWidgetItem(QString::fromStdString(viewmodel::GameViewModel::formatDuration(s.elapsed_time)))); table->setItem(row, 4, - new QTableWidgetItem( - s.puzzle_rating > 0.0 - ? QString::fromStdString(locFormat(RatingFormat, fmt::format("{:.1f}", s.puzzle_rating))) - : qstr(loc(StatsTimeNa)))); + new QTableWidgetItem(s.puzzle_rating > 0.0 ? QString::fromStdString(core::locFormat( + core::loc("Sudoku", "SE {0}"), + fmt::format("{:.1f}", s.puzzle_rating))) + : qstr(core::loc("Sudoku", "N/A")))); } table->resizeColumnsToContents(); layout->addWidget(table); @@ -891,7 +944,7 @@ void MainWindow::showStatisticsDialog() { const auto& display = view_model_->statistics.get(); auto* dialog = new QDialog(this); - dialog->setWindowTitle(qstr(loc(DialogStatistics))); + dialog->setWindowTitle(qstr(core::loc("Sudoku", "Statistics"))); dialog->setMinimumSize(520, 400); dialog->setAttribute(Qt::WA_DeleteOnClose); @@ -900,28 +953,30 @@ void MainWindow::showStatisticsDialog() { // === Overview tab === auto* overview_page = new QWidget(); auto* overview_layout = new QFormLayout(overview_page); - auto addStatRow = [&](std::string_view key, const auto& value) { - overview_layout->addRow(new QLabel(QString::fromStdString(locFormat(key, value)))); + auto addStatRow = [&](const std::string& text) { + overview_layout->addRow(new QLabel(QString::fromStdString(text))); }; - addStatRow(StatsGamesPlayed, display.games_played); - addStatRow(StatsGamesCompleted, display.games_completed); - addStatRow(StatsCompletionRate, display.completion_rate); - addStatRow(StatsBestTime, display.best_time); - addStatRow(StatsAverageTime, display.average_time); - addStatRow(StatsCurrentStreak, display.current_streak); - addStatRow(StatsBestStreak, display.best_streak); + addStatRow(core::locFormat(core::loc("Sudoku", "Games Played: {0}"), display.games_played)); + addStatRow(core::locFormat(core::loc("Sudoku", "Games Completed: {0}"), display.games_completed)); + addStatRow(core::locFormat(core::loc("Sudoku", "Completion Rate: {0:.1f}%"), display.completion_rate)); + addStatRow(core::locFormat(core::loc("Sudoku", "Best Time: {0}"), display.best_time)); + addStatRow(core::locFormat(core::loc("Sudoku", "Average Time: {0}"), display.average_time)); + addStatRow(core::locFormat(core::loc("Sudoku", "Current Streak: {0}"), display.current_streak)); + addStatRow(core::locFormat(core::loc("Sudoku", "Best Streak: {0}"), display.best_streak)); if (maybe_stats) { const auto& agg = *maybe_stats; overview_layout->addRow(new QLabel("")); // spacer - overview_layout->addRow(qstr(loc(StatsTotalMoves)), new QLabel(QString::number(agg.total_moves))); - overview_layout->addRow(qstr(loc(StatsTotalHints)), new QLabel(QString::number(agg.total_hints))); - overview_layout->addRow(qstr(loc(StatsTotalMistakes)), new QLabel(QString::number(agg.total_mistakes))); + overview_layout->addRow(qstr(core::loc("Sudoku", "Total Moves")), new QLabel(QString::number(agg.total_moves))); + overview_layout->addRow(qstr(core::loc("Sudoku", "Total Hints Used")), + new QLabel(QString::number(agg.total_hints))); + overview_layout->addRow(qstr(core::loc("Sudoku", "Total Mistakes")), + new QLabel(QString::number(agg.total_mistakes))); overview_layout->addRow( - qstr(loc(StatsTotalTime)), + qstr(core::loc("Sudoku", "Total Time Played")), new QLabel(QString::fromStdString(viewmodel::GameViewModel::formatDuration(agg.total_time_played)))); } - tabs->addTab(overview_page, qstr(loc(StatsTabOverview))); + tabs->addTab(overview_page, qstr(core::loc("Sudoku", "Overview"))); // === Per-difficulty tab === if (maybe_stats) { @@ -941,9 +996,9 @@ void MainWindow::showStatisticsDialog() { table->setHorizontalHeaderLabels(col_headers); // Row headers: metric names - table->setVerticalHeaderLabels({qstr(loc(StatsRowPlayed)), qstr(loc(StatsRowCompleted)), - qstr(loc(StatsRowBestTime)), qstr(loc(StatsRowAvgTime)), - qstr(loc(StatsRowAvgRating))}); + table->setVerticalHeaderLabels({qstr(core::loc("Sudoku", "Played")), qstr(core::loc("Sudoku", "Completed")), + qstr(core::loc("Sudoku", "Best Time")), qstr(core::loc("Sudoku", "Avg Time")), + qstr(core::loc("Sudoku", "Avg SE Rating"))}); for (int d = 0; d < NUM_DIFFICULTIES; ++d) { table->setItem(0, d, new QTableWidgetItem(QString::number(agg.games_played[d]))); @@ -954,26 +1009,27 @@ void MainWindow::showStatisticsDialog() { 2, d, new QTableWidgetItem((bt != std::chrono::milliseconds::max() && agg.games_completed[d] > 0) ? QString::fromStdString(viewmodel::GameViewModel::formatDuration(bt)) - : qstr(loc(StatsTimeNa)))); + : qstr(core::loc("Sudoku", "N/A")))); auto at = agg.average_times[d]; table->setItem(3, d, new QTableWidgetItem( at.count() > 0 ? QString::fromStdString(viewmodel::GameViewModel::formatDuration(at)) - : qstr(loc(StatsTimeNa)))); + : qstr(core::loc("Sudoku", "N/A")))); table->setItem(4, d, - new QTableWidgetItem(agg.average_ratings[d] > 0.0 - ? QString::fromStdString(locFormat( - RatingFormat, fmt::format("{:.1f}", agg.average_ratings[d]))) - : qstr(loc(StatsTimeNa)))); + new QTableWidgetItem( + agg.average_ratings[d] > 0.0 + ? QString::fromStdString(core::locFormat( + core::loc("Sudoku", "SE {0}"), fmt::format("{:.1f}", agg.average_ratings[d]))) + : qstr(core::loc("Sudoku", "N/A")))); } table->setEditTriggers(QAbstractItemView::NoEditTriggers); table->horizontalHeader()->setStretchLastSection(true); table->resizeColumnsToContents(); diff_layout->addWidget(table); - tabs->addTab(diff_page, qstr(loc(StatsTabPerDifficulty))); + tabs->addTab(diff_page, qstr(core::loc("Sudoku", "Per Difficulty"))); } // === Recent Games tab (conditional on collect_detailed_stats) === @@ -986,9 +1042,10 @@ void MainWindow::showStatisticsDialog() { auto* recent_layout = new QVBoxLayout(recent_page); auto* recent_table = new QTableWidget(static_cast(recent.size()), NUM_COLS, recent_page); - recent_table->setHorizontalHeaderLabels({qstr(loc(StatsColDate)), qstr(loc(StatsColDifficulty)), - qstr(loc(StatsColTime)), qstr(loc(StatsColRating)), - qstr(loc(StatsColMoves)), qstr(loc(StatsColMistakes))}); + recent_table->setHorizontalHeaderLabels( + {qstr(core::loc("Sudoku", "Date")), qstr(core::loc("Sudoku", "Difficulty")), + qstr(core::loc("Sudoku", "Time")), qstr(core::loc("Sudoku", "Rating")), + qstr(core::loc("Sudoku", "Moves")), qstr(core::loc("Sudoku", "Mistakes"))}); for (int row = 0; std::cmp_less(row, recent.size()); ++row) { const auto& g = recent[row]; @@ -1001,12 +1058,12 @@ void MainWindow::showStatisticsDialog() { recent_table->setItem(row, 2, new QTableWidgetItem(QString::fromStdString( viewmodel::GameViewModel::formatDuration(g.time_played)))); - recent_table->setItem( - row, 3, - new QTableWidgetItem( - g.puzzle_rating > 0.0 - ? QString::fromStdString(locFormat(RatingFormat, fmt::format("{:.1f}", g.puzzle_rating))) - : qstr(loc(StatsTimeNa)))); + recent_table->setItem(row, 3, + new QTableWidgetItem(g.puzzle_rating > 0.0 + ? QString::fromStdString(core::locFormat( + core::loc("Sudoku", "SE {0}"), + fmt::format("{:.1f}", g.puzzle_rating))) + : qstr(core::loc("Sudoku", "N/A")))); recent_table->setItem(row, 4, new QTableWidgetItem(QString::number(g.moves_made))); recent_table->setItem(row, 5, new QTableWidgetItem(QString::number(g.mistakes))); } @@ -1016,7 +1073,7 @@ void MainWindow::showStatisticsDialog() { recent_table->horizontalHeader()->setStretchLastSection(true); recent_table->resizeColumnsToContents(); recent_layout->addWidget(recent_table); - tabs->addTab(recent_page, qstr(loc(StatsTabRecentGames))); + tabs->addTab(recent_page, qstr(core::loc("Sudoku", "Recent Games"))); } } @@ -1030,16 +1087,18 @@ void MainWindow::showStatisticsDialog() { } void MainWindow::showAboutDialog() { - QMessageBox::about(this, qstr(loc(DialogAbout)), + QMessageBox::about(this, qstr(core::loc("Sudoku", "About")), QString("%1\n\n%2\n\n" "%3\n- Qt6\n- C++23\n\n" "Copyright (C) 2025-2026 Alexander Bendlin") - .arg(qstr(loc(AboutSudokuGame)), qstr(loc(AboutDescription)), qstr(loc(AboutBuiltWith)))); + .arg(qstr(core::loc("Sudoku", "Sudoku Game")), + qstr(core::loc("Sudoku", "A feature-rich offline Sudoku application.")), + qstr(core::loc("Sudoku", "Built with:")))); } void MainWindow::showThirdPartyLicensesDialog() { auto* dialog = new QDialog(this); - dialog->setWindowTitle(qstr(loc(DialogThirdPartyLicenses))); + dialog->setWindowTitle(qstr(core::loc("Sudoku", "Third-Party Licenses"))); dialog->resize(600, 480); auto* text = new QTextEdit(dialog); @@ -1099,21 +1158,22 @@ void MainWindow::showTechniquesDialog() { return; } - QString text = QString::fromStdString( - locFormat(DialogPuzzleRating, QString::number(ui_state.puzzle_rating, 'f', 1).toStdString())) + - "\n\n"; + QString text = + QString::fromStdString(core::locFormat(core::loc("Sudoku", "Puzzle Rating: SE {0}"), + QString::number(ui_state.puzzle_rating, 'f', 1).toStdString())) + + "\n\n"; const auto& techniques = ui_state.puzzle_techniques; if (!techniques.empty()) { - text += QString("%1\n\n").arg(qstr(loc(DialogTechniquesRequired))); + text += QString("%1\n\n").arg(qstr(core::loc("Sudoku", "Techniques required to solve:"))); for (const auto& tech : techniques) { text += QString(" %1\n").arg(QString::fromStdString(tech)); } } else { - text += qstr(loc(DialogNoTechniqueDetails)); + text += qstr(core::loc("Sudoku", "No technique details available.")); } - QMessageBox::information(this, qstr(loc(DialogPuzzleDifficulty)), text); + QMessageBox::information(this, qstr(core::loc("Sudoku", "Puzzle Difficulty")), text); } void MainWindow::exportAggregateStatsCsv() { @@ -1123,9 +1183,10 @@ void MainWindow::exportAggregateStatsCsv() { auto result = view_model_->exportAggregateStatsCsv(); if (result) { - toast_widget_->show(qstr(loc(ToastAggregateExported))); + toast_widget_->show(qstr(core::loc("Sudoku", "Aggregate stats exported to CSV"))); } else { - toast_widget_->show(QString::fromStdString(locFormat(ToastExportFailed, result.error()))); + toast_widget_->show( + QString::fromStdString(core::locFormat(core::loc("Sudoku", "Export failed: {0}"), result.error()))); } } @@ -1136,41 +1197,42 @@ void MainWindow::exportGameSessionsCsv() { auto result = view_model_->exportGameSessionsCsv(); if (result) { - toast_widget_->show(qstr(loc(ToastSessionsExported))); + toast_widget_->show(qstr(core::loc("Sudoku", "Game sessions exported to CSV"))); } else { - toast_widget_->show(QString::fromStdString(locFormat(ToastExportFailed, result.error()))); + toast_widget_->show( + QString::fromStdString(core::locFormat(core::loc("Sudoku", "Export failed: {0}"), result.error()))); } } -std::string_view MainWindow::difficultyString(core::Difficulty difficulty) const { +std::string MainWindow::difficultyString(core::Difficulty difficulty) const { switch (difficulty) { case core::Difficulty::Easy: - return loc(DifficultyEasy); + return core::loc("Sudoku", "Easy"); case core::Difficulty::Medium: - return loc(DifficultyMedium); + return core::loc("Sudoku", "Medium"); case core::Difficulty::Hard: - return loc(DifficultyHard); + return core::loc("Sudoku", "Hard"); case core::Difficulty::Expert: - return loc(DifficultyExpert); + return core::loc("Sudoku", "Expert"); case core::Difficulty::Master: - return loc(DifficultyMaster); + return core::loc("Sudoku", "Master"); default: - return loc(DifficultyUnknown); + return core::loc("Sudoku", "Unknown"); } } void MainWindow::retranslateUi() { // Window title - setWindowTitle(qstr(loc(AppTitle))); + setWindowTitle(qstr(core::loc("Sudoku", "Sudoku"))); // Menu bar: rebuild entirely (avoids storing ~15 action pointers) menuBar()->clear(); setupMenuBar(); // Toolbar labels - new_game_btn_->setText(qstr(loc(ToolbarNewGame))); - difficulty_label_->setText(QString(" %1 ").arg(qstr(loc(ToolbarDifficulty)))); - hints_text_label_->setText(QString(" %1 ").arg(qstr(loc(ToolbarHints)))); + new_game_btn_->setText(qstr(core::loc("Sudoku", "▶ New Game"))); + difficulty_label_->setText(QString(" %1 ").arg(qstr(core::loc("Sudoku", "Difficulty:")))); + hints_text_label_->setText(QString(" %1 ").arg(qstr(core::loc("Sudoku", "Hints:")))); // Difficulty combo items difficulty_combo_->blockSignals(true); @@ -1180,19 +1242,12 @@ void MainWindow::retranslateUi() { difficulty_combo_->blockSignals(false); // Button panel - undo_btn_->setText(qstr(loc(ButtonUndo))); - redo_btn_->setText(qstr(loc(ButtonRedo))); - undo_valid_btn_->setText(qstr(loc(ButtonUndoUntilValid))); - auto_notes_btn_->setText(qstr(loc(auto_notes_btn_->isChecked() ? ButtonClearNotes : ButtonFillNotes))); - mode_btn_->setToolTip(qstr(loc(TooltipInputMode))); - - // Training widget: rebuild pages with new locale, then re-bind VM - if (training_widget_ && loc_manager_) { - training_widget_->setLocalizationManager(loc_manager_); - if (training_vm_) { - training_widget_->setTrainingViewModel(training_vm_); - } - } + undo_btn_->setText(qstr(core::loc("Sudoku", "Undo"))); + redo_btn_->setText(qstr(core::loc("Sudoku", "Redo"))); + undo_valid_btn_->setText(qstr(core::loc("Sudoku", "Undo Until Valid"))); + auto_notes_btn_->setText(auto_notes_btn_->isChecked() ? qstr(core::loc("Sudoku", "Clear Notes")) + : qstr(core::loc("Sudoku", "Fill Notes"))); + mode_btn_->setToolTip(qstr(core::loc("Sudoku", "Input mode (Space to cycle, N for Notes)"))); // Status bar and mode button updateStatusBar(); @@ -1206,7 +1261,7 @@ void MainWindow::showSettingsDialog() { } auto* dialog = new QDialog(this); - dialog->setWindowTitle(qstr(loc(DialogSettings))); + dialog->setWindowTitle(qstr(core::loc("Sudoku", "Settings"))); dialog->setMinimumWidth(400); dialog->setAttribute(Qt::WA_DeleteOnClose); @@ -1219,45 +1274,48 @@ void MainWindow::showSettingsDialog() { auto* max_hints_spin = new QSpinBox(); max_hints_spin->setRange(1, 50); max_hints_spin->setValue(settings_manager_->getSettings().max_hints); - gameplay_layout->addRow(qstr(loc(SettingsMaxHints)), max_hints_spin); + gameplay_layout->addRow(qstr(core::loc("Sudoku", "Maximum Hints:")), max_hints_spin); auto* auto_save_spin = new QSpinBox(); auto_save_spin->setRange(10, 300); - auto_save_spin->setSuffix(qstr(loc(SettingsSecondsSuffix))); + auto_save_spin->setSuffix(qstr(core::loc("Sudoku", " seconds"))); auto_save_spin->setValue(settings_manager_->getSettings().auto_save_interval_ms / 1000); - gameplay_layout->addRow(qstr(loc(SettingsAutoSaveInterval)), auto_save_spin); + gameplay_layout->addRow(qstr(core::loc("Sudoku", "Auto-save Interval:")), auto_save_spin); auto* difficulty_combo = new QComboBox(); - difficulty_combo->addItems({qstr(loc(DifficultyEasy)), qstr(loc(DifficultyMedium)), qstr(loc(DifficultyHard)), - qstr(loc(DifficultyExpert)), qstr(loc(DifficultyMaster))}); + difficulty_combo->addItems({qstr(core::loc("Sudoku", "Easy")), qstr(core::loc("Sudoku", "Medium")), + qstr(core::loc("Sudoku", "Hard")), qstr(core::loc("Sudoku", "Expert")), + qstr(core::loc("Sudoku", "Master"))}); difficulty_combo->setCurrentIndex(static_cast(settings_manager_->getSettings().default_difficulty)); - gameplay_layout->addRow(qstr(loc(SettingsDefaultDifficulty)), difficulty_combo); + gameplay_layout->addRow(qstr(core::loc("Sudoku", "Default Difficulty:")), difficulty_combo); - tabs->addTab(gameplay_page, qstr(loc(SettingsTabGameplay))); + tabs->addTab(gameplay_page, qstr(core::loc("Sudoku", "Gameplay"))); // === Display Tab === auto* display_page = new QWidget(); auto* display_layout = new QVBoxLayout(display_page); - auto* show_conflicts_cb = new QCheckBox(qstr(loc(SettingsHighlightConflicts))); + auto* show_conflicts_cb = new QCheckBox(qstr(core::loc("Sudoku", "Highlight Conflicts"))); show_conflicts_cb->setChecked(settings_manager_->getSettings().show_conflicts); display_layout->addWidget(show_conflicts_cb); - auto* show_hints_cb = new QCheckBox(qstr(loc(SettingsShowHints))); + auto* show_hints_cb = new QCheckBox(qstr(core::loc("Sudoku", "Show Hints"))); show_hints_cb->setChecked(settings_manager_->getSettings().show_hints); display_layout->addWidget(show_hints_cb); - // Language selection - if (loc_manager_) { + // Language selection. Setting persists across restart but does not retranslate + // the running UI (Qt Linguist runtime swap not yet wired). + { display_layout->addSpacing(10); auto* lang_layout = new QHBoxLayout(); - lang_layout->addWidget(new QLabel(qstr(loc(SidebarLanguage)))); + lang_layout->addWidget(new QLabel(qstr(core::loc("Sudoku", "Language")))); auto* lang_combo = new QComboBox(); - auto locales = loc_manager_->getAvailableLocales(); + static const std::array, 2> kLocales = { + {{"en", "English"}, {"de", "Deutsch"}}}; int current_idx = 0; - for (size_t i = 0; i < locales.size(); ++i) { - lang_combo->addItem(QString::fromStdString(locales[i].second), QString::fromStdString(locales[i].first)); - if (locales[i].first == settings_manager_->getSettings().language) { + for (size_t i = 0; i < kLocales.size(); ++i) { + lang_combo->addItem(QString::fromUtf8(kLocales[i].second), QString::fromUtf8(kLocales[i].first)); + if (kLocales[i].first == settings_manager_->getSettings().language) { current_idx = static_cast(i); } } @@ -1272,24 +1330,25 @@ void MainWindow::showSettingsDialog() { } display_layout->addStretch(); - tabs->addTab(display_page, qstr(loc(SettingsTabDisplay))); + tabs->addTab(display_page, qstr(core::loc("Sudoku", "Display"))); // === Statistics Tab === auto* stats_page = new QWidget(); auto* stats_layout = new QVBoxLayout(stats_page); - auto* collect_stats_cb = new QCheckBox(qstr(loc(SettingsCollectDetailedStats))); + auto* collect_stats_cb = new QCheckBox(qstr(core::loc("Sudoku", "Collect detailed match statistics"))); collect_stats_cb->setChecked(settings_manager_->getSettings().collect_detailed_stats); stats_layout->addWidget(collect_stats_cb); - auto* encrypt_stats_cb = new QCheckBox(qstr(loc(SettingsEncryptDetailedStats))); + auto* encrypt_stats_cb = new QCheckBox(qstr(core::loc("Sudoku", "Encrypt session data"))); encrypt_stats_cb->setChecked(settings_manager_->getSettings().encrypt_detailed_stats); - encrypt_stats_cb->setToolTip(qstr(loc(SettingsEncryptDetailedStatsTooltip))); + encrypt_stats_cb->setToolTip(qstr(core::loc( + "Sudoku", "Session data is encrypted by default for privacy. Disable to inspect the raw data file yourself."))); encrypt_stats_cb->setEnabled(settings_manager_->getSettings().collect_detailed_stats); stats_layout->addWidget(encrypt_stats_cb); stats_layout->addStretch(); - tabs->addTab(stats_page, qstr(loc(SettingsTabStatistics))); + tabs->addTab(stats_page, qstr(core::loc("Sudoku", "Statistics"))); // === Dialog layout === auto* main_layout = new QVBoxLayout(dialog); @@ -1332,7 +1391,11 @@ void MainWindow::showSettingsDialog() { connectCheckBox(collect_stats_cb, [this, encrypt_stats_cb](bool checked) { encrypt_stats_cb->setEnabled(checked); if (!checked) { - auto answer = QMessageBox::question(this, qstr(loc(DialogSettings)), qstr(loc(StatsDeletePrompt))); + auto answer = QMessageBox::question( + this, qstr(core::loc("Sudoku", "Settings")), + qstr(core::loc("Sudoku", + "Disabling session tracking will stop recording per-game statistics. Would you like to " + "delete existing session history?"))); if (answer == QMessageBox::Yes && view_model_) { view_model_->deleteSessionHistory(); } diff --git a/src/view/main_window.h b/src/view/main_window.h index 725b992..2c8540a 100644 --- a/src/view/main_window.h +++ b/src/view/main_window.h @@ -16,7 +16,6 @@ #pragma once -#include "../core/i_localization_manager.h" #include "../core/i_settings_manager.h" #include "../core/observable.h" #include "../view_model/game_view_model.h" @@ -29,6 +28,7 @@ #include #include +#include #include #include #include @@ -70,12 +70,12 @@ class MainWindow : public QMainWindow { void setViewModel(std::shared_ptr view_model); void setTrainingViewModel(std::shared_ptr training_vm); - void setLocalizationManager(std::shared_ptr loc_manager); void setSettingsManager(std::shared_ptr settings_manager); protected: void closeEvent(QCloseEvent* event) override; bool event(QEvent* event) override; + void changeEvent(QEvent* event) override; void keyPressEvent(QKeyEvent* event) override; private: @@ -84,25 +84,20 @@ class MainWindow : public QMainWindow { std::shared_ptr training_vm_; core::CompositeObserver observer_; - // Localization - std::shared_ptr loc_manager_; + // Settings std::shared_ptr settings_manager_; - int selected_language_{0}; - [[nodiscard]] std::string_view loc(std::string_view key) const { - return loc_manager_ ? loc_manager_->getString(key) : key; - } + // Owns the active QTranslator so language can be swapped at runtime. + // Initially empty — installed on the first setSettingsManager() call, + // reloaded via applyLocale() when the user changes the language setting. + QTranslator translator_; + std::string current_locale_; [[nodiscard]] static QString qstr(std::string_view sv) { return QString::fromUtf8(sv.data(), static_cast(sv.size())); } - template - [[nodiscard]] std::string locFormat(std::string_view key, Args&&... args) const { - return fmt::format(fmt::runtime(loc(key)), std::forward(args)...); - } - - [[nodiscard]] std::string_view difficultyString(core::Difficulty difficulty) const; + [[nodiscard]] std::string difficultyString(core::Difficulty difficulty) const; // UI components SudokuBoardWidget* board_widget_{nullptr}; @@ -163,6 +158,7 @@ class MainWindow : public QMainWindow { void showTechniquesDialog(); void showSettingsDialog(); void retranslateUi(); + void applyLocale(const std::string& locale_code); // CSV export void exportAggregateStatsCsv(); diff --git a/src/view/sudoku_board_widget.cpp b/src/view/sudoku_board_widget.cpp index b3f8ee0..a7c0f07 100644 --- a/src/view/sudoku_board_widget.cpp +++ b/src/view/sudoku_board_widget.cpp @@ -17,7 +17,7 @@ #include "sudoku_board_widget.h" #include "core/constants.h" -#include "core/string_keys.h" +#include "core/i18n_helpers.h" #include #include @@ -55,10 +55,6 @@ void SudokuBoardWidget::clearBoard() { update(); } -void SudokuBoardWidget::setLocalizationManager(std::shared_ptr loc_manager) { - loc_manager_ = std::move(loc_manager); -} - void SudokuBoardWidget::setReadOnly(bool read_only) { read_only_ = read_only; if (read_only) { @@ -88,9 +84,8 @@ void SudokuBoardWidget::paintEvent(QPaintEvent* /*event*/) { painter.setRenderHint(QPainter::Antialiasing); if (!has_board_) { - painter.drawText(rect(), Qt::AlignCenter, - QString::fromUtf8(loc(core::StringKeys::BoardNoGameLoaded).data(), - static_cast(loc(core::StringKeys::BoardNoGameLoaded).size()))); + auto msg = core::loc("Sudoku", "No game loaded. Start a new game!"); + painter.drawText(rect(), Qt::AlignCenter, QString::fromUtf8(msg.data(), static_cast(msg.size()))); return; } diff --git a/src/view/sudoku_board_widget.h b/src/view/sudoku_board_widget.h index 0bda42b..baed82a 100644 --- a/src/view/sudoku_board_widget.h +++ b/src/view/sudoku_board_widget.h @@ -16,7 +16,6 @@ #pragma once -#include "../core/i_localization_manager.h" #include "board_painter.h" #include "board_render_data.h" @@ -65,9 +64,6 @@ class SudokuBoardWidget : public QWidget { /// Clear the board (shows "no game loaded" state) void clearBoard(); - - void setLocalizationManager(std::shared_ptr loc_manager); - /// Set read-only mode (disables all interaction) void setReadOnly(bool read_only); @@ -95,15 +91,10 @@ class SudokuBoardWidget : public QWidget { void leaveEvent(QEvent* event) override; private: - std::shared_ptr loc_manager_; BoardPainter painter_; BoardRenderData board_{}; bool has_board_{false}; bool read_only_{false}; - - [[nodiscard]] std::string_view loc(std::string_view key) const { - return loc_manager_ ? loc_manager_->getString(key) : key; - } int hovered_candidate_{0}; ///< Currently hovered candidate value (0 = none) std::optional hovered_cell_; ///< Currently hovered cell (nullopt = mouse outside board) std::optional selected_cell_; ///< Currently selected cell for editing diff --git a/src/view/training_number_pad.cpp b/src/view/training_number_pad.cpp index dd05f78..e341269 100644 --- a/src/view/training_number_pad.cpp +++ b/src/view/training_number_pad.cpp @@ -17,7 +17,7 @@ #include "training_number_pad.h" #include "core/constants.h" -#include "core/string_keys.h" +#include "core/i18n_helpers.h" #include "core/training_types.h" #include @@ -50,19 +50,13 @@ TrainingNumberPad::TrainingNumberPad(QWidget* parent) : QWidget(parent) { layout->addStretch(); } -void TrainingNumberPad::setLocalizationManager(std::shared_ptr loc_manager) { - loc_manager_ = std::move(loc_manager); - // Re-apply tooltips with new locale - setInteractionMode(mode_); -} - void TrainingNumberPad::setInteractionMode(core::TrainingInteractionMode mode) { mode_ = mode; - using namespace core::StringKeys; for (int i = 0; i < static_cast(core::MAX_VALUE); ++i) { - auto key = mode == core::TrainingInteractionMode::Placement ? TooltipPlaceDigit : TooltipEliminateDigit; - auto tooltip = fmt::format(fmt::runtime(loc(key)), i + 1); + auto tooltip = mode == core::TrainingInteractionMode::Placement + ? core::locFormat(core::loc("Sudoku", "Place {0} in selected cell"), i + 1) + : core::locFormat(core::loc("Sudoku", "Eliminate {0} from selected cell"), i + 1); buttons_[static_cast(i)]->setToolTip(QString::fromStdString(tooltip)); } } diff --git a/src/view/training_number_pad.h b/src/view/training_number_pad.h index 57a274e..e78447d 100644 --- a/src/view/training_number_pad.h +++ b/src/view/training_number_pad.h @@ -16,7 +16,6 @@ #pragma once -#include "../core/i_localization_manager.h" #include "../core/training_types.h" #include @@ -41,8 +40,6 @@ class TrainingNumberPad : public QWidget { explicit TrainingNumberPad(QWidget* parent = nullptr); /// Inject localization manager for tooltip translations - void setLocalizationManager(std::shared_ptr loc_manager); - /// Set the interaction mode (changes button labels/behavior) void setInteractionMode(core::TrainingInteractionMode mode); @@ -54,13 +51,8 @@ class TrainingNumberPad : public QWidget { void numberPressed(int value); private: - std::shared_ptr loc_manager_; std::array buttons_{}; core::TrainingInteractionMode mode_{core::TrainingInteractionMode::Placement}; - - [[nodiscard]] std::string_view loc(std::string_view key) const { - return loc_manager_ ? loc_manager_->getString(key) : key; - } }; } // namespace sudoku::view diff --git a/src/view/training_widget.cpp b/src/view/training_widget.cpp index ce16686..e30f918 100644 --- a/src/view/training_widget.cpp +++ b/src/view/training_widget.cpp @@ -17,7 +17,7 @@ #include "training_widget.h" #include "../core/solving_technique.h" -#include "../core/string_keys.h" +#include "core/i18n_helpers.h" #include "core/i_training_statistics_manager.h" #include "core/technique_descriptions.h" #include "core/training_learning_path.h" @@ -33,6 +33,7 @@ #include #include +#include #include #include #include @@ -53,27 +54,26 @@ namespace { /// Format a technique button label with mastery badge and recommendation marker std::string formatTechniqueLabel(sudoku::core::SolvingTechnique technique, sudoku::core::MasteryLevel mastery, - bool is_recommended, const std::function& loc_fn) { + bool is_recommended) { auto name = sudoku::core::getTechniqueName(technique); auto points = sudoku::core::getTechniqueRating(technique); std::string badge; - using namespace sudoku::core::StringKeys; switch (mastery) { case sudoku::core::MasteryLevel::Beginner: break; case sudoku::core::MasteryLevel::Intermediate: - badge = fmt::format(" [{}]", loc_fn(MasteryIntermediate)); + badge = fmt::format(" [{}]", sudoku::core::loc("Sudoku", "Intermediate")); break; case sudoku::core::MasteryLevel::Proficient: - badge = fmt::format(" [{}]", loc_fn(MasteryProficient)); + badge = fmt::format(" [{}]", sudoku::core::loc("Sudoku", "Proficient")); break; case sudoku::core::MasteryLevel::Mastered: - badge = fmt::format(" [{}]", loc_fn(MasteryMastered)); + badge = fmt::format(" [{}]", sudoku::core::loc("Sudoku", "Mastered")); break; } - auto points_str = fmt::format(fmt::runtime(loc_fn(TrainingPointsFmt)), name, points); + auto points_str = sudoku::core::locFormat(sudoku::core::loc("Sudoku", "{0} ({1} pts)"), name, points); return fmt::format("{}{}{}", is_recommended ? ">> " : "", points_str, badge); } @@ -81,8 +81,6 @@ std::string formatTechniqueLabel(sudoku::core::SolvingTechnique technique, sudok namespace sudoku::view { -using namespace core::StringKeys; - TrainingWidget::TrainingWidget(QWidget* parent) : QWidget(parent), pages_(new QStackedWidget) { auto* layout = new QVBoxLayout(this); layout->addWidget(pages_); @@ -94,9 +92,14 @@ TrainingWidget::~TrainingWidget() { observer_.unsubscribeAll(); } -void TrainingWidget::setLocalizationManager(std::shared_ptr loc_manager) { - loc_manager_ = std::move(loc_manager); - rebuildPages(); +void TrainingWidget::changeEvent(QEvent* event) { + if (event->type() == QEvent::LanguageChange) { + // The widget tree is built from translated literals in rebuildPages(); + // re-running it picks up the new locale. Cheaper than threading a + // dedicated retranslate path through every page builder. + rebuildPages(); + } + QWidget::changeEvent(event); } void TrainingWidget::rebuildPages() { @@ -185,9 +188,9 @@ void TrainingWidget::buildTechniqueSelectionPage() { auto* page = new QWidget; auto* layout = new QVBoxLayout(page); - auto* title = new QLabel(QString("

%1

").arg(qstr(loc(TrainingTitle)))); + auto* title = new QLabel(QString("

%1

").arg(qstr(core::loc("Sudoku", "Training Mode")))); layout->addWidget(title); - layout->addWidget(new QLabel(qstr(loc(TrainingSelectTechnique)))); + layout->addWidget(new QLabel(qstr(core::loc("Sudoku", "Select a technique to practice:")))); // clang-format off struct TechniqueGroup { @@ -196,24 +199,24 @@ void TrainingWidget::buildTechniqueSelectionPage() { }; std::array groups = {{ - {.name = qstr(loc(TrainingGroupFoundations)), .techniques = {core::SolvingTechnique::NakedSingle, core::SolvingTechnique::HiddenSingle}}, - {.name = qstr(loc(TrainingGroupSubsetBasics)), .techniques = {core::SolvingTechnique::NakedPair, core::SolvingTechnique::NakedTriple, + {.name = qstr(core::loc("Sudoku", "Foundations")), .techniques = {core::SolvingTechnique::NakedSingle, core::SolvingTechnique::HiddenSingle}}, + {.name = qstr(core::loc("Sudoku", "Subset Basics")), .techniques = {core::SolvingTechnique::NakedPair, core::SolvingTechnique::NakedTriple, core::SolvingTechnique::HiddenPair, core::SolvingTechnique::HiddenTriple}}, - {.name = qstr(loc(TrainingGroupIntersections)), .techniques = {core::SolvingTechnique::PointingPair, core::SolvingTechnique::BoxLineReduction, + {.name = qstr(core::loc("Sudoku", "Intersections & Quads")), .techniques = {core::SolvingTechnique::PointingPair, core::SolvingTechnique::BoxLineReduction, core::SolvingTechnique::NakedQuad, core::SolvingTechnique::HiddenQuad}}, - {.name = qstr(loc(TrainingGroupBasicFish)), .techniques = {core::SolvingTechnique::XWing, core::SolvingTechnique::XYWing, + {.name = qstr(core::loc("Sudoku", "Basic Fish & Wings")), .techniques = {core::SolvingTechnique::XWing, core::SolvingTechnique::XYWing, core::SolvingTechnique::Swordfish, core::SolvingTechnique::Skyscraper, core::SolvingTechnique::TwoStringKite, core::SolvingTechnique::XYZWing}}, - {.name = qstr(loc(TrainingGroupLinks)), .techniques = {core::SolvingTechnique::UniqueRectangle, core::SolvingTechnique::WWing, + {.name = qstr(core::loc("Sudoku", "Links & Rectangles")), .techniques = {core::SolvingTechnique::UniqueRectangle, core::SolvingTechnique::WWing, core::SolvingTechnique::SimpleColoring, core::SolvingTechnique::FinnedXWing, core::SolvingTechnique::RemotePairs, core::SolvingTechnique::BUG}}, - {.name = qstr(loc(TrainingGroupAdvancedFish)), .techniques = {core::SolvingTechnique::Jellyfish, core::SolvingTechnique::FinnedSwordfish, + {.name = qstr(core::loc("Sudoku", "Advanced Fish & Wings")), .techniques = {core::SolvingTechnique::Jellyfish, core::SolvingTechnique::FinnedSwordfish, core::SolvingTechnique::EmptyRectangle, core::SolvingTechnique::WXYZWing, core::SolvingTechnique::MultiColoring}}, - {.name = qstr(loc(TrainingGroupFinnedFish)), .techniques = {core::SolvingTechnique::FinnedJellyfish}}, - {.name = qstr(loc(TrainingGroupChains)), .techniques = {core::SolvingTechnique::XYChain, core::SolvingTechnique::ALSxZ, + {.name = qstr(core::loc("Sudoku", "Advanced Fish (Finned)")), .techniques = {core::SolvingTechnique::FinnedJellyfish}}, + {.name = qstr(core::loc("Sudoku", "Chains & Set Logic")), .techniques = {core::SolvingTechnique::XYChain, core::SolvingTechnique::ALSxZ, core::SolvingTechnique::SueDeCoq}}, - {.name = qstr(loc(TrainingGroupInference)), .techniques = {core::SolvingTechnique::ForcingChain, core::SolvingTechnique::NiceLoop}}, + {.name = qstr(core::loc("Sudoku", "Inference Engines")), .techniques = {core::SolvingTechnique::ForcingChain, core::SolvingTechnique::NiceLoop}}, }}; // clang-format on @@ -225,7 +228,7 @@ void TrainingWidget::buildTechniqueSelectionPage() { for (auto technique : group.techniques) { auto name = core::getTechniqueName(technique); auto points = core::getTechniqueRating(technique); - auto label = locFormat(TrainingPointsFmt, name, points); + auto label = core::locFormat(core::loc("Sudoku", "{0} ({1} pts)"), name, points); auto* btn = new QPushButton(QString::fromStdString(label)); btn->setFlat(true); @@ -243,7 +246,7 @@ void TrainingWidget::buildTechniqueSelectionPage() { layout->addWidget(group_box); } - auto* back_btn = new QPushButton(qstr(loc(TrainingBackToGame))); + auto* back_btn = new QPushButton(qstr(core::loc("Sudoku", "Back to Game"))); connect(back_btn, &QPushButton::clicked, this, &TrainingWidget::backToGame); layout->addWidget(back_btn); @@ -259,12 +262,12 @@ void TrainingWidget::buildTechniqueSelectionPage() { // Build set of applicable techniques in analysis mode std::set applicable_techniques; if (training_vm_->isAnalysisMode()) { - title->setText(QString("

%1

").arg(qstr(loc(TrainingAnalyzeTitle)))); + title->setText(QString("

%1

").arg(qstr(core::loc("Sudoku", "Analyze Position")))); for (const auto& step : training_vm_->analysisSteps()) { applicable_techniques.insert(step.technique); } } else { - title->setText(QString("

%1

").arg(qstr(loc(TrainingTitle)))); + title->setText(QString("

%1

").arg(qstr(core::loc("Sudoku", "Training Mode")))); } auto stats_mgr = training_vm_->statsManager(); @@ -286,21 +289,20 @@ void TrainingWidget::buildTechniqueSelectionPage() { bool applicable = applicable_techniques.contains(technique); btn->setEnabled(applicable); btn->setVisible(applicable); - btn->setToolTip(applicable ? qstr(loc(TrainingApplicable)) : ""); + btn->setToolTip(applicable ? qstr(core::loc("Sudoku", "Applicable at current position")) : ""); } else { btn->setVisible(true); auto mastery = stats_mgr ? stats_mgr->getMastery(technique) : core::MasteryLevel::Beginner; bool is_recommended = recommended.has_value() && *recommended == technique; - auto loc_fn = [this](std::string_view key) { return loc(key); }; - btn->setText(QString::fromStdString(formatTechniqueLabel(technique, mastery, is_recommended, loc_fn))); + btn->setText(QString::fromStdString(formatTechniqueLabel(technique, mastery, is_recommended))); bool prereqs_met = !stats_mgr || core::arePrerequisitesMet(technique, *stats_mgr); btn->setEnabled(prereqs_met); if (!prereqs_met) { - btn->setToolTip(qstr(loc(TrainingPrereqNotMet))); + btn->setToolTip(qstr(core::loc("Sudoku", "Prerequisites not met"))); } else if (is_recommended) { - btn->setToolTip(qstr(loc(TrainingRecommended))); + btn->setToolTip(qstr(core::loc("Sudoku", "Recommended next technique"))); } else { btn->setToolTip(""); } @@ -335,21 +337,21 @@ void TrainingWidget::buildTheoryPage() { prereqs_label->setStyleSheet(QString("color: %1; font-style: italic;").arg(StyleColors::TEXT_SUBTLE)); layout->addWidget(prereqs_label); - layout->addWidget(new QLabel(QString("%1").arg(qstr(loc(TrainingWhatItIs))))); + layout->addWidget(new QLabel(QString("%1").arg(qstr(core::loc("Sudoku", "What It Is:"))))); auto* what_label = new QLabel; what_label->setObjectName("theoryWhat"); what_label->setWordWrap(true); layout->addWidget(what_label); - layout->addWidget(new QLabel(QString("%1").arg(qstr(loc(TrainingWhatToLookFor))))); + layout->addWidget(new QLabel(QString("%1").arg(qstr(core::loc("Sudoku", "What to Look For:"))))); auto* look_label = new QLabel; look_label->setObjectName("theoryLook"); look_label->setWordWrap(true); layout->addWidget(look_label); auto* btn_layout = new QHBoxLayout; - auto* start_btn = new QPushButton(qstr(loc(TrainingStartExercises))); - auto* back_btn = new QPushButton(qstr(loc(TrainingBack))); + auto* start_btn = new QPushButton(qstr(core::loc("Sudoku", "Start Exercises"))); + auto* back_btn = new QPushButton(qstr(core::loc("Sudoku", "Back"))); btn_layout->addWidget(start_btn); btn_layout->addWidget(back_btn); btn_layout->addStretch(); @@ -378,13 +380,14 @@ void TrainingWidget::buildTheoryPage() { auto points = core::getTechniqueRating(state.current_technique); title_label->setText(QString::fromUtf8(desc.title.data(), static_cast(desc.title.size()))); - points_label->setText(QString::fromStdString(locFormat(TrainingDifficultyPoints, points))); + points_label->setText( + QString::fromStdString(core::locFormat(core::loc("Sudoku", "{0} difficulty points"), points))); auto prereqs = core::getPrerequisites(state.current_technique); if (prereqs.empty()) { prereqs_label->hide(); } else { - std::string prereq_text(loc(TrainingPrerequisites)); + std::string prereq_text(core::loc("Sudoku", "Prerequisites: ")); for (size_t i = 0; i < prereqs.size(); ++i) { if (i > 0) { prereq_text += ", "; @@ -419,16 +422,13 @@ void TrainingWidget::buildExercisePage() { // Number pad number_pad_ = new TrainingNumberPad; - if (loc_manager_) { - number_pad_->setLocalizationManager(loc_manager_); - } layout->addWidget(number_pad_); // Color palette (hidden by default, shown only for Coloring exercises) color_palette_ = new QWidget; auto* color_layout = new QHBoxLayout(color_palette_); color_layout->setContentsMargins(0, 0, 0, 0); - auto* color_label = new QLabel(qstr(loc(TrainingColor))); + auto* color_label = new QLabel(qstr(core::loc("Sudoku", "Color:"))); color_a_btn_ = new QPushButton("A"); color_a_btn_->setFixedSize(44, 44); color_a_btn_->setStyleSheet( @@ -457,14 +457,14 @@ void TrainingWidget::buildExercisePage() { // Action buttons auto* btn_layout = new QHBoxLayout; - undo_btn_ = new QPushButton(qstr(loc(ButtonUndo))); - redo_btn_ = new QPushButton(qstr(loc(ButtonRedo))); + undo_btn_ = new QPushButton(qstr(core::loc("Sudoku", "Undo"))); + redo_btn_ = new QPushButton(qstr(core::loc("Sudoku", "Redo"))); undo_btn_->setEnabled(false); redo_btn_->setEnabled(false); - auto* submit_btn = new QPushButton(qstr(loc(TrainingSubmit))); - auto* hint_btn = new QPushButton(qstr(loc(TrainingHint))); - auto* skip_btn = new QPushButton(qstr(loc(TrainingSkip))); - auto* quit_btn = new QPushButton(qstr(loc(TrainingQuitLesson))); + auto* submit_btn = new QPushButton(qstr(core::loc("Sudoku", "Submit"))); + auto* hint_btn = new QPushButton(qstr(core::loc("Sudoku", "Hint"))); + auto* skip_btn = new QPushButton(qstr(core::loc("Sudoku", "Skip"))); + auto* quit_btn = new QPushButton(qstr(core::loc("Sudoku", "Quit Lesson"))); btn_layout->addWidget(undo_btn_); btn_layout->addWidget(redo_btn_); btn_layout->addWidget(submit_btn); @@ -611,8 +611,8 @@ void TrainingWidget::buildExercisePage() { } const auto& state = training_vm_->trainingState.get(); auto name = core::getTechniqueName(state.current_technique); - header->setText(QString::fromStdString( - locFormat(TrainingExerciseHeader, state.exercise_index + 1, state.total_exercises, name))); + header->setText(QString::fromStdString(core::locFormat(core::loc("Sudoku", "Exercise {0} / {1} - {2}"), + state.exercise_index + 1, state.total_exercises, name))); // Show/hide color palette based on interaction mode const auto& exercises = training_vm_->exercises(); @@ -672,10 +672,10 @@ void TrainingWidget::buildFeedbackPage() { layout->addWidget(feedback_board_, 1); auto* btn_layout = new QHBoxLayout; - auto* next_btn = new QPushButton(qstr(loc(TrainingNextExercise))); - auto* retry_btn = new QPushButton(qstr(loc(TrainingRetry))); - auto* solution_btn = new QPushButton(qstr(loc(TrainingShowSolution))); - auto* quit_btn = new QPushButton(qstr(loc(TrainingQuitLesson))); + auto* next_btn = new QPushButton(qstr(core::loc("Sudoku", "Next Exercise"))); + auto* retry_btn = new QPushButton(qstr(core::loc("Sudoku", "Retry"))); + auto* solution_btn = new QPushButton(qstr(core::loc("Sudoku", "Show Solution"))); + auto* quit_btn = new QPushButton(qstr(core::loc("Sudoku", "Quit Lesson"))); btn_layout->addWidget(next_btn); btn_layout->addWidget(retry_btn); btn_layout->addWidget(solution_btn); @@ -714,15 +714,15 @@ void TrainingWidget::buildFeedbackPage() { QString color; switch (state.last_result) { case core::AnswerResult::Correct: - result_text = qstr(loc(TrainingCorrect)); + result_text = qstr(core::loc("Sudoku", "Correct!")); color = QString("color: %1;").arg(StyleColors::SUCCESS); break; case core::AnswerResult::PartiallyCorrect: - result_text = qstr(loc(TrainingPartiallyCorrect)); + result_text = qstr(core::loc("Sudoku", "Partially Correct")); color = QString("color: %1;").arg(StyleColors::WARNING); break; case core::AnswerResult::Incorrect: - result_text = qstr(loc(TrainingIncorrect)); + result_text = qstr(core::loc("Sudoku", "Incorrect")); color = QString("color: %1;").arg(StyleColors::ERROR_COLOR); break; } @@ -730,8 +730,8 @@ void TrainingWidget::buildFeedbackPage() { result_label->setText(result_text); result_label->setStyleSheet(QString("font-size: 24px; font-weight: bold; %1").arg(color)); msg_label->setText(QString::fromStdString(state.feedback_message)); - score_label->setText( - QString::fromStdString(locFormat(TrainingScore, state.correct_count, state.exercise_index + 1))); + score_label->setText(QString::fromStdString( + core::locFormat(core::loc("Sudoku", "Score: {0} / {1}"), state.correct_count, state.exercise_index + 1))); // Update feedback board with diff highlights if (feedback_board_) { @@ -747,7 +747,7 @@ void TrainingWidget::buildLessonCompletePage() { auto* page = new QWidget; auto* layout = new QVBoxLayout(page); - auto* title = new QLabel(QString("

%1

").arg(qstr(loc(TrainingLessonComplete)))); + auto* title = new QLabel(QString("

%1

").arg(qstr(core::loc("Sudoku", "Lesson Complete!")))); layout->addWidget(title); auto* info_label = new QLabel; @@ -759,9 +759,9 @@ void TrainingWidget::buildLessonCompletePage() { layout->addWidget(verdict_label); auto* btn_layout = new QHBoxLayout; - auto* again_btn = new QPushButton(qstr(loc(TrainingTryAgain))); - auto* pick_btn = new QPushButton(qstr(loc(TrainingPickTechnique))); - auto* game_btn = new QPushButton(qstr(loc(TrainingReturnToGame))); + auto* again_btn = new QPushButton(qstr(core::loc("Sudoku", "Try Again"))); + auto* pick_btn = new QPushButton(qstr(core::loc("Sudoku", "Pick Technique"))); + auto* game_btn = new QPushButton(qstr(core::loc("Sudoku", "Return to Game"))); btn_layout->addWidget(again_btn); btn_layout->addWidget(pick_btn); btn_layout->addWidget(game_btn); @@ -793,31 +793,33 @@ void TrainingWidget::buildLessonCompletePage() { const auto& state = training_vm_->trainingState.get(); auto name = core::getTechniqueName(state.current_technique); - auto technique_str = locFormat(TrainingTechnique, name); - auto score_str = locFormat(TrainingScore, state.correct_count, state.total_exercises); - auto hints_str = locFormat(TrainingHintsUsed, state.hints_used); + auto technique_str = core::locFormat(core::loc("Sudoku", "Technique: {0}"), name); + auto score_str = + core::locFormat(core::loc("Sudoku", "Score: {0} / {1}"), state.correct_count, state.total_exercises); + auto hints_str = core::locFormat(core::loc("Sudoku", "Hints used: {0}"), state.hints_used); info_label->setText(QString::fromStdString(fmt::format("{}\n{}\n{}", technique_str, score_str, hints_str))); // Show mastery level if stats manager is available auto stats_mgr = training_vm_->statsManager(); if (stats_mgr) { auto mastery = stats_mgr->getMastery(state.current_technique); - std::string_view mastery_text = loc(MasteryBeginner); + std::string mastery_text = core::loc("Sudoku", "Beginner"); switch (mastery) { case core::MasteryLevel::Beginner: - mastery_text = loc(MasteryBeginner); + mastery_text = core::loc("Sudoku", "Beginner"); break; case core::MasteryLevel::Intermediate: - mastery_text = loc(MasteryIntermediate); + mastery_text = core::loc("Sudoku", "Intermediate"); break; case core::MasteryLevel::Proficient: - mastery_text = loc(MasteryProficient); + mastery_text = core::loc("Sudoku", "Proficient"); break; case core::MasteryLevel::Mastered: - mastery_text = loc(MasteryMastered); + mastery_text = core::loc("Sudoku", "Mastered"); break; } - verdict_label->setText(QString::fromStdString(locFormat(TrainingMastery, mastery_text))); + verdict_label->setText( + QString::fromStdString(core::locFormat(core::loc("Sudoku", "Mastery: {0}"), mastery_text))); verdict_label->setStyleSheet( mastery == core::MasteryLevel::Mastered ? QString("color: %1; font-weight: bold;").arg(StyleColors::SUCCESS) @@ -827,13 +829,13 @@ void TrainingWidget::buildLessonCompletePage() { ? static_cast(state.correct_count) / static_cast(state.total_exercises) : 0.0f; if (ratio >= 0.8f) { - verdict_label->setText(qstr(loc(TrainingExcellent))); + verdict_label->setText(qstr(core::loc("Sudoku", "Excellent! You've mastered this technique."))); verdict_label->setStyleSheet(QString("color: %1; font-weight: bold;").arg(StyleColors::SUCCESS)); } else if (ratio >= 0.5f) { - verdict_label->setText(qstr(loc(TrainingGoodProgress))); + verdict_label->setText(qstr(core::loc("Sudoku", "Good progress. Try again for a higher score."))); verdict_label->setStyleSheet(QString("color: %1; font-weight: bold;").arg(StyleColors::WARNING)); } else { - verdict_label->setText(qstr(loc(TrainingKeepPracticing))); + verdict_label->setText(qstr(core::loc("Sudoku", "Keep practicing! Review the theory and try again."))); verdict_label->setStyleSheet(QString("color: %1; font-weight: bold;").arg(StyleColors::ERROR_COLOR)); } } diff --git a/src/view/training_widget.h b/src/view/training_widget.h index 9d25cf8..8dfb1b8 100644 --- a/src/view/training_widget.h +++ b/src/view/training_widget.h @@ -17,7 +17,6 @@ #pragma once #include "../view_model/training_view_model.h" -#include "core/i_localization_manager.h" #include "core/observable.h" #include @@ -54,30 +53,20 @@ class TrainingWidget : public QWidget { TrainingWidget& operator=(TrainingWidget&&) = delete; void setTrainingViewModel(std::shared_ptr training_vm); - void setLocalizationManager(std::shared_ptr loc_manager); + +protected: + void changeEvent(QEvent* event) override; signals: void backToGame(); private: std::shared_ptr training_vm_; - std::shared_ptr loc_manager_; core::CompositeObserver observer_; QStackedWidget* pages_{nullptr}; - - [[nodiscard]] std::string_view loc(std::string_view key) const { - return loc_manager_ ? loc_manager_->getString(key) : key; - } - [[nodiscard]] static QString qstr(std::string_view sv) { return QString::fromUtf8(sv.data(), static_cast(sv.size())); } - - template - [[nodiscard]] std::string locFormat(std::string_view key, Args&&... args) const { - return fmt::format(fmt::runtime(loc(key)), std::forward(args)...); - } - // Exercise page widgets (owned by Qt parent) SudokuBoardWidget* training_board_{nullptr}; SudokuBoardWidget* feedback_board_{nullptr}; diff --git a/src/view_model/game_view_model.cpp b/src/view_model/game_view_model.cpp index 23e5b58..e8bea99 100644 --- a/src/view_model/game_view_model.cpp +++ b/src/view_model/game_view_model.cpp @@ -19,14 +19,13 @@ #include "../core/solving_technique.h" #include "core/board_utils.h" #include "core/constants.h" +#include "core/i18n_helpers.h" #include "core/i_game_validator.h" -#include "core/i_localization_manager.h" #include "core/i_puzzle_generator.h" #include "core/i_save_manager.h" #include "core/i_statistics_manager.h" #include "core/i_sudoku_solver.h" #include "core/observable.h" -#include "core/string_keys.h" #include "model/game_state.h" #include @@ -42,13 +41,12 @@ GameViewModel::GameViewModel(std::shared_ptr validator, std::shared_ptr solver, std::shared_ptr stats_manager, std::shared_ptr save_manager, - std::shared_ptr loc_manager, std::shared_ptr settings_manager) : gameState(model::GameState{}), uiState(UIState{}), statistics(StatsDisplay{}), recentSaves(std::vector{}), errorMessage(std::string{}), hintMessage(std::string{}), coachingState(viewmodel::CoachingState{}), validator_(std::move(validator)), generator_(std::move(generator)), solver_(std::move(solver)), stats_manager_(std::move(stats_manager)), save_manager_(std::move(save_manager)), - loc_manager_(std::move(loc_manager)), settings_manager_(std::move(settings_manager)) { + settings_manager_(std::move(settings_manager)) { // Apply initial settings if available if (settings_manager_) { const auto& settings = settings_manager_->getSettings(); @@ -68,30 +66,22 @@ GameViewModel::GameViewModel(std::shared_ptr validator, refreshRecentSaves(); } -std::string_view GameViewModel::statisticsErrorToString(core::StatisticsError error) const { - using core::StringKeys::StatsErrFileAccess; - using core::StringKeys::StatsErrGameAlreadyEnded; - using core::StringKeys::StatsErrGameNotStarted; - using core::StringKeys::StatsErrInvalidData; - using core::StringKeys::StatsErrInvalidDifficulty; - using core::StringKeys::StatsErrSerialization; - using core::StringKeys::StatsErrUnknown; - +std::string GameViewModel::statisticsErrorToString(core::StatisticsError error) const { switch (error) { case core::StatisticsError::InvalidGameData: - return loc(StatsErrInvalidData); + return core::loc("Sudoku", "Invalid game data"); case core::StatisticsError::FileAccessError: - return loc(StatsErrFileAccess); + return core::loc("Sudoku", "File access error"); case core::StatisticsError::SerializationError: - return loc(StatsErrSerialization); + return core::loc("Sudoku", "Serialization error"); case core::StatisticsError::InvalidDifficulty: - return loc(StatsErrInvalidDifficulty); + return core::loc("Sudoku", "Invalid difficulty"); case core::StatisticsError::GameNotStarted: - return loc(StatsErrGameNotStarted); + return core::loc("Sudoku", "Game not started"); case core::StatisticsError::GameAlreadyEnded: - return loc(StatsErrGameAlreadyEnded); + return core::loc("Sudoku", "Game already ended"); default: - return loc(StatsErrUnknown); + return core::loc("Sudoku", "Unknown statistics error"); } } @@ -111,7 +101,7 @@ void GameViewModel::startNewGame(core::Difficulty difficulty) { auto puzzle_result = generator_->generatePuzzle(settings); if (!puzzle_result) { - handleError(loc(core::StringKeys::ErrorGeneratePuzzle)); + handleError(core::loc("Sudoku", "Failed to generate puzzle")); return; } @@ -194,7 +184,7 @@ void GameViewModel::loadGame(const std::string& save_id) { auto load_result = save_manager_->loadGame(save_id); if (!load_result) { - handleError(loc(core::StringKeys::ErrorLoadGame)); + handleError(core::loc("Sudoku", "Failed to load game")); return; } @@ -291,7 +281,7 @@ bool GameViewModel::saveCurrentGame(const std::string& name) { spdlog::info("Saving current game: {}", name.empty() ? "auto-save" : name); if (!isGameActive()) { - handleError(loc(core::StringKeys::ErrorNoActiveGame)); + handleError(core::loc("Sudoku", "No active game to save")); return false; } @@ -328,7 +318,7 @@ bool GameViewModel::saveCurrentGame(const std::string& name) { auto save_result = save_manager_->saveGame(saved_game, settings); if (!save_result) { - handleError(loc(core::StringKeys::ErrorSaveGame)); + handleError(core::loc("Sudoku", "Failed to save game")); return false; } diff --git a/src/view_model/game_view_model.h b/src/view_model/game_view_model.h index 6e3fedf..16c7c7f 100644 --- a/src/view_model/game_view_model.h +++ b/src/view_model/game_view_model.h @@ -17,7 +17,6 @@ #pragma once #include "../core/i_game_validator.h" -#include "../core/i_localization_manager.h" #include "../core/i_puzzle_generator.h" #include "../core/i_save_manager.h" #include "../core/i_settings_manager.h" @@ -133,7 +132,6 @@ class GameViewModel { GameViewModel(std::shared_ptr validator, std::shared_ptr generator, std::shared_ptr solver, std::shared_ptr stats_manager, std::shared_ptr save_manager, - std::shared_ptr loc_manager, std::shared_ptr settings_manager = nullptr); ~GameViewModel() = default; @@ -259,19 +257,8 @@ class GameViewModel { std::shared_ptr solver_; std::shared_ptr stats_manager_; std::shared_ptr save_manager_; - std::shared_ptr loc_manager_; std::shared_ptr settings_manager_; - // Localization helpers - [[nodiscard]] std::string_view loc(std::string_view key) const { - return loc_manager_->getString(key); - } - - template - [[nodiscard]] std::string locFormat(std::string_view key, Args&&... args) const { - return fmt::format(fmt::runtime(loc_manager_->getString(key)), std::forward(args)...); - } - // Internal state uint64_t current_game_session_{0}; std::vector move_history_; @@ -304,7 +291,7 @@ class GameViewModel { // Hint system helpers [[nodiscard]] std::string formatHintExplanation(const core::SolveStep& step) const; - [[nodiscard]] std::string_view statisticsErrorToString(core::StatisticsError error) const; + [[nodiscard]] std::string statisticsErrorToString(core::StatisticsError error) const; // Coaching hint state — step and snapshot are always set/cleared together struct CoachingContext { diff --git a/src/view_model/game_view_model_hints.cpp b/src/view_model/game_view_model_hints.cpp index e52be3b..5ebdb17 100644 --- a/src/view_model/game_view_model_hints.cpp +++ b/src/view_model/game_view_model_hints.cpp @@ -16,9 +16,9 @@ #include "../core/localized_explanations.h" #include "../core/solving_technique.h" -#include "../core/string_keys.h" #include "../core/technique_descriptions.h" #include "../core/training_hints.h" +#include "core/i18n_helpers.h" #include "core/i_game_validator.h" #include "core/i_statistics_manager.h" #include "core/i_sudoku_solver.h" @@ -128,7 +128,7 @@ void GameViewModel::getHint(std::optional pos_opt) { const int hints_remaining = getHintCount(); if (!isGameActive() || hints_remaining <= 0) { if (hints_remaining <= 0) { - errorMessage.set(std::string(loc(core::StringKeys::HintNoRemaining))); + errorMessage.set(std::string(core::loc("Sudoku", "No hints remaining (0/10 used)"))); } return; } @@ -139,19 +139,19 @@ void GameViewModel::getHint(std::optional pos_opt) { const auto& state = gameState.get(); if (!pos_opt.has_value()) { - errorMessage.set(std::string(loc(core::StringKeys::HintSelectCell))); + errorMessage.set(std::string(core::loc("Sudoku", "Please select a cell first"))); return; // Don't consume hint } const auto& pos = *pos_opt; if (state.isGiven(pos)) { - errorMessage.set(std::string(loc(core::StringKeys::HintCannotRevealGiven))); + errorMessage.set(std::string(core::loc("Sudoku", "Cannot reveal hint for given cells"))); return; // Don't consume hint } if (state.getValue(pos) != 0) { - errorMessage.set(std::string(loc(core::StringKeys::HintCellHasValue))); + errorMessage.set(std::string(core::loc("Sudoku", "Cell already has a value"))); return; // Don't consume hint } @@ -163,7 +163,7 @@ void GameViewModel::getHint(std::optional pos_opt) { auto step_result = solver_->findNextStep(board, original_puzzle); if (!step_result.has_value()) { - errorMessage.set(std::string(loc(core::StringKeys::HintNoTechnique))); + errorMessage.set(std::string(core::loc("Sudoku", "No logical technique found for this puzzle"))); return; // Don't consume hint } @@ -204,17 +204,17 @@ std::string GameViewModel::formatHintExplanation(const core::SolveStep& step) co std::string message; // Technique name header - message += std::string(core::getLocalizedTechniqueName(*loc_manager_, step.technique)); + message += std::string(core::getLocalizedTechniqueName(step.technique)); message += ":\n\n"; // Explanation from strategy (localized) - message += core::getLocalizedExplanation(*loc_manager_, step); + message += core::getLocalizedExplanation(step); // Add placement suggestion if applicable if (step.type == core::SolveStepType::Placement) { message += "\n\n"; - message += - locFormat(core::StringKeys::HintSuggestionPlace, step.value, step.position.row + 1, step.position.col + 1); + message += core::locFormat(core::loc("Sudoku", "Suggestion: Place {0} at R{1}C{2}"), step.value, + step.position.row + 1, step.position.col + 1); } return message; @@ -239,7 +239,7 @@ void GameViewModel::requestCoachingHint() { const int hints_remaining = getHintCount(); if (!isGameActive() || hints_remaining <= 0) { if (hints_remaining <= 0) { - errorMessage.set(std::string(loc(core::StringKeys::CoachingNoRemaining))); + errorMessage.set(std::string(core::loc("Sudoku", "No coaching hints remaining"))); } return; } @@ -259,7 +259,7 @@ void GameViewModel::requestCoachingHint() { auto step_result = solver_->findNextStep(board, original_puzzle); if (!step_result.has_value()) { - errorMessage.set(std::string(loc(core::StringKeys::CoachingNoTechnique))); + errorMessage.set(std::string(core::loc("Sudoku", "No logical technique found"))); return; } coaching_context_ = CoachingContext{.step = step_result.value(), .snapshot = state}; @@ -277,7 +277,7 @@ void GameViewModel::requestCoachingHint() { const auto& step = coaching_context_->step; // Get progressive hint from training infrastructure - auto hint = core::getTrainingHint(*loc_manager_, step.technique, new_level, step); + auto hint = core::getTrainingHint(step.technique, new_level, step); // For level 1, prepend technique description with what_to_look_for if (new_level == 1) { @@ -296,7 +296,8 @@ void GameViewModel::requestCoachingHint() { if (new_level == 3) { new_state.phase = CoachingPhase::TryIt; new_state.message += "\n\n"; - new_state.message += std::string(loc(core::StringKeys::CoachingTryIt)); + new_state.message += + std::string(core::loc("Sudoku", "Try applying this step yourself, then press Check to verify.")); coaching_context_->snapshot = gameState.get(); } @@ -321,7 +322,7 @@ void GameViewModel::navigateCoachingLevel(int direction) { return; } const auto& step = coaching_context_->step; - auto hint = core::getTrainingHint(*loc_manager_, step.technique, target_level, step); + auto hint = core::getTrainingHint(step.technique, target_level, step); // For level 1, prepend technique description with what_to_look_for if (target_level == 1) { @@ -367,13 +368,15 @@ void GameViewModel::checkCoachingAnswer() { const int total = result.correct + result.missed; std::string message; if (result.wrong > 0) { - message = locFormat(core::StringKeys::CoachingCheckWrong, result.correct, total, result.wrong); + message = core::locFormat(core::loc("Sudoku", "Some actions were incorrect. {0}/{1} correct, {2} wrong."), + result.correct, total, result.wrong); } else if (result.missed == 0 && result.correct > 0) { - message = locFormat(core::StringKeys::CoachingCheckCorrect, result.correct, total); + message = core::locFormat(core::loc("Sudoku", "Correct! You found all {0}/{1}."), result.correct, total); } else if (result.correct > 0) { - message = locFormat(core::StringKeys::CoachingCheckPartial, result.correct, total, result.missed); + message = + core::locFormat(core::loc("Sudoku", "{0}/{1} correct, {2} missed."), result.correct, total, result.missed); } else { - message = locFormat(core::StringKeys::CoachingCheckZero, total); + message = core::locFormat(core::loc("Sudoku", "0/{0} correct — try making some changes first."), total); } CoachingState new_state; @@ -457,13 +460,13 @@ void GameViewModel::resetCoachingIfNotTryIt() { } std::string GameViewModel::buildLevel1Message(const core::TrainingHint& hint, const core::SolveStep& step) const { - auto desc = core::getTechniqueDescription(*loc_manager_, step.technique); + auto desc = core::getTechniqueDescription(step.technique); std::string message; - message += std::string(core::getLocalizedTechniqueName(*loc_manager_, step.technique)); + message += std::string(core::getLocalizedTechniqueName(step.technique)); message += "\n\n"; message += std::string(desc.what_it_is); message += "\n\n"; - message += std::string(loc(core::StringKeys::CoachingWhatToLookFor)); + message += std::string(core::loc("Sudoku", "What to look for: ")); message += std::string(desc.what_to_look_for); message += "\n\n"; message += hint.text; diff --git a/src/view_model/game_view_model_state.cpp b/src/view_model/game_view_model_state.cpp index 1e2072b..fbcaa1c 100644 --- a/src/view_model/game_view_model_state.cpp +++ b/src/view_model/game_view_model_state.cpp @@ -15,8 +15,8 @@ // along with this program. If not, see . #include "../core/solving_technique.h" -#include "../core/string_keys.h" #include "core/constants.h" +#include "core/i18n_helpers.h" #include "core/i_game_validator.h" #include "core/i_save_manager.h" #include "core/i_statistics_manager.h" @@ -221,13 +221,13 @@ std::vector GameViewModel::formatTechniques(const std::set result; result.reserve(sorted.size() + (requires_backtracking ? 1 : 0)); for (const auto& tech : sorted) { - result.push_back(locFormat(core::StringKeys::TechniquePointsFmt, - std::string(core::getLocalizedTechniqueName(*loc_manager_, tech)), - fmt::format("{:.1f}", core::getTechniqueRating(tech)))); + result.push_back(core::locFormat(core::loc("Sudoku", "{0} (SE {1})"), + std::string(core::getLocalizedTechniqueName(tech)), + fmt::format("{:.1f}", core::getTechniqueRating(tech)))); } if (requires_backtracking) { - result.emplace_back(loc(core::StringKeys::TechniqueBacktracking)); + result.emplace_back(core::loc("Sudoku", "Backtracking (trial & error)")); } return result; @@ -326,7 +326,7 @@ StatsDisplay GameViewModel::createStatsDisplay(const core::AggregateStats& stats std::chrono::milliseconds weighted_average = total_average_time / total_completed_games; display.average_time = formatTime(weighted_average); } else { - display.average_time = std::string(loc(core::StringKeys::StatsTimeNa)); + display.average_time = std::string(core::loc("Sudoku", "N/A")); } // Find overall best time across all difficulties @@ -338,7 +338,7 @@ StatsDisplay GameViewModel::createStatsDisplay(const core::AggregateStats& stats } } display.best_time = - (best != std::chrono::milliseconds::max()) ? formatTime(best) : std::string(loc(core::StringKeys::StatsTimeNa)); + (best != std::chrono::milliseconds::max()) ? formatTime(best) : std::string(core::loc("Sudoku", "N/A")); return display; } @@ -366,7 +366,7 @@ void GameViewModel::setShowHints(bool show) { void GameViewModel::exportStatistics(const std::string& file_path) { auto export_result = stats_manager_->exportStats(file_path); if (!export_result) { - handleError(std::string(loc(core::StringKeys::ErrorExportStats))); + handleError(std::string(core::loc("Sudoku", "Failed to export statistics"))); } } @@ -379,7 +379,7 @@ std::expected GameViewModel::exportAggregateStatsCsv() const // Export aggregate statistics auto result = stats_manager_->exportAggregateStatsCsv(file_path.string()); if (!result) { - std::string error_msg(loc(core::StringKeys::ErrorExportAggregate)); + std::string error_msg(core::loc("Sudoku", "Failed to export aggregate stats")); error_msg += ": "; error_msg += statisticsErrorToString(result.error()); return std::unexpected(error_msg); @@ -398,7 +398,7 @@ std::expected GameViewModel::exportGameSessionsCsv() const { // Export game sessions auto result = stats_manager_->exportGameSessionsCsv(file_path.string()); if (!result) { - std::string error_msg(loc(core::StringKeys::ErrorExportSessions)); + std::string error_msg(core::loc("Sudoku", "Failed to export game sessions")); error_msg += ": "; error_msg += statisticsErrorToString(result.error()); return std::unexpected(error_msg); diff --git a/src/view_model/game_view_model_undo.cpp b/src/view_model/game_view_model_undo.cpp index f38e753..81069b8 100644 --- a/src/view_model/game_view_model_undo.cpp +++ b/src/view_model/game_view_model_undo.cpp @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -#include "../core/string_keys.h" +#include "core/i18n_helpers.h" #include "core/i_game_validator.h" #include "core/i_statistics_manager.h" #include "core/observable.h" @@ -84,13 +84,14 @@ void GameViewModel::undoToLastValid() { // Check if we have a recorded valid state if (last_valid_state_index_ < 0) { uiState.update( - [this](auto& ui) { ui.status_message = std::string(loc(core::StringKeys::StatusNoValidState)); }); + [this](auto& ui) { ui.status_message = std::string(core::loc("Sudoku", "No valid state in history")); }); return; } // Check if current state has errors (conflicts OR wrong values vs solution) if (!hasBoardErrors()) { - uiState.update([this](auto& ui) { ui.status_message = std::string(loc(core::StringKeys::StatusBoardValid)); }); + uiState.update( + [this](auto& ui) { ui.status_message = std::string(core::loc("Sudoku", "Board is already valid")); }); return; } @@ -99,7 +100,8 @@ void GameViewModel::undoToLastValid() { undo(); } - uiState.update([this](auto& ui) { ui.status_message = std::string(loc(core::StringKeys::StatusUndoneToValid)); }); + uiState.update( + [this](auto& ui) { ui.status_message = std::string(core::loc("Sudoku", "Undone to last valid state")); }); } bool GameViewModel::canUndo() const { @@ -138,8 +140,8 @@ void GameViewModel::checkSolution() { auto minutes = std::chrono::duration_cast(completion_time); auto seconds = std::chrono::duration_cast(completion_time - minutes); ui.status_message = - locFormat(core::StringKeys::StatusPuzzleCompleted, fmt::format("{:02d}", minutes.count()), - fmt::format("{:02d}", seconds.count())); + core::locFormat(core::loc("Sudoku", "Puzzle completed in {0}:{1}! New game started."), + fmt::format("{:02d}", minutes.count()), fmt::format("{:02d}", seconds.count())); }); spdlog::info("New game started automatically at same difficulty"); @@ -159,8 +161,9 @@ void GameViewModel::checkSolution() { } }); - uiState.update( - [this](auto& ui) { ui.status_message = std::string(loc(core::StringKeys::StatusSolutionErrors)); }); + uiState.update([this](auto& ui) { + ui.status_message = std::string(core::loc("Sudoku", "Solution has errors. Keep trying!")); + }); } } diff --git a/src/view_model/training_view_model.cpp b/src/view_model/training_view_model.cpp index 2d43ea4..07fc0f1 100644 --- a/src/view_model/training_view_model.cpp +++ b/src/view_model/training_view_model.cpp @@ -16,8 +16,8 @@ #include "training_view_model.h" +#include "core/i18n_helpers.h" #include "core/i_game_validator.h" -#include "core/i_localization_manager.h" #include "core/i_training_exercise_generator.h" #include "core/observable.h" #include "core/technique_descriptions.h" @@ -44,15 +44,14 @@ namespace sudoku::viewmodel { using namespace core; TrainingViewModel::TrainingViewModel(std::shared_ptr exercise_generator, - std::shared_ptr loc_manager, std::shared_ptr stats_manager) - : exercise_generator_(std::move(exercise_generator)), loc_manager_(std::move(loc_manager)), - stats_manager_(std::move(stats_manager)) { + : exercise_generator_(std::move(exercise_generator)), stats_manager_(std::move(stats_manager)) { } void TrainingViewModel::selectTechnique(SolvingTechnique technique) { if (technique == SolvingTechnique::Backtracking) { - errorMessage.set(std::string(loc(core::StringKeys::TrainingErrorBacktracking))); + errorMessage.set( + std::string(core::loc("Sudoku", "Cannot practice Backtracking — it is not a logical technique."))); return; } @@ -90,7 +89,7 @@ void TrainingViewModel::startExercises() { } } if (exercises_.empty()) { - errorMessage.set(std::string(loc(core::StringKeys::TrainingErrorNoStep))); + errorMessage.set(std::string(core::loc("Sudoku", "No applicable step found for this technique."))); return; } } else { @@ -144,7 +143,8 @@ void TrainingViewModel::submitAnswer() { // More steps remain — apply and continue applyContinue(feedback_step); found_step_count_++; - auto msg = locFormat(core::StringKeys::TrainingCorrectContinue, feedback_step.explanation); + auto msg = + core::locFormat(core::loc("Sudoku", "Correct! {0} Find the next one."), feedback_step.explanation); trainingState.update([&msg](TrainingUIState& s) { s.correct_count++; s.found_step_message = msg; @@ -185,7 +185,7 @@ void TrainingViewModel::requestHint() { const auto& expected = exercise.expected_step; int new_level = state.current_hint_level + 1; - auto hint = getTrainingHint(*loc_manager_, exercise.technique, new_level, expected); + auto hint = getTrainingHint(exercise.technique, new_level, expected); trainingState.update([new_level, &hint](TrainingUIState& s) { s.current_hint_level = new_level; @@ -394,13 +394,13 @@ void TrainingViewModel::revealSolution() { } const auto& exercise = exercises_[idx]; - auto hint = getTrainingHint(*loc_manager_, exercise.technique, 3, exercise.expected_step); + auto hint = getTrainingHint(exercise.technique, 3, exercise.expected_step); feedbackBoard.update([&hint](TrainingBoard& board) { applyHintHighlights(board, hint); }); } TechniqueDescription TrainingViewModel::currentDescription() const { - return getTechniqueDescription(*loc_manager_, trainingState.get().current_technique); + return getTechniqueDescription(trainingState.get().current_technique); } // --- Private helpers --- @@ -674,16 +674,15 @@ TrainingViewModel::EvalResult TrainingViewModel::evaluateElimination(const Train } std::string TrainingViewModel::buildFeedback(AnswerResult result, const SolveStep& step) const { - using namespace core::StringKeys; switch (result) { case AnswerResult::Correct: - return locFormat(TrainingFeedbackCorrect, step.explanation); + return core::locFormat(core::loc("Sudoku", "Correct! {0}"), step.explanation); case AnswerResult::PartiallyCorrect: - return locFormat(TrainingFeedbackPartial, step.explanation); + return core::locFormat(core::loc("Sudoku", "Partially correct. {0}"), step.explanation); case AnswerResult::Incorrect: - return locFormat(TrainingFeedbackIncorrect, step.explanation); + return core::locFormat(core::loc("Sudoku", "Not quite. {0}"), step.explanation); } - return std::string(loc(TrainingFeedbackUnknown)); + return std::string(core::loc("Sudoku", "Unknown result.")); } // NOLINTNEXTLINE(readability-function-cognitive-complexity) — builds diff board with placement/elimination logic; nesting is inherent diff --git a/src/view_model/training_view_model.h b/src/view_model/training_view_model.h index 6bf5ec3..c9726ac 100644 --- a/src/view_model/training_view_model.h +++ b/src/view_model/training_view_model.h @@ -16,7 +16,6 @@ #pragma once -#include "../core/i_localization_manager.h" #include "../core/i_training_exercise_generator.h" #include "../core/i_training_statistics_manager.h" #include "../core/observable.h" @@ -43,9 +42,8 @@ class TrainingViewModel { public: /// Constructor /// @param exercise_generator Exercise generation engine - /// @param loc_manager Localization manager for UI strings + /// @param stats_manager Optional statistics manager TrainingViewModel(std::shared_ptr exercise_generator, - std::shared_ptr loc_manager, std::shared_ptr stats_manager = nullptr); // --- Observable properties --- @@ -142,18 +140,8 @@ class TrainingViewModel { private: std::shared_ptr exercise_generator_; - std::shared_ptr loc_manager_; std::shared_ptr stats_manager_; - [[nodiscard]] std::string_view loc(std::string_view key) const { - return loc_manager_ ? loc_manager_->getString(key) : key; - } - - template - [[nodiscard]] std::string locFormat(std::string_view key, Args&&... args) const { - return fmt::format(fmt::runtime(loc(key)), std::forward(args)...); - } - // Exercise state std::vector exercises_; int initial_step_count_{0}; ///< Number of valid steps when exercise loaded diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f205659..14b29b9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -27,7 +27,7 @@ list(FILTER LIB_SOURCES EXCLUDE REGEX ".*simd_avx512_ops\\.cpp$") list(FILTER LIB_SOURCES EXCLUDE REGEX ".*/view/.*\\.(cpp|h)$") list(FILTER LIB_SOURCES EXCLUDE REGEX ".*log_manager\\.cpp$") -# Create a library from source files for testing (no Qt6 dependency) +# Create a library from source files for testing (links Qt6::Core for translations) add_library(sudoku_lib STATIC ${LIB_SOURCES}) set_target_properties(sudoku_lib PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF) @@ -45,6 +45,7 @@ target_link_libraries(sudoku_lib PUBLIC yaml-cpp::yaml-cpp ZLIB::ZLIB libsodium::libsodium + Qt6::Core ) # Platform-specific library settings diff --git a/tests/helpers/game_view_model_fixture.h b/tests/helpers/game_view_model_fixture.h index 2e39116..d5bf228 100644 --- a/tests/helpers/game_view_model_fixture.h +++ b/tests/helpers/game_view_model_fixture.h @@ -22,7 +22,6 @@ #include "../../src/core/statistics_manager.h" #include "../../src/core/sudoku_solver.h" #include "../../src/view_model/game_view_model.h" -#include "mock_localization_manager.h" #include "test_utils.h" #include @@ -38,7 +37,6 @@ struct GameViewModelFixture { std::shared_ptr solver; std::shared_ptr stats_manager; std::shared_ptr save_manager; - std::shared_ptr loc_manager; std::unique_ptr view_model; GameViewModelFixture() { @@ -47,9 +45,8 @@ struct GameViewModelFixture { solver = std::make_shared(validator); stats_manager = std::make_shared(temp_dir.path()); save_manager = std::make_shared(temp_dir.path()); - loc_manager = std::make_shared(); - view_model = std::make_unique(validator, generator, solver, stats_manager, - save_manager, loc_manager); + view_model = + std::make_unique(validator, generator, solver, stats_manager, save_manager); } }; diff --git a/tests/helpers/mock_localization_manager.h b/tests/helpers/mock_localization_manager.h deleted file mode 100644 index a18b29e..0000000 --- a/tests/helpers/mock_localization_manager.h +++ /dev/null @@ -1,145 +0,0 @@ -// sudoku-cpp - Offline Sudoku Game -// Copyright (C) 2025-2026 Alexander Bendlin (darkstar79) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#pragma once - -#include "core/i_localization_manager.h" - -#include -#include - -namespace sudoku::core { - -/// Lightweight mock for testing code that depends on ILocalizationManager. -/// Returns English format templates for keys used with fmt::runtime(), -/// and the key itself for all other keys (allowing tests to verify lookups). -class MockLocalizationManager final : public ILocalizationManager { -public: - MockLocalizationManager() { - // Keys that are used as fmt format templates need English values - // so fmt::runtime() doesn't crash on the raw key string. - // clang-format off - format_templates_ = { - // Position & region formatting - {"position.fmt", "R{0}C{1}"}, - {"region.row", "row"}, - {"region.column", "column"}, - {"region.box", "box"}, - {"region.unknown", "unknown region"}, - // Rating format - {"rating.format", "SE {0}"}, - // Coaching feedback messages - {"coaching.check_correct", "Correct! You found all {0}/{1}."}, - {"coaching.check_partial", "{0}/{1} correct, {2} missed."}, - {"coaching.check_wrong", "Some actions were incorrect. {0}/{1} correct, {2} wrong."}, - {"coaching.check_zero", "0/{0} correct — try making some changes first."}, - {"coaching.level_header", "Level {0}/3"}, - // Hint suggestion format - {"hint.suggestion_place", "Suggestion: Place {0} at R{1}C{2}"}, - // Training hints — Singles - {"hint.singles.l1", "Look at cell {0}."}, - {"hint.singles.l2_region", "Focus on {0} — count the candidates."}, - {"hint.singles.l2_no_region", "Count the candidates in cell {0}."}, - {"hint.singles.l3", "The value is {0}."}, - // Training hints — Subsets - {"hint.subsets.l1_region", "Focus on {0}."}, - {"hint.subsets.l1_no_region", "Look for cells that share the same candidates in a unit."}, - {"hint.subsets.l2_values", "These cells form a [{0}] subset. Values in the subset can only go in these cells — eliminate them from other cells in the region."}, - {"hint.subsets.l2_no_values", "These cells form the subset. Values in the subset can only go in these cells — eliminate them from other cells in the region."}, - {"hint.subsets.l3", "Eliminate candidates from cells that see all subset cells."}, - // Training hints — Intersections - {"hint.intersections.l1_value", "Look for value {0} confined to an intersection."}, - {"hint.intersections.l1_no_value", "Look for a candidate confined to the intersection of a box and a line."}, - {"hint.intersections.l2", "The intersection cells. The candidate is confined to these cells — eliminate it from other cells in the line or box outside this intersection."}, - {"hint.intersections.l3", "Eliminate the candidate from cells outside the intersection."}, - // Training hints — Fish - {"hint.fish.l1_value", "Look for a fish pattern on value {0}."}, - {"hint.fish.l1_no_value", "Look for a fish pattern (rows/columns with restricted candidate positions)."}, - {"hint.fish.l2", "Base and cover sets. Blue cells are the base set (rows/columns where the candidate is restricted). Green cells are the cover set. Eliminate the candidate from cover set cells that aren't in the base set."}, - {"hint.fish.l3", "Eliminate the candidate from cover set cells outside the base set."}, - // Training hints — Wings - {"hint.wings.l1", "Find the pivot cell at {0}."}, - {"hint.wings.l2", "Pivot and wing cells. The orange pivot connects to the green wings. Candidates shared by both wings can be eliminated from cells that see all wing endpoints."}, - {"hint.wings.l3", "Eliminate the shared candidate from cells that see all wing endpoints."}, - // Training hints — SingleDigit - {"hint.single_digit.l1_value", "Look for conjugate pairs on value {0}."}, - {"hint.single_digit.l1_no_value", "Look for conjugate pairs (cells where a digit appears exactly twice in a unit)."}, - {"hint.single_digit.l2", "The chain cells. These cells form conjugate pairs (a digit appears exactly twice in a unit). Follow the alternating pattern to find eliminations."}, - {"hint.single_digit.l3", "Cells that see both endpoints of the pattern can be eliminated."}, - // Training hints — Coloring - {"hint.coloring.l1_value", "Build a coloring chain on value {0}."}, - {"hint.coloring.l1_no_value", "Start coloring conjugate pairs with two alternating colors."}, - {"hint.coloring.l2", "The coloring chain. Blue and green are two alternating colors — one must be true, one false. Cells that see both colors can have the candidate eliminated."}, - {"hint.coloring.l3", "One color must be false — eliminate from cells that see both colors."}, - // Training hints — UniqueRect - {"hint.unique_rect.l1", "Look for a deadly pattern — four cells forming a rectangle across two boxes."}, - {"hint.unique_rect.l2", "The rectangle corners. These four cells across two boxes form a potential deadly pattern. To keep the puzzle unique, eliminate the candidate that would complete the rectangle."}, - {"hint.unique_rect.l3", "To avoid the deadly pattern, eliminate the candidate that would complete it."}, - // Training hints — Chains - {"hint.chains.l1_pos", "Start the chain from cell {0}."}, - {"hint.chains.l1_no_pos", "Look for a chain of linked cells with alternating strong/weak links."}, - {"hint.chains.l2", "The chain path. Follow the alternating strong (blue) and weak (green) links. The chain's logic forces a conclusion at the endpoints."}, - {"hint.chains.l3_placement", "All chains lead to value {0} at {1}."}, - {"hint.chains.l3_elimination", "Eliminate candidates that contradict the chain logic."}, - // Training hints — SetLogic - {"hint.set_logic.l1", "Look for an Almost Locked Set (a group of N cells with N+1 candidates)."}, - {"hint.set_logic.l2", "The ALS cells and restricted common. An ALS is N cells with N+1 candidates. The restricted common candidate links the sets — eliminations apply to cells that see all relevant ALS members."}, - {"hint.set_logic.l3", "Eliminate candidates from cells that see all relevant ALS members."}, - // Training hints — Special - {"hint.special.l1", "Look for the cell with three candidates (the only non-bivalue cell)."}, - {"hint.special.l2", "The key cell is {0}."}, - // Technique descriptions (selected keys needed by tests) - {"tech.desc.naked_single.what_it_is", "A cell has only one possible candidate left. All other values are eliminated by row, column, and box constraints."}, - {"tech.desc.naked_single.what_to_look_for", "Look for cells where 8 of the 9 values are already present in the cell's row, column, or box."}, - {"tech.desc.forcing_chain.what_it_is", "For a cell with 2-3 candidates, assume each candidate is true and propagate the consequences. If all assumptions lead to the same conclusion (a placement or elimination), that conclusion must be true."}, - {"tech.desc.forcing_chain.what_to_look_for", "Pick a cell with few candidates. Try each value and propagate. Look for common outcomes."}, - {"tech.desc.nice_loop.what_it_is", "Build a chain of alternating strong and weak links between (cell, digit) pairs. If the chain forms a loop or its endpoints share a digit, eliminations can be derived from the alternating inference chain rules."}, - {"tech.desc.nice_loop.what_to_look_for", "Trace alternating strong/weak links. Check if endpoints share a digit for eliminations."}, - {"tech.desc.backtracking.what_it_is", "A brute-force trial-and-error method. Not a logical technique — used as a fallback when no logical strategy can make progress."}, - {"tech.desc.backtracking.what_to_look_for", "This technique is not used in training exercises."}, - }; - // clang-format on - } - - [[nodiscard]] std::string_view getString(std::string_view key) const override { - // Check format templates first (needed for fmt::runtime() calls) - auto fmt_it = format_templates_.find(std::string(key)); - if (fmt_it != format_templates_.end()) { - return fmt_it->second; - } - // Fall back to returning the key itself - auto [it, _] = key_cache_.try_emplace(std::string(key), std::string(key)); - return it->second; - } - - [[nodiscard]] std::expected setLocale(std::string_view /*locale_code*/) override { - return {}; - } - - [[nodiscard]] std::string_view getCurrentLocale() const override { - return "en"; - } - - [[nodiscard]] std::vector> getAvailableLocales() const override { - return {{"en", "English"}}; - } - -private: - std::unordered_map format_templates_; - mutable std::unordered_map key_cache_; -}; - -} // namespace sudoku::core diff --git a/tests/helpers/qt_test_main.cpp b/tests/helpers/qt_test_main.cpp new file mode 100644 index 0000000..82a529e --- /dev/null +++ b/tests/helpers/qt_test_main.cpp @@ -0,0 +1,21 @@ +// sudoku-cpp - Offline Sudoku Game +// Copyright (C) 2025-2026 Alexander Bendlin (darkstar79) +// +// Custom Catch2 entry point that owns a QCoreApplication for the lifetime of +// the test run. Without this, calls to QCoreApplication::translate() in unit +// tests fall through Qt's null-qApp path — an undocumented short-circuit that +// has historically returned the source string but is not part of Qt's public +// contract. With qApp present and no translator installed, translate() runs +// the documented "no installed translators" branch and returns the source +// string deterministically, the way the production code expects. +// +// Tests that want to exercise translation can installTranslator() on this +// qApp and removeTranslator() before the test ends. + +#include +#include + +int main(int argc, char* argv[]) { + QCoreApplication qt_app(argc, argv); + return Catch::Session().run(argc, argv); +} diff --git a/tests/integration/CMakeLists.txt b/tests/integration/CMakeLists.txt index f8dcce7..ea5ebe0 100644 --- a/tests/integration/CMakeLists.txt +++ b/tests/integration/CMakeLists.txt @@ -3,14 +3,17 @@ cmake_minimum_required(VERSION 3.28) # Collect all integration test files file(GLOB_RECURSE INTEGRATION_TEST_SOURCES "*.cpp") -# Create integration test executable -add_executable(integration_tests ${INTEGRATION_TEST_SOURCES}) +# Custom Catch2 entry point that owns a QCoreApplication for the run. +# See tests/helpers/qt_test_main.cpp for rationale. Replaces Catch2WithMain. +add_executable(integration_tests + ${INTEGRATION_TEST_SOURCES} + ${CMAKE_SOURCE_DIR}/tests/helpers/qt_test_main.cpp +) -# Link with the main library, test helpers, and Catch2 target_link_libraries(integration_tests PUBLIC sudoku_lib test_helpers - Catch2::Catch2WithMain + Catch2::Catch2 ) # MSVC: suppress C4834 (discarded [[nodiscard]] result) and C4702 (unreachable code in templates) in tests diff --git a/tests/integration/test_game_view_model_integration.cpp b/tests/integration/test_game_view_model_integration.cpp index 27a0bd6..c86874a 100644 --- a/tests/integration/test_game_view_model_integration.cpp +++ b/tests/integration/test_game_view_model_integration.cpp @@ -431,8 +431,7 @@ TEST_CASE("GameViewModel - Hint-Revealed State Persists After Save/Load", "[game auto solver = std::make_shared(validator); auto stats_manager = std::make_shared(); - GameViewModel view_model(validator, generator, solver, stats_manager, save_manager, - std::make_shared()); + GameViewModel view_model(validator, generator, solver, stats_manager, save_manager); // Use a fixed seed known to generate a solvable puzzle with techniques the solver supports // NOTE: Random seed (0) can generate puzzles that require techniques beyond the solver's @@ -497,8 +496,7 @@ TEST_CASE("GameViewModel - Hint-Revealed State Persists After Save/Load", "[game REQUIRE(!saves_result->empty()); std::string save_id = saves_result->front().save_id; - GameViewModel view_model2(validator, generator, solver, stats_manager, save_manager, - std::make_shared()); + GameViewModel view_model2(validator, generator, solver, stats_manager, save_manager); view_model2.loadGame(save_id); const auto& loaded = view_model2.gameState.get(); diff --git a/tests/ui/CMakeLists.txt b/tests/ui/CMakeLists.txt index 6cab9ab..d583a8f 100644 --- a/tests/ui/CMakeLists.txt +++ b/tests/ui/CMakeLists.txt @@ -7,6 +7,7 @@ set(UI_TEST_SOURCES test_menu_toolbar_actions.cpp test_view_model_binding.cpp test_training_widget.cpp + test_language_change.cpp ) foreach(TEST_SOURCE ${UI_TEST_SOURCES}) @@ -23,6 +24,13 @@ foreach(TEST_SOURCE ${UI_TEST_SOURCES}) ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/tests ) + # SUDOKU_QM_DIR points at the directory where qt_add_lrelease emits + # sudoku_.qm files. Used by test_language_change to load the + # bundled German translator. Built by the main 'sudoku' target's + # lrelease step. + target_compile_definitions(${TEST_NAME} PRIVATE + SUDOKU_QM_DIR="${CMAKE_BINARY_DIR}" + ) add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) set_target_properties(${TEST_NAME} PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/tests/ui" diff --git a/tests/ui/test_fixture.h b/tests/ui/test_fixture.h index 4cb21c2..d3699b5 100644 --- a/tests/ui/test_fixture.h +++ b/tests/ui/test_fixture.h @@ -15,7 +15,6 @@ #include "core/statistics_manager.h" #include "core/sudoku_solver.h" #include "core/training_exercise_generator.h" -#include "helpers/mock_localization_manager.h" #include "view/main_window.h" #include "view_model/game_view_model.h" #include "view_model/training_view_model.h" @@ -33,7 +32,6 @@ struct UITestContext { std::shared_ptr solver; std::shared_ptr save_manager; std::shared_ptr stats_manager; - std::shared_ptr loc_manager; std::shared_ptr game_vm; std::shared_ptr training_vm; std::filesystem::path test_dir; @@ -49,13 +47,11 @@ struct UITestContext { generator = std::make_shared(rater); save_manager = std::make_shared(test_dir.string()); stats_manager = std::make_shared(test_dir.string()); - loc_manager = std::make_shared(); - game_vm = std::make_shared(validator, generator, solver, stats_manager, save_manager, - loc_manager); + game_vm = std::make_shared(validator, generator, solver, stats_manager, save_manager); auto exercise_gen = std::make_shared(generator, solver); - training_vm = std::make_shared(exercise_gen, loc_manager); + training_vm = std::make_shared(exercise_gen); } ~UITestContext() { @@ -68,7 +64,6 @@ struct UITestContext { void setupMainWindow(view::MainWindow& window) { window.setViewModel(game_vm); window.setTrainingViewModel(training_vm); - window.setLocalizationManager(loc_manager); // also propagates to training widget } }; diff --git a/tests/ui/test_language_change.cpp b/tests/ui/test_language_change.cpp new file mode 100644 index 0000000..ba23532 --- /dev/null +++ b/tests/ui/test_language_change.cpp @@ -0,0 +1,92 @@ +// sudoku-cpp - Offline Sudoku Game +// Copyright (C) 2025-2026 Alexander Bendlin (darkstar79) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +#include "view/main_window.h" +#include "view/training_widget.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace sudoku; + +class TestLanguageChange : public QObject { + Q_OBJECT + +private slots: + void menuBarRetranslatesOnLanguageChange(); + void trainingPagesRebuildOnLanguageChange(); + +private: + [[nodiscard]] static bool loadGermanTranslator(QTranslator& translator); +}; + +bool TestLanguageChange::loadGermanTranslator(QTranslator& translator) { + // SUDOKU_QM_DIR is set by tests/ui/CMakeLists.txt to the build directory + // where qt_add_lrelease emits sudoku_.qm. Building the main + // 'sudoku' target produces them. + return translator.load(QString("sudoku_de"), QString::fromUtf8(SUDOKU_QM_DIR)); +} + +void TestLanguageChange::menuBarRetranslatesOnLanguageChange() { + view::MainWindow window; + + auto initial_actions = window.menuBar()->actions(); + QVERIFY(!initial_actions.isEmpty()); + QCOMPARE(initial_actions.first()->text(), QString("&Game")); + + QTranslator translator; + if (!loadGermanTranslator(translator)) { + QSKIP("sudoku_de.qm not built; build the main 'sudoku' target first"); + } + + QVERIFY(QCoreApplication::installTranslator(&translator)); + QApplication::processEvents(); + + auto translated_actions = window.menuBar()->actions(); + QVERIFY(!translated_actions.isEmpty()); + QCOMPARE(translated_actions.first()->text(), QString("&Spiel")); + + QCoreApplication::removeTranslator(&translator); + QApplication::processEvents(); + + auto restored_actions = window.menuBar()->actions(); + QCOMPARE(restored_actions.first()->text(), QString("&Game")); +} + +void TestLanguageChange::trainingPagesRebuildOnLanguageChange() { + view::MainWindow window; + auto* training = window.findChild(); + QVERIFY(training != nullptr); + + auto* pages_before = training->findChild(); + QVERIFY(pages_before != nullptr); + + QTranslator translator; + if (!loadGermanTranslator(translator)) { + QSKIP("sudoku_de.qm not built; build the main 'sudoku' target first"); + } + + QVERIFY(QCoreApplication::installTranslator(&translator)); + QApplication::processEvents(); + + auto* pages_after = training->findChild(); + QVERIFY(pages_after != nullptr); + QVERIFY(pages_after->count() > 0); + + QCoreApplication::removeTranslator(&translator); + QApplication::processEvents(); +} + +QTEST_MAIN(TestLanguageChange) +#include "test_language_change.moc" diff --git a/tests/ui/test_main_window_construction.cpp b/tests/ui/test_main_window_construction.cpp index 512d7e0..ddf12cf 100644 --- a/tests/ui/test_main_window_construction.cpp +++ b/tests/ui/test_main_window_construction.cpp @@ -49,7 +49,6 @@ void TestMainWindowConstruction::constructsWithViewModel() { QVERIFY(window.view_model_ != nullptr); QVERIFY(window.training_vm_ != nullptr); - QVERIFY(window.loc_manager_ != nullptr); } void TestMainWindowConstruction::centralStackDefaultsToGameBoard() { @@ -60,13 +59,13 @@ void TestMainWindowConstruction::centralStackDefaultsToGameBoard() { void TestMainWindowConstruction::difficultyComboHasFiveEntries() { view::MainWindow window; QCOMPARE(window.difficulty_combo_->count(), 5); - QCOMPARE(window.difficulty_combo_->itemText(0), QString("difficulty.easy")); - QCOMPARE(window.difficulty_combo_->itemText(4), QString("difficulty.master")); + QCOMPARE(window.difficulty_combo_->itemText(0), QString("Easy")); + QCOMPARE(window.difficulty_combo_->itemText(4), QString("Master")); } void TestMainWindowConstruction::statusBarShowsReady() { view::MainWindow window; - QCOMPARE(window.status_label_->text(), QString("status.ready")); + QCOMPARE(window.status_label_->text(), QString("Ready")); } void TestMainWindowConstruction::boardWidgetExists() { @@ -90,10 +89,10 @@ void TestMainWindowConstruction::buttonPanelExists() { void TestMainWindowConstruction::buttonPanelInitialState() { view::MainWindow window; - QCOMPARE(window.undo_btn_->text(), QString("button.undo")); - QCOMPARE(window.redo_btn_->text(), QString("button.redo")); - QCOMPARE(window.undo_valid_btn_->text(), QString("button.undo_until_valid")); - QCOMPARE(window.auto_notes_btn_->text(), QString("button.fill_notes")); + QCOMPARE(window.undo_btn_->text(), QString("Undo")); + QCOMPARE(window.redo_btn_->text(), QString("Redo")); + QCOMPARE(window.undo_valid_btn_->text(), QString("Undo Until Valid")); + QCOMPARE(window.auto_notes_btn_->text(), QString("Fill Notes")); QVERIFY(window.auto_notes_btn_->isCheckable()); } diff --git a/tests/ui/test_menu_toolbar_actions.cpp b/tests/ui/test_menu_toolbar_actions.cpp index 4aac871..d54b2cf 100644 --- a/tests/ui/test_menu_toolbar_actions.cpp +++ b/tests/ui/test_menu_toolbar_actions.cpp @@ -68,7 +68,7 @@ void TestMenuToolbarActions::redoMenuActionExists() { void TestMenuToolbarActions::trainingModeMenuSwitchesStack() { QCOMPARE(window_->central_stack_->currentIndex(), 0); - auto* action = findMenuAction("training_mode"); + auto* action = findMenuAction("Training Mode"); QVERIFY(action != nullptr); action->trigger(); QApplication::processEvents(); @@ -82,7 +82,7 @@ void TestMenuToolbarActions::resumeGameMenuSwitchesBack() { QApplication::processEvents(); QCOMPARE(window_->central_stack_->currentIndex(), 1); - auto* action = findMenuAction("resume"); + auto* action = findMenuAction("Resume Game"); QVERIFY(action != nullptr); action->trigger(); QApplication::processEvents(); @@ -92,7 +92,7 @@ void TestMenuToolbarActions::resumeGameMenuSwitchesBack() { void TestMenuToolbarActions::difficultyComboDefaultIsMedium() { QCOMPARE(window_->difficulty_combo_->currentIndex(), 1); - QCOMPARE(window_->difficulty_combo_->currentText(), QString("difficulty.medium")); + QCOMPARE(window_->difficulty_combo_->currentText(), QString("Medium")); } QTEST_MAIN(TestMenuToolbarActions) diff --git a/tests/ui/test_training_widget.cpp b/tests/ui/test_training_widget.cpp index be913bb..ed3afed 100644 --- a/tests/ui/test_training_widget.cpp +++ b/tests/ui/test_training_widget.cpp @@ -103,10 +103,10 @@ void TestTrainingWidget::testFeedbackButtonsExist() { button_texts << btn->text(); } - QVERIFY(button_texts.contains("training.next_exercise")); - QVERIFY(button_texts.contains("training.retry")); - QVERIFY(button_texts.contains("training.show_solution")); - QVERIFY(button_texts.contains("training.quit_lesson")); + QVERIFY(button_texts.contains("Next Exercise")); + QVERIFY(button_texts.contains("Retry")); + QVERIFY(button_texts.contains("Show Solution")); + QVERIFY(button_texts.contains("Quit Lesson")); } void TestTrainingWidget::testReturnToSelection() { @@ -125,7 +125,7 @@ void TestTrainingWidget::testBackToGameSignal() { QPushButton* back_btn = nullptr; for (auto* btn : buttons) { - if (btn->text() == "training.back_to_game") { + if (btn->text() == "Back to Game") { back_btn = btn; break; } diff --git a/tests/ui/test_view_model_binding.cpp b/tests/ui/test_view_model_binding.cpp index 3eecb84..ae157db 100644 --- a/tests/ui/test_view_model_binding.cpp +++ b/tests/ui/test_view_model_binding.cpp @@ -41,7 +41,7 @@ void TestViewModelBinding::statusLabelUpdatesOnNewGame() { QApplication::processEvents(); // After starting a game, status should show playing text - QCOMPARE(window.status_label_->text(), QString("status.playing")); + QCOMPARE(window.status_label_->text(), QString("Playing")); } void TestViewModelBinding::statusLabelShowsReadyBeforeGame() { @@ -50,7 +50,7 @@ void TestViewModelBinding::statusLabelShowsReadyBeforeGame() { ctx.setupMainWindow(window); // Before starting any game, should show ready text - QCOMPARE(window.status_label_->text(), QString("status.ready")); + QCOMPARE(window.status_label_->text(), QString("Ready")); } void TestViewModelBinding::hintsLabelUpdatesOnNewGame() { diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index bda0ceb..5c6d880 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -3,14 +3,17 @@ cmake_minimum_required(VERSION 3.28) # Collect all unit test files file(GLOB_RECURSE UNIT_TEST_SOURCES "*.cpp") -# Create unit test executable -add_executable(unit_tests ${UNIT_TEST_SOURCES}) +# Custom Catch2 entry point that owns a QCoreApplication for the run. +# See tests/helpers/qt_test_main.cpp for rationale. Replaces Catch2WithMain. +add_executable(unit_tests + ${UNIT_TEST_SOURCES} + ${CMAKE_SOURCE_DIR}/tests/helpers/qt_test_main.cpp +) -# Link with the main library, test helpers, and Catch2 target_link_libraries(unit_tests PUBLIC sudoku_lib test_helpers - Catch2::Catch2WithMain + Catch2::Catch2 ) # MSVC: suppress C4834 (discarded [[nodiscard]] result) and C4702 (unreachable code in templates) in tests diff --git a/tests/unit/test_format_techniques.cpp b/tests/unit/test_format_techniques.cpp index 676b7dd..b2c3e78 100644 --- a/tests/unit/test_format_techniques.cpp +++ b/tests/unit/test_format_techniques.cpp @@ -15,7 +15,6 @@ // along with this program. If not, see . #include "../../src/core/game_validator.h" -#include "../../src/core/localization_manager.h" #include "../../src/core/puzzle_generator.h" #include "../../src/core/save_manager.h" #include "../../src/core/solving_technique.h" @@ -35,19 +34,14 @@ using namespace sudoku::viewmodel; namespace { -/// Compute project root from __FILE__ (tests/unit/test_format_techniques.cpp → project root) -[[nodiscard]] std::filesystem::path projectRoot() { - return std::filesystem::path(__FILE__).parent_path().parent_path().parent_path(); -} - /// Helper to create a minimal GameViewModel for formatTechniques testing. -/// Uses real LocalizationManager with English locale so formatted output matches expected strings. +/// The test runner (tests/helpers/qt_test_main.cpp) instantiates a +/// QCoreApplication with no QTranslator installed, so +/// QCoreApplication::translate returns the source-language English strings. [[nodiscard]] GameViewModel createTestViewModel() { auto validator = std::make_shared(); - auto loc_manager = std::make_shared(projectRoot() / "resources" / "locales"); - [[maybe_unused]] auto result = loc_manager->setLocale("en"); return GameViewModel(validator, std::make_shared(), std::make_shared(validator), - std::make_shared(), std::make_shared(), loc_manager); + std::make_shared(), std::make_shared()); } } // namespace diff --git a/tests/unit/test_game_coaching_hints.cpp b/tests/unit/test_game_coaching_hints.cpp index dafa05b..690963d 100644 --- a/tests/unit/test_game_coaching_hints.cpp +++ b/tests/unit/test_game_coaching_hints.cpp @@ -462,7 +462,7 @@ TEST_CASE("GameViewModel - Coaching Hints", "[game_view_model][coaching]") { const auto& state = fixture.view_model->coachingState.get(); // The technique description's what_to_look_for should appear in level 1 - auto desc = core::getTechniqueDescription(*fixture.loc_manager, state.technique); + auto desc = core::getTechniqueDescription(state.technique); REQUIRE(state.message.find(desc.what_to_look_for) != std::string::npos); } @@ -471,7 +471,7 @@ TEST_CASE("GameViewModel - Coaching Hints", "[game_view_model][coaching]") { fixture.view_model->requestCoachingHint(); const auto& state = fixture.view_model->coachingState.get(); - auto desc = core::getTechniqueDescription(*fixture.loc_manager, state.technique); + auto desc = core::getTechniqueDescription(state.technique); REQUIRE(state.message.find(desc.what_it_is) != std::string::npos); } @@ -485,7 +485,7 @@ TEST_CASE("GameViewModel - Coaching Hints", "[game_view_model][coaching]") { const auto& state = fixture.view_model->coachingState.get(); REQUIRE(state.level == 1); - auto desc = core::getTechniqueDescription(*fixture.loc_manager, state.technique); + auto desc = core::getTechniqueDescription(state.technique); REQUIRE(state.message.find(desc.what_to_look_for) != std::string::npos); } diff --git a/tests/unit/test_game_view_model_hints.cpp b/tests/unit/test_game_view_model_hints.cpp index a85c069..745fa6f 100644 --- a/tests/unit/test_game_view_model_hints.cpp +++ b/tests/unit/test_game_view_model_hints.cpp @@ -57,9 +57,9 @@ TEST_CASE("GameViewModel - Educational Hint System", "[game_view_model][hints]") const auto& hint = fixture.view_model->hintMessage.get(); REQUIRE_FALSE(hint.empty()); - // Should contain technique name key (MockLocalizationManager returns keys as-is) + // Should contain technique English name (no QTranslator installed -> source returned) bool has_technique_name = - hint.find("tech.naked_single") != std::string::npos || hint.find("tech.hidden_single") != std::string::npos; + hint.find("Naked Single") != std::string::npos || hint.find("Hidden Single") != std::string::npos; REQUIRE(has_technique_name); } diff --git a/tests/unit/test_hint_revelation.cpp b/tests/unit/test_hint_revelation.cpp index 9bfe987..653ee9f 100644 --- a/tests/unit/test_hint_revelation.cpp +++ b/tests/unit/test_hint_revelation.cpp @@ -155,7 +155,7 @@ TEST_CASE("Hint Revelation - Error Handling", "[hint][error]") { fixture.view_model->getHint(std::nullopt); const auto& error = fixture.view_model->errorMessage.get(); - REQUIRE(error == "hint.select_cell"); + REQUIRE(error == "Please select a cell first"); } SECTION("Error when trying to hint a given cell") { @@ -171,7 +171,7 @@ TEST_CASE("Hint Revelation - Error Handling", "[hint][error]") { fixture.view_model->getHint(given_pos); const auto& error = fixture.view_model->errorMessage.get(); - REQUIRE(error == "hint.cannot_reveal_given"); + REQUIRE(error == "Cannot reveal hint for given cells"); } SECTION("Error when trying to hint an already filled cell") { @@ -188,7 +188,7 @@ TEST_CASE("Hint Revelation - Error Handling", "[hint][error]") { fixture.view_model->getHint(pos); const auto& error = fixture.view_model->errorMessage.get(); - REQUIRE(error == "hint.cell_has_value"); + REQUIRE(error == "Cell already has a value"); } SECTION("Hint respects maximum limit of 10") { @@ -394,7 +394,7 @@ TEST_CASE("Hint Revelation - Undo Behavior", "[hint][undo]") { REQUIRE(fixture.view_model->getHintCount() == hints_initial); const auto& error = fixture.view_model->errorMessage.get(); - REQUIRE(error == "hint.select_cell"); + REQUIRE(error == "Please select a cell first"); } SECTION("Hints not consumed on validation errors - given cell") { @@ -413,7 +413,7 @@ TEST_CASE("Hint Revelation - Undo Behavior", "[hint][undo]") { REQUIRE(fixture.view_model->getHintCount() == hints_initial); const auto& error = fixture.view_model->errorMessage.get(); - REQUIRE(error == "hint.cannot_reveal_given"); + REQUIRE(error == "Cannot reveal hint for given cells"); } SECTION("Hints not consumed on validation errors - filled cell") { @@ -436,7 +436,7 @@ TEST_CASE("Hint Revelation - Undo Behavior", "[hint][undo]") { REQUIRE(fixture.view_model->getHintCount() == hints_initial); const auto& error = fixture.view_model->errorMessage.get(); - REQUIRE(error == "hint.cell_has_value"); + REQUIRE(error == "Cell already has a value"); } SECTION("Multiple undo skips all hints") { diff --git a/tests/unit/test_localization_manager.cpp b/tests/unit/test_localization_manager.cpp deleted file mode 100644 index bd6c614..0000000 --- a/tests/unit/test_localization_manager.cpp +++ /dev/null @@ -1,378 +0,0 @@ -// sudoku-cpp - Offline Sudoku Game -// Copyright (C) 2025-2026 Alexander Bendlin (darkstar79) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -#include "../helpers/test_utils.h" -#include "core/i_localization_manager.h" -#include "core/localization_manager.h" - -#include -#include - -#include - -using namespace sudoku::core; -using sudoku::test::TempTestDir; - -namespace { - -/// Write a YAML file to the given directory. -void writeFile(const std::filesystem::path& dir, const std::string& filename, const std::string& content) { - std::ofstream out(dir / filename); - out << content; -} - -const std::string ENGLISH_YAML = R"(locale: en -name: English - -strings: - menu.game: "Game" - menu.new_game: "New Game" - stats.games_played: "Games Played: {0}" - stats.completion_rate: "Completion Rate: {0:.1f}%" -)"; - -const std::string GERMAN_YAML = R"(locale: de -name: Deutsch - -strings: - menu.game: "Spiel" - menu.new_game: "Neues Spiel" - stats.games_played: "Gespielte Spiele: {0}" -)"; - -const std::string INVALID_YAML = R"(this is not: [valid: yaml: {{)"; - -const std::string MISSING_STRINGS_YAML = R"(locale: fr -name: Français -)"; - -} // namespace - -TEST_CASE("LocalizationManager - Load and retrieve strings", "[localization]") { - TempTestDir temp; - writeFile(temp.path(), "en.yaml", ENGLISH_YAML); - - LocalizationManager manager(temp.path()); - - SECTION("getString returns localized value after setLocale") { - auto result = manager.setLocale("en"); - - REQUIRE(result.has_value()); - REQUIRE(manager.getString("menu.game") == "Game"); - REQUIRE(manager.getString("menu.new_game") == "New Game"); - } - - SECTION("getString returns parameterized template strings") { - auto result = manager.setLocale("en"); - - REQUIRE(result.has_value()); - REQUIRE(manager.getString("stats.games_played") == "Games Played: {0}"); - } - - SECTION("getCurrentLocale returns active locale code") { - auto result = manager.setLocale("en"); - - REQUIRE(result.has_value()); - REQUIRE(manager.getCurrentLocale() == "en"); - } -} - -TEST_CASE("LocalizationManager - Missing key handling", "[localization]") { - TempTestDir temp; - writeFile(temp.path(), "en.yaml", ENGLISH_YAML); - - LocalizationManager manager(temp.path()); - auto result = manager.setLocale("en"); - REQUIRE(result.has_value()); - - SECTION("Missing key returns the key itself") { - auto value = manager.getString("nonexistent.key"); - - REQUIRE(value == "nonexistent.key"); - } - - SECTION("Missing key returns stable view on repeated calls") { - auto first = manager.getString("missing.key"); - auto second = manager.getString("missing.key"); - - REQUIRE(first == second); - } -} - -TEST_CASE("LocalizationManager - Locale switching", "[localization]") { - TempTestDir temp; - writeFile(temp.path(), "en.yaml", ENGLISH_YAML); - writeFile(temp.path(), "de.yaml", GERMAN_YAML); - - LocalizationManager manager(temp.path()); - - SECTION("Switching locale changes returned strings") { - auto en_result = manager.setLocale("en"); - REQUIRE(en_result.has_value()); - REQUIRE(manager.getString("menu.game") == "Game"); - - auto de_result = manager.setLocale("de"); - REQUIRE(de_result.has_value()); - REQUIRE(manager.getString("menu.game") == "Spiel"); - REQUIRE(manager.getCurrentLocale() == "de"); - } - - SECTION("Switching to non-English locale uses English fallback for missing keys") { - auto en_result = manager.setLocale("en"); - REQUIRE(en_result.has_value()); - - auto de_result = manager.setLocale("de"); - REQUIRE(de_result.has_value()); - - // German YAML is missing stats.completion_rate, should fall back to English - REQUIRE(manager.getString("stats.completion_rate") == "Completion Rate: {0:.1f}%"); - } -} - -TEST_CASE("LocalizationManager - Error handling", "[localization]") { - SECTION("setLocale with non-existent locale returns error") { - TempTestDir temp; - writeFile(temp.path(), "en.yaml", ENGLISH_YAML); - - LocalizationManager manager(temp.path()); - - auto result = manager.setLocale("xx"); - - REQUIRE(!result.has_value()); - REQUIRE(result.error().find("not found") != std::string::npos); - } - - SECTION("Construction with non-existent directory does not throw") { - auto nonexistent = std::filesystem::temp_directory_path() / "sudoku_nonexistent_dir_12345"; - - LocalizationManager manager(nonexistent); - - REQUIRE(manager.getAvailableLocales().empty()); - } - - SECTION("setLocale with invalid YAML returns error") { - TempTestDir temp; - writeFile(temp.path(), "bad.yaml", INVALID_YAML); - - LocalizationManager manager(temp.path()); - auto result = manager.setLocale("bad"); - - REQUIRE(!result.has_value()); - } - - SECTION("setLocale with YAML missing strings section returns error") { - TempTestDir temp; - writeFile(temp.path(), "fr.yaml", MISSING_STRINGS_YAML); - - LocalizationManager manager(temp.path()); - auto result = manager.setLocale("fr"); - - REQUIRE(!result.has_value()); - REQUIRE(result.error().find("missing") != std::string::npos); - } -} - -TEST_CASE("LocalizationManager - Available locales discovery", "[localization]") { - TempTestDir temp; - writeFile(temp.path(), "en.yaml", ENGLISH_YAML); - writeFile(temp.path(), "de.yaml", GERMAN_YAML); - - LocalizationManager manager(temp.path()); - - SECTION("Discovers all YAML locale files") { - auto locales = manager.getAvailableLocales(); - - REQUIRE(locales.size() == 2); - } - - SECTION("Available locales are sorted by code") { - auto locales = manager.getAvailableLocales(); - - REQUIRE(locales[0].first == "de"); - REQUIRE(locales[0].second == "Deutsch"); - REQUIRE(locales[1].first == "en"); - REQUIRE(locales[1].second == "English"); - } - - SECTION("Invalid YAML files are skipped during discovery") { - writeFile(temp.path(), "broken.yaml", INVALID_YAML); - - // Re-create manager to re-discover - LocalizationManager manager2(temp.path()); - auto locales = manager2.getAvailableLocales(); - - // Only valid locale files are discovered - REQUIRE(locales.size() == 2); - } -} - -TEST_CASE("LocalizationManager - English fallback loading", "[localization]") { - TempTestDir temp; - writeFile(temp.path(), "en.yaml", ENGLISH_YAML); - writeFile(temp.path(), "de.yaml", GERMAN_YAML); - - SECTION("Direct non-English setLocale loads English fallback automatically") { - LocalizationManager manager(temp.path()); - - // Set German directly without loading English first - auto result = manager.setLocale("de"); - REQUIRE(result.has_value()); - - // German key works - REQUIRE(manager.getString("menu.game") == "Spiel"); - - // Missing key in German falls back to English - REQUIRE(manager.getString("stats.completion_rate") == "Completion Rate: {0:.1f}%"); - } - - SECTION("English locale is used as its own fallback") { - LocalizationManager manager(temp.path()); - - auto result = manager.setLocale("en"); - REQUIRE(result.has_value()); - - // All English keys should resolve - REQUIRE(manager.getString("menu.game") == "Game"); - REQUIRE(manager.getString("stats.completion_rate") == "Completion Rate: {0:.1f}%"); - } -} - -TEST_CASE("LocalizationManager - Fallback edge cases", "[localization]") { - SECTION("Non-English locale without en.yaml: no English fallback loaded") { - // Only German, no English — the 'exists(en_file)' false branch - TempTestDir temp; - writeFile(temp.path(), "de.yaml", GERMAN_YAML); - - LocalizationManager manager(temp.path()); - auto result = manager.setLocale("de"); - - // Load should succeed (German locale still loads) - REQUIRE(result.has_value()); - REQUIRE(manager.getString("menu.game") == "Spiel"); - // Missing key in German falls back to key name (no English fallback available) - auto missing = manager.getString("stats.completion_rate"); - REQUIRE(missing == "stats.completion_rate"); - } - - SECTION("getString key missing in both active and fallback returns key name") { - TempTestDir temp; - writeFile(temp.path(), "en.yaml", ENGLISH_YAML); - writeFile(temp.path(), "de.yaml", GERMAN_YAML); - - LocalizationManager manager(temp.path()); - auto result = manager.setLocale("de"); - REQUIRE(result.has_value()); - - // This key is in neither German nor English YAML - auto val = manager.getString("totally.unknown.key"); - REQUIRE(val == "totally.unknown.key"); - } -} - -TEST_CASE("LocalizationManager - locales path is a file not a directory", "[localization]") { - // Covers the !is_directory() branch at discoverLocales line 110 - auto file_path = std::filesystem::temp_directory_path() / "sudoku_test_not_a_dir.yaml"; - { - std::ofstream f(file_path); - f << "not_a_dir: true\n"; - } - - // Pass a FILE path where a directory is expected - LocalizationManager manager(file_path); - - // discoverLocales should warn and return; no locales discovered - REQUIRE(manager.getAvailableLocales().empty()); - - std::filesystem::remove(file_path); -} - -TEST_CASE("LocalizationManager - Polymorphic usage through interface", "[localization]") { - TempTestDir temp; - writeFile(temp.path(), "en.yaml", ENGLISH_YAML); - - SECTION("Can use through ILocalizationManager pointer") { - std::shared_ptr manager = std::make_shared(temp.path()); - - auto result = manager->setLocale("en"); - REQUIRE(result.has_value()); - REQUIRE(manager->getString("menu.game") == "Game"); - } -} - -TEST_CASE("LocalizationManager - Failed English fallback load is handled gracefully", "[localization]") { - // Covers localization_manager.cpp line 50: warn branch when en.yaml fails to parse - // while loading a non-English locale. - // Setup: en.yaml is present but contains invalid YAML (parse fails → fallback warn) - TempTestDir temp; - writeFile(temp.path(), "en.yaml", INVALID_YAML); // broken en.yaml - writeFile(temp.path(), "de.yaml", GERMAN_YAML); // valid German locale - - LocalizationManager manager(temp.path()); - - // setLocale("de") sees en.yaml exists but loadYamlFile fails → warns, proceeds - // The German locale itself may also fail (depends on broken en.yaml not affecting it) - auto result = manager.setLocale("de"); - - // German YAML is valid so setLocale should succeed; just no fallback available - REQUIRE(result.has_value()); - // German key loads correctly - REQUIRE(manager.getString("menu.game") == "Spiel"); -} - -TEST_CASE("LocalizationManager - Available locales sorted with many locales", "[localization]") { - // Add 4 locales to exercise the sort lambda more thoroughly - TempTestDir temp; - writeFile(temp.path(), "en.yaml", ENGLISH_YAML); - writeFile(temp.path(), "de.yaml", GERMAN_YAML); - writeFile(temp.path(), "fr.yaml", R"(locale: fr -name: Français - -strings: - menu.game: "Jeu" - menu.new_game: "Nouveau jeu" -)"); - writeFile(temp.path(), "es.yaml", R"(locale: es -name: Español - -strings: - menu.game: "Juego" - menu.new_game: "Nuevo juego" -)"); - - LocalizationManager manager(temp.path()); - auto locales = manager.getAvailableLocales(); - - REQUIRE(locales.size() == 4); - // Should be sorted: de, en, es, fr - REQUIRE(locales[0].first == "de"); - REQUIRE(locales[1].first == "en"); - REQUIRE(locales[2].first == "es"); - REQUIRE(locales[3].first == "fr"); -} - -TEST_CASE("LocalizationManager - Non-yaml files in locale directory are skipped", "[localization]") { - // Covers the extension() != ".yaml" false branch (line 116) in discoverLocales. - TempTestDir temp; - writeFile(temp.path(), "en.yaml", ENGLISH_YAML); - writeFile(temp.path(), "readme.txt", "This is not a locale file"); - - LocalizationManager manager(temp.path()); - auto locales = manager.getAvailableLocales(); - - // Only the .yaml file is discovered; readme.txt is silently skipped - REQUIRE(locales.size() == 1); - REQUIRE(locales[0].first == "en"); -} diff --git a/tests/unit/test_localized_explanations.cpp b/tests/unit/test_localized_explanations.cpp index a34cdd2..8f29243 100644 --- a/tests/unit/test_localized_explanations.cpp +++ b/tests/unit/test_localized_explanations.cpp @@ -14,7 +14,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -#include "../../src/core/localization_manager.h" #include "../../src/core/localized_explanations.h" #include "../../src/core/solving_technique.h" @@ -26,18 +25,6 @@ using namespace sudoku::core; namespace { -/// Compute project root from __FILE__ (tests/unit/test_localized_explanations.cpp → project root) -[[nodiscard]] std::filesystem::path projectRoot() { - return std::filesystem::path(__FILE__).parent_path().parent_path().parent_path(); -} - -/// Create a real LocalizationManager with English locale loaded -[[nodiscard]] std::shared_ptr createEnglishLocManager() { - auto loc = std::make_shared(projectRoot() / "resources" / "locales"); - [[maybe_unused]] auto result = loc->setLocale("en"); - return loc; -} - /// Helper to build a minimal SolveStep for testing [[nodiscard]] SolveStep makeStep(SolvingTechnique technique, ExplanationData data, std::string fallback = "fallback explanation") { @@ -58,34 +45,30 @@ namespace { // ============================================================================ TEST_CASE("localizedPosition formats position with English locale", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - SECTION("Top-left corner") { - REQUIRE(localizedPosition(*loc, {.row = 0, .col = 0}) == "R1C1"); + REQUIRE(localizedPosition({.row = 0, .col = 0}) == "R1C1"); } SECTION("Middle cell") { - REQUIRE(localizedPosition(*loc, {.row = 4, .col = 4}) == "R5C5"); + REQUIRE(localizedPosition({.row = 4, .col = 4}) == "R5C5"); } SECTION("Bottom-right corner") { - REQUIRE(localizedPosition(*loc, {.row = 8, .col = 8}) == "R9C9"); + REQUIRE(localizedPosition({.row = 8, .col = 8}) == "R9C9"); } } TEST_CASE("localizedRegion formats region name with English locale", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - SECTION("Row") { - REQUIRE(localizedRegion(*loc, RegionType::Row, 2) == "Row 3"); + REQUIRE(localizedRegion(RegionType::Row, 2) == "Row 3"); } SECTION("Column") { - REQUIRE(localizedRegion(*loc, RegionType::Col, 5) == "Column 6"); + REQUIRE(localizedRegion(RegionType::Col, 5) == "Column 6"); } SECTION("Box") { - REQUIRE(localizedRegion(*loc, RegionType::Box, 0) == "Box 1"); + REQUIRE(localizedRegion(RegionType::Box, 0) == "Box 1"); } } @@ -104,14 +87,12 @@ TEST_CASE("formatValueList formats comma-separated values", "[localized_explanat } TEST_CASE("formatPositionList formats comma-separated positions", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - SECTION("Single position") { - REQUIRE(formatPositionList(*loc, {{.row = 2, .col = 3}}) == "R3C4"); + REQUIRE(formatPositionList({{.row = 2, .col = 3}}) == "R3C4"); } SECTION("Two positions") { - REQUIRE(formatPositionList(*loc, {{.row = 0, .col = 0}, {.row = 8, .col = 8}}) == "R1C1, R9C9"); + REQUIRE(formatPositionList({{.row = 0, .col = 0}, {.row = 8, .col = 8}}) == "R1C1, R9C9"); } } @@ -120,71 +101,63 @@ TEST_CASE("formatPositionList formats comma-separated positions", "[localized_ex // ============================================================================ TEST_CASE("getLocalizedExplanation - NakedSingle", "[localized_explanations]") { - auto loc = createEnglishLocManager(); auto step = makeStep(SolvingTechnique::NakedSingle, {.positions = {{.row = 2, .col = 4}}, .values = {7}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Naked Single at R3C5: only value 7 is possible"); } TEST_CASE("getLocalizedExplanation - HiddenSingle", "[localized_explanations]") { - auto loc = createEnglishLocManager(); auto step = makeStep(SolvingTechnique::HiddenSingle, {.positions = {{.row = 0, .col = 0}}, .values = {3}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Hidden Single at R1C1: value 3 can only appear in this cell within its region"); } TEST_CASE("getLocalizedExplanation - NakedPair", "[localized_explanations]") { - auto loc = createEnglishLocManager(); auto step = makeStep(SolvingTechnique::NakedPair, {.positions = {{.row = 0, .col = 0}, {.row = 0, .col = 3}}, .values = {2, 5}, .region_type = RegionType::Row, .region_index = 0}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Naked Pair [2, 5] at R1C1, R1C4 in Row 1 eliminates candidates from other cells"); } TEST_CASE("getLocalizedExplanation - NakedTriple", "[localized_explanations]") { - auto loc = createEnglishLocManager(); auto step = makeStep(SolvingTechnique::NakedTriple, {.positions = {{.row = 1, .col = 0}, {.row = 1, .col = 3}, {.row = 1, .col = 6}}, .values = {1, 4, 9}, .region_type = RegionType::Row, .region_index = 1}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Naked Triple [1, 4, 9] at R2C1, R2C4, R2C7 in Row 2 eliminates candidates from other cells"); } TEST_CASE("getLocalizedExplanation - HiddenPair", "[localized_explanations]") { - auto loc = createEnglishLocManager(); auto step = makeStep(SolvingTechnique::HiddenPair, {.positions = {{.row = 3, .col = 1}, {.row = 3, .col = 5}}, .values = {6, 8}, .region_type = RegionType::Row, .region_index = 3}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Hidden Pair [6, 8] at R4C2, R4C6 in Row 4 eliminates other candidates from these cells"); } TEST_CASE("getLocalizedExplanation - HiddenTriple", "[localized_explanations]") { - auto loc = createEnglishLocManager(); auto step = makeStep(SolvingTechnique::HiddenTriple, {.positions = {{.row = 5, .col = 0}, {.row = 5, .col = 2}, {.row = 5, .col = 7}}, .values = {2, 4, 7}, .region_type = RegionType::Row, .region_index = 5}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Hidden Triple [2, 4, 7] at R6C1, R6C3, R6C8 in Row 6 eliminates other candidates from these cells"); } TEST_CASE("getLocalizedExplanation - PointingPair", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - SECTION("Confined to row") { auto step = makeStep(SolvingTechnique::PointingPair, {.positions = {}, .values = {3}, @@ -193,7 +166,7 @@ TEST_CASE("getLocalizedExplanation - PointingPair", "[localized_explanations]") .secondary_region_type = RegionType::Row, .secondary_region_index = 1}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Pointing Pair: 3 in Box 1 confined to Row 2 eliminates 3 from other cells in Row 2"); } @@ -205,14 +178,12 @@ TEST_CASE("getLocalizedExplanation - PointingPair", "[localized_explanations]") .secondary_region_type = RegionType::Col, .secondary_region_index = 3}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Pointing Pair: 5 in Box 5 confined to Column 4 eliminates 5 from other cells in Column 4"); } } TEST_CASE("getLocalizedExplanation - BoxLineReduction", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - SECTION("Row confined to box") { auto step = makeStep(SolvingTechnique::BoxLineReduction, {.positions = {}, .values = {7}, @@ -221,7 +192,7 @@ TEST_CASE("getLocalizedExplanation - BoxLineReduction", "[localized_explanations .secondary_region_type = RegionType::Box, .secondary_region_index = 0}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Box/Line Reduction: 7 in Row 3 confined to Box 1 eliminates 7 from other cells in Box 1"); } @@ -233,13 +204,12 @@ TEST_CASE("getLocalizedExplanation - BoxLineReduction", "[localized_explanations .secondary_region_type = RegionType::Box, .secondary_region_index = 7}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Box/Line Reduction: 4 in Column 6 confined to Box 8 eliminates 4 from other cells in Box 8"); } } TEST_CASE("getLocalizedExplanation - NakedQuad", "[localized_explanations]") { - auto loc = createEnglishLocManager(); auto step = makeStep(SolvingTechnique::NakedQuad, {.positions = {{.row = 0, .col = 0}, {.row = 0, .col = 1}, {.row = 0, .col = 2}, {.row = 0, .col = 3}}, @@ -247,13 +217,12 @@ TEST_CASE("getLocalizedExplanation - NakedQuad", "[localized_explanations]") { .region_type = RegionType::Row, .region_index = 0}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Naked Quad [1, 2, 3, 4] at R1C1, R1C2, R1C3, R1C4 in Row 1 eliminates candidates from other cells"); } TEST_CASE("getLocalizedExplanation - HiddenQuad", "[localized_explanations]") { - auto loc = createEnglishLocManager(); auto step = makeStep(SolvingTechnique::HiddenQuad, {.positions = {{.row = 6, .col = 0}, {.row = 6, .col = 2}, {.row = 6, .col = 5}, {.row = 6, .col = 8}}, @@ -261,13 +230,12 @@ TEST_CASE("getLocalizedExplanation - HiddenQuad", "[localized_explanations]") { .region_type = RegionType::Row, .region_index = 6}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Hidden Quad [1, 3, 5, 9] at R7C1, R7C3, R7C6, R7C9 in Row 7 eliminates other candidates from these cells"); } TEST_CASE("getLocalizedExplanation - XWing row-based", "[localized_explanations]") { - auto loc = createEnglishLocManager(); // Positions: [r1c1, r1c2, r2c1, r2c2] = corners of the X-Wing auto step = makeStep(SolvingTechnique::XWing, @@ -275,40 +243,37 @@ TEST_CASE("getLocalizedExplanation - XWing row-based", "[localized_explanations] .values = {6}, .region_type = RegionType::Row}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "X-Wing on value 6 in Rows 2 and 6, Columns 3 and 8 eliminates 6 from other cells in those columns"); } TEST_CASE("getLocalizedExplanation - XWing col-based", "[localized_explanations]") { - auto loc = createEnglishLocManager(); auto step = makeStep(SolvingTechnique::XWing, {.positions = {{.row = 0, .col = 3}, {.row = 4, .col = 3}, {.row = 0, .col = 7}, {.row = 4, .col = 7}}, .values = {2}, .region_type = RegionType::Col}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "X-Wing on value 2 in Columns 4 and 4, Rows 1 and 1 eliminates 2 from other cells in those rows"); } TEST_CASE("getLocalizedExplanation - XYWing", "[localized_explanations]") { - auto loc = createEnglishLocManager(); // pivot has {a,b}, wing1 has {a,c}, wing2 has {b,c} auto step = makeStep( SolvingTechnique::XYWing, {.positions = {{.row = 3, .col = 3}, {.row = 3, .col = 7}, {.row = 6, .col = 3}}, .values = {2, 5, 8}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "XY-Wing: pivot R4C4 {2,5}, wing R4C8 {2,8}, wing R7C4 {5,8} eliminates 8 from cells seeing both wings"); } TEST_CASE("getLocalizedExplanation - Backtracking returns raw explanation", "[localized_explanations]") { - auto loc = createEnglishLocManager(); auto step = makeStep(SolvingTechnique::Backtracking, {}, "Solved by backtracking"); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Solved by backtracking"); } @@ -317,24 +282,22 @@ TEST_CASE("getLocalizedExplanation - Backtracking returns raw explanation", "[lo // ============================================================================ TEST_CASE("getLocalizedExplanation falls back when data is insufficient", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - SECTION("NakedSingle with no positions") { auto step = makeStep(SolvingTechnique::NakedSingle, {.positions = {}, .values = {5}}, "raw naked single"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw naked single"); + REQUIRE(getLocalizedExplanation(step) == "raw naked single"); } SECTION("NakedSingle with no values") { auto step = makeStep(SolvingTechnique::NakedSingle, {.positions = {{.row = 0, .col = 0}}, .values = {}}, "raw naked single"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw naked single"); + REQUIRE(getLocalizedExplanation(step) == "raw naked single"); } SECTION("NakedPair with insufficient positions") { auto step = makeStep(SolvingTechnique::NakedPair, {.positions = {{.row = 0, .col = 0}}, .values = {1, 2}, .region_type = RegionType::Row}, "raw naked pair"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw naked pair"); + REQUIRE(getLocalizedExplanation(step) == "raw naked pair"); } SECTION("XWing with no region_type") { @@ -343,7 +306,7 @@ TEST_CASE("getLocalizedExplanation falls back when data is insufficient", "[loca {.positions = {{.row = 0, .col = 0}, {.row = 0, .col = 1}, {.row = 1, .col = 0}, {.row = 1, .col = 1}}, .values = {3}}, "raw x-wing"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw x-wing"); + REQUIRE(getLocalizedExplanation(step) == "raw x-wing"); } } @@ -352,9 +315,8 @@ TEST_CASE("getLocalizedExplanation falls back when data is insufficient", "[loca // ============================================================================ TEST_CASE("localizedRegion with None type returns unknown region string", "[localized_explanations]") { - auto loc = createEnglishLocManager(); // RegionType::None triggers the default case in localizedRegion - auto result = localizedRegion(*loc, RegionType::None, 0); + auto result = localizedRegion(RegionType::None, 0); // Just verify it doesn't crash and returns something REQUIRE(!result.empty()); } @@ -364,15 +326,13 @@ TEST_CASE("localizedRegion with None type returns unknown region string", "[loca // ============================================================================ TEST_CASE("getLocalizedExplanation sub-condition fallbacks", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - SECTION("NakedPair: values too few → fallback") { auto step = makeStep(SolvingTechnique::NakedPair, {.positions = {{.row = 0, .col = 0}, {.row = 0, .col = 3}}, .values = {1}, // only 1 value, need 2 .region_type = RegionType::Row}, "raw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw"); + REQUIRE(getLocalizedExplanation(step) == "raw"); } SECTION("NakedPair: region=None → fallback") { @@ -381,7 +341,7 @@ TEST_CASE("getLocalizedExplanation sub-condition fallbacks", "[localized_explana .values = {1, 2}, .region_type = RegionType::None}, // no region "raw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw"); + REQUIRE(getLocalizedExplanation(step) == "raw"); } SECTION("NakedTriple: values too few → fallback") { @@ -390,7 +350,7 @@ TEST_CASE("getLocalizedExplanation sub-condition fallbacks", "[localized_explana .values = {1, 2}, // only 2 values, need 3 .region_type = RegionType::Row}, "raw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw"); + REQUIRE(getLocalizedExplanation(step) == "raw"); } SECTION("NakedTriple: region=None → fallback") { @@ -399,7 +359,7 @@ TEST_CASE("getLocalizedExplanation sub-condition fallbacks", "[localized_explana .values = {1, 2, 3}, .region_type = RegionType::None}, "raw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw"); + REQUIRE(getLocalizedExplanation(step) == "raw"); } SECTION("HiddenPair: values too few → fallback") { @@ -408,7 +368,7 @@ TEST_CASE("getLocalizedExplanation sub-condition fallbacks", "[localized_explana .values = {1}, // need 2 .region_type = RegionType::Row}, "raw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw"); + REQUIRE(getLocalizedExplanation(step) == "raw"); } SECTION("HiddenPair: region=None → fallback") { @@ -417,7 +377,7 @@ TEST_CASE("getLocalizedExplanation sub-condition fallbacks", "[localized_explana .values = {1, 2}, .region_type = RegionType::None}, "raw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw"); + REQUIRE(getLocalizedExplanation(step) == "raw"); } SECTION("HiddenTriple: values too few → fallback") { @@ -426,7 +386,7 @@ TEST_CASE("getLocalizedExplanation sub-condition fallbacks", "[localized_explana .values = {1, 2}, // need 3 .region_type = RegionType::Row}, "raw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw"); + REQUIRE(getLocalizedExplanation(step) == "raw"); } SECTION("HiddenTriple: region=None → fallback") { @@ -435,7 +395,7 @@ TEST_CASE("getLocalizedExplanation sub-condition fallbacks", "[localized_explana .values = {1, 2, 3}, .region_type = RegionType::None}, "raw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw"); + REQUIRE(getLocalizedExplanation(step) == "raw"); } SECTION("NakedQuad: values too few → fallback") { @@ -445,7 +405,7 @@ TEST_CASE("getLocalizedExplanation sub-condition fallbacks", "[localized_explana .values = {1, 2, 3}, // need 4 .region_type = RegionType::Row}, "raw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw"); + REQUIRE(getLocalizedExplanation(step) == "raw"); } SECTION("NakedQuad: region=None → fallback") { @@ -455,7 +415,7 @@ TEST_CASE("getLocalizedExplanation sub-condition fallbacks", "[localized_explana .values = {1, 2, 3, 4}, .region_type = RegionType::None}, "raw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw"); + REQUIRE(getLocalizedExplanation(step) == "raw"); } SECTION("HiddenQuad: values too few → fallback") { @@ -465,7 +425,7 @@ TEST_CASE("getLocalizedExplanation sub-condition fallbacks", "[localized_explana .values = {1, 2, 3}, // need 4 .region_type = RegionType::Row}, "raw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw"); + REQUIRE(getLocalizedExplanation(step) == "raw"); } SECTION("HiddenQuad: region=None → fallback") { @@ -475,7 +435,7 @@ TEST_CASE("getLocalizedExplanation sub-condition fallbacks", "[localized_explana .values = {1, 2, 3, 4}, .region_type = RegionType::None}, "raw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw"); + REQUIRE(getLocalizedExplanation(step) == "raw"); } SECTION("PointingPair: values empty → fallback") { @@ -485,7 +445,7 @@ TEST_CASE("getLocalizedExplanation sub-condition fallbacks", "[localized_explana .region_type = RegionType::Box, .secondary_region_type = RegionType::Row}, "raw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw"); + REQUIRE(getLocalizedExplanation(step) == "raw"); } SECTION("BoxLineReduction: region=None → fallback") { @@ -495,7 +455,7 @@ TEST_CASE("getLocalizedExplanation sub-condition fallbacks", "[localized_explana .region_type = RegionType::None, // no region .secondary_region_type = RegionType::Box}, "raw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw"); + REQUIRE(getLocalizedExplanation(step) == "raw"); } SECTION("BoxLineReduction: secondary=None → fallback") { @@ -503,7 +463,7 @@ TEST_CASE("getLocalizedExplanation sub-condition fallbacks", "[localized_explana SolvingTechnique::BoxLineReduction, {.positions = {}, .values = {3}, .region_type = RegionType::Row, .secondary_region_type = RegionType::None}, "raw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw"); + REQUIRE(getLocalizedExplanation(step) == "raw"); } } @@ -512,36 +472,32 @@ TEST_CASE("getLocalizedExplanation sub-condition fallbacks", "[localized_explana // ============================================================================ TEST_CASE("getLocalizedExplanation HiddenSingle fallback", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - SECTION("No positions") { auto step = makeStep(SolvingTechnique::HiddenSingle, {.positions = {}, .values = {5}}, "raw hs"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw hs"); + REQUIRE(getLocalizedExplanation(step) == "raw hs"); } SECTION("No values") { auto step = makeStep(SolvingTechnique::HiddenSingle, {.positions = {{.row = 0, .col = 0}}, .values = {}}, "raw hs2"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw hs2"); + REQUIRE(getLocalizedExplanation(step) == "raw hs2"); } } TEST_CASE("getLocalizedExplanation positions-too-few fallbacks", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - SECTION("NakedTriple: positions<3 → fallback") { auto step = makeStep(SolvingTechnique::NakedTriple, {.positions = {{.row = 0, .col = 0}, {.row = 0, .col = 3}}, .values = {1, 2, 3}, .region_type = RegionType::Row}, "raw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw"); + REQUIRE(getLocalizedExplanation(step) == "raw"); } SECTION("HiddenPair: positions<2 → fallback") { auto step = makeStep(SolvingTechnique::HiddenPair, {.positions = {}, .values = {1, 2}, .region_type = RegionType::Row}, "raw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw"); + REQUIRE(getLocalizedExplanation(step) == "raw"); } SECTION("HiddenTriple: positions<3 → fallback") { @@ -550,7 +506,7 @@ TEST_CASE("getLocalizedExplanation positions-too-few fallbacks", "[localized_exp .values = {1, 2, 3}, .region_type = RegionType::Row}, "raw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw"); + REQUIRE(getLocalizedExplanation(step) == "raw"); } SECTION("NakedQuad: positions<4 → fallback") { @@ -559,7 +515,7 @@ TEST_CASE("getLocalizedExplanation positions-too-few fallbacks", "[localized_exp .values = {1, 2, 3, 4}, .region_type = RegionType::Row}, "raw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw"); + REQUIRE(getLocalizedExplanation(step) == "raw"); } SECTION("HiddenQuad: positions<4 → fallback") { @@ -568,7 +524,7 @@ TEST_CASE("getLocalizedExplanation positions-too-few fallbacks", "[localized_exp .values = {1, 2, 3, 4}, .region_type = RegionType::Row}, "raw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw"); + REQUIRE(getLocalizedExplanation(step) == "raw"); } SECTION("XWing Row branch: positions<4 → fallback") { @@ -578,7 +534,7 @@ TEST_CASE("getLocalizedExplanation positions-too-few fallbacks", "[localized_exp .values = {6}, .region_type = RegionType::Row}, "raw xw row"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw xw row"); + REQUIRE(getLocalizedExplanation(step) == "raw xw row"); } SECTION("XWing Col branch: positions<4 → fallback") { @@ -587,25 +543,23 @@ TEST_CASE("getLocalizedExplanation positions-too-few fallbacks", "[localized_exp .values = {2}, .region_type = RegionType::Col}, "raw xw col"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw xw col"); + REQUIRE(getLocalizedExplanation(step) == "raw xw col"); } SECTION("XYWing: positions<3 → fallback") { auto step = makeStep(SolvingTechnique::XYWing, {.positions = {{.row = 3, .col = 3}}, .values = {2, 5, 8}}, "raw xywing"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw xywing"); + REQUIRE(getLocalizedExplanation(step) == "raw xywing"); } } TEST_CASE("getLocalizedExplanation PointingPair additional sub-conditions", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - SECTION("region_type=None → fallback") { auto step = makeStep( SolvingTechnique::PointingPair, {.positions = {}, .values = {3}, .region_type = RegionType::None, .secondary_region_type = RegionType::Row}, "raw pp"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw pp"); + REQUIRE(getLocalizedExplanation(step) == "raw pp"); } SECTION("secondary_region_type=None → fallback") { @@ -613,7 +567,7 @@ TEST_CASE("getLocalizedExplanation PointingPair additional sub-conditions", "[lo SolvingTechnique::PointingPair, {.positions = {}, .values = {3}, .region_type = RegionType::Box, .secondary_region_type = RegionType::None}, "raw pp2"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw pp2"); + REQUIRE(getLocalizedExplanation(step) == "raw pp2"); } SECTION("secondary_region_type=Box → inner default case") { @@ -624,20 +578,18 @@ TEST_CASE("getLocalizedExplanation PointingPair additional sub-conditions", "[lo .region_index = 2, .secondary_region_type = RegionType::Box, .secondary_region_index = 2}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); // returns something (uses RegionUnknown) } } TEST_CASE("getLocalizedExplanation BoxLineReduction additional sub-conditions", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - SECTION("values empty → fallback") { auto step = makeStep( SolvingTechnique::BoxLineReduction, {.positions = {}, .values = {}, .region_type = RegionType::Row, .secondary_region_type = RegionType::Box}, "raw blr"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw blr"); + REQUIRE(getLocalizedExplanation(step) == "raw blr"); } SECTION("region_type=Box → inner default case") { @@ -649,33 +601,27 @@ TEST_CASE("getLocalizedExplanation BoxLineReduction additional sub-conditions", .region_index = 0, .secondary_region_type = RegionType::Row, .secondary_region_index = 0}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } } TEST_CASE("getLocalizedExplanation Backtracking case", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - // Backtracking case just returns step.explanation directly auto step = makeStep(SolvingTechnique::Backtracking, {}, "backtracking raw explanation"); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "backtracking raw explanation"); } TEST_CASE("getLocalizedExplanation localizedRegion None type", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - // RegionType::None hits the default case in localizedRegion switch - REQUIRE_FALSE(localizedRegion(*loc, RegionType::None, 0).empty()); + REQUIRE_FALSE(localizedRegion(RegionType::None, 0).empty()); } TEST_CASE("getLocalizedExplanation formatPositionList with empty list", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - // formatPositionList with empty positions should return "" std::vector empty; - REQUIRE(formatPositionList(*loc, empty).empty()); + REQUIRE(formatPositionList(empty).empty()); } // ============================================================================ @@ -683,22 +629,20 @@ TEST_CASE("getLocalizedExplanation formatPositionList with empty list", "[locali // ============================================================================ TEST_CASE("getLocalizedTechniqueName returns English names", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - using namespace std::string_view_literals; - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::NakedSingle) == "Naked Single"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::HiddenSingle) == "Hidden Single"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::NakedPair) == "Naked Pair"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::NakedTriple) == "Naked Triple"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::HiddenPair) == "Hidden Pair"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::HiddenTriple) == "Hidden Triple"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::PointingPair) == "Pointing Pair"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::BoxLineReduction) == "Box/Line Reduction"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::NakedQuad) == "Naked Quad"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::HiddenQuad) == "Hidden Quad"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::XWing) == "X-Wing"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::XYWing) == "XY-Wing"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::Backtracking) == "Backtracking"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::NakedSingle) == "Naked Single"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::HiddenSingle) == "Hidden Single"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::NakedPair) == "Naked Pair"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::NakedTriple) == "Naked Triple"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::HiddenPair) == "Hidden Pair"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::HiddenTriple) == "Hidden Triple"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::PointingPair) == "Pointing Pair"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::BoxLineReduction) == "Box/Line Reduction"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::NakedQuad) == "Naked Quad"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::HiddenQuad) == "Hidden Quad"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::XWing) == "X-Wing"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::XYWing) == "XY-Wing"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::Backtracking) == "Backtracking"sv); } // ============================================================================ @@ -709,40 +653,38 @@ TEST_CASE("getLocalizedTechniqueName returns English names", "[localized_explana // ============================================================================ TEST_CASE("getLocalizedExplanation - Swordfish", "[localized_explanations]") { - auto loc = createEnglishLocManager(); // values = {candidate, r1, r2, r3, c1, c2, c3} (7 values) SECTION("Row-based Swordfish") { auto step = makeStep(SolvingTechnique::Swordfish, {.values = {5, 1, 2, 3, 4, 5, 6}, .region_type = RegionType::Row}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Col-based Swordfish") { auto step = makeStep(SolvingTechnique::Swordfish, {.values = {5, 1, 2, 3, 4, 5, 6}, .region_type = RegionType::Col}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: values too few") { auto step = makeStep(SolvingTechnique::Swordfish, {.values = {5, 1}, .region_type = RegionType::Row}, "swordfish fallback"); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "swordfish fallback"); } SECTION("Fallback: region_type None") { auto step = makeStep(SolvingTechnique::Swordfish, {.values = {5, 1, 2, 3, 4, 5, 6}, .region_type = RegionType::None}, "swordfish fallback"); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "swordfish fallback"); } } TEST_CASE("getLocalizedExplanation - Skyscraper", "[localized_explanations]") { - auto loc = createEnglishLocManager(); const std::vector four_pos = {{0, 0}, {0, 4}, {2, 0}, {5, 4}}; SECTION("Happy path") { @@ -752,56 +694,53 @@ TEST_CASE("getLocalizedExplanation - Skyscraper", "[localized_explanations]") { .region_index = 0, .secondary_region_type = RegionType::Col, .secondary_region_index = 0}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: positions too few") { auto step = makeStep(SolvingTechnique::Skyscraper, {.positions = {{0, 0}}, .values = {5}}, "skyscraper fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "skyscraper fallback"); + REQUIRE(getLocalizedExplanation(step) == "skyscraper fallback"); } SECTION("Fallback: values empty") { auto step = makeStep(SolvingTechnique::Skyscraper, {.positions = four_pos}, "skyscraper fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "skyscraper fallback"); + REQUIRE(getLocalizedExplanation(step) == "skyscraper fallback"); } } TEST_CASE("getLocalizedExplanation - TwoStringKite", "[localized_explanations]") { - auto loc = createEnglishLocManager(); const std::vector four_pos = {{0, 0}, {0, 4}, {3, 0}, {6, 4}}; SECTION("Happy path") { auto step = makeStep(SolvingTechnique::TwoStringKite, {.positions = four_pos, .values = {5}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: positions too few") { auto step = makeStep(SolvingTechnique::TwoStringKite, {.positions = {{0, 0}}, .values = {5}}, "kite fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "kite fallback"); + REQUIRE(getLocalizedExplanation(step) == "kite fallback"); } } TEST_CASE("getLocalizedExplanation - XYZWing", "[localized_explanations]") { - auto loc = createEnglishLocManager(); const std::vector three_pos = {{0, 0}, {0, 4}, {3, 0}}; SECTION("Happy path") { auto step = makeStep(SolvingTechnique::XYZWing, {.positions = three_pos, .values = {1, 2, 3}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: values too few") { auto step = makeStep(SolvingTechnique::XYZWing, {.positions = three_pos, .values = {1, 2}}, "xyzwing fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "xyzwing fallback"); + REQUIRE(getLocalizedExplanation(step) == "xyzwing fallback"); } } TEST_CASE("getLocalizedExplanation - UniqueRectangle", "[localized_explanations]") { - auto loc = createEnglishLocManager(); const std::vector four_pos = {{0, 0}, {0, 3}, {3, 0}, {3, 3}}; SECTION("Type 1 (subtype=0)") { @@ -810,7 +749,7 @@ TEST_CASE("getLocalizedExplanation - UniqueRectangle", "[localized_explanations] .secondary_region_type = RegionType::Row, .secondary_region_index = 0, .technique_subtype = 0}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } @@ -820,7 +759,7 @@ TEST_CASE("getLocalizedExplanation - UniqueRectangle", "[localized_explanations] .secondary_region_type = RegionType::Row, .secondary_region_index = 0, .technique_subtype = 1}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } @@ -830,7 +769,7 @@ TEST_CASE("getLocalizedExplanation - UniqueRectangle", "[localized_explanations] .secondary_region_type = RegionType::Row, .secondary_region_index = 0, .technique_subtype = 2}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } @@ -840,7 +779,7 @@ TEST_CASE("getLocalizedExplanation - UniqueRectangle", "[localized_explanations] .secondary_region_type = RegionType::Row, .secondary_region_index = 0, .technique_subtype = 3}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } @@ -850,69 +789,65 @@ TEST_CASE("getLocalizedExplanation - UniqueRectangle", "[localized_explanations] .secondary_region_type = RegionType::Row, .secondary_region_index = 0, .technique_subtype = 5}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: positions too few") { auto step = makeStep(SolvingTechnique::UniqueRectangle, {.positions = {{0, 0}}, .values = {1, 2}}, "ur fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "ur fallback"); + REQUIRE(getLocalizedExplanation(step) == "ur fallback"); } } TEST_CASE("getLocalizedExplanation - WWing", "[localized_explanations]") { - auto loc = createEnglishLocManager(); const std::vector four_pos = { {.row = 0, .col = 0}, {.row = 0, .col = 4}, {.row = 3, .col = 0}, {.row = 6, .col = 4}}; SECTION("Happy path") { auto step = makeStep(SolvingTechnique::WWing, {.positions = four_pos, .values = {1, 2}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: positions too few") { auto step = makeStep(SolvingTechnique::WWing, {.positions = {{0, 0}}, .values = {1, 2}}, "wwing fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "wwing fallback"); + REQUIRE(getLocalizedExplanation(step) == "wwing fallback"); } SECTION("Fallback: values too few") { auto step = makeStep(SolvingTechnique::WWing, {.positions = four_pos, .values = {1}}, "wwing fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "wwing fallback"); + REQUIRE(getLocalizedExplanation(step) == "wwing fallback"); } } TEST_CASE("getLocalizedExplanation - SimpleColoring", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - SECTION("Contradiction (subtype=0)") { auto step = makeStep(SolvingTechnique::SimpleColoring, {.values = {5}, .technique_subtype = 0}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Exclusion (subtype=1) with position") { auto step = makeStep(SolvingTechnique::SimpleColoring, {.positions = {{2, 3}}, .values = {5}, .technique_subtype = 1}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: values empty") { auto step = makeStep(SolvingTechnique::SimpleColoring, {}, "simple fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "simple fallback"); + REQUIRE(getLocalizedExplanation(step) == "simple fallback"); } SECTION("Fallback: subtype=1 but positions empty") { auto step = makeStep(SolvingTechnique::SimpleColoring, {.values = {5}, .technique_subtype = 1}, "simple fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "simple fallback"); + REQUIRE(getLocalizedExplanation(step) == "simple fallback"); } } TEST_CASE("getLocalizedExplanation - FinnedXWing", "[localized_explanations]") { - auto loc = createEnglishLocManager(); // values[0..4] = {candidate, row1, row2, col1, col2}; positions.back() = fin const std::vector vals = {5, 1, 2, 3, 4}; const std::vector fin_pos = {{.row = 0, .col = 5}}; @@ -920,92 +855,86 @@ TEST_CASE("getLocalizedExplanation - FinnedXWing", "[localized_explanations]") { SECTION("Row-based FinnedXWing") { auto step = makeStep(SolvingTechnique::FinnedXWing, {.positions = fin_pos, .values = vals, .region_type = RegionType::Row}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Col-based FinnedXWing") { auto step = makeStep(SolvingTechnique::FinnedXWing, {.positions = fin_pos, .values = vals, .region_type = RegionType::Col}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: values empty") { auto step = makeStep(SolvingTechnique::FinnedXWing, {.positions = fin_pos, .region_type = RegionType::Row}, "finxwing fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "finxwing fallback"); + REQUIRE(getLocalizedExplanation(step) == "finxwing fallback"); } SECTION("Fallback: positions empty (second guard)") { auto step = makeStep(SolvingTechnique::FinnedXWing, {.values = vals, .region_type = RegionType::Row}, "finxwing fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "finxwing fallback"); + REQUIRE(getLocalizedExplanation(step) == "finxwing fallback"); } } TEST_CASE("getLocalizedExplanation - RemotePairs", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - SECTION("Happy path") { auto step = makeStep(SolvingTechnique::RemotePairs, {.positions = {{0, 0}, {5, 5}}, .values = {1, 2, 4}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: values too few") { auto step = makeStep(SolvingTechnique::RemotePairs, {.positions = {{0, 0}, {5, 5}}, .values = {1, 2}}, "rp fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "rp fallback"); + REQUIRE(getLocalizedExplanation(step) == "rp fallback"); } } TEST_CASE("getLocalizedExplanation - BUG", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - SECTION("Happy path") { auto step = makeStep(SolvingTechnique::BUG, {.positions = {{3, 4}}, .values = {7}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: positions empty") { auto step = makeStep(SolvingTechnique::BUG, {.values = {7}}, "bug fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "bug fallback"); + REQUIRE(getLocalizedExplanation(step) == "bug fallback"); } SECTION("Fallback: values empty") { auto step = makeStep(SolvingTechnique::BUG, {.positions = {{3, 4}}}, "bug fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "bug fallback"); + REQUIRE(getLocalizedExplanation(step) == "bug fallback"); } } TEST_CASE("getLocalizedExplanation - Jellyfish", "[localized_explanations]") { - auto loc = createEnglishLocManager(); // values = {candidate, r1, r2, r3, r4, c1, c2, c3, c4} (9 values) const std::vector vals = {5, 1, 2, 3, 4, 5, 6, 7, 8}; SECTION("Row-based Jellyfish") { auto step = makeStep(SolvingTechnique::Jellyfish, {.values = vals, .region_type = RegionType::Row}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Col-based Jellyfish") { auto step = makeStep(SolvingTechnique::Jellyfish, {.values = vals, .region_type = RegionType::Col}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: values too few") { auto step = makeStep(SolvingTechnique::Jellyfish, {.values = {5, 1}, .region_type = RegionType::Row}, "jf fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "jf fallback"); + REQUIRE(getLocalizedExplanation(step) == "jf fallback"); } } TEST_CASE("getLocalizedExplanation - FinnedSwordfish", "[localized_explanations]") { - auto loc = createEnglishLocManager(); // values = {candidate, row/col1..3}; positions.back() = fin const std::vector vals = {5, 1, 2, 3}; const std::vector fin_pos = {{.row = 0, .col = 5}}; @@ -1013,66 +942,62 @@ TEST_CASE("getLocalizedExplanation - FinnedSwordfish", "[localized_explanations] SECTION("Row-based FinnedSwordfish") { auto step = makeStep(SolvingTechnique::FinnedSwordfish, {.positions = fin_pos, .values = vals, .region_type = RegionType::Row}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Col-based FinnedSwordfish") { auto step = makeStep(SolvingTechnique::FinnedSwordfish, {.positions = fin_pos, .values = vals, .region_type = RegionType::Col}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: positions empty") { auto step = makeStep(SolvingTechnique::FinnedSwordfish, {.values = vals, .region_type = RegionType::Row}, "fsf fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "fsf fallback"); + REQUIRE(getLocalizedExplanation(step) == "fsf fallback"); } } TEST_CASE("getLocalizedExplanation - EmptyRectangle", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - SECTION("Happy path") { auto step = makeStep( SolvingTechnique::EmptyRectangle, {.positions = {{.row = 4, .col = 4}}, .values = {5, 3}, .region_type = RegionType::Row, .region_index = 2}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: positions empty") { auto step = makeStep(SolvingTechnique::EmptyRectangle, {.values = {5, 3}, .region_type = RegionType::Row}, "er fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "er fallback"); + REQUIRE(getLocalizedExplanation(step) == "er fallback"); } SECTION("Fallback: values too few") { auto step = makeStep(SolvingTechnique::EmptyRectangle, {.positions = {{4, 4}}, .values = {5}, .region_type = RegionType::Row}, "er fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "er fallback"); + REQUIRE(getLocalizedExplanation(step) == "er fallback"); } } TEST_CASE("getLocalizedExplanation - WXYZWing", "[localized_explanations]") { - auto loc = createEnglishLocManager(); const std::vector four_pos = {{0, 0}, {0, 4}, {3, 0}, {6, 4}}; SECTION("Happy path") { auto step = makeStep(SolvingTechnique::WXYZWing, {.positions = four_pos, .values = {5}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: positions too few") { auto step = makeStep(SolvingTechnique::WXYZWing, {.positions = {{0, 0}}, .values = {5}}, "wxyz fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "wxyz fallback"); + REQUIRE(getLocalizedExplanation(step) == "wxyz fallback"); } } TEST_CASE("getLocalizedExplanation - FinnedJellyfish", "[localized_explanations]") { - auto loc = createEnglishLocManager(); // values = {candidate, row/col1..4}; positions.back() = fin const std::vector vals = {5, 1, 2, 3, 4}; const std::vector fin_pos = {{0, 5}}; @@ -1080,148 +1005,137 @@ TEST_CASE("getLocalizedExplanation - FinnedJellyfish", "[localized_explanations] SECTION("Row-based FinnedJellyfish") { auto step = makeStep(SolvingTechnique::FinnedJellyfish, {.positions = fin_pos, .values = vals, .region_type = RegionType::Row}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Col-based FinnedJellyfish") { auto step = makeStep(SolvingTechnique::FinnedJellyfish, {.positions = fin_pos, .values = vals, .region_type = RegionType::Col}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: values too few") { auto step = makeStep(SolvingTechnique::FinnedJellyfish, {.positions = fin_pos, .values = {5}, .region_type = RegionType::Row}, "fjf fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "fjf fallback"); + REQUIRE(getLocalizedExplanation(step) == "fjf fallback"); } } TEST_CASE("getLocalizedExplanation - XYChain", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - SECTION("Happy path") { auto step = makeStep(SolvingTechnique::XYChain, {.positions = {{0, 0}, {5, 5}}, .values = {5, 3}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: positions too few") { auto step = makeStep(SolvingTechnique::XYChain, {.positions = {{0, 0}}, .values = {5, 3}}, "xychain fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "xychain fallback"); + REQUIRE(getLocalizedExplanation(step) == "xychain fallback"); } } TEST_CASE("getLocalizedExplanation - MultiColoring", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - SECTION("Wrap contradiction (subtype=0)") { auto step = makeStep(SolvingTechnique::MultiColoring, {.values = {5}, .technique_subtype = 0}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Trap exclusion (subtype=1) with position") { auto step = makeStep(SolvingTechnique::MultiColoring, {.positions = {{3, 4}}, .values = {5}, .technique_subtype = 1}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: values empty") { auto step = makeStep(SolvingTechnique::MultiColoring, {}, "mc fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "mc fallback"); + REQUIRE(getLocalizedExplanation(step) == "mc fallback"); } SECTION("Fallback: subtype=1 but positions empty") { auto step = makeStep(SolvingTechnique::MultiColoring, {.values = {5}, .technique_subtype = 1}, "mc fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "mc fallback"); + REQUIRE(getLocalizedExplanation(step) == "mc fallback"); } } TEST_CASE("getLocalizedExplanation - ALSxZ", "[localized_explanations]") { - auto loc = createEnglishLocManager(); // values = {X, Z, als_a_size, als_b_size}; positions = als_a_cells + als_b_cells const std::vector positions = {{0, 0}, {0, 1}, {1, 0}, {1, 1}}; SECTION("Happy path") { auto step = makeStep(SolvingTechnique::ALSxZ, {.positions = positions, .values = {1, 2, 2, 2}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: values too few") { auto step = makeStep(SolvingTechnique::ALSxZ, {.positions = positions, .values = {1, 2}}, "als fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "als fallback"); + REQUIRE(getLocalizedExplanation(step) == "als fallback"); } SECTION("Fallback: positions empty") { auto step = makeStep(SolvingTechnique::ALSxZ, {.values = {1, 2, 2, 2}}, "als fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "als fallback"); + REQUIRE(getLocalizedExplanation(step) == "als fallback"); } } TEST_CASE("getLocalizedExplanation - SueDeCoq", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - SECTION("Happy path") { auto step = makeStep(SolvingTechnique::SueDeCoq, {.values = {5}, .region_type = RegionType::Row, .region_index = 2, .secondary_region_index = 1}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: values empty") { auto step = makeStep(SolvingTechnique::SueDeCoq, {.region_type = RegionType::Row}, "sdc fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "sdc fallback"); + REQUIRE(getLocalizedExplanation(step) == "sdc fallback"); } SECTION("Fallback: region_type None") { auto step = makeStep(SolvingTechnique::SueDeCoq, {.values = {5}, .region_type = RegionType::None}, "sdc fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "sdc fallback"); + REQUIRE(getLocalizedExplanation(step) == "sdc fallback"); } } TEST_CASE("getLocalizedExplanation - ForcingChain", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - SECTION("Happy path") { auto step = makeStep(SolvingTechnique::ForcingChain, {.positions = {{0, 0}}, .values = {5}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: positions empty") { auto step = makeStep(SolvingTechnique::ForcingChain, {.values = {5}}, "fc fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "fc fallback"); + REQUIRE(getLocalizedExplanation(step) == "fc fallback"); } SECTION("Fallback: values empty") { auto step = makeStep(SolvingTechnique::ForcingChain, {.positions = {{0, 0}}}, "fc fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "fc fallback"); + REQUIRE(getLocalizedExplanation(step) == "fc fallback"); } } TEST_CASE("getLocalizedExplanation - NiceLoop", "[localized_explanations]") { - auto loc = createEnglishLocManager(); - SECTION("Happy path") { auto step = makeStep(SolvingTechnique::NiceLoop, {.positions = {{0, 0}, {5, 5}}, .values = {5}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(!result.empty()); } SECTION("Fallback: positions too few") { auto step = makeStep(SolvingTechnique::NiceLoop, {.positions = {{0, 0}}, .values = {5}}, "nl fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "nl fallback"); + REQUIRE(getLocalizedExplanation(step) == "nl fallback"); } SECTION("Fallback: values empty") { auto step = makeStep(SolvingTechnique::NiceLoop, {.positions = {{0, 0}, {5, 5}}}, "nl fallback"); - REQUIRE(getLocalizedExplanation(*loc, step) == "nl fallback"); + REQUIRE(getLocalizedExplanation(step) == "nl fallback"); } } @@ -1231,28 +1145,27 @@ TEST_CASE("getLocalizedExplanation - NiceLoop", "[localized_explanations]") { // ============================================================================ TEST_CASE("getLocalizedTechniqueName - advanced techniques", "[localized_explanations]") { - auto loc = createEnglishLocManager(); using namespace std::string_view_literals; - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::Swordfish) == "Swordfish"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::Skyscraper) == "Skyscraper"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::TwoStringKite) == "2-String Kite"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::XYZWing) == "XYZ-Wing"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::UniqueRectangle) == "Unique Rectangle"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::WWing) == "W-Wing"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::SimpleColoring) == "Simple Coloring"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::FinnedXWing) == "Finned X-Wing"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::RemotePairs) == "Remote Pairs"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::BUG) == "BUG"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::Jellyfish) == "Jellyfish"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::FinnedSwordfish) == "Finned Swordfish"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::EmptyRectangle) == "Empty Rectangle"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::WXYZWing) == "WXYZ-Wing"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::FinnedJellyfish) == "Finned Jellyfish"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::XYChain) == "XY-Chain"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::MultiColoring) == "Multi-Coloring"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::ALSxZ) == "ALS-XZ"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::SueDeCoq) == "Sue de Coq"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::ForcingChain) == "Forcing Chain"sv); - REQUIRE(getLocalizedTechniqueName(*loc, SolvingTechnique::NiceLoop) == "Nice Loop"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::Swordfish) == "Swordfish"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::Skyscraper) == "Skyscraper"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::TwoStringKite) == "2-String Kite"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::XYZWing) == "XYZ-Wing"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::UniqueRectangle) == "Unique Rectangle"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::WWing) == "W-Wing"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::SimpleColoring) == "Simple Coloring"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::FinnedXWing) == "Finned X-Wing"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::RemotePairs) == "Remote Pairs"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::BUG) == "BUG"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::Jellyfish) == "Jellyfish"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::FinnedSwordfish) == "Finned Swordfish"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::EmptyRectangle) == "Empty Rectangle"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::WXYZWing) == "WXYZ-Wing"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::FinnedJellyfish) == "Finned Jellyfish"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::XYChain) == "XY-Chain"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::MultiColoring) == "Multi-Coloring"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::ALSxZ) == "ALS-XZ"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::SueDeCoq) == "Sue de Coq"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::ForcingChain) == "Forcing Chain"sv); + REQUIRE(getLocalizedTechniqueName(SolvingTechnique::NiceLoop) == "Nice Loop"sv); } diff --git a/tests/unit/test_localized_explanations_advanced.cpp b/tests/unit/test_localized_explanations_advanced.cpp index 5aa1df8..b109790 100644 --- a/tests/unit/test_localized_explanations_advanced.cpp +++ b/tests/unit/test_localized_explanations_advanced.cpp @@ -21,7 +21,6 @@ /// FinnedJellyfish, XYChain, MultiColoring, ALSxZ, SueDeCoq, ForcingChain, /// NiceLoop. -#include "../../src/core/localization_manager.h" #include "../../src/core/localized_explanations.h" #include "../../src/core/solving_technique.h" @@ -33,16 +32,6 @@ using namespace sudoku::core; namespace { -[[nodiscard]] std::filesystem::path projectRoot() { - return std::filesystem::path(__FILE__).parent_path().parent_path().parent_path(); -} - -[[nodiscard]] std::shared_ptr makeEnglishLoc() { - auto loc = std::make_shared(projectRoot() / "resources" / "locales"); - [[maybe_unused]] auto r = loc->setLocale("en"); - return loc; -} - [[nodiscard]] SolveStep makeStep(SolvingTechnique technique, ExplanationData data, std::string fallback = "fallback") { return SolveStep{.type = SolveStepType::Elimination, .technique = technique, @@ -61,29 +50,26 @@ namespace { // ============================================================================ TEST_CASE("getLocalizedExplanation - Swordfish row-based", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); // values = {candidate, r1, r2, r3, c1, c2, c3} auto step = makeStep(SolvingTechnique::Swordfish, {.positions = {}, .values = {3, 1, 4, 7, 2, 5, 8}, .region_type = RegionType::Row}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Swordfish on value 3 in Rows 1, 4, 7 and Columns 2, 5, 8 eliminates 3 from other cells in those columns"); } TEST_CASE("getLocalizedExplanation - Swordfish col-based", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::Swordfish, {.positions = {}, .values = {5, 2, 6, 9, 3, 7, 1}, .region_type = RegionType::Col}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Swordfish on value 5 in Columns 2, 6, 9 and Rows 3, 7, 1 eliminates 5 from other cells in those rows"); } TEST_CASE("getLocalizedExplanation - Swordfish fallback on insufficient values", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::Swordfish, {.positions = {}, .values = {3, 1, 4}, .region_type = RegionType::Row}, "raw swordfish"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw swordfish"); + REQUIRE(getLocalizedExplanation(step) == "raw swordfish"); } // ============================================================================ @@ -91,7 +77,6 @@ TEST_CASE("getLocalizedExplanation - Swordfish fallback on insufficient values", // ============================================================================ TEST_CASE("getLocalizedExplanation - Skyscraper", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); // positions: [shared1, non-shared1, shared2, non-shared2] auto step = makeStep(SolvingTechnique::Skyscraper, @@ -101,16 +86,15 @@ TEST_CASE("getLocalizedExplanation - Skyscraper", "[localized_explanations_advan .region_index = 0, .secondary_region_type = RegionType::Row, .secondary_region_index = 4}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Skyscraper on value 7: conjugate pairs in Row 1 and Row 5 share endpoint R1C3 — eliminates 7 " "from cells seeing both R1C7 and R5C7"); } TEST_CASE("getLocalizedExplanation - Skyscraper fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::Skyscraper, {.positions = {{.row = 0, .col = 0}}, .values = {7}}, "raw skyscraper"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw skyscraper"); + REQUIRE(getLocalizedExplanation(step) == "raw skyscraper"); } // ============================================================================ @@ -118,21 +102,19 @@ TEST_CASE("getLocalizedExplanation - Skyscraper fallback", "[localized_explanati // ============================================================================ TEST_CASE("getLocalizedExplanation - TwoStringKite", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); // positions: [row_end1, row_end2, col_end1, col_end2] auto step = makeStep(SolvingTechnique::TwoStringKite, {.positions = {{.row = 0, .col = 1}, {.row = 0, .col = 4}, {.row = 2, .col = 1}, {.row = 6, .col = 4}}, .values = {5}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "2-String Kite on value 5: row pair R1C2,R1C5 and column pair R3C2,R7C5 connected through shared " "box — eliminates 5 from cells seeing both endpoints"); } TEST_CASE("getLocalizedExplanation - TwoStringKite fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::TwoStringKite, {.positions = {}, .values = {5}}, "raw kite"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw kite"); + REQUIRE(getLocalizedExplanation(step) == "raw kite"); } // ============================================================================ @@ -140,18 +122,16 @@ TEST_CASE("getLocalizedExplanation - TwoStringKite fallback", "[localized_explan // ============================================================================ TEST_CASE("getLocalizedExplanation - XYZWing", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep( SolvingTechnique::XYZWing, {.positions = {{.row = 3, .col = 3}, {.row = 3, .col = 7}, {.row = 6, .col = 3}}, .values = {2, 5, 8}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "XYZ-Wing: pivot R4C4 {2,5,8}, wing R4C8 and wing R7C4 eliminate 8 from cells seeing all three"); } TEST_CASE("getLocalizedExplanation - XYZWing fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::XYZWing, {.positions = {{.row = 0, .col = 0}}, .values = {2, 5}}, "raw xyz"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw xyz"); + REQUIRE(getLocalizedExplanation(step) == "raw xyz"); } // ============================================================================ @@ -159,19 +139,17 @@ TEST_CASE("getLocalizedExplanation - XYZWing fallback", "[localized_explanations // ============================================================================ TEST_CASE("getLocalizedExplanation - UniqueRectangle Type1", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::UniqueRectangle, {.positions = {{.row = 0, .col = 0}, {.row = 0, .col = 4}, {.row = 4, .col = 0}, {.row = 4, .col = 4}}, .values = {2, 8}, .technique_subtype = 0}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Unique Rectangle: cells R1C1, R1C5, R5C1, R5C5 with values {2,8} — eliminates 2,8 from R5C5 to " "avoid deadly pattern"); } TEST_CASE("getLocalizedExplanation - UniqueRectangle Type2", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::UniqueRectangle, {.positions = {{.row = 0, .col = 0}, {.row = 0, .col = 4}, {.row = 4, .col = 0}, {.row = 4, .col = 4}}, @@ -179,13 +157,12 @@ TEST_CASE("getLocalizedExplanation - UniqueRectangle Type2", "[localized_explana .secondary_region_type = RegionType::Row, .secondary_region_index = 3, .technique_subtype = 1}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Unique Rectangle Type 2: cells R1C1, R1C5, R5C1, R5C5 with values {2,8} — extra candidate 5 " "eliminated from cells seeing both floor cells in shared Row 4"); } TEST_CASE("getLocalizedExplanation - UniqueRectangle Type3", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::UniqueRectangle, {.positions = {{.row = 0, .col = 0}, {.row = 0, .col = 4}, {.row = 4, .col = 0}, {.row = 4, .col = 4}}, @@ -193,13 +170,12 @@ TEST_CASE("getLocalizedExplanation - UniqueRectangle Type3", "[localized_explana .secondary_region_type = RegionType::Col, .secondary_region_index = 4, .technique_subtype = 2}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Unique Rectangle Type 3: cells R1C1, R1C5, R5C1, R5C5 with values {2,8} — floor extras form " "naked subset in Column 5, eliminating from other cells"); } TEST_CASE("getLocalizedExplanation - UniqueRectangle Type4", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::UniqueRectangle, {.positions = {{.row = 0, .col = 0}, {.row = 0, .col = 4}, {.row = 4, .col = 0}, {.row = 4, .col = 4}}, @@ -207,16 +183,15 @@ TEST_CASE("getLocalizedExplanation - UniqueRectangle Type4", "[localized_explana .secondary_region_type = RegionType::Box, .secondary_region_index = 0, .technique_subtype = 3}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Unique Rectangle Type 4: cells R1C1, R1C5, R5C1, R5C5 with values {2,8} — strong link on 5 in " "Box 1 eliminates 3 from floor cells"); } TEST_CASE("getLocalizedExplanation - UniqueRectangle fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::UniqueRectangle, {.positions = {{.row = 0, .col = 0}}, .values = {2, 8}}, "raw ur"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw ur"); + REQUIRE(getLocalizedExplanation(step) == "raw ur"); } // ============================================================================ @@ -224,20 +199,18 @@ TEST_CASE("getLocalizedExplanation - UniqueRectangle fallback", "[localized_expl // ============================================================================ TEST_CASE("getLocalizedExplanation - WWing", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::WWing, {.positions = {{.row = 2, .col = 1}, {.row = 6, .col = 7}, {.row = 2, .col = 4}, {.row = 6, .col = 4}}, .values = {4, 9}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "W-Wing: cells R3C2 and R7C8 {4,9} connected by strong link on 4 — eliminates 9 from cells seeing both"); } TEST_CASE("getLocalizedExplanation - WWing fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::WWing, {.positions = {}, .values = {4, 9}}, "raw wwing"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw wwing"); + REQUIRE(getLocalizedExplanation(step) == "raw wwing"); } // ============================================================================ @@ -245,34 +218,30 @@ TEST_CASE("getLocalizedExplanation - WWing fallback", "[localized_explanations_a // ============================================================================ TEST_CASE("getLocalizedExplanation - SimpleColoring contradiction", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::SimpleColoring, {.positions = {}, .values = {6}, .technique_subtype = 0}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Simple Coloring on 6: same-color cells see each other — eliminates 6 from all cells of that color"); } TEST_CASE("getLocalizedExplanation - SimpleColoring exclusion", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::SimpleColoring, {.positions = {{.row = 3, .col = 4}}, .values = {6}, .technique_subtype = 1}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Simple Coloring on 6: cell R4C5 sees both colors — eliminates 6 from R4C5"); } TEST_CASE("getLocalizedExplanation - SimpleColoring fallback on no values", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::SimpleColoring, {}, "raw sc"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw sc"); + REQUIRE(getLocalizedExplanation(step) == "raw sc"); } TEST_CASE("getLocalizedExplanation - SimpleColoring exclusion fallback on empty positions", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); // technique_subtype != 0 but positions empty → second fallback auto step = makeStep(SolvingTechnique::SimpleColoring, {.positions = {}, .values = {6}, .technique_subtype = 1}, "raw sc2"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw sc2"); + REQUIRE(getLocalizedExplanation(step) == "raw sc2"); } // ============================================================================ @@ -280,31 +249,28 @@ TEST_CASE("getLocalizedExplanation - SimpleColoring exclusion fallback on empty // ============================================================================ TEST_CASE("getLocalizedExplanation - FinnedXWing row-based", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); // values = {candidate, row1, row2, col1, col2}; positions.back() = fin auto step = makeStep(SolvingTechnique::FinnedXWing, {.positions = {{.row = 4, .col = 2}}, .values = {7, 2, 5, 3, 8}, .region_type = RegionType::Row}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Finned X-Wing on value 7 in Rows 2 and 5, Columns 3 and 8 with fin at R5C3 — eliminates 7 from " "cells in fin's box"); } TEST_CASE("getLocalizedExplanation - FinnedXWing col-based", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::FinnedXWing, {.positions = {{.row = 0, .col = 6}}, .values = {4, 3, 7, 1, 6}, .region_type = RegionType::Col}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Finned X-Wing on value 4 in Columns 3 and 7, Rows 1 and 6 with fin at R1C7 — eliminates 4 from " "cells in fin's box"); } TEST_CASE("getLocalizedExplanation - FinnedXWing fallback on no values", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::FinnedXWing, {.positions = {}, .values = {}, .region_type = RegionType::Row}, "raw finned x"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw finned x"); + REQUIRE(getLocalizedExplanation(step) == "raw finned x"); } // ============================================================================ @@ -312,19 +278,17 @@ TEST_CASE("getLocalizedExplanation - FinnedXWing fallback on no values", "[local // ============================================================================ TEST_CASE("getLocalizedExplanation - RemotePairs", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); // values = {A, B, chain_length} auto step = makeStep(SolvingTechnique::RemotePairs, {.positions = {{.row = 0, .col = 0}, {.row = 7, .col = 7}}, .values = {3, 7, 6}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Remote Pairs: chain of {3,7} cells from R1C1 to R8C8 (length 6) — eliminates 3,7 from cells " "seeing both endpoints"); } TEST_CASE("getLocalizedExplanation - RemotePairs fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::RemotePairs, {.positions = {}, .values = {3, 7, 6}}, "raw rp"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw rp"); + REQUIRE(getLocalizedExplanation(step) == "raw rp"); } // ============================================================================ @@ -332,16 +296,14 @@ TEST_CASE("getLocalizedExplanation - RemotePairs fallback", "[localized_explanat // ============================================================================ TEST_CASE("getLocalizedExplanation - BUG", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::BUG, {.positions = {{.row = 4, .col = 4}}, .values = {7}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "BUG: all cells bivalue except R5C5 — value 7 must be placed to avoid deadly pattern"); } TEST_CASE("getLocalizedExplanation - BUG fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::BUG, {}, "raw bug"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw bug"); + REQUIRE(getLocalizedExplanation(step) == "raw bug"); } // ============================================================================ @@ -349,30 +311,27 @@ TEST_CASE("getLocalizedExplanation - BUG fallback", "[localized_explanations_adv // ============================================================================ TEST_CASE("getLocalizedExplanation - Jellyfish row-based", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); // values = {candidate, r1, r2, r3, r4, c1, c2, c3, c4} (1-indexed) auto step = makeStep(SolvingTechnique::Jellyfish, {.positions = {}, .values = {2, 1, 3, 6, 8, 2, 4, 7, 9}, .region_type = RegionType::Row}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Jellyfish on value 2 in Rows 1, 3, 6, 8 and Columns 2, 4, 7, 9 eliminates 2 from other cells in " "those columns"); } TEST_CASE("getLocalizedExplanation - Jellyfish col-based", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::Jellyfish, {.positions = {}, .values = {5, 2, 4, 7, 9, 1, 3, 6, 8}, .region_type = RegionType::Col}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE( result == "Jellyfish on value 5 in Columns 2, 4, 7, 9 and Rows 1, 3, 6, 8 eliminates 5 from other cells in those rows"); } TEST_CASE("getLocalizedExplanation - Jellyfish fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::Jellyfish, {.positions = {}, .values = {2, 1, 3}, .region_type = RegionType::Row}, "raw jellyfish"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw jellyfish"); + REQUIRE(getLocalizedExplanation(step) == "raw jellyfish"); } // ============================================================================ @@ -380,29 +339,26 @@ TEST_CASE("getLocalizedExplanation - Jellyfish fallback", "[localized_explanatio // ============================================================================ TEST_CASE("getLocalizedExplanation - FinnedSwordfish row-based", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); // values = {candidate, row/col1, row/col2, row/col3}; positions.back() = fin auto step = makeStep(SolvingTechnique::FinnedSwordfish, {.positions = {{.row = 4, .col = 3}}, .values = {6, 2, 5, 8}, .region_type = RegionType::Row}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Finned Swordfish on value 6 in Rows 2, 5, 8 with fin at R5C4 — eliminates 6 from cells in fin's box"); } TEST_CASE("getLocalizedExplanation - FinnedSwordfish col-based", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::FinnedSwordfish, {.positions = {{.row = 6, .col = 3}}, .values = {3, 1, 4, 7}, .region_type = RegionType::Col}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Finned Swordfish on value 3 in Columns 1, 4, 7 with fin at R7C4 — eliminates 3 from cells in fin's box"); } TEST_CASE("getLocalizedExplanation - FinnedSwordfish fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::FinnedSwordfish, {.positions = {}, .values = {6, 2, 5, 8}, .region_type = RegionType::Row}, "raw fs"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw fs"); + REQUIRE(getLocalizedExplanation(step) == "raw fs"); } // ============================================================================ @@ -410,19 +366,17 @@ TEST_CASE("getLocalizedExplanation - FinnedSwordfish fallback", "[localized_expl // ============================================================================ TEST_CASE("getLocalizedExplanation - EmptyRectangle", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); // values = {candidate, box+1}; positions.back() = elimination target auto step = makeStep( SolvingTechnique::EmptyRectangle, {.positions = {{.row = 7, .col = 5}}, .values = {4, 5}, .region_type = RegionType::Row, .region_index = 2}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Empty Rectangle on value 4: ER in Box 5 with conjugate pair in Row 3 — eliminates 4 from R8C6"); } TEST_CASE("getLocalizedExplanation - EmptyRectangle fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::EmptyRectangle, {.positions = {}, .values = {4, 5}}, "raw er"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw er"); + REQUIRE(getLocalizedExplanation(step) == "raw er"); } // ============================================================================ @@ -430,20 +384,18 @@ TEST_CASE("getLocalizedExplanation - EmptyRectangle fallback", "[localized_expla // ============================================================================ TEST_CASE("getLocalizedExplanation - WXYZWing", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); // positions: [pivot, w1, w2, w3]; values: [Z] auto step = makeStep(SolvingTechnique::WXYZWing, {.positions = {{.row = 0, .col = 0}, {.row = 0, .col = 4}, {.row = 4, .col = 0}, {.row = 4, .col = 4}}, .values = {7}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "WXYZ-Wing: pivot R1C1 with wings R1C5, R5C1, R5C5 — eliminates 7 from cells seeing all four"); } TEST_CASE("getLocalizedExplanation - WXYZWing fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::WXYZWing, {.positions = {}, .values = {7}}, "raw wxyz"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw wxyz"); + REQUIRE(getLocalizedExplanation(step) == "raw wxyz"); } // ============================================================================ @@ -451,32 +403,29 @@ TEST_CASE("getLocalizedExplanation - WXYZWing fallback", "[localized_explanation // ============================================================================ TEST_CASE("getLocalizedExplanation - FinnedJellyfish row-based", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); // values = {candidate, row/col1..4}; positions.back() = fin auto step = makeStep(SolvingTechnique::FinnedJellyfish, {.positions = {{.row = 2, .col = 4}}, .values = {8, 1, 3, 5, 7}, .region_type = RegionType::Row}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Finned Jellyfish on value 8 in Rows 1, 3, 5, 7 with fin at R3C5 — eliminates 8 from cells in fin's box"); } TEST_CASE("getLocalizedExplanation - FinnedJellyfish col-based", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::FinnedJellyfish, {.positions = {{.row = 2, .col = 4}}, .values = {8, 1, 3, 5, 7}, .region_type = RegionType::Col}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE( result == "Finned Jellyfish on value 8 in Columns 1, 3, 5, 7 with fin at R3C5 — eliminates 8 from cells in fin's box"); } TEST_CASE("getLocalizedExplanation - FinnedJellyfish fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::FinnedJellyfish, {.positions = {}, .values = {8, 1, 3, 5, 7}, .region_type = RegionType::Row}, "raw fj"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw fj"); + REQUIRE(getLocalizedExplanation(step) == "raw fj"); } // ============================================================================ @@ -484,19 +433,17 @@ TEST_CASE("getLocalizedExplanation - FinnedJellyfish fallback", "[localized_expl // ============================================================================ TEST_CASE("getLocalizedExplanation - XYChain", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); // values = {X, chain_length} auto step = makeStep(SolvingTechnique::XYChain, {.positions = {{.row = 0, .col = 0}, {.row = 7, .col = 7}}, .values = {9, 6}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "XY-Chain: chain of 6 bivalue cells from R1C1 to R8C8 — eliminates 9 from cells seeing both endpoints"); } TEST_CASE("getLocalizedExplanation - XYChain fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::XYChain, {.positions = {}, .values = {9}}, "raw xychain"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw xychain"); + REQUIRE(getLocalizedExplanation(step) == "raw xychain"); } // ============================================================================ @@ -504,35 +451,31 @@ TEST_CASE("getLocalizedExplanation - XYChain fallback", "[localized_explanations // ============================================================================ TEST_CASE("getLocalizedExplanation - MultiColoring wrap", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::MultiColoring, {.positions = {}, .values = {4}, .technique_subtype = 0}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE( result == "Multi-Coloring on 4: color sees both colors of another cluster — eliminates 4 from all cells of that color"); } TEST_CASE("getLocalizedExplanation - MultiColoring trap", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::MultiColoring, {.positions = {{.row = 5, .col = 3}}, .values = {4}, .technique_subtype = 1}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Multi-Coloring on 4: cell R6C4 sees complementary colors from two clusters — eliminates 4"); } TEST_CASE("getLocalizedExplanation - MultiColoring fallback on empty values", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::MultiColoring, {}, "raw mc"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw mc"); + REQUIRE(getLocalizedExplanation(step) == "raw mc"); } TEST_CASE("getLocalizedExplanation - MultiColoring trap fallback on empty positions", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); // technique_subtype != 0, no positions → second fallback auto step = makeStep(SolvingTechnique::MultiColoring, {.positions = {}, .values = {4}, .technique_subtype = 1}, "raw mc2"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw mc2"); + REQUIRE(getLocalizedExplanation(step) == "raw mc2"); } // ============================================================================ @@ -540,22 +483,20 @@ TEST_CASE("getLocalizedExplanation - MultiColoring trap fallback on empty positi // ============================================================================ TEST_CASE("getLocalizedExplanation - ALSxZ", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); // values = {X, Z, als_a_size, als_b_size} // positions = als_a cells... + als_b cells... auto step = makeStep(SolvingTechnique::ALSxZ, {.positions = {{.row = 0, .col = 1}, {.row = 0, .col = 3}, {.row = 2, .col = 1}, {.row = 2, .col = 3}}, .values = {5, 3, 2, 2}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "ALS-XZ: ALS R1C2, R1C4 and ALS R3C2, R3C4 linked by restricted common 5 — eliminates 3 from " "cells seeing both ALSs"); } TEST_CASE("getLocalizedExplanation - ALSxZ fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::ALSxZ, {.positions = {}, .values = {5, 3}}, "raw als"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw als"); + REQUIRE(getLocalizedExplanation(step) == "raw als"); } // ============================================================================ @@ -563,20 +504,18 @@ TEST_CASE("getLocalizedExplanation - ALSxZ fallback", "[localized_explanations_a // ============================================================================ TEST_CASE("getLocalizedExplanation - SueDeCoq", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::SueDeCoq, {.positions = {}, .values = {3, 5}, .region_type = RegionType::Row, .region_index = 2, .secondary_region_index = 1}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Sue de Coq: intersection of Row 3 and Box 2 — eliminates candidates from rest of line and box"); } TEST_CASE("getLocalizedExplanation - SueDeCoq fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::SueDeCoq, {.positions = {}, .values = {}}, "raw sdc"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw sdc"); + REQUIRE(getLocalizedExplanation(step) == "raw sdc"); } // ============================================================================ @@ -584,16 +523,14 @@ TEST_CASE("getLocalizedExplanation - SueDeCoq fallback", "[localized_explanation // ============================================================================ TEST_CASE("getLocalizedExplanation - ForcingChain", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::ForcingChain, {.positions = {{.row = 4, .col = 4}}, .values = {7}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Forcing Chain: assuming each candidate in R5C5 leads to the same conclusion — 7"); } TEST_CASE("getLocalizedExplanation - ForcingChain fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::ForcingChain, {}, "raw fc"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw fc"); + REQUIRE(getLocalizedExplanation(step) == "raw fc"); } // ============================================================================ @@ -601,17 +538,15 @@ TEST_CASE("getLocalizedExplanation - ForcingChain fallback", "[localized_explana // ============================================================================ TEST_CASE("getLocalizedExplanation - NiceLoop", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::NiceLoop, {.positions = {{.row = 0, .col = 0}, {.row = 8, .col = 8}}, .values = {5}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result == "Nice Loop: alternating inference chain from R1C1 to R9C9 — eliminates 5"); } TEST_CASE("getLocalizedExplanation - NiceLoop fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::NiceLoop, {.positions = {{.row = 0, .col = 0}}, .values = {}}, "raw nl"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw nl"); + REQUIRE(getLocalizedExplanation(step) == "raw nl"); } // ============================================================================ @@ -619,33 +554,29 @@ TEST_CASE("getLocalizedExplanation - NiceLoop fallback", "[localized_explanation // ============================================================================ TEST_CASE("getLocalizedExplanation - XCycles Type1", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::XCycles, {.values = {5}, .technique_subtype = 0}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result.find("X-Cycles on value 5") != std::string::npos); REQUIRE(result.find("continuous loop") != std::string::npos); } TEST_CASE("getLocalizedExplanation - XCycles Type2", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::XCycles, {.positions = {{.row = 2, .col = 3}}, .values = {7}, .technique_subtype = 1}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result.find("strong-strong discontinuity at R3C4") != std::string::npos); } TEST_CASE("getLocalizedExplanation - XCycles Type3", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::XCycles, {.positions = {{.row = 1, .col = 1}}, .values = {3}, .technique_subtype = 2}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result.find("weak-weak discontinuity at R2C2") != std::string::npos); } TEST_CASE("getLocalizedExplanation - XCycles fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::XCycles, {}, "raw xc"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw xc"); + REQUIRE(getLocalizedExplanation(step) == "raw xc"); } // ============================================================================ @@ -653,16 +584,14 @@ TEST_CASE("getLocalizedExplanation - XCycles fallback", "[localized_explanations // ============================================================================ TEST_CASE("getLocalizedExplanation - ThreeDMedusa", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::ThreeDMedusa, {.values = {4}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result.find("3D Medusa") != std::string::npos); } TEST_CASE("getLocalizedExplanation - ThreeDMedusa fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::ThreeDMedusa, {}, "raw 3dm"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw 3dm"); + REQUIRE(getLocalizedExplanation(step) == "raw 3dm"); } // ============================================================================ @@ -670,20 +599,18 @@ TEST_CASE("getLocalizedExplanation - ThreeDMedusa fallback", "[localized_explana // ============================================================================ TEST_CASE("getLocalizedExplanation - HiddenUniqueRectangle", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::HiddenUniqueRectangle, {.positions = {{.row = 0, .col = 0}, {.row = 0, .col = 1}, {.row = 1, .col = 0}, {.row = 1, .col = 1}}, .values = {1, 2, 3, 2}}); // values[3]=2 is index into positions for target cell - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result.find("Hidden Unique Rectangle") != std::string::npos); REQUIRE(result.find("deadly pattern") != std::string::npos); } TEST_CASE("getLocalizedExplanation - HiddenUniqueRectangle fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::HiddenUniqueRectangle, {}, "raw hur"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw hur"); + REQUIRE(getLocalizedExplanation(step) == "raw hur"); } // ============================================================================ @@ -691,20 +618,18 @@ TEST_CASE("getLocalizedExplanation - HiddenUniqueRectangle fallback", "[localize // ============================================================================ TEST_CASE("getLocalizedExplanation - AvoidableRectangle", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::AvoidableRectangle, {.positions = {{.row = 2, .col = 2}, {.row = 2, .col = 5}, {.row = 5, .col = 2}, {.row = 5, .col = 5}}, .values = {4, 6, 4, 3}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result.find("Avoidable Rectangle") != std::string::npos); REQUIRE(result.find("deadly pattern") != std::string::npos); } TEST_CASE("getLocalizedExplanation - AvoidableRectangle fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::AvoidableRectangle, {}, "raw ar"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw ar"); + REQUIRE(getLocalizedExplanation(step) == "raw ar"); } // ============================================================================ @@ -712,20 +637,18 @@ TEST_CASE("getLocalizedExplanation - AvoidableRectangle fallback", "[localized_e // ============================================================================ TEST_CASE("getLocalizedExplanation - ALSXYWing", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); // values = {X, Y, Z, als_a_size, als_b_size, als_c_size} // positions = {als_a cells, als_b cells, als_c cells} auto step = makeStep(SolvingTechnique::ALSXYWing, {.positions = {{.row = 0, .col = 0}, {.row = 1, .col = 1}, {.row = 2, .col = 2}}, .values = {3, 5, 7, 1, 1, 1}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result.find("ALS-XY-Wing") != std::string::npos); } TEST_CASE("getLocalizedExplanation - ALSXYWing fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::ALSXYWing, {}, "raw alsxy"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw alsxy"); + REQUIRE(getLocalizedExplanation(step) == "raw alsxy"); } // ============================================================================ @@ -733,17 +656,15 @@ TEST_CASE("getLocalizedExplanation - ALSXYWing fallback", "[localized_explanatio // ============================================================================ TEST_CASE("getLocalizedExplanation - DeathBlossom", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::DeathBlossom, {.positions = {{.row = 3, .col = 3}}, .values = {5, 2}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result.find("Death Blossom") != std::string::npos); REQUIRE(result.find("stem R4C4") != std::string::npos); } TEST_CASE("getLocalizedExplanation - DeathBlossom fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::DeathBlossom, {}, "raw db"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw db"); + REQUIRE(getLocalizedExplanation(step) == "raw db"); } // ============================================================================ @@ -751,21 +672,19 @@ TEST_CASE("getLocalizedExplanation - DeathBlossom fallback", "[localized_explana // ============================================================================ TEST_CASE("getLocalizedExplanation - VWXYZWing", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::VWXYZWing, {.positions = {{.row = 0, .col = 0}, {.row = 0, .col = 1}, {.row = 0, .col = 2}, {.row = 0, .col = 3}, {.row = 0, .col = 4}}, .values = {9}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result.find("VWXYZ-Wing") != std::string::npos); } TEST_CASE("getLocalizedExplanation - VWXYZWing fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::VWXYZWing, {}, "raw vw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw vw"); + REQUIRE(getLocalizedExplanation(step) == "raw vw"); } // ============================================================================ @@ -773,17 +692,15 @@ TEST_CASE("getLocalizedExplanation - VWXYZWing fallback", "[localized_explanatio // ============================================================================ TEST_CASE("getLocalizedExplanation - FrankenFish", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::FrankenFish, {.positions = {{.row = 0, .col = 0}, {.row = 1, .col = 1}}, .values = {2, 7, 3}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result.find("Franken") != std::string::npos); } TEST_CASE("getLocalizedExplanation - FrankenFish fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::FrankenFish, {.values = {1}}, "raw ff"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw ff"); + REQUIRE(getLocalizedExplanation(step) == "raw ff"); } // ============================================================================ @@ -791,18 +708,16 @@ TEST_CASE("getLocalizedExplanation - FrankenFish fallback", "[localized_explanat // ============================================================================ TEST_CASE("getLocalizedExplanation - MutantFish", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::MutantFish, {.positions = {{.row = 3, .col = 3}}, .values = {6}}); step.eliminations = {{.position = {.row = 4, .col = 4}, .value = 6}, {.position = {.row = 5, .col = 5}, .value = 6}}; - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result.find("Mutant Fish") != std::string::npos); } TEST_CASE("getLocalizedExplanation - MutantFish fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::MutantFish, {}, "raw mf"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw mf"); + REQUIRE(getLocalizedExplanation(step) == "raw mf"); } // ============================================================================ @@ -810,16 +725,14 @@ TEST_CASE("getLocalizedExplanation - MutantFish fallback", "[localized_explanati // ============================================================================ TEST_CASE("getLocalizedExplanation - GroupedXCycles", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::GroupedXCycles, {.values = {8, 12}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result.find("Grouped X-Cycles") != std::string::npos); } TEST_CASE("getLocalizedExplanation - GroupedXCycles fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::GroupedXCycles, {}, "raw gxc"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw gxc"); + REQUIRE(getLocalizedExplanation(step) == "raw gxc"); } // ============================================================================ @@ -827,17 +740,15 @@ TEST_CASE("getLocalizedExplanation - GroupedXCycles fallback", "[localized_expla // ============================================================================ TEST_CASE("getLocalizedExplanation - SashimiXWing fallback empty values", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::SashimiXWing, {}, "raw sxw"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw sxw"); + REQUIRE(getLocalizedExplanation(step) == "raw sxw"); } TEST_CASE("getLocalizedExplanation - SashimiXWing fallback insufficient values", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::SashimiXWing, {.positions = {{.row = 1, .col = 3}}, .values = {5, 2}, .region_type = RegionType::Row}, "raw sxw partial"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw sxw partial"); + REQUIRE(getLocalizedExplanation(step) == "raw sxw partial"); } // ============================================================================ @@ -845,9 +756,8 @@ TEST_CASE("getLocalizedExplanation - SashimiXWing fallback insufficient values", // ============================================================================ TEST_CASE("getLocalizedExplanation - SashimiSwordfish fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::SashimiSwordfish, {}, "raw ssf"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw ssf"); + REQUIRE(getLocalizedExplanation(step) == "raw ssf"); } // ============================================================================ @@ -855,9 +765,8 @@ TEST_CASE("getLocalizedExplanation - SashimiSwordfish fallback", "[localized_exp // ============================================================================ TEST_CASE("getLocalizedExplanation - SashimiJellyfish fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::SashimiJellyfish, {}, "raw sjf"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw sjf"); + REQUIRE(getLocalizedExplanation(step) == "raw sjf"); } // ============================================================================ @@ -865,17 +774,15 @@ TEST_CASE("getLocalizedExplanation - SashimiJellyfish fallback", "[localized_exp // ============================================================================ TEST_CASE("getLocalizedExplanation - KrakenFish", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::KrakenFish, {.positions = {{.row = 0, .col = 0}, {.row = 8, .col = 8}}, .values = {4}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result.find("Kraken Fish") != std::string::npos); } TEST_CASE("getLocalizedExplanation - KrakenFish fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::KrakenFish, {}, "raw kf"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw kf"); + REQUIRE(getLocalizedExplanation(step) == "raw kf"); } // ============================================================================ @@ -883,19 +790,17 @@ TEST_CASE("getLocalizedExplanation - KrakenFish fallback", "[localized_explanati // ============================================================================ TEST_CASE("getLocalizedExplanation - ALSChain", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); // values = [rc1, rc2, ..., z, chain_length]; positions = all ALS cells auto step = makeStep( SolvingTechnique::ALSChain, {.positions = {{.row = 1, .col = 1}, {.row = 2, .col = 2}, {.row = 3, .col = 3}}, .values = {1, 2, 5, 3}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result.find("ALS Chain") != std::string::npos); } TEST_CASE("getLocalizedExplanation - ALSChain fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::ALSChain, {}, "raw alsc"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw alsc"); + REQUIRE(getLocalizedExplanation(step) == "raw alsc"); } // ============================================================================ @@ -903,20 +808,18 @@ TEST_CASE("getLocalizedExplanation - ALSChain fallback", "[localized_explanation // ============================================================================ TEST_CASE("getLocalizedExplanation - JuniorExocet", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::JuniorExocet, {.positions = {{.row = 0, .col = 0}, {.row = 0, .col = 1}, {.row = 1, .col = 3}, {.row = 2, .col = 6}}, .values = {3, 5, 7}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result.find("Junior Exocet") != std::string::npos); REQUIRE(result.find("base cells") != std::string::npos); } TEST_CASE("getLocalizedExplanation - JuniorExocet fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::JuniorExocet, {}, "raw je"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw je"); + REQUIRE(getLocalizedExplanation(step) == "raw je"); } // ============================================================================ @@ -924,20 +827,18 @@ TEST_CASE("getLocalizedExplanation - JuniorExocet fallback", "[localized_explana // ============================================================================ TEST_CASE("getLocalizedExplanation - UniqueLoop", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::UniqueLoop, {.positions = {{.row = 0, .col = 0}, {.row = 0, .col = 3}, {.row = 3, .col = 3}, {.row = 3, .col = 0}}, .values = {1, 2}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result.find("Unique Loop") != std::string::npos); REQUIRE(result.find("deadly pattern") != std::string::npos); } TEST_CASE("getLocalizedExplanation - UniqueLoop fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::UniqueLoop, {}, "raw ul"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw ul"); + REQUIRE(getLocalizedExplanation(step) == "raw ul"); } // ============================================================================ @@ -945,17 +846,15 @@ TEST_CASE("getLocalizedExplanation - UniqueLoop fallback", "[localized_explanati // ============================================================================ TEST_CASE("getLocalizedExplanation - ContinuousNiceLoop", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::ContinuousNiceLoop, {.values = {12, 4}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result.find("Continuous Nice Loop") != std::string::npos); REQUIRE(result.find("12 nodes") != std::string::npos); } TEST_CASE("getLocalizedExplanation - ContinuousNiceLoop fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::ContinuousNiceLoop, {}, "raw cnl"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw cnl"); + REQUIRE(getLocalizedExplanation(step) == "raw cnl"); } // ============================================================================ @@ -963,18 +862,16 @@ TEST_CASE("getLocalizedExplanation - ContinuousNiceLoop fallback", "[localized_e // ============================================================================ TEST_CASE("getLocalizedExplanation - GroupedNiceLoop", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::GroupedNiceLoop, {.positions = {{.row = 0, .col = 0}, {.row = 8, .col = 8}}, .values = {7}}); - auto result = getLocalizedExplanation(*loc, step); + auto result = getLocalizedExplanation(step); REQUIRE(result.find("Grouped Nice Loop") != std::string::npos); } TEST_CASE("getLocalizedExplanation - GroupedNiceLoop fallback", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::GroupedNiceLoop, {.positions = {{.row = 0, .col = 0}}, .values = {}}, "raw gnl"); - REQUIRE(getLocalizedExplanation(*loc, step) == "raw gnl"); + REQUIRE(getLocalizedExplanation(step) == "raw gnl"); } // ============================================================================ @@ -982,13 +879,11 @@ TEST_CASE("getLocalizedExplanation - GroupedNiceLoop fallback", "[localized_expl // ============================================================================ TEST_CASE("getLocalizedExplanation - UnitForcingChain passthrough", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::UnitForcingChain, {}, "unit forcing result"); - REQUIRE(getLocalizedExplanation(*loc, step) == "unit forcing result"); + REQUIRE(getLocalizedExplanation(step) == "unit forcing result"); } TEST_CASE("getLocalizedExplanation - RegionForcingChain passthrough", "[localized_explanations_advanced]") { - auto loc = makeEnglishLoc(); auto step = makeStep(SolvingTechnique::RegionForcingChain, {}, "region forcing result"); - REQUIRE(getLocalizedExplanation(*loc, step) == "region forcing result"); + REQUIRE(getLocalizedExplanation(step) == "region forcing result"); } diff --git a/tests/unit/test_mock_localization_manager.cpp b/tests/unit/test_mock_localization_manager.cpp deleted file mode 100644 index af5ea83..0000000 --- a/tests/unit/test_mock_localization_manager.cpp +++ /dev/null @@ -1,54 +0,0 @@ -// sudoku-cpp - Offline Sudoku Game -// Copyright (C) 2025-2026 Alexander Bendlin (darkstar79) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -/// Branch-coverage tests for MockLocalizationManager: -/// - setLocale (lines 21-22) -/// - getCurrentLocale (lines 25-26) -/// - getAvailableLocales (lines 29-31) - -#include "../helpers/mock_localization_manager.h" - -#include - -using namespace sudoku::core; - -TEST_CASE("MockLocalizationManager - getString returns key", "[mock_localization]") { - MockLocalizationManager mock; - REQUIRE(std::string_view(mock.getString("sudoku.difficulty.easy")) == "sudoku.difficulty.easy"); - REQUIRE(std::string_view(mock.getString("some.key")) == "some.key"); -} - -TEST_CASE("MockLocalizationManager - setLocale returns success", "[mock_localization]") { - // Covers lines 21-22: setLocale() always returns {} - MockLocalizationManager mock; - auto result = mock.setLocale("fr"); - REQUIRE(result.has_value()); -} - -TEST_CASE("MockLocalizationManager - getCurrentLocale returns en", "[mock_localization]") { - // Covers lines 25-26: getCurrentLocale() always returns "en" - MockLocalizationManager mock; - REQUIRE(mock.getCurrentLocale() == "en"); -} - -TEST_CASE("MockLocalizationManager - getAvailableLocales returns en entry", "[mock_localization]") { - // Covers lines 29-31: getAvailableLocales() returns {{"en", "English"}} - MockLocalizationManager mock; - auto locales = mock.getAvailableLocales(); - REQUIRE(locales.size() == 1); - REQUIRE(locales[0].first == "en"); - REQUIRE(locales[0].second == "English"); -} diff --git a/tests/unit/test_technique_descriptions.cpp b/tests/unit/test_technique_descriptions.cpp index f89a699..2df162d 100644 --- a/tests/unit/test_technique_descriptions.cpp +++ b/tests/unit/test_technique_descriptions.cpp @@ -15,7 +15,6 @@ // along with this program. If not, see . #include "../../src/core/technique_descriptions.h" -#include "../helpers/mock_localization_manager.h" #include #include @@ -24,15 +23,13 @@ using namespace sudoku::core; -namespace { -MockLocalizationManager mock_loc; -} // namespace +namespace {} // namespace TEST_CASE("TechniqueDescription - All techniques have descriptions", "[technique_descriptions]") { SECTION("Every SolvingTechnique (0-32) has non-empty title") { for (int i = 0; i <= 32; ++i) { auto technique = static_cast(i); - auto desc = getTechniqueDescription(mock_loc, technique); + auto desc = getTechniqueDescription(technique); INFO("Technique enum value: " << i << " (" << getTechniqueName(technique) << ")"); REQUIRE_FALSE(desc.title.empty()); @@ -42,7 +39,7 @@ TEST_CASE("TechniqueDescription - All techniques have descriptions", "[technique SECTION("Every SolvingTechnique (0-32) has non-empty what_it_is") { for (int i = 0; i <= 32; ++i) { auto technique = static_cast(i); - auto desc = getTechniqueDescription(mock_loc, technique); + auto desc = getTechniqueDescription(technique); INFO("Technique enum value: " << i); REQUIRE_FALSE(desc.what_it_is.empty()); @@ -52,7 +49,7 @@ TEST_CASE("TechniqueDescription - All techniques have descriptions", "[technique SECTION("Every SolvingTechnique (0-32) has non-empty what_to_look_for") { for (int i = 0; i <= 32; ++i) { auto technique = static_cast(i); - auto desc = getTechniqueDescription(mock_loc, technique); + auto desc = getTechniqueDescription(technique); INFO("Technique enum value: " << i); REQUIRE_FALSE(desc.what_to_look_for.empty()); @@ -60,7 +57,7 @@ TEST_CASE("TechniqueDescription - All techniques have descriptions", "[technique } SECTION("Backtracking has a description") { - auto desc = getTechniqueDescription(mock_loc, SolvingTechnique::Backtracking); + auto desc = getTechniqueDescription(SolvingTechnique::Backtracking); REQUIRE_FALSE(desc.title.empty()); REQUIRE_FALSE(desc.what_it_is.empty()); @@ -72,10 +69,10 @@ TEST_CASE("TechniqueDescription - No duplicate titles", "[technique_descriptions std::set titles; // 0-32 + 255 (Backtracking) for (int i = 0; i <= 32; ++i) { - auto desc = getTechniqueDescription(mock_loc, static_cast(i)); + auto desc = getTechniqueDescription(static_cast(i)); titles.emplace(desc.title); } - titles.emplace(getTechniqueDescription(mock_loc, SolvingTechnique::Backtracking).title); + titles.emplace(getTechniqueDescription(SolvingTechnique::Backtracking).title); REQUIRE(titles.size() == 34); } @@ -83,21 +80,21 @@ TEST_CASE("TechniqueDescription - No duplicate titles", "[technique_descriptions TEST_CASE("TechniqueDescription - Specific descriptions present", "[technique_descriptions]") { SECTION("ForcingChain description mentions propagation") { - auto desc = getTechniqueDescription(mock_loc, SolvingTechnique::ForcingChain); + auto desc = getTechniqueDescription(SolvingTechnique::ForcingChain); REQUIRE_FALSE(desc.title.empty()); REQUIRE(desc.what_it_is.find("propagate") != std::string_view::npos); } SECTION("NiceLoop description mentions alternating") { - auto desc = getTechniqueDescription(mock_loc, SolvingTechnique::NiceLoop); + auto desc = getTechniqueDescription(SolvingTechnique::NiceLoop); REQUIRE_FALSE(desc.title.empty()); REQUIRE(desc.what_it_is.find("alternating") != std::string_view::npos); } SECTION("NakedSingle description present") { - auto desc = getTechniqueDescription(mock_loc, SolvingTechnique::NakedSingle); + auto desc = getTechniqueDescription(SolvingTechnique::NakedSingle); REQUIRE_FALSE(desc.title.empty()); REQUIRE_FALSE(desc.what_it_is.empty()); diff --git a/tests/unit/test_training_hints.cpp b/tests/unit/test_training_hints.cpp index 07ecf73..083062f 100644 --- a/tests/unit/test_training_hints.cpp +++ b/tests/unit/test_training_hints.cpp @@ -15,16 +15,11 @@ // along with this program. If not, see . #include "../../src/core/training_hints.h" -#include "../helpers/mock_localization_manager.h" #include using namespace sudoku::core; -namespace { -MockLocalizationManager mock_loc; -} // namespace - // --- getTechniqueCategory --- TEST_CASE("getTechniqueCategory — maps all techniques", "[training_hints]") { @@ -94,7 +89,7 @@ TEST_CASE("getTrainingHint — Singles category", "[training_hints]") { auto step = makePlacementStep(SolvingTechnique::NakedSingle, Position{.row = 1, .col = 0}, 9); SECTION("Level 1: points to cell") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::NakedSingle, 1, step); + auto hint = getTrainingHint(SolvingTechnique::NakedSingle, 1, step); CHECK(hint.text.find("R2C1") != std::string::npos); CHECK(hint.highlights.size() == 1); CHECK(hint.highlights.size() == 1); @@ -102,18 +97,18 @@ TEST_CASE("getTrainingHint — Singles category", "[training_hints]") { } SECTION("Level 2: mentions region") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::NakedSingle, 2, step); - CHECK(hint.text.find("row 1") != std::string::npos); + auto hint = getTrainingHint(SolvingTechnique::NakedSingle, 2, step); + CHECK(hint.text.find("Row 1") != std::string::npos); } SECTION("Level 2: no region fallback") { step.explanation_data.region_type = RegionType::None; - auto hint = getTrainingHint(mock_loc, SolvingTechnique::NakedSingle, 2, step); + auto hint = getTrainingHint(SolvingTechnique::NakedSingle, 2, step); CHECK(hint.text.find("candidates") != std::string::npos); } SECTION("Level 3: reveals value") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::NakedSingle, 3, step); + auto hint = getTrainingHint(SolvingTechnique::NakedSingle, 3, step); CHECK(hint.text.find("9") != std::string::npos); } } @@ -123,20 +118,20 @@ TEST_CASE("getTrainingHint — Subsets category", "[training_hints]") { auto step = makeEliminationStep(SolvingTechnique::NakedPair, elims); SECTION("Level 1: mentions region") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::NakedPair, 1, step); - CHECK(hint.text.find("box 3") != std::string::npos); + auto hint = getTrainingHint(SolvingTechnique::NakedPair, 1, step); + CHECK(hint.text.find("Box 3") != std::string::npos); } SECTION("Level 1: no region fallback") { step.explanation_data.region_type = RegionType::None; - auto hint = getTrainingHint(mock_loc, SolvingTechnique::NakedPair, 1, step); + auto hint = getTrainingHint(SolvingTechnique::NakedPair, 1, step); CHECK(hint.text.find("same candidates") != std::string::npos); } SECTION("Level 2: highlights subset cells and shows values") { // The step's explanation_data.values should appear in level 2 text step.explanation_data.values = {3, 7}; - auto hint = getTrainingHint(mock_loc, SolvingTechnique::NakedPair, 2, step); + auto hint = getTrainingHint(SolvingTechnique::NakedPair, 2, step); CHECK(hint.text.find("subset") != std::string::npos); CHECK(hint.text.find("3") != std::string::npos); CHECK(hint.text.find("7") != std::string::npos); @@ -145,13 +140,13 @@ TEST_CASE("getTrainingHint — Subsets category", "[training_hints]") { SECTION("Level 2: works without values") { step.explanation_data.values.clear(); - auto hint = getTrainingHint(mock_loc, SolvingTechnique::NakedPair, 2, step); + auto hint = getTrainingHint(SolvingTechnique::NakedPair, 2, step); CHECK(hint.text.find("subset") != std::string::npos); CHECK_FALSE(hint.highlights.empty()); } SECTION("Level 3: highlights elimination targets") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::NakedPair, 3, step); + auto hint = getTrainingHint(SolvingTechnique::NakedPair, 3, step); CHECK(hint.text.find("Eliminate") != std::string::npos); CHECK(hint.highlights.size() == 1); CHECK(hint.highlights[0].role == CellRole::Fin); @@ -163,18 +158,18 @@ TEST_CASE("getTrainingHint — Intersections category", "[training_hints]") { auto step = makeEliminationStep(SolvingTechnique::PointingPair, elims); SECTION("Level 1 with value") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::PointingPair, 1, step); + auto hint = getTrainingHint(SolvingTechnique::PointingPair, 1, step); CHECK(hint.text.find("1") != std::string::npos); // data.values[0] = 1 } SECTION("Level 1 without value") { step.explanation_data.values.clear(); - auto hint = getTrainingHint(mock_loc, SolvingTechnique::PointingPair, 1, step); + auto hint = getTrainingHint(SolvingTechnique::PointingPair, 1, step); CHECK(hint.text.find("intersection") != std::string::npos); } SECTION("Level 3: elimination targets") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::PointingPair, 3, step); + auto hint = getTrainingHint(SolvingTechnique::PointingPair, 3, step); CHECK(hint.highlights[0].role == CellRole::Fin); } } @@ -184,24 +179,24 @@ TEST_CASE("getTrainingHint — Fish category", "[training_hints]") { auto step = makeEliminationStep(SolvingTechnique::XWing, elims); SECTION("Level 1 with value") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::XWing, 1, step); + auto hint = getTrainingHint(SolvingTechnique::XWing, 1, step); CHECK(hint.text.find("fish") != std::string::npos); CHECK(hint.text.find("1") != std::string::npos); } SECTION("Level 1 without value") { step.explanation_data.values.clear(); - auto hint = getTrainingHint(mock_loc, SolvingTechnique::XWing, 1, step); + auto hint = getTrainingHint(SolvingTechnique::XWing, 1, step); CHECK(hint.text.find("fish") != std::string::npos); } SECTION("Level 2: base/cover sets") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::XWing, 2, step); + auto hint = getTrainingHint(SolvingTechnique::XWing, 2, step); CHECK(hint.text.find("Base") != std::string::npos); } SECTION("Level 3: elimination") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::XWing, 3, step); + auto hint = getTrainingHint(SolvingTechnique::XWing, 3, step); CHECK(hint.highlights[0].role == CellRole::Fin); } } @@ -213,18 +208,18 @@ TEST_CASE("getTrainingHint — Wings category", "[training_hints]") { step.explanation_data.position_roles = {CellRole::Pivot, CellRole::Wing}; SECTION("Level 1: finds pivot") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::XYWing, 1, step); + auto hint = getTrainingHint(SolvingTechnique::XYWing, 1, step); CHECK(hint.text.find("pivot") != std::string::npos); CHECK(hint.highlights[0].role == CellRole::Pivot); } SECTION("Level 2: pivot and wing cells") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::XYWing, 2, step); + auto hint = getTrainingHint(SolvingTechnique::XYWing, 2, step); CHECK(hint.text.find("Pivot") != std::string::npos); } SECTION("Level 3: elimination") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::XYWing, 3, step); + auto hint = getTrainingHint(SolvingTechnique::XYWing, 3, step); CHECK(hint.highlights[0].role == CellRole::Fin); } } @@ -234,18 +229,18 @@ TEST_CASE("getTrainingHint — SingleDigit category", "[training_hints]") { auto step = makeEliminationStep(SolvingTechnique::Skyscraper, elims); SECTION("Level 1 with value") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::Skyscraper, 1, step); + auto hint = getTrainingHint(SolvingTechnique::Skyscraper, 1, step); CHECK(hint.text.find("conjugate") != std::string::npos); } SECTION("Level 1 without value") { step.explanation_data.values.clear(); - auto hint = getTrainingHint(mock_loc, SolvingTechnique::Skyscraper, 1, step); + auto hint = getTrainingHint(SolvingTechnique::Skyscraper, 1, step); CHECK(hint.text.find("conjugate") != std::string::npos); } SECTION("Level 3: elimination") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::Skyscraper, 3, step); + auto hint = getTrainingHint(SolvingTechnique::Skyscraper, 3, step); CHECK(hint.text.find("endpoints") != std::string::npos); } } @@ -255,23 +250,23 @@ TEST_CASE("getTrainingHint — Coloring category", "[training_hints]") { auto step = makeEliminationStep(SolvingTechnique::SimpleColoring, elims); SECTION("Level 1 with value") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::SimpleColoring, 1, step); + auto hint = getTrainingHint(SolvingTechnique::SimpleColoring, 1, step); CHECK(hint.text.find("coloring") != std::string::npos); } SECTION("Level 1 without value") { step.explanation_data.values.clear(); - auto hint = getTrainingHint(mock_loc, SolvingTechnique::SimpleColoring, 1, step); + auto hint = getTrainingHint(SolvingTechnique::SimpleColoring, 1, step); CHECK(hint.text.find("coloring") != std::string::npos); } SECTION("Level 2: chain cells") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::SimpleColoring, 2, step); + auto hint = getTrainingHint(SolvingTechnique::SimpleColoring, 2, step); CHECK(hint.text.find("chain") != std::string::npos); } SECTION("Level 3: elimination") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::SimpleColoring, 3, step); + auto hint = getTrainingHint(SolvingTechnique::SimpleColoring, 3, step); CHECK(hint.text.find("color") != std::string::npos); } } @@ -281,17 +276,17 @@ TEST_CASE("getTrainingHint — UniqueRect category", "[training_hints]") { auto step = makeEliminationStep(SolvingTechnique::UniqueRectangle, elims); SECTION("Level 1: deadly pattern") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::UniqueRectangle, 1, step); + auto hint = getTrainingHint(SolvingTechnique::UniqueRectangle, 1, step); CHECK(hint.text.find("deadly") != std::string::npos); } SECTION("Level 2: rectangle corners") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::UniqueRectangle, 2, step); + auto hint = getTrainingHint(SolvingTechnique::UniqueRectangle, 2, step); CHECK(hint.text.find("corners") != std::string::npos); } SECTION("Level 3: elimination") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::UniqueRectangle, 3, step); + auto hint = getTrainingHint(SolvingTechnique::UniqueRectangle, 3, step); CHECK(hint.text.find("deadly") != std::string::npos); } } @@ -301,20 +296,20 @@ TEST_CASE("getTrainingHint — Chains category", "[training_hints]") { auto elims = std::vector{{Position{.row = 7, .col = 0}, 8}}; auto step = makeEliminationStep(SolvingTechnique::XYChain, elims); - auto hint1 = getTrainingHint(mock_loc, SolvingTechnique::XYChain, 1, step); + auto hint1 = getTrainingHint(SolvingTechnique::XYChain, 1, step); CHECK(hint1.text.find("chain") != std::string::npos); CHECK(hint1.highlights[0].role == CellRole::ChainA); - auto hint2 = getTrainingHint(mock_loc, SolvingTechnique::XYChain, 2, step); + auto hint2 = getTrainingHint(SolvingTechnique::XYChain, 2, step); CHECK(hint2.text.find("path") != std::string::npos); - auto hint3 = getTrainingHint(mock_loc, SolvingTechnique::XYChain, 3, step); + auto hint3 = getTrainingHint(SolvingTechnique::XYChain, 3, step); CHECK(hint3.text.find("Eliminate") != std::string::npos); } SECTION("Placement chain") { auto step = makePlacementStep(SolvingTechnique::ForcingChain, Position{.row = 2, .col = 5}, 4); - auto hint3 = getTrainingHint(mock_loc, SolvingTechnique::ForcingChain, 3, step); + auto hint3 = getTrainingHint(SolvingTechnique::ForcingChain, 3, step); CHECK(hint3.text.find("4") != std::string::npos); CHECK(hint3.text.find("R3C6") != std::string::npos); } @@ -322,7 +317,7 @@ TEST_CASE("getTrainingHint — Chains category", "[training_hints]") { SECTION("Level 1 without positions") { auto step = makeEliminationStep(SolvingTechnique::XYChain, {}); step.explanation_data.positions.clear(); - auto hint = getTrainingHint(mock_loc, SolvingTechnique::XYChain, 1, step); + auto hint = getTrainingHint(SolvingTechnique::XYChain, 1, step); CHECK(hint.text.find("chain") != std::string::npos); } } @@ -332,17 +327,17 @@ TEST_CASE("getTrainingHint — SetLogic category", "[training_hints]") { auto step = makeEliminationStep(SolvingTechnique::ALSxZ, elims); SECTION("Level 1") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::ALSxZ, 1, step); + auto hint = getTrainingHint(SolvingTechnique::ALSxZ, 1, step); CHECK(hint.text.find("Almost Locked Set") != std::string::npos); } SECTION("Level 2") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::ALSxZ, 2, step); + auto hint = getTrainingHint(SolvingTechnique::ALSxZ, 2, step); CHECK(hint.text.find("ALS") != std::string::npos); } SECTION("Level 3") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::ALSxZ, 3, step); + auto hint = getTrainingHint(SolvingTechnique::ALSxZ, 3, step); CHECK(hint.highlights[0].role == CellRole::Fin); } } @@ -352,23 +347,23 @@ TEST_CASE("getTrainingHint — Special (BUG) category", "[training_hints]") { auto step = makeEliminationStep(SolvingTechnique::BUG, elims); SECTION("Level 1") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::BUG, 1, step); + auto hint = getTrainingHint(SolvingTechnique::BUG, 1, step); CHECK(hint.text.find("three candidates") != std::string::npos); } SECTION("Level 2 with positions") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::BUG, 2, step); + auto hint = getTrainingHint(SolvingTechnique::BUG, 2, step); CHECK(hint.text.find("key cell") != std::string::npos); } SECTION("Level 2 without positions") { step.explanation_data.positions.clear(); - auto hint = getTrainingHint(mock_loc, SolvingTechnique::BUG, 2, step); + auto hint = getTrainingHint(SolvingTechnique::BUG, 2, step); CHECK(hint.text == step.explanation); } SECTION("Level 3") { - auto hint = getTrainingHint(mock_loc, SolvingTechnique::BUG, 3, step); + auto hint = getTrainingHint(SolvingTechnique::BUG, 3, step); CHECK(hint.highlights[0].role == CellRole::Fin); } } diff --git a/tests/unit/test_training_view_model.cpp b/tests/unit/test_training_view_model.cpp index 553f8da..7546220 100644 --- a/tests/unit/test_training_view_model.cpp +++ b/tests/unit/test_training_view_model.cpp @@ -15,7 +15,6 @@ // along with this program. If not, see . #include "../../src/core/candidate_grid.h" -#include "../../src/core/i_localization_manager.h" #include "../../src/core/i_training_exercise_generator.h" #include "../../src/core/i_training_statistics_manager.h" #include "../../src/view_model/training_view_model.h" @@ -95,39 +94,9 @@ class MockExerciseGenerator : public ITrainingExerciseGenerator { } }; -/// Minimal localization manager for tests — returns key as-is, except for -/// keys that are used as fmt::format templates (contain {0} in the real locale). -/// Those need a valid placeholder so locFormat() doesn't crash. -class MockLocManager : public ILocalizationManager { -public: - [[nodiscard]] std::string_view getString(std::string_view key) const override { - // Keys used with locFormat() need a valid {0} placeholder. - // Detect by suffix pattern rather than hardcoding specific keys. - if (key.find("feedback_") != std::string_view::npos || key.find("correct_continue") != std::string_view::npos) { - return format_placeholder_; - } - return key; - } - - [[nodiscard]] std::expected setLocale(std::string_view /*locale_code*/) override { - return {}; - } - [[nodiscard]] std::string_view getCurrentLocale() const override { - return "en"; - } - [[nodiscard]] std::vector> getAvailableLocales() const override { - return {{"en", "English"}}; - } - -private: - // Generic format template for any key that locFormat() will expand - std::string format_placeholder_ = "{0}"; -}; - struct VMFixture { std::shared_ptr mock_gen = std::make_shared(); - std::shared_ptr mock_loc = std::make_shared(); - TrainingViewModel vm{mock_gen, mock_loc}; + TrainingViewModel vm{mock_gen}; }; /// Mock stats manager for verifying recordLesson calls @@ -731,9 +700,8 @@ TEST_CASE("TrainingViewModel - skipExercise to LessonComplete", "[training_view_ TEST_CASE("TrainingViewModel - records stats on lesson complete", "[training_view_model]") { auto mock_gen = std::make_shared(); - auto mock_loc = std::make_shared(); auto mock_stats = std::make_shared(); - TrainingViewModel vm{mock_gen, mock_loc, mock_stats}; + TrainingViewModel vm{mock_gen, mock_stats}; vm.selectTechnique(SolvingTechnique::NakedSingle); vm.startExercises(); @@ -761,9 +729,8 @@ TEST_CASE("TrainingViewModel - records stats on lesson complete", "[training_vie TEST_CASE("TrainingViewModel - records stats on skip to complete", "[training_view_model]") { auto mock_gen = std::make_shared(); - auto mock_loc = std::make_shared(); auto mock_stats = std::make_shared(); - TrainingViewModel vm{mock_gen, mock_loc, mock_stats}; + TrainingViewModel vm{mock_gen, mock_stats}; vm.selectTechnique(SolvingTechnique::NakedSingle); vm.startExercises(); @@ -778,10 +745,9 @@ TEST_CASE("TrainingViewModel - records stats on skip to complete", "[training_vi TEST_CASE("TrainingViewModel - stats recording failure does not crash", "[training_view_model]") { auto mock_gen = std::make_shared(); - auto mock_loc = std::make_shared(); auto mock_stats = std::make_shared(); mock_stats->should_fail = true; - TrainingViewModel vm{mock_gen, mock_loc, mock_stats}; + TrainingViewModel vm{mock_gen, mock_stats}; vm.selectTechnique(SolvingTechnique::NakedSingle); vm.startExercises(); @@ -1001,8 +967,7 @@ class MultiStepMockGenerator : public ITrainingExerciseGenerator { TEST_CASE("TrainingViewModel - continue on correct with multiple steps", "[training_view_model]") { auto mock_gen = std::make_shared(); - auto mock_loc = std::make_shared(); - TrainingViewModel vm{mock_gen, mock_loc}; + TrainingViewModel vm{mock_gen}; vm.selectTechnique(SolvingTechnique::NakedSingle); vm.startExercises(); diff --git a/tests/unit/test_undo_pencil_marks.cpp b/tests/unit/test_undo_pencil_marks.cpp index 142c64b..9b5b0f7 100644 --- a/tests/unit/test_undo_pencil_marks.cpp +++ b/tests/unit/test_undo_pencil_marks.cpp @@ -20,7 +20,6 @@ #include "../../src/core/statistics_manager.h" #include "../../src/core/sudoku_solver.h" #include "../../src/view_model/game_view_model.h" -#include "../helpers/mock_localization_manager.h" #include "../helpers/test_utils.h" #include @@ -39,8 +38,7 @@ TEST_CASE("Undo restores pencil marks after placing number", "[undo][pencil_mark auto stats_mgr = std::make_shared("./test_stats_undo_pencil"); auto save_mgr = std::make_shared("./test_saves_undo_pencil"); - GameViewModel view_model(validator, generator, solver, stats_mgr, save_mgr, - std::make_shared()); + GameViewModel view_model(validator, generator, solver, stats_mgr, save_mgr); view_model.startNewGame(Difficulty::Easy); SECTION("Placing number clears pencil marks, undo restores them") {