@langapi/mcp-server 1.1.1 → 1.1.2
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 +6 -42
- package/dist/handlers/xcstrings-sync-handler.d.ts +2 -4
- package/dist/handlers/xcstrings-sync-handler.d.ts.map +1 -1
- package/dist/handlers/xcstrings-sync-handler.js +6 -12
- package/dist/handlers/xcstrings-sync-handler.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +0 -2
- package/dist/server.js.map +1 -1
- package/dist/tools/sync-translations.d.ts +3 -0
- package/dist/tools/sync-translations.d.ts.map +1 -1
- package/dist/tools/sync-translations.js +92 -43
- package/dist/tools/sync-translations.js.map +1 -1
- package/dist/utils/format-preserve.d.ts +5 -1
- package/dist/utils/format-preserve.d.ts.map +1 -1
- package/dist/utils/format-preserve.js +109 -4
- package/dist/utils/format-preserve.js.map +1 -1
- package/package.json +1 -1
- package/dist/tools/get-diff.d.ts +0 -23
- package/dist/tools/get-diff.js +0 -88
- package/dist/tools/get-diff.js.map +0 -1
- package/dist/utils/sync-cache.d.ts +0 -40
- package/dist/utils/sync-cache.js +0 -140
- package/dist/utils/sync-cache.js.map +0 -1
- package/dist/utils/sync-cache.test.js +0 -205
- package/dist/utils/sync-cache.test.js.map +0 -1
package/README.md
CHANGED
|
@@ -20,12 +20,13 @@ This package enables AI assistants like Claude, Cursor, and VS Code extensions t
|
|
|
20
20
|
|
|
21
21
|
## Features
|
|
22
22
|
|
|
23
|
-
- **Locale Detection**: Automatically detect i18n framework (next-intl, i18next, react-intl) and locale files
|
|
23
|
+
- **Locale Detection**: Automatically detect i18n framework (next-intl, i18next, react-intl, iOS/macOS) and locale files
|
|
24
24
|
- **Translation Status**: Compare source and target locales to find missing translations
|
|
25
25
|
- **Sync Translations**: Translate missing keys via LangAPI with credit-based billing
|
|
26
26
|
- **Dry Run Mode**: Preview changes and costs before syncing (enabled by default)
|
|
27
27
|
- **Format Preservation**: Maintains JSON formatting when writing translated files
|
|
28
28
|
- **Delta Detection**: Only translate new/changed keys, saving up to 90% on costs
|
|
29
|
+
- **Apple Localization**: Support for iOS/macOS `.strings`, `.xcstrings`, and `.stringsdict` files
|
|
29
30
|
|
|
30
31
|
## Installation
|
|
31
32
|
|
|
@@ -76,14 +77,10 @@ After editing, **restart Claude Desktop** for changes to take effect.
|
|
|
76
77
|
|
|
77
78
|
```bash
|
|
78
79
|
# Add to current project (stored in .mcp.json)
|
|
79
|
-
claude mcp add
|
|
80
|
+
claude mcp add langapi \
|
|
80
81
|
--env LANGAPI_API_KEY=your-api-key-here \
|
|
81
|
-
-- npx @langapi/mcp-server
|
|
82
|
+
-- npx -y @langapi/mcp-server
|
|
82
83
|
|
|
83
|
-
# Or add globally for all projects (stored in ~/.claude.json)
|
|
84
|
-
claude mcp add --transport stdio langapi --scope user \
|
|
85
|
-
--env LANGAPI_API_KEY=your-api-key-here \
|
|
86
|
-
-- npx @langapi/mcp-server
|
|
87
84
|
```
|
|
88
85
|
|
|
89
86
|
**Option 2: Project-level config** (recommended for teams)
|
|
@@ -301,8 +298,7 @@ Compare source locale against targets to identify missing keys and estimate cost
|
|
|
301
298
|
{
|
|
302
299
|
"source_lang": "en",
|
|
303
300
|
"target_langs": ["de", "fr"], // optional, all non-source by default
|
|
304
|
-
"project_path": "/path/to/project"
|
|
305
|
-
"app_id": "your-app-id" // optional, for accurate cost estimate
|
|
301
|
+
"project_path": "/path/to/project" // optional
|
|
306
302
|
}
|
|
307
303
|
```
|
|
308
304
|
|
|
@@ -380,37 +376,6 @@ Sync translations via the LangAPI API. **Default is dry_run=true for safety.**
|
|
|
380
376
|
}
|
|
381
377
|
```
|
|
382
378
|
|
|
383
|
-
### `get_diff`
|
|
384
|
-
|
|
385
|
-
Compare current source locale against the last synced version.
|
|
386
|
-
|
|
387
|
-
**Input:**
|
|
388
|
-
```json
|
|
389
|
-
{
|
|
390
|
-
"source_lang": "en",
|
|
391
|
-
"project_path": "/path/to/project" // optional
|
|
392
|
-
}
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
**Output:**
|
|
396
|
-
```json
|
|
397
|
-
{
|
|
398
|
-
"has_changes": true,
|
|
399
|
-
"summary": {
|
|
400
|
-
"new_keys": 3,
|
|
401
|
-
"changed_keys": 1,
|
|
402
|
-
"removed_keys": 0,
|
|
403
|
-
"unchanged_keys": 146
|
|
404
|
-
},
|
|
405
|
-
"diff": {
|
|
406
|
-
"new": ["feature.title", "feature.description", "feature.cta"],
|
|
407
|
-
"changed": ["home.welcome"],
|
|
408
|
-
"removed": [],
|
|
409
|
-
"unchanged": ["..."]
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
```
|
|
413
|
-
|
|
414
379
|
---
|
|
415
380
|
|
|
416
381
|
## Prompt Examples
|
|
@@ -459,8 +424,6 @@ Compare current source locale against the last synced version.
|
|
|
459
424
|
### Advanced Operations
|
|
460
425
|
|
|
461
426
|
```
|
|
462
|
-
"What changed since my last sync?"
|
|
463
|
-
"Show diff between current and last synced version"
|
|
464
427
|
"Are there any extra keys in German that aren't in English?"
|
|
465
428
|
"Skip the settings.* keys when syncing"
|
|
466
429
|
"Only sync the home.* and nav.* keys"
|
|
@@ -502,6 +465,7 @@ The server automatically detects these i18n frameworks:
|
|
|
502
465
|
| **next-intl** | `messages/*.json`, `locales/*.json` | `i18n.ts`, `next.config.js` |
|
|
503
466
|
| **i18next** | `public/locales/*/*.json`, `locales/*/*.json` | `i18next.config.js`, `i18n.js` |
|
|
504
467
|
| **react-intl** | `src/lang/*.json`, `lang/*.json` | `src/i18n.ts` |
|
|
468
|
+
| **iOS/macOS** | `.strings`, `.xcstrings`, `.stringsdict` | `Info.plist` |
|
|
505
469
|
| **generic** | Various common patterns | - |
|
|
506
470
|
|
|
507
471
|
---
|
|
@@ -45,14 +45,12 @@ export declare function xcstringsHasMissingKeys(xcstringsData: XCStringsFile, ta
|
|
|
45
45
|
*
|
|
46
46
|
* @param sourceData Parsed xcstrings source data
|
|
47
47
|
* @param targetLang Target language code
|
|
48
|
-
* @param cachedContent Cached content from previous sync (null if no cache)
|
|
49
|
-
* @param deltaContent Content that changed since last sync
|
|
50
48
|
* @param isMissingLang Whether the target language is completely missing
|
|
51
|
-
* @param hasMissingKeys Whether target has any missing keys
|
|
52
49
|
* @param skipKeys Set of keys to skip for this language
|
|
50
|
+
* @param hardSync If true, re-translate all keys even if target already has translations
|
|
53
51
|
* @returns Content to sync (after filtering skip keys)
|
|
54
52
|
*/
|
|
55
|
-
export declare function getXCStringsContentToSync(sourceData: XCStringsSourceData, targetLang: string,
|
|
53
|
+
export declare function getXCStringsContentToSync(sourceData: XCStringsSourceData, targetLang: string, isMissingLang: boolean, skipKeys: Set<string>, hardSync?: boolean): {
|
|
56
54
|
contentToSync: KeyValue[];
|
|
57
55
|
skippedKeys: string[];
|
|
58
56
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xcstrings-sync-handler.d.ts","sourceRoot":"","sources":["../../src/handlers/xcstrings-sync-handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AAEtC;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,UAAU,CAAC;IACjB,WAAW,EAAE,QAAQ,EAAE,CAAC;IACxB,aAAa,EAAE,aAAa,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,MAAM,GACd,mBAAmB,GAAG,IAAI,CAW5B;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,aAAa,EAAE,aAAa,EAC5B,UAAU,EAAE,MAAM,GACjB,GAAG,CAAC,MAAM,CAAC,CAWb;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,aAAa,EAAE,aAAa,EAC5B,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,QAAQ,EAAE,GACxB,OAAO,CAIT;AAED
|
|
1
|
+
{"version":3,"file":"xcstrings-sync-handler.d.ts","sourceRoot":"","sources":["../../src/handlers/xcstrings-sync-handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAKL,KAAK,aAAa,EACnB,MAAM,8BAA8B,CAAC;AAEtC;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,UAAU,CAAC;IACjB,WAAW,EAAE,QAAQ,EAAE,CAAC;IACxB,aAAa,EAAE,aAAa,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE,MAAM,GACd,mBAAmB,GAAG,IAAI,CAW5B;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,aAAa,EAAE,aAAa,EAC5B,UAAU,EAAE,MAAM,GACjB,GAAG,CAAC,MAAM,CAAC,CAWb;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,aAAa,EAAE,aAAa,EAC5B,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,QAAQ,EAAE,GACxB,OAAO,CAIT;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,mBAAmB,EAC/B,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,OAAO,EACtB,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,EACrB,QAAQ,GAAE,OAAe,GACxB;IAAE,aAAa,EAAE,QAAQ,EAAE,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAA;CAAE,CA4BtD;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,0BAA0B,CAC9C,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,aAAa,EAC1B,UAAU,EAAE,MAAM,EAClB,YAAY,EAAE,QAAQ,EAAE,GACvB,OAAO,CAAC,aAAa,CAAC,CAqBxB"}
|
|
@@ -58,28 +58,22 @@ export function xcstringsHasMissingKeys(xcstringsData, targetLang, sourceContent
|
|
|
58
58
|
*
|
|
59
59
|
* @param sourceData Parsed xcstrings source data
|
|
60
60
|
* @param targetLang Target language code
|
|
61
|
-
* @param cachedContent Cached content from previous sync (null if no cache)
|
|
62
|
-
* @param deltaContent Content that changed since last sync
|
|
63
61
|
* @param isMissingLang Whether the target language is completely missing
|
|
64
|
-
* @param hasMissingKeys Whether target has any missing keys
|
|
65
62
|
* @param skipKeys Set of keys to skip for this language
|
|
63
|
+
* @param hardSync If true, re-translate all keys even if target already has translations
|
|
66
64
|
* @returns Content to sync (after filtering skip keys)
|
|
67
65
|
*/
|
|
68
|
-
export function getXCStringsContentToSync(sourceData, targetLang,
|
|
66
|
+
export function getXCStringsContentToSync(sourceData, targetLang, isMissingLang, skipKeys, hardSync = false) {
|
|
69
67
|
let contentToSync;
|
|
70
|
-
if (isMissingLang) {
|
|
71
|
-
//
|
|
68
|
+
if (hardSync || isMissingLang) {
|
|
69
|
+
// Hard sync or new language: sync all source content
|
|
72
70
|
contentToSync = sourceData.flatContent;
|
|
73
71
|
}
|
|
74
|
-
else
|
|
75
|
-
//
|
|
72
|
+
else {
|
|
73
|
+
// Existing language: sync only keys missing from target
|
|
76
74
|
const existingTargetKeys = getXCStringsExistingKeys(sourceData.xcstringsData, targetLang);
|
|
77
75
|
contentToSync = sourceData.flatContent.filter((item) => !existingTargetKeys.has(item.key));
|
|
78
76
|
}
|
|
79
|
-
else {
|
|
80
|
-
// Has cache: use delta
|
|
81
|
-
contentToSync = deltaContent;
|
|
82
|
-
}
|
|
83
77
|
// Apply skip_keys filter
|
|
84
78
|
const skippedKeys = [];
|
|
85
79
|
const filteredContent = contentToSync.filter((item) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"xcstrings-sync-handler.js","sourceRoot":"","sources":["../../src/handlers/xcstrings-sync-handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAGlD,OAAO,EACL,qBAAqB,EACrB,qBAAqB,EACrB,2BAA2B,EAC3B,0BAA0B,GAE3B,MAAM,8BAA8B,CAAC;AAWtC;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAClC,IAAgB,EAChB,OAAe;IAEf,MAAM,MAAM,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAC;IAC9C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,IAAI;QACJ,WAAW,EAAE,MAAM,CAAC,OAAO;QAC3B,aAAa,EAAE,MAAM,CAAC,QAAQ;KAC/B,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,wBAAwB,CACtC,aAA4B,EAC5B,UAAkB;IAElB,MAAM,aAAa,GAAG,0BAA0B,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;IAC5E,MAAM,YAAY,GAAG,IAAI,GAAG,EAAU,CAAC;IAEvC,KAAK,MAAM,KAAK,IAAI,aAAa,EAAE,CAAC;QAClC,IAAI,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;YAC7C,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;IAED,OAAO,YAAY,CAAC;AACtB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,uBAAuB,CACrC,aAA4B,EAC5B,UAAkB,EAClB,aAAyB;IAEzB,MAAM,kBAAkB,GAAG,wBAAwB,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC;IAE/E,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AACzE,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"xcstrings-sync-handler.js","sourceRoot":"","sources":["../../src/handlers/xcstrings-sync-handler.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* - list_local_locales: Scan project for locale files
|
|
10
10
|
* - get_translation_status: Compare source vs target locales
|
|
11
11
|
* - sync_translations: Sync translations via LangAPI API
|
|
12
|
-
* - get_diff: Compare source against sync cache for delta detection
|
|
13
12
|
*/
|
|
14
13
|
export {};
|
|
15
14
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;GAUG"}
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* - list_local_locales: Scan project for locale files
|
|
10
10
|
* - get_translation_status: Compare source vs target locales
|
|
11
11
|
* - sync_translations: Sync translations via LangAPI API
|
|
12
|
-
* - get_diff: Compare source against sync cache for delta detection
|
|
13
12
|
*/
|
|
14
13
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
14
|
import { createServer } from "./server.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAE7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,qCAAqC,EAAE,KAAK,CAAC,CAAC;IAC5D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE;;GAEG;AACH,wBAAgB,YAAY,IAAI,SAAS,CAYxC"}
|
package/dist/server.js
CHANGED
|
@@ -5,7 +5,6 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
5
5
|
import { registerListLocalLocales } from "./tools/list-local-locales.js";
|
|
6
6
|
import { registerGetTranslationStatus } from "./tools/get-translation-status.js";
|
|
7
7
|
import { registerSyncTranslations } from "./tools/sync-translations.js";
|
|
8
|
-
import { registerGetDiff } from "./tools/get-diff.js";
|
|
9
8
|
/**
|
|
10
9
|
* Create and configure the MCP server
|
|
11
10
|
*/
|
|
@@ -18,7 +17,6 @@ export function createServer() {
|
|
|
18
17
|
registerListLocalLocales(server);
|
|
19
18
|
registerGetTranslationStatus(server);
|
|
20
19
|
registerSyncTranslations(server);
|
|
21
|
-
registerGetDiff(server);
|
|
22
20
|
return server;
|
|
23
21
|
}
|
|
24
22
|
//# sourceMappingURL=server.js.map
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AACzE,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAExE;;GAEG;AACH,MAAM,UAAU,YAAY;IAC1B,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,oBAAoB;QAC1B,OAAO,EAAE,OAAO;KACjB,CAAC,CAAC;IAEH,qBAAqB;IACrB,wBAAwB,CAAC,MAAM,CAAC,CAAC;IACjC,4BAA4B,CAAC,MAAM,CAAC,CAAC;IACrC,wBAAwB,CAAC,MAAM,CAAC,CAAC;IAEjC,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -11,11 +11,13 @@ declare const SyncTranslationsSchema: z.ZodObject<{
|
|
|
11
11
|
project_path: z.ZodOptional<z.ZodString>;
|
|
12
12
|
write_to_files: z.ZodDefault<z.ZodBoolean>;
|
|
13
13
|
skip_keys: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString, "many">>>;
|
|
14
|
+
hard_sync: z.ZodDefault<z.ZodBoolean>;
|
|
14
15
|
}, "strip", z.ZodTypeAny, {
|
|
15
16
|
source_lang: string;
|
|
16
17
|
target_langs: string[];
|
|
17
18
|
dry_run: boolean;
|
|
18
19
|
write_to_files: boolean;
|
|
20
|
+
hard_sync: boolean;
|
|
19
21
|
project_path?: string | undefined;
|
|
20
22
|
skip_keys?: Record<string, string[]> | undefined;
|
|
21
23
|
}, {
|
|
@@ -25,6 +27,7 @@ declare const SyncTranslationsSchema: z.ZodObject<{
|
|
|
25
27
|
dry_run?: boolean | undefined;
|
|
26
28
|
write_to_files?: boolean | undefined;
|
|
27
29
|
skip_keys?: Record<string, string[]> | undefined;
|
|
30
|
+
hard_sync?: boolean | undefined;
|
|
28
31
|
}>;
|
|
29
32
|
export type SyncTranslationsInput = z.infer<typeof SyncTranslationsSchema>;
|
|
30
33
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-translations.d.ts","sourceRoot":"","sources":["../../src/tools/sync-translations.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"sync-translations.d.ts","sourceRoot":"","sources":["../../src/tools/sync-translations.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAmDzE,QAAA,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;EA2B1B,CAAC;AAEH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AA8N3E;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA83BhE"}
|
|
@@ -12,7 +12,6 @@ import { parseJsonWithFormat, stringifyWithFormat, } from "../utils/format-prese
|
|
|
12
12
|
import { LangAPIClient } from "../api/client.js";
|
|
13
13
|
import { isApiKeyConfigured } from "../config/env.js";
|
|
14
14
|
import { languageCodeSchema, languageCodesArraySchema, isPathWithinProject, } from "../utils/validation.js";
|
|
15
|
-
import { readSyncCache, writeSyncCache, detectLocalDelta, } from "../utils/sync-cache.js";
|
|
16
15
|
import { detectAppleFileType, isXCStringsFile, computeAppleLprojTargetPath, } from "../utils/apple-common.js";
|
|
17
16
|
import { parseStringsContent, mergeStringsContent, } from "../utils/strings-parser.js";
|
|
18
17
|
import { parseXCStringsSource, xcstringsHasMissingKeys, getXCStringsContentToSync, writeXCStringsTranslations, } from "../handlers/xcstrings-sync-handler.js";
|
|
@@ -37,6 +36,10 @@ const SyncTranslationsSchema = z.object({
|
|
|
37
36
|
.record(z.string(), z.array(z.string()))
|
|
38
37
|
.optional()
|
|
39
38
|
.describe("Keys to skip per language, e.g., { 'fr': ['subtitle', 'brand'] }"),
|
|
39
|
+
hard_sync: z
|
|
40
|
+
.boolean()
|
|
41
|
+
.default(false)
|
|
42
|
+
.describe("If true, re-translate all changed keys even if target already has translations. If false (default), only translate keys missing in target."),
|
|
40
43
|
});
|
|
41
44
|
/**
|
|
42
45
|
* Get keys to skip for a specific language
|
|
@@ -200,7 +203,7 @@ export function registerSyncTranslations(server) {
|
|
|
200
203
|
file,
|
|
201
204
|
content: {},
|
|
202
205
|
flatContent: stringsContent.entries,
|
|
203
|
-
format: { indent: " ", trailingNewline: true },
|
|
206
|
+
format: { indent: " ", trailingNewline: true, keyStructure: "flat" },
|
|
204
207
|
appleType: "strings",
|
|
205
208
|
stringsContent,
|
|
206
209
|
});
|
|
@@ -213,7 +216,7 @@ export function registerSyncTranslations(server) {
|
|
|
213
216
|
file: parsed.file,
|
|
214
217
|
content: {},
|
|
215
218
|
flatContent: parsed.flatContent,
|
|
216
|
-
format: { indent: " ", trailingNewline: true },
|
|
219
|
+
format: { indent: " ", trailingNewline: true, keyStructure: "flat" },
|
|
217
220
|
appleType: "xcstrings",
|
|
218
221
|
xcstringsData: parsed.xcstringsData,
|
|
219
222
|
});
|
|
@@ -228,7 +231,7 @@ export function registerSyncTranslations(server) {
|
|
|
228
231
|
file,
|
|
229
232
|
content: {},
|
|
230
233
|
flatContent,
|
|
231
|
-
format: { indent: " ", trailingNewline: true },
|
|
234
|
+
format: { indent: " ", trailingNewline: true, keyStructure: "flat" },
|
|
232
235
|
appleType: "stringsdict",
|
|
233
236
|
stringsDictEntries: stringsDictContent.entries,
|
|
234
237
|
});
|
|
@@ -263,15 +266,12 @@ export function registerSyncTranslations(server) {
|
|
|
263
266
|
// This is needed because Apple formats (.strings, .xcstrings, .stringsdict)
|
|
264
267
|
// store keys in flatContent, not in the content object
|
|
265
268
|
let flatContent = [];
|
|
266
|
-
let sourceFormat = { indent: " ", trailingNewline: true };
|
|
269
|
+
let sourceFormat = { indent: " ", trailingNewline: true, keyStructure: "nested" };
|
|
267
270
|
for (const fileData of sourceFilesData) {
|
|
268
271
|
flatContent = flatContent.concat(fileData.flatContent);
|
|
269
272
|
sourceFormat = fileData.format;
|
|
270
273
|
}
|
|
271
274
|
const sourceKeys = new Set(flatContent.map((item) => item.key));
|
|
272
|
-
// Read cache and detect local delta
|
|
273
|
-
const cachedContent = await readSyncCache(projectPath, input.source_lang);
|
|
274
|
-
const localDelta = detectLocalDelta(flatContent, cachedContent);
|
|
275
275
|
// Detect missing target languages (files that don't exist yet)
|
|
276
276
|
const missingLanguages = [];
|
|
277
277
|
const existingLanguages = [];
|
|
@@ -303,9 +303,34 @@ export function registerSyncTranslations(server) {
|
|
|
303
303
|
if (!targetFilePath || targetFilePath === sourceFileData.file.path) {
|
|
304
304
|
continue;
|
|
305
305
|
}
|
|
306
|
-
// Check if file exists
|
|
306
|
+
// Check if file exists and has all source keys
|
|
307
307
|
try {
|
|
308
|
-
await readFile(targetFilePath, "utf-8");
|
|
308
|
+
const targetContent = await readFile(targetFilePath, "utf-8");
|
|
309
|
+
const sourceFileKeys = new Set(sourceFileData.flatContent.map((item) => item.key));
|
|
310
|
+
let existingKeys = new Set();
|
|
311
|
+
// Use appropriate parser based on source file type
|
|
312
|
+
if (sourceFileData.appleType === "strings") {
|
|
313
|
+
const parsed = parseStringsContent(targetContent);
|
|
314
|
+
existingKeys = new Set(parsed.entries.filter(e => e.value && e.value.trim() !== "").map(e => e.key));
|
|
315
|
+
}
|
|
316
|
+
else if (sourceFileData.appleType === "stringsdict") {
|
|
317
|
+
const parsed = parseStringsDictContent(targetContent);
|
|
318
|
+
if (parsed) {
|
|
319
|
+
existingKeys = new Set(parsed.entries.map(e => e.key));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
// JSON/ARB files
|
|
324
|
+
const parsed = parseJsonWithFormat(targetContent);
|
|
325
|
+
if (parsed) {
|
|
326
|
+
const flatTarget = flattenJson(parsed.data);
|
|
327
|
+
existingKeys = new Set(flatTarget.map((item) => item.key));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const hasMissingKeys = [...sourceFileKeys].some((key) => !existingKeys.has(key));
|
|
331
|
+
if (hasMissingKeys && !languagesWithMissingFiles.includes(targetLang)) {
|
|
332
|
+
languagesWithMissingFiles.push(targetLang);
|
|
333
|
+
}
|
|
309
334
|
}
|
|
310
335
|
catch {
|
|
311
336
|
// File doesn't exist - this language has missing files
|
|
@@ -315,8 +340,9 @@ export function registerSyncTranslations(server) {
|
|
|
315
340
|
}
|
|
316
341
|
}
|
|
317
342
|
}
|
|
318
|
-
// If no
|
|
319
|
-
|
|
343
|
+
// If no missing languages AND no languages with missing files, return early
|
|
344
|
+
// (This means all target files exist and have all source keys)
|
|
345
|
+
if (missingLanguages.length === 0 && languagesWithMissingFiles.length === 0) {
|
|
320
346
|
if (input.dry_run) {
|
|
321
347
|
// Check for extra keys in target files even in dry_run mode
|
|
322
348
|
let totalExtraKeys = 0;
|
|
@@ -426,7 +452,6 @@ export function registerSyncTranslations(server) {
|
|
|
426
452
|
results.push({ language: targetLang, translated_count: 0, file_written: null });
|
|
427
453
|
}
|
|
428
454
|
}
|
|
429
|
-
await writeSyncCache(projectPath, input.source_lang, flatContent);
|
|
430
455
|
const filesCleanedCount = results.filter((r) => r.keys_removed && r.keys_removed > 0).length;
|
|
431
456
|
const totalKeysRemoved = results.reduce((sum, r) => sum + (r.keys_removed || 0), 0);
|
|
432
457
|
const output = {
|
|
@@ -471,11 +496,11 @@ export function registerSyncTranslations(server) {
|
|
|
471
496
|
// Track completed files for partial error reporting
|
|
472
497
|
const completedFiles = [];
|
|
473
498
|
const allFilesWritten = [];
|
|
499
|
+
// Track all unique keys being synced (for dry_run response)
|
|
500
|
+
const allKeysToSync = new Set();
|
|
474
501
|
// Process each source file
|
|
475
502
|
for (const sourceFileData of sourceFilesData) {
|
|
476
503
|
const sourceFileKeys = new Set(sourceFileData.flatContent.map((item) => item.key));
|
|
477
|
-
// Filter delta content to only keys in this source file
|
|
478
|
-
const fileKeysInDelta = localDelta.contentToSync.filter((item) => sourceFileKeys.has(item.key));
|
|
479
504
|
// Determine which languages need this file's translations
|
|
480
505
|
// and what content to sync per language
|
|
481
506
|
const langContentMap = new Map();
|
|
@@ -485,9 +510,8 @@ export function registerSyncTranslations(server) {
|
|
|
485
510
|
// Handle xcstrings files specially (same file for all languages)
|
|
486
511
|
if (sourceFileData.appleType === "xcstrings" && sourceFileData.xcstringsData) {
|
|
487
512
|
const isMissingLang = missingLanguages.includes(targetLang);
|
|
488
|
-
const targetHasMissingKeys = languagesWithMissingFiles.includes(targetLang);
|
|
489
513
|
const skipSet = getSkipKeysForLang(input.skip_keys, targetLang);
|
|
490
|
-
const { contentToSync, skippedKeys } = getXCStringsContentToSync({ file: sourceFileData.file, flatContent: sourceFileData.flatContent, xcstringsData: sourceFileData.xcstringsData }, targetLang,
|
|
514
|
+
const { contentToSync, skippedKeys } = getXCStringsContentToSync({ file: sourceFileData.file, flatContent: sourceFileData.flatContent, xcstringsData: sourceFileData.xcstringsData }, targetLang, isMissingLang, skipSet, input.hard_sync);
|
|
491
515
|
// Track skipped keys
|
|
492
516
|
if (skippedKeys.length > 0) {
|
|
493
517
|
const existing = skippedKeysReport[targetLang] || [];
|
|
@@ -495,6 +519,10 @@ export function registerSyncTranslations(server) {
|
|
|
495
519
|
}
|
|
496
520
|
if (contentToSync.length > 0) {
|
|
497
521
|
langContentMap.set(targetLang, contentToSync);
|
|
522
|
+
// Track keys for dry_run response
|
|
523
|
+
for (const item of contentToSync) {
|
|
524
|
+
allKeysToSync.add(item.key);
|
|
525
|
+
}
|
|
498
526
|
}
|
|
499
527
|
continue;
|
|
500
528
|
}
|
|
@@ -549,17 +577,13 @@ export function registerSyncTranslations(server) {
|
|
|
549
577
|
}
|
|
550
578
|
// Determine what content to sync
|
|
551
579
|
let contentToSync;
|
|
552
|
-
if (isMissingLang || !targetFileExists) {
|
|
553
|
-
//
|
|
580
|
+
if (input.hard_sync || isMissingLang || !targetFileExists) {
|
|
581
|
+
// Hard sync, new language, or missing file: sync all keys from this source file
|
|
554
582
|
contentToSync = sourceFileData.flatContent;
|
|
555
583
|
}
|
|
556
|
-
else if (!cachedContent) {
|
|
557
|
-
// No cache: sync only keys missing from target
|
|
558
|
-
contentToSync = sourceFileData.flatContent.filter((item) => !existingTargetKeys.has(item.key));
|
|
559
|
-
}
|
|
560
584
|
else {
|
|
561
|
-
//
|
|
562
|
-
contentToSync =
|
|
585
|
+
// Existing target: sync only keys missing from target
|
|
586
|
+
contentToSync = sourceFileData.flatContent.filter((item) => !existingTargetKeys.has(item.key));
|
|
563
587
|
}
|
|
564
588
|
// Apply skip_keys filter
|
|
565
589
|
const skipSet = getSkipKeysForLang(input.skip_keys, targetLang);
|
|
@@ -576,6 +600,10 @@ export function registerSyncTranslations(server) {
|
|
|
576
600
|
}
|
|
577
601
|
if (filteredContent.length > 0) {
|
|
578
602
|
langContentMap.set(targetLang, filteredContent);
|
|
603
|
+
// Track keys for dry_run response
|
|
604
|
+
for (const item of filteredContent) {
|
|
605
|
+
allKeysToSync.add(item.key);
|
|
606
|
+
}
|
|
579
607
|
}
|
|
580
608
|
}
|
|
581
609
|
// Skip API call if no languages need this file
|
|
@@ -584,14 +612,14 @@ export function registerSyncTranslations(server) {
|
|
|
584
612
|
continue;
|
|
585
613
|
}
|
|
586
614
|
// Collect all unique keys to sync for this file (union across all languages)
|
|
587
|
-
const
|
|
615
|
+
const fileKeysToSync = new Set();
|
|
588
616
|
for (const content of langContentMap.values()) {
|
|
589
617
|
for (const item of content) {
|
|
590
|
-
|
|
618
|
+
fileKeysToSync.add(item.key);
|
|
591
619
|
}
|
|
592
620
|
}
|
|
593
621
|
// Get source content for these keys
|
|
594
|
-
const contentForApi = sourceFileData.flatContent.filter((item) =>
|
|
622
|
+
const contentForApi = sourceFileData.flatContent.filter((item) => fileKeysToSync.has(item.key));
|
|
595
623
|
if (contentForApi.length === 0) {
|
|
596
624
|
completedFiles.push(sourceFileData.file.path);
|
|
597
625
|
continue;
|
|
@@ -661,6 +689,13 @@ export function registerSyncTranslations(server) {
|
|
|
661
689
|
// Write translations for each language
|
|
662
690
|
for (const result of response.results) {
|
|
663
691
|
const targetLang = result.language;
|
|
692
|
+
// Filter translations to only keys this language actually needed
|
|
693
|
+
// This prevents overwriting existing translations when syncing multiple languages
|
|
694
|
+
const keysNeededForLang = langContentMap.get(targetLang);
|
|
695
|
+
const keysNeededSet = keysNeededForLang
|
|
696
|
+
? new Set(keysNeededForLang.map(item => item.key))
|
|
697
|
+
: new Set();
|
|
698
|
+
const translationsForLang = result.translations.filter(t => keysNeededSet.has(t.key));
|
|
664
699
|
// Always compute target path from source (no namespace matching!)
|
|
665
700
|
const targetFilePath = computeTargetFilePath(sourceFileData.file.path, input.source_lang, targetLang);
|
|
666
701
|
// For xcstrings, targetFilePath equals sourceFilePath - that's OK, handle below
|
|
@@ -686,7 +721,7 @@ export function registerSyncTranslations(server) {
|
|
|
686
721
|
catch {
|
|
687
722
|
// File doesn't exist yet
|
|
688
723
|
}
|
|
689
|
-
const fileContent = mergeStringsContent(existingContent,
|
|
724
|
+
const fileContent = mergeStringsContent(existingContent, translationsForLang, sourceFileData.stringsContent?.comments || new Map(), sourceFileKeys);
|
|
690
725
|
await mkdir(dirname(resolvedPath), { recursive: true });
|
|
691
726
|
await writeFile(resolvedPath, fileContent, "utf-8");
|
|
692
727
|
allFilesWritten.push(resolvedPath);
|
|
@@ -696,7 +731,7 @@ export function registerSyncTranslations(server) {
|
|
|
696
731
|
}
|
|
697
732
|
else if (sourceFileData.appleType === "xcstrings" && sourceFileData.xcstringsData) {
|
|
698
733
|
// .xcstrings file: update in-place (single file with all languages)
|
|
699
|
-
sourceFileData.xcstringsData = await writeXCStringsTranslations(sourceFileData.file.path, sourceFileData.xcstringsData, targetLang,
|
|
734
|
+
sourceFileData.xcstringsData = await writeXCStringsTranslations(sourceFileData.file.path, sourceFileData.xcstringsData, targetLang, translationsForLang);
|
|
700
735
|
if (!allFilesWritten.includes(sourceFileData.file.path)) {
|
|
701
736
|
allFilesWritten.push(sourceFileData.file.path);
|
|
702
737
|
}
|
|
@@ -715,7 +750,7 @@ export function registerSyncTranslations(server) {
|
|
|
715
750
|
catch {
|
|
716
751
|
// File doesn't exist yet
|
|
717
752
|
}
|
|
718
|
-
const fileContent = mergeStringsDictContent(existingContent,
|
|
753
|
+
const fileContent = mergeStringsDictContent(existingContent, translationsForLang, sourceFileData.stringsDictEntries, sourceFileKeys);
|
|
719
754
|
await mkdir(dirname(resolvedPath), { recursive: true });
|
|
720
755
|
await writeFile(resolvedPath, fileContent, "utf-8");
|
|
721
756
|
allFilesWritten.push(resolvedPath);
|
|
@@ -740,13 +775,30 @@ export function registerSyncTranslations(server) {
|
|
|
740
775
|
}
|
|
741
776
|
if (isArbFile(resolvedPath) && sourceFileData.arbMetadata) {
|
|
742
777
|
// ARB file: merge with existing, preserve metadata, update locale
|
|
743
|
-
mergedContent = mergeArbContent(existingContent,
|
|
778
|
+
mergedContent = mergeArbContent(existingContent, translationsForLang, sourceFileData.arbMetadata, sourceFileKeys, targetLang);
|
|
744
779
|
}
|
|
745
780
|
else {
|
|
746
781
|
// Regular JSON: merge and remove extra keys
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
782
|
+
// Check if source uses flat keys (keys containing dots at root level)
|
|
783
|
+
if (sourceFileData.format.keyStructure === "flat") {
|
|
784
|
+
// For flat key structure, keep translations flat (don't unflatten)
|
|
785
|
+
mergedContent = { ...existingContent };
|
|
786
|
+
for (const { key, value } of translationsForLang) {
|
|
787
|
+
mergedContent[key] = value;
|
|
788
|
+
}
|
|
789
|
+
// Remove keys not in source
|
|
790
|
+
for (const key of Object.keys(mergedContent)) {
|
|
791
|
+
if (!sourceFileKeys.has(key)) {
|
|
792
|
+
delete mergedContent[key];
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
// For nested key structure, unflatten and deep merge
|
|
798
|
+
const newTranslations = unflattenJson(translationsForLang);
|
|
799
|
+
mergedContent = deepMerge(existingContent, newTranslations);
|
|
800
|
+
mergedContent = removeExtraKeys(mergedContent, sourceFileKeys);
|
|
801
|
+
}
|
|
750
802
|
}
|
|
751
803
|
// Write file
|
|
752
804
|
await mkdir(dirname(resolvedPath), { recursive: true });
|
|
@@ -768,19 +820,16 @@ export function registerSyncTranslations(server) {
|
|
|
768
820
|
completedFiles.push(sourceFileData.file.path);
|
|
769
821
|
}
|
|
770
822
|
}
|
|
771
|
-
// Update cache after all files processed
|
|
772
|
-
if (input.write_to_files && !input.dry_run) {
|
|
773
|
-
await writeSyncCache(projectPath, input.source_lang, flatContent);
|
|
774
|
-
}
|
|
775
823
|
// Build final response
|
|
776
824
|
if (input.dry_run) {
|
|
825
|
+
const keysToSyncArray = Array.from(allKeysToSync);
|
|
777
826
|
const output = {
|
|
778
827
|
success: true,
|
|
779
828
|
dry_run: true,
|
|
780
829
|
delta: {
|
|
781
|
-
new_keys:
|
|
782
|
-
changed_keys:
|
|
783
|
-
total_keys_to_sync:
|
|
830
|
+
new_keys: keysToSyncArray,
|
|
831
|
+
changed_keys: [],
|
|
832
|
+
total_keys_to_sync: keysToSyncArray.length,
|
|
784
833
|
},
|
|
785
834
|
cost: {
|
|
786
835
|
words_to_translate: totalWordsToTranslate,
|
|
@@ -788,7 +837,7 @@ export function registerSyncTranslations(server) {
|
|
|
788
837
|
current_balance: currentBalance,
|
|
789
838
|
balance_after_sync: currentBalance - totalCreditsUsed,
|
|
790
839
|
},
|
|
791
|
-
message: `Preview: ${
|
|
840
|
+
message: `Preview: ${keysToSyncArray.length} keys to sync across ${sourceFilesData.length} file(s), ${totalCreditsUsed} credits required. Run with dry_run=false to execute.`,
|
|
792
841
|
};
|
|
793
842
|
return {
|
|
794
843
|
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|