@langapi/mcp-server 1.1.0 → 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 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 --transport stdio langapi \
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", // optional
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, cachedContent: Record<string, string> | null, deltaContent: KeyValue[], isMissingLang: boolean, hasMissingKeys: boolean, skipKeys: Set<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;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CACvC,UAAU,EAAE,mBAAmB,EAC/B,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,EAC5C,YAAY,EAAE,QAAQ,EAAE,EACxB,aAAa,EAAE,OAAO,EACtB,cAAc,EAAE,OAAO,EACvB,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,GACpB;IAAE,aAAa,EAAE,QAAQ,EAAE,CAAC;IAAC,WAAW,EAAE,MAAM,EAAE,CAAA;CAAE,CA+BtD;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"}
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, cachedContent, deltaContent, isMissingLang, hasMissingKeys, skipKeys) {
66
+ export function getXCStringsContentToSync(sourceData, targetLang, isMissingLang, skipKeys, hardSync = false) {
69
67
  let contentToSync;
70
- if (isMissingLang) {
71
- // New language: sync all source content
68
+ if (hardSync || isMissingLang) {
69
+ // Hard sync or new language: sync all source content
72
70
  contentToSync = sourceData.flatContent;
73
71
  }
74
- else if (!cachedContent || hasMissingKeys) {
75
- // No cache OR target has missing translations: sync missing keys
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":"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"}
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
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;GAWG"}
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;;;;;;;;;;;GAWG;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"}
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"}
@@ -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;AAMpE;;GAEG;AACH,wBAAgB,YAAY,IAAI,SAAS,CAaxC"}
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
@@ -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;AACxE,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD;;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;IACjC,eAAe,CAAC,MAAM,CAAC,CAAC;IAExB,OAAO,MAAM,CAAC;AAChB,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;AAwDzE,QAAA,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;EAqB1B,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,CAk1BhE"}
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 content to sync AND no missing languages AND no missing files, return early
319
- if (localDelta.contentToSync.length === 0 && missingLanguages.length === 0 && languagesWithMissingFiles.length === 0) {
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, cachedContent, fileKeysInDelta, isMissingLang, targetHasMissingKeys, skipSet);
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
- // New language or missing file: sync all keys from this source file
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
- // Has cache: use delta, but only keys from this file
562
- contentToSync = fileKeysInDelta;
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 allKeysToSync = new Set();
615
+ const fileKeysToSync = new Set();
588
616
  for (const content of langContentMap.values()) {
589
617
  for (const item of content) {
590
- allKeysToSync.add(item.key);
618
+ fileKeysToSync.add(item.key);
591
619
  }
592
620
  }
593
621
  // Get source content for these keys
594
- const contentForApi = sourceFileData.flatContent.filter((item) => allKeysToSync.has(item.key));
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, result.translations, sourceFileData.stringsContent?.comments || new Map(), sourceFileKeys);
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, result.translations);
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, result.translations, sourceFileData.stringsDictEntries, sourceFileKeys);
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, result.translations, sourceFileData.arbMetadata, sourceFileKeys, targetLang);
778
+ mergedContent = mergeArbContent(existingContent, translationsForLang, sourceFileData.arbMetadata, sourceFileKeys, targetLang);
744
779
  }
745
780
  else {
746
781
  // Regular JSON: merge and remove extra keys
747
- const newTranslations = unflattenJson(result.translations);
748
- mergedContent = deepMerge(existingContent, newTranslations);
749
- mergedContent = removeExtraKeys(mergedContent, sourceFileKeys);
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: localDelta.newKeys,
782
- changed_keys: localDelta.changedKeys,
783
- total_keys_to_sync: localDelta.contentToSync.length,
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: ${localDelta.contentToSync.length} keys to sync across ${sourceFilesData.length} file(s), ${totalCreditsUsed} credits required. Run with dry_run=false to execute.`,
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) }],