1#![feature(let_chains)]
2use std::borrow::Cow;
3
4use common::{
5 comp::{
6 BuffKind, ChatMsg, ChatType, Content,
7 body::Gender,
8 chat::{KillSource, KillType},
9 },
10 uid::Uid,
11};
12use common_net::msg::{ChatTypeContext, PlayerInfo};
13use i18n::Localization;
14
15pub fn localize_chat_message(
16 msg: &ChatMsg,
17 info: &ChatTypeContext,
18 localization: &Localization,
19 show_char_name: bool,
20) -> (ChatType<String>, String) {
21 let name_format_or_complex = |complex, uid: &Uid| match info.player_info.get(uid).cloned() {
22 Some(pi) => {
23 if complex {
24 insert_alias(info.you == *uid, pi, localization)
25 } else {
26 pi.player_alias
27 }
28 },
29 None => info
30 .entity_name
31 .get(uid)
32 .cloned()
33 .expect("client didn't provided enough info"),
34 };
35
36 let name_format = |uid: &Uid| name_format_or_complex(false, uid);
39
40 let gender_str = |uid: &Uid| {
77 if let Some(pi) = info.player_info.get(uid) {
78 match pi.character.as_ref().and_then(|c| c.gender) {
79 Some(Gender::Feminine) => "she".to_owned(),
80 Some(Gender::Masculine) => "he".to_owned(),
81 None => "??".to_owned(),
82 }
83 } else {
84 "??".to_owned()
85 }
86 };
87
88 let _gender_str_npc = || "idk".to_owned();
131
132 let message_format = |from: &Uid, content: &Content, group: Option<&String>| {
133 let alias = name_format_or_complex(true, from);
134
135 let name = if let Some(pi) = info.player_info.get(from).cloned()
136 && show_char_name
137 {
138 pi.character.map(|c| c.name)
139 } else {
140 None
141 };
142
143 let message = localization.get_content(content);
144
145 let line = match group {
146 Some(group) => match name {
147 Some(name) => localization.get_msg_ctx(
148 "hud-chat-message-in-group-with-name",
149 &i18n::fluent_args! {
150 "group" => group,
151 "alias" => alias,
152 "name" => name,
153 "msg" => message,
154 },
155 ),
156 None => {
157 localization.get_msg_ctx("hud-chat-message-in-group", &i18n::fluent_args! {
158 "group" => group,
159 "alias" => alias,
160 "msg" => message,
161 })
162 },
163 },
164 None => match name {
165 Some(name) => {
166 localization.get_msg_ctx("hud-chat-message-with-name", &i18n::fluent_args! {
167 "alias" => alias,
168 "name" => name,
169 "msg" => message,
170 })
171 },
172 None => localization.get_msg_ctx("hud-chat-message", &i18n::fluent_args! {
173 "alias" => alias,
174 "msg" => message,
175 }),
176 },
177 };
178
179 line.into_owned()
180 };
181
182 let new_msg = match &msg.chat_type {
183 ChatType::Online(uid) => localization
184 .get_msg_ctx("hud-chat-online_msg", &i18n::fluent_args! {
185 "user_gender" => gender_str(uid),
186 "name" => name_format(uid),
187 })
188 .into_owned(),
189 ChatType::Offline(uid) => localization
190 .get_msg_ctx("hud-chat-offline_msg", &i18n::fluent_args! {
191 "user_gender" => gender_str(uid),
192 "name" => name_format(uid),
193 })
194 .into_owned(),
195 ChatType::CommandError
196 | ChatType::CommandInfo
197 | ChatType::Meta
198 | ChatType::FactionMeta(_)
199 | ChatType::GroupMeta(_) => localization.get_content(msg.content()),
200 ChatType::Tell(from, to) => {
201 let (key, person_to_show) = if info.you == *from {
207 ("hud-chat-tell-to", to)
208 } else {
209 ("hud-chat-tell-from", from)
210 };
211
212 localization
213 .get_msg_ctx(key, &i18n::fluent_args! {
214 "alias" => name_format(person_to_show),
215 "user_gender" => gender_str(person_to_show),
216 "msg" => localization.get_content(msg.content()),
217 })
218 .into_owned()
219 },
220 ChatType::Say(uid) | ChatType::Region(uid) | ChatType::World(uid) => {
221 message_format(uid, msg.content(), None)
222 },
223 ChatType::Group(uid, descriptor) | ChatType::Faction(uid, descriptor) => {
224 message_format(uid, msg.content(), Some(descriptor))
225 },
226 ChatType::Npc(uid) | ChatType::NpcSay(uid) => message_format(uid, msg.content(), None),
227 ChatType::NpcTell(from, to) => {
228 let (key, person_to_show) = if info.you == *from {
237 ("hud-chat-tell-to-npc", to)
238 } else {
239 ("hud-chat-tell-from-npc", from)
240 };
241
242 localization
243 .get_msg_ctx(key, &i18n::fluent_args! {
244 "alias" => name_format(person_to_show),
245 "msg" => localization.get_content(msg.content()),
246 })
247 .into_owned()
248 },
249 ChatType::Kill(kill_source, victim) => {
250 localize_kill_message(kill_source, victim, name_format, gender_str, localization)
251 },
252 };
253
254 (msg.chat_type.clone(), new_msg)
255}
256
257fn localize_kill_message(
258 kill_source: &KillSource,
259 victim: &Uid,
260 name_format: impl Fn(&Uid) -> String,
261 gender_str: impl Fn(&Uid) -> String,
262 localization: &Localization,
263) -> String {
264 match kill_source {
265 KillSource::Player(attacker, kill_type) => {
267 let key = match kill_type {
268 KillType::Melee => "hud-chat-pvp_melee_kill_msg",
269 KillType::Projectile => "hud-chat-pvp_ranged_kill_msg",
270 KillType::Explosion => "hud-chat-pvp_explosion_kill_msg",
271 KillType::Energy => "hud-chat-pvp_energy_kill_msg",
272 KillType::Other => "hud-chat-pvp_other_kill_msg",
273 KillType::Buff(buff_kind) => {
274 let buff_ident = get_buff_ident(*buff_kind);
275
276 return localization
277 .get_attr_ctx(
278 "hud-chat-died_of_pvp_buff_msg",
279 buff_ident,
280 &i18n::fluent_args! {
281 "victim" => name_format(victim),
282 "victim_gender" => gender_str(victim),
283 "attacker" => name_format(attacker),
284 "attacker_gender" => gender_str(attacker),
285 },
286 )
287 .into_owned();
288 },
289 };
290 localization.get_msg_ctx(key, &i18n::fluent_args! {
291 "victim" => name_format(victim),
292 "victim_gender" => gender_str(victim),
293 "attacker" => name_format(attacker),
294 "attacker_gender" => gender_str(attacker),
295 })
296 },
297 KillSource::NonPlayer(attacker_name, kill_type) => {
299 let key = match kill_type {
300 KillType::Melee => "hud-chat-npc_melee_kill_msg",
301 KillType::Projectile => "hud-chat-npc_ranged_kill_msg",
302 KillType::Explosion => "hud-chat-npc_explosion_kill_msg",
303 KillType::Energy => "hud-chat-npc_energy_kill_msg",
304 KillType::Other => "hud-chat-npc_other_kill_msg",
305 KillType::Buff(buff_kind) => {
306 let buff_ident = get_buff_ident(*buff_kind);
307
308 return localization
309 .get_attr_ctx(
310 "hud-chat-died_of_npc_buff_msg",
311 buff_ident,
312 &i18n::fluent_args! {
313 "victim" => name_format(victim),
314 "victim_gender" => gender_str(victim),
315 "attacker" => attacker_name,
316 },
317 )
318 .into_owned();
319 },
320 };
321 localization.get_msg_ctx(key, &i18n::fluent_args! {
322 "victim" => name_format(victim),
323 "victim_gender" => gender_str(victim),
324 "attacker" => attacker_name,
325 })
326 },
327 KillSource::FallDamage => {
329 localization.get_msg_ctx("hud-chat-fall_kill_msg", &i18n::fluent_args! {
330 "name" => name_format(victim),
331 "victim_gender" => gender_str(victim),
332 })
333 },
334 KillSource::Suicide => {
335 localization.get_msg_ctx("hud-chat-suicide_msg", &i18n::fluent_args! {
336 "name" => name_format(victim),
337 "victim_gender" => gender_str(victim),
338 })
339 },
340 KillSource::NonExistent(KillType::Buff(buff_kind)) => {
341 let buff_ident = get_buff_ident(*buff_kind);
342
343 let s = localization
344 .get_attr_ctx(
345 "hud-chat-died_of_buff_nonexistent_msg",
346 buff_ident,
347 &i18n::fluent_args! {
348 "victim" => name_format(victim),
349 "victim_gender" => gender_str(victim),
350 },
351 )
352 .into_owned();
353 Cow::Owned(s)
354 },
355 KillSource::NonExistent(_) | KillSource::Other => {
356 localization.get_msg_ctx("hud-chat-default_death_msg", &i18n::fluent_args! {
357 "name" => name_format(victim),
358 "victim_gender" => gender_str(victim),
359 })
360 },
361 }
362 .into_owned()
363}
364
365fn get_buff_ident(buff: BuffKind) -> &'static str {
367 match buff {
368 BuffKind::Burning => "burning",
369 BuffKind::Bleeding => "bleeding",
370 BuffKind::Cursed => "curse",
371 BuffKind::Crippled => "crippled",
372 BuffKind::Frozen => "frozen",
373 BuffKind::Regeneration
374 | BuffKind::Saturation
375 | BuffKind::Potion
376 | BuffKind::Agility
377 | BuffKind::CampfireHeal
378 | BuffKind::EnergyRegen
379 | BuffKind::IncreaseMaxEnergy
380 | BuffKind::IncreaseMaxHealth
381 | BuffKind::Invulnerability
382 | BuffKind::ProtectingWard
383 | BuffKind::Frenzied
384 | BuffKind::Hastened
385 | BuffKind::Fortitude
386 | BuffKind::Reckless
387 | BuffKind::Flame
388 | BuffKind::Frigid
389 | BuffKind::Lifesteal
390 | BuffKind::ImminentCritical
392 | BuffKind::Fury
393 | BuffKind::Sunderer
394 | BuffKind::Defiance
395 | BuffKind::Bloodfeast
396 | BuffKind::Berserk
397 | BuffKind::ScornfulTaunt
398 | BuffKind::Tenacity
399 | BuffKind::Resilience => {
400 tracing::error!("Player was killed by a positive buff!");
401 "mysterious"
402 },
403 BuffKind::Wet
404 | BuffKind::Ensnared
405 | BuffKind::Poisoned
406 | BuffKind::Parried
407 | BuffKind::PotionSickness
408 | BuffKind::Polymorphed
409 | BuffKind::Heatstroke
410 | BuffKind::Rooted
411 | BuffKind::Winded
412 | BuffKind::Concussion
413 | BuffKind::Staggered => {
414 tracing::error!("Player was killed by a debuff that doesn't do damage!");
415 "mysterious"
416 },
417 }
418}
419
420fn insert_alias(_replace_you: bool, info: PlayerInfo, _localization: &Localization) -> String {
424 const MOD_SPACING: &str = " ";
426
427 if info.is_moderator {
428 format!("{}{}", MOD_SPACING, info.player_alias)
429 } else {
430 info.player_alias
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 #[expect(unused)] use super::*;
437 use common::comp::{
438 Content,
439 inventory::item::{ItemDesc, ItemI18n, all_items_expect},
440 };
441 use i18n::LocalizationHandle;
442
443 #[test]
447 fn test_item_text() {
448 let manifest = ItemI18n::new_expect();
449 let localization = LocalizationHandle::load_expect("en").read();
450 let items = all_items_expect();
451
452 for item in items {
453 let (name, desc) = item.i18n(&manifest);
454
455 let Content::Key(key) = name else {
457 panic!("name is expected to be Key, please fix the test");
458 };
459 localization.try_msg(&key).unwrap_or_else(|| {
460 panic!("'{key}' name doesn't have i18n");
461 });
462
463 let Content::Attr(key, attr) = desc else {
465 panic!("desc is expected to be Attr, please fix the test");
466 };
467 localization.try_attr(&key, &attr).unwrap_or_else(|| {
468 panic!("'{key}' description doesn't have i18n");
469 });
470 }
471 }
472}