361 lines
14 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|