pub const WHALE_NICKNAMES: &[&str] = &[
"Blue",
"蓝鲸",
"Humpback",
"座头鲸",
"Sperm",
"抹香鲸",
"Fin",
"长须鲸",
"Sei",
"塞鲸",
"Bryde's",
"布氏鲸",
"Minke",
"小须鲸",
"Antarctic Minke",
"南极小须鲸",
"Pygmy Right",
"小露脊鲸",
"Omura's",
"大村鲸",
"Eden's",
"艾氏鲸",
"Rice's",
"赖斯鲸",
"Gray",
"灰鲸",
"Bowhead",
"弓头鲸",
"North Atlantic Right",
"北大西洋露脊鲸",
"North Pacific Right",
"北太平洋露脊鲸",
"Southern Right",
"南露脊鲸",
"Beluga",
"白鲸",
"Narwhal",
"独角鲸",
"Orca",
"虎鲸",
"Pilot",
"领航鲸",
"False Killer",
"伪虎鲸",
"Pygmy Killer",
"小虎鲸",
"Melon-headed",
"瓜头鲸",
"Beaked",
"喙鲸",
"Cuvier's Beaked",
"柯氏喙鲸",
"Baird's Beaked",
"贝氏喙鲸",
"Blainville's Beaked",
"柏氏喙鲸",
"Ginkgo-toothed Beaked",
"银杏齿喙鲸",
"Strap-toothed",
"带齿喙鲸",
"Stejneger's Beaked",
"斯氏喙鲸",
"Dwarf Sperm",
"小抹香鲸",
"Pygmy Sperm",
"侏儒抹香鲸",
"Rough-toothed",
"糙齿海豚",
"Atlantic Spotted",
"大西洋斑海豚",
"Pantropical Spotted",
"热带斑海豚",
"Spinner",
"长吻飞旋海豚",
"Clymene",
"短吻飞旋海豚",
"Striped",
"条纹海豚",
"Common Bottlenose",
"宽吻海豚",
"Indo-Pacific Bottlenose",
"印太瓶鼻海豚",
"Risso's",
"灰海豚",
"Commerson's",
"花斑海豚",
"Chilean",
"智利海豚",
"Heaviside's",
"海氏矮海豚",
"Hector's",
"赫氏矮海豚",
"Amazon River",
"亚马逊河豚",
"Ganges River",
"恒河豚",
"Indus River",
"印度河豚",
"La Plata",
"拉普拉塔河豚",
"Franciscana",
"拉河豚",
];
pub(super) const WHALE_NICKNAMES_JA: &[&str] = &[
"シロナガスクジラ",
"ザトウクジラ",
"マッコウクジラ",
"ナガスクジラ",
"イワシクジラ",
"ミンククジラ",
"コククジラ",
"ホッキョククジラ",
"シロイルカ",
"イッカク",
"シャチ",
"ゴンドウクジラ",
];
pub(super) const WHALE_NICKNAMES_ZH_HANT: &[&str] = &[
"藍鯨",
"座頭鯨",
"抹香鯨",
"長鬚鯨",
"塞鯨",
"布氏鯨",
"小鬚鯨",
"灰鯨",
"弓頭鯨",
"白鯨",
"獨角鯨",
"虎鯨",
];
pub(super) const WHALE_NICKNAMES_PT_BR: &[&str] = &[
"Azul",
"Jubarte",
"Cachalote",
"Baleia-fin",
"Baleia-sei",
"Baleia-de-bryde",
"Baleia-minke",
"Cinzenta",
"Baleia-franca",
"Beluga",
"Narval",
"Orca",
];
pub(super) const WHALE_NICKNAMES_ES_419: &[&str] = &[
"Azul",
"Jorobada",
"Cachalote",
"Rorcual común",
"Rorcual sei",
"Rorcual de Bryde",
"Rorcual aliblanco",
"Gris",
"Ballena franca",
"Beluga",
"Narval",
"Orca",
];
pub(super) const WHALE_NICKNAMES_VI: &[&str] = &[
"Cá voi xanh",
"Cá voi lưng gù",
"Cá nhà táng",
"Cá voi vây",
"Cá voi Sei",
"Cá voi Bryde",
"Cá voi Minke",
"Cá voi xám",
"Cá voi đầu cong",
"Cá voi trắng",
"Kỳ lân biển",
"Cá voi sát thủ",
];
pub(super) const WHALE_NICKNAMES_KO: &[&str] = &[
"대왕고래",
"혹등고래",
"향유고래",
"참고래",
"보리고래",
"브라이드고래",
"밍크고래",
"귀신고래",
"북극고래",
"흰고래",
"외뿔고래",
"범고래",
];
pub(super) const WHALE_NICKNAMES_CA: &[&str] = &[
"Balena blava",
"Balena geperuda",
"Catxalot",
"Rorcual comú",
"Rorcual boreal",
"Balena minke",
"Balena franca",
"Balena de Groenlàndia",
"Beluga",
"Narval",
"Orca",
"Calderó",
];
pub(super) const WHALE_NICKNAMES_DE: &[&str] = &[
"Blauwal",
"Buckelwal",
"Pottwal",
"Finnwal",
"Seiwal",
"Zwergwal",
"Glattwal",
"Grönlandwal",
"Beluga",
"Narwal",
"Orca",
"Pilotwal",
];
pub(super) const WHALE_NICKNAMES_FR: &[&str] = &[
"Baleine bleue",
"Baleine à bosse",
"Cachalot",
"Rorqual commun",
"Rorqual boréal",
"Petit rorqual",
"Baleine franche",
"Baleine boréale",
"Béluga",
"Narval",
"Orque",
"Globicéphale",
];
pub(super) const WHALE_NICKNAMES_ID: &[&str] = &[
"Paus biru",
"Paus bungkuk",
"Paus sperma",
"Paus sirip",
"Paus sei",
"Paus minke",
"Paus sikat",
"Paus bowhead",
"Beluga",
"Narwal",
"Orca",
"Paus pilot",
];
pub(super) const WHALE_NICKNAMES_HI: &[&str] = &[
"नीली व्हेल",
"कूबड़ व्हेल",
"स्पर्म व्हेल",
"फिन व्हेल",
"सेई व्हेल",
"मिंक व्हेल",
"राइट व्हेल",
"बोहेड व्हेल",
"बेलुगा",
"नार्व्हल",
"ओर्का",
"पायलट व्हेल",
];
pub(super) const WHALE_NICKNAMES_RU: &[&str] = &[
"Синий кит",
"Горбатый кит",
"Кашалот",
"Финвал",
"Сейвал",
"Малый полосатик",
"Гладкий кит",
"Гренландский кит",
"Белуха",
"Нарвал",
"Косатка",
"Гринда",
];
pub(super) const WHALE_NICKNAMES_UK: &[&str] = &[
"Синій кит",
"Горбатий кит",
"Кашалот",
"Фінвал",
"Сейвал",
"Малий смугач",
"Гладкий кит",
"Гренландський кит",
"Белуга",
"Нарвал",
"Косатка",
"Гринда",
];
#[must_use]
pub fn whale_name_for_id_in_locale(id: &str, locale_tag: &str) -> String {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
id.hash(&mut hasher);
let hash = hasher.finish() as usize;
let normalized = locale_tag.trim().to_ascii_lowercase();
let localized_pool = match normalized.as_str() {
"ja" => Some(WHALE_NICKNAMES_JA),
"zh-hant" => Some(WHALE_NICKNAMES_ZH_HANT),
"pt-br" => Some(WHALE_NICKNAMES_PT_BR),
"es-419" => Some(WHALE_NICKNAMES_ES_419),
"vi" => Some(WHALE_NICKNAMES_VI),
"ko" => Some(WHALE_NICKNAMES_KO),
"ca" => Some(WHALE_NICKNAMES_CA),
"de" => Some(WHALE_NICKNAMES_DE),
"fr" => Some(WHALE_NICKNAMES_FR),
"id" => Some(WHALE_NICKNAMES_ID),
"hi" => Some(WHALE_NICKNAMES_HI),
"ru" => Some(WHALE_NICKNAMES_RU),
"uk" => Some(WHALE_NICKNAMES_UK),
_ => None,
};
if let Some(pool) = localized_pool {
return pool[hash % pool.len()].to_string();
}
debug_assert_eq!(WHALE_NICKNAMES.len() % 2, 0);
let pair_count = WHALE_NICKNAMES.len() / 2;
let pair = hash % pair_count;
let language_offset = usize::from(normalized == "zh-hans");
let idx = pair * 2 + language_offset;
WHALE_NICKNAMES[idx].to_string()
}
#[must_use]
pub fn assign_unique_whale_name_in_locale(
id: &str,
active_names: &std::collections::HashSet<String>,
locale_tag: &str,
) -> String {
let base = whale_name_for_id_in_locale(id, locale_tag);
if !active_names.contains(&base) {
return base;
}
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
id.hash(&mut hasher);
let suffix_seed = hasher.finish();
for i in 2.. {
let candidate = format!("{base} ({i})");
if !active_names.contains(&candidate) {
return candidate;
}
let probe = (suffix_seed.wrapping_add(i as u64)) % 100;
let candidate2 = format!("{base} ({probe})");
if !active_names.contains(&candidate2) {
return candidate2;
}
}
format!("{base} ({})", id.get(..4).unwrap_or("?"))
}
pub(super) fn generated_whale_name_base<'a>(agent_id: &str, name: &'a str) -> Option<&'a str> {
let name = name.trim();
if name.is_empty() {
return None;
}
let base = name
.rsplit_once(" (")
.and_then(|(base, suffix)| {
suffix
.strip_suffix(')')
.filter(|number| !number.is_empty() && number.chars().all(|ch| ch.is_ascii_digit()))
.map(|_| base)
})
.unwrap_or(name);
crate::localization::Locale::shipped()
.iter()
.any(|locale| whale_name_for_id_in_locale(agent_id, locale.tag()) == base)
.then_some(base)
}
#[must_use]
pub(crate) fn localized_whale_display_names<'a>(
agents: impl IntoIterator<Item = (&'a str, Option<&'a str>)>,
locale_tag: &str,
) -> std::collections::HashMap<String, String> {
let mut by_id = std::collections::BTreeMap::<String, Option<String>>::new();
for (agent_id, nickname) in agents {
if agent_id.trim().is_empty() {
continue;
}
let nickname = nickname
.map(str::trim)
.filter(|name| !name.is_empty())
.map(str::to_string);
by_id
.entry(agent_id.to_string())
.and_modify(|existing| {
if existing.is_none() && nickname.is_some() {
*existing = nickname.clone();
}
})
.or_insert(nickname);
}
let mut names = std::collections::HashMap::with_capacity(by_id.len());
let mut active_names = std::collections::HashSet::new();
for (agent_id, nickname) in &by_id {
let Some(nickname) = nickname
.as_deref()
.filter(|name| generated_whale_name_base(agent_id, name).is_none())
else {
continue;
};
active_names.insert(nickname.to_string());
names.insert(agent_id.clone(), nickname.to_string());
}
for agent_id in by_id.keys() {
if names.contains_key(agent_id) {
continue;
}
let name = assign_unique_whale_name_in_locale(agent_id, &active_names, locale_tag);
active_names.insert(name.clone());
names.insert(agent_id.clone(), name);
}
names
}