# i18n 契约 — 多语言规范

> 本契约只管**界面文案**（`lg()` + 语言包）。存进数据库的名称（零件、分类、品牌……）走主表中英列 + `_lang` 翻译表，
> 规则见 [DB.md](DB.md)「业务数据多语言」，**不要把数据库里的名称放进语言包**。

## 语言包文件

| 文件 | 语言 | 用途 |
|------|------|------|
| `lang/zh-CN.php` | 中文（源语言、默认） | key 和 value 均为中文原文 |
| `lang/{code}.php` | `sys_language` 里其它启用的语言（目前 `en`） | key 为中文原文，value 为该语言译文 |
| `lang/scan.php` | — | 扫描工具生成的 key 列表，不要手动编辑 |

语言包格式：`'中文原文' => '译文'`，key 统一用中文原文（不用英文 key）。

### 启用哪些语言

- 由 `sys_language` 决定（`sys_language_enabled = 1`，按 `sys_language_sort` 排序），`SysLanguage::enabledNames()` 读取
- 入口 `admin/index.php` 把它作为 `locales` 传给 `Mvc::init()`，之后 `Mvc::$cfg['locales']`（`[编码 => 自称]`）就是全站唯一口径：
  `?lang=` 校验、首页和登录页的语言切换菜单、langTool 的扫描和可编辑文件都用它
- 语言编码与语言包文件名、`APP.LOCALE` 一致（`zh-CN` / `en` / `ru` …），显示名用该语言的自称（中文 / English / Русский），不走 `lg()`
- **新增语言**：在 `sys_language` 加一行并启用 → langTool 点「扫描」自动生成 `lang/{code}.php`，条目先用中文原文占位 → 在 langTool 里逐条翻译
- **停用语言**：语言文件**保留不删**，扫描时不同步也不改（扫描结果里列为「未启用语言的文件已保留，未同步」），langTool 里不能编辑；
  重新启用后下次扫描补齐新 key、删废弃 key，原有译文保留
- `zh-CN` 是源语言，始终同步、始终可切换

## lg() 使用规则

### 基本用法

```php
lg('用户列表')         // 取当前语言的译文，找不到时原样返回中文
```

### 符号/箭头/标点必须在 lg() 外面

```php
// 正确
lg('请先选择') . ' →'
'← ' . lg('请先在左侧选择一个属性类型')
lg('共') . ' ' . $count . ' ' . lg('条')

// 错误（符号被包进去，英文语言下符号跟着一起被翻译掉）
lg('← 请先在左侧选择一个属性类型')
lg('共 100 条')
```

### 动态数字/变量不进 lg()

```php
// 正确
lg('共') . $count . lg('个文案')

// 错误
lg("共 {$count} 个文案")
```

### 哪些内容不需要 lg()

- 纯英文专有名词（`DevExtreme`、`Grid`、`Excel`）
- 数字、日期格式字符串
- HTML 属性值（`class`、`id`、`name`）
- JS / CSS 中的字符串（JS 端从 `APP.GRID_TEXT` 读，不直接调用 `lg()`）
- 数据库里的业务数据名称（见本文开头）

## 语言包维护流程

> **触发条件**：下面第 2 步起的扫描与翻译**只在用户明确提出时才执行**（根契约「会话使用规则」第 6 条）。
> 平时开发只做第 1 步，会话结束在 `status/modules.md` 备注「待扫描翻译」。

### 新增文案时

1. 直接在 PHP / 视图里写 `lg('新文案')`，先用中文开发
2. 用户提出扫描需求时，在后台 langTool 页面（`langTool/index`）点「扫描」（`lang/scan.php` 是扫描结果，不是可执行脚本）
3. 扫描工具会：
   - 找出 `admin/view`、`admin/action`、`admin/static/js/lib`、`model`、`config`、`plugin`（客户插件，P2 加）里所有 `lg('...')` 的字面量
   - 在 `zh-CN.php` 和每个启用语言的文件里补充缺失的 key（新 key 先用中文原文占位），已有的译文不覆盖
   - 删除代码里已不再引用的 key（只动启用语言的文件）
4. 在 langTool 里给每个启用语言的新增条目填译文（译文仍等于中文原文的就是待翻译）

### 删除文案时

- 直接删代码里的 `lg('...')` 调用，下次扫描会自动从启用语言的文件里删掉对应 key
- 因为扫描会删 key，写了 `lg()` 的新目录必须加进 langTool 的 `SCAN_DIRS`，否则那里的文案会被当成废弃 key 删掉

## 语言切换机制

- URL 传参 `?lang=en` 切换语言，写入 `$_SESSION['lang']`，立即生效；刷新后从 Session 恢复，无 Session 时默认 `zh-CN`
- **只接受启用的语言**：大小写不敏感（`?lang=EN` 规范成 `en`）；不合法或已停用的值直接忽略，按 Session → 默认 `zh-CN` 回退；
  Session 里记着的语言被停用后，下次请求自动回到默认
- 前端 JS 通过 `APP.LOCALE` 读取当前语言
- DevExtreme 组件语言由 `header.view.php` 自动加载的 `dx.messages.*.js` 控制：DevExtreme 没有该语言的消息包时，
  依次回退到语言段（`pt-BR` → `pt`）、`en`；`DevExpress.localization.locale()` 仍传当前语言，所以控件提示是英文、日期数字格式仍按当前语言
- 语言切换菜单按 `Mvc::$cfg['locales']` 输出，不要在视图里写死语言列表

## 菜单文案特殊规则

`config/admin.php` 的菜单配置里，title 必须用 `lg('中文原文')` 字面量调用：

```php
// 正确（扫描工具能识别字面量）
['title' => lg('用户'), 'url' => '/users/index']

// 错误（扫描工具无法扫描变量）
$title = '用户';
['title' => lg($title), 'url' => '/users/index']
```

## header.view.php 注入规则

Grid 工具栏文案统一在 `header.view.php` 用 PHP `lg()` 翻译后注入到 `APP.GRID_TEXT`：

```php
'GRID_TEXT' => [
    'refresh' => lg('刷新'),
    'export'  => lg('导出'),
    // 新增工具栏按钮时，在这里添加对应 key
]
```

新增 Grid 工具栏按钮时，**必须同时**：
1. 在 `header.view.php` 的 `GRID_TEXT` 里添加 key
2. 在 `dxGrid.js` 里用 `APP.GRID_TEXT.xxx` 读取
3. 在 `status/modules.md` 备注待扫描，用户提出时再到 langTool 点「扫描」同步语言包
