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 chineseTask = LocalizationKit.LocalizeParamAsync( "l10n.hs.exp.log1.target", chinese); Task 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 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 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( "Assets/StreamingAssets/Config/params.csv"), Is.Null); } [Test] public void ExpressionCatalog_IsValidAndCaseSensitive() { ExpressionContentCatalog catalog = AssetDatabase.LoadAssetAtPath(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(); 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 { 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 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(); 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); } } }