Add Chinese (Simplified) localization#819
Conversation
There was a problem hiding this comment.
💡 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"; |
There was a problem hiding this comment.
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 👍 / 👎.
|
|
||
| private func localizedBundle() -> Bundle { | ||
| let language = appLanguageDefaults().string(forKey: "appLanguage") ?? "" | ||
| let target = language.isEmpty ? "en" : language.lowercased() |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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 packagedefaultLocalization, and addsen+zh-HansLocalizable.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.
| "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"; |
There was a problem hiding this comment.
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 %@…).
| "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."; |
| "version_format" = "version_format"; | ||
| "vertex_ai_login_instructions" = "vertex_ai_login_instructions"; |
There was a problem hiding this comment.
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.
| "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."; |
| "extra_usage_format" = "extra_usage_format"; | ||
| "jetbrains_detected_generate" = "jetbrains_detected_generate"; | ||
| "jetbrains_detected_select" = "jetbrains_detected_select"; | ||
| "last_fetch_failed" = "last_fetch_failed"; |
There was a problem hiding this comment.
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.
| "last_fetch_failed" = "last_fetch_failed"; | |
| "last_fetch_failed_generic" = "last_fetch_failed_generic"; |
| Picker("Language", selection: self.$settings.appLanguage) { | ||
| ForEach(AppLanguage.allCases) { option in | ||
| Text(option.label).tag(option.rawValue) | ||
| } | ||
| } | ||
| .labelsHidden() | ||
| .pickerStyle(.menu) |
There was a problem hiding this comment.
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.
| 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), |
There was a problem hiding this comment.
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.
| 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"))", |
| "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"; |
There was a problem hiding this comment.
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).
| "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$@"; |
| "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"; |
There was a problem hiding this comment.
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).
| "extra_usage_format" = "额外用量:%1$@ / %2$@"; | ||
| "jetbrains_detected_generate" = "检测到:%@。使用一次 AI 助手以生成配额数据,然后刷新 CodexBar。"; | ||
| "jetbrains_detected_select" = "检测到:%@。在设置中选择您偏好的 IDE,然后刷新 CodexBar。"; | ||
| "last_fetch_failed" = "上次获取 %1$@ 失败:"; |
There was a problem hiding this comment.
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.
| "last_fetch_failed" = "上次获取 %1$@ 失败:"; | |
| "last_fetch_failed_with_provider" = "上次获取 %1$@ 失败:"; |
|
|
||
| private func localizedBundle() -> Bundle { | ||
| let language = appLanguageDefaults().string(forKey: "appLanguage") ?? "" | ||
| let target = language.isEmpty ? "en" : language.lowercased() |
There was a problem hiding this comment.
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.
| let target = language.isEmpty ? "en" : language.lowercased() | |
| let target = language.isEmpty ? "en" : language |
| // 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 | ||
| } |
There was a problem hiding this comment.
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.
|
@codex review |
- 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)
39d4eb5 to
b248bad
Compare
Add Chinese (Simplified) localization support for CodexBar.