From d04117abdc10be9b217b4e50ff0af29c86821c69 Mon Sep 17 00:00:00 2001 From: Chili Palmer Date: Fri, 24 Apr 2026 06:46:44 +0200 Subject: [PATCH] feat: more complete i18n infrastructure --- lib/bds/i18n.ex | 158 ++++++++++++++++++++++++ lib/bds/rendering.ex | 2 +- lib/bds/rendering/filters.ex | 2 +- lib/bds/rendering/i18n.ex | 224 +---------------------------------- priv/i18n/locales/de.json | 48 ++++++++ priv/i18n/locales/en.json | 48 ++++++++ priv/i18n/locales/es.json | 48 ++++++++ priv/i18n/locales/fr.json | 48 ++++++++ priv/i18n/locales/it.json | 48 ++++++++ test/bds/i18n_test.exs | 49 ++++++++ 10 files changed, 453 insertions(+), 222 deletions(-) create mode 100644 lib/bds/i18n.ex create mode 100644 priv/i18n/locales/de.json create mode 100644 priv/i18n/locales/en.json create mode 100644 priv/i18n/locales/es.json create mode 100644 priv/i18n/locales/fr.json create mode 100644 priv/i18n/locales/it.json create mode 100644 test/bds/i18n_test.exs diff --git a/lib/bds/i18n.ex b/lib/bds/i18n.ex new file mode 100644 index 0000000..f341855 --- /dev/null +++ b/lib/bds/i18n.ex @@ -0,0 +1,158 @@ +defmodule BDS.I18n do + @moduledoc false + + @supported_languages [ + %{code: "en", flag: "GB"}, + %{code: "de", flag: "DE"}, + %{code: "fr", flag: "FR"}, + %{code: "it", flag: "IT"}, + %{code: "es", flag: "ES"} + ] + @supported_language_codes Enum.map(@supported_languages, & &1.code) + + @format_locales %{ + "en" => "en-US", + "de" => "de-DE", + "fr" => "fr-FR", + "it" => "it-IT", + "es" => "es-ES" + } + + @flag_emoji %{ + "GB" => "🇬🇧", + "DE" => "🇩🇪", + "FR" => "🇫🇷", + "IT" => "🇮🇹", + "ES" => "🇪🇸" + } + + @default_language "en" + @default_format_locale "en-US" + @locale_files Path.expand("../../priv/i18n/locales/*.json", __DIR__) + |> Path.wildcard() + |> Enum.sort() + + for file <- @locale_files do + @external_resource file + end + + @catalogs Enum.into(@locale_files, %{}, fn file -> + locale = file |> Path.basename(".json") |> String.downcase() + {locale, Jason.decode!(File.read!(file))} + end) + + def supported_languages, do: @supported_languages + + def default_language, do: @default_language + + def normalize_language(language) do + language + |> normalize_locale_prefix() + |> case do + value when value in @supported_language_codes -> value + _other -> @default_language + end + end + + def resolve_ui_locale(locale), do: resolve_supported_locale(locale) || @default_language + + def current_ui_locale do + (System.get_env("LC_ALL") || System.get_env("LC_MESSAGES") || System.get_env("LANG")) + |> resolve_ui_locale() + end + + def resolve_render_locale(language), do: resolve_supported_locale(language) || @default_language + + def format_locale(language) do + language + |> resolve_supported_locale() + |> case do + nil -> @default_format_locale + locale -> Map.get(@format_locales, locale, @default_format_locale) + end + end + + def locale_mapping(language) do + ui_locale = resolve_ui_locale(language) + + %{ + ui_locale: ui_locale, + format_locale: format_locale(ui_locale) + } + end + + def get_render_translations(language) do + language + |> resolve_render_locale() + |> catalog_for_locale() + end + + def get_ui_translations(locale) do + locale + |> resolve_ui_locale() + |> catalog_for_locale() + end + + def translate(language, key) do + key = key |> to_string() |> String.trim() + + case resolve_supported_locale(language) do + nil -> + Map.get(catalog_for_locale(@default_language), key, key) + + locale -> + Map.get(catalog_for_locale(locale), key, key) + end + end + + def translate_render(language, key), do: translate(language, key) + def translate_ui(locale, key), do: translate(locale, key) + + def flag(language) do + language + |> resolve_supported_locale() + |> case do + nil -> @default_language + locale -> locale + end + |> flag_code() + |> then(&Map.get(@flag_emoji, &1, &1)) + end + + def flag_code(language) do + language + |> resolve_supported_locale() + |> case do + nil -> @default_language + locale -> locale + end + |> then(fn locale -> + @supported_languages + |> Enum.find(hd(@supported_languages), &(&1.code == locale)) + |> Map.fetch!(:flag) + end) + end + + defp resolve_supported_locale(language) do + language + |> normalize_locale_prefix() + |> case do + value when value in @supported_language_codes -> value + _other -> nil + end + end + + defp normalize_locale_prefix(language) do + language + |> to_string() + |> String.trim() + |> String.replace("_", "-") + |> String.downcase() + |> String.split("-", parts: 2) + |> List.first() + end + + defp catalog_for_locale(locale) do + Map.get(@catalogs, locale, Map.get(@catalogs, @default_language, %{})) + end +end diff --git a/lib/bds/rendering.ex b/lib/bds/rendering.ex index c5bf218..7a9d0ae 100644 --- a/lib/bds/rendering.ex +++ b/lib/bds/rendering.ex @@ -10,7 +10,7 @@ defmodule BDS.Rendering do alias BDS.Metadata alias BDS.Projects alias BDS.Rendering.Filters - alias BDS.Rendering.I18n + alias BDS.I18n alias BDS.Repo alias BDS.Tags.Tag alias BDS.Posts.Post diff --git a/lib/bds/rendering/filters.ex b/lib/bds/rendering/filters.ex index 31533b8..cf272cb 100644 --- a/lib/bds/rendering/filters.ex +++ b/lib/bds/rendering/filters.ex @@ -3,7 +3,7 @@ defmodule BDS.Rendering.Filters do use Liquex.Filter - alias BDS.Rendering.I18n + alias BDS.I18n def i18n(value, language, _context) do key = value |> to_string() |> String.trim() diff --git a/lib/bds/rendering/i18n.ex b/lib/bds/rendering/i18n.ex index b99c7e8..6903037 100644 --- a/lib/bds/rendering/i18n.ex +++ b/lib/bds/rendering/i18n.ex @@ -1,224 +1,8 @@ defmodule BDS.Rendering.I18n do @moduledoc false - @supported_languages ~w(en de fr it es) - - @flags %{ - "en" => "🇬🇧", - "de" => "🇩🇪", - "fr" => "🇫🇷", - "it" => "🇮🇹", - "es" => "🇪🇸" - } - - @catalog %{ - "en" => %{ - "render.archive" => "Archive", - "render.pagination.label" => "Pagination", - "render.pagination.newer" => "newer", - "render.pagination.older" => "older", - "render.notFound.message" => "The requested preview page could not be found.", - "render.notFound.back" => "Back to preview home", - "render.photoArchive.empty" => "No photos found for this archive.", - "render.gallery.empty" => "No linked images found.", - "render.tagCloud.empty" => "No tags found.", - "render.tagCloud.ariaLabel" => "Tag cloud", - "render.calendar.open" => "Open calendar", - "render.calendar.close" => "Close calendar", - "render.calendar.title" => "Archive calendar", - "render.calendar.loading" => "Loading calendar…", - "render.calendar.error" => "Calendar data could not be loaded.", - "render.taxonomy.ariaLabel" => "Taxonomy", - "render.backlinks.label" => "Linked from", - "render.backlinks.ariaLabel" => "Backlinks", - "render.languageSwitcher.ariaLabel" => "Language", - "render.video.youtubeTitle" => "YouTube video", - "render.video.vimeoTitle" => "Vimeo video", - "render.search.placeholder" => "Search...", - "render.search.ariaLabel" => "Site search", - "render.month.1" => "January", - "render.month.2" => "February", - "render.month.3" => "March", - "render.month.4" => "April", - "render.month.5" => "May", - "render.month.6" => "June", - "render.month.7" => "July", - "render.month.8" => "August", - "render.month.9" => "September", - "render.month.10" => "October", - "render.month.11" => "November", - "render.month.12" => "December" - }, - "de" => %{ - "render.archive" => "Archiv", - "render.pagination.label" => "Seitennummerierung", - "render.pagination.newer" => "neuer", - "render.pagination.older" => "älter", - "render.notFound.message" => "Die angeforderte Vorschauseite konnte nicht gefunden werden.", - "render.notFound.back" => "Zurück zur Vorschau-Startseite", - "render.photoArchive.empty" => "Keine Fotos für dieses Archiv gefunden.", - "render.gallery.empty" => "Keine verknüpften Bilder gefunden.", - "render.tagCloud.empty" => "Keine Tags gefunden.", - "render.tagCloud.ariaLabel" => "Tag-Wolke", - "render.calendar.open" => "Kalender öffnen", - "render.calendar.close" => "Kalender schließen", - "render.calendar.title" => "Archivkalender", - "render.calendar.loading" => "Kalender wird geladen …", - "render.calendar.error" => "Kalenderdaten konnten nicht geladen werden.", - "render.taxonomy.ariaLabel" => "Taxonomie", - "render.backlinks.label" => "Verlinkt von", - "render.backlinks.ariaLabel" => "Rückverweise", - "render.languageSwitcher.ariaLabel" => "Sprache", - "render.video.youtubeTitle" => "YouTube-Video", - "render.video.vimeoTitle" => "Vimeo-Video", - "render.search.placeholder" => "Suchen...", - "render.search.ariaLabel" => "Seitensuche", - "render.month.1" => "Januar", - "render.month.2" => "Februar", - "render.month.3" => "März", - "render.month.4" => "Apr.", - "render.month.5" => "Mai", - "render.month.6" => "Juni", - "render.month.7" => "Juli", - "render.month.8" => "Aug.", - "render.month.9" => "Sept.", - "render.month.10" => "Oktober", - "render.month.11" => "Nov.", - "render.month.12" => "Dezember" - }, - "fr" => %{ - "render.archive" => "Archives", - "render.pagination.label" => "Navigation paginée", - "render.pagination.newer" => "plus récent", - "render.pagination.older" => "plus ancien", - "render.notFound.message" => "La page d’aperçu demandée est introuvable.", - "render.notFound.back" => "Retour à l’accueil de l’aperçu", - "render.photoArchive.empty" => "Aucune photo trouvée pour cette archive.", - "render.gallery.empty" => "Aucune image liée trouvée.", - "render.tagCloud.empty" => "Aucun tag trouvé.", - "render.tagCloud.ariaLabel" => "Nuage de tags", - "render.calendar.open" => "Ouvrir le calendrier", - "render.calendar.close" => "Fermer le calendrier", - "render.calendar.title" => "Calendrier des archives", - "render.calendar.loading" => "Chargement du calendrier…", - "render.calendar.error" => "Impossible de charger les données du calendrier.", - "render.taxonomy.ariaLabel" => "Taxonomie", - "render.backlinks.label" => "Lié depuis", - "render.backlinks.ariaLabel" => "Rétroliens", - "render.languageSwitcher.ariaLabel" => "Langue", - "render.video.youtubeTitle" => "Vidéo YouTube", - "render.video.vimeoTitle" => "Vidéo Vimeo", - "render.search.placeholder" => "Rechercher...", - "render.search.ariaLabel" => "Recherche du site", - "render.month.1" => "janvier", - "render.month.2" => "février", - "render.month.3" => "mars", - "render.month.4" => "avril", - "render.month.5" => "mai", - "render.month.6" => "juin", - "render.month.7" => "juillet", - "render.month.8" => "août", - "render.month.9" => "septembre", - "render.month.10" => "octobre", - "render.month.11" => "novembre", - "render.month.12" => "décembre" - }, - "it" => %{ - "render.archive" => "Archivio", - "render.pagination.label" => "Paginazione", - "render.pagination.newer" => "più recente", - "render.pagination.older" => "più vecchio", - "render.notFound.message" => "La pagina di anteprima richiesta non è stata trovata.", - "render.notFound.back" => "Torna alla home di anteprima", - "render.photoArchive.empty" => "Nessuna foto trovata per questo archivio.", - "render.gallery.empty" => "Nessuna immagine collegata trovata.", - "render.tagCloud.empty" => "Nessun tag trovato.", - "render.tagCloud.ariaLabel" => "Nuvola di tag", - "render.calendar.open" => "Apri calendario", - "render.calendar.close" => "Chiudi calendario", - "render.calendar.title" => "Calendario archivio", - "render.calendar.loading" => "Caricamento calendario…", - "render.calendar.error" => "Impossibile caricare i dati del calendario.", - "render.taxonomy.ariaLabel" => "Tassonomia", - "render.backlinks.label" => "Collegato da", - "render.backlinks.ariaLabel" => "Retrocollegamenti", - "render.languageSwitcher.ariaLabel" => "Lingua", - "render.video.youtubeTitle" => "Video YouTube", - "render.video.vimeoTitle" => "Video Vimeo", - "render.search.placeholder" => "Cerca...", - "render.search.ariaLabel" => "Ricerca nel sito", - "render.month.1" => "gennaio", - "render.month.2" => "febbraio", - "render.month.3" => "marzo", - "render.month.4" => "aprile", - "render.month.5" => "maggio", - "render.month.6" => "giugno", - "render.month.7" => "luglio", - "render.month.8" => "agosto", - "render.month.9" => "settembre", - "render.month.10" => "ottobre", - "render.month.11" => "novembre", - "render.month.12" => "dicembre" - }, - "es" => %{ - "render.archive" => "Archivo", - "render.pagination.label" => "Paginación", - "render.pagination.newer" => "más reciente", - "render.pagination.older" => "más antiguo", - "render.notFound.message" => "No se pudo encontrar la página de vista previa solicitada.", - "render.notFound.back" => "Volver al inicio de vista previa", - "render.photoArchive.empty" => "No se encontraron fotos para este archivo.", - "render.gallery.empty" => "No se encontraron imágenes vinculadas.", - "render.tagCloud.empty" => "No se encontraron etiquetas.", - "render.tagCloud.ariaLabel" => "Nube de etiquetas", - "render.calendar.open" => "Abrir calendario", - "render.calendar.close" => "Cerrar calendario", - "render.calendar.title" => "Calendario de archivo", - "render.calendar.loading" => "Cargando calendario…", - "render.calendar.error" => "No se pudieron cargar los datos del calendario.", - "render.taxonomy.ariaLabel" => "Taxonomía", - "render.backlinks.label" => "Enlazado desde", - "render.backlinks.ariaLabel" => "Retroenlaces", - "render.languageSwitcher.ariaLabel" => "Idioma", - "render.video.youtubeTitle" => "Vídeo de YouTube", - "render.video.vimeoTitle" => "Vídeo de Vimeo", - "render.search.placeholder" => "Buscar...", - "render.search.ariaLabel" => "Buscar en el sitio", - "render.month.1" => "enero", - "render.month.2" => "febrero", - "render.month.3" => "marzo", - "render.month.4" => "abril", - "render.month.5" => "mayo", - "render.month.6" => "junio", - "render.month.7" => "julio", - "render.month.8" => "agosto", - "render.month.9" => "septiembre", - "render.month.10" => "octubre", - "render.month.11" => "noviembre", - "render.month.12" => "diciembre" - } - } - - def normalize_language(language) do - language - |> to_string() - |> String.trim() - |> String.downcase() - |> String.split("-", parts: 2) - |> List.first() - |> case do - value when value in @supported_languages -> value - _other -> "en" - end - end - - def translate(language, key) do - normalized_language = normalize_language(language) - @catalog[normalized_language][key] || @catalog["en"][key] || key - end - - def flag(language) do - normalized_language = normalize_language(language) - Map.get(@flags, normalized_language, String.upcase(normalized_language)) - end + defdelegate supported_languages(), to: BDS.I18n + defdelegate normalize_language(language), to: BDS.I18n + defdelegate translate(language, key), to: BDS.I18n + defdelegate flag(language), to: BDS.I18n end diff --git a/priv/i18n/locales/de.json b/priv/i18n/locales/de.json new file mode 100644 index 0000000..e6d968b --- /dev/null +++ b/priv/i18n/locales/de.json @@ -0,0 +1,48 @@ +{ + "activity.aiAssistant": "KI-Assistent", + "activity.import": "Importieren", + "activity.media": "Medien", + "activity.pages": "Seiten", + "activity.posts": "Beiträge", + "activity.scripts": "Skripte", + "activity.sourceControl": "Quellcodeverwaltung", + "activity.tags": "Tags", + "activity.templates": "Vorlagen", + "common.settings": "Einstellungen", + "render.archive": "Archiv", + "render.backlinks.ariaLabel": "Rückverweise", + "render.backlinks.label": "Verlinkt von", + "render.calendar.close": "Kalender schließen", + "render.calendar.error": "Kalenderdaten konnten nicht geladen werden.", + "render.calendar.loading": "Kalender wird geladen …", + "render.calendar.open": "Kalender öffnen", + "render.calendar.title": "Archivkalender", + "render.gallery.empty": "Keine verknüpften Bilder gefunden.", + "render.languageSwitcher.ariaLabel": "Sprache", + "render.month.1": "Januar", + "render.month.10": "Oktober", + "render.month.11": "Nov.", + "render.month.12": "Dezember", + "render.month.2": "Februar", + "render.month.3": "März", + "render.month.4": "Apr.", + "render.month.5": "Mai", + "render.month.6": "Juni", + "render.month.7": "Juli", + "render.month.8": "Aug.", + "render.month.9": "Sept.", + "render.notFound.back": "Zurück zur Vorschau-Startseite", + "render.notFound.message": "Die angeforderte Vorschauseite konnte nicht gefunden werden.", + "render.pagination.label": "Seitennummerierung", + "render.pagination.newer": "neuer", + "render.pagination.older": "älter", + "render.photoArchive.empty": "Keine Fotos für dieses Archiv gefunden.", + "render.search.ariaLabel": "Seitensuche", + "render.search.placeholder": "Suchen...", + "render.tagCloud.ariaLabel": "Tag-Wolke", + "render.tagCloud.empty": "Keine Tags gefunden.", + "render.taxonomy.ariaLabel": "Taxonomie", + "render.video.vimeoTitle": "Vimeo-Video", + "render.video.youtubeTitle": "YouTube-Video", + "sidebar.chat.yesterday": "Gestern" +} \ No newline at end of file diff --git a/priv/i18n/locales/en.json b/priv/i18n/locales/en.json new file mode 100644 index 0000000..e42134f --- /dev/null +++ b/priv/i18n/locales/en.json @@ -0,0 +1,48 @@ +{ + "activity.aiAssistant": "AI Assistant", + "activity.import": "Import", + "activity.media": "Media", + "activity.pages": "Pages", + "activity.posts": "Posts", + "activity.scripts": "Scripts", + "activity.sourceControl": "Source Control", + "activity.tags": "Tags", + "activity.templates": "Templates", + "common.settings": "Settings", + "render.archive": "Archive", + "render.backlinks.ariaLabel": "Backlinks", + "render.backlinks.label": "Linked from", + "render.calendar.close": "Close calendar", + "render.calendar.error": "Calendar data could not be loaded.", + "render.calendar.loading": "Loading calendar…", + "render.calendar.open": "Open calendar", + "render.calendar.title": "Archive calendar", + "render.gallery.empty": "No linked images found.", + "render.languageSwitcher.ariaLabel": "Language", + "render.month.1": "January", + "render.month.10": "October", + "render.month.11": "November", + "render.month.12": "December", + "render.month.2": "February", + "render.month.3": "March", + "render.month.4": "April", + "render.month.5": "May", + "render.month.6": "June", + "render.month.7": "July", + "render.month.8": "August", + "render.month.9": "September", + "render.notFound.back": "Back to preview home", + "render.notFound.message": "The requested preview page could not be found.", + "render.pagination.label": "Pagination", + "render.pagination.newer": "newer", + "render.pagination.older": "older", + "render.photoArchive.empty": "No photos found for this archive.", + "render.search.ariaLabel": "Site search", + "render.search.placeholder": "Search...", + "render.tagCloud.ariaLabel": "Tag cloud", + "render.tagCloud.empty": "No tags found.", + "render.taxonomy.ariaLabel": "Taxonomy", + "render.video.vimeoTitle": "Vimeo video", + "render.video.youtubeTitle": "YouTube video", + "sidebar.chat.yesterday": "Yesterday" +} \ No newline at end of file diff --git a/priv/i18n/locales/es.json b/priv/i18n/locales/es.json new file mode 100644 index 0000000..c1d5c10 --- /dev/null +++ b/priv/i18n/locales/es.json @@ -0,0 +1,48 @@ +{ + "activity.aiAssistant": "Asistente de IA", + "activity.import": "Importar", + "activity.media": "Medios", + "activity.pages": "Páginas", + "activity.posts": "Publicaciones", + "activity.scripts": "Scripts", + "activity.sourceControl": "Control de código fuente", + "activity.tags": "Etiquetas", + "activity.templates": "Plantillas", + "common.settings": "Configuración", + "render.archive": "Archivo", + "render.backlinks.ariaLabel": "Retroenlaces", + "render.backlinks.label": "Enlazado desde", + "render.calendar.close": "Cerrar calendario", + "render.calendar.error": "No se pudieron cargar los datos del calendario.", + "render.calendar.loading": "Cargando calendario…", + "render.calendar.open": "Abrir calendario", + "render.calendar.title": "Calendario de archivo", + "render.gallery.empty": "No se encontraron imágenes vinculadas.", + "render.languageSwitcher.ariaLabel": "Idioma", + "render.month.1": "enero", + "render.month.10": "octubre", + "render.month.11": "noviembre", + "render.month.12": "diciembre", + "render.month.2": "febrero", + "render.month.3": "marzo", + "render.month.4": "abril", + "render.month.5": "mayo", + "render.month.6": "junio", + "render.month.7": "julio", + "render.month.8": "agosto", + "render.month.9": "septiembre", + "render.notFound.back": "Volver al inicio de vista previa", + "render.notFound.message": "No se pudo encontrar la página de vista previa solicitada.", + "render.pagination.label": "Paginación", + "render.pagination.newer": "más reciente", + "render.pagination.older": "más antiguo", + "render.photoArchive.empty": "No se encontraron fotos para este archivo.", + "render.search.ariaLabel": "Buscar en el sitio", + "render.search.placeholder": "Buscar...", + "render.tagCloud.ariaLabel": "Nube de etiquetas", + "render.tagCloud.empty": "No se encontraron etiquetas.", + "render.taxonomy.ariaLabel": "Taxonomía", + "render.video.vimeoTitle": "Vídeo de Vimeo", + "render.video.youtubeTitle": "Vídeo de YouTube", + "sidebar.chat.yesterday": "Ayer" +} \ No newline at end of file diff --git a/priv/i18n/locales/fr.json b/priv/i18n/locales/fr.json new file mode 100644 index 0000000..44f8b43 --- /dev/null +++ b/priv/i18n/locales/fr.json @@ -0,0 +1,48 @@ +{ + "activity.aiAssistant": "Assistant IA", + "activity.import": "Importer", + "activity.media": "Médias", + "activity.pages": "Pages", + "activity.posts": "Articles", + "activity.scripts": "Scripts", + "activity.sourceControl": "Contrôle de source", + "activity.tags": "Tags", + "activity.templates": "Modèles", + "common.settings": "Paramètres", + "render.archive": "Archives", + "render.backlinks.ariaLabel": "Rétroliens", + "render.backlinks.label": "Lié depuis", + "render.calendar.close": "Fermer le calendrier", + "render.calendar.error": "Impossible de charger les données du calendrier.", + "render.calendar.loading": "Chargement du calendrier…", + "render.calendar.open": "Ouvrir le calendrier", + "render.calendar.title": "Calendrier des archives", + "render.gallery.empty": "Aucune image liée trouvée.", + "render.languageSwitcher.ariaLabel": "Langue", + "render.month.1": "janvier", + "render.month.10": "octobre", + "render.month.11": "novembre", + "render.month.12": "décembre", + "render.month.2": "février", + "render.month.3": "mars", + "render.month.4": "avril", + "render.month.5": "mai", + "render.month.6": "juin", + "render.month.7": "juillet", + "render.month.8": "août", + "render.month.9": "septembre", + "render.notFound.back": "Retour à l’accueil de l’aperçu", + "render.notFound.message": "La page d’aperçu demandée est introuvable.", + "render.pagination.label": "Navigation paginée", + "render.pagination.newer": "plus récent", + "render.pagination.older": "plus ancien", + "render.photoArchive.empty": "Aucune photo trouvée pour cette archive.", + "render.search.ariaLabel": "Recherche du site", + "render.search.placeholder": "Rechercher...", + "render.tagCloud.ariaLabel": "Nuage de tags", + "render.tagCloud.empty": "Aucun tag trouvé.", + "render.taxonomy.ariaLabel": "Taxonomie", + "render.video.vimeoTitle": "Vidéo Vimeo", + "render.video.youtubeTitle": "Vidéo YouTube", + "sidebar.chat.yesterday": "Hier" +} \ No newline at end of file diff --git a/priv/i18n/locales/it.json b/priv/i18n/locales/it.json new file mode 100644 index 0000000..23a70d3 --- /dev/null +++ b/priv/i18n/locales/it.json @@ -0,0 +1,48 @@ +{ + "activity.aiAssistant": "Assistente IA", + "activity.import": "Importa", + "activity.media": "Media", + "activity.pages": "Pagine", + "activity.posts": "Post", + "activity.scripts": "Script", + "activity.sourceControl": "Controllo del codice sorgente", + "activity.tags": "Tag", + "activity.templates": "Template", + "common.settings": "Impostazioni", + "render.archive": "Archivio", + "render.backlinks.ariaLabel": "Retrocollegamenti", + "render.backlinks.label": "Collegato da", + "render.calendar.close": "Chiudi calendario", + "render.calendar.error": "Impossibile caricare i dati del calendario.", + "render.calendar.loading": "Caricamento calendario…", + "render.calendar.open": "Apri calendario", + "render.calendar.title": "Calendario archivio", + "render.gallery.empty": "Nessuna immagine collegata trovata.", + "render.languageSwitcher.ariaLabel": "Lingua", + "render.month.1": "gennaio", + "render.month.10": "ottobre", + "render.month.11": "novembre", + "render.month.12": "dicembre", + "render.month.2": "febbraio", + "render.month.3": "marzo", + "render.month.4": "aprile", + "render.month.5": "maggio", + "render.month.6": "giugno", + "render.month.7": "luglio", + "render.month.8": "agosto", + "render.month.9": "settembre", + "render.notFound.back": "Torna alla home di anteprima", + "render.notFound.message": "La pagina di anteprima richiesta non è stata trovata.", + "render.pagination.label": "Paginazione", + "render.pagination.newer": "più recente", + "render.pagination.older": "più vecchio", + "render.photoArchive.empty": "Nessuna foto trovata per questo archivio.", + "render.search.ariaLabel": "Ricerca nel sito", + "render.search.placeholder": "Cerca...", + "render.tagCloud.ariaLabel": "Nuvola di tag", + "render.tagCloud.empty": "Nessun tag trovato.", + "render.taxonomy.ariaLabel": "Tassonomia", + "render.video.vimeoTitle": "Video Vimeo", + "render.video.youtubeTitle": "Video YouTube", + "sidebar.chat.yesterday": "Ieri" +} \ No newline at end of file diff --git a/test/bds/i18n_test.exs b/test/bds/i18n_test.exs new file mode 100644 index 0000000..c78ad1c --- /dev/null +++ b/test/bds/i18n_test.exs @@ -0,0 +1,49 @@ +defmodule BDS.I18nTest do + use ExUnit.Case, async: true + + test "supported languages, normalization, and UI locale resolution follow the spec" do + assert Enum.map(BDS.I18n.supported_languages(), & &1.code) == ["en", "de", "fr", "it", "es"] + + assert BDS.I18n.normalize_language("de-DE") == "de" + assert BDS.I18n.normalize_language("FR_ca") == "fr" + assert BDS.I18n.normalize_language("pt-BR") == "en" + + assert BDS.I18n.resolve_ui_locale("it-IT") == "it" + assert BDS.I18n.resolve_ui_locale("es_ES.UTF-8") == "es" + assert BDS.I18n.resolve_ui_locale(nil) == "en" + + assert BDS.I18n.resolve_render_locale("fr-FR") == "fr" + assert BDS.I18n.resolve_render_locale("unknown") == "en" + end + + test "format locale mapping uses the spec locale table" do + assert BDS.I18n.format_locale("en") == "en-US" + assert BDS.I18n.format_locale("de-DE") == "de-DE" + assert BDS.I18n.format_locale("fr") == "fr-FR" + assert BDS.I18n.format_locale("it-CH") == "it-IT" + assert BDS.I18n.format_locale("es_MX") == "es-ES" + assert BDS.I18n.format_locale("pt-BR") == "en-US" + end + + test "render translations are loaded from locale json files with unsupported-locale fallback" do + locale_dir = Application.app_dir(:bds, "priv/i18n/locales") + + assert File.exists?(Path.join(locale_dir, "en.json")) + assert File.exists?(Path.join(locale_dir, "de.json")) + assert File.exists?(Path.join(locale_dir, "fr.json")) + assert File.exists?(Path.join(locale_dir, "it.json")) + assert File.exists?(Path.join(locale_dir, "es.json")) + + assert BDS.I18n.get_render_translations("de") == + Jason.decode!(File.read!(Path.join(locale_dir, "de.json"))) + + assert BDS.I18n.translate("de", "render.notFound.back") == + "Zurück zur Vorschau-Startseite" + + assert BDS.I18n.translate("pt-BR", "render.notFound.back") == "Back to preview home" + end + + test "supported locales do not silently fall back to english per key" do + assert BDS.I18n.translate("de", "missing.key") == "missing.key" + end +end