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
17pub const SUPPORTED_LANG_ID: &[&str] = &["en-US", "ja-JP"];
19
20pub const DEFAULT_LANG_ID: &str = "en-US";
22
23static CURRENT_RESOURCE_BUNDLE: LazyLock<RwLock<CurrentI18nBundle>> =
24 LazyLock::new(|| RwLock::new(CurrentI18nBundle::new(None)));
25
26pub fn get_locale_lang_id() -> String {
28 get_locale().unwrap_or(DEFAULT_LANG_ID.into())
29}
30
31pub 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
40pub 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 pub fn get_bundle(&self) -> Option<&FluentBundle<FluentResource>> {
56 self.bundle.as_ref()
57 }
58}
59
60#[derive(Error, Debug)]
61pub 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]
119pub 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#[inline]
192pub fn get_lang_bundle() -> &'static RwLock<CurrentI18nBundle> {
193 &CURRENT_RESOURCE_BUNDLE
194}
195
196pub fn i18n(id: &str) -> String {
198 i18n_fmt(id, None)
199}
200
201pub fn i18n_w(id: &str) -> Text<'_> {
203 ftext(i18n_fmt(id, None))
204}
205
206pub fn i18n_fmt_w<'a>(id: &str, args: Option<&FluentArgs<'_>>) -> Text<'a> {
208 ftext(i18n_fmt(id, args))
209}
210
211#[tracing::instrument]
212pub 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
252pub 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}