Internationalization
XinAdmin includes a complete i18n solution covering both the frontend (React + react-i18next) and backend (Laravel), supporting Chinese and English out of the box, with easy extensibility for additional languages.
Architecture Overview
┌──────────────────┐ User-Language Header ┌────────────────────┐
│ Frontend React │ ────────────────────────> │ Backend Laravel │
│ │ Axios interceptor │ │
│ i18next │ auto-attaches header │ LanguageMiddleware │
│ localStorage │ │ App::setLocale() │
│ antd/dayjs │ │ __('key') │
└──────────────────┘ └────────────────────┘
The frontend uses react-i18next to manage UI text, syncing Ant Design components and dayjs locale on language switch. The backend uses the LanguageMiddleware to detect the request language and Laravel's __() helper to return localized text.
Frontend i18n
Tech Stack
Directory Structure
source_code/web/locales/
├── i18n.ts # i18next initialization
├── index.ts # Resource barrel export
├── options.ts # Supported languages + antd/dayjs mapping
├── zh_CN/ # Chinese translation files
│ ├── index.ts # Barrel export
│ ├── layout.ts # Layout strings
│ ├── menu.ts # Menu labels
│ ├── login.ts # Login page
│ ├── dashboard.ts # Dashboard
│ ├── xin-table.ts # XinTable component
│ ├── xin-form.ts # XinForm component
│ ├── xin-crud.ts # CRUD component
│ ├── sys-user-list.ts # User management
│ ├── sys-user-role.ts # Role management
│ ├── sys-user-rule.ts # Permission rules
│ ├── sys-user-dept.ts # Department management
│ ├── sys-file.ts # File management
│ ├── sys-dict.ts # Dictionary management
│ ├── sys-mail.ts # Mail management
│ ├── sys-setting.ts # System settings
│ ├── sys-storage.ts # Storage config
│ ├── user-setting.ts # User settings
│ ├── watcher.ts # System monitor
│ └── system-info.ts # System info
└── en_US/ # English translation files (identical structure)
└── ...(19 files, same as above)
i18next Initialization
// locales/i18n.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import resources from "@/locales/index.ts";
i18n.use(initReactI18next).init({
resources,
lng: localStorage.getItem('i18nextLng') || "zh",
fallbackLng: "zh",
debug: false,
interpolation: { escapeValue: false },
});
export default i18n;
The entry file main.tsx imports this module at the top level, ensuring i18next initializes before the app renders.
Language Options
// locales/options.ts
import zh_CN from "antd/es/locale/zh_CN";
import en_US from "antd/es/locale/en_US";
import 'dayjs/locale/en';
import 'dayjs/locale/zh-cn';
export default [
{ label: '简体中文', value: 'zh', antdLocale: zh_CN, dayjsLocale: 'zh-cn' },
{ label: 'English', value: 'en', antdLocale: en_US, dayjsLocale: 'en' },
];
Resource Barrel Export
// locales/index.ts
import en_US from "@/locales/en_US";
import zh_CN from "@/locales/zh_CN";
export default {
en: { translation: en_US },
zh: { translation: zh_CN },
};
Each locale directory has its own index.ts barrel export that merges all module translations into a single object.
Each module file exports an object with dot-separated namespace keys:
// locales/en_US/xin-table.ts
export default {
'xinTable.add': 'Add',
'xinTable.edit': 'Edit',
'xinTable.delete': 'Delete',
'xinTable.deleteConfirm': 'Are you sure you want to delete record {{id}}?',
'xinTable.total': 'Total {{total}} records',
'xinTable.search.search': 'Search',
'xinTable.search.reset': 'Reset',
};
Supports {{variable}} interpolation syntax.
Translation Key Conventions
useLanguage Hook
The core language switching logic is encapsulated in the useLanguage hook:
// hooks/useLanguage.ts
import { useTranslation } from 'react-i18next';
import { useMemo } from 'react';
import dayjs from 'dayjs';
import options from '@/locales/options';
const useLanguage = () => {
const { i18n } = useTranslation();
const changeLanguage = async (lng: string) => {
const config = getLanguageConfig(lng);
dayjs.locale(config.dayjsLocale); // Update dayjs
await i18n.changeLanguage(lng); // Update i18next
localStorage.setItem('i18nextLng', lng); // Persist
};
const antdLocale = useMemo(() => {
const config = options.find(opt => opt.value === i18n.language);
return config ? config.antdLocale : options[0].antdLocale;
}, [i18n.language]);
return {
antdLocale, // Ant Design component locale
language: i18n.language,
changeLanguage, // Language switch method
getLanguageConfig,
languageOptions: options,
};
};
Switching languages updates three layers simultaneously:
- dayjs locale — Date formatting
- i18next language — UI text
- localStorage — Persistence
AntdProvider Integration
// components/AntdProvider/index.tsx
import { ConfigProvider } from 'antd';
import useLanguage from '@/hooks/useLanguage';
const AntdProvider = ({ children }) => {
const { antdLocale } = useLanguage();
return (
<ConfigProvider locale={antdLocale}>
{children}
</ConfigProvider>
);
};
LanguageSwitcher Component
// components/LanguageSwitcher/index.tsx
const LanguageSwitcher: React.FC<ButtonProps> = (props) => {
const { languageOptions, language, changeLanguage } = useLanguage();
const items = languageOptions.map(option => ({
key: option.value,
label: option.label,
onClick: () => changeLanguage(option.value),
}));
return (
<Dropdown menu={{ items, selectedKeys: [language] }}>
<Button icon={<TranslationOutlined />} {...props} />
</Dropdown>
);
};
Rendered in the header, users click the icon to switch languages.
Using Translations in Components
import { useTranslation } from 'react-i18next';
function MyComponent() {
const { t } = useTranslation();
return (
<div>
<h1>{t('menu.dashboard')}</h1>
<Button>{t('xinTable.add')}</Button>
<span>{t('xinTable.total', { total: 100 })}</span>
</div>
);
}
Axios Request Interceptor
Every API request automatically attaches the current language to the User-Language header:
// utils/request.ts - request interceptor
instance.interceptors.request.use((config) => {
config.headers = config.headers || {};
// Auto-attach i18n language to request header
const currentLanguage = localStorage.getItem('i18nextLng');
if (currentLanguage) {
config.headers['User-Language'] = currentLanguage;
}
return config;
});
Adding a New Language (Frontend)
Example: Adding Japanese support.
1. Create translation file directory
locales/ja_JP/
├── index.ts
├── layout.ts
├── menu.ts
├── xin-table.ts
└── ...(all module files)
2. Update language options
// locales/options.ts
import ja_JP from "antd/es/locale/ja_JP";
import 'dayjs/locale/ja';
export default [
{ label: '简体中文', value: 'zh', antdLocale: zh_CN, dayjsLocale: 'zh-cn' },
{ label: 'English', value: 'en', antdLocale: en_US, dayjsLocale: 'en' },
{ label: '日本語', value: 'ja', antdLocale: ja_JP, dayjsLocale: 'ja' }, // New
];
3. Register to resource mapping
// locales/index.ts
import ja_JP from "@/locales/ja_JP";
export default {
en: { translation: en_US },
zh: { translation: zh_CN },
ja: { translation: ja_JP }, // New
};
Backend i18n
Configuration
// config/app.php
'locale' => env('APP_LOCALE', 'en'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
Override via .env:
APP_LOCALE=zh
APP_FALLBACK_LOCALE=en
Directory Structure
source_code/lang/
├── en/
│ ├── auth.php # Authentication messages
│ ├── pagination.php # Pagination labels
│ ├── passwords.php # Password reset messages
│ ├── validation.php # Form validation messages
│ ├── system.php # System/business messages
│ └── user.php # User module messages
└── zh/
└── ...(same 6 files)
Uses Laravel standard PHP array format:
<?php
// lang/en/user.php
return [
'not_login' => 'Please log in first',
'login_success' => 'Login Success',
'login_error' => 'Login Error, Please check your username and password',
'logout_success' => 'Logout Success',
'old_password_error' => 'Old password error',
'user_not_exist' => 'User does not exist, please register first',
'user_is_disabled' => 'User is disabled',
];
Nested arrays use dot notation:
// lang/en/system.php
return [
'file' => [
'image' => 'image',
'audio' => 'audio',
'upload_failed' => 'Upload failed',
'not_found' => 'File not found',
],
'error' => [
'no_permission' => 'Sorry, you do not have this permission, please contact the administrator',
'route_not_exist' => 'Route does not exist',
],
'data_not_exist' => 'Data does not exist',
];
// Usage
__('system.file.upload_failed'); // "Upload failed"
__('system.error.no_permission'); // "Sorry, you do not have this permission..."
Translation File Categories
LanguageMiddleware
Locale detection priority chain (high to low):
// modules/Common/Middlewares/LanguageMiddleware.php
class LanguageMiddleware
{
protected array $supportedLanguages = [
'en' => 'en',
'zh' => 'zh',
'jp' => 'ja',
];
public function handle(Request $request, Closure $next)
{
$locale = $this->getLocale($request);
App::setLocale($locale);
return $next($request);
}
protected function getLocale(Request $request): string
{
// Priority 1: URL parameter ?lang=en
if ($request->has('lang') && $this->isSupported($request->get('lang'))) {
return $request->get('lang');
}
// Priority 2: User-Language header (sent by frontend Axios)
$browserLocale = $request->header('User-Language');
if ($browserLocale && $this->isSupported($browserLocale)) {
return $browserLocale;
}
// Priority 3: Session-stored locale
if (Session::has('locale') && $this->isSupported(Session::get('locale'))) {
return Session::get('locale');
}
// Priority 4: config('app.locale') default
return config('app.locale', 'zh');
}
}
CORS Configuration
AllowCrossDomainMiddleware explicitly allows the User-Language header in CORS:
$response->headers->set(
'Access-Control-Allow-Headers',
'Content-Type, Authorization, X-Requested-With, User-Language'
);
Using Translations in Controllers
class IndexController extends Controller
{
public function login(Request $request)
{
if ($valid) {
return response()->json([
'success' => true,
'msg' => __('user.login_success'),
]);
}
return response()->json([
'success' => false,
'msg' => __('user.login_error'),
]);
}
}
Adding a New Language (Backend)
Example: Adding Japanese support.
1. Create language files
// lang/ja/user.php
return [
'not_login' => 'まずログインしてください',
'login_success' => 'ログイン成功',
'login_error' => 'ログイン失敗',
'logout_success' => 'ログアウト成功',
];
Copy all 6 files to lang/ja/ and translate each.
2. Register in middleware
// LanguageMiddleware.php
protected array $supportedLanguages = [
'en' => 'en',
'zh' => 'zh',
'ja' => 'ja', // New, key must match the frontend language code
];
Important: The value in the frontend options.ts must match the key in the middleware's supportedLanguages array, otherwise the middleware won't recognize it.
End-to-End Data Flow
A complete i18n request lifecycle:
1. User clicks LanguageSwitcher in the UI
│
2. useLanguage().changeLanguage('en')
├── dayjs.locale('en') # Date format switch
├── i18n.changeLanguage('en') # React UI text switch
└── localStorage.setItem(..., 'en') # Persist
│
3. AntdProvider detects i18n.language change
└── ConfigProvider locale={en_US} # Ant Design component switch
│
4. API request sent
└── Axios interceptor reads localStorage
└── Sets headers['User-Language'] = 'en'
│
5. Backend LanguageMiddleware
├── Check ?lang= param (none)
├── Check User-Language header = 'en' ✓
└── App::setLocale('en')
│
6. Controller __() calls
└── Reads lang/en/*.php
└── Returns localized msg text
│
7. Frontend receives response with translated text
Best Practices
1. Key Naming Conventions
- Frontend:
module.page.field format, e.g., sysUserList.username, layout.headerHeight
- Backend:
file.nested_key format, e.g., user.login_success, system.file.upload_failed
- Keep keys identical across all locales for easy comparison and completion
2. Interpolation Usage
// Frontend
t('xinTable.total', { total: 100 }) // "Total 100 records"
t('xinTable.deleteConfirm', { id: 42 }) // "Are you sure you want to delete record 42?"
// Backend
__('system.file.ext_limit', ['ext' => 'exe']) // "The file extension is not allowed exe"
3. Text Management
- Keep each feature module in its own translation file to avoid oversized files
- Sync all language files when adding new keys — at minimum keep English as fallback
- Periodically clean up unused keys
4. Language Switch Optimization
- Language preference persists in
localStorage, survives page refresh
- No page reload needed on language switch — React re-renders automatically
useMemo caches antdLocale to avoid unnecessary recomputation
5. Adding More Languages
Recommended order when extending to additional languages:
- Complete English translations first (as the fallback language)
- Update both frontend
options.ts and backend LanguageMiddleware.supportedLanguages
- Add matching
antd locale and dayjs locale
- Test: switch language and verify Ant Design components, date formats, and API response text