From 4a7cf4ff9d551649bcdabf5224d23b658ef48b69 Mon Sep 17 00:00:00 2001 From: Ziyi Zhang Date: Tue, 11 Aug 2026 18:49:59 -0400 Subject: [PATCH 1/3] feat: add the text and localization skills --- README.md | 2 + skills/localization/SKILL.md | 128 ++++++++++++ skills/localization/references/api-notes.md | 76 ++++++++ .../resources/L10nBatchProcessor.cs | 58 ++++++ .../resources/LocalizedFontAsset.cs | 18 ++ skills/optimize-text-mesh-pro/SKILL.md | 182 ++++++++++++++++++ 6 files changed, 464 insertions(+) create mode 100644 skills/localization/SKILL.md create mode 100644 skills/localization/references/api-notes.md create mode 100644 skills/localization/resources/L10nBatchProcessor.cs create mode 100644 skills/localization/resources/LocalizedFontAsset.cs create mode 100644 skills/optimize-text-mesh-pro/SKILL.md diff --git a/README.md b/README.md index e60ed11..0b46846 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ npx skills add Unity-Technologies/skills | `ui-uitk` | UI Toolkit (Unity 6.0+) — author UXML/USS, flex layout, custom elements, Painter2D, runtime binding | | `ui-ugui` | uGUI — Canvas hierarchies, RectTransform anchoring, Layout Groups, prefab UI | | `ui-imgui` | IMGUI editor tooling — EditorWindows, custom Inspectors, PropertyDrawers | +| `optimize-text-mesh-pro` | TextMeshPro font stacks, dynamic atlases, SDF quality, CJK fallback, and text memory | +| `localization` | Unity Localization — locales, String and Asset Tables, CJK fonts, Addressables | ## Usage diff --git a/skills/localization/SKILL.md b/skills/localization/SKILL.md new file mode 100644 index 0000000..b8cea20 --- /dev/null +++ b/skills/localization/SKILL.md @@ -0,0 +1,128 @@ +--- +name: localization +description: "Sets up and configures Unity Localization, including locales, String/Asset Tables, CJK font support, and Addressables workflows. Use when the user wants to add languages to a project, translate UI text, support Asian (CJK) languages with TMP fonts, or mentions i18n, l10n, multilingual support, or making a game support multiple languages." +--- + +This guide covers setting up and configuring Unity Localization, including locales, String and Asset Tables, Addressables integration, and CJK font support via Asset Tables. + +## 0. Package Installation Check +Before doing anything else, verify that the Localization packages is installed. Many APIs in this skill will fail silently or throw confusing errors if the package isn't present. +1. **Check:** Use `UnityEditor.PackageManager.Client.List(true)` to check for `com.unity.localization`. +2. **Install:** If missing, use `Client.Add("com.unity.localization")`. +3. **Wait:** Do not proceed until `Client.List` confirms installation. + +## 1. Localization Settings & Locales +If `LocalizationEditorSettings.ActiveLocalizationSettings` is null, you must find or create it: +1. **Find:** Use `AssetDatabase.FindAssets("t:LocalizationSettings")`. If found, load the first one and assign it to `LocalizationEditorSettings.ActiveLocalizationSettings`. +2. **Create:** If not found, create a new instance and save it to `Assets/Localization/LocalizationSettings.asset`. Use `ScriptableObject.CreateInstance()` followed by `AssetDatabase.CreateAsset()`. +3. **Activate:** Set `LocalizationEditorSettings.ActiveLocalizationSettings = settings`. +4. **Locales:** Ensure locales (en, fr, de, etc.) exist. Create them if missing and add them to settings using `LocalizationEditorSettings.AddLocale(locale)`. + +## 2. Modifying Localization Tables +Programmatic changes to String or Asset tables require notification to the Editor. +Always create the required asset tables, unless there is already an existing one in the project. + +### **Safe Population Pattern** +When populating tables from a dataset, match by `Locale.Identifier.Code` explicitly. The order of `GetLocales()` is not guaranteed to match your input data array — assuming it does will cause silent data mismatches that are very hard to debug. +For **Asset Tables**, use the GUID of the asset: `table.GetEntry(sharedId) ?? table.AddEntry(sharedId, guid);`. + +### **Refresh & Notification** +After any modification (adding keys, updating values), notify the Editor so it can refresh its internal state. Skipping this will leave the Editor showing stale data until the next reimport. +1. Call `EditorUtility.SetDirty(collection)`, `EditorUtility.SetDirty(collection.SharedData)`, on each modified `Table`. +2. **Unity 6+ Notification:** `LocalizationEditorSettings.EditorEvents.RaiseCollectionModified(sender, collection);` +3. Always call `AssetDatabase.SaveAssets()` at the end. + +## 3. UI Localization and Layout +### **Namespacing & Conflicts** +- **Always qualify names:** Use `UnityEngine.UI.Image`, `UnityEngine.UI.VerticalLayoutGroup`, `UnityEngine.UI.ScrollRect`, `UnityEngine.UI.Mask`, `UnityEngine.UI.CanvasScaler`, `UnityEngine.UI.GraphicRaycaster`, `UnityEngine.UI.ContentSizeFitter`, `UnityEngine.UI.LayoutRebuilder`, etc. +- `UnityEngine.UI` is both a namespace and a class container, so unqualified names produce `CS0118` (namespace used like a type). Full qualification avoids this entirely. +- **Single Instance:** Always check `GameObject.Find("YourCanvasName")` and destroy the old one before creating a new one. +- **No Debug Dropdown:** NEVER create a manual UI dropdown or debug menu to change the locale. The Localization package has a built-in way to do this properly (e.g., via the "Localization Scene Controls" window for previews). + +### **Localized String Events (Robust Binding)** +- **Check Component Type:** Identify if the target is `TextMeshPro` or legacy `UnityEngine.UI.Text`. +- **Bind Correctly:** + - **TextMeshPro:** Use reflection to call `UnityEditor.Localization.Plugins.TMPro.LocalizeComponent_TMPro.SetupForLocalization`. + - **Legacy Text:** Use reflection to call `UnityEditor.Localization.Plugins.UGUI.LocalizeComponent_UGUI.SetupForLocalization`. +- **Layout Rebuild:** After setting localized text or populating a list, call `UnityEngine.UI.LayoutRebuilder.ForceRebuildLayoutImmediate(parentTransform)` to ensure dimensions update. + +## 4. Asian Language Font Support (CJK) +Avoid TMP Fallback Fonts for CJK locales. Use **Asset Table Font Swapping** for each specific locale instead — fallbacks are unreliable and hard to debug when glyphs are missing. + +1. **Use locale-specific fonts:** Western fonts like Arial or Liberation Sans don't contain CJK glyphs, which results in "tofu" (square blocks). Always use a font designed for the target language: + - For **Simplified Chinese (zh-Hans)**: Use `msyh.ttc` (Microsoft YaHei) or equivalent. + - For **Japanese (ja)**: Use `msgothic.ttc` (MS Gothic) or equivalent. + - For **Korean (ko)**: Use `malgun.ttf` (Malgun Gothic) or equivalent. + - If system font copying fails, stop and report it. Do not substitute with a Western font. +2. **Robust Font Creation:** Create dynamic `TMP_FontAsset` from imported fonts. +3. **Multi-Atlas & Dynamic:** CJK character sets are too large for static atlases; a single atlas will run out of space immediately. + - `fontAsset.atlasPopulationMode = AtlasPopulationMode.Dynamic;` + - `fontAsset.isMultiAtlasTexturesEnabled = true;` +4. **Sub-Assets:** Save atlas and material as sub-assets, or they'll be lost on reimport: `AssetDatabase.AddObjectToAsset(fontAsset.atlasTexture, fontAsset);`. + - Explicitly link the material's texture: `fontAsset.material.mainTexture = fontAsset.atlasTexture;` and set both as dirty before saving. +5. **Addressables:** Every asset referenced in an Asset Table must be marked as Addressable. + - Do not reference assets inside a `Resources/` folder in an Asset Table. This causes `OperationException: Failed to load sub-asset` errors. If an asset is in `Resources/`, copy it to `Assets/Fonts/` or similar before making it Addressable. + - If a font asset is deleted and recreated, the new GUID must be manually updated in the Asset Table and re-added to Addressables. +6. **Specialized Types:** For TextMesh Pro font swapping, prefer `LocalizedTmpFont` over `LocalizedAsset` to avoid implicit conversion errors. +7. **Build Requirement:** After updating Asset Tables or Addressable groups, trigger a build: `AddressableAssetSettings.BuildPlayerContent();`. + +### **Verification Step** +Before concluding any CJK localization task: +1. **The Tofu Check:** Switch the editor locale to `zh-Hans`, `ja`, and `ko`. Inspect the UI. If any characters appear as squares (tofu), the font setup has FAILED. +2. **Asset Table Check:** Verify that the `AssetTable` for the CJK locale points to the correct CJK `TMP_FontAsset`, NOT a default Western font. +3. **Multi-Atlas Check:** Confirm `isMultiAtlasTexturesEnabled` is `true` on the CJK font assets. + +## 5. Automatic Layout (UGUI) +- **Parent:** `VerticalLayoutGroup` with `Child Control Height: True`, `Child Force Expand Height: False`. +- **Labels:** Each label must have a `ContentSizeFitter` set to `Vertical Fit: Preferred Size`. +- **TMP:** Set `Enable Word Wrapping: True` and `Overflow: Overflow`. + +### Notes when translating an existing project +- **Minimal Code Changes**: Never modify code unrelated to localization. Use a static helper class (e.g., `L10n`) to wrap `LocalizationSettings.StringDatabase.GetLocalizedString` for easy injection into existing scripts. +- **Robust Mapping Strategy**: When mapping existing UI text to keys, sort keys by string length (descending) and match longest strings first. This prevents short strings (like "NO") from matching parts of longer sentences. Use case-insensitive matching where appropriate. +- **Component Event Listeners**: When setting up `LocalizeStringEvent` via script, avoid `UnityEventTools.AddPersistentListener` as it often fails to set the dynamic mode (Mode 0) correctly. +Instead, use the **SerializedObject Pattern** described in Section 3 to explicitly set `m_MethodName` to `set_text` and `m_Mode` to `0`. Persistent listeners **MUST** point to a method on a `UnityEngine.Object`; lambdas will fail. +- **Initialization & Refresh**: + - `LocalizationEditorSettings.CreateStringTableCollection` expects a **directory path** (e.g., `Assets/Localization`), not a full asset path. + - Always call `lEvent.RefreshString()` after assigning a `LocalizedString` reference programmatically to update the UI immediately. + - Ensure keys are added to **all** tables in a collection (en, de, ja, etc.) to avoid "No translation found" errors. +- **Namespaces & Linq**: Always include `using System.Linq;` when searching collections and `using UnityEngine.Localization;` when working with locales or tables. +- **Verification**: After modifying tables or addressables, run `AddressableAssetSettings.BuildPlayerContent()` and switch the Editor locale to verify changes. Check `LocalizationSettings.Instance` status after activation. +- **Smart Strings**: Set up smart strings where needed. Inspect the context of each string by taking the entire UI it is on, and any scripts that affect it, into account. Set the context on the string table to ensure translations make sense. + +## 6. Recommended Translation Strategy +To efficiently translate an existing project, follow this multi-step workflow: + +1. **Extraction & Component Setup:** + - **Find all occurrences:** Scan all scenes and prefabs for strings in code and UI components (Legacy `UnityEngine.UI.Text`, `TextMeshPro`, buttons, etc.). + - **Shared Table:** Create a central String Table (e.g., `UIStrings`) with the base language and a "Context" column for each key to guide translators. + - **Attach Components:** For every UI element found, attach a `LocalizeStringEvent` (for text) and a `LocalizedFont` helper (for font swapping). + - **Validation:** Ensure these components are set up with persistent listeners (`EditorAndRuntime`) so they update in the Editor immediately when the locale changes. + +2. **Context-Aware Translation:** + - **Translate:** Once the table is populated, provide translations for each locale. + - **Context is King:** Always refer to the "Context" column or inspect the UI layout to ensure the translation fits the intended meaning and space. + - **Grammar & Tone:** Ensure the tone matches the game's style. For example, use imperative verbs for buttons (e.g., German: "Lauf!" instead of "Laufen") and correct pluralization for labels (e.g., "Punkte" instead of "Punkt"). + +3. **Quality Assurance (QA):** + - **Scene Controls:** Use `Window > Asset Management > Localization Scene Controls` or script: `LocalizationSettings.SelectedLocale = LocalizationSettings.AvailableLocales.GetLocale("de");`. + - **Visual Inspection:** Methodically inspect every prefab and scene in the base language and all target languages. + - **Layout Fit:** Check for text overflows or "tofu" (missing glyphs). Adjust font sizes or use `ContentSizeFitter` if strings are too long. + + +## API Reference +For detailed API usage, common namespace conflicts, Addressables patterns, and font repair steps, see [references/api-notes.md](references/api-notes.md). + +## 7. Accelerated Localization Workflow +To localize an entire project efficiently, use a batch processing script that handles all scenes in one pass. + +**Ask before acting:** Before running any batch operation, confirm with the user: +> "This will open every scene in the project, attach `LocalizeStringEvent` components, and save all modified scenes. This cannot be undone automatically. Shall I proceed?" + +Only proceed once the user has confirmed. The batch processor template is in [resources/L10nBatchProcessor.cs](resources/L10nBatchProcessor.cs). + +### **Technical Tips for Speed** +- **Table References:** Use `TableReference` names (strings) instead of GUIDs — they are easier to read and maintain. +- **Batch Refresh:** Use `LocalizationSettings.Instance.ForceRefresh()` after modifications to force the UI to update in the editor. +- **Font Swap Automation:** Create the `GameAssets` table once and use a script to re-assign `LocalizeFontEvent` to all labels in one pass. +- **LocalizedFontAsset component:** The template is in [resources/LocalizedFontAsset.cs](resources/LocalizedFontAsset.cs). diff --git a/skills/localization/references/api-notes.md b/skills/localization/references/api-notes.md new file mode 100644 index 0000000..98a7b13 --- /dev/null +++ b/skills/localization/references/api-notes.md @@ -0,0 +1,76 @@ +# Localization API Reference + +## LocalizationEditorSettings +The primary entry point for Editor-time localization settings. +- `GetStringTableCollections()` / `GetAssetTableCollections()`: Retrieves collections. +- `CreateStringTableCollection(name, path)`: Factory methods. Expects a **directory path** (e.g., `Assets/Localization`), not a full asset path. +- `EditorEvents.RaiseCollectionModified`: Must be called after any modification to refresh the editor. + +```csharp +LocalizationEditorSettings.EditorEvents.RaiseCollectionModified(sender, collection); +``` + +## Asset Table Entries +When adding entries to an Asset Table (fonts, textures, etc.), always use the asset's GUID: + +```csharp +string guid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(myAsset)); +var entry = table.GetEntry(sharedEntryId) ?? table.AddEntry(sharedEntryId, guid); +entry.Guid = guid; +EditorUtility.SetDirty(table); +``` + +## Common Namespace Conflicts (CS0118) +`UnityEngine.UI` is both a namespace and a class container — always fully qualify these types: +- `UnityEngine.UI.Image` +- `UnityEngine.UI.ScrollRect` +- `UnityEngine.UI.Mask` +- `UnityEngine.UI.CanvasScaler` +- `UnityEngine.UI.GraphicRaycaster` +- `UnityEngine.UI.VerticalLayoutGroup` +- `UnityEngine.UI.ContentSizeFitter` + +## Addressables Requirement +Any asset referenced in an Asset Table must be marked as Addressable: + +```csharp +var guid = AssetDatabase.AssetPathToGUID(assetPath); +var settings = UnityEditor.AddressableAssets.AddressableAssetSettingsDefaultObject.Settings; +settings.CreateOrMoveEntry(guid, settings.DefaultGroup); +``` + +## Avoiding the Resources Folder +Do not reference assets inside a `Resources/` folder in Asset Tables — the Addressables system +cannot load sub-assets from Resources, causing `OperationException: Failed to load sub-asset`. +Copy the asset out first: + +```csharp +string source = "Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF.asset"; +string target = "Assets/Fonts/LiberationSans SDF Localized.asset"; +if (!File.Exists(target)) { + AssetDatabase.CopyAsset(source, target); + AssetDatabase.ImportAsset(target); +} +// Use target path for Addressables and Asset Tables +``` + +## Triggering an Addressables Build +After modifying Addressable entries or Asset Tables: + +```csharp +using UnityEditor.AddressableAssets.Settings; +AddressableAssetSettings.BuildPlayerContent(); +``` + +## UI Layout Refresh +Programmatic text changes require a layout rebuild to update dimensions: + +```csharp +UnityEngine.UI.LayoutRebuilder.ForceRebuildLayoutImmediate(parentRectTransform); +``` + +## Robust TMP_FontAsset Repair +If fonts appear as "tofu" or throw `UnassignedReferenceException`: +- Check that the Material and Atlas Texture are nested under the Font Asset in the Project window. +- Re-assign `fontAsset.material.mainTexture = fontAsset.atlasTexture;` and re-save. +- If the font was deleted and recreated, update the GUID in both the Asset Table and the Addressables Group. diff --git a/skills/localization/resources/L10nBatchProcessor.cs b/skills/localization/resources/L10nBatchProcessor.cs new file mode 100644 index 0000000..5b8fef8 --- /dev/null +++ b/skills/localization/resources/L10nBatchProcessor.cs @@ -0,0 +1,58 @@ +using UnityEngine; +using UnityEditor; +using UnityEditor.SceneManagement; +using UnityEngine.UI; +using UnityEngine.Localization.Components; +using System.Collections.Generic; + +/// +/// Batch-processes all scenes in the project, attaching LocalizeStringEvent components +/// to every Text element whose content matches a key in the provided mapping. +/// +/// Only call LocalizeAll() after confirming with the user — it modifies and saves every scene. +/// +public static class L10nBatchProcessor +{ + public static void LocalizeAll(Dictionary mapping, string table) + { + string[] scenes = AssetDatabase.FindAssets("t:Scene"); + foreach (var guid in scenes) + { + var path = AssetDatabase.GUIDToAssetPath(guid); + var scene = EditorSceneManager.OpenScene(path, OpenSceneMode.Single); + LocalizeHierarchy(mapping, table); + EditorSceneManager.MarkSceneDirty(scene); + EditorSceneManager.SaveScene(scene); + } + } + + static void LocalizeHierarchy(Dictionary mapping, string table) + { + var allText = Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); + foreach (var text in allText) + { + foreach (var kvp in mapping) + { + if (!text.text.Contains(kvp.Key)) continue; + + var lse = text.gameObject.GetComponent() + ?? text.gameObject.AddComponent(); + lse.StringReference = new UnityEngine.Localization.LocalizedString(table, kvp.Value); + + // Use SerializedObject to set dynamic binding mode (Mode 0). + // UnityEventTools.AddPersistentListener cannot reliably set Mode 0. + var so = new SerializedObject(lse); + var calls = so.FindProperty("m_UpdateString.m_PersistentCalls.m_Calls"); + calls.ClearArray(); + calls.InsertArrayElementAtIndex(0); + var call = calls.GetArrayElementAtIndex(0); + call.FindPropertyRelative("m_Target").objectReferenceValue = text; + call.FindPropertyRelative("m_MethodName").stringValue = "set_text"; + call.FindPropertyRelative("m_Mode").enumValueIndex = 0; // Dynamic + call.FindPropertyRelative("m_CallState").enumValueIndex = 2; // EditorAndRuntime + so.ApplyModifiedProperties(); + break; + } + } + } +} diff --git a/skills/localization/resources/LocalizedFontAsset.cs b/skills/localization/resources/LocalizedFontAsset.cs new file mode 100644 index 0000000..9ab56a4 --- /dev/null +++ b/skills/localization/resources/LocalizedFontAsset.cs @@ -0,0 +1,18 @@ +using UnityEngine; +using TMPro; +using UnityEngine.Localization; +using UnityEngine.Localization.Components; + +/// +/// Swaps a TMP_Text font based on the active locale using an Asset Table. +/// Add to any GameObject with a TMP_Text that needs locale-specific fonts (e.g., CJK). +/// +[AddComponentMenu("Localization/Asset/Localized Font Asset")] +public class LocalizedFontAsset : LocalizedAssetBehaviour +{ + protected override void UpdateAsset(TMP_FontAsset font) + { + var tmp = GetComponent(); + if (tmp != null) tmp.font = font; + } +} diff --git a/skills/optimize-text-mesh-pro/SKILL.md b/skills/optimize-text-mesh-pro/SKILL.md new file mode 100644 index 0000000..f57a703 --- /dev/null +++ b/skills/optimize-text-mesh-pro/SKILL.md @@ -0,0 +1,182 @@ +--- +name: optimize-text-mesh-pro +description: > + Covers TextMeshPro font stacks, dynamic fallback atlases, padding and + sampling ratios, SDF16, AutoSize discipline, worldspace vs UGUI, and Memory + Profiler font-data capture. Use when the user mentions TextMeshPro, + Text Mesh Pro, TMP (TextMeshPro), font asset, dynamic atlas, TMP localization, + CJK (Chinese, Japanese, Korean) fonts, font alignment across + scripts, mixed western and eastern fonts, text rendering performance, profiler + markers related to text generation or glyph rasterization, font fallback + strategy, font normalization, multilingual or localized text rendering, SDF + font quality, or text-related memory issues—not for UI Toolkit layout + (unity-ui-toolkit) or non-TMP uGUI (unity-ui). +--- + +# Optimize TextMeshPro + +## Triage — identify the symptom first + +Before providing tips, identify which category the user's issue falls into. If the user has not described a specific symptom, ask: "Are you seeing a **memory/atlas bloat**, **visual quality**, **CPU/performance**, **build size**, or **localization/alignment** issue with TextMeshPro?" + +| Symptom | Go To | +|---|---| +| Memory Profiler shows large or multiple TMP atlases | [Font Stack & Dynamic Fallbacks](#font-stack--dynamic-fallbacks), [Memory Profiler: Include Font Data](#memory-profiler-include-font-data) | +| Inconsistent glyph weight, fuzzy edges, visual quality | [Padding & Sampling Ratios](#padding--sampling-ratios), [Font Asset Scale](#font-asset-scale), [Atlas Render Mode: SDF16](#atlas-render-mode-sdf16) | +| CPU spikes during text updates or Canvas rebuilds | [AutoSize](#autosize), [Worldspace vs Canvas Text](#worldspace-vs-canvas-text) | +| Build size too large from shipped font files | [Dynamic OS Atlas Population](#dynamic-os-atlas-population-tmp-320-pre3) | +| Mixed Latin + CJK alignment looks off | [Font Normalization](#font-normalization) | +| Need multiple font styles (italic, outline, glow) | [Material Presets](#material-presets) | + +--- + +## Core Rules + +- **Main font = static asset with all glyphs baked in.** Add **dynamic** fallbacks via the Fallback list (or TMP Settings) for everything else. Keep dynamic atlas size at **512-1024** to bound peak memory. +- **Dynamic fallback fonts -> enable `Clear Dynamic Data On Build`.** Otherwise editor-baked glyphs ship in the player. +- **Keep Padding-to-Sampling-Point-Size ratio consistent across primary + fallback fonts.** Mismatch produces inconsistent glyph weight on the same line. +- **Latin sampling point size 70-90; CJK 36-50.** Different scripts need different sampling sizes for clean SDF. +- **Font asset Scale = 1.** Anything else (e.g., 0.9) breaks standard point-size math. +- **Disable AutoSize at runtime once layout is locked.** AutoSize is for design, not for live counters. +- **Worldspace text -> use `TextMeshPro`, not `TextMeshProUGUI`.** Canvas overhead in worldspace is not free. +- **Parent often-changing TMP UI to its own Canvas** to bound rebuild cost. +- **TMP material presets > duplicating font assets** for italic / bold / outline / glow variants of the same font. +- **For shipping multilingual builds on iOS/Android, evaluate `Atlas Population Mode = Dynamic OS`** (TMP 3.2.0-pre.3+) to leverage system fonts and shrink the build. + +--- + +## Font Stack & Dynamic Fallbacks + +If the user reports memory bloat from TMP atlases, advise this font stack pattern: + +``` +Main font asset (static, all required Latin glyphs baked) + -> Fallback 1: Dynamic font (atlas 512 or 1024) for CJK + -> Fallback 2: Dynamic font for symbols / emoji +``` + +**NEVER ship a dynamic fallback font asset without enabling `Clear Dynamic Data On Build`.** Every glyph baked while testing in the editor is included in the player build if this toggle is off. + +--- + +## Padding & Sampling Ratios + +If the user reports inconsistent stroke widths or glyph weight differences between primary and fallback fonts, check the padding-to-sampling-point-size ratio. + +The ratio is `Padding / SamplingPointSize`. With Padding = 9 and Sampling Point Size = 90, ratio = **10%**. + +- A primary font with one ratio and a fallback with a different ratio produces **inconsistent stroke widths** on the same line. +- Pick a ratio (10% is a safe default), apply it to all font assets in the chain. + +Recommended sampling point sizes: + +- **Latin scripts**: 70-90. +- **CJK scripts**: 36-50 (CJK glyphs are visually denser; smaller sampling sizes still produce clean SDF and save atlas memory). + +--- + +## Font Asset Scale + +If the user reports point sizes not matching design specs, check the font asset Scale value. Some imported TMP font assets ship with `Scale = 0.9` instead of `1.0`. The Scale value participates in the point-size-to-pixels math, so a non-1 scale produces non-standard point sizes. Advise the user to **set Scale = 1 on all font assets before adjusting padding ratios**. + +--- + +## Sprite Assets + +If the user reports slow loading times for TMP Sprite Assets on mobile, check the source texture's Texture Type. It must be set to **Default** (not Sprite). Sprite type creates child sub-objects that TMP doesn't use; Default avoids them. + +--- + +## AutoSize + +If the user reports CPU spikes on text fields that change frequently (timers, counters, chat, dynamic player names), check whether `enableAutoSizing` is on. AutoSize resizes the text whenever the string changes, causing constant CPU spikes. + +Advise: **disable AutoSize and hard-code the chosen point size** once layout is locked. Keep AutoSize on only for genuinely static labels that auto-fit on locale change. + +--- + +## Atlas Render Mode: SDF16 + +If a static font with point size **72 or larger** looks unclear or has fuzzy edges, advise switching the **Atlas Render Mode** to **SDF16**. Higher precision SDF for big glyphs, at slightly more atlas memory. + +--- + +## Font Normalization + +If the user reports misaligned Latin + CJK text on the same line, walk them through this procedure: + +1. **Window -> TextMeshPro -> Settings -> Import TMP Example & Extras** (one-time per project). +2. Add the **`TMP_TextInfoDebugTool`** component to the TextMeshPro object displaying misaligned text. +3. Enable **ShowLines** toggle - the ascender, descender, and baseline render as overlays. +4. Mix Latin + CJK strings; if the lines diverge, **adjust ascender/descender on the TMP Font Asset** until they align. + +> **Caveat**: importing TMP Examples & Extras has been observed to cause an infinite import loop on some project layouts. If it happens, close Unity and re-open - the import resolves on the second attempt. + +--- + +## Material Presets + +If the user needs multiple styles (italic, bold, outline, glow) of the same font, advise material presets instead of duplicating font assets. Presets share the same font texture but override shader parameters. + +How to create: + +1. Select a TMP Text GameObject. +2. In Inspector, find the **Material** section. +3. **Right-click the Material header -> Create Material Preset.** +4. Rename the new material and tweak settings. +5. On the TMP Text component, pick the preset from the **Material Preset dropdown**. + +--- + +## Dynamic OS Atlas Population (TMP 3.2.0-pre.3) + +If the user is shipping multilingual builds and concerned about build size, advise evaluating **`Atlas Population Mode = Dynamic OS`** (TMP 3.2.0-pre.3+): + +- In Editor: still uses the source font from the project. +- In a player build: **the source font is not included**. At runtime, Unity searches the device for a font with the matching Family + Style name. + +Recommended system fonts for CJK: + +| Platform | Recommended system font | +|---|---| +| **Android** | NotoSans (covers Chinese, Japanese, Korean glyphs broadly). | +| **iOS** | PingFang for Simplified/Traditional Chinese. iOS uses **unique fonts per language** for CJK (different families for Chinese, Japanese, Korean) - check the fallback chain when shipping a single TMP setup across all three. | + +Wins: build size shrinks (no shipped CJK font files) and memory drops (system font is shared with the OS). + +--- + +## Memory Profiler: Include Font Data + +If Memory Profiler shows unexpectedly large font asset sizes in the Editor, check whether **Include Font Data** is enabled on the `.ttf` / `.ttc` import settings. The Editor includes the source font file in the asset by default, but on device (especially with Dynamic OS), this cost is not paid. + +To make Editor captures match device: on the font file -> deselect **Include Font Data** in the import settings. Memory Profiler will then show overhead **without** the underlying font file. + +--- + +## Worldspace vs Canvas Text + +If the user has worldspace text (damage numbers, signs, holograms) using `TextMeshProUGUI`, advise switching to **`TextMeshPro`**. Worldspace Canvas is a known inefficiency. + +If a `TextMeshProUGUI` element's `text` changes often (timers, counters, chat), advise **parenting it under a child GameObject with its own Canvas component**. Canvas rebuilds are scoped per-Canvas, so isolating the volatile field cuts rebuild cost on the rest of the UI. + +--- + +## Common Pitfalls + +If the user's setup matches any of these, flag it: + +- One giant dynamic font asset for all languages instead of static main + dynamic fallback - the dynamic atlas balloons. +- Inconsistent padding ratio across primary + fallback - same line of text looks like two fonts. +- Font asset Scale = 0.9 inherited from import - point sizes won't match design specs. +- Leaving AutoSize on for live counters - hidden CPU spikes. +- World-space `TextMeshProUGUI` inside a worldspace Canvas - extra rebuilds for no benefit; use `TextMeshPro`. +- Forgetting **Clear Dynamic Data On Build** on dynamic fallback fonts - editor-test glyphs ship in the player. +- Capturing Memory Profiler in Editor with Include Font Data on, then being surprised the on-device build is smaller. +- Sprite asset source texture set to Sprite type - mobile loading slows from extra child sub-objects. + +--- + +## References + +- TextMeshPro - Atlas Population Mode (Unity Manual): https://docs.unity3d.com/Packages/com.unity.textmeshpro@latest/manual/FontAssets.html From b7300fee4d199386ce04a5d39fa44b443b6325aa Mon Sep 17 00:00:00 2001 From: Ziyi Zhang Date: Wed, 12 Aug 2026 09:11:58 -0400 Subject: [PATCH 2/3] fix: bind localized strings through the public LocalizeStringEvent component --- skills/localization/SKILL.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/skills/localization/SKILL.md b/skills/localization/SKILL.md index b8cea20..fe579b2 100644 --- a/skills/localization/SKILL.md +++ b/skills/localization/SKILL.md @@ -41,9 +41,16 @@ After any modification (adding keys, updating values), notify the Editor so it c ### **Localized String Events (Robust Binding)** - **Check Component Type:** Identify if the target is `TextMeshPro` or legacy `UnityEngine.UI.Text`. -- **Bind Correctly:** - - **TextMeshPro:** Use reflection to call `UnityEditor.Localization.Plugins.TMPro.LocalizeComponent_TMPro.SetupForLocalization`. - - **Legacy Text:** Use reflection to call `UnityEditor.Localization.Plugins.UGUI.LocalizeComponent_UGUI.SetupForLocalization`. +- **Bind Correctly:** add the public `UnityEngine.Localization.Components.LocalizeStringEvent` + component and wire it yourself — set `StringReference` to the table entry, then add an + `OnUpdateString` listener that assigns the value to the text component (`TMP_Text.text` for + TextMeshPro, `UnityEngine.UI.Text.text` for legacy Text). + + Do **not** reflect into `UnityEditor.Localization.Plugins.TMPro.LocalizeComponent_TMPro` or its + UGUI counterpart. Those are `internal` (measured on Localization 1.5.12), so reaching them means + routing around access control to reach an API Unity makes no stability commitment about — it can + change or disappear in any package release. `LocalizeStringEvent` is public and does the same job + with the wiring made explicit. - **Layout Rebuild:** After setting localized text or populating a list, call `UnityEngine.UI.LayoutRebuilder.ForceRebuildLayoutImmediate(parentTransform)` to ensure dimensions update. ## 4. Asian Language Font Support (CJK) From 6ea128819205faba14d59e017b742783f5fe96d4 Mon Sep 17 00:00:00 2001 From: Ziyi Zhang Date: Wed, 12 Aug 2026 13:07:18 -0400 Subject: [PATCH 3/3] fix: wire the localized string listener through public UnityEventTools --- .../resources/L10nBatchProcessor.cs | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/skills/localization/resources/L10nBatchProcessor.cs b/skills/localization/resources/L10nBatchProcessor.cs index 5b8fef8..d4739c3 100644 --- a/skills/localization/resources/L10nBatchProcessor.cs +++ b/skills/localization/resources/L10nBatchProcessor.cs @@ -2,6 +2,8 @@ using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine.UI; +using UnityEditor.Events; +using UnityEngine.Events; using UnityEngine.Localization.Components; using System.Collections.Generic; @@ -39,18 +41,27 @@ static void LocalizeHierarchy(Dictionary mapping, string table) ?? text.gameObject.AddComponent(); lse.StringReference = new UnityEngine.Localization.LocalizedString(table, kvp.Value); - // Use SerializedObject to set dynamic binding mode (Mode 0). - // UnityEventTools.AddPersistentListener cannot reliably set Mode 0. - var so = new SerializedObject(lse); - var calls = so.FindProperty("m_UpdateString.m_PersistentCalls.m_Calls"); - calls.ClearArray(); - calls.InsertArrayElementAtIndex(0); - var call = calls.GetArrayElementAtIndex(0); - call.FindPropertyRelative("m_Target").objectReferenceValue = text; - call.FindPropertyRelative("m_MethodName").stringValue = "set_text"; - call.FindPropertyRelative("m_Mode").enumValueIndex = 0; // Dynamic - call.FindPropertyRelative("m_CallState").enumValueIndex = 2; // EditorAndRuntime - so.ApplyModifiedProperties(); + // Wire the listener through the public UnityEventTools API. The persistent-call + // fields could be written directly through SerializedObject instead, but those + // are private serialized names with no compatibility guarantee — and it isn't + // necessary. A delegate to Text's public `text` setter, handed to + // AddPersistentListener, serializes to exactly the same thing: target = the Text + // component, method = set_text, mode = EventDefined (dynamic), call state = + // EditorAndRuntime. Measured on Unity 6000.5.7f1. + // + // The setter has no C# method-group name, so the delegate is built by name. + // That is reflection over a *public* member, which is fine; reaching for the + // private m_* fields would not be. + var setText = (UnityAction)System.Delegate.CreateDelegate( + typeof(UnityAction), text, "set_text"); + + for (int i = lse.OnUpdateString.GetPersistentEventCount() - 1; i >= 0; i--) + { + UnityEventTools.RemovePersistentListener(lse.OnUpdateString, i); + } + UnityEventTools.AddPersistentListener(lse.OnUpdateString, setText); + + EditorUtility.SetDirty(lse); break; } }