com.xmobitea.changx.mini-localization 1.5.2 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +23 -23
- package/AI_ADD_LOCALIZATION_KEY.md +78 -78
- package/AI_ADD_LOCALIZATION_KEY.md.meta +7 -7
- package/AI_API_REFERENCE.md +9 -9
- package/AI_BEHAVIOR.md +10 -10
- package/AI_BIND_UI_TEXT.md +97 -97
- package/AI_BIND_UI_TEXT.md.meta +7 -7
- package/AI_IMPORT_TRANSLATIONS.md +84 -84
- package/AI_IMPORT_TRANSLATIONS.md.meta +7 -7
- package/AI_SETUP.md +10 -10
- package/AI_SETUP_LOCALIZATION_MANAGER.md +6 -6
- package/AI_SETUP_LOCALIZATION_SETTINGS.md +22 -22
- package/AI_SWITCH_LANGUAGE.md +107 -107
- package/AI_SWITCH_LANGUAGE.md.meta +7 -7
- package/AI_USAGE.md +18 -18
- package/CHANGELOG.md +9 -4
- package/Editor/KeyUsageAnalyzerWindowsEditor.cs +884 -0
- package/Editor/KeyUsageAnalyzerWindowsEditor.cs.meta +11 -0
- package/Editor/LocalizationComponentEditor.cs +0 -28
- package/Editor/LocalizationComponentWindowsEditor.cs +1 -1
- package/Editor/LocalizationManagerEditor.cs +2 -2
- package/Editor/TMP_LocalizationComponentEditor.cs +0 -28
- package/README.md +24 -24
- package/Runtime/LocalizationManager.cs +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
namespace XmobiTea.MiniLocalization.Editor
|
|
2
|
+
{
|
|
3
|
+
using XmobiTea.MiniLocalization;
|
|
4
|
+
using UnityEditor;
|
|
5
|
+
using UnityEditor.SceneManagement;
|
|
6
|
+
using UnityEngine;
|
|
7
|
+
using UnityEngine.SceneManagement;
|
|
8
|
+
|
|
9
|
+
using System;
|
|
10
|
+
using System.Collections.Generic;
|
|
11
|
+
using System.IO;
|
|
12
|
+
using System.Linq;
|
|
13
|
+
using System.Reflection;
|
|
14
|
+
using System.Text.RegularExpressions;
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
public class KeyUsageAnalyzerWindowsEditor : EditorWindow
|
|
18
|
+
{
|
|
19
|
+
private const string StringLiteralPattern = @"@""(?:""""|[^""])*""|""(?:\\.|[^""\\])*""";
|
|
20
|
+
|
|
21
|
+
private static readonly Regex ConstantRegex = new Regex(
|
|
22
|
+
@"public\s+(?:const|static\s+readonly)\s+string\s+(?<field>[A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?<literal>" + StringLiteralPattern + @")",
|
|
23
|
+
RegexOptions.Compiled);
|
|
24
|
+
|
|
25
|
+
private static readonly Regex ConstantReferenceRegex = new Regex(
|
|
26
|
+
@"\bLocalizationConstantId\s*\.\s*(?<field>[A-Za-z_][A-Za-z0-9_]*)\b",
|
|
27
|
+
RegexOptions.Compiled);
|
|
28
|
+
|
|
29
|
+
private static readonly Regex LocalizationGetTextRegex = new Regex(
|
|
30
|
+
@"\b(?:XmobiTea\s*\.\s*MiniLocalization\s*\.\s*)?LocalizationManager\s*\.\s*GetText\s*\(\s*(?<literal>" + StringLiteralPattern + @")",
|
|
31
|
+
RegexOptions.Compiled);
|
|
32
|
+
|
|
33
|
+
private static readonly Regex LocalizationKeyAssignmentRegex = new Regex(
|
|
34
|
+
@"\b(?<target>[A-Za-z_][A-Za-z0-9_]*(?:\s*\.\s*[A-Za-z_][A-Za-z0-9_]*)*)\s*\.\s*(?:_key|key)\s*=\s*(?<literal>" + StringLiteralPattern + @")",
|
|
35
|
+
RegexOptions.Compiled);
|
|
36
|
+
|
|
37
|
+
private static readonly Regex AnyStringLiteralRegex = new Regex(
|
|
38
|
+
@"(?<literal>" + StringLiteralPattern + @")",
|
|
39
|
+
RegexOptions.Compiled);
|
|
40
|
+
|
|
41
|
+
private readonly Dictionary<string, LocalizationConstantInfo> _constantsByKey = new Dictionary<string, LocalizationConstantInfo>();
|
|
42
|
+
private readonly Dictionary<string, string> _fieldToKey = new Dictionary<string, string>();
|
|
43
|
+
private readonly Dictionary<string, KeyUsageInfo> _usageByFingerprint = new Dictionary<string, KeyUsageInfo>();
|
|
44
|
+
private readonly Dictionary<string, List<KeyUsageInfo>> _usagesByKey = new Dictionary<string, List<KeyUsageInfo>>();
|
|
45
|
+
|
|
46
|
+
private List<KeyUsageInfo> _missingConstantUsages = new List<KeyUsageInfo>();
|
|
47
|
+
private List<LocalizationConstantInfo> _unusedConstants = new List<LocalizationConstantInfo>();
|
|
48
|
+
|
|
49
|
+
private Vector2 _scrollPos;
|
|
50
|
+
private string _searchText = string.Empty;
|
|
51
|
+
private string _lastScanSummary = "Not scanned";
|
|
52
|
+
private bool _showMissingKeys = true;
|
|
53
|
+
private bool _showUnusedKeys = true;
|
|
54
|
+
|
|
55
|
+
[MenuItem("XmobiTea Tools/Localization/Show KeyUsageAnalyzerWindows")]
|
|
56
|
+
public static void ShowWindow()
|
|
57
|
+
{
|
|
58
|
+
var window = GetWindow<KeyUsageAnalyzerWindowsEditor>();
|
|
59
|
+
|
|
60
|
+
window.minSize = new Vector2(900, 420);
|
|
61
|
+
window.Refresh();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private void OnEnable()
|
|
65
|
+
{
|
|
66
|
+
_lastScanSummary = "Click Refresh to scan localization key usage.";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private void OnGUI()
|
|
70
|
+
{
|
|
71
|
+
DrawToolbar();
|
|
72
|
+
|
|
73
|
+
_searchText = EditorGUILayout.TextField("Search", _searchText);
|
|
74
|
+
|
|
75
|
+
EditorGUILayout.Space(4);
|
|
76
|
+
EditorGUILayout.LabelField(_lastScanSummary, EditorStyles.miniLabel);
|
|
77
|
+
|
|
78
|
+
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
|
|
79
|
+
|
|
80
|
+
_showMissingKeys = EditorGUILayout.Foldout(_showMissingKeys, "Used keys missing in LocalizationConstantId.cs (" + GetFilteredMissingUsages().Count + ")", true);
|
|
81
|
+
if (_showMissingKeys)
|
|
82
|
+
{
|
|
83
|
+
DrawMissingConstantUsages();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
EditorGUILayout.Space(8);
|
|
87
|
+
|
|
88
|
+
_showUnusedKeys = EditorGUILayout.Foldout(_showUnusedKeys, "Unused keys in LocalizationConstantId.cs (" + GetFilteredUnusedConstants().Count + ")", true);
|
|
89
|
+
if (_showUnusedKeys)
|
|
90
|
+
{
|
|
91
|
+
DrawUnusedConstants();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
EditorGUILayout.EndScrollView();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private void DrawToolbar()
|
|
98
|
+
{
|
|
99
|
+
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
|
|
100
|
+
|
|
101
|
+
if (GUILayout.Button("Refresh", EditorStyles.toolbarButton, GUILayout.Width(70)))
|
|
102
|
+
{
|
|
103
|
+
Refresh();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
GUILayout.FlexibleSpace();
|
|
107
|
+
|
|
108
|
+
if (GUILayout.Button("Clear Search", EditorStyles.toolbarButton, GUILayout.Width(90)))
|
|
109
|
+
{
|
|
110
|
+
_searchText = string.Empty;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
EditorGUILayout.EndHorizontal();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private void DrawMissingConstantUsages()
|
|
117
|
+
{
|
|
118
|
+
var usages = GetFilteredMissingUsages();
|
|
119
|
+
if (usages.Count == 0)
|
|
120
|
+
{
|
|
121
|
+
EditorGUILayout.LabelField("No used keys are missing from LocalizationConstantId.cs.");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
DrawMissingHeader();
|
|
126
|
+
|
|
127
|
+
foreach (var usage in usages)
|
|
128
|
+
{
|
|
129
|
+
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
|
|
130
|
+
|
|
131
|
+
EditorGUILayout.SelectableLabel(usage.key, GUILayout.Width(220), GUILayout.Height(EditorGUIUtility.singleLineHeight));
|
|
132
|
+
EditorGUILayout.LabelField(usage.matchType, GUILayout.Width(140));
|
|
133
|
+
EditorGUILayout.LabelField(usage.location.sourceType.ToString(), GUILayout.Width(80));
|
|
134
|
+
EditorGUILayout.SelectableLabel(usage.location.GetDisplayLocation(), GUILayout.Height(EditorGUIUtility.singleLineHeight));
|
|
135
|
+
|
|
136
|
+
if (GUILayout.Button("Ping", GUILayout.Width(50)))
|
|
137
|
+
{
|
|
138
|
+
PingLocation(usage.location);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
EditorGUILayout.EndHorizontal();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private void DrawUnusedConstants()
|
|
146
|
+
{
|
|
147
|
+
var constants = GetFilteredUnusedConstants();
|
|
148
|
+
if (constants.Count == 0)
|
|
149
|
+
{
|
|
150
|
+
EditorGUILayout.LabelField("No unused key was found in LocalizationConstantId.cs.");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
DrawUnusedHeader();
|
|
155
|
+
|
|
156
|
+
foreach (var constantInfo in constants)
|
|
157
|
+
{
|
|
158
|
+
EditorGUILayout.BeginHorizontal(EditorStyles.helpBox);
|
|
159
|
+
|
|
160
|
+
EditorGUILayout.SelectableLabel(constantInfo.key, GUILayout.Width(220), GUILayout.Height(EditorGUIUtility.singleLineHeight));
|
|
161
|
+
EditorGUILayout.LabelField(constantInfo.fieldName, GUILayout.Width(160));
|
|
162
|
+
EditorGUILayout.SelectableLabel(constantInfo.GetDisplayLocation(), GUILayout.Height(EditorGUIUtility.singleLineHeight));
|
|
163
|
+
|
|
164
|
+
using (new EditorGUI.DisabledScope(string.IsNullOrEmpty(constantInfo.assetPath)))
|
|
165
|
+
{
|
|
166
|
+
if (GUILayout.Button("Ping", GUILayout.Width(50)))
|
|
167
|
+
{
|
|
168
|
+
PingAsset(constantInfo.assetPath);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
EditorGUILayout.EndHorizontal();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private static void DrawMissingHeader()
|
|
177
|
+
{
|
|
178
|
+
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
|
|
179
|
+
GUILayout.Label("Key", EditorStyles.boldLabel, GUILayout.Width(220));
|
|
180
|
+
GUILayout.Label("Usage", EditorStyles.boldLabel, GUILayout.Width(140));
|
|
181
|
+
GUILayout.Label("Source", EditorStyles.boldLabel, GUILayout.Width(80));
|
|
182
|
+
GUILayout.Label("Location", EditorStyles.boldLabel);
|
|
183
|
+
GUILayout.Label("", GUILayout.Width(50));
|
|
184
|
+
EditorGUILayout.EndHorizontal();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private static void DrawUnusedHeader()
|
|
188
|
+
{
|
|
189
|
+
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
|
|
190
|
+
GUILayout.Label("Key", EditorStyles.boldLabel, GUILayout.Width(220));
|
|
191
|
+
GUILayout.Label("Field", EditorStyles.boldLabel, GUILayout.Width(160));
|
|
192
|
+
GUILayout.Label("Location", EditorStyles.boldLabel);
|
|
193
|
+
GUILayout.Label("", GUILayout.Width(50));
|
|
194
|
+
EditorGUILayout.EndHorizontal();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private void Refresh()
|
|
198
|
+
{
|
|
199
|
+
_constantsByKey.Clear();
|
|
200
|
+
_fieldToKey.Clear();
|
|
201
|
+
_usageByFingerprint.Clear();
|
|
202
|
+
_usagesByKey.Clear();
|
|
203
|
+
|
|
204
|
+
try
|
|
205
|
+
{
|
|
206
|
+
CollectConstants();
|
|
207
|
+
CollectCodeUsages();
|
|
208
|
+
CollectPrefabComponentUsages();
|
|
209
|
+
CollectSceneComponentUsages();
|
|
210
|
+
BuildResults();
|
|
211
|
+
}
|
|
212
|
+
finally
|
|
213
|
+
{
|
|
214
|
+
EditorUtility.ClearProgressBar();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private void CollectConstants()
|
|
219
|
+
{
|
|
220
|
+
foreach (var fullPath in Directory.GetFiles(Application.dataPath, "LocalizationConstantId.cs", SearchOption.AllDirectories))
|
|
221
|
+
{
|
|
222
|
+
var assetPath = ToAssetPath(fullPath);
|
|
223
|
+
var lines = File.ReadAllLines(fullPath);
|
|
224
|
+
|
|
225
|
+
for (var i = 0; i < lines.Length; i++)
|
|
226
|
+
{
|
|
227
|
+
var match = ConstantRegex.Match(lines[i]);
|
|
228
|
+
if (!match.Success) continue;
|
|
229
|
+
|
|
230
|
+
if (!TryReadCSharpStringLiteral(match.Groups["literal"].Value, out var key)) continue;
|
|
231
|
+
|
|
232
|
+
AddConstant(new LocalizationConstantInfo
|
|
233
|
+
{
|
|
234
|
+
key = key,
|
|
235
|
+
fieldName = match.Groups["field"].Value,
|
|
236
|
+
assetPath = assetPath,
|
|
237
|
+
line = i + 1
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
foreach (var type in AppDomain.CurrentDomain.GetAssemblies()
|
|
243
|
+
.SelectMany(GetTypesSafe)
|
|
244
|
+
.Where(p => p != null && p.Name == "LocalizationConstantId" && p.Namespace == "XmobiTea.MiniLocalization"))
|
|
245
|
+
{
|
|
246
|
+
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy))
|
|
247
|
+
{
|
|
248
|
+
if (field.FieldType != typeof(string)) continue;
|
|
249
|
+
|
|
250
|
+
var key = field.GetValue(null) as string;
|
|
251
|
+
if (string.IsNullOrEmpty(key)) continue;
|
|
252
|
+
|
|
253
|
+
AddConstant(new LocalizationConstantInfo
|
|
254
|
+
{
|
|
255
|
+
key = key,
|
|
256
|
+
fieldName = field.Name
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private static IEnumerable<Type> GetTypesSafe(Assembly assembly)
|
|
263
|
+
{
|
|
264
|
+
try
|
|
265
|
+
{
|
|
266
|
+
return assembly.GetTypes();
|
|
267
|
+
}
|
|
268
|
+
catch (ReflectionTypeLoadException exception)
|
|
269
|
+
{
|
|
270
|
+
return exception.Types.Where(p => p != null).Cast<Type>();
|
|
271
|
+
}
|
|
272
|
+
catch
|
|
273
|
+
{
|
|
274
|
+
return Array.Empty<Type>();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private void AddConstant(LocalizationConstantInfo constantInfo)
|
|
279
|
+
{
|
|
280
|
+
if (string.IsNullOrEmpty(constantInfo.key)) return;
|
|
281
|
+
|
|
282
|
+
if (!_constantsByKey.TryGetValue(constantInfo.key, out var existingConstant)
|
|
283
|
+
|| string.IsNullOrEmpty(existingConstant.assetPath))
|
|
284
|
+
{
|
|
285
|
+
_constantsByKey[constantInfo.key] = constantInfo;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (!string.IsNullOrEmpty(constantInfo.fieldName) && !_fieldToKey.ContainsKey(constantInfo.fieldName))
|
|
289
|
+
{
|
|
290
|
+
_fieldToKey[constantInfo.fieldName] = constantInfo.key;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private void CollectCodeUsages()
|
|
295
|
+
{
|
|
296
|
+
var files = Directory.GetFiles(Application.dataPath, "*.cs", SearchOption.AllDirectories)
|
|
297
|
+
.OrderBy(ToAssetPath)
|
|
298
|
+
.ToArray();
|
|
299
|
+
|
|
300
|
+
for (var i = 0; i < files.Length; i++)
|
|
301
|
+
{
|
|
302
|
+
var fullPath = files[i];
|
|
303
|
+
var assetPath = ToAssetPath(fullPath);
|
|
304
|
+
|
|
305
|
+
if (IsLocalizationConstantFile(assetPath)) continue;
|
|
306
|
+
|
|
307
|
+
EditorUtility.DisplayProgressBar("Localization Key Usage Analyzer", "Scanning code " + assetPath, files.Length == 0 ? 1 : (float)i / files.Length);
|
|
308
|
+
|
|
309
|
+
var lines = File.ReadAllLines(fullPath);
|
|
310
|
+
var insideBlockComment = false;
|
|
311
|
+
for (var lineIndex = 0; lineIndex < lines.Length; lineIndex++)
|
|
312
|
+
{
|
|
313
|
+
var line = lines[lineIndex];
|
|
314
|
+
var codeLine = RemoveCSharpComments(line, ref insideBlockComment);
|
|
315
|
+
if (string.IsNullOrWhiteSpace(codeLine)) continue;
|
|
316
|
+
|
|
317
|
+
var lineNumber = lineIndex + 1;
|
|
318
|
+
ScanConstantReferences(assetPath, lineNumber, MaskCSharpStringLiterals(codeLine));
|
|
319
|
+
ScanLocalizationStringLiterals(assetPath, lineNumber, codeLine);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private void ScanConstantReferences(string assetPath, int lineNumber, string line)
|
|
325
|
+
{
|
|
326
|
+
foreach (Match match in ConstantReferenceRegex.Matches(line))
|
|
327
|
+
{
|
|
328
|
+
var fieldName = match.Groups["field"].Value;
|
|
329
|
+
if (_fieldToKey.TryGetValue(fieldName, out var key))
|
|
330
|
+
{
|
|
331
|
+
AddUsage(key, "LocalizationConstantId." + fieldName, new UsageLocation
|
|
332
|
+
{
|
|
333
|
+
sourceType = UsageSourceType.Code,
|
|
334
|
+
assetPath = assetPath,
|
|
335
|
+
line = lineNumber
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
else
|
|
339
|
+
{
|
|
340
|
+
AddUsage(fieldName, "Missing constant field", new UsageLocation
|
|
341
|
+
{
|
|
342
|
+
sourceType = UsageSourceType.Code,
|
|
343
|
+
assetPath = assetPath,
|
|
344
|
+
line = lineNumber
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private void ScanLocalizationStringLiterals(string assetPath, int lineNumber, string line)
|
|
351
|
+
{
|
|
352
|
+
foreach (Match match in LocalizationGetTextRegex.Matches(line))
|
|
353
|
+
{
|
|
354
|
+
AddStringLiteralUsage(assetPath, lineNumber, match, "LocalizationManager.GetText");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
foreach (Match match in LocalizationKeyAssignmentRegex.Matches(line))
|
|
358
|
+
{
|
|
359
|
+
if (!IsLikelyLocalizationComponentKeyAssignment(match, line)) continue;
|
|
360
|
+
|
|
361
|
+
AddStringLiteralUsage(assetPath, lineNumber, match, "Localization key assignment");
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private static bool IsLikelyLocalizationComponentKeyAssignment(Match match, string line)
|
|
366
|
+
{
|
|
367
|
+
var target = Regex.Replace(match.Groups["target"].Value, @"\s+", string.Empty);
|
|
368
|
+
if (target.IndexOf("LocalizationComponent", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
|
369
|
+
if (target.IndexOf("TMP_LocalizationComponent", StringComparison.OrdinalIgnoreCase) >= 0) return true;
|
|
370
|
+
|
|
371
|
+
return Regex.IsMatch(line, @"\b(?:LocalizationComponentBase|LocalizationComponent|TMP_LocalizationComponent)\b");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
private void AddStringLiteralUsage(string assetPath, int lineNumber, Match match, string matchType)
|
|
375
|
+
{
|
|
376
|
+
if (!TryReadCSharpStringLiteral(match.Groups["literal"].Value, out var key)) return;
|
|
377
|
+
|
|
378
|
+
AddUsage(key, matchType, new UsageLocation
|
|
379
|
+
{
|
|
380
|
+
sourceType = UsageSourceType.Code,
|
|
381
|
+
assetPath = assetPath,
|
|
382
|
+
line = lineNumber
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private void CollectPrefabComponentUsages()
|
|
387
|
+
{
|
|
388
|
+
var prefabGuids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets" });
|
|
389
|
+
|
|
390
|
+
for (var i = 0; i < prefabGuids.Length; i++)
|
|
391
|
+
{
|
|
392
|
+
var assetPath = AssetDatabase.GUIDToAssetPath(prefabGuids[i]);
|
|
393
|
+
EditorUtility.DisplayProgressBar("Localization Key Usage Analyzer", "Scanning prefab " + assetPath, prefabGuids.Length == 0 ? 1 : (float)i / prefabGuids.Length);
|
|
394
|
+
|
|
395
|
+
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(assetPath);
|
|
396
|
+
if (prefab == null) continue;
|
|
397
|
+
|
|
398
|
+
foreach (var component in prefab.GetComponentsInChildren<LocalizationComponentBase>(true))
|
|
399
|
+
{
|
|
400
|
+
AddComponentUsage(assetPath, UsageSourceType.Prefab, component);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private void CollectSceneComponentUsages()
|
|
406
|
+
{
|
|
407
|
+
var sceneGuids = AssetDatabase.FindAssets("t:Scene", new[] { "Assets" });
|
|
408
|
+
var activeScene = SceneManager.GetActiveScene();
|
|
409
|
+
|
|
410
|
+
try
|
|
411
|
+
{
|
|
412
|
+
for (var i = 0; i < sceneGuids.Length; i++)
|
|
413
|
+
{
|
|
414
|
+
var assetPath = AssetDatabase.GUIDToAssetPath(sceneGuids[i]);
|
|
415
|
+
EditorUtility.DisplayProgressBar("Localization Key Usage Analyzer", "Scanning scene " + assetPath, sceneGuids.Length == 0 ? 1 : (float)i / sceneGuids.Length);
|
|
416
|
+
|
|
417
|
+
var scene = GetLoadedSceneByPath(assetPath);
|
|
418
|
+
var openedForScan = false;
|
|
419
|
+
|
|
420
|
+
try
|
|
421
|
+
{
|
|
422
|
+
if (!scene.IsValid() || !scene.isLoaded)
|
|
423
|
+
{
|
|
424
|
+
scene = EditorSceneManager.OpenScene(assetPath, OpenSceneMode.Additive);
|
|
425
|
+
openedForScan = true;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
foreach (var rootGameObject in scene.GetRootGameObjects())
|
|
429
|
+
{
|
|
430
|
+
foreach (var component in rootGameObject.GetComponentsInChildren<LocalizationComponentBase>(true))
|
|
431
|
+
{
|
|
432
|
+
AddComponentUsage(assetPath, UsageSourceType.Scene, component);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
finally
|
|
437
|
+
{
|
|
438
|
+
if (openedForScan && scene.IsValid() && scene.isLoaded)
|
|
439
|
+
{
|
|
440
|
+
EditorSceneManager.CloseScene(scene, true);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
finally
|
|
446
|
+
{
|
|
447
|
+
if (activeScene.IsValid() && activeScene.isLoaded)
|
|
448
|
+
{
|
|
449
|
+
SceneManager.SetActiveScene(activeScene);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private static Scene GetLoadedSceneByPath(string assetPath)
|
|
455
|
+
{
|
|
456
|
+
for (var i = 0; i < SceneManager.sceneCount; i++)
|
|
457
|
+
{
|
|
458
|
+
var scene = SceneManager.GetSceneAt(i);
|
|
459
|
+
if (scene.path == assetPath) return scene;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return default;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private void AddComponentUsage(string assetPath, UsageSourceType sourceType, LocalizationComponentBase component)
|
|
466
|
+
{
|
|
467
|
+
if (component == null || string.IsNullOrEmpty(component.key)) return;
|
|
468
|
+
|
|
469
|
+
AddUsage(component.key, component.GetType().Name, new UsageLocation
|
|
470
|
+
{
|
|
471
|
+
sourceType = sourceType,
|
|
472
|
+
assetPath = assetPath,
|
|
473
|
+
gameObjectPath = GetTransformPath(component.transform),
|
|
474
|
+
componentType = component.GetType().Name,
|
|
475
|
+
globalObjectId = GlobalObjectId.GetGlobalObjectIdSlow(component.gameObject).ToString()
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
private void AddUsage(string key, string matchType, UsageLocation location)
|
|
480
|
+
{
|
|
481
|
+
if (string.IsNullOrEmpty(key)) return;
|
|
482
|
+
|
|
483
|
+
var fingerprint = key + "|" + location.sourceType + "|" + location.assetPath + "|" + location.line + "|" + location.gameObjectPath + "|" + location.globalObjectId;
|
|
484
|
+
if (_usageByFingerprint.ContainsKey(fingerprint)) return;
|
|
485
|
+
|
|
486
|
+
var usage = new KeyUsageInfo
|
|
487
|
+
{
|
|
488
|
+
key = key,
|
|
489
|
+
matchType = matchType,
|
|
490
|
+
location = location
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
_usageByFingerprint[fingerprint] = usage;
|
|
494
|
+
|
|
495
|
+
if (!_usagesByKey.TryGetValue(key, out var usages))
|
|
496
|
+
{
|
|
497
|
+
usages = new List<KeyUsageInfo>();
|
|
498
|
+
_usagesByKey[key] = usages;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
usages.Add(usage);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private void BuildResults()
|
|
505
|
+
{
|
|
506
|
+
_missingConstantUsages = _usagesByKey
|
|
507
|
+
.Where(p => !_constantsByKey.ContainsKey(p.Key))
|
|
508
|
+
.SelectMany(p => p.Value)
|
|
509
|
+
.OrderBy(p => p.key)
|
|
510
|
+
.ThenBy(p => p.location.assetPath)
|
|
511
|
+
.ThenBy(p => p.location.line)
|
|
512
|
+
.ToList();
|
|
513
|
+
|
|
514
|
+
_unusedConstants = _constantsByKey.Values
|
|
515
|
+
.Where(p => !_usagesByKey.ContainsKey(p.key))
|
|
516
|
+
.OrderBy(p => p.key)
|
|
517
|
+
.ToList();
|
|
518
|
+
|
|
519
|
+
_lastScanSummary = "Constants: " + _constantsByKey.Count
|
|
520
|
+
+ " | Usages: " + _usageByFingerprint.Count
|
|
521
|
+
+ " | Missing constants: " + _missingConstantUsages.Count
|
|
522
|
+
+ " | Unused constants: " + _unusedConstants.Count;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private List<KeyUsageInfo> GetFilteredMissingUsages()
|
|
526
|
+
{
|
|
527
|
+
if (string.IsNullOrEmpty(_searchText)) return _missingConstantUsages;
|
|
528
|
+
|
|
529
|
+
return _missingConstantUsages
|
|
530
|
+
.Where(p => p.Contains(_searchText))
|
|
531
|
+
.ToList();
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
private List<LocalizationConstantInfo> GetFilteredUnusedConstants()
|
|
535
|
+
{
|
|
536
|
+
if (string.IsNullOrEmpty(_searchText)) return _unusedConstants;
|
|
537
|
+
|
|
538
|
+
return _unusedConstants
|
|
539
|
+
.Where(p => p.Contains(_searchText))
|
|
540
|
+
.ToList();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private static void PingLocation(UsageLocation location)
|
|
544
|
+
{
|
|
545
|
+
if (location.sourceType == UsageSourceType.Code)
|
|
546
|
+
{
|
|
547
|
+
PingAsset(location.assetPath);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (location.sourceType == UsageSourceType.Prefab)
|
|
552
|
+
{
|
|
553
|
+
PingPrefabObject(location);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (location.sourceType == UsageSourceType.Scene)
|
|
558
|
+
{
|
|
559
|
+
PingSceneObject(location);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private static void PingAsset(string assetPath)
|
|
564
|
+
{
|
|
565
|
+
if (string.IsNullOrEmpty(assetPath)) return;
|
|
566
|
+
|
|
567
|
+
var asset = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(assetPath);
|
|
568
|
+
if (asset == null) return;
|
|
569
|
+
|
|
570
|
+
Selection.activeObject = asset;
|
|
571
|
+
EditorGUIUtility.PingObject(asset);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private static void PingPrefabObject(UsageLocation location)
|
|
575
|
+
{
|
|
576
|
+
if (PingGlobalObjectId(location.globalObjectId)) return;
|
|
577
|
+
|
|
578
|
+
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(location.assetPath);
|
|
579
|
+
if (prefab == null)
|
|
580
|
+
{
|
|
581
|
+
PingAsset(location.assetPath);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
var gameObject = FindGameObjectInRoot(prefab, location.gameObjectPath);
|
|
586
|
+
var target = gameObject != null ? gameObject : prefab;
|
|
587
|
+
|
|
588
|
+
Selection.activeObject = target;
|
|
589
|
+
EditorGUIUtility.PingObject(target);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
private static void PingSceneObject(UsageLocation location)
|
|
593
|
+
{
|
|
594
|
+
var scene = GetLoadedSceneByPath(location.assetPath);
|
|
595
|
+
if (!scene.IsValid() || !scene.isLoaded)
|
|
596
|
+
{
|
|
597
|
+
PingAsset(location.assetPath);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (PingGlobalObjectId(location.globalObjectId)) return;
|
|
602
|
+
|
|
603
|
+
foreach (var rootGameObject in scene.GetRootGameObjects())
|
|
604
|
+
{
|
|
605
|
+
var gameObject = FindGameObjectInRoot(rootGameObject, location.gameObjectPath);
|
|
606
|
+
if (gameObject == null) continue;
|
|
607
|
+
|
|
608
|
+
Selection.activeObject = gameObject;
|
|
609
|
+
EditorGUIUtility.PingObject(gameObject);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
PingAsset(location.assetPath);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
private static bool PingGlobalObjectId(string globalObjectId)
|
|
617
|
+
{
|
|
618
|
+
if (string.IsNullOrEmpty(globalObjectId)) return false;
|
|
619
|
+
if (!GlobalObjectId.TryParse(globalObjectId, out var id)) return false;
|
|
620
|
+
|
|
621
|
+
var target = GlobalObjectId.GlobalObjectIdentifierToObjectSlow(id);
|
|
622
|
+
if (target == null) return false;
|
|
623
|
+
|
|
624
|
+
Selection.activeObject = target;
|
|
625
|
+
EditorGUIUtility.PingObject(target);
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
private static GameObject FindGameObjectInRoot(GameObject root, string gameObjectPath)
|
|
630
|
+
{
|
|
631
|
+
if (root == null || string.IsNullOrEmpty(gameObjectPath)) return null;
|
|
632
|
+
|
|
633
|
+
var parts = gameObjectPath.Split('/');
|
|
634
|
+
if (parts.Length == 0 || parts[0] != root.name) return null;
|
|
635
|
+
|
|
636
|
+
var current = root.transform;
|
|
637
|
+
for (var i = 1; i < parts.Length; i++)
|
|
638
|
+
{
|
|
639
|
+
current = current.Find(parts[i]);
|
|
640
|
+
if (current == null) return null;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return current.gameObject;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
private static string GetTransformPath(Transform transform)
|
|
647
|
+
{
|
|
648
|
+
var names = new Stack<string>();
|
|
649
|
+
while (transform != null)
|
|
650
|
+
{
|
|
651
|
+
names.Push(transform.name);
|
|
652
|
+
transform = transform.parent;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return string.Join("/", names.ToArray());
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
private static bool IsLocalizationConstantFile(string assetPath)
|
|
659
|
+
{
|
|
660
|
+
return string.Equals(Path.GetFileName(assetPath), "LocalizationConstantId.cs", StringComparison.OrdinalIgnoreCase);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
private static string ToAssetPath(string fullPath)
|
|
664
|
+
{
|
|
665
|
+
var normalizedFullPath = fullPath.Replace('\\', '/');
|
|
666
|
+
var normalizedDataPath = Application.dataPath.Replace('\\', '/');
|
|
667
|
+
|
|
668
|
+
if (!normalizedFullPath.StartsWith(normalizedDataPath, StringComparison.OrdinalIgnoreCase))
|
|
669
|
+
{
|
|
670
|
+
return normalizedFullPath;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return "Assets" + normalizedFullPath.Substring(normalizedDataPath.Length);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
private static string RemoveCSharpComments(string line, ref bool insideBlockComment)
|
|
677
|
+
{
|
|
678
|
+
if (string.IsNullOrEmpty(line)) return string.Empty;
|
|
679
|
+
|
|
680
|
+
var result = new System.Text.StringBuilder(line.Length);
|
|
681
|
+
|
|
682
|
+
for (var i = 0; i < line.Length; i++)
|
|
683
|
+
{
|
|
684
|
+
if (insideBlockComment)
|
|
685
|
+
{
|
|
686
|
+
if (i + 1 < line.Length && line[i] == '*' && line[i + 1] == '/')
|
|
687
|
+
{
|
|
688
|
+
insideBlockComment = false;
|
|
689
|
+
i++;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (i + 1 < line.Length && line[i] == '/' && line[i + 1] == '/')
|
|
696
|
+
{
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (i + 1 < line.Length && line[i] == '/' && line[i + 1] == '*')
|
|
701
|
+
{
|
|
702
|
+
insideBlockComment = true;
|
|
703
|
+
i++;
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (line[i] == '@' && i + 1 < line.Length && line[i + 1] == '"')
|
|
708
|
+
{
|
|
709
|
+
result.Append(line[i]);
|
|
710
|
+
i++;
|
|
711
|
+
result.Append(line[i]);
|
|
712
|
+
|
|
713
|
+
while (i + 1 < line.Length)
|
|
714
|
+
{
|
|
715
|
+
i++;
|
|
716
|
+
result.Append(line[i]);
|
|
717
|
+
|
|
718
|
+
if (line[i] == '"')
|
|
719
|
+
{
|
|
720
|
+
if (i + 1 < line.Length && line[i + 1] == '"')
|
|
721
|
+
{
|
|
722
|
+
i++;
|
|
723
|
+
result.Append(line[i]);
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
break;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (line[i] == '"' || line[i] == '\'')
|
|
735
|
+
{
|
|
736
|
+
var quote = line[i];
|
|
737
|
+
result.Append(line[i]);
|
|
738
|
+
|
|
739
|
+
while (i + 1 < line.Length)
|
|
740
|
+
{
|
|
741
|
+
i++;
|
|
742
|
+
result.Append(line[i]);
|
|
743
|
+
|
|
744
|
+
if (line[i] == '\\' && i + 1 < line.Length)
|
|
745
|
+
{
|
|
746
|
+
i++;
|
|
747
|
+
result.Append(line[i]);
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (line[i] == quote)
|
|
752
|
+
{
|
|
753
|
+
break;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
result.Append(line[i]);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
return result.ToString();
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
private static string MaskCSharpStringLiterals(string line)
|
|
767
|
+
{
|
|
768
|
+
return AnyStringLiteralRegex.Replace(line, "\"\"");
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
private static bool TryReadCSharpStringLiteral(string literal, out string value)
|
|
772
|
+
{
|
|
773
|
+
value = string.Empty;
|
|
774
|
+
if (string.IsNullOrEmpty(literal)) return false;
|
|
775
|
+
|
|
776
|
+
if (literal.StartsWith("@\"", StringComparison.Ordinal))
|
|
777
|
+
{
|
|
778
|
+
if (literal.Length < 3 || literal[literal.Length - 1] != '"') return false;
|
|
779
|
+
|
|
780
|
+
value = literal.Substring(2, literal.Length - 3).Replace("\"\"", "\"");
|
|
781
|
+
return true;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (literal.Length < 2 || literal[0] != '"' || literal[literal.Length - 1] != '"') return false;
|
|
785
|
+
|
|
786
|
+
var innerValue = literal.Substring(1, literal.Length - 2);
|
|
787
|
+
try
|
|
788
|
+
{
|
|
789
|
+
value = Regex.Unescape(innerValue);
|
|
790
|
+
}
|
|
791
|
+
catch
|
|
792
|
+
{
|
|
793
|
+
value = innerValue.Replace("\\\"", "\"").Replace("\\\\", "\\");
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private enum UsageSourceType
|
|
800
|
+
{
|
|
801
|
+
Code,
|
|
802
|
+
Prefab,
|
|
803
|
+
Scene
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
private sealed class LocalizationConstantInfo
|
|
807
|
+
{
|
|
808
|
+
public string key;
|
|
809
|
+
public string fieldName;
|
|
810
|
+
public string assetPath;
|
|
811
|
+
public int line;
|
|
812
|
+
|
|
813
|
+
public string GetDisplayLocation()
|
|
814
|
+
{
|
|
815
|
+
if (string.IsNullOrEmpty(assetPath)) return "Compiled LocalizationConstantId";
|
|
816
|
+
if (line <= 0) return assetPath;
|
|
817
|
+
|
|
818
|
+
return assetPath + ":" + line;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
public bool Contains(string searchText)
|
|
822
|
+
{
|
|
823
|
+
return ContainsText(key, searchText)
|
|
824
|
+
|| ContainsText(fieldName, searchText)
|
|
825
|
+
|| ContainsText(assetPath, searchText);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
private sealed class KeyUsageInfo
|
|
830
|
+
{
|
|
831
|
+
public string key;
|
|
832
|
+
public string matchType;
|
|
833
|
+
public UsageLocation location;
|
|
834
|
+
|
|
835
|
+
public bool Contains(string searchText)
|
|
836
|
+
{
|
|
837
|
+
return ContainsText(key, searchText)
|
|
838
|
+
|| ContainsText(matchType, searchText)
|
|
839
|
+
|| location.Contains(searchText);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
private sealed class UsageLocation
|
|
844
|
+
{
|
|
845
|
+
public UsageSourceType sourceType;
|
|
846
|
+
public string assetPath;
|
|
847
|
+
public int line;
|
|
848
|
+
public string gameObjectPath;
|
|
849
|
+
public string componentType;
|
|
850
|
+
public string globalObjectId;
|
|
851
|
+
|
|
852
|
+
public string GetDisplayLocation()
|
|
853
|
+
{
|
|
854
|
+
if (sourceType == UsageSourceType.Code)
|
|
855
|
+
{
|
|
856
|
+
return assetPath + ":" + line;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (!string.IsNullOrEmpty(componentType))
|
|
860
|
+
{
|
|
861
|
+
return assetPath + " -> " + gameObjectPath + " (" + componentType + ")";
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
return assetPath + " -> " + gameObjectPath;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
public bool Contains(string searchText)
|
|
868
|
+
{
|
|
869
|
+
return ContainsText(assetPath, searchText)
|
|
870
|
+
|| ContainsText(gameObjectPath, searchText)
|
|
871
|
+
|| ContainsText(componentType, searchText)
|
|
872
|
+
|| ContainsText(sourceType.ToString(), searchText);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
private static bool ContainsText(string source, string searchText)
|
|
877
|
+
{
|
|
878
|
+
if (string.IsNullOrEmpty(searchText)) return true;
|
|
879
|
+
if (string.IsNullOrEmpty(source)) return false;
|
|
880
|
+
|
|
881
|
+
return source.IndexOf(searchText, StringComparison.OrdinalIgnoreCase) >= 0;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|