10 个版本
0.0.1-alpha.10 | 2023年5月30日 |
---|---|
0.0.1-alpha.9 | 2023年4月20日 |
0.0.1-alpha.4 | 2023年3月27日 |
#51 在 国际化 (i18n) 中
在 3 个包中使用
185KB
4K SLoC
γλῶσσα
Glossa 是一个语言本地化库。
功能
它可以分为两类。
- 常量映射:通过常量数据高效地加载本地化数据。
- 描述:在编译时将配置文件转换为常量(
const fn
)代码,并在运行时读取常量数据。 - 优点:高效
- 缺点
- 需要
codegen
,可能产生一些冗余代码。 - 目前仅支持简单的键值对 (K-V)。
- 需要
- 描述:在编译时将配置文件转换为常量(
- fluent
- 描述:在运行时管理 Fluent 资源。
- 优点:Fluent 语法可能更适合本地化。
- 缺点:比
const map
需要更多资源。
注意:Fluent 也支持在编译时加载本地化资源(本地化文件),但数据需要在运行时解析。
前者只是使用一些来自 phf 的常量映射存储数据的简单 K-V 对。因为它简单,所以高效。
这两种功能互不依赖。对于后者,请阅读 Fluent.md。
代码生成
使用代码生成器生成代码。
特性
glossa-codegen
具有以下特性
- yaml
- 默认启用。
- 默认文件扩展名为 "yaml" 或 "yml"
- ron
- 默认扩展名为 "ron"
- toml
- 扩展名为 "toml"
- json
- 扩展名为 "json"
- highlight
除了 highlight
之外,这对应于不同的配置格式。您可以选择所有功能或根据需要添加它们。
文件和映射名称
默认情况下,文件格式根据文件名扩展名确定,映射名称(表名)根据文件名设置。是否需要在编译时进行反序列化由启用的功能确定。
假设在目录 assets/l10n/en
下有两个文件,分别命名为 test.yaml
和 test.yml
,则我们可以认为它们具有相同的名称。
为了避免冲突,它们的映射名称分别是
- test
- test.yml
如果我们有这些文件
- test.yaml
- test.json
- test.yml
- test.ron
- test.toml
排序后结果为
- test.json
- test.ron
- test.toml
- test.yaml
- test.yml
只有名为 test
的映射在 test.json 中,其余映射的名称都对应各自的文件名。
当使用
.get()
与MapLoader
一起使用时,您需要传递映射名称
准备工作
在编写 build.rs
之前,我们需要准备本地化资源文件。
de (Deutsch, Lateinisch, Deutschland)
- assets/l10n/de/error.yaml
text-not-found: Kein lokalisierter Text gefunden
en (English, Latin, United States)
- assets/l10n/en/error.yaml
text-not-found: No localized text found
en-GB (English, Latin, Great Britain)
- assets/l10n/en-GB/error.yaml
text-not-found: No localised text found
es (español, latino, España)
- assets/l10n/es/error.yaml
text-not-found: No se encontró texto localizado
pt (português, latim, Brasil)
- assets/l10n/pt/error.yaml
注意:“pt”指的是“葡萄牙语(巴西)”,而不是“葡萄牙语(葡萄牙)”。
text-not-found: Nenhum texto localizado encontrado
构建脚本
首先,添加依赖项
cargo add --build glossa-codegen
然后开始创建 build.rs
。
此文件与 Cargo.toml 处于同一级别。
项目结构和文件位置
对于简单的单一项目结构
稍微复杂的多项目结构
您也可以手动指定
build.rs
的路径,而不是使用默认路径。
build.rs
use glossa_codegen::{consts::*, prelude::*};
use std::{io, path::PathBuf};
fn main() -> io::Result<()> {
// Specify the version as the current package version to avoid repetitive compilation for the same version.
let ver = get_pkg_version!();
// This is a constant array: ["src", "assets", "localisation.rs"], which is converted into a path for storing automatically generated Rust code related to localisation.
// path: "src/assets/localisation.rs".
let rs_path = PathBuf::from_iter(default_l10n_rs_file_arr());
// If it's the same version, then exit.
if is_same_version(&rs_path, Some(ver))? {
// When developing, we can comment out the `return` statement below so that every change will be recompiled and won't exit prematurely.
return Ok(());
}
// If the path is "src/assets/localisation.rs", then it will append `mod localisation;` and related `use` statements to "src/assets/mod.rs".
append_to_l10n_mod(&rs_path)?;
// A new file will be created here:
// - Linux(non android): "/dev/shm/localisation.tmp"
// - Other:"src/assets/localisation.tmp"
// After the code generation is complete, rename(move) the file: "/dev/shm/localisation.tmp" -> "src/assets/localisation.rs".
// Note: If written directly to `rs_path` and `cargo` is interrupted during building, it may result in incomplete generated code. Therefore, `tmp_path` is used as a temporary buffer file.
let tmp_path = get_shmem_path(&rs_path)?;
let writer = MapWriter::new(&tmp_path, &rs_path);
// default_l10n_dir_arr() is also a constant array: ["assets", "l10n"].
// If the current localisation resource path is at the parent level, then you can use `path = PathBuf::from_iter([".."].into_iter().chain(default_l10n_dir_arr()));`.
let l10n_path = PathBuf::from_iter(default_l10n_dir_arr());
let generator = Generator::new(l10n_path).with_version(ver);
// Invoke the generator here to generate code.
generator.run(writer)
}
MapWriter
我们上面创建了一个写入器。
现在让我们修改代码,将 writer
改为 mut writer
,以便它可以被修改。
// Whether to automatically generate documentation, defaults to true
*writer.get_gen_doc_mut() = false;
// Modify the visibility of the automatically generated function, defaults to `pub(crate)`
*writer.get_visibility_mut() = "pub(super)";
附加信息
以上内容是最基本的用法,但实际上还有更多高级用法。
从 0.0.1-alpha.4 版本开始,可以在编译时将本地化文本编译为具有 语法高亮 的字符串。
与在运行时缓存/解析正则表达式相比,常量字符串不需要昂贵的运行时解析。
以下是正在开发中的 CLI 工具的帮助信息截图,该工具使用 glossa-codegen
的高级功能。
请忽略内部使用的语言。作为一个支持 l10n 的项目,它支持各种语言。这只是一个展示其本地化和语法高亮效果的示例。
L10n + 常量语法高亮 = 😍
别担心,我们会慢慢来。在完成初学者教程后,我们将介绍这些功能。
顺便说一下,它可能没有您想象的那么完美。
如果在编译前选择 Monokai
主题,它将生成带有 Monokai
主题的高亮文本。
如果我们需要类似 One Dark
和 ayu-dark
的主题,我们可以在运行时生成它们,或者在编译期间为每个主题生成高亮文本。
后者是在空间(二进制文件大小)和时间之间的权衡。
构建
运行 cargo b
后,代码将自动生成。如果您的 l10n rs 文件是 src/assets/localisation.rs
,则还需要手动将 pub(crate) mod assets;
添加到 lib.rs
或 main.rs
(取决于您的 crate 类型)。
获取文本
代码生成完毕后,让我们写一个函数来测试它!
但在那之前,我们需要添加一些依赖项。
cargo add phf glossa
测试函数如下
#[test]
fn new_loader() {
use crate::assets::localisation::locale_hashmap;
use glossa::{fallback::FallbackChain, GetText, MapLoader};
let loader = MapLoader::new(locale_hashmap());
loader.show_chain();
// Here, for simplicity, `get_or_default()` is used.
// Actually, the usage of `.get()` is the same, but it returns Result<&str>, not Cow<str>.
let msg = loader.get_or_default("error", "text-not-found");
assert_eq!(msg, "No localized text found");
}
如果你的系统语言是“en”,测试应该通过。
请注意,locale_hashmap()
不是一个const fn
,而是一个普通函数。然而,这并不意味着它特别昂贵。
HashMap查询操作的时间复杂度是O(1)。
其值指向一个子映射,所有子映射及其子映射都是consts
。
此外,如果启用了ahash
功能,默认情况下将使用ahash的RandomState,而不是std::collections
。
您还可以使用OnceCell创建全局静态数据,仅创建一次数据。
pub(crate) fn locales() -> &'static MapLoader {
static RES: OnceCell<MapLoader> = OnceCell::new();
RES.get_or_init(|| MapLoader::new(locale_hashmap()))
}
等一下,不要在这些事情上浪费时间,我们之前的测试失败了。
好的,让我们回顾一下之前做了什么。
我们之前为德语、西班牙语和葡萄牙语创建了本地化资源文件。
首先,它会自动检测系统语言。如果本地化资源不存在,它会自动使用备用链。如果本地化资源存在,并且你的系统语言不是英语,那么上述测试将失败。
让我们继续测试
let loader = locales();
let msg = loader.get("error", "text-not-found")?;
假设你的语言是德语(de-Latn-DE)
assert_eq!(msg, "Kein lokalisierter Text gefunden");
西班牙语(es-Latn-ES)
assert_eq!(msg, "No se encontró texto localizado");
葡萄牙语(pt-Latn-BR)
assert_eq!(msg, "Nenhum texto localizado encontrado");
常量语法高亮文本
你需要启用highlight
功能
cargo add --build glossa-codegen --features=highlight
在build.rs
文件中,你需要导入以下模块
use glossa_codegen::{
consts::*,
highlight::{HighLight, HighLightFmt, HighLightRes},
prelude::*,
};
use std::{
borrow::Cow,
collections::HashMap,
ffi::OsStr,
fs::File,
io::{self, BufWriter},
path::PathBuf,
};
快速入门
让我们从一个简单的例子开始!
在创建
generator
之前,请参考上一节中的必要准备工作。
let mut generator = Generator::new(path).with_version(ver);
// Use the default syntax highlighting resources.
// The default theme is Monokai Extended, and the default syntax set only contains a few syntaxes.
let res = HighLightRes::default();
let os_str = |s| Cow::from(OsStr::new(s));
// The default format is markdown, and the default map name suffix is `_md`.
let fname_and_fmt = |s| (os_str(s), HighLightFmt::default());
// Specify the file names that need to be highlighted.
let map = HashMap::from_iter([fname_and_fmt("opt.toml"), fname_and_fmt("parser.yaml")]);
*generator.get_highlight_mut() = Some(HighLight::new(res, map));
generator.run(writer)?;
高亮资源
您可以使用syntect
加载自定义主题集和语法集。
这些资源本质上是Sublime主题和语法。您可以使用HighLightRes::new()
来指定theme_set
,或者首先使用*res.get_theme_set_mut()
获取主题集的可变引用,然后修改它。
let mut res = HighLightRes::default();
// *res.get_theme_set_mut() = custom theme set
// *res.get_syntax_set_mut() = custom syntax set // requires 'static lifetime, you can create it using OnceCell
// Custom theme name, e.g. ayu-dark
*res.get_name_mut() = Cow::from("ayu-dark");
// Whether to enable theme background
*res.get_background_mut() = false;
高亮格式
首先,让我解释一下“映射名称”这个术语的含义。
映射指的是映射关系,通常被称为“表”。
因为它将本地化文本转换为映射,因此理解与高亮文本对应的映射名称至关重要。
let mut fmt = HighLightFmt::default();
// Here, the syntax name is specified as "md". By default, the syntax set only supports Markdown, TOML, JSON, and YAML etc.
// If you want to support more syntaxes, you need to customise the syntax-set of HighLightRes.
// md corresponds to the filename extension of the Markdown format.
// You can think of the syntax name as corresponding to different file extensions.
*fmt.get_syntax_mut() = Cow::from("md");
// Modifies the suffix of the default map name.
// Assuming a file is named opt.toml, the raw text corresponds to the map name `opt`.
// Since the suffix is `_markdown`, the map name of the generated highlighted text is `opt_markdown`.
// If it is None, there will be no raw text map, only a highlighted text map.
*fmt.get_suffix_mut() = Some(Cow::from("_markdown"));
额外
语法高亮是可选的。如果需要语法高亮,则主题是必需的。
之前,我们在HighLightRes
中指定了一个常见的主题名称。
如果您需要为更多主题生成高亮文本,您需要修改extra
。
// This tuple contains (map name suffix, (theme name, whether to enable the theme background))
let ayu_light = ("_ayu_light", ("ayu-light", true));
let monokai_bright = ("_Monokai-Bright", ("Monokai Extended Bright", false));
let extra_map = HashMap::from_iter([monokai_bright, ayu_light]);
*fmt.get_extra_mut() = Some(extra_map);
关于常见主题和额外主题
常见主题名称包含在HighLightRes
结构中,而额外主题名称包含在HighLightFmt
中。
强调“名称”的原因是,主题名称可以单独存储,但主题资源不能。
如果您不需要精细地控制“不同文件对应不同主题”,则不需要关注本节。
您可以选择一个常用主题+额外主题,只需修改 *fmt.get_extra_mut()
,例如 Extra
。
如果您不需要常用主题,必须将 HighLightRes
的主题名称设置为空。
*res.get_name_mut() = Cow::from("");
其余的工作是修改不同格式的 extra
。
例如
*md_fmt.get_extra_mut() = Some(ext_map1)
*rs_fmt.get_extra_mut() = Some(ext_map2)
*html_fmt.get_extra_mut() = Some(ext_map3)
高亮文件映射
以下语句用于创建高亮文件映射。
let fname_and_fmt = |s| (os_str(s), HighLightFmt::default());
let map = HashMap::from_iter([fname_and_fmt("opt.toml"), fname_and_fmt("parser.yaml")]);
如果我们将其更改为这样,所有指定的文件都将使用上面指定的 HighLightFmt
let fname_and_fmt = |s| (os_str(s), fmt.clone());
别忘了我们在上面为 fmt
创建了额外主题。
在许多情况下,我们可能需要做更多调整,而不是为所有文件使用额外主题。
// Assuming you have set a custom syntax-set that includes the syntax you want for LaTex.
let mut tex_fmt = HighLightFmt::default();
*tex_fmt.get_suffix_mut() = Some(Cow::from("_tex"));
*tex_fmt.get_syntax_mut() = Cow::from("latex");
// tex_fmt specifies an extra dracula theme.
*tex_fmt.get_extra_mut() =
Some(HashMap::from_iter([("_tex_dracula", ("dracula", false))]));
let dracula_latex = |s| (os_str(s), tex_fmt);
let highlight_map = HashMap::from_iter([
dracula_latex("math.toml"),
fname_and_fmt("file.json"),
fname_and_fmt("test.yaml"),
(os_str("parser.ron"), HighLightFmt::default()),
]);
实际上,以 LaTex 为例可能不太合适,因为它只能提供语法高亮,无法将 LaTex 渲染为 SVG。
总结
依赖关系
~2–11MB
~112K SLoC