Skip to content

Instantly share code, notes, and snippets.

@forestrf
Forked from karljj1/FindUsedKeys.cs
Last active September 1, 2025 00:05
Show Gist options
  • Save forestrf/7b317d53ff98ff8e8ebfc51050c033ad to your computer and use it in GitHub Desktop.
Save forestrf/7b317d53ff98ff8e8ebfc51050c033ad to your computer and use it in GitHub Desktop.
Find localization entries that are not used
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.Localization;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.Localization.Tables;
using Object = UnityEngine.Object;
public class FindUnusedLocalizationKeys : EditorWindow {
HashSet<(string collectionName, long entryId)> m_FoundEntries;
[MenuItem("Window/Asset Management/Localization unused key finder")]
static void CreateWizard() {
EditorWindow.GetWindow<FindUnusedLocalizationKeys>();
}
void OnGUI() {
if (GUILayout.Button("Search")) {
Search();
}
}
void Search() {
EditorUtility.DisplayProgressBar("Localization unused key finder", "Searching", 0);
m_FoundEntries = new HashSet<(string collectionName, long entryId)>();
var sceneGUIDs = AssetDatabase.FindAssets("t:Scene");
string[] guids = new string[] { "t:Prefab", "t:ScriptableObject" }.SelectMany(e => AssetDatabase.FindAssets(e)).ToArray();
var i = 0;
bool cancelled = false;
try {
for (int j = 0; j < sceneGUIDs.Length; j++) {
var sceneGUID = sceneGUIDs[j];
var path = AssetDatabase.GUIDToAssetPath(sceneGUID);
try {
var scene = EditorSceneManager.OpenScene(path, OpenSceneMode.Single);
if (EditorUtility.DisplayCancelableProgressBar("Localization unused key finder", $"Checking scene [{j}/{sceneGUIDs.Length}]. {path}", j / (float) sceneGUIDs.Length)) {
cancelled = true;
break;
}
var components = FindObjectsOfType<GameObject>().SelectMany(t => t.GetComponents<Component>()).ToArray();
for (int k = 0; k < components.Length; k++) {
if (i % 200 == 0 && EditorUtility.DisplayCancelableProgressBar("Localization unused key finder", $"(Pass 1/2) Checking scene [{j}/{sceneGUIDs.Length}], object [{k}/{components.Length}]. {path}", j / (float) sceneGUIDs.Length + k / (float) components.Length / sceneGUIDs.Length)) {
cancelled = true;
break;
}
ExtractTableReferences(components[k]);
}
if (cancelled) break;
}
catch (ArgumentException a) {
// Scene may be read only. NEXT!
}
}
for (int j = 0; j < guids.Length; j++) {
if (cancelled) break;
var guid = guids[j];
var path = AssetDatabase.GUIDToAssetPath(guid);
var objs = AssetDatabase.LoadAllAssetsAtPath(path);
for (int k = 0; k < objs.Length; k++) {
if (i % 200 == 0 && EditorUtility.DisplayCancelableProgressBar("Localization unused key finder", $"(Pass 2/2) Checking rest of assets [{j}/{guids.Length}], component [{k}/{objs.Length}]. {path}", i / (float) sceneGUIDs.Length + j / (float) guids.Length / sceneGUIDs.Length + k / (float) guids.Length / sceneGUIDs.Length / objs.Length)) {
cancelled = true;
break;
}
var obj = objs[k];
ExtractTableReferences(obj);
}
}
}
finally {
EditorUtility.ClearProgressBar();
}
FindMissingEntries();
}
void FindMissingEntries() {
var sb = new StringBuilder();
foreach (var collection in LocalizationEditorSettings.GetStringTableCollections()) {
foreach (var entry in collection.SharedData.Entries) {
if (m_FoundEntries.Contains((collection.TableCollectionName, entry.Id))) {
// Entry is used
sb.AppendLine($"[X] YES USED - {collection.TableCollectionName} / {entry.Key}");
}
else {
// Entry is not used.
sb.AppendLine($"[ ] NOT USED - {collection.TableCollectionName} / {entry.Key}");
}
}
}
Debug.Log(sb.ToString());
}
void ExtractTableReferences(Object unityObject) {
if (unityObject == null)
return;
var so = new SerializedObject(unityObject);
var itr = so.GetIterator();
// This is ultra slow
while (itr.Next(true)) {
if (itr.type == "LocalizedString") {
if (itr.isArray) {
for (int i = 0; i < itr.arraySize; i++) {
Process(itr.GetArrayElementAtIndex(i));
}
}
else Process(itr);
void Process(SerializedProperty itr) {
var collectionNameProperty = itr.FindPropertyRelative("m_TableReference.m_TableCollectionName");
//if (collectionNameProperty == null) return;
var collectionName = collectionNameProperty.stringValue;
TableReference tableReference;
if (collectionName.StartsWith("GUID:"))
tableReference = Guid.Parse(collectionName.Substring("GUID:".Length, collectionName.Length - "GUID:".Length));
else
tableReference = collectionName;
var key = itr.FindPropertyRelative("m_TableEntryReference.m_Key");
var keyId = itr.FindPropertyRelative("m_TableEntryReference.m_KeyId");
TableEntryReference tableEntryReference;
if (keyId.longValue == 0)
tableEntryReference = key.stringValue;
else
tableEntryReference = keyId.longValue;
var collection = LocalizationEditorSettings.GetStringTableCollection(tableReference);
var stringTableEntry = collection?.SharedData.GetEntryFromReference(tableEntryReference);
if (stringTableEntry != null) {
m_FoundEntries.Add((collection.TableCollectionName, stringTableEntry.Id));
}
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment