Skip to content

feat: add Traditional Chinese (zh-Hant) localization - #188

Open
squall wants to merge 5 commits into
chattymin:mainfrom
squall:feat/zh-hant-localization
Open

squall wants to merge 5 commits into
chattymin:mainfrom
squall:feat/zh-hant-localization

Conversation

@squall

@squall squall commented Aug 18, 2026

Copy link
Copy Markdown

Summary

Adds Traditional Chinese (zh-Hant) as the fifth UI language, alongside ko / en / ja / es.

  • AppLanguage gains case zhHant = "zh-Hant". The raw value doubles as the persisted save format and as the displayLocale identifier, so the popover's relative dates follow the app language.
  • All 207 t(...) call sites and the 25 nature names get their zh-Hant column. The existing ko/en/ja/es columns are untouched — I verified this mechanically by re-parsing both revisions and diffing the first four arguments of every call (0 differences).
  • Nature names use the official Traditional Chinese names from the main series (勤奮 / 怕寂寞 / 固執 …).
  • Pokémon names come from PokéAPI's zh-hant data at runtime, so no name table is bundled.
  • The system-language guess matches Traditional variants only (zh-Hant, zh-TW, zh-HK, zh-MO). Simplified users (zh-Hans, zh-CN, zh-SG) keep the English fallback rather than being shown a script they did not ask for, and a bare zh is not assumed to be Traditional.
  • Adds README.zh-Hant.md, linked from the language row of the other three.

Why zh-Hant rather than zh-TW: the script subtag is region-neutral, so one entry serves TW/HK/MO; it matches PokéAPI's own zh-hant code, keeping one concept end to end; and it stays consistent with the region-neutral ko/en/ja/es entries. A future zh-HK for Hong Kong vocabulary can be added later without renaming anything. The wording itself leans Taiwan Mandarin.

One change worth a second look

PokeAPIClient.langCodes was a literal array that has to be edited by hand for every new language, and forgetting it is silent — the new language resolves no species name and quietly falls back to English, which reads like a data gap rather than a missing line of code. I changed it to derive from AppLanguage.apiCodes, which also meant widening it from private let to static let so a test can assert the invariant. The resulting set is a superset of the old list, so lookups are unchanged for the existing languages. It is an isolated commit (0904bc8) — happy to drop or split it if you would rather keep that line explicit.

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Documentation
  • Other:

UI changes

No layout changes. The Settings language picker is .menu style and driven by AppLanguage.allCases, so it gains one entry with no change to the view code. Every other screen keeps its layout with Traditional Chinese text.

Before After
Language picker: 한국어 / English / 日本語 / Español Language picker: 한국어 / English / 日本語 / Español / 繁體中文

PerformanceTests.testLocalizedAlertBubbleFitsDefaultPanel already iterates AppLanguage.allCases, so the new alert strings are verified to fit the floating pet's speech bubble without truncating.

Checklist

  • swift build and swift test pass locally — 686 tests, and ./scripts/test-gate.sh reports 89.93% line coverage on the logic core
  • PR title and description are written in English
  • UI changes are described above (before/after — images optional)
  • No copyrighted assets, secrets, or private tooling references are committed (see CONTRIBUTING)
  • Tests were added or updated for this change

On the tests

The argument count of t(...) is compiler-enforced, so a missing string cannot ship. TraditionalChineseTests covers what the compiler cannot see: the Traditional/Simplified matching table (including script tags winning over region — zh-Hant-CN is Traditional, zh-Hans-TW is not), every branch of resolve(preferredLanguages:) with the locale injected rather than read from the host machine, that the relative-time formatter produces 小時 and not 小时, that the client keeps an API code for every language, that interpolations survived translation, and script leakage (every sampled string carries Han characters and no Hangul or kana — which is what a copy-pasted column looks like). Both guards were checked by injecting the defect they are meant to catch.

squall added 4 commits August 19, 2026 07:32
Adds zh-Hant as the fifth UI language alongside ko/en/ja/es.

- AppLanguage gains `case zhHant = "zh-Hant"`. The raw value doubles as the
  persisted save format and as the `displayLocale` identifier, so relative
  dates in the popover follow the app language.
- `apiCodes` uses the lowercase PokéAPI code ("zh-hant"); the API returns
  `language.name` in lowercase, so an uppercase tag would silently fall back
  to the English species name.
- The system-language guess only matches Traditional variants
  (zh-Hant / zh-TW / zh-HK / zh-MO). Simplified users (zh-Hans / zh-CN /
  zh-SG) keep the English fallback rather than being shown a script they did
  not ask for; a bare "zh" is not assumed to be Traditional either.
- `systemDefault` now delegates to a pure `resolve(preferredLanguages:)` so
  every branch can be exercised without depending on the machine's locale.
- All 207 `t(...)` call sites and the 25 nature names get their zh-Hant
  column. The existing ko/en/ja/es columns are untouched.

The language picker in Settings is driven by `AppLanguage.allCases`, so it
picks the new entry up with no UI change.
`langCodes` was a literal list that has to be edited by hand every time a
language is added. Forgetting it is silent: the new language resolves no
species name and falls back to English, which looks like a data gap rather
than a missing line of code.

Deriving the set from `AppLanguage.apiCodes` makes the enum the single
source, matching the provider-extension convention in CONTRIBUTING.md. The
resulting set is a superset of the previous list, so lookups are unchanged
for the existing languages.
The argument count of `t(...)` is compiler-enforced, so a missing string
cannot ship. These tests cover what the compiler cannot see:

- raw value / display locale, including that the relative-time formatter
  produces Traditional ("小時") and not Simplified ("小时")
- the Traditional/Simplified matching table, including script tags winning
  over region (zh-Hant-CN is Traditional, zh-Hans-TW is not)
- every branch of `resolve(preferredLanguages:)`, injected rather than read
  from the host machine
- that the client keeps an API code for every language, iterating
  `allCases` so reverting to a literal list fails the suite
- script leakage: every sampled string carries Han characters and no
  Hangul or kana, which is what a copy-pasted column looks like
- that interpolations survived translation

Both guards were verified by injecting the defect they are meant to catch.
Adds README.zh-Hant.md and links it from the language row of the other three.
The file is named after the enum's raw value (zh-Hant) rather than a region,
so the docs and the language identifier stay in sync.

Also corrects the "KO / EN / JA" claim in the feature table: Spanish was
already shipping and was never reflected there. It now reads
KO / EN / JA / ES / ZH-Hant.
Copilot AI lite review requested due to automatic review settings August 18, 2026 23:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Traditional Chinese (zh-Hant) as a new UI language end-to-end (UI strings, language selection, system-language inference, and PokéAPI name resolution), plus targeted tests and documentation updates to ensure the new locale behaves correctly and doesn’t regress existing languages.

Changes:

  • Extends AppLanguage with zhHant (persisted raw value zh-Hant), adds Traditional-Chinese matching logic, and updates nature-name localization.
  • Expands the L.t(...) localization helper to include a zh-Hant column across UI strings.
  • Adds TraditionalChineseLocalizationTests and a new README.zh-Hant.md, and links the new README from existing language READMEs.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
Tests/PokeTokenBarTests/TraditionalChineseTests.swift Adds coverage for zh-Hant persistence/locale behavior, system-language resolution, PokéAPI code invariants, and script leakage checks.
Sources/PokeTokenBar/Core/PokeAPIClient.swift Derives supported PokéAPI language codes from AppLanguage.apiCodes (single source) and uses a Set for filtering.
Sources/PokeTokenBar/Core/Localization.swift Adds zh-Hant as the 5th argument to all localized UI strings via t(...).
Sources/PokeTokenBar/Core/CompanionModel.swift Adds zhHant, language matching (resolve, isTraditionalChinese), updated labels, and zh-Hant nature names.
README.zh-Hant.md New Traditional Chinese README.
README.md Links Traditional Chinese README and updates supported-language mention.
README.ko.md Links Traditional Chinese README and updates supported-language mention.
README.ja.md Links Traditional Chinese README and updates supported-language mention.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Sources/PokeTokenBar/Core/CompanionModel.swift Outdated
`apiCodes` listed both "zh-hant" and "zh-Hant" while the comment directly
above it states that an uppercase tag never matches a PokéAPI response. The
uppercase entry contradicted its own comment, and it was carried into
`PokeAPIClient.langCodes` as a permanently unmatchable filter key.

Keeps only the lowercase code the API actually returns, and pins it with an
assertion so the variant does not come back.
@chattymin

Copy link
Copy Markdown
Owner

Hi @squall — apologies for how long this has sat. It needs a rewrite rather than a rebase, and I'd rather tell you that clearly than leave it drifting.

This branch was written when the app had four languages: it changes t(ko, en, ja, es) to add a zhHant column. main is now at six — French landed in #185 and Brazilian Portuguese in #215 — and a German PR (#234) is approved and waiting on its own rebase. So the 176 strings here are two columns behind, and conflict across six files.

If you're still up for it, what's needed is to redo it against the current signature. Please base it on main after #234 merges rather than now, otherwise the same thing happens again — I'll comment here when that lands so you have a stable base.

The parts specific to Traditional Chinese looked good on the read-through: matching zh-Hant/TW/HK/MO while deliberately not matching Simplified is the right call, and persisting the raw value as zh-Hant rather than a bare zh will age better.

Entirely understandable if you'd rather not redo it — say so and I'll close it with thanks rather than leave it open indefinitely.

@chattymin

Copy link
Copy Markdown
Owner

Following up as promised: #234 (German) is merged, so main is a stable base now at seven languages — ko, en, ja, es, fr, pt, de.

If you take this on, t(...) needs eight columns with zh-Hant added. One thing worth knowing before you start: I rebased #234 myself rather than sending it back, so the same is on offer here — if you'd rather write only the Traditional Chinese strings and leave the mechanical column-widening to me, say so and I'll do that half.

Also carrying over the one substantive point from the automated review on this PR: the comment above AppLanguage.apiCodes says an uppercase tag won't match PokéAPI responses, but the implementation returns both zh-hant and zh-Hant. Worth settling which is true while you're rewriting it.

And the earlier offer stands — if you'd rather not redo it, just say so and I'll close it with thanks.

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.

3 participants