mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
Refactored hotkey management to improve thread safety by replacing `Dictionary` with `ConcurrentDictionary` and introducing a lock for updates. Added new methods for hotkey initialization, validation, and translation updates. Enhanced plugin initialization by integrating hotkey checks and removing redundant calls. Refactored action keyword management with new methods for adding and removing keywords, ensuring thread safety with `TryAdd`, `AddOrUpdate`, and `TryRemove`. Introduced the `PluginHotkeyChangedEvent` class to encapsulate hotkey change events. Improved thread safety and performance by using thread-safe collections and LINQ. Cleaned up code by removing deprecated methods, redundant comments, and outdated TODOs. Fixed concurrency issues and ensured consistent updates to plugin metadata and hotkey information.
410 lines
14 KiB
C#
410 lines
14 KiB
C#
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<string> _languageDirectories = [];
|
|
private readonly List<ResourceDictionary> _oldResources = [];
|
|
private static string SystemLanguageCode;
|
|
private readonly SemaphoreSlim _langChangeLock = new(1, 1);
|
|
|
|
public Internationalization(Settings settings)
|
|
{
|
|
_settings = settings;
|
|
}
|
|
|
|
#region Initialization
|
|
|
|
/// <summary>
|
|
/// Initialize the system language code based on the current culture.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initialize language. Will change app language and plugin language based on settings.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Change language during runtime. Will change app language and plugin language & save settings.
|
|
/// </summary>
|
|
/// <param name="languageCode"></param>
|
|
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<Language> 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 & plugin hotkey 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();
|
|
PluginManager.UpdatePluginHotkeyInfoTranslations(p);
|
|
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 & plugin hotkey name & description
|
|
if (p.Plugin is not IPluginI18n pluginI18N) return;
|
|
try
|
|
{
|
|
p.Metadata.Name = pluginI18N.GetTranslatedPluginTitle();
|
|
p.Metadata.Description = pluginI18N.GetTranslatedPluginDescription();
|
|
PluginManager.UpdatePluginHotkeyInfoTranslations(p);
|
|
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
|
|
}
|
|
}
|