Rosetta is a lightweight, backend-agnostic localization library for .NET. It works anywhere — Unity, custom engines, desktop apps, and games.
dotnet add package Prowl.Rosetta// 1. Configure once at startup
Loc.Configure(cfg => cfg
.SetFallbackLocale("en")
.AddProvider(new JsonFileProvider("Localization/{locale}.json"))
.SetLocale("fr")
);
// 2. Look up strings anywhere
string text = Loc.Get("ui.button.confirm");That's it. Loc.Get is a static accessor so you can call it from anywhere without dependency injection.
string text = Loc.Get("ui.button.confirm");
// → "Confirm"Named placeholders — not positional {0} — so translators always have context.
string text = Loc.Get("player.killed", new { name = "Zara", count = 3 });
// → "Zara defeated 3 enemies"Rosetta follows Unicode CLDR plural rules, so languages with complex plural forms (Russian, Arabic, Polish) work correctly out of the box.
string text = Loc.GetPlural("item.collected", count: 1); // → "1 item"
string text = Loc.GetPlural("item.collected", count: 7); // → "7 items"
// With interpolation
string text = Loc.GetPlural("enemy.remaining", count: 3, new { zone = "Northgate" });
// → "3 enemies remain in Northgate"string text = Loc.GetGender("npc.greeting", gender: Gender.Female);
// → "She smiles at you."Rosetta uses flat JSON by default. Keys use dot notation. Pluralization and gender are expressed as nested objects.
{
"ui.button.confirm": "Confirm",
"player.killed": "{{name}} defeated {{count}} enemies",
"item.collected": {
"one": "{{count}} item",
"other": "{{count}} items"
},
"npc.greeting": {
"male": "He nods at you.",
"female": "She smiles at you.",
"other": "They glance over."
}
}Providers are the source of truth for translations. They are tried in order, with later providers overriding earlier ones on key conflicts. This lets you ship bundled strings and override them from a remote backend.
Rosetta ships with the following built-in providers:
| Provider | Description |
|---|---|
JsonFileProvider |
Loads .json files from disk. Path supports {locale} token. |
EmbeddedResourceProvider |
Loads from embedded assembly resources. Good for libraries and engines. |
InMemoryProvider |
Loads from a Dictionary<string, string>. Useful for testing. |
File-based:
Loc.Configure(cfg => cfg
.SetFallbackLocale("en")
.AddProvider(new JsonFileProvider("Localization/{locale}.json"))
.SetLocale("fr")
);Prowl Engine (embedded resources):
Loc.Configure(cfg => cfg
.SetFallbackLocale("en")
.AddProvider(new EmbeddedResourceProvider(Assembly.GetExecutingAssembly()))
.SetLocale("en")
.OnMissingKey(MissingKeyBehavior.ReturnKey) // never crash in editor
);Hybrid — local fallback with remote overrides:
Loc.Configure(cfg => cfg
.SetFallbackLocale("en")
.AddProvider(new JsonFileProvider("Localization/{locale}.json")) // bundled baseline
.AddProvider(new MyRemoteProvider()) // remote overrides
);// Switch at runtime — reloads and fires LocaleChanged
Loc.SetLocale("ja");
// Subscribe to locale changes for UI refresh
Loc.LocaleChanged += (oldLocale, newLocale) => RefreshAllUI();Temporarily override the locale without changing the global setting — useful for previewing translations or localizing a specific NPC.
using (var scope = Loc.BeginScope("de"))
{
string preview = Loc.Get("ui.title"); // German, inside the scope
}
// Global locale restored outside the using blockControl what happens when a key is not found.
.OnMissingKey(MissingKeyBehavior.ReturnKey) // returns "ui.button.confirm" — safe for editors
.OnMissingKey(MissingKeyBehavior.ReturnFallback) // tries the fallback locale first
.OnMissingKey(MissingKeyBehavior.Throw) // throws KeyNotFoundException — strict modeYou can also hook into missing key events to track untranslated strings:
Loc.MissingKey += (locale, key) =>
{
Analytics.Track("missing_translation", new { locale, key });
};Implementing your own provider requires a single interface:
public class MyCustomProvider : ILocalizationProvider
{
public IReadOnlyDictionary<string, string> Load(string locale)
{
// Return a flat key → value dictionary for this locale
return FetchFromMyBackend(locale);
}
public bool SupportsLocale(string locale) =>
_supportedLocales.Contains(locale);
}Then register it like any built-in provider:
.AddProvider(new MyCustomProvider())To force a reload of all cached translations — for example after a live content update:
Loc.Reload();- One static entry point.
Loc.Getworks from anywhere — no service locator, no constructor injection required. - Provider order is override order. Bundle your baseline translations and let later providers win on conflict.
- Named placeholders only.
{{name}}instead of{0}keeps translation files readable for non-developers. - CLDR pluralization. Plural rules are locale-aware from the start. You don't bolt this on later.
- Never throws in production. The default missing key behaviour is
ReturnFallbackthenReturnKey. Your game keeps running.