ENbext.dev
DocumentationFonctionnalités

Traduction automatique depuis l’anglais. Consulter l’original.

Sur cette page

I18n

La fonctionnalité I18n est la forme utilisée par bext pour restituer les chaînes localisées. Un I18nPlugin prend une clé, des paramètres régionaux et un sac de variables et renvoie le texte traduit. Échangez le plugin et le même appel i18n.t("hello", { name: "Ada" }) lit à partir d'un fichier JSON, d'un bundle Mozilla Fluent ou d'un catalogue ICU MessageFormat - votre code ne change pas.

Cette page couvre la forme du trait, l'énumération typée I18nValue qui permet à la sélection plurielle de fonctionner correctement sur tous les backends, la manière dont la négociation des paramètres régionaux BCP 47 est gérée comme méthode par défaut et ce que font les deux plugins de référence - @bext/i18n-static et @bext/i18n-fluent - derrière la limite du trait.

Le trait#

La forme complète se trouve dans bext-plugin-api::i18n :

rust
pub trait I18nPlugin: Send + Sync {
    fn name(&self) -> &str;

    fn translate(
        &self,
        key: &str,
        locale: &str,
        vars: &TransVars,
    ) -> Result<String, I18nError>;

    fn supported_locales(&self) -> Vec<String>;
    fn fallback_locale(&self) -> String;

    fn has_locale(&self, locale: &str) -> bool { /* default */ }
    fn negotiate(&self, requested: &str) -> String { /* default */ }
}

Quatre méthodes requises, deux valeurs par défaut. translate est le cheval de bataille ; supported_locales et fallback_locale répondent aux questions de configuration ; has_locale et negotiate sont les valeurs par défaut qui offrent gratuitement à chaque backend une recherche BCP 47 fonctionnelle.

TransVars est un HashMap<String, I18nValue> où I18nValue est une énumération balisée sur les quatre types de variables que chaque bibliothèque i18n sérieuse comprend :

rust
pub enum I18nValue {
    String(String),
    I64(i64),
    F64(f64),
    Bool(bool),
}

Pas de dates, pas d'objets imbriqués, pas de types personnalisés. Une date dans une traduction est presque toujours un String préformaté ou un horodatage Unix comme I64, et une union étiquetée de quatre primitives est trivialement portable dans FFI.

Les clés manquantes n'ont jamais d'erreur#

Remarque

Une clé de traduction manquante renvoie Ok("key.that.was.missing") — la clé textuellement — et non une erreur. Cela signifie qu'une faute de frappe dans un nom de clé s'affichera silencieusement en production. Auditez vos clés dans CI avec supported_locales() + has_locale() plutôt que de vous fier aux erreurs d'exécution pour les détecter.

Le contrat de trait est délibérément asymétrique : translate renvoie Result<String, I18nError>, mais le chemin Ok est un rendu au mieux. Si la clé demandée est inconnue, le backend renvoie Ok("key.that.was.missing") — la clé elle-même, textuellement. Si un espace réservé {name} n’a pas de variable correspondante, le backend laisse {name} dans la sortie. Aucun cas d'erreur n'est éliminé, car une traduction manquante ne doit jamais supprimer une interface utilisateur.

Err(I18nError) est réservé à quatre situations que chaque backend peut rencontrer, mais aucune d'entre elles n'est "le concepteur a oublié une clé" :

  • LocaleUnavailable — les paramètres régionaux demandés ont été promis par supported_locales() mais n'ont pas pu être chargé (fichier corrompu, erreur d'analyse, bundle manquant après un rechargement).
  • FormatError — les paramètres régionaux se sont correctement chargés mais le modèle spécifique a échoué (l'espace réservé ICU ne correspondait pas au sac de variables, un appel de fonction Fluent erroné).
  • Unavailable — le backend est temporairement incapable de servir quoi que ce soit pour le moment (rechargement en cours, expulsion du cache).
  • Unknown — une classification que le backend n'a pas pu attribuer, à traiter comme Unavailable.

Le résultat est que le code de l’appelant ne renvoie presque jamais sur Err : il n’apparaît que lorsque quelque chose nécessite réellement l’attention de l’opérateur. Le chemin de rendu courant est un one-liner.

Pourquoi posséder String, pas Cow<'_, str>#

L'esquisse de plan dans 02-capabilities.md utilise Cow<'_, str> afin qu'un backend JSON statique puisse restituer une tranche empruntée d'un modèle interne lorsqu'aucune interpolation n'a eu lieu. Cet emprunt devrait durer aussi longtemps que le backend, ce qui impose une durée de vie au trait - ce qui exclut à son tour la distribution via dyn I18nPlugin, car les méthodes trait-objet ne peuvent pas introduire proprement de nouveaux paramètres de durée de vie.

Le String détenu coûte une allocation par appel et l'échange contre un trait qui fonctionne via FFI, WASM, QuickJS et le pontage asynchrone sans cas particuliers. Pour une capacité appelée une fois par chaîne rendue, cette allocation n’apparaît pas comme un coût significatif dans la pratique.

Négociation BCP 47 comme méthode par défaut#

fallback_locale() renvoie une seule balise - pas une chaîne - car le passage d'une balise demandée spécifique à une balise plus large prise en charge est la recherche RFC 4647 §3.4 standard, et chaque backend devrait obtenir la même réponse pour les mêmes entrées. Le trait le fournit donc par défaut :

rust
fn negotiate(&self, requested: &str) -> String {
    let mut candidate = requested.to_owned();
    loop {
        if self.has_locale(&candidate) {
            return candidate;
        }
        match candidate.rfind('-') {
            Some(idx) => candidate.truncate(idx),
            None => return self.fallback_locale(),
        }
    }
}

Demandez fr-CA, obtenez fr-CA si pris en charge, sinon fr, sinon la solution de secours. Le plugin Fluent remplace negotiate par un matcher plus riche qui prend en compte les sous-balises de script ; le plugin statique hérite de la valeur par défaut. Quoi qu'il en soit, les appelants remettent le Accept-Language ou les paramètres régionaux de session dont ils disposent et récupèrent une balise que le plugin peut réellement servir.

Les deux plugins de référence#

@bext/i18n-static#

crates/bext-impls/bext-i18n-static est le backend de développement et de petits projets. Il charge un répertoire de fichiers JSON — un par locale — et effectue une interpolation de style {name} :

json
{
  "hello": "Hello, {name}!",
  "items": "{count} items"
}

La seule dépendance au-delà de la caisse de traits est serde_json. load_dir("en", "./locales") lit en.json, fr.json, fr-CA.json, etc. — le nom de fichier moins l'extension devient la balise locale. Les clés manquantes renvoient la clé elle-même. {vars} manquant, laissez l'espace réservé en place. L'interpolateur parcourt le modèle une fois, n'effectue aucune allocation à moins qu'il ne voie un espace réservé et traite un { mal formé sans accolade fermante comme un caractère littéral.

@bext/i18n-fluent#

crates/bext-impls/bext-i18n-fluent est le backend de production pour tout ce qui se soucie des pluriels et du genre. Il enveloppe la caisse fluent, qui est la même implémentation utilisée par Firefox, donc tout fichier .ftl valide fonctionne immédiatement :

ftl
hello = Hello, { $name }!
items =
    { $count ->
        [one] 1 item
       *[other] { $count } items
    }

Le plugin possède un HashMap<String, FluentBundle<FluentResource>> – un bundle par locale – et translate recherche le message, crée un FluentArgs à partir du sac TransVars et formate. La sélection plurielle CLDR, les sélecteurs de genre, les messages imbriqués, la grammaire locale fonctionnent tous car la caisse fluent les gère. Cette caisse est le seul endroit de l'espace de travail qui importe depuis fluent::* ; tous les autres appelants voient I18nPlugin.

Les erreurs d'analyse fluide sur add_bundle deviennent LocaleUnavailable. Les erreurs de format lors de l'exécution (type de variable inconnu, incompatibilité des bras de sélection) deviennent FormatError.

Configuration#

Choisir un backend est une ligne dans bext.config.toml :

toml
[i18n]
provider = "static"
locales_dir = "./locales"
fallback_locale = "en"
reload_on_change = true

Remplacez provider = "static" par provider = "fluent" et pointez bundles_dir vers vos fichiers .ftl - le trait reste le même, le code qui appelle i18n.t(...) reste le même, la sélection plurielle s'améliore.

Émettre une traduction depuis un plugin#

Les plugins atteignent le backend i18n actif via la table de fonctions hôte ( host.i18n() ), qui renvoie un &dyn I18nPlugin. Un plugin qui souhaite afficher un message d'accueil pour les paramètres régionaux de la requête actuelle ressemble à ceci :

rust
use bext_plugin_api::i18n::{I18nValue, TransVars};

pub fn greet(host: &Host, name: &str) -> Result<String, String> {
    let i18n = host.i18n();
    let locale = i18n.negotiate(&host.request_locale());
    let mut vars = TransVars::new();
    vars.insert("name".into(), I18nValue::from(name));
    i18n.translate("hello", &locale, &vars)
        .map_err(|e| e.to_string())
}

negotiate parcourt les paramètres régionaux de la requête jusqu'à quelque chose que le backend peut servir. translate renvoie une chaîne au mieux ; le .map_err est défensif et ne se déclenche qu'en cas de véritables problèmes de backend.

Où aller ensuite#

  • Le aperçu des capacités couvre l'échelle de promotion que I18n parcourra une fois qu'il aura atterri en tant que première partie.
  • Construire un plugin parcourt la déclaration de requires_capabilities = ["i18n"] dans le manifeste et atteint le backend actif à partir du code de votre plugin.
  • Référence de configuration répertorie toutes les clés [i18n] comprises par le runtime.
Modifier cette page ↗Besoin d’aide ? ↗
Rechercher dans la documentation

Saisissez un sujet, une commande ou une question.