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.
@@ -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
+ }