Discourage random i18n and prettify get_content

- Add the comment that recommends avoiding all `get_variation` methods.
- Add the comment that recommends avoiding Content with random i18n.
- Improve `get_content` docs.
This commit is contained in:
juliancoffee 2024-01-11 10:38:15 +02:00
parent 1347a31108
commit 8263154a7e
2 changed files with 89 additions and 22 deletions

View File

@ -17,10 +17,9 @@ use serde::{Deserialize, Serialize};
use std::{borrow::Cow, io}; use std::{borrow::Cow, io};
use assets::{source::DirEntry, AssetExt, AssetGuard, AssetHandle, ReloadWatcher, SharedString}; use assets::{source::DirEntry, AssetExt, AssetGuard, AssetHandle, ReloadWatcher, SharedString};
use tracing::warn;
// Re-export because I don't like prefix
use common_assets as assets; use common_assets as assets;
use common_i18n::{Content, LocalizationArg}; use common_i18n::{Content, LocalizationArg};
use tracing::warn;
// Re-export for argument creation // Re-export for argument creation
pub use fluent::{fluent_args, FluentValue}; pub use fluent::{fluent_args, FluentValue};
@ -107,6 +106,8 @@ impl Language {
Some(msg) Some(msg)
} }
/// NOTE: Exists for legacy reasons, avoid.
// Read more in the issue on get_variation at Gitlab
fn try_variation<'a>( fn try_variation<'a>(
&'a self, &'a self,
key: &str, key: &str,
@ -221,12 +222,12 @@ pub struct LocalizationHandle {
pub use_english_fallback: bool, pub use_english_fallback: bool,
} }
/// Read [LocalizationGuard] /// Read [`LocalizationGuard`]
// arbitrary choice to minimize changing all of veloren // arbitrary choice to minimize changing all of veloren
pub type Localization = LocalizationGuard; pub type Localization = LocalizationGuard;
/// RAII guard returned from [LocalizationHandle::read()], resembles /// RAII guard returned from [`LocalizationHandle::read()`], resembles
/// [AssetGuard] /// [`AssetGuard`]
pub struct LocalizationGuard { pub struct LocalizationGuard {
active: AssetGuard<Language>, active: AssetGuard<Language>,
fallback: Option<AssetGuard<Language>>, fallback: Option<AssetGuard<Language>>,
@ -289,10 +290,13 @@ impl LocalizationGuard {
.unwrap_or_else(|| Cow::Owned(key.to_owned())) .unwrap_or_else(|| Cow::Owned(key.to_owned()))
} }
/// NOTE: Exists for legacy reasons, avoid.
///
/// Get a localized text from the variation of given key /// Get a localized text from the variation of given key
/// ///
/// First lookup is done in the active language, second in /// First lookup is done in the active language, second in
/// the fallback (if present). /// the fallback (if present).
// Read more in the issue on get_variation at Gitlab
pub fn try_variation(&self, key: &str, seed: u16) -> Option<Cow<str>> { pub fn try_variation(&self, key: &str, seed: u16) -> Option<Cow<str>> {
self.active.try_variation(key, seed, None).or_else(|| { self.active.try_variation(key, seed, None).or_else(|| {
self.fallback self.fallback
@ -301,22 +305,28 @@ impl LocalizationGuard {
}) })
} }
/// NOTE: Exists for legacy reasons, avoid.
///
/// Get a localized text from the variation of given key /// Get a localized text from the variation of given key
/// ///
/// First lookup is done in the active language, second in /// First lookup is done in the active language, second in
/// the fallback (if present). /// the fallback (if present).
/// If the key is not present in the localization object /// If the key is not present in the localization object
/// then the key itself is returned. /// then the key itself is returned.
// Read more in the issue on get_variation at Gitlab
pub fn get_variation(&self, key: &str, seed: u16) -> Cow<str> { pub fn get_variation(&self, key: &str, seed: u16) -> Cow<str> {
self.try_variation(key, seed) self.try_variation(key, seed)
.unwrap_or_else(|| Cow::Owned(key.to_owned())) .unwrap_or_else(|| Cow::Owned(key.to_owned()))
} }
/// NOTE: Exists for legacy reasons, avoid.
///
/// Get a localized text from the variation of given key with given /// Get a localized text from the variation of given key with given
/// arguments /// arguments
/// ///
/// First lookup is done in the active language, second in /// First lookup is done in the active language, second in
/// the fallback (if present). /// the fallback (if present).
// Read more in the issue on get_variation at Gitlab
pub fn try_variation_ctx<'a>( pub fn try_variation_ctx<'a>(
&'a self, &'a self,
key: &str, key: &str,
@ -332,47 +342,99 @@ impl LocalizationGuard {
}) })
} }
/// Localize the given content. /// Tries its best to localize compound message.
///
/// # Example
/// ```text
/// Content::Localized { "npc-speech-tell_site", seed, {
/// "dir" => Content::Localized("npc-speech-dir_north", seed, {})
/// "dist" => Content::Localized("npc-speech-dist_very_far", seed, {})
/// "site" => Content::Plain(site)
/// }}
/// ```
/// ```fluent
/// npc-speech-tell_site =
/// .a0 = Have you visited { $site }? It's just { $dir } of here!
/// .a1 = You should visit { $site } some time.
/// .a2 = If you travel { $dist } to the { $dir }, you can get to { $site }.
/// .a3 = To the { $dir } you'll find { $site }, it's { $dist }.
///
/// npc-speech-dir_north = north
/// # ... other keys
///
/// npc-speech-dist_very_far = very far away
/// # ... other keys
/// ```
///
/// 1) Because content we want is localized itself and has arguments, we
/// iterate over them and localize, recursively. Having that, we localize
/// our content.
///
/// 2) Now there is a chance that some of args have missing
/// internalization. In that case, we insert arg name as placeholder and
/// mark it as broken. Then we repeat *whole* procedure on fallback
/// language if we have it.
///
/// 3) Otherwise, return result from (1).
// NOTE: it's important that we only use one language at the time, because
// otherwise we will get partially-translated message.
pub fn get_content(&self, content: &Content) -> String { pub fn get_content(&self, content: &Content) -> String {
// On error, produces the localisation but with the missing key inline // Function to localize content for given language.
fn get_content_inner(lang: &Language, content: &Content) -> Result<String, String> { //
// Returns Ok(localized_text) if found no errors.
// Returns Err(broken_text) on failure.
//
// broken_text will have i18n keys in it, just i18n key if it was instant miss
// or text with missed keys inlined if it was missed down the chain.
fn get_content_for_lang(lang: &Language, content: &Content) -> Result<String, String> {
match content { match content {
Content::Plain(text) => Ok(text.clone()), Content::Plain(text) => Ok(text.clone()),
Content::Localized { key, seed, args } => { Content::Localized { key, seed, args } => {
let mut is_err = false; // flag to detect failure down the chain
let mut is_arg_failure = false;
let mut fargs = FluentArgs::new(); let mut fargs = FluentArgs::new();
for (k, arg) in args { for (k, arg) in args {
fargs.set(k, match arg { let arg_val = match arg {
LocalizationArg::Content(content) => FluentValue::String( LocalizationArg::Content(content) => {
get_content_inner(lang, content) let arg_res = get_content_for_lang(lang, content)
.unwrap_or_else(|broken_text| { .unwrap_or_else(|broken_text| {
is_err = true; is_arg_failure = true;
broken_text broken_text
}) })
.into(), .into();
),
FluentValue::String(arg_res)
},
LocalizationArg::Nat(n) => FluentValue::from(n), LocalizationArg::Nat(n) => FluentValue::from(n),
}); };
fargs.set(k, arg_val);
} }
lang.try_variation(key, *seed, Some(&fargs)) lang.try_variation(key, *seed, Some(&fargs))
.map(Cow::into_owned) .map(Cow::into_owned)
.ok_or_else(|| key.clone()) .ok_or_else(|| key.clone())
.and_then(|text| if is_err { Err(text) } else { Ok(text) }) .and_then(|text| if is_arg_failure { Err(text) } else { Ok(text) })
}, },
} }
} }
match get_content_inner(&self.active, content) { match get_content_for_lang(&self.active, content) {
Ok(text) => text, Ok(text) => text,
// If part of the localisation failed, use the fallback language // If localisation or some part of it failed, repeat with fallback.
Err(broken_text) => self.fallback.as_ref() // If it did fail as well, it's probably because fallback was disabled,
.and_then(|fb| get_content_inner(fb, content).ok()) // so we don't have better option other than returning broken text
// If all else fails, localise with the active language, but with the missing key included inline // we produced earlier.
Err(broken_text) => self
.fallback
.as_ref()
.and_then(|fb| get_content_for_lang(fb, content).ok())
.unwrap_or(broken_text), .unwrap_or(broken_text),
} }
} }
/// NOTE: Exists for legacy reasons, avoid.
///
/// Get a localized text from the variation of given key with given /// Get a localized text from the variation of given key with given
/// arguments /// arguments
/// ///
@ -380,6 +442,7 @@ impl LocalizationGuard {
/// the fallback (if present). /// the fallback (if present).
/// If the key is not present in the localization object /// If the key is not present in the localization object
/// then the key itself is returned. /// then the key itself is returned.
// Read more in the issue on get_variation at Gitlab
pub fn get_variation_ctx<'a>(&'a self, key: &str, seed: u16, args: &'a FluentArgs) -> Cow<str> { pub fn get_variation_ctx<'a>(&'a self, key: &str, seed: u16, args: &'a FluentArgs) -> Cow<str> {
self.try_variation_ctx(key, seed, args) self.try_variation_ctx(key, seed, args)
.unwrap_or_else(|| Cow::Owned(key.to_owned())) .unwrap_or_else(|| Cow::Owned(key.to_owned()))

View File

@ -21,6 +21,10 @@ pub enum Content {
/// verbatim. /// verbatim.
Plain(String), Plain(String),
/// The content is a localizable message with the given arguments. /// The content is a localizable message with the given arguments.
// TODO: reduce usages of random i18n as much as possible
//
// It's ok to have random messages, just not at i18n step.
// Look for issue on `get_vartion` at Gitlab for more.
Localized { Localized {
/// i18n key /// i18n key
key: String, key: String,