Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/actions/spelling/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,4 @@ Reloadable
metadatas
WMP
VSTHRD
CJK
120 changes: 77 additions & 43 deletions Flow.Launcher.Infrastructure/PinyinAlphabet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,8 @@ namespace Flow.Launcher.Infrastructure
{
public class PinyinAlphabet : IAlphabet
{
private ConcurrentDictionary<string, (string translation, TranslationMapping map)> _pinyinCache =
new();

private readonly ConcurrentDictionary<string, (string translation, TranslationMapping map)> _pinyinCache = new();
private readonly Settings _settings;

private ReadOnlyDictionary<string, string> currentDoublePinyinTable;

public PinyinAlphabet()
Expand All @@ -44,105 +41,142 @@ public void Reload()

private void CreateDoublePinyinTableFromStream(Stream jsonStream)
{
Dictionary<string, Dictionary<string, string>> table = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(jsonStream);
string schemaKey = _settings.DoublePinyinSchema.ToString(); // Convert enum to string
if (!table.TryGetValue(schemaKey, out var value))
var table = JsonSerializer.Deserialize<Dictionary<string, Dictionary<string, string>>>(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<string, string>(value);

currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(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<string, string>(new Dictionary<string, string>());
}
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
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<string, string>(new Dictionary<string, string>());
}
else
catch (DirectoryNotFoundException e)
{
Log.Exception(nameof(PinyinAlphabet), $"Directory not found for double pinyin table: {tablePath}", e);
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
}
catch (UnauthorizedAccessException e)
{
Log.Exception(nameof(PinyinAlphabet), $"Access denied to double pinyin table: {tablePath}", e);
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
}
catch (System.Exception e)
{
Log.Exception(nameof(PinyinAlphabet), $"Failed to load double pinyin table from file: {tablePath}", e);
currentDoublePinyinTable = new ReadOnlyDictionary<string, string>(new Dictionary<string, string>());
}
}

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)
/// <summary>
/// Optimized Chinese character detection using the comprehensive CJK Unicode ranges
/// </summary>
private static bool ContainsChinese(ReadOnlySpan<char> 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
/// <summary>
/// Check if a character is a Chinese character using comprehensive Unicode ranges
/// Covers CJK Unified Ideographs, Extension A
/// </summary>
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;
}
}
}
25 changes: 12 additions & 13 deletions Flow.Launcher.Infrastructure/TranslationMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> 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<int> _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;
}
}
}
17 changes: 9 additions & 8 deletions Flow.Launcher.Test/TranslationMappingTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Flow.Launcher.Infrastructure;
using System.Collections.Generic;
using System.Reflection;
using Flow.Launcher.Infrastructure;
using NUnit.Framework;

Check warning on line 4 in Flow.Launcher.Test/TranslationMappingTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`NUnit` is not a recognized word. (unrecognized-spelling)
using NUnit.Framework.Legacy;

Check warning on line 5 in Flow.Launcher.Test/TranslationMappingTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`NUnit` is not a recognized word. (unrecognized-spelling)

Check warning on line 5 in Flow.Launcher.Test/TranslationMappingTest.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`NUnit` is not a recognized word. (unrecognized-spelling)

namespace Flow.Launcher.Test
{
Expand Down Expand Up @@ -34,22 +36,21 @@
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<int>)field.GetValue(mapping);
var field = typeof(TranslationMapping).GetField("_originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance);
var list = (List<int>)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<int>)field.GetValue(mapping);
var field = typeof(TranslationMapping).GetField("_originalToTranslated", BindingFlags.NonPublic | BindingFlags.Instance);
var list = (List<int>)field.GetValue(mapping);
return list[index];
}
}
Expand Down
Loading