Instruction file imported from lablup/backend.ai-webui (
.github/instructions/i18n.instructions.md). Copyright stays with the author.
Internationalization (i18n) Guidelines
These instructions ensure proper internationalization practices across the Backend.AI WebUI codebase.
Always Use i18n Functions
Never Hard-Code User-Facing Text
- All user-facing text must use i18n functions
- Never hard-code strings in English or any other language
- This includes: labels, messages, tooltips, error messages, notifications
// ❌ Bad: Hard-coded text
<Button>Submit</Button>
<span>Loading data...</span>
// ✅ Good: Using i18n
<Button>{t('button.submit')}</Button>
<span>{t('message.loadingData')}</span>
Translation Functions
The project runs two physically separate i18next instances — one owned
by the host app (react/), one owned by the BUI package
(packages/backend.ai-ui/). Which hook a component uses depends on which
package it lives in. Do not import useTranslation / Trans from
react-i18next directly inside packages/backend.ai-ui/src/** — ESLint
will reject it (see FR-2986).
| Where the component lives | Translation hook | Rich-text component |
|---|---|---|
react/src/** (host app) |
useTranslation() from react-i18next |
<Trans> from react-i18next |
packages/backend.ai-ui/src/** (BUI) |
useBAIi18n() from '../hooks/useBAIi18n' (relative) |
<BAITrans> from '../components/BAITrans' (relative) |
Host components (react/src/**)
Use react-i18next directly. The hook binds to the host's i18n via React Context.
import { useTranslation } from 'react-i18next';
const MyComponent = () => {
'use memo';
const { t } = useTranslation();
return (
<div>
<h1>{t('title.myPage')}</h1>
<Button>{t('button.save')}</Button>
</div>
);
};
BUI components (packages/backend.ai-ui/src/**)
Use the internal useBAIi18n hook (and BAITrans for rich text). They
bind explicitly to BUI's own i18next instance via
useTranslation(undefined, { i18n: buiI18n }) so the lookup never falls
back to React Context. The import path is relative — depth depends on the
file location.
// File: packages/backend.ai-ui/src/components/BAIPropertyFilter.tsx
import { useBAIi18n } from '../hooks/useBAIi18n';
const BAIPropertyFilter = () => {
'use memo';
const { t } = useBAIi18n();
return (
<>
<Input placeholder={t('comp:BAIPropertyFilter.PlaceHolder')} />
<Button>{t('comp:BAIPropertyFilter.ResetFilter')}</Button>
</>
);
};
For <Trans> (rich-text interpolation with React component children) use
BAITrans instead — it wraps <Trans i18n={buiI18n}> so callers cannot
forget to bind the instance.
// File: packages/backend.ai-ui/src/components/BAIDeleteConfirmModal.tsx
import { BAITrans } from './BAITrans';
<BAITrans
i18nKey="comp:BAIDeleteConfirmModal.TypeToConfirm"
values={{ confirmText }}
components={{ code: <BAIText code /> }}
/>
Why two instances?
React-i18next discovers the i18n instance through React Context. When
host and BUI share one physical react-i18next module (which they do
under pnpm + Vite dedup), one <I18nextProvider> from either side
shadows the other for the entire tree. Binding BUI calls to the
buiI18n instance explicitly via the useBAIi18n hook bypasses Context
discovery, so the two sides cannot leak into each other.
The ESLint rule in packages/backend.ai-ui/eslint.config.js blocks
direct imports of useTranslation, withTranslation, Translation,
Trans, and I18nextProvider from react-i18next inside BUI source
(only useBAIi18n.ts and BAITrans.tsx are exempt). This guarantees
the convention is enforced, not just documented.
Translation Key Structures
Main WebUI (/resources/i18n/)
Use dot-notation for hierarchical organization:
{
"button.submit": "Submit",
"button.cancel": "Cancel",
"error.network.timeout": "Network timeout occurred",
"message.success.saved": "Successfully saved"
}
Backend.AI UI Package (/packages/backend.ai-ui/src/locale/)
Use component-scoped naming with comp: prefix:
{
"comp:BAIPropertyFilter": {
"PlaceHolder": "Search",
"ResetFilter": "Reset filters"
},
"comp:FileExplorer": {
"UploadFiles": "Upload Files",
"CreateANewFolder": "Create a new folder",
"DeleteSelectedItemDesc": "Deleted files and folders cannot be restored."
},
"general": {
"NSelected": "{{count}} selected",
"button": {
"Cancel": "Cancel",
"Delete": "Delete"
}
}
}
Usage in Components
See the "BUI components" example in the Translation Functions
section above. BUI components use the useBAIi18n hook (internal to the
package), never useTranslation directly from react-i18next.
Translation Guidelines
Refer to /i18n-translation-instruction.md for comprehensive translation guidelines, including:
Key Principles
- Context Awareness: Consider UI/UX context and maintain consistency
- Placeholder Preservation: Always preserve placeholders exactly (e.g.,
{{count}},{{name}},{{variable}}) - Conciseness: Keep translations concise and appropriate for UI elements
- Professional Tone: Maintain professional and user-friendly tone
- Technical Terms: Handle technical terms appropriately
- Backend.AI Context: Translations should be contextually aware of:
- GPU virtualization, container orchestration, resource management
- Multi-tenancy, domains, sessions, workspaces
- Virtual folders (vfolders), mounts, sharing
- Kernels, images, agents, scaling rules
- UI Components: cards, pagination, forms, dialogs, notifications
Language-Specific Guidelines
Korean (ko)
- Use appropriate honorifics (존댓말)
- Maintain consistency between 합니다체 and 해요체
- Keep widely-used English technical terms when appropriate
Japanese (ja)
- Use appropriate politeness levels (敬語)
- Consider hiragana, katakana, and kanji usage
- Keep commonly used English loanwords in katakana
Chinese (zh-CN / zh-TW)
- Be aware of simplified vs traditional character differences
- Consider regional vocabulary differences
- Maintain appropriate formality levels
Naming Conventions
Key Casing Rules
Translation keys follow strict casing rules based on their value type:
- Keys whose value is a string (leaf keys) → start with Uppercase (PascalCase)
- Keys whose value is an object (namespace/group keys) → start with lowercase (camelCase)
// ✅ Good: Correct casing based on value type
{
"example": { // lowercase: value is an object { Title: "..." }
"Title": "Example", // Uppercase: value is a string
"Description": "..." // Uppercase: value is a string
},
"general": { // lowercase: value is an object
"NSelected": "{{count}} selected", // Uppercase: value is a string
"button": { // lowercase: value is an object { Cancel: "...", Delete: "..." }
"Cancel": "Cancel", // Uppercase: value is a string
"Delete": "Delete" // Uppercase: value is a string
}
}
}
// ❌ Bad: Incorrect casing
{
"Example": { // ❌ Object value should start with lowercase
"title": "Example" // ❌ String value should start with Uppercase
},
"General": { // ❌ Object value should start with lowercase
"nSelected": "..." // ❌ String value should start with Uppercase
}
}
Note: The comp: prefix for component namespaces (e.g., comp:BAIModal) follows a special convention where the component name after comp: uses PascalCase to match the component name.
Main WebUI Keys
category.subcategory.key
Common categories:
button.*- Button labelslabel.*- Form labels and field namesmessage.*- General messageserror.*- Error messagesdialog.*- Dialog titles and contenttooltip.*- Tooltip textplaceholder.*- Input placeholderstitle.*- Page and section titles
Backend.AI UI Package Keys
comp:<ComponentName>.<key>
Structure:
comp:<ComponentName>.*- Component-specific translationsgeneral.*- Shared general translationserror.*- Shared error messages
Examples:
{
"comp:BAIModal": {
"ConfirmTitle": "Confirmation",
"Cancel": "Cancel"
},
"comp:BAITable": {
"SearchTableColumn": "Search table columns",
"SelectColumnToDisplay": "Select columns to display"
},
"general": {
"NSelected": "{{count}} selected",
"TotalItems": "Total {{total}} items"
}
}
Variables and Placeholders
Preserve Placeholder Format
Always maintain the exact placeholder format:
// ✅ Good: Preserving placeholders
t("message.welcome", { username: user.name });
// Translation: "Welcome, {{username}}!"
t("comp:FileExplorer.DownloadStarted", { fileName: file.name });
// Translation: "File \"{{fileName}}\" download has started."
t("general.NSelected", { count: selectedItems.length });
// Translation: "{{count}} selected"
Common Placeholder Formats
{{variable}},{variable}
Code Review Checklist
When reviewing code for i18n, check for:
- No hard-coded user-facing text in any language
- All labels, messages, errors use i18n functions
- Translation keys follow naming conventions:
category.keyfor main WebUIcomp:ComponentName.keyfor backend.ai-ui package
- Key casing follows value type: Uppercase for string values, lowercase for object values
- Placeholders are preserved correctly (e.g.,
{{count}},{{name}}) - Context is appropriate for Backend.AI platform
- Technical terms are handled consistently
- Translations are concise and UI-appropriate
- No concatenated translated strings (use placeholders instead)
- Component-specific translations use
comp:prefix in backend.ai-ui package - BUI components (
packages/backend.ai-ui/src/**) useuseBAIi18n/BAITrans— notuseTranslation/Transfromreact-i18nextdirectly (ESLint enforces this; see FR-2986)
Common Mistakes to Avoid
String Concatenation
// ❌ Bad: Concatenating translated strings
const message = t("hello") + " " + username + "!";
// ✅ Good: Use placeholders
const message = t("greeting.hello", { username });
Wrong Key Format in backend.ai-ui Package
// ❌ Bad: Missing comp: prefix
t("BAIModal.ConfirmTitle");
// ✅ Good: Use comp: prefix
t("comp:BAIModal.ConfirmTitle");
Wrong i18n hook inside BUI
// ❌ Bad: Importing useTranslation directly inside packages/backend.ai-ui/src/**
// (ESLint error — `'useTranslation' import from 'react-i18next' is restricted`)
import { useTranslation } from 'react-i18next';
const MyBuiComponent = () => {
const { t } = useTranslation();
...
};
// ✅ Good: Use BUI's internal hook
import { useBAIi18n } from '../hooks/useBAIi18n'; // path depth varies
const MyBuiComponent = () => {
'use memo';
const { t } = useBAIi18n();
...
};
Conditional Text
// ❌ Bad: Hard-coded conditional text
const status = isActive ? "Active" : "Inactive";
// ✅ Good: Translate both states
const status = isActive ? t("status.active") : t("status.inactive");
Plural Forms
// ❌ Bad: Manual plural handling
const text = count === 1 ? "1 item" : `${count} items`;
// ✅ Good: Use i18n plural support with placeholders
const text = t("general.NSelected", { count });
// Translation supports: "{{count}} selected"
Adding New Translations
Main WebUI (/resources/i18n/)
- Add translation keys to JSON files in
/resources/i18n/ - Provide translations for all supported languages
- Use the translation key in component code
- Run
make i18nto extract and validate translation strings
Backend.AI UI Package (/packages/backend.ai-ui/src/locale/)
- Add translation keys to JSON files (e.g.,
en.json,ko.json) - Use
comp:<ComponentName>as the top-level key for component-specific translations - Use
generalorerrorfor shared translations - Ensure all supported languages have corresponding translations
Supported Languages
en- English (default)ko- Koreanja- Japanesezh-CN- Chinese (Simplified)zh-TW- Chinese (Traditional)- And others as configured
Testing Translations
- Test UI in all supported languages
- Verify placeholder substitution works correctly
- Check for text overflow in different languages
- Ensure proper alignment and layout with longer translations
- Test that
comp:prefix works correctly in backend.ai-ui components