10 个版本

0.0.1-alpha.102023年5月30日
0.0.1-alpha.92023年4月20日
0.0.1-alpha.42023年3月27日

#51国际化 (i18n)


3 个包中使用

Apache-2.0

185KB
4K SLoC

γλῶσσα

glossa-version

Documentation

Apache-2 licensed

中文

Glossa 是一个语言本地化库。

功能

它可以分为两类。

  • 常量映射:通过常量数据高效地加载本地化数据。
    • 描述:在编译时将配置文件转换为常量(const fn)代码,并在运行时读取常量数据。
    • 优点:高效
    • 缺点
      • 需要 codegen,可能产生一些冗余代码。
      • 目前仅支持简单的键值对 (K-V)。
  • fluent
    • 描述:在运行时管理 Fluent 资源。
    • 优点:Fluent 语法可能更适合本地化。
    • 缺点:比 const map 需要更多资源。

注意:Fluent 也支持在编译时加载本地化资源(本地化文件),但数据需要在运行时解析。
前者只是使用一些来自 phf 的常量映射存储数据的简单 K-V 对。因为它简单,所以高效。

这两种功能互不依赖。对于后者,请阅读 Fluent.md

代码生成

glossa-codegen-version

使用代码生成器生成代码。

特性

glossa-codegen 具有以下特性

  • yaml
    • 默认启用。
    • 默认文件扩展名为 "yaml" 或 "yml"
  • ron
    • 默认扩展名为 "ron"
  • toml
    • 扩展名为 "toml"
  • json
    • 扩展名为 "json"
  • highlight

除了 highlight 之外,这对应于不同的配置格式。您可以选择所有功能或根据需要添加它们。

文件和映射名称

默认情况下,文件格式根据文件名扩展名确定,映射名称(表名)根据文件名设置。是否需要在编译时进行反序列化由启用的功能确定。

yaml map name

假设在目录 assets/l10n/en 下有两个文件,分别命名为 test.yamltest.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 in the single project structure.svg

稍微复杂的多项目结构

build_rs in the multi projects.svg

您也可以手动指定 build.rs 的路径,而不是使用默认路径。


build.rs

generator.svg

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 的项目,它支持各种语言。这只是一个展示其本地化和语法高亮效果的示例。

hl

L10n + 常量语法高亮 = 😍

别担心,我们会慢慢来。在完成初学者教程后,我们将介绍这些功能。

顺便说一下,它可能没有您想象的那么完美。

如果在编译前选择 Monokai 主题,它将生成带有 Monokai 主题的高亮文本。

如果我们需要类似 One Darkayu-dark 的主题,我们可以在运行时生成它们,或者在编译期间为每个主题生成高亮文本。

后者是在空间(二进制文件大小)和时间之间的权衡。

构建

运行 cargo b 后,代码将自动生成。如果您的 l10n rs 文件是 src/assets/localisation.rs,则还需要手动将 pub(crate) mod assets; 添加到 lib.rsmain.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之前,请参考上一节中的必要准备工作。

highlight_struct.svg

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)?;

高亮资源

HighLightRes_struct.svg

您可以使用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;

高亮格式

首先,让我解释一下“映射名称”这个术语的含义。

about map name

映射指的是映射关系,通常被称为“表”。

因为它将本地化文本转换为映射,因此理解与高亮文本对应的映射名称至关重要。

HighLightFmt_struct.svg

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"));

额外

extra_theme_map.svg

语法高亮是可选的。如果需要语法高亮,则主题是必需的。

之前,我们在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)

高亮文件映射

highlight_struct.svg

以下语句用于创建高亮文件映射。

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。

总结

highlight_struct.svg

HighLightRes_struct.svg

HighLightFmt_struct.svg extra_theme_map.svg

依赖关系

~2–11MB
~112K SLoC