日本語

Overview

hypersig_i18n keeps a text and its translations together in one value, so a text missing a translation into any language fails to compile.

Add the crate

Add it next to hypersig. Its axum feature, on by default, adds the redirect from / that Choosing the language describes.

Cargo.toml
[dependencies]
hypersig = "0.2"
hypersig_i18n = "0.2"

Declare the languages

List the languages in an enum with #[derive(Segment)], and implement Locale for it. Locale::Text names a struct with one field per language, and pick returns the field of the language. serde serializes each variant to the language's BCP 47 tag, which tag returns, so with #[serde(rename_all = "lowercase")] the tags are en and 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");

A missing translation fails to compile

A text is a struct literal, and Rust requires a literal to give every field, so a text without its Japanese fails to compile. A language added to the enum also causes compile errors in two places until it is handled. One is the match in pick, and the other is the struct. Every text has to give the field added to the struct.

// error[E0063]: missing field `ja` in initializer of `ByLang<_>`
let title = ByLang { en: "Settings" };

What stays unchecked

  • Locale::ALL lists the languages for negotiate, and the compiler does not check that it lists every variant. A variant missing from it is never chosen.
  • The check is that a translation exists, not that it is correct. A Japanese field that holds English compiles.
  • Plural rules and the formats of numbers and dates are not handled. Write them in the application, or use a library such as ICU4X.
This site is built the same way. Every page is one Rust function, rendered once in English and once in Japanese, and each of its texts is written as an L { en, ja }.