using System; using System.IO; using System.Windows; namespace MmdPdf.Services { internal enum Locale { EnUS, Es, ZhTW, ZhCN, Bn, TrTR, De, Fr, JaJP, CsCZ, PlPL, HuHU } internal static class LocaleManager { private static Locale _current = Locale.EnUS; public static Locale Current => _current; // ── Translator test mode (#211, thanks bovirus) ───────────────────────── // --lang-file loads an external translation xaml as the language override, winning // over the saved locale, and re-applies it on every save of the file - so a translation // can be checked for string length and context in the live app before it is built in. // Untranslated keys fall back to the en-US base like any partial locale. /// Full path of the external translation file, or null. Set from the /// --lang-file switch before runs. internal static string? ExternalFile; /// Raised on the UI thread after a successful live re-apply, so the window can /// rebuild its code-built captions (toolbar, context menu) the way a language switch does. internal static event Action? ExternalReloaded; private static FileSystemWatcher? _watcher; private static System.Windows.Threading.DispatcherTimer? _reloadDebounce; private static bool _externalLoadedOnce; /// /// Call once at startup (after ThemeManager.Initialize) to restore the saved locale. /// public static void Initialize() { var saved = App.GetSetting("Locale"); _current = Enum.TryParse(saved, out var l) ? l : Locale.EnUS; ApplyInternal(_current); } /// /// Switch locale, persist choice, and hot-swap the string ResourceDictionary. /// public static void Apply(Locale locale) { _current = locale; App.SetSetting("Locale", locale.ToString()); ApplyInternal(locale); } // ── Internal ───────────────────────────────────────────────────── private static void ApplyInternal(Locale locale) { var merged = Application.Current.Resources.MergedDictionaries; // [0] theme. [1] en-US BASE - always present so any partial locale falls back to English for // keys it doesn't translate. [2] the chosen locale's overrides (absent for English). if (merged.Count > 1) merged[1] = new ResourceDictionary { Source = new Uri("pack://application:,,,/Strings/en-US.xaml") }; // Translator test mode wins over the chosen locale for the override slot. if (ExternalFile is not null) { if (TryApplyExternal()) { EnsureWatcher(); return; } if (!_externalLoadedOnce) MessageBox.Show($"Could not load the translation file:\n{ExternalFile}\n\nCheck the path and the file's XML, then start MmdPdf again.", "MmdPdf --lang-file", MessageBoxButton.OK, MessageBoxImage.Warning); // Fall through to the normal locale so the app still comes up usable. } Uri? overrideUri = locale switch { Locale.Es => new Uri("pack://application:,,,/Strings/es.xaml"), Locale.Fr => new Uri("pack://application:,,,/Strings/fr-FR.xaml"), Locale.ZhTW => new Uri("pack://application:,,,/Strings/zh-TW.xaml"), Locale.ZhCN => new Uri("pack://application:,,,/Strings/zh-CN.xaml"), Locale.Bn => new Uri("pack://application:,,,/Strings/bn.xaml"), Locale.TrTR => new Uri("pack://application:,,,/Strings/tr-TR.xaml"), Locale.De => new Uri("pack://application:,,,/Strings/de-DE.xaml"), Locale.JaJP => new Uri("pack://application:,,,/Strings/ja-JP.xaml"), Locale.CsCZ => new Uri("pack://application:,,,/Strings/cs-CZ.xaml"), Locale.PlPL => new Uri("pack://application:,,,/Strings/pl-PL.xaml"), Locale.HuHU => new Uri("pack://application:,,,/Strings/hu-HU.xaml"), _ => null, // English: base only }; if (overrideUri is not null) { var ov = new ResourceDictionary { Source = overrideUri }; if (merged.Count > 2) merged[2] = ov; else merged.Add(ov); } else if (merged.Count > 2) { merged.RemoveAt(2); } } /// Loads into the override slot. False on any parse or /// IO failure - during live reload the last good version simply stays applied, since a /// text editor mid-save routinely produces momentarily unreadable or invalid XML. private static bool TryApplyExternal() { try { using var fs = new FileStream(ExternalFile!, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); if (System.Windows.Markup.XamlReader.Load(fs) is not ResourceDictionary rd) return false; var merged = Application.Current.Resources.MergedDictionaries; if (merged.Count > 2) merged[2] = rd; else merged.Add(rd); _externalLoadedOnce = true; return true; } catch { return false; } } private static void EnsureWatcher() { if (_watcher is not null) return; string dir = Path.GetDirectoryName(ExternalFile!) is { Length: > 0 } d ? d : "."; string name = Path.GetFileName(ExternalFile!); _watcher = new FileSystemWatcher(dir, name) { EnableRaisingEvents = true }; // Editors save as write, replace, or delete-and-rename depending on the editor, so // watch every shape. Events arrive on a worker thread and usually several per save - // marshal to the UI thread and debounce into one re-apply. _watcher.Changed += (_, _) => QueueExternalReload(); _watcher.Created += (_, _) => QueueExternalReload(); _watcher.Renamed += (_, _) => QueueExternalReload(); } private static void QueueExternalReload() { Application.Current?.Dispatcher.BeginInvoke((Action)(() => { _reloadDebounce ??= NewDebounce(); _reloadDebounce.Stop(); _reloadDebounce.Start(); })); } private static System.Windows.Threading.DispatcherTimer NewDebounce() { var t = new System.Windows.Threading.DispatcherTimer { Interval = TimeSpan.FromMilliseconds(300) }; t.Tick += (_, _) => { t.Stop(); if (TryApplyExternal()) ExternalReloaded?.Invoke(); }; return t; } } }