mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
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:
parent
18e507315f
commit
aba8ec7558
@ -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 = ""
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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| {
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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),
|
||||
}
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user