Implement item localization

- Add Content::Key as proxy to Language::try_msg
- Add Content::Attr as proxy to Language::try_attr
- Extend ItemKey::TagExamples so it includes base asset id
- Implement ItemDesc::l10n using new Content variants
- Add all_items_expect() function to grab all items, because
  try_all_item_defs() covers only items in asset folder.

Required assets will go in next commit
This commit is contained in:
juliancoffee 2024-01-13 16:56:36 +02:00
parent 18e507315f
commit aba8ec7558
10 changed files with 181 additions and 31 deletions

View File

@ -9,8 +9,11 @@ common-abilities-staff-fireshockwave = Ring of Fire
common-abilities-sceptre-wardingaura = Warding Aura common-abilities-sceptre-wardingaura = Warding Aura
.desc = Wards your allies against enemy attacks. .desc = Wards your allies against enemy attacks.
# internally translations, currently only used in zh-Hans # internal terms, currently only used in zh-Hans
# If we remove them here, they also get auto-removed in zh-Hans, so please keep them, even when not used in en # If we remove them here, they also get auto-removed in zh-Hans,
# so please keep them, even when not used in English file.
# See https://github.com/WeblateOrg/weblate/issues/9895
-heavy_stance = "" -heavy_stance = ""
-agile_stance = "" -agile_stance = ""
-defensive_stance = "" -defensive_stance = ""

View File

@ -378,6 +378,8 @@ impl LocalizationGuard {
/// 3) Otherwise, return result from (1). /// 3) Otherwise, return result from (1).
// NOTE: it's important that we only use one language at the time, because // NOTE: it's important that we only use one language at the time, because
// otherwise we will get partially-translated message. // otherwise we will get partially-translated message.
//
// TODO: return Cow<str>?
pub fn get_content(&self, content: &Content) -> String { pub fn get_content(&self, content: &Content) -> String {
// Function to localize content for given language. // Function to localize content for given language.
// //
@ -389,6 +391,16 @@ impl LocalizationGuard {
fn get_content_for_lang(lang: &Language, content: &Content) -> Result<String, String> { 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::Key(key) => {
lang.try_msg(key, None)
.map(Cow::into_owned)
.ok_or_else(|| format!("{key}"))
},
Content::Attr(key, attr) => {
lang.try_attr(key, attr, None)
.map(Cow::into_owned)
.ok_or_else(|| format!("{key}.{attr}"))
},
Content::Localized { key, seed, args } => { Content::Localized { key, seed, args } => {
// flag to detect failure down the chain // flag to detect failure down the chain
let mut is_arg_failure = false; let mut is_arg_failure = false;

View File

@ -1,25 +1,27 @@
use hashbrown::HashMap; use hashbrown::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
// TODO: expose convinience macros ala 'fluent_args!'?
/// The type to represent generic localization request, to be sent from server /// The type to represent generic localization request, to be sent from server
/// to client and then localized (or internationalized) there. /// to client and then localized (or internationalized) there.
// TODO: This could be generalised to *any* in-game text, not just chat messages (hence it not being // TODO: Ideally we would need to fully cover API of our `i18n::Language`, including
// called `ChatContent`). A few examples: // Fluent values.
//
// - Signposts, both those appearing as overhead messages and those displayed 'in-world' on a shop
// sign
// - UI elements
// - In-game notes/books (we could add a variant that allows structuring complex, novel textual
// information as a syntax tree or some other intermediate format that can be localised by the
// client)
// TODO: We probably want to have this type be able to represent similar things to
// `fluent::FluentValue`, such as numeric values, so that they can be properly localised in whatever
// manner is required.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Content { pub enum Content {
/// Plain(text)
///
/// The content is a plaintext string that should be shown to the user /// The content is a plaintext string that should be shown to the user
/// verbatim. /// verbatim.
Plain(String), Plain(String),
/// Key(i18n_key)
///
/// The content is defined just by the key
Key(String),
/// Attr(i18n_key, attr)
///
/// The content is the attribute of the key
Attr(String, 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 // TODO: reduce usages of random i18n as much as possible
// //
@ -49,6 +51,8 @@ impl<'a> From<&'a str> for Content {
} }
/// A localisation argument for localised content (see [`Content::Localized`]). /// A localisation argument for localised content (see [`Content::Localized`]).
// TODO: Do we want it to be Enum or just wrapper around Content, to add
// additional `impl From<T>` for our arguments?
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum LocalizationArg { pub enum LocalizationArg {
/// The localisation argument is itself a section of content. /// The localisation argument is itself a section of content.
@ -74,18 +78,20 @@ impl From<Content> for LocalizationArg {
// TODO: Remove impl and make use of `Content(Plain(...))` explicit (to // TODO: Remove impl and make use of `Content(Plain(...))` explicit (to
// discourage it) // discourage it)
//
// Or not?
impl From<String> for LocalizationArg { impl From<String> for LocalizationArg {
fn from(text: String) -> Self { Self::Content(Content::Plain(text)) } fn from(text: String) -> Self { Self::Content(Content::Plain(text)) }
} }
// TODO: Remove impl and make use of `Content(Plain(...))` explicit (to // TODO: Remove impl and make use of `Content(Plain(...))` explicit (to
// discourage it) // discourage it)
//
// Or not?
impl<'a> From<&'a str> for LocalizationArg { impl<'a> From<&'a str> for LocalizationArg {
fn from(text: &'a str) -> Self { Self::Content(Content::Plain(text.to_string())) } fn from(text: &'a str) -> Self { Self::Content(Content::Plain(text.to_string())) }
} }
// TODO: Remove impl and make use of `Content(Plain(...))` explicit (to
// discourage it)
impl From<u64> for LocalizationArg { impl From<u64> for LocalizationArg {
fn from(n: u64) -> Self { Self::Nat(n) } fn from(n: u64) -> Self { Self::Nat(n) }
} }
@ -118,7 +124,7 @@ impl Content {
pub fn as_plain(&self) -> Option<&str> { pub fn as_plain(&self) -> Option<&str> {
match self { match self {
Self::Plain(text) => Some(text.as_str()), Self::Plain(text) => Some(text.as_str()),
Self::Localized { .. } => None, Self::Localized { .. } | Self::Attr { .. } | Self::Key { .. } => None,
} }
} }
} }

View File

@ -219,7 +219,9 @@ lazy_static! {
static ref ROLES: Vec<String> = ["admin", "moderator"].iter().copied().map(Into::into).collect(); static ref ROLES: Vec<String> = ["admin", "moderator"].iter().copied().map(Into::into).collect();
/// List of item specifiers. Useful for tab completing /// List of item's asset specifiers. Useful for tab completing.
/// Doesn't cover all items (like modulars), includes "fake" items like
/// TagExamples.
pub static ref ITEM_SPECS: Vec<String> = { pub static ref ITEM_SPECS: Vec<String> = {
let mut items = try_all_item_defs() let mut items = try_all_item_defs()
.unwrap_or_else(|e| { .unwrap_or_else(|e| {

View File

@ -10,7 +10,7 @@ pub enum ItemKey {
Simple(String), Simple(String),
ModularWeapon(modular::ModularWeaponKey), ModularWeapon(modular::ModularWeaponKey),
ModularWeaponComponent(modular::ModularWeaponComponentKey), ModularWeaponComponent(modular::ModularWeaponComponentKey),
TagExamples(Vec<ItemKey>), TagExamples(Vec<ItemKey>, String),
Empty, Empty,
} }
@ -24,6 +24,10 @@ impl<T: ItemDesc + ?Sized> From<&T> for ItemKey {
.iter() .iter()
.map(|id| ItemKey::from(&*Arc::<ItemDef>::load_expect_cloned(id))) .map(|id| ItemKey::from(&*Arc::<ItemDef>::load_expect_cloned(id)))
.collect(), .collect(),
item_definition_id
.itemdef_id()
.unwrap_or("?modular?")
.to_owned(),
) )
} else { } else {
match item_definition_id { match item_definition_id {

View File

@ -26,7 +26,7 @@ use item_key::ItemKey;
use serde::{de, Deserialize, Serialize, Serializer}; use serde::{de, Deserialize, Serialize, Serializer};
use specs::{Component, DenseVecStorage, DerefFlaggedStorage}; use specs::{Component, DenseVecStorage, DerefFlaggedStorage};
use std::{borrow::Cow, collections::hash_map::DefaultHasher, fmt, sync::Arc}; use std::{borrow::Cow, collections::hash_map::DefaultHasher, fmt, sync::Arc};
use strum::{EnumString, IntoStaticStr}; use strum::{EnumIter, EnumString, IntoEnumIterator, IntoStaticStr};
use tracing::error; use tracing::error;
use vek::Rgb; use vek::Rgb;
@ -105,7 +105,17 @@ pub enum MaterialKind {
} }
#[derive( #[derive(
Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, IntoStaticStr, EnumString, Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
IntoStaticStr,
EnumString,
EnumIter,
)] )]
#[strum(serialize_all = "snake_case")] #[strum(serialize_all = "snake_case")]
pub enum Material { pub enum Material {
@ -479,6 +489,17 @@ type I18nId = String;
// TODO: add hot-reloading similar to how ItemImgs does it? // TODO: add hot-reloading similar to how ItemImgs does it?
// TODO: make it work with plugins (via Concatenate?) // TODO: make it work with plugins (via Concatenate?)
/// To be used with ItemDesc::l10n /// To be used with ItemDesc::l10n
///
/// NOTE: there is a limitation to this manifest, as it uses ItemKey and
/// ItemKey isn't uniquely identifies Item, when it comes to modular items.
///
/// If modular weapon has the same primary component and the same hand-ness,
/// we use the same model EVEN IF it has different secondary components, like
/// Staff with Heavy core or Light core.
///
/// Translations currently do the same, but *maybe* they shouldn't in which case
/// we should either extend ItemKey or use new identifier. We could use
/// ItemDefinitionId, but it's very generic and cumbersome.
pub struct ItemL10n { pub struct ItemL10n {
/// maps ItemKey to i18n identifier /// maps ItemKey to i18n identifier
map: HashMap<ItemKey, I18nId>, map: HashMap<ItemKey, I18nId>,
@ -492,6 +513,24 @@ impl assets::Asset for ItemL10n {
impl ItemL10n { impl ItemL10n {
pub fn new_expect() -> Self { ItemL10n::load_expect("common.item_l10n").read().clone() } pub fn new_expect() -> Self { ItemL10n::load_expect("common.item_l10n").read().clone() }
/// Returns (name, description) in Content form.
// TODO: after we remove legacy text from ItemDef, consider making this
// function non-fallible?
fn item_text_opt(&self, mut item_key: ItemKey) -> Option<(Content, Content)> {
// we don't put TagExamples into manifest
if let ItemKey::TagExamples(_, id) = item_key {
item_key = ItemKey::Simple(id.to_string());
}
let key = self.map.get(&item_key);
key.map(|key| {
(
Content::Key(key.to_owned()),
Content::Attr(key.to_owned(), "desc".to_owned()),
)
})
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -1437,12 +1476,12 @@ pub trait ItemDesc {
fn l10n(&self, l10n: &ItemL10n) -> (Content, Content) { fn l10n(&self, l10n: &ItemL10n) -> (Content, Content) {
let item_key: ItemKey = self.into(); let item_key: ItemKey = self.into();
let _key = l10n.map.get(&item_key); l10n.item_text_opt(item_key).unwrap_or_else(|| {
( (
// construct smth like Content::Attr Content::Plain(self.name().to_string()),
todo!(), Content::Plain(self.name().to_string()),
todo!(), )
) })
} }
} }
@ -1565,6 +1604,67 @@ pub fn try_all_item_defs() -> Result<Vec<String>, Error> {
Ok(defs.ids().map(|id| id.to_string()).collect()) Ok(defs.ids().map(|id| id.to_string()).collect())
} }
/// Designed to return all possible items, including modulars.
/// And some impossible too, like ItemKind::TagExamples.
pub fn all_items_expect() -> Vec<Item> {
let defs = assets::load_dir::<RawItemDef>("common.items", true)
.expect("failed to load item asset directory");
// Grab all items from assets
let mut asset_items: Vec<Item> = defs
.ids()
.map(|id| Item::new_from_asset_expect(id))
.collect();
let mut material_parse_table = HashMap::new();
for mat in Material::iter() {
if let Some(id) = mat.asset_identifier() {
material_parse_table.insert(id.to_owned(), mat);
}
}
let primary_comp_pool = modular::PRIMARY_COMPONENT_POOL.clone();
// Grab weapon primary components
let mut primary_comps: Vec<Item> = primary_comp_pool
.values()
.flatten()
.map(|(item, _hand_rules)| item.clone())
.collect();
// Grab modular weapons
let mut modular_items: Vec<Item> = primary_comp_pool
.keys()
.map(|(tool, mat_id)| {
let mat = material_parse_table
.get(mat_id)
.expect("unexpected material ident");
// get all weapons without imposing additional hand restrictions
let its = modular::generate_weapons(*tool, *mat, None)
.expect("failure during modular weapon generation");
its
})
.flatten()
.collect();
// 1. Append asset items, that should include pretty much everything,
// except modular items
// 2. Append primary weapon components, which are modular as well.
// 3. Finally append modular weapons that are made from (1) and (2)
// extend when we get some new exotic stuff
//
// P. s. I still can't wrap my head around the idea that you can put
// tag example into your inventory.
let mut all = Vec::new();
all.append(&mut asset_items);
all.append(&mut primary_comps);
all.append(&mut modular_items);
all
}
impl PartialEq<ItemDefinitionId<'_>> for ItemDefinitionIdOwned { impl PartialEq<ItemDefinitionId<'_>> for ItemDefinitionIdOwned {
fn eq(&self, other: &ItemDefinitionId<'_>) -> bool { fn eq(&self, other: &ItemDefinitionId<'_>) -> bool {
use ItemDefinitionId as DefId; use ItemDefinitionId as DefId;
@ -1616,4 +1716,23 @@ mod tests {
drop(item) drop(item)
} }
} }
#[test]
fn test_item_l10n() { let _ = ItemL10n::new_expect(); }
#[test]
// Probably can't fail, but better safe than crashing production server
fn test_all_items() { let _ = all_items_expect(); }
#[test]
// All items in Veloren should have localization.
// If no, add some common dummy i18n id.
fn ensure_item_localization() {
let manifest = ItemL10n::new_expect();
let items = all_items_expect();
for item in items {
let item_key: ItemKey = (&item).into();
let _ = manifest.item_text_opt(item_key).unwrap();
}
}
} }

View File

@ -323,7 +323,7 @@ type PrimaryComponentPool = HashMap<(ToolKind, String), Vec<(Item, Option<Hands>
type SecondaryComponentPool = HashMap<ToolKind, Vec<(Arc<ItemDef>, Option<Hands>)>>; type SecondaryComponentPool = HashMap<ToolKind, Vec<(Arc<ItemDef>, Option<Hands>)>>;
lazy_static! { lazy_static! {
static ref PRIMARY_COMPONENT_POOL: PrimaryComponentPool = { pub static ref PRIMARY_COMPONENT_POOL: PrimaryComponentPool = {
let mut component_pool = HashMap::new(); let mut component_pool = HashMap::new();
// Load recipe book // Load recipe book
@ -509,12 +509,13 @@ pub fn generate_weapons(
ability_map, ability_map,
msm, msm,
); );
weapons.push(Item::new_from_item_base( let it = Item::new_from_item_base(
ItemBase::Modular(ModularBase::Tool), ItemBase::Modular(ModularBase::Tool),
vec![comp.duplicate(ability_map, msm), secondary], vec![comp.duplicate(ability_map, msm), secondary],
ability_map, ability_map,
msm, msm,
)); );
weapons.push(it);
} }
} }

View File

@ -778,9 +778,11 @@ pub enum UnlockKind {
Free, Free,
/// The sprite requires that the opening character has a given item in their /// The sprite requires that the opening character has a given item in their
/// inventory /// inventory
// TODO: use ItemKey here?
Requires(ItemDefinitionIdOwned), Requires(ItemDefinitionIdOwned),
/// The sprite will consume the given item from the opening character's /// The sprite will consume the given item from the opening character's
/// inventory /// inventory
// TODO: use ItemKey here?
Consumes(ItemDefinitionIdOwned), Consumes(ItemDefinitionIdOwned),
} }

View File

@ -115,7 +115,7 @@ impl ItemImgs {
} }
pub fn img_ids(&self, item_key: ItemKey) -> Vec<Id> { pub fn img_ids(&self, item_key: ItemKey) -> Vec<Id> {
if let ItemKey::TagExamples(keys) = item_key { if let ItemKey::TagExamples(keys, _) = item_key {
return keys return keys
.iter() .iter()
.filter_map(|k| self.map.get(k)) .filter_map(|k| self.map.get(k))

View File

@ -2096,6 +2096,7 @@ impl Hud {
}, },
BlockInteraction::Unlock(kind) => { BlockInteraction::Unlock(kind) => {
let item_name = |item_id: &ItemDefinitionIdOwned| { let item_name = |item_id: &ItemDefinitionIdOwned| {
// TODO: get ItemKey and use it with l10n?
item_id item_id
.as_ref() .as_ref()
.itemdef_id() .itemdef_id()