use std::collections::HashMap;
use gpui::{App, Global, SharedString};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum UiString {
SelectPlaceholder,
NoResults,
Close,
Loading,
Previous,
Next,
Remove,
Breadcrumbs,
Search,
}
impl UiString {
pub const ALL: [UiString; 9] = [
UiString::SelectPlaceholder,
UiString::NoResults,
UiString::Close,
UiString::Loading,
UiString::Previous,
UiString::Next,
UiString::Remove,
UiString::Breadcrumbs,
UiString::Search,
];
fn index(self) -> usize {
self as usize
}
}
pub const LOCALES: [&str; 9] = [
"en-US", "de-DE", "es-ES", "fr-FR", "it-IT", "nl-NL", "pl-PL", "pt-BR", "sv-SE",
];
const EN: [&str; 9] = [
"Select an item",
"No results found",
"Close",
"Loading",
"Previous",
"Next",
"Remove",
"Breadcrumbs",
"Search",
];
type Row = [Option<&'static str>; 9];
const TABLE: [Row; 8] = [
[
Some("Element wählen"),
None,
Some("Schließen"),
None,
Some("Zurück"),
Some("Weiter"),
Some("Entfernen"),
Some("Breadcrumbs"),
None,
],
[
Some("Seleccionar un artículo"),
None,
Some("Cerrar"),
None,
Some("Anterior"),
Some("Siguiente"),
Some("Quitar"),
Some("Migas de pan"),
None,
],
[
Some("Sélectionner un élément"),
None,
Some("Fermer"),
None,
Some("Précédent"),
Some("Suivant"),
Some("Supprimer"),
Some("Chemin de navigation"),
None,
],
[
Some("Seleziona un elemento"),
None,
Some("Chiudi"),
None,
Some("Precedente"),
Some("Successivo"),
Some("Rimuovi"),
Some("Breadcrumb"),
None,
],
[
Some("Selecteer een item"),
None,
Some("Sluiten"),
None,
Some("Vorige"),
Some("Volgende"),
Some("Verwijderen"),
Some("Broodkruimels"),
None,
],
[
Some("Wybierz element"),
None,
Some("Zamknij"),
None,
Some("Wstecz"),
Some("Dalej"),
Some("Usuń"),
Some("Struktura nawigacyjna"),
None,
],
[
Some("Selecione um item"),
None,
Some("Fechar"),
None,
Some("Anterior"),
Some("Próximo"),
Some("Remover"),
Some("Caminho detalhado"),
None,
],
[
Some("Välj en artikel"),
None,
Some("Stäng"),
None,
Some("Föregående"),
Some("Nästa"),
Some("Ta bort"),
Some("Sökvägar"),
None,
],
];
fn language(tag: &str) -> &str {
tag.split(['-', '_']).next().unwrap_or(tag)
}
pub fn resolve_locale(tag: &str) -> &'static str {
let normalized = tag.replace('_', "-");
if let Some(exact) = LOCALES.iter().find(|l| l.eq_ignore_ascii_case(&normalized)) {
return exact;
}
let lang = language(&normalized);
LOCALES
.iter()
.find(|l| language(l).eq_ignore_ascii_case(lang))
.copied()
.unwrap_or("en-US")
}
pub fn lookup(locale: &str, key: UiString) -> &'static str {
let resolved = resolve_locale(locale);
LOCALES
.iter()
.position(|l| *l == resolved)
.and_then(|row| row.checked_sub(1))
.and_then(|row| TABLE[row][key.index()])
.unwrap_or(EN[key.index()])
}
#[derive(Default)]
struct UiStrings {
locale: Option<SharedString>,
overrides: HashMap<(SharedString, UiString), SharedString>,
}
impl Global for UiStrings {}
pub fn set_locale(locale: impl Into<SharedString>, cx: &mut App) {
cx.default_global::<UiStrings>().locale = Some(locale.into());
cx.refresh_windows();
}
pub fn locale(cx: &App) -> SharedString {
cx.try_global::<UiStrings>()
.and_then(|s| s.locale.clone())
.unwrap_or_else(|| SharedString::new_static("en-US"))
}
pub fn set_ui_string(
locale: impl Into<SharedString>,
key: UiString,
value: impl Into<SharedString>,
cx: &mut App,
) {
cx.default_global::<UiStrings>()
.overrides
.insert((locale.into(), key), value.into());
cx.refresh_windows();
}
pub fn ui_string(key: UiString, cx: &App) -> SharedString {
let Some(strings) = cx.try_global::<UiStrings>() else {
return SharedString::new_static(EN[key.index()]);
};
let active = strings
.locale
.clone()
.unwrap_or_else(|| SharedString::new_static("en-US"));
let lang = language(&active);
let found = strings
.overrides
.get(&(active.clone(), key))
.or_else(|| {
strings
.overrides
.get(&(SharedString::from(lang.to_owned()), key))
})
.cloned();
found.unwrap_or_else(|| SharedString::new_static(lookup(&active, key)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn en_us_matches_the_strings_components_drew_before() {
for key in UiString::ALL {
assert_eq!(lookup("en-US", key), EN[key.index()]);
}
assert_eq!(
lookup("en-US", UiString::SelectPlaceholder),
"Select an item"
);
}
#[test]
fn every_locale_resolves_every_key() {
for locale in LOCALES {
for key in UiString::ALL {
assert!(!lookup(locale, key).is_empty(), "{locale} {key:?}");
}
}
}
#[test]
fn fallback_chain() {
assert_eq!(resolve_locale("fr_CA"), "fr-FR");
assert_eq!(resolve_locale("PT-pt"), "pt-BR");
assert_eq!(resolve_locale("ja-JP"), "en-US");
assert_eq!(lookup("fr-FR", UiString::Loading), "Loading");
assert_eq!(lookup("sv-SE", UiString::Previous), "Föregående");
}
}