diff --git a/Cargo.lock b/Cargo.lock index cbf9c2c434..f2ce94e2c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6683,6 +6683,8 @@ dependencies = [ "veloren-common-state", "veloren-common-systems", "veloren-network", + "veloren-voxygen-chat-i18n", + "veloren-voxygen-i18n", ] [[package]] @@ -7089,6 +7091,7 @@ dependencies = [ "veloren-common-systems", "veloren-server", "veloren-voxygen-anim", + "veloren-voxygen-chat-i18n", "veloren-voxygen-egui", "veloren-voxygen-i18n", "veloren-world", @@ -7111,6 +7114,16 @@ dependencies = [ "veloren-common-dynlib", ] +[[package]] +name = "veloren-voxygen-chat-i18n" +version = "0.10.0" +dependencies = [ + "tracing", + "veloren-common", + "veloren-common-net", + "veloren-voxygen-i18n", +] + [[package]] name = "veloren-voxygen-egui" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 3a8b40f2f3..532b277454 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "server-cli", "voxygen", "voxygen/anim", + "voxygen/chat-i18n", "voxygen/i18n", "voxygen/egui", "world", diff --git a/client/Cargo.toml b/client/Cargo.toml index 525d79f4c8..e7c1328379 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [features] simd = ["vek/platform_intrinsics"] plugins = ["common-state/plugins"] -bin_bot = ["common-ecs", "serde", "ron", "clap", "structopt", "rustyline", "common-frontend", "async-channel"] +bin_bot = ["common-ecs", "serde", "ron", "clap", "structopt", "rustyline", "common-frontend", "async-channel", "voxygen-chat-i18n", "voxygen-i18n"] tracy = ["common-base/tracy"] tick_network = [] @@ -37,6 +37,8 @@ authc = { git = "https://gitlab.com/veloren/auth.git", rev = "fb3dcbc4962b367253 #bot only async-channel = { version = "1.6", optional = true } common-ecs = { package = "veloren-common-ecs", path = "../common/ecs", optional = true } +voxygen-chat-i18n = { package = "veloren-voxygen-chat-i18n", path = "../voxygen/chat-i18n", optional = true } +voxygen-i18n = { package = "veloren-voxygen-i18n", path = "../voxygen/i18n", optional = true } serde = { version = "1.0", features = [ "rc", "derive" ], optional = true } ron = { version = "0.8", default-features = false, optional = true } clap = { version = "3.1.8", optional = true, features = ["color", "std"] } diff --git a/client/examples/chat-cli/main.rs b/client/examples/chat-cli/main.rs index 87f9113870..0a4ddd6460 100644 --- a/client/examples/chat-cli/main.rs +++ b/client/examples/chat-cli/main.rs @@ -11,6 +11,7 @@ use std::{ use tokio::runtime::Runtime; use tracing::{error, info}; use veloren_client::{addr::ConnectionArgs, Client, Event}; +use voxygen_chat_i18n::internationalisate_chat_message; const TPS: u64 = 10; // Low value is okay, just reading messages. @@ -28,6 +29,10 @@ fn main() { // Initialize logging. common_frontend::init_stdout(None); + info!("locading localisation"); + + let localisation = voxygen_i18n::LocalizationHandle::load_expect("en"); + info!("Starting chat-cli..."); // Set up an fps clock. @@ -63,7 +68,7 @@ fn main() { println!("Server info: {:?}", client.server_info()); - println!("Players online: {:?}", client.players().collect::>()); + let mut player_printed = false; let (tx, rx) = mpsc::channel(); thread::spawn(move || { @@ -89,7 +94,16 @@ fn main() { const SHOW_NAME: bool = false; for event in events { match event { - Event::Chat(m) => println!("{}", client.format_message(&m, SHOW_NAME)), + Event::Chat(m) => println!( + "{}", + internationalisate_chat_message( + m, + |msg| client.lockup_msg_context(msg), + &localisation.read(), + SHOW_NAME, + ) + .message + ), Event::Disconnect => {}, // TODO Event::DisconnectionNotification(time) => { let message = match time { @@ -108,5 +122,10 @@ fn main() { // Wait for the next tick. clock.tick(); + + if !player_printed { + println!("Players online: {:?}", client.players().collect::>()); + player_printed = true; + } } -} +} \ No newline at end of file diff --git a/client/src/lib.rs b/client/src/lib.rs index 529efdde58..96b580f6af 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -2643,8 +2643,13 @@ impl Client { /// Get important information from client that is necessary for message /// localisation - pub fn lockup_msg_context(&self, msg: &comp::ChatMsg) -> HashMap<&str, ChatTypeContext> { - let mut result = HashMap::new(); + /// + /// Note: it uses the suffix `name` e.g. in `attacker_name` if Context is Raw, otherwise it returns just `attacker` + pub fn lockup_msg_context( + &self, + msg: &comp::ChatMsg, + ) -> std::collections::HashMap<&'static str, ChatTypeContext> { + let mut result = std::collections::HashMap::new(); let comp::ChatMsg { chat_type, .. } = &msg; let name_of_uid = |uid| { let ecs = self.state.ecs(); diff --git a/voxygen/Cargo.toml b/voxygen/Cargo.toml index 2f76128f27..e821f68ece 100644 --- a/voxygen/Cargo.toml +++ b/voxygen/Cargo.toml @@ -50,6 +50,7 @@ common-state = {package = "veloren-common-state", path = "../common/state"} anim = {package = "veloren-voxygen-anim", path = "anim"} i18n = {package = "veloren-voxygen-i18n", path = "i18n"} +chat-i18n = {package = "veloren-voxygen-chat-i18n", path = "chat-i18n"} voxygen-egui = {package = "veloren-voxygen-egui", path = "egui", optional = true } # Graphics diff --git a/voxygen/chat-i18n/Cargo.toml b/voxygen/chat-i18n/Cargo.toml new file mode 100644 index 0000000000..e48e8e093c --- /dev/null +++ b/voxygen/chat-i18n/Cargo.toml @@ -0,0 +1,13 @@ +[package] +authors = ["juliancoffee "] +edition = "2021" +name = "veloren-voxygen-chat-i18n" +description = "Crate for internalization and diagnostic of existing localizations." +version = "0.10.0" + +[dependencies] +common-net = {package = "veloren-common-net", path = "../../common/net"} +common = {package = "veloren-common", path = "../../common"} +i18n = {package = "veloren-voxygen-i18n", path = "../i18n"} +# Utility +tracing = "0.1" \ No newline at end of file diff --git a/voxygen/chat-i18n/src/lib.rs b/voxygen/chat-i18n/src/lib.rs new file mode 100644 index 0000000000..715e6687ee --- /dev/null +++ b/voxygen/chat-i18n/src/lib.rs @@ -0,0 +1,136 @@ +use common::comp::{ + chat::{KillSource, KillType}, + BuffKind, ChatMsg, ChatType, +}; +use common_net::msg::{ChatTypeContext, PlayerInfo}; +use i18n::Localization; +use std::collections::HashMap; + +pub fn internationalisate_chat_message( + mut msg: ChatMsg, + lookup_fn: impl Fn(&ChatMsg) -> HashMap<&'static str, ChatTypeContext>, + localized_strings: &Localization, + show_char_name: bool, +) -> ChatMsg { + if let Some(template_key) = get_chat_template_key(&msg.chat_type) { + msg.message = localized_strings + .get_msg_ctx(template_key, &i18n::fluent_args! { + "attacker" => "{attacker}", + "attacker" => "{attacker_name}", + "name" => "{player}", + "died_of_buff" => "{died_of_buff}", + "victim" => "{victim}", + "environment" => "{environment}", + }) + .into_owned(); + + if let ChatType::Kill(kill_source, _) = &msg.chat_type { + match kill_source { + KillSource::Player(_, KillType::Buff(buffkind)) + | KillSource::NonExistent(KillType::Buff(buffkind)) + | KillSource::NonPlayer(_, KillType::Buff(buffkind)) => { + msg.message = insert_killing_buff(*buffkind, localized_strings, &msg.message); + }, + _ => {}, + } + } + } + let info = lookup_fn(&msg); + let gen_alias = |you, info: PlayerInfo| { + let mod_str = if info.is_moderator { "MOD - " } else { "" }; + let you_str = if you { "You" } else { &info.player_alias }; + format!("{}{}", mod_str, you_str) + }; + let message_format = |you, info: PlayerInfo, message: &str, group: Option<&String>| { + let alias = gen_alias(you, info.clone()); + let name = if show_char_name { + info.character.map(|c| c.name) + } else { + None + }; + match (group, name) { + (Some(group), None) => format!("({}) [{}]: {}", group, alias, message), + (None, None) => format!("[{}]: {}", alias, message), + (Some(group), Some(name)) => { + format!("({}) [{}] {}: {}", group, alias, name, message) + }, + (None, Some(name)) => format!("[{}] {}: {}", alias, name, message), + } + }; + if let Some(ChatTypeContext::PlayerAlias { you, info }) = info.get("from").cloned() { + msg.message = match &msg.chat_type { + ChatType::Say(_) => message_format(you, info, &msg.message, None), + ChatType::Group(_, s) => message_format(you, info, &msg.message, Some(s)), + ChatType::Faction(_, s) => message_format(you, info, &msg.message, Some(s)), + ChatType::Region(_) => message_format(you, info, &msg.message, None), + ChatType::World(_) => message_format(you, info, &msg.message, None), + ChatType::NpcSay(_, _r) => message_format(you, info, &msg.message, None), + _ => msg.message, + }; + } + for (name, datum) in info.into_iter() { + let replacement = match datum { + ChatTypeContext::PlayerAlias { you, info } => gen_alias(you, info), + ChatTypeContext::Raw(text) => text, + }; + msg.message = msg.message.replace(&format!("{{{}}}", name), &replacement); + } + msg +} + +fn get_chat_template_key(chat_type: &ChatType) -> Option<&str> { + Some(match chat_type { + ChatType::Online(_) => "hud-chat-online_msg", + ChatType::Offline(_) => "hud-chat-offline_msg", + ChatType::Kill(kill_source, _) => match kill_source { + KillSource::Player(_, KillType::Buff(_)) => "hud-chat-died_of_pvp_buff_msg", + KillSource::Player(_, KillType::Melee) => "hud-chat-pvp_melee_kill_msg", + KillSource::Player(_, KillType::Projectile) => "hud-chat-pvp_ranged_kill_msg", + KillSource::Player(_, KillType::Explosion) => "hud-chat-pvp_explosion_kill_msg", + KillSource::Player(_, KillType::Energy) => "hud-chat-pvp_energy_kill_msg", + KillSource::Player(_, KillType::Other) => "hud-chat-pvp_other_kill_msg", + KillSource::NonExistent(KillType::Buff(_)) => "hud-chat-died_of_buff_nonexistent_msg", + KillSource::NonPlayer(_, KillType::Buff(_)) => "hud-chat-died_of_npc_buff_msg", + KillSource::NonPlayer(_, KillType::Melee) => "hud-chat-npc_melee_kill_msg", + KillSource::NonPlayer(_, KillType::Projectile) => "hud-chat-npc_ranged_kill_msg", + KillSource::NonPlayer(_, KillType::Explosion) => "hud-chat-npc_explosion_kill_msg", + KillSource::NonPlayer(_, KillType::Energy) => "hud-chat-npc_energy_kill_msg", + KillSource::NonPlayer(_, KillType::Other) => "hud-chat-npc_other_kill_msg", + KillSource::Environment(_) => "hud-chat-environmental_kill_msg", + KillSource::FallDamage => "hud-chat-fall_kill_msg", + KillSource::Suicide => "hud-chat-suicide_msg", + KillSource::NonExistent(_) | KillSource::Other => "hud-chat-default_death_msg", + }, + _ => return None, + }) +} + +fn insert_killing_buff(buff: BuffKind, localized_strings: &Localization, template: &str) -> String { + let buff_outcome = match buff { + BuffKind::Burning => "hud-outcome-burning", + BuffKind::Bleeding => "hud-outcome-bleeding", + BuffKind::Cursed => "hud-outcome-curse", + BuffKind::Crippled => "hud-outcome-crippled", + BuffKind::Frozen => "hud-outcome-frozen", + BuffKind::Regeneration + | BuffKind::Saturation + | BuffKind::Potion + | BuffKind::CampfireHeal + | BuffKind::EnergyRegen + | BuffKind::IncreaseMaxEnergy + | BuffKind::IncreaseMaxHealth + | BuffKind::Invulnerability + | BuffKind::ProtectingWard + | BuffKind::Frenzied + | BuffKind::Hastened => { + tracing::error!("Player was killed by a positive buff!"); + "hud-outcome-mysterious" + }, + BuffKind::Wet | BuffKind::Ensnared | BuffKind::Poisoned => { + tracing::error!("Player was killed by a debuff that doesn't do damage!"); + "hud-outcome-mysterious" + }, + }; + + template.replace("{died_of_buff}", &localized_strings.get_msg(buff_outcome)) +} diff --git a/voxygen/src/hud/chat.rs b/voxygen/src/hud/chat.rs index 0621d1935f..7d3539d830 100644 --- a/voxygen/src/hud/chat.rs +++ b/voxygen/src/hud/chat.rs @@ -3,13 +3,9 @@ use super::{ OFFLINE_COLOR, ONLINE_COLOR, REGION_COLOR, SAY_COLOR, TELL_COLOR, TEXT_COLOR, WORLD_COLOR, }; use crate::{cmd::complete, settings::chat::MAX_CHAT_TABS, ui::fonts::Fonts, GlobalState}; +use chat_i18n::internationalisate_chat_message; use client::Client; -use common::comp::{ - chat::{KillSource, KillType}, - group::Role, - BuffKind, ChatMode, ChatMsg, ChatType, -}; -use common_net::msg::{ChatTypeContext, PlayerInfo}; +use common::comp::{group::Role, ChatMode, ChatMsg, ChatType}; use conrod_core::{ color, input::Key, @@ -425,8 +421,8 @@ impl<'a> Widget for Chat<'a> { .map(|m| { internationalisate_chat_message( m.clone(), - &self.client, - &self.localized_strings, + |msg| self.client.lockup_msg_context(msg), + self.localized_strings, show_char_name, ) }) @@ -662,80 +658,6 @@ impl<'a> Widget for Chat<'a> { } } -fn internationalisate_chat_message( - mut msg: ChatMsg, - client: &Client, - localized_strings: &Localization, - show_char_name: bool, -) -> ChatMsg { - if let Some(template_key) = get_chat_template_key(&msg.chat_type) { - // FIXME (i18n death messages): - // Death message is half localized in voxygen, half in client. - // Make this not. - msg.message = localized_strings - .get_msg_ctx(template_key, &i18n::fluent_args! { - "attacker" => "{attacker}", - "name" => "{name}", - "died_of_buff" => "{died_of_buff}", - "victim" => "{victim}", - "environment" => "{environment}", - }) - .into_owned(); - - if let ChatType::Kill(kill_source, _) = &msg.chat_type { - match kill_source { - KillSource::Player(_, KillType::Buff(buffkind)) - | KillSource::NonExistent(KillType::Buff(buffkind)) - | KillSource::NonPlayer(_, KillType::Buff(buffkind)) => { - msg.message = insert_killing_buff(*buffkind, localized_strings, &msg.message); - }, - _ => {}, - } - } - } - let info = client.lockup_msg_context(&msg); - let gen_alias = |you, info: PlayerInfo| { - let mod_str = if info.is_moderator { "MOD - " } else { "" }; - let you_str = if you { "You" } else { &info.player_alias }; - format!("{}{}", mod_str, you_str) - }; - let message_format = |you, info: PlayerInfo, message: &str, group: Option<&String>| { - let alias = gen_alias(you, info.clone()); - let name = if show_char_name { - info.character.map(|c| c.name) - } else { - None - }; - match (group, name) { - (Some(group), None) => format!("({}) [{}]: {}", group, alias, message), - (None, None) => format!("[{}]: {}", alias, message), - (Some(group), Some(name)) => { - format!("({}) [{}] {}: {}", group, alias, name, message) - }, - (None, Some(name)) => format!("[{}] {}: {}", alias, name, message), - } - }; - if let Some(ChatTypeContext::PlayerAlias { you, info }) = info.get("from").cloned() { - msg.message = match &msg.chat_type { - ChatType::Say(_) => message_format(you, info, &msg.message, None), - ChatType::Group(_, s) => message_format(you, info, &msg.message, Some(s)), - ChatType::Faction(_, s) => message_format(you, info, &msg.message, Some(s)), - ChatType::Region(_) => message_format(you, info, &msg.message, None), - ChatType::World(_) => message_format(you, info, &msg.message, None), - ChatType::NpcSay(_, _r) => message_format(you, info, &msg.message, None), - _ => msg.message, - }; - } - for (name, datum) in info.into_iter() { - let replacement = match datum { - ChatTypeContext::PlayerAlias { you, info } => gen_alias(you, info), - ChatTypeContext::Raw(text) => text, - }; - msg.message = msg.message.replace(&format!("{{{}}}", name), &replacement); - } - msg -} - fn do_tab_completion(cursor: usize, input: &str, word: &str) -> (String, usize) { let mut pre_ws = None; let mut post_ws = None; @@ -825,63 +747,6 @@ fn render_chat_line(chat_type: &ChatType, imgs: &Imgs) -> (Color, conrod } } -fn insert_killing_buff(buff: BuffKind, localized_strings: &Localization, template: &str) -> String { - let buff_outcome = match buff { - BuffKind::Burning => "hud-outcome-burning", - BuffKind::Bleeding => "hud-outcome-bleeding", - BuffKind::Cursed => "hud-outcome-curse", - BuffKind::Crippled => "hud-outcome-crippled", - BuffKind::Frozen => "hud-outcome-frozen", - BuffKind::Regeneration - | BuffKind::Saturation - | BuffKind::Potion - | BuffKind::CampfireHeal - | BuffKind::EnergyRegen - | BuffKind::IncreaseMaxEnergy - | BuffKind::IncreaseMaxHealth - | BuffKind::Invulnerability - | BuffKind::ProtectingWard - | BuffKind::Frenzied - | BuffKind::Hastened => { - tracing::error!("Player was killed by a positive buff!"); - "hud-outcome-mysterious" - }, - BuffKind::Wet | BuffKind::Ensnared | BuffKind::Poisoned => { - tracing::error!("Player was killed by a debuff that doesn't do damage!"); - "hud-outcome-mysterious" - }, - }; - - template.replace("{died_of_buff}", &localized_strings.get_msg(buff_outcome)) -} - -fn get_chat_template_key(chat_type: &ChatType) -> Option<&str> { - Some(match chat_type { - ChatType::Online(_) => "hud-chat-online_msg", - ChatType::Offline(_) => "hud-chat-offline_msg", - ChatType::Kill(kill_source, _) => match kill_source { - KillSource::Player(_, KillType::Buff(_)) => "hud-chat-died_of_pvp_buff_msg", - KillSource::Player(_, KillType::Melee) => "hud-chat-pvp_melee_kill_msg", - KillSource::Player(_, KillType::Projectile) => "hud-chat-pvp_ranged_kill_msg", - KillSource::Player(_, KillType::Explosion) => "hud-chat-pvp_explosion_kill_msg", - KillSource::Player(_, KillType::Energy) => "hud-chat-pvp_energy_kill_msg", - KillSource::Player(_, KillType::Other) => "hud-chat-pvp_other_kill_msg", - KillSource::NonExistent(KillType::Buff(_)) => "hud-chat-died_of_buff_nonexistent_msg", - KillSource::NonPlayer(_, KillType::Buff(_)) => "hud-chat-died_of_npc_buff_msg", - KillSource::NonPlayer(_, KillType::Melee) => "hud-chat-npc_melee_kill_msg", - KillSource::NonPlayer(_, KillType::Projectile) => "hud-chat-npc_ranged_kill_msg", - KillSource::NonPlayer(_, KillType::Explosion) => "hud-chat-npc_explosion_kill_msg", - KillSource::NonPlayer(_, KillType::Energy) => "hud-chat-npc_energy_kill_msg", - KillSource::NonPlayer(_, KillType::Other) => "hud-chat-npc_other_kill_msg", - KillSource::Environment(_) => "hud-chat-environmental_kill_msg", - KillSource::FallDamage => "hud-chat-fall_kill_msg", - KillSource::Suicide => "hud-chat-suicide_msg", - KillSource::NonExistent(_) | KillSource::Other => "hud-chat-default_death_msg", - }, - _ => return None, - }) -} - fn parse_cmd(msg: &str) -> Result<(String, Vec), String> { use chumsky::prelude::*;