DeepLoc is a Unity localization tool. It scans UI text and assets in your project, translates them with DeepL, and applies the correct language at runtime.
1. Introduction
What DeepLoc does
- Scan — finds text in scenes, prefabs, and scripts, plus localizable assets (images, audio, video, etc.)
- Translate — sends text to DeepL and stores translations in a central data asset
- Apply — at runtime, components read from that asset and update when the player changes language
Module structure
| Module | Folder | Purpose |
|---|---|---|
| Runtime | Core/ |
Build-safe scripts used in your game |
| Editor | Editor/ |
Scanning, translation, and setup tools |
| Sample | Sample/ |
Demo scene and scripts (optional — safe to delete) |
Requirements
- Unity 2021.3 LTS or newer
- TextMeshPro package
- DeepL API key — required for translation in the editor; not required to run the sample scene in Play mode
Third-party software
Asset uses Google Noto fonts (Noto Naskh Arabic, Noto Sans Hebrew, Noto Sans Devanagari, Noto Sans JP, Noto Sans KR, Noto Sans SC) under the SIL Open Font License, Version 1.1; see Third-Party Notices.txt in the package root for full license text.
2. Quick Start
Path A — New project setup
On first import the DeepLoc Editor opens on Home with a setup guide (reopen it any time with Tools → DeepLoc → Setup Wizard or ? → Setup guide):
- Localization data: click Create Localization Data (creates
Assets/DeepLoc/Data/LocalizationData.asset) - DeepL API key (optional): paste your key and Test API Key, or skip and add it later in Settings
- Target languages: add the languages to translate into (e.g. DE, FR, HI)
- Fonts (optional): Fix fonts so TextMesh Pro can render every language
- First scan (optional): pick what to scan and click Scan & Translate
Click Finish setup and Home switches to the project dashboard.
Path B — Run the sample scene now
- Open
Sample/Scenes/DeepLocShowcase.unity - Press Play
- Pick a language in the top bar (or on the Welcome page) and browse the pages on the left
The sample has its own data asset (Sample/Data/SampleLocalizationData.asset) with translations for 7 languages, so no import step or DeepL key is needed. See Sample Scene.
Menu reference
| Menu | Opens |
|---|---|
| Tools → DeepLoc → Editor | Main localization window |
| Tools → DeepLoc → Setup Wizard | Opens the setup guide on the Home page |
| Tools → DeepLoc → Script Scanner | Opens the Scripts page of the DeepLoc Editor |
| GameObject → DeepLoc → Localization → Language Selector | Creates a language dropdown under Canvas |
3. Core Concepts
LocalizationData
A ScriptableObject that stores everything:
- Text entries — key, original text (EN), translations per language
- Asset entries — key, asset type, per-language assets (sprites, clips, etc.)
Create one with Create Localization Data on the Home page, or Create new… in the toolbar’s data dropdown.
LocalizationManager
A singleton in your scene (use the prefab at Prefabs/LocalizationManager.prefab). It:
- Holds the current language
- Refreshes all localized components when language changes
- Provides
GetTranslation()andGetAsset<T>()for scripts
Keys
Each piece of localized content has a string key (e.g. Change_Language). Optionally click Generate LocKeys (Home → Quick actions) to write Core/Generated/LocKeys.cs with a constant for every key.
Default language
English (EN) is the source language. If a translation is missing at runtime, GetTranslation returns N/A_Key_LANG (e.g. N/A_boy_DE) so you can spot gaps easily.
4. Runtime Components
Add these to GameObjects in your scene. Each reads a key from LocalizationData and updates when SetLanguage is called.
| Component | Unity target | Localizes |
|---|---|---|
LocalizedText |
Text, TMP UI, TMP 3D | String |
LocalizedImage |
UI Image | Sprite |
LocalizedSpriteRenderer |
SpriteRenderer | Sprite |
LocalizedRawImage |
RawImage | Texture2D |
LocalizedMaterial |
Renderer | Material |
LocalizedVideo |
VideoPlayer | VideoClip |
LocalizedPrefab |
Transform (child) | GameObject prefab |
LocalizedAudioSource |
AudioSource | AudioClip |
LanguageSelector |
Dropdown | Populates languages, calls SetLanguage |
TextBackgroundScaler |
RectTransform + text child | Resizes background to fit text |
IgnoreLocalization |
Any | Excludes object from scene scan |
Quick add: Right-click a Text, TMP, Image, or other supported component in the Inspector → Localize this component. This adds the localized component and creates the entry in LocalizationData.
5. Runtime API
using Cubehole.Localization;
// Change language (updates all localized components)
LocalizationManager.Instance.SetLanguage("DE");
// Get translated text
string text = LocalizationManager.GetTranslation("MyKey");
// Text with variables — translation uses {0}, {1}, etc. (any argument types)
string greeting = LocalizationManager.GetTranslationFormat("GreetingKey", playerName, scoreValue);
// Pluralization — picks the right form by count
string items = LocalizationManager.GetTranslation("ItemsKey", itemCount);
// Get localized asset
Sprite sprite = LocalizationManager.GetAsset<Sprite>("MyImageKey");
Also available:
- Variables — use
GetTranslationFormatfor format arguments.GetTranslation(key, "text")treats a string second argument as a language code. - RTL — DeepLoc shapes Arabic/Persian (contextual glyphs, lam-alef, tashkeel) and reorders Hebrew for display.
LocalizedTextapplies the correct transform for TextMeshPro (isRightToLeftText) or legacy Unity UIText. PublicGetTranslationreturns TMP-ready RTL text; useGetTranslationLogicalwhen you need the unshaped string. Run Tools → DeepLoc → Expand RTL Font Atlases once so Arabic presentation forms and the Hebrew font are in TMP fallbacks. Layout/alignment mirroring is still your project’s responsibility. - Devanagari (Hindi, Marathi, Nepali) — TextMesh Pro cannot join Devanagari letters, so
हिन्दीwould show the vowel sign ि after ह and conjuncts with a visible virama. Click Bake conjuncts on the Fonts page (or Tools → DeepLoc → Bake Devanagari Conjuncts) once: it bakes the font’s conjunct, half-form, rakar/reph and ि-length glyphs intoNotoSansDevanagari-Medium Conjuncts SDF(added as a fallback of the Devanagari font) and writesResources/DeepLoc/DevanagariShapingData.LocalizedText,LanguageSelectorandGetTranslationthen shape any text containing Devanagari, whatever the current language. Legacy UIText, fonts that cannot reach the baked asset, and projects without a bake get reorder-only shaping (ि placed correctly, conjuncts with a visible virama). For your own components useDisplayTextShaper.Shape(GetTranslationLogical(key), language, RtlOutputKind.Tmp, text.font). Bake again after changing the Devanagari font (it needs a font with glyph names, like the bundled Noto). Marathi-specific letter forms (eyelash ra,.MARalternates) are not selected. - Pseudo-localization — enable on
LocalizationManagerto stress-test UI with expanded strings (rich-text tags and{0}/%splaceholders are left intact) - Custom localized components — implement
ILocalizedAsset, callLocalizationManager.Register(this)inOnEnable/Unregister(this)inOnDisable, and apply the current language inOnEnable. In Play mode only registered components receive language changes. Alternatively subscribe toLocalizationManager.OnLanguageChanged. - Language persistence —
LocalizationManagerreadsPlayerPrefs("Language")on startup if set (includingEN);LanguageSelectorsaves it. Auto-detect of the system language only applies when nothing is saved.
6. Editor Tools
DeepLoc Editor (Tools → DeepLoc → Editor)
The window has a toolbar across the top, a page list on the left, and a status bar along the bottom.
Toolbar
| Control | What it does |
|---|---|
| Data dropdown | Switch between LocalizationData assets, create a new one, or select it in the Project window |
| Preview | Language shown by localized components in the open scene (needs a LocalizationManager) |
| Pseudo | Pseudo-localization on/off, to spot truncation and hard-coded text. Prefix, suffix, and expansion are under Settings → Pseudo-localization |
| DeepL pill | Key status and characters left this billing period. Click to test the key, refresh usage, or open DeepL settings |
| ? | Setup guide and this user guide |
Pages
| Page | What it does |
|---|---|
| Home | Until setup is finished: the setup guide (see Quick Start). After that: Setup checklist with one-click fixes (data asset, LocalizationManager, DeepL key, target languages, duplicate keys, font glyph coverage), coverage bar per language, and quick actions (Refresh scene text, Generate LocKeys) |
| Strings | The place to add, edit, and delete text keys. The table shows every key × language with % translated per language in the column headers; missing cells are highlighted, locked ones show a lock, and keys with problems (duplicate or empty key, empty source, a translation missing a placeholder like {0}) are marked in red. Add keys in the row under the table (the key is suggested from the source text). Double-click a cell or press F2 to edit it in place; Delete removes the selected keys (undoable). The panel on the right edits the selected key: rename (optionally updating LocalizedText components in open scenes), category, source text, context, each translation with lock and DeepL, plural forms, platform overrides, and where it is used. Select several keys to set a category, lock/unlock, or delete them together. Filters: search, category, Missing, Problems, Unused (one-off project scan) |
| Assets | The place to add, edit, and delete localized asset keys (sprites, textures, materials, video, audio, prefabs). Drag assets from the Project window onto the table to create one key per asset, or use the row under the table (key, type, optional source asset). Languages without an asset are marked fallback (they use the source asset); click an asset to find it in the Project window. The panel on the right edits the selected key: rename (optionally updating localized asset components in open scenes), type, category, context, the asset for each language with a lock, and where it is used. Select several keys to set a category, lock/unlock, or delete them. Filters: search, type, category, Missing, Problems (duplicate keys, assets whose type does not match the key) |
| Scan | Choose what to scan (UI Text, TMP, C# scripts, asset component types) and where (active scene, build scenes, prefabs), then Scan & Translate. Also lists text in the active scene without LocalizedText, summarizes hard-coded script strings with a link to the Scripts page (Section 7), and removes localized components from a selection |
| Scripts | Finds string literals in your C# scripts and replaces them with GetTranslation calls; see Section 7 |
| Fonts | Fix Fonts adds package fonts to TMP fallbacks; Expand RTL Atlases adds Arabic presentation forms + Hebrew TMP; Bake conjuncts bakes Devanagari conjunct glyphs (see Devanagari in Section 5); Validate Glyphs flags unsupported characters |
| Sheets | Push/pull sync with translators; see Section 8 |
| Settings | DeepL API key, target languages, pseudo-localization prefix/suffix/expansion, variable protection (regex masks for {0}, %s, rich text tags), Addressables (Section 9), and logging |
Status bar shows the latest message (warnings and errors appear under the toolbar until dismissed). On the Strings page it holds Translate missing, which shows the approximate number of characters DeepL will receive, and Translate selected. When translations are missing, Home also shows Translate missing under Quick actions. While a scan or translation runs, the status bar shows its progress with Pause / Resume and Stop (finished translations are kept). Log opens the scan / translate log above the status bar.
The LocalizationData asset’s Inspector only shows a summary; double-click the asset (or click Open in DeepLoc Editor) to edit it.
Scan & Translate workflow
- On the Scan page, choose scope: Active Scene, All Scenes in Build, or Selected Prefabs
- Toggle what to scan: UI text, scripts, images, audio, etc.
- Click Scan & Translate (or Translate missing on Home / the Strings status bar to only fill gaps in existing keys)
- Lock key+language cells on the Strings page to protect manual edits
Translations are sent in batches (up to 50 texts per DeepL request, grouped by language and context). Protected placeholders are sent as XML tags so DeepL translates whole sentences and can reorder them; rate-limit (429) and server errors are retried automatically.
Other editor menus
| Menu | Action |
|---|---|
| Scene View overlay → Language | Switch language while editing (Unity 2021.2+) |
| Assets → DeepLoc → Localization → Split by Locale | Creates one LocalizationData asset per language (for Addressables) |
| GameObject → DeepLoc → Localization → Remove All Localized Components In Hierarchy | Bulk-remove localized components |
7. Script Scanner
The Scripts page of the DeepLoc Editor finds hard-coded string literals in your C# code (also opened by Tools → DeepLoc → Script Scanner).
What it finds: string literals in .cs files — field initializers, method arguments, $"..." interpolation, etc.
What it skips: strings passed to GetTranslation() or GetTranslations() (already localized), plus everything the preset rules filter out.
Preset rules (scan settings → Rules; all on by default, Recommended restores them, Report everything turns them off). Each one drops strings the player never sees:
| Rule | Skips |
|---|---|
| Logs | Debug.Log*, print, Console.Write, ...Logger.* calls |
| PlayerPrefs keys | PlayerPrefs.* arguments |
| Attributes | [Tooltip("...")], [Header], [MenuItem], [CreateAssetMenu] …, including attributes split over several lines |
| Lookup tables | dictionary rows { "EN", "English" }, ["EN"] = "English", keys dict["key"] |
| Names passed to Unity APIs | GetComponent, Find, CompareTag, Animator / Material parameters, Shader.Find, Resources.Load, SceneManager, Input, Invoke, SendMessage, reflection, file paths, web request headers/fields |
| Exceptions and asserts | throw new ...Exception("..."), Debug.Assert, Assert.* |
| Comparisons and string ops | == "x", Equals, StartsWith, Contains, Split, Replace, ToString("F2"), case labels |
| Identifiers, paths, URLs | literals without spaces that aren’t words: snake_case, camelCase, lowercase ids, paths, URLs, file names, number formats, hex colors, lone rich-text tags |
#if UNITY_EDITOR code |
code that never ships in a player build |
Only UI text (strict) goes further: it reports only literals that are clearly displayed — assignments to .text / title / message / label …, SetText, Show* / Popup / Dialog / Toast / Tooltip calls, new GUIContent(...), title: / message: arguments. Strings stored in a plain variable or array first are not reported in this mode. Add your own popup/dialog API under Display phrases (e.g. PopupManager.Open): statements containing a phrase count as UI text. Display-context strings are ranked High in either mode.
Workflow:
- With nothing selected, the panel on the right shows the scan settings: folders to scan, excluded folders, rules (preset rules, display phrases, skip-line regex patterns), and where results are stored (
HardcodedStringsData.asset, plus CSV export). Rule changes apply to the next scan. - Click Scan scripts. Results are grouped by file; filter by search text or confidence (High = very likely player-facing).
- Select a string to see its code, set its key (existing keys are reused automatically), then Replace in script — the literal becomes
GetTranslation("key"), the key is added to Localization Data, and it is translated if DeepL is set up. Add to data only adds the key without touching the script. Ignore hides noise (turn on Show ignored to bring it back). - Select a file row to Replace all in that file, Rescan it, or ignore the whole file. Select several strings to replace or ignore them together.
Replace edits scripts on disk; use version control to revert.
Listed, but no Replace. Some strings are real UI text but must not become GetTranslation(...) calls. They stay in the results, marked · no replace (hover or select for the reason), and only Add to data only is offered:
- Always:
constfields, attribute arguments,caselabels, default parameter values (C# needs a compile-time constant there). - Field initializers (on by default):
string[] tips = { "..." },static readonlyfields, property initializers. They run beforeLocalizationManageris ready, soGetTranslationwould return the key and never follow language changes. Keep the key in the field and translate where the text is shown. - Constructors (on by default): instance and static constructors, same timing problem.
- Editor-only code (on by default):
OnValidate,Reset,OnDrawGizmos*,[MenuItem]/[ContextMenu]methods,#if UNITY_EDITORblocks.
Toggle these under Rules → List, but don’t offer Replace in. Rescan after changing them.
The sample script Sample/Scripts/LocalizationTest.cs contains intentional hardcoded strings for testing the scanner. The showcase scripts in Sample/Scripts/Showcase/ also contain key names and a few literals; exclude the Sample/ folder in the scan settings if you only want your own code.
8. Google Sheets Sync
Keep a Google Sheet in sync with your LocalizationData. Unity only needs a web app URL — no Service Account or Sheets API key.
Setup in Unity
- Open Tools → DeepLoc → Editor
- Open the Sheets page
- Paste your Web App URL
- Pick the
LocalizationDataasset from the toolbar dropdown
Push / Pull
- Push to Sheet — sends current
LocalizationDatato the sheet (overwrites sheet) - Pull from Sheet — reads the sheet; if anything differs, the changes appear at the top of the Sheets page with a checkbox per item. Added keys and Modified fields (per key and per language) start ticked; keys Only in Unity start unticked and are deleted only if you tick them. Apply selected applies just the ticked items (one Undo step); Discard drops them. Locked translations can’t be ticked
- Spaces, line breaks and invisible characters are removed from the Web App and Sheet URLs automatically
- CSV file — offline fallback: Push to Sheet exports a CSV, Pull from Sheet loads one into the same review
Locked languages are respected on import. New language columns in the sheet are added automatically (codes are upper-cased). Blank cells are ignored, so they never erase an existing translation.
Apps Script (one-time setup)
- Create a Google Sheet with header row:
Key,OriginalText,Context, then language codes (DE,FR, …) - Go to Extensions → Apps Script, paste the script below, and deploy as a Web app
- Set
SECRETat the top of the script to a random string - Set Execute as = Me, Who has access = Anyone
- Copy the deployment URL into Unity and append
?token=<your SECRET>
Apps Script code (click to expand)
// Google Apps Script for Localization Tool Google Sheets live sync.
// Attach this script to your localization Google Sheet (Extensions > Apps Script),
// then deploy it as a Web app and paste the Web App URL into the Unity Localization Tool.
//
// Security: set SECRET to a random string and append ?token=<SECRET> to the Web App URL in Unity.
// Without it, anyone who learns the URL can read or overwrite the sheet.
var SECRET = '';
function getActiveSheet_() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheets = ss.getSheets();
return sheets[0];
}
function isAuthorized_(e) {
return !SECRET || (e && e.parameter && e.parameter.token === SECRET);
}
function toCsv_(values) {
return values.map(function (row) {
return row.map(function (field) {
if (field === null || field === undefined) field = '';
field = String(field);
if (/[",\r\n]/.test(field)) {
field = '"' + field.replace(/"/g, '""') + '"';
}
return field;
}).join(',');
}).join('\n');
}
// GET: return current sheet as CSV (header + rows)
function doGet(e) {
if (!isAuthorized_(e)) {
return ContentService.createTextOutput('Unauthorized').setMimeType(ContentService.MimeType.TEXT);
}
var sheet = getActiveSheet_();
// getDisplayValues returns cell text exactly as shown (no Date/number conversion).
var values = sheet.getDataRange().getDisplayValues();
var csv = toCsv_(values);
return ContentService
.createTextOutput(csv)
.setMimeType(ContentService.MimeType.CSV);
}
// POST: receive CSV body and overwrite sheet contents
function doPost(e) {
if (!isAuthorized_(e)) {
return ContentService.createTextOutput('Unauthorized').setMimeType(ContentService.MimeType.TEXT);
}
if (!e || !e.postData || !e.postData.contents) {
return ContentService
.createTextOutput('No CSV body')
.setMimeType(ContentService.MimeType.TEXT);
}
var csv = e.postData.contents;
var rows = Utilities.parseCsv(csv);
if (!rows || rows.length === 0) {
return ContentService
.createTextOutput('Empty CSV')
.setMimeType(ContentService.MimeType.TEXT);
}
// setValues needs a rectangle; pad short rows.
var width = rows.reduce(function (w, r) { return Math.max(w, r.length); }, 0);
rows = rows.map(function (r) { while (r.length < width) r.push(''); return r; });
var sheet = getActiveSheet_();
sheet.clearContents();
var range = sheet.getRange(1, 1, rows.length, width);
// Plain text format so values like "=x", "+5", "1/2", "TRUE" or "001" are not turned into formulas, numbers or dates.
range.setNumberFormat('@');
range.setValues(rows);
return ContentService
.createTextOutput('OK')
.setMimeType(ContentService.MimeType.TEXT);
}
9. Addressables (Optional)
Load only the current locale’s data at runtime (e.g. French data when the player selects French).
- Install the Addressables package
- In DeepLoc Editor → Settings → Addressables, enable Use Addressables and set the address pattern (e.g.
LocalizationData_{0}) - Click Add Loader to Scene — adds
LocalizationAddressablesLoaderto the scene - Split by Locale — creates one
LocalizationDataasset per language - Add those assets to an Addressables group, set addresses to match the pattern, and build
If Addressables keys are missing, the loader falls back to the manager’s single LocalizationData and logs a setup hint. You can disable Use Addressables on the loader to skip this entirely.
10. Sample Scene
Scene location
Sample/Scenes/DeepLocShowcase.unity
Run it
- Open the scene and press Play
- Use the top bar:
- Language — switches every text, image, sound and prefab in the scene
- Platform — previews per-platform text (e.g. “Click” on desktop, “Tap” on mobile, “Press Ⓐ” on console)
- Pseudo — turns on pseudo-localization to spot untranslated or overflowing text
- Browse the pages on the left rail. Every card lists the keys it uses at the bottom, and a Try it in the Editor box points to the matching page of the DeepLoc Editor.
The status strip at the bottom shows the latest OnLanguageChanged event.
Sample data
The scene’s LocalizationManager uses Sample/Data/SampleLocalizationData.asset, not your project’s Data/LocalizationData.asset, so the demo never adds keys to your own data. All sample keys start with Sample..
- Languages: EN (source), FR, DE, ES, HI, AR, ZH — fully translated, no DeepL key needed
- To inspect or edit it, open Tools → DeepLoc → Editor, then choose the sample asset from the data selector in the toolbar
- Deleting the
Sample/folder removes the scene, its data and every sample asset without touching your project
What each page demonstrates
| Page | Features |
|---|---|
| Welcome | Language buttons built from LocalizationData, current language with a localized sprite |
| Text | Rich-text tags, {0} placeholders via GetTranslationFormat, TextBackgroundScaler, missing-translation placeholder (N/A_<Key>_<LANG>) |
| Assets | LocalizedImage, LocalizedAudioSource (placeholder chimes — swap in real voice lines), LocalizedPrefab (per-language HUD with English fallback) |
| Right-to-left | The same key in English and Arabic, rich text in Arabic, Western vs Arabic-Indic digits (PreserveRtlNumbers) |
| Plurals | Count slider; each language picks its CLDR plural form (GetTranslation(key, count)) |
| Platforms | Platform overrides for one key, resolved for every LocPlatform |
| Code API | GetTranslation, GetAsset<T> and OnLanguageChanged with live results |
| Editor tools | Targets for the Scan page (two deliberately unlocalized labels and one IgnoreLocalization label) and steps for Scripts, DeepL, Sheets, Fonts and Addressables |
Sample scripts
| Script | Role |
|---|---|
Showcase/ShowcaseNavigation.cs |
Left-rail page switching |
Showcase/ShowcaseDynamicText.cs |
Script-driven text: fixed language, format arguments, plural count or platform |
Showcase/ShowcaseLanguageGrid.cs |
Language buttons with native names |
Showcase/ShowcasePlatformSelector.cs, ShowcasePseudoToggle.cs, ShowcaseDigitsToggle.cs |
Top-bar and RTL controls |
Showcase/ShowcaseCodeDemo.cs |
The calls behind the Code API page |
Showcase/ShowcaseTheme.cs, ThemedGraphic.cs |
Color theme (Sample/UI/ShowcaseTheme.asset) |
LocalizationTest.cs |
Intentional hardcoded strings for Script Scanner testing (not in the scene) |
11. Troubleshooting
| Problem | Fix |
|---|---|
| Translations not showing | Check the key on LocalizedText; click Refresh scene text on the Home page |
Text shows N/A_Key_LANG |
Add the missing translation in LocalizationData |
| Language not switching | Assign LocalizationData to LocalizationManager |
| Addressables errors in console | Disable Use Addressables on the loader, or complete Split by Locale setup |
| Arabic / rich text corrupted | Enable Variable Protection, re-translate the entry |
| Missing glyphs (boxes/squares) | Run Fix Fonts and Validate Glyphs on the Fonts page |
| Hindi vowel signs in the wrong place (e.g. “ि” after its consonant) or conjuncts with a visible virama | Click Bake conjuncts on the Fonts page, and show the text through LocalizedText or DisplayTextShaper (see Devanagari in Section 5) |
| API key not working | Use Test API Key in DeepLoc Editor |
File Reference
| Path | Purpose |
|---|---|
Data/LocalizationData.asset |
Main localization data |
Data/HardcodedStringsData.asset |
Script scanner results |
Prefabs/LocalizationManager.prefab |
Drop into any scene |
Sample/Scenes/DeepLocShowcase.unity |
Sample scene |
Sample/Data/SampleLocalizationData.asset |
Sample data (7 languages, Sample.* keys) |
Core/Generated/LocKeys.cs |
Optional key constants (Generate LocKeys) |