# glossa
[](https://crates.io/crates/glossa)
[](https://docs.rs/glossa)
[](../License)
<details open>
<summary>
<img alt="Language/语言" src="./svg/language.svg" />
</summary>
- [zh-Hant: 繁體中文](Readme-zh-Hant.md)
- [en: English](Readme.md)
- [zh: 简体中文](Readme-zh.md)
</details>
<details open>
<summary>
<img alt="目录" src="./svg/toc/目录.svg"/>
</summary>
- [Locale Fallback 链](#locale-fallback-链)
- [案例:zh-Hans-HK](#案例zh-hans-hk)
- [案例:en-AU](#案例en-au)
- [例子: gsw-LI](#例子-gsw-li)
- [实战](#实战)
- [codegen](#codegen)
- [LocaleContext](#localecontext)
- [Trait 例子](#trait-例子)
- [双语](#双语)
</details>
## Locale Fallback 链
glossa crate 的核心功能:
- 根据当前语言与所有语言的**相似性**,生成一个数组。
- (理论上)与当前语言的相似性越高,排名越前。
Q: 为什么需要 fallback?
A:
因为当 current locale 的本地化文本缺失时,fallback 到您更为熟悉的语言(e.g., 当前语言的其他变体)可以确保良好的用户体验。
> 一个人可以掌握不同的语言(或者是同一门语言的不同变体)
假设当前 locale 为 pt-PT(português, Portugal), 所有本地化资源的 locales 为 pt-PT, pt(português, Brasil), es-419(español, Latinoamérica), en。
此时 i18n 库应根据 `[pt-PT, pt, en]` 的顺序来获取本地化文本,而非 `[pt-PT, en]`。
若忽略了语言的相似性,直接 fallback 到 en,则不仅降低了本地化(L10n)覆盖率,还可能会增加使用者的认知负担。
### 案例:zh-Hans-HK
假设当前 locale 为 zh-Hans-HK, 所有本地化资源的 locales 为 zh-Hant-MO, zh-SG, ru, zh-Hant, fr, zh, ar, zh-HK, en-001, lzh。
调用 `try_init_chain()` 后,自动生成的 locale 链为: `["zh", "zh-SG", "zh-HK", "zh-Hant-MO", "zh-Hant"]`
当 log level 为 debug 或 trace 时,我们能看到 `[... DEBUG glossa::fallback] ...<(id, score)>`:
```rust
[
("zh", 37), // zh-Hans-CN
("zh-SG", 36), // zh-Hans-SG
("zh-HK", 35), // zh-Hant-HK
("zh-Hant-MO", 31)
("zh-Hant", 28) // zh-Hant-TW
]
```
> 分数越高,优先级越高
- 完全相同,得满分(50分)
- 部分相同
- 相同语言:+20分
- 由于当前语言为 zh (中文),且内置规则不包含其他语言,因此语言链里只有 ^zh。
- 从理论上来说, lzh(文言)与现代汉语之间也有一定的相似性,只不过内置的 zh-Hans-HK 的 fallback 规则不包含 lzh。
- 相同 script:+15分
- 当前 script 为 Hans (简体),Hans 的分数比 Hant 更高
- zh-HK 本质上是 zh-Hant-HK。
- 由于 Hans 的分数高于 Hant,且当前存在 ^zh-Hans 的本地化资源,因此 zh-HK 的分数并不是最高的。
- 符合内置 fallback 规则的语言,享受加分
- 完全符合:+3+6 => +9分
- 只符合 lang+script:+6分
- 相同region:+4分
- 将 zh-Hant (zh-Hant-TW), zh-Hant-MO, zh-HK (zh-Hant-HK) 进行对比
- zh-HK 与当前 locale (zh-Hans-HK) 是相同的区域(HK)。
- zh-HK: +4分
- 由于 zh-Hant 与 zh-Hant-MO 的区域与当前区域(HK) 不同,故无法享受 +4 分的优待。
- 相近地域,享受加分
- 共同位于同一大洲的子区域(比如同时位于东亚地区): +2分
- 共同位于同一大洲 (比如同时位于亚洲): +1分
- 将 zh(zh-Hans-CN) 与 zh-SG (zh-Hans-SG) 进行对比。
- 当前 locale (zh-Hans-HK) 的区域为 HK, HK(中国香港)作为中国的一部分,与CN(中国内地)共同位于东亚地区;而 SG(新加坡) 位于东南亚,与 HK 共同位于亚洲。
- zh: +2分
- zh-SG: +1分
### 案例:en-AU
假设当前 locale 为 en-AU,不同地区的本地化资源非常齐全(包括几乎无人的小岛屿)。
从语言相似性的角度来说,en-NZ (New Zealand English) 与 en-AU (Australian English) 的关系,会比 en-GB(British English) 更为密切。
遗憾的是, glossa 生成的 chain 不能保证 100% 的准确度。
```rust
// <(id, score)>:
[
("en-AU", 50), ("en-GB", 44), ("en-CC", 43), ("en-CX", 43), ("en-NF", 43),
("en-NZ", 43), ("en-UM", 42), ("en-CK", 42), ("en-DG", 42), ("en-FJ", 42),
("en-FM", 42), ("en-KI", 42), ("en-NR", 42), ("en-NU", 42), ("en-PG", 42),
("en-PN", 42), ("en-PW", 42), ("en-SB", 42), ("en-TK", 42), ("en-TO", 42),
("en-TV", 42), ("en-VU", 42), ("en-WS", 42), ("en-AS", 42), ("en-GU", 42),
("en-MH", 42), ("en-MP", 42), ("en-US", 22), ...
]
```
### 例子: gsw-LI
> gsw 是瑞士德语(Schwiizertüütsch),de 是德国德语(Deutsch)。
```rust
use glossa::{
error::GlossaError, fallback::conv_to_str_chain,
try_init_chain_from_slice,
};
let chain = try_init_chain_from_slice(
// current:
"gsw-LI",
// all_locales:
&[
"en", "es", "pt", "zh", "gsw", "gsw-FR", "gsw-LI", "de", "de-AT", "de-BE", "de-CH", "de-IT",
"de-LI", "de-LU",
],
)?;
// <(id, score)>:
// [ ("gsw-LI", 50), ("gsw", 37), ("gsw-FR", 37), ("de-LI", 27), ("de", 26),
// ("de-AT", 23), ("de-BE", 23), ("de-CH", 23), ("de-LU", 23), ("de-IT", 22) ]
let v = conv_to_str_chain(&chain);
assert_eq!(
v.as_ref(),
[
"gsw-LI", "gsw", "gsw-FR", "de-LI", "de", "de-AT", "de-BE", "de-CH",
"de-LU", "de-IT",
]
);
```
## 实战
> 我们需要根据 glossa-codegen 生成的**本地化资源(L10n Map)**的类型,来实现相应的逻辑。
### codegen
```rust
use glossa_codegen::{Generator, L10nResources, Visibility, generator::MapType};
let generator = Generator::default()
.with_resources(L10nResources::new("locales").with_include_map_names(["yes-no"]))
.with_visibility(Visibility::Pub);
```
Generator 支持输出多种不同的类型。
若我们调用了 `generator.output_match_fn_all_in_one_without_map_name(MapType::Regular)?`,则其输出的内容如下所示。
```rust
pub const fn map(language: &[u8], key: &[u8]) -> &'static str {
match (language, key) {
(b"cs", b"cancel") => r#####"Zrušit"#####,
(b"cs", b"no") => r#####"Ne"#####,
(b"cs", b"yes") => r#####"Ano"#####,
(b"de", b"cancel") => r#####"Abbrechen"#####,
(b"de", b"no") => r#####"Nein"#####,
(b"de", b"yes") => r#####"Ja"#####,
(b"en", b"cancel") => r#####"Cancel"#####,
(b"en", b"no") => r#####"No"#####,
(b"en", b"ok") => r#####"OK"#####,
(b"en", b"yes") => r#####"Yes"#####,
(b"es", b"cancel") => r#####"Cancelar"#####,
(b"es", b"ok") => r#####"Aceptar"#####,
(b"es", b"yes") => r#####"Sí"#####,
(b"fr", b"cancel") => r#####"Annuler"#####,
(b"fr", b"no") => r#####"Non"#####,
(b"fr", b"yes") => r#####"Oui"#####,
(b"ja", b"cancel") => r#####"取消"#####,
(b"ja", b"no") => r#####"いいえ"#####,
(b"ja", b"ok") => r#####"了解"#####,
(b"ja", b"yes") => r#####"はい"#####,
(b"ko", b"cancel") => r#####"취소"#####,
(b"ko", b"no") => r#####"아니오"#####,
(b"ko", b"ok") => r#####"확인"#####,
(b"ko", b"yes") => r#####"예"#####,
(b"ru", b"no") => r#####"Нет"#####,
(b"ru", b"yes") => r#####"Да"#####,
(b"zh-Hant", b"cancel") => r#####"取消"#####,
(b"zh-Hant", b"no") => r#####"否"#####,
(b"zh-Hant", b"ok") => r#####"確定"#####,
(b"zh-Hant", b"yes") => r#####"是"#####,
(b"zh-Latn-CN", b"cancel") => r#####"QuXiao"#####,
(b"zh-Latn-CN", b"no") => r#####"Fou"#####,
(b"zh-Latn-CN", b"ok") => r#####"QueDing"#####,
(b"zh-Latn-CN", b"yes") => r#####"Shi"#####,
_ => "",
}
}
```
调用 `generator.output_locales_fn(MapType::Regular, true)?` 后, 我们将得到如下函数。
```rust
// super: use glossa_shared::lang_id;
pub const fn all_locales() -> [super::lang_id::LangID; 10] {
#[allow(unused_imports)]
use super::lang_id::RawID;
use super::lang_id::consts::*;
[
lang_id_cs(),
lang_id_de(),
lang_id_en(),
lang_id_es(),
lang_id_fr(),
lang_id_ja(),
lang_id_ko(),
lang_id_ru(),
lang_id_zh_hant(),
lang_id_zh_pinyin(),
]
}
```
### LocaleContext
接下来,我们需要根据 codegen 生成的代码/数据的类型,来实现查询本地化文本的逻辑。
由上文可知,codegen 生成了 `match_fn`。
根据函数的定义: `const fn map(language: &[u8], key: &[u8]) -> &'static str`
我们编写了如下的查询逻辑:
```rust
s => Some(s),
};
```
如果 codegen 生成的函数的定义为: `const fn map(language: &[u8], map_name: &[u8], key: &[u8]) -> &'static str`,则查询逻辑也不一样。
我们可以这样子写:
```rust
s => Some(s),
};
```
若 codegen 生成了 bincode file,则将其反序列化后,会得到普通的 HashMap 或 BTreeMap。
我们可以使用 `map.get(&language)?.get(&(map_name, key))` 进行查询。
```rust
let map = glossa_shared::decode::file::decode_file_to_maps(path)?;
.get(language)?
.get(&tuple_key)
};
```
### Trait 例子
```rust
use glossa::{LocaleContext, traits::ChainProvider};
trait GetL10nText: ChainProvider {
fn try_get_by_key<'t>(&self, key: &[u8]) -> Option<&'t str> {
let lookup = |(language, key)| match map(language, key) {
"" => None,
s => Some(s),
};
self
.provide_chain()?
.iter()
.map(|id| (id.as_bytes(), key))
.find_map(lookup)
}
}
impl GetL10nText for LocaleContext {}
#[test]
pub(crate) fn print_l10n_text() {
let new_ctx = || LocaleContext::default().with_all_locales(all_locales());
// #[cfg(any(target_os = "macos", target_os = "linux"))]
let display = |ctx: &LocaleContext, key: &str| {
let text = ctx
.try_get_by_key(key.as_bytes())
.unwrap_or_else(|| panic!("{}", glossa::Error::new_text_not_found(key)));
println!("{key}: {text}")
};
{
// set_env_lang("gsw_CH.UTF-8");
//
let ctx = new_ctx()
.with_current_locale(Some(glossa_shared::lang_id::consts::lang_id_gsw()));
// [("de", 26)]
for key in ["yes", "no", "ok", "cancel"] {
display(&ctx, key)
}
}
// Output:
// yes: Ja
// no: Nein
// ok: OK
// cancel: Abbrechen
{
set_env_lang("zh_MO.UTF-8");
// new_ctx(); // current_locale => get_static_locale()
let ctx = new_ctx().with_current_locale(None);
log::debug!("\n---\n--- current locale => zh-MO");
// [("zh-Hant", 43), ("zh-Latn-CN", 22)]
for key in ["yes", "no", "ok", "cancel", "confirm"] {
display(&ctx, key)
}
}
// Output:
// yes: 是
// no: 否
// ok: 確定
// cancel: 取消
// confirm: Confirm
}
```
### 双语
**场景1**:
在某些资源受限环境中,汉字可能无法正常显示。
这时候,我们可以将本地化语言切换为汉语拼音。
由于 汉语-普通话 中存在 同音多义字,因此在只能用拼音不能用汉字的情况下,可能会产生歧义。
此时,就是“双语功能”闪亮登场✨的时刻了!
> 我们需要手动实现 “双语功能”。
---
```rust
#[ignore]
#[test]
// en-GB, zh-pinyin
fn test_bilingual() {
use glossa_shared::lang_id::consts::{lang_id_en_gb, lang_id_zh_pinyin};
let new_ctx = |id| {
LocaleContext::default()
.with_current_locale(Some(id))
.with_all_locales(all_locales())
};
let zh_pinyin_ctx = new_ctx(lang_id_zh_pinyin());
let en_gb_ctx = new_ctx(lang_id_en_gb());
fn get_text<'a>(ctx: &LocaleContext, key: &str) -> Option<&'a str> {
let key_bytes = key.as_bytes();
let lookup = |language| match map(language, key_bytes) {
"" => None,
x => Some(x),
};
ctx
.get_or_try_init_chain()?
.iter()
.map(|id| id.as_bytes())
.find_map(lookup)
}
let zh_pinyin_text = get_cancel_text(&zh_pinyin_ctx);
let en_gb_text = get_cancel_text(&en_gb_ctx);
let text = match zh_pinyin_text == en_gb_text {
true => zh_pinyin_text.into(),
_ => glossa_shared::fmt_compact!("{en_gb_text}; {zh_pinyin_text}"),
};
assert_eq!(text, "Cancel; QuXiao")
}
```