Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,8 @@ the name Windows has for the device ("8BitDo Ultimate 2 Wireless Controller for
"XInput Controller"), and generic names are replaced from a table of 922 known models. A
controller that copies another's ids, as the Onikuma C1 copies the Switch Pro Controller's, can
be told which model it is under Model on its Information page, and takes that name and drawing.
The interface follows Windows' light or dark mode, and is in English or Brazilian Portuguese, picked in
Settings (English until you pick).
The interface follows Windows' light or dark mode, and is in English, Brazilian Portuguese or
Simplified Chinese, picked in Settings (English until you pick).

![A controller's page: the 8BitDo drawn with its live input, its profile, and its four extra buttons, one assigned to Xbox A, one to Ctrl+Shift+M and one to a macro](docs/controller.png)

Expand Down
28 changes: 23 additions & 5 deletions crates/open-controller-core/src/i18n.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
//! Interface text in English and Brazilian Portuguese. English until the user picks another
//! language in the settings, which keep the choice. Strings with `{}` take one value, filled in
//! by [`fill`].
//! Interface text in English, Brazilian Portuguese and Simplified Chinese. English until the
//! user picks another language in the settings, which keep the choice. Strings with `{}` take
//! one value, filled in by [`fill`].

use serde::{Deserialize, Serialize};

mod zh;
pub use zh::ZH;

/// A language the interface is written in.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Lang {
#[default]
En,
Pt,
#[serde(rename = "zh-cn", alias = "zh", alias = "zh-CN")]
ZhCn,
}

impl Lang {
pub const ALL: [Lang; 2] = [Lang::En, Lang::Pt];
pub const ALL: [Lang; 3] = [Lang::En, Lang::Pt, Lang::ZhCn];

/// Its name in itself, as language pickers show it.
pub fn name(self) -> &'static str {
match self {
Lang::En => "English",
Lang::Pt => "Português",
Lang::ZhCn => "简体中文",
}
}
}
Expand Down Expand Up @@ -1382,14 +1388,16 @@ pub fn fill(template: &str, value: impl std::fmt::Display) -> String {
template.replacen("{}", &value.to_string(), 1)
}

/// The text in `lang`. `OPEN_CONTROLLER_LANG=en` or `pt` overrides it.
/// The text in `lang`. `OPEN_CONTROLLER_LANG=en`, `pt` or `zh-cn` overrides it.
pub fn text(lang: Lang) -> &'static Text {
match std::env::var("OPEN_CONTROLLER_LANG").as_deref() {
Ok("en") => &EN,
Ok("pt") => &PT,
Ok("zh" | "zh-cn" | "zh-CN") => &ZH,
_ => match lang {
Lang::En => &EN,
Lang::Pt => &PT,
Lang::ZhCn => &ZH,
},
}
}
Expand All @@ -1402,6 +1410,8 @@ mod tests {
fn fill_replaces_the_placeholder() {
assert_eq!(fill(EN.player, 2), "Player 2");
assert_eq!(fill(PT.wait_ms, 50), "Esperar 50 ms");
assert_eq!(fill(ZH.player, 2), "玩家 2");
assert_eq!(fill(ZH.wait_ms, 50), "等待 50 毫秒");
}

#[test]
Expand All @@ -1410,4 +1420,12 @@ mod tests {
assert_eq!(serde_json::from_str::<Lang>(r#""en""#).unwrap(), Lang::En);
assert_eq!(Lang::default(), Lang::En);
}

#[test]
fn simplified_chinese_language_names_round_trip() {
assert_eq!(serde_json::to_string(&Lang::ZhCn).unwrap(), r#""zh-cn""#);
for name in ["zh", "zh-cn", "zh-CN"] {
assert_eq!(serde_json::from_str::<Lang>(&format!("\"{name}\"")).unwrap(), Lang::ZhCn);
}
}
}
Loading
Loading