Skip to content

Commit 96ce602

Browse files
committed
allow file level language configuration
1 parent 9f498e2 commit 96ce602

File tree

16 files changed

+199
-158
lines changed

16 files changed

+199
-158
lines changed
Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
//! The major configuration file for this app, containing information about which version to skip,
22
//! when the updates are checked, how long until next updates will be checked etc.
33
4+
use crate::setter;
5+
use crate::{dirs::rim_config_dir, types::TomlParser};
46
use anyhow::Result;
57
use chrono::{NaiveDateTime, Utc};
6-
use rim_common::{dirs::rim_config_dir, types::TomlParser};
78
use serde::{Deserialize, Serialize};
9+
use std::str::FromStr;
810
use std::{collections::HashMap, fmt::Display, time::Duration};
911

1012
/// Default update check timeout is 1440 minutes (1 day)
@@ -15,6 +17,7 @@ pub const DEFAULT_UPDATE_CHECK_DURATION: Duration =
1517

1618
#[derive(Debug, Deserialize, Serialize, Default)]
1719
pub struct Configuration {
20+
pub language: Option<Language>,
1821
pub update: UpdateCheckerOpt,
1922
}
2023

@@ -36,12 +39,17 @@ impl Configuration {
3639
self
3740
}
3841

39-
/// Try loading from [`rim_config_dir`].
42+
/// Try loading from [`rim_config_dir`], return `None` if it doesn't exists yet.
43+
pub fn try_load_from_config_dir() -> Option<Self> {
44+
Self::load_from_dir(rim_config_dir()).ok()
45+
}
46+
47+
/// Loading from [`rim_config_dir`] or return default.
4048
///
4149
/// This guarantee to return a [`VersionSkip`] object,
4250
/// even if the file does not exists, the default will got returned.
4351
pub fn load_from_config_dir() -> Self {
44-
Self::load_from_dir(rim_config_dir()).unwrap_or_default()
52+
Self::try_load_from_config_dir().unwrap_or_default()
4553
}
4654

4755
/// Write the configuration to [`rim_config_dir`].
@@ -52,6 +60,51 @@ impl Configuration {
5260
pub fn update_skipped<T: AsRef<str>>(&self, target: UpdateTarget, version: T) -> bool {
5361
self.update.is_skipped(target, version)
5462
}
63+
64+
setter!(set_language(self.language, Option<Language>));
65+
}
66+
67+
#[derive(Debug, Deserialize, Serialize, Default, PartialEq, Eq)]
68+
#[non_exhaustive]
69+
pub enum Language {
70+
CN,
71+
#[default]
72+
EN,
73+
}
74+
75+
impl Language {
76+
pub fn possible_values() -> &'static [Language] {
77+
&[Self::CN, Self::EN]
78+
}
79+
/// Returns the string representation of this enum,
80+
/// this will be the same one that parsed from commandline input.
81+
pub fn as_str(&self) -> &'static str {
82+
match self {
83+
Self::CN => "cn",
84+
Self::EN => "en",
85+
}
86+
}
87+
/// This is the `str` used for setting locale,
88+
/// make sure the values match the filenames under `<root>/locales`.
89+
pub fn locale_str(&self) -> &str {
90+
match self {
91+
Self::CN => "zh-CN",
92+
Self::EN => "en-US",
93+
}
94+
}
95+
}
96+
97+
impl FromStr for Language {
98+
type Err = anyhow::Error;
99+
fn from_str(s: &str) -> Result<Self, Self::Err> {
100+
match s.to_lowercase().as_str() {
101+
"cn" | "zh-cn" => Ok(Self::CN),
102+
"en" | "en-us" => Ok(Self::EN),
103+
_ => Err(anyhow::anyhow!(
104+
"invalid or unsupported language option: {s}"
105+
)),
106+
}
107+
}
55108
}
56109

57110
// If we ever need to support more things for update checker,
@@ -227,4 +280,19 @@ manager = { last-run = "1970-01-01T00:00:00" }"#;
227280
expected = expected.remind_later(manager, 60);
228281
assert_eq!(expected.conf_mut(manager).timeout, Some(120));
229282
}
283+
284+
#[test]
285+
fn lang_config() {
286+
let input = r#"language = "CN"
287+
288+
[update]
289+
"#;
290+
291+
let expected = Configuration::from_str(input).unwrap();
292+
assert_eq!(expected.language, Some(Language::CN));
293+
294+
// check if the language consistance since we have a `FromStr` impl for it.
295+
let back_to_str = toml::to_string(&expected).unwrap();
296+
assert_eq!(back_to_str, input);
297+
}
230298
}

rim_common/src/types/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
mod build_config;
2+
mod configuration;
23
mod tool_info;
34
mod tool_map;
45
mod toolkit_manifest;
56

67
// re-exports
78
pub use build_config::*;
9+
pub use configuration::*;
810
pub use tool_info::*;
911
pub use tool_map::*;
1012
pub use toolkit_manifest::*;

rim_common/src/utils/mod.rs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@ pub use progress_bar::*;
1919
use std::{
2020
ffi::OsStr,
2121
path::{Path, PathBuf},
22+
str::FromStr,
2223
sync::{LazyLock, Mutex},
2324
time::Duration,
2425
};
2526

2627
use anyhow::Result;
2728
use url::Url;
2829

30+
use crate::types::{Configuration, Language};
31+
2932
static CURRENT_LOCALE: LazyLock<Mutex<String>> = LazyLock::new(|| Mutex::new(String::new()));
3033

3134
/// Insert a `.exe` postfix to given input.
@@ -188,17 +191,38 @@ pub fn to_string_lossy<S: AsRef<OsStr>>(s: S) -> String {
188191
s.as_ref().to_string_lossy().to_string()
189192
}
190193

191-
/// Allowing the i18n framework to use the current system locale.
194+
/// Use configured locale or detect system's current locale.
192195
pub fn use_current_locale() {
193-
let locale = sys_locale::get_locale().unwrap_or_else(|| "en".to_string());
194-
set_locale(&locale);
196+
set_locale(&get_locale());
197+
}
198+
199+
/// Getting the current locale by:
200+
/// 1. Checking RIM configuration file, return the configured language if has.
201+
/// 2. Check system's current locale using [`sys_locale`] crate.
202+
/// 3. Fallback to english locale.
203+
pub fn get_locale() -> String {
204+
Configuration::try_load_from_config_dir()
205+
.and_then(|c| c.language)
206+
.map(|lang| lang.locale_str().to_string())
207+
.or_else(|| sys_locale::get_locale())
208+
.unwrap_or_else(|| Language::EN.locale_str().to_string())
195209
}
196210

197211
pub fn set_locale(loc: &str) {
198212
rust_i18n::set_locale(loc);
199213

200214
// update the current locale
201215
*CURRENT_LOCALE.lock().unwrap() = loc.to_string();
216+
// update persistant locale config, but don't fail the program,
217+
// because locale setting is not that critical.
218+
let set_locale_inner_ = || -> Result<()> {
219+
Configuration::load_from_config_dir()
220+
.set_language(Some(Language::from_str(loc)?))
221+
.write()
222+
};
223+
if let Err(e) = set_locale_inner_() {
224+
error!("unable to save locale settings after changing to '{loc}': {e}");
225+
}
202226
}
203227

204228
/// Get the configured locale string from `configuration.toml`

rim_dev/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// bool::then_some().unwrap_or*() is much clean, clippy, why be a hater?
12
#![allow(clippy::obfuscated_if_else)]
23

34
mod common;

rim_gui/src-tauri/src/common.rs

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use rim::{
1616
update::UpdateCheckBlocker,
1717
AppInfo, InstallConfiguration, UninstallConfiguration,
1818
};
19+
use rim_common::types::Language as DisplayLanguage;
1920
use rim_common::{types::ToolkitManifest, utils};
2021
use serde::{Deserialize, Serialize};
2122
use tauri::{App, AppHandle, Manager, Window, WindowUrl};
@@ -169,44 +170,36 @@ pub(crate) fn uninstall_toolkit_in_new_thread(window: tauri::Window, remove_self
169170

170171
#[derive(serde::Serialize)]
171172
pub struct Language {
172-
pub id: String,
173-
pub name: String,
173+
id: String,
174+
name: String,
174175
}
175176

176177
#[tauri::command]
177178
pub(crate) fn supported_languages() -> Vec<Language> {
178-
rim::Language::possible_values()
179+
DisplayLanguage::possible_values()
179180
.iter()
180181
.map(|lang| {
181-
let id = lang.as_str();
182-
match lang {
183-
rim::Language::EN => Language {
184-
id: id.to_string(),
185-
name: "English".to_string(),
186-
},
187-
rim::Language::CN => Language {
188-
id: id.to_string(),
189-
name: "简体中文".to_string(),
190-
},
191-
_ => Language {
192-
id: id.to_string(),
193-
name: id.to_string(),
194-
},
195-
}
182+
let id = lang.locale_str().to_string();
183+
let name = match lang {
184+
DisplayLanguage::EN => "English".to_string(),
185+
DisplayLanguage::CN => "简体中文".to_string(),
186+
_ => id.clone(),
187+
};
188+
Language { id, name }
196189
})
197190
.collect()
198191
}
199192

200193
#[tauri::command]
201194
pub(crate) fn set_locale(language: String) -> Result<()> {
202-
let lang: rim::Language = language.parse()?;
195+
let lang: DisplayLanguage = language.parse()?;
203196
utils::set_locale(lang.locale_str());
204197
Ok(())
205198
}
206199

207200
#[tauri::command]
208201
pub(crate) fn get_locale() -> String {
209-
rust_i18n::locale().to_string()
202+
utils::get_locale()
210203
}
211204

212205
#[tauri::command]

rim_gui/src-tauri/src/manager_mode.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ use rim::{
1414
toolkit::{self, Toolkit},
1515
update::{self, UpdateCheckBlocker, UpdateOpt},
1616
};
17-
use rim::{
18-
configuration::{Configuration, UpdateTarget, DEFAULT_UPDATE_CHECK_DURATION},
19-
get_toolkit_manifest, AppInfo,
17+
use rim::{get_toolkit_manifest, AppInfo};
18+
use rim_common::types::{
19+
Configuration, ToolkitManifest, UpdateTarget, DEFAULT_UPDATE_CHECK_DURATION,
2020
};
21-
use rim_common::{types::ToolkitManifest, utils};
21+
use rim_common::utils;
2222
use tauri::{async_runtime, AppHandle, Manager};
2323
use url::Url;
2424

@@ -55,6 +55,7 @@ pub(super) fn main(
5555
check_updates_in_background,
5656
get_toolkit_from_url,
5757
common::supported_languages,
58+
common::get_locale,
5859
common::set_locale,
5960
common::app_info,
6061
self_update_now,

rim_gui/src/i18n.ts

Lines changed: 0 additions & 37 deletions
This file was deleted.

rim_gui/src/main.ts

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,52 @@ import App from './App.vue';
33
import { router } from './router';
44
import theme from './theme';
55
import 'virtual:uno.css';
6-
import i18n from './i18n';
6+
import { createI18n } from 'vue-i18n';
7+
import enUS from '../../locales/en-US.json';
8+
import zhCN from '../../locales/zh-CN.json';
9+
import { invokeCommand } from './utils';
10+
11+
// rust-i18n uses Ruby-on-rails styled placeholder `%{}`,
12+
// but vue-i18n uses its own (maybe) placeholder style where the
13+
// `%` is not needed, therefore the percent sign need to be removed
14+
// before passing to vue-i18n.
15+
// NB (J-ZhengLi): I would use custom formatter if the documentation of
16+
// vue-i18n is not that damn limited!
17+
function convertRailsPlaceholders(obj: any): any {
18+
if (typeof obj === 'string') {
19+
return obj.replace(/%\{(\w+)\}/g, '{$1}')
20+
} else if (Array.isArray(obj)) {
21+
return obj.map(convertRailsPlaceholders)
22+
} else if (typeof obj === 'object' && obj !== null) {
23+
const result: Record<string, any> = {}
24+
for (const key in obj) {
25+
result[key] = convertRailsPlaceholders(obj[key])
26+
}
27+
return result
28+
}
29+
return obj
30+
}
31+
32+
async function setup() {
33+
const locale = await invokeCommand('get_locale') as string;
34+
console.log("current locale:", locale);
35+
const i18n = createI18n({
36+
legacy: false,
37+
locale,
38+
messages: {
39+
'en-US': convertRailsPlaceholders(enUS),
40+
'zh-CN': convertRailsPlaceholders(zhCN),
41+
}
42+
});
43+
44+
createApp(App)
45+
.use(router)
46+
.use(theme)
47+
.use(i18n)
48+
.mount('#app');
49+
}
750

851
// disable context menu on right click
952
document.addEventListener('contextmenu', event => event.preventDefault());
1053

11-
createApp(App)
12-
.use(router)
13-
.use(theme)
14-
.use(i18n)
15-
.mount('#app');
54+
setup();

0 commit comments

Comments
 (0)