لغة - Arabic and Swahili for language: lugha
Typed localisation for .NET 10 - compile-time enforced text contracts with CLDR pluralisation and bidirectional text support.
Zero string keys. Zero resource files. Zero runtime lookups. Zero ambient state.
Most .NET localisation approaches - .resx/ResourceManager, .resw/ResourceLoader, IStringLocalizer - rely on string keys. Nothing connects definition site and usage site at compile time.
| Failure mode | Cause | Detection |
|---|---|---|
| Missing translation | Key absent in target locale | Runtime (silent fallback) |
| Orphaned resource | Key removed from code, retained in file | Never |
| Key typo | "StausDiscovering" vs "StatusDiscovering" |
Runtime (silent) |
| Parameter mismatch | "Connected to {0}" in EN, "Connecte" (missing {0}) in FR |
Runtime |
| Missing string | New feature, no resource entry | Runtime (empty string) |
| Ambient culture | ResourceManager reads CultureInfo.CurrentUICulture |
Non-deterministic |
Lugha eliminates the entire category.
Text contracts are interfaces. Locales are implementations. The compiler enforces exhaustiveness.
Interface member added -> Every locale fails to compile until implemented.
Interface member removed -> Every locale providing it fails to compile.
Parameter signature changed -> Every locale fails to compile until matched.
A text scope is an interface defining the text surface for a bounded domain. Properties return invariant text (labels, titles, static messages). Methods return parameterised text (formatted messages, interpolated values). All members must return string - the Roslyn analyser LGH001 (severity: Error) enforces this.
public interface IConnectionText : ITextScope
{
string Discovering { get; }
string Connecting(string host);
string Connected(string host);
string Unavailable(string reason);
}
public interface INavigationText : ITextScope
{
string Dashboard { get; }
string Directory { get; }
string Catalogue { get; }
string Settings { get; }
}ITextScope is a marker interface. Its purpose:
- The Lugha analyser and source generator scan for
ITextScopederivatives. - Generic constraints:
where TScope : ITextScope. - Self-documenting: any interface extending
ITextScopeis recognisable as a text contract.
A locale composes all text scope implementations for a specific language/region. Locale instances are constructed once and reused. Text scope implementations must be stateless and pure.
public interface IAppLocale : ILocale
{
IConnectionText Connection { get; }
INavigationText Navigation { get; }
IDirectoryText Directory { get; }
ICommandText Commands { get; }
}ILocale<TCardinal, TOrdinal> binds independent cardinal and ordinal rule sets with default interface methods that enforce non-negative counts. Cardinal and ordinal rules are separate type parameters because languages that share cardinal rules frequently diverge on ordinals (e.g. Russian and Ukrainian share EastSlavicCardinal but have different ordinal systems).
public sealed class EnGbLocale : IAppLocale, ILocale<OneOtherCardinal, EnglishOrdinal>
{
private static readonly CultureInfo EnGb =
CultureInfo.GetCultureInfo("en-GB");
public CultureInfo Culture => EnGb;
public IConnectionText Connection { get; } = new EnGbConnectionText();
public INavigationText Navigation { get; } = new EnGbNavigationText();
public IDirectoryText Directory { get; } = new EnGbDirectoryText(EnGb);
public ICommandText Commands { get; } = new EnGbCommandText();
}CLDR evidence (from ordinals.xml):
| Language | Cardinal rule | Ordinal rule | Can share single struct? |
|---|---|---|---|
| English (en) | one/other | one/two/few/other | No - unique ordinals |
| German (de) | one/other | other-only | No - different ordinal than English |
| Swedish (sv) | one/other | one/other (n%10=1,2 and n%100!=11,12) | No - different ordinal than German |
| Italian (it) | one/other | many/other (8,11,80,800) | No - unique ordinals |
| Russian (ru) | one/few/many/other | other-only | No - different ordinal than Ukrainian |
| Ukrainian (uk) | one/few/many/other | few/other (n%10=3 and n%100!=13) | No - different ordinal than Russian |
| French (fr) | one/other | one/other (n=1) | No - different ordinal than Spanish |
A single IPluralRules<TSelf> forced incorrect groupings. The split eliminates this entirely.
IAppLocale uses property composition, not interface inheritance.
| Flat inheritance | Property composition |
|---|---|
locale.Dashboard - ambiguous origin |
locale.Navigation.Dashboard - unambiguous |
| All members on one surface - polluted autocomplete | Nested access - scoped autocomplete |
| Name collisions require disambiguation | Impossible - each scope is a separate type |
| Cannot pass a single scope to a component | IConnectionText is independently referenceable |
- Locale text implementations carry only behaviour (returning strings), not data.
- Interface implementation is enforced unconditionally by the compiler.
- Interfaces allow locale implementations to spread across files/classes naturally.
- The JIT devirtualises single-implementation interface calls - zero overhead for single-locale applications. For multi-locale applications, the cost is dwarfed by string formatting. PGO further assists monomorphic sites.
| Aspect | .resx / ResourceManager |
IStringLocalizer |
Lugha |
|---|---|---|---|
| Missing key | Runtime empty string | ResourceNotFound |
Compile error |
| Missing locale coverage | Silent | Silent | Compile error |
| Parameter mismatch | Runtime FormatException |
Runtime FormatException |
Compile error |
| Ambient culture | CurrentUICulture |
CurrentUICulture |
None (explicit) |
| Runtime lookup cost | Hashtable + assembly probe | Hashtable + assembly probe | Zero (direct call) |
| Pluralisation | Manual | None | CLDR-typed (cardinal + ordinal) |
| RTL support | None | None | Bidi isolation (string + span) |
| Framework coupling | ResourceManager |
IServiceCollection |
None |
| Hot-path allocation | String per lookup | String per lookup | Zero (TryFormat + TryIsolate) |
Hand-authored locales (recommended starting point): install Lugha alone and follow the quick start guide.
Existing .resx or .resw files: install Lugha and Lugha.Import.Resx. The source generator converts resource files to typed text scopes at compile time. See Lugha.Import.Resx.
Existing Gettext .po/.pot files: install Lugha and Lugha.Import.Gettext. See Lugha.Import.Gettext.
Design-time CLI import: install the Lugha.Cli global tool for one-off file conversion. See Lugha.Cli.
WinUI 3 runtime language switching: install Lugha.WinUI for the reactive locale host and registry. See Lugha.WinUI.
Working sample: the WinUI sample app demonstrates Gettext import, four locales, runtime switching, RTL layout, and pluralisation in a packaged MSIX application.
| Package | Description |
|---|---|
Lugha |
Core runtime library, Roslyn analysers, and source generators. API documentation. |
Lugha.Analysers |
Roslyn diagnostic analysers (LGH001, LGH003-LGH008). Packed into Lugha. Documentation. |
Lugha.Generators |
Incremental source generators (LGH002, LocaleManifest). Packed into Lugha. Documentation. |
Lugha.Common |
Shared types (language-to-CLDR-rule mapping) for the import ecosystem. Documentation. |
Lugha.Import |
Shared import library - parsers and code emitter for converting translation files to typed source. Documentation. |
Lugha.Import.Gettext |
Source generator for GNU Gettext .po/.pot files. Documentation. |
Lugha.Import.Resx |
Source generator for .resx/.resw resource files. Documentation. |
Lugha.Cli |
.NET global tool for design-time translation import. Documentation. |
Lugha.WinUI |
WinUI 3 integration - reactive locale host and registry. Documentation. |
| Sample | Description |
|---|---|
Lugha.Samples.WinUI |
Packaged WinUI 3 app with four locales, Gettext import, runtime switching, RTL, and plurals. Documentation. |
- Pure functions. Every Lugha API is a pure function. No I/O, no ambient state.
CultureInfois always an explicit parameter, never read fromCultureInfo.CurrentUICulture. - Thread-safe. All Lugha types are thread-safe. Locale instances may be shared freely across threads.
- Zero runtime dependencies. All CLDR rules are hand-implemented as pure functions. The only dependency is the .NET 10 BCL.
- Framework-agnostic. No framework coupling. WinUI, WPF, Blazor, console - any host works. For WinUI/WPF,
x:BindwithMode=OneTimeevaluates text scope properties once at load; missing members fail the XAML codegen build. - Total functions. Every function returns a valid result for all inputs. Negative
countvalues are clamped to zero. Errors are values (Result<T,E>), never exceptions.
- .NET 10 SDK 10.0.100 or later
- C# 14 (uses
fieldkeyword, extension blocks,static abstractinterface members, default interface methods)
dotnet build
dotnet test