prehnite_core/
i18n.rs

1#![allow(unused)]
2#![doc = "多言語対応"]
3use crate::settings::registry::SettingRegistry;
4use crate::settings::GlobalSettingKey;
5use crate::widget::font::ftext;
6use fluent_bundle::concurrent::FluentBundle;
7use fluent_bundle::{FluentArgs, FluentError, FluentResource};
8use iced::widget::Text;
9use sqlx::SqliteConnection;
10use std::sync::{Arc, LazyLock, RwLock};
11use sys_locale::get_locale;
12use thiserror::Error;
13use tracing::{debug, error};
14use tracing_unwrap::ResultExt;
15use unic_langid::{LanguageIdentifier, LanguageIdentifierError};
16
17/// 対応言語
18pub const SUPPORTED_LANG_ID: &[&str] = &["en-US", "ja-JP"];
19
20/// デフォルトの言語
21pub const DEFAULT_LANG_ID: &str = "en-US";
22
23static CURRENT_RESOURCE_BUNDLE: LazyLock<RwLock<CurrentI18nBundle>> =
24    LazyLock::new(|| RwLock::new(CurrentI18nBundle::new(None)));
25
26/// ローカルの`lang_id`を取得します。
27pub fn get_locale_lang_id() -> String {
28    get_locale().unwrap_or(DEFAULT_LANG_ID.into())
29}
30
31/// ローカルの言語を取得します。
32pub fn get_locale_language() -> String {
33    if let Ok(v) = get_locale_lang_id().parse::<LanguageIdentifier>() {
34        v.language.to_string()
35    } else {
36        "en".to_string()
37    }
38}
39
40/// 現在読み込まれている言語バンドル
41pub struct CurrentI18nBundle {
42    bundle: Option<FluentBundle<FluentResource>>,
43}
44
45impl CurrentI18nBundle {
46    fn new(bundle: Option<FluentBundle<FluentResource>>) -> Self {
47        Self { bundle }
48    }
49
50    fn set_bundle(&mut self, bundle: Option<FluentBundle<FluentResource>>) {
51        self.bundle = bundle;
52    }
53
54    /// 現在読み込まれている[`FluentBundle`]を取得する。
55    pub fn get_bundle(&self) -> Option<&FluentBundle<FluentResource>> {
56        self.bundle.as_ref()
57    }
58}
59
60#[derive(Error, Debug)]
61/// 多言語対応のエラー
62pub enum I18nError {
63    #[error("Invalid lang id received")]
64    FailedToParseLangId(#[from] LanguageIdentifierError),
65    #[error("Invalid ftl syntax")]
66    FailedToParseFTL((FluentResource, Vec<fluent_syntax::parser::ParserError>)),
67    #[error("Failed to add resource")]
68    FailedToAddResource(Vec<FluentError>),
69    #[error("Failed to execute statements")]
70    DbError(#[from] sqlx::Error),
71    #[error("Failed to apply settings")]
72    FailedToApplySetting,
73}
74
75impl From<(FluentResource, Vec<fluent_syntax::parser::ParserError>)> for I18nError {
76    fn from(value: (FluentResource, Vec<fluent_syntax::parser::ParserError>)) -> Self {
77        I18nError::FailedToParseFTL(value)
78    }
79}
80
81impl From<Vec<FluentError>> for I18nError {
82    fn from(value: Vec<FluentError>) -> Self {
83        I18nError::FailedToAddResource(value)
84    }
85}
86
87#[derive(Error, Debug)]
88enum TryGetFtlPathError {
89    #[error("language resource not found")]
90    LangNotFound,
91}
92
93fn try_get_ftl_str(lang_id: &str) -> Result<String, TryGetFtlPathError> {
94    Ok(match lang_id {
95        "ja-JP" | "ja" => include_str!("../../assets/locales/ja-JP.ftl"),
96        "en-US" | "en" => include_str!("../../assets/locales/en-US.ftl"),
97        _ => return Err(TryGetFtlPathError::LangNotFound),
98    }
99    .to_string())
100}
101
102fn get_ftl_str(lang_id: &str) -> String {
103    try_get_ftl_str(lang_id).unwrap_or_else(|_| {
104        try_get_ftl_str(DEFAULT_LANG_ID).expect_or_log("Default locale not found.")
105    })
106}
107
108fn parse_lang_bundle(lang_id: &str) -> Result<FluentBundle<FluentResource>, I18nError> {
109    let language_identifier: LanguageIdentifier = lang_id.parse()?;
110    let resource = FluentResource::try_new(get_ftl_str(lang_id))?;
111
112    let mut bundle = FluentBundle::new_concurrent(vec![language_identifier]);
113    bundle.add_resource(resource)?;
114    bundle.set_use_isolating(false);
115    Ok(bundle)
116}
117
118#[tracing::instrument]
119/// 言語バンドルを差し替えます。
120pub async fn change_lang_bundle(arg_lang_id_str: &str) -> Result<(), I18nError> {
121    let (lang_id, lang_id_str): (LanguageIdentifier, &str) = match arg_lang_id_str.parse() {
122        Ok(v) => (v, arg_lang_id_str),
123        Err(e) => {
124            debug!("Parse failed!! set default lang_id ...");
125            debug!("Error: {e:#?}");
126            (DEFAULT_LANG_ID.parse()?, DEFAULT_LANG_ID)
127        }
128    };
129    if CURRENT_RESOURCE_BUNDLE
130        .read()
131        .unwrap_or_log()
132        .get_bundle()
133        .is_none_or(|v| !v.locales.contains(&lang_id))
134    {
135        CURRENT_RESOURCE_BUNDLE
136            .write()
137            .unwrap_or_log()
138            .set_bundle(Some(parse_lang_bundle(lang_id_str)?));
139        if SettingRegistry::immediate_apply(
140            GlobalSettingKey::Locale.into(),
141            lang_id.to_string().into(),
142        )
143        .await
144        .ok_or_log()
145        .is_none()
146        {
147            return Err(I18nError::FailedToApplySetting);
148        };
149    }
150    Ok(())
151}
152
153pub(crate) async fn change_lang_bundle_with_conn(
154    conn: &mut SqliteConnection,
155    arg_lang_id_str: &str,
156) -> Result<(), I18nError> {
157    let (lang_id, lang_id_str): (LanguageIdentifier, &str) = match arg_lang_id_str.parse() {
158        Ok(v) => (v, arg_lang_id_str),
159        Err(e) => {
160            debug!("Parse failed!! set default lang_id ...");
161            debug!("Error: {e:#?}");
162            (DEFAULT_LANG_ID.parse()?, DEFAULT_LANG_ID)
163        }
164    };
165    if CURRENT_RESOURCE_BUNDLE
166        .read()
167        .unwrap_or_log()
168        .get_bundle()
169        .is_none_or(|v| !v.locales.contains(&lang_id))
170    {
171        CURRENT_RESOURCE_BUNDLE
172            .write()
173            .unwrap_or_log()
174            .set_bundle(Some(parse_lang_bundle(lang_id_str)?));
175        if SettingRegistry::immediate_apply_with_conn(
176            conn,
177            GlobalSettingKey::Locale.into(),
178            lang_id.to_string().into(),
179        )
180        .await
181        .ok_or_log()
182        .is_none()
183        {
184            return Err(I18nError::FailedToApplySetting);
185        };
186    }
187    Ok(())
188}
189
190/// 現在の言語バンドルを取得します。
191#[inline]
192pub fn get_lang_bundle() -> &'static RwLock<CurrentI18nBundle> {
193    &CURRENT_RESOURCE_BUNDLE
194}
195
196/// i18nキーから表示内容を取得します。
197pub fn i18n(id: &str) -> String {
198    i18n_fmt(id, None)
199}
200
201/// i18nキーから表示内容を[`Text`]として取得します。
202pub fn i18n_w(id: &str) -> Text<'_> {
203    ftext(i18n_fmt(id, None))
204}
205
206/// i18nキーとフォーマットを使用し表示内容を[`Text`]として取得します。
207pub fn i18n_fmt_w<'a>(id: &str, args: Option<&FluentArgs<'_>>) -> Text<'a> {
208    ftext(i18n_fmt(id, args))
209}
210
211#[tracing::instrument]
212/// i18nキーとフォーマットを使用し表示内容を取得します。
213pub fn i18n_fmt(id: &str, args: Option<&FluentArgs<'_>>) -> String {
214    #[derive(Debug)]
215    enum Error {
216        DoesNotBeInitialized,
217        MessageDoesNotExists,
218        FailedToFetchMessage,
219    }
220    fn func<'a>(id: &str, args: Option<&FluentArgs<'_>>) -> Result<String, Error> {
221        let mut errors = vec![];
222        Ok(get_lang_bundle()
223            .read()
224            .unwrap_or_log()
225            .get_bundle()
226            .ok_or(Error::DoesNotBeInitialized)?
227            .format_pattern(
228                get_lang_bundle()
229                    .read()
230                    .unwrap_or_log()
231                    .get_bundle()
232                    .ok_or(Error::DoesNotBeInitialized)?
233                    .get_message(id)
234                    .ok_or(Error::MessageDoesNotExists)?
235                    .value()
236                    .ok_or(Error::FailedToFetchMessage)?,
237                args,
238                &mut errors,
239            )
240            .to_string())
241    }
242    func(id, args).unwrap_or_else(|e| {
243        match e {
244            Error::DoesNotBeInitialized => error!("i18n does not be initialized."),
245            Error::MessageDoesNotExists => error!("Message {id} does not exist."),
246            Error::FailedToFetchMessage => error!("Unable to get value for message {id}."),
247        }
248        id.to_string()
249    })
250}
251
252/// i18nを初期化します。
253pub async fn initialize_i18n_from_db() -> Result<(), sqlx::Error> {
254    let lang_id =
255        SettingRegistry::get(&GlobalSettingKey::Locale.into()).and_then(|v| v.to_opt_string());
256    change_lang_bundle(lang_id.unwrap_or(get_locale_lang_id()).as_str())
257        .await
258        .expect_or_log("lang_id not found.");
259    Ok(())
260}
261
262pub(crate) async fn initialize_i18n_from_db_with_conn(
263    conn: &mut SqliteConnection,
264) -> Result<(), sqlx::Error> {
265    let lang_id =
266        SettingRegistry::get(&GlobalSettingKey::Locale.into()).and_then(|v| v.to_opt_string());
267    change_lang_bundle_with_conn(conn, lang_id.unwrap_or(get_locale_lang_id()).as_str())
268        .await
269        .expect_or_log("lang_id not found.");
270    Ok(())
271}
272
273#[cfg(test)]
274mod tests {
275    use crate::i18n::{
276        change_lang_bundle_with_conn, get_lang_bundle, parse_lang_bundle, try_get_ftl_str,
277        SUPPORTED_LANG_ID,
278    };
279    use sqlx::SqlitePool;
280
281    #[test]
282    fn valid_check_get_for_all_supported_languages() {
283        for i in SUPPORTED_LANG_ID {
284            try_get_ftl_str(i).unwrap();
285        }
286    }
287
288    #[sqlx::test(migrator = "crate::db::migrate::app_global::MIGRATOR")]
289    async fn valid_check_ftl(pool: SqlitePool) {
290        let mut conn = pool.acquire().await.unwrap();
291        for i in SUPPORTED_LANG_ID {
292            change_lang_bundle_with_conn(&mut *conn, i)
293                .await
294                .expect(format!("Failed to parse ftl: {}", i).as_str());
295            assert!(
296                get_lang_bundle()
297                    .read()
298                    .expect("Failed to read lock lang bundle.")
299                    .get_bundle()
300                    .expect("Failed to get lang bundle.")
301                    .locales
302                    .contains(&i.parse().unwrap())
303            )
304        }
305    }
306
307    #[test]
308    #[should_panic]
309    fn invalid_check_unsupported() {
310        try_get_ftl_str("AYgAV6Lky").unwrap();
311    }
312
313    #[test]
314    #[should_panic]
315    fn invalid_lang_id() {
316        parse_lang_bundle("AYgAV6Lky").unwrap();
317    }
318}