glossa 0.0.6

Generates an array based on the similarity between the current locale and all available locales.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# glossa

[![glossa.crate](https://img.shields.io/crates/v/glossa.svg?logo=rust&logoColor=lightsalmon&label=glossa)](https://crates.io/crates/glossa)

[![Documentation](https://docs.rs/glossa/badge.svg)](https://docs.rs/glossa)
[![Apache-2 licensed](https://img.shields.io/crates/l/glossa.svg?logo=apache)](../License)

<!-- Language -->
<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>

<!-- TOC -->
<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
let lookup = |(language, key)| match map(language, key) {
  "" => None,
  s => Some(s),
};
```

如果 codegen 生成的函数的定义为: `const fn map(language: &[u8], map_name: &[u8], key: &[u8]) -> &'static str`,则查询逻辑也不一样。

我们可以这样子写:

```rust
let lookup = |(language, map_name, key)| match map(language, map_name, key) {
  "" => None,
  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)?;
let lookup = |language, tuple_key| {
  map
    .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 set_env_lang = |value| unsafe { std::env::set_var("LANG", value) };

  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 get_cancel_text = |ctx| get_text(ctx, "cancel").unwrap_or_default();

  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")
}
```