@transglot/pseudo
v0.1.0
Published
Zero-dependency pseudo-localization library + CLI: turn a source i18n file into a pseudo-locale to catch hardcoded strings and layout/overflow bugs before translating. Preserves ICU/i18next/printf placeholders, colon params, HTML tags, and ICU plural bloc
Maintainers
Readme
@transglot/pseudo
Zero-dependency pseudo-localization library + CLI. It turns a source i18n file into a pseudo-locale, an obviously-transformed, length-expanded, bracket-wrapped version of your strings, so you can catch two classes of bug before you spend a cent on real translation:
- Hardcoded strings. Anything that renders in plain, un-accented Latin text in the pseudo-locale was never sent through i18n. It jumps out instantly.
- Layout / overflow breakage. German and Finnish routinely run 30–40%
longer than English. The pseudo-locale expands every string and wraps it in
[[ … ]]brackets, so clipped labels, truncated buttons, and broken flexbox are visible at a glance: the brackets show you exactly where a string starts and ends.
Example:
"Save changes" → "[[Šáṽé áéíçĥáñğéš]]"
"Hello {name}" → "[[Ĥéłłö {name}]]"This is a first-class feature almost no localization platform ships. It costs nothing to run and pays for itself the first time it catches an un-i18n'd string in a screenshot review.
Why the accents and brackets
- Accented look-alikes (
a→á,e→é,o→ö,A→Á, …) keep the text readable while making transformed vs. untransformed text unmistakable. If a label readsSáṽéit went through i18n; if it readsSaveit is hardcoded. - Length expansion (default +40%) pads the readable text with repeated accented vowels (never random noise) to simulate longer languages so you see overflow now, not after launch.
[[ … ]]bracket wrapping makes truncation obvious at the edges: if you only see[[Šáṽé áéíçĥwith no closing]], the string is being clipped.
What is never transformed
Getting this right is the whole point. Only human-readable text is pseudo-ized; every interpolation token and markup construct is preserved byte-for-byte:
| Kind | Example | Preserved as |
|------|---------|--------------|
| ICU / i18next placeholder | Hello {name} | {name} |
| ICU simple arg | {count, number} | {count, number} |
| i18next double-brace | Hi {{firstName}} | {{firstName}} |
| ICU plural / select / selectordinal | {count, plural, one {# item} other {# items}} | whole block |
| printf (incl. positional) | %s, %d, %1$s, %.2f, %% | verbatim |
| Colon params (Laravel/Symfony) | Welcome :name | :name |
| HTML / XML tags | Click <b>here</b> | <b>, </b> |
Prose that merely looks like a token is left alone: 100% done keeps its
%, 12:30 is not read as a :param, https://x.io is not either, and a bare
a < b is not mistaken for a tag.
CLI
Installed as the transglot-pseudo bin.
transglot-pseudo <sourceFile> [--out <file>] [--format json_nested|json_flat]
[--expansion 0.4] [--no-brackets]| Option | Default | Meaning |
|--------|---------|---------|
| --out <file> | stdout | Write the pseudo-locale to a file instead of printing it. |
| --format | json_nested | json_nested recurses into nested objects; json_flat transforms only top-level values (keys may contain dots). |
| --expansion <n> | 0.4 | Length-expansion ratio. 0 disables padding. |
| --no-brackets | (on) | Do not wrap values in [[ … ]]. |
| -h, --help | | Show usage. |
Non-string leaves (numbers, booleans, null) pass through unchanged.
Example
# nested source (e.g. en.json) → pseudo-locale file
transglot-pseudo locales/en.json --out locales/en-XA.json
# pipe to stdout, no expansion, to eyeball placeholder handling
transglot-pseudo locales/en.json --no-brackets --expansion 0Given locales/en.json:
{
"app": {
"greeting": "Hello {name}, welcome back!",
"messages": "You have {count, plural, one {# message} other {# messages}}"
}
}you get:
{
"app": {
"greeting": "[[Ĥéłłö {name}, ŵéłçöáéíöüáéíɱé ƀáçķ!]]",
"messages": "[[Ýöü áéíöĥáṽé {count, plural, one {# message} other {# messages}}]]"
}
}Library
import { pseudoLocalize, pseudoLocalizeJson } from '@transglot/pseudo';
pseudoLocalize('Hello {name}');
// → "[[Ĥéłłö {name}]]"
pseudoLocalize('Save', { brackets: false, expansion: 0 });
// → "Šáṽé"
pseudoLocalizeJson(
{ title: 'Save', nav: { home: 'Home' } },
{ expansion: 0 },
);
// → { title: "[[Šáṽé]]", nav: { home: "[[Ĥöɱé]]" } }API
pseudoLocalize(value: string, options?): pseudo-localize one string.pseudoLocalizeJson(data: unknown, options?): walk a parsed JSON i18n tree (flat or nested) and transform every string leaf.tokenize(value: string): the underlying splitter intotext(transformed) andprotected(verbatim) runs; exported for inspection/testing.
options:
{
expansion?: number; // default 0.4; 0 disables padding
brackets?: boolean; // default true
bracketOpen?: string; // default '[['
bracketClose?: string; // default ']]'
nested?: boolean; // pseudoLocalizeJson only; default true
}Adding the pseudo-locale to your app
Generate it alongside your source locale and give it a locale code that no real user has. The CLDR convention is
en-XA(a private-use pseudo region):transglot-pseudo locales/en.json --out locales/en-XA.jsonRegister
en-XAas a locale in your i18n framework, exactly like any other:// i18next i18n.init({ resources: { en: { translation: en }, 'en-XA': { translation: enXA }, }, });Switch your app's language to
en-XA(a query param, a dev-menu toggle, or?lng=en-XA) and click through your screens. Now:- any text that is not accented is a hardcoded string, so go i18n it;
- any label that is clipped, missing its closing
]], or wrapping badly has a layout/overflow bug at the expanded length real translations will hit.
Regenerate the pseudo-locale whenever your source strings change (a good CI or
pre-commit step) so it never drifts from en.json.
Develop / run from source
Not published to any registry yet. To use it from a checkout of this repo:
cd packages/pseudo
npm install
npm run build # emits dist/ (library + bin)
node dist/cli.js locales/en.json --out locales/en-XA.jsonGate the package:
npm run typecheck # tsc --noEmit
npm test # vitest
npm run build # tsc → dist/License
MIT
