gettext: move gettext_impl into dedicated crate

This is part of the larger effort of splitting up fish's huge main crate
to improve incremental build speed.

We could extract more logic from `src/wutil/gettext.rs` into the new
crate, but this would require putting wide-string handling into that
crate, which I'm not sure we want. Doing so would have the advantage
that crates which don't depend on fish's main crate (i.e. all crates
other than fish's main crate itself and the binary crates built on top
of it) could then localize messages as well. This will be less relevant
if we replace gettext with Fluent for messages originating from the Rust
sources.

Closes #12108
This commit is contained in:
Daniel Rainer
2025-11-25 00:36:12 +01:00
committed by Johannes Altmanninger
parent aa8f5fc77e
commit 074ab45049
5 changed files with 308 additions and 288 deletions

12
Cargo.lock generated
View File

@@ -160,8 +160,8 @@ dependencies = [
"errno",
"fish-build-helper",
"fish-build-man-pages",
"fish-gettext",
"fish-gettext-extraction",
"fish-gettext-maps",
"fish-gettext-mo-file-parser",
"fish-printf",
"fish-tempfile",
@@ -172,7 +172,6 @@ dependencies = [
"num-traits",
"once_cell",
"pcre2",
"phf 0.12.1",
"phf_codegen 0.12.1",
"portable-atomic",
"rand 0.9.2",
@@ -200,6 +199,15 @@ dependencies = [
"rsconf",
]
[[package]]
name = "fish-gettext"
version = "0.0.0"
dependencies = [
"fish-gettext-maps",
"once_cell",
"phf 0.12.1",
]
[[package]]
name = "fish-gettext-extraction"
version = "0.0.0"

View File

@@ -17,6 +17,7 @@ cfg-if = "1.0.3"
errno = "0.3.0"
fish-build-helper = { path = "crates/build-helper" }
fish-build-man-pages = { path = "crates/build-man-pages" }
fish-gettext = { path = "crates/gettext" }
fish-gettext-extraction = { path = "crates/gettext-extraction" }
fish-gettext-maps = { path = "crates/gettext-maps" }
fish-gettext-mo-file-parser = { path = "crates/gettext-mo-file-parser" }
@@ -87,8 +88,8 @@ cfg-if.workspace = true
errno.workspace = true
fish-build-helper.workspace = true
fish-build-man-pages = { workspace = true, optional = true }
fish-gettext = { workspace = true, optional = true }
fish-gettext-extraction = { workspace = true, optional = true }
fish-gettext-maps = { workspace = true, optional = true }
fish-printf.workspace = true
fish-tempfile.workspace = true
libc.workspace = true
@@ -98,7 +99,6 @@ nix.workspace = true
num-traits.workspace = true
once_cell.workspace = true
pcre2.workspace = true
phf = { workspace = true, optional = true }
rand.workspace = true
terminfo.workspace = true
xterm-color.workspace = true
@@ -156,7 +156,7 @@ benchmark = []
embed-manpages = ["dep:fish-build-man-pages"]
# Enable gettext localization at runtime. Requires the `msgfmt` tool to generate catalog data at
# build time.
localize-messages = ["dep:phf", "dep:fish-gettext-maps"]
localize-messages = ["dep:fish-gettext"]
# This feature is used to enable extracting messages from the source code for localization.
# It only needs to be enabled if updating these messages (and the corresponding PO files) is
# desired. This happens when running tests via `build_tools/check.sh` and when calling

15
crates/gettext/Cargo.toml Normal file
View File

@@ -0,0 +1,15 @@
[package]
name = "fish-gettext"
edition.workspace = true
rust-version.workspace = true
version = "0.0.0"
repository.workspace = true
license.workspace = true
[dependencies]
fish-gettext-maps.workspace = true
once_cell.workspace = true
phf.workspace = true
[lints]
workspace = true

268
crates/gettext/src/lib.rs Normal file
View File

@@ -0,0 +1,268 @@
use fish_gettext_maps::CATALOGS;
use once_cell::sync::Lazy;
use std::{collections::HashSet, sync::Mutex};
type Catalog = &'static phf::Map<&'static str, &'static str>;
pub struct SetLanguageLints<'a> {
pub duplicates: Vec<&'a str>,
pub non_existing: Vec<&'a str>,
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum LanguagePrecedenceOrigin {
Default,
LocaleVariable(LocaleVariable),
LanguageEnvVar,
StatusLanguage,
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum LocaleVariable {
#[allow(clippy::upper_case_acronyms)]
LANG,
#[allow(non_camel_case_types)]
LC_MESSAGES,
#[allow(non_camel_case_types)]
LC_ALL,
}
impl LocaleVariable {
fn as_language_precedence_origin(&self) -> LanguagePrecedenceOrigin {
LanguagePrecedenceOrigin::LocaleVariable(*self)
}
pub fn as_str(&self) -> &'static str {
match self {
Self::LANG => "LANG",
Self::LC_MESSAGES => "LC_MESSAGES",
Self::LC_ALL => "LC_ALL",
}
}
}
impl std::fmt::Display for LocaleVariable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
struct InternalLocalizationState {
precedence_origin: LanguagePrecedenceOrigin,
language_precedence: Vec<(String, Catalog)>,
}
pub struct PublicLocalizationState {
pub precedence_origin: LanguagePrecedenceOrigin,
pub language_precedence: Vec<String>,
}
/// Stores the current localization status.
/// `is_active` indicates whether localization is currently active, and the reason if it is
/// not.
/// The `origin` indicates where the values in `language_precedence` were taken from.
/// `language_precedence` stores the catalogs in the order they should be used.
///
/// This struct should be updated when the relevant variables change or `status language` is used
/// to modify the localization state.
static LOCALIZATION_STATE: Lazy<Mutex<InternalLocalizationState>> =
Lazy::new(|| Mutex::new(InternalLocalizationState::new()));
impl InternalLocalizationState {
fn new() -> Self {
Self {
precedence_origin: LanguagePrecedenceOrigin::Default,
language_precedence: vec![],
}
}
fn to_public(&self) -> PublicLocalizationState {
PublicLocalizationState {
precedence_origin: self.precedence_origin,
language_precedence: self
.language_precedence
.iter()
.map(|(lang, _)| lang.to_owned())
.collect(),
}
}
fn update_from_env(
&mut self,
message_locale: Option<(LocaleVariable, String)>,
language_var: Option<Vec<String>>,
) {
// Do not override values set via `status language`.
if self.precedence_origin == LanguagePrecedenceOrigin::StatusLanguage {
return;
}
if let Some((precedence_origin, locale)) = &message_locale {
// Regular locale names start with lowercase letters (`ll_CC`, followed by some suffix).
// The C or POSIX locale is special, and often used to disable localization.
// Their names are upper-case, but variants with suffixes (`C.UTF-8`) exist.
// To ensure that such variants are accounted for, we match on prefixes of the
// locale name.
// https://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap07.html#tag_07_02
fn is_c_locale(locale: &str) -> bool {
locale.starts_with('C') || locale.starts_with("POSIX")
}
if is_c_locale(locale) {
self.precedence_origin =
LanguagePrecedenceOrigin::LocaleVariable(*precedence_origin);
self.language_precedence.clear();
return;
}
}
let (precedence_origin, language_list) = if let Some(list) = language_var {
(LanguagePrecedenceOrigin::LanguageEnvVar, list)
} else if let Some((precedence_origin, locale)) = message_locale {
let mut normalized_name = String::new();
// Strip off encoding and modifier. (We always expect UTF-8 and don't support modifiers.)
for c in locale.chars() {
if c.is_alphabetic() || c == '_' {
normalized_name.push(c);
} else {
break;
}
}
// At this point, the normalized_name should have the shape `ll` or `ll_CC`.
(
precedence_origin.as_language_precedence_origin(),
vec![normalized_name],
)
} else {
(LanguagePrecedenceOrigin::Default, vec![])
};
let mut seen_languages = HashSet::new();
self.language_precedence = language_list
.into_iter()
.flat_map(|lang| find_existing_catalogs(&lang))
.filter(|(lang, _)| seen_languages.insert(lang.to_owned()))
.collect();
self.precedence_origin = precedence_origin;
}
fn update_from_status_language_builtin<'a, 'b: 'a, S: AsRef<str> + 'a>(
&mut self,
langs: &'b [S],
) -> SetLanguageLints<'a> {
let mut seen = HashSet::new();
let mut duplicates = vec![];
for lang in langs {
let lang = lang.as_ref();
if !seen.insert(lang) {
duplicates.push(lang)
}
}
let mut existing_langs = vec![];
let mut non_existing = vec![];
for lang in langs {
let lang = lang.as_ref();
if let Some(catalog) = CATALOGS.get(lang) {
existing_langs.push((lang.to_owned(), *catalog));
} else {
non_existing.push(lang);
}
}
let mut seen = HashSet::new();
let unique_langs = existing_langs
.into_iter()
.filter(|(lang, _)| seen.insert(lang.to_owned()))
.collect();
self.language_precedence = unique_langs;
self.precedence_origin = LanguagePrecedenceOrigin::StatusLanguage;
SetLanguageLints {
duplicates,
non_existing,
}
}
}
/// Tries to find catalogs for `language`.
/// `language` must be an ISO 639 language code, optionally followed by an underscore and an ISO
/// 3166 country/territory code.
/// Uses the catalog with the exact same name as `language` if it exists.
/// If a country code is present (`ll_CC`), only the catalog named `ll` will be considered as a fallback.
/// If no country code is present (`ll`), all catalogs whose names start with `ll_` will be used in
/// arbitrary order.
fn find_existing_catalogs(language: &str) -> Vec<(String, Catalog)> {
// Try the exact name first.
// If there already is a corresponding catalog return the language.
if let Some(catalog) = CATALOGS.get(language) {
return vec![(language.to_owned(), catalog)];
}
let language_without_country_code = language.split_once('_').map_or(language, |(ll, _cc)| ll);
if language == language_without_country_code {
// We have `ll` format. In this case, try to find any catalog whose name starts with `ll_`.
// Note that it is important to include the underscore in the pattern, otherwise `ll` might
// fall back to `llx_CC`, where `llx` is a 3-letter language identifier.
let ll_prefix = format!("{language}_");
let mut lang_catalogs = vec![];
for (&lang_name, &catalog) in CATALOGS.entries() {
if lang_name.starts_with(&ll_prefix) {
lang_catalogs.push((lang_name.to_owned(), catalog));
}
}
lang_catalogs
} else {
// If `language` contained a country code, we only try to fall back to a catalog
// without a country code.
if let Some(catalog) = CATALOGS.get(language_without_country_code) {
vec![(language_without_country_code.to_owned(), catalog)]
} else {
vec![]
}
}
}
pub fn update_from_env(
locale: Option<(LocaleVariable, String)>,
language_var: Option<Vec<String>>,
) {
let mut localization_state = LOCALIZATION_STATE.lock().unwrap();
localization_state.update_from_env(locale, language_var);
}
pub fn update_from_status_language_builtin<'a, 'b: 'a, S: AsRef<str> + 'a>(
langs: &'b [S],
) -> SetLanguageLints<'a> {
let mut localization_state = LOCALIZATION_STATE.lock().unwrap();
localization_state.update_from_status_language_builtin(langs)
}
pub fn unset_from_status_language_builtin(
locale: Option<(LocaleVariable, String)>,
language_var: Option<Vec<String>>,
) {
let mut localization_state = LOCALIZATION_STATE.lock().unwrap();
localization_state.precedence_origin = LanguagePrecedenceOrigin::Default;
localization_state.update_from_env(locale, language_var);
}
pub fn status_language() -> PublicLocalizationState {
let localization_state = LOCALIZATION_STATE.lock().unwrap();
localization_state.to_public()
}
pub fn gettext(message_str: &'static str) -> Option<&'static str> {
let localization_state = LOCALIZATION_STATE.lock().unwrap();
// Use the localization from the highest-precedence language that has one available.
for (_, catalog) in localization_state.language_precedence.iter() {
if let Some(localized_str) = catalog.get(message_str) {
return Some(localized_str);
}
}
None
}
pub fn list_available_languages() -> Vec<&'static str> {
let mut langs: Vec<_> = CATALOGS.entries().map(|(&lang, _)| lang).collect();
langs.sort();
langs
}

View File

@@ -6,279 +6,8 @@
use once_cell::sync::Lazy;
#[cfg(feature = "localize-messages")]
mod gettext_impl {
use fish_gettext_maps::CATALOGS;
use once_cell::sync::Lazy;
use std::{collections::HashSet, sync::Mutex};
type Catalog = &'static phf::Map<&'static str, &'static str>;
pub struct SetLanguageLints<'a> {
pub duplicates: Vec<&'a str>,
pub non_existing: Vec<&'a str>,
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum LanguagePrecedenceOrigin {
Default,
LocaleVariable(LocaleVariable),
LanguageEnvVar,
StatusLanguage,
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum LocaleVariable {
#[allow(clippy::upper_case_acronyms)]
LANG,
LC_MESSAGES,
LC_ALL,
}
impl LocaleVariable {
fn as_language_precedence_origin(&self) -> LanguagePrecedenceOrigin {
LanguagePrecedenceOrigin::LocaleVariable(*self)
}
pub fn as_str(&self) -> &'static str {
match self {
Self::LANG => "LANG",
Self::LC_MESSAGES => "LC_MESSAGES",
Self::LC_ALL => "LC_ALL",
}
}
}
impl std::fmt::Display for LocaleVariable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
struct InternalLocalizationState {
precedence_origin: LanguagePrecedenceOrigin,
language_precedence: Vec<(String, Catalog)>,
}
pub struct PublicLocalizationState {
pub precedence_origin: LanguagePrecedenceOrigin,
pub language_precedence: Vec<String>,
}
/// Stores the current localization status.
/// `is_active` indicates whether localization is currently active, and the reason if it is
/// not.
/// The `origin` indicates where the values in `language_precedence` were taken from.
/// `language_precedence` stores the catalogs in the order they should be used.
///
/// This struct should be updated when the relevant variables change or `status language` is used
/// to modify the localization state.
static LOCALIZATION_STATE: Lazy<Mutex<InternalLocalizationState>> =
Lazy::new(|| Mutex::new(InternalLocalizationState::new()));
impl InternalLocalizationState {
fn new() -> Self {
Self {
precedence_origin: LanguagePrecedenceOrigin::Default,
language_precedence: vec![],
}
}
fn to_public(&self) -> PublicLocalizationState {
PublicLocalizationState {
precedence_origin: self.precedence_origin,
language_precedence: self
.language_precedence
.iter()
.map(|(lang, _)| lang.to_owned())
.collect(),
}
}
fn update_from_env(
&mut self,
message_locale: Option<(LocaleVariable, String)>,
language_var: Option<Vec<String>>,
) {
// Do not override values set via `status language`.
if self.precedence_origin == LanguagePrecedenceOrigin::StatusLanguage {
return;
}
if let Some((precedence_origin, locale)) = &message_locale {
// Regular locale names start with lowercase letters (`ll_CC`, followed by some suffix).
// The C or POSIX locale is special, and often used to disable localization.
// Their names are upper-case, but variants with suffixes (`C.UTF-8`) exist.
// To ensure that such variants are accounted for, we match on prefixes of the
// locale name.
// https://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap07.html#tag_07_02
fn is_c_locale(locale: &str) -> bool {
locale.starts_with('C') || locale.starts_with("POSIX")
}
if is_c_locale(locale) {
self.precedence_origin =
LanguagePrecedenceOrigin::LocaleVariable(*precedence_origin);
self.language_precedence.clear();
return;
}
}
let (precedence_origin, language_list) = if let Some(list) = language_var {
(LanguagePrecedenceOrigin::LanguageEnvVar, list)
} else if let Some((precedence_origin, locale)) = message_locale {
let mut normalized_name = String::new();
// Strip off encoding and modifier. (We always expect UTF-8 and don't support modifiers.)
for c in locale.chars() {
if c.is_alphabetic() || c == '_' {
normalized_name.push(c);
} else {
break;
}
}
// At this point, the normalized_name should have the shape `ll` or `ll_CC`.
(
precedence_origin.as_language_precedence_origin(),
vec![normalized_name],
)
} else {
(LanguagePrecedenceOrigin::Default, vec![])
};
let mut seen_languages = HashSet::new();
self.language_precedence = language_list
.into_iter()
.flat_map(|lang| find_existing_catalogs(&lang))
.filter(|(lang, _)| seen_languages.insert(lang.to_owned()))
.collect();
self.precedence_origin = precedence_origin;
}
fn update_from_status_language_builtin<'a, 'b: 'a, S: AsRef<str> + 'a>(
&mut self,
langs: &'b [S],
) -> SetLanguageLints<'a> {
let mut seen = HashSet::new();
let mut duplicates = vec![];
for lang in langs {
let lang = lang.as_ref();
if !seen.insert(lang) {
duplicates.push(lang)
}
}
let mut existing_langs = vec![];
let mut non_existing = vec![];
for lang in langs {
let lang = lang.as_ref();
if let Some(catalog) = CATALOGS.get(lang) {
existing_langs.push((lang.to_owned(), *catalog));
} else {
non_existing.push(lang);
}
}
let mut seen = HashSet::new();
let unique_langs = existing_langs
.into_iter()
.filter(|(lang, _)| seen.insert(lang.to_owned()))
.collect();
self.language_precedence = unique_langs;
self.precedence_origin = LanguagePrecedenceOrigin::StatusLanguage;
SetLanguageLints {
duplicates,
non_existing,
}
}
}
/// Tries to find catalogs for `language`.
/// `language` must be an ISO 639 language code, optionally followed by an underscore and an ISO
/// 3166 country/territory code.
/// Uses the catalog with the exact same name as `language` if it exists.
/// If a country code is present (`ll_CC`), only the catalog named `ll` will be considered as a fallback.
/// If no country code is present (`ll`), all catalogs whose names start with `ll_` will be used in
/// arbitrary order.
fn find_existing_catalogs(language: &str) -> Vec<(String, Catalog)> {
// Try the exact name first.
// If there already is a corresponding catalog return the language.
if let Some(catalog) = CATALOGS.get(language) {
return vec![(language.to_owned(), catalog)];
}
let language_without_country_code =
language.split_once('_').map_or(language, |(ll, _cc)| ll);
if language == language_without_country_code {
// We have `ll` format. In this case, try to find any catalog whose name starts with `ll_`.
// Note that it is important to include the underscore in the pattern, otherwise `ll` might
// fall back to `llx_CC`, where `llx` is a 3-letter language identifier.
let ll_prefix = format!("{language}_");
let mut lang_catalogs = vec![];
for (&lang_name, &catalog) in CATALOGS.entries() {
if lang_name.starts_with(&ll_prefix) {
lang_catalogs.push((lang_name.to_owned(), catalog));
}
}
lang_catalogs
} else {
// If `language` contained a country code, we only try to fall back to a catalog
// without a country code.
if let Some(catalog) = CATALOGS.get(language_without_country_code) {
vec![(language_without_country_code.to_owned(), catalog)]
} else {
vec![]
}
}
}
pub(super) fn update_from_env(
locale: Option<(LocaleVariable, String)>,
language_var: Option<Vec<String>>,
) {
let mut localization_state = LOCALIZATION_STATE.lock().unwrap();
localization_state.update_from_env(locale, language_var);
}
pub(super) fn update_from_status_language_builtin<'a, 'b: 'a, S: AsRef<str> + 'a>(
langs: &'b [S],
) -> SetLanguageLints<'a> {
let mut localization_state = LOCALIZATION_STATE.lock().unwrap();
localization_state.update_from_status_language_builtin(langs)
}
pub(super) fn unset_from_status_language_builtin(
locale: Option<(LocaleVariable, String)>,
language_var: Option<Vec<String>>,
) {
let mut localization_state = LOCALIZATION_STATE.lock().unwrap();
localization_state.precedence_origin = LanguagePrecedenceOrigin::Default;
localization_state.update_from_env(locale, language_var);
}
pub(super) fn status_language() -> PublicLocalizationState {
let localization_state = LOCALIZATION_STATE.lock().unwrap();
localization_state.to_public()
}
pub(super) fn gettext(message_str: &'static str) -> Option<&'static str> {
let localization_state = LOCALIZATION_STATE.lock().unwrap();
// Use the localization from the highest-precedence language that has one available.
for (_, catalog) in localization_state.language_precedence.iter() {
if let Some(localized_str) = catalog.get(message_str) {
return Some(localized_str);
}
}
None
}
pub(super) fn list_available_languages() -> Vec<&'static str> {
let mut langs: Vec<_> = CATALOGS.entries().map(|(&lang, _)| lang).collect();
langs.sort();
langs
}
}
#[cfg(feature = "localize-messages")]
fn get_message_locale(vars: &EnvStack) -> Option<(gettext_impl::LocaleVariable, String)> {
use gettext_impl::LocaleVariable;
fn get_message_locale(vars: &EnvStack) -> Option<(fish_gettext::LocaleVariable, String)> {
use fish_gettext::LocaleVariable;
let get = |var_str: &wstr, var: LocaleVariable| {
vars.get_unless_empty(var_str)
.map(|val| (var, val.as_string().to_string()))
@@ -327,7 +56,7 @@ fn get_language_var(vars: &EnvStack) -> Option<Vec<String>> {
/// them will be used, in arbitrary order.
#[cfg(feature = "localize-messages")]
pub fn update_from_env(vars: &EnvStack) {
gettext_impl::update_from_env(get_message_locale(vars), get_language_var(vars));
fish_gettext::update_from_env(get_message_locale(vars), get_language_var(vars));
}
#[cfg(feature = "localize-messages")]
@@ -350,8 +79,8 @@ pub struct SetLanguageLints<'a> {
}
#[cfg(feature = "localize-messages")]
impl<'a> From<gettext_impl::SetLanguageLints<'a>> for SetLanguageLints<'a> {
fn from(lints: gettext_impl::SetLanguageLints<'a>) -> Self {
impl<'a> From<fish_gettext::SetLanguageLints<'a>> for SetLanguageLints<'a> {
fn from(lints: fish_gettext::SetLanguageLints<'a>) -> Self {
Self {
duplicates: lints.duplicates,
non_existing: lints.non_existing,
@@ -396,12 +125,12 @@ pub fn display_all(&self) -> WString {
pub fn update_from_status_language_builtin<'a, 'b: 'a, S: AsRef<str> + 'a>(
langs: &'b [S],
) -> SetLanguageLints<'a> {
gettext_impl::update_from_status_language_builtin(langs).into()
fish_gettext::update_from_status_language_builtin(langs).into()
}
#[cfg(feature = "localize-messages")]
pub fn unset_from_status_language_builtin(vars: &EnvStack) {
gettext_impl::unset_from_status_language_builtin(
fish_gettext::unset_from_status_language_builtin(
get_message_locale(vars),
get_language_var(vars),
);
@@ -409,8 +138,8 @@ pub fn unset_from_status_language_builtin(vars: &EnvStack) {
#[cfg(feature = "localize-messages")]
pub fn status_language() -> WString {
use gettext_impl::LanguagePrecedenceOrigin;
let localization_state = gettext_impl::status_language();
use fish_gettext::LanguagePrecedenceOrigin;
let localization_state = fish_gettext::status_language();
let mut result = WString::new();
localizable_consts!(
LANGUAGE_LIST_VARIABLE_ORIGIN "from variable %s"
@@ -437,7 +166,7 @@ pub fn status_language() -> WString {
#[cfg(feature = "localize-messages")]
pub fn list_available_languages() -> WString {
let mut languages = WString::new();
for lang in gettext_impl::list_available_languages() {
for lang in fish_gettext::list_available_languages() {
languages.push_str(lang);
languages.push('\n');
}
@@ -457,7 +186,7 @@ pub fn initialize_gettext() {
env_stack_set_from_env!(vars, "LC_MESSAGES");
env_stack_set_from_env!(vars, "LANG");
gettext_impl::update_from_env(get_message_locale(&vars), get_language_var(&vars));
fish_gettext::update_from_env(get_message_locale(&vars), get_language_var(&vars));
}
/// Use this function to localize a message.
@@ -495,7 +224,7 @@ fn gettext(message: MaybeStatic) -> &'static wstr {
let () = message_str;
#[cfg(feature = "localize-messages")]
{
if let Some(localized_str) = gettext_impl::gettext(message_str) {
if let Some(localized_str) = fish_gettext::gettext(message_str) {
static LOCALIZATION_TO_WIDE: Lazy<Mutex<HashMap<&'static str, &'static wstr>>> =
Lazy::new(|| Mutex::new(HashMap::default()));
let mut localizations_to_wide = LOCALIZATION_TO_WIDE.lock().unwrap();