Skip to main content

Internationalization

DEVELOPER Intermediate

Support multiple languages with i18next in EZ-Console applications.

Overview

EZ-Console uses react-i18next for internationalization (i18n) with built-in support for multiple languages. This allows you to create applications that support multiple languages and locales, providing a better user experience for global audiences.

Quick Start

Basic Usage

// src/App.tsx
import { i18n, useTranslation } from 'ez-console';

// Add translations
i18n.addResource('en', 'translation', 'product.name', 'Product Name');
i18n.addResource('zh-CN', 'translation', 'product.name', '产品名称');

// Use in components
export const ProductForm: React.FC = () => {
const { t } = useTranslation();

return (
<Form>
<Form.Item label={t('product.name')}>
<Input />
</Form.Item>
</Form>
);
};

Adding Translations

In App Component

Add translations directly in your App component:

// src/App.tsx
import { i18n } from 'ez-console';

// Add single translation
i18n.addResource('en', 'translation', 'product.name', 'Product Name');
i18n.addResource('zh-CN', 'translation', 'product.name', '产品名称');

// Add multiple translations at once
i18n.addResources('en', 'translation', {
'product.name': 'Product Name',
'product.description': 'Description',
'product.price': 'Price',
'product.create': 'Create Product',
'product.edit': 'Edit Product',
'product.delete': 'Delete Product',
});

i18n.addResources('zh-CN', 'translation', {
'product.name': '产品名称',
'product.description': '描述',
'product.price': '价格',
'product.create': '创建产品',
'product.edit': '编辑产品',
'product.delete': '删除产品',
});

In Separate Files

For better organization, create separate translation files:

// src/i18n/en/product.ts
export default {
'product.name': 'Product Name',
'product.description': 'Description',
'product.price': 'Price',
'product.stock': 'Stock',
'product.category': 'Category',
'product.create': 'Create Product',
'product.edit': 'Edit Product',
'product.delete': 'Delete Product',
'product.list': 'Product List',
'product.detail': 'Product Detail',
'product.searchPlaceholder': 'Search products...',
};
// src/i18n/zh-CN/product.ts
export default {
'product.name': '产品名称',
'product.description': '描述',
'product.price': '价格',
'product.stock': '库存',
'product.category': '分类',
'product.create': '创建产品',
'product.edit': '编辑产品',
'product.delete': '删除产品',
'product.list': '产品列表',
'product.detail': '产品详情',
'product.searchPlaceholder': '搜索产品...',
};

Then load translations in your App component:

// src/App.tsx
import { i18n } from 'ez-console';
import enProduct from './i18n/en/product';
import zhProduct from './i18n/zh-CN/product';

i18n.addResources('en', 'translation', enProduct);
i18n.addResources('zh-CN', 'translation', zhProduct);

Using Translations

In Components

Use the useTranslation hook to access translations:

import { useTranslation } from 'ez-console';

export const ProductForm: React.FC = () => {
const { t } = useTranslation();

return (
<Form>
<Form.Item label={t('product.name')}>
<Input placeholder={t('product.searchPlaceholder')} />
</Form.Item>
<Button type="primary">{t('product.create')}</Button>
</Form>
);
};

With Parameters

Use parameters for dynamic content:

// Add translation with parameter
i18n.addResource('en', 'translation', 'product.itemsCount',
'Showing {{count}} products');
i18n.addResource('zh-CN', 'translation', 'product.itemsCount',
'显示 {{count}} 个产品');

// Use in component
const { t } = useTranslation();

<p>{t('product.itemsCount', { count: 10 })}</p>
// Output: "Showing 10 products" (en) or "显示 10 个产品" (zh-CN)

Pluralization

Handle plural forms correctly:

// English (requires plural forms)
i18n.addResources('en', 'translation', {
'product.count_one': '{{count}} product',
'product.count_other': '{{count}} products',
});

// Chinese (no plural form needed)
i18n.addResources('zh-CN', 'translation', {
'product.count': '{{count}} 个产品',
});

// Use in component
const { t } = useTranslation();

<p>{t('product.count', { count: 1 })}</p> // "1 product" (en) or "1 个产品" (zh-CN)
<p>{t('product.count', { count: 5 })}</p> // "5 products" (en) or "5 个产品" (zh-CN)

Default Values

Provide fallback text if a translation key doesn't exist:

const { t } = useTranslation();

<p>{t('product.unknownKey', { defaultValue: 'Fallback Text' })}</p>

Nested Keys

Use dot notation for nested translation keys:

// Translation structure
i18n.addResources('en', 'translation', {
'product.form.name': 'Product Name',
'product.form.price': 'Price',
'product.messages.createSuccess': 'Product created successfully',
});

// Usage
<Form.Item label={t('product.form.name')}>
<Input />
</Form.Item>

message.success(t('product.messages.createSuccess'));

Language Switching

Built-in Language Switcher

EZ-Console includes a built-in language switcher in the header that is automatically available when you use the EZApp component. Users can switch languages from the header dropdown.

// Automatically included in EZApp
import { EZApp } from 'ez-console';

<EZApp basePath='/' />

Programmatic Language Change

Change language programmatically using the i18n object:

import { useTranslation } from 'ez-console';

export const LanguageSelector: React.FC = () => {
const { i18n } = useTranslation();

const handleChange = (lang: string) => {
i18n.changeLanguage(lang);
};

return (
<Select
value={i18n.language}
onChange={handleChange}
>
<Select.Option value="en">English</Select.Option>
<Select.Option value="zh-CN">中文</Select.Option>
<Select.Option value="fr">Français</Select.Option>
</Select>
);
};

Getting Current Language

import { useTranslation } from 'ez-console';

export const MyComponent: React.FC = () => {
const { i18n } = useTranslation();
const currentLang = i18n.language; // e.g., 'en', 'zh-CN'

return <div>Current language: {currentLang}</div>;
};

Supported Languages

EZ-Console includes built-in translations for the following languages:

  • English (en) - Default
  • Chinese Simplified (zh-CN)
  • French (fr)
  • German (de)
  • Spanish (es)
  • Swedish (sv)
  • Arabic (ar)

You can add support for additional languages by providing translation resources.

Translation Organization

Naming Convention

Use dot notation with a hierarchical structure:

module.component.action
module.component.field
module.component.message

Examples

product.list.title
product.form.name
product.form.submit
product.message.createSuccess
user.profile.title
user.profile.email
common.save
common.cancel

Common Translations

Keep common translations in a shared namespace for consistency:

// src/i18n/en/common.ts
export default {
'common.save': 'Save',
'common.cancel': 'Cancel',
'common.edit': 'Edit',
'common.delete': 'Delete',
'common.create': 'Create',
'common.update': 'Update',
'common.search': 'Search',
'common.reset': 'Reset',
'common.confirm': 'Confirm',
'common.submit': 'Submit',
'common.loading': 'Loading...',
'common.success': 'Success',
'common.error': 'Error',
'common.confirmDelete': 'Are you sure you want to delete?',
'common.confirmDeleteItem': 'Are you sure you want to delete {{item}}?',
'common.confirmDeleteMessage': 'This action cannot be undone.',
};
// src/i18n/zh-CN/common.ts
export default {
'common.save': '保存',
'common.cancel': '取消',
'common.edit': '编辑',
'common.delete': '删除',
'common.create': '创建',
'common.update': '更新',
'common.search': '搜索',
'common.reset': '重置',
'common.confirm': '确认',
'common.submit': '提交',
'common.loading': '加载中...',
'common.success': '成功',
'common.error': '错误',
'common.confirmDelete': '确定要删除吗?',
'common.confirmDeleteItem': '确定要删除 {{item}} 吗?',
'common.confirmDeleteMessage': '此操作无法撤销。',
};

Translation File Structure

Organize translation files by language and module:

src/i18n/
├── en/
│ ├── common.ts
│ ├── product.ts
│ ├── user.ts
│ └── index.ts
├── zh-CN/
│ ├── common.ts
│ ├── product.ts
│ ├── user.ts
│ └── index.ts
└── index.ts
// src/i18n/en/index.ts
import common from './common';
import product from './product';
import user from './user';

export default {
...common,
...product,
...user,
};
// src/App.tsx
import { i18n } from 'ez-console';
import enTranslations from './i18n/en';
import zhCNTranslations from './i18n/zh-CN';

i18n.addResources('en', 'translation', enTranslations);
i18n.addResources('zh-CN', 'translation', zhCNTranslations);

Backend i18n

Error Messages

Backend error messages should always be in English. The client can translate error codes if needed:

// ✅ Good: English error message
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "Product name is required"))

// ❌ Bad: Non-English error message
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "产品名称必填"))

Client-Side Error Translation

Map error codes to translation keys on the client:

// src/i18n/en/errors.ts
export default {
'errors.badRequest': 'Invalid request',
'errors.unauthorized': 'Unauthorized access',
'errors.forbidden': 'Access forbidden',
'errors.notFound': 'Resource not found',
'errors.serverError': 'Internal server error',
};

// src/utils/errorHandler.ts
import { useTranslation } from 'ez-console';

export const useErrorHandler = () => {
const { t } = useTranslation();

const getErrorMessage = (errorCode: string) => {
const errorMap: Record<string, string> = {
'E4001': t('errors.badRequest'),
'E4011': t('errors.unauthorized'),
'E4031': t('errors.forbidden'),
'E4041': t('errors.notFound'),
'E5001': t('errors.serverError'),
};

return errorMap[errorCode] || errorCode;
};

return { getErrorMessage };
};

Date and Number Formatting

Date Formatting

Use date-fns for locale-aware date formatting:

import { format } from 'date-fns';
import { enUS, zhCN, fr } from 'date-fns/locale';

const locales: Record<string, Locale> = {
'en': enUS,
'zh-CN': zhCN,
'fr': fr,
};

export const useDateFormatter = () => {
const { i18n } = useTranslation();

const formatDate = (date: Date, formatStr: string = 'PPP') => {
return format(date, formatStr, { locale: locales[i18n.language] });
};

return { formatDate };
};

Usage

const { formatDate } = useDateFormatter();

<p>{formatDate(new Date(), 'PPP')}</p>
// Output: "January 1, 2024" (en) or "2024年1月1日" (zh-CN)

Number Formatting

Use Intl.NumberFormat for locale-aware number formatting:

export const useNumberFormatter = () => {
const { i18n } = useTranslation();

const formatNumber = (num: number) => {
return new Intl.NumberFormat(i18n.language).format(num);
};

const formatCurrency = (amount: number, currency: string = 'USD') => {
return new Intl.NumberFormat(i18n.language, {
style: 'currency',
currency: currency,
}).format(amount);
};

return { formatNumber, formatCurrency };
};

Usage

const { formatNumber, formatCurrency } = useNumberFormatter();

<p>{formatNumber(1234567.89)}</p>
// Output: "1,234,567.89" (en) or "1,234,567.89" (zh-CN)

<p>{formatCurrency(99.99, 'USD')}</p>
// Output: "$99.99" (en) or "US$99.99" (zh-CN)

Best Practices

1. Use Translation Keys

Never hardcode user-facing text:

// ✅ Good
<Button>{t('common.save')}</Button>

// ❌ Bad
<Button>Save</Button>

2. Consistent Naming

Follow the naming convention consistently:

// ✅ Good
'product.form.name'
'product.form.price'
'product.list.title'

// ❌ Bad
'productName'
'priceOfProduct'
'listTitle'

3. Complete Translations

Provide translations for all supported languages:

// ✅ Good: All languages covered
i18n.addResources('en', 'translation', { 'product.name': 'Product' });
i18n.addResources('zh-CN', 'translation', { 'product.name': '产品' });
i18n.addResources('fr', 'translation', { 'product.name': 'Produit' });

// ❌ Bad: Missing translations
i18n.addResources('en', 'translation', { 'product.name': 'Product' });
// Missing zh-CN and fr

4. Context for Translators

Provide context comments in translation files:

// src/i18n/en/product.ts
export default {
// Product form labels
'product.form.name': 'Product Name', // Main product identifier
'product.form.price': 'Price', // Product price in USD
'product.form.stock': 'Stock Quantity', // Available inventory count

// Product actions
'product.actions.create': 'Create Product', // Button to create new product
'product.actions.edit': 'Edit Product', // Button to edit existing product
};

5. Parameters for Dynamic Content

Always use parameters for dynamic content:

// ✅ Good
t('product.itemsCount', { count: 10 })

// ❌ Bad
`${t('product.items')} ${count}`

6. Test All Languages

Test your application in all supported languages:

// Test helper
export const renderWithi18n = (component: ReactNode, options?: { language?: string }) => {
if (options?.language) {
i18n.changeLanguage(options.language);
}
return render(component);
};

// Test
test('renders English translation', () => {
const { getByText } = renderWithi18n(<ProductForm />, { language: 'en' });
expect(getByText('Product Name')).toBeInTheDocument();
});

test('renders Chinese translation', () => {
const { getByText } = renderWithi18n(<ProductForm />, { language: 'zh-CN' });
expect(getByText('产品名称')).toBeInTheDocument();
});

Troubleshooting

Translation Key Not Found

Issue: Translation key returns the key itself instead of the translated text.

Solution:

  1. Verify the translation key exists in your translation files
  2. Check that translations are loaded before the component renders
  3. Use defaultValue as a fallback
// Use defaultValue as fallback
<p>{t('product.unknownKey', { defaultValue: 'Fallback Text' })}</p>

Language Not Switching

Issue: Language doesn't change when using the language switcher.

Solution:

  1. Ensure i18n.changeLanguage() is called correctly
  2. Check that translations exist for the target language
  3. Verify the language code matches (e.g., 'zh-CN' not 'zh')

Pluralization Not Working

Issue: Plural forms not displaying correctly.

Solution:

  1. For English, use _one and _other suffixes
  2. For languages without plurals, use a single key
// English (requires plural forms)
'product.count_one': '{{count}} product',
'product.count_other': '{{count}} products',

// Chinese (no plural form)
'product.count': '{{count}} 个产品',

Next Steps


Need help? Ask in GitHub Discussions.