redis/i18n
>- Internationalization conventions for the RedisInsight UI (i18next). Use when adding or changing user-facing strings under redisinsight/ui/**, editing the locale files (en.json/bg.json), translating API errors or notifications, or when the user mentions i18n, translations, locales, i18next, or <Trans>.
npx skills add https://github.com/redis/RedisInsight --skill i18n
RedisInsight UI is localized with i18next + react-i18next. English (en)
is the source of truth; Bulgarian (bg) is the second locale.
redisinsight/ui/src/i18n/ — the i18next instance and barrel. Import from uiSrc/i18n.redisinsight/ui/src/i18n/locales/en.json and bg.json — the translations (flat keys).redisinsight/ui/src/i18n/i18next.d.ts — augments i18next types from en.json, so keys are type-checked (a typo in t('…') fails type-check).i18next.config.mjs — extraction config.scripts/check-i18n-locales.js — duplicate-key CI check.| Use | When |
| -------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| useTranslation() hook → t | Inside a React component rendered in the tree. |
| i18n.t singleton (import i18n from 'uiSrc/i18n') | Non-React code: redux thunks, factories, utils, message builders (e.g. error-messages.tsx, success-messages.tsx, INFINITE_MESSAGES). These run outside React, so the hook is unavailable. |
| <Trans> (from uiSrc/i18n) | A message with mid-sentence markup/links — bold spans, inline <a>. Keys use component tags: "… <consoleLink>Cloud console</consoleLink> …" + components={{ consoleLink: <a … /> }}. |
Hook — inside a React component:
import { useTranslation } from 'uiSrc/i18n';
const AddKeyButton = () => {
const { t } = useTranslation();
return <PrimaryButton>{t('browser.addKey.button.submit')}</PrimaryButton>;
};
Singleton — non-React code (thunks, factories, utils):
import i18n from 'uiSrc/i18n';
export const deletedKeyMessage = () => ({
title: i18n.t('browser.deletedKey.title'),
});
Interpolation — pass values; reference {{vars}} in the string:
// en.json: "browser.deletedKey.message": "{{name}} has been deleted."
t('browser.deletedKey.message', { name: keyName });
<Trans> — mid-sentence markup or links (map tags to components):
import { Trans } from 'uiSrc/i18n';
// en.json: "browser.docs": "See the <docsLink>documentation</docsLink> for details."
<Trans
i18nKey="browser.docs"
components={{
docsLink: <a href={DOCS_URL} target="_blank" rel="noreferrer" />,
}}
/>;
// interpolation still works alongside components via `values={{ … }}`
Backend error resource — interpolated automatically:
// Response: { errorCode: 11200, resource: { databaseId: 'abc' } }
// en.json: "api.error.code.11200.message": "Database {{databaseId}} already exists."
// getTranslatedApiError() fills {{databaseId}} from response.data.resource — no extra code.
Use i18next's native count-based plurals — never a hand-rolled isPlural branch with
.single/.plural keys.
key_one, key_other (a language mayneed more forms — _few, _many — but en/bg only use _one/_other).
count; i18next selects the form:t('key', { count }) or <Trans i18nKey="key" count={n} …/>.
en.json — i18next'stypes resolve it from the _one/_other entries.
fragment — word order, agreement, and the number of plural forms vary by language.
.single → _one) leaves the old key behind in bg.json becausei18n:extract doesn't prune — delete the orphan so en/bg parity holds.
// en.json:
// "workbench.runConfirm.body_one": "…This command is part of…"
// "workbench.runConfirm.body_other": "…These commands are part of…"
<Trans i18nKey="workbench.runConfirm.body" count={commands.length} components={{ bold }} />
keySeparator and nsSeparator are false, so a dot is a literal character, not nesting. "api.error.code.11000.title" is a single key.en.json is the type source — every literal t('…') / i18nKey="…" must exist in en.json or type-check fails. Dynamic/computed keys can't be statically typed — cast with as never (e.g. i18n.t(api.error.code.${code}.message as never)), the same pattern used across the codebase.i18n:extract produces). Edit values in place where possible; only re-sort when adding keys.bg values are OK as a "translate later" placeholder — returnEmptyString: false makes them fall back to English, not render blank.escapeValue: false — interpolated values are not HTML-escaped (React escapes plain strings at render).The top-level segment says where a string belongs. There are two kinds:
1. Cross-cutting (by source), not tied to a page:
api.* — content keyed by a backend identifier (the API contract). Today api.error.code.<errorCode>.{title,message}; scales to api.<type>.* for any future API-originated content.notification.* — FE-authored toast copy: notification.{error,success,infinite}.*.common.* — labels reused across many pages: common.button.save, common.button.cancel, common.button.delete, common.loading, common.yes, common.no.2. By page/module — most UI copy. Top-level = the feature (mirror the folder in
redisinsight/ui/src/pages/<page> → <page>.*); nest by section/component; the leaf
describes the string. A page owns its keys; only promote to common.* when genuinely shared.
| Namespace | Module (pages/…) | Example keys |
| ------------- | ------------------ | ------------------------------------------------------------------------------------------------------------- |
| browser.* | browser | browser.keyList.empty, browser.addKey.title, browser.filter.placeholder, browser.addKey.button.submit |
| workbench.* | workbench | workbench.editor.runTooltip, workbench.results.empty |
| rdi.* | rdi | rdi.pipeline.deploy.title, rdi.config.button.deploy |
| settings.* | settings | settings.section.general, settings.language.title |
Common leaf segments: *.title, *.description, *.label, *.placeholder, *.tooltip,
and *.button.<action> for action labels (keep them distinct from titles/descriptions,
e.g. browser.addKey.button.submit, api.error.code.11024.button.signIn).
The backend ships a stable errorCode on every user-facing error (see
redisinsight/api/src/constants/custom-error-codes.ts). The UI translates by that code:
getTranslatedApiError(error) and getTranslatedApiTitle(error) inutils/apiResponse.ts look up
api.error.code.<n>.message / .title and fall back to the backend text when the key is absent.
parseCustomError in utils/errors.tsx does the same in its default case for coded errors it doesn't special-case.resource interpolation: any response.data.resource object fills {{vars}} in the message.en.json and the same key to bg.json (translated, or empty to defer). Keep both sorted and in parity.t('my.key') / i18n.t('my.key') / <Trans i18nKey="my.key" …/>.values (interpolation) or resource (backend errors).npm run i18n:extract to sync/sort, and npm run i18n:check to catch duplicate keys.npm run type-check (new literal keys must resolve) and npm run lint:ui.npm run i18n:extract — scans t()/<Trans> usages and syncs en.json/bg.json (alphabetical; does not prune unused keys). Note: dynamic (as never) keys aren't discovered by extraction — keep them in the locale files manually.npm run i18n:check — fails if a locale file has a duplicate key (JSON silently keeps the last, so a dup would shadow a value). Runs in CI on PRs touching locales/**.?lang=bg to the URL to preview Bulgarian.i18n singleton; components use useTranslation.<page>.* mirroring pages/<page>), or by source (api.*, notification.*, common.*); shared labels go in common.*.i18n:extract sort.count + key_one/key_other (see Plurals).Take redis/i18n from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.