Check .NET RESX files in CI
String.Format throws FormatException when a translation uses {1} but only one argument is passed, and the resource compiler rejects duplicate names. The gate reads the neutral .resx and each culture file next to it and checks every <data> value.
What the gate reads
| Format id | Name | Extensions | Placeholders and plurals | Layout |
|---|---|---|---|---|
resx | .NET RESX | .resx | String.Format items: {0}, {0:N2}, {name} | One file per locale |
Detected layout
For a .NET project laid out like this:
Shop/Resources/Strings.de.resx
Shop/Resources/Strings.fr.resx
Shop/Resources/Strings.resxnpx shipglot check finds the files without a config. npx shipglot init writes what it detected to shipglot.config.json:
{
"sourceLocale": "en",
"files": [
{
"format": "resx",
"path": "Shop/Resources/Strings{.locale}.resx"
}
]
}Add "locales" to restrict target languages, "glossary" for approved terms, and "checks" to change a check's severity. The gate's README lists every option.
GitHub Actions workflow
Save as .github/workflows/translations.yml. The step fails on errors and annotates the pull request; warnings show in the job summary.
name: Translations
on:
pull_request:
paths:
- "Shop/Resources/**"
- "shipglot.config.json"
jobs:
shipglot:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v5
- uses: aurelien64/shipglot@v0
with:
fail-on: errorTo see findings in GitHub code scanning as well, add sarif-file: shipglot.sarif under with: and upload it with github/codeql-action/upload-sarif@v3 (the job then needs security-events: write).
Defects the gate catches
Each example below was run through the gate when this page was built, with the config above. The findings under it are the gate's output.
1. A format item renumbered
The code passes one argument. {1} makes String.Format throw FormatException in French.
Shop/Resources/Strings.resx
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<data name="Greeting" xml:space="preserve">
<value>Hello {0}, welcome back</value>
</data>
</root>Shop/Resources/Strings.fr.resx
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<data name="Greeting" xml:space="preserve">
<value>Bonjour {1}, bon retour</value>
</data>
</root>Gate output
Greetingplaceholder mismatch: missing{0}; unexpected{1}Shop/Resources/Strings.fr.resx:6
2. A duplicate resource name
The resource generator fails on duplicate names; which value wins depends on the tool.
Shop/Resources/Strings.resx
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<data name="Total" xml:space="preserve">
<value>Total amount</value>
</data>
</root>Shop/Resources/Strings.fr.resx
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<data name="Total" xml:space="preserve">
<value>Montant total</value>
</data>
<data name="Total" xml:space="preserve">
<value>Total</value>
</data>
</root>Gate output
TotalDuplicate resource name "Total"Shop/Resources/Strings.fr.resx:9
3. A resource missing from a culture
ResourceManager falls back to the neutral culture, so German users see English.
Shop/Resources/Strings.resx
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<data name="Save" xml:space="preserve">
<value>Save</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>Cancel</value>
</data>
</root>Shop/Resources/Strings.de.resx
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<data name="Save" xml:space="preserve">
<value>Speichern</value>
</data>
</root>Gate output
Cancelmissing in deShop/Resources/Strings.resx:9
4. Personal data pasted into a translation
A translator's or tester's email address ended up in the shipped string.
Shop/Resources/Strings.resx
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<data name="Footer" xml:space="preserve">
<value>Questions? Write to us any time.</value>
</data>
</root>Shop/Resources/Strings.fr.resx
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<data name="Footer" xml:space="preserve">
<value>Des questions ? Écrivez à marie.dupont@gmail.com.</value>
</data>
</root>Gate output
Footeremail address introduced by translation: marie.dupont@gmail.comShop/Resources/Strings.fr.resx:6
Run it on your machine
npx shipglot check
npx shipglot check --reporter json=shipglot.jsonExit code 0 means clean, 1 means findings at or above --fail-on, 2 means a usage or config error. Checks used on this page: placeholder-mismatch, format-validity, missing-key, personal-data.
To check a single file without installing anything, use the in-browser checker.
Translation in the same workflow
With a Shipglot account, npx shipglot translate translates new and changed keys and writes them into these files before the check runs. Hosted translation is not open yet; see the signup page.