diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 5b3419041a6..0fea6d9ab24 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -103,3 +103,4 @@ Reloadable metadatas WMP VSTHRD +CJK diff --git a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs index b5344c7e914..cc4eccdc52c 100644 --- a/Flow.Launcher.Infrastructure/PinyinAlphabet.cs +++ b/Flow.Launcher.Infrastructure/PinyinAlphabet.cs @@ -14,11 +14,8 @@ namespace Flow.Launcher.Infrastructure { public class PinyinAlphabet : IAlphabet { - private ConcurrentDictionary _pinyinCache = - new(); - + private readonly ConcurrentDictionary _pinyinCache = new(); private readonly Settings _settings; - private ReadOnlyDictionary currentDoublePinyinTable; public PinyinAlphabet() @@ -44,105 +41,142 @@ public void Reload() private void CreateDoublePinyinTableFromStream(Stream jsonStream) { - Dictionary> table = JsonSerializer.Deserialize>>(jsonStream); - string schemaKey = _settings.DoublePinyinSchema.ToString(); // Convert enum to string - if (!table.TryGetValue(schemaKey, out var value)) + var table = JsonSerializer.Deserialize>>(jsonStream) ?? + throw new InvalidOperationException("Failed to deserialize double pinyin table: result is null"); + + var schemaKey = _settings.DoublePinyinSchema.ToString(); + if (!table.TryGetValue(schemaKey, out var schemaDict)) { - throw new ArgumentException("DoublePinyinSchema is invalid or double pinyin table is broken."); + throw new ArgumentException($"DoublePinyinSchema '{schemaKey}' is invalid or double pinyin table is broken."); } - currentDoublePinyinTable = new ReadOnlyDictionary(value); + + currentDoublePinyinTable = new ReadOnlyDictionary(schemaDict); } private void LoadDoublePinyinTable() { - if (_settings.UseDoublePinyin) + if (!_settings.UseDoublePinyin) { - var tablePath = Path.Join(AppContext.BaseDirectory, "Resources", "double_pinyin.json"); - try - { - using var fs = File.OpenRead(tablePath); - CreateDoublePinyinTableFromStream(fs); - } - catch (System.Exception e) - { - Log.Exception(nameof(PinyinAlphabet), "Failed to load double pinyin table from file: " + tablePath, e); - currentDoublePinyinTable = new ReadOnlyDictionary(new Dictionary()); - } + currentDoublePinyinTable = new ReadOnlyDictionary(new Dictionary()); + return; + } + + var tablePath = Path.Combine(AppContext.BaseDirectory, "Resources", "double_pinyin.json"); + try + { + using var fs = File.OpenRead(tablePath); + CreateDoublePinyinTableFromStream(fs); + } + catch (FileNotFoundException e) + { + Log.Exception(nameof(PinyinAlphabet), $"Double pinyin table file not found: {tablePath}", e); + currentDoublePinyinTable = new ReadOnlyDictionary(new Dictionary()); } - else + catch (DirectoryNotFoundException e) { + Log.Exception(nameof(PinyinAlphabet), $"Directory not found for double pinyin table: {tablePath}", e); + currentDoublePinyinTable = new ReadOnlyDictionary(new Dictionary()); + } + catch (UnauthorizedAccessException e) + { + Log.Exception(nameof(PinyinAlphabet), $"Access denied to double pinyin table: {tablePath}", e); + currentDoublePinyinTable = new ReadOnlyDictionary(new Dictionary()); + } + catch (System.Exception e) + { + Log.Exception(nameof(PinyinAlphabet), $"Failed to load double pinyin table from file: {tablePath}", e); currentDoublePinyinTable = new ReadOnlyDictionary(new Dictionary()); } } public bool ShouldTranslate(string stringToTranslate) { - // If a string has Chinese characters, we don't need to translate it to pinyin. - return _settings.ShouldUsePinyin && !WordsHelper.HasChinese(stringToTranslate); + // If the query (stringToTranslate) does NOT contain Chinese characters, + // we should translate the target string to pinyin for matching + return _settings.ShouldUsePinyin && !ContainsChinese(stringToTranslate); } public (string translation, TranslationMapping map) Translate(string content) { - if (!_settings.ShouldUsePinyin || !WordsHelper.HasChinese(content)) + if (!_settings.ShouldUsePinyin || !ContainsChinese(content)) return (content, null); - return _pinyinCache.TryGetValue(content, out var value) - ? value - : BuildCacheFromContent(content); + return _pinyinCache.TryGetValue(content, out var cached) ? cached : BuildCacheFromContent(content); } private (string translation, TranslationMapping map) BuildCacheFromContent(string content) { var resultList = WordsHelper.GetPinyinList(content); - - var resultBuilder = new StringBuilder(); + var resultBuilder = new StringBuilder(_settings.UseDoublePinyin ? 3 : 4); // Pre-allocate with estimated capacity var map = new TranslationMapping(); var previousIsChinese = false; for (var i = 0; i < resultList.Length; i++) { - if (content[i] >= 0x3400 && content[i] <= 0x9FD5) + if (IsChineseCharacter(content[i])) { - string translated = _settings.UseDoublePinyin ? ToDoublePin(resultList[i]) : resultList[i]; + var translated = _settings.UseDoublePinyin ? ToDoublePinyin(resultList[i]) : resultList[i]; + if (i > 0) { resultBuilder.Append(' '); } + map.AddNewIndex(resultBuilder.Length, translated.Length); resultBuilder.Append(translated); previousIsChinese = true; } else { + // Add space after Chinese characters before non-Chinese characters if (previousIsChinese) { previousIsChinese = false; resultBuilder.Append(' '); } + map.AddNewIndex(resultBuilder.Length, resultList[i].Length); resultBuilder.Append(resultList[i]); } } - map.endConstruct(); + map.EndConstruct(); - var key = resultBuilder.ToString(); - - return _pinyinCache[content] = (key, map); + var translation = resultBuilder.ToString(); + var result = (translation, map); + + return _pinyinCache[content] = result; } - #region Double Pinyin - - private string ToDoublePin(string fullPinyin) + /// + /// Optimized Chinese character detection using the comprehensive CJK Unicode ranges + /// + private static bool ContainsChinese(ReadOnlySpan text) { - if (currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue)) + foreach (var c in text) { - return doublePinyinValue; + if (IsChineseCharacter(c)) + return true; } - return fullPinyin; + return false; } - #endregion + /// + /// Check if a character is a Chinese character using comprehensive Unicode ranges + /// Covers CJK Unified Ideographs, Extension A + /// + private static bool IsChineseCharacter(char c) + { + return (c >= 0x4E00 && c <= 0x9FFF) || // CJK Unified Ideographs + (c >= 0x3400 && c <= 0x4DBF); // CJK Extension A + } + + private string ToDoublePinyin(string fullPinyin) + { + return currentDoublePinyinTable.TryGetValue(fullPinyin, out var doublePinyinValue) + ? doublePinyinValue + : fullPinyin; + } } } diff --git a/Flow.Launcher.Infrastructure/TranslationMapping.cs b/Flow.Launcher.Infrastructure/TranslationMapping.cs index 5b02ae666f9..b4c6764df1a 100644 --- a/Flow.Launcher.Infrastructure/TranslationMapping.cs +++ b/Flow.Launcher.Infrastructure/TranslationMapping.cs @@ -5,31 +5,30 @@ namespace Flow.Launcher.Infrastructure { public class TranslationMapping { - private bool constructed; + private bool _isConstructed; - // Assuming one original item maps to multi translated items - // list[i] is the last translated index + 1 of original index i - private readonly List originalToTranslated = new(); + // Assuming one original item maps to multi translated items + // list[i] is the last translated index + 1 of original index i + private readonly List _originalToTranslated = new(); public void AddNewIndex(int translatedIndex, int length) { - if (constructed) - throw new InvalidOperationException("Mapping shouldn't be changed after constructed"); - - originalToTranslated.Add(translatedIndex + length); + if (_isConstructed) + throw new InvalidOperationException("Mapping shouldn't be changed after construction"); + _originalToTranslated.Add(translatedIndex + length); } public int MapToOriginalIndex(int translatedIndex) { - int loc = originalToTranslated.BinarySearch(translatedIndex); - return loc >= 0 ? loc : ~loc; + var searchResult = _originalToTranslated.BinarySearch(translatedIndex); + return searchResult >= 0 ? searchResult : ~searchResult; } - public void endConstruct() + public void EndConstruct() { - if (constructed) + if (_isConstructed) throw new InvalidOperationException("Mapping has already been constructed"); - constructed = true; + _isConstructed = true; } } } diff --git a/Flow.Launcher.Test/TranslationMappingTest.cs b/Flow.Launcher.Test/TranslationMappingTest.cs index 10d765f5ae4..bd3636f0ad8 100644 --- a/Flow.Launcher.Test/TranslationMappingTest.cs +++ b/Flow.Launcher.Test/TranslationMappingTest.cs @@ -1,4 +1,6 @@ -using Flow.Launcher.Infrastructure; +using System.Collections.Generic; +using System.Reflection; +using Flow.Launcher.Infrastructure; using NUnit.Framework; using NUnit.Framework.Legacy; @@ -34,22 +36,21 @@ public void MapToOriginalIndex_ShouldReturnExpectedIndex(int translatedIndex, in mapping.AddNewIndex(2, 2); mapping.AddNewIndex(5, 3); - var result = mapping.MapToOriginalIndex(translatedIndex); ClassicAssert.AreEqual(expectedOriginalIndex, result); } - private int GetOriginalToTranslatedCount(TranslationMapping mapping) + private static int GetOriginalToTranslatedCount(TranslationMapping mapping) { - var field = typeof(TranslationMapping).GetField("originalToTranslated", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var list = (System.Collections.Generic.List)field.GetValue(mapping); + var field = typeof(TranslationMapping).GetField("_originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance); + var list = (List)field.GetValue(mapping); return list.Count; } - private int GetOriginalToTranslatedAt(TranslationMapping mapping, int index) + private static int GetOriginalToTranslatedAt(TranslationMapping mapping, int index) { - var field = typeof(TranslationMapping).GetField("originalToTranslated", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var list = (System.Collections.Generic.List)field.GetValue(mapping); + var field = typeof(TranslationMapping).GetField("_originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance); + var list = (List)field.GetValue(mapping); return list[index]; } }