Skip to content

kids.kapish.localisation — Developer Guide

unity

Package identity

FieldValue
Namekids.kapish.localisation
Display nameCarrot.Localisation
Version0.1.0
Unity6000.0+
LicenseMIT

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.cs

Runtime 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:

  1. Current locale — if set and the key exists, return it.
  2. Fallback locale — if different from current and the key exists, return it.
  3. Provided per-call fallback — the fallback arg or the Fallback field of the serialized struct.
  4. Emptystring.Empty for strings, null for 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 when SetLocale / TrySetLocale succeeds 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:

  1. Refresh() cancels any in-flight load (cts.Cancel()), nulls the cts, and creates a new one.
  2. The async LoadAndApply snapshots the previous handle, awaits the new load on the fresh token.
  3. If the token is cancelled mid-flight, the newly-loaded handle is disposed and the method returns without touching target.
  4. On successful load, the new handle is assigned to currentHandle.
  5. The finally block 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:

  1. The CMS publishes locale-manifest.json to the addressable build.
  2. Game invokes Localisation.Instance.LoadAllAsync() on boot.
  3. ManifestLocaleLoader downloads and parses the manifest, then downloads each LocaleData referenced by dataAddressableKey.
  4. Each downloaded locale is registered via Localisation.Register, firing LocaleRegistered.
  5. A UI language picker enumerates Localisation.AvailableLocales and calls SetLocale on 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 referencesLocalisedString must be embeddable in any other serialized type without assigning a separate asset. The implicit string conversion keeps call sites clean.
  • Implicit string on LocalisedString only — 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 BuiltInLocaleLoader for 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.Format or build a richer formatter on top.

Dependencies

PackageVersionPurpose
kids.kapish0.1.0MonoSingleton, CarrotObject base types
kids.kapish.signals0.1.0Signal<T> for change notifications
kids.kapish.addressables0.1.0AssetLoader, AssetHandle<T>, AssetAddress
com.unity.textmeshprobuilt-inTMP_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 DirectionLeftToRight or RightToLeft.
  • Strings — array of Key / Value pairs, inline (the Value field is TextArea, so multi-line copy is fine).
  • Audio / Sprites / Textures — array of Key / AddressableKey pairs. The addressable key is what kids.kapish.addressables will load when the key is resolved.
  • Font Set (optional) — a LocaleFontSet asset carrying a TMP font + legacy Font.

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 LocaleData assets shipped with the build.
  • Auto Load On Start — if true, LoadAllAsync runs automatically in Start().

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 every LocaleData in 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.
  • LocalisedString property drawer — renders Key + Fallback side-by-side in any serialized field.

Carrot