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

LibraryPurpose
i18nextCore i18n framework
react-i18nextReact integration
antd/es/localeAnt Design component internationalization
dayjs/localeDate formatting internationalization

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.

Translation File Format

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

ConventionDescription
Module files split by featuree.g., sys-user-list.ts, layout.ts
Key namespace prefixxinTable.*, layout.*, sysUserList.*
Nested objects use dotsxinTable.density.default, sysFile.field.name
Identical keys across localesBoth files must have the same keys, only values differ

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:

  1. dayjs locale — Date formatting
  2. i18next language — UI text
  3. 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)

Translation File Format

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

FilePurposeKey Examples
auth.phpLaravel Auth messagesfailed, password, throttle
pagination.phpPagination labelsprevious, next
passwords.phpPassword reset flowreset, sent, throttled, token, user
validation.phpForm validation errorsrequired, email, min, max, and ~80 more
system.phpSystem/business messagesfile.*, error.*, data_not_exist
user.phpUser/authentication messageslogin_success, not_login, password_error

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');
    }
}
PrioritySourceDescription
1URL parameter?lang=en, useful for shareable links and API testing
2User-Language headerAuto-sent by frontend Axios interceptor, applies to all APIs
3SessionServer-side session storage
4config('app.locale')Fallback from .env APP_LOCALE

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:

  1. Complete English translations first (as the fallback language)
  2. Update both frontend options.ts and backend LanguageMiddleware.supportedLanguages
  3. Add matching antd locale and dayjs locale
  4. Test: switch language and verify Ant Design components, date formats, and API response text