DEV Community

creathree
creathree

Posted on

I got tired of inventing i18n keys, so the source

Most i18n setups ask you to name a string before you have finished writing it.

You invent pricing.hero.title, open a JSON file, wire a lookup, then finally type the sentence. Multiply that by a few thousand UI strings and naming becomes the work.

I got tired of that, so I built the opposite habit into the toolchain:

Write the sentence where it belongs. Wrap it in double brackets. Let the tool derive the ID.

That tool is bracket-i18n. This post is a short record of the idea — not a feature tour.

The loop

You write this:

export default function Pricing({ locale }: { locale: string }) {
  return <h1>[[Choose a plan that grows with you]]</h1>;
}
Enter fullscreen mode Exit fullscreen mode

After scan (or a save that runs the watch hook), it becomes:

import { Languages } from '@/i18n';

export default function Pricing({ locale }: { locale: string }) {
  return <h1>{Languages('eb573cd25ff_', locale) /* Choose a plan… */}</h1>;
}
Enter fullscreen mode Exit fullscreen mode

And the catalog gains an entry:

eb573cd25ff_: { en: `Choose a plan that grows with you`, /* EMPTY */ },
Enter fullscreen mode Exit fullscreen mode

You never invent the key. The source string is the source.

One hard rule: the converted call uses the identifier locale literally. Destructure it in scope, or the file will not compile.

What the machine is allowed to do

fill can ask Gemini to draft the other locales you configured — the ones still marked /* EMPTY */.

That is optional. Shipping still goes through a person:

  • CSV export / import for review
  • pending for “not ready yet”
  • lock: true when a string is done

AI drafts. You decide what leaves the building.

Billing note: with Run on Save, a converting save can chain into fill and cost money. While you are mass-editing [[ ]] markers, pause the watcher (i18n:pause), finish the copy, then resumescan → deliberate fill.

Install (minimal)

npm install -D bracket-i18n
npx bracket-i18n init
Enter fullscreen mode Exit fullscreen mode

init writes a config file, a small runtime under src/i18n/, npm scripts, and (optionally) a save hook. Edit the locale list before the first fill. Put GEMINI_API_KEY in a root dotenv file only if you use fill.

Built-in fill uses Gemini on purpose: cost and accuracy are a reasonable default for large catalogs. Need another provider? Fork the fill engine, or skip fill and use CSV round-trip. That is clearer than half-supported multi-provider plugins.

Why TypeScript catalogs (not only JSON)

Entries live in src/i18n/data/part*.ts as template literals. Quotes and apostrophes do not need escaping. Plurals and gender branches are plain objects; plural categories come from Intl.PluralRules.

Markers can carry flags when you need them:

[[--n Steve has {count} dogs.]]
[[--g He posted.]]
[[--n --g She has {count} dogs.]]
Enter fullscreen mode Exit fullscreen mode

The runtime stays one function — Languages(id, locale, opts?) — no provider tree required.

What this is not

  • Not a zero-config toy. You own the locale list and the review step.
  • Not “AI replaces translators.” Drafts are a first pass.
  • Not a claim that every app should work this way. It is a workflow that stopped me from naming keys for a living.

I use it in production on a browser music product that ships several locales from one JSX habit. The interesting part was never the hash ID. It was removing the moment where copy and catalog drift apart.

Links

If the [[ ]] habit fits your repo, try it on a small surface first — one page, one locale pair, one fill, then lock what you trust.

Top comments (0)