Skip to content

Add support for various languages #24

Description

@HadenSmith

Language Library — High-Level Plan

Context

The framework currently has runtime theme switching (Light / Blue / Dark) via a clean two-layer service architecture, but all UI text is hardcoded English. The user wants an analogous Language library so users can switch GUI language at runtime from OptionsDialog, with the same UX as theme switching.

Target user base (from USACE partnerships, deployments, and Google Analytics):

  • Direct partnerships: UK, Netherlands, Spain (dam and levee safety)
  • Wide deployment: US, Australia, Canada
  • Heavy analytics traffic: Singapore, China
  • External peer reviewer: Ribatet (France)
  • Industry citation network: Italian statisticians (academic engineering community)
  • Strategic allies / engineering research communities: Germany, Japan, Sweden
  • Strategic Latin American expansion: Brazil

Selected Languages (locked)

The initial release supports 10 languages:

# Language Locale Audience driver AvalonDock free?
1 English en Base — UK, US, AU, anglo-CA, Singapore n/a (source)
2 Spanish es-ES Spain partnership; serves LatAm acceptably
3 Dutch nl-NL Netherlands partnership (water-management authority audience) ✓ (nl-BE ~98% compatible)
4 Simplified Chinese zh-Hans China users + Singapore Mandarin speakers
5 French fr-FR Ribatet (external peer reviewer) + Quebec users (fr-FR ≈ fr-CA for UI strings)
6 German de-DE Ally + active dam/civil engineering community
7 Portuguese (Brazil) pt-BR Strategic Brazil/LatAm reach
8 Italian it-IT Italian statisticians cited in domain
9 Japanese ja-JP World-class tsunami/dam engineering research; ally
10 Swedish sv-SE Nordic ally; AvalonDock ships it free

Result: 9 of 10 get AvalonDock localization for free (Dutch via the close-cousin nl-BE bundle). Only the framework's own strings need translation.

Answers to the User's 3 Questions

1. Is this possible?

Yes, fully. WPF supports runtime UI culture changes. Because the framework already uses DynamicResource heavily (for theme brushes), the same mechanism swaps localized strings: replace hardcoded Header="File" with Header="{DynamicResource Strings.Menu.File}", then swap a ResourceDictionary at runtime. No app restart required.

The cleanest approach is mirror the theme architecture exactly — same shape, same patterns, same persistence layer. This minimizes architectural surface area and lets the same patterns reviewers already know carry forward.

2. How much effort is it?

Medium — roughly 5–8 developer-weeks for the locked 10 languages, broken down:

Phase Effort What
Infrastructure 1–2 weeks Language enum, services, markup extension, settings wiring
String extraction 1–2 weeks Find/replace ~1,300 XAML strings + ~200 code-behind strings, build base EnglishStrings.xaml dictionary with keys (per-library: FrameworkUI, DatabaseControls, OxyPlotControls, GenericControls, etc.)
Translation (LLM-assisted) ~1 week LLM-produces all 9 non-English dictionaries from canonical English file. Most time spent on QA/glossary, not raw translation
Native-speaker review pass ~1 week Especially critical for Dutch (high English fluency) and Chinese (technical-term variance). Other 7 languages can ship LLM-only with a glossary review
QA / visual review ~1 week Verify each dialog renders correctly in each language. German + Dutch are longest-text — watch for clipped buttons/labels

Vendor code: AvalonDock already ships translations for 9 of the 10 selected languages — zero work. OxyPlot has 1 hardcoded string — negligible.

Per-language additional cost is small once infrastructure exists — adding an 11th language post-launch is mostly translation work + a one-line enum entry.

3. How should we design it?

Mirror the theme architecture, with one twist: pipe Thread.CurrentUICulture so AvalonDock's existing .resx-based localization comes along for the ride.


Recommended Architecture

Core library — src/Themes/Core/ (extend existing) or new src/Languages/Core/

Mirror exactly:

Theme system Language system
Theme enum (Light/Blue/Dark) Language enum — English, Spanish, Dutch, ChineseSimplified, French, German, PortugueseBrazil, Italian, Japanese, Swedish
ThemeService (singleton) LanguageService (singleton)
ThemeResourceHelper (URI constants) LanguageResourceHelper (URI constants + culture codes — see locale column above)
ThemeChanged event LanguageChanged event
MergedDictionaries swap on app resources MergedDictionaries swap on app resources (same mechanism!)

LanguageService.SetLanguage(Language):

  1. Remove old strings dictionary from Application.Current.Resources.MergedDictionaries
  2. Add new strings dictionary
  3. Set Thread.CurrentThread.CurrentUICulture = newCulture (this makes AvalonDock's .resx files pick up the new language automatically)
  4. Update FrameworkElement.LanguageProperty (already done in App.xaml.cs:47-51 for number formatting — extend it)
  5. Raise LanguageChanged event

FrameworkUI facade — src/FrameworkUI/Languages/

ThemeManager (static facade) LanguageManager (static facade)
ThemeColor enum UILanguage enum

Same shape as ThemeManager. Subscribers can hook LanguageChanged if they need to refresh non-string content (e.g., re-rendered icons or auto-generated text).

Per-language string dictionaries

src/Languages/Resources/Strings/EnglishStrings.xaml (and Spanish*, French*, etc.) — same structure as LightColors.xaml:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:sys="clr-namespace:System;assembly=mscorlib">
    <sys:String x:Key="Strings.Menu.File">File</sys:String>
    <sys:String x:Key="Strings.Menu.New">New Project...</sys:String>
    <sys:String x:Key="Strings.Dialog.ConfirmDelete">Are you sure you would like to delete the selected project elements? This action is permanent.</sys:String>
    ...
</ResourceDictionary>

Each language file uses identical x:Key names (same convention as the theme color files). English is the canonical/default fallback.

Per-library dictionaries — exactly like themes

Yes — every consuming library/app ships its own per-language dictionaries, mirroring how each control library has its own theme dictionary today (e.g., FrameworkUI/Themes/VS2013/{Light,Blue,Dark}Theme.xaml, AvalonDock's Generic.xaml, etc.).

Layered structure:

Tier Theme example Language example
Core/shared Themes/Resources/Colors/LightColors.xaml (universal brushes) Languages/Resources/Strings/EnglishStrings.xaml (universal terms: OK, Cancel, Yes, No, File, Open...)
Per-library FrameworkUI/Themes/VS2013/LightTheme.xaml FrameworkUI/Languages/EnglishStrings.xaml
Per-library (DatabaseControls would have its own theme dict if needed) DatabaseControls/Languages/EnglishStrings.xaml
Per-library (OxyPlotControls) OxyPlotControls/Languages/EnglishStrings.xaml
Per-app (apps add their own theme dict if they have custom controls) MyApp/Languages/EnglishStrings.xaml

Each library gets its own keyspace prefix to avoid collisions (Strings.FrameworkUI.Menu.File, Strings.OxyPlotControls.Toolbar.Zoom, Strings.DatabaseControls.Stats.Title).

Registration pattern — libraries register their dictionary set with LanguageService at startup:

// In a library's module init or App.xaml.cs OnStartup
LanguageService.Instance.RegisterStringSet(
    setKey: "DatabaseControls",
    dictionariesByLanguage: new Dictionary<Language, Uri>
    {
        [Language.English] = new Uri("pack://application:,,,/DatabaseControls;component/Languages/EnglishStrings.xaml"),
        [Language.Spanish] = new Uri("pack://application:,,,/DatabaseControls;component/Languages/SpanishStrings.xaml"),
        // ...
    });

When SetLanguage(Language.Spanish) is called, LanguageService walks every registered set, removes the current dictionary, and adds the Spanish one. Adding a new control library to a downstream app is just one RegisterStringSet call — exact mirror of how themes work today.

This is also how end-user apps add their own app-specific strings (custom dialog titles, menu items, etc.) without modifying the framework.

XAML usage

Replace literals with DynamicResource:

<!-- before -->
<MenuItem Header="File">

<!-- after -->
<MenuItem Header="{DynamicResource Strings.Menu.File}">

Optional convenience: add a {loc:String Strings.Menu.File} markup extension wrapping DynamicResource for terser syntax later.

Code-behind usage

Add a small helper:

public static class LocalizedStrings
{
    public static string Get(string key) =>
        Application.Current?.TryFindResource(key) as string ?? key;

    public static string Format(string key, params object[] args) =>
        string.Format(Get(key), args);
}

Replace MessageBox.Show("Are you sure...") with MessageBox.Show(LocalizedStrings.Get("Strings.Dialog.ConfirmDelete")).

OptionsDialog wiring

Mirror GeneralOptions.xaml:34-36 (the existing theme combo) — add a sibling LanguageComboBox:

<ComboBox x:Name="LanguageComboBox"
          ItemsSource="{Binding LanguageList}"
          SelectedValue="{Binding Language, ...}"/>

OptionsDialog.xaml.cs Apply handler calls LanguageManager.SetLanguage(...) right next to the existing ThemeManager.SetTheme(...) call.

Persistence

Add one line to UserSettings.cs next to ColorTheme:

public static string Language { get; set; } = "English";

Already saved/loaded by the same XML mechanism — no changes to persistence code.

Startup

App.xaml.cs Application_Startup: add LanguageManager.SetLanguage(parsed) next to the existing ThemeManager.SetTheme(...) call (line ~105). Defaults to system culture or English.


Critical Files to Create / Modify

Create — core (one-time):

  • src/Languages/Core/Language.cs — enum
  • src/Languages/Core/LanguageService.cs — singleton with RegisterStringSet + SetLanguage
  • src/Languages/Core/LanguageResourceHelper.cs — URI / culture-code constants
  • src/Languages/Resources/Strings/{English,Spanish,Dutch,ChineseSimplified,French,German,PortugueseBrazil,Italian,Japanese,Swedish}Strings.xaml — universal/shared terms (10 files)
  • src/FrameworkUI/Languages/LanguageManager.cs — facade
  • src/FrameworkUI/Languages/UILanguage.cs — enum
  • src/FrameworkUI/Languages/LocalizedStrings.cs — code-behind helper
  • (Optional) src/FrameworkUI/Languages/LocStringExtension.cs{loc:String Key} markup extension

Create — per-library (one set per consuming library, mirroring theme dictionaries):

  • src/FrameworkUI/Languages/{10 language}Strings.xaml + RegisterStringSet call at startup
  • src/DatabaseControls/Languages/{10 language}Strings.xaml + registration
  • src/OxyPlotControls/Languages/{10 language}Strings.xaml + registration
  • src/GenericControls/Languages/{10 language}Strings.xaml + registration
  • (Same pattern for any other control library: NumericControls, etc.)
  • Downstream apps would create their own MyApp/Languages/{10 language}Strings.xaml for app-specific text

Modify:

  • src/FrameworkUI/User Settings/UserSettings.cs — add Language property (~line 31)
  • src/FrameworkUI/Tools Menu/Options/GeneralOptions.xaml(.cs) — add language ComboBox + DependencyProperty
  • src/FrameworkUI/Tools Menu/Options/OptionsDialog.xaml.cs — call LanguageManager.SetLanguage() in Apply handler (~line 138)
  • src/FrameworkUI.Demo/App.xaml.cs — call LanguageManager.SetLanguage() at startup (~line 105)
  • src/FrameworkUI/Main Window/MainWindow.xaml.cs — subscribe to LanguageChanged (next to existing ThemeChanged at ~line 95) for any non-resource refresh logic
  • All XAML/code with hardcoded strings — extraction work

Existing Utilities to Reuse

  • Themes/Core/ThemeService.cs — copy/adapt the singleton initialization, MergedDictionaries swap, and event-raising pattern verbatim.
  • UserSettings XML save/load — already atomic (temp-file + rename); just add the Language property.
  • App.xaml.cs:47-51 already sets FrameworkElement.LanguageProperty = CultureInfo.CurrentCulture.IetfLanguageTag for number formatting — generalize to react to language changes.
  • AvalonDock's existing .resx localization — works automatically when Thread.CurrentUICulture is set; do not duplicate.

Translation Tooling — Do You Need an External Service?

No — I (the assistant) can produce the translations directly as part of the work. UI strings are mostly short, idiomatic phrases ("File", "Save As...", "Are you sure?", "Connection failed"), which modern LLMs translate at near-professional quality for the major European languages (ES/FR/NL/DE/IT/PT/etc.).

Tradeoffs across the realistic options:

Option Cost Speed Quality Best for
LLM (me) Free Hours ~95% pro for short UI strings; weaker on highly technical terms Initial pass for all languages, especially during infrastructure rollout
DeepL / Google Translate API Cheap Minutes Solid but more literal; less context-aware Bulk machine baseline if you don't want LLM-in-the-loop
Professional human translation $$ ($1.5–3k/lang for ~1.5k strings) Weeks Gold standard Final polish for shipped product, especially for languages outside your team's reading ability
Native-speaker engineer review Internal time Days Catches dialect/register issues Recommended pass on top of any of the above

Recommended workflow:

  1. Build out EnglishStrings.xaml with all keys (the canonical source).
  2. Hand me the English .xaml file; I produce SpanishStrings.xaml, FrenchStrings.xaml, etc. directly. Output format matches input — same x:Key values, same XML structure, just translated <sys:String> content.
  3. Have a native speaker (or pro translator) review the longest dialog texts and any domain-specific terms (database, charting jargon).
  4. For specialized terminology, maintain a glossary file (e.g., LocalizationGlossary.md) that pins canonical translations of recurring terms ("project", "database connection", "axis", "series") so revisions stay consistent.
  5. Translation memory: keep the source EnglishStrings.xaml as the single source of truth. When new strings are added, only the new keys need re-translation — existing translations stay stable.

No external converter required, but if you prefer bulk machine output as a baseline, DeepL has the best quality among the API services for the European languages you mentioned.

Phased Rollout Recommendation

  1. Infrastructure-only first. Land the services, enums, settings, OptionsDialog wiring, with English as the only language. Verify hot-swap works by adding a temporary stub "TestLanguage" with a few translated keys. Get the architecture reviewed before mass extraction.
  2. Extract strings region by region. Start with MainWindow.xaml (highest visibility), then dialog-by-dialog. Each region is an independently shippable PR. Build out per-library English dictionaries in parallel.
  3. Translation rollout — recommended order:
    1. Spanish first — large speaker base, similar string lengths to English, low layout risk. Validates the translation pipeline.
    2. German second — long words stress-test the layout (catches clipped buttons/labels early, before more translations pile on).
    3. Dutch + French + Italian + Portuguese (BR) — Latin/Germanic batch, similar handling.
    4. Chinese (zh-Hans) + Japanese + Swedish — different script / typography surface; handle as a final batch with extra QA on font rendering.
  4. Translation memory: treat EnglishStrings.xaml (per library) as the single source of truth. New strings added later only require translating the diff, not re-translating everything.

Risks / Open Questions

  • Layout breakage in long-text languages — German and Dutch are the main risks; Swedish and Finnish-style compound words are also long. Many fixed-width buttons and labels will need MinWidth audits.
  • CJK typography — Chinese and Japanese rendering may need font-family fallbacks (default WPF font may render poorly for some glyphs). Test with the actual selected font on a Windows install without CJK fonts pre-installed.
  • Right-to-left languages (Arabic, Hebrew) — not in current scope. If added later, requires FlowDirection="RightToLeft" plumbing through the entire UI.
  • Pluralization ("1 file" vs "5 files") — string.Format covers most cases; dedicated pluralization libraries (e.g., SmartFormat) are overkill for current scope.
  • Strings in vendored OxyPlot — only 1 string; leave English or add to its own dictionary if/when more accumulate.
  • fr-CA fidelity — fr-FR ships first; if Quebec users push back on idioms, add a thin fr-CA overlay later (only the strings that differ — not a full re-translation).
  • High-stakes audiences — Dutch (high English fluency, will judge translation quality harshly) and French (Ribatet is an external peer reviewer evaluating the software). Both deserve extra native-speaker review.

Verification

End-to-end test once infrastructure lands:

  1. Run FrameworkUI.Demo.
  2. Open Tools → Options, switch language ComboBox from English to (test) Spanish, click Apply.
  3. Verify menus, toolbars, and any open dialogs update without restart.
  4. Open a fresh dialog (e.g., About) — verify it uses the new language.
  5. Open an AvalonDock floating window's context menu — verify it picks up Spanish from AvalonDock's bundled .resx.
  6. Close and reopen the app — verify language persists via UserSettings.
  7. Compare against the same flow for theme switching — they should feel identical.

Unit test target: LanguageService.SetLanguage() raises LanguageChanged and updates Application.Current.Resources.MergedDictionaries correctly.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Fields

No fields configured for Feature.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions