Files
aibis-dream/Assets/Editor/Huoshan/ExpressionLocalizationTests.cs
T
2026-07-28 21:34:42 +08:00

361 lines
14 KiB
C#

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using AibisDream.Framework;
using AibisDream.MiniGame.Language;
using AibisDream.Utility;
using NUnit.Framework;
using UnityEditor;
using UnityEditor.Localization;
using UnityEngine;
using UnityEngine.Localization;
using UnityEngine.Localization.Settings;
using UnityEngine.Localization.Tables;
using UnityEngine.TestTools;
namespace AibisDream.EditorTests.Huoshan
{
public sealed class ExpressionLocalizationTests
{
private const string CatalogPath =
"Assets/GameContent/Feature_Huoshan/Expression/ExpressionContentCatalog.asset";
[TestCase(null, false)]
[TestCase("", false)]
[TestCase("plain", false)]
[TestCase("L10N.key", false)]
[TestCase("l10n.key", true)]
public void LocalizedParamPrefix_IsStrict(string value, bool expected)
{
Assert.That(LocalizationKit.IsLocalizedParam(value), Is.EqualTo(expected));
}
[Test]
public void LocalizedParamKey_StripsOnlyKnownPrefix()
{
Assert.That(LocalizationKit.GetL10NParamKey("l10n.hs.exp.log1.target"),
Is.EqualTo("hs.exp.log1.target"));
Assert.That(LocalizationKit.GetL10NParamKey("raw"), Is.EqualTo("raw"));
}
[Test]
public void MissingParamResult_RequiresMatchingLocalizedReference()
{
Assert.That(
LocalizationKit.IsMissingParamResult(
"l10n.hs.exp.pool",
"⟦hs.exp.pool⟧"),
Is.True);
Assert.That(
LocalizationKit.IsMissingParamResult(
"l10n.hs.exp.pool",
"⟦another.key⟧"),
Is.False);
Assert.That(
LocalizationKit.IsMissingParamResult(
"raw",
"⟦raw⟧"),
Is.False);
}
[UnityTest]
public IEnumerator LocalizeParamAsync_UsesExplicitLocaleAndDoesNotFallback()
{
Locale chinese = LocalizationEditorSettings.GetLocale(
new LocaleIdentifier("zh-Hans"));
Locale english = LocalizationEditorSettings.GetLocale(
new LocaleIdentifier("en"));
Assert.That(chinese, Is.Not.Null);
Assert.That(english, Is.Not.Null);
Locale previous = LocalizationSettings.SelectedLocale;
LocalizationSettings.SelectedLocale = english;
Task<string> chineseTask = LocalizationKit.LocalizeParamAsync(
"l10n.hs.exp.log1.target",
chinese);
Task<string> englishTask = LocalizationKit.LocalizeParamAsync(
"l10n.hs.exp.log1.target",
english);
while (!chineseTask.IsCompleted || !englishTask.IsCompleted)
yield return null;
LocalizationSettings.SelectedLocale = previous;
Assert.That(chineseTask.Result, Is.EqualTo("我就是个笑话"));
Assert.That(englishTask.Result, Is.Empty);
}
[UnityTest]
public IEnumerator LocalizeParamAsync_ReturnsRawTextAndMissingKeyMarker()
{
Task<string> rawTask = LocalizationKit.LocalizeParamAsync("raw text");
while (!rawTask.IsCompleted)
yield return null;
Assert.That(rawTask.Result, Is.EqualTo("raw text"));
Locale chinese = LocalizationEditorSettings.GetLocale(
new LocaleIdentifier("zh-Hans"));
LogAssert.Expect(
LogType.Error,
new Regex(@"Params 缺少条目.*Key=hs\.exp\.missing.*Locale=zh-Hans"));
Task<string> missingTask = LocalizationKit.LocalizeParamAsync(
"l10n.hs.exp.missing",
chinese);
while (!missingTask.IsCompleted)
yield return null;
Assert.That(missingTask.Result, Is.EqualTo("⟦hs.exp.missing⟧"));
}
[Test]
public void LegacyCommonParams_ArePresentInUnityParams()
{
StringTableCollection collection =
LocalizationEditorSettings.GetStringTableCollection(ConstRef.ParamsTable);
Assert.That(collection, Is.Not.Null);
string[] legacyKeys = { "天空", "海报", "植物", "朋友", "情绪", "记忆", "逻辑" };
foreach (string key in legacyKeys)
Assert.That(collection.SharedData.GetEntry(key), Is.Not.Null, key);
Assert.That(AssetDatabase.LoadAssetAtPath<TextAsset>(
"Assets/StreamingAssets/Config/params.csv"), Is.Null);
}
[Test]
public void ExpressionCatalog_IsValidAndCaseSensitive()
{
ExpressionContentCatalog catalog =
AssetDatabase.LoadAssetAtPath<ExpressionContentCatalog>(CatalogPath);
Assert.That(catalog, Is.Not.Null);
Assert.That(catalog.GetValidationErrors(), Is.Empty);
Assert.That(catalog.Rounds.Select(round => round.Id),
Is.EqualTo(new[] { "log1", "log2", "log3" }));
Assert.That(catalog.TryGetRound("log1", out ExpressionRoundDefinition log1), Is.True);
Assert.That(log1.NonTargetParticleCount, Is.EqualTo(16));
Assert.That(catalog.TryGetRound("LOG1", out _), Is.False);
Assert.That(catalog.TryGetRound("missing", out _), Is.False);
}
[Test]
public void ExpressionCatalog_RejectsDuplicateIdsAndNonLocalizedReferences()
{
ExpressionContentCatalog catalog =
ScriptableObject.CreateInstance<ExpressionContentCatalog>();
try
{
var first = new ExpressionRoundDefinition();
var second = new ExpressionRoundDefinition();
ConfigureRound(first, "log1", "l10n.target", "l10n.tokens", 1);
ConfigureRound(second, "log1", "raw", "l10n.tokens", 1);
SetPrivateField(
catalog,
"rounds",
new List<ExpressionRoundDefinition> { first, second });
string errors = string.Join("\n", catalog.GetValidationErrors());
Assert.That(errors, Does.Contain("重复"));
Assert.That(errors, Does.Contain("l10n.*"));
}
finally
{
Object.DestroyImmediate(catalog);
}
}
[Test]
public void TokenParser_TrimsDropsEmptyAndRejectsFullwidthSeparator()
{
Assert.That(
LanguageYarnCommand.TryParseExpressionTokens(
" 哈哈 | | 嘿嘿|呵呵 ",
out var tokens),
Is.True);
Assert.That(tokens, Is.EqualTo(new[] { "哈哈", "嘿嘿", "呵呵" }));
LogAssert.Expect(LogType.Error, new Regex("全角分隔符"));
Assert.That(
LanguageYarnCommand.TryParseExpressionTokens("哈哈|嘿嘿", out _),
Is.False);
Assert.That(
LanguageYarnCommand.TryParseExpressionTokens(" | | ", out _),
Is.False);
}
[Test]
public void CharacterPoolParser_PreservesUnicodeDuplicatesAndRejectsSeparators()
{
string combining = "e\u0301";
Assert.That(
LanguageYarnCommand.TryParseExpressionPool(
$" 中 中 あ {combining} 😀。 ",
out List<string> pool),
Is.True);
Assert.That(
pool,
Is.EqualTo(new[] { "中", "中", "あ", combining, "😀", "。" }));
Assert.That(
LanguageYarnCommand.TryParseExpressionPool("中|あ", out _),
Is.False);
Assert.That(
LanguageYarnCommand.TryParseExpressionPool("中|あ", out _),
Is.False);
Assert.That(
LanguageYarnCommand.TryParseExpressionPool(" \t ", out _),
Is.False);
}
[Test]
public void Params_ContainsLocalizedExpressPoolForEveryLocale()
{
StringTableCollection collection =
LocalizationEditorSettings.GetStringTableCollection(ConstRef.ParamsTable);
Assert.That(collection, Is.Not.Null);
string key =
LocalizationKit.GetL10NParamKey(ConstRef.ExpressParticlePoolParam);
Assert.That(collection.SharedData.GetEntry(key), Is.Not.Null);
string[] localeCodes = { "zh-Hans", "en", "ja-JP", "es", "ru", "pt-BR" };
foreach (string localeCode in localeCodes)
{
StringTable table =
collection.GetTable(new LocaleIdentifier(localeCode)) as StringTable;
Assert.That(table, Is.Not.Null, localeCode);
Assert.That(table.GetEntry(key), Is.Not.Null, localeCode);
}
StringTable chinese =
collection.GetTable(new LocaleIdentifier("zh-Hans")) as StringTable;
Assert.That(
ExpressionTextTokenizer.GetVisibleElements(
chinese.GetEntry(key).LocalizedValue),
Is.Not.Empty);
foreach (string localeCode in localeCodes.Skip(1))
{
StringTable table =
collection.GetTable(new LocaleIdentifier(localeCode)) as StringTable;
Assert.That(table.GetEntry(key).LocalizedValue, Is.Empty, localeCode);
}
}
[Test]
public void TextParticle_UsesConfiguredRoleAndOverridePoolsWithoutFirstFrameFallback()
{
var gameObject = new GameObject("TextParticlePoolTest");
try
{
CandidateParticle particle = gameObject.AddComponent<CandidateParticle>();
Assert.That(particle.currentChar, Is.Empty);
string combining = "e\u0301";
particle.SetCharacterPools(
new[] { "😀" },
new[] { combining });
particle.originalIsRed = true;
particle.InitializeRandomCharacter();
Assert.That(particle.currentChar, Is.EqualTo("😀"));
particle.originalIsRed = false;
particle.InitializeRandomCharacter();
Assert.That(particle.currentChar, Is.EqualTo(combining));
particle.SetOverrideCharacterPool(new[] { "。" });
particle.InitializeRandomCharacter();
Assert.That(particle.currentChar, Is.EqualTo("。"));
particle.SetOverrideCharacterPool(null);
particle.InitializeRandomCharacter();
Assert.That(particle.currentChar, Is.EqualTo(combining));
}
finally
{
Object.DestroyImmediate(gameObject);
}
}
[Test]
public void UnicodeTokenizer_PreservesTextElementsAndDropsWhitespace()
{
string combining = "e\u0301";
var elements =
ExpressionTextTokenizer.GetVisibleElements($"中 A あ {combining} 😀。");
Assert.That(
elements,
Is.EqualTo(new[] { "中", "A", "あ", combining, "😀", "。" }));
}
[Test]
public void UnicodeLayout_PreservesWordGapWithoutWhitespaceParticle()
{
ExpressionTextTokenizer.Layout layout =
ExpressionTextTokenizer.BuildLayout("A B", 1f, 0.6f);
Assert.That(layout.VisibleElements, Is.EqualTo(new[] { "A", "B" }));
Assert.That(layout.Offsets[0], Is.EqualTo(-0.8f).Within(0.0001f));
Assert.That(layout.Offsets[1], Is.EqualTo(0.8f).Within(0.0001f));
}
[Test]
public void UnicodeSequenceMatch_UsesWholeTextElements()
{
var source = ExpressionTextTokenizer.GetVisibleElements("A😀e\u0301。");
var fragment = ExpressionTextTokenizer.GetVisibleElements("😀e\u0301");
Assert.That(ExpressionTextTokenizer.FindVisibleSequence(source, fragment),
Is.EqualTo(1));
Assert.That(
ExpressionTextTokenizer.FindVisibleSequence(
source,
ExpressionTextTokenizer.GetVisibleElements("😀x")),
Is.EqualTo(-1));
}
[Test]
public void ExpressContentValidation_HasNoBlockingErrors()
{
var issues = ExpressionLocalizationValidator.Validate();
string errors = string.Join(
"\n",
issues
.Where(issue =>
issue.Severity == ExpressionLocalizationIssueSeverity.Error)
.Select(issue => issue.Message));
Assert.That(errors, Is.Empty);
Assert.That(
issues.Any(issue =>
issue.Severity == ExpressionLocalizationIssueSeverity.Warning),
Is.True,
"未翻译 Locale 的空条目应作为待翻译警告保留。");
}
private static void ConfigureRound(
ExpressionRoundDefinition round,
string id,
string target,
string tokens,
int nonTargetCount)
{
SetPrivateField(round, "id", id);
SetPrivateField(round, "targetReference", target);
SetPrivateField(round, "tokenReference", tokens);
SetPrivateField(round, "nonTargetParticleCount", nonTargetCount);
}
private static void SetPrivateField(object target, string fieldName, object value)
{
FieldInfo field = target.GetType().GetField(
fieldName,
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(field, Is.Not.Null, fieldName);
field.SetValue(target, value);
}
}
}