Skip to content

i18n: route all player-facing text through L10n - #25

Merged
ThoriaDevelopment merged 11 commits into
ThoriaDevelopment:mainfrom
Rgxaa:feature/i18n
Nov 1, 2026
Merged

ThoriaDevelopment merged 11 commits into
ThoriaDevelopment:mainfrom
Rgxaa:feature/i18n

Conversation

@Rgxaa

@Rgxaa Rgxaa commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Every player-visible string — chat alerts, command feedback, config screens, HUD, nametags — was hardcoded English, so the mod spoke English whatever the client language was. All of it now goes through dev.iustitia.i18n.L10n, which landed as the pattern in #43; this PR is the migration on top of it plus the en_us.json key set.

Check ids, flag labels, /ius command names and config keys stay raw identifiers: the live-test assertions and documented commands rely on them.

Changes

  • Migrated call sites: alerts, /ius command feedback, config (YACL) screens, HUD, nametags, flags/tiers, keybind hub, info screens, replay/clip/session/UI screens.
  • en_us.json 36 → 668 keys. zh_cn.json added at full key parity (668/668, 0 drift either way).
  • verify_contribution.py --static extended: runtime-assembled keys are now pinned — check descriptions from checks.json, preset label/blurb keys, keybind ids, and every Tier constant. That is what lets Keybinds drop its hardcoded English fallback instead of silently showing English.
  • L10n.formatEntry split out of L10n.s so the fail-open formatting path is reachable from a pure-JVM test; new L10nTest asserts the key-miss sentinel, argument substitution, a malformed % spec, a missing argument, and %%.
  • Comments on the two places localized prose leaves the client (Snapshot report JSON tier, .iusclip alert label).

Affected areas

  • Detection/check logic
  • Packet observation or inference
  • Config or presets
  • Commands or UI
  • Rendering or HUD
  • Replay, clips, camera, or input
  • Persistence or local data
  • Localization/translation strings
  • Documentation/tests/tooling

Verification

  • python scripts/verify_contribution.py --static: PASS on this branch (warnings only: no build/selftest-manifest.json, and key.category.iustitia.keybinds is referenced from Keybinds rather than an L10n call).
  • ./gradlew test: PASS, including the 5 new L10n tests.
  • ./gradlew build: PASS in CI at this head (see checks).
  • Live client (run by the contributor, not by the maintainer): client launched with zh_cn and with an unsupported language; /ius commands, config screen, HUD, alerts, tier labels and the keybind hub follow the client language, with English fallback, and identifiers stayed raw. No screenshot, recording or log was attached, so this is a report rather than evidence. It is not repeatable from this PR as it stands.
  • /ius debugfps for the uncached per-frame lookup: NOT RUN. No measurement exists. The argument in the thread (Language.get ≈ 12 ns vs String.format 140–260 ns, and the per-frame HUD lines are already tick-cached by hud: resolve crosshair target once per frame and cache panel model pe… #42) is an argument, not a number.

Known limitations

  • zh_cn.json is machine-translated (AI pipeline, nine reflection rounds, 277 strings rewritten) and has not been proofread by a native speaker. Merging this ships unreviewed translation. en_us.json is the reviewed source of truth; a wrong zh_cn string is a text-only defect, but it is still unreviewed text in users' clients.
  • No debugfps measurement for the uncached lookup (above).
  • Localized prose is now baked into exported artifacts: a report exported on a zh_cn client carries a Chinese tier, and a .iusclip carries the alert labels of the client that recorded it. checkId stays raw in both, so nothing that matches identifiers breaks.
  • Any new player-visible string needs an en_us.json key. A missed key fails open to the key itself and --static catches it before release.

AI assistance

  • Tools: DeepSeek V4.1 Flash (1017) driven by an agent pipeline for the zh_cn translation; AI assistance for the call-site migration.
  • What it contributed: implementation and translation.
  • What was reviewed: the contributor reviewed the logic, the reference code and the final diff, and launched the client. The maintainer reviewed the diff hunk by hunk, added the L10n fail-open tests and the export-field notes, and recorded the untested items above as untested.

Every player-visible string — chat alerts, command feedback, config
screens, HUD, nametags — was hardcoded English; it now goes through
dev.iustitia.i18n.L10n and lives in lang/en_us.json. Check ids, flag
labels, /ius command names and config keys stay raw identifiers: the
live-test assertions and documented commands rely on them.

L10n fails open: an unknown key renders as the key itself, a bad % spec
degrades to the entry's raw text instead of vanilla's "Format error:", and
neither can throw into a render or tick path. Lookup is uncached on purpose
— Language is swapped on a language change, and every caller, including the
per-frame HUD lines, must see the new language immediately.

en_us.json is the source of truth; 23 translations sit beside it
(machine-generated, not yet proofread by native speakers).
verify_contribution.py --static gained check_language_assets — key coverage,
per-language key parity, duplicate keys, and % / § spec drift — the only
place a missing key can be caught before release, since L10n would
otherwise just show the key.
@ThoriaDevelopment

Copy link
Copy Markdown
Owner

Thanks for taking this on. The shape of it makes sense to me: one chokepoint in L10n, fail open on missing keys and bad format specs, identifiers staying raw so the live tests and documented commands keep working. The verifier addition for key parity and placeholder drift is the right guard, and I am glad it is there.

I cannot merge it at this size with machine translations included. Twenty-three unproofread languages plus every screen touched in one PR is more than I can review with confidence, and the translations would ship looking fluent when nobody has checked them.

Here is what I would like instead.

First rebase past #22, since you both touch Keybinds.kt, en_us.json, and the hub screen.
Then split the work: one small PR with L10n, the verifier check, en_us.json, and a few migrated call sites so I can review the pattern, followed by the rest of the migration.
Keep the machine translations out of the merge path until native speakers have looked at them, or mark them clearly as drafts so nobody mistakes them for finished work.

One more thing to check on your side: the uncached lookup on per-frame HUD lines. That has potential to cost framerate on busy servers, so please measure it by using /ius debugfps and report what you find before the migration PR.

@ThoriaDevelopment

Copy link
Copy Markdown
Owner

I've pushed a merge resolution to your branch to clear the conflict with #22 (now in main), so you don't need to rebase. It keeps your L10n calls everywhere, keeps both the new keybind category and your KDoc in Keybinds.kt , adds the new key.category.iustitia.keybinds key to all 23 translations (proper noun, same value), and updates the en_us hub hint to point at the new Iustitia category. Verifier passes on the merged tree. One thing I left for you: the translated hint sentences still say the old category name.
I'm still holding the PR itself: it's too big to review as one unit with 23 unproofread machine translations inside. Please split it (pattern PR with L10n + verifier + en_us first, migration after), keep machine translations out of the merge path until reviewed, and measure the per-frame HUD lookup cost with /ius debugfps before the migration PR.

The merge that resolved ThoriaDevelopment#22 added key.category.iustitia.keybinds to all 23
translations and rewrote the en_us hub hint, but left the translated hints
naming the old generic category, so every non-English client was told to look
under "Miscellaneous" / "杂项" / "Sonstiges" -- a menu entry that no longer
exists for these binds.

Replaces the final segment of the Options -> Controls -> <category> path in
iustitia.screen.keybinds.hint with the literal "Iustitia" (a proper noun, so
it is not translated) across all 23 files. Menu wording, colour codes and
placeholders are untouched; the § and % counts still match en_us, which is
what check_language_assets asserts.
@Rgxaa

Rgxaa commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

This might be a translation issue—the earlier description isn’t quite accurate. Those 23 language files were actually translated by AI, not by Google-Translate-style mechanical translation. That said, AI translations still aren’t as natural as native-speaker translations. For now, I plan to keep only the en_us.json source file and the zh_cn.json file, which will be translated and reviewed by native Chinese speakers. (It hasn’t been done yet, but it’s at the top of my list.)

@ThoriaDevelopment

Copy link
Copy Markdown
Owner

Thanks for trimming this to en_us plus zh_cn. That helps.
I still can't take it as one PR. L10n plus the verifier check plus en_us is the pattern I need to review first, then the migration of the call sites after that. The zh_cn file also still reads as unreviewed by your own note, so I won't merge it until a native speaker has signed off.
Two things that would unblock the first PR: put up the small pattern one, and report the /ius debugfps number for the HUD lines so we know the uncached lookup is fine. I can review that fast.

@Rgxaa

Rgxaa commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor Author

For me, all public holidays are already over, and I most likely won’t have much time to deal with these things. Yes, I’m still busy with my studies. I think this is an interesting mod, and I really like it. You may need to handle these things, or someone else may need to help. To be honest, given the size of the mod’s user base, probably no one will carefully proofread every translation, or we can just leave it as it is. Of course, I’m not blaming anyone—after all, this has only just begun. I’m sorry for leaving a mess behind. Due to time constraints, here’s a somewhat informal report: the flight check, or the landing/on-ground state check, has some issues. It may be a bit hard to describe. I wanted to record a video to show you, but I can’t. You can try making the second account sneak at the edge of the void, and you’ll find it triggering fly(B) like crazy. To be more specific, when half of the hitbox is over the void, it gets flagged as fly(B). It seems to be because it doesn’t take the player’s hitbox into account and only uses the center coordinates. There’s also a minor issue: the speed check reports many tiny VLs, almost every second, but they don’t accumulate; they just stay at 1 over time. However, this still shows that there are some subtle discrepancies in the speed check that could be optimized

(English is for AI translation.)

ThoriaDevelopment pushed a commit that referenced this pull request Oct 12, 2026
)

First, reviewable slice of #25 as requested in its review: the L10n
chokepoint, the verify_contribution.py language-asset check, en_us.json
keys, and four migrated call sites (HudOverlay, PlayerSearchScreen,
TranscriptPanelScreen, WatchState). No translations are included.

The verifier's generated-key pins (check descriptions, presets, keybind
hub rows, tier labels) are left for the migration PR, since those call
sites are not on L10n yet.

Co-authored-by: Rgxaa <169570958+Rgxaa@users.noreply.github.com>
Rgxaa added 2 commits October 15, 2026 23:20
…lopment#37/ThoriaDevelopment#32/ThoriaDevelopment#45 conflicts)

- HudOverlay: adopt main's per-tick PanelModel architecture (ThoriaDevelopment#42); L10n
  calls already live in panelModel() from the auto-merge.
- YaclScreenBuilder: keep ThresholdBounds-aware thresholdOption (ThoriaDevelopment#32),
  add the creative-flight-exempt option via new L10n keys (ThoriaDevelopment#37).
- IustitiaCommand: keep L10n subcommand rows; update en_us presets /
  threshold texts to match ThoriaDevelopment#45/ThoriaDevelopment#23.
- verify_contribution: keep the generated-key pins (check descriptions,
  presets, keybind binds, tier labels) now that those call sites are on
  L10n.
- CONTRIBUTING: keep the full-migration wording of the user-facing-text
  section.
- lang: add iustitia.cfg.option.creativeFlightExempt.* to en_us + zh_cn.
The KDoc's "Lookup is deliberately not cached" read as if each frame
re-loads translations, which invited performance concerns about the HUD
lines. The lookup is just vanilla's Language hashmap (~12ns, the same
path Text.translatable uses); what goes un-memoized is only the
String.format result (~0.2us). Skipping it keeps a runtime language
switch instant for every caller and leaves no stale cache to invalidate.
Hot HUD lines are already built once per client tick.
@Rgxaa

Rgxaa commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

I dug into what the "uncached" claim actually refers to.

L10n resolves every string through vanilla's own Language.getInstance().get(key), the same lookup Text.translatable uses. Vanilla's caching has two layers. The first is the key-to-string hashmap built once at resource reload; we get that for free, about 12 ns per lookup, and language files are never re-read at runtime. The second is the memoized formatted result inside TranslatableTextContent, the only layer L10n skips; vanilla keeps it fresh by comparing the Language reference.

We skip that layer on purpose, because for this mod's text it would lose more than it saves. Most call sites are one-shot: command feedback, chat alerts, config screens. The formatted string is used once and discarded, so a memo entry would be written and never read. The strings also carry new variables nearly every time (player names, VLs, scores), so the hit rate would be low anyway. The lines that actually render every frame are already cached one level up by the per-tick panel model in #42, so they don't format per frame at all. Skipping the memo also removes the stale-cache failure mode vanilla has hit before: MC-162441, signs that don't update after a language switch.

Numbers: Language.get ≈ 12 ns, String.format ≈ 140–260 ns. Outside the tick-cached panel, the per-frame HUD path only formats the lag icon and replay status, at most a couple of calls per frame, well under 0.01% of the frame budget.

On the report: I haven't produced a meaningful /ius debugfps run yet. I'm not sure how to get the most out of the command, and I don't have a busy server like the one you mentioned to reproduce that scenario on. The code paths above don't suggest a performance problem, but that's an argument, not a measurement. If you tell me what you'd look for in the output, I'll run it and post what I get.

Rgxaa added 2 commits October 16, 2026 00:44
The share strings from ThoriaDevelopment#45 were hardcoded English: decode/schema
failures and the /ius presets export/import feedback. Added 32 keys
to en_us and zh_cn. The tests assert on the key now, because L10n
returns the key when no language is loaded.
The earlier zh translation was confusing; at the very least I could not
tell what it was saying. That is fixed now.

Over the past two weeks I built an agent pipeline for it. In total it ran
for 231.5 hours, used 5,987,059,713 tokens, took 71,681 steps, made
100,968 tool calls and spawned 5,540 subagent sessions; the session files
alone take up 1.6 GB.

Roughly, the loop works like this: take one string to translate with its
en_us value and its current zh_cn value (plus any pending change and
reason), put it behind a long hand-written prompt template, and start a
fresh session that:

1. finds the string in the source and works out what it means from the
   code around it;
2. cross-checks the Minecraft-Mod-Language-Modpack 1.21 community
   translation corpus to match the wording I already expect in Chinese
   Minecraft modding;
3. argues the two sides against each other until it lands on one answer;
4. writes the change and the reason for it into a JSON file on my disk.

It then walks from key 1 to key 668 and repeats, nine rounds in all, until
the file stops changing. 277 of the 668 strings ended up rewritten, 554
diff lines in total, one deletion plus one insertion each. Round 1
produced 500+ changes, the count fell every round after that, and the
smallest round had 2.

The model was DeepSeek V4.1 Flash 1017, the strongest open model right
now, and as far as I know it has more Chinese training data, so it does
better in Chinese. (I am not advertising it, just making clear that I did
not use a junk model.)

The same workflow can be pointed at the other language files, though each
one still takes a lot of time and tokens to grind through. I can provide
it if needed.
@Rgxaa

Rgxaa commented Oct 25, 2026

Copy link
Copy Markdown
Contributor Author

@ThoriaDevelopment

The earlier zh translation was confusing; at the very least I could not tell what it was saying. That is fixed now.

Over the past two weeks I built an agent pipeline for it. In total it ran for 231.5 hours, used 5,987,059,713 tokens, took 71,681 steps, made 100,968 tool calls and spawned 5,540 subagent sessions; the session files alone take up 1.6 GB.🤯🤯🤯

Roughly, the loop works like this: take one string to translate with its en_us value and its current zh_cn value (plus any pending change and reason), put it behind a long hand-written prompt template, and start a fresh session that:

  1. finds the string in the source and works out what it means from the code around it;
  2. cross-checks the Minecraft-Mod-Language-Modpack 1.21 community translation corpus to match the wording I already expect in Chinese Minecraft modding;
  3. argues the two sides against each other until it lands on one answer;
  4. writes the change and the reason for it into a JSON file on my disk.

It then walks from key 1 to key 668 and repeats, nine rounds in all, until the file stops changing. 277 of the 668 strings ended up rewritten, 554 diff lines in total, one deletion plus one insertion each. Round 1 produced 500+ changes, the count fell every round after that, and the smallest round had 2.

The model was DeepSeek V4.1 Flash 1017, the strongest open model right now, and as far as I know it has more Chinese training data, so it does better in Chinese. (I am not AD it, just making clear that I did not use a * model.)

The same workflow can be pointed at the other language files, though each one still takes a lot of time and tokens to grind through. I can provide it if needed.

The migration's safety argument rests entirely on L10n degrading instead of
throwing, and nothing tested it: a pure-JVM test never loads a language file,
so every key comes back as the key and the formatting branch was unreachable.

Extract formatEntry() from s() so the % spec path can be asserted directly, and
add L10nTest covering the key-miss sentinel, substitution, a malformed spec, a
missing argument and %%. Behaviour is unchanged; s() still returns the key when
the lookup itself throws and the raw entry when formatting fails.

Also record where localized prose now leaves the client: the report JSON tier
and the .iusclip alert label. checkId stays the raw identifier in both.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
@ThoriaDevelopment

Copy link
Copy Markdown
Owner

Maintainer note: I pushed 6fd8279 to feature/i18n to close the review gaps that were blocking this, rather than asking for another round.

What it adds: an extracted L10n.formatEntry plus L10nTest (the fail-open path had no test — a unit test never loads a language file, so the % branch was unreachable), and comments where localized prose now reaches exported files (report JSON tier, .iusclip alert label).

I also rewrote the description, which still claimed 23 shipped translations after they were removed, and recorded /ius debugfps as NOT RUN with the reasoning from the thread, instead of leaving it implied as verified.

Still open, and I am making the call on both: zh_cn.json is machine-translated and has no native-speaker sign-off (my earlier condition), and there is still no debugfps number. Neither is a blocker I am willing to keep the migration hostage to — but they ship as known limitations, explicitly listed.

@ThoriaDevelopment
ThoriaDevelopment merged commit 5fd30dd into ThoriaDevelopment:main Nov 1, 2026
2 checks passed
@Rgxaa
Rgxaa deleted the feature/i18n branch November 2, 2026 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants