using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.Resource { public class Internationalization : IDisposable { private static readonly string ClassName = nameof(Internationalization); private const string Folder = "Languages"; private const string DefaultLanguageCode = "en"; private const string DefaultFile = "en.xaml"; private const string Extension = ".xaml"; private readonly Settings _settings; private readonly List _languageDirectories = []; private readonly List _oldResources = []; private static string SystemLanguageCode; private readonly SemaphoreSlim _langChangeLock = new(1, 1); public Internationalization(Settings settings) { _settings = settings; } #region Initialization /// /// Initialize the system language code based on the current culture. /// public static void InitSystemLanguageCode() { var availableLanguages = AvailableLanguages.GetAvailableLanguages(); // Retrieve the language identifiers for the current culture. // ChangeLanguage method overrides the CultureInfo.CurrentCulture, so this needs to // be called at startup in order to get the correct lang code of system. var currentCulture = CultureInfo.CurrentCulture; var twoLetterCode = currentCulture.TwoLetterISOLanguageName; var threeLetterCode = currentCulture.ThreeLetterISOLanguageName; var fullName = currentCulture.Name; // Try to find a match in the available languages list foreach (var language in availableLanguages) { var languageCode = language.LanguageCode; if (string.Equals(languageCode, twoLetterCode, StringComparison.OrdinalIgnoreCase) || string.Equals(languageCode, threeLetterCode, StringComparison.OrdinalIgnoreCase) || string.Equals(languageCode, fullName, StringComparison.OrdinalIgnoreCase)) { SystemLanguageCode = languageCode; } } SystemLanguageCode = DefaultLanguageCode; } /// /// Initialize language. Will change app language and plugin language based on settings. /// public async Task InitializeLanguageAsync() { // Get actual language var languageCode = _settings.Language; if (languageCode == Constant.SystemLanguageCode) { languageCode = SystemLanguageCode; } // Get language by language code and change language var language = GetLanguageByLanguageCode(languageCode); // Add Flow Launcher language directory AddFlowLauncherLanguageDirectory(); // Add plugin language directories first so that we can load language files from plugins AddPluginLanguageDirectories(); // Load default language resources LoadDefaultLanguage(); // Change language await ChangeLanguageAsync(language, false); } private void AddFlowLauncherLanguageDirectory() { // Check if Flow Launcher language directory exists var directory = Path.Combine(Constant.ProgramDirectory, Folder); if (!Directory.Exists(directory)) { PublicApi.Instance.LogError(ClassName, $"Flow Launcher language directory can't be found <{directory}>"); return; } _languageDirectories.Add(directory); } private void AddPluginLanguageDirectories() { foreach (var pluginsDir in PluginManager.Directories) { if (!Directory.Exists(pluginsDir)) continue; // Enumerate all top directories in the plugin directory foreach (var dir in Directory.GetDirectories(pluginsDir)) { // Check if the directory contains a language folder var pluginLanguageDir = Path.Combine(dir, Folder); if (!Directory.Exists(pluginLanguageDir)) continue; // Check if the language directory contains default language file since it will be checked later _languageDirectories.Add(pluginLanguageDir); } } } private void LoadDefaultLanguage() { // Removes language files loaded before any plugins were loaded. // Prevents the language Flow started in from overwriting English if the user switches back to English RemoveOldLanguageFiles(); LoadLanguage(AvailableLanguages.English); _oldResources.Clear(); } #endregion #region Change Language /// /// Change language during runtime. Will change app language and plugin language & save settings. /// /// public void ChangeLanguage(string languageCode) { languageCode = languageCode.NonNull(); // Get actual language if language code is system var isSystem = false; if (languageCode == Constant.SystemLanguageCode) { languageCode = SystemLanguageCode; isSystem = true; } // Get language by language code and change language var language = GetLanguageByLanguageCode(languageCode); // Change language _ = ChangeLanguageAsync(language); // Save settings _settings.Language = isSystem ? Constant.SystemLanguageCode : language.LanguageCode; } private static Language GetLanguageByLanguageCode(string languageCode) { var language = AvailableLanguages.GetAvailableLanguages(). FirstOrDefault(o => o.LanguageCode.Equals(languageCode, StringComparison.OrdinalIgnoreCase)); if (language == null) { PublicApi.Instance.LogError(ClassName, $"Language code can't be found <{languageCode}>"); return AvailableLanguages.English; } else { return language; } } private async Task ChangeLanguageAsync(Language language, bool updateMetadata = true) { await _langChangeLock.WaitAsync(); try { // Remove old language files and load language RemoveOldLanguageFiles(); if (language != AvailableLanguages.English) { LoadLanguage(language); } // Change culture info ChangeCultureInfo(language.LanguageCode); if (updateMetadata) { // Raise event for plugins after culture is set await Task.Run(UpdatePluginMetadataTranslations); } } catch (Exception e) { PublicApi.Instance.LogException(ClassName, $"Failed to change language to <{language.LanguageCode}>", e); } finally { _langChangeLock.Release(); } } public static void ChangeCultureInfo(string languageCode) { // Culture of main thread // Use CreateSpecificCulture to preserve possible user-override settings in Windows, if Flow's language culture is the same as Windows's CultureInfo currentCulture; try { currentCulture = CultureInfo.CreateSpecificCulture(languageCode); } catch (CultureNotFoundException) { currentCulture = CultureInfo.CreateSpecificCulture(SystemLanguageCode); } CultureInfo.CurrentCulture = currentCulture; CultureInfo.CurrentUICulture = currentCulture; var thread = Thread.CurrentThread; thread.CurrentCulture = currentCulture; thread.CurrentUICulture = currentCulture; } #endregion #region Prompt Pinyin public bool PromptShouldUsePinyin(string languageCodeToSet) { var languageToSet = GetLanguageByLanguageCode(languageCodeToSet); if (_settings.ShouldUsePinyin) return false; if (languageToSet != AvailableLanguages.Chinese && languageToSet != AvailableLanguages.Chinese_TW) return false; // No other languages should show the following text so just make it hard-coded // "Do you want to search with pinyin?" string text = languageToSet == AvailableLanguages.Chinese ? "是否启用拼音搜索?" : "是否啓用拼音搜索?"; if (PublicApi.Instance.ShowMsgBox(text, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No) return false; return true; } #endregion #region Language Resources Management private void RemoveOldLanguageFiles() { var dicts = Application.Current.Resources.MergedDictionaries; foreach (var r in _oldResources) { dicts.Remove(r); } _oldResources.Clear(); } private void LoadLanguage(Language language) { var flowEnglishFile = Path.Combine(Constant.ProgramDirectory, Folder, DefaultFile); var dicts = Application.Current.Resources.MergedDictionaries; var filename = $"{language.LanguageCode}{Extension}"; var files = _languageDirectories .Select(d => LanguageFile(d, filename)) // Exclude Flow's English language file since it's built into the binary, and there's no need to load // it again from the file system. .Where(f => !string.IsNullOrEmpty(f) && f != flowEnglishFile) .ToArray(); if (files.Length > 0) { foreach (var f in files) { var r = new ResourceDictionary { Source = new Uri(f, UriKind.Absolute) }; dicts.Add(r); _oldResources.Add(r); } } } private static string LanguageFile(string folder, string language) { if (Directory.Exists(folder)) { var path = Path.Combine(folder, language); if (File.Exists(path)) { return path; } else { PublicApi.Instance.LogError(ClassName, $"Language path can't be found <{path}>"); var english = Path.Combine(folder, DefaultFile); if (File.Exists(english)) { return english; } else { PublicApi.Instance.LogError(ClassName, $"Default English Language path can't be found <{path}>"); return string.Empty; } } } else { return string.Empty; } } #endregion #region Available Languages public List LoadAvailableLanguages() { var list = AvailableLanguages.GetAvailableLanguages(); list.Insert(0, new Language(Constant.SystemLanguageCode, AvailableLanguages.GetSystemTranslation(SystemLanguageCode))); return list; } #endregion #region Get Translations public static string GetTranslation(string key) { var translation = Application.Current.TryFindResource(key); if (translation is string) { return translation.ToString(); } else { PublicApi.Instance.LogError(ClassName, $"No Translation for key {key}"); return $"No Translation for key {key}"; } } #endregion #region Update Metadata public static void UpdatePluginMetadataTranslations() { // Update plugin metadata name & description foreach (var p in PluginManager.GetTranslationPlugins()) { if (p.Plugin is not IPluginI18n pluginI18N) return; try { p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle(); p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription(); pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture); } catch (Exception e) { PublicApi.Instance.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e); } } } public static void UpdatePluginMetadataTranslation(PluginPair p) { // Update plugin metadata name & description if (p.Plugin is not IPluginI18n pluginI18N) return; try { p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle(); p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription(); pluginI18N.OnCultureInfoChanged(CultureInfo.CurrentCulture); } catch (Exception e) { PublicApi.Instance.LogException(ClassName, $"Failed for <{p.Metadata.Name}>", e); } } #endregion #region IDisposable public void Dispose() { RemoveOldLanguageFiles(); _langChangeLock.Dispose(); } #endregion } }