Add veloren-common-i18n

- Move common::comp::chat::Content to its own place
This commit is contained in:
juliancoffee 2024-01-09 12:06:31 +02:00
parent b4ae34cd7c
commit 9264fe77b1
15 changed files with 156 additions and 131 deletions

12
Cargo.lock generated
View File

@ -6946,8 +6946,8 @@ dependencies = [
"serde", "serde",
"tracing", "tracing",
"unic-langid", "unic-langid",
"veloren-common",
"veloren-common-assets", "veloren-common-assets",
"veloren-common-i18n",
] ]
[[package]] [[package]]
@ -6994,6 +6994,7 @@ dependencies = [
"vek 0.15.8", "vek 0.15.8",
"veloren-common-assets", "veloren-common-assets",
"veloren-common-base", "veloren-common-base",
"veloren-common-i18n",
] ]
[[package]] [[package]]
@ -7054,6 +7055,15 @@ dependencies = [
"veloren-common-base", "veloren-common-base",
] ]
[[package]]
name = "veloren-common-i18n"
version = "0.1.0"
dependencies = [
"hashbrown 0.13.2",
"rand 0.8.5",
"serde",
]
[[package]] [[package]]
name = "veloren-common-net" name = "veloren-common-net"
version = "0.10.0" version = "0.10.0"

View File

@ -5,6 +5,7 @@ resolver = "2"
members = [ members = [
"common", "common",
"common/assets", "common/assets",
"common/i18n",
"common/base", "common/base",
"common/dynlib", "common/dynlib",
"common/ecs", "common/ecs",

View File

@ -7,8 +7,8 @@ version = "0.13.0"
[dependencies] [dependencies]
# Assets # Assets
common = {package = "veloren-common", path = "../../common"}
common-assets = {package = "veloren-common-assets", path = "../../common/assets"} common-assets = {package = "veloren-common-assets", path = "../../common/assets"}
common-i18n = { package = "veloren-common-i18n", path = "../../common/i18n" }
serde = { workspace = true } serde = { workspace = true }
# Localization # Localization
unic-langid = { version = "0.9"} unic-langid = { version = "0.9"}

View File

@ -19,8 +19,8 @@ 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; use tracing::warn;
// Re-export because I don't like prefix // Re-export because I don't like prefix
use common::comp::{Content, LocalizationArg};
use common_assets as assets; use common_assets as assets;
use common_i18n::{Content, LocalizationArg};
// Re-export for argument creation // Re-export for argument creation
pub use fluent::{fluent_args, FluentValue}; pub use fluent::{fluent_args, FluentValue};

View File

@ -20,6 +20,7 @@ default = ["simd"]
[dependencies] [dependencies]
common-base = { package = "veloren-common-base", path = "base" } common-base = { package = "veloren-common-base", path = "base" }
common-i18n = { package = "veloren-common-i18n", path = "i18n" }
# inline_tweak = { workspace = true } # inline_tweak = { workspace = true }
# Serde # Serde

10
common/i18n/Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[package]
name = "veloren-common-i18n"
version = "0.1.0"
edition = "2021"
description = "Crate for structs and methods that acknowledge the need for localization of the game"
[dependencies]
serde = { workspace = true, features = ["rc"] }
hashbrown = { workspace = true }
rand = { workspace = true }

120
common/i18n/src/lib.rs Normal file
View File

@ -0,0 +1,120 @@
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
/// 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.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Content {
/// The content is a plaintext string that should be shown to the user
/// verbatim.
Plain(String),
/// The content is a localizable message with the given arguments.
Localized {
/// i18n key
key: String,
/// Pseudorandom seed value that allows frontends to select a
/// deterministic (but pseudorandom) localised output
#[serde(default = "random_seed")]
seed: u16,
/// i18n arguments
#[serde(default)]
args: HashMap<String, LocalizationArg>,
},
}
// TODO: Remove impl and make use of `Plain(...)` explicit (to discourage it)
impl From<String> for Content {
fn from(text: String) -> Self { Self::Plain(text) }
}
// TODO: Remove impl and make use of `Plain(...)` explicit (to discourage it)
impl<'a> From<&'a str> for Content {
fn from(text: &'a str) -> Self { Self::Plain(text.to_string()) }
}
/// A localisation argument for localised content (see [`Content::Localized`]).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum LocalizationArg {
/// The localisation argument is itself a section of content.
///
/// Note that this allows [`Content`] to recursively refer to itself. It may
/// be tempting to decide to parameterise everything, having dialogue
/// generated with a compact tree. "It's simpler!", you might say. False.
/// Over-parameterisation is an anti-pattern that hurts translators. Where
/// possible, prefer fewer levels of nesting unless doing so would result
/// in an intractably larger number of combinations. See [here] for the
/// guidance provided by the docs for `fluent`, the localisation library
/// used by clients.
///
/// [here]: https://github.com/projectfluent/fluent/wiki/Good-Practices-for-Developers#prefer-wet-over-dry
Content(Content),
/// The localisation argument is a natural number
Nat(u64),
}
impl From<Content> for LocalizationArg {
fn from(content: Content) -> Self { Self::Content(content) }
}
// TODO: Remove impl and make use of `Content(Plain(...))` explicit (to
// discourage it)
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)
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) }
}
fn random_seed() -> u16 { rand::random() }
impl Content {
pub fn localized(key: impl ToString) -> Self {
Self::Localized {
key: key.to_string(),
seed: random_seed(),
args: HashMap::default(),
}
}
pub fn localized_with_args<'a, A: Into<LocalizationArg>>(
key: impl ToString,
args: impl IntoIterator<Item = (&'a str, A)>,
) -> Self {
Self::Localized {
key: key.to_string(),
seed: rand::random(),
args: args
.into_iter()
.map(|(k, v)| (k.to_string(), v.into()))
.collect(),
}
}
pub fn as_plain(&self) -> Option<&str> {
match self {
Self::Plain(text) => Some(text.as_str()),
Self::Localized { .. } => None,
}
}
}

View File

@ -19,11 +19,11 @@ pub mod theropod;
use crate::{ use crate::{
assets::{self, Asset}, assets::{self, Asset},
comp::Content,
consts::{HUMAN_DENSITY, WATER_DENSITY}, consts::{HUMAN_DENSITY, WATER_DENSITY},
make_case_elim, make_case_elim,
npc::NpcKind, npc::NpcKind,
}; };
use common_i18n::Content;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specs::{Component, DerefFlaggedStorage}; use specs::{Component, DerefFlaggedStorage};
use strum::Display; use strum::Display;

View File

@ -1,4 +1,5 @@
use crate::{comp::Content, make_case_elim, make_proj_elim}; use crate::{make_case_elim, make_proj_elim};
use common_i18n::Content;
use rand::{seq::SliceRandom, thread_rng}; use rand::{seq::SliceRandom, thread_rng};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@ -2,7 +2,7 @@ use crate::{
comp::{group::Group, BuffKind}, comp::{group::Group, BuffKind},
uid::Uid, uid::Uid,
}; };
use hashbrown::HashMap; use common_i18n::Content;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specs::{Component, DenseVecStorage}; use specs::{Component, DenseVecStorage};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@ -179,121 +179,6 @@ impl<G> ChatType<G> {
} }
} }
/// The content of a chat message.
// 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.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Content {
/// The content is a plaintext string that should be shown to the user
/// verbatim.
Plain(String),
/// The content is a localizable message with the given arguments.
Localized {
/// i18n key
key: String,
/// Pseudorandom seed value that allows frontends to select a
/// deterministic (but pseudorandom) localised output
#[serde(default = "random_seed")]
seed: u16,
/// i18n arguments
#[serde(default)]
args: HashMap<String, LocalizationArg>,
},
}
// TODO: Remove impl and make use of `Plain(...)` explicit (to discourage it)
impl From<String> for Content {
fn from(text: String) -> Self { Self::Plain(text) }
}
// TODO: Remove impl and make use of `Plain(...)` explicit (to discourage it)
impl<'a> From<&'a str> for Content {
fn from(text: &'a str) -> Self { Self::Plain(text.to_string()) }
}
/// A localisation argument for localised content (see [`Content::Localized`]).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum LocalizationArg {
/// The localisation argument is itself a section of content.
///
/// Note that this allows [`Content`] to recursively refer to itself. It may
/// be tempting to decide to parameterise everything, having dialogue
/// generated with a compact tree. "It's simpler!", you might say. False.
/// Over-parameterisation is an anti-pattern that hurts translators. Where
/// possible, prefer fewer levels of nesting unless doing so would
/// result in an intractably larger number of combinations. See [here](https://github.com/projectfluent/fluent/wiki/Good-Practices-for-Developers#prefer-wet-over-dry) for the
/// guidance provided by the docs for `fluent`, the localisation library
/// used by clients.
Content(Content),
/// The localisation argument is a natural number
Nat(u64),
}
impl From<Content> for LocalizationArg {
fn from(content: Content) -> Self { Self::Content(content) }
}
// TODO: Remove impl and make use of `Content(Plain(...))` explicit (to
// discourage it)
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)
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) }
}
fn random_seed() -> u16 { rand::random() }
impl Content {
pub fn localized(key: impl ToString) -> Self {
Self::Localized {
key: key.to_string(),
seed: random_seed(),
args: HashMap::default(),
}
}
pub fn localized_with_args<'a, A: Into<LocalizationArg>>(
key: impl ToString,
args: impl IntoIterator<Item = (&'a str, A)>,
) -> Self {
Self::Localized {
key: key.to_string(),
seed: rand::random(),
args: args
.into_iter()
.map(|(k, v)| (k.to_string(), v.into()))
.collect(),
}
}
pub fn as_plain(&self) -> Option<&str> {
match self {
Self::Plain(text) => Some(text.as_str()),
Self::Localized { .. } => None,
}
}
}
// Stores chat text, type // Stores chat text, type
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenericChatMsg<G> { pub struct GenericChatMsg<G> {

View File

@ -1,4 +1,4 @@
use super::Content; use common_i18n::Content;
use vek::Vec2; use vek::Vec2;
// TODO: Move this to common/src/, it's not a component // TODO: Move this to common/src/, it's not a component

View File

@ -62,8 +62,7 @@ pub use self::{
}, },
character_state::{CharacterActivity, CharacterState, StateUpdate}, character_state::{CharacterActivity, CharacterState, StateUpdate},
chat::{ chat::{
ChatMode, ChatMsg, ChatType, Content, Faction, LocalizationArg, SpeechBubble, ChatMode, ChatMsg, ChatType, Faction, SpeechBubble, SpeechBubbleType, UnresolvedChatMsg,
SpeechBubbleType, UnresolvedChatMsg,
}, },
combo::Combo, combo::Combo,
controller::{ controller::{
@ -107,5 +106,6 @@ pub use self::{
teleport::Teleporting, teleport::Teleporting,
visual::{LightAnimation, LightEmitter}, visual::{LightAnimation, LightEmitter},
}; };
pub use common_i18n::{Content, LocalizationArg};
pub use health::{Health, HealthChange}; pub use health::{Health, HealthChange};

View File

@ -3,11 +3,8 @@
// `Agent`). When possible, this should be moved to the `rtsim` // `Agent`). When possible, this should be moved to the `rtsim`
// module in `server`. // module in `server`.
use crate::{ use crate::{character::CharacterId, comp::dialogue::Subject, util::Dir};
character::CharacterId, use common_i18n::Content;
comp::{dialogue::Subject, Content},
util::Dir,
};
use rand::{seq::IteratorRandom, Rng}; use rand::{seq::IteratorRandom, Rng};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specs::Component; use specs::Component;

View File

@ -2,12 +2,12 @@ use crate::{
comp::{ comp::{
item::{ItemDefinitionId, ItemDefinitionIdOwned}, item::{ItemDefinitionId, ItemDefinitionIdOwned},
tool::ToolKind, tool::ToolKind,
Content,
}, },
lottery::LootSpec, lottery::LootSpec,
make_case_elim, make_case_elim,
terrain::Block, terrain::Block,
}; };
use common_i18n::Content;
use hashbrown::HashMap; use hashbrown::HashMap;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use num_derive::FromPrimitive; use num_derive::FromPrimitive;

View File

@ -1,11 +1,11 @@
use super::{BlockKind, SpriteKind}; use super::{BlockKind, SpriteKind};
use crate::{ use crate::{
assets::{self, AssetExt, AssetHandle, BoxedError, DotVoxAsset}, assets::{self, AssetExt, AssetHandle, BoxedError, DotVoxAsset},
comp::Content,
make_case_elim, make_case_elim,
vol::{BaseVol, ReadVol, SizedVol, WriteVol}, vol::{BaseVol, ReadVol, SizedVol, WriteVol},
volumes::dyna::{Dyna, DynaError}, volumes::dyna::{Dyna, DynaError},
}; };
use common_i18n::Content;
use dot_vox::DotVoxData; use dot_vox::DotVoxData;
use hashbrown::HashMap; use hashbrown::HashMap;
use serde::Deserialize; use serde::Deserialize;