SimpleLocalizations 0.1.0
See the version list below for details.
dotnet add package SimpleLocalizations --version 0.1.0
NuGet\Install-Package SimpleLocalizations -Version 0.1.0
<PackageReference Include="SimpleLocalizations" Version="0.1.0" />
<PackageVersion Include="SimpleLocalizations" Version="0.1.0" />
<PackageReference Include="SimpleLocalizations" />
paket add SimpleLocalizations --version 0.1.0
#r "nuget: SimpleLocalizations, 0.1.0"
#:package SimpleLocalizations@0.1.0
#addin nuget:?package=SimpleLocalizations&version=0.1.0
#tool nuget:?package=SimpleLocalizations&version=0.1.0
SimpleLocalizations
Localized text whose identity is stored.
Most localization libraries answer "what does this string say in the reader's language." This one answers a narrower question: what do you do when the text is also a key — when a verdict is filed against a record's title, a rule matches on it, or a row in a database points at it. Translate that text in place and every stored reference is orphaned. Not a crash; a quieter kind of wrong.
The answer here is three rules, and the library exists to make the compiler hold them:
- Keys are identity, words are rendered late. Nothing localizes text where a record is produced. The neutral English is stored, and the reader's culture is resolved from the key at the read edge.
- A
.resxis the one declaration of both. A source generator turns each resource into typed key members, so a key renamed there moves its callers, and one no longer authored stops compiling. There is no second list. - The vocabulary is closed. A key type's factory is reachable by its generated declaration and nowhere else, so no call site can invent an identity that resolves to no text and reads as a finished record.
Getting started
<PackageReference Include="SimpleLocalizations" Version="0.1.0" />
Declare a key type — one per kind of thing you key, because a key of one kind standing in for another resolves to nothing and falls back without a word of complaint. You declare the name; the generator writes the body, because the shape is the rule — a private constructor and one factory are what keep the vocabulary closed, and a hand-rolled type that grew a public constructor would open it again silently:
using SimpleLocalizations;
[VocabularyKey]
public readonly partial record struct FindingKey;
Author the words in a .resx, keyed family.name, lowercase and dotted:
<data name="cookies.insecure" xml:space="preserve">
<value>Session cookie sent without Secure</value>
<comment>Becomes the generated member's XmlDoc.</comment>
</data>
Point the generator at it:
<ItemGroup>
<VocabularyResource Include="Localization/SecurityStrings.resx"
VocabularyClass="SecurityKeys"
VocabularyNamespace="MyApp.Security"
VocabularyKeyType="MyApp.FindingKey" />
</ItemGroup>
And name the member, never the key:
var cultures = new TextCultures("en-US", "en-GB", "sv-SE");
var catalog = cultures.Catalog("MyApp.Security.SecurityStrings", typeof(Thing).Assembly);
var stored = catalog.Neutral(SecurityKeys.Cookies.Insecure.Key); // what you persist
var shown = catalog.Get(SecurityKeys.Cookies.Insecure.Key); // what this reader sees
Declaring a vocabulary
Everything but the keys themselves is metadata on the VocabularyResource item. Metadata carries over from
the source item, so an EmbeddedResource glob and the generator read one declaration.
| Metadata | What it settles |
|---|---|
VocabularyClass |
The generated static class's name. |
VocabularyNamespace |
Where it lands. |
VocabularyKeyType |
One default type, optionally followed by family=Type entries. |
VocabularyDerived |
Suffixes a read edge appends to another key, so no member is generated for them. |
Everything else is declared in code, on the type it is about, so a typeof cannot go stale and no name
is written twice:
[VocabularyFamily("category")] // every member needs a `category.{member}` key — SL1011
public enum FindingCategories { Tls, Stack, Company }
[VocabularyKey(Families = typeof(FindingCategories))] // a key of this type is filed under one — SL1012
public readonly partial record struct FindingKey;
Every list-shaped value is separated by
|, never;. The metadata reaches the generator through a generated.editorconfig, where;begins a comment — so a declaration written with MSBuild's own list separator arrives truncated to its first entry, generates the wrong key types, and compiles clean.SL1009catches it in MSBuild, which is the last place the whole value still exists.
Generated shape
cookies.insecure becomes SecurityKeys.Cookies.Insecure. Each family also carries a
Prefix and a Covers(key), so matching a family is a member rather than a hand-written constant and a
StartsWith at the call site. A resx <comment> becomes the member's XmlDoc.
An empty value is not a missing translation — it is the declaration that the identity is yours and the
words are the data's (a record titled by the thing it detected, a contact titled by a person's name). The
key is still generated, because it is stored and matched; TryGet reads it as absent so a read edge falls
back to what was stored.
The unsuffixed resx is the neutral set: it holds every key, a culture file holds only what differs, and
ResourceManager falls back — which makes a culture file an override list rather than a copy. A key absent
from every culture throws, deliberately: text rendering as its own key is how a half-translated build reaches
a customer.
Diagnostics
| Id | What it refuses |
|---|---|
SL1001 |
A key that is not two or more lowercase dotted segments of letters, digits and hyphens. |
SL1002 |
A key that is a proper prefix of another — the segment is the family others nest under. |
SL1003 |
A resource marked as a vocabulary with no VocabularyKeyType. |
SL1004 |
A resource the generator cannot read: unparseable, or a root that is not a resx <root>. |
SL1005 |
A readable resource authoring no key. |
SL1006 |
A VocabularyKeyType that is not one default optionally followed by family=Type entries. |
SL1007 |
Two keys whose segments produce one member name. |
SL1008 |
A segment naming the class it would be emitted into. |
SL1009 |
A list-shaped declaration containing ;, which never reaches the generator. |
SL1010 |
A From factory reached anywhere but its generated declaration. |
SL1011 |
A member of a declared set with no key authored for it. |
SL1012 |
A key of the declared type whose family names no member of the declared set. |
SL1013 |
A [VocabularyKey] type that is not partial, so its body cannot be written. |
They ship as warnings. Escalate them in the projects that want them fatal — TreatWarningsAsErrors, or
<WarningsAsErrors>SL1001;SL1002;…</WarningsAsErrors> for the set. Two caveats worth knowing:
SL1003–SL1006fire when the generator emits nothing. Left as warnings, the build then fails with a pile ofCS0117/CS0246at call sites naming no resource file — which is the failure they exist to prevent. Escalating them is strongly recommended.SL1009is an MSBuild warning, not a compiler one, soTreatWarningsAsErrorsdoes not reach it. SetMSBuildTreatWarningsAsErrorsto make it fatal.
What is not here
- A read edge. Resolving a stored record's title through a chain of registered sources is shaped by what
your records are;
ILocalizedTextSourceis the seam, and composing it is yours. - A refusal's wording.
TextCultures.TryResolveanswersbooland no message. This package authors no resource your reader can translate, and nothing may word a refusal itself — which is also whyLocalizedText's constructor is private and its one factory takes a catalog and a key. - Cultures beyond
en-US,en-GBandsv-SEforListFormatter. The list patterns are the package's own resource set; a consumer needing another ships a satellite beside it.
Why a list formatter
string.Join(", ", items) is not a translation-neutral operation and looks like one. en-US writes the serial
(Oxford) comma before the final conjunction and en-GB does not, so the same call has to produce a, b, and c
for one reader and a, b and c for the other. .NET ships no list formatter; ICU and Java both do.
Only for lists read as prose. An enumeration after a colon is punctuation rather than grammar and should stay a plain join.
License
MIT.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.