feat(ui): add launcher language selection and translations #47

Merged
HumanBean merged 3 commits from TrashPanda/LongshotLauncher:feat/ui-language into main 2026-11-14 02:11:28 +00:00
Contributor

The launcher and its build wizard only speak English, while the game itself ships in 16 languages: a player who builds Longshot in Japanese or Polish gets a launcher they may not read. This adds a language picker to the launcher and complete translations for every language the game has.

What changes

  • Settings → Launcher language: a picker listing the 16 languages, each named in itself (English (US), English (UK), Français, Français (Canada), Deutsch, Italiano, Español, Español (Latinoamérica), Português (Brasil), Nederlands, Polski, Русский, 日本語, 한국어, 简体中文, 繁體中文). It switches the whole window at once, without a restart. It is kept in settings.json as uiLanguage.
  • First start follows the system language: LANGUAGE / LC_ALL / LC_MESSAGES / LANG on Linux, GetUserDefaultLocaleName on Windows (the publish uses InvariantGlobalization, so .NET's culture doesn't report it). It falls back to English.
  • What is translated: the home screen, settings, the setup wizard, build progress and its stage names, save import and the save-file screens, problems and requirement checks, dates and durations. make_game.py's own console output stays English. The game's language is still picked separately in the build settings.
  • Core/Tr.cs: each text is looked up by its English. Regional catalogs fall back to their language's (fr-CA to fr, es-419 to es, en-GB to en-US), and anything missing falls back to English. It handles CLDR plural forms (one / few / many / other, so Polish and Russian get all four) and {0} placeholders, so word order can change per language.
  • Views use {c:T 'English text'}; view models use Tr.T / Tr.F / Tr.P (plural) / Tr.N (kept English). Sentences that build on each other are now whole format strings, not English joined piece by piece.
  • Core/Lang/<code>.json: 15 complete catalogs (471 texts each), embedded in the build.
  • scripts/translations.py finds every text in the source (C#, XAML and the language list). --missing <code> prints what a catalog still lacks, ready to fill in. Without arguments it checks every catalog for missing or stale texts, wrong placeholders and missing plural forms. The core tests run it, so a new English text without translations fails CI.

Tested

  • Linux (KDE Plasma), Debug build: switched the language live in Settings (English, German, Japanese); the whole window changed without a restart. With uiLanguage unset, LANG=ja_JP.UTF-8 and LANG=pl_PL.UTF-8 each started the launcher in that language. Japanese renders with the system's fallback fonts. The plural forms (Polish and Russian four-way, French 0–1 as "one", CJK single form) are covered by TranslationChecks, not checked by eye on every screen.
  • CI's steps locally: dotnet build LongshotLauncher.slnx -c Release -p:RestoreLockedMode=true (0 warnings), tests/test_launcher_core.py --strict --longshot … (799 checks, including the new TranslationChecks), tests/test_release.py, scripts/translations.py (471 texts, 15 catalogs, all complete).
  • Not tested on Windows: the GetUserDefaultLocaleName path and font fallback there need a run.

Translations need a native read

The catalogs were machine-written in full, with each language's plural rules, register (informal "du"/"tu"/"ty", formal "вы"/"です・ます") and the Windows/Steam terms players know. They have not been reviewed by native speakers, so corrections are welcome: edit Core/Lang/<code>.json and run python3 -I scripts/translations.py.

Screenshots

Settings in German, with the new Launcher language picker

Settings in German, with the new Launcher language picker

Settings in Japanese

Settings in Japanese

Home in Polish, picked from LANG=pl_PL.UTF-8 on first start

Home in Polish, picked from LANG=pl_PL.UTF-8 on first start

The launcher and its build wizard only speak English, while the game itself ships in 16 languages: a player who builds Longshot in Japanese or Polish gets a launcher they may not read. This adds a language picker to the launcher and complete translations for every language the game has. ## What changes - **Settings → Launcher language**: a picker listing the 16 languages, each named in itself (English (US), English (UK), Français, Français (Canada), Deutsch, Italiano, Español, Español (Latinoamérica), Português (Brasil), Nederlands, Polski, Русский, 日本語, 한국어, 简体中文, 繁體中文). It switches the whole window at once, without a restart. It is kept in `settings.json` as `uiLanguage`. - **First start** follows the system language: `LANGUAGE` / `LC_ALL` / `LC_MESSAGES` / `LANG` on Linux, `GetUserDefaultLocaleName` on Windows (the publish uses `InvariantGlobalization`, so .NET's culture doesn't report it). It falls back to English. - **What is translated**: the home screen, settings, the setup wizard, build progress and its stage names, save import and the save-file screens, problems and requirement checks, dates and durations. `make_game.py`'s own console output stays English. The game's language is still picked separately in the build settings. - **`Core/Tr.cs`**: each text is looked up by its English. Regional catalogs fall back to their language's (fr-CA to fr, es-419 to es, en-GB to en-US), and anything missing falls back to English. It handles CLDR plural forms (one / few / many / other, so Polish and Russian get all four) and `{0}` placeholders, so word order can change per language. - **Views** use `{c:T 'English text'}`; view models use `Tr.T` / `Tr.F` / `Tr.P` (plural) / `Tr.N` (kept English). Sentences that build on each other are now whole format strings, not English joined piece by piece. - **`Core/Lang/<code>.json`**: 15 complete catalogs (471 texts each), embedded in the build. - **`scripts/translations.py`** finds every text in the source (C#, XAML and the language list). `--missing <code>` prints what a catalog still lacks, ready to fill in. Without arguments it checks every catalog for missing or stale texts, wrong placeholders and missing plural forms. The core tests run it, so a new English text without translations fails CI. ## Tested - Linux (KDE Plasma), Debug build: switched the language live in Settings (English, German, Japanese); the whole window changed without a restart. With `uiLanguage` unset, `LANG=ja_JP.UTF-8` and `LANG=pl_PL.UTF-8` each started the launcher in that language. Japanese renders with the system's fallback fonts. The plural forms (Polish and Russian four-way, French 0–1 as "one", CJK single form) are covered by `TranslationChecks`, not checked by eye on every screen. - CI's steps locally: `dotnet build LongshotLauncher.slnx -c Release -p:RestoreLockedMode=true` (0 warnings), `tests/test_launcher_core.py --strict --longshot …` (799 checks, including the new `TranslationChecks`), `tests/test_release.py`, `scripts/translations.py` (471 texts, 15 catalogs, all complete). - **Not tested on Windows**: the `GetUserDefaultLocaleName` path and font fallback there need a run. ## Translations need a native read The catalogs were machine-written in full, with each language's plural rules, register (informal "du"/"tu"/"ty", formal "вы"/"です・ます") and the Windows/Steam terms players know. They have not been reviewed by native speakers, so corrections are welcome: edit `Core/Lang/<code>.json` and run `python3 -I scripts/translations.py`. ## Screenshots **Settings in German, with the new Launcher language picker** ![Settings in German, with the new Launcher language picker](https://git.magicbean.dev/attachments/d26fb944-4a27-488c-900a-5a96121b7a4b) **Settings in Japanese** ![Settings in Japanese](https://git.magicbean.dev/attachments/9550a557-130c-491c-ad02-c38bfd1777c2) **Home in Polish, picked from LANG=pl_PL.UTF-8 on first start** ![Home in Polish, picked from LANG=pl_PL.UTF-8 on first start](https://git.magicbean.dev/attachments/d15a4d32-16be-4068-94f6-628a182ad8ac)
feat(ui): add launcher language selection and translations
Some checks failed
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Compile and test (pull_request) Has been cancelled
b40ae1328f
The launcher and its build wizard now speak the 16 languages the game does,
picked in Settings or taken from the system language on first start.

- Core/Tr.cs: catalogs looked up by their English text, regional fallback
  (fr-CA to fr, es-419 to es, en-GB to en-US), CLDR plural forms, live switching
- {c:T '...'} for the views; view models through Tr.T / Tr.F / Tr.P / Tr.N
- Core/Lang/*.json: complete catalogs for all 15 non-English languages
- scripts/translations.py: lists texts to translate and checks every catalog
  for missing texts, placeholders and plural forms (run by the core tests)

Co-authored-by: Cursor <[email protected]>
TrashPanda force-pushed feat/ui-language from b40ae1328f
Some checks failed
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Compile and test (pull_request) Has been cancelled
to 93eab1b790
Some checks failed
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Compile and test (pull_request) Has been cancelled
2026-11-13 17:32:31 +00:00
Compare
Author
Contributor

Wording choices worth a native speaker's look. Each is in Core/Lang/<code>.json.

French / Italian

  • "Build": French uses "build" (masc.) for the result, "créer" for the verb and "création" for the process. Italian does the same with "build" (fem.), "creare" and "creazione". This keeps them apart from the separate "Compile" step.
  • The "Translate" build step is "Traduire" / "Traduci". The alternative is "Conversion" / "Conversione".
  • A backup in French is "copie de secours", since "sauvegarde" already means a game save.
  • fr-CA overrides only Québec terms: "bogue", "nuanceur(s) de maillage", "téléverser".

Polish / Russian

  • Polish uses informal "ty"; Russian uses formal "вы".
  • "Build": the noun is "kompilacja" / "сборка", and the button is "Zbuduj" / "Собрать". The "Translate" step is "Translacja" / "Трансляция".
  • Placeholders that hold whole phrases follow a colon ("Pozostało: {0}") so they stay in the nominative.
Wording choices worth a native speaker's look. Each is in `Core/Lang/<code>.json`. **French / Italian** - "Build": French uses "build" (masc.) for the result, "créer" for the verb and "création" for the process. Italian does the same with "build" (fem.), "creare" and "creazione". This keeps them apart from the separate "Compile" step. - The "Translate" build step is "Traduire" / "Traduci". The alternative is "Conversion" / "Conversione". - A backup in French is "copie de secours", since "sauvegarde" already means a game save. - fr-CA overrides only Québec terms: "bogue", "nuanceur(s) de maillage", "téléverser". **Polish / Russian** - Polish uses informal "ty"; Russian uses formal "вы". - "Build": the noun is "kompilacja" / "сборка", and the button is "Zbuduj" / "Собрать". The "Translate" step is "Translacja" / "Трансляция". - Placeholders that hold whole phrases follow a colon ("Pozostało: {0}") so they stay in the nominative.
HumanBean requested changes 2026-11-13 17:40:48 +00:00
Dismissed
HumanBean left a comment

Review: LongshotLauncher #47, launcher language selection and translations

#47 (head 93eab1b7, on top of current main 0e6e1d7, so it includes #44; no conflicts.)

Verdict: request changes. Only small fixes are needed: one string in each of two catalogs, and a few cleanups.

What's good

  • Core/Tr.cs is small and sound. Each text is looked up by its English. The chain is fr-CA → fr → English (and es-419 → es, en-GB → English), and the catalogs are embedded resources parsed once on first use, which costs little at startup. T/F/P read an array reference that Use swaps in one step, so build-thread calls are safe. A translation whose placeholders don't fit falls back to the English instead of throwing a FormatException.
  • Placeholders are consistent. I checked every catalog mechanically with my own parser: every {n} set matches its English key in all 15 catalogs (473 entries in each full one), including each plural form, and no catalog has a lone brace. scripts/translations.py checks the same thing, and tests/test_launcher_core.py runs it, so a broken placeholder fails the tests.
  • Plurals and parsing. The CLDR rules for ru and pl (one/few/many) and for fr and pt (0 counts as "one") are right and tested. Tr.Pick and FromEnvironment are well covered (de_AT.UTF-8, zh-Hant-TW, C.UTF-8, LANGUAGE lists, precedence).
  • Strings moved. The views look completely converted: no literal Text=/Content=/ToolTip.Tip= is left apart from product names. Sentences that were joined piece by piece are now whole format strings, which is the right fix for word order. In the licence texts the SDK version, file names and URLs are placeholders, so a pin bump doesn't make the translations stale, and check_licences still finds the COMMIT.
  • Culture. InvariantGlobalization is on, and dates and numbers stay invariant (HH:mm, 0.#), so config, versions and paths aren't affected by culture (no Turkish-i or comma-decimal risk). The saved uiLanguage is normalised through Tr.Pick, so an unknown value means "none picked". A change applies live, without a restart.
  • Translations. I spot-checked the licence, consent and privacy text for the DLSS and both FSR 4 prompts, the "nothing is uploaded" line, delete and close-first errors and the save-gather notice in de, fr, es, it, pl, ru, ja, ko and zh-Hans. They are accurate, keep NVIDIA/AMD/DLSS/FSR/Longshot/Hookshot/SHA-256 untouched, and use the localised Windows names ("Apps & Features", "Aplikacje i funkcje", "アプリと機能").

Must change

  1. The game's name is translated in two catalogs, and Korean adds "Zelda". In Core/Lang/ja.json:390 "Ocarina of Time" becomes 「時のオカリナ」. In Core/Lang/ko.json:390 it becomes "젤다의 전설 시간의 오카리나" ("The Legend of Zelda: Ocarina of Time"), which adds Nintendo branding that the English avoids, against the README's neutral game wording. Every other catalog, zh-Hans and zh-Hant included, keeps "Ocarina of Time" as written. Use "Ocarina of Time" in both.

Should change

  1. pt-BR shows "1" for zero. In CLDR, Portuguese uses the one form for 0 as well as 1, but 13 of pt-BR's one forms hard-code "1" instead of {0} (e.g. Core/Lang/pt-BR.json:143 "1 slot de salvamento"). The Saves page's SlotCount (SavesViewModel.cs:79) is shown with zero slots, so a player with no saves reads "1 slot de salvamento". Use {0} in pt-BR's one forms. fr already does this, and ru and pl use {0} in theirs. translations.py should also require {0} in any category that covers more than one number (fr/pt one; ru/pl one, which covers 21, 31 and so on).
  2. On Windows the first-start language comes from the region format, not the display language. GetUserDefaultLocaleName (App/Controls/Translate.cs:21-29) returns the user's regional-format locale. An English Windows with German date and number formats would start the launcher in German. The README says "Windows' display language". Use GetUserPreferredUILanguages (or GetUserDefaultUILanguage + LCIDToLocaleName). The PR says the Windows path is untested, so this needs a Windows run either way.
  3. The PR description overstates the CI check. It says "a new English text without translations fails CI", but check_translations in tests/test_launcher_core.py only prints texts a catalog lacks, and the README says the same. Treating missing texts as non-fatal seems right; fix the description.

Nits (fine to leave):

  • SettingsViewModel.Languages builds the "System language" entry once (SettingsViewModel.cs:65), so it stays in the old language right after a switch, on the very page where the switch happens. The wizard's "Automatic" and "{0} at once" choices are built the same way, but they are rebuilt per wizard.
  • LogLevels.Name (Errors/Warnings/Info/Debug/Trace in the build log's level picker) is still hard-coded English.
  • Generic keys such as "{0} {1}" and "{0} {1} {2}" (the date order, Core/MakeGameRun.cs:264) and "{0} of {1}" will collide with any later text that has the same English, because the key is the text. A distinctive English key (or an @ note) would make the meaning clear to translators.
  • Copy for a bug report includes why, which can now be a translated launcher message ("The build tools could not start: …"), while the README says the report stays English.
  • CJK uses the system's fallback fonts (Inter has no CJK). This is fine on Linux per the PR; Windows still needs a look.

Not run: per instructions, nothing was built or tested. The checks above come from reading the code plus a read-only parse of the 15 catalogs.

# Review: LongshotLauncher #47, launcher language selection and translations https://git.magicbean.dev/MagicBeanDev/LongshotLauncher/pulls/47 (head 93eab1b7, on top of current main 0e6e1d7, so it includes https://git.magicbean.dev/MagicBeanDev/LongshotLauncher/pulls/44; no conflicts.) Verdict: **request changes**. Only small fixes are needed: one string in each of two catalogs, and a few cleanups. ## What's good - `Core/Tr.cs` is small and sound. Each text is looked up by its English. The chain is fr-CA → fr → English (and es-419 → es, en-GB → English), and the catalogs are embedded resources parsed once on first use, which costs little at startup. `T`/`F`/`P` read an array reference that `Use` swaps in one step, so build-thread calls are safe. A translation whose placeholders don't fit falls back to the English instead of throwing a `FormatException`. - **Placeholders are consistent.** I checked every catalog mechanically with my own parser: every `{n}` set matches its English key in all 15 catalogs (473 entries in each full one), including each plural form, and no catalog has a lone brace. `scripts/translations.py` checks the same thing, and `tests/test_launcher_core.py` runs it, so a broken placeholder fails the tests. - **Plurals and parsing.** The CLDR rules for ru and pl (one/few/many) and for fr and pt (0 counts as "one") are right and tested. `Tr.Pick` and `FromEnvironment` are well covered (`de_AT.UTF-8`, `zh-Hant-TW`, `C.UTF-8`, `LANGUAGE` lists, precedence). - **Strings moved.** The views look completely converted: no literal `Text=`/`Content=`/`ToolTip.Tip=` is left apart from product names. Sentences that were joined piece by piece are now whole format strings, which is the right fix for word order. In the licence texts the SDK version, file names and URLs are placeholders, so a pin bump doesn't make the translations stale, and `check_licences` still finds the COMMIT. - **Culture.** `InvariantGlobalization` is on, and dates and numbers stay invariant (`HH:mm`, `0.#`), so config, versions and paths aren't affected by culture (no Turkish-i or comma-decimal risk). The saved `uiLanguage` is normalised through `Tr.Pick`, so an unknown value means "none picked". A change applies live, without a restart. - **Translations.** I spot-checked the licence, consent and privacy text for the DLSS and both FSR 4 prompts, the "nothing is uploaded" line, delete and close-first errors and the save-gather notice in de, fr, es, it, pl, ru, ja, ko and zh-Hans. They are accurate, keep NVIDIA/AMD/DLSS/FSR/Longshot/Hookshot/SHA-256 untouched, and use the localised Windows names ("Apps & Features", "Aplikacje i funkcje", "アプリと機能"). ## Must change 1. **The game's name is translated in two catalogs, and Korean adds "Zelda".** In `Core/Lang/ja.json:390` "Ocarina of Time" becomes 「時のオカリナ」. In `Core/Lang/ko.json:390` it becomes "젤다의 전설 시간의 오카리나" ("The Legend of Zelda: Ocarina of Time"), which adds Nintendo branding that the English avoids, against the README's neutral game wording. Every other catalog, zh-Hans and zh-Hant included, keeps "Ocarina of Time" as written. Use "Ocarina of Time" in both. ## Should change 2. **pt-BR shows "1" for zero.** In CLDR, Portuguese uses the `one` form for 0 as well as 1, but 13 of pt-BR's `one` forms hard-code "1" instead of `{0}` (e.g. `Core/Lang/pt-BR.json:143` "1 slot de salvamento"). The Saves page's `SlotCount` (`SavesViewModel.cs:79`) is shown with zero slots, so a player with no saves reads "1 slot de salvamento". Use `{0}` in pt-BR's `one` forms. fr already does this, and ru and pl use `{0}` in theirs. `translations.py` should also require `{0}` in any category that covers more than one number (fr/pt `one`; ru/pl `one`, which covers 21, 31 and so on). 3. **On Windows the first-start language comes from the region format, not the display language.** `GetUserDefaultLocaleName` (`App/Controls/Translate.cs:21-29`) returns the user's regional-format locale. An English Windows with German date and number formats would start the launcher in German. The README says "Windows' display language". Use `GetUserPreferredUILanguages` (or `GetUserDefaultUILanguage` + `LCIDToLocaleName`). The PR says the Windows path is untested, so this needs a Windows run either way. 4. **The PR description overstates the CI check.** It says "a new English text without translations fails CI", but `check_translations` in `tests/test_launcher_core.py` only prints texts a catalog lacks, and the README says the same. Treating missing texts as non-fatal seems right; fix the description. Nits (fine to leave): - `SettingsViewModel.Languages` builds the "System language" entry once (`SettingsViewModel.cs:65`), so it stays in the old language right after a switch, on the very page where the switch happens. The wizard's "Automatic" and "{0} at once" choices are built the same way, but they are rebuilt per wizard. - `LogLevels.Name` (Errors/Warnings/Info/Debug/Trace in the build log's level picker) is still hard-coded English. - Generic keys such as `"{0} {1}"` and `"{0} {1} {2}"` (the date order, `Core/MakeGameRun.cs:264`) and `"{0} of {1}"` will collide with any later text that has the same English, because the key is the text. A distinctive English key (or an `@` note) would make the meaning clear to translators. - *Copy for a bug report* includes `why`, which can now be a translated launcher message ("The build tools could not start: …"), while the README says the report stays English. - CJK uses the system's fallback fonts (Inter has no CJK). This is fine on Linux per the PR; Windows still needs a look. Not run: per instructions, nothing was built or tested. The checks above come from reading the code plus a read-only parse of the 15 catalogs.
@ -0,0 +26,4 @@
try
{
var buffer = new char[85];
var n = GetUserDefaultLocaleName(buffer, buffer.Length);
Owner

GetUserDefaultLocaleName returns the user's regional-format locale, not the display language, so an English Windows with German formats starts in German. The README says "Windows' display language". GetUserPreferredUILanguages (or GetUserDefaultUILanguage + LCIDToLocaleName) gives the UI language. This path is untested per the PR, so it needs a Windows run either way.

`GetUserDefaultLocaleName` returns the user's regional-format locale, not the display language, so an English Windows with German formats starts in German. The README says "Windows' display language". `GetUserPreferredUILanguages` (or `GetUserDefaultUILanguage` + `LCIDToLocaleName`) gives the UI language. This path is untested per the PR, so it needs a Windows run either way.
@ -62,3 +62,4 @@
ReleaseNotesCommand = new Command(() => Main.Host?.Open(Releases.Repo + "/releases"));
AddToMenuCommand = new Command(AddToMenu, () => Program.Layout.File != null);
RemoveFromMenuCommand = new Command(RemoveFromMenu);
Languages = new[] { new UiLanguageChoice(null, Tr.T("System language")) }
Owner

Nit: "System language" is translated once, when the page is built, so right after a switch it stays in the old language on this same page until Settings is reopened.

Nit: "System language" is translated once, when the page is built, so right after a switch it stays in the old language on this same page until Settings is reopened.
@ -0,0 +387,4 @@
"Import…": "インポート…",
"Dismiss": "非表示",
"Build your game": "ゲームをビルド",
"Longshot turns your own copy of Ocarina of Time into a PC game. It takes about 15 to 30 minutes, once per version.": "Longshot は、お持ちの「時のオカリナ」を PC ゲームにします。かかる時間は約 15〜30 分で、バージョンごとに 1 回だけです。",
Owner

The game's name is translated here (「時のオカリナ」). Every other catalog, zh-Hans and zh-Hant included, keeps "Ocarina of Time" as written; product and game names stay untranslated. Please use "Ocarina of Time".

The game's name is translated here (「時のオカリナ」). Every other catalog, zh-Hans and zh-Hant included, keeps "Ocarina of Time" as written; product and game names stay untranslated. Please use "Ocarina of Time".
@ -0,0 +387,4 @@
"Import…": "가져오기…",
"Dismiss": "닫기",
"Build your game": "게임 빌드",
"Longshot turns your own copy of Ocarina of Time into a PC game. It takes about 15 to 30 minutes, once per version.": "Longshot은 내가 가진 젤다의 전설 시간의 오카리나를 PC 게임으로 만들어 줍니다. 버전마다 한 번, 약 15~30분 걸립니다.",
Owner

This translates the game's name and adds "The Legend of Zelda" (젤다의 전설 시간의 오카리나), Nintendo branding that the English avoids (the README's neutral game wording). Please keep "Ocarina of Time" as written, without "Zelda".

This translates the game's name and adds "The Legend of Zelda" (젤다의 전설 시간의 오카리나), Nintendo branding that the English avoids (the README's neutral game wording). Please keep "Ocarina of Time" as written, without "Zelda".
@ -0,0 +140,4 @@
"All your Longshot versions share these saves.": "Todas as suas versões do Longshot compartilham esses jogos salvos.",
"Import saves from an emulator or Hookshot. They will be available to every Longshot version.": "Importe jogos salvos de um emulador ou do Hookshot. Eles ficarão disponíveis para todas as versões do Longshot.",
"{0} save slots": {
"one": "1 slot de salvamento",
Owner

In CLDR, Portuguese uses one for 0 as well as 1, so the Saves page (SlotCount, shown with zero slots) reads "1 slot de salvamento" when there are none. pt-BR has 13 one forms with a hard-coded "1"; please use {0} in all of them, as fr does. translations.py could require {0} in any category that covers more than one number (fr/pt one, ru/pl one).

In CLDR, Portuguese uses `one` for 0 as well as 1, so the Saves page (`SlotCount`, shown with zero slots) reads "1 slot de salvamento" when there are none. pt-BR has 13 `one` forms with a hard-coded "1"; please use `{0}` in all of them, as fr does. `translations.py` could require `{0}` in any category that covers more than one number (fr/pt `one`, ru/pl `one`).
@ -254,0 +261,4 @@
static string Date(DateTime t, DateTime now)
{
var month = Tr.T(Months[t.Month - 1]);
return t.Year == now.Year ? Tr.F("{0} {1}", t.Day, month) : Tr.F("{0} {1} {2}", t.Day, month, t.Year);
Owner

Nit: "{0} {1}" / "{0} {1} {2}" as the date-order keys are generic. Since the key is the English text, any later Tr.F("{0} {1}", ...) would get the date's translation (ja {1}{0}日). A distinctive English key, or an @ note for translators, would avoid that.

Nit: `"{0} {1}"` / `"{0} {1} {2}"` as the date-order keys are generic. Since the key is the English text, any later `Tr.F("{0} {1}", ...)` would get the date's translation (ja `{1}{0}日`). A distinctive English key, or an `@` note for translators, would avoid that.
fix(ui): keep Ocarina of Time untranslated, show the count in pt-BR's one forms, start in Windows' display language
All checks were successful
CI / Compile and test (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
dc2029a578
- ja, ko: the game's name stays "Ocarina of Time" (ko no longer adds "Zelda")
- pt-BR: the one forms use {0}, since Portuguese's one is 0 as well as 1
- scripts/translations.py: a plural form whose category holds more than one number must show {0}
- Windows: the system language is the first of the user's preferred UI languages the launcher has
  (GetUserPreferredUILanguages), not the regional-format locale

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Owner

Fixes for HumanBean's review (request changes, review 1138), pushed on top of your head as dc2029a5 (fast-forward from 93eab1b7, your commit untouched):

  1. Game name (must change): Core/Lang/ja.json and Core/Lang/ko.json now keep "Ocarina of Time" as written; ko no longer adds "Zelda" (내가 가진 Ocarina of Time을, ja お持ちの Ocarina of Time を).
  2. pt-BR zero shown as "1": all 14 of pt-BR's one forms that hard-coded "1" now use {0} (the review counted 13; "1 processo" was the 14th). scripts/translations.py now fails a plural form without {0} when its category holds more than one number: every category except one in languages whose one is exactly 1. So fr and pt one (0 and 1) and ru one (21, 31...) need {0}. pl's one is only 1 (Tr.PluralOf), so it may still write the 1 out. A read-only parse of the 15 catalogs finds no other form the new rule rejects.
  3. Windows first-start language: SystemLanguage.Name() (App/Controls/Translate.cs) now calls GetUserPreferredUILanguages(MUI_LANGUAGE_NAME) and returns the first display language in the user's list that the launcher has (via Tr.Pick), the same way LANGUAGE lists are read on Linux, instead of the regional-format locale. This matches the README's "Windows' display language". It still needs a Windows run.
  4. PR description: not edited, since it is yours. Please change "a new English text without translations fails CI": check_translations only prints missing texts, and only faults fail it.

Nits not changed (the review said they were fine to leave): "System language" not re-translated after a switch, LogLevels.Name, the generic date keys, the translated why in the bug-report copy, CJK fonts on Windows.

Nothing was built or tested here: no dotnet build, no tests/test_launcher_core.py, no scripts/translations.py run. The test boxes are yours: please run the checks and try the Windows path.

Fixes for HumanBean's review (request changes, review 1138), pushed on top of your head as dc2029a5 (fast-forward from 93eab1b7, your commit untouched): 1. **Game name (must change):** `Core/Lang/ja.json` and `Core/Lang/ko.json` now keep "Ocarina of Time" as written; ko no longer adds "Zelda" (`내가 가진 Ocarina of Time을`, ja `お持ちの Ocarina of Time を`). 2. **pt-BR zero shown as "1":** all 14 of pt-BR's `one` forms that hard-coded "1" now use `{0}` (the review counted 13; "1 processo" was the 14th). `scripts/translations.py` now fails a plural form without `{0}` when its category holds more than one number: every category except `one` in languages whose `one` is exactly 1. So fr and pt `one` (0 and 1) and ru `one` (21, 31...) need `{0}`. pl's `one` is only 1 (`Tr.PluralOf`), so it may still write the 1 out. A read-only parse of the 15 catalogs finds no other form the new rule rejects. 3. **Windows first-start language:** `SystemLanguage.Name()` (`App/Controls/Translate.cs`) now calls `GetUserPreferredUILanguages(MUI_LANGUAGE_NAME)` and returns the first display language in the user's list that the launcher has (via `Tr.Pick`), the same way `LANGUAGE` lists are read on Linux, instead of the regional-format locale. This matches the README's "Windows' display language". It still needs a Windows run. 4. **PR description:** not edited, since it is yours. Please change "a new English text without translations fails CI": `check_translations` only prints missing texts, and only faults fail it. Nits not changed (the review said they were fine to leave): "System language" not re-translated after a switch, `LogLevels.Name`, the generic date keys, the translated `why` in the bug-report copy, CJK fonts on Windows. Nothing was built or tested here: no `dotnet build`, no `tests/test_launcher_core.py`, no `scripts/translations.py` run. The test boxes are yours: please run the checks and try the Windows path.
HumanBean approved these changes 2026-11-14 00:31:36 +00:00
Dismissed
HumanBean left a comment

Re-review of the fix commit dc2029a5 only, against our earlier review (#47 (comment), review 1138). Verdict: approve. Every code point is fixed, and the fix doesn't break anything I could see.

  1. Ocarina of Time in ja/ko: fixed. Both catalogs now keep "Ocarina of Time" as written, and "Zelda" is gone from ko.
  2. pt-BR one forms: fixed. All 14 one forms now use {0}, and none of the forms in fr, fr-CA, ru, pl or pt-BR is left without the count. The new one_number() rule in translations.py matches Tr.PluralOf. fr's and pt's one covers 0 and 1, and ru's covers 1, 21, 31 and so on, so those must show {0}. pl's one really is only 1, so letting it write the 1 out is correct (our earlier review was wrong to group pl with ru). few, many and other must always show {0}, and the check sits in the elif after the placeholder-set check, so a form that already fails that check doesn't get a second fault.
  3. Windows display language: fixed in the code. The P/Invoke matches the Win32 signature (DWORD, PULONG, PZZWSTR, PULONG, BOOL marshalled as Bool, Unicode). The size query with a null buffer, then the fill, is the documented pattern. The double-NUL list is cut to size and split with RemoveEmptyEntries, so the final terminator gives no empty name. Taking the first preferred language the launcher has, through Tr.Pick, is a good use of the list. UiLanguageOr runs Pick again, which is harmless because an exact code in All returns itself. This still needs a run on Windows (an English display language with a German region format should start in English).
  4. The description is the author's to fix. It still says "fails CI", and it still names GetUserDefaultLocaleName for Windows. Please update both before merge.

Not run: per instructions, this is a code read only.

Re-review of the fix commit dc2029a5 only, against our earlier review (https://git.magicbean.dev/MagicBeanDev/LongshotLauncher/pulls/47#issuecomment-1138, review 1138). Verdict: **approve**. Every code point is fixed, and the fix doesn't break anything I could see. 1. **Ocarina of Time in ja/ko: fixed.** Both catalogs now keep "Ocarina of Time" as written, and "Zelda" is gone from ko. 2. **pt-BR `one` forms: fixed.** All 14 `one` forms now use `{0}`, and none of the forms in fr, fr-CA, ru, pl or pt-BR is left without the count. The new `one_number()` rule in `translations.py` matches `Tr.PluralOf`. fr's and pt's `one` covers 0 and 1, and ru's covers 1, 21, 31 and so on, so those must show `{0}`. pl's `one` really is only 1, so letting it write the 1 out is correct (our earlier review was wrong to group pl with ru). `few`, `many` and `other` must always show `{0}`, and the check sits in the `elif` after the placeholder-set check, so a form that already fails that check doesn't get a second fault. 3. **Windows display language: fixed in the code.** The P/Invoke matches the Win32 signature (`DWORD`, `PULONG`, `PZZWSTR`, `PULONG`, `BOOL` marshalled as `Bool`, Unicode). The size query with a null buffer, then the fill, is the documented pattern. The double-NUL list is cut to `size` and split with `RemoveEmptyEntries`, so the final terminator gives no empty name. Taking the first preferred language the launcher has, through `Tr.Pick`, is a good use of the list. `UiLanguageOr` runs `Pick` again, which is harmless because an exact code in `All` returns itself. **This still needs a run on Windows** (an English display language with a German region format should start in English). 4. **The description is the author's to fix.** It still says "fails CI", and it still names `GetUserDefaultLocaleName` for Windows. Please update both before merge. Not run: per instructions, this is a code read only.
HumanBean force-pushed feat/ui-language from dc2029a578
All checks were successful
CI / Compile and test (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
to 19f8944072
All checks were successful
CI / Compile and test (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
2026-11-14 02:05:28 +00:00
Compare
HumanBean dismissed HumanBean's review 2026-11-14 02:05:28 +00:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

Owner

Rebased onto current main (c5440787, which has #48, #51 and #52) and force-pushed: the head is now 19f89440. There are three commits. The first is Joey's (516b8f1e, still authored by him, with the conflicts resolved in it). The second is the earlier fix (d5e0c7cc, unchanged, rebased with no conflict). The third is new (19f89440, see below).

Conflicts, all resolved in the author's commit, keeping both sides

  • App/ViewModels/BuildStepViewModel.cs (ProblemViewModel): kept #48's Known/Unknown/Memory/Said/Body. Head falls back to Tr.T("make_game.py stopped"), and Said is the PR's existing Tr.F("Building {0} stopped in {1}, …"). The PR's "The PC ran out of memory while building" headline isn't used any more (#48's Problems give the headline now), so I took that key out of every catalog. Otherwise the check would flag it as left over.
  • App/Views/HookshotImportView.axaml: kept #52's new Import graphics and controls… button, written as {c:T …} like its neighbours.
  • tests/launcher_core/Program.cs: both ProblemChecks() and TranslationChecks() are registered.
  • tests/test_launcher_core.py: both check_save_user (#52) and check_translations (PR) are kept, and main() calls both.

I compared main's #48/#52 changes against the result file by file. All the lines the stack removes are only those wrapped in Tr, and nothing of main's logic is reverted. One small change: the NX2navi offer no longer lowercases SaveText.Long(...), the same as the PR already does for the Hookshot offer, because lowercasing would break German nouns.

New commit 19f89440: main's new texts through the translation layer

  • Core/Problems.cs: headlines, details and fix labels are marked Tr.N and stay English in the Problem record, so the signature, the tests and bug reports are unchanged. The screens show them with Tr.T, which also follows a language change. ProblemViewModel.OfferLabel and ProblemPageViewModel.FixLabel replace the Offer.Label/Fix.Label bindings. The crash page's Sub, What you can do and Close are translated too.
  • Home: the NX2navi card and offer. Several profiles now go through Tr.P, so the English reads "Found saves in 3 profiles in …".
  • HookshotImportViewModel/SavesViewModel: the two "Longshot's save location changed…" messages.
  • HookshotSettingsViewModel and HookshotSettingsView: everything. The skipped-options count is Tr.P, so a single skipped option now reads "1 Hookshot option is…". The language name goes through Tr.T(Languages.NameOf(…)). The toast and the backup note are whole sentences (Tr.F, Tr.Sentences).
  • Catalogs: 50 new texts (2 of them plurals with every CLDR form, {0} in each form that needs it) in all 12 full catalogs (de, es, fr, it, nl, pl, pt-BR, ru, ja, ko, zh-Hans, zh-Hant). es-419 overrides 17 of them (past tense, configuración, reemplazar, computadora) and fr-CA 1 (nuanceurs de maillage). French uses the catalog's no-break spaces. "Ocarina of Time" is left untranslated. The terms follow each catalog's existing words (Build memory, step 2's Advanced, graphics driver, dump, saves).
  • Left in English on purpose: the SaveFailure messages thrown inside Core (Core/HookshotSettings.cs), the same as Core/Saves.cs's, which the PR doesn't translate either. Translating Core's exception messages could be a follow-up.

Not done here: nothing was built or run on this PC: no dotnet, no tests/run_all.py, no scripts/translations.py. I checked the placeholders, plural forms and keys against the sources by reading them, and the C#/XAML only by reading. The next run of the checks (python -I scripts/translations.py, tests/test_launcher_core.py) and a build should confirm it.

Rebased onto current `main` (c5440787, which has [#48](https://git.magicbean.dev/MagicBeanDev/LongshotLauncher/pulls/48), [#51](https://git.magicbean.dev/MagicBeanDev/LongshotLauncher/pulls/51) and [#52](https://git.magicbean.dev/MagicBeanDev/LongshotLauncher/pulls/52)) and force-pushed: the head is now 19f89440. There are three commits. The first is Joey's (`516b8f1e`, still authored by him, with the conflicts resolved in it). The second is the earlier fix (`d5e0c7cc`, unchanged, rebased with no conflict). The third is new (`19f89440`, see below). **Conflicts, all resolved in the author's commit, keeping both sides** - `App/ViewModels/BuildStepViewModel.cs` (ProblemViewModel): kept #48's `Known`/`Unknown`/`Memory`/`Said`/`Body`. `Head` falls back to `Tr.T("make_game.py stopped")`, and `Said` is the PR's existing `Tr.F("Building {0} stopped in {1}, …")`. The PR's "The PC ran out of memory while building" headline isn't used any more (#48's Problems give the headline now), so I took that key out of every catalog. Otherwise the check would flag it as left over. - `App/Views/HookshotImportView.axaml`: kept #52's new *Import graphics and controls…* button, written as `{c:T …}` like its neighbours. - `tests/launcher_core/Program.cs`: both `ProblemChecks()` and `TranslationChecks()` are registered. - `tests/test_launcher_core.py`: both `check_save_user` (#52) and `check_translations` (PR) are kept, and `main()` calls both. I compared main's #48/#52 changes against the result file by file. All the lines the stack removes are only those wrapped in `Tr`, and nothing of main's logic is reverted. One small change: the NX2navi offer no longer lowercases `SaveText.Long(...)`, the same as the PR already does for the Hookshot offer, because lowercasing would break German nouns. **New commit `19f89440`: main's new texts through the translation layer** - `Core/Problems.cs`: headlines, details and fix labels are marked `Tr.N` and stay English in the `Problem` record, so the signature, the tests and bug reports are unchanged. The screens show them with `Tr.T`, which also follows a language change. `ProblemViewModel.OfferLabel` and `ProblemPageViewModel.FixLabel` replace the `Offer.Label`/`Fix.Label` bindings. The crash page's `Sub`, *What you can do* and *Close* are translated too. - Home: the NX2navi card and offer. Several profiles now go through `Tr.P`, so the English reads "Found saves in 3 profiles in …". - `HookshotImportViewModel`/`SavesViewModel`: the two "Longshot's save location changed…" messages. - `HookshotSettingsViewModel` and `HookshotSettingsView`: everything. The skipped-options count is `Tr.P`, so a single skipped option now reads "1 Hookshot option is…". The language name goes through `Tr.T(Languages.NameOf(…))`. The toast and the backup note are whole sentences (`Tr.F`, `Tr.Sentences`). - Catalogs: 50 new texts (2 of them plurals with every CLDR form, `{0}` in each form that needs it) in all 12 full catalogs (de, es, fr, it, nl, pl, pt-BR, ru, ja, ko, zh-Hans, zh-Hant). es-419 overrides 17 of them (past tense, *configuración*, *reemplazar*, *computadora*) and fr-CA 1 (*nuanceurs de maillage*). French uses the catalog's no-break spaces. "Ocarina of Time" is left untranslated. The terms follow each catalog's existing words (Build memory, step 2's Advanced, graphics driver, dump, saves). - Left in English on purpose: the `SaveFailure` messages thrown inside Core (`Core/HookshotSettings.cs`), the same as `Core/Saves.cs`'s, which the PR doesn't translate either. Translating Core's exception messages could be a follow-up. **Not done here:** nothing was built or run on this PC: no `dotnet`, no `tests/run_all.py`, no `scripts/translations.py`. I checked the placeholders, plural forms and keys against the sources by reading them, and the C#/XAML only by reading. The next run of the checks (`python -I scripts/translations.py`, `tests/test_launcher_core.py`) and a build should confirm it.
HumanBean approved these changes 2026-11-14 02:10:40 +00:00
HumanBean left a comment

Re-review after the rebase onto main c5440787 (#48 and #52), head 19f89440. Read only: nothing was built or run here.

The rebase keeps both sides. Against main, BuildStepViewModel.cs, HomeViewModel.cs, MainViewModel.cs, HookshotImportViewModel.cs, HookshotImportView.axaml, Core/Problems.cs and the tests differ only by text going through Tr. #48's Known/Unknown/Memory/Said/Body, the offer and its TakeOffer routing, the crash page and #52's import flows (target re-check, overwrite/keep, language carry-over, backup text) are all still there. tests/launcher_core/Program.cs runs both ProblemChecks() and TranslationChecks(), and test_launcher_core.py keeps main's checks and adds check_translations(). The removed "The PC ran out of memory while building" key appears nowhere in App/ or Core/ (the headline is "Your PC ran out of memory while building", which has its own key).

The new commit holds up:

  • Every Tr.N text in Problems.cs is shown through Tr.T, in ProblemViewModel.Head/Body/OfferLabel and ProblemPageViewModel.Headline/Detail/FixLabel. Nothing else reads Headline, Detail or Label, and nothing is translated twice. Tr.N is the identity, so ProblemChecks and the logs still see English.
  • The old {Binding Offer.Label} and {Binding Fix.Label} are gone. OfferLabel and FixLabel exist on the x:DataType view models, so compiled bindings resolve. xmlns:c is on every view that uses c:T, including HookshotSettingsView, and no literal English is left in any .axaml (only Longshot, NVIDIA DLSS and AMD FSR 4).
  • All 50 new keys are in all 12 full catalogs. The {0}/{1} placeholders match the English in every catalog. The es-419 and fr-CA overrides all have matching keys in es/fr. "Ocarina of Time" is kept as is in every catalog. A grep found no leftover keys.
  • Plurals: the forms match PluralCategories (fr/es/it/pt-BR one/many/other, ru/pl one/few/many/other, ja/ko/zh other). Every form shows {0}, so one_number holds, including ru's one (21 профиле) and fr/pt's one. The NX2navi Tr.P runs only for 2 or more profiles, but its one form is still needed for ru 21, 31 and so on.
  • Translations I spot-checked in fr, de, pt-BR, ru, pl, ja, zh-Hans and es-419 read well and use the terms the catalogs already use (création/bauen/сборка/ビルド/构建, Build memory, dump/zrzut/转储). Core's own SaveFailure messages staying English matches Saves.cs and the commit message says so.

CI: the commit status on 19f89440 is success, but only Conventional Commit title ran. Compile and test was skipped: ci.yml runs it only on pushes to main or by hand, never on pull requests. So nothing compiled this head, and nothing ran scripts/translations.py or the core tests on it. By reading, the C# and XAML compile: the Tr.P(int, …) calls widen to long, Fix is { } fix resolves to the property, and the \' escapes in {c:T} follow the form the views already use.

One nit inline, which doesn't block this. Approving.

Re-review after the rebase onto main c5440787 (https://git.magicbean.dev/MagicBeanDev/LongshotLauncher/pulls/48 and https://git.magicbean.dev/MagicBeanDev/LongshotLauncher/pulls/52), head 19f89440. Read only: nothing was built or run here. **The rebase keeps both sides.** Against main, `BuildStepViewModel.cs`, `HomeViewModel.cs`, `MainViewModel.cs`, `HookshotImportViewModel.cs`, `HookshotImportView.axaml`, `Core/Problems.cs` and the tests differ only by text going through `Tr`. #48's `Known`/`Unknown`/`Memory`/`Said`/`Body`, the offer and its `TakeOffer` routing, the crash page and #52's import flows (target re-check, overwrite/keep, language carry-over, backup text) are all still there. `tests/launcher_core/Program.cs` runs both `ProblemChecks()` and `TranslationChecks()`, and `test_launcher_core.py` keeps main's checks and adds `check_translations()`. The removed "The PC ran out of memory while building" key appears nowhere in App/ or Core/ (the headline is "Your PC ran out of memory while building", which has its own key). **The new commit holds up:** - Every `Tr.N` text in `Problems.cs` is shown through `Tr.T`, in `ProblemViewModel.Head`/`Body`/`OfferLabel` and `ProblemPageViewModel.Headline`/`Detail`/`FixLabel`. Nothing else reads `Headline`, `Detail` or `Label`, and nothing is translated twice. `Tr.N` is the identity, so `ProblemChecks` and the logs still see English. - The old `{Binding Offer.Label}` and `{Binding Fix.Label}` are gone. `OfferLabel` and `FixLabel` exist on the `x:DataType` view models, so compiled bindings resolve. `xmlns:c` is on every view that uses `c:T`, including HookshotSettingsView, and no literal English is left in any .axaml (only Longshot, NVIDIA DLSS and AMD FSR 4). - All 50 new keys are in all 12 full catalogs. The `{0}`/`{1}` placeholders match the English in every catalog. The `es-419` and `fr-CA` overrides all have matching keys in es/fr. "Ocarina of Time" is kept as is in every catalog. A grep found no leftover keys. - Plurals: the forms match `PluralCategories` (fr/es/it/pt-BR one/many/other, ru/pl one/few/many/other, ja/ko/zh other). Every form shows `{0}`, so `one_number` holds, including ru's one (21 профиле) and fr/pt's one. The NX2navi `Tr.P` runs only for 2 or more profiles, but its one form is still needed for ru 21, 31 and so on. - Translations I spot-checked in fr, de, pt-BR, ru, pl, ja, zh-Hans and es-419 read well and use the terms the catalogs already use (création/bauen/сборка/ビルド/构建, Build memory, dump/zrzut/转储). Core's own `SaveFailure` messages staying English matches Saves.cs and the commit message says so. **CI:** the commit status on 19f89440 is success, but only *Conventional Commit title* ran. *Compile and test* was **skipped**: ci.yml runs it only on pushes to main or by hand, never on pull requests. So nothing compiled this head, and nothing ran `scripts/translations.py` or the core tests on it. By reading, the C# and XAML compile: the `Tr.P(int, …)` calls widen to long, `Fix is { } fix` resolves to the property, and the `\'` escapes in `{c:T}` follow the form the views already use. One nit inline, which doesn't block this. Approving.
@ -0,0 +540,4 @@
"The build needs about 45 GB free while it runs. Free up space on that drive, or build to another drive.": "La création a besoin d'environ 45 Go libres pendant qu'elle tourne. Libérez de l'espace sur ce disque, ou lancez la création sur un autre disque.",
"Build to another folder": "Créer dans un autre dossier",
"Your PC ran out of memory while building": "Votre PC a manqué de mémoire pendant la création",
"Close other programs, or set Build memory lower in step 2's Advanced, then build again: what was built so far is kept.": "Fermez d'autres programmes, ou baissez la Mémoire de création dans les options avancées de l'étape 2, puis relancez la création : ce qui a déjà été fait est conservé.",
Owner

Nit: step 2's section is labelled Avancé in this catalog ("Advanced": "Avancé"), so « dans les options avancées de l'étape 2 » doesn't name the label the player looks for. Maybe « dans « Avancé » à l'étape 2 », the way de (« unter „Erweitert“ ») and ru (« в разделе «Дополнительно» ») quote it.

Nit: step 2's section is labelled *Avancé* in this catalog (`"Advanced": "Avancé"`), so « dans les options avancées de l'étape 2 » doesn't name the label the player looks for. Maybe « dans « Avancé » à l'étape 2 », the way de (« unter „Erweitert“ ») and ru (« в разделе «Дополнительно» ») quote it.
HumanBean deleted branch feat/ui-language 2026-11-14 02:11:29 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/LongshotLauncher!47
No description provided.