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
.desc = Wards your allies against enemy attacks.
# internally translations, 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
# 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 English file.
# See https://github.com/WeblateOrg/weblate/issues/9895
-heavy_stance = ""
-agile_stance = ""
-defensive_stance = ""

View File

@ -378,6 +378,8 @@ impl LocalizationGuard {
/// 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.
//
// TODO: return Cow<str>?
pub fn get_content(&self, content: &Content) -> String {
// 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> {
match content {
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 } => {
// flag to detect failure down the chain
let mut is_arg_failure = false;

View File

@ -1,25 +1,27 @@
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
// TODO: expose convinience macros ala 'fluent_args!'?
/// The type to represent generic localization request, to be sent from server
/// 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
// called `ChatContent`). A few examples:
//
// - 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.
// TODO: Ideally we would need to fully cover API of our `i18n::Language`, including
// Fluent values.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Content {
/// Plain(text)
///
/// The content is a plaintext string that should be shown to the user
/// verbatim.
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.
// 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`]).
// 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)]
pub enum LocalizationArg {
/// 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
// discourage it)
//
// Or not?
impl From<String> for LocalizationArg {
fn from(text: String) -> Self { Self::Content(Content::Plain(text)) }
}
// TODO: Remove impl and make use of `Content(Plain(...))` explicit (to
// discourage it)
//
// Or not?
impl<'a> From<&'a str> for LocalizationArg {
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 {
fn from(n: u64) -> Self { Self::Nat(n) }
}
@ -118,7 +124,7 @@ impl Content {
pub fn as_plain(&self) -> Option<&str> {
match self {
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();
/// 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> = {
let mut items = try_all_item_defs()
.unwrap_or_else(|e| {

View File

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

View File

@ -26,7 +26,7 @@ use item_key::ItemKey;
use serde::{de, Deserialize, Serialize, Serializer};
use specs::{Component, DenseVecStorage, DerefFlaggedStorage};
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 vek::Rgb;
@ -105,7 +105,17 @@ pub enum MaterialKind {
}
#[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")]
pub enum Material {
@ -479,6 +489,17 @@ type I18nId = String;
// TODO: add hot-reloading similar to how ItemImgs does it?
// TODO: make it work with plugins (via Concatenate?)
/// 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 {
/// maps ItemKey to i18n identifier
map: HashMap<ItemKey, I18nId>,
@ -492,6 +513,24 @@ impl assets::Asset for ItemL10n {
impl ItemL10n {
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)]
@ -1437,12 +1476,12 @@ pub trait ItemDesc {
fn l10n(&self, l10n: &ItemL10n) -> (Content, Content) {
let item_key: ItemKey = self.into();
let _key = l10n.map.get(&item_key);
(
// construct smth like Content::Attr
todo!(),
todo!(),
)
l10n.item_text_opt(item_key).unwrap_or_else(|| {
(
Content::Plain(self.name().to_string()),
Content::Plain(self.name().to_string()),
)
})
}
}
@ -1565,6 +1604,67 @@ pub fn try_all_item_defs() -> Result<Vec<String>, Error> {
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 {
fn eq(&self, other: &ItemDefinitionId<'_>) -> bool {
use ItemDefinitionId as DefId;
@ -1616,4 +1716,23 @@ mod tests {
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>)>>;
lazy_static! {
static ref PRIMARY_COMPONENT_POOL: PrimaryComponentPool = {
pub static ref PRIMARY_COMPONENT_POOL: PrimaryComponentPool = {
let mut component_pool = HashMap::new();
// Load recipe book
@ -509,12 +509,13 @@ pub fn generate_weapons(
ability_map,
msm,
);
weapons.push(Item::new_from_item_base(
let it = Item::new_from_item_base(
ItemBase::Modular(ModularBase::Tool),
vec![comp.duplicate(ability_map, msm), secondary],
ability_map,
msm,
));
);
weapons.push(it);
}
}

View File

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

View File

@ -115,7 +115,7 @@ impl ItemImgs {
}
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
.iter()
.filter_map(|k| self.map.get(k))

View File

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