Use when replacing hardcoded English strings with i18n keys in a kaynos component. Encodes the react-intl + useTranslation patterns, en.json/es.json parity requirement, and the English-fallback convention.
All user-visible strings in kaynos must go through react-intl. This catches untranslated strings in Spanish and enables adding more locales without code changes.
react-intl (FormatJS) — ICU message syntax, plurals, date/number formattingI18nProvider in src/hooks/useTranslation.jsx — dynamic locale loading (KAY-655)src/i18n/en.json (fallback) and src/i18n/es.jsont() helper via useTranslation hookBest for short inline strings.
import { useTranslation } from "../hooks/useTranslation";
export default function SaveButton() {
const { t } = useTranslation();
return <button>{t("common.save")}</button>;
}
The t() helper resolves:
<FormattedMessage> componentBest for longer strings with embedded HTML / pluralization.
import { FormattedMessage } from "react-intl";
<h2>
<FormattedMessage
id="admin.section.title"
defaultMessage="Academy Settings"
/>
</h2>
defaultMessage is required — it's the English fallback and is what the UI shows if the key doesn't exist in the loaded locale.
t())FormattedMessage)t() with values object)// Before
<button aria-label="Mark all as read">Mark all read</button>
<p>No sessions yet. Upload your first video to get started.</p>
Naming convention: <area>.<subarea>.<purpose>. Match existing keys in en.json before inventing new ones.
notifications.markAllRead.button
notifications.markAllRead.ariaLabel
sessions.empty.title
sessions.empty.hint
src/i18n/en.json:
{
"notifications.markAllRead.button": "Mark all read",
"notifications.markAllRead.ariaLabel": "Mark all as read",
"sessions.empty.title": "No sessions yet",
"sessions.empty.hint": "Upload your first video to get started."
}
src/i18n/es.json:
{
"notifications.markAllRead.button": "Marcar todo como leído",
"notifications.markAllRead.ariaLabel": "Marcar todo como leído",
"sessions.empty.title": "Aún no hay sesiones",
"sessions.empty.hint": "Sube tu primer video para comenzar."
}
If you don't speak Spanish, prefix the value with [EN] so a translator can find them later:
{
"sessions.empty.title": "[EN] No sessions yet",
"sessions.empty.hint": "[EN] Upload your first video to get started."
}
Parity is non-negotiable — both files must have identical keys, even if values are flagged English.
// After
import { useTranslation } from "../hooks/useTranslation";
export default function NotificationsHeader() {
const { t } = useTranslation();
return (
<button aria-label={t("notifications.markAllRead.ariaLabel")}>
{t("notifications.markAllRead.button")}
</button>
);
}
Values with dynamic parts use react-intl's ICU syntax:
{
"sessions.progress": "{count} of {total} watched",
"sessions.countLabel": "{count, plural, one {# session} other {# sessions}}"
}
<span>{t("sessions.progress", { count: 3, total: 10 })}</span>
// OR for plural
<FormattedMessage
id="sessions.countLabel"
values={{ count: sessions.length }}
/>
src/components/DrawingCanvas.jsx — tool labelssrc/components/ClipEditor.jsx — form labelsCheck with:
# Find likely English strings inside JSX (imperfect but useful)
grep -En '>[A-Z][a-z]+[^<]{3,30}<' src/components/MyComponent.jsx
Before committing:
node -e "
const en = require('./src/i18n/en.json');
const es = require('./src/i18n/es.json');
const enKeys = new Set(Object.keys(en));
const esKeys = new Set(Object.keys(es));
const inEnOnly = [...enKeys].filter(k => !esKeys.has(k));
const inEsOnly = [...esKeys].filter(k => !enKeys.has(k));
console.log('Missing in ES:', inEnOnly.length ? inEnOnly : 'none');
console.log('Missing in EN:', inEsOnly.length ? inEsOnly : 'none');
"
If keys are missing on either side, add them before commit. Every key in one MUST exist in the other.
"admin", "student", "session_type")console.error("DB query failed")) — those stay EnglishformatUserFacingErrorNever concatenate translated pieces — it breaks locales with different word order. Always use a single key with placeholders:
// BAD
{t("prefix") + " " + user.name + " " + t("suffix")}
// GOOD
{t("greeting.withName", { name: user.name })}
// en: "Welcome, {name}!"
// es: "¡Bienvenido, {name}!"
These count as user-visible strings and MUST be translated:
<input
type="search"
placeholder={t("search.placeholder")}
aria-label={t("search.ariaLabel")}
/>
npm run build — no runtime errors from missing keysnpm test — no tests broken by key renamesSee also: kaynos-add-component-test, kaynos-decompose-file.