use std::sync::{Arc, OnceLock};
use ashlar_i18n::I18n;
use crate::chat::ChatLabels;
use crate::commands::keymap_editor::KeymapLabels;
use crate::diff_view::DiffLabels;
use crate::form::FormLabels;
use crate::markdown::MarkdownLabels;
use crate::sidebar::SidebarLabels;
use crate::toasts::ToastLabels;
pub const CATALOGS: &[(&str, &str)] = &[("en", include_str!("../i18n/en.json")), ("de", include_str!("../i18n/de.json"))];
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TabLabels {
pub overflow: String,
pub close: String,
}
impl Default for TabLabels {
fn default() -> Self {
TabLabels { overflow: "More tabs".into(), close: "Close tab".into() }
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PrintLabels {
pub printer: String,
pub select_printer: String,
pub refresh: String,
pub copies: String,
pub printer_default: String,
pub duplex: String,
pub one_sided: String,
pub long_edge: String,
pub short_edge: String,
pub grayscale: String,
}
impl Default for PrintLabels {
fn default() -> Self {
PrintLabels {
printer: "Printer".into(),
select_printer: "Select a printer".into(),
refresh: "Refresh".into(),
copies: "Copies".into(),
printer_default: "Printer default".into(),
duplex: "Two-sided".into(),
one_sided: "One-sided".into(),
long_edge: "Long edge".into(),
short_edge: "Short edge".into(),
grayscale: "Grayscale".into(),
}
}
}
impl PrintLabels {
pub fn as_labels(&self) -> crate::printing::Labels<'_> {
crate::printing::Labels {
printer: &self.printer,
select_printer: &self.select_printer,
refresh: &self.refresh,
copies: &self.copies,
printer_default: &self.printer_default,
duplex: &self.duplex,
one_sided: &self.one_sided,
long_edge: &self.long_edge,
short_edge: &self.short_edge,
grayscale: &self.grayscale,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct UiLabels {
pub toast: ToastLabels,
pub form: FormLabels,
pub chat: ChatLabels,
pub diff: DiffLabels,
pub keymap: KeymapLabels,
pub tabs: TabLabels,
pub print: PrintLabels,
pub search: SearchLabels,
pub markdown: MarkdownLabels,
pub controls: ControlLabels,
pub sidebar: SidebarLabels,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ControlLabels {
pub loading: String,
}
impl Default for ControlLabels {
fn default() -> Self {
ControlLabels { loading: "Loading".into() }
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SearchLabels {
pub field: String,
pub clear: String,
pub palette: String,
}
impl Default for SearchLabels {
fn default() -> Self {
SearchLabels { field: "Search".into(), clear: "Clear search".into(), palette: "Command palette".into() }
}
}
pub const KEYS: &[&str] = &[
"ui.toast.dismiss",
"ui.toast.more",
"ui.form.no_match",
"ui.form.no_match_body",
"ui.form.reset",
"ui.form.required",
"ui.chat.transcript",
"ui.chat.jump_to_latest",
"ui.chat.user",
"ui.chat.assistant",
"ui.chat.system",
"ui.chat.tool",
"ui.chat.streaming",
"ui.chat.approve",
"ui.chat.deny",
"ui.chat.show_details",
"ui.chat.hide_details",
"ui.chat.pending",
"ui.chat.running",
"ui.chat.succeeded",
"ui.chat.failed",
"ui.chat.needs_approval",
"ui.chat.approved",
"ui.chat.denied",
"ui.chat.suggestions",
"ui.chat.composer",
"ui.diff.expand",
"ui.diff.hidden",
"ui.diff.binary",
"ui.diff.added",
"ui.diff.removed",
"ui.keymap.search",
"ui.keymap.command",
"ui.keymap.shortcut",
"ui.keymap.recording",
"ui.keymap.unbound",
"ui.keymap.reset",
"ui.keymap.clear",
"ui.keymap.conflicts_with",
"ui.keymap.table",
"ui.keymap.actions",
"ui.search.field",
"ui.search.clear",
"ui.search.palette",
"ui.tabs.overflow",
"ui.tabs.close",
"ui.print.printer",
"ui.print.select_printer",
"ui.print.refresh",
"ui.print.copies",
"ui.print.printer_default",
"ui.print.duplex",
"ui.print.one_sided",
"ui.print.long_edge",
"ui.print.short_edge",
"ui.print.grayscale",
"ui.markdown.copy",
"ui.markdown.copied",
"ui.markdown.code",
"ui.markdown.more_rows",
"ui.controls.loading",
"ui.sidebar.rename",
"ui.sidebar.items",
];
pub fn add_catalogs(i18n: &mut I18n) {
for (locale, json) in CATALOGS {
i18n.add(locale, *json);
}
}
impl UiLabels {
pub fn from_i18n(i18n: &I18n) -> Self {
let mut l = UiLabels::default();
let set = |field: &mut String, key: &str| {
if let Some(text) = i18n.get(key) {
text.clone_into(field);
}
};
let t = &mut l.toast;
set(&mut t.dismiss, "ui.toast.dismiss");
set(&mut t.more, "ui.toast.more");
let f = &mut l.form;
set(&mut f.no_match, "ui.form.no_match");
set(&mut f.no_match_body, "ui.form.no_match_body");
set(&mut f.reset, "ui.form.reset");
set(&mut f.required, "ui.form.required");
let c = &mut l.chat;
for (field, key) in [
(&mut c.transcript, "ui.chat.transcript"),
(&mut c.jump_to_latest, "ui.chat.jump_to_latest"),
(&mut c.user, "ui.chat.user"),
(&mut c.assistant, "ui.chat.assistant"),
(&mut c.system, "ui.chat.system"),
(&mut c.tool, "ui.chat.tool"),
(&mut c.streaming, "ui.chat.streaming"),
(&mut c.approve, "ui.chat.approve"),
(&mut c.deny, "ui.chat.deny"),
(&mut c.show_details, "ui.chat.show_details"),
(&mut c.hide_details, "ui.chat.hide_details"),
(&mut c.pending, "ui.chat.pending"),
(&mut c.running, "ui.chat.running"),
(&mut c.succeeded, "ui.chat.succeeded"),
(&mut c.failed, "ui.chat.failed"),
(&mut c.needs_approval, "ui.chat.needs_approval"),
(&mut c.approved, "ui.chat.approved"),
(&mut c.denied, "ui.chat.denied"),
(&mut c.suggestions, "ui.chat.suggestions"),
(&mut c.composer, "ui.chat.composer"),
] {
set(field, key);
}
let d = &mut l.diff;
set(&mut d.expand, "ui.diff.expand");
set(&mut d.hidden, "ui.diff.hidden");
set(&mut d.binary, "ui.diff.binary");
set(&mut d.added, "ui.diff.added");
set(&mut d.removed, "ui.diff.removed");
let k = &mut l.keymap;
for (field, key) in [
(&mut k.search, "ui.keymap.search"),
(&mut k.command, "ui.keymap.command"),
(&mut k.shortcut, "ui.keymap.shortcut"),
(&mut k.recording, "ui.keymap.recording"),
(&mut k.unbound, "ui.keymap.unbound"),
(&mut k.reset, "ui.keymap.reset"),
(&mut k.clear, "ui.keymap.clear"),
(&mut k.conflicts_with, "ui.keymap.conflicts_with"),
(&mut k.table, "ui.keymap.table"),
(&mut k.actions, "ui.keymap.actions"),
] {
set(field, key);
}
set(&mut l.search.field, "ui.search.field");
set(&mut l.search.clear, "ui.search.clear");
set(&mut l.search.palette, "ui.search.palette");
set(&mut l.tabs.overflow, "ui.tabs.overflow");
set(&mut l.tabs.close, "ui.tabs.close");
let p = &mut l.print;
for (field, key) in [
(&mut p.printer, "ui.print.printer"),
(&mut p.select_printer, "ui.print.select_printer"),
(&mut p.refresh, "ui.print.refresh"),
(&mut p.copies, "ui.print.copies"),
(&mut p.printer_default, "ui.print.printer_default"),
(&mut p.duplex, "ui.print.duplex"),
(&mut p.one_sided, "ui.print.one_sided"),
(&mut p.long_edge, "ui.print.long_edge"),
(&mut p.short_edge, "ui.print.short_edge"),
(&mut p.grayscale, "ui.print.grayscale"),
] {
set(field, key);
}
let m = &mut l.markdown;
for (field, key) in [
(&mut m.copy, "ui.markdown.copy"),
(&mut m.copied, "ui.markdown.copied"),
(&mut m.code, "ui.markdown.code"),
(&mut m.more_rows, "ui.markdown.more_rows"),
] {
if let Some(text) = i18n.get(key) {
*field = text.to_owned().into();
}
}
set(&mut l.controls.loading, "ui.controls.loading");
set(&mut l.sidebar.rename, "ui.sidebar.rename");
set(&mut l.sidebar.items, "ui.sidebar.items");
l
}
}
fn id() -> egui::Id {
egui::Id::new("ashlar_ui::labels")
}
pub fn install(ctx: &egui::Context, labels: UiLabels) {
ctx.data_mut(|d| d.insert_temp(id(), Arc::new(labels)));
crate::wake::wake(ctx, crate::wake::Reason::SETTINGS);
}
pub fn current(ctx: &egui::Context) -> Arc<UiLabels> {
ctx.data(|d| d.get_temp::<Arc<UiLabels>>(id())).unwrap_or_else(|| english().clone())
}
pub const PLURAL_KEYS: &[&str] = &["ui.toast.more", "ui.keymap.shared"];
#[derive(Clone, Default)]
pub struct Plurals {
i18n: Option<Arc<I18n>>,
}
impl std::fmt::Debug for Plurals {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Plurals").field("locale", &self.i18n.as_ref().map(|i| i.locale().to_string())).finish()
}
}
impl Plurals {
pub fn from_i18n(i18n: &I18n) -> Self {
Plurals { i18n: Some(Arc::new(i18n.clone())) }
}
pub fn text(&self, key: &str, n: usize) -> Option<String> {
let i18n = self.i18n.as_ref()?;
let other = format!("{key}.other");
if i18n.get(key).is_none() && i18n.get(&other).is_none() {
return None;
}
let n = i64::try_from(n).unwrap_or(i64::MAX);
let shown = i18n.integer(n);
Some(i18n.plural(key, n, &[("n", &shown)]))
}
}
fn plurals_id() -> egui::Id {
egui::Id::new("ashlar_ui::labels::plurals")
}
pub fn install_plurals(ctx: &egui::Context, plurals: Plurals) {
ctx.data_mut(|d| d.insert_temp(plurals_id(), plurals));
crate::wake::wake(ctx, crate::wake::Reason::SETTINGS);
}
pub fn install_i18n(ctx: &egui::Context, i18n: &I18n) {
install(ctx, UiLabels::from_i18n(i18n));
install_plurals(ctx, Plurals::from_i18n(i18n));
}
pub fn plurals(ctx: &egui::Context) -> Plurals {
ctx.data(|d| d.get_temp::<Plurals>(plurals_id())).unwrap_or_default()
}
pub fn count_text(ctx: &egui::Context, key: &str, n: usize, template: &str) -> String {
plurals(ctx).text(key, n).unwrap_or_else(|| template.replace("{n}", &n.to_string()))
}
pub fn english() -> &'static Arc<UiLabels> {
static ENGLISH: OnceLock<Arc<UiLabels>> = OnceLock::new();
ENGLISH.get_or_init(|| Arc::new(UiLabels::default()))
}
#[cfg(test)]
mod tests {
use super::*;
fn i18n() -> I18n {
let mut i18n = I18n::new("en");
add_catalogs(&mut i18n);
i18n
}
#[test]
fn catalogs_are_complete_and_match_the_defaults() {
let i18n = i18n();
assert_eq!(i18n.check(), vec![]);
assert_eq!(i18n.missing_source_keys(KEYS), Vec::<&str>::new());
assert_eq!(UiLabels::from_i18n(&i18n), UiLabels::default());
}
#[test]
fn plural_keys_are_in_every_catalog_and_follow_the_locale() {
let mut i18n = i18n();
assert_eq!(i18n.missing_source_keys(PLURAL_KEYS), Vec::<&str>::new());
i18n.add("en", r#"{"ui.toast.more.one": "+{n} more toast", "ui.toast.more.other": "+{n} more toasts"}"#);
let ctx = egui::Context::default();
assert_eq!(count_text(&ctx, "ui.toast.more", 1, "+{n} more"), "+1 more", "nothing installed: the template");
install_i18n(&ctx, &i18n);
assert_eq!(count_text(&ctx, "ui.toast.more", 1, "x"), "+1 more toast");
assert_eq!(count_text(&ctx, "ui.toast.more", 1200, "x"), "+1,200 more toasts");
i18n.set_locale("de");
install_i18n(&ctx, &i18n);
assert_eq!(count_text(&ctx, "ui.keymap.shared", 3, "x"), "3 Befehle teilen sich ein Tastenkürzel");
assert_eq!(plurals(&ctx).text("no.such.key", 2), None);
}
#[test]
fn switching_locale_switches_labels() {
let mut i18n = i18n();
i18n.set_locale("de-DE");
let de = UiLabels::from_i18n(&i18n);
assert_eq!(de.toast.dismiss, "Schließen");
assert_eq!(de.tabs.close, "Tab schließen");
i18n.set_locale("ja");
assert_eq!(UiLabels::from_i18n(&i18n), UiLabels::default());
}
#[test]
fn current_falls_back_to_english() {
let ctx = egui::Context::default();
assert_eq!(current(&ctx).toast.dismiss, "Dismiss");
let mut l = UiLabels::default();
l.toast.dismiss = "Close".into();
install(&ctx, l);
assert_eq!(current(&ctx).toast.dismiss, "Close");
}
}