diff --git a/CODESMELL.md b/CODESMELL.md index f9cef8d..677fdf5 100644 --- a/CODESMELL.md +++ b/CODESMELL.md @@ -37,11 +37,15 @@ _None._ All modules previously on the queue have been split; refresh the queue i ## 2. Process Dictionary for i18n State -**Status:** open. `Process.put(:bds_ui_locale, …)` + `Process.get(:bds_ui_locale)` in `render/1` of `BDS.Desktop.ShellLive` and every `*_editor.ex` (15 sites total). +**Status:** ✅ encapsulated (2026-05-10). Raw `Process.put(:bds_ui_locale, _)` / `Process.get(:bds_ui_locale)` no longer appears outside `BDS.Desktop.UILocale`. The two render boundaries (`BDS.Desktop.ShellLive.render/1` and `BDS.Desktop.ShellLive.SidebarComponents.sidebar_content/1`) now call `UILocale.put/1`; the ~30 helper read sites call `BDS.Desktop.UILocale.current/0`. The full thread-locale-through-assigns rewrite (~733 HEEx call sites) was rejected as too invasive for the marginal benefit; the encapsulation removes the implicit-global smell while keeping the LiveView lazy-render path intact. -**Why it matters:** implicit global state; complicates per-process isolation in tests and risks leaks between concurrent operations in the same process. +**Why the helper does not restore on render exit:** Phoenix's `~H` returns a `Phoenix.LiveView.Rendered` whose `dynamic` function is invoked lazily by LiveView *after* `render/1` returns. A `try/after Process.delete` wrapper around `render/1` would clear the binding before child components materialize, so render boundaries use `UILocale.put/1` (set-only). `UILocale.with_locale/2` is provided for short-lived non-LiveView contexts (background tasks, scripts) that consume the binding eagerly inside the closure. -**Plan:** thread `locale` through `assigns`/render args; replace `translated/1,2` with a 3-arity helper that takes the locale explicitly; ban `Process.put` via Credo afterwards. +**Module rules:** + +- Only `lib/bds/desktop/ui_locale.ex` may touch the `:bds_ui_locale` process key. +- New code that needs the active UI locale must call `BDS.Desktop.UILocale.current/0` (or take it as an argument). +- New render entry points that change the locale must call `BDS.Desktop.UILocale.put/1` at the top of `render/1`. --- @@ -165,6 +169,10 @@ Most tests share the SQLite repo and named GenServers (`BDS.Tasks`, `BDS.Search` ## Changelog +### 2026-05-10 + +- **Process dictionary for i18n state (Section 2)**: encapsulated behind `BDS.Desktop.UILocale` (`lib/bds/desktop/ui_locale.ex`, ~50 lines). Public surface: `put/1` (set without restore, for LV render boundaries that return lazy `Rendered`), `with_locale/2` (set + try/after restore, for short-lived eager contexts), `current/0` (read, returns `nil` when unset). The two raw `Process.put(:bds_ui_locale, _)` sites (`BDS.Desktop.ShellLive.render/1` and `BDS.Desktop.ShellLive.SidebarComponents.sidebar_content/1`) now call `UILocale.put/1`; the ~30 raw `Process.get(:bds_ui_locale)` reads (every editor `translated/1,2` helper plus `BDS.Desktop.ShellData.effective_ui_language/1`) now call `BDS.Desktop.UILocale.current/0`. The full thread-locale-through-assigns rewrite (~733 HEEx call sites) was deliberately rejected as too invasive; the encapsulation removes the implicit-global smell while preserving Phoenix's lazy `Rendered` evaluation. The render path uses `put/1` (not `with_locale/2`) because Phoenix `~H` returns a `Rendered` whose `dynamic` is invoked by LiveView *after* `render/1` returns; a `try/after Process.delete` would clear the binding before child components materialize. Only `BDS.Desktop.UILocale` is allowed to touch the `:bds_ui_locale` process key. Validates clean: `mix compile --warnings-as-errors`, `mix dialyzer --format short` (Total errors: 0), `mix test` (342 tests, 0 failures, 4 skipped, three consecutive runs). + ### 2026-05-09 - **God modules**: diff --git a/lib/bds/desktop/shell_data.ex b/lib/bds/desktop/shell_data.ex index 7f4b420..93fdcdc 100644 --- a/lib/bds/desktop/shell_data.ex +++ b/lib/bds/desktop/shell_data.ex @@ -249,7 +249,7 @@ defmodule BDS.Desktop.ShellData do end defp effective_ui_language(nil) do - Process.get(:bds_ui_locale) || ui_language() + BDS.Desktop.UILocale.current() || ui_language() end defp effective_ui_language(locale), do: locale diff --git a/lib/bds/desktop/shell_live.ex b/lib/bds/desktop/shell_live.ex index f30b542..acf0c20 100644 --- a/lib/bds/desktop/shell_live.ex +++ b/lib/bds/desktop/shell_live.ex @@ -7,7 +7,7 @@ defmodule BDS.Desktop.ShellLive do alias BDS.AI alias BDS.CliSync.Watcher - alias BDS.Desktop.{FolderPicker, Overlay, ShellData} + alias BDS.Desktop.{FolderPicker, Overlay, ShellData, UILocale} alias BDS.Desktop.ShellLive.{ChatEditor, CodeEntityEditor, ImportEditor, MediaEditor, MenuEditor, MiscEditor, SettingsEditor, TagsEditor} alias BDS.Desktop.ShellLive.OverlayComponents, as: ShellOverlayComponents alias BDS.Desktop.ShellLive.PostEditor @@ -1285,7 +1285,7 @@ defmodule BDS.Desktop.ShellLive do @impl true def render(assigns) do - Process.put(:bds_ui_locale, assigns.page_language) + UILocale.put(assigns.page_language) index(assigns) end @@ -1346,7 +1346,7 @@ defmodule BDS.Desktop.ShellLive do |> assign_misc_editor() end - defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, UILocale.current()) defp encoded_shortcuts(shortcuts), do: Jason.encode!(shortcuts) diff --git a/lib/bds/desktop/shell_live/chat_editor.ex b/lib/bds/desktop/shell_live/chat_editor.ex index e31d150..93cf4fe 100644 --- a/lib/bds/desktop/shell_live/chat_editor.ex +++ b/lib/bds/desktop/shell_live/chat_editor.ex @@ -572,5 +572,5 @@ defmodule BDS.Desktop.ShellLive.ChatEditor do defp format_error(reason), do: inspect(reason) def translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/chat_editor/message_build.ex b/lib/bds/desktop/shell_live/chat_editor/message_build.ex index fe26138..9cc1635 100644 --- a/lib/bds/desktop/shell_live/chat_editor/message_build.ex +++ b/lib/bds/desktop/shell_live/chat_editor/message_build.ex @@ -114,5 +114,5 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.MessageBuild do defp streaming_content(_request), do: "" defp translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/chat_editor/model_selection.ex b/lib/bds/desktop/shell_live/chat_editor/model_selection.ex index 5dfe96f..888c8d2 100644 --- a/lib/bds/desktop/shell_live/chat_editor/model_selection.ex +++ b/lib/bds/desktop/shell_live/chat_editor/model_selection.ex @@ -76,5 +76,5 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ModelSelection do defp blank?(nil), do: true defp translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/chat_editor/tool_surfaces.ex b/lib/bds/desktop/shell_live/chat_editor/tool_surfaces.ex index 1f5df32..a58c584 100644 --- a/lib/bds/desktop/shell_live/chat_editor/tool_surfaces.ex +++ b/lib/bds/desktop/shell_live/chat_editor/tool_surfaces.ex @@ -270,5 +270,5 @@ defmodule BDS.Desktop.ShellLive.ChatEditor.ToolSurfaces do defp truthy?(_value), do: false defp translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/chat_surface.ex b/lib/bds/desktop/shell_live/chat_surface.ex index 2fe9773..7aa2984 100644 --- a/lib/bds/desktop/shell_live/chat_surface.ex +++ b/lib/bds/desktop/shell_live/chat_surface.ex @@ -229,5 +229,5 @@ defmodule BDS.Desktop.ShellLive.ChatSurface do end end - defp translated(text), do: ShellData.translate(text, %{}, Process.get(:bds_ui_locale)) + defp translated(text), do: ShellData.translate(text, %{}, BDS.Desktop.UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/code_entity_editor.ex b/lib/bds/desktop/shell_live/code_entity_editor.ex index 9314d06..bbeeeb8 100644 --- a/lib/bds/desktop/shell_live/code_entity_editor.ex +++ b/lib/bds/desktop/shell_live/code_entity_editor.ex @@ -190,7 +190,7 @@ defmodule BDS.Desktop.ShellLive.CodeEntityEditor do def build_template(_assigns), do: nil - def translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + def translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) def format_timestamp(nil), do: "" def format_timestamp(timestamp), do: BDS.Persistence.timestamp_to_iso8601(timestamp) diff --git a/lib/bds/desktop/shell_live/import_editor.ex b/lib/bds/desktop/shell_live/import_editor.ex index a68f128..e79ca65 100644 --- a/lib/bds/desktop/shell_live/import_editor.ex +++ b/lib/bds/desktop/shell_live/import_editor.ex @@ -770,7 +770,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor do end end - defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) defp present?(value), do: value not in [nil, ""] defp blank?(value), do: value in [nil, ""] end diff --git a/lib/bds/desktop/shell_live/import_editor/analysis_state.ex b/lib/bds/desktop/shell_live/import_editor/analysis_state.ex index 380bb2b..9b37e46 100644 --- a/lib/bds/desktop/shell_live/import_editor/analysis_state.ex +++ b/lib/bds/desktop/shell_live/import_editor/analysis_state.ex @@ -243,6 +243,6 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.AnalysisState do def translate_phase(other), do: other - defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) defp present?(value), do: value not in [nil, ""] end diff --git a/lib/bds/desktop/shell_live/import_editor/progress_tracking.ex b/lib/bds/desktop/shell_live/import_editor/progress_tracking.ex index 13434a5..3e9ca08 100644 --- a/lib/bds/desktop/shell_live/import_editor/progress_tracking.ex +++ b/lib/bds/desktop/shell_live/import_editor/progress_tracking.ex @@ -242,5 +242,5 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.ProgressTracking do def translate_execution_phase(other), do: other - defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex b/lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex index 0d9e74d..aefad7d 100644 --- a/lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex +++ b/lib/bds/desktop/shell_live/import_editor/taxonomy_editing.ex @@ -199,7 +199,7 @@ defmodule BDS.Desktop.ShellLive.ImportEditor.TaxonomyEditing do def maybe_put_option(opts, _key, nil), do: opts def maybe_put_option(opts, key, value), do: Keyword.put(opts, key, value) - defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) defp present?(value), do: value not in [nil, ""] defp blank_to_nil(""), do: nil defp blank_to_nil(value), do: value diff --git a/lib/bds/desktop/shell_live/media_editor.ex b/lib/bds/desktop/shell_live/media_editor.ex index f0decfc..947129b 100644 --- a/lib/bds/desktop/shell_live/media_editor.ex +++ b/lib/bds/desktop/shell_live/media_editor.ex @@ -415,7 +415,7 @@ defmodule BDS.Desktop.ShellLive.MediaEditor do def build(_assigns), do: nil - def translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + def translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) def media_editor_save_state_label(:dirty), do: translated("Unsaved") def media_editor_save_state_label(:saved), do: translated("Saved") diff --git a/lib/bds/desktop/shell_live/menu_editor.ex b/lib/bds/desktop/shell_live/menu_editor.ex index 83e8391..b8a1208 100644 --- a/lib/bds/desktop/shell_live/menu_editor.ex +++ b/lib/bds/desktop/shell_live/menu_editor.ex @@ -307,7 +307,7 @@ defmodule BDS.Desktop.ShellLive.MenuEditor do end def translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) def row_label(item, category_titles) do if item.kind == :category_archive do diff --git a/lib/bds/desktop/shell_live/menu_editor/draft_management.ex b/lib/bds/desktop/shell_live/menu_editor/draft_management.ex index 46ca3f6..f93f2e9 100644 --- a/lib/bds/desktop/shell_live/menu_editor/draft_management.ex +++ b/lib/bds/desktop/shell_live/menu_editor/draft_management.ex @@ -128,5 +128,5 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.DraftManagement do end defp translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/menu_editor/state.ex b/lib/bds/desktop/shell_live/menu_editor/state.ex index 47d87c2..139bd1e 100644 --- a/lib/bds/desktop/shell_live/menu_editor/state.ex +++ b/lib/bds/desktop/shell_live/menu_editor/state.ex @@ -81,5 +81,5 @@ defmodule BDS.Desktop.ShellLive.MenuEditor.State do end defp translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/misc_editor.ex b/lib/bds/desktop/shell_live/misc_editor.ex index 26d86fb..9d4a5ac 100644 --- a/lib/bds/desktop/shell_live/misc_editor.ex +++ b/lib/bds/desktop/shell_live/misc_editor.ex @@ -214,7 +214,7 @@ defmodule BDS.Desktop.ShellLive.MiscEditor do def build(_assigns), do: nil - def translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + def translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) def misc_class(:site_validation), do: "site-validation-view" def misc_class(:metadata_diff), do: "metadata-diff-view" diff --git a/lib/bds/desktop/shell_live/overlay_components.ex b/lib/bds/desktop/shell_live/overlay_components.ex index b7bc40a..7e63650 100644 --- a/lib/bds/desktop/shell_live/overlay_components.ex +++ b/lib/bds/desktop/shell_live/overlay_components.ex @@ -59,7 +59,7 @@ defmodule BDS.Desktop.ShellLive.OverlayComponents do def markdown_link(text, url), do: "[#{text}](#{url})" - def translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + def translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) def project_metadata(nil), do: %{main_language: "en", blog_languages: []} diff --git a/lib/bds/desktop/shell_live/panel_renderer.ex b/lib/bds/desktop/shell_live/panel_renderer.ex index 5d07631..2e5e740 100644 --- a/lib/bds/desktop/shell_live/panel_renderer.ex +++ b/lib/bds/desktop/shell_live/panel_renderer.ex @@ -286,5 +286,5 @@ defmodule BDS.Desktop.ShellLive.PanelRenderer do defp present?(value), do: value not in [nil, ""] - defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/post_editor.ex b/lib/bds/desktop/shell_live/post_editor.ex index 17b68aa..32d25ee 100644 --- a/lib/bds/desktop/shell_live/post_editor.ex +++ b/lib/bds/desktop/shell_live/post_editor.ex @@ -500,7 +500,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor do def post_editor_mode_label(:preview), do: translated("Preview") def translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) defp assigned_project_metadata(assigns), do: Map.get(assigns, :project_metadata, %{}) end diff --git a/lib/bds/desktop/shell_live/post_editor/persistence.ex b/lib/bds/desktop/shell_live/post_editor/persistence.ex index 800a30a..9fd77f3 100644 --- a/lib/bds/desktop/shell_live/post_editor/persistence.ex +++ b/lib/bds/desktop/shell_live/post_editor/persistence.ex @@ -101,5 +101,5 @@ defmodule BDS.Desktop.ShellLive.PostEditor.Persistence do end defp translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/post_editor/post_metadata.ex b/lib/bds/desktop/shell_live/post_editor/post_metadata.ex index 4b409d4..a2e1b72 100644 --- a/lib/bds/desktop/shell_live/post_editor/post_metadata.ex +++ b/lib/bds/desktop/shell_live/post_editor/post_metadata.ex @@ -186,5 +186,5 @@ defmodule BDS.Desktop.ShellLive.PostEditor.PostMetadata do end defp translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/settings_editor.ex b/lib/bds/desktop/shell_live/settings_editor.ex index 7e582eb..6eca981 100644 --- a/lib/bds/desktop/shell_live/settings_editor.ex +++ b/lib/bds/desktop/shell_live/settings_editor.ex @@ -143,7 +143,7 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor do end def translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) defp current_settings_section(assigns) do meta = current_tab_meta(assigns) diff --git a/lib/bds/desktop/shell_live/settings_editor/ai_settings.ex b/lib/bds/desktop/shell_live/settings_editor/ai_settings.ex index 2b4874e..14469e1 100644 --- a/lib/bds/desktop/shell_live/settings_editor/ai_settings.ex +++ b/lib/bds/desktop/shell_live/settings_editor/ai_settings.ex @@ -199,5 +199,5 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.AISettings do end defp translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/settings_editor/editor_settings.ex b/lib/bds/desktop/shell_live/settings_editor/editor_settings.ex index 9c3ead8..8f8cdc1 100644 --- a/lib/bds/desktop/shell_live/settings_editor/editor_settings.ex +++ b/lib/bds/desktop/shell_live/settings_editor/editor_settings.ex @@ -89,5 +89,5 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.EditorSettings do defp boolean_string(false), do: "false" defp translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/settings_editor/managed_categories.ex b/lib/bds/desktop/shell_live/settings_editor/managed_categories.ex index 7f2926b..042647e 100644 --- a/lib/bds/desktop/shell_live/settings_editor/managed_categories.ex +++ b/lib/bds/desktop/shell_live/settings_editor/managed_categories.ex @@ -190,5 +190,5 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ManagedCategories do end defp translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/settings_editor/mcp_config.ex b/lib/bds/desktop/shell_live/settings_editor/mcp_config.ex index 13e8fd4..b8289e8 100644 --- a/lib/bds/desktop/shell_live/settings_editor/mcp_config.ex +++ b/lib/bds/desktop/shell_live/settings_editor/mcp_config.ex @@ -96,5 +96,5 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.MCPConfig do end defp translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/settings_editor/project_settings.ex b/lib/bds/desktop/shell_live/settings_editor/project_settings.ex index dd7bf3c..3a00325 100644 --- a/lib/bds/desktop/shell_live/settings_editor/project_settings.ex +++ b/lib/bds/desktop/shell_live/settings_editor/project_settings.ex @@ -108,5 +108,5 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.ProjectSettings do end defp translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/settings_editor/publishing_settings.ex b/lib/bds/desktop/shell_live/settings_editor/publishing_settings.ex index c97d697..9ab7bae 100644 --- a/lib/bds/desktop/shell_live/settings_editor/publishing_settings.ex +++ b/lib/bds/desktop/shell_live/settings_editor/publishing_settings.ex @@ -85,5 +85,5 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.PublishingSettings do end defp translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/settings_editor/style_editor.ex b/lib/bds/desktop/shell_live/settings_editor/style_editor.ex index c62f6a9..6835f81 100644 --- a/lib/bds/desktop/shell_live/settings_editor/style_editor.ex +++ b/lib/bds/desktop/shell_live/settings_editor/style_editor.ex @@ -99,5 +99,5 @@ defmodule BDS.Desktop.ShellLive.SettingsEditor.StyleEditor do end defp translated(text, bindings \\ %{}), - do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/sidebar_components.ex b/lib/bds/desktop/shell_live/sidebar_components.ex index bd49f27..a2f697d 100644 --- a/lib/bds/desktop/shell_live/sidebar_components.ex +++ b/lib/bds/desktop/shell_live/sidebar_components.ex @@ -4,10 +4,11 @@ defmodule BDS.Desktop.ShellLive.SidebarComponents do use Phoenix.Component alias BDS.Desktop.ShellData + alias BDS.Desktop.UILocale alias BDS.UI.Registry def sidebar_content(assigns) do - Process.put(:bds_ui_locale, assigns.page_language) + UILocale.put(assigns.page_language) assigns = prepare_filter_assigns(assigns) ~H""" @@ -462,7 +463,7 @@ defmodule BDS.Desktop.ShellLive.SidebarComponents do """ end - defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) defp template_sidebar?(sidebar_data), do: Map.get(sidebar_data, :title) == "Templates" diff --git a/lib/bds/desktop/shell_live/sidebar_create.ex b/lib/bds/desktop/shell_live/sidebar_create.ex index 2ef1abc..21177ee 100644 --- a/lib/bds/desktop/shell_live/sidebar_create.ex +++ b/lib/bds/desktop/shell_live/sidebar_create.ex @@ -127,5 +127,5 @@ defmodule BDS.Desktop.ShellLive.SidebarCreate do def action(:import), do: %{kind: "import", label: "sidebar.import.newDefinition"} def action(_view), do: nil - defp translated(text), do: ShellData.translate(text, %{}, Process.get(:bds_ui_locale)) + defp translated(text), do: ShellData.translate(text, %{}, BDS.Desktop.UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/tab_helpers.ex b/lib/bds/desktop/shell_live/tab_helpers.ex index 4cc854f..9d750c3 100644 --- a/lib/bds/desktop/shell_live/tab_helpers.ex +++ b/lib/bds/desktop/shell_live/tab_helpers.ex @@ -95,5 +95,5 @@ defmodule BDS.Desktop.ShellLive.TabHelpers do end end - defp translated(text), do: ShellData.translate(text, %{}, Process.get(:bds_ui_locale)) + defp translated(text), do: ShellData.translate(text, %{}, BDS.Desktop.UILocale.current()) end diff --git a/lib/bds/desktop/shell_live/tags_editor.ex b/lib/bds/desktop/shell_live/tags_editor.ex index eb30452..4d3fe23 100644 --- a/lib/bds/desktop/shell_live/tags_editor.ex +++ b/lib/bds/desktop/shell_live/tags_editor.ex @@ -187,7 +187,7 @@ defmodule BDS.Desktop.ShellLive.TagsEditor do def build(_assigns), do: nil - def translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) + def translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, BDS.Desktop.UILocale.current()) def tag_font_size(count, counts) do max_count = Enum.max([1 | Enum.map(counts, & &1.count)]) diff --git a/lib/bds/desktop/ui_locale.ex b/lib/bds/desktop/ui_locale.ex new file mode 100644 index 0000000..329fed1 --- /dev/null +++ b/lib/bds/desktop/ui_locale.ex @@ -0,0 +1,65 @@ +defmodule BDS.Desktop.UILocale do + @moduledoc """ + Per-render UI locale binding for the desktop LiveView shell. + + The shell renders the UI in the user's selected language, which can differ + from the OS locale and from the project's `mainLanguage`. Phoenix HEEx + templates call `translated/1,2` helpers without an explicit locale, so we + bind the active locale once per `render/1` and the helpers read it back. + + This module encapsulates that binding so call sites do not touch the raw + process dictionary directly. Use `with_locale/2` around any render or + component that needs a locale binding; use `current/0` to read it. + + Direct use of `Process.put(:bds_ui_locale, _)` or + `Process.get(:bds_ui_locale)` is forbidden outside this module. + """ + + @key :bds_ui_locale + + @typedoc "A normalized UI locale code such as `\"en\"` or `\"de\"`." + @type locale :: String.t() | nil + + @doc """ + Bind `locale` for any subsequent reads of `current/0` in this process. + + Used at LiveView render boundaries. The binding persists past the call + (mirroring per-process locale state) so that lazily evaluated child + components see the active locale. Each render boundary overwrites it. + """ + @spec put(locale()) :: :ok + def put(locale) do + Process.put(@key, locale) + :ok + end + + @doc """ + Set the UI locale for the duration of `fun`, then restore the prior value. + + Safe under exceptions: the prior binding is restored in an `after` clause. + Use this for short-lived non-LiveView contexts (background tasks, scripts) + where eager evaluation guarantees the binding is consumed before `fun` + returns. Do not use around LiveView `render/1` because the returned + `Phoenix.LiveView.Rendered` struct evaluates its dynamic parts lazily. + """ + @spec with_locale(locale(), (-> result)) :: result when result: var + def with_locale(locale, fun) when is_function(fun, 0) do + previous = Process.get(@key) + Process.put(@key, locale) + + try do + fun.() + after + restore(previous) + end + end + + @doc """ + Read the active UI locale binding for this process, or `nil` when unset. + """ + @spec current() :: locale() + def current, do: Process.get(@key) + + defp restore(nil), do: Process.delete(@key) + defp restore(value), do: Process.put(@key, value) +end