多语言

XinAdmin 内置完整的多语言方案,覆盖前端(React + react-i18next)和后端(Laravel),支持中英文双语言,并可通过简单配置扩展更多语言。

整体架构

┌──────────────┐    User-Language Header    ┌────────────────────┐
│   前端 React  │ ─────────────────────────> │    后端 Laravel      │
│              │    Axios 请求拦截器自动附加     │                    │
│  i18next     │                             │  LanguageMiddleware │
│ localStorage │                             │  App::setLocale()  │
│  antd/dayjs  │                             │  __('key')         │
└──────────────┘                             └────────────────────┘

前端通过 react-i18next 管理 UI 文本,切换语言时同步更新 Ant Design 组件和 dayjs 日期库的 locale;后端通过 LanguageMiddleware 中间件检测请求语言,使用 Laravel 的 __() 辅助函数返回对应语言的文本。


前端多语言

技术栈

用途
i18next核心 i18n 框架
react-i18nextReact 集成
antd/es/localeAnt Design 组件国际化
dayjs/locale日期格式化国际化

目录结构

source_code/web/locales/
├── i18n.ts              # i18next 初始化配置
├── index.ts             # 语言资源汇总导出
├── options.ts           # 支持的语言列表 + antd/dayjs 映射
├── zh_CN/               # 中文翻译文件
│   ├── index.ts         # 汇总导出
│   ├── layout.ts        # 布局相关
│   ├── menu.ts          # 菜单
│   ├── login.ts         # 登录页
│   ├── dashboard.ts     # 仪表盘
│   ├── xin-table.ts     # XinTable 组件
│   ├── xin-form.ts      # XinForm 组件
│   ├── xin-crud.ts      # CRUD 组件
│   ├── sys-user-list.ts # 用户管理
│   ├── sys-user-role.ts # 角色管理
│   ├── sys-user-rule.ts # 权限规则
│   ├── sys-user-dept.ts # 部门管理
│   ├── sys-file.ts      # 文件管理
│   ├── sys-dict.ts      # 字典管理
│   ├── sys-mail.ts      # 邮件管理
│   ├── sys-setting.ts   # 系统配置
│   ├── sys-storage.ts   # 存储配置
│   ├── user-setting.ts  # 用户设置
│   ├── watcher.ts       # 系统监控
│   └── system-info.ts   # 系统信息
└── en_US/               # 英文翻译文件(与 zh_CN 结构完全一致)
    └── ...(同上 19 个文件)

i18next 初始化

// 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;

入口文件 main.tsx 顶层导入该模块,确保 i18next 在应用渲染前完成初始化。

语言选项配置

// 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' },
];

资源汇总导出

// 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 },
};

每个 locale 目录内有自己的 index.ts 做 barrel export,将所有模块的翻译键值合并到一个对象中。

翻译文件格式

每个模块文件导出一个对象,key 使用点号分隔的命名空间:

// locales/zh_CN/xin-table.ts
export default {
  'xinTable.add': '新增',
  'xinTable.edit': '编辑',
  'xinTable.delete': '删除',
  'xinTable.deleteConfirm': '你是否要删除 {{id}} 记录?',
  'xinTable.total': '共 {{total}} 条',
  'xinTable.search.search': '搜索',
  'xinTable.search.reset': '重置',
};

支持 {{variable}} 插值语法。

翻译文件命名规范

约定说明
模块文件按功能拆分sys-user-list.tslayout.ts
Key 命名空间前缀xinTable.*layout.*sysUserList.*
嵌套对象用点号xinTable.density.defaultsysFile.field.name
中英文 key 完全一致两份文件的 key 必须相同,仅值不同

useLanguage Hook

核心语言切换逻辑封装在 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);           // 更新 dayjs
    await i18n.changeLanguage(lng);              // 更新 i18next
    localStorage.setItem('i18nextLng', lng);     // 持久化
  };

  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 组件 locale
    language: i18n.language,
    changeLanguage,     // 切换语言方法
    getLanguageConfig,
    languageOptions: options,
  };
};

切换语言时同步更新三个层面:

  1. dayjs locale — 日期格式化
  2. i18next language — UI 文本
  3. localStorage — 持久化存储

AntdProvider 集成

// 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 组件

// 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>
  );
};

在顶栏头部渲染,用户点击图标即可切换语言。

在组件中使用翻译

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 请求拦截器

每次 API 请求自动附带当前语言到 User-Language 请求头:

// utils/request.ts - 请求拦截器
instance.interceptors.request.use((config) => {
  config.headers = config.headers || {};

  // 自动附加国际化语言到请求头
  const currentLanguage = localStorage.getItem('i18nextLng');
  if (currentLanguage) {
    config.headers['User-Language'] = currentLanguage;
  }

  return config;
});

添加新语言(前端)

以添加日语为例:

1. 创建翻译文件目录

locales/ja_JP/
├── index.ts
├── layout.ts
├── menu.ts
├── xin-table.ts
└── ...(所有模块文件)

2. 更新语言选项

// 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' },  // 新增
];

3. 注册到资源映射

// locales/index.ts
import ja_JP from "@/locales/ja_JP";

export default {
  en: { translation: en_US },
  zh: { translation: zh_CN },
  ja: { translation: ja_JP },  // 新增
};

后端多语言

配置

// config/app.php
'locale' => env('APP_LOCALE', 'en'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),

环境变量默认值可通过 .env 文件覆盖:

APP_LOCALE=zh
APP_FALLBACK_LOCALE=en

目录结构

source_code/lang/
├── en/
│   ├── auth.php        # 认证消息
│   ├── pagination.php  # 分页标签
│   ├── passwords.php   # 密码重置消息
│   ├── validation.php  # 表单验证消息
│   ├── system.php      # 系统业务消息
│   └── user.php        # 用户模块消息
└── zh/
    └── ...(同上 6 个文件)

翻译文件格式

采用 Laravel 标准 PHP 数组格式:

<?php
// lang/zh/user.php
return [
    'not_login'           => '请先登录',
    'login_success'       => '登录成功',
    'login_error'         => '登录失败,用户名或者密码错误!',
    'logout_success'      => '退出成功',
    'old_password_error'  => '旧密码错误',
    'user_not_exist'      => '用户不存在,请先注册',
    'user_is_disabled'    => '用户已被禁用',
];
<?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',
];

嵌套数组使用点号访问:

// lang/zh/system.php
return [
    'file' => [
        'image'           => '图片',
        'audio'           => '音频',
        'video'           => '视频',
        'upload_failed'   => '上传失败',
        'not_found'       => '文件不存在',
    ],
    'error' => [
        'no_permission'  => '对不起,你暂时没有该权限,请联系管理员',
        'route_not_exist' => '路由不存在',
    ],
    'data_not_exist' => '数据不存在',
];

// 使用
__('system.file.upload_failed');   // "上传失败"
__('system.error.no_permission');  // "对不起,你暂时没有该权限..."

翻译文件分类

文件用途主要 Key
auth.phpLaravel Auth 认证消息failed, password, throttle
pagination.php分页控件标签previous, next
passwords.php密码重置流程reset, sent, throttled, token, user
validation.php表单验证错误消息required, email, min, max 等 ~80 个 key
system.php系统业务消息file.*, error.*, data_not_exist
user.php用户认证消息login_success, not_login, password_error

LanguageMiddleware 中间件

语言检测的优先级链(从高到低):

// 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
    {
        // 优先级 1: URL 参数 ?lang=en
        if ($request->has('lang') && $this->isSupported($request->get('lang'))) {
            return $request->get('lang');
        }
        // 优先级 2: User-Language 请求头(前端 Axios 自动发送)
        $browserLocale = $request->header('User-Language');
        if ($browserLocale && $this->isSupported($browserLocale)) {
            return $browserLocale;
        }
        // 优先级 3: Session 中存储的语言
        if (Session::has('locale') && $this->isSupported(Session::get('locale'))) {
            return Session::get('locale');
        }
        // 优先级 4: config('app.locale') 默认语言
        return config('app.locale', 'zh');
    }
}
优先级来源说明
1URL 参数?lang=en,适用于分享链接、API 测试
2User-Language 请求头前端 Axios 拦截器自动发送,用户切换语言后全量 API 生效
3Session服务端会话存储
4config('app.locale').envAPP_LOCALE 配置,兜底值

CORS 配置

AllowCrossDomainMiddleware 显式允许 User-Language 头跨域传输:

$response->headers->set(
    'Access-Control-Allow-Headers',
    'Content-Type, Authorization, X-Requested-With, User-Language'
);

在控制器中使用

use App\Http\Controllers\Controller;

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'),
        ]);
    }
}

添加新语言(后端)

以添加日语为例:

1. 创建语言文件

// lang/ja/user.php
return [
    'not_login'      => 'まずログインしてください',
    'login_success'  => 'ログイン成功',
    'login_error'    => 'ログイン失敗、ユーザー名かパスワードが間違っています',
    'logout_success' => 'ログアウト成功',
];

复制全部 6 个文件到 lang/ja/ 并翻译为日语。

2. 注册到中间件

// LanguageMiddleware.php
protected array $supportedLanguages = [
    'en' => 'en',
    'zh' => 'zh',
    'ja' => 'ja',    // 新增,key 为前端传递的语言码
];

注意:前端 options.ts 中的 value 值必须与中间件 supportedLanguages 的 key 一致,否则中间件无法匹配。


前后端数据流

完整的一次多语言请求流程:

1. 用户在 UI 点击 LanguageSwitcher

2. useLanguage().changeLanguage('en')
   ├── dayjs.locale('en')              # 日期格式切换
   ├── i18n.changeLanguage('en')       # React UI 文本切换
   └── localStorage.setItem(..., 'en') # 持久化

3. AntdProvider 检测 i18n.language 变化
   └── ConfigProvider locale={en_US}   # Ant Design 组件切换

4. 发送 API 请求
   └── Axios 拦截器读取 localStorage
   └── 设置 headers['User-Language'] = 'en'

5. 后端 LanguageMiddleware
   ├── 检测 ?lang= 参数 (无)
   ├── 检测 User-Language 头 = 'en' ✓
   └── App::setLocale('en')

6. 控制器 __() 调用
   └── 读取 lang/en/*.php
   └── 返回对应语言的 msg 文本

7. 前端收到响应,显示后端返回的翻译文本

最佳实践

1. Key 命名规范

  • 前端:模块.页面.字段 格式,如 sysUserList.usernamelayout.headerHeight
  • 后端:文件名.嵌套键 格式,如 user.login_successsystem.file.upload_failed
  • 中英文 key 保持一致,方便对照和补全

2. 插值使用

// 前端
t('xinTable.total', { total: 100 })       // "共 100 条"
t('xinTable.deleteConfirm', { id: 42 })   // "你是否要删除 42 记录?"

// 后端
__('system.file.ext_limit', ['ext' => 'exe'])  // "文件扩展名不允许 exe"

3. 文本管理

  • 每个功能模块独立翻译文件,避免单文件过大
  • 新增翻译 key 时同步更新所有语言文件,至少保留英文作为 fallback
  • 定期清理不再使用的 key

4. 语言切换优化

  • 语言偏好持久化到 localStorage,刷新页面不丢失
  • 切换语言后无需刷新页面,React 自动重渲染
  • useMemo 缓存 antdLocale,避免不必要的重新计算

5. 国际化支持建议

如需扩展到更多语言,建议按以下顺序处理:

  1. 先补充英文翻译(作为 fallback 语言)
  2. 同步更新前端 options.ts 和后端 LanguageMiddleware.supportedLanguages
  3. 添加对应的 antd locale 和 dayjs locale
  4. 测试语言切换后 Ant Design 组件、日期格式和 API 返回文本是否正常