string-catalog-mcp 1.0.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.
Files changed (62) hide show
  1. package/.prettierrc +8 -0
  2. package/README.md +127 -0
  3. package/dist/index.d.ts +2 -0
  4. package/dist/index.js +23 -0
  5. package/dist/mcp/prompts/batch-translate.d.ts +2 -0
  6. package/dist/mcp/prompts/batch-translate.js +88 -0
  7. package/dist/mcp/prompts/index.d.ts +2 -0
  8. package/dist/mcp/prompts/index.js +12 -0
  9. package/dist/mcp/prompts/review-translations.d.ts +2 -0
  10. package/dist/mcp/prompts/review-translations.js +75 -0
  11. package/dist/mcp/prompts/translate-strings.d.ts +2 -0
  12. package/dist/mcp/prompts/translate-strings.js +81 -0
  13. package/dist/mcp/tools/get-catalog-statistics.d.ts +2 -0
  14. package/dist/mcp/tools/get-catalog-statistics.js +25 -0
  15. package/dist/mcp/tools/get-translations-for-key.d.ts +2 -0
  16. package/dist/mcp/tools/get-translations-for-key.js +36 -0
  17. package/dist/mcp/tools/index.d.ts +2 -0
  18. package/dist/mcp/tools/index.js +18 -0
  19. package/dist/mcp/tools/list-all-keys.d.ts +2 -0
  20. package/dist/mcp/tools/list-all-keys.js +44 -0
  21. package/dist/mcp/tools/list-supported-languages.d.ts +2 -0
  22. package/dist/mcp/tools/list-supported-languages.js +30 -0
  23. package/dist/mcp/tools/search-keys.d.ts +2 -0
  24. package/dist/mcp/tools/search-keys.js +32 -0
  25. package/dist/mcp/tools/update-translations.d.ts +2 -0
  26. package/dist/mcp/tools/update-translations.js +78 -0
  27. package/dist/string-catalog.d.ts +53 -0
  28. package/dist/string-catalog.js +220 -0
  29. package/dist/tools/get-catalog-statistics.d.ts +2 -0
  30. package/dist/tools/get-catalog-statistics.js +25 -0
  31. package/dist/tools/get-translations-for-key.d.ts +2 -0
  32. package/dist/tools/get-translations-for-key.js +36 -0
  33. package/dist/tools/index.d.ts +2 -0
  34. package/dist/tools/index.js +18 -0
  35. package/dist/tools/list-all-keys.d.ts +2 -0
  36. package/dist/tools/list-all-keys.js +44 -0
  37. package/dist/tools/list-supported-languages.d.ts +2 -0
  38. package/dist/tools/list-supported-languages.js +30 -0
  39. package/dist/tools/search-keys.d.ts +2 -0
  40. package/dist/tools/search-keys.js +32 -0
  41. package/dist/tools/update-translations.d.ts +2 -0
  42. package/dist/tools/update-translations.js +78 -0
  43. package/dist/types.d.ts +86 -0
  44. package/dist/types.js +6 -0
  45. package/eslint.config.js +23 -0
  46. package/images/mcp.jpeg +0 -0
  47. package/package.json +49 -0
  48. package/src/index.ts +25 -0
  49. package/src/mcp/prompts/batch-translate.ts +91 -0
  50. package/src/mcp/prompts/index.ts +10 -0
  51. package/src/mcp/prompts/review-translations.ts +79 -0
  52. package/src/mcp/prompts/translate-strings.ts +85 -0
  53. package/src/mcp/tools/get-catalog-statistics.ts +29 -0
  54. package/src/mcp/tools/get-translations-for-key.ts +45 -0
  55. package/src/mcp/tools/index.ts +16 -0
  56. package/src/mcp/tools/list-all-keys.ts +52 -0
  57. package/src/mcp/tools/list-supported-languages.ts +38 -0
  58. package/src/mcp/tools/search-keys.ts +40 -0
  59. package/src/mcp/tools/update-translations.ts +89 -0
  60. package/src/string-catalog.ts +232 -0
  61. package/src/types.ts +82 -0
  62. package/tsconfig.json +28 -0
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerListSupportedLanguages = registerListSupportedLanguages;
4
+ const zod_1 = require("zod");
5
+ const string_catalog_1 = require("../string-catalog");
6
+ function registerListSupportedLanguages(server) {
7
+ server.registerTool('list_supported_languages', {
8
+ description: 'List all supported languages in a given Xcode String Catalog (.xcstrings) file. Returns the source language and all languages that have translations.',
9
+ inputSchema: {
10
+ filePath: zod_1.z.string().describe('Absolute path to the .xcstrings file'),
11
+ },
12
+ }, async ({ filePath }) => {
13
+ const catalog = new string_catalog_1.StringCatalog(filePath);
14
+ const languages = catalog.getSupportedLanguages();
15
+ const sourceLanguage = catalog.getSourceLanguage();
16
+ return {
17
+ content: [
18
+ {
19
+ type: 'text',
20
+ text: JSON.stringify({
21
+ sourceLanguage,
22
+ supportedLanguages: languages,
23
+ count: languages.length,
24
+ }, null, 2),
25
+ },
26
+ ],
27
+ };
28
+ });
29
+ }
30
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibGlzdC1zdXBwb3J0ZWQtbGFuZ3VhZ2VzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3Rvb2xzL2xpc3Qtc3VwcG9ydGVkLWxhbmd1YWdlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUlBLHdFQWlDQztBQXBDRCw2QkFBd0I7QUFDeEIsc0RBQWtEO0FBRWxELFNBQWdCLDhCQUE4QixDQUFDLE1BQWlCO0lBQzVELE1BQU0sQ0FBQyxZQUFZLENBQ2YsMEJBQTBCLEVBQzFCO1FBQ0ksV0FBVyxFQUNQLHVKQUF1SjtRQUMzSixXQUFXLEVBQUU7WUFDVCxRQUFRLEVBQUUsT0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLFFBQVEsQ0FBQyxzQ0FBc0MsQ0FBQztTQUN4RTtLQUNKLEVBQ0QsS0FBSyxFQUFFLEVBQUUsUUFBUSxFQUFFLEVBQUUsRUFBRTtRQUNuQixNQUFNLE9BQU8sR0FBRyxJQUFJLDhCQUFhLENBQUMsUUFBUSxDQUFDLENBQUM7UUFDNUMsTUFBTSxTQUFTLEdBQUcsT0FBTyxDQUFDLHFCQUFxQixFQUFFLENBQUM7UUFDbEQsTUFBTSxjQUFjLEdBQUcsT0FBTyxDQUFDLGlCQUFpQixFQUFFLENBQUM7UUFFbkQsT0FBTztZQUNILE9BQU8sRUFBRTtnQkFDTDtvQkFDSSxJQUFJLEVBQUUsTUFBZTtvQkFDckIsSUFBSSxFQUFFLElBQUksQ0FBQyxTQUFTLENBQ2hCO3dCQUNJLGNBQWM7d0JBQ2Qsa0JBQWtCLEVBQUUsU0FBUzt3QkFDN0IsS0FBSyxFQUFFLFNBQVMsQ0FBQyxNQUFNO3FCQUMxQixFQUNELElBQUksRUFDSixDQUFDLENBQ0o7aUJBQ0o7YUFDSjtTQUNKLENBQUM7SUFDTixDQUFDLENBQ0osQ0FBQztBQUNOLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBNY3BTZXJ2ZXIgfSBmcm9tICdAbW9kZWxjb250ZXh0cHJvdG9jb2wvc2RrL3NlcnZlci9tY3AuanMnO1xuaW1wb3J0IHsgeiB9IGZyb20gJ3pvZCc7XG5pbXBvcnQgeyBTdHJpbmdDYXRhbG9nIH0gZnJvbSAnLi4vc3RyaW5nLWNhdGFsb2cnO1xuXG5leHBvcnQgZnVuY3Rpb24gcmVnaXN0ZXJMaXN0U3VwcG9ydGVkTGFuZ3VhZ2VzKHNlcnZlcjogTWNwU2VydmVyKSB7XG4gICAgc2VydmVyLnJlZ2lzdGVyVG9vbChcbiAgICAgICAgJ2xpc3Rfc3VwcG9ydGVkX2xhbmd1YWdlcycsXG4gICAgICAgIHtcbiAgICAgICAgICAgIGRlc2NyaXB0aW9uOlxuICAgICAgICAgICAgICAgICdMaXN0IGFsbCBzdXBwb3J0ZWQgbGFuZ3VhZ2VzIGluIGEgZ2l2ZW4gWGNvZGUgU3RyaW5nIENhdGFsb2cgKC54Y3N0cmluZ3MpIGZpbGUuIFJldHVybnMgdGhlIHNvdXJjZSBsYW5ndWFnZSBhbmQgYWxsIGxhbmd1YWdlcyB0aGF0IGhhdmUgdHJhbnNsYXRpb25zLicsXG4gICAgICAgICAgICBpbnB1dFNjaGVtYToge1xuICAgICAgICAgICAgICAgIGZpbGVQYXRoOiB6LnN0cmluZygpLmRlc2NyaWJlKCdBYnNvbHV0ZSBwYXRoIHRvIHRoZSAueGNzdHJpbmdzIGZpbGUnKSxcbiAgICAgICAgICAgIH0sXG4gICAgICAgIH0sXG4gICAgICAgIGFzeW5jICh7IGZpbGVQYXRoIH0pID0+IHtcbiAgICAgICAgICAgIGNvbnN0IGNhdGFsb2cgPSBuZXcgU3RyaW5nQ2F0YWxvZyhmaWxlUGF0aCk7XG4gICAgICAgICAgICBjb25zdCBsYW5ndWFnZXMgPSBjYXRhbG9nLmdldFN1cHBvcnRlZExhbmd1YWdlcygpO1xuICAgICAgICAgICAgY29uc3Qgc291cmNlTGFuZ3VhZ2UgPSBjYXRhbG9nLmdldFNvdXJjZUxhbmd1YWdlKCk7XG5cbiAgICAgICAgICAgIHJldHVybiB7XG4gICAgICAgICAgICAgICAgY29udGVudDogW1xuICAgICAgICAgICAgICAgICAgICB7XG4gICAgICAgICAgICAgICAgICAgICAgICB0eXBlOiAndGV4dCcgYXMgY29uc3QsXG4gICAgICAgICAgICAgICAgICAgICAgICB0ZXh0OiBKU09OLnN0cmluZ2lmeShcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICB7XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNvdXJjZUxhbmd1YWdlLFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBzdXBwb3J0ZWRMYW5ndWFnZXM6IGxhbmd1YWdlcyxcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY291bnQ6IGxhbmd1YWdlcy5sZW5ndGgsXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBudWxsLFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgIDJcbiAgICAgICAgICAgICAgICAgICAgICAgICksXG4gICAgICAgICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICAgICAgXSxcbiAgICAgICAgICAgIH07XG4gICAgICAgIH1cbiAgICApO1xufVxuIl19
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerSearchKeys(server: McpServer): void;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerSearchKeys = registerSearchKeys;
4
+ const zod_1 = require("zod");
5
+ const string_catalog_1 = require("../string-catalog");
6
+ function registerSearchKeys(server) {
7
+ server.registerTool('search_keys', {
8
+ description: 'Search for localization keys containing a specific substring. Useful for finding keys when you only know part of the key name.',
9
+ inputSchema: {
10
+ filePath: zod_1.z.string().describe('Absolute path to the .xcstrings file'),
11
+ query: zod_1.z
12
+ .string()
13
+ .describe('Substring to search for in key names (case-insensitive)'),
14
+ },
15
+ }, async ({ filePath, query }) => {
16
+ const catalog = new string_catalog_1.StringCatalog(filePath);
17
+ const keys = catalog.searchKeys(query);
18
+ return {
19
+ content: [
20
+ {
21
+ type: 'text',
22
+ text: JSON.stringify({
23
+ query,
24
+ matchingKeys: keys,
25
+ count: keys.length,
26
+ }, null, 2),
27
+ },
28
+ ],
29
+ };
30
+ });
31
+ }
32
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VhcmNoLWtleXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvdG9vbHMvc2VhcmNoLWtleXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFJQSxnREFtQ0M7QUF0Q0QsNkJBQXdCO0FBQ3hCLHNEQUFrRDtBQUVsRCxTQUFnQixrQkFBa0IsQ0FBQyxNQUFpQjtJQUNoRCxNQUFNLENBQUMsWUFBWSxDQUNmLGFBQWEsRUFDYjtRQUNJLFdBQVcsRUFDUCxnSUFBZ0k7UUFDcEksV0FBVyxFQUFFO1lBQ1QsUUFBUSxFQUFFLE9BQUMsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxRQUFRLENBQUMsc0NBQXNDLENBQUM7WUFDckUsS0FBSyxFQUFFLE9BQUM7aUJBQ0gsTUFBTSxFQUFFO2lCQUNSLFFBQVEsQ0FBQyx5REFBeUQsQ0FBQztTQUMzRTtLQUNKLEVBQ0QsS0FBSyxFQUFFLEVBQUUsUUFBUSxFQUFFLEtBQUssRUFBRSxFQUFFLEVBQUU7UUFDMUIsTUFBTSxPQUFPLEdBQUcsSUFBSSw4QkFBYSxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBQzVDLE1BQU0sSUFBSSxHQUFHLE9BQU8sQ0FBQyxVQUFVLENBQUMsS0FBSyxDQUFDLENBQUM7UUFFdkMsT0FBTztZQUNILE9BQU8sRUFBRTtnQkFDTDtvQkFDSSxJQUFJLEVBQUUsTUFBZTtvQkFDckIsSUFBSSxFQUFFLElBQUksQ0FBQyxTQUFTLENBQ2hCO3dCQUNJLEtBQUs7d0JBQ0wsWUFBWSxFQUFFLElBQUk7d0JBQ2xCLEtBQUssRUFBRSxJQUFJLENBQUMsTUFBTTtxQkFDckIsRUFDRCxJQUFJLEVBQ0osQ0FBQyxDQUNKO2lCQUNKO2FBQ0o7U0FDSixDQUFDO0lBQ04sQ0FBQyxDQUNKLENBQUM7QUFDTixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgTWNwU2VydmVyIH0gZnJvbSAnQG1vZGVsY29udGV4dHByb3RvY29sL3Nkay9zZXJ2ZXIvbWNwLmpzJztcbmltcG9ydCB7IHogfSBmcm9tICd6b2QnO1xuaW1wb3J0IHsgU3RyaW5nQ2F0YWxvZyB9IGZyb20gJy4uL3N0cmluZy1jYXRhbG9nJztcblxuZXhwb3J0IGZ1bmN0aW9uIHJlZ2lzdGVyU2VhcmNoS2V5cyhzZXJ2ZXI6IE1jcFNlcnZlcikge1xuICAgIHNlcnZlci5yZWdpc3RlclRvb2woXG4gICAgICAgICdzZWFyY2hfa2V5cycsXG4gICAgICAgIHtcbiAgICAgICAgICAgIGRlc2NyaXB0aW9uOlxuICAgICAgICAgICAgICAgICdTZWFyY2ggZm9yIGxvY2FsaXphdGlvbiBrZXlzIGNvbnRhaW5pbmcgYSBzcGVjaWZpYyBzdWJzdHJpbmcuIFVzZWZ1bCBmb3IgZmluZGluZyBrZXlzIHdoZW4geW91IG9ubHkga25vdyBwYXJ0IG9mIHRoZSBrZXkgbmFtZS4nLFxuICAgICAgICAgICAgaW5wdXRTY2hlbWE6IHtcbiAgICAgICAgICAgICAgICBmaWxlUGF0aDogei5zdHJpbmcoKS5kZXNjcmliZSgnQWJzb2x1dGUgcGF0aCB0byB0aGUgLnhjc3RyaW5ncyBmaWxlJyksXG4gICAgICAgICAgICAgICAgcXVlcnk6IHpcbiAgICAgICAgICAgICAgICAgICAgLnN0cmluZygpXG4gICAgICAgICAgICAgICAgICAgIC5kZXNjcmliZSgnU3Vic3RyaW5nIHRvIHNlYXJjaCBmb3IgaW4ga2V5IG5hbWVzIChjYXNlLWluc2Vuc2l0aXZlKScpLFxuICAgICAgICAgICAgfSxcbiAgICAgICAgfSxcbiAgICAgICAgYXN5bmMgKHsgZmlsZVBhdGgsIHF1ZXJ5IH0pID0+IHtcbiAgICAgICAgICAgIGNvbnN0IGNhdGFsb2cgPSBuZXcgU3RyaW5nQ2F0YWxvZyhmaWxlUGF0aCk7XG4gICAgICAgICAgICBjb25zdCBrZXlzID0gY2F0YWxvZy5zZWFyY2hLZXlzKHF1ZXJ5KTtcblxuICAgICAgICAgICAgcmV0dXJuIHtcbiAgICAgICAgICAgICAgICBjb250ZW50OiBbXG4gICAgICAgICAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgICAgICAgICAgIHR5cGU6ICd0ZXh0JyBhcyBjb25zdCxcbiAgICAgICAgICAgICAgICAgICAgICAgIHRleHQ6IEpTT04uc3RyaW5naWZ5KFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcXVlcnksXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIG1hdGNoaW5nS2V5czoga2V5cyxcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY291bnQ6IGtleXMubGVuZ3RoLFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgbnVsbCxcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICAyXG4gICAgICAgICAgICAgICAgICAgICAgICApLFxuICAgICAgICAgICAgICAgICAgICB9LFxuICAgICAgICAgICAgICAgIF0sXG4gICAgICAgICAgICB9O1xuICAgICAgICB9XG4gICAgKTtcbn1cbiJdfQ==
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerUpdateTranslations(server: McpServer): void;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerUpdateTranslations = registerUpdateTranslations;
4
+ const zod_1 = require("zod");
5
+ const string_catalog_1 = require("../string-catalog");
6
+ const toolDescription = `Update or add translations to a String Catalog. Accepts an array of translation entries.
7
+
8
+ IMPORTANT: iOS strings support format placeholders that must be preserved in translations:
9
+ - %@ for strings (objects)
10
+ - %d or %lld for integers
11
+ - %f for floating point numbers
12
+ - %1$@, %2$@ etc. for positional arguments (order can be changed in translations)
13
+
14
+ Example input:
15
+ {
16
+ "data": [
17
+ {
18
+ "key": "hello_world",
19
+ "translations": [
20
+ { "language": "en", "value": "Hello World" },
21
+ { "language": "de", "value": "Hallo Welt" },
22
+ { "language": "no", "value": "Hei Verden" }
23
+ ],
24
+ "comment": "Greeting message shown on home screen"
25
+ },
26
+ {
27
+ "key": "items_count",
28
+ "translations": [
29
+ { "language": "en", "value": "%lld items" },
30
+ { "language": "de", "value": "%lld Elemente" }
31
+ ]
32
+ }
33
+ ]
34
+ }`;
35
+ const translationEntrySchema = zod_1.z.object({
36
+ language: zod_1.z.string().describe('Language code (e.g., "en", "de", "no", "vi")'),
37
+ value: zod_1.z
38
+ .string()
39
+ .describe('The translated text. Preserve any format placeholders like %@, %lld, %d'),
40
+ state: zod_1.z
41
+ .enum(['new', 'translated', 'needs_review', 'stale'])
42
+ .optional()
43
+ .describe('Translation state (defaults to "translated")'),
44
+ });
45
+ const dataEntrySchema = zod_1.z.object({
46
+ key: zod_1.z.string().describe('The localization key'),
47
+ translations: zod_1.z.array(translationEntrySchema).describe('Array of language translations'),
48
+ comment: zod_1.z.string().optional().describe('Optional comment describing the string context'),
49
+ });
50
+ const inputSchema = zod_1.z.object({
51
+ filePath: zod_1.z.string().describe('Absolute path to the .xcstrings file'),
52
+ data: zod_1.z.array(dataEntrySchema).describe('Array of translation entries to add or update'),
53
+ });
54
+ function registerUpdateTranslations(server) {
55
+ server.registerTool('update_translations', {
56
+ description: toolDescription,
57
+ inputSchema,
58
+ }, async ({ filePath, data }) => {
59
+ const catalog = new string_catalog_1.StringCatalog(filePath);
60
+ const result = catalog.updateTranslations(data);
61
+ catalog.save();
62
+ return {
63
+ content: [
64
+ {
65
+ type: 'text',
66
+ text: JSON.stringify({
67
+ success: true,
68
+ updatedKeys: result.updated,
69
+ createdKeys: result.created,
70
+ totalUpdated: result.updated.length,
71
+ totalCreated: result.created.length,
72
+ }, null, 2),
73
+ },
74
+ ],
75
+ };
76
+ });
77
+ }
78
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidXBkYXRlLXRyYW5zbGF0aW9ucy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy90b29scy91cGRhdGUtdHJhbnNsYXRpb25zLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBd0RBLGdFQWdDQztBQXZGRCw2QkFBd0I7QUFDeEIsc0RBQWtEO0FBRWxELE1BQU0sZUFBZSxHQUFHOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0VBNEJ0QixDQUFDO0FBRUgsTUFBTSxzQkFBc0IsR0FBRyxPQUFDLENBQUMsTUFBTSxDQUFDO0lBQ3BDLFFBQVEsRUFBRSxPQUFDLENBQUMsTUFBTSxFQUFFLENBQUMsUUFBUSxDQUFDLDhDQUE4QyxDQUFDO0lBQzdFLEtBQUssRUFBRSxPQUFDO1NBQ0gsTUFBTSxFQUFFO1NBQ1IsUUFBUSxDQUFDLHlFQUF5RSxDQUFDO0lBQ3hGLEtBQUssRUFBRSxPQUFDO1NBQ0gsSUFBSSxDQUFDLENBQUMsS0FBSyxFQUFFLFlBQVksRUFBRSxjQUFjLEVBQUUsT0FBTyxDQUFDLENBQUM7U0FDcEQsUUFBUSxFQUFFO1NBQ1YsUUFBUSxDQUFDLDhDQUE4QyxDQUFDO0NBQ2hFLENBQUMsQ0FBQztBQUVILE1BQU0sZUFBZSxHQUFHLE9BQUMsQ0FBQyxNQUFNLENBQUM7SUFDN0IsR0FBRyxFQUFFLE9BQUMsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxRQUFRLENBQUMsc0JBQXNCLENBQUM7SUFDaEQsWUFBWSxFQUFFLE9BQUMsQ0FBQyxLQUFLLENBQUMsc0JBQXNCLENBQUMsQ0FBQyxRQUFRLENBQUMsZ0NBQWdDLENBQUM7SUFDeEYsT0FBTyxFQUFFLE9BQUMsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxRQUFRLEVBQUUsQ0FBQyxRQUFRLENBQUMsZ0RBQWdELENBQUM7Q0FDNUYsQ0FBQyxDQUFDO0FBRUgsTUFBTSxXQUFXLEdBQUcsT0FBQyxDQUFDLE1BQU0sQ0FBQztJQUN6QixRQUFRLEVBQUUsT0FBQyxDQUFDLE1BQU0sRUFBRSxDQUFDLFFBQVEsQ0FBQyxzQ0FBc0MsQ0FBQztJQUNyRSxJQUFJLEVBQUUsT0FBQyxDQUFDLEtBQUssQ0FBQyxlQUFlLENBQUMsQ0FBQyxRQUFRLENBQUMsK0NBQStDLENBQUM7Q0FDM0YsQ0FBQyxDQUFDO0FBRUgsU0FBZ0IsMEJBQTBCLENBQUMsTUFBaUI7SUFDeEQsTUFBTSxDQUFDLFlBQVksQ0FDZixxQkFBcUIsRUFDckI7UUFDSSxXQUFXLEVBQUUsZUFBZTtRQUM1QixXQUFXO0tBQ2QsRUFDRCxLQUFLLEVBQUUsRUFBRSxRQUFRLEVBQUUsSUFBSSxFQUFFLEVBQUUsRUFBRTtRQUN6QixNQUFNLE9BQU8sR0FBRyxJQUFJLDhCQUFhLENBQUMsUUFBUSxDQUFDLENBQUM7UUFDNUMsTUFBTSxNQUFNLEdBQUcsT0FBTyxDQUFDLGtCQUFrQixDQUFDLElBQUksQ0FBQyxDQUFDO1FBQ2hELE9BQU8sQ0FBQyxJQUFJLEVBQUUsQ0FBQztRQUVmLE9BQU87WUFDSCxPQUFPLEVBQUU7Z0JBQ0w7b0JBQ0ksSUFBSSxFQUFFLE1BQWU7b0JBQ3JCLElBQUksRUFBRSxJQUFJLENBQUMsU0FBUyxDQUNoQjt3QkFDSSxPQUFPLEVBQUUsSUFBSTt3QkFDYixXQUFXLEVBQUUsTUFBTSxDQUFDLE9BQU87d0JBQzNCLFdBQVcsRUFBRSxNQUFNLENBQUMsT0FBTzt3QkFDM0IsWUFBWSxFQUFFLE1BQU0sQ0FBQyxPQUFPLENBQUMsTUFBTTt3QkFDbkMsWUFBWSxFQUFFLE1BQU0sQ0FBQyxPQUFPLENBQUMsTUFBTTtxQkFDdEMsRUFDRCxJQUFJLEVBQ0osQ0FBQyxDQUNKO2lCQUNKO2FBQ0o7U0FDSixDQUFDO0lBQ04sQ0FBQyxDQUNKLENBQUM7QUFDTixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgTWNwU2VydmVyIH0gZnJvbSAnQG1vZGVsY29udGV4dHByb3RvY29sL3Nkay9zZXJ2ZXIvbWNwLmpzJztcbmltcG9ydCB7IHogfSBmcm9tICd6b2QnO1xuaW1wb3J0IHsgU3RyaW5nQ2F0YWxvZyB9IGZyb20gJy4uL3N0cmluZy1jYXRhbG9nJztcblxuY29uc3QgdG9vbERlc2NyaXB0aW9uID0gYFVwZGF0ZSBvciBhZGQgdHJhbnNsYXRpb25zIHRvIGEgU3RyaW5nIENhdGFsb2cuIEFjY2VwdHMgYW4gYXJyYXkgb2YgdHJhbnNsYXRpb24gZW50cmllcy5cblxuSU1QT1JUQU5UOiBpT1Mgc3RyaW5ncyBzdXBwb3J0IGZvcm1hdCBwbGFjZWhvbGRlcnMgdGhhdCBtdXN0IGJlIHByZXNlcnZlZCBpbiB0cmFuc2xhdGlvbnM6XG4tICVAIGZvciBzdHJpbmdzIChvYmplY3RzKVxuLSAlZCBvciAlbGxkIGZvciBpbnRlZ2Vyc1xuLSAlZiBmb3IgZmxvYXRpbmcgcG9pbnQgbnVtYmVyc1xuLSAlMSRALCAlMiRAIGV0Yy4gZm9yIHBvc2l0aW9uYWwgYXJndW1lbnRzIChvcmRlciBjYW4gYmUgY2hhbmdlZCBpbiB0cmFuc2xhdGlvbnMpXG5cbkV4YW1wbGUgaW5wdXQ6XG57XG4gIFwiZGF0YVwiOiBbXG4gICAge1xuICAgICAgXCJrZXlcIjogXCJoZWxsb193b3JsZFwiLFxuICAgICAgXCJ0cmFuc2xhdGlvbnNcIjogW1xuICAgICAgICB7IFwibGFuZ3VhZ2VcIjogXCJlblwiLCBcInZhbHVlXCI6IFwiSGVsbG8gV29ybGRcIiB9LFxuICAgICAgICB7IFwibGFuZ3VhZ2VcIjogXCJkZVwiLCBcInZhbHVlXCI6IFwiSGFsbG8gV2VsdFwiIH0sXG4gICAgICAgIHsgXCJsYW5ndWFnZVwiOiBcIm5vXCIsIFwidmFsdWVcIjogXCJIZWkgVmVyZGVuXCIgfVxuICAgICAgXSxcbiAgICAgIFwiY29tbWVudFwiOiBcIkdyZWV0aW5nIG1lc3NhZ2Ugc2hvd24gb24gaG9tZSBzY3JlZW5cIlxuICAgIH0sXG4gICAge1xuICAgICAgXCJrZXlcIjogXCJpdGVtc19jb3VudFwiLFxuICAgICAgXCJ0cmFuc2xhdGlvbnNcIjogW1xuICAgICAgICB7IFwibGFuZ3VhZ2VcIjogXCJlblwiLCBcInZhbHVlXCI6IFwiJWxsZCBpdGVtc1wiIH0sXG4gICAgICAgIHsgXCJsYW5ndWFnZVwiOiBcImRlXCIsIFwidmFsdWVcIjogXCIlbGxkIEVsZW1lbnRlXCIgfVxuICAgICAgXVxuICAgIH1cbiAgXVxufWA7XG5cbmNvbnN0IHRyYW5zbGF0aW9uRW50cnlTY2hlbWEgPSB6Lm9iamVjdCh7XG4gICAgbGFuZ3VhZ2U6IHouc3RyaW5nKCkuZGVzY3JpYmUoJ0xhbmd1YWdlIGNvZGUgKGUuZy4sIFwiZW5cIiwgXCJkZVwiLCBcIm5vXCIsIFwidmlcIiknKSxcbiAgICB2YWx1ZTogelxuICAgICAgICAuc3RyaW5nKClcbiAgICAgICAgLmRlc2NyaWJlKCdUaGUgdHJhbnNsYXRlZCB0ZXh0LiBQcmVzZXJ2ZSBhbnkgZm9ybWF0IHBsYWNlaG9sZGVycyBsaWtlICVALCAlbGxkLCAlZCcpLFxuICAgIHN0YXRlOiB6XG4gICAgICAgIC5lbnVtKFsnbmV3JywgJ3RyYW5zbGF0ZWQnLCAnbmVlZHNfcmV2aWV3JywgJ3N0YWxlJ10pXG4gICAgICAgIC5vcHRpb25hbCgpXG4gICAgICAgIC5kZXNjcmliZSgnVHJhbnNsYXRpb24gc3RhdGUgKGRlZmF1bHRzIHRvIFwidHJhbnNsYXRlZFwiKScpLFxufSk7XG5cbmNvbnN0IGRhdGFFbnRyeVNjaGVtYSA9IHoub2JqZWN0KHtcbiAgICBrZXk6IHouc3RyaW5nKCkuZGVzY3JpYmUoJ1RoZSBsb2NhbGl6YXRpb24ga2V5JyksXG4gICAgdHJhbnNsYXRpb25zOiB6LmFycmF5KHRyYW5zbGF0aW9uRW50cnlTY2hlbWEpLmRlc2NyaWJlKCdBcnJheSBvZiBsYW5ndWFnZSB0cmFuc2xhdGlvbnMnKSxcbiAgICBjb21tZW50OiB6LnN0cmluZygpLm9wdGlvbmFsKCkuZGVzY3JpYmUoJ09wdGlvbmFsIGNvbW1lbnQgZGVzY3JpYmluZyB0aGUgc3RyaW5nIGNvbnRleHQnKSxcbn0pO1xuXG5jb25zdCBpbnB1dFNjaGVtYSA9IHoub2JqZWN0KHtcbiAgICBmaWxlUGF0aDogei5zdHJpbmcoKS5kZXNjcmliZSgnQWJzb2x1dGUgcGF0aCB0byB0aGUgLnhjc3RyaW5ncyBmaWxlJyksXG4gICAgZGF0YTogei5hcnJheShkYXRhRW50cnlTY2hlbWEpLmRlc2NyaWJlKCdBcnJheSBvZiB0cmFuc2xhdGlvbiBlbnRyaWVzIHRvIGFkZCBvciB1cGRhdGUnKSxcbn0pO1xuXG5leHBvcnQgZnVuY3Rpb24gcmVnaXN0ZXJVcGRhdGVUcmFuc2xhdGlvbnMoc2VydmVyOiBNY3BTZXJ2ZXIpIHtcbiAgICBzZXJ2ZXIucmVnaXN0ZXJUb29sKFxuICAgICAgICAndXBkYXRlX3RyYW5zbGF0aW9ucycsXG4gICAgICAgIHtcbiAgICAgICAgICAgIGRlc2NyaXB0aW9uOiB0b29sRGVzY3JpcHRpb24sXG4gICAgICAgICAgICBpbnB1dFNjaGVtYSxcbiAgICAgICAgfSxcbiAgICAgICAgYXN5bmMgKHsgZmlsZVBhdGgsIGRhdGEgfSkgPT4ge1xuICAgICAgICAgICAgY29uc3QgY2F0YWxvZyA9IG5ldyBTdHJpbmdDYXRhbG9nKGZpbGVQYXRoKTtcbiAgICAgICAgICAgIGNvbnN0IHJlc3VsdCA9IGNhdGFsb2cudXBkYXRlVHJhbnNsYXRpb25zKGRhdGEpO1xuICAgICAgICAgICAgY2F0YWxvZy5zYXZlKCk7XG5cbiAgICAgICAgICAgIHJldHVybiB7XG4gICAgICAgICAgICAgICAgY29udGVudDogW1xuICAgICAgICAgICAgICAgICAgICB7XG4gICAgICAgICAgICAgICAgICAgICAgICB0eXBlOiAndGV4dCcgYXMgY29uc3QsXG4gICAgICAgICAgICAgICAgICAgICAgICB0ZXh0OiBKU09OLnN0cmluZ2lmeShcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICB7XG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHN1Y2Nlc3M6IHRydWUsXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHVwZGF0ZWRLZXlzOiByZXN1bHQudXBkYXRlZCxcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgY3JlYXRlZEtleXM6IHJlc3VsdC5jcmVhdGVkLFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB0b3RhbFVwZGF0ZWQ6IHJlc3VsdC51cGRhdGVkLmxlbmd0aCxcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdG90YWxDcmVhdGVkOiByZXN1bHQuY3JlYXRlZC5sZW5ndGgsXG4gICAgICAgICAgICAgICAgICAgICAgICAgICAgfSxcbiAgICAgICAgICAgICAgICAgICAgICAgICAgICBudWxsLFxuICAgICAgICAgICAgICAgICAgICAgICAgICAgIDJcbiAgICAgICAgICAgICAgICAgICAgICAgICksXG4gICAgICAgICAgICAgICAgICAgIH0sXG4gICAgICAgICAgICAgICAgXSxcbiAgICAgICAgICAgIH07XG4gICAgICAgIH1cbiAgICApO1xufVxuIl19
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Types for Xcode String Catalog (.xcstrings) file format
3
+ */
4
+ /** State of a localization string */
5
+ export type LocalizationState = 'new' | 'translated' | 'needs_review' | 'stale';
6
+ /** A single string unit containing the translation value and state */
7
+ export interface StringUnit {
8
+ state: LocalizationState;
9
+ value: string;
10
+ }
11
+ /** Localization entry for a single language */
12
+ export interface LocalizationEntry {
13
+ stringUnit?: StringUnit;
14
+ /** For plural variations */
15
+ variations?: {
16
+ plural?: {
17
+ zero?: {
18
+ stringUnit: StringUnit;
19
+ };
20
+ one?: {
21
+ stringUnit: StringUnit;
22
+ };
23
+ two?: {
24
+ stringUnit: StringUnit;
25
+ };
26
+ few?: {
27
+ stringUnit: StringUnit;
28
+ };
29
+ many?: {
30
+ stringUnit: StringUnit;
31
+ };
32
+ other?: {
33
+ stringUnit: StringUnit;
34
+ };
35
+ };
36
+ device?: Record<string, {
37
+ stringUnit: StringUnit;
38
+ }>;
39
+ };
40
+ }
41
+ /** All localizations for a single string key */
42
+ export interface StringLocalizations {
43
+ [languageCode: string]: LocalizationEntry;
44
+ }
45
+ /** A single string entry in the catalog */
46
+ export interface StringEntry {
47
+ comment?: string;
48
+ extractionState?: 'manual' | 'extracted_with_value' | 'stale';
49
+ localizations?: StringLocalizations;
50
+ }
51
+ /** Root structure of an .xcstrings file */
52
+ export interface XCStrings {
53
+ sourceLanguage: string;
54
+ version?: string;
55
+ strings: {
56
+ [key: string]: StringEntry;
57
+ };
58
+ }
59
+ /**
60
+ * Translation input format for the update tool
61
+ */
62
+ export interface TranslationInput {
63
+ key: string;
64
+ translations: {
65
+ language: string;
66
+ value: string;
67
+ state?: LocalizationState;
68
+ }[];
69
+ comment?: string;
70
+ }
71
+ export interface TranslationData {
72
+ data: TranslationInput[];
73
+ }
74
+ /**
75
+ * Output format for listing translations of a key
76
+ */
77
+ export interface KeyTranslation {
78
+ language: string;
79
+ value: string;
80
+ state: LocalizationState;
81
+ }
82
+ export interface KeyTranslationsResult {
83
+ key: string;
84
+ sourceLanguage: string;
85
+ translations: KeyTranslation[];
86
+ }
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ /**
3
+ * Types for Xcode String Catalog (.xcstrings) file format
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHlwZXMuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvdHlwZXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOztHQUVHIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBUeXBlcyBmb3IgWGNvZGUgU3RyaW5nIENhdGFsb2cgKC54Y3N0cmluZ3MpIGZpbGUgZm9ybWF0XG4gKi9cblxuLyoqIFN0YXRlIG9mIGEgbG9jYWxpemF0aW9uIHN0cmluZyAqL1xuZXhwb3J0IHR5cGUgTG9jYWxpemF0aW9uU3RhdGUgPSAnbmV3JyB8ICd0cmFuc2xhdGVkJyB8ICduZWVkc19yZXZpZXcnIHwgJ3N0YWxlJztcblxuLyoqIEEgc2luZ2xlIHN0cmluZyB1bml0IGNvbnRhaW5pbmcgdGhlIHRyYW5zbGF0aW9uIHZhbHVlIGFuZCBzdGF0ZSAqL1xuZXhwb3J0IGludGVyZmFjZSBTdHJpbmdVbml0IHtcbiAgICBzdGF0ZTogTG9jYWxpemF0aW9uU3RhdGU7XG4gICAgdmFsdWU6IHN0cmluZztcbn1cblxuLyoqIExvY2FsaXphdGlvbiBlbnRyeSBmb3IgYSBzaW5nbGUgbGFuZ3VhZ2UgKi9cbmV4cG9ydCBpbnRlcmZhY2UgTG9jYWxpemF0aW9uRW50cnkge1xuICAgIHN0cmluZ1VuaXQ/OiBTdHJpbmdVbml0O1xuICAgIC8qKiBGb3IgcGx1cmFsIHZhcmlhdGlvbnMgKi9cbiAgICB2YXJpYXRpb25zPzoge1xuICAgICAgICBwbHVyYWw/OiB7XG4gICAgICAgICAgICB6ZXJvPzogeyBzdHJpbmdVbml0OiBTdHJpbmdVbml0IH07XG4gICAgICAgICAgICBvbmU/OiB7IHN0cmluZ1VuaXQ6IFN0cmluZ1VuaXQgfTtcbiAgICAgICAgICAgIHR3bz86IHsgc3RyaW5nVW5pdDogU3RyaW5nVW5pdCB9O1xuICAgICAgICAgICAgZmV3PzogeyBzdHJpbmdVbml0OiBTdHJpbmdVbml0IH07XG4gICAgICAgICAgICBtYW55PzogeyBzdHJpbmdVbml0OiBTdHJpbmdVbml0IH07XG4gICAgICAgICAgICBvdGhlcj86IHsgc3RyaW5nVW5pdDogU3RyaW5nVW5pdCB9O1xuICAgICAgICB9O1xuICAgICAgICBkZXZpY2U/OiBSZWNvcmQ8c3RyaW5nLCB7IHN0cmluZ1VuaXQ6IFN0cmluZ1VuaXQgfT47XG4gICAgfTtcbn1cblxuLyoqIEFsbCBsb2NhbGl6YXRpb25zIGZvciBhIHNpbmdsZSBzdHJpbmcga2V5ICovXG5leHBvcnQgaW50ZXJmYWNlIFN0cmluZ0xvY2FsaXphdGlvbnMge1xuICAgIFtsYW5ndWFnZUNvZGU6IHN0cmluZ106IExvY2FsaXphdGlvbkVudHJ5O1xufVxuXG4vKiogQSBzaW5nbGUgc3RyaW5nIGVudHJ5IGluIHRoZSBjYXRhbG9nICovXG5leHBvcnQgaW50ZXJmYWNlIFN0cmluZ0VudHJ5IHtcbiAgICBjb21tZW50Pzogc3RyaW5nO1xuICAgIGV4dHJhY3Rpb25TdGF0ZT86ICdtYW51YWwnIHwgJ2V4dHJhY3RlZF93aXRoX3ZhbHVlJyB8ICdzdGFsZSc7XG4gICAgbG9jYWxpemF0aW9ucz86IFN0cmluZ0xvY2FsaXphdGlvbnM7XG59XG5cbi8qKiBSb290IHN0cnVjdHVyZSBvZiBhbiAueGNzdHJpbmdzIGZpbGUgKi9cbmV4cG9ydCBpbnRlcmZhY2UgWENTdHJpbmdzIHtcbiAgICBzb3VyY2VMYW5ndWFnZTogc3RyaW5nO1xuICAgIHZlcnNpb24/OiBzdHJpbmc7XG4gICAgc3RyaW5nczoge1xuICAgICAgICBba2V5OiBzdHJpbmddOiBTdHJpbmdFbnRyeTtcbiAgICB9O1xufVxuXG4vKipcbiAqIFRyYW5zbGF0aW9uIGlucHV0IGZvcm1hdCBmb3IgdGhlIHVwZGF0ZSB0b29sXG4gKi9cbmV4cG9ydCBpbnRlcmZhY2UgVHJhbnNsYXRpb25JbnB1dCB7XG4gICAga2V5OiBzdHJpbmc7XG4gICAgdHJhbnNsYXRpb25zOiB7XG4gICAgICAgIGxhbmd1YWdlOiBzdHJpbmc7XG4gICAgICAgIHZhbHVlOiBzdHJpbmc7XG4gICAgICAgIHN0YXRlPzogTG9jYWxpemF0aW9uU3RhdGU7XG4gICAgfVtdO1xuICAgIGNvbW1lbnQ/OiBzdHJpbmc7XG59XG5cbmV4cG9ydCBpbnRlcmZhY2UgVHJhbnNsYXRpb25EYXRhIHtcbiAgICBkYXRhOiBUcmFuc2xhdGlvbklucHV0W107XG59XG5cbi8qKlxuICogT3V0cHV0IGZvcm1hdCBmb3IgbGlzdGluZyB0cmFuc2xhdGlvbnMgb2YgYSBrZXlcbiAqL1xuZXhwb3J0IGludGVyZmFjZSBLZXlUcmFuc2xhdGlvbiB7XG4gICAgbGFuZ3VhZ2U6IHN0cmluZztcbiAgICB2YWx1ZTogc3RyaW5nO1xuICAgIHN0YXRlOiBMb2NhbGl6YXRpb25TdGF0ZTtcbn1cblxuZXhwb3J0IGludGVyZmFjZSBLZXlUcmFuc2xhdGlvbnNSZXN1bHQge1xuICAgIGtleTogc3RyaW5nO1xuICAgIHNvdXJjZUxhbmd1YWdlOiBzdHJpbmc7XG4gICAgdHJhbnNsYXRpb25zOiBLZXlUcmFuc2xhdGlvbltdO1xufVxuIl19
@@ -0,0 +1,23 @@
1
+ import eslint from '@eslint/js';
2
+ import tseslint from 'typescript-eslint';
3
+
4
+ export default tseslint.config(
5
+ eslint.configs.recommended,
6
+ ...tseslint.configs.recommended,
7
+ {
8
+ files: ['src/**/*.ts'],
9
+ languageOptions: {
10
+ parserOptions: {
11
+ project: './tsconfig.json',
12
+ },
13
+ },
14
+ rules: {
15
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
16
+ '@typescript-eslint/no-explicit-any': 'off',
17
+ '@typescript-eslint/ban-ts-comment': 'off',
18
+ },
19
+ },
20
+ {
21
+ ignores: ['dist/**', 'node_modules/**'],
22
+ }
23
+ );
Binary file
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "string-catalog-mcp",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "MCP server for interacting with Xcode String Catalog (.xcstrings) files",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "string-catalog-mcp": "dist/index.js"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "start": "node dist/index.js",
14
+ "dev": "tsx src/index.ts",
15
+ "inspect": "pnpm dlx @modelcontextprotocol/inspector tsx src/index.ts",
16
+ "mcp:add": "claude mcp add string-catalog-mcp -- pnpm --dir=$PWD dlx tsx $PWD/src/index.ts",
17
+ "mcp:remove": "claude mcp remove string-catalog-mcp",
18
+ "typecheck": "tsc --noEmit",
19
+ "lint": "eslint src/**/*.ts",
20
+ "lint:fix": "eslint src/**/*.ts --fix",
21
+ "format": "prettier --write \"src/**/*.ts\"",
22
+ "format:check": "prettier --check \"src/**/*.ts\""
23
+ },
24
+ "keywords": [
25
+ "mcp",
26
+ "xcode",
27
+ "string-catalog",
28
+ "xcstrings",
29
+ "localization",
30
+ "i18n"
31
+ ],
32
+ "author": "Khoa Pham",
33
+ "license": "MIT",
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.25.1",
36
+ "zod": "^4.2.1"
37
+ },
38
+ "devDependencies": {
39
+ "@eslint/js": "^9.39.2",
40
+ "@types/node": "^20.10.0",
41
+ "@typescript-eslint/eslint-plugin": "^8.50.0",
42
+ "@typescript-eslint/parser": "^8.50.0",
43
+ "eslint": "^9.39.2",
44
+ "prettier": "^3.4.2",
45
+ "tsx": "^4.19.0",
46
+ "typescript": "^5.3.0",
47
+ "typescript-eslint": "^8.50.0"
48
+ }
49
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { registerAllTools } from './mcp/tools/index';
6
+ import { registerAllPrompts } from './mcp/prompts/index';
7
+
8
+ const server = new McpServer({
9
+ name: 'string-catalog-mcp',
10
+ version: '1.0.0',
11
+ });
12
+
13
+ registerAllTools(server);
14
+ registerAllPrompts(server);
15
+
16
+ async function main() {
17
+ const transport = new StdioServerTransport();
18
+ await server.connect(transport);
19
+ console.error('String Catalog MCP Server running on stdio');
20
+ }
21
+
22
+ main().catch((error) => {
23
+ console.error('Fatal error:', error);
24
+ process.exit(1);
25
+ });
@@ -0,0 +1,91 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+
4
+ export function registerBatchTranslatePrompt(server: McpServer) {
5
+ server.registerPrompt(
6
+ 'batch-translate',
7
+ {
8
+ description: 'Translate all untranslated or stale strings in a catalog for specified languages.',
9
+ argsSchema: {
10
+ filePath: z.string().describe('Absolute path to the .xcstrings file'),
11
+ targetLanguages: z.string().describe('Comma-separated list of target language codes (e.g., "de,fr,ja")'),
12
+ includeStale: z.boolean().default(false).describe('Whether to re-translate stale entries'),
13
+ batchSize: z.number().default(20).describe('Number of keys to translate per batch (default: 20)'),
14
+ },
15
+ },
16
+ async ({ filePath, targetLanguages, includeStale, batchSize }) => {
17
+ const targetLangList = targetLanguages.split(',').map(l => l.trim()).filter(Boolean);
18
+
19
+ return {
20
+ messages: [
21
+ {
22
+ role: 'user',
23
+ content: {
24
+ type: 'text',
25
+ text: `# Batch Translation Request
26
+
27
+ ## String Catalog File
28
+ ${filePath}
29
+
30
+ ## Target Languages
31
+ ${targetLangList.map(l => `- ${l}`).join('\n')}
32
+
33
+ ## Options
34
+ - Include stale translations: ${includeStale ? 'Yes' : 'No'}
35
+ - Batch size: ${batchSize} keys per batch
36
+
37
+ ## Workflow
38
+
39
+ ### Step 1: Analyze the Catalog
40
+ Use \`get_catalog_statistics\` to understand the current translation coverage.
41
+
42
+ ### Step 2: Identify Keys Needing Translation
43
+ Use \`search_keys\` or \`list_all_keys\` to find:
44
+ - Keys with missing translations for target languages
45
+ ${includeStale ? '- Keys with stale translations that need updating' : ''}
46
+
47
+ ### Step 3: Translate in Batches
48
+ For each batch of up to ${batchSize} keys:
49
+ 1. Get the source text using \`get_translations_for_key\`
50
+ 2. Translate to all target languages
51
+ 3. Prepare the JSON payload for \`update_translations\`
52
+
53
+ ## iOS Format Placeholders Reference
54
+ Preserve these placeholders in translations:
55
+ - \`%@\` - String (object)
56
+ - \`%d\` / \`%lld\` - Integer
57
+ - \`%f\` - Float
58
+ - \`%1$@\`, \`%2$@\` - Positional (can reorder for grammar)
59
+
60
+ ## Output Format
61
+ For each batch, provide:
62
+
63
+ \`\`\`json
64
+ {
65
+ "data": [
66
+ {
67
+ "key": "key_name",
68
+ "translations": [
69
+ { "language": "de", "value": "German translation" },
70
+ { "language": "fr", "value": "French translation" }
71
+ ]
72
+ }
73
+ ]
74
+ }
75
+ \`\`\`
76
+
77
+ ## Instructions
78
+ 1. Start by analyzing the catalog
79
+ 2. Identify which keys need translation
80
+ 3. Process keys in batches
81
+ 4. After each batch, use \`update_translations\` to save
82
+ 5. Report progress after each batch
83
+
84
+ Begin the batch translation process now.`,
85
+ },
86
+ },
87
+ ],
88
+ };
89
+ }
90
+ );
91
+ }
@@ -0,0 +1,10 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { registerTranslateStringsPrompt } from './translate-strings';
3
+ import { registerReviewTranslationsPrompt } from './review-translations';
4
+ import { registerBatchTranslatePrompt } from './batch-translate';
5
+
6
+ export function registerAllPrompts(server: McpServer) {
7
+ registerTranslateStringsPrompt(server);
8
+ registerReviewTranslationsPrompt(server);
9
+ registerBatchTranslatePrompt(server);
10
+ }
@@ -0,0 +1,79 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+
4
+ export function registerReviewTranslationsPrompt(server: McpServer) {
5
+ server.registerPrompt(
6
+ 'review-translations',
7
+ {
8
+ description: 'Review existing translations for quality, consistency, and proper placeholder usage.',
9
+ argsSchema: {
10
+ filePath: z.string().describe('Absolute path to the .xcstrings file'),
11
+ languages: z.string().optional().describe('Comma-separated list of language codes to review (default: all)'),
12
+ focusAreas: z.string().optional().describe('Comma-separated areas to focus on (e.g., "placeholders,consistency,tone")'),
13
+ },
14
+ },
15
+ async ({ filePath, languages, focusAreas }) => {
16
+ const langSection = languages
17
+ ? `\n## Languages to Review\n${languages.split(',').map(l => `- ${l.trim()}`).join('\n')}\n`
18
+ : '\n## Languages to Review\nAll available languages in the catalog.\n';
19
+
20
+ const focusSection = focusAreas
21
+ ? `\n## Focus Areas\n${focusAreas.split(',').map(f => `- ${f.trim()}`).join('\n')}\n`
22
+ : '';
23
+
24
+ return {
25
+ messages: [
26
+ {
27
+ role: 'user',
28
+ content: {
29
+ type: 'text',
30
+ text: `# Translation Review Request
31
+
32
+ ## String Catalog File
33
+ ${filePath}
34
+ ${langSection}${focusSection}
35
+ ## Review Checklist
36
+
37
+ ### 1. Format Placeholder Verification
38
+ - Ensure all \`%@\`, \`%d\`, \`%lld\`, \`%f\` placeholders are preserved
39
+ - Verify positional arguments (\`%1$@\`, \`%2$@\`) are used correctly
40
+ - Check that placeholder count matches the source string
41
+
42
+ ### 2. Translation Quality
43
+ - Verify translations are accurate and natural-sounding
44
+ - Check for grammatical errors
45
+ - Ensure translations fit the context of a mobile app UI
46
+
47
+ ### 3. Consistency
48
+ - Similar strings should have consistent translations
49
+ - Terminology should be uniform across the app
50
+ - UI element names should match platform conventions
51
+
52
+ ### 4. Cultural Appropriateness
53
+ - Verify idioms are properly localized
54
+ - Check for culturally sensitive content
55
+ - Ensure date/number formats are appropriate
56
+
57
+ ### 5. Length Considerations
58
+ - Flag translations that are significantly longer than source
59
+ - Consider UI space constraints for mobile apps
60
+
61
+ ## Instructions
62
+ 1. Use the \`get_catalog_statistics\` tool to see overall translation coverage
63
+ 2. Use the \`list_all_keys\` tool to see available keys
64
+ 3. Use the \`get_translations_for_key\` tool to examine specific translations
65
+ 4. Report any issues found with specific keys and languages
66
+ 5. Suggest corrections using the \`update_translations\` tool format
67
+
68
+ ## Output Format
69
+ Provide a structured review report:
70
+ - Summary of findings
71
+ - List of issues by severity (critical, warning, suggestion)
72
+ - Recommended fixes in JSON format for the update_translations tool`,
73
+ },
74
+ },
75
+ ],
76
+ };
77
+ }
78
+ );
79
+ }
@@ -0,0 +1,85 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+
4
+ export function registerTranslateStringsPrompt(server: McpServer) {
5
+ server.registerPrompt(
6
+ 'translate-strings',
7
+ {
8
+ description: 'Generate translations for iOS string catalog keys. Provides guidance on format placeholders and returns structured JSON for the update_translations tool.',
9
+ argsSchema: {
10
+ keys: z.string().describe('Comma-separated list of keys to translate (e.g., "hello_world,goodbye,welcome_message")'),
11
+ sourceLanguage: z.string().default('en').describe('Source language code (default: en)'),
12
+ targetLanguages: z.string().describe('Comma-separated list of target language codes (e.g., "de,fr,ja,zh-Hans")'),
13
+ context: z.string().optional().describe('Optional context about where these strings are used in the app'),
14
+ },
15
+ },
16
+ async ({ keys, sourceLanguage, targetLanguages, context }) => {
17
+ const keyList = keys.split(',').map(k => k.trim()).filter(Boolean);
18
+ const targetLangList = targetLanguages.split(',').map(l => l.trim()).filter(Boolean);
19
+
20
+ const contextSection = context
21
+ ? `\n## Context\nThese strings are used in: ${context}\n`
22
+ : '';
23
+
24
+ return {
25
+ messages: [
26
+ {
27
+ role: 'user',
28
+ content: {
29
+ type: 'text',
30
+ text: `# Translation Request for iOS String Catalog
31
+
32
+ ## Keys to Translate
33
+ ${keyList.map(k => `- "${k}"`).join('\n')}
34
+
35
+ ## Source Language
36
+ ${sourceLanguage}
37
+
38
+ ## Target Languages
39
+ ${targetLangList.map(l => `- ${l}`).join('\n')}
40
+ ${contextSection}
41
+ ## iOS Format Placeholders
42
+ When translating, preserve these iOS format placeholders exactly as they appear:
43
+ - \`%@\` - String placeholder (objects)
44
+ - \`%d\` or \`%lld\` - Integer placeholder
45
+ - \`%f\` - Floating point number placeholder
46
+ - \`%1$@\`, \`%2$@\` - Positional arguments (order CAN be changed to fit natural language grammar)
47
+
48
+ ## Example
49
+ If source is: "Hello %@, you have %lld items"
50
+ German could be: "Hallo %@, Sie haben %lld Artikel"
51
+ Japanese could be: "%@さん、%lld個のアイテムがあります"
52
+
53
+ ## Instructions
54
+ 1. Translate each key into all target languages
55
+ 2. Preserve all format placeholders
56
+ 3. Ensure translations sound natural in each language
57
+ 4. Consider cultural context and localization best practices
58
+
59
+ ## Required Output Format
60
+ Return the translations as JSON that can be used with the \`update_translations\` tool:
61
+
62
+ \`\`\`json
63
+ {
64
+ "data": [
65
+ {
66
+ "key": "key_name",
67
+ "translations": [
68
+ { "language": "en", "value": "English text" },
69
+ { "language": "de", "value": "German text" },
70
+ { "language": "fr", "value": "French text" }
71
+ ],
72
+ "comment": "Optional: describe where this string is used"
73
+ }
74
+ ]
75
+ }
76
+ \`\`\`
77
+
78
+ Please translate the keys now.`,
79
+ },
80
+ },
81
+ ],
82
+ };
83
+ }
84
+ );
85
+ }