feat(ui): add ja-JP locale and split i18n into per-language modules (#372)

The single en/zh tuple table becomes one module per language behind a
`SUPPORTED_LANGUAGES` table, and Japanese joins English and Simplified Chinese.

- `gui_language` accepts `ja-JP`; anything unrecognized still falls back to `en`.
- The language picker and `refresh_locale_state` both read `SUPPORTED_LANGUAGES`
  instead of keeping their own copy of the code list.
- Language names in the picker stay endonyms (English / 简体中文 / 日本語) in
  every locale, as English and Chinese already were.
- The zh and ja key tables are exhaustive, so a new `L10nKey` fails the build
  until it is translated rather than silently rendering English.

Co-authored-by: Chihiro WATANABE <chihiro.watanabe@live.jp>
This commit is contained in:
cwatanab
2026-08-07 12:56:38 +08:00
committed by GitHub
parent bb72be338d
commit 05bfa8a269
9 changed files with 5976 additions and 3934 deletions
+7 -3
View File
@@ -4,6 +4,8 @@ use std::sync::{Arc, OnceLock};
use serde::{Deserialize, Serialize};
pub const SUPPORTED_GUI_LANGUAGES: &[&str] = &["en", "zh-CN", "ja-JP"];
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct FontFeatures(pub Arc<Vec<(String, u32)>>);
@@ -519,9 +521,8 @@ impl Config {
.take()
.map(|proxy| proxy.trim().to_string())
.filter(|proxy| !proxy.is_empty());
match self.gui_language.as_str() {
"en" | "zh-CN" => {}
_ => self.gui_language = default_gui_language(),
if !SUPPORTED_GUI_LANGUAGES.contains(&self.gui_language.as_str()) {
self.gui_language = default_gui_language();
}
}
@@ -1138,6 +1139,9 @@ mod tests {
let cfg: Config = serde_json::from_str(r#"{"gui_language": "zh-CN"}"#).unwrap();
assert_eq!(cfg.gui_language, "zh-CN");
let cfg: Config = serde_json::from_str(r#"{"gui_language": "ja-JP"}"#).unwrap();
assert_eq!(cfg.gui_language, "ja-JP");
let mut cfg: Config = serde_json::from_str(r#"{"gui_language": "ko"}"#).unwrap();
cfg.sanitize();
assert_eq!(cfg.gui_language, "en");
+4 -3
View File
@@ -143,13 +143,14 @@ whatever you ran in the pane, and you can revoke it under Privacy & Security.
## Localization
The GUI ships English and Simplified Chinese strings. Pick one in Settings →
Appearance → Language, or in `config.json`:
The GUI ships English, Simplified Chinese and Japanese strings. Pick one in
Settings → Appearance → Language, or in `config.json`:
```json
{ "gui_language": "zh-CN" }
```
`en` and `zh-CN` are the only accepted values; anything else falls back to `en`.
`en`, `zh-CN` and `ja-JP` are the only accepted values; anything else falls back
to `en`.
The choice is explicit — the system language is never inferred. CLI output stays
English so agent and script integrations keep a stable, predictable surface.
+2 -2
View File
@@ -137,12 +137,12 @@ Apple Events、系统管理),这样程序才能正常弹出一次性授权
## 本地化
GUI 目前提供英文简体中文套文案。在「设置 → 外观 → 语言」中选择,或直接改
GUI 目前提供英文简体中文和日文三套文案。在「设置 → 外观 → 语言」中选择,或直接改
`config.json`
```json
{ "gui_language": "zh-CN" }
```
只接受 `en``zh-CN` 个值,其它值一律回落到 `en`。语言必须显式指定,不会
只接受 `en``zh-CN` `ja-JP`个值,其它值一律回落到 `en`。语言必须显式指定,不会
去猜系统语言。CLI 输出保持英文,保证 agent、脚本和开发者工作流的输出稳定可预测。
+22 -17
View File
@@ -3702,17 +3702,19 @@ impl Tty7App {
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<SelectState<SearchableVec<String>>> {
const CODES: &[&str] = &["en", "zh-CN"];
let labels = || {
vec![
t(L10nKey::SettingsLanguageEnglish).to_string(),
t(L10nKey::SettingsLanguageChinese).to_string(),
]
crate::ui::i18n::SUPPORTED_LANGUAGES
.iter()
.map(|lang| t(lang.label_key).to_string())
.collect::<Vec<_>>()
};
let cfg = cx.global::<Config>();
let current = Self::normalize_gui_language(&cfg.gui_language);
let rows = labels();
let selected = CODES.iter().position(|c| *c == current).unwrap_or(0);
let selected = crate::ui::i18n::SUPPORTED_LANGUAGES
.iter()
.position(|lang| lang.code == current)
.unwrap_or(0);
let language_select = cx.new(|cx| {
SelectState::new(
SearchableVec::new(rows),
@@ -3728,7 +3730,9 @@ impl Tty7App {
if let SelectEvent::Confirm(Some(label)) = ev {
let rows = labels();
if let Some(idx) = rows.iter().position(|r| r == label) {
this.set_gui_language(CODES[idx], window, cx);
if let Some(lang) = crate::ui::i18n::SUPPORTED_LANGUAGES.get(idx) {
this.set_gui_language(lang.code, window, cx);
}
}
}
},
@@ -3737,10 +3741,9 @@ impl Tty7App {
}
fn normalize_gui_language(code: &str) -> &'static str {
match code {
"zh-CN" => "zh-CN",
_ => "en",
}
crate::ui::i18n::find_language(code)
.map(|lang| lang.code)
.unwrap_or_else(crate::ui::i18n::default_language_code)
}
pub(crate) fn set_gui_language(
@@ -3762,7 +3765,6 @@ impl Tty7App {
}
pub(crate) fn refresh_locale_state(&mut self, window: &mut Window, cx: &mut Context<Self>) {
const CODES: &[&str] = &["en", "zh-CN"];
self.sidebar_search.update(cx, |state, cx| {
state.set_placeholder(t(L10nKey::SearchTabs), window, cx)
});
@@ -3770,14 +3772,17 @@ impl Tty7App {
state.set_placeholder(t(L10nKey::SearchFiles), window, cx)
});
if let Some(s) = self.active_settings() {
let rows = vec![
t(L10nKey::SettingsLanguageEnglish).to_string(),
t(L10nKey::SettingsLanguageChinese).to_string(),
];
let rows = crate::ui::i18n::SUPPORTED_LANGUAGES
.iter()
.map(|lang| t(lang.label_key).to_string())
.collect::<Vec<_>>();
s.language_select.update(cx, |state, cx| {
state.set_items(SearchableVec::new(rows), window, cx);
let code = Self::normalize_gui_language(&cx.global::<Config>().gui_language);
let selected = CODES.iter().position(|c| *c == code).unwrap_or(0);
let selected = crate::ui::i18n::SUPPORTED_LANGUAGES
.iter()
.position(|lang| lang.code == code)
.unwrap_or(0);
state.set_selected_index(Some(IndexPath::default().row(selected)), window, cx);
});
s.search.update(cx, |state, cx| {
-3909
View File
File diff suppressed because it is too large Load Diff
+1323
View File
File diff suppressed because it is too large Load Diff
+1352
View File
File diff suppressed because it is too large Load Diff
+2025
View File
File diff suppressed because it is too large Load Diff
+1241
View File
File diff suppressed because it is too large Load Diff