feat(ui): add launcher language selection and translations #47
No reviewers
Labels
No labels
area/build
area/install
area/saves
area/ui
area/updates
good first issue
gpu/amd
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
platform/linux
platform/steam-deck
platform/windows
priority
critical
priority
high
priority
low
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/LongshotLauncher!47
Loading…
Reference in a new issue
No description provided.
Delete branch "TrashPanda/LongshotLauncher:feat/ui-language"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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.jsonasuiLanguage.LANGUAGE/LC_ALL/LC_MESSAGES/LANGon Linux,GetUserDefaultLocaleNameon Windows (the publish usesInvariantGlobalization, so .NET's culture doesn't report it). It falls back to English.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.{c:T 'English text'}; view models useTr.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.pyfinds 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
uiLanguageunset,LANG=ja_JP.UTF-8andLANG=pl_PL.UTF-8each 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 byTranslationChecks, not checked by eye on every screen.dotnet build LongshotLauncher.slnx -c Release -p:RestoreLockedMode=true(0 warnings),tests/test_launcher_core.py --strict --longshot …(799 checks, including the newTranslationChecks),tests/test_release.py,scripts/translations.py(471 texts, 15 catalogs, all complete).GetUserDefaultLocaleNamepath 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>.jsonand runpython3 -I scripts/translations.py.Screenshots
Settings in German, with the new Launcher language picker
Settings in Japanese
Home in Polish, picked from LANG=pl_PL.UTF-8 on first start
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]>b40ae1328f93eab1b790Wording choices worth a native speaker's look. Each is in
Core/Lang/<code>.json.French / Italian
Polish / Russian
Review: LongshotLauncher #47, launcher language selection and translations
#47 (head
93eab1b7, on top of current main0e6e1d7, 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.csis 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/Pread an array reference thatUseswaps in one step, so build-thread calls are safe. A translation whose placeholders don't fit falls back to the English instead of throwing aFormatException.{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.pychecks the same thing, andtests/test_launcher_core.pyruns it, so a broken placeholder fails the tests.Tr.PickandFromEnvironmentare well covered (de_AT.UTF-8,zh-Hant-TW,C.UTF-8,LANGUAGElists, precedence).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, andcheck_licencesstill finds the COMMIT.InvariantGlobalizationis 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 saveduiLanguageis normalised throughTr.Pick, so an unknown value means "none picked". A change applies live, without a restart.Must change
Core/Lang/ja.json:390"Ocarina of Time" becomes 「時のオカリナ」. InCore/Lang/ko.json:390it 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
oneform for 0 as well as 1, but 13 of pt-BR'soneforms hard-code "1" instead of{0}(e.g.Core/Lang/pt-BR.json:143"1 slot de salvamento"). The Saves page'sSlotCount(SavesViewModel.cs:79) is shown with zero slots, so a player with no saves reads "1 slot de salvamento". Use{0}in pt-BR'soneforms. fr already does this, and ru and pl use{0}in theirs.translations.pyshould also require{0}in any category that covers more than one number (fr/ptone; ru/plone, which covers 21, 31 and so on).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". UseGetUserPreferredUILanguages(orGetUserDefaultUILanguage+LCIDToLocaleName). The PR says the Windows path is untested, so this needs a Windows run either way.check_translationsintests/test_launcher_core.pyonly 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.Languagesbuilds 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."{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.why, which can now be a translated launcher message ("The build tools could not start: …"), while the README says the report stays English.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);GetUserDefaultLocaleNamereturns 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(orGetUserDefaultUILanguage+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")) }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 回だけです。",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분 걸립니다.",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",In CLDR, Portuguese uses
onefor 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 13oneforms with a hard-coded "1"; please use{0}in all of them, as fr does.translations.pycould require{0}in any category that covers more than one number (fr/ptone, ru/plone).@ -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);Nit:
"{0} {1}"/"{0} {1} {2}"as the date-order keys are generic. Since the key is the English text, any laterTr.F("{0} {1}", ...)would get the date's translation (ja{1}{0}日). A distinctive English key, or an@note for translators, would avoid that.- 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]>Fixes for HumanBean's review (request changes, review 1138), pushed on top of your head as
dc2029a5(fast-forward from93eab1b7, your commit untouched):Core/Lang/ja.jsonandCore/Lang/ko.jsonnow keep "Ocarina of Time" as written; ko no longer adds "Zelda" (내가 가진 Ocarina of Time을, jaお持ちの Ocarina of Time を).oneforms that hard-coded "1" now use{0}(the review counted 13; "1 processo" was the 14th).scripts/translations.pynow fails a plural form without{0}when its category holds more than one number: every category exceptonein languages whoseoneis exactly 1. So fr and ptone(0 and 1) and ruone(21, 31...) need{0}. pl'soneis 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.SystemLanguage.Name()(App/Controls/Translate.cs) now callsGetUserPreferredUILanguages(MUI_LANGUAGE_NAME)and returns the first display language in the user's list that the launcher has (viaTr.Pick), the same wayLANGUAGElists are read on Linux, instead of the regional-format locale. This matches the README's "Windows' display language". It still needs a Windows run.check_translationsonly 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 translatedwhyin the bug-report copy, CJK fonts on Windows.Nothing was built or tested here: no
dotnet build, notests/test_launcher_core.py, noscripts/translations.pyrun. The test boxes are yours: please run the checks and try the Windows path.Re-review of the fix commit
dc2029a5only, 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.oneforms: fixed. All 14oneforms now use{0}, and none of the forms in fr, fr-CA, ru, pl or pt-BR is left without the count. The newone_number()rule intranslations.pymatchesTr.PluralOf. fr's and pt'sonecovers 0 and 1, and ru's covers 1, 21, 31 and so on, so those must show{0}. pl'sonereally is only 1, so letting it write the 1 out is correct (our earlier review was wrong to group pl with ru).few,manyandothermust always show{0}, and the check sits in theelifafter the placeholder-set check, so a form that already fails that check doesn't get a second fault.DWORD,PULONG,PZZWSTR,PULONG,BOOLmarshalled asBool, Unicode). The size query with a null buffer, then the fill, is the documented pattern. The double-NUL list is cut tosizeand split withRemoveEmptyEntries, so the final terminator gives no empty name. Taking the first preferred language the launcher has, throughTr.Pick, is a good use of the list.UiLanguageOrrunsPickagain, which is harmless because an exact code inAllreturns itself. This still needs a run on Windows (an English display language with a German region format should start in English).GetUserDefaultLocaleNamefor Windows. Please update both before merge.Not run: per instructions, this is a code read only.
dc2029a57819f8944072New commits pushed, approval review dismissed automatically according to repository settings
Rebased onto current
main(c5440787, which has #48, #51 and #52) and force-pushed: the head is now19f89440. 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'sKnown/Unknown/Memory/Said/Body.Headfalls back toTr.T("make_game.py stopped"), andSaidis the PR's existingTr.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: bothProblemChecks()andTranslationChecks()are registered.tests/test_launcher_core.py: bothcheck_save_user(#52) andcheck_translations(PR) are kept, andmain()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 lowercasesSaveText.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 layerCore/Problems.cs: headlines, details and fix labels are markedTr.Nand stay English in theProblemrecord, so the signature, the tests and bug reports are unchanged. The screens show them withTr.T, which also follows a language change.ProblemViewModel.OfferLabelandProblemPageViewModel.FixLabelreplace theOffer.Label/Fix.Labelbindings. The crash page'sSub, What you can do and Close are translated too.Tr.P, so the English reads "Found saves in 3 profiles in …".HookshotImportViewModel/SavesViewModel: the two "Longshot's save location changed…" messages.HookshotSettingsViewModelandHookshotSettingsView: everything. The skipped-options count isTr.P, so a single skipped option now reads "1 Hookshot option is…". The language name goes throughTr.T(Languages.NameOf(…)). The toast and the backup note are whole sentences (Tr.F,Tr.Sentences).{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).SaveFailuremessages thrown inside Core (Core/HookshotSettings.cs), the same asCore/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, notests/run_all.py, noscripts/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.Re-review after the rebase onto main
c5440787(#48 and #52), head19f89440. 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.csand the tests differ only by text going throughTr. #48'sKnown/Unknown/Memory/Said/Body, the offer and itsTakeOfferrouting, 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.csruns bothProblemChecks()andTranslationChecks(), andtest_launcher_core.pykeeps main's checks and addscheck_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:
Tr.Ntext inProblems.csis shown throughTr.T, inProblemViewModel.Head/Body/OfferLabelandProblemPageViewModel.Headline/Detail/FixLabel. Nothing else readsHeadline,DetailorLabel, and nothing is translated twice.Tr.Nis the identity, soProblemChecksand the logs still see English.{Binding Offer.Label}and{Binding Fix.Label}are gone.OfferLabelandFixLabelexist on thex:DataTypeview models, so compiled bindings resolve.xmlns:cis on every view that usesc:T, including HookshotSettingsView, and no literal English is left in any .axaml (only Longshot, NVIDIA DLSS and AMD FSR 4).{0}/{1}placeholders match the English in every catalog. Thees-419andfr-CAoverrides all have matching keys in es/fr. "Ocarina of Time" is kept as is in every catalog. A grep found no leftover keys.PluralCategories(fr/es/it/pt-BR one/many/other, ru/pl one/few/many/other, ja/ko/zh other). Every form shows{0}, soone_numberholds, including ru's one (21 профиле) and fr/pt's one. The NX2naviTr.Pruns only for 2 or more profiles, but its one form is still needed for ru 21, 31 and so on.SaveFailuremessages staying English matches Saves.cs and the commit message says so.CI: the commit status on
19f89440is 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 ranscripts/translations.pyor the core tests on it. By reading, the C# and XAML compile: theTr.P(int, …)calls widen to long,Fix is { } fixresolves 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é.",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.