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.
- package/.prettierrc +8 -0
- package/README.md +127 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +23 -0
- package/dist/mcp/prompts/batch-translate.d.ts +2 -0
- package/dist/mcp/prompts/batch-translate.js +88 -0
- package/dist/mcp/prompts/index.d.ts +2 -0
- package/dist/mcp/prompts/index.js +12 -0
- package/dist/mcp/prompts/review-translations.d.ts +2 -0
- package/dist/mcp/prompts/review-translations.js +75 -0
- package/dist/mcp/prompts/translate-strings.d.ts +2 -0
- package/dist/mcp/prompts/translate-strings.js +81 -0
- package/dist/mcp/tools/get-catalog-statistics.d.ts +2 -0
- package/dist/mcp/tools/get-catalog-statistics.js +25 -0
- package/dist/mcp/tools/get-translations-for-key.d.ts +2 -0
- package/dist/mcp/tools/get-translations-for-key.js +36 -0
- package/dist/mcp/tools/index.d.ts +2 -0
- package/dist/mcp/tools/index.js +18 -0
- package/dist/mcp/tools/list-all-keys.d.ts +2 -0
- package/dist/mcp/tools/list-all-keys.js +44 -0
- package/dist/mcp/tools/list-supported-languages.d.ts +2 -0
- package/dist/mcp/tools/list-supported-languages.js +30 -0
- package/dist/mcp/tools/search-keys.d.ts +2 -0
- package/dist/mcp/tools/search-keys.js +32 -0
- package/dist/mcp/tools/update-translations.d.ts +2 -0
- package/dist/mcp/tools/update-translations.js +78 -0
- package/dist/string-catalog.d.ts +53 -0
- package/dist/string-catalog.js +220 -0
- package/dist/tools/get-catalog-statistics.d.ts +2 -0
- package/dist/tools/get-catalog-statistics.js +25 -0
- package/dist/tools/get-translations-for-key.d.ts +2 -0
- package/dist/tools/get-translations-for-key.js +36 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +18 -0
- package/dist/tools/list-all-keys.d.ts +2 -0
- package/dist/tools/list-all-keys.js +44 -0
- package/dist/tools/list-supported-languages.d.ts +2 -0
- package/dist/tools/list-supported-languages.js +30 -0
- package/dist/tools/search-keys.d.ts +2 -0
- package/dist/tools/search-keys.js +32 -0
- package/dist/tools/update-translations.d.ts +2 -0
- package/dist/tools/update-translations.js +78 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.js +6 -0
- package/eslint.config.js +23 -0
- package/images/mcp.jpeg +0 -0
- package/package.json +49 -0
- package/src/index.ts +25 -0
- package/src/mcp/prompts/batch-translate.ts +91 -0
- package/src/mcp/prompts/index.ts +10 -0
- package/src/mcp/prompts/review-translations.ts +79 -0
- package/src/mcp/prompts/translate-strings.ts +85 -0
- package/src/mcp/tools/get-catalog-statistics.ts +29 -0
- package/src/mcp/tools/get-translations-for-key.ts +45 -0
- package/src/mcp/tools/index.ts +16 -0
- package/src/mcp/tools/list-all-keys.ts +52 -0
- package/src/mcp/tools/list-supported-languages.ts +38 -0
- package/src/mcp/tools/search-keys.ts +40 -0
- package/src/mcp/tools/update-translations.ts +89 -0
- package/src/string-catalog.ts +232 -0
- package/src/types.ts +82 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { StringCatalog } from '../../string-catalog';
|
|
4
|
+
|
|
5
|
+
export function registerGetCatalogStatistics(server: McpServer) {
|
|
6
|
+
server.registerTool(
|
|
7
|
+
'get_catalog_statistics',
|
|
8
|
+
{
|
|
9
|
+
description:
|
|
10
|
+
'Get statistics about a String Catalog including total keys, supported languages, and translation coverage percentage for each language.',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
filePath: z.string().describe('Absolute path to the .xcstrings file'),
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
async ({ filePath }) => {
|
|
16
|
+
const catalog = new StringCatalog(filePath);
|
|
17
|
+
const stats = catalog.getStatistics();
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: 'text' as const,
|
|
23
|
+
text: JSON.stringify(stats, null, 2),
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { StringCatalog } from '../../string-catalog';
|
|
4
|
+
|
|
5
|
+
export function registerGetTranslationsForKey(server: McpServer) {
|
|
6
|
+
server.registerTool(
|
|
7
|
+
'get_translations_for_key',
|
|
8
|
+
{
|
|
9
|
+
description:
|
|
10
|
+
'Get all translations for a specific key in a String Catalog. Shows the translated text in each supported language along with the translation state.',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
filePath: z.string().describe('Absolute path to the .xcstrings file'),
|
|
13
|
+
key: z.string().describe('The localization key to look up'),
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
async ({ filePath, key }) => {
|
|
17
|
+
const catalog = new StringCatalog(filePath);
|
|
18
|
+
const result = catalog.getTranslationsForKey(key);
|
|
19
|
+
|
|
20
|
+
if (!result) {
|
|
21
|
+
return {
|
|
22
|
+
content: [
|
|
23
|
+
{
|
|
24
|
+
type: 'text' as const,
|
|
25
|
+
text: JSON.stringify(
|
|
26
|
+
{ error: `Key "${key}" not found in catalog` },
|
|
27
|
+
null,
|
|
28
|
+
2
|
|
29
|
+
),
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
type: 'text' as const,
|
|
39
|
+
text: JSON.stringify(result, null, 2),
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { registerListSupportedLanguages } from './list-supported-languages';
|
|
3
|
+
import { registerGetTranslationsForKey } from './get-translations-for-key';
|
|
4
|
+
import { registerSearchKeys } from './search-keys';
|
|
5
|
+
import { registerUpdateTranslations } from './update-translations';
|
|
6
|
+
import { registerGetCatalogStatistics } from './get-catalog-statistics';
|
|
7
|
+
import { registerListAllKeys } from './list-all-keys';
|
|
8
|
+
|
|
9
|
+
export function registerAllTools(server: McpServer) {
|
|
10
|
+
registerListSupportedLanguages(server);
|
|
11
|
+
registerGetTranslationsForKey(server);
|
|
12
|
+
registerSearchKeys(server);
|
|
13
|
+
registerUpdateTranslations(server);
|
|
14
|
+
registerGetCatalogStatistics(server);
|
|
15
|
+
registerListAllKeys(server);
|
|
16
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { StringCatalog } from '../../string-catalog';
|
|
4
|
+
|
|
5
|
+
export function registerListAllKeys(server: McpServer) {
|
|
6
|
+
server.registerTool(
|
|
7
|
+
'list_all_keys',
|
|
8
|
+
{
|
|
9
|
+
description:
|
|
10
|
+
'List all localization keys in a String Catalog. Returns keys sorted alphabetically.',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
filePath: z.string().describe('Absolute path to the .xcstrings file'),
|
|
13
|
+
limit: z
|
|
14
|
+
.number()
|
|
15
|
+
.optional()
|
|
16
|
+
.default(100)
|
|
17
|
+
.describe('Maximum number of keys to return (default: 100)'),
|
|
18
|
+
offset: z
|
|
19
|
+
.number()
|
|
20
|
+
.optional()
|
|
21
|
+
.default(0)
|
|
22
|
+
.describe('Number of keys to skip (for pagination, default: 0)'),
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
async ({ filePath, limit: limitArg, offset: offsetArg }) => {
|
|
26
|
+
const limit = limitArg ?? 100;
|
|
27
|
+
const offset = offsetArg ?? 0;
|
|
28
|
+
const catalog = new StringCatalog(filePath);
|
|
29
|
+
const allKeys = catalog.getAllKeys();
|
|
30
|
+
const paginatedKeys = allKeys.slice(offset, offset + limit);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
content: [
|
|
34
|
+
{
|
|
35
|
+
type: 'text' as const,
|
|
36
|
+
text: JSON.stringify(
|
|
37
|
+
{
|
|
38
|
+
keys: paginatedKeys,
|
|
39
|
+
total: allKeys.length,
|
|
40
|
+
offset,
|
|
41
|
+
limit,
|
|
42
|
+
hasMore: offset + limit < allKeys.length,
|
|
43
|
+
},
|
|
44
|
+
null,
|
|
45
|
+
2
|
|
46
|
+
),
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { StringCatalog } from '../../string-catalog';
|
|
4
|
+
|
|
5
|
+
export function registerListSupportedLanguages(server: McpServer) {
|
|
6
|
+
server.registerTool(
|
|
7
|
+
'list_supported_languages',
|
|
8
|
+
{
|
|
9
|
+
description:
|
|
10
|
+
'List all supported languages in a given Xcode String Catalog (.xcstrings) file. Returns the source language and all languages that have translations.',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
filePath: z.string().describe('Absolute path to the .xcstrings file'),
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
async ({ filePath }) => {
|
|
16
|
+
const catalog = new StringCatalog(filePath);
|
|
17
|
+
const languages = catalog.getSupportedLanguages();
|
|
18
|
+
const sourceLanguage = catalog.getSourceLanguage();
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
content: [
|
|
22
|
+
{
|
|
23
|
+
type: 'text' as const,
|
|
24
|
+
text: JSON.stringify(
|
|
25
|
+
{
|
|
26
|
+
sourceLanguage,
|
|
27
|
+
supportedLanguages: languages,
|
|
28
|
+
count: languages.length,
|
|
29
|
+
},
|
|
30
|
+
null,
|
|
31
|
+
2
|
|
32
|
+
),
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { StringCatalog } from '../../string-catalog';
|
|
4
|
+
|
|
5
|
+
export function registerSearchKeys(server: McpServer) {
|
|
6
|
+
server.registerTool(
|
|
7
|
+
'search_keys',
|
|
8
|
+
{
|
|
9
|
+
description:
|
|
10
|
+
'Search for localization keys containing a specific substring. Useful for finding keys when you only know part of the key name.',
|
|
11
|
+
inputSchema: {
|
|
12
|
+
filePath: z.string().describe('Absolute path to the .xcstrings file'),
|
|
13
|
+
query: z
|
|
14
|
+
.string()
|
|
15
|
+
.describe('Substring to search for in key names (case-insensitive)'),
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
async ({ filePath, query }) => {
|
|
19
|
+
const catalog = new StringCatalog(filePath);
|
|
20
|
+
const keys = catalog.searchKeys(query);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
content: [
|
|
24
|
+
{
|
|
25
|
+
type: 'text' as const,
|
|
26
|
+
text: JSON.stringify(
|
|
27
|
+
{
|
|
28
|
+
query,
|
|
29
|
+
matchingKeys: keys,
|
|
30
|
+
count: keys.length,
|
|
31
|
+
},
|
|
32
|
+
null,
|
|
33
|
+
2
|
|
34
|
+
),
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { StringCatalog } from '../../string-catalog';
|
|
4
|
+
|
|
5
|
+
const toolDescription = `Update or add translations to a String Catalog. Accepts an array of translation entries.
|
|
6
|
+
|
|
7
|
+
IMPORTANT: iOS strings support format placeholders that must be preserved in translations:
|
|
8
|
+
- %@ for strings (objects)
|
|
9
|
+
- %d or %lld for integers
|
|
10
|
+
- %f for floating point numbers
|
|
11
|
+
- %1$@, %2$@ etc. for positional arguments (order can be changed in translations)
|
|
12
|
+
|
|
13
|
+
Example input:
|
|
14
|
+
{
|
|
15
|
+
"data": [
|
|
16
|
+
{
|
|
17
|
+
"key": "hello_world",
|
|
18
|
+
"translations": [
|
|
19
|
+
{ "language": "en", "value": "Hello World" },
|
|
20
|
+
{ "language": "de", "value": "Hallo Welt" },
|
|
21
|
+
{ "language": "no", "value": "Hei Verden" }
|
|
22
|
+
],
|
|
23
|
+
"comment": "Greeting message shown on home screen"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"key": "items_count",
|
|
27
|
+
"translations": [
|
|
28
|
+
{ "language": "en", "value": "%lld items" },
|
|
29
|
+
{ "language": "de", "value": "%lld Elemente" }
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}`;
|
|
34
|
+
|
|
35
|
+
const translationSchema = z.object({
|
|
36
|
+
language: z.string().describe('Language code (e.g., "en", "de", "no", "vi")'),
|
|
37
|
+
value: z
|
|
38
|
+
.string()
|
|
39
|
+
.describe('The translated text. Preserve any format placeholders like %@, %lld, %d'),
|
|
40
|
+
state: z
|
|
41
|
+
.enum(['new', 'translated', 'needs_review', 'stale'])
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('Translation state (defaults to "translated")'),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const translationsSchema = z.object({
|
|
47
|
+
key: z.string().describe('The localization key'),
|
|
48
|
+
translations: z.array(translationSchema).describe('Array of language translations'),
|
|
49
|
+
comment: z.string().optional().describe('Optional comment describing the string context'),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const inputSchema = z.object({
|
|
53
|
+
filePath: z.string().describe('Absolute path to the .xcstrings file'),
|
|
54
|
+
data: z.array(translationsSchema).describe('Array of translation entries to add or update'),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export function registerUpdateTranslations(server: McpServer) {
|
|
58
|
+
server.registerTool(
|
|
59
|
+
'update_translations',
|
|
60
|
+
{
|
|
61
|
+
description: toolDescription,
|
|
62
|
+
inputSchema,
|
|
63
|
+
},
|
|
64
|
+
async ({ filePath, data }) => {
|
|
65
|
+
const catalog = new StringCatalog(filePath);
|
|
66
|
+
const result = catalog.updateTranslations(data);
|
|
67
|
+
catalog.save();
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
content: [
|
|
71
|
+
{
|
|
72
|
+
type: 'text' as const,
|
|
73
|
+
text: JSON.stringify(
|
|
74
|
+
{
|
|
75
|
+
success: true,
|
|
76
|
+
updatedKeys: result.updated,
|
|
77
|
+
createdKeys: result.created,
|
|
78
|
+
totalUpdated: result.updated.length,
|
|
79
|
+
totalCreated: result.created.length,
|
|
80
|
+
},
|
|
81
|
+
null,
|
|
82
|
+
2
|
|
83
|
+
),
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import {
|
|
4
|
+
XCStrings,
|
|
5
|
+
StringEntry,
|
|
6
|
+
TranslationInput,
|
|
7
|
+
KeyTranslationsResult,
|
|
8
|
+
KeyTranslation,
|
|
9
|
+
LocalizationState,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* StringCatalog class for reading and manipulating .xcstrings files
|
|
14
|
+
*/
|
|
15
|
+
export class StringCatalog {
|
|
16
|
+
private filePath: string;
|
|
17
|
+
private data: XCStrings;
|
|
18
|
+
|
|
19
|
+
constructor(filePath: string) {
|
|
20
|
+
this.filePath = path.resolve(filePath);
|
|
21
|
+
this.data = this.load();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private load(): XCStrings {
|
|
25
|
+
if (!fs.existsSync(this.filePath)) {
|
|
26
|
+
throw new Error(`String catalog file not found: ${this.filePath}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const content = fs.readFileSync(this.filePath, 'utf-8');
|
|
30
|
+
return JSON.parse(content) as XCStrings;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the source language of the catalog
|
|
35
|
+
*/
|
|
36
|
+
getSourceLanguage(): string {
|
|
37
|
+
return this.data.sourceLanguage;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get all supported languages in the catalog
|
|
42
|
+
*/
|
|
43
|
+
getSupportedLanguages(): string[] {
|
|
44
|
+
const languages = new Set<string>();
|
|
45
|
+
languages.add(this.data.sourceLanguage);
|
|
46
|
+
|
|
47
|
+
for (const key in this.data.strings) {
|
|
48
|
+
const entry = this.data.strings[key];
|
|
49
|
+
if (entry.localizations) {
|
|
50
|
+
for (const lang of Object.keys(entry.localizations)) {
|
|
51
|
+
languages.add(lang);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return Array.from(languages).sort();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get all keys in the catalog
|
|
61
|
+
*/
|
|
62
|
+
getAllKeys(): string[] {
|
|
63
|
+
return Object.keys(this.data.strings).sort();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get translations for a specific key
|
|
68
|
+
*/
|
|
69
|
+
getTranslationsForKey(key: string): KeyTranslationsResult | null {
|
|
70
|
+
const entry = this.data.strings[key];
|
|
71
|
+
if (!entry) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const translations: KeyTranslation[] = [];
|
|
76
|
+
|
|
77
|
+
if (entry.localizations) {
|
|
78
|
+
for (const [language, localization] of Object.entries(entry.localizations)) {
|
|
79
|
+
if (localization.stringUnit) {
|
|
80
|
+
translations.push({
|
|
81
|
+
language,
|
|
82
|
+
value: localization.stringUnit.value,
|
|
83
|
+
state: localization.stringUnit.state,
|
|
84
|
+
});
|
|
85
|
+
} else if (localization.variations?.plural) {
|
|
86
|
+
// For plural strings, show the "other" form as the primary value
|
|
87
|
+
const plural = localization.variations.plural;
|
|
88
|
+
const primaryForm = plural.other || plural.one || plural.zero;
|
|
89
|
+
if (primaryForm) {
|
|
90
|
+
translations.push({
|
|
91
|
+
language,
|
|
92
|
+
value: `[plural] ${primaryForm.stringUnit.value}`,
|
|
93
|
+
state: primaryForm.stringUnit.state,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
key,
|
|
102
|
+
sourceLanguage: this.data.sourceLanguage,
|
|
103
|
+
translations: translations.sort((a, b) => a.language.localeCompare(b.language)),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Search for keys containing a specific substring
|
|
109
|
+
*/
|
|
110
|
+
searchKeys(query: string): string[] {
|
|
111
|
+
const lowerQuery = query.toLowerCase();
|
|
112
|
+
return this.getAllKeys().filter((key) => key.toLowerCase().includes(lowerQuery));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Update translations for one or more keys
|
|
117
|
+
*/
|
|
118
|
+
updateTranslations(translations: TranslationInput[]): { updated: string[]; created: string[] } {
|
|
119
|
+
const updated: string[] = [];
|
|
120
|
+
const created: string[] = [];
|
|
121
|
+
|
|
122
|
+
for (const translation of translations) {
|
|
123
|
+
const { key, translations: langTranslations, comment } = translation;
|
|
124
|
+
|
|
125
|
+
const isNew = !this.data.strings[key];
|
|
126
|
+
|
|
127
|
+
if (isNew) {
|
|
128
|
+
this.data.strings[key] = {};
|
|
129
|
+
created.push(key);
|
|
130
|
+
} else {
|
|
131
|
+
updated.push(key);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const entry = this.data.strings[key];
|
|
135
|
+
|
|
136
|
+
if (comment) {
|
|
137
|
+
entry.comment = comment;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!entry.localizations) {
|
|
141
|
+
entry.localizations = {};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const langTrans of langTranslations) {
|
|
145
|
+
entry.localizations[langTrans.language] = {
|
|
146
|
+
stringUnit: {
|
|
147
|
+
state: langTrans.state || 'translated',
|
|
148
|
+
value: langTrans.value,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { updated, created };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Save changes to the file
|
|
159
|
+
*/
|
|
160
|
+
save(): void {
|
|
161
|
+
// Sort keys alphabetically for consistent output
|
|
162
|
+
const sortedStrings: { [key: string]: StringEntry } = {};
|
|
163
|
+
const keys = Object.keys(this.data.strings).sort();
|
|
164
|
+
|
|
165
|
+
for (const key of keys) {
|
|
166
|
+
sortedStrings[key] = this.data.strings[key];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const output: XCStrings = {
|
|
170
|
+
sourceLanguage: this.data.sourceLanguage,
|
|
171
|
+
strings: sortedStrings,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
if (this.data.version) {
|
|
175
|
+
output.version = this.data.version;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Write with 2-space indentation to match Xcode format
|
|
179
|
+
fs.writeFileSync(this.filePath, JSON.stringify(output, null, 2) + '\n', 'utf-8');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get statistics about the catalog
|
|
184
|
+
*/
|
|
185
|
+
getStatistics(): {
|
|
186
|
+
totalKeys: number;
|
|
187
|
+
languages: string[];
|
|
188
|
+
translationCoverage: Record<
|
|
189
|
+
string,
|
|
190
|
+
{ translated: number; total: number; percentage: number }
|
|
191
|
+
>;
|
|
192
|
+
} {
|
|
193
|
+
const languages = this.getSupportedLanguages();
|
|
194
|
+
const allKeys = this.getAllKeys();
|
|
195
|
+
const totalKeys = allKeys.length;
|
|
196
|
+
|
|
197
|
+
const translationCoverage: Record<
|
|
198
|
+
string,
|
|
199
|
+
{ translated: number; total: number; percentage: number }
|
|
200
|
+
> = {};
|
|
201
|
+
|
|
202
|
+
for (const lang of languages) {
|
|
203
|
+
let translated = 0;
|
|
204
|
+
|
|
205
|
+
for (const key of allKeys) {
|
|
206
|
+
const entry = this.data.strings[key];
|
|
207
|
+
const localization = entry.localizations?.[lang];
|
|
208
|
+
|
|
209
|
+
if (localization?.stringUnit?.state === 'translated') {
|
|
210
|
+
translated++;
|
|
211
|
+
} else if (localization?.variations?.plural) {
|
|
212
|
+
// Count plural as translated if it has an "other" form
|
|
213
|
+
if (localization.variations.plural.other?.stringUnit?.state === 'translated') {
|
|
214
|
+
translated++;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
translationCoverage[lang] = {
|
|
220
|
+
translated,
|
|
221
|
+
total: totalKeys,
|
|
222
|
+
percentage: totalKeys > 0 ? Math.round((translated / totalKeys) * 100) : 0,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
totalKeys,
|
|
228
|
+
languages,
|
|
229
|
+
translationCoverage,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for Xcode String Catalog (.xcstrings) file format
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** State of a localization string */
|
|
6
|
+
export type LocalizationState = 'new' | 'translated' | 'needs_review' | 'stale';
|
|
7
|
+
|
|
8
|
+
/** A single string unit containing the translation value and state */
|
|
9
|
+
export interface StringUnit {
|
|
10
|
+
state: LocalizationState;
|
|
11
|
+
value: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Localization entry for a single language */
|
|
15
|
+
export interface LocalizationEntry {
|
|
16
|
+
stringUnit?: StringUnit;
|
|
17
|
+
/** For plural variations */
|
|
18
|
+
variations?: {
|
|
19
|
+
plural?: {
|
|
20
|
+
zero?: { stringUnit: StringUnit };
|
|
21
|
+
one?: { stringUnit: StringUnit };
|
|
22
|
+
two?: { stringUnit: StringUnit };
|
|
23
|
+
few?: { stringUnit: StringUnit };
|
|
24
|
+
many?: { stringUnit: StringUnit };
|
|
25
|
+
other?: { stringUnit: StringUnit };
|
|
26
|
+
};
|
|
27
|
+
device?: Record<string, { stringUnit: StringUnit }>;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** All localizations for a single string key */
|
|
32
|
+
export interface StringLocalizations {
|
|
33
|
+
[languageCode: string]: LocalizationEntry;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A single string entry in the catalog */
|
|
37
|
+
export interface StringEntry {
|
|
38
|
+
comment?: string;
|
|
39
|
+
extractionState?: 'manual' | 'extracted_with_value' | 'stale';
|
|
40
|
+
localizations?: StringLocalizations;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Root structure of an .xcstrings file */
|
|
44
|
+
export interface XCStrings {
|
|
45
|
+
sourceLanguage: string;
|
|
46
|
+
version?: string;
|
|
47
|
+
strings: {
|
|
48
|
+
[key: string]: StringEntry;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Translation input format for the update tool
|
|
54
|
+
*/
|
|
55
|
+
export interface TranslationInput {
|
|
56
|
+
key: string;
|
|
57
|
+
translations: {
|
|
58
|
+
language: string;
|
|
59
|
+
value: string;
|
|
60
|
+
state?: LocalizationState;
|
|
61
|
+
}[];
|
|
62
|
+
comment?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface TranslationData {
|
|
66
|
+
data: TranslationInput[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Output format for listing translations of a key
|
|
71
|
+
*/
|
|
72
|
+
export interface KeyTranslation {
|
|
73
|
+
language: string;
|
|
74
|
+
value: string;
|
|
75
|
+
state: LocalizationState;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface KeyTranslationsResult {
|
|
79
|
+
key: string;
|
|
80
|
+
sourceLanguage: string;
|
|
81
|
+
translations: KeyTranslation[];
|
|
82
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noImplicitAny": true,
|
|
9
|
+
"strictNullChecks": true,
|
|
10
|
+
"noImplicitThis": true,
|
|
11
|
+
"alwaysStrict": true,
|
|
12
|
+
"noUnusedLocals": false,
|
|
13
|
+
"noUnusedParameters": false,
|
|
14
|
+
"noImplicitReturns": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": false,
|
|
16
|
+
"inlineSourceMap": true,
|
|
17
|
+
"inlineSources": true,
|
|
18
|
+
"experimentalDecorators": true,
|
|
19
|
+
"strictPropertyInitialization": false,
|
|
20
|
+
"outDir": "./dist",
|
|
21
|
+
"rootDir": "./src",
|
|
22
|
+
"skipLibCheck": true,
|
|
23
|
+
"esModuleInterop": true,
|
|
24
|
+
"resolveJsonModule": true
|
|
25
|
+
},
|
|
26
|
+
"include": ["src/**/*"],
|
|
27
|
+
"exclude": ["node_modules", "dist"]
|
|
28
|
+
}
|