Skip to content

feat(l10n): add Simplified and Traditional Chinese localizations - #419

Merged
sozercan merged 2 commits into
sozercan:mainfrom
senshinya:feat/chinese-localization
Aug 7, 2026
Merged

feat(l10n): add Simplified and Traditional Chinese localizations#419
sozercan merged 2 commits into
sozercan:mainfrom
senshinya:feat/chinese-localization

Conversation

@senshinya

Copy link
Copy Markdown
Contributor

Description

Adds Simplified and Traditional Chinese UI localizations (zh-Hans, zh-Hant), bringing Kaset to 17 languages.

Both variants are translated from the English source independently. Traditional Chinese is not a simplified-to-traditional character conversion — that approach produces character-correct but lexically wrong output (软件 → 軟件 rather than 軟體, 视频 → 視頻 rather than 影片).

Why zh-Hans/zh-Hant rather than zh-CN/zh-TW

This is the one naming decision worth flagging, since every existing locale here is a plain ISO 639-1 code.

ISO 639-1 cannot express this distinction: zh alone would mean shipping one Chinese to every Chinese user regardless of script. So the two-letter pattern has to break either way — zh-CN is not two letters either.

Given that, this PR uses script codes because:

  • They describe what actually differs between the two files. The split is script, not regional policy. One traditional bundle serves TW/HK/MO, and zh-Hant describes it honestly for all three; zh-TW would label the same content as Taiwan-specific.
  • It matches the convention among independently-developed native macOS apps. Cross-platform ports (Electron, Chromium, Office) use zh_CN/zh_TW inherited from their own localization pipelines, and Apple's first-party bundles use the legacy underscore-region form throughout (en_AU.lproj and friends), not just for Chinese.

Verified as functionally equivalent, so this is convention rather than correctness: Bundle.preferredLocalizations resolves zh-Hans-CN, zh-Hant-TW, zh-Hant-HK, zh-Hans-SG and zh-Hant-MO correctly under either scheme, with no fallback to English. Happy to rename if you prefer region codes.

zh-Hant follows Taiwan usage. CLDR expands the bare tag to zh-Hant-TW, and YouTube's own zh-Hant responses use Taiwan vocabulary (播放清單), so the UI strings stay consistent with the API-sourced content rendered beside them. Hong Kong (zh-HK) is a separate Apple localization and is deliberately out of scope; adding it later is purely additive.

apiLanguageCode needs no special case

ContentLanguage.apiLanguageCode feeds the InnerTube hl parameter, and YouTube's hl vocabulary is not obviously the same as Apple's localization identifiers — so this looked like it would need the first-ever divergence between the bundle code and the API code.

It does not. Probing browse FEmusic_home with each code and counting simplified-only versus traditional-only characters:

hl traditional-only simplified-only result
zh-Hans 0 186 correct simplified
zh-CN 0 287 correct simplified
zh-Hant 476 10 correct traditional
zh-TW 478 9 correct traditional

YouTube accepts both codes and scripts them correctly, so apiLanguageCode keeps returning languageCode unchanged, exactly like the other 16 locales. A test pins this so the divergence is not "helpfully" added back later.

AI Prompt (Optional)

🤖 AI Prompt Used
I'm planning two upstream PRs. The second is Chinese localization — Simplified
plus Traditional. For Traditional, should it be zh-TW or zh-HK?

[After investigation established zh-Hans/zh-Hant and Taiwan usage for zh-Hant:]

Produce the translations yourself and I'll review them. One PR covering both
variants. Verify the hl mapping against the real API before writing it into
production code, and add the flag to api-explorer rather than writing a
throwaway script.

The locale-identifier question in the opening prompt was answered by investigation during the work, not assumed: the first hypothesis (that hl would need zh-CN/zh-TW while bundles used zh-Hans/zh-Hant) was tested against the live API and disproved, which removed a whole layer of planned complexity.

Translations were AI-drafted against a fixed glossary, then mechanically checked (placeholder parity, script purity, cross-key collisions) and reviewed by a native speaker in the running app before submission.

AI Tool: Claude Code

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to change)
  • 📚 Documentation update
  • 🎨 UI/UX improvement
  • ♻️ Refactoring (no functional changes)
  • 🧪 Test update
  • 🔧 Build/CI configuration

Related Issues

None open for Chinese localization.

Changes Made

  • Localizable.xcstrings — 564 keys × 2 locales, matching the set every other locale translates
  • zh-Hans.lproj/ and zh-Hant.lproj/ — regenerated from the catalog, not hand-edited
  • SettingsManager.ContentLanguage.simplifiedChinese / .traditionalChinese, displaying as 简体中文 / 繁體中文
  • Package.swift — registers both .lproj resources
  • Sources/APIExplorer/main.swift — adds --hl <code> to override the InnerTube hl parameter, so response localization can be probed directly instead of inferred. This is what produced the table above, and follows AGENTS.md's "improve the API Explorer, don't write one-off scripts" rule
  • AppLocalizationTests — Smart Shuffle entries for both locales, plus a test asserting script-appropriate vocabulary (播放列表/播放清單, 搜索/搜尋, 视频/影片, 登录/登入). Asserting on wording rather than character shape means a machine-converted or swapped file fails; a character-shape check would pass one
  • SettingsManagerTests — extends the locale-code ordering test and pins apiLanguageCode
  • README.md, docs/adr/0013-localization-strategy.md — locale list, and corrects the "ISO 639-1" wording that zh-Hans/zh-Hant do not satisfy

Glossary

Terms where Simplified and Traditional genuinely diverge, listed so these choices can be challenged directly rather than reverse-engineered from the diff:

English 简体 繁體
Search 搜索 搜尋
Settings 设置 設定
Video 视频 影片
Sign In / Sign Out 登录 / 退出登录 登入 / 登出
Default 默认 預設
Preset 预设 預設集
Cache 缓存 快取
Playlist 播放列表 播放清單
Queue 队列 待播清單
Equalizer 均衡器 等化器
Picture in Picture 画中画 子母畫面
Comments 评论 留言
Smart 智能 智慧
Genres 流派 曲風
Extension 扩展 擴充功能
Quality 质量 品質
Podcast 播客 Podcast

Judgment calls worth a second opinion: Library → 音乐库/音樂庫 (rather than Apple Music's 资料库); Collection → 我的内容/我的內容 (「收藏」collided with Favorites); Scrobbling left untranslated, per Last.fm convention.

Testing

  • Unit tests pass — swift test --skip KasetUITests, 2950 tests in 231 suites
  • Manual testing performed
  • UI tested on macOS 26+ — see note below

Mechanical checks beyond the suite:

  • 564/564 keys covered per locale, no missing and no extra
  • Placeholder parity (%lld, %@, %d) exact against every source string
  • Script purity: no traditional characters in zh-Hans, no simplified in zh-Hant
  • .lproj files re-read with plutil and compared entry-by-entry against the catalog
  • Cross-key collisions reviewed; the remaining ones are legitimate (Chinese has no plural inflection, so Album/Albums and Song/Songs correctly collapse)

Manual pass on a packaged build, both variants, reviewed by a native speaker. Only 51 of 564 strings are wider than their English source (Traditional is the worst case, e.g. 待播清單 for Queue), and the settings panes, player bar, and queue panel were checked specifically for truncation.

Signed-in surfaces (Library, Playlists, Subscriptions) were not visually checked: a locally built ad-hoc-signed bundle cannot reach the Keychain cookie path. Their strings are covered by the automated parity and vocabulary tests.

Checklist

  • My code follows the project's style guidelines
  • I have run swiftlint --strict && swiftformat .
  • I have added tests that prove my fix/feature works
  • New and existing unit tests pass locally
  • I have updated documentation if needed
  • I have checked for any performance implications
  • My changes generate no new warnings

Additional Notes

The diff is large (~7900 added lines) but almost entirely generated: the catalog and the two .lproj mirrors account for ~7900 of it. Hand-written code is about 60 lines across SettingsManager, Package.swift, api-explorer, and the tests.

The catalog diff is pure insertion with zero deletions — existing key order and all 15 current locales are untouched.

senshinya and others added 2 commits August 6, 2026 22:49
Adds zh-Hans and zh-Hant, bringing the UI to 17 languages. Each variant is
translated from the English source rather than character-converted from the
other, since conversion produces character-correct but lexically wrong output
(软件 -> 軟件 instead of 軟體, 视频 -> 視頻 instead of 影片).

Chinese is identified by script rather than region because ISO 639-1 cannot
express the distinction at all: bare `zh` would ship one Chinese to every
Chinese user regardless of script. Script codes also describe what actually
differs between the two files, so one traditional bundle serves TW/HK/MO
without labelling itself Taiwan-specific. Verified equivalent to region codes:
Bundle.preferredLocalizations resolves zh-Hans-CN, zh-Hant-TW, zh-Hant-HK,
zh-Hans-SG and zh-Hant-MO correctly either way, with no fallback to English.

zh-Hant follows Taiwan usage, matching both CLDR's expansion of the bare tag
and YouTube's own zh-Hant responses, so UI strings stay consistent with the
API-sourced content beside them. zh-HK is a separate Apple localization and is
left out; adding it later is purely additive.

apiLanguageCode needs no Chinese special case. Probing browse FEmusic_home
with each code showed YouTube accepts zh-Hans and zh-Hant and scripts the
responses correctly, so the bundle code passes through unchanged as it does
for every other locale. A test pins this. The probe needed an `--hl` override,
added to api-explorer rather than scripted ad hoc.

Tests assert script-appropriate vocabulary rather than character shape, so a
machine-converted or swapped file fails where a shape check would pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Sertac Ozercan <sozercan@gmail.com>

@sozercan sozercan left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

thanks! lgtm

@sozercan
sozercan merged commit 0576fd5 into sozercan:main Aug 7, 2026
8 of 9 checks passed
Yoddikko pushed a commit to Yoddikko/kasetPlus that referenced this pull request Aug 27, 2026
…ercan#419)

Adapted for the fork. Our catalog already carried zh-Hans/zh-Hant
translations (via Crowdin), but the .lproj mirrors were unregistered, there
was no ContentLanguage case to select Chinese, and no apiRegionCode mapping.
This ports the still-missing pieces from upstream sozercan#419:

- register both zh mirrors in Package.swift (were never shipped in SwiftPM builds)
- add .simplifiedChinese/.traditionalChinese ContentLanguage cases (region CN/TW)
- regenerate zh-Hans/zh-Hant .lproj from our catalog via Scripts/sync-lproj.py
- port the APIExplorer `--hl` InnerTube language probe flag

README language-count line skipped: the fork tracks languages via Crowdin
badges, not a bullet list. Cherry-picked/adapted from upstream 0576fd5.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Yoddikko added a commit to Yoddikko/kasetPlus that referenced this pull request Aug 27, 2026
…upstream ledger

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Yoddikko added a commit to Yoddikko/kasetPlus that referenced this pull request Aug 27, 2026
sozercan#384/sozercan#415 already-ported

Content already present via cherry-picks and prior independent ports; records
ancestry so the fork reads 0-behind. No code changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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