@camscanner/mcp-language-server 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,6 +37,27 @@ claude plugin install i18n@camscanner-plugins
37
37
  - 浏览器数据持久化在 `~/.language-mcp/browser-data/`,保存的密码下次自动填充
38
38
  - 认证信息保存在 `~/.language-mcp/credentials.json`,有效期 24 小时
39
39
 
40
+ ## 开发者指南
41
+
42
+ ### 发布新版本
43
+
44
+ ```bash
45
+ # 1. 修改代码并构建
46
+ npm run build
47
+
48
+ # 2. 更新版本号并发布到 npm
49
+ npm version patch # bug fix: 1.0.0 → 1.0.1
50
+ npm version minor # 新功能: 1.0.0 → 1.1.0
51
+ npm version major # 破坏性变更: 1.0.0 → 2.0.0
52
+
53
+ npm publish --registry https://registry.npmjs.org/ --access public
54
+
55
+ # 3. 推送 tag 到远端
56
+ git push && git push --tags
57
+ ```
58
+
59
+ 用户下次启动 Claude Code 时,`npx -y @camscanner/mcp-language-server@latest` 会自动拉取新版本。
60
+
40
61
  ## 常用产品 ID
41
62
 
42
63
  | ID | 产品 |
package/dist/index.js CHANGED
@@ -163,6 +163,71 @@ server.tool('search-string', '按关键词搜索多语言字符串。可指定
163
163
  }
164
164
  return { content: [{ type: 'text', text: output }] };
165
165
  });
166
+ // Tool: batch-search-string
167
+ server.tool('batch-search-string', '批量搜索多语言字符串。支持同时搜索多个关键词,每个关键词独立返回结果。适合一次性查询大量字符串是否已在平台录入。', {
168
+ product_id: v3_1.z.string().describe('产品ID,先调用 list-products 查询'),
169
+ words: v3_1.z.array(v3_1.z.string()).describe('搜索关键词数组(中文、英文或 key 名)'),
170
+ version_id: v3_1.z.string().optional().describe('版本ID,不传则搜索所有版本'),
171
+ fuzzy: v3_1.z.string().optional().default('0').describe('1=模糊匹配,0=精确匹配(默认)'),
172
+ page_size: v3_1.z.string().optional().default('20').describe('每个关键词的每页条数,最大100'),
173
+ }, async ({ product_id, words, version_id, fuzzy, page_size }) => {
174
+ const authErr = await requireAuth();
175
+ if (authErr)
176
+ return { content: [{ type: 'text', text: authErr }] };
177
+ const results = [];
178
+ let foundCount = 0;
179
+ let notFoundCount = 0;
180
+ // 并发搜索所有关键词
181
+ const tasks = words.map(async (word) => {
182
+ const params = {
183
+ product_id, word, fuzzy: fuzzy || '0', page: '1', page_size: page_size || '20',
184
+ };
185
+ if (version_id)
186
+ params.version_id = version_id;
187
+ try {
188
+ const data = await client.post('/language/mcp-language/get-string-search', params);
189
+ if (data.errno !== 0) {
190
+ return { word, found: false, output: `❌ "${word}" — 查询失败: ${data.message || '未知错误'}` };
191
+ }
192
+ const versions = Array.isArray(data.data) ? data.data : (data.data?.list || []);
193
+ if (versions.length === 0) {
194
+ return { word, found: false, output: `❌ "${word}" — 未找到` };
195
+ }
196
+ let output = `✅ "${word}" — ${versions.length} 个版本匹配:\n`;
197
+ for (const version of versions) {
198
+ const strings = version.ar_string || version.strings || [];
199
+ for (const str of strings) {
200
+ const keys = str.keys
201
+ ? Object.entries(str.keys).map(([p, k]) => `platform_${p}: ${k}`).join(', ')
202
+ : '无';
203
+ const zhCN = str.values?.['1'] || str.values?.['0'] || '';
204
+ const enUS = str.values?.['2'] || str.values?.['0'] || '';
205
+ const zhTW = str.values?.['7'] || '';
206
+ output += ` - key: ${keys} | 中: ${zhCN} | 英: ${enUS}`;
207
+ if (zhTW)
208
+ output += ` | 繁: ${zhTW}`;
209
+ output += ` (v${version.version_number})\n`;
210
+ }
211
+ }
212
+ return { word, found: true, output };
213
+ }
214
+ catch (err) {
215
+ return { word, found: false, output: `❌ "${word}" — 请求异常: ${err.message}` };
216
+ }
217
+ });
218
+ const settled = await Promise.all(tasks);
219
+ for (const r of settled) {
220
+ results.push(r.output);
221
+ if (r.found)
222
+ foundCount++;
223
+ else
224
+ notFoundCount++;
225
+ }
226
+ let output = `## 批量搜索结果\n\n`;
227
+ output += `共搜索 ${words.length} 个关键词: ✅ ${foundCount} 个找到, ❌ ${notFoundCount} 个未找到\n\n`;
228
+ output += results.join('\n');
229
+ return { content: [{ type: 'text', text: output }] };
230
+ });
166
231
  // Tool: export-string
167
232
  server.tool('export-string', '搜索多语言字符串并导出为兼容 cs-i18n 的 locale JSON 格式。支持单语言或全部语言导出,自动将 %s 替换为 {0}/{1}/{2}。', {
168
233
  product_id: v3_1.z.string().describe('产品ID。常用: 1=CamCard, 2=CamScanner, 44=CS Lite, 47=CS PDF, 53=CS Harmony'),
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export declare const PLATFORM_MAP: Record<number, string>;
2
+ export declare function getLocaleName(langId: string | number): string | null;
2
3
  export declare const LANGUAGE_LOCALE_MAP: Record<string, string>;
3
4
  export declare function fixPlaceholders(value: string): string;
4
5
  export declare function extractStrings(versions: any[], platformId: string): Record<string, Record<string, string>>;
package/dist/utils.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // --- Constants ---
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.LANGUAGE_LOCALE_MAP = exports.PLATFORM_MAP = void 0;
5
+ exports.getLocaleName = getLocaleName;
5
6
  exports.fixPlaceholders = fixPlaceholders;
6
7
  exports.extractStrings = extractStrings;
7
8
  exports.mergeLocaleEntries = mergeLocaleEntries;
@@ -15,35 +16,54 @@ exports.PLATFORM_MAP = {
15
16
  8: 'Harmony',
16
17
  9: 'PC',
17
18
  };
18
- exports.LANGUAGE_LOCALE_MAP = {
19
- '1': 'ZhCn',
20
- '2': 'EnUs',
21
- '3': 'JaJp',
22
- '4': 'KoKr',
23
- '5': 'FrFr',
24
- '6': 'DeDe',
25
- '7': 'ZhTw',
26
- '8': 'PtBr',
27
- '9': 'EsEs',
28
- '10': 'ItIt',
29
- '11': 'RuRu',
30
- '12': 'TrTr',
31
- '13': 'ArSa',
32
- '14': 'ThTh',
33
- '15': 'PlPl',
34
- '16': 'ViVn',
35
- '17': 'InId',
36
- '19': 'MsMy',
37
- '20': 'NlNl',
38
- '22': 'HiDi',
39
- '23': 'BnBd',
40
- '24': 'CsCs',
41
- '25': 'SkSk',
42
- '26': 'FilPh',
43
- '27': 'ElEl',
44
- '28': 'PtPt',
45
- '29': 'RoRo',
19
+ // 语言ID → locale 字符串映射(从 operate-main AppConfMacro::$ar_language_map 复制)
20
+ const LANGUAGE_STRING_MAP = {
21
+ 1: 'zh-cn', 2: 'en-us', 3: 'de-de', 4: 'fr-fr',
22
+ 5: 'ja-jp', 6: 'ko-kr', 7: 'zh-tw', 8: 'es-es',
23
+ 9: 'ru-ru', 10: 'sk-sk', 11: 'cs-cs', 12: 'pt-pt',
24
+ 13: 'pl-pl', 14: 'it-it', 15: 'tr-tr', 16: 'ar-ar',
25
+ 17: 'pt-br', 19: 'ag-ag', 20: 'sm-sm', 22: 'id-id',
26
+ 23: 'th-th', 24: 'fil-ph', 25: 'ms-my', 26: 'vi-vn',
27
+ 27: 'bn-bd', 28: 'fa-ir', 29: 'hi-in', 30: 'nl-nl',
28
+ 31: 'el-gr', 32: 'hu-hu', 33: 'uk-ua', 34: 'no-no',
29
+ 35: 'da-dk', 36: 'ur-pk', 37: 'hr-hr', 38: 'hy-am',
30
+ 39: 'bg-bg', 40: 'si-lk', 41: 'is-is', 42: 'kk-kz',
31
+ 43: 'sr-rs', 44: 'ne-np', 45: 'lv-lv', 46: 'sl-si',
32
+ 47: 'sw-ke', 48: 'ka-ge', 49: 'et-ee', 50: 'sv-se',
33
+ 51: 'be-by', 52: 'zu-za', 53: 'lt-lt', 54: 'my-mm',
34
+ 55: 'ro-ro', 56: 'lo-la', 57: 'mn-mn', 58: 'az-az',
35
+ 59: 'am-et', 60: 'sq-al', 61: 'mk-mk', 62: 'gl-es',
36
+ 63: 'ca-es', 64: 'af-za', 65: 'kn-in', 66: 'gu-in',
37
+ 67: 'eu-es', 68: 'iw-il', 69: 'pa-in', 70: 'ky-kg',
38
+ 71: 'te-in', 72: 'ta-in', 73: 'rm-ch', 74: 'mr-in',
39
+ 75: 'ml-in', 76: 'km-kh', 77: 'bs-ba', 78: 'lb-lu',
40
+ 79: 'rw-rw', 80: 'mt-mt', 81: 'uz-uz', 82: 'ga-ie',
46
41
  };
42
+ // locale 字符串 → PascalCase 文件名(从 operate-main MacroExport::filter2 复制)
43
+ function toLocaleName(localeStr) {
44
+ return localeStr.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('');
45
+ }
46
+ // 特殊映射:项目 locale 文件名与 filter2 输出不一致的语言
47
+ const LOCALE_NAME_OVERRIDE = {
48
+ 16: 'ArSa', // filter2: ArAr → 实际: ArSa
49
+ 20: 'FiFi', // filter2: SmSm → 实际: FiFi (芬兰语)
50
+ 22: 'InId', // filter2: IdId → 实际: InId (印尼语)
51
+ 23: 'Th', // filter2: ThTh → 实际: Th (泰语)
52
+ 29: 'HiDi', // filter2: HiIn → 实际: HiDi (印地语)
53
+ 31: 'ElEl', // filter2: ElGr → 实际: ElEl (希腊语)
54
+ };
55
+ // 获取 locale 文件名(优先用 override,否则 filter2 动态生成)
56
+ function getLocaleName(langId) {
57
+ const id = Number(langId);
58
+ if (LOCALE_NAME_OVERRIDE[id])
59
+ return LOCALE_NAME_OVERRIDE[id];
60
+ const localeStr = LANGUAGE_STRING_MAP[id];
61
+ if (!localeStr)
62
+ return null;
63
+ return toLocaleName(localeStr);
64
+ }
65
+ // 兼容旧接口:动态生成完整映射表
66
+ exports.LANGUAGE_LOCALE_MAP = Object.fromEntries(Object.keys(LANGUAGE_STRING_MAP).map(id => [String(id), getLocaleName(id)]).filter(([, v]) => v));
47
67
  // --- Helpers ---
48
68
  function fixPlaceholders(value) {
49
69
  let cnt = 0;
package/index.js CHANGED
@@ -116,37 +116,55 @@ const PRODUCT_MAP = {
116
116
  53: "CS Harmony",
117
117
  };
118
118
 
119
- // 语言ID → locale 文件名映射(兼容 cs-i18n 工具)
120
- const LANGUAGE_LOCALE_MAP = {
121
- "1": "ZhCn", // 简体中文
122
- "2": "EnUs", // 英语
123
- "3": "JaJp", // 日语
124
- "4": "KoKr", // 韩语
125
- "5": "FrFr", // 法语
126
- "6": "DeDe", // 德语
127
- "7": "ZhTw", // 繁体中文
128
- "8": "PtBr", // 巴西葡萄牙语
129
- "9": "EsEs", // 西班牙语
130
- "10": "ItIt", // 意大利语
131
- "11": "RuRu", // 俄语
132
- "12": "TrTr", // 土耳其语
133
- "13": "ArSa", // 阿拉伯语
134
- "14": "ThTh", // 泰语(Th)
135
- "15": "PlPl", // 波兰语
136
- "16": "ViVn", // 越南语
137
- "17": "InId", // 印度尼西亚语
138
- "19": "MsMy", // 马来语
139
- "20": "NlNl", // 荷兰语
140
- "22": "HiDi", // 印地语
141
- "23": "BnBd", // 孟加拉语
142
- "24": "CsCs", // 捷克语
143
- "25": "SkSk", // 斯洛伐克语
144
- "26": "FilPh", // 菲律宾语
145
- "27": "ElEl", // 希腊语
146
- "28": "PtPt", // 葡萄牙语
147
- "29": "RoRo", // 罗马尼亚语
119
+ // 语言ID → locale 字符串映射(从 operate-main AppConfMacro::$ar_language_map 复制)
120
+ const LANGUAGE_STRING_MAP = {
121
+ 1: "zh-cn", 2: "en-us", 3: "de-de", 4: "fr-fr",
122
+ 5: "ja-jp", 6: "ko-kr", 7: "zh-tw", 8: "es-es",
123
+ 9: "ru-ru", 10: "sk-sk", 11: "cs-cs", 12: "pt-pt",
124
+ 13: "pl-pl", 14: "it-it", 15: "tr-tr", 16: "ar-ar",
125
+ 17: "pt-br", 19: "ag-ag", 20: "sm-sm", 22: "id-id",
126
+ 23: "th-th", 24: "fil-ph", 25: "ms-my", 26: "vi-vn",
127
+ 27: "bn-bd", 28: "fa-ir", 29: "hi-in", 30: "nl-nl",
128
+ 31: "el-gr", 32: "hu-hu", 33: "uk-ua", 34: "no-no",
129
+ 35: "da-dk", 36: "ur-pk", 37: "hr-hr", 38: "hy-am",
130
+ 39: "bg-bg", 40: "si-lk", 41: "is-is", 42: "kk-kz",
131
+ 43: "sr-rs", 44: "ne-np", 45: "lv-lv", 46: "sl-si",
132
+ 47: "sw-ke", 48: "ka-ge", 49: "et-ee", 50: "sv-se",
133
+ 51: "be-by", 52: "zu-za", 53: "lt-lt", 54: "my-mm",
134
+ 55: "ro-ro", 56: "lo-la", 57: "mn-mn", 58: "az-az",
135
+ 59: "am-et", 60: "sq-al", 61: "mk-mk", 62: "gl-es",
136
+ 63: "ca-es", 64: "af-za", 65: "kn-in", 66: "gu-in",
137
+ 67: "eu-es", 68: "iw-il", 69: "pa-in", 70: "ky-kg",
138
+ 71: "te-in", 72: "ta-in", 73: "rm-ch", 74: "mr-in",
139
+ 75: "ml-in", 76: "km-kh", 77: "bs-ba", 78: "lb-lu",
140
+ 79: "rw-rw", 80: "mt-mt", 81: "uz-uz", 82: "ga-ie",
148
141
  };
149
142
 
143
+ // locale 字符串 → PascalCase 文件名(从 operate-main MacroExport::filter2 复制)
144
+ // 'zh-cn' → 'ZhCn', 'fil-ph' → 'FilPh'
145
+ function toLocaleName(localeStr) {
146
+ return localeStr.split("-").map(s => s.charAt(0).toUpperCase() + s.slice(1)).join("");
147
+ }
148
+
149
+ // 特殊映射:项目 locale 文件名与 filter2 输出不一致的语言
150
+ const LOCALE_NAME_OVERRIDE = {
151
+ 16: "ArSa", // filter2: ArAr → 实际: ArSa
152
+ 20: "FiFi", // filter2: SmSm → 实际: FiFi (芬兰语)
153
+ 22: "InId", // filter2: IdId → 实际: InId (印尼语)
154
+ 23: "Th", // filter2: ThTh → 实际: Th (泰语)
155
+ 29: "HiDi", // filter2: HiIn → 实际: HiDi (印地语)
156
+ 31: "ElEl", // filter2: ElGr → 实际: ElEl (希腊语)
157
+ };
158
+
159
+ // 获取 locale 文件名(优先用 override,否则 filter2 动态生成)
160
+ function getLocaleName(langId) {
161
+ const id = Number(langId);
162
+ if (LOCALE_NAME_OVERRIDE[id]) return LOCALE_NAME_OVERRIDE[id];
163
+ const localeStr = LANGUAGE_STRING_MAP[id];
164
+ if (!localeStr) return null;
165
+ return toLocaleName(localeStr);
166
+ }
167
+
150
168
  // %s → {0}, {1}, {2}... 替换
151
169
  function fixPlaceholders(value) {
152
170
  let cnt = 0;
@@ -460,7 +478,7 @@ function registerTools(server) {
460
478
  content: [{ type: "text", text: `找到字符串但语言ID=${language_id} 无翻译内容` }],
461
479
  };
462
480
  }
463
- const localeName = LANGUAGE_LOCALE_MAP[language_id] || `lang_${language_id}`;
481
+ const localeName = getLocaleName(language_id) || `lang_${language_id}`;
464
482
  const json = JSON.stringify(localeObj, null, 2);
465
483
  return {
466
484
  content: [{
@@ -473,7 +491,7 @@ function registerTools(server) {
473
491
  // 全部语言导出
474
492
  let output = `共匹配 ${Object.keys(allStrings).length} 种语言:\n\n`;
475
493
  for (const [langId, localeObj] of Object.entries(allStrings)) {
476
- const localeName = LANGUAGE_LOCALE_MAP[langId] || `lang_${langId}`;
494
+ const localeName = getLocaleName(langId) || `lang_${langId}`;
477
495
  const json = JSON.stringify(localeObj, null, 2);
478
496
  output += `### ${localeName}.json (语言ID=${langId}, ${Object.keys(localeObj).length} 条)\n\`\`\`json\n${json}\n\`\`\`\n\n`;
479
497
  }
@@ -540,7 +558,7 @@ function registerTools(server) {
540
558
  let filesSkipped = 0;
541
559
 
542
560
  for (const [langId, newEntries] of Object.entries(allStrings)) {
543
- const localeName = LANGUAGE_LOCALE_MAP[langId];
561
+ const localeName = getLocaleName(langId);
544
562
  if (!localeName) {
545
563
  filesSkipped++;
546
564
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camscanner/mcp-language-server",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "MCP Server for multi-language string management",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "language": {
3
3
  "command": "npx",
4
- "args": ["-y", "git+https://github.com/tianmuji/mcp-language-server.git"],
4
+ "args": ["-y", "@camscanner/mcp-language-server@latest"],
5
5
  "env": {
6
6
  "OPERATE_BASE_URL": "https://operate.intsig.net",
7
7
  "SSO_LOGIN_URL": "https://web-sso.intsig.net/login",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "yapi": {
3
3
  "command": "npx",
4
- "args": ["-y", "git+https://gitlab.intsig.net/cs-templates/skills/yapi-mcp-server.git"],
4
+ "args": ["-y", "@camscanner/yapi-mcp-server@latest"],
5
5
  "env": {
6
6
  "YAPI_BASE_URL": "https://web-api.intsig.net",
7
7
  "SSO_LOGIN_URL": "https://web-sso.intsig.net/login",
package/src/index.ts CHANGED
@@ -210,6 +210,79 @@ server.tool(
210
210
  }
211
211
  )
212
212
 
213
+ // Tool: batch-search-string
214
+ server.tool(
215
+ 'batch-search-string',
216
+ '批量搜索多语言字符串。支持同时搜索多个关键词,每个关键词独立返回结果。适合一次性查询大量字符串是否已在平台录入。',
217
+ {
218
+ product_id: z.string().describe('产品ID,先调用 list-products 查询'),
219
+ words: z.array(z.string()).describe('搜索关键词数组(中文、英文或 key 名)'),
220
+ version_id: z.string().optional().describe('版本ID,不传则搜索所有版本'),
221
+ fuzzy: z.string().optional().default('0').describe('1=模糊匹配,0=精确匹配(默认)'),
222
+ page_size: z.string().optional().default('20').describe('每个关键词的每页条数,最大100'),
223
+ },
224
+ async ({ product_id, words, version_id, fuzzy, page_size }) => {
225
+ const authErr = await requireAuth()
226
+ if (authErr) return { content: [{ type: 'text', text: authErr }] }
227
+
228
+ const results: string[] = []
229
+ let foundCount = 0
230
+ let notFoundCount = 0
231
+
232
+ // 并发搜索所有关键词
233
+ const tasks = words.map(async (word) => {
234
+ const params: Record<string, string> = {
235
+ product_id, word, fuzzy: fuzzy || '0', page: '1', page_size: page_size || '20',
236
+ }
237
+ if (version_id) params.version_id = version_id
238
+
239
+ try {
240
+ const data = await client.post('/language/mcp-language/get-string-search', params)
241
+ if (data.errno !== 0) {
242
+ return { word, found: false, output: `❌ "${word}" — 查询失败: ${data.message || '未知错误'}` }
243
+ }
244
+
245
+ const versions = Array.isArray(data.data) ? data.data : (data.data?.list || [])
246
+ if (versions.length === 0) {
247
+ return { word, found: false, output: `❌ "${word}" — 未找到` }
248
+ }
249
+
250
+ let output = `✅ "${word}" — ${versions.length} 个版本匹配:\n`
251
+ for (const version of versions) {
252
+ const strings = version.ar_string || version.strings || []
253
+ for (const str of strings) {
254
+ const keys = str.keys
255
+ ? Object.entries(str.keys).map(([p, k]) => `platform_${p}: ${k}`).join(', ')
256
+ : '无'
257
+ const zhCN = str.values?.['1'] || str.values?.['0'] || ''
258
+ const enUS = str.values?.['2'] || str.values?.['0'] || ''
259
+ const zhTW = str.values?.['7'] || ''
260
+ output += ` - key: ${keys} | 中: ${zhCN} | 英: ${enUS}`
261
+ if (zhTW) output += ` | 繁: ${zhTW}`
262
+ output += ` (v${version.version_number})\n`
263
+ }
264
+ }
265
+ return { word, found: true, output }
266
+ } catch (err: any) {
267
+ return { word, found: false, output: `❌ "${word}" — 请求异常: ${err.message}` }
268
+ }
269
+ })
270
+
271
+ const settled = await Promise.all(tasks)
272
+ for (const r of settled) {
273
+ results.push(r.output)
274
+ if (r.found) foundCount++
275
+ else notFoundCount++
276
+ }
277
+
278
+ let output = `## 批量搜索结果\n\n`
279
+ output += `共搜索 ${words.length} 个关键词: ✅ ${foundCount} 个找到, ❌ ${notFoundCount} 个未找到\n\n`
280
+ output += results.join('\n')
281
+
282
+ return { content: [{ type: 'text', text: output }] }
283
+ }
284
+ )
285
+
213
286
  // Tool: export-string
214
287
  server.tool(
215
288
  'export-string',
package/src/utils.ts CHANGED
@@ -10,36 +10,59 @@ export const PLATFORM_MAP: Record<number, string> = {
10
10
  9: 'PC',
11
11
  }
12
12
 
13
- export const LANGUAGE_LOCALE_MAP: Record<string, string> = {
14
- '1': 'ZhCn',
15
- '2': 'EnUs',
16
- '3': 'JaJp',
17
- '4': 'KoKr',
18
- '5': 'FrFr',
19
- '6': 'DeDe',
20
- '7': 'ZhTw',
21
- '8': 'PtBr',
22
- '9': 'EsEs',
23
- '10': 'ItIt',
24
- '11': 'RuRu',
25
- '12': 'TrTr',
26
- '13': 'ArSa',
27
- '14': 'ThTh',
28
- '15': 'PlPl',
29
- '16': 'ViVn',
30
- '17': 'InId',
31
- '19': 'MsMy',
32
- '20': 'NlNl',
33
- '22': 'HiDi',
34
- '23': 'BnBd',
35
- '24': 'CsCs',
36
- '25': 'SkSk',
37
- '26': 'FilPh',
38
- '27': 'ElEl',
39
- '28': 'PtPt',
40
- '29': 'RoRo',
13
+ // 语言ID locale 字符串映射(从 operate-main AppConfMacro::$ar_language_map 复制)
14
+ const LANGUAGE_STRING_MAP: Record<number, string> = {
15
+ 1: 'zh-cn', 2: 'en-us', 3: 'de-de', 4: 'fr-fr',
16
+ 5: 'ja-jp', 6: 'ko-kr', 7: 'zh-tw', 8: 'es-es',
17
+ 9: 'ru-ru', 10: 'sk-sk', 11: 'cs-cs', 12: 'pt-pt',
18
+ 13: 'pl-pl', 14: 'it-it', 15: 'tr-tr', 16: 'ar-ar',
19
+ 17: 'pt-br', 19: 'ag-ag', 20: 'sm-sm', 22: 'id-id',
20
+ 23: 'th-th', 24: 'fil-ph', 25: 'ms-my', 26: 'vi-vn',
21
+ 27: 'bn-bd', 28: 'fa-ir', 29: 'hi-in', 30: 'nl-nl',
22
+ 31: 'el-gr', 32: 'hu-hu', 33: 'uk-ua', 34: 'no-no',
23
+ 35: 'da-dk', 36: 'ur-pk', 37: 'hr-hr', 38: 'hy-am',
24
+ 39: 'bg-bg', 40: 'si-lk', 41: 'is-is', 42: 'kk-kz',
25
+ 43: 'sr-rs', 44: 'ne-np', 45: 'lv-lv', 46: 'sl-si',
26
+ 47: 'sw-ke', 48: 'ka-ge', 49: 'et-ee', 50: 'sv-se',
27
+ 51: 'be-by', 52: 'zu-za', 53: 'lt-lt', 54: 'my-mm',
28
+ 55: 'ro-ro', 56: 'lo-la', 57: 'mn-mn', 58: 'az-az',
29
+ 59: 'am-et', 60: 'sq-al', 61: 'mk-mk', 62: 'gl-es',
30
+ 63: 'ca-es', 64: 'af-za', 65: 'kn-in', 66: 'gu-in',
31
+ 67: 'eu-es', 68: 'iw-il', 69: 'pa-in', 70: 'ky-kg',
32
+ 71: 'te-in', 72: 'ta-in', 73: 'rm-ch', 74: 'mr-in',
33
+ 75: 'ml-in', 76: 'km-kh', 77: 'bs-ba', 78: 'lb-lu',
34
+ 79: 'rw-rw', 80: 'mt-mt', 81: 'uz-uz', 82: 'ga-ie',
41
35
  }
42
36
 
37
+ // locale 字符串 → PascalCase 文件名(从 operate-main MacroExport::filter2 复制)
38
+ function toLocaleName(localeStr: string): string {
39
+ return localeStr.split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join('')
40
+ }
41
+
42
+ // 特殊映射:项目 locale 文件名与 filter2 输出不一致的语言
43
+ const LOCALE_NAME_OVERRIDE: Record<number, string> = {
44
+ 16: 'ArSa', // filter2: ArAr → 实际: ArSa
45
+ 20: 'FiFi', // filter2: SmSm → 实际: FiFi (芬兰语)
46
+ 22: 'InId', // filter2: IdId → 实际: InId (印尼语)
47
+ 23: 'Th', // filter2: ThTh → 实际: Th (泰语)
48
+ 29: 'HiDi', // filter2: HiIn → 实际: HiDi (印地语)
49
+ 31: 'ElEl', // filter2: ElGr → 实际: ElEl (希腊语)
50
+ }
51
+
52
+ // 获取 locale 文件名(优先用 override,否则 filter2 动态生成)
53
+ export function getLocaleName(langId: string | number): string | null {
54
+ const id = Number(langId)
55
+ if (LOCALE_NAME_OVERRIDE[id]) return LOCALE_NAME_OVERRIDE[id]
56
+ const localeStr = LANGUAGE_STRING_MAP[id]
57
+ if (!localeStr) return null
58
+ return toLocaleName(localeStr)
59
+ }
60
+
61
+ // 兼容旧接口:动态生成完整映射表
62
+ export const LANGUAGE_LOCALE_MAP: Record<string, string> = Object.fromEntries(
63
+ Object.keys(LANGUAGE_STRING_MAP).map(id => [String(id), getLocaleName(id)!]).filter(([, v]) => v)
64
+ )
65
+
43
66
  // --- Helpers ---
44
67
 
45
68
  export function fixPlaceholders(value: string): string {