Back

Stop Wrapping Your Strings in Function Calls

Ernest McCarter avatarErnest McCarter
gt-reacti18ntagged-templatemacrodeveloper-experience

The Problem with t("Hello, {name}", { name })

If you've ever internationalised a React app, you've written something like this:

const gt = useGT();

return <p>{gt("Hello, {name}! You have {count} items.", { name, count })}</p>;

It works. But it's clunky. You're writing ICU MessageFormat syntax by hand, duplicating every variable name in the options object, and pulling t out of a hook that requires React context. For something that's supposed to be a thin layer over your existing strings, that's a lot of ceremony.

And it gets worse. The moment you need a translation outside a React component — in a utility function, an event handler, a server action — you're reaching for workarounds, because hooks don't work there.

Template literals should just work

Here's what the same code looks like with the t macro:

import { t } from "gt-react/browser";

return <p>{t`Hello, ${name}! You have ${count} items.`}</p>;

That's it. Standard JavaScript template literal syntax. No ICU placeholders, no options object, no hook, no context provider. You write your string the way you'd write any template literal, and the compiler handles the rest.

At build time, the GT compiler transforms:

t`Hello, ${name}!`

into:

t("Hello, {0}!", { "0": name })

The tagged template is pure syntactic sugar — the runtime behaviour is identical to calling t() with a string. But the developer experience is dramatically better.

No more dependency on React context

The bigger change here isn't syntax — it's architecture. The t function exported from gt-react/browser doesn't use React context. It doesn't need a hook. It doesn't need to be called inside a component.

This means you can use t in:

  • Event handlers: onClick={() => alert(tSaved!)}
  • Utility functions: function formatError(code) { return tError: $ }
  • Constants and config: const LABELS = { save: tSave, cancel: tCancel }
  • Anywhere JavaScript runs on the client

The old pattern — useGT() returning a function scoped to React context — was a bottleneck. It forced translation to be a React concern when it's really a string concern.

Global registration

If you don't want to import t in every file, you can register it globally:

// In your app's entry point
import "gt-react/macros";

This sets globalThis.t, making the tagged template available everywhere without an import. The compiler is smart enough to detect this — if t is already imported from a GT source, it won't inject a duplicate import. If it's not, and you've used t as a tagged template, the compiler injects the import for you.

Concatenation works too

The macro expansion isn't limited to tagged templates. It also handles template literals passed as arguments and string concatenation:

// Template literal as argument — also transformed
t(`Welcome back, ${user}`)

// String concatenation — also transformed
t("Hello, " + name + "! Welcome.")

Both are normalised to the same t("...", { ... }) call at build time.

Getting started

1. Install the latest version of gt-react

npm install gt-react@latest

2. Use the t macro

Either import directly:

import { t } from "gt-react/browser";

export function Greeting({ name }) {
  return <p>{t`Hello, ${name}!`}</p>;
}

Or register globally and skip imports:

// app/layout.tsx or entry point
import "gt-react/macros";
// Anywhere in your app
export function Greeting({ name }) {
  return <p>{t`Hello, ${name}!`}</p>;
}

3. That's it

The GT compiler handles the transformation automatically during your build. No additional configuration is needed — macro expansion is enabled by default.


The t macro is a small change to the API surface, but it reflects a larger shift: translations should feel native to JavaScript, not like a framework-specific escape hatch. Write your strings naturally. Let the toolchain do the rest.