Arkian logo
Start free
Menu
← All articles

Translation File Versioning: Match Every Build to Its Git SHA

Translation File Versioning: Match Every Build to Its Git SHA

Engineer reviewing a locale file change

Treat translation files as code. Commit them to Git in small, atomic changes, track releases with tags and a consistent locales folder layout, and run key-parity and format validation in CI. Then export a language package stamped with the release tag and commit SHA, so every software build can be matched to the exact strings it shipped with.


TL;DR:

  • Keep translation changes in small, focused commits with clear reviewer friendly messages; use feature branches for UI changes and release tags for shipped versions.
  • Tags attached to individual keys simplify exports, but freeze their state to prevent later edits from leaking into packages; reserve branches for supported release lines.
  • Use one file per locale with consistent IETF language tags by default; move to feature folders only when module ownership requires them.
  • Make CI reject invalid syntax, missing or extra keys, mismatched placeholders, and incomplete plural rules, then build packages only after every check passes.
  • Generate each language package from its release tag and stamp its manifest with the tag, commit SHA, and version; archive packages for older builds.

Arkian
Simplify Release-Ready Localization
Arkian automates multilingual scripts, voice outputs, validation, and structured language packages for small teams, without complex translation management systems.
Explore Arkian

Table of Contents

How do you manage translation changes in Git?

The core idea is simple: a translation update is a code change, so review it like one. The Git project’s own guidance on maintaining a project recommends splitting work into small, logical, independently testable commits with clear log messages. Apply that to locale files and every string change stays explainable months later.

The gitworkflows documentation makes the payoff concrete. When translation updates are distinct, well-described commits, tools like git-blame and git-bisect can trace why a string changed and which change broke a screen. A single commit titled “update translations” that touches many locales defeats both tools.

A workable commit and branch routine looks like this:

  • One logical change per commit. Add keys for a feature in one commit, translations for one locale in another, and a rename in a third.
  • Write the message for the reviewer. “Add checkout.error.card_declined (en, de, fr)” tells a maintainer more than “fix strings”.
  • Use feature branches for UI-driven string changes. New keys travel with the feature code and merge together.
  • Use release branches or tags for release-bound translations. Branches suit long-lived release lines; tags suit a simple mainline that ships frequently.
  • Name tags so they map to package versions. Running git describe produces readable identifiers like v1.4.0-12-g3f9a2c1, which you can reuse as the language package name.

Keeping strings in the app repository gives you one review flow, one CI pipeline and atomic changes between code and text. A separate localization repository gives translators narrower access and a lighter history, but you must sync versions between the two repositories on purpose. Small teams usually do better in-repo; larger organizations with external translators often split.

Pro Tip: Run git describe --tags in your packaging script and write the output into the package manifest, so a language package can never be confused with a different build.

Small, verified changes also reduce regression risk in general, a point the team behind verified PR reviews makes about code quality metrics. The same logic holds for locale files: reviewers catch problems faster when each diff is short and focused.

How do key-level tags track which strings belong to which release?

Branches isolate work, but they multiply merges. Tags on individual translation keys offer a lighter option: each key carries labels, and exports filter by label. You get per-release packages without keeping a branch alive for every release line.

Tagged translation keys filtered into release packages

Useful tags tend to be few and plain: a feature name (checkout), a release marker, a status (deprecated) and a priority (high-priority). Keep the vocabulary small and documented, or tags drift into noise.

A practical tag workflow runs like this:

  1. Tag at extraction. When a developer adds a key, the extraction step or editor applies the feature and release tags automatically.
  2. Add translator context. Include a comment with the string so ambiguity does not reach translators. The Git project’s coding guidelines describe a "TRANSLATORS: " comment convention for exactly this purpose.
  3. Filter on export. Your export script requests every key tagged for the release and writes one file per locale.
  4. Map the tag to the build. The release tag in Git and the release tag on the keys share a name, so the export is reproducible from the build identifier alone.
  5. Retire tags with the release. When a release line ends, archive its tag set so it never silently changes.

The trade-off is isolation. Branch-based workflows give strict separation: a hotfix branch cannot be affected by a string edited on main. Tag-based workflows are simpler, but a key edited after tagging can leak into a later export unless your pipeline freezes the tagged state. For most small teams, tags plus Git release tags offer the best balance of simplicity and safety. If you maintain files like JSON language files, the same tagging scheme carries across formats.

What folder structure and file naming should you use for locales?

Predictable names make CI, loaders and packaging scripts boring, which is the goal. Decide the layout once and enforce it.

Two layouts dominate. A per-locale layout (locales/en.json, locales/de.json) makes it easy to update or add one language and to diff parity between files. A per-feature layout (locales/checkout/en.json) keeps modules self-contained and suits large apps with feature teams, at the cost of more files to validate. Pick per-locale until module ownership genuinely demands the other.

Conventions worth adopting:

  • Use IETF language tags. Write pt-BR, not brazilian or pt_br, and use the same casing everywhere.
  • Keep one default locale file. Shopify’s locale file documentation shows the pattern with en.default.json as the fallback and other locales alongside it.
  • Expect file constraints. The same documentation notes limits such as a maximum number of translations per file and value-length constraints, so check your platform’s rules before splitting or merging files.
  • Choose formats by consumer. JSON or YAML suit machine validation in CI, while iOS .strings and Android XML feed platform builds.
  • Mirror the layout in the package. The structure inside your release artifact should match the structure the loader expects, with no renaming step in between.

Naming and structure feed directly into packaging. A script that globs locales/*.json breaks the moment someone adds a nested folder, so write the convention into your contributing guide and let CI reject deviations. For output organization, our notes on a multilingual output folder structure show how to lay out distributable artifacts.

Which CI checks keep translations from breaking the build?

Most translation failures are mundane: a missing key, a dropped placeholder, a stray comma in JSON. CI catches all of them before a release does.

The Git project’s maintenance guidance leans on automated checks to enforce consistency, and key parity is the first one to add. Every required key must exist in every locale, and a missing one fails the pipeline.

A sensible check set:

  • Syntax validation. Parse every JSON, YAML and XML file; an invalid file fails immediately.
  • Key parity. Compare each locale’s key set against the default locale and report missing and extra keys.
  • Placeholder validation. Confirm {name} or %s tokens in the source also appear in each translation, with matching counts.
  • Plural validation. Check that each locale supplies the plural categories its language needs.
  • MessageFormat and ICU checks. The Unicode CLDR 47 release advanced MessageFormat 2.0, so message syntax and pluralization deserve explicit validation rather than assumption. Our walkthrough of ICU Message Format checks covers what to test before shipping.
  • Artifact gating. Build the language package only when every check above passes.

Run the fast checks on every pull request and the full set on release pipelines. For batch workflows, patterns for keeping plurals and placeholders intact apply directly to CI design.

Pro Tip: Make the failure message name the file, the key and the locale. A parity error that says “missing key” without saying which one gets ignored; one that points at de.json: checkout.error.card_declined gets fixed.

How do you align translation versions with software releases?

The goal is reproducibility: given a release, you can rebuild the exact language package that shipped with it. That requires tying the package to the Git release identifier and recording it.

The steps that make this work:

  1. Export from a tag. Check out the release tag, or filter keys by the matching release tag, and generate the package from that state only.
  2. Stamp a manifest. Include the release tag, the commit SHA and the version, using git describe output for a readable name.
  3. Choose formats the app consumes. Ship JSON, YAML, .strings or Android XML as your platforms require, with the version in the filename or manifest.
  4. Attach the package to the CI artifact. Store it next to the build so deployment pipelines pull the matching strings, not whatever is on main.
  5. Plan for LTS and backports. Keep a branch per supported release line and cherry-pick translation fixes into it, so older builds keep receiving compatible strings.

Semantic versioning principles transfer well to language packages. A patch bump covers corrected translations, a minor bump covers added keys, and a major bump signals removed or renamed keys that older builds may depend on. Stamping every package with that version makes mismatches visible instead of mysterious. Platform-specific details for iOS strings and Android XML follow the same pattern.

How do you remove or deprecate keys without breaking older releases?

Deleting a key is a breaking change for any build that still references it. Treat removal as a process with a schedule, not a cleanup chore.

A safe workflow:

  • Mark first, delete later. Tag the key deprecated and keep it in every locale for a defined number of releases.
  • Record removals in release notes. Developers and translators should see what is going away and when.
  • Use aliases or runtime fallbacks. Map an old key to its replacement, or fall back to the default locale string, so a missing translation never renders a raw key.
  • Remove on major versions. Coordinate deletions with major releases, and keep the keys on any LTS branch that still ships them.
  • Archive historical packages. Keep the language package for every released version so you can reproduce old builds exactly.

The archive matters more than it seems. When a customer reports a bug on an older build, you need that build’s strings, not today’s. Because each package carries its tag and SHA, reproducing the report is easier than detective work.

How do we package release-ready language files at Arkian?

Our workflow automates the repetitive middle of everything above. We validate keys and bundle the language files into structured packages. Small teams get organized output without handing over repository access or adopting a full translation management system.

Our work with the Quiet Harbour app shows the result in practice: one coherent localized product experience across multiple languages, documented in our Quiet Harbour case study.

Whatever tool you evaluate, a short checklist keeps the comparison honest:

  • Validation depth: does it check key parity, placeholders, plurals and syntax?
  • Packaging formats: does it output the formats your platforms load?
  • Version stamping: does the package carry the release tag and commit SHA?
  • Repository access: does it need broad access, or only the files you approve?
  • CI fit: can the output attach to your existing pipeline artifacts?

What matters most when versioning translation files?

My judgement: teams overinvest in tooling choices and underinvest in discipline. The most common advice is to pick a translation platform and let it handle versions. That skips the part that actually prevents incidents, which is small commits, a fixed folder convention and a CI gate that blocks a broken package. A fancy editor on top of an unreviewed, untagged repository just moves the mess.

Branch-per-release is also overrated for small teams. It feels rigorous, but it creates merge work that tags and a stamped manifest avoid. Reserve branches for release lines you genuinely support.

If you do one thing first, add the parity and placeholder check to CI. It costs an afternoon, catches the failures users actually see, and gives every later improvement, tagging, packaging and deprecation, a safe foundation to stand on.

— Arkian

Skip the manual packaging with release-stamped language files

We build validated, structured language packages so your team does not have to script them by hand. Our language file packaging handles bundling without broad repository access, and our localization strings service prepares your files in the formats you ship.

Arkian

Check our pricing and membership options, run a sample packaging job, or ask us for a demo.

FAQ

How should you structure translation files in a repository?

Use one file per locale in a single locales folder, named with IETF language tags such as pt-BR, plus one default locale file. Switch to a per-feature layout only when feature teams need to own their strings. Enforce the convention in CI so nobody drifts from it.

How do you version translation files alongside releases?

Export the language package from a Git release tag and stamp a manifest with the tag, commit SHA and version. Using git describe produces readable names you can reuse for the package. Attach the package to the same CI artifact as the build.

What should CI check in translation files?

At minimum: valid syntax, key parity across locales, matching placeholders and correct plural categories. Add MessageFormat and ICU validation if you use those syntaxes, then build the package only when everything passes.

How do you remove old translation keys safely?

Mark the key deprecated, keep it for a set number of releases, and document the removal in release notes. Delete it on a major version, and keep it on any long-term support branch that still ships it.

Sources