From 119364f65240021641ee5cb5b0ea4472f3d53d11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 11 Nov 2024 22:55:20 +0000 Subject: [PATCH 01/17] fix(rust): fix the D-Bus manager's LocaleProxy path --- rust/agama-lib/src/proxies/locale.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rust/agama-lib/src/proxies/locale.rs b/rust/agama-lib/src/proxies/locale.rs index ce49c9c2b3..1ff5221870 100644 --- a/rust/agama-lib/src/proxies/locale.rs +++ b/rust/agama-lib/src/proxies/locale.rs @@ -20,7 +20,8 @@ //! [D-Bus standard interfaces]: https://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces, use zbus::proxy; #[proxy( - default_service = "org.opensuse.Agama1", + default_service = "org.opensuse.Agama.Manager1", + default_path = "/org/opensuse/Agama/Manager1", interface = "org.opensuse.Agama1.Locale", assume_defaults = true )] From 970ce9b8b55886c2a03cb8ce7926af02ca0092c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 12 Nov 2024 11:27:31 +0000 Subject: [PATCH 02/17] refactor(web): convert installerL10n to TypeScript --- web/src/context/{installerL10n.jsx => installerL10n.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename web/src/context/{installerL10n.jsx => installerL10n.tsx} (100%) diff --git a/web/src/context/installerL10n.jsx b/web/src/context/installerL10n.tsx similarity index 100% rename from web/src/context/installerL10n.jsx rename to web/src/context/installerL10n.tsx From 33309f3443247e28aa7c3e6c1c06d0af4be70217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 12 Nov 2024 11:38:24 +0000 Subject: [PATCH 03/17] refactor(web): annotate installerL10n types --- web/src/context/installerL10n.tsx | 67 +++++++++++++++---------------- 1 file changed, 33 insertions(+), 34 deletions(-) diff --git a/web/src/context/installerL10n.tsx b/web/src/context/installerL10n.tsx index ec10ce6947..3b50c09360 100644 --- a/web/src/context/installerL10n.tsx +++ b/web/src/context/installerL10n.tsx @@ -34,16 +34,15 @@ const L10nContext = React.createContext(null); /** * Installer localization context. - * - * @typedef {object} L10nContext - * @property {string|undefined} language - Current language. - * @property {string|undefined} keymap - Current keymap. - * @property {(language: string) => Promise} changeLanguage - Function to change the current language. - * @property {(keymap: string) => Promise} changeKeymap - Function to change the current keymap. - * - * @return {L10nContext} */ -function useInstallerL10n() { +interface L10nContext { + language: string | undefined; + keymap: string | undefined; + changeLanguage: (language: string) => Promise; + changeKeymap: (keymap: string) => Promise; +} + +function useInstallerL10n(): L10nContext { const context = React.useContext(L10nContext); if (!context) { @@ -58,9 +57,9 @@ function useInstallerL10n() { * * It takes the language from the agamaLang cookie. * - * @return {string|undefined} Undefined if language is not set. + * @return Undefined if language is not set. */ -function agamaLanguage() { +function agamaLanguage(): string | undefined { // language from cookie, empty string if not set (regexp taken from Cockpit) // https://github.com/cockpit-project/cockpit/blob/98a2e093c42ea8cd2431cf15c7ca0e44bb4ce3f1/pkg/shell/shell-modals.jsx#L91 const languageString = decodeURIComponent( @@ -76,10 +75,10 @@ function agamaLanguage() { * * Automatically converts the language from xx_XX to xx-xx, as it is the one used by Agama. * - * @param {string} language - The new locale (e.g., "cs", "cs_CZ"). - * @return {boolean} True if the locale was changed. + * @param language - The new locale (e.g., "cs", "cs_CZ"). + * @return True if the locale was changed. */ -function storeAgamaLanguage(language) { +function storeAgamaLanguage(language: string): boolean { const current = agamaLanguage(); if (current === language) return false; @@ -105,9 +104,9 @@ function storeAgamaLanguage(language) { * * Query supports 'xx-xx', 'xx_xx', 'xx-XX' and 'xx_XX' formats. * - * @return {string|undefined} Undefined if not set. + * @return Undefined if not set. */ -function languageFromQuery() { +function languageFromQuery(): string | undefined { const lang = new URLSearchParams(window.location.search).get("lang"); if (!lang) return undefined; @@ -118,14 +117,14 @@ function languageFromQuery() { /** * Generates a RFC 5646 (or BCP 78) language tag from a locale. * - * @param {string} locale - * @return {string} + * @param locale + * @return RFC 5646 language tag (e.g., "en-US") * * @private * @see https://datatracker.ietf.org/doc/html/rfc5646 * @see https://www.rfc-editor.org/info/bcp78 */ -function languageFromLocale(locale) { +function languageFromLocale(locale: string): string { const [language] = locale.split("."); return language.replace("_", "-").toLowerCase(); } @@ -135,8 +134,8 @@ function languageFromLocale(locale) { * * It forces the encoding to "UTF-8". * - * @param {string} language - * @return {string} + * @param language as a TFC 5646 language tag (e.g., "en-US") + * @return locale (e.g., "en_US.UTF-8") * * @private * @see https://datatracker.ietf.org/doc/html/rfc5646 @@ -151,19 +150,19 @@ function languageToLocale(language) { /** * List of RFC 5646 (or BCP 78) language tags from the navigator. * - * @return {Array} + * @return RFC 5646 language tags (e.g., ["en-US", "en"]) */ -function navigatorLanguages() { +function navigatorLanguages(): Array { return navigator.languages.map((l) => l.toLowerCase()); } /** * Returns the first supported language from the given list. * - * @param {Array} languages - * @return {string|undefined} Undefined if none of the given languages is supported. + * @param languages - list of RFC 5646 language tags (e.g., ["en-US", "en"]) to check + * @return Undefined if none of the given languages is supported. */ -function findSupportedLanguage(languages) { +function findSupportedLanguage(languages: Array): string | undefined { const supported = Object.keys(supportedLanguages); for (const candidate of languages) { @@ -187,9 +186,9 @@ function findSupportedLanguage(languages) { * It uses the window.location.replace instead of the reload function synchronizing the "lang" * argument from the URL if present. * - * @param {string} newLanguage + * @param newLanguage - new language to use. */ -function reload(newLanguage) { +function reload(newLanguage: string) { const query = new URLSearchParams(window.location.search); if (query.has("lang") && query.get("lang") !== newLanguage) { query.set("lang", newLanguage); @@ -210,12 +209,12 @@ function reload(newLanguage) { * The format of the language tag in the query parameter follows the * [RFC 5646](https://datatracker.ietf.org/doc/html/rfc5646) specification. * - * @param {object} props - * @param {React.ReactNode} [props.children] - Content to display within the wrapper. + * @param props + * @param [props.children] - Content to display within the wrapper. * * @see useInstallerL10n */ -function InstallerL10nProvider({ children }) { +function InstallerL10nProvider({ children }: { children?: React.ReactNode }) { const { connected } = useInstallerClientStatus(); const [language, setLanguage] = useState(undefined); const [keymap, setKeymap] = useState(undefined); @@ -223,7 +222,7 @@ function InstallerL10nProvider({ children }) { const { cancellablePromise } = useCancellablePromise(); const storeInstallerLanguage = useCallback( - async (newLanguage) => { + async (newLanguage: string) => { if (!connected) { setBackendPending(true); return false; @@ -244,7 +243,7 @@ function InstallerL10nProvider({ children }) { ); const changeLanguage = useCallback( - async (lang) => { + async (lang?: string) => { const wanted = lang || languageFromQuery(); if (wanted === "xx" || wanted === "xx-xx") { @@ -270,7 +269,7 @@ function InstallerL10nProvider({ children }) { ); const changeKeymap = useCallback( - async (id) => { + async (id: string) => { if (!connected) return; setKeymap(id); From 4145587bef4c58bcb24294052f94e3aabc6429e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 12 Nov 2024 12:40:00 +0000 Subject: [PATCH 04/17] fix(web): fix capitalization of RFC 5646 language tags --- web/share/locales.json | 126 +++++++++++++++++----------------- web/share/update-languages.py | 13 ++-- web/src/languages.json | 20 +++--- 3 files changed, 80 insertions(+), 79 deletions(-) diff --git a/web/share/locales.json b/web/share/locales.json index ff8ab3a492..3ce13e0d69 100644 --- a/web/share/locales.json +++ b/web/share/locales.json @@ -1,65 +1,65 @@ { - "af": "za", - "am": "et", - "ar": "eg", - "ast": "es", - "be": "by", - "bg": "bg", - "bn": "in", - "bs": "ba", - "ca": "es", - "cs": "cz", - "cy": "gb", - "da": "dk", - "de": "de", - "en": "us", - "el": "gr", - "es": "es", - "et": "ee", - "eu": "es", - "fa": "ir", - "fi": "fi", - "fr": "fr", - "gl": "es", - "gu": "in", - "he": "il", - "hi": "in", - "hr": "hr", - "hu": "hu", - "id": "id", - "it": "it", - "ja": "jp", - "ka": "kz", - "km": "kh", - "kn": "in", - "ko": "kr", - "lt": "lt", - "lv": "lv", - "mk": "mk", - "mr": "in", - "ms": "my", - "my": "mm", - "nb": "no", - "nds": "de", - "ne": "np", - "nl": "nl", - "nn": "no", - "pa": "in", - "pl": "pl", - "pt": "pt", - "ro": "ro", - "ru": "ru", - "si": "lk", - "sk": "sk", - "sl": "si", - "sq": "al", - "sr": "rs", - "sv": "se", - "ta": "in", - "tg": "tj", - "th": "th", - "tr": "tr", - "uk": "ua", - "vi": "vn", - "zu": "za" + "af": "ZA", + "am": "ET", + "ar": "EG", + "ast": "ES", + "be": "BY", + "bg": "BG", + "bn": "IN", + "bs": "BA", + "ca": "ES", + "cs": "CZ", + "cy": "GB", + "da": "DK", + "de": "DE", + "en": "US", + "el": "GR", + "es": "ES", + "et": "EE", + "eu": "ES", + "fa": "IR", + "fi": "FI", + "fr": "FR", + "gl": "ES", + "gu": "IN", + "he": "IL", + "hi": "IN", + "hr": "HR", + "hu": "HU", + "id": "ID", + "it": "IT", + "ja": "JP", + "ka": "KZ", + "km": "KH", + "kn": "IN", + "ko": "KR", + "lt": "LT", + "lv": "LV", + "mk": "MK", + "mr": "IN", + "ms": "MY", + "my": "MM", + "nb": "NO", + "nds": "DE", + "ne": "NP", + "nl": "NL", + "nn": "NO", + "pa": "IN", + "pl": "PL", + "pt": "PT", + "ro": "RO", + "ru": "RU", + "si": "LK", + "sk": "SK", + "sl": "SI", + "sq": "AL", + "sr": "RS", + "sv": "SE", + "ta": "IN", + "tg": "TJ", + "th": "TH", + "tr": "TR", + "uk": "UA", + "vi": "VN", + "zu": "ZA" } diff --git a/web/share/update-languages.py b/web/share/update-languages.py index 41971b43d3..b7e1c15aa9 100755 --- a/web/share/update-languages.py +++ b/web/share/update-languages.py @@ -24,6 +24,7 @@ # from argparse import ArgumentParser +from typing import Optional from langtable import language_name from pathlib import Path import json @@ -32,9 +33,9 @@ class Locale: language: str - territory: str + territory: Optional[str] - def __init__(self, language: str, territory: str = None): + def __init__(self, language: str, territory: Optional[str] = None): self.language = language self.territory = territory @@ -44,13 +45,13 @@ def code(self): def name(self, include_territory: bool = False): if include_territory: return language_name(languageId=self.language, - territoryId=self.territory.upper()) + territoryId=self.territory) else: return language_name(languageId=self.language) class PoFile: - path: str + path: Path locale: Locale def __init__(self, path: Path): @@ -85,7 +86,7 @@ class Languages: def __init__(self): self.content = dict() - def update(self, po_files, lang2territory: str, threshold: int): + def update(self, po_files, lang2territory, threshold: int): """ Generate the list of supported locales @@ -97,7 +98,7 @@ def update(self, po_files, lang2territory: str, threshold: int): :param threshold Percentage of the strings that must be covered to include the locale in the manifest """ - supported = [Locale("en", "us")] + supported = [Locale("en", "US")] for path in po_files: po_file = PoFile(path) diff --git a/web/src/languages.json b/web/src/languages.json index ed1dec31eb..11b8afb8a3 100644 --- a/web/src/languages.json +++ b/web/src/languages.json @@ -1,15 +1,15 @@ { - "ca-es": "Català", - "cs-cz": "Čeština", - "de-de": "Deutsch", - "en-us": "English", - "es-es": "Español", - "fr-fr": "Français", - "ja-jp": "日本語", + "ca-ES": "Català", + "cs-CZ": "Čeština", + "de-DE": "Deutsch", + "en-US": "English", + "es-ES": "Español", + "fr-FR": "Français", + "ja-JP": "日本語", "nb-NO": "Norsk bokmål", "pt-BR": "Português", - "ru-ru": "Русский", - "sv-se": "Svenska", - "tr-tr": "Türkçe", + "ru-RU": "Русский", + "sv-SE": "Svenska", + "tr-TR": "Türkçe", "zh-Hans": "中文" } \ No newline at end of file From fc46977a3fec43a0bf2e55a934fd75a9e4abd4a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 12 Nov 2024 16:12:24 +0000 Subject: [PATCH 05/17] fix(web): fix installer language handling --- web/src/context/installerL10n.tsx | 81 ++++++++++--------------------- 1 file changed, 25 insertions(+), 56 deletions(-) diff --git a/web/src/context/installerL10n.tsx b/web/src/context/installerL10n.tsx index 3b50c09360..f6ab7934bd 100644 --- a/web/src/context/installerL10n.tsx +++ b/web/src/context/installerL10n.tsx @@ -62,12 +62,9 @@ function useInstallerL10n(): L10nContext { function agamaLanguage(): string | undefined { // language from cookie, empty string if not set (regexp taken from Cockpit) // https://github.com/cockpit-project/cockpit/blob/98a2e093c42ea8cd2431cf15c7ca0e44bb4ce3f1/pkg/shell/shell-modals.jsx#L91 - const languageString = decodeURIComponent( + return decodeURIComponent( document.cookie.replace(/(?:(?:^|.*;\s*)agamaLang\s*=\s*([^;]*).*$)|^.*$/, "$1"), ); - if (languageString) { - return languageString.toLowerCase(); - } } /** @@ -87,15 +84,6 @@ function storeAgamaLanguage(language: string): boolean { "agamaLang=" + encodeURIComponent(language) + "; path=/; expires=Sun, 16 Jul 3567 06:23:41 GMT"; document.cookie = cookie; - // for backward compatibility, CockpitLang cookie is needed to load correct po.js content from Cockpit - // TODO: remove after dropping Cockpit completely - const cockpit_cookie = - "CockpitLang=" + - encodeURIComponent(language) + - "; path=/; expires=Sun, 16 Jul 3567 06:23:41 GMT"; - document.cookie = cockpit_cookie; - window.localStorage.setItem("cockpit.lang", language); - return true; } @@ -110,8 +98,8 @@ function languageFromQuery(): string | undefined { const lang = new URLSearchParams(window.location.search).get("lang"); if (!lang) return undefined; - const [language, country] = lang.toLowerCase().split(/[-_]/); - return country ? `${language}-${country}` : language; + const [language, country] = lang.split(/[-_]/); + return country ? `${language.toLowerCase()}-${country.toUpperCase()}` : language; } /** @@ -126,7 +114,7 @@ function languageFromQuery(): string | undefined { */ function languageFromLocale(locale: string): string { const [language] = locale.split("."); - return language.replace("_", "-").toLowerCase(); + return language.replace("_", "-"); } /** @@ -141,21 +129,12 @@ function languageFromLocale(locale: string): string { * @see https://datatracker.ietf.org/doc/html/rfc5646 * @see https://www.rfc-editor.org/info/bcp78 */ -function languageToLocale(language) { +function languageToLocale(language: string): string { const [lang, country] = language.split("-"); const locale = country ? `${lang}_${country.toUpperCase()}` : lang; return `${locale}.UTF-8`; } -/** - * List of RFC 5646 (or BCP 78) language tags from the navigator. - * - * @return RFC 5646 language tags (e.g., ["en-US", "en"]) - */ -function navigatorLanguages(): Array { - return navigator.languages.map((l) => l.toLowerCase()); -} - /** * Returns the first supported language from the given list. * @@ -218,46 +197,36 @@ function InstallerL10nProvider({ children }: { children?: React.ReactNode }) { const { connected } = useInstallerClientStatus(); const [language, setLanguage] = useState(undefined); const [keymap, setKeymap] = useState(undefined); - const [backendPending, setBackendPending] = useState(false); const { cancellablePromise } = useCancellablePromise(); - const storeInstallerLanguage = useCallback( - async (newLanguage: string) => { - if (!connected) { - setBackendPending(true); - return false; - } + const syncBackendLanguage = useCallback(async () => { + const config = await cancellablePromise(fetchConfig()); + const backendLanguage = languageFromLocale(config.uiLocale); - const config = await cancellablePromise(fetchConfig()); - const currentLanguage = languageFromLocale(config.uiLocale); + if (backendLanguage !== language) { + // FIXME: fallback to en-US if the language is not supported. + await cancellablePromise(updateConfig({ uiLocale: languageToLocale(language) })); + return true; + } - if (currentLanguage !== newLanguage) { - // FIXME: fallback to en-US if the language is not supported. - await cancellablePromise(updateConfig({ uiLocale: languageToLocale(newLanguage) })); - return true; - } - - return false; - }, - [connected, cancellablePromise], - ); + return false; + }, [language, cancellablePromise]); const changeLanguage = useCallback( async (lang?: string) => { const wanted = lang || languageFromQuery(); - if (wanted === "xx" || wanted === "xx-xx") { + // Just for development purposes (do not commit the language change to the backend) + if (wanted === "xx" || wanted === "xx-XX") { agama.language = wanted; setLanguage(wanted); return; } const current = agamaLanguage(); - const candidateLanguages = [wanted, current].concat(navigatorLanguages()).filter((l) => l); - const newLanguage = findSupportedLanguage(candidateLanguages) || "en-us"; - - let mustReload = storeAgamaLanguage(newLanguage); - mustReload = (await storeInstallerLanguage(newLanguage)) || mustReload; + const candidateLanguages = [wanted, current].concat(navigator.languages).filter((l) => l); + const newLanguage = findSupportedLanguage(candidateLanguages) || "en-US"; + const mustReload = storeAgamaLanguage(newLanguage); if (mustReload) { reload(newLanguage); @@ -265,7 +234,7 @@ function InstallerL10nProvider({ children }: { children?: React.ReactNode }) { setLanguage(newLanguage); } }, - [storeInstallerLanguage, setLanguage], + [setLanguage], ); const changeKeymap = useCallback( @@ -283,14 +252,14 @@ function InstallerL10nProvider({ children }: { children?: React.ReactNode }) { }, [changeLanguage, language]); useEffect(() => { - if (!connected || !backendPending) return; + if (!connected || !language) return; - storeInstallerLanguage(language); - setBackendPending(false); - }, [connected, language, backendPending, storeInstallerLanguage]); + syncBackendLanguage(); + }, [connected, language, syncBackendLanguage]); useEffect(() => { if (!connected) return; + fetchConfig().then((c) => setKeymap(c.uiKeymap)); }, [setKeymap, connected]); From d72bcbb8bc6cbdaa9bba6b8e0a9f1a79a9702631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 12 Nov 2024 16:37:36 +0000 Subject: [PATCH 06/17] fix(web): better fallback value for unsupported languages --- web/src/context/installerL10n.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/web/src/context/installerL10n.tsx b/web/src/context/installerL10n.tsx index f6ab7934bd..3c084d1a22 100644 --- a/web/src/context/installerL10n.tsx +++ b/web/src/context/installerL10n.tsx @@ -216,15 +216,19 @@ function InstallerL10nProvider({ children }: { children?: React.ReactNode }) { async (lang?: string) => { const wanted = lang || languageFromQuery(); - // Just for development purposes (do not commit the language change to the backend) + // Just for development purposes if (wanted === "xx" || wanted === "xx-XX") { agama.language = wanted; setLanguage(wanted); return; } - const current = agamaLanguage(); - const candidateLanguages = [wanted, current].concat(navigator.languages).filter((l) => l); + const candidateLanguages = [ + wanted, + wanted?.split("-")[0], // fallback to the language (e.g., "es" for "es-AR") + agamaLanguage(), + ...navigator.languages, + ].filter((l) => l); const newLanguage = findSupportedLanguage(candidateLanguages) || "en-US"; const mustReload = storeAgamaLanguage(newLanguage); From 9d57f26af024fbd17f463490d3126f167f5a3148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Tue, 12 Nov 2024 16:47:07 +0000 Subject: [PATCH 07/17] fix(web): refresh products on language change --- web/src/queries/software.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/src/queries/software.ts b/web/src/queries/software.ts index 9ad08b7ec3..6030489bd7 100644 --- a/web/src/queries/software.ts +++ b/web/src/queries/software.ts @@ -188,6 +188,10 @@ const useProductChanges = () => { if (event.type === "ProductChanged") { queryClient.invalidateQueries({ queryKey: ["software/config"] }); } + + if (event.type === "LocaleChanged") { + queryClient.invalidateQueries({ queryKey: ["software/products"] }); + } }); }, [client, queryClient]); }; From 92d21fe3cd33e2bf3ed3b4b9690dcb308df5dce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 13 Nov 2024 08:45:37 +0000 Subject: [PATCH 08/17] fix(web): set UI language after the keymap --- web/src/components/core/InstallerOptions.tsx | 2 +- web/src/context/installerL10n.tsx | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/web/src/components/core/InstallerOptions.tsx b/web/src/components/core/InstallerOptions.tsx index cb109ee4fc..0e9e0bf966 100644 --- a/web/src/components/core/InstallerOptions.tsx +++ b/web/src/components/core/InstallerOptions.tsx @@ -63,7 +63,7 @@ export default function InstallerOptions({ isOpen = false, onClose }: InstallerO const onSubmit = async (e: React.FormEvent) => { e.preventDefault(); setInProgress(true); - changeKeymap(keymap); + await changeKeymap(keymap); changeLanguage(language) .then(close) .catch(() => setInProgress(false)); diff --git a/web/src/context/installerL10n.tsx b/web/src/context/installerL10n.tsx index 3c084d1a22..b7fb3cda91 100644 --- a/web/src/context/installerL10n.tsx +++ b/web/src/context/installerL10n.tsx @@ -202,14 +202,10 @@ function InstallerL10nProvider({ children }: { children?: React.ReactNode }) { const syncBackendLanguage = useCallback(async () => { const config = await cancellablePromise(fetchConfig()); const backendLanguage = languageFromLocale(config.uiLocale); + if (backendLanguage === language) return; - if (backendLanguage !== language) { - // FIXME: fallback to en-US if the language is not supported. - await cancellablePromise(updateConfig({ uiLocale: languageToLocale(language) })); - return true; - } - - return false; + // FIXME: fallback to en-US if the language is not supported. + await cancellablePromise(updateConfig({ uiLocale: languageToLocale(language) })); }, [language, cancellablePromise]); const changeLanguage = useCallback( @@ -246,7 +242,7 @@ function InstallerL10nProvider({ children }: { children?: React.ReactNode }) { if (!connected) return; setKeymap(id); - updateConfig({ uiKeymap: id }); + await updateConfig({ uiKeymap: id }); }, [setKeymap, connected], ); From 8edf7bbc8544b1ce8011897d8894858ea01a9596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 13 Nov 2024 09:52:41 +0000 Subject: [PATCH 09/17] fix(web): move installerL10n tests to TypeScript --- .../{installerL10n.test.jsx => installerL10n.test.tsx} | 8 +++++--- web/src/context/installerL10n.tsx | 1 - 2 files changed, 5 insertions(+), 4 deletions(-) rename web/src/context/{installerL10n.test.jsx => installerL10n.test.tsx} (97%) diff --git a/web/src/context/installerL10n.test.jsx b/web/src/context/installerL10n.test.tsx similarity index 97% rename from web/src/context/installerL10n.test.jsx rename to web/src/context/installerL10n.test.tsx index 3c18780203..c73092823f 100644 --- a/web/src/context/installerL10n.test.jsx +++ b/web/src/context/installerL10n.test.tsx @@ -45,8 +45,11 @@ jest.mock("~/api/l10n", () => ({ })); const client = { + isConnected: jest.fn().mockResolvedValue(true), + isRecoverable: jest.fn(), onConnect: jest.fn(), onDisconnect: jest.fn(), + onEvent: jest.fn(), }; jest.mock("~/languages.json", () => ({ @@ -80,8 +83,7 @@ describe("InstallerL10nProvider", () => { jest.spyOn(utils, "setLocationSearch"); mockUpdateConfigFn.mockResolvedValue(true); - delete window.navigator; - window.navigator = { languages: ["es-es", "cs-cz"] }; + jest.spyOn(window.navigator, "languages", "get").mockReturnValue(["es-ES", "cs-CZ"]); }); // remove the language cookie after each test @@ -181,7 +183,7 @@ describe("InstallerL10nProvider", () => { describe("when the browser language does not contain the full locale", () => { beforeEach(() => { - window.navigator = { languages: ["es", "cs-cz"] }; + jest.spyOn(window.navigator, "languages", "get").mockReturnValue(["es", "cs-CZ"]); }); it("sets the first which language matches", async () => { diff --git a/web/src/context/installerL10n.tsx b/web/src/context/installerL10n.tsx index b7fb3cda91..d0bdb300b9 100644 --- a/web/src/context/installerL10n.tsx +++ b/web/src/context/installerL10n.tsx @@ -21,7 +21,6 @@ */ // cspell:ignore localectl setxkbmap xorg -// @ts-check import React, { useCallback, useEffect, useState } from "react"; import { useCancellablePromise, locationReload, setLocationSearch } from "~/utils"; From 9c7b1254dd98bbfd05c253bab885ad0270dbad14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 13 Nov 2024 09:53:30 +0000 Subject: [PATCH 10/17] fix(web): adapt installerL10n tests --- web/src/context/installerL10n.test.tsx | 51 +++++++++++--------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/web/src/context/installerL10n.test.tsx b/web/src/context/installerL10n.test.tsx index c73092823f..e97d324920 100644 --- a/web/src/context/installerL10n.test.tsx +++ b/web/src/context/installerL10n.test.tsx @@ -53,25 +53,25 @@ const client = { }; jest.mock("~/languages.json", () => ({ - "es-ar": "Español (Argentina)", - "cs-cz": "čeština", - "en-us": "English (US)", - "es-es": "Español", + "es-AR": "Español (Argentina)", + "cs-CZ": "čeština", + "en-US": "English (US)", + "es-ES": "Español", })); // Helper component that displays a translated message depending on the // agamaLang value. const TranslatedContent = () => { const text = { - "cs-cz": "ahoj", - "en-us": "hello", - "es-es": "hola", - "es-ar": "hola!", + "cs-CZ": "ahoj", + "en-US": "hello", + "es-ES": "hola", + "es-AR": "hola!", }; const regexp = /agamaLang=([^;]+)/; const found = document.cookie.match(regexp); - if (!found) return <>{text["en-us"]}; + if (!found) return <>{text["en-US"]}; const [, lang] = found; return <>{text[lang]}; @@ -99,7 +99,7 @@ describe("InstallerL10nProvider", () => { describe("when the language is already set", () => { beforeEach(() => { - document.cookie = "agamaLang=en-us; path=/;"; + document.cookie = "agamaLang=en-US; path=/;"; mockFetchConfigFn.mockResolvedValue({ uiLocale: "en_US.UTF-8" }); }); @@ -121,9 +121,8 @@ describe("InstallerL10nProvider", () => { describe("when the language is set to an unsupported language", () => { beforeEach(() => { - document.cookie = "agamaLang=de-de; path=/;"; - mockFetchConfigFn.mockResolvedValueOnce({ uiLocale: "de_DE.UTF-8" }); - mockFetchConfigFn.mockResolvedValue({ uiLocale: "es_ES.UTF-8" }); + document.cookie = "agamaLang=de-DE; path=/;"; + mockFetchConfigFn.mockResolvedValue({ uiLocale: "de_DE.UTF-8" }); }); it("uses the first supported language from the browser", async () => { @@ -216,9 +215,9 @@ describe("InstallerL10nProvider", () => { history.replaceState(history.state, null, `http://localhost/?lang=cs-CZ`); }); - describe("when the language is already set to 'cs-cz'", () => { + describe("when the language is already set to 'cs-CZ'", () => { beforeEach(() => { - document.cookie = "agamaLang=cs-cz; path=/;"; + document.cookie = "agamaLang=cs-CZ; path=/;"; mockFetchConfigFn.mockResolvedValue({ uiLocale: "cs_CZ.UTF-8" }); }); @@ -235,21 +234,19 @@ describe("InstallerL10nProvider", () => { await screen.findByText("ahoj"); expect(mockUpdateConfigFn).not.toHaveBeenCalled(); - expect(document.cookie).toMatch(/agamaLang=cs-cz/); + expect(document.cookie).toMatch(/agamaLang=cs-CZ/); expect(utils.locationReload).not.toHaveBeenCalled(); expect(utils.setLocationSearch).not.toHaveBeenCalled(); }); }); - describe("when the language is set to 'en-us'", () => { + describe("when the language is set to 'en-US'", () => { beforeEach(() => { - document.cookie = "agamaLang=en-us; path=/;"; - mockFetchConfigFn.mockResolvedValueOnce({ uiLocale: "en_US" }); - mockFetchConfigFn.mockResolvedValueOnce({ uiLocale: "cs_CZ" }); - mockUpdateConfigFn.mockResolvedValue(); + document.cookie = "agamaLang=en-US; path=/;"; + mockFetchConfigFn.mockResolvedValue({ uiLocale: "en_US" }); }); - it("sets the 'cs-cz' language and reloads", async () => { + it("sets the 'cs-CZ' language and reloads", async () => { render( @@ -258,8 +255,6 @@ describe("InstallerL10nProvider", () => { , ); - await waitFor(() => expect(utils.setLocationSearch).toHaveBeenCalledWith("lang=cs-cz")); - // renders again after reloading render( @@ -276,12 +271,10 @@ describe("InstallerL10nProvider", () => { describe("when the language is not set", () => { beforeEach(() => { - mockFetchConfigFn.mockResolvedValueOnce({ uiLocale: "en_US.UTF-8" }); - mockFetchConfigFn.mockResolvedValue({ uiLocale: "cs_CZ.UTF-8" }); - mockUpdateConfigFn.mockResolvedValue(); + mockFetchConfigFn.mockResolvedValue({ uiLocale: "en_US.UTF-8" }); }); - it("sets the 'cs-cz' language and reloads", async () => { + it("sets the 'cs-CZ' language and reloads", async () => { render( @@ -290,8 +283,6 @@ describe("InstallerL10nProvider", () => { , ); - await waitFor(() => expect(utils.setLocationSearch).toHaveBeenCalledWith("lang=cs-cz")); - // reload the component render( From c3f64258f2beed5b05fedbbd6a23b2efca5ff003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 13 Nov 2024 10:17:41 +0000 Subject: [PATCH 11/17] fix(web): adapt App tests to language fixes --- web/src/App.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx index 3c4aa32bbb..e0fc16b0b8 100644 --- a/web/src/App.test.tsx +++ b/web/src/App.test.tsx @@ -34,7 +34,7 @@ jest.mock("~/api/l10n", () => ({ ...jest.requireActual("~/api/l10n"), fetchConfig: jest.fn().mockResolvedValue({ uiKeymap: "en", - uiLocale: "en_us", + uiLocale: "en_US", }), updateConfig: jest.fn(), })); @@ -98,7 +98,7 @@ jest.mock("~/components/product/ProductSelectionProgress", () => () =>
Prod describe("App", () => { beforeEach(() => { // setting the language through a cookie - document.cookie = "agamaLang=en-us; path=/;"; + document.cookie = "agamaLang=en-US; path=/;"; (createClient as jest.Mock).mockImplementation(() => { return {}; }); From a6e0cc568e8277e6f692548c0a80bea9b074a3ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 13 Nov 2024 11:08:53 +0000 Subject: [PATCH 12/17] fix(service): do not crash when checking storage issues before probing --- service/lib/agama/storage/manager.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/service/lib/agama/storage/manager.rb b/service/lib/agama/storage/manager.rb index 2cd5546abf..d7ea94436c 100644 --- a/service/lib/agama/storage/manager.rb +++ b/service/lib/agama/storage/manager.rb @@ -262,6 +262,7 @@ def system_issues # @return [Array] def probing_issues y2storage_issues = Y2Storage::StorageManager.instance.raw_probed.probing_issues + return [] if y2storage_issues.nil? y2storage_issues.map do |y2storage_issue| Issue.new(y2storage_issue.message, From b8acc1d35d4b12544e807473a9caa3fbdd3639cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 13 Nov 2024 11:29:12 +0000 Subject: [PATCH 13/17] fix(rust): fix the Locale proxy default path --- rust/agama-lib/src/localization/proxies.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/rust/agama-lib/src/localization/proxies.rs b/rust/agama-lib/src/localization/proxies.rs index 0f37e70a76..401e95470d 100644 --- a/rust/agama-lib/src/localization/proxies.rs +++ b/rust/agama-lib/src/localization/proxies.rs @@ -42,6 +42,7 @@ use zbus::proxy; #[proxy( default_service = "org.opensuse.Agama1", + default_path = "/org/opensuse/Agama1/Locale", interface = "org.opensuse.Agama1.Locale", assume_defaults = true )] From 01153b50ba04bb5f8a0e04692a213c7da50d7605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 13 Nov 2024 12:45:46 +0000 Subject: [PATCH 14/17] docs: update changes files --- rust/package/agama.changes | 6 ++++++ service/package/rubygem-agama-yast.changes | 6 ++++++ web/package/agama-web-ui.changes | 11 +++++++++++ 3 files changed, 23 insertions(+) diff --git a/rust/package/agama.changes b/rust/package/agama.changes index 9c13557a3a..98058d31a7 100644 --- a/rust/package/agama.changes +++ b/rust/package/agama.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Nov 13 12:03:02 UTC 2024 - Imobach Gonzalez Sosa + +- Properly update the localization settings of the D-Bus services + (bsc#1233159, bsc#1233160). + ------------------------------------------------------------------- Thu Nov 7 14:20:48 UTC 2024 - Imobach Gonzalez Sosa diff --git a/service/package/rubygem-agama-yast.changes b/service/package/rubygem-agama-yast.changes index 0579572f9a..5a0eb97197 100644 --- a/service/package/rubygem-agama-yast.changes +++ b/service/package/rubygem-agama-yast.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Wed Nov 13 12:14:06 UTC 2024 - Imobach Gonzalez Sosa + +- Do not crash when trying to change the language of the storage + service before the "config" phase (gh#agama-project/agama#1746). + ------------------------------------------------------------------- Tue Nov 5 16:11:35 UTC 2024 - Martin Vidner diff --git a/web/package/agama-web-ui.changes b/web/package/agama-web-ui.changes index 8f1188efa8..69f7354082 100644 --- a/web/package/agama-web-ui.changes +++ b/web/package/agama-web-ui.changes @@ -1,3 +1,14 @@ +------------------------------------------------------------------- +Wed Nov 13 12:06:41 UTC 2024 - Imobach Gonzalez Sosa + +- Several translation fixes (gh#agama-project/agama#1746): + - Use the correct capitalization for RFC 5646 language tags + (e.g., "pt-BR" instead of "pt-BR" instead of "pt-br") (bsc#1233160). + - Translate the products descriptions when the user changes + the language (gh#agama-project/agama#1724). + - Fallback to a similar language if the given one is not supported + (e.g., "es" for "es-AR") (gh#agama-project/agama#860). + ------------------------------------------------------------------- Wed Nov 6 06:06:51 UTC 2024 - Michal Filka From c1f640e7f0200265ae900c353a81069051612b27 Mon Sep 17 00:00:00 2001 From: Martin Vidner Date: Wed, 13 Nov 2024 14:58:42 +0100 Subject: [PATCH 15/17] Rename a duplicate interface to org.opensuse.Agama1.LocaleMixin it has only a SetLocale method, used as a mixin, to localize the output of various components, but it collided with org.opensuse.Agama1.Locale which is the main locale config for the system --- rust/agama-lib/src/proxies.rs | 2 +- rust/agama-lib/src/proxies/locale.rs | 6 +++--- rust/agama-server/src/l10n/web.rs | 2 +- service/lib/agama/dbus/interfaces/locale.rb | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/rust/agama-lib/src/proxies.rs b/rust/agama-lib/src/proxies.rs index e478d05cb3..8b7452ca1e 100644 --- a/rust/agama-lib/src/proxies.rs +++ b/rust/agama-lib/src/proxies.rs @@ -33,6 +33,6 @@ mod issues; pub use issues::IssuesProxy; mod locale; -pub use locale::LocaleProxy; +pub use locale::LocaleMixinProxy; pub mod jobs; diff --git a/rust/agama-lib/src/proxies/locale.rs b/rust/agama-lib/src/proxies/locale.rs index 1ff5221870..ea7d57f94a 100644 --- a/rust/agama-lib/src/proxies/locale.rs +++ b/rust/agama-lib/src/proxies/locale.rs @@ -1,4 +1,4 @@ -//! # D-Bus interface proxy for: `org.opensuse.Agama1.Locale` +//! # D-Bus interface proxy for: `org.opensuse.Agama1.LocaleMixin` //! //! This code was generated by `zbus-xmlgen` `5.0.0` from D-Bus introspection data. //! Source: `org.opensuse.Agama1.Manager.bus.xml`. @@ -22,10 +22,10 @@ use zbus::proxy; #[proxy( default_service = "org.opensuse.Agama.Manager1", default_path = "/org/opensuse/Agama/Manager1", - interface = "org.opensuse.Agama1.Locale", + interface = "org.opensuse.Agama1.LocaleMixin", assume_defaults = true )] -pub trait Locale { +pub trait LocaleMixin { /// SetLocale method fn set_locale(&self, locale: &str) -> zbus::Result<()>; } diff --git a/rust/agama-server/src/l10n/web.rs b/rust/agama-server/src/l10n/web.rs index 193ef0de6d..25d87df664 100644 --- a/rust/agama-server/src/l10n/web.rs +++ b/rust/agama-server/src/l10n/web.rs @@ -29,7 +29,7 @@ use crate::{ }; use agama_lib::{ error::ServiceError, localization::model::LocaleConfig, localization::LocaleProxy, - proxies::LocaleProxy as ManagerLocaleProxy, + proxies::LocaleMixinProxy as ManagerLocaleProxy, }; use agama_locale_data::LocaleId; use axum::{ diff --git a/service/lib/agama/dbus/interfaces/locale.rb b/service/lib/agama/dbus/interfaces/locale.rb index 506cbc4c8c..6a9b358ee6 100644 --- a/service/lib/agama/dbus/interfaces/locale.rb +++ b/service/lib/agama/dbus/interfaces/locale.rb @@ -35,7 +35,7 @@ module Locale include Yast::I18n include Yast::Logger - LOCALE_INTERFACE = "org.opensuse.Agama1.Locale" + LOCALE_INTERFACE = "org.opensuse.Agama1.LocaleMixin" def self.included(base) base.class_eval do From 688397aa536b26baa6f6c11ec9ad705f5386c7b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 13 Nov 2024 20:50:37 +0000 Subject: [PATCH 16/17] refactor(web): do not use cancellablePromise in installerL10n context --- web/src/context/installerL10n.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/web/src/context/installerL10n.tsx b/web/src/context/installerL10n.tsx index d0bdb300b9..ebfe868b21 100644 --- a/web/src/context/installerL10n.tsx +++ b/web/src/context/installerL10n.tsx @@ -23,7 +23,7 @@ // cspell:ignore localectl setxkbmap xorg import React, { useCallback, useEffect, useState } from "react"; -import { useCancellablePromise, locationReload, setLocationSearch } from "~/utils"; +import { locationReload, setLocationSearch } from "~/utils"; import { useInstallerClientStatus } from "./installer"; import agama from "~/agama"; import supportedLanguages from "~/languages.json"; @@ -196,16 +196,15 @@ function InstallerL10nProvider({ children }: { children?: React.ReactNode }) { const { connected } = useInstallerClientStatus(); const [language, setLanguage] = useState(undefined); const [keymap, setKeymap] = useState(undefined); - const { cancellablePromise } = useCancellablePromise(); const syncBackendLanguage = useCallback(async () => { - const config = await cancellablePromise(fetchConfig()); + const config = await fetchConfig(); const backendLanguage = languageFromLocale(config.uiLocale); if (backendLanguage === language) return; // FIXME: fallback to en-US if the language is not supported. - await cancellablePromise(updateConfig({ uiLocale: languageToLocale(language) })); - }, [language, cancellablePromise]); + await updateConfig({ uiLocale: languageToLocale(language) }); + }, [language]); const changeLanguage = useCallback( async (lang?: string) => { From e4631f9c3998ccce121f0cfab6c9b4d254d05cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Wed, 13 Nov 2024 20:50:55 +0000 Subject: [PATCH 17/17] docs(web): fix a typo --- web/src/context/installerL10n.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/context/installerL10n.tsx b/web/src/context/installerL10n.tsx index ebfe868b21..3e3dfed660 100644 --- a/web/src/context/installerL10n.tsx +++ b/web/src/context/installerL10n.tsx @@ -121,7 +121,7 @@ function languageFromLocale(locale: string): string { * * It forces the encoding to "UTF-8". * - * @param language as a TFC 5646 language tag (e.g., "en-US") + * @param language as a RFC 5646 language tag (e.g., "en-US") * @return locale (e.g., "en_US.UTF-8") * * @private