H073.Local.Tool
2.0.0-alpha.2
Prefix Reserved
dotnet tool install --global H073.Local.Tool --version 2.0.0-alpha.2
dotnet new tool-manifest
dotnet tool install --local H073.Local.Tool --version 2.0.0-alpha.2
#tool dotnet:?package=H073.Local.Tool&version=2.0.0-alpha.2&prerelease
nuke :add-package H073.Local.Tool --version 2.0.0-alpha.2
H073.Local.Tool
Command-line tools for H073.Local, the .NET localization library.
This is not a library you reference from code. It installs a program called hxloc
onto your machine, which you run from a terminal or a CI job. It never ends up inside your
application.
dotnet tool install -g H073.Local.Tool --prerelease
Three commands:
hxloc check |
Validate a source tree. Fails a build on real problems. |
hxloc compile |
Turn JSON catalogues into the compact .loc3 binary format. |
hxloc import |
Turn XLIFF back into a source tree. |
check is the one you want first, and it works whether or not you ever compile.
hxloc check — find broken translations before you ship
hxloc check ./Localization
Schema: 412 keys in 3 bank(s)
de/ui
error 'welcome' uses undeclared placeholder(s) {player}
error missing key 'menu.settings'
warn 'menu.play' is 21 characters, over its limit of 12
pl/items
warn 'stones' has no "few", "many" form(s); pl needs "one", "few", "many", "other"
2 error(s), 2 warning(s).
Why you need this
The library reads a folder of files, one folder per language. Unless you write a schema, nothing declares which keys are supposed to exist — each file simply lists what it happens to contain. If English is missing a key that German has, no error is raised anywhere: at runtime the fallback quietly covers for it, and the missing-key log only ever sees keys that something actually asked for. A key on a screen nobody opened during testing stays invisible until a user finds it.
hxloc check is the only thing that catches that before release.
With a schema
A _schema directory declares what exists, one file per bank:
// Localization/_schema/ui.json
{
"bank": { "tags": ["always", "interface"] },
"keys": {
"menu.play": { "context": "Main menu button", "maxLength": 12 },
"welcome": { "variables": { "name": "the player's display name" } },
"files": { "forms": "plural" }
}
}
The schema is then the authority, and every language is checked against it — including your source language. Without a schema the source language is the one thing never checked, because it defines the answer.
| Severity | |
|---|---|
| A declared key missing from a language | error |
| A key the schema does not declare | error |
| A bank the schema does not declare | error |
| A placeholder the schema does not declare | error |
| A declared placeholder the text never uses | warning |
Tagged forms on a key declared single |
error |
A plain string where plural was declared |
warning |
| A plural category the language needs but the file lacks | warning (error if other is missing) |
A translation over its maxLength |
warning |
| A bank missing from a language entirely | warning |
| Malformed JSON, or two keys that collide | error |
Without a schema
Everything still works; there is simply nothing authoritative to compare against, so the
language with the most keys stands in as a reference. Override it with --reference. That
catches most drift, but it cannot tell a key missing everywhere from a key that was never
meant to exist.
On character limits
maxLength is reported as a warning, deliberately. A character count stands in for
what actually overflows a button and does it badly: MMMMMMMMMM and iiiiiiiiii are the
same length and roughly three times apart in width. It is the only approximation a build
can make without knowing your font. Measure real width in your UI layer, where the font
is known.
Options
--reference <code> |
Compare against this language. Only used when there is no schema. |
--strict |
Fail on warnings too, not just errors. |
--quiet |
Print only the summary line. |
In CI
Non-zero exit on errors, so it fails a build:
- run: dotnet tool install -g H073.Local.Tool --prerelease
- run: hxloc check ./Localization
hxloc compile — build the binary format
hxloc compile ./Localization ./Localization.compiled
de/ui 412 keys 18,204 → 14,880 bytes
en/ui 412 keys 17,993 → 14,612 bytes
2 catalogue(s) → ./Localization.compiled 29,492 bytes, 82% of the JSON
It walks every language folder and turns each .json into a .loc3:
Localization/ Localization.compiled/
├── _schema/ui.json (not copied — it describes sources)
├── de/ui.json → ├── de/ui.loc3
└── en/ui.json → └── en/ui.loc3
Ship the compiled folder and point Loc3Source at it. The source tree stays in version
control, and the schema never travels — it describes sources, and a compiled catalogue is
the result of applying it.
Compiling is optional. JsonSource reads the source tree directly and supports every
feature. Compiling buys startup time and memory; skipping it costs you nothing else.
Run it as part of the build
<Target Name="CompileLocalization" BeforeTargets="Build"
Condition="'$(Configuration)' == 'Release'">
<Exec Command="hxloc compile ./Localization ./Localization.compiled" />
</Target>
This belongs in the build, not in your application. Compiling at startup would ship the JSON as well and pay the conversion cost on every launch — strictly worse than reading the JSON directly.
Options
--keys |
Keep key names in the output. Larger files, but missing-key reports name the key instead of a hash. Useful for internal test builds. |
--language <code> |
Compile only this language. Repeatable. |
--clean |
Delete the output folder first. |
--quiet |
Print only errors. |
What the compiler refuses to emit
Anything a reader could misinterpret fails the build instead: two keys whose hashes collide, a duplicate key, duplicate labels within one entry, a value over the format's limits, or a labelled entry with no category. All of them name the offending key.
hxloc import — XLIFF into a source tree
hxloc import ./from-translators ./Localization
XLIFF is the standard format for sending text out for translation and getting it back. One
file carries a language pair, so an import writes both catalogues — plus the schema,
since XLIFF also carries maxwidth and <note>:
de.xliff → _schema/ui.json ← from maxwidth and note
en/ui.json ← from <source>
de/ui.json ← from <target>
It lands in the source tree rather than compiling directly, so the result passes through
check before it becomes a catalogue. Translations that arrive from outside are the ones
most worth checking.
Three things worth knowing
Plural forms travel as key[form] unit ids. XLIFF 1.2 has no element for plural
forms — the specification does not model them — so every toolchain invents a convention,
and this is ours. Units named stones[one] and stones[few] are regrouped into one key.
Without that, a round trip silently turns one key with four forms into four unrelated keys.
Inline markup is flattened. A placeholder may arrive wrapped in <ph>, and formatting
in <g> or <bpt>. Reading only an element's own text would drop what they contain;
reading the raw XML would leave tags in your strings. Both are reduced to the text they
wrap.
Only XLIFF 1.2 is read. Version 2.0 restructured the format — different namespace, different elements, different nesting. It is refused with a clear message rather than parsed as 1.2, which would find no units and report an empty but successful import.
Options
--state <name> |
Only import units whose target has this state, e.g. translated or final. |
--quiet |
Print only errors. |
A unit with an empty target is left out of the target catalogue entirely, so the key falls through to your fallback language rather than rendering as blank.
Exit codes
0 |
Success. For check, no problems — or warnings only, without --strict. |
1 |
check found errors; compile or import failed on one or more files. |
2 |
Bad usage — missing arguments, unknown option, path does not exist, unreadable schema. |
Help
hxloc --help
hxloc --version
For the file format, the JSON layout and the library itself, see the H073.Local documentation.
Licence
Apache-2.0
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. 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. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.0.0-alpha.2 | 67 | 8/18/2026 |
| 2.0.0-alpha.1 | 59 | 8/18/2026 |