Simple internationalization library with React integration
Note: This package is developed in AdGuardSoftwareLimited/ext-translate. The AdguardTeam/translate repository is a public mirror.
AdGuard Translate is a TypeScript internationalization library for AdGuard product developers building browser extensions and web applications. It provides a unified i18n layer across AdGuard products, solving the problem of inconsistent translation handling between different codebases.
The library supports message translation with placeholder substitution, XML tag interpolation (essential for UI framework integration), and plural form rules for ~80 locales. It ships with React and Preact integrations and a translation validator for CI/CD quality gates. It has zero runtime dependencies.
npm install @adguard/translateOr with pnpm:
pnpm add @adguard/translateReact and Preact are optional peer dependencies — install them only if you use the corresponding plugin.
Import the library, implement the I18nInterface, and start translating:
import { translate, I18nInterface } from '@adguard/translate';
const i18n: I18nInterface = {
getMessage(key) {
const messages: Record<string, string> = {
greeting: 'Hello, %name%!',
};
return messages[key];
},
getUILanguage() { return 'en'; },
getBaseMessage(key) {
const messages: Record<string, string> = {
greeting: 'Hello, %name%!',
};
return messages[key];
},
getBaseUILanguage() { return 'en'; },
};
const t = translate.createTranslator(i18n);
t.getMessage('greeting', { name: 'World' }); // "Hello, World!"Placeholders are wrapped in % marks:
"agreement_consent": {
"message": "Servers number %count%"
}
Literal % characters must be escaped with another %:
"discount": {
"message": "You have 50%% discount"
}
Messages support open/close tags with custom renderers:
<a>link</a> to the text
And void (self-closing) tags:
<img>
The following tags are available by default and do not need explicit
renderers: b, p, strong, tt, s, i.
Plural strings are separated by |:
No servers | %count% server | %count% servers
Rules for plural strings:
- Forms are divided by
|. - The number of forms must match the locale's plural rule count (see the CLDR plural rules table).
- The first form is the zero form. If omitted, the zero form returns an empty string:
| %count% server | %count% servers
Messages support three types of dynamic labels. All of them pull values
from the params object passed to getMessage() or getPlural():
| Type | Syntax | params value type |
Example |
|---|---|---|---|
| Placeholder | %name% |
string |
{ name: 'World' } |
| Tag | <tag>...</tag> |
(children: string) => string |
{ a: (c) => '<a>'+c+'</a>' } |
| Void tag | <tag/> |
string |
{ img: '<img src="..."/>' } |
Placeholders are replaced verbatim with the matching string value
from params:
"message": "Hello, %username%!"
// params: { username: 'Alice' }
// result: "Hello, Alice!"
Tags are replaced by calling the render function with the tag's inner content as a string argument. The function can wrap or transform the children:
"message": "Read our <terms>Terms of Service</terms>"
// params: { terms: (c) => `<a href="/terms">${c}</a>` }
// result: 'Read our <a href="/terms">Terms of Service</a>'
If the params value for a tag is a string instead of a function, the
string is used directly and the children are discarded.
Void tags are self-closing and replaced with a plain string:
"message": "Status: <status-icon/>"
// params: { 'status-icon': '<span class="ok"/>' }
| Scenario | Behavior |
|---|---|
Escaped percent %% |
Produces a literal % in output. Use this where a percent sign must appear in a translated message. |
Unclosed placeholder %name (no trailing %) |
Throws Error with an "Unclosed placeholder marker" message. |
Unclosed tag < without > |
The malformed tag is treated as literal text in the output. |
Unbalanced tags <b>text without </b> |
Throws Error with an "unbalanced tags" message. |
Improperly nested tags <a><b>text</a></b> |
Throws Error with an "unbalanced tags" message. |
Tag with attributes <a class="link"> |
Throws `Error** with a "Tags should not have attributes" message. |
Missing params key for a placeholder, tag, or void tag |
Throws Error with a "value was not provided" message in the formatter. |
Void tag with a function in params |
Throws Error (void tags only accept strings). |
Default tags (<b>, <p>, <strong>, <tt>, <s>, <i>) |
Built in — no explicit params needed. Can be overridden via the defaults parameter. |
| Nested tags | Tags can contain other tags. The inner tag's render function receives the already-rendered outer content. |
count in getPlural() |
The count parameter is automatically set to the numeric number argument. Any user-supplied params.count is overridden. |
| Empty zero form in plural strings | If the first form (before the first |) is omitted, the zero form returns an empty string. |
Creates a Translator instance that returns strings. Accepts an optional
messageConstructor for custom output formats (e.g., building DOM nodes
instead of strings) and optional default tag values.
Creates a Translator that returns React nodes. Tag handler functions
receive React children and must return React elements.
Creates a Translator that returns Preact component children. Tag handler
functions receive Preact children and must return Preact elements.
Translates a message by key, substituting placeholders and tags with values
from params. Throws if the key is not found.
Selects the correct plural form based on number and the current locale,
then translates it. Adds count to params automatically.
Validates that a translated message has the same AST structure (tags and
placeholders) as the base message. For plural strings, also validates that
the number of forms is correct for the locale. Returns true if valid,
throws on structurally invalid input.
Checks that a message has the correct number of plural forms for the given
locale. Returns true if valid, false otherwise.
const t = translate.createTranslator(i18n);
const message = t.getMessage('agreement_consent', {
eula: (chunks) => `<button class="privacy-link">${chunks}</button>`,
});
// '<button class="privacy-link">EULA</button>'t.getPlural('servers_count', 1); // "1 server"
t.getPlural('servers_count', 5); // "5 servers"
t.getPlural('servers_count', 0); // "No servers"const t = translate.createReactTranslator(i18n, React);
const element = t.getMessage('agreement_consent', {
eula: (chunks) => (
<button className="auth__term" onClick={handleEulaClick}>
{chunks}
</button>
),
});
// Renders: You agree to our <button class="auth__term">EULA</button>import { validator } from '@adguard/translate';
// Simple message validation
validator.isTranslationValid(
'test string <a>has node</a>',
'тестовая строка <a>с нодой</a>',
'ru',
); // true
// Plural form validation
validator.isPluralFormValid(
'%count% серверов | %count% сервер | %count% сервера | %count% серверов',
'ru',
); // true (Russian has 4 plural forms)Consumers must implement I18nInterface to provide translations:
interface I18nInterface {
getMessage(key: string): string;
getUILanguage(): Locale;
getBaseMessage(key: string): string;
getBaseUILanguage(): Locale;
}In browser extensions, this is typically implemented using browser.i18n.
The React and Preact translators include built-in renderers for p, b,
strong, tt, s, and i. You can override or extend these via the
defaults parameter:
const t = createReactTranslator(i18n, React, {
override: false,
tags: [{ key: 'custom', createdTag: 'span' }],
});For advanced use cases (e.g., building virtual DOM nodes directly), pass a
MessageConstructorInterface function. It receives an array of formatted
string parts and returns your desired output type.