Skip to content

Add Chinese (Simplified) localization#819

Open
markhome1 wants to merge 2 commits intosteipete:mainfrom
markhome1:feat/chinese-localization
Open

Add Chinese (Simplified) localization#819
markhome1 wants to merge 2 commits intosteipete:mainfrom
markhome1:feat/chinese-localization

Conversation

@markhome1
Copy link
Copy Markdown

Add Chinese (Simplified) localization support for CodexBar.

Copilot AI review requested due to automatic review settings April 30, 2026 14:03
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d511add9d3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

"overview_choose_providers" = "overview_choose_providers";
"peak_ends_in" = "peak_ends_in";
"remove_account_message" = "remove_account_message";
"version_format" = "version_format";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Populate English format localization entries

Several newly introduced localization keys are self-mapped in the English table (for example "version_format" = "version_format"), but their call sites use String(format:) expecting placeholders and human-readable text. In the English UI this renders raw key names and drops interpolated values (version/build strings, usage values, account emails, etc.), which is a visible regression for default users. These entries should be replaced with real English strings containing the correct format specifiers.

Useful? React with 👍 / 👎.

Comment thread Sources/CodexBar/Localization.swift Outdated

private func localizedBundle() -> Bundle {
let language = appLanguageDefaults().string(forKey: "appLanguage") ?? ""
let target = language.isEmpty ? "en" : language.lowercased()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor system locale when no app language is set

The System language option stores an empty appLanguage, but localizedBundle() forces empty values to "en", so the app never follows macOS language preferences in that mode. As a result, users selecting “System” always get English instead of their system locale (including supported locales like Simplified Chinese). The empty-language path should resolve via preferred localizations/AppleLanguages rather than hardcoding English.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a localization system and Chinese (Simplified) translations for CodexBar, and migrates a number of user-facing strings to localization keys (including a new in-app language preference).

Changes:

  • Introduces L(...) localization helper, sets package defaultLocalization, and adds en + zh-Hans Localizable.strings.
  • Adds an app language preference persisted in SettingsStore, plus a language picker in General settings.
  • Replaces various hard-coded UI/menu strings with localized keys across preferences panes and several provider flows.

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
Sources/CodexBar/UsageStoreSupport.swift Localizes provider status indicator labels.
Sources/CodexBar/StatusItemController+Menu.swift Localizes MCP submenu strings (window/resets/model usage).
Sources/CodexBar/SettingsStoreState.swift Adds stored state for app language preference.
Sources/CodexBar/SettingsStore.swift Loads appLanguage from defaults; localizes enum labels.
Sources/CodexBar/SettingsStore+Defaults.swift Adds appLanguage computed preference and persistence.
Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings Adds Simplified Chinese translations/keys.
Sources/CodexBar/Resources/en.lproj/Localizable.strings Adds English base/fallback strings/keys.
Sources/CodexBar/Providers/VertexAI/VertexAILoginFlow.swift Localizes Vertex AI login alert text/buttons.
Sources/CodexBar/Providers/JetBrains/JetBrainsLoginFlow.swift Localizes JetBrains login-ready messaging.
Sources/CodexBar/Providers/Cursor/CursorProviderImplementation.swift Localizes Cursor “On-Demand” cost strings.
Sources/CodexBar/Providers/Copilot/CopilotLoginFlow.swift Localizes Copilot device flow alerts.
Sources/CodexBar/Providers/Codex/CodexProviderImplementation.swift Localizes credits/last spend menu entries.
Sources/CodexBar/Providers/Claude/ClaudeProviderImplementation.swift Localizes weekly usage unavailable + extra usage.
Sources/CodexBar/PreferencesView.swift Localizes preferences tab titles and tab items.
Sources/CodexBar/PreferencesProvidersPane.swift Localizes provider pane UI, confirmations, picker labels.
Sources/CodexBar/PreferencesProviderErrorView.swift Localizes “Show/Hide details” toggle.
Sources/CodexBar/PreferencesProviderDetailView.swift Localizes “last fetch failed” title.
Sources/CodexBar/PreferencesGeneralPane.swift Adds language picker + localizes General pane UI/cost status lines.
Sources/CodexBar/PreferencesDisplayPane.swift Localizes Display pane labels/help text.
Sources/CodexBar/PreferencesDebugPane.swift Localizes Debug pane labels/captions/status messages.
Sources/CodexBar/PreferencesAdvancedPane.swift Localizes Advanced pane labels/captions/status messages.
Sources/CodexBar/PreferencesAboutPane.swift Localizes About pane labels and build/version strings.
Sources/CodexBar/MenuBarDisplayMode.swift Localizes display mode labels/descriptions.
Sources/CodexBar/ManagedCodexAccountService.swift Localizes workspace selection alert.
Sources/CodexBar/Localization.swift Adds localization bundle resolution + L(...) helpers.
Sources/CodexBar/KeychainPromptCoordinator.swift Localizes “OK” button.
Sources/CodexBar/CodexbarApp.swift Applies language preference at app start.
Package.swift Sets SwiftPM defaultLocalization to en.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +367 to +392
"cost_status_snapshot" = "cost_status_snapshot";
"cost_status_unsupported" = "cost_status_unsupported";
"credits_remaining" = "credits_remaining";
"cursor_on_demand" = "cursor_on_demand";
"cursor_on_demand_with_limit" = "cursor_on_demand_with_limit";
"extra_usage_format" = "extra_usage_format";
"jetbrains_detected_generate" = "jetbrains_detected_generate";
"jetbrains_detected_select" = "jetbrains_detected_select";
"last_fetch_failed" = "last_fetch_failed";
"last_spend" = "last_spend";
"mcp_model_usage" = "mcp_model_usage";
"mcp_resets" = "mcp_resets";
"mcp_window" = "mcp_window";
"metric_average" = "metric_average";
"metric_primary" = "metric_primary";
"metric_secondary" = "metric_secondary";
"metric_tertiary" = "metric_tertiary";
"multiple_workspaces_found" = "multiple_workspaces_found";
"off_peak" = "off_peak";
"off_peak_peak_in" = "off_peak_peak_in";
"ory_session_…=…; csrftoken=…" = "ory_session_…=…; csrftoken=…";
"overview_choose_providers" = "overview_choose_providers";
"peak_ends_in" = "peak_ends_in";
"remove_account_message" = "remove_account_message";
"version_format" = "version_format";
"vertex_ai_login_instructions" = "vertex_ai_login_instructions";
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

These keys (mcp_*, metric_*, multiple_workspaces_found, etc.) are used with String(format:) in menu/UI, but currently have placeholder values. Provide the real English strings with placeholders so menu items/alerts render correctly (e.g. Window: %@, Primary (%@), CodexBar found multiple workspaces for %@…).

Suggested change
"cost_status_snapshot" = "cost_status_snapshot";
"cost_status_unsupported" = "cost_status_unsupported";
"credits_remaining" = "credits_remaining";
"cursor_on_demand" = "cursor_on_demand";
"cursor_on_demand_with_limit" = "cursor_on_demand_with_limit";
"extra_usage_format" = "extra_usage_format";
"jetbrains_detected_generate" = "jetbrains_detected_generate";
"jetbrains_detected_select" = "jetbrains_detected_select";
"last_fetch_failed" = "last_fetch_failed";
"last_spend" = "last_spend";
"mcp_model_usage" = "mcp_model_usage";
"mcp_resets" = "mcp_resets";
"mcp_window" = "mcp_window";
"metric_average" = "metric_average";
"metric_primary" = "metric_primary";
"metric_secondary" = "metric_secondary";
"metric_tertiary" = "metric_tertiary";
"multiple_workspaces_found" = "multiple_workspaces_found";
"off_peak" = "off_peak";
"off_peak_peak_in" = "off_peak_peak_in";
"ory_session_…=…; csrftoken=…" = "ory_session_…=…; csrftoken=…";
"overview_choose_providers" = "overview_choose_providers";
"peak_ends_in" = "peak_ends_in";
"remove_account_message" = "remove_account_message";
"version_format" = "version_format";
"vertex_ai_login_instructions" = "vertex_ai_login_instructions";
"cost_status_snapshot" = "Snapshot";
"cost_status_unsupported" = "Usage information unavailable";
"credits_remaining" = "Credits remaining";
"cursor_on_demand" = "Cursor on-demand";
"cursor_on_demand_with_limit" = "Cursor on-demand (%@)";
"extra_usage_format" = "+%@ extra";
"jetbrains_detected_generate" = "Generate from %@";
"jetbrains_detected_select" = "Select %@";
"last_fetch_failed" = "Last fetch failed";
"last_spend" = "Last spend";
"mcp_model_usage" = "Model usage: %@";
"mcp_resets" = "Resets: %@";
"mcp_window" = "Window: %@";
"metric_average" = "Average (%@)";
"metric_primary" = "Primary (%@)";
"metric_secondary" = "Secondary (%@)";
"metric_tertiary" = "Tertiary (%@)";
"multiple_workspaces_found" = "CodexBar found multiple workspaces for %@. Please choose the workspace to use.";
"off_peak" = "Off-peak";
"off_peak_peak_in" = "Off-peak · peak in %@";
"ory_session_…=…; csrftoken=…" = "ory_session_…=…; csrftoken=…";
"overview_choose_providers" = "Choose providers";
"peak_ends_in" = "Peak ends in %@";
"remove_account_message" = "Are you sure you want to remove %@?";
"version_format" = "Version %@";
"vertex_ai_login_instructions" = "Open the following URL in your browser and complete the Vertex AI login flow.";

Copilot uses AI. Check for mistakes.
Comment on lines +391 to +392
"version_format" = "version_format";
"vertex_ai_login_instructions" = "vertex_ai_login_instructions";
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

version_format and vertex_ai_login_instructions are used directly in About/Vertex AI login flow, but the English values are still placeholders. Add the actual English format string (e.g. Version %@) and the full instruction text so the alert/readout is usable.

Suggested change
"version_format" = "version_format";
"vertex_ai_login_instructions" = "vertex_ai_login_instructions";
"version_format" = "Version %@";
"vertex_ai_login_instructions" = "To sign in with Vertex AI, open Google Cloud Console, create or select a project with Vertex AI API enabled, then provide your project ID, region, and authentication credentials in CodexBar.";

Copilot uses AI. Check for mistakes.
"extra_usage_format" = "extra_usage_format";
"jetbrains_detected_generate" = "jetbrains_detected_generate";
"jetbrains_detected_select" = "jetbrains_detected_select";
"last_fetch_failed" = "last_fetch_failed";
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

last_fetch_failed is defined twice in this file (once here and again later). The duplicate means one value will silently override the other, and it also makes it unclear whether the string is meant to be generic or a formatted title. Use two distinct keys (generic vs. %@-formatted) and remove the duplicate entry to avoid incorrect UI text.

Suggested change
"last_fetch_failed" = "last_fetch_failed";
"last_fetch_failed_generic" = "last_fetch_failed_generic";

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +52
Picker("Language", selection: self.$settings.appLanguage) {
ForEach(AppLanguage.allCases) { option in
Text(option.label).tag(option.rawValue)
}
}
.labelsHidden()
.pickerStyle(.menu)
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

The Picker’s label is hard-coded as "Language" while the visible label is hidden. .labelsHidden() only hides it visually, so VoiceOver will still announce the untranslated label. Use a localized label (e.g. the same key as language_title) for accessibility consistency.

Copilot uses AI. Check for mistakes.
if let errorDisplay {
ProviderErrorView(
title: "Last \(self.store.metadata(for: self.provider).displayName) fetch failed:",
title: String(format: L("last_fetch_failed"), self.store.metadata(for: self.provider).displayName),
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

String(format: L("last_fetch_failed"), …) assumes last_fetch_failed is a format string containing a placeholder for the provider name, but Localizable.strings currently defines last_fetch_failed as a non-format, generic string (and it’s duplicated), so the provider name will be dropped. Use a dedicated key for the titled form (e.g. last_fetch_failed_with_provider) and keep the generic last_fetch_failed for the status text.

Suggested change
title: String(format: L("last_fetch_failed"), self.store.metadata(for: self.provider).displayName),
title: "\(self.store.metadata(for: self.provider).displayName): \(L("last_fetch_failed"))",

Copilot uses AI. Check for mistakes.
Comment on lines +356 to +372
"built_format" = "built_format";
"copilot_complete_in_browser" = "copilot_complete_in_browser";
"copilot_device_code" = "copilot_device_code";
"copilot_device_code_copied" = "copilot_device_code_copied";
"copilot_verify_at" = "copilot_verify_at";
"copilot_waiting_text" = "copilot_waiting_text";
"copilot_window_closes_auto" = "copilot_window_closes_auto";
"cost_status_error" = "cost_status_error";
"cost_status_fetching" = "cost_status_fetching";
"cost_status_last_attempt" = "cost_status_last_attempt";
"cost_status_no_data" = "cost_status_no_data";
"cost_status_snapshot" = "cost_status_snapshot";
"cost_status_unsupported" = "cost_status_unsupported";
"credits_remaining" = "credits_remaining";
"cursor_on_demand" = "cursor_on_demand";
"cursor_on_demand_with_limit" = "cursor_on_demand_with_limit";
"extra_usage_format" = "extra_usage_format";
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

Several keys here still map to their own key names (e.g. built_format, copilot_device_code, etc.). These are used by the app via L(...)/String(format:), so the UI will literally display the key names in English builds. Replace these placeholder values with the intended English strings (and include the required %@ / %1$@ format specifiers where arguments are provided).

Suggested change
"built_format" = "built_format";
"copilot_complete_in_browser" = "copilot_complete_in_browser";
"copilot_device_code" = "copilot_device_code";
"copilot_device_code_copied" = "copilot_device_code_copied";
"copilot_verify_at" = "copilot_verify_at";
"copilot_waiting_text" = "copilot_waiting_text";
"copilot_window_closes_auto" = "copilot_window_closes_auto";
"cost_status_error" = "cost_status_error";
"cost_status_fetching" = "cost_status_fetching";
"cost_status_last_attempt" = "cost_status_last_attempt";
"cost_status_no_data" = "cost_status_no_data";
"cost_status_snapshot" = "cost_status_snapshot";
"cost_status_unsupported" = "cost_status_unsupported";
"credits_remaining" = "credits_remaining";
"cursor_on_demand" = "cursor_on_demand";
"cursor_on_demand_with_limit" = "cursor_on_demand_with_limit";
"extra_usage_format" = "extra_usage_format";
"built_format" = "Built %@";
"copilot_complete_in_browser" = "Complete sign in in your browser.";
"copilot_device_code" = "Device code: %@";
"copilot_device_code_copied" = "Device code copied.";
"copilot_verify_at" = "Verify at %@";
"copilot_waiting_text" = "Waiting for GitHub Copilot authorization…";
"copilot_window_closes_auto" = "This window closes automatically when sign-in completes.";
"cost_status_error" = "%@: error";
"cost_status_fetching" = "%1$@: fetching…%2$@";
"cost_status_last_attempt" = "%1$@: last attempt %2$@";
"cost_status_no_data" = "%@: no data yet";
"cost_status_snapshot" = "%1$@: %2$@ · 30d %3$@";
"cost_status_unsupported" = "%@: unsupported";
"credits_remaining" = "%@ remaining";
"cursor_on_demand" = "On-demand";
"cursor_on_demand_with_limit" = "On-demand (%@ limit)";
"extra_usage_format" = "%1$@: %2$@";

Copilot uses AI. Check for mistakes.
Comment on lines +363 to +368
"cost_status_error" = "cost_status_error";
"cost_status_fetching" = "cost_status_fetching";
"cost_status_last_attempt" = "cost_status_last_attempt";
"cost_status_no_data" = "cost_status_no_data";
"cost_status_snapshot" = "cost_status_snapshot";
"cost_status_unsupported" = "cost_status_unsupported";
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

cost_status_* entries are still placeholders (value == key) but are formatted and displayed in GeneralPane (fetching/last attempt/snapshot/error/no data). Populate these with real English messages and ensure the format specifiers match how they’re called (some take 1 arg, others 2–3).

Copilot uses AI. Check for mistakes.
"extra_usage_format" = "额外用量:%1$@ / %2$@";
"jetbrains_detected_generate" = "检测到:%@。使用一次 AI 助手以生成配额数据,然后刷新 CodexBar。";
"jetbrains_detected_select" = "检测到:%@。在设置中选择您偏好的 IDE,然后刷新 CodexBar。";
"last_fetch_failed" = "上次获取 %1$@ 失败:";
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

last_fetch_failed is duplicated in this file (one formatted value with %1$@ and another generic value later). The later entry will override the formatted one, causing String(format: L("last_fetch_failed"), providerName) to drop the provider name. Split this into two separate keys (generic vs. formatted) and remove the duplicate.

Suggested change
"last_fetch_failed" = "上次获取 %1$@ 失败:";
"last_fetch_failed_with_provider" = "上次获取 %1$@ 失败:";

Copilot uses AI. Check for mistakes.
Comment thread Sources/CodexBar/Localization.swift Outdated

private func localizedBundle() -> Bundle {
let language = appLanguageDefaults().string(forKey: "appLanguage") ?? ""
let target = language.isEmpty ? "en" : language.lowercased()
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

target is derived via language.lowercased(), but the resource folder is zh-Hans.lproj (mixed case). On case-sensitive filesystems this will fail to resolve the bundle and localization will silently fall back. Prefer using the BCP-47 code as stored (no lowercasing), or explicitly map known language codes to the exact .lproj folder names.

Suggested change
let target = language.isEmpty ? "en" : language.lowercased()
let target = language.isEmpty ? "en" : language

Copilot uses AI. Check for mistakes.
Comment thread Sources/CodexBar/Localization.swift Outdated
Comment on lines +18 to +22
// Fallback to en.lproj to avoid following system language
if let path = Bundle.module.path(forResource: "en", ofType: "lproj"),
let bundle = Bundle(path: path) {
return bundle
}
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

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

When appLanguage is unset ("" / “System”), localizedBundle() forces en and the subsequent fallback also forces en, which prevents following the user/system preferred language even though the UI offers a “System” option and AppleLanguages is removed. Consider returning Bundle.module (or using Bundle.module.preferredLocalizations) when appLanguage is empty so the app can follow system language, and keep the en fallback only for truly missing resources.

Copilot uses AI. Check for mistakes.
@ratulsarna
Copy link
Copy Markdown
Collaborator

@codex review

markhome1 added 2 commits May 3, 2026 11:21
- Add Localization.swift with L() helper for string lookup
- Add en.lproj/Localizable.strings (base English translations)
- Add zh-Hans.lproj/Localizable.strings (Simplified Chinese translations)
- Replace hardcoded strings with L() calls in 25 source files
- Add defaultLocalization to Package.swift
- Add new localized setting keys in SettingsStore+Defaults.swift

This enables full Chinese UI for users with zh-Hans locale preference.
…on keys

- Replace 35 placeholder format-string entries in en.lproj with real
  English values (keys like version_format, cost_status_*, mcp_*, etc.
  were self-mapped causing raw key names to render in English UI)
- Split duplicate last_fetch_failed into plain and format-string variants
  (last_fetch_failed + last_fetch_failed_with_provider) in both en
  and zh-Hans, preventing the plain variant from silently overriding
  the formatted one and dropping the provider name
- Fix Localization.swift to follow system locale when appLanguage is
  empty ("System" mode), instead of always forcing English
- Remove lowercased() from lproj path lookup to avoid mismatch on
  case-sensitive filesystems
- Localize the Picker accessibility label in General preferences
  (was hard-coded "Language", now uses language_title)
@markhome1 markhome1 force-pushed the feat/chinese-localization branch from 39d4eb5 to b248bad Compare May 3, 2026 03:21
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