@questlang/mcp-server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +88 -0
- package/SKILL.md +50 -0
- package/dist/client.d.ts +31 -0
- package/dist/client.js +120 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +105 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +1 -0
- package/dist/init.js +84 -0
- package/dist/init.js.map +1 -0
- package/dist/parsers/android-xml.d.ts +2 -0
- package/dist/parsers/android-xml.js +41 -0
- package/dist/parsers/android-xml.js.map +1 -0
- package/dist/parsers/csv.d.ts +2 -0
- package/dist/parsers/csv.js +55 -0
- package/dist/parsers/csv.js.map +1 -0
- package/dist/parsers/index.d.ts +5 -0
- package/dist/parsers/index.js +55 -0
- package/dist/parsers/index.js.map +1 -0
- package/dist/parsers/ios-strings.d.ts +2 -0
- package/dist/parsers/ios-strings.js +42 -0
- package/dist/parsers/ios-strings.js.map +1 -0
- package/dist/parsers/json.d.ts +2 -0
- package/dist/parsers/json.js +41 -0
- package/dist/parsers/json.js.map +1 -0
- package/dist/parsers/po.d.ts +2 -0
- package/dist/parsers/po.js +77 -0
- package/dist/parsers/po.js.map +1 -0
- package/dist/parsers/xliff.d.ts +2 -0
- package/dist/parsers/xliff.js +48 -0
- package/dist/parsers/xliff.js.map +1 -0
- package/dist/tools/balance.d.ts +4 -0
- package/dist/tools/balance.js +4 -0
- package/dist/tools/balance.js.map +1 -0
- package/dist/tools/estimate.d.ts +11 -0
- package/dist/tools/estimate.js +43 -0
- package/dist/tools/estimate.js.map +1 -0
- package/dist/tools/glossary.d.ts +7 -0
- package/dist/tools/glossary.js +28 -0
- package/dist/tools/glossary.js.map +1 -0
- package/dist/tools/orders.d.ts +10 -0
- package/dist/tools/orders.js +13 -0
- package/dist/tools/orders.js.map +1 -0
- package/dist/tools/review.d.ts +3 -0
- package/dist/tools/review.js +52 -0
- package/dist/tools/review.js.map +1 -0
- package/dist/tools/scan.d.ts +12 -0
- package/dist/tools/scan.js +65 -0
- package/dist/tools/scan.js.map +1 -0
- package/dist/tools/status.d.ts +12 -0
- package/dist/tools/status.js +41 -0
- package/dist/tools/status.js.map +1 -0
- package/dist/tools/translate.d.ts +8 -0
- package/dist/tools/translate.js +21 -0
- package/dist/tools/translate.js.map +1 -0
- package/dist/types.d.ts +38 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 QuestLang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# @questlang/mcp-server
|
|
2
|
+
|
|
3
|
+
MCP server for AI-powered translation of i18n/localization files. Works with Claude Code, Cursor, and VS Code.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx -y @questlang/mcp-server init
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This creates:
|
|
12
|
+
- `.mcp.json` — MCP server configuration for your IDE
|
|
13
|
+
- `.claude/commands/translate.md` — the `/translate` skill for Claude Code
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
1. Create an account at [questlang.ru](https://questlang.ru)
|
|
18
|
+
2. Get an API key from [API Keys](https://questlang.ru/api-keys)
|
|
19
|
+
3. Set the key:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
export QUESTLANG_API_KEY=ql_your_key_here
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or paste it into the `QUESTLANG_API_KEY` field in `.mcp.json`.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
In Claude Code, use the `/translate` slash command:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
/translate Spanish, French, Japanese
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or ask in natural language:
|
|
36
|
+
|
|
37
|
+
> Scan my project for localization files and translate them to Spanish
|
|
38
|
+
|
|
39
|
+
## Supported Formats
|
|
40
|
+
|
|
41
|
+
| Format | Extensions |
|
|
42
|
+
|--------|-----------|
|
|
43
|
+
| JSON / ARB | `.json`, `.arb` |
|
|
44
|
+
| CSV | `.csv` |
|
|
45
|
+
| Android XML | `strings.xml` |
|
|
46
|
+
| iOS Strings | `.strings` |
|
|
47
|
+
| GNU gettext | `.po`, `.pot` |
|
|
48
|
+
| XLIFF | `.xliff`, `.xlf` |
|
|
49
|
+
|
|
50
|
+
## Available MCP Tools
|
|
51
|
+
|
|
52
|
+
| Tool | Description |
|
|
53
|
+
|------|-------------|
|
|
54
|
+
| `questlang_scan_i18n_files` | Scan directory for i18n files |
|
|
55
|
+
| `questlang_check_balance` | Check token balance |
|
|
56
|
+
| `questlang_estimate_cost` | Estimate translation cost |
|
|
57
|
+
| `questlang_translate_file` | Submit file for translation |
|
|
58
|
+
| `questlang_get_order_status` | Check translation status |
|
|
59
|
+
| `questlang_list_orders` | List recent orders |
|
|
60
|
+
| `questlang_update_glossary` | Add glossary terms |
|
|
61
|
+
| `questlang_request_review` | Request AI quality review |
|
|
62
|
+
| `questlang_get_review` | Get review report |
|
|
63
|
+
|
|
64
|
+
## 20 Languages
|
|
65
|
+
|
|
66
|
+
English, Russian, Spanish, French, German, Chinese (Simplified), Chinese (Traditional), Japanese, Korean, Arabic, Portuguese, Italian, Turkish, Hindi, Dutch, Polish, Swedish, Greek, Hebrew, Indonesian, Thai.
|
|
67
|
+
|
|
68
|
+
## Manual Setup
|
|
69
|
+
|
|
70
|
+
If you prefer to configure manually, add to `.mcp.json` in your project root:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"mcpServers": {
|
|
75
|
+
"questlang": {
|
|
76
|
+
"command": "npx",
|
|
77
|
+
"args": ["-y", "@questlang/mcp-server"],
|
|
78
|
+
"env": {
|
|
79
|
+
"QUESTLANG_API_KEY": "ql_your_key_here"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Translate i18n files with QuestLang
|
|
2
|
+
|
|
3
|
+
You are a translation assistant using the QuestLang MCP tools. Follow this workflow to translate the user's localization files.
|
|
4
|
+
|
|
5
|
+
## Arguments
|
|
6
|
+
|
|
7
|
+
$ARGUMENTS
|
|
8
|
+
|
|
9
|
+
If arguments are provided, use them as target languages and/or file paths. Otherwise, follow the interactive flow below.
|
|
10
|
+
|
|
11
|
+
## Workflow
|
|
12
|
+
|
|
13
|
+
### Step 1: Scan for i18n files
|
|
14
|
+
|
|
15
|
+
Use `questlang_scan_i18n_files` to scan the project directory for localization files. Present the results to the user:
|
|
16
|
+
- File path, format, and number of keys
|
|
17
|
+
- Highlight which files look like source (vs already-translated) files
|
|
18
|
+
|
|
19
|
+
### Step 2: Select files and languages
|
|
20
|
+
|
|
21
|
+
Ask the user to confirm:
|
|
22
|
+
- Which file(s) to translate (default: the source file found in Step 1)
|
|
23
|
+
- Which target languages (e.g. "Spanish, French, Japanese")
|
|
24
|
+
- Source language if not obvious (auto-detected by default)
|
|
25
|
+
|
|
26
|
+
### Step 3: Check balance and estimate cost
|
|
27
|
+
|
|
28
|
+
1. Use `questlang_check_balance` to show current token balance
|
|
29
|
+
2. Use `questlang_estimate_cost` with the selected file and languages to show the estimated cost
|
|
30
|
+
3. If balance is insufficient, tell the user to top up at https://questlang.ru and stop
|
|
31
|
+
|
|
32
|
+
### Step 4: Submit translation
|
|
33
|
+
|
|
34
|
+
After the user confirms:
|
|
35
|
+
1. Use `questlang_translate_file` with the selected file, languages, and any options (model, game context)
|
|
36
|
+
2. Save the returned order ID
|
|
37
|
+
|
|
38
|
+
### Step 5: Poll for completion
|
|
39
|
+
|
|
40
|
+
1. Wait 10 seconds, then use `questlang_get_order_status` to check progress
|
|
41
|
+
2. Report the progress percentage to the user
|
|
42
|
+
3. If not complete, wait another 15 seconds and check again
|
|
43
|
+
4. Repeat until the order is complete or fails
|
|
44
|
+
5. When complete, report the results and tell the user where to find translated files
|
|
45
|
+
|
|
46
|
+
## Tips
|
|
47
|
+
|
|
48
|
+
- If the user mentions a glossary, use `questlang_update_glossary` to add terms before translating
|
|
49
|
+
- Use `questlang_list_orders` if the user asks about previous translations
|
|
50
|
+
- For game projects, ask about world name, genre, and setting to improve translation quality
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { QuestlangConfig, GlossaryEntry } from './types.js';
|
|
2
|
+
export declare class QuestlangClient {
|
|
3
|
+
private apiKey;
|
|
4
|
+
private baseUrl;
|
|
5
|
+
constructor(config: QuestlangConfig);
|
|
6
|
+
private request;
|
|
7
|
+
getBalance(): Promise<{
|
|
8
|
+
balance: number;
|
|
9
|
+
}>;
|
|
10
|
+
translateAsync(file: Uint8Array, filename: string, opts: {
|
|
11
|
+
targetLanguages: string[];
|
|
12
|
+
sourceLanguage?: string;
|
|
13
|
+
model?: string;
|
|
14
|
+
gameContext?: {
|
|
15
|
+
worldName?: string;
|
|
16
|
+
genre?: string;
|
|
17
|
+
setting?: string;
|
|
18
|
+
};
|
|
19
|
+
}): Promise<{
|
|
20
|
+
orderId: string;
|
|
21
|
+
}>;
|
|
22
|
+
getOrder(orderId: string): Promise<Record<string, unknown>>;
|
|
23
|
+
getOrders(): Promise<Record<string, unknown>>;
|
|
24
|
+
downloadOrder(orderId: string): Promise<string>;
|
|
25
|
+
getGlossary(): Promise<Record<string, unknown>[]>;
|
|
26
|
+
addGlossaryEntry(entry: GlossaryEntry): Promise<Record<string, unknown>>;
|
|
27
|
+
updateGlossaryEntry(id: string, entry: Partial<GlossaryEntry>): Promise<Record<string, unknown>>;
|
|
28
|
+
estimateReviewCost(orderId: string, model?: string): Promise<Record<string, unknown>>;
|
|
29
|
+
requestReview(orderId: string, model?: string, languages?: string[]): Promise<Record<string, unknown>>;
|
|
30
|
+
getReviewStatus(orderId: string): Promise<Record<string, unknown>>;
|
|
31
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export class QuestlangClient {
|
|
2
|
+
apiKey;
|
|
3
|
+
baseUrl;
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.apiKey = config.apiKey;
|
|
6
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, '');
|
|
7
|
+
}
|
|
8
|
+
async request(path, options = {}) {
|
|
9
|
+
const url = `${this.baseUrl}${path}`;
|
|
10
|
+
const headers = new Headers(options.headers);
|
|
11
|
+
headers.set('X-API-Key', this.apiKey);
|
|
12
|
+
if (!headers.has('Content-Type') && !(options.body instanceof FormData)) {
|
|
13
|
+
headers.set('Content-Type', 'application/json');
|
|
14
|
+
}
|
|
15
|
+
// FormData needs the browser/node to set Content-Type with boundary
|
|
16
|
+
if (options.body instanceof FormData) {
|
|
17
|
+
headers.delete('Content-Type');
|
|
18
|
+
}
|
|
19
|
+
const response = await fetch(url, {
|
|
20
|
+
...options,
|
|
21
|
+
headers,
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
let message;
|
|
25
|
+
try {
|
|
26
|
+
const body = await response.json();
|
|
27
|
+
message = body.error || body.message || response.statusText;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
message = response.statusText;
|
|
31
|
+
}
|
|
32
|
+
throw new Error(`API error ${response.status}: ${message}`);
|
|
33
|
+
}
|
|
34
|
+
return response;
|
|
35
|
+
}
|
|
36
|
+
async getBalance() {
|
|
37
|
+
const response = await this.request('/api/users/me/tokens');
|
|
38
|
+
return response.json();
|
|
39
|
+
}
|
|
40
|
+
async translateAsync(file, filename, opts) {
|
|
41
|
+
const formData = new FormData();
|
|
42
|
+
const blob = new Blob([new Uint8Array(file)], { type: 'text/csv' });
|
|
43
|
+
formData.append('file', blob, filename);
|
|
44
|
+
formData.append('targetLanguages', JSON.stringify(opts.targetLanguages));
|
|
45
|
+
if (opts.sourceLanguage) {
|
|
46
|
+
formData.append('sourceLanguage', opts.sourceLanguage);
|
|
47
|
+
}
|
|
48
|
+
if (opts.model) {
|
|
49
|
+
formData.append('model', opts.model);
|
|
50
|
+
}
|
|
51
|
+
if (opts.gameContext) {
|
|
52
|
+
formData.append('gameContext', JSON.stringify(opts.gameContext));
|
|
53
|
+
}
|
|
54
|
+
const response = await this.request('/api/translate/async', {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
body: formData,
|
|
57
|
+
});
|
|
58
|
+
return response.json();
|
|
59
|
+
}
|
|
60
|
+
async getOrder(orderId) {
|
|
61
|
+
const response = await this.request(`/api/orders/${orderId}`);
|
|
62
|
+
return response.json();
|
|
63
|
+
}
|
|
64
|
+
async getOrders() {
|
|
65
|
+
const response = await this.request('/api/orders');
|
|
66
|
+
return response.json();
|
|
67
|
+
}
|
|
68
|
+
async downloadOrder(orderId) {
|
|
69
|
+
const response = await this.request(`/api/orders/${orderId}/download`);
|
|
70
|
+
return response.text();
|
|
71
|
+
}
|
|
72
|
+
async getGlossary() {
|
|
73
|
+
const response = await this.request('/api/glossary');
|
|
74
|
+
return response.json();
|
|
75
|
+
}
|
|
76
|
+
async addGlossaryEntry(entry) {
|
|
77
|
+
const response = await this.request('/api/glossary', {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
body: JSON.stringify(entry),
|
|
80
|
+
});
|
|
81
|
+
return response.json();
|
|
82
|
+
}
|
|
83
|
+
async updateGlossaryEntry(id, entry) {
|
|
84
|
+
const response = await this.request(`/api/glossary/${id}`, {
|
|
85
|
+
method: 'PUT',
|
|
86
|
+
body: JSON.stringify(entry),
|
|
87
|
+
});
|
|
88
|
+
return response.json();
|
|
89
|
+
}
|
|
90
|
+
async estimateReviewCost(orderId, model) {
|
|
91
|
+
const body = {};
|
|
92
|
+
if (model) {
|
|
93
|
+
body.model = model;
|
|
94
|
+
}
|
|
95
|
+
const response = await this.request(`/api/orders/${orderId}/review/estimate`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
body: JSON.stringify(body),
|
|
98
|
+
});
|
|
99
|
+
return response.json();
|
|
100
|
+
}
|
|
101
|
+
async requestReview(orderId, model, languages) {
|
|
102
|
+
const body = {};
|
|
103
|
+
if (model) {
|
|
104
|
+
body.model = model;
|
|
105
|
+
}
|
|
106
|
+
if (languages) {
|
|
107
|
+
body.languages = languages;
|
|
108
|
+
}
|
|
109
|
+
const response = await this.request(`/api/orders/${orderId}/review`, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
body: JSON.stringify(body),
|
|
112
|
+
});
|
|
113
|
+
return response.json();
|
|
114
|
+
}
|
|
115
|
+
async getReviewStatus(orderId) {
|
|
116
|
+
const response = await this.request(`/api/orders/${orderId}/review`);
|
|
117
|
+
return response.json();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,eAAe;IAClB,MAAM,CAAS;IACf,OAAO,CAAS;IAExB,YAAY,MAAuB;QACjC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACnD,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,IAAY,EAAE,UAAuB,EAAE;QAC3D,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,CAAC;QACrC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;QAC7C,OAAO,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QACtC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,YAAY,QAAQ,CAAC,EAAE,CAAC;YACxE,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;QAClD,CAAC;QACD,oEAAoE;QACpE,IAAI,OAAO,CAAC,IAAI,YAAY,QAAQ,EAAE,CAAC;YACrC,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;QACjC,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAChC,GAAG,OAAO;YACV,OAAO;SACR,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,IAAI,OAAe,CAAC;YACpB,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAA0C,CAAC;gBAC3E,OAAO,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,OAAO,IAAI,QAAQ,CAAC,UAAU,CAAC;YAC9D,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,GAAG,QAAQ,CAAC,UAAU,CAAC;YAChC,CAAC;YACD,MAAM,IAAI,KAAK,CAAC,aAAa,QAAQ,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC,CAAC;QAC9D,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,UAAU;QACd,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;QAC5D,OAAO,QAAQ,CAAC,IAAI,EAAkC,CAAC;IACzD,CAAC;IAED,KAAK,CAAC,cAAc,CAClB,IAAgB,EAChB,QAAgB,EAChB,IAKC;QAED,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC;QAChC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAC;QACpE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;QACxC,QAAQ,CAAC,MAAM,CAAC,iBAAiB,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC;QACzE,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,QAAQ,CAAC,MAAM,CAAC,gBAAgB,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC;QACzD,CAAC;QACD,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,QAAQ,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QACvC,CAAC;QACD,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACrB,QAAQ,CAAC,MAAM,CAAC,aAAa,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC;QACnE,CAAC;QAED,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,sBAAsB,EAAE;YAC1D,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,QAAQ;SACf,CAAC,CAAC;QACH,OAAO,QAAQ,CAAC,IAAI,EAAkC,CAAC;IACzD,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,OAAe;QAC5B,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,eAAe,OAAO,EAAE,CAAC,CAAC;QAC9D,OAAO,QAAQ,CAAC,IAAI,EAAsC,CAAC;IAC7D,CAAC;IAED,KAAK,CAAC,SAAS;QACb,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;QACnD,OAAO,QAAQ,CAAC,IAAI,EAAsC,CAAC;IAC7D,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,OAAe;QACjC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,eAAe,OAAO,WAAW,CAAC,CAAC;QACvE,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,WAAW;QACf,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;QACrD,OAAO,QAAQ,CAAC,IAAI,EAAwC,CAAC;IAC/D,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,KAAoB;QACzC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE;YACnD,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;SAC5B,CAAC,CAAC;QACH,OAAO,QAAQ,CAAC,IAAI,EAAsC,CAAC;IAC7D,CAAC;IAED,KAAK,CAAC,mBAAmB,CAAC,EAAU,EAAE,KAA6B;QACjE,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE,EAAE,EAAE;YACzD,MAAM,EAAE,KAAK;YACb,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;SAC5B,CAAC,CAAC;QACH,OAAO,QAAQ,CAAC,IAAI,EAAsC,CAAC;IAC7D,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,OAAe,EAAE,KAAc;QACtD,MAAM,IAAI,GAA2B,EAAE,CAAC;QACxC,IAAI,KAAK,EAAE,CAAC;YACV,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACrB,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,eAAe,OAAO,kBAAkB,EAAE;YAC5E,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;SAC3B,CAAC,CAAC;QACH,OAAO,QAAQ,CAAC,IAAI,EAAsC,CAAC;IAC7D,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,OAAe,EAAE,KAAc,EAAE,SAAoB;QACvE,MAAM,IAAI,GAA4B,EAAE,CAAC;QACzC,IAAI,KAAK,EAAE,CAAC;YACV,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACrB,CAAC;QACD,IAAI,SAAS,EAAE,CAAC;YACd,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC7B,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,eAAe,OAAO,SAAS,EAAE;YACnE,MAAM,EAAE,MAAM;YACd,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;SAC3B,CAAC,CAAC;QACH,OAAO,QAAQ,CAAC,IAAI,EAAsC,CAAC;IAC7D,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,OAAe;QACnC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,eAAe,OAAO,SAAS,CAAC,CAAC;QACrE,OAAO,QAAQ,CAAC,IAAI,EAAsC,CAAC;IAC7D,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
if (process.argv[2] === 'init') {
|
|
3
|
+
const { init } = await import('./init.js');
|
|
4
|
+
await init();
|
|
5
|
+
process.exit(0);
|
|
6
|
+
}
|
|
7
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
8
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import { QuestlangClient } from './client.js';
|
|
11
|
+
import { scan } from './tools/scan.js';
|
|
12
|
+
import { checkBalance } from './tools/balance.js';
|
|
13
|
+
import { estimateCost } from './tools/estimate.js';
|
|
14
|
+
import { translateFile } from './tools/translate.js';
|
|
15
|
+
import { getOrderStatus } from './tools/status.js';
|
|
16
|
+
import { listOrders } from './tools/orders.js';
|
|
17
|
+
import { updateGlossary } from './tools/glossary.js';
|
|
18
|
+
import { requestQualityReview, getReviewReport } from './tools/review.js';
|
|
19
|
+
const apiKey = process.env.QUESTLANG_API_KEY;
|
|
20
|
+
const baseUrl = process.env.QUESTLANG_BASE_URL || 'https://questlang.ru';
|
|
21
|
+
if (!apiKey) {
|
|
22
|
+
console.error('QUESTLANG_API_KEY environment variable is required');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const client = new QuestlangClient({ apiKey, baseUrl });
|
|
26
|
+
const server = new McpServer({
|
|
27
|
+
name: 'questlang',
|
|
28
|
+
version: '0.1.0',
|
|
29
|
+
});
|
|
30
|
+
server.tool('questlang_scan_i18n_files', 'Scan a directory for i18n/localization files (JSON, CSV, Android XML, iOS .strings, PO, XLIFF). Returns file paths, formats, key counts, and sample keys.', {
|
|
31
|
+
directory: z.string().describe('Absolute path to the directory to scan'),
|
|
32
|
+
maxDepth: z.number().optional().default(5).describe('Maximum directory depth to scan (default: 5)'),
|
|
33
|
+
}, async ({ directory, maxDepth }) => {
|
|
34
|
+
const result = await scan(directory, maxDepth);
|
|
35
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
36
|
+
});
|
|
37
|
+
server.tool('questlang_check_balance', 'Check the current token balance for your Questlang account.', {}, async () => {
|
|
38
|
+
const result = await checkBalance(client);
|
|
39
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
40
|
+
});
|
|
41
|
+
server.tool('questlang_estimate_cost', 'Estimate the token cost of translating a file into specified languages. Reads the file locally and calculates based on character count and language multipliers.', {
|
|
42
|
+
filePath: z.string().describe('Absolute path to the i18n file'),
|
|
43
|
+
targetLanguages: z.array(z.string()).describe('Target languages (e.g. ["Spanish", "French", "Japanese"])'),
|
|
44
|
+
}, async ({ filePath, targetLanguages }) => {
|
|
45
|
+
const result = await estimateCost(filePath, targetLanguages);
|
|
46
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
47
|
+
});
|
|
48
|
+
server.tool('questlang_translate_file', 'Submit an i18n file for translation via the Questlang API. Parses the file locally, converts to CSV, and starts an async translation job. Returns an order ID for tracking.', {
|
|
49
|
+
filePath: z.string().describe('Absolute path to the i18n file'),
|
|
50
|
+
targetLanguages: z.array(z.string()).describe('Target languages (e.g. ["Spanish", "French", "Japanese"])'),
|
|
51
|
+
sourceLanguage: z.string().optional().describe('Source language (auto-detected if not specified)'),
|
|
52
|
+
model: z.string().optional().describe('Translation model (e.g. "claude-haiku-4-5-20251001", "deepseek-chat")'),
|
|
53
|
+
gameContext: z.object({
|
|
54
|
+
worldName: z.string().optional(),
|
|
55
|
+
genre: z.string().optional(),
|
|
56
|
+
setting: z.string().optional(),
|
|
57
|
+
}).optional().describe('Game context for specialized game translation'),
|
|
58
|
+
}, async ({ filePath, targetLanguages, sourceLanguage, model, gameContext }) => {
|
|
59
|
+
const result = await translateFile(client, filePath, targetLanguages, sourceLanguage, model, gameContext);
|
|
60
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
61
|
+
});
|
|
62
|
+
server.tool('questlang_get_order_status', 'Check the status of a translation order. If completed, also returns the translated content.', {
|
|
63
|
+
orderId: z.string().describe('The order ID returned from questlang_translate_file'),
|
|
64
|
+
}, async ({ orderId }) => {
|
|
65
|
+
const result = await getOrderStatus(client, orderId);
|
|
66
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
67
|
+
});
|
|
68
|
+
server.tool('questlang_list_orders', 'List recent translation orders with their status and progress.', {
|
|
69
|
+
limit: z.number().optional().default(10).describe('Maximum number of orders to return (default: 10)'),
|
|
70
|
+
}, async ({ limit }) => {
|
|
71
|
+
const result = await listOrders(client, limit);
|
|
72
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
73
|
+
});
|
|
74
|
+
server.tool('questlang_update_glossary', 'Add a term to the translation glossary with translations for specific languages.', {
|
|
75
|
+
term: z.string().describe('The source term to add'),
|
|
76
|
+
translations: z.record(z.string()).describe('Map of language name to translation (e.g. {"Spanish": "hola", "French": "bonjour"})'),
|
|
77
|
+
type: z.string().optional().describe('Term type (e.g. "noun", "verb", "proper noun")'),
|
|
78
|
+
context: z.string().optional().describe('Context hint for translators'),
|
|
79
|
+
inflections: z.array(z.string()).optional().describe('Inflected forms of the term'),
|
|
80
|
+
}, async ({ term, translations, type, context, inflections }) => {
|
|
81
|
+
const result = await updateGlossary(client, term, translations, type, context, inflections);
|
|
82
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
83
|
+
});
|
|
84
|
+
server.tool('questlang_request_review', 'Request an AI quality review for a completed translation order. Costs ~30% of original translation cost.', {
|
|
85
|
+
orderId: z.string().describe('The order ID returned from questlang_translate_file'),
|
|
86
|
+
model: z.string().optional().describe('Review model (e.g. "claude-haiku-4-5-20251001", "claude-sonnet-4-5-20250929")'),
|
|
87
|
+
}, async ({ orderId, model }) => {
|
|
88
|
+
const result = await requestQualityReview(client, orderId, model);
|
|
89
|
+
return { content: [{ type: 'text', text: result }] };
|
|
90
|
+
});
|
|
91
|
+
server.tool('questlang_get_review', 'Get the quality review report for a translation order.', {
|
|
92
|
+
orderId: z.string().describe('The order ID returned from questlang_translate_file'),
|
|
93
|
+
}, async ({ orderId }) => {
|
|
94
|
+
const result = await getReviewReport(client, orderId);
|
|
95
|
+
return { content: [{ type: 'text', text: result }] };
|
|
96
|
+
});
|
|
97
|
+
async function main() {
|
|
98
|
+
const transport = new StdioServerTransport();
|
|
99
|
+
await server.connect(transport);
|
|
100
|
+
}
|
|
101
|
+
main().catch((error) => {
|
|
102
|
+
console.error('Failed to start MCP server:', error);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
});
|
|
105
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/init.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function init(): Promise<void>;
|
package/dist/init.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
export async function init() {
|
|
5
|
+
const cwd = process.cwd();
|
|
6
|
+
console.log('\n🔧 QuestLang MCP — Setting up your project...\n');
|
|
7
|
+
// 1. Locate SKILL.md relative to this file
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
// In dist/, go up one level to package root; in src/ during dev, also up one
|
|
10
|
+
const packageRoot = join(__dirname, '..');
|
|
11
|
+
const skillPath = join(packageRoot, 'SKILL.md');
|
|
12
|
+
let skillContent;
|
|
13
|
+
try {
|
|
14
|
+
skillContent = readFileSync(skillPath, 'utf-8');
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Fallback: try two levels up (e.g. dist/src/)
|
|
18
|
+
try {
|
|
19
|
+
skillContent = readFileSync(join(packageRoot, '..', 'SKILL.md'), 'utf-8');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
console.error('❌ Could not find SKILL.md template. Make sure @questlang/mcp-server is installed correctly.');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// 2. Create or merge .mcp.json
|
|
27
|
+
const mcpJsonPath = join(cwd, '.mcp.json');
|
|
28
|
+
const questlangConfig = {
|
|
29
|
+
command: 'npx',
|
|
30
|
+
args: ['-y', '@questlang/mcp-server'],
|
|
31
|
+
env: {
|
|
32
|
+
QUESTLANG_API_KEY: '',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
if (existsSync(mcpJsonPath)) {
|
|
36
|
+
try {
|
|
37
|
+
const existing = JSON.parse(readFileSync(mcpJsonPath, 'utf-8'));
|
|
38
|
+
if (existing.mcpServers?.questlang) {
|
|
39
|
+
console.log('✓ .mcp.json already has questlang configured — skipping');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
existing.mcpServers = existing.mcpServers || {};
|
|
43
|
+
existing.mcpServers.questlang = questlangConfig;
|
|
44
|
+
writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2) + '\n');
|
|
45
|
+
console.log('✓ Added questlang to existing .mcp.json');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
console.error('⚠ Could not parse existing .mcp.json — skipping');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const config = { mcpServers: { questlang: questlangConfig } };
|
|
54
|
+
writeFileSync(mcpJsonPath, JSON.stringify(config, null, 2) + '\n');
|
|
55
|
+
console.log('✓ Created .mcp.json');
|
|
56
|
+
}
|
|
57
|
+
// 3. Create .claude/commands/translate.md
|
|
58
|
+
const commandsDir = join(cwd, '.claude', 'commands');
|
|
59
|
+
const translatePath = join(commandsDir, 'translate.md');
|
|
60
|
+
if (existsSync(translatePath)) {
|
|
61
|
+
console.log('✓ .claude/commands/translate.md already exists — skipping');
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
mkdirSync(commandsDir, { recursive: true });
|
|
65
|
+
writeFileSync(translatePath, skillContent);
|
|
66
|
+
console.log('✓ Created .claude/commands/translate.md');
|
|
67
|
+
}
|
|
68
|
+
// 4. Print next steps
|
|
69
|
+
console.log(`
|
|
70
|
+
📋 Next steps:
|
|
71
|
+
|
|
72
|
+
1. Set your API key:
|
|
73
|
+
export QUESTLANG_API_KEY=ql_...
|
|
74
|
+
|
|
75
|
+
Or edit .mcp.json and paste your key into the QUESTLANG_API_KEY field.
|
|
76
|
+
Get a key at: https://questlang.ru/api-keys
|
|
77
|
+
|
|
78
|
+
2. Use the /translate command in Claude Code to translate your i18n files.
|
|
79
|
+
|
|
80
|
+
3. Or just ask your AI assistant:
|
|
81
|
+
"Scan my project for localization files and translate them to Spanish"
|
|
82
|
+
`);
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=init.js.map
|
package/dist/init.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function unescapeXml(str) {
|
|
2
|
+
return str
|
|
3
|
+
.replace(/&/g, '&')
|
|
4
|
+
.replace(/</g, '<')
|
|
5
|
+
.replace(/>/g, '>')
|
|
6
|
+
.replace(/'/g, "'")
|
|
7
|
+
.replace(/"/g, '"')
|
|
8
|
+
.replace(/\\'/g, "'")
|
|
9
|
+
.replace(/\\"/g, '"')
|
|
10
|
+
.replace(/\\n/g, '\n');
|
|
11
|
+
}
|
|
12
|
+
export function parseAndroidXml(content) {
|
|
13
|
+
const entries = [];
|
|
14
|
+
// Match single <string> elements
|
|
15
|
+
const stringRegex = /<string\s+name="(.+?)">([\s\S]*?)<\/string>/g;
|
|
16
|
+
let match;
|
|
17
|
+
while ((match = stringRegex.exec(content)) !== null) {
|
|
18
|
+
entries.push({
|
|
19
|
+
key: match[1],
|
|
20
|
+
value: unescapeXml(match[2]),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
// Match <string-array> elements with <item> children
|
|
24
|
+
const arrayRegex = /<string-array\s+name="(.+?)">([\s\S]*?)<\/string-array>/g;
|
|
25
|
+
while ((match = arrayRegex.exec(content)) !== null) {
|
|
26
|
+
const arrayName = match[1];
|
|
27
|
+
const arrayContent = match[2];
|
|
28
|
+
const itemRegex = /<item>([\s\S]*?)<\/item>/g;
|
|
29
|
+
let itemMatch;
|
|
30
|
+
let index = 0;
|
|
31
|
+
while ((itemMatch = itemRegex.exec(arrayContent)) !== null) {
|
|
32
|
+
entries.push({
|
|
33
|
+
key: `${arrayName}[${index}]`,
|
|
34
|
+
value: unescapeXml(itemMatch[1]),
|
|
35
|
+
});
|
|
36
|
+
index++;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { entries, format: 'android-xml' };
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=android-xml.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"android-xml.js","sourceRoot":"","sources":["../../src/parsers/android-xml.ts"],"names":[],"mappings":"AAEA,SAAS,WAAW,CAAC,GAAW;IAC9B,OAAO,GAAG;SACP,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC;SACtB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;SACrB,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC;SACrB,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;SACvB,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC;SACvB,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,OAAO,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,MAAM,OAAO,GAAkB,EAAE,CAAC;IAElC,iCAAiC;IACjC,MAAM,WAAW,GAAG,8CAA8C,CAAC;IACnE,IAAI,KAA6B,CAAC;IAElC,OAAO,CAAC,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACpD,OAAO,CAAC,IAAI,CAAC;YACX,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;YACb,KAAK,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;SAC7B,CAAC,CAAC;IACL,CAAC;IAED,qDAAqD;IACrD,MAAM,UAAU,GAAG,0DAA0D,CAAC;IAE9E,OAAO,CAAC,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACnD,MAAM,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QAC3B,MAAM,YAAY,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QAC9B,MAAM,SAAS,GAAG,2BAA2B,CAAC;QAC9C,IAAI,SAAiC,CAAC;QACtC,IAAI,KAAK,GAAG,CAAC,CAAC;QAEd,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;YAC3D,OAAO,CAAC,IAAI,CAAC;gBACX,GAAG,EAAE,GAAG,SAAS,IAAI,KAAK,GAAG;gBAC7B,KAAK,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;aACjC,CAAC,CAAC;YACH,KAAK,EAAE,CAAC;QACV,CAAC;IACH,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;AAC5C,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export function parseCsv(content) {
|
|
2
|
+
const lines = content.split('\n').filter(line => line.trim().length > 0);
|
|
3
|
+
const entries = [];
|
|
4
|
+
for (const line of lines) {
|
|
5
|
+
const fields = parseCsvLine(line);
|
|
6
|
+
if (fields.length < 2) {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
entries.push({ key: fields[0], value: fields[1] });
|
|
10
|
+
}
|
|
11
|
+
return { entries, format: 'csv' };
|
|
12
|
+
}
|
|
13
|
+
function parseCsvLine(line) {
|
|
14
|
+
const fields = [];
|
|
15
|
+
let current = '';
|
|
16
|
+
let inQuotes = false;
|
|
17
|
+
let i = 0;
|
|
18
|
+
while (i < line.length) {
|
|
19
|
+
const char = line[i];
|
|
20
|
+
if (inQuotes) {
|
|
21
|
+
if (char === '"') {
|
|
22
|
+
if (i + 1 < line.length && line[i + 1] === '"') {
|
|
23
|
+
current += '"';
|
|
24
|
+
i += 2;
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
inQuotes = false;
|
|
28
|
+
i++;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
current += char;
|
|
33
|
+
i++;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
if (char === '"') {
|
|
38
|
+
inQuotes = true;
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
41
|
+
else if (char === ',') {
|
|
42
|
+
fields.push(current);
|
|
43
|
+
current = '';
|
|
44
|
+
i++;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
current += char;
|
|
48
|
+
i++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
fields.push(current);
|
|
53
|
+
return fields;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=csv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"csv.js","sourceRoot":"","sources":["../../src/parsers/csv.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,QAAQ,CAAC,OAAe;IACtC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACzE,MAAM,OAAO,GAAkB,EAAE,CAAC;IAElC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtB,SAAS;QACX,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACrD,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;AACpC,CAAC;AAED,SAAS,YAAY,CAAC,IAAY;IAChC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,CAAC,GAAG,CAAC,CAAC;IAEV,OAAO,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QACvB,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAErB,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;gBACjB,IAAI,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;oBAC/C,OAAO,IAAI,GAAG,CAAC;oBACf,CAAC,IAAI,CAAC,CAAC;gBACT,CAAC;qBAAM,CAAC;oBACN,QAAQ,GAAG,KAAK,CAAC;oBACjB,CAAC,EAAE,CAAC;gBACN,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,OAAO,IAAI,IAAI,CAAC;gBAChB,CAAC,EAAE,CAAC;YACN,CAAC;QACH,CAAC;aAAM,CAAC;YACN,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;gBACjB,QAAQ,GAAG,IAAI,CAAC;gBAChB,CAAC,EAAE,CAAC;YACN,CAAC;iBAAM,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;gBACxB,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACrB,OAAO,GAAG,EAAE,CAAC;gBACb,CAAC,EAAE,CAAC;YACN,CAAC;iBAAM,CAAC;gBACN,OAAO,IAAI,IAAI,CAAC;gBAChB,CAAC,EAAE,CAAC;YACN,CAAC;QACH,CAAC;IACH,CAAC;IAED,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACrB,OAAO,MAAM,CAAC;AAChB,CAAC"}
|