feat: 火山玩法本地化
仍有硬编码残留,待处理
This commit is contained in:
@@ -0,0 +1,292 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using AibisDream.Framework;
|
||||
using AibisDream.MiniGame.Language;
|
||||
using AibisDream.Utility;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Localization;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Localization;
|
||||
using UnityEngine.Localization.Tables;
|
||||
|
||||
namespace AibisDream.EditorTests.Huoshan
|
||||
{
|
||||
public enum ExpressionLocalizationIssueSeverity
|
||||
{
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
|
||||
public readonly struct ExpressionLocalizationIssue
|
||||
{
|
||||
public ExpressionLocalizationIssue(
|
||||
ExpressionLocalizationIssueSeverity severity,
|
||||
string message)
|
||||
{
|
||||
Severity = severity;
|
||||
Message = message;
|
||||
}
|
||||
|
||||
public ExpressionLocalizationIssueSeverity Severity { get; }
|
||||
public string Message { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验 Stage6 Express 命令、轮次 Catalog 与 Params String Table 的引用关系。
|
||||
/// 英日空值是待翻译警告,不阻断内容校验。
|
||||
/// </summary>
|
||||
public static class ExpressionLocalizationValidator
|
||||
{
|
||||
private const string Stage6Path = "Assets/Yarn/FP/FP_Huoshan1/Stage6.yarn";
|
||||
private const string CatalogPath =
|
||||
"Assets/GameContent/Feature_Huoshan/Expression/ExpressionContentCatalog.asset";
|
||||
|
||||
private static readonly Regex YarnCommandRegex =
|
||||
new(@"^\s*<<(?<command>[A-Za-z0-9_]+)(?<arguments>.*?)>>", RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex QuotedArgumentRegex =
|
||||
new("\"(?<value>(?:\\\\.|[^\"])*)\"", RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex L10NRegex =
|
||||
new(@"l10n\.(?<key>[A-Za-z0-9._-]+)", RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex ChineseRegex =
|
||||
new(@"[\u3400-\u9FFF]", RegexOptions.Compiled);
|
||||
|
||||
private static readonly Dictionary<string, int[]> VisibleArgumentIndices =
|
||||
new(StringComparer.Ordinal)
|
||||
{
|
||||
["start_expression"] = new[] { 0, 1 },
|
||||
["expression_lie_begin"] = new[] { 0 },
|
||||
["expression_truth_lie_flicker"] = new[] { 0, 1 },
|
||||
["expression_lie_morph_truth"] = new[] { 0, 1 },
|
||||
["expression_truth_leak"] = new[] { 0 },
|
||||
["expression_lie_shuffle"] = new[] { 0 },
|
||||
["expression_actor_flash"] = new[] { 0 },
|
||||
["expression_actor_flash_in"] = new[] { 0 },
|
||||
["expression_flash_in"] = new[] { 0 },
|
||||
["expression_actor_truth_flash"] = new[] { 0 },
|
||||
["expression_truth_flash"] = new[] { 0 },
|
||||
["expression_actor_word_hit"] = new[] { 0 },
|
||||
["expression_word_hit"] = new[] { 0 },
|
||||
["expression_actor_slot"] = new[] { 0, 1 },
|
||||
["expression_actor_scroll"] = new[] { 0 },
|
||||
["expression_truth_attack"] = new[] { 0 },
|
||||
["expression_truth_destabilize"] = new[] { 0 },
|
||||
["expression_truth_settle"] = new[] { 0 }
|
||||
};
|
||||
|
||||
[MenuItem("Tools/AIBIS/Huoshan/Validate Express Localization")]
|
||||
public static void ValidateFromMenu()
|
||||
{
|
||||
IReadOnlyList<ExpressionLocalizationIssue> issues = Validate();
|
||||
int errorCount = issues.Count(issue =>
|
||||
issue.Severity == ExpressionLocalizationIssueSeverity.Error);
|
||||
int warningCount = issues.Count - errorCount;
|
||||
|
||||
foreach (ExpressionLocalizationIssue issue in issues)
|
||||
{
|
||||
if (issue.Severity == ExpressionLocalizationIssueSeverity.Error)
|
||||
Debug.LogError($"[ExpressL10N] {issue.Message}");
|
||||
else
|
||||
Debug.LogWarning($"[ExpressL10N] {issue.Message}");
|
||||
}
|
||||
|
||||
if (errorCount == 0)
|
||||
{
|
||||
Debug.Log(
|
||||
$"[ExpressL10N] 校验通过。errors=0, pending translations={warningCount}");
|
||||
}
|
||||
}
|
||||
|
||||
public static IReadOnlyList<ExpressionLocalizationIssue> Validate()
|
||||
{
|
||||
var issues = new List<ExpressionLocalizationIssue>();
|
||||
var referencedKeys = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
ValidateStage6(issues, referencedKeys);
|
||||
ValidateCatalog(issues, referencedKeys);
|
||||
ValidateParams(issues, referencedKeys);
|
||||
return issues;
|
||||
}
|
||||
|
||||
private static void ValidateStage6(
|
||||
List<ExpressionLocalizationIssue> issues,
|
||||
HashSet<string> referencedKeys)
|
||||
{
|
||||
if (!File.Exists(Stage6Path))
|
||||
{
|
||||
AddError(issues, $"找不到 Stage6 Yarn:{Stage6Path}");
|
||||
return;
|
||||
}
|
||||
|
||||
string[] lines = File.ReadAllLines(Stage6Path);
|
||||
for (int lineIndex = 0; lineIndex < lines.Length; lineIndex++)
|
||||
{
|
||||
string line = lines[lineIndex];
|
||||
if (line.TrimStart().StartsWith("//", StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
foreach (Match keyMatch in L10NRegex.Matches(line))
|
||||
referencedKeys.Add(keyMatch.Groups["key"].Value);
|
||||
|
||||
Match commandMatch = YarnCommandRegex.Match(line);
|
||||
if (!commandMatch.Success)
|
||||
continue;
|
||||
|
||||
string command = commandMatch.Groups["command"].Value;
|
||||
if (!VisibleArgumentIndices.TryGetValue(command, out int[] visibleIndices))
|
||||
continue;
|
||||
|
||||
string[] arguments = QuotedArgumentRegex
|
||||
.Matches(commandMatch.Groups["arguments"].Value)
|
||||
.Cast<Match>()
|
||||
.Select(match => match.Groups["value"].Value)
|
||||
.ToArray();
|
||||
|
||||
foreach (int argumentIndex in visibleIndices)
|
||||
{
|
||||
if (argumentIndex >= arguments.Length)
|
||||
continue;
|
||||
|
||||
string value = arguments[argumentIndex];
|
||||
if (!LocalizationKit.IsLocalizedParam(value) && ChineseRegex.IsMatch(value))
|
||||
{
|
||||
AddError(
|
||||
issues,
|
||||
$"{Stage6Path}:{lineIndex + 1} 的 {command} 玩家文字参数仍为中文硬编码:{value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateCatalog(
|
||||
List<ExpressionLocalizationIssue> issues,
|
||||
HashSet<string> referencedKeys)
|
||||
{
|
||||
ExpressionContentCatalog catalog =
|
||||
AssetDatabase.LoadAssetAtPath<ExpressionContentCatalog>(CatalogPath);
|
||||
if (catalog == null)
|
||||
{
|
||||
AddError(issues, $"找不到轮次 Catalog:{CatalogPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (string validationError in catalog.GetValidationErrors())
|
||||
AddError(issues, validationError);
|
||||
|
||||
AddCatalogReference(catalog.ActorLoopReference, referencedKeys);
|
||||
foreach (ExpressionRoundDefinition round in catalog.Rounds)
|
||||
{
|
||||
if (round == null)
|
||||
continue;
|
||||
AddCatalogReference(round.TargetReference, referencedKeys);
|
||||
AddCatalogReference(round.TokenReference, referencedKeys);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateParams(
|
||||
List<ExpressionLocalizationIssue> issues,
|
||||
HashSet<string> referencedKeys)
|
||||
{
|
||||
StringTableCollection collection =
|
||||
LocalizationEditorSettings.GetStringTableCollection(ConstRef.ParamsTable);
|
||||
if (collection == null)
|
||||
{
|
||||
AddError(issues, $"找不到 String Table Collection:{ConstRef.ParamsTable}");
|
||||
return;
|
||||
}
|
||||
|
||||
StringTable chinese =
|
||||
collection.GetTable(new LocaleIdentifier("zh-Hans")) as StringTable;
|
||||
StringTable english =
|
||||
collection.GetTable(new LocaleIdentifier("en")) as StringTable;
|
||||
StringTable japanese =
|
||||
collection.GetTable(new LocaleIdentifier("ja-JP")) as StringTable;
|
||||
|
||||
foreach (string key in referencedKeys.OrderBy(value => value, StringComparer.Ordinal))
|
||||
{
|
||||
if (collection.SharedData.GetEntry(key) == null)
|
||||
{
|
||||
AddError(issues, $"Params Shared Data 缺少 Key:{key}");
|
||||
continue;
|
||||
}
|
||||
|
||||
StringTableEntry chineseEntry = chinese?.GetEntry(key);
|
||||
if (string.IsNullOrWhiteSpace(chineseEntry?.LocalizedValue))
|
||||
{
|
||||
AddError(issues, $"Params 中文值为空:{key}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key.EndsWith(".tokens", StringComparison.Ordinal) ||
|
||||
string.Equals(key, "hs.exp.actor.loop", StringComparison.Ordinal))
|
||||
{
|
||||
ValidatePipeList(issues, key, chineseEntry.LocalizedValue);
|
||||
}
|
||||
|
||||
AddPendingTranslation(issues, english, "en", key);
|
||||
AddPendingTranslation(issues, japanese, "ja-JP", key);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidatePipeList(
|
||||
List<ExpressionLocalizationIssue> issues,
|
||||
string key,
|
||||
string value)
|
||||
{
|
||||
if (value.Contains('|'))
|
||||
{
|
||||
AddError(issues, $"Params/{key} 使用了全角分隔符“|”,必须使用半角“|”。");
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasToken = value
|
||||
.Split('|')
|
||||
.Any(token => !string.IsNullOrWhiteSpace(token));
|
||||
if (!hasToken)
|
||||
AddError(issues, $"Params/{key} 没有有效项。");
|
||||
}
|
||||
|
||||
private static void AddPendingTranslation(
|
||||
List<ExpressionLocalizationIssue> issues,
|
||||
StringTable table,
|
||||
string localeCode,
|
||||
string key)
|
||||
{
|
||||
StringTableEntry entry = table?.GetEntry(key);
|
||||
if (entry == null)
|
||||
{
|
||||
AddError(issues, $"Params/{key} 缺少 {localeCode} 条目。");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(entry.LocalizedValue))
|
||||
{
|
||||
issues.Add(new ExpressionLocalizationIssue(
|
||||
ExpressionLocalizationIssueSeverity.Warning,
|
||||
$"待翻译:Params/{key}, Locale={localeCode}"));
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddCatalogReference(
|
||||
string reference,
|
||||
HashSet<string> referencedKeys)
|
||||
{
|
||||
if (LocalizationKit.IsLocalizedParam(reference))
|
||||
referencedKeys.Add(LocalizationKit.GetL10NParamKey(reference));
|
||||
}
|
||||
|
||||
private static void AddError(
|
||||
List<ExpressionLocalizationIssue> issues,
|
||||
string message)
|
||||
{
|
||||
issues.Add(new ExpressionLocalizationIssue(
|
||||
ExpressionLocalizationIssueSeverity.Error,
|
||||
message));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user