Skip to content

Commit a6e91ae

Browse files
authored
feat: merge i18n add and update tool (#1645)
1 parent f7dec37 commit a6e91ae

7 files changed

Lines changed: 191 additions & 171 deletions

File tree

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { HOOK_NAME } from '@opentiny/tiny-engine-meta-register'
22
import useTranslate from './useTranslate'
3-
import { addI18n, delI18n, updateI18n, getI18n } from './tools'
3+
import { delI18n, getI18n, saveI18n } from './tools'
44

55
export const TranslateService = {
66
id: 'engine.service.translate',
@@ -10,6 +10,6 @@ export const TranslateService = {
1010
name: HOOK_NAME.useTranslate
1111
},
1212
mcp: {
13-
tools: [addI18n, delI18n, updateI18n, getI18n]
13+
tools: [saveI18n, delI18n, getI18n]
1414
}
1515
}

packages/plugins/i18n/src/composable/tools/addI18n.ts

Lines changed: 0 additions & 71 deletions
This file was deleted.
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// i18n 工具集合
22
// 统一导出所有的 i18n 相关工具
33

4-
export { addI18n } from './addI18n'
54
export { delI18n } from './delI18n'
65
export { getI18n } from './getI18n'
7-
export { updateI18n } from './updateI18n'
6+
export { saveI18n } from './saveI18n'
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { z } from 'zod'
2+
import useTranslate from '../useTranslate'
3+
import { createOutputSchema, createSuccessResponse, createErrorResponse } from './commonSchema'
4+
5+
// 输入 Schema:统一保存工具,支持 add / update / upsert 与合并策略
6+
const inputSchema = z.object({
7+
key: z
8+
.string()
9+
.describe(
10+
'唯一的 i18n 键名,用于标识翻译词条。建议使用点分割命名规范,如 "common.button.save" 或 "page.user.title"'
11+
),
12+
operation: z
13+
.enum(['add', 'update', 'upsert'])
14+
.optional()
15+
.describe(
16+
'操作模式:add=仅新增(键已存在时报错),update=仅更新(键不存在时报错),upsert=智能新增或更新(默认推荐)'
17+
),
18+
translations: z
19+
.record(z.string())
20+
.optional()
21+
.describe(
22+
'多语言翻译映射对象,键为语言代码(如 zh_CN, en_US),值为对应翻译文本。优先级高于单独的 zh_CN/en_US 字段'
23+
),
24+
zh_CN: z
25+
.string()
26+
.optional()
27+
.describe('中文翻译文本(语法糖字段)。当只需设置中文时使用,会被 translations 中的 zh_CN 覆盖'),
28+
en_US: z
29+
.string()
30+
.optional()
31+
.describe('英文翻译文本(语法糖字段)。当只需设置英文时使用,会被 translations 中的 en_US 覆盖'),
32+
mergeStrategy: z
33+
.enum(['partial', 'replace'])
34+
.optional()
35+
.describe(
36+
'更新现有词条时的合并策略:partial=部分更新(仅更新提供的语言,其他语言保持不变,默认),replace=替换更新(清空未提供的语言)'
37+
),
38+
idempotent: z
39+
.boolean()
40+
.optional()
41+
.describe(
42+
'幂等模式开关。true=遇到边界冲突时静默处理不报错(如 add 已存在的键、update 不存在的键),false=严格模式会抛出错误(默认)'
43+
)
44+
})
45+
46+
// 输出 Schema:统一数据结构
47+
const dataSchema = z.object({
48+
key: z.string(),
49+
type: z.string(),
50+
translations: z.record(z.string()),
51+
operation: z.enum(['add', 'update', 'upsert']),
52+
mergeStrategy: z.enum(['partial', 'replace']),
53+
originalEntry: z.record(z.any()).optional(),
54+
noOp: z.boolean().optional()
55+
})
56+
57+
const outputSchema = createOutputSchema(dataSchema)
58+
59+
export const saveI18n = {
60+
name: 'save_i18n',
61+
title: '保存 I18n 词条',
62+
description: `在当前 TinyEngine 低代码应用中创建或更新国际化(i18n)翻译词条。
63+
64+
适用场景:
65+
• 添加新的多语言文本(按钮文本、提示信息、页面标题等)
66+
• 更新现有翻译内容(修正翻译、优化措辞)
67+
• 批量管理多语言词条(支持中英文及其他语言)
68+
69+
核心特性:
70+
• 支持 add/update/upsert 三种操作模式,满足不同业务需求
71+
• 提供 partial/replace 合并策略,灵活控制更新范围
72+
• 内置幂等性支持,避免重复操作引起的错误
73+
• 支持语法糖字段(zh_CN/en_US)和完整翻译映射(translations)
74+
75+
调用时机:
76+
• 当需要为界面元素添加多语言支持时
77+
• 当需要修改现有的翻译文本时
78+
• 当需要确保翻译词条存在且内容正确时(使用 upsert 模式)`,
79+
inputSchema: inputSchema.shape,
80+
outputSchema: outputSchema.shape,
81+
annotations: {
82+
title: 'Save I18n Entry',
83+
readOnlyHint: false,
84+
destructiveHint: false,
85+
idempotentHint: true,
86+
openWorldHint: false
87+
},
88+
callback: async (args: z.infer<typeof inputSchema>) => {
89+
const {
90+
key,
91+
operation = 'upsert',
92+
translations: inputTranslations,
93+
zh_CN,
94+
en_US,
95+
mergeStrategy = 'partial',
96+
idempotent = false
97+
} = args
98+
99+
try {
100+
const { getLangs, i18nResource, ensureI18n } = useTranslate()
101+
const langs = getLangs() as Record<string, any>
102+
const locales = (i18nResource?.locales || []).map((l: any) => l.lang)
103+
104+
// 归并顶层糖衣字段到 translations,translations 优先
105+
const mergedInputTranslations: Record<string, string> = {
106+
...(zh_CN !== undefined ? { zh_CN } : {}),
107+
...(en_US !== undefined ? { en_US } : {}),
108+
...(inputTranslations || {})
109+
}
110+
111+
const existingEntry = langs[key]
112+
113+
// 边界条件处理(严格模式与幂等模式)
114+
if (operation === 'add' && existingEntry) {
115+
if (idempotent) {
116+
return createSuccessResponse('未更改:词条已存在', {
117+
key,
118+
type: existingEntry.type || 'i18n',
119+
translations: Object.fromEntries(locales.map((lg) => [lg, existingEntry[lg] || ''])),
120+
operation: 'add',
121+
mergeStrategy,
122+
originalEntry: existingEntry,
123+
noOp: true
124+
})
125+
}
126+
return createErrorResponse('I18n 键已存在', `键 "${key}" 已在 i18n 字典中定义`)
127+
}
128+
129+
if (operation === 'update' && !existingEntry) {
130+
if (idempotent) {
131+
return createSuccessResponse('未更改:词条不存在', {
132+
key,
133+
type: 'i18n',
134+
translations: Object.fromEntries(locales.map((lg) => [lg, ''])),
135+
operation: 'update',
136+
mergeStrategy,
137+
noOp: true
138+
})
139+
}
140+
return createErrorResponse('未找到 I18n 键', `键 "${key}" 不存在于 i18n 字典中`)
141+
}
142+
143+
// 计算最终写入的 translations(保护 partial 合并不被空串覆盖)
144+
const baseTranslations: Record<string, string> = Object.fromEntries(
145+
locales.map((lg: string) => [lg, existingEntry?.[lg] ?? ''])
146+
)
147+
148+
const finalTranslations: Record<string, string> = { ...baseTranslations }
149+
150+
if (mergeStrategy === 'partial') {
151+
Object.entries(mergedInputTranslations).forEach(([lg, val]) => {
152+
if (locales.includes(lg)) finalTranslations[lg] = val
153+
})
154+
} else {
155+
// replace:仅对提供的语言键进行覆盖,其它语言保持原值
156+
Object.entries(mergedInputTranslations).forEach(([lg, val]) => {
157+
if (locales.includes(lg)) finalTranslations[lg] = val
158+
})
159+
}
160+
161+
const finalEntry = { key, type: existingEntry?.type || 'i18n', ...finalTranslations }
162+
163+
// 写入(ensureI18n 内部会根据是否存在选择 create 或 update)
164+
await ensureI18n(finalEntry, true)
165+
166+
const resData = {
167+
key,
168+
type: finalEntry.type,
169+
translations: Object.fromEntries(locales.map((lg) => [lg, finalTranslations[lg] || ''])),
170+
operation: existingEntry ? (operation === 'add' ? 'add' : 'update') : operation === 'update' ? 'update' : 'add',
171+
mergeStrategy,
172+
...(existingEntry ? { originalEntry: existingEntry } : {})
173+
}
174+
175+
return createSuccessResponse('I18n 词条保存成功', resData)
176+
} catch (error) {
177+
const errorMessage = error instanceof Error ? error.message : '发生未知错误'
178+
return createErrorResponse('保存 i18n 词条失败', errorMessage)
179+
}
180+
}
181+
}

packages/plugins/i18n/src/composable/tools/updateI18n.ts

Lines changed: 0 additions & 89 deletions
This file was deleted.

packages/plugins/i18n/src/composable/useTranslate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ const ensureI18n = (obj: { [x: string]: any; key: string }, send?: boolean) => {
115115

116116
langs[key] = { key, ...contents, type: PROP_DATA_TYPE.I18N }
117117

118-
return langs[contents.key]
118+
return langs[key]
119119
}
120120

121121
const getI18nData = () => {

0 commit comments

Comments
 (0)