mirror of
https://github.com/kuaifan/dootask.git
synced 2026-07-22 14:08:24 +00:00
feat(i18n): support contextual translation keys
This commit is contained in:
parent
df6c1fc04d
commit
df4658cddf
@ -44,6 +44,8 @@ description: 从 `pro` 分支发布 DooTask 前端新版本:翻译 → 版本
|
||||
|
||||
多语言数据流:`language/original-{web,api}.txt`(原文/简体中文)→ 经翻译写入 `language/translate.json`(含 9 种语言)→ 生成 `public/language/{web,api}/*`。
|
||||
|
||||
上下文翻译键用于保持短文案的界面宽度,同时区分不同语义,格式固定为 `[lower_snake_case].原文`,例如 `[weekday].一`、`[task_unit].个`。上下文前缀只参与翻译查找,不参与显示;运行时完整键缺失时会回退普通原文键。普通 key 的 `zh` 留空,上下文 key 的 `zh` 必须填写去掉前缀后的原文。
|
||||
|
||||
**1.1 检测差异**
|
||||
|
||||
```shell
|
||||
@ -55,11 +57,11 @@ php .claude/skills/dootask-release/scripts/language.php diff
|
||||
- `regexErrorCount > 0`:translate.json **已有条目**的占位符与某语言值不一致 → **停止**,报告 `regexErrors`,交用户修复(这是历史数据问题,不要自行猜测修改)
|
||||
- `redundantCount > 0`:translate.json 里有、但原文已删除的条目 → 仅作提示(apply 时会自动剔除,不致命)
|
||||
- `needsCount == 0`:无新文案 → **跳到 1.4 直接生成**
|
||||
- `needsCount > 0`:`needs` 数组即待翻译清单,每项 `key` 已转成占位符形式(如 `(%T1)`)→ 进入 1.2
|
||||
- `needsCount > 0`:`needs` 数组即待翻译清单,每项 `key` 已转成占位符形式(如 `(%T1)`);上下文 key 还会带自动填充的 `zh` → 进入 1.2
|
||||
|
||||
**1.2 翻译**
|
||||
|
||||
对 `needs` 里的每个 `key`,翻成 8 种语言(`zh` 留空、`key` 原样保留):`zh-CHT` `en` `ko` `ja` `de` `fr` `id` `ru`。
|
||||
对 `needs` 里的每个 `key`,翻成 8 种语言(`key` 原样保留):`zh-CHT` `en` `ko` `ja` `de` `fr` `id` `ru`。普通 key 的 `zh` 留空;上下文 key 的 `zh` 使用 `needs` 已给出的原文,不得留空或改写。
|
||||
|
||||
要求:贴合「项目任务管理系统」语境;占位符 `(%T1)`/`(%M1)` 等原样保留、不可增删改,位置可随目标语言语序调整:
|
||||
|
||||
@ -69,7 +71,7 @@ php .claude/skills/dootask-release/scripts/language.php diff
|
||||
| (%T1)提交的「(%M2)」待你审批 | '(%M2)' submitted by (%T1) is waiting for your approval |
|
||||
|
||||
把结果写成一个 JSON 数组文件(建议放 `/tmp/dootask-release-translated.json`,避免污染工作区),每个元素含全部 10 个字段,顺序为:
|
||||
`key, zh, zh-CHT, en, ko, ja, de, fr, id, ru`(`zh` 写 `""`)。
|
||||
`key, zh, zh-CHT, en, ko, ja, de, fr, id, ru`。
|
||||
|
||||
```json
|
||||
[
|
||||
@ -77,13 +79,21 @@ php .claude/skills/dootask-release/scripts/language.php diff
|
||||
]
|
||||
```
|
||||
|
||||
上下文短词示例:
|
||||
|
||||
```json
|
||||
[
|
||||
{"key":"[task_unit].个","zh":"个","zh-CHT":"個","en":"tasks","ko":"개 작업","ja":"タスク","de":"Aufgaben","fr":"tâches","id":"tugas","ru":"задач"}
|
||||
]
|
||||
```
|
||||
|
||||
**1.3 合并进 translate.json**
|
||||
|
||||
```shell
|
||||
php .claude/skills/dootask-release/scripts/language.php apply /tmp/dootask-release-translated.json
|
||||
```
|
||||
|
||||
脚本会校验字段完整性与占位符完整性、追加新条目、剔除冗余项,并按项目原生格式写回 `translate.json`。参数化 key 必须使用 `(%T1)`/`(%M1)` 形式,禁止输入 raw `(*)`/`(**)`;编号须按出现顺序从 1 连续递增,各语言值可调整占位符顺序,但占位符的类型、编号和数量必须与 key 完全一致。输入 JSON 内相同或规范化后相同的 key 会被拒绝;已存在于 `translate.json` 的非待补项也会被拒绝,避免覆盖歧义。任一条不合格会明确报错并停止,按提示修正翻译后重试。
|
||||
脚本会校验字段完整性、占位符完整性与上下文键格式,追加新条目、剔除冗余项,并按项目原生格式写回 `translate.json`。参数化 key 必须使用 `(%T1)`/`(%M1)` 形式,禁止输入 raw `(*)`/`(**)`;编号须按出现顺序从 1 连续递增,各语言值可调整占位符顺序,但占位符的类型、编号和数量必须与 key 完全一致。上下文 key 必须使用 `[lower_snake_case].原文`,且 `zh` 必须与原文部分完全一致。输入 JSON 内相同或规范化后相同的 key 会被拒绝;已存在于 `translate.json` 的非待补项也会被拒绝,避免覆盖歧义。任一条不合格会明确报错并停止,按提示修正翻译后重试。
|
||||
|
||||
**1.4 生成前端/后端语言文件**
|
||||
|
||||
|
||||
@ -54,6 +54,28 @@ function normalize_key(string $key): string
|
||||
return preg_replace(["/\(%T\d+\)/", "/\(%M\d+\)/"], ["(*)", "(**)"], $key);
|
||||
}
|
||||
|
||||
function context_source_key(string $key): ?string
|
||||
{
|
||||
if (preg_match('/^\[([a-z][a-z0-9_]*)]\.([\s\S]+)$/', $key, $matches)) {
|
||||
return $matches[2];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validate_original_contexts(array $originals): array
|
||||
{
|
||||
$errors = [];
|
||||
foreach ($originals as $index => $key) {
|
||||
if (preg_match('/^\[[^\]]+]\./', $key) && context_source_key($key) === null) {
|
||||
$errors[] = [
|
||||
'location' => "originals[index $index]",
|
||||
'message' => "上下文翻译键格式错误:$key",
|
||||
];
|
||||
}
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
|
||||
function validate_entry(array $obj, string $location, bool $requireTranslations): array
|
||||
{
|
||||
$formatErrors = [];
|
||||
@ -72,6 +94,17 @@ function validate_entry(array $obj, string $location, bool $requireTranslations)
|
||||
}
|
||||
|
||||
$key = $obj['key'];
|
||||
if (preg_match('/^\[[^\]]+]\./', $key)) {
|
||||
$sourceKey = context_source_key($key);
|
||||
if ($sourceKey === null) {
|
||||
$formatErrors[] = ['location' => "$location.key", 'message' => "上下文翻译键格式错误:$key"];
|
||||
} elseif (($obj['zh'] ?? null) !== $sourceKey) {
|
||||
$formatErrors[] = [
|
||||
'location' => "$location.zh",
|
||||
'message' => "上下文翻译键必须填写原文:$sourceKey",
|
||||
];
|
||||
}
|
||||
}
|
||||
if (preg_match('/\(\*{1,2}\)/', $key)) {
|
||||
$formatErrors[] = [
|
||||
'location' => "$location.key",
|
||||
@ -134,7 +167,7 @@ function build_translations(array $originals): array
|
||||
$translations = [];
|
||||
$redundants = [];
|
||||
$regexErrors = [];
|
||||
$formatErrors = [];
|
||||
$formatErrors = validate_original_contexts($originals);
|
||||
if (!file_exists("translate.json")) {
|
||||
fwrite(STDERR, "translate.json not exists\n");
|
||||
exit(1);
|
||||
@ -241,7 +274,12 @@ if ($cmd === 'diff') {
|
||||
$label = strlen($m[1]) > 1 ? "M" : "T";
|
||||
return "(%" . $label . $c++ . ")";
|
||||
}, $key);
|
||||
$needsOut[] = ['key' => $converted];
|
||||
$need = ['key' => $converted];
|
||||
$contextSource = context_source_key($converted);
|
||||
if ($contextSource !== null) {
|
||||
$need['zh'] = $contextSource;
|
||||
}
|
||||
$needsOut[] = $need;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
@ -293,10 +331,10 @@ if ($cmd === 'apply') {
|
||||
print_validation_errors($incomingFormatErrors, $incomingRegexErrors);
|
||||
exit(1);
|
||||
}
|
||||
// 规范化:固定字段顺序 + zh 置空
|
||||
// 规范化:固定字段顺序;上下文翻译键保留 zh 作为实际显示原文
|
||||
$item = [];
|
||||
foreach ($GLOBALS['LANG_FIELDS'] as $f) {
|
||||
$item[$f] = $f === 'zh' ? '' : $raw[$f];
|
||||
$item[$f] = $f === 'zh' && context_source_key($raw['key']) === null ? '' : $raw[$f];
|
||||
}
|
||||
$originalKey = normalize_key($item['key']);
|
||||
if (!isset($originalSet[$originalKey])) {
|
||||
|
||||
@ -68,6 +68,7 @@ Laravel 13 (LaravelS/Swoole, PHP 8.4) + Vue 2 (Vite) + Electron。开源任务/
|
||||
|
||||
- 新增用户可见文本须追加原文(简体中文)到:前端 `language/original-web.txt`,后端 `language/original-api.txt`(去重)
|
||||
- 前端翻译用 `$L("文本")`,动态值用 `(*)` 占位:`$L('共(*)条', n)`——禁止拼接翻译
|
||||
- 单字或短词因上下文不同可能产生歧义时,使用 `[lower_snake_case].原文` 上下文键,例如 `$L('[weekday].一')`、`$L('[task_unit].个')`;前缀只参与翻译查找,界面仍显示原文。此类键须完整登记到 `original-web.txt`,且 `translate.json` 的 `zh` 必须填写去除前缀后的原文;普通键的 `zh` 仍留空
|
||||
|
||||
## ai-kb 同步规则
|
||||
|
||||
|
||||
@ -851,21 +851,7 @@ Pro版
|
||||
颜色
|
||||
黄色
|
||||
默认
|
||||
一
|
||||
三
|
||||
二
|
||||
五
|
||||
你
|
||||
六
|
||||
周
|
||||
四
|
||||
天
|
||||
或
|
||||
日
|
||||
月
|
||||
秒
|
||||
(*)秒
|
||||
给
|
||||
(*)是一款轻量级的开源在线项目任务管理工具,提供各类文档协作工具、在线思维导图、在线流程图、项目管理、任务分发、即时IM,文件管理等工具。
|
||||
流转到【(*)】时,[任务负责人] 和 [项目管理员] 可以修改状态。
|
||||
文件(*)格式不正确,请上传(*)格式的图片。
|
||||
@ -1346,7 +1332,6 @@ APP 推送
|
||||
接龙
|
||||
参与接龙
|
||||
发起接龙
|
||||
由
|
||||
发起接龙,参与接龙目前共(*)人
|
||||
请输入接龙主题
|
||||
请输入接龙内容
|
||||
@ -1354,7 +1339,6 @@ APP 推送
|
||||
重复内容将不再计入接龙结果
|
||||
返回编辑
|
||||
继续发送
|
||||
例
|
||||
接龙结果
|
||||
选择群组发起接龙
|
||||
来自
|
||||
@ -1371,7 +1355,6 @@ APP 推送
|
||||
单选
|
||||
请选择后投票
|
||||
立即投票
|
||||
票
|
||||
再次发送
|
||||
再次发送投票?
|
||||
结束投票
|
||||
@ -1758,7 +1741,6 @@ WiFi签到延迟时长为±1分钟。
|
||||
|
||||
签到半径设置
|
||||
半径
|
||||
米
|
||||
经度
|
||||
纬度
|
||||
地图类型
|
||||
@ -2064,7 +2046,6 @@ OKR群组
|
||||
邀请地址不存在或已被删除!
|
||||
|
||||
会话名称
|
||||
值
|
||||
结果
|
||||
命令
|
||||
接口地址
|
||||
@ -2509,7 +2490,6 @@ AI任务分析
|
||||
个人视角
|
||||
部门任务总览
|
||||
(*) 人
|
||||
项
|
||||
最久 (*) 天
|
||||
今天 (*) 项到期
|
||||
还有 (*) 项
|
||||
@ -2622,13 +2602,11 @@ AgoraIO 声网
|
||||
任务状态
|
||||
已还原
|
||||
下个周期
|
||||
每
|
||||
LDAP 用户禁止修改邮箱
|
||||
共(*)个
|
||||
标签名称最多只能设置(*)个字
|
||||
添加失败
|
||||
未开启通知权限
|
||||
个
|
||||
使用端到端加密传输数据。
|
||||
删除前,请确认以下事项:
|
||||
已清楚风险,确定删除
|
||||
@ -2658,3 +2636,23 @@ LDAP 用户禁止修改邮箱
|
||||
(*) 负责的部门、项目、任务和文件将移交给交接人;同时退出所有群(如果是群主则转让给交接人)
|
||||
正在编辑帐号【ID:(*), (*)】的信息。
|
||||
打包下载(*)
|
||||
[calendar_view].日
|
||||
[calendar_view].周
|
||||
[calendar_view].月
|
||||
[day_unit].天
|
||||
[distance_unit].米
|
||||
[example_label].例
|
||||
[initiator_label].由
|
||||
[recurrence_prefix].每
|
||||
[self_sender].你
|
||||
[task_unit].个
|
||||
[task_unit].项
|
||||
[todo_target].给
|
||||
[vote_unit].票
|
||||
[weekday].日
|
||||
[weekday].一
|
||||
[weekday].二
|
||||
[weekday].三
|
||||
[weekday].四
|
||||
[weekday].五
|
||||
[weekday].六
|
||||
|
||||
@ -10847,162 +10847,6 @@
|
||||
"id": "Default",
|
||||
"ru": "По умолчанию"
|
||||
},
|
||||
{
|
||||
"key": "一",
|
||||
"zh": "",
|
||||
"zh-CHT": "一",
|
||||
"en": "One",
|
||||
"ko": "하나",
|
||||
"ja": "一",
|
||||
"de": "Eins",
|
||||
"fr": "Un",
|
||||
"id": "Satu",
|
||||
"ru": "Один"
|
||||
},
|
||||
{
|
||||
"key": "三",
|
||||
"zh": "",
|
||||
"zh-CHT": "三",
|
||||
"en": "Three",
|
||||
"ko": "셋",
|
||||
"ja": "三",
|
||||
"de": "Drei",
|
||||
"fr": "Trois",
|
||||
"id": "Tiga",
|
||||
"ru": "Три"
|
||||
},
|
||||
{
|
||||
"key": "二",
|
||||
"zh": "",
|
||||
"zh-CHT": "二",
|
||||
"en": "Two",
|
||||
"ko": "둘",
|
||||
"ja": "二",
|
||||
"de": "Zwei",
|
||||
"fr": "Deux",
|
||||
"id": "Dua",
|
||||
"ru": "Два"
|
||||
},
|
||||
{
|
||||
"key": "五",
|
||||
"zh": "",
|
||||
"zh-CHT": "五",
|
||||
"en": "Five",
|
||||
"ko": "다섯",
|
||||
"ja": "五",
|
||||
"de": "Fünf",
|
||||
"fr": "Cinq",
|
||||
"id": "Lima",
|
||||
"ru": "Пять"
|
||||
},
|
||||
{
|
||||
"key": "你",
|
||||
"zh": "",
|
||||
"zh-CHT": "你",
|
||||
"en": "You",
|
||||
"ko": "당신",
|
||||
"ja": "あなた",
|
||||
"de": "Du",
|
||||
"fr": "Vous",
|
||||
"id": "Anda",
|
||||
"ru": "Вы"
|
||||
},
|
||||
{
|
||||
"key": "六",
|
||||
"zh": "",
|
||||
"zh-CHT": "六",
|
||||
"en": "Six",
|
||||
"ko": "여섯",
|
||||
"ja": "六",
|
||||
"de": "Sechs",
|
||||
"fr": "Six",
|
||||
"id": "Enam",
|
||||
"ru": "Шесть"
|
||||
},
|
||||
{
|
||||
"key": "周",
|
||||
"zh": "",
|
||||
"zh-CHT": "周",
|
||||
"en": "Week",
|
||||
"ko": "주",
|
||||
"ja": "週",
|
||||
"de": "Woche",
|
||||
"fr": "Semaine",
|
||||
"id": "Minggu",
|
||||
"ru": "Неделя"
|
||||
},
|
||||
{
|
||||
"key": "四",
|
||||
"zh": "",
|
||||
"zh-CHT": "四",
|
||||
"en": "Four",
|
||||
"ko": "넷",
|
||||
"ja": "四",
|
||||
"de": "Vier",
|
||||
"fr": "Quatre",
|
||||
"id": "Empat",
|
||||
"ru": "Четыре"
|
||||
},
|
||||
{
|
||||
"key": "天",
|
||||
"zh": "",
|
||||
"zh-CHT": "天",
|
||||
"en": "Day",
|
||||
"ko": "일",
|
||||
"ja": "日",
|
||||
"de": "Tag",
|
||||
"fr": "Jour",
|
||||
"id": "Hari",
|
||||
"ru": "День"
|
||||
},
|
||||
{
|
||||
"key": "或",
|
||||
"zh": "",
|
||||
"zh-CHT": "或",
|
||||
"en": "Or",
|
||||
"ko": "또는",
|
||||
"ja": "または",
|
||||
"de": "Oder",
|
||||
"fr": "Ou",
|
||||
"id": "Atau",
|
||||
"ru": "Или"
|
||||
},
|
||||
{
|
||||
"key": "日",
|
||||
"zh": "",
|
||||
"zh-CHT": "日",
|
||||
"en": "Day",
|
||||
"ko": "일",
|
||||
"ja": "日",
|
||||
"de": "Tag",
|
||||
"fr": "Jour",
|
||||
"id": "Hari",
|
||||
"ru": "День"
|
||||
},
|
||||
{
|
||||
"key": "月",
|
||||
"zh": "",
|
||||
"zh-CHT": "月",
|
||||
"en": "Month",
|
||||
"ko": "월",
|
||||
"ja": "月",
|
||||
"de": "Monat",
|
||||
"fr": "Mois",
|
||||
"id": "Bulan",
|
||||
"ru": "Месяц"
|
||||
},
|
||||
{
|
||||
"key": "秒",
|
||||
"zh": "",
|
||||
"zh-CHT": "秒",
|
||||
"en": "Second",
|
||||
"ko": "초",
|
||||
"ja": "秒",
|
||||
"de": "Sekunde",
|
||||
"fr": "Seconde",
|
||||
"id": "Detik",
|
||||
"ru": "Секунда"
|
||||
},
|
||||
{
|
||||
"key": "(%T1)秒",
|
||||
"zh": "",
|
||||
@ -11015,18 +10859,6 @@
|
||||
"id": "(%T1) detik",
|
||||
"ru": "(%T1) секунд"
|
||||
},
|
||||
{
|
||||
"key": "给",
|
||||
"zh": "",
|
||||
"zh-CHT": "給",
|
||||
"en": "Give",
|
||||
"ko": "주다",
|
||||
"ja": "与える",
|
||||
"de": "Geben",
|
||||
"fr": "Donner",
|
||||
"id": "Beri",
|
||||
"ru": "Дать"
|
||||
},
|
||||
{
|
||||
"key": "不在有效时间内,有效时间为:(%T1)",
|
||||
"zh": "",
|
||||
@ -15647,18 +15479,6 @@
|
||||
"id": "Mulai Balas Berantai",
|
||||
"ru": "Инициировать цепочку ответов"
|
||||
},
|
||||
{
|
||||
"key": "由",
|
||||
"zh": "",
|
||||
"zh-CHT": "由",
|
||||
"en": "By",
|
||||
"ko": "작성자",
|
||||
"ja": "作成者",
|
||||
"de": "Von",
|
||||
"fr": "Par",
|
||||
"id": "Oleh",
|
||||
"ru": "От"
|
||||
},
|
||||
{
|
||||
"key": "发起接龙,参与接龙目前共(%T1)人",
|
||||
"zh": "",
|
||||
@ -15743,18 +15563,6 @@
|
||||
"id": "Lanjutkan Pengiriman",
|
||||
"ru": "Продолжить отправку"
|
||||
},
|
||||
{
|
||||
"key": "例",
|
||||
"zh": "",
|
||||
"zh-CHT": "例",
|
||||
"en": "Example",
|
||||
"ko": "예시",
|
||||
"ja": "例",
|
||||
"de": "Beispiel",
|
||||
"fr": "Exemple",
|
||||
"id": "Contoh",
|
||||
"ru": "Пример"
|
||||
},
|
||||
{
|
||||
"key": "接龙结果",
|
||||
"zh": "",
|
||||
@ -15947,18 +15755,6 @@
|
||||
"id": "Voting Sekarang",
|
||||
"ru": "Голосовать сейчас"
|
||||
},
|
||||
{
|
||||
"key": "票",
|
||||
"zh": "",
|
||||
"zh-CHT": "票",
|
||||
"en": "Votes",
|
||||
"ko": "표",
|
||||
"ja": "票",
|
||||
"de": "Stimmen",
|
||||
"fr": "Votes",
|
||||
"id": "Suara",
|
||||
"ru": "Голоса"
|
||||
},
|
||||
{
|
||||
"key": "再次发送",
|
||||
"zh": "",
|
||||
@ -29327,18 +29123,6 @@
|
||||
"id": "Nama Sesi",
|
||||
"ru": "Название сессии"
|
||||
},
|
||||
{
|
||||
"key": "值",
|
||||
"zh": "",
|
||||
"zh-CHT": "值",
|
||||
"en": "Value",
|
||||
"ko": "값",
|
||||
"ja": "値",
|
||||
"de": "Wert",
|
||||
"fr": "Valeur",
|
||||
"id": "Nilai",
|
||||
"ru": "Значение"
|
||||
},
|
||||
{
|
||||
"key": "结果",
|
||||
"zh": "",
|
||||
@ -29699,18 +29483,6 @@
|
||||
"id": "Radius",
|
||||
"ru": "Радиус"
|
||||
},
|
||||
{
|
||||
"key": "米",
|
||||
"zh": "",
|
||||
"zh-CHT": "米",
|
||||
"en": "meters",
|
||||
"ko": "미터",
|
||||
"ja": "メートル",
|
||||
"de": "Meter",
|
||||
"fr": "mètres",
|
||||
"id": "meter",
|
||||
"ru": "метры"
|
||||
},
|
||||
{
|
||||
"key": "经度",
|
||||
"zh": "",
|
||||
@ -36863,18 +36635,6 @@
|
||||
"id": "Siklus berikutnya",
|
||||
"ru": "Следующий цикл"
|
||||
},
|
||||
{
|
||||
"key": "每",
|
||||
"zh": "",
|
||||
"zh-CHT": "每",
|
||||
"en": "Every",
|
||||
"ko": "매",
|
||||
"ja": "毎",
|
||||
"de": "Alle",
|
||||
"fr": "Tous les",
|
||||
"id": "Setiap",
|
||||
"ru": "Каждые"
|
||||
},
|
||||
{
|
||||
"key": "共(%T1)个",
|
||||
"zh": "",
|
||||
@ -36899,18 +36659,6 @@
|
||||
"id": "Izin notifikasi belum diaktifkan",
|
||||
"ru": "Разрешение на уведомления не включено"
|
||||
},
|
||||
{
|
||||
"key": "个",
|
||||
"zh": "",
|
||||
"zh-CHT": "個",
|
||||
"en": "items",
|
||||
"ko": "개",
|
||||
"ja": "個",
|
||||
"de": "Elemente",
|
||||
"fr": "éléments",
|
||||
"id": "item",
|
||||
"ru": "шт."
|
||||
},
|
||||
{
|
||||
"key": "使用端到端加密传输数据。",
|
||||
"zh": "",
|
||||
@ -37283,18 +37031,6 @@
|
||||
"id": "(%T1) orang",
|
||||
"ru": "(%T1) чел."
|
||||
},
|
||||
{
|
||||
"key": "项",
|
||||
"zh": "",
|
||||
"zh-CHT": "項",
|
||||
"en": "items",
|
||||
"ko": "항목",
|
||||
"ja": "項目",
|
||||
"de": "Elemente",
|
||||
"fr": "éléments",
|
||||
"id": "item",
|
||||
"ru": "элементов"
|
||||
},
|
||||
{
|
||||
"key": "最久 (%T1) 天",
|
||||
"zh": "",
|
||||
@ -38326,5 +38062,245 @@
|
||||
"fr": "Télécharger (%T1) en paquet",
|
||||
"id": "Unduh paket (%T1)",
|
||||
"ru": "Скачать пакет (%T1)"
|
||||
},
|
||||
{
|
||||
"key": "[calendar_view].日",
|
||||
"zh": "日",
|
||||
"zh-CHT": "日",
|
||||
"en": "Day",
|
||||
"ko": "일",
|
||||
"ja": "日",
|
||||
"de": "Tag",
|
||||
"fr": "Jour",
|
||||
"id": "Hari",
|
||||
"ru": "День"
|
||||
},
|
||||
{
|
||||
"key": "[calendar_view].周",
|
||||
"zh": "周",
|
||||
"zh-CHT": "週",
|
||||
"en": "Week",
|
||||
"ko": "주",
|
||||
"ja": "週",
|
||||
"de": "Woche",
|
||||
"fr": "Semaine",
|
||||
"id": "Minggu",
|
||||
"ru": "Неделя"
|
||||
},
|
||||
{
|
||||
"key": "[calendar_view].月",
|
||||
"zh": "月",
|
||||
"zh-CHT": "月",
|
||||
"en": "Month",
|
||||
"ko": "월",
|
||||
"ja": "月",
|
||||
"de": "Monat",
|
||||
"fr": "Mois",
|
||||
"id": "Bulan",
|
||||
"ru": "Месяц"
|
||||
},
|
||||
{
|
||||
"key": "[day_unit].天",
|
||||
"zh": "天",
|
||||
"zh-CHT": "天",
|
||||
"en": "days",
|
||||
"ko": "일",
|
||||
"ja": "日",
|
||||
"de": "Tage",
|
||||
"fr": "jours",
|
||||
"id": "hari",
|
||||
"ru": "дн."
|
||||
},
|
||||
{
|
||||
"key": "[distance_unit].米",
|
||||
"zh": "米",
|
||||
"zh-CHT": "米",
|
||||
"en": "meters",
|
||||
"ko": "미터",
|
||||
"ja": "メートル",
|
||||
"de": "Meter",
|
||||
"fr": "mètres",
|
||||
"id": "meter",
|
||||
"ru": "метры"
|
||||
},
|
||||
{
|
||||
"key": "[example_label].例",
|
||||
"zh": "例",
|
||||
"zh-CHT": "例",
|
||||
"en": "Example",
|
||||
"ko": "예시",
|
||||
"ja": "例",
|
||||
"de": "Beispiel",
|
||||
"fr": "Exemple",
|
||||
"id": "Contoh",
|
||||
"ru": "Пример"
|
||||
},
|
||||
{
|
||||
"key": "[initiator_label].由",
|
||||
"zh": "由",
|
||||
"zh-CHT": "由",
|
||||
"en": "By",
|
||||
"ko": "작성자",
|
||||
"ja": "作成者",
|
||||
"de": "Von",
|
||||
"fr": "Par",
|
||||
"id": "Oleh",
|
||||
"ru": "От"
|
||||
},
|
||||
{
|
||||
"key": "[recurrence_prefix].每",
|
||||
"zh": "每",
|
||||
"zh-CHT": "每",
|
||||
"en": "Every",
|
||||
"ko": "매",
|
||||
"ja": "毎",
|
||||
"de": "Alle",
|
||||
"fr": "Tous les",
|
||||
"id": "Setiap",
|
||||
"ru": "Каждые"
|
||||
},
|
||||
{
|
||||
"key": "[self_sender].你",
|
||||
"zh": "你",
|
||||
"zh-CHT": "你",
|
||||
"en": "You",
|
||||
"ko": "나",
|
||||
"ja": "あなた",
|
||||
"de": "Du",
|
||||
"fr": "Vous",
|
||||
"id": "Anda",
|
||||
"ru": "Вы"
|
||||
},
|
||||
{
|
||||
"key": "[task_unit].个",
|
||||
"zh": "个",
|
||||
"zh-CHT": "個",
|
||||
"en": "tasks",
|
||||
"ko": "개",
|
||||
"ja": "件",
|
||||
"de": "Aufgaben",
|
||||
"fr": "tâches",
|
||||
"id": "tugas",
|
||||
"ru": "задач"
|
||||
},
|
||||
{
|
||||
"key": "[task_unit].项",
|
||||
"zh": "项",
|
||||
"zh-CHT": "項",
|
||||
"en": "tasks",
|
||||
"ko": "개",
|
||||
"ja": "件",
|
||||
"de": "Aufgaben",
|
||||
"fr": "tâches",
|
||||
"id": "tugas",
|
||||
"ru": "задач"
|
||||
},
|
||||
{
|
||||
"key": "[todo_target].给",
|
||||
"zh": "给",
|
||||
"zh-CHT": "給",
|
||||
"en": "For",
|
||||
"ko": "대상",
|
||||
"ja": "対象",
|
||||
"de": "Für",
|
||||
"fr": "Pour",
|
||||
"id": "Untuk",
|
||||
"ru": "Для"
|
||||
},
|
||||
{
|
||||
"key": "[vote_unit].票",
|
||||
"zh": "票",
|
||||
"zh-CHT": "票",
|
||||
"en": "votes",
|
||||
"ko": "표",
|
||||
"ja": "票",
|
||||
"de": "Stimmen",
|
||||
"fr": "votes",
|
||||
"id": "suara",
|
||||
"ru": "голосов"
|
||||
},
|
||||
{
|
||||
"key": "[weekday].日",
|
||||
"zh": "日",
|
||||
"zh-CHT": "日",
|
||||
"en": "Sun",
|
||||
"ko": "일",
|
||||
"ja": "日",
|
||||
"de": "So",
|
||||
"fr": "Dim",
|
||||
"id": "Min",
|
||||
"ru": "Вс"
|
||||
},
|
||||
{
|
||||
"key": "[weekday].一",
|
||||
"zh": "一",
|
||||
"zh-CHT": "一",
|
||||
"en": "Mon",
|
||||
"ko": "월",
|
||||
"ja": "月",
|
||||
"de": "Mo",
|
||||
"fr": "Lun",
|
||||
"id": "Sen",
|
||||
"ru": "Пн"
|
||||
},
|
||||
{
|
||||
"key": "[weekday].二",
|
||||
"zh": "二",
|
||||
"zh-CHT": "二",
|
||||
"en": "Tue",
|
||||
"ko": "화",
|
||||
"ja": "火",
|
||||
"de": "Di",
|
||||
"fr": "Mar",
|
||||
"id": "Sel",
|
||||
"ru": "Вт"
|
||||
},
|
||||
{
|
||||
"key": "[weekday].三",
|
||||
"zh": "三",
|
||||
"zh-CHT": "三",
|
||||
"en": "Wed",
|
||||
"ko": "수",
|
||||
"ja": "水",
|
||||
"de": "Mi",
|
||||
"fr": "Mer",
|
||||
"id": "Rab",
|
||||
"ru": "Ср"
|
||||
},
|
||||
{
|
||||
"key": "[weekday].四",
|
||||
"zh": "四",
|
||||
"zh-CHT": "四",
|
||||
"en": "Thu",
|
||||
"ko": "목",
|
||||
"ja": "木",
|
||||
"de": "Do",
|
||||
"fr": "Jeu",
|
||||
"id": "Kam",
|
||||
"ru": "Чт"
|
||||
},
|
||||
{
|
||||
"key": "[weekday].五",
|
||||
"zh": "五",
|
||||
"zh-CHT": "五",
|
||||
"en": "Fri",
|
||||
"ko": "금",
|
||||
"ja": "金",
|
||||
"de": "Fr",
|
||||
"fr": "Ven",
|
||||
"id": "Jum",
|
||||
"ru": "Пт"
|
||||
},
|
||||
{
|
||||
"key": "[weekday].六",
|
||||
"zh": "六",
|
||||
"zh-CHT": "六",
|
||||
"en": "Sat",
|
||||
"ko": "토",
|
||||
"ja": "土",
|
||||
"de": "Sa",
|
||||
"fr": "Sam",
|
||||
"id": "Sab",
|
||||
"ru": "Сб"
|
||||
}
|
||||
]
|
||||
]
|
||||
2
public/language/web/de.js
vendored
2
public/language/web/de.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/en.js
vendored
2
public/language/web/en.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/fr.js
vendored
2
public/language/web/fr.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/id.js
vendored
2
public/language/web/id.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/ja.js
vendored
2
public/language/web/ja.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/key.js
vendored
2
public/language/web/key.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/ko.js
vendored
2
public/language/web/ko.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/ru.js
vendored
2
public/language/web/ru.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/zh-CHT.js
vendored
2
public/language/web/zh-CHT.js
vendored
File diff suppressed because one or more lines are too long
2
public/language/web/zh.js
vendored
2
public/language/web/zh.js
vendored
File diff suppressed because one or more lines are too long
33
resources/assets/js/i18n/index.js
vendored
33
resources/assets/js/i18n/index.js
vendored
@ -42,8 +42,10 @@ function initLanguage() {
|
||||
if (typeof languageList[language] === "undefined") {
|
||||
continue
|
||||
}
|
||||
const result = window.LANGUAGE_DATA[language][index] || key
|
||||
languageCache.set(`${key}-${language}`, result);
|
||||
const result = window.LANGUAGE_DATA[language][index]
|
||||
if (result || utils.stripContextLanguageKey(key) === key) {
|
||||
languageCache.set(`${key}-${language}`, result || key);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -122,19 +124,30 @@ function switchLanguage(inputString) {
|
||||
return inputString
|
||||
}
|
||||
|
||||
const fallbackString = utils.stripContextLanguageKey(inputString);
|
||||
const lookupStrings = fallbackString === inputString ? [inputString] : [inputString, fallbackString];
|
||||
|
||||
if (arguments.length > 1) {
|
||||
const templateKey = `${inputString}-${languageName}`;
|
||||
if (languageTemplateCache.has(templateKey)) {
|
||||
const {text, translateArguments} = languageTemplateCache.get(templateKey);
|
||||
if (!text) {
|
||||
return utils.replaceArgumentsLanguage(inputString, arguments);
|
||||
for (const lookupString of lookupStrings) {
|
||||
const templateKey = `${lookupString}-${languageName}`;
|
||||
const template = languageTemplateCache.get(templateKey);
|
||||
if (!template?.text) {
|
||||
continue;
|
||||
}
|
||||
return text.replace(/\(%[TM](\d+)\)/g, (_, index) => {
|
||||
return template.text.replace(/\(%[TM](\d+)\)/g, (_, index) => {
|
||||
const value = utils.getArgumentLanguage(arguments[index]);
|
||||
return translateArguments.has(index) ? switchLanguage(String(value)) : value;
|
||||
return template.translateArguments.has(index) ? switchLanguage(String(value)) : value;
|
||||
});
|
||||
}
|
||||
inputString = utils.replaceArgumentsLanguage(inputString, arguments)
|
||||
inputString = utils.replaceArgumentsLanguage(fallbackString, arguments)
|
||||
} else {
|
||||
for (const lookupString of lookupStrings) {
|
||||
const result = languageCache.get(`${lookupString}-${languageName}`);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
inputString = fallbackString;
|
||||
}
|
||||
|
||||
// 读取缓存
|
||||
|
||||
12
resources/assets/js/i18n/utils.js
vendored
12
resources/assets/js/i18n/utils.js
vendored
@ -1,4 +1,6 @@
|
||||
export default {
|
||||
contextKeyPattern: /^\[([a-z][a-z0-9_]*)]\.([\s\S]+)$/,
|
||||
|
||||
/**
|
||||
* 语言类型
|
||||
*/
|
||||
@ -46,6 +48,16 @@ export default {
|
||||
return text.replace(/\(%[TM]\d+\)/g, "(*)");
|
||||
},
|
||||
|
||||
/**
|
||||
* 移除翻译上下文前缀,例如 [weekday].一 -> 一
|
||||
*/
|
||||
stripContextLanguageKey(text) {
|
||||
if (typeof text !== "string") {
|
||||
return text;
|
||||
}
|
||||
return text.match(this.contextKeyPattern)?.[2] || text;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取语言
|
||||
* @returns {string}
|
||||
|
||||
@ -280,7 +280,7 @@
|
||||
</FormItem>
|
||||
<FormItem prop="clear_day" :label="$L('消息保留')">
|
||||
<Input v-model="mybotModifyData.clear_day" :maxlength="3" type="number" :placeholder="$L('默认:90天')">
|
||||
<div slot="append">{{ $L('天') }}</div>
|
||||
<div slot="append">{{ $L('[day_unit].天') }}</div>
|
||||
</Input>
|
||||
</FormItem>
|
||||
<FormItem prop="webhook_url" label="Webhook">
|
||||
|
||||
@ -21,9 +21,9 @@
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<ButtonGroup class="calendar-view">
|
||||
<Button @click="setView('day')" :type="options.view == 'day' ? 'primary' : 'default'">{{ $L('日') }}</Button>
|
||||
<Button @click="setView('week')" :type="options.view == 'week' ? 'primary' : 'default'">{{ $L('周') }}</Button>
|
||||
<Button @click="setView('month')" :type="options.view == 'month' ? 'primary' : 'default'">{{ $L('月') }}</Button>
|
||||
<Button @click="setView('day')" :type="options.view == 'day' ? 'primary' : 'default'">{{ $L('[calendar_view].日') }}</Button>
|
||||
<Button @click="setView('week')" :type="options.view == 'week' ? 'primary' : 'default'">{{ $L('[calendar_view].周') }}</Button>
|
||||
<Button @click="setView('month')" :type="options.view == 'month' ? 'primary' : 'default'">{{ $L('[calendar_view].月') }}</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -11,13 +11,13 @@
|
||||
<table class="check-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{$L('日')}}</th>
|
||||
<th>{{$L('一')}}</th>
|
||||
<th>{{$L('二')}}</th>
|
||||
<th>{{$L('三')}}</th>
|
||||
<th>{{$L('四')}}</th>
|
||||
<th>{{$L('五')}}</th>
|
||||
<th>{{$L('六')}}</th>
|
||||
<th>{{$L('[weekday].日')}}</th>
|
||||
<th>{{$L('[weekday].一')}}</th>
|
||||
<th>{{$L('[weekday].二')}}</th>
|
||||
<th>{{$L('[weekday].三')}}</th>
|
||||
<th>{{$L('[weekday].四')}}</th>
|
||||
<th>{{$L('[weekday].五')}}</th>
|
||||
<th>{{$L('[weekday].六')}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<div class="card-label">{{$L('未完成')}}</div>
|
||||
<div class="card-data">
|
||||
<span class="card-num">{{blocks.uncompleted || 0}}</span>
|
||||
<span class="card-sub">{{$L('项')}}</span>
|
||||
<span class="card-sub">{{$L('[task_unit].项')}}</span>
|
||||
</div>
|
||||
<div class="card-link">{{$L('查看任务')}} →</div>
|
||||
</li>
|
||||
@ -14,7 +14,7 @@
|
||||
<div class="card-label">{{$L('已超期')}}</div>
|
||||
<div class="card-data">
|
||||
<span class="card-num num-red">{{blocks.overdue || 0}}</span>
|
||||
<span class="card-sub">{{$L('项')}}<template v-if="blocks.overdue_owner_count"> · {{$L('涉及 (*) 人', blocks.overdue_owner_count)}}</template></span>
|
||||
<span class="card-sub">{{$L('[task_unit].项')}}<template v-if="blocks.overdue_owner_count"> · {{$L('涉及 (*) 人', blocks.overdue_owner_count)}}</template></span>
|
||||
</div>
|
||||
<div class="card-link link-red">{{$L('查看任务')}} →</div>
|
||||
</li>
|
||||
@ -22,7 +22,7 @@
|
||||
<div class="card-label">{{$L('(*) 天内到期', 3)}}</div>
|
||||
<div class="card-data">
|
||||
<span class="card-num num-orange">{{blocks.due_soon || 0}}</span>
|
||||
<span class="card-sub">{{$L('项')}}</span>
|
||||
<span class="card-sub">{{$L('[task_unit].项')}}</span>
|
||||
</div>
|
||||
<div class="card-link link-orange">{{$L('查看任务')}} →</div>
|
||||
</li>
|
||||
@ -30,7 +30,7 @@
|
||||
<div class="card-label">{{$L('本周完成')}}</div>
|
||||
<div class="card-data">
|
||||
<span class="card-num num-green">{{blocks.week_completed || 0}}</span>
|
||||
<span class="card-sub">{{$L('项')}}<template v-if="weekDiffText"> · {{weekDiffText}}</template></span>
|
||||
<span class="card-sub">{{$L('[task_unit].项')}}<template v-if="weekDiffText"> · {{weekDiffText}}</template></span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
<span>{{ dialog.name }}</span>
|
||||
</div>
|
||||
<div class="initiate">
|
||||
<span>{{ $L('由') }}</span>
|
||||
<span>{{ $L('[initiator_label].由') }}</span>
|
||||
<UserAvatar :userid="createId" :size="22" :showName="true" tooltipDisabled/>
|
||||
<span> {{ $L('发起') }}</span>
|
||||
</div>
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
<span>{{ dialog.name }}</span>
|
||||
</div>
|
||||
<div class="initiate">
|
||||
<span>{{ $L('由') }}</span>
|
||||
<span>{{ $L('[initiator_label].由') }}</span>
|
||||
<UserAvatar :userid="createId" :size="22" :showName="true"/>
|
||||
<span> {{ $L('发起,参与接龙目前共(*)人', num) }}</span>
|
||||
</div>
|
||||
@ -46,7 +46,7 @@
|
||||
</div>
|
||||
<ul ref="wordChainListRef">
|
||||
<li v-for="(item) in list" v-if="item.type == 'case' && (dialogDroupWordChain.type == 'create' || item.text)">
|
||||
<span>{{ $L('例') }}</span>
|
||||
<span>{{ $L('[example_label].例') }}</span>
|
||||
<Input v-model="item.text" :placeholder="$L('可填写接龙格式')" :disabled="dialogDroupWordChain.type != 'create'" />
|
||||
</li>
|
||||
<li v-for="(item,index) in list.filter(h=>h.type != 'case')">
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
{{$L(source.msg.action === 'remove' ? '取消待办' : (source.msg.action === 'done' ? '完成' : '设待办'))}}
|
||||
"{{$A.getMsgSimpleDesc(source.msg.data)}}"
|
||||
<div v-if="source.msg.action === 'add' && formatTodoUser(source.msg.data).length > 0" class="todo-users">
|
||||
<span>{{$L('给')}}</span>
|
||||
<span>{{$L('[todo_target].给')}}</span>
|
||||
<template v-for="(item, index) in formatTodoUser(source.msg.data)">
|
||||
<div v-if="index < 3" class="todo-user"><UserAvatar :userid="item" :show-name="true" :show-icon="false"/></div>
|
||||
<div v-else-if="index == 3" class="todo-user">+{{formatTodoUser(source.msg.data).length - 3}}</div>
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
<li v-for="item in (msg.list || [])">
|
||||
<div class="vote-option-title">{{ item.text }}</div>
|
||||
<div class="ticket-num">
|
||||
<span>{{ getVoteProgress(msg, item.id).num }}{{ $L('票') }}</span>
|
||||
<span>{{ getVoteProgress(msg, item.id).num }}{{ $L('[vote_unit].票') }}</span>
|
||||
<span>{{ getVoteProgress(msg, item.id).progress + '%' }}</span>
|
||||
</div>
|
||||
<Progress :percent="Number(getVoteProgress(msg,item.id).progress)" :stroke-width="5" hide-info/>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<pre v-html="$A.formatTextMsg(msg.text, userId)"></pre>
|
||||
<ul :class="{'expand': unfoldWordChainData.indexOf(msgId) !== -1 }">
|
||||
<li v-for="(item) in (msg.list || []).filter(h=>h.type == 'case')">
|
||||
{{ $L('例') }} {{ item.text }}
|
||||
{{ $L('[example_label].例') }} {{ item.text }}
|
||||
</li>
|
||||
<li v-for="(item, index) in (msg.list || []).filter(h=>h.type != 'case' && h.text)">
|
||||
<span class="expand" v-if="index == 2 && msg.list.length > 4" @click="unfoldWordChain">
|
||||
|
||||
@ -472,7 +472,7 @@
|
||||
<template v-if="dialogData.bot == userId">
|
||||
<FormItem v-if="typeof modifyData.clear_day !== 'undefined'" prop="clear_day" :label="$L('消息保留')">
|
||||
<Input v-model="modifyData.clear_day" :maxlength="3" type="number">
|
||||
<div slot="append">{{$L('天')}}</div>
|
||||
<div slot="append">{{$L('[day_unit].天')}}</div>
|
||||
</Input>
|
||||
</FormItem>
|
||||
<FormItem v-if="typeof modifyData.webhook_url !== 'undefined'" prop="webhook_url" label="Webhook">
|
||||
|
||||
@ -429,7 +429,7 @@
|
||||
<div class="form-tip">{{$L('任务完成(*)天后自动归档。', settingData.archive_days || 'n')}}</div>
|
||||
<div class="setting-auto-day">
|
||||
<Input v-model="settingData.archive_days" type="number">
|
||||
<span slot="append">{{$L('天')}}</span>
|
||||
<span slot="append">{{$L('[day_unit].天')}}</span>
|
||||
</Input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -492,7 +492,7 @@ export default {
|
||||
if (days <= 0) {
|
||||
return item.name + ' (' + this.$L('无时间限制') + ')';
|
||||
}
|
||||
return item.name + ' (' + days + this.$L('天') + ')';
|
||||
return item.name + ' (' + days + this.$L('[day_unit].天') + ')';
|
||||
},
|
||||
|
||||
async choosePriority(item) {
|
||||
|
||||
@ -247,7 +247,7 @@ export default {
|
||||
if (days <= 0) {
|
||||
return item.name + ' (' + this.$L('无时间限制') + ')';
|
||||
}
|
||||
return item.name + ' (' + days + this.$L('天') + ')';
|
||||
return item.name + ' (' + days + this.$L('[day_unit].天') + ')';
|
||||
},
|
||||
|
||||
choosePriority(item, focus = true) {
|
||||
|
||||
@ -529,7 +529,7 @@
|
||||
<template #append>
|
||||
<Select v-model="delayTaskForm.type" style="width:auto">
|
||||
<Option value="hour">{{$L('小时')}}</Option>
|
||||
<Option value="day">{{$L('天')}}</Option>
|
||||
<Option value="day">{{$L('[day_unit].天')}}</Option>
|
||||
</Select>
|
||||
</template>
|
||||
</Input>
|
||||
@ -1448,8 +1448,8 @@ export default {
|
||||
}
|
||||
}
|
||||
}, [
|
||||
h('span', {slot: 'prepend'}, this.$L('每')),
|
||||
h('span', {slot: 'append'}, this.$L('天'))
|
||||
h('span', {slot: 'prepend'}, this.$L('[recurrence_prefix].每')),
|
||||
h('span', {slot: 'append'}, this.$L('[day_unit].天'))
|
||||
])
|
||||
])
|
||||
},
|
||||
|
||||
@ -77,21 +77,21 @@
|
||||
<div class="card-label">{{getTitle('overdue')}}</div>
|
||||
<div class="card-data">
|
||||
<span class="card-num num-red">{{dashboardTask.overdue_count}}</span>
|
||||
<span class="card-sub">{{$L('项')}}<template v-if="overdueMaxDays > 0"> · {{$L('最久 (*) 天', overdueMaxDays)}}</template></span>
|
||||
<span class="card-sub">{{$L('[task_unit].项')}}<template v-if="overdueMaxDays > 0"> · {{$L('最久 (*) 天', overdueMaxDays)}}</template></span>
|
||||
</div>
|
||||
</li>
|
||||
<li :class="{'card-off': dashboardTask.today_count === 0}" @click="scrollTo('today')">
|
||||
<div class="card-label">{{getTitle('today')}}</div>
|
||||
<div class="card-data">
|
||||
<span class="card-num num-orange">{{dashboardTask.today_count}}</span>
|
||||
<span class="card-sub">{{$L('项')}}<template v-if="todayNearest"> · {{$L('最近')}} {{todayNearest}}</template></span>
|
||||
<span class="card-sub">{{$L('[task_unit].项')}}<template v-if="todayNearest"> · {{$L('最近')}} {{todayNearest}}</template></span>
|
||||
</div>
|
||||
</li>
|
||||
<li :class="{'card-off': dashboardTask.todo_count === 0}" @click="scrollTo('todo')">
|
||||
<div class="card-label">{{getTitle('todo')}}</div>
|
||||
<div class="card-data">
|
||||
<span class="card-num">{{dashboardTask.todo_count}}</span>
|
||||
<span class="card-sub">{{$L('项')}}</span>
|
||||
<span class="card-sub">{{$L('[task_unit].项')}}</span>
|
||||
</div>
|
||||
<div v-if="upcomingTask.count > 0" class="duo-upcoming" @click.stop="scrollTo('upcoming')">
|
||||
<div class="side-num">{{upcomingTask.count}}</div>
|
||||
@ -102,7 +102,7 @@
|
||||
<div class="card-label">{{getTitle('assist')}}</div>
|
||||
<div class="card-data">
|
||||
<span class="card-num">{{assistTask.length}}</span>
|
||||
<span class="card-sub">{{$L('项')}}<template v-if="assistTodayCount > 0"> · {{$L('今天 (*) 项到期', assistTodayCount)}}</template></span>
|
||||
<span class="card-sub">{{$L('[task_unit].项')}}<template v-if="assistTodayCount > 0"> · {{$L('今天 (*) 项到期', assistTodayCount)}}</template></span>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@ -111,7 +111,7 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="dialog.type=='group' && dialog.last_msg && getLastMsgSenderId(dialog.last_msg)">
|
||||
<div v-if="getLastMsgSenderId(dialog.last_msg) == userId" class="last-self">{{$L('你')}}</div>
|
||||
<div v-if="getLastMsgSenderId(dialog.last_msg) == userId" class="last-self">{{$L('[self_sender].你')}}</div>
|
||||
<UserAvatar v-else :userid="getLastMsgSenderId(dialog.last_msg)" :show-name="true" :show-icon="false"/>
|
||||
</template>
|
||||
<div class="last-text">
|
||||
|
||||
@ -245,7 +245,7 @@
|
||||
<div class="radius-control-body">
|
||||
<Input :value="mapSelectPoint.radius" @on-change="onRadiusChange" @on-blur="onRadiusBlur">
|
||||
<span slot="prepend">{{ $L('半径') }}</span>
|
||||
<span slot="append">{{ $L('米') }}</span>
|
||||
<span slot="append">{{ $L('[distance_unit].米') }}</span>
|
||||
</Input>
|
||||
<div class="location-info">
|
||||
<div class="info-item">
|
||||
@ -258,7 +258,7 @@
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">{{ $L('半径') }}:</span>
|
||||
<span class="info-value">{{ mapSelectPoint.radius || '-' }} {{ $L('米') }}</span>
|
||||
<span class="info-value">{{ mapSelectPoint.radius || '-' }} {{ $L('[distance_unit].米') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="radius-control-tip">
|
||||
|
||||
@ -116,7 +116,7 @@
|
||||
<ETooltip v-if="formDatum.auto_archived=='open'" placement="right" :disabled="$isEEUIApp || windowTouch">
|
||||
<div class="setting-auto-day">
|
||||
<Input v-model="formDatum.archived_day" type="number">
|
||||
<span slot="append">{{$L('天')}}</span>
|
||||
<span slot="append">{{$L('[day_unit].天')}}</span>
|
||||
</Input>
|
||||
</div>
|
||||
<div slot="content">{{$L('任务完成(*)天后自动归档。', formDatum.archived_day || 'n')}}</div>
|
||||
@ -163,7 +163,7 @@
|
||||
<div style="width: 110px;">
|
||||
<Input type="number" number v-model="formDatum.task_user_limit" @on-keyup="$A.inputNumberLimit($event, 1, 2000)">
|
||||
<template #append>
|
||||
<span>{{$L('个')}}</span>
|
||||
<span>{{$L('[task_unit].个')}}</span>
|
||||
</template>
|
||||
</Input>
|
||||
</div>
|
||||
|
||||
@ -65,6 +65,8 @@ const PARAMETER_TEST_RE = /\(%[TM]\d+\)/;
|
||||
const RAW_PARAMETER_TEST_RE = /\(\*{1,2}\)/;
|
||||
const NAMED_PARAMETER_RE = /\{[A-Za-z_][A-Za-z0-9_.-]*\}/g;
|
||||
const CHINESE_RE = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]/;
|
||||
const CONTEXT_KEY_RE = /^\[([a-z][a-z0-9_]*)]\.([\s\S]+)$/;
|
||||
const CONTEXT_KEY_PREFIX_RE = /^\[[^\]]+]\./;
|
||||
|
||||
function normalizePath(file) {
|
||||
return file.split(sep).join("/");
|
||||
@ -109,6 +111,10 @@ function normalizeParameterKey(key) {
|
||||
return key.replace(/\(%T\d+\)/g, "(*)").replace(/\(%M\d+\)/g, "(**)");
|
||||
}
|
||||
|
||||
function contextSourceKey(key) {
|
||||
return key.match(CONTEXT_KEY_RE)?.[2] || null;
|
||||
}
|
||||
|
||||
function placeholders(value) {
|
||||
return [...value.matchAll(PARAMETER_RE)].map(match => match[0]);
|
||||
}
|
||||
@ -1016,6 +1022,12 @@ const originalDuplicates = [...originalLocations]
|
||||
.filter(([, lines]) => lines.length > 1)
|
||||
.map(([key, lines]) => ({ key, lines }));
|
||||
const originalKeySet = new Set(originalKeys);
|
||||
const originalContextErrors = originalLineItems.flatMap(item => {
|
||||
if (!CONTEXT_KEY_PREFIX_RE.test(item.text)) return [];
|
||||
const source = contextSourceKey(item.text);
|
||||
if (!source) return [`language/original-web.txt:${item.line}:上下文翻译键格式错误「${item.text}」`];
|
||||
return source.trim() === "" ? [`language/original-web.txt:${item.line}:上下文翻译键原文不能为空`] : [];
|
||||
});
|
||||
|
||||
let translationRows = [];
|
||||
const translationFormatErrors = [];
|
||||
@ -1043,6 +1055,14 @@ for (const [index, row] of translationRows.entries()) {
|
||||
if (typeof row[field] !== "string") translationFormatErrors.push(`${location}.${field}:必须是字符串`);
|
||||
}
|
||||
if (typeof row.key !== "string") continue;
|
||||
if (CONTEXT_KEY_PREFIX_RE.test(row.key)) {
|
||||
const source = contextSourceKey(row.key);
|
||||
if (!source) {
|
||||
translationFormatErrors.push(`${location}.key:上下文翻译键格式错误「${row.key}」`);
|
||||
} else if (row.zh !== source) {
|
||||
translationFormatErrors.push(`${location}.zh:上下文翻译键必须填写原文「${source}」`);
|
||||
}
|
||||
}
|
||||
if (RAW_PARAMETER_TEST_RE.test(row.key)) rawTranslateKeys.push({ key: row.key, location });
|
||||
const keyParameters = placeholders(row.key);
|
||||
if (row.key.replace(PARAMETER_RE, "").includes("(%")) {
|
||||
@ -1089,6 +1109,7 @@ const missingFrontendTranslations = originalKeys
|
||||
.filter(key => !translationsByNormalizedKey.has(key))
|
||||
.map(key => ({ key, line: originalLocations.get(key)[0] }));
|
||||
const skipGeneratedIndexComparison = originalDuplicates.length > 0
|
||||
|| originalContextErrors.length > 0
|
||||
|| normalizedDuplicates.length > 0
|
||||
|| missingFrontendTranslations.length > 0
|
||||
|| translationFormatErrors.length > 0
|
||||
@ -1105,6 +1126,7 @@ const blockerCounts = {
|
||||
"Vue 模板中文硬编码": frontend.templateTexts.length,
|
||||
"命名变量占位符": namedParameters.length,
|
||||
"original-web.txt 重复键": originalDuplicates.length,
|
||||
"上下文翻译键格式错误": originalContextErrors.length,
|
||||
"translate.json raw (*)/(**) key": rawTranslateKeys.length,
|
||||
"translate.json 结构错误": translationFormatErrors.length,
|
||||
"translate.json 参数错误": translationParameterErrors.length,
|
||||
@ -1136,6 +1158,10 @@ if (originalDuplicates.length) {
|
||||
console.log("\n-- original-web.txt 重复键 --");
|
||||
for (const item of originalDuplicates) console.log(` 行 ${item.lines.join(", ")} 「${item.key}」`);
|
||||
}
|
||||
if (originalContextErrors.length) {
|
||||
console.log("\n-- 上下文翻译键格式错误 --");
|
||||
for (const error of originalContextErrors) console.log(` ${error}`);
|
||||
}
|
||||
if (rawTranslateKeys.length) {
|
||||
console.log("\n-- translate.json raw key(请改用 (%Tn)/(%Mn))--");
|
||||
for (const item of rawTranslateKeys) console.log(` ${item.location} 「${item.key}」`);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user