Appearance
kids.kapish.localisation — Developer Guide
unity
Package identity
| Field | Value |
|---|---|
| Name | kids.kapish.localisation |
| Display name | Carrot.Localisation |
| Version | 0.1.0 |
| Unity | 6000.0+ |
| License | MIT |
Assembly structure
kids.kapish.localisation/
Runtime/
Carrot.Localisation.asmdef → Carrot.Localisation namespace
Localisation.cs → MonoSingleton manager
LocaleData.cs → ScriptableObject + entry structs
LocaleFontSet.cs → Optional per-locale fonts
LocaleManifest.cs → JSON manifest types
ILocaleLoader.cs → Loader contract
TextDirection.cs → LTR/RTL enum
LocalisedString.cs → Serializable string ref
LocalisedAudioKey.cs → Serializable audio ref
LocalisedSpriteKey.cs → Serializable sprite ref
LocalisedTextureKey.cs → Serializable texture ref
Loaders/
BuiltInLocaleLoader.cs
AddressableLocaleLoader.cs
ManifestLocaleLoader.cs
Components/
LocalisedText.cs → TMP
LocalisedTextUGui.cs → Legacy UGUI
LocalisedImage.cs → Sprite via addressables
LocalisedRawImage.cs → Texture2D via addressables
Editor/
Carrot.Localisation.Editor.asmdef → Editor-only
LocaleDataEditor.cs
LocalisedStringDrawer.cs
LocalisationMenu.csRuntime references
Carrot, Carrot.Precompiled, Carrot.Signals, Carrot.Addressables, Unity.Addressables, Unity.ResourceManager, Unity.TextMeshPro
Editor references
Carrot, Carrot.Localisation, Carrot.Editor
Architecture
The four-asset-type design
Strings are small, frequently-accessed, and should resolve synchronously — so they live inline on LocaleData and are cached in a Dictionary<string, string>.
Audio, sprites, and textures are large enough that baking them into the locale asset would bloat memory and defeat addressable streaming. So the locale stores only keys → addressable keys. The serializable reference structs (LocalisedAudioKey, LocalisedSpriteKey, LocalisedTextureKey) resolve the key against the current locale, then hand off to kids.kapish.addressables for the actual load.
This split keeps LocaleData assets cheap to load and keeps the hot path for strings allocation-free after the first cache build.
Lookup order
Every Get* call and every Has* call walks the same chain:
- Current locale — if set and the key exists, return it.
- Fallback locale — if different from current and the key exists, return it.
- Provided per-call fallback — the
fallbackarg or theFallbackfield of the serialized struct. - Empty —
string.Emptyfor strings,nullfor addressable keys.
The fallback locale is declared on the Localisation MonoSingleton (fallbackLocaleCode, defaults to en-GB). The per-call fallback is the last line of defence — typically a hardcoded English string or a baseline addressable key that ships with the game.
Signal flow
Localisation exposes three signals from kids.kapish.signals:
LocaleChanged<string>— fired whenSetLocale/TrySetLocalesucceeds with a new code.LocaleRegistered<LocaleData>— fired when a locale is added to the manager.LocaleUnregistered<LocaleData>— fired on removal.
UI components subscribe to LocaleChanged in OnEnable and unsubscribe in OnDisable. The unsubscribe function is stored on the component (returned by Signal<T>.Add) so there's no need to keep a strong reference to the handler.
Async image loading with handle management
LocalisedImage and LocalisedRawImage own both an AssetHandle<T> and a CancellationTokenSource. The refresh flow is:
Refresh()cancels any in-flight load (cts.Cancel()), nulls thects, and creates a new one.- The async
LoadAndApplysnapshots the previous handle, awaits the new load on the fresh token. - If the token is cancelled mid-flight, the newly-loaded handle is disposed and the method returns without touching
target. - On successful load, the new handle is assigned to
currentHandle. - The
finallyblock disposes the previous handle — so rapid locale switches never leak.
On OnDisable, both the pending cts and the currentHandle are disposed. This keeps the component safe to toggle, re-parent, or pool without leaking addressable references.
Manifest-driven runtime extension
ManifestLocaleLoader is the primary integration point for a content platform / CMS workflow:
- The CMS publishes
locale-manifest.jsonto the addressable build. - Game invokes
Localisation.Instance.LoadAllAsync()on boot. ManifestLocaleLoaderdownloads and parses the manifest, then downloads eachLocaleDatareferenced bydataAddressableKey.- Each downloaded locale is registered via
Localisation.Register, firingLocaleRegistered. - A UI language picker enumerates
Localisation.AvailableLocalesand callsSetLocaleon the user's choice.
Because the manifest is just JSON and is itself addressable, new locales can ship without rebuilding the game — just publish a new manifest + locale bundles through the same content pipeline. The version field on LocaleManifestEntry is there to support cache-busting or A/B rollouts.
Why MonoSingleton
Localisation inherits MonoSingleton<Localisation> from kids.kapish. Localisation is inherently global, session-scoped state — not a per-scene concern. The singleton simplifies access from LocalisedString.Resolve() (which runs from value types without a DI container) and from the bound components. Signals handle the reactivity; the singleton handles the access.
Caching strategy on LocaleData
LocaleData rebuilds its four dictionaries in OnEnable and OnValidate. OnValidate keeps editor previews in sync when you edit entries in the inspector. EnsureCache() guards the first access in case neither fired yet. If you mutate entries at runtime (e.g. patching a locale), call Rebuild() explicitly.
Design decisions
- Serializable structs, not ScriptableObjects, for references —
LocalisedStringmust be embeddable in any other serialized type without assigning a separate asset. The implicitstringconversion keeps call sites clean. - Implicit
stringonLocalisedStringonly — audio/sprite/texture keys resolve to addressable keys, not the assets themselves, so implicit conversion would be misleading. - Loaders are constructor-configured, added at runtime — no ScriptableObject loader configs. Loaders are cheap objects, and runtime composition is the normal path.
- Built-in locales are a serialized array on the manager, not a loader — by far the common case; avoiding the ceremony of constructing a
BuiltInLocaleLoaderfor every project. - No translation tables at build time — strings live on
LocaleData, not in a central TSV/CSV. Multiple locale assets can be authored in parallel without merge conflicts on a shared table. - No pluralisation / interpolation helpers here — keep the core simple. Consumers can compose
string.Formator build a richer formatter on top.
Dependencies
| Package | Version | Purpose |
|---|---|---|
kids.kapish | 0.1.0 | MonoSingleton, CarrotObject base types |
kids.kapish.signals | 0.1.0 | Signal<T> for change notifications |
kids.kapish.addressables | 0.1.0 | AssetLoader, AssetHandle<T>, AssetAddress |
com.unity.textmeshpro | built-in | TMP_Text, TMP_FontAsset |
Usage Guide
Creating a LocaleData asset
Right-click in the Project window → Create > Carrot > Localisation > Locale Data.
Fill in:
- Code — BCP 47 tag, e.g.
en-GB,fr-FR,ja-JP. This is the lookup key. - Display Name — English-language name for admin UIs (
"French"). - Display Name Native — native-script name for pickers (
"Français"). - Text Direction —
LeftToRightorRightToLeft. - Strings — array of
Key/Valuepairs, inline (theValuefield isTextArea, so multi-line copy is fine). - Audio / Sprites / Textures — array of
Key/AddressableKeypairs. The addressable key is whatkids.kapish.addressableswill load when the key is resolved. - Font Set (optional) — a
LocaleFontSetasset carrying a TMP font + legacyFont.
Then create a LocaleFontSet the same way (Create > Carrot > Localisation > Locale Font Set) if the locale needs specific fonts (CJK, Arabic, Devanagari, etc.).
Bootstrap flow
Drop a Localisation component into your bootstrap scene. Configure:
- Default Locale Code — what the user starts on (e.g.
en-GB). - Fallback Locale Code — used when a key is missing in the current locale.
- Built-In Locales — array of
LocaleDataassets shipped with the build. - Auto Load On Start — if true,
LoadAllAsyncruns automatically inStart().
For manual control:
csharp
using Carrot.Localisation;
using Carrot.Localisation.Loaders;
public class LocalisationBootstrap : MonoBehaviour
{
[SerializeField] private string manifestKey = "localisation/manifest";
async void Start()
{
// Add any runtime-configured loaders before LoadAllAsync
Localisation.Instance.AddLoader(new ManifestLocaleLoader(manifestKey));
await Localisation.Instance.LoadAllAsync();
Debug.Log($"Registered {Localisation.Instance.RegisteredCount} locale(s).");
}
}Switching language
csharp
// Fire-and-forget — throws if the code isn't registered
Localisation.Instance.SetLocale("fr-FR");
// Safe variant
if (!Localisation.Instance.TrySetLocale("ja-JP"))
{
Debug.LogWarning("Japanese not available yet.");
}Every bound component refreshes automatically via the LocaleChanged signal.
Using LocalisedString
Use it anywhere you'd normally hold a string:
csharp
public class TooltipPresenter : MonoBehaviour
{
[SerializeField] private LocalisedString body = new("tooltip.inventory.body",
"Press I to open inventory.");
void ShowTooltip()
{
// Implicit conversion — resolves against current locale on read
TooltipService.Show(body);
}
}Explicit resolution, useful for formatting:
csharp
[SerializeField] private LocalisedString greeting = new("ui.greeting", "Hello, {0}!");
void Greet(string playerName)
{
string line = string.Format(greeting.Resolve(), playerName);
ChatLog.Append(line);
}Binding UI components
LocalisedText (TMP)
Add Carrot/Localisation/Localised Text (TMP) to a GameObject that already has a TMP_Text. The target auto-assigns via Reset(). Set the key in the inspector; it refreshes on enable and on every locale change.
Tick Apply Font From Locale if you want the component to swap TMP_FontAsset based on the current locale's LocaleFontSet.
csharp
// Runtime change
myLocalisedText.SetKey("menu.play");
// Or assign a whole struct
myLocalisedText.Text = new LocalisedString("menu.quit", "Quit");LocalisedTextUGui (legacy)
Same pattern against UnityEngine.UI.Text. Prefer TMP unless you have a reason not to.
LocalisedImage (sprites)
Add Carrot/Localisation/Localised Image next to a UGUI/Image. Configure a LocalisedSpriteKey. The component loads the sprite via addressables on enable, swaps it on locale change, and disposes the previous handle automatically.
csharp
myLocalisedImage.SetKey("banner.seasonal");LocalisedRawImage (textures)
Same pattern against UGUI/RawImage with a LocalisedTextureKey.
LocalisedAudioKey with an AudioManager
Audio keys resolve to an addressable key that feeds your audio pipeline:
csharp
public class VoiceBarks : MonoBehaviour
{
[SerializeField] private LocalisedAudioKey greeting = new("vo.npc.greeting", "vo/fallback/greeting");
async void Play()
{
// Resolves current locale → fallback locale → provided addressable key
using var handle = await greeting.LoadAsync();
AudioManager.PlayOneShot(handle.Asset);
}
}If your AudioManager prefers to drive its own loading, use ResolveAddressableKey() directly:
csharp
string key = greeting.ResolveAddressableKey();
AudioManager.PlayByAddressable(key);Manifest flow for CDN-driven locales
Publish a manifest JSON through your content pipeline:
json
{
"locales": [
{
"code": "de-DE",
"displayName": "German",
"displayNameNative": "Deutsch",
"dataAddressableKey": "localisation/locales/de-DE",
"version": "2026.04.1",
"textDirection": "LeftToRight"
},
{
"code": "ar-SA",
"displayName": "Arabic",
"displayNameNative": "العربية",
"dataAddressableKey": "localisation/locales/ar-SA",
"version": "2026.04.1",
"textDirection": "RightToLeft"
}
]
}Register the loader and await:
csharp
Localisation.Instance.AddLoader(new ManifestLocaleLoader("localisation/manifest"));
await Localisation.Instance.LoadAllAsync();React to new locales arriving (e.g. to rebuild a picker):
csharp
Localisation.Instance.LocaleRegistered.Add(locale =>
Debug.Log($"New locale available: {locale.Code} — {locale.DisplayNameNative}"));Populate a language picker:
csharp
foreach (LocaleData locale in Localisation.Instance.AvailableLocales)
{
picker.AddOption(locale.DisplayNameNative, () => Localisation.Instance.SetLocale(locale.Code));
}Custom loaders
Implement ILocaleLoader for bespoke sources — a remote JSON endpoint you control, a save file, etc.:
csharp
using System.Threading;
using Carrot.Localisation;
using UnityEngine;
public class PlayerProfileLocaleLoader : ILocaleLoader
{
public string Name => "PlayerProfile";
public async Awaitable<LocaleData[]> LoadLocalesAsync(CancellationToken ct = default)
{
// e.g. fetch an entitlement list and map to addressable keys,
// or deserialize user-modded locale files from disk.
LocaleData[] locales = await MyBackend.FetchEntitledLocalesAsync(ct);
return locales;
}
}
// Registration
Localisation.Instance.AddLoader(new PlayerProfileLocaleLoader());
await Localisation.Instance.LoadAllAsync();Loaders are composable — LoadAllAsync runs every registered loader in order, registering returned locales as they arrive. Exceptions from a single loader are logged and don't abort the others (except OperationCanceledException, which propagates).
Runtime registration / unregistration
You can register and unregister locales at any time — useful for DLC, mod support, or hot-swapping locale bundles:
csharp
Localisation.Instance.Register(myRuntimeLocaleData);
// Later
Localisation.Instance.Unregister("de-DE");If you unregister the current locale, the manager automatically switches to the fallback (if registered).
Editor tooling
Tools > Carrot > Localisation > Find Missing Keys— scans everyLocaleDatain the project, builds the union of all keys across all four tables, and reports which keys are missing from each locale. Great pre-ship check.Tools > Carrot > Localisation > Validate Locales— flags duplicate codes, empty codes, and missing display names.- LocaleData inspector — shows a summary header, a search filter for the strings table, and a "Rebuild Caches" button.
LocalisedStringproperty drawer — renders Key + Fallback side-by-side in any serialized field.