Frontend i18n
Skill LaoitdevOpen/laoitdev-skills/skills/frontend/frontend-i18n
Translation and i18n rules for the AMIS admin project. Use this whenever writing any user-visible string, building a form with validation messages, handling API or business errors, or adding new translation keys. Also use when the user asks 'how do I translate X', 'add translation for Y', 'don't hardcode this string', or 'add an error message'. Covers: useTranslation usage, Zod schema factory pattern, validation key structure, API error translation via useAPIErrorTranslator, and adding new error codes to both translation files.From its SKILL.md
npx -y skills add LaoitdevOpen/laoitdev-skills --skill frontend-i18nAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
8.7 KB, ~2.2k tokens by cl100k_base, as published. Nobody here has run it
Translation & i18n
The project uses react-i18next with a single translation namespace. All user-visible text — labels, placeholders, validation messages, snackbar messages, error strings — must go through t(). Never hardcode English (or Lao) strings directly in components or schemas.
Config: src/core/config/i18n.config.ts
Translation files: src/assets/locales/en/translation.json and src/assets/locales/lo/translation.json
Default language: lo (Lao). Fallback: lo.
1. Using t() in Components
Always call useTranslation() at the top of the component. Pass the result of t() everywhere a string is displayed to the user.
const { t } = useTranslation()
// Labels, buttons, headings
<Button>{t('common.save')}</Button>
<Typography>{t('ministries.edit')}</Typography>
// Dynamic values
<Typography>{t('dashboard.welcome', { name: user.name })}</Typography>
// Option lists — store the key, translate at render time
const GROUP_OPTIONS = [
{ value: 'CENTER', label: 'ministries.groups.CENTER' },
{ value: 'PROVINCE', label: 'ministries.groups.PROVINCE' },
]
{GROUP_OPTIONS.map(opt => <MenuItem key={opt.value}>{t(opt.label)}</MenuItem>)}
Never do this:
// Bad — hardcoded string
<Button>Save</Button>
label="ຊື່"
// Bad — string in a constant without t()
const label = 'Province'
2. Zod Validation Schemas — Factory Function Pattern
Schemas that produce user-visible error messages must be factory functions that accept t: TFunction. This lets them produce translated messages at runtime.
import { z } from 'zod'
import type { TFunction } from 'i18next'
export const getFeatureSchema = (t: TFunction) => z.object({
name: z
.string()
.min(1, t('validation.required'))
.max(255, t('validation.maxLength', { max: 255 })),
code: z
.string()
.max(50, t('validation.maxLength', { max: 50 }))
.optional()
.or(z.literal('')),
group: z.enum(['A', 'B'], {
message: t('validation.required'),
}),
})
export type FeatureFormData = z.infer<ReturnType<typeof getFeatureSchema>>
In the form component, call the factory with t:
const { t } = useTranslation()
const schema = getFeatureSchema(t)
const form = useAppForm({
defaultValues: { name: '', code: '', group: 'A' } as FeatureFormData,
validators: { onChange: schema },
onSubmit: async ({ value }) => { ... }
})
Available validation.* keys
These shared keys live at the top level of translation.json:
| Key | Parameters | Use for |
|---|---|---|
validation.required | — | Field is empty / required |
validation.minLength | { min } | Minimum character count |
validation.maxLength | { max } | Maximum character count |
validation.email | — | Email format |
validation.number | — | Must be a number |
validation.positive | — | Must be positive |
validation.min | { min } | Numeric minimum |
validation.max | { max } | Numeric maximum |
validation.passwordMismatch | — | Password confirmation |
If you need a validation message that doesn't fit any of these, add a new key under validation.* in both en/translation.json and lo/translation.json.
For feature-specific validation messages that only apply to one domain, put them under featureName.validation.*:
// in translation.json
"ministries": {
"validation": {
"codeFormat": "Code must be alphanumeric"
}
}
3. API Error Translation
The project uses useAPIErrorTranslator (at src/shared/hooks/useAPIErrorTranslator.ts) to translate server errors into user-friendly strings. Use it in every mutation onError callback.
import { useAPIErrorTranslator } from '@/shared/hooks'
const { translateError } = useAPIErrorTranslator()
createMutation.mutate(value, {
onSuccess: () => showSuccess(t('feature.createSuccess')),
onError: (error) => showError(translateError(error)),
})
How translateError works (priority order)
- Network / no response →
errors.http.503orerrors.http.504 - HTTP 422 with
errorsarray → field-level validation viaapiValidation.* - Legacy
detailarray → same field-level validation statuscode present → business error viaerrors.{category}.*- HTTP status code →
errors.http.{status} - Fallback → raw message or
errors.general.unexpected
apiValidation.* keys — server-side field errors
When the API returns a 422 with validation details ({ field, tag, param }), translateError maps the tag to apiValidation.{tag}:
| Key | Parameters | Tag |
|---|---|---|
apiValidation.required | { field } | required |
apiValidation.min | { field, n } | min |
apiValidation.max | { field, n } | max |
apiValidation.email | { field } | email |
apiValidation.gt | { field, n } | gt |
apiValidation.gte | { field, n } | gte |
apiValidation.numeric | { field } | numeric |
apiValidation.alpha | { field } | alpha |
apiValidation.alphanum | { field } | alphanum |
Field names in the error output come from fields.{fieldName}. Add new field names to fields.* in both translation files.
4. Adding New Business Errors
When the backend returns a new error code (e.g., CONTRACT_ALREADY_SIGNED), add it in two places.
Step 1: Add to useAPIErrorTranslator.ts
Find the right translate*Error function and add the code to its errorCodeMap:
// In translateInvalidArgumentError:
const errorCodeMap: Record<string, string> = {
// ... existing entries
'CONTRACT_ALREADY_SIGNED': 'contractAlreadySigned', // add here
}
Which function to use:
translateNotFoundError→ statusNOT_FOUND(resource doesn't exist)translateAlreadyExistsError→ statusALREADY_EXISTS(duplicate)translateInvalidArgumentError→ statusINVALID_ARGUMENT(bad state / business rule violation)translateBusinessErrorin theINTERNAL/UNAVAILABLEcases → for service-level errors
If the error belongs in errors.businessLogic.* (not errors.invalidArgument.*), add the key name to the businessLogic check inside translateInvalidArgumentError:
if (['resourceFeeAlreadyPaid', 'contractAlreadySigned'].includes(errorType)) {
return t(`errors.businessLogic.${errorType}`)
}
Step 2: Add translation keys to both files
In src/assets/locales/en/translation.json:
"errors": {
"invalidArgument": {
"contractAlreadySigned": "Contract has already been signed"
}
}
In src/assets/locales/lo/translation.json:
"errors": {
"invalidArgument": {
"contractAlreadySigned": "ສັນຍານີ້ໄດ້ຖືກລົງນາມແລ້ວ"
}
}
Always add keys to both files at the same time.
5. common.errors.* — Module-Specific Export/Operation Errors
There is a flat error map under common.errors used for module-specific operation codes (e.g. VEHICLE_ALREADY_EXPORTED). These are used directly in components, not through useAPIErrorTranslator:
// Used in ExportDialog components
const errorKey = `common.errors.${error.code}`
showError(t(errorKey, { defaultValue: error.message }))
To add a new one, add to common.errors in both translation files:
"common": {
"errors": {
"LAND_ALREADY_EXPORTED": "Land record has already been exported"
}
}
6. Translation Key Naming Conventions
- Use camelCase for all keys:
createSuccess, notcreate_successorCreateSuccess - Feature-level keys nest under the feature name:
ministries.createSuccess - Shared labels go under
common.*orfields.* - Validation messages:
validation.*(generic) orfeatureName.validation.*(specific) - Error messages:
errors.{category}.{key}— never put error strings in the feature namespace
7. Adding Translation Keys Checklist
When adding any new user-visible text:
- Add the English key to
en/translation.json - Add the Lao key to
lo/translation.json - Use
t('key.path')in the component — never the raw string - If it's a validation message in a Zod schema, use the factory function pattern
- If it's an API error code, register it in
useAPIErrorTranslator.ts+ both translation files
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.