概要
hypersig_i18n は 1 つの文とその訳を 1 つの値にまとめて持ちます。そのため、どれかの言語の訳が欠けた文はコンパイルエラーになります。
クレートを追加する
hypersig と並べて追加します。既定で有効な axum の feature は、言語の選び方 で説明する / からのリダイレクトを追加します。
Cargo.toml
[dependencies]
hypersig = "0.2"
hypersig_i18n = "0.2"言語を宣言する
言語を #[derive(Segment)] を付けた enum に並べ、その enum に Locale を実装します。Locale::Text には言語ごとに 1 つのフィールドを持つ構造体を指定し、pick はその言語のフィールドを返します。serde がバリアントをシリアライズした文字列がその言語の BCP 47 のタグになり、tag はその文字列を返します。そのため #[serde(rename_all = "lowercase")] を付けると、タグは en と ja になります。
use hypersig::Segment;
use hypersig_i18n::Locale;
use serde::{Deserialize, Serialize};
// The segment of each language is its BCP 47 tag, here `en` and `ja`.
#[derive(Segment, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
#[serde(rename_all = "lowercase")]
enum Lang {
En,
Ja,
}
/// One value per language.
struct ByLang<T> {
en: T,
ja: T,
}
impl Locale for Lang {
const ALL: &'static [Self] = &[Self::En, Self::Ja];
type Text<T> = ByLang<T>;
fn pick<T>(self, text: ByLang<T>) -> T {
match self {
Self::En => text.en,
Self::Ja => text.ja,
}
}
}
let greeting = ByLang { en: "Hello", ja: "こんにちは" };
assert_eq!(Lang::Ja.pick(greeting), "こんにちは");
assert_eq!(Lang::Ja.tag(), "ja");
// A message that takes a value is a function per language.
let items: ByLang<fn(u32) -> String> = ByLang {
en: |n| format!("{n} items"),
ja: |n| format!("{n} 件"),
};
assert_eq!(Lang::En.pick(items)(3), "3 items");訳が欠けるとコンパイルエラーになる
文は構造体のリテラルで、Rust はリテラルにすべてのフィールドを書くよう求めます。そのため、日本語の訳がない文はコンパイルエラーになります。enum に言語を足したときも、その言語への対応を書くまで 2 か所がコンパイルエラーになります。1 つは pick の match で、もう 1 つは構造体です。構造体に足したフィールドは、すべての文で書く必要があります。
// error[E0063]: missing field `ja` in initializer of `ByLang<_>`
let title = ByLang { en: "Settings" };検査しないこと
Locale::ALLはnegotiateが選ぶ言語の一覧で、すべてのバリアントが並んでいるかはコンパイラが検査しません。一覧にないバリアントが選ばれることはありません。- 検査するのは訳があることで、訳が正しいことではありません。日本語のフィールドに英語を書いても、コンパイルは通ります。
- 複数形の規則や、数値と日付の書式は扱いません。アプリで書くか、ICU4X のようなライブラリを使ってください。
このサイトも同じ方法で作っています。どのページも 1 つの Rust の関数で、英語と日本語で 1 回ずつ描画し、文はどれも
L { en, ja } で書いています。