From 066999586bef4a30f45de0edb872ef0dddd7adf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Fri, 5 Sep 2025 18:50:25 +0200 Subject: [PATCH] feat: add i18n support for libcosmic widgets --- Cargo.toml | 7 ++++ i18n.toml | 4 +++ i18n/en/libcosmic.ftl | 11 +++++++ i18n/sr-Cyrl/libcosmic.ftl | 11 +++++++ i18n/sr-Latn/libcosmic.ftl | 11 +++++++ src/lib.rs | 2 ++ src/localize.rs | 51 +++++++++++++++++++++++++++++ src/widget/about.rs | 18 +++++----- src/widget/context_drawer/widget.rs | 10 +++--- 9 files changed, 110 insertions(+), 15 deletions(-) create mode 100644 i18n.toml create mode 100644 i18n/en/libcosmic.ftl create mode 100644 i18n/sr-Cyrl/libcosmic.ftl create mode 100644 i18n/sr-Latn/libcosmic.ftl create mode 100644 src/localize.rs diff --git a/Cargo.toml b/Cargo.toml index 33bf7c49..076fc00f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,6 +107,13 @@ cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-c chrono = "0.4.41" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } +# Internationalization +i18n-embed = { version = "0.16.0", features = [ + "fluent-system", + "desktop-requester", +] } +i18n-embed-fl = "0.10" +rust-embed = "8.7.2" css-color = "0.2.8" derive_setters = "0.1.8" futures = "0.3" diff --git a/i18n.toml b/i18n.toml new file mode 100644 index 00000000..76f7c310 --- /dev/null +++ b/i18n.toml @@ -0,0 +1,4 @@ +fallback_language = "en" + +[fluent] +assets_dir = "i18n" diff --git a/i18n/en/libcosmic.ftl b/i18n/en/libcosmic.ftl new file mode 100644 index 00000000..45266a9b --- /dev/null +++ b/i18n/en/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = Close + +# About +license = License +links = Links +developers = Developers +designers = Designers +artists = Artists +translators = Translators +documenters = Documenters diff --git a/i18n/sr-Cyrl/libcosmic.ftl b/i18n/sr-Cyrl/libcosmic.ftl new file mode 100644 index 00000000..579392f4 --- /dev/null +++ b/i18n/sr-Cyrl/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = Затвори + +# About +license = Лиценца +links = Линкови +Developers = Програмери +Designers = Дизајнери +Artists = Уметници +Translators = Преводиоци +Documenters = Документатори diff --git a/i18n/sr-Latn/libcosmic.ftl b/i18n/sr-Latn/libcosmic.ftl new file mode 100644 index 00000000..9fbe9a21 --- /dev/null +++ b/i18n/sr-Latn/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = Zatvori + +# About +license = Licenca +links = Linkovi +developers = Programeri +designers = Dizajneri +artists = Umetnici +translators = Prevodioci +documenters = Dokumentatori diff --git a/src/lib.rs b/src/lib.rs index e8aeeedd..a180c224 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -90,6 +90,8 @@ pub use iced_wgpu; pub mod icon_theme; pub mod keyboard_nav; +mod localize; + #[cfg(all(target_env = "gnu", not(target_os = "windows")))] pub(crate) mod malloc; diff --git a/src/localize.rs b/src/localize.rs new file mode 100644 index 00000000..95a31655 --- /dev/null +++ b/src/localize.rs @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use i18n_embed::{ + DefaultLocalizer, LanguageLoader, Localizer, + fluent::{FluentLanguageLoader, fluent_language_loader}, +}; +use rust_embed::RustEmbed; +use std::sync::{LazyLock, OnceLock}; + +#[derive(RustEmbed)] +#[folder = "i18n/"] +struct Localizations; + +pub static LANGUAGE_LOADER: LazyLock = LazyLock::new(|| { + let loader: FluentLanguageLoader = fluent_language_loader!(); + + loader + .load_fallback_language(&Localizations) + .expect("Error while loading fallback language"); + + loader +}); + +static LOCALIZATION_INITIALIZED: OnceLock<()> = OnceLock::new(); + +#[macro_export] +macro_rules! fl { + ($message_id:literal) => {{ + $crate::localize::localize(); + i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id) + }}; + ($message_id:literal, $($args:expr),*) => {{ + $crate::localize::localize(); + i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id, $($args), *) + }}; +} + +// Get the `Localizer` to be used for localizing this library. +pub fn localizer() -> Box { + Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations)) +} + +pub fn localize() { + LOCALIZATION_INITIALIZED.get_or_init(|| { + let localizer = localizer(); + let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages(); + if let Err(error) = localizer.select(&requested_languages) { + eprintln!("Error while loading language for libcosmic {}", error); + } + }); +} diff --git a/src/widget/about.rs b/src/widget/about.rs index aea92991..f1f84106 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -1,6 +1,6 @@ use { crate::{ - Element, + Element, fl, iced::{Alignment, Length}, widget::{self, horizontal_space}, }, @@ -116,7 +116,7 @@ pub fn about<'a, Message: Clone + 'static>( space_xxs, space_m, .. } = crate::theme::spacing(); - let section = |list: &'a Vec<(String, String)>, title: &'a str| { + let section = |list: &'a Vec<(String, String)>, title: String| { (!list.is_empty()).then_some({ let items: Vec> = list.iter() @@ -150,15 +150,15 @@ pub fn about<'a, Message: Clone + 'static>( }); let author = about.author.as_ref().map(widget::text::body); let version = about.version.as_ref().map(widget::button::standard); - let links_section = section(&about.links, "Links"); - let developers_section = section(&about.developers, "Developers"); - let designers_section = section(&about.designers, "Designers"); - let artists_section = section(&about.artists, "Artists"); - let translators_section = section(&about.translators, "Translators"); - let documenters_section = section(&about.documenters, "Documenters"); + let links_section = section(&about.links, fl!("links")); + let developers_section = section(&about.developers, fl!("developers")); + let designers_section = section(&about.designers, fl!("designers")); + let artists_section = section(&about.artists, fl!("artists")); + let translators_section = section(&about.translators, fl!("translators")); + let documenters_section = section(&about.documenters, fl!("documenters")); let license = about.license.as_ref().map(|license| { let url = about.get_license_url(); - widget::settings::section().title("License").add( + widget::settings::section().title(fl!("license")).add( widget::button::custom( widget::row() .push(widget::text(license)) diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index e618fbcf..b46f6017 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -1,12 +1,10 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -use std::borrow::Cow; - -use crate::widget::{LayerContainer, button, column, container, icon, row, scrollable, text}; -use crate::{Apply, Element, Renderer, Theme}; - use super::overlay::Overlay; +use crate::widget::{LayerContainer, button, column, container, icon, row, scrollable, text}; +use crate::{Apply, Element, Renderer, Theme, fl}; +use std::borrow::Cow; use iced_core::Alignment; use iced_core::event::{self, Event}; @@ -86,7 +84,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { ) .push_maybe(title) .push( - button::text("Close") + button::text(fl!("close")) .trailing_icon(icon::from_name("go-next-symbolic")) .on_press(on_close) .apply(container)