localingos 0.1.36 → 0.1.38
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/package.json +1 -1
- package/src/api.js +11 -11
- package/src/commands/init.js +4 -4
- package/src/commands/mcp-serve.js +35 -35
- package/src/commands/pull.js +2 -2
- package/src/commands/push.js +2 -2
- package/src/commands/sync.js +6 -6
- package/src/config.js +2 -2
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -6,7 +6,7 @@ export class LocalingosApi {
|
|
|
6
6
|
this.apiKey = apiKey;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
async sync(
|
|
9
|
+
async sync(projectId, translatables, { batchSize = 100, onProgress } = {}) {
|
|
10
10
|
// Push in batches to avoid API timeout
|
|
11
11
|
const batches = [];
|
|
12
12
|
for (let i = 0; i < translatables.length; i += batchSize) {
|
|
@@ -15,7 +15,7 @@ export class LocalingosApi {
|
|
|
15
15
|
|
|
16
16
|
for (let i = 0; i < batches.length; i++) {
|
|
17
17
|
await this._post('/translatable', {
|
|
18
|
-
|
|
18
|
+
projectId,
|
|
19
19
|
translatableList: batches[i]
|
|
20
20
|
});
|
|
21
21
|
if (onProgress) onProgress(Math.min((i + 1) * batchSize, translatables.length), translatables.length, 'push');
|
|
@@ -27,7 +27,7 @@ export class LocalingosApi {
|
|
|
27
27
|
const pullBatchSize = 50;
|
|
28
28
|
for (let i = 0; i < ids.length; i += pullBatchSize) {
|
|
29
29
|
const batch = ids.slice(i, i + pullBatchSize);
|
|
30
|
-
const batchTranslations = await this._post(`/translation/${
|
|
30
|
+
const batchTranslations = await this._post(`/translation/${projectId}/batch`, { foreignIds: batch });
|
|
31
31
|
if (Array.isArray(batchTranslations)) allTranslations.push(...batchTranslations);
|
|
32
32
|
if (onProgress) onProgress(Math.min(i + pullBatchSize, ids.length), ids.length, 'pull');
|
|
33
33
|
}
|
|
@@ -38,23 +38,23 @@ export class LocalingosApi {
|
|
|
38
38
|
return { translations: allTranslations, pending };
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
async getTranslatables(
|
|
42
|
-
return this._get(`/translatable/${
|
|
41
|
+
async getTranslatables(projectId) {
|
|
42
|
+
return this._get(`/translatable/${projectId}`);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
async getTranslations(
|
|
46
|
-
return this._get(`/translation/${
|
|
45
|
+
async getTranslations(projectId) {
|
|
46
|
+
return this._get(`/translation/${projectId}`);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
async postTranslatables(
|
|
49
|
+
async postTranslatables(projectId, translatableList) {
|
|
50
50
|
return this._post('/translatable', {
|
|
51
|
-
|
|
51
|
+
projectId,
|
|
52
52
|
translatableList
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
async deleteTranslatables(
|
|
57
|
-
return this._request('DELETE', `/translatable/${
|
|
56
|
+
async deleteTranslatables(projectId, foreignIds) {
|
|
57
|
+
return this._request('DELETE', `/translatable/${projectId}`, { foreignIds });
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
async _request(method, path, body) {
|
package/src/commands/init.js
CHANGED
|
@@ -57,9 +57,9 @@ export async function initCommand() {
|
|
|
57
57
|
},
|
|
58
58
|
{
|
|
59
59
|
type: 'input',
|
|
60
|
-
name: '
|
|
61
|
-
message: '
|
|
62
|
-
validate: (input) => input.trim().length > 0 || '
|
|
60
|
+
name: 'projectId',
|
|
61
|
+
message: 'Project ID (copy from Projects page → Project ID column):',
|
|
62
|
+
validate: (input) => input.trim().length > 0 || 'Project ID is required'
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
type: 'input',
|
|
@@ -95,7 +95,7 @@ export async function initCommand() {
|
|
|
95
95
|
|
|
96
96
|
const config = {
|
|
97
97
|
apiKey: answers.apiKey.trim(),
|
|
98
|
-
|
|
98
|
+
projectId: answers.projectId.trim(),
|
|
99
99
|
sourceLocale: answers.sourceLocale.trim(),
|
|
100
100
|
format: answers.format,
|
|
101
101
|
sourceFile: answers.sourceFile.trim(),
|
|
@@ -89,7 +89,7 @@ export async function mcpServeCommand() {
|
|
|
89
89
|
}
|
|
90
90
|
const entries = readSourceEntries(config, cwd);
|
|
91
91
|
if (!entries.length) return { content: [{ type: 'text', text: 'No source strings found.' }] };
|
|
92
|
-
const result = await api('POST', '/sync', apiUrl, config.apiKey, {
|
|
92
|
+
const result = await api('POST', '/sync', apiUrl, config.apiKey, { projectId: config.projectId, translatables: entries });
|
|
93
93
|
const written = writeTranslations(result.translations || [], config, cwd);
|
|
94
94
|
const pending = result.pending || [];
|
|
95
95
|
const lines = [`Pushed ${entries.length} source strings`, `${(result.translations || []).length} translations available`,
|
|
@@ -102,18 +102,18 @@ export async function mcpServeCommand() {
|
|
|
102
102
|
server.tool('localingos_push', 'Push source strings only.', {}, async () => {
|
|
103
103
|
const entries = readSourceEntries(config, cwd);
|
|
104
104
|
if (!entries.length) return { content: [{ type: 'text', text: 'No source strings found.' }] };
|
|
105
|
-
await api('POST', '/translatable', apiUrl, config.apiKey, {
|
|
105
|
+
await api('POST', '/translatable', apiUrl, config.apiKey, { projectId: config.projectId, translatableList: entries });
|
|
106
106
|
return { content: [{ type: 'text', text: `Pushed ${entries.length} source strings.` }] };
|
|
107
107
|
});
|
|
108
108
|
|
|
109
109
|
server.tool('localingos_pull', 'Pull latest translations and write locale files.', {}, async () => {
|
|
110
|
-
const translations = await api('GET', `/translation/${config.
|
|
110
|
+
const translations = await api('GET', `/translation/${config.projectId}`, apiUrl, config.apiKey);
|
|
111
111
|
if (!translations?.length) return { content: [{ type: 'text', text: 'No translations available yet.' }] };
|
|
112
112
|
const written = writeTranslations(translations, config, cwd);
|
|
113
113
|
return { content: [{ type: 'text', text: `Pulled:\n${written.join('\n')}` }] };
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
-
server.tool('localingos_status', 'Show project status:
|
|
116
|
+
server.tool('localingos_status', 'Show project status: project, locales, key counts.', {}, async () => {
|
|
117
117
|
const entries = readSourceEntries(config, cwd);
|
|
118
118
|
const outputDir = path.resolve(cwd, config.outputDir);
|
|
119
119
|
let locales = [];
|
|
@@ -121,7 +121,7 @@ export async function mcpServeCommand() {
|
|
|
121
121
|
const re = new RegExp('^' + (config.outputPattern || '{locale}.json').replace('{locale}', '(.+)').replace('.', '\\.') + '$');
|
|
122
122
|
locales = fs.readdirSync(outputDir).map(f => f.match(re)?.[1]).filter(Boolean);
|
|
123
123
|
}
|
|
124
|
-
return { content: [{ type: 'text', text: `
|
|
124
|
+
return { content: [{ type: 'text', text: `Project: ${config.projectId}\nSource: ${config.sourceLocale}\nKeys: ${entries.length}\nTranslations: ${locales.join(', ') || 'none'}` }] };
|
|
125
125
|
});
|
|
126
126
|
|
|
127
127
|
server.tool('localingos_search', 'Search translation keys or text.',
|
|
@@ -148,44 +148,44 @@ export async function mcpServeCommand() {
|
|
|
148
148
|
}
|
|
149
149
|
);
|
|
150
150
|
|
|
151
|
-
// ──
|
|
151
|
+
// ── Project management tools ───────────────────────────────────────────
|
|
152
152
|
|
|
153
|
-
server.tool('
|
|
154
|
-
{
|
|
155
|
-
async ({
|
|
156
|
-
const members = await api('GET', `/
|
|
153
|
+
server.tool('localingos_project_members', 'List members of a project.',
|
|
154
|
+
{ projectId: z.string().describe('Project ID') },
|
|
155
|
+
async ({ projectId }) => {
|
|
156
|
+
const members = await api('GET', `/project/${projectId}/members`, apiUrl, config.apiKey);
|
|
157
157
|
if (!members?.length) return { content: [{ type: 'text', text: 'No members found.' }] };
|
|
158
158
|
const lines = members.map(m => `${m.userId} — ${m.authorization}`);
|
|
159
159
|
return { content: [{ type: 'text', text: `${members.length} member(s):\n${lines.join('\n')}` }] };
|
|
160
160
|
}
|
|
161
161
|
);
|
|
162
162
|
|
|
163
|
-
server.tool('localingos_add_member', 'Add a member to a
|
|
164
|
-
{
|
|
165
|
-
async ({
|
|
163
|
+
server.tool('localingos_add_member', 'Add a member to a project (admin only).',
|
|
164
|
+
{ projectId: z.string(), userId: z.string().describe('User ID to add'), role: z.enum(['ADMIN', 'WRITE', 'READ']).default('WRITE').describe('Role for the new member') },
|
|
165
|
+
async ({ projectId, userId, role }) => {
|
|
166
166
|
try {
|
|
167
|
-
await api('POST', `/
|
|
168
|
-
return { content: [{ type: 'text', text: `Added ${userId} as ${role} to
|
|
167
|
+
await api('POST', `/project/${projectId}/members`, apiUrl, config.apiKey, { userId, authorization: role });
|
|
168
|
+
return { content: [{ type: 'text', text: `Added ${userId} as ${role} to project ${projectId}` }] };
|
|
169
169
|
} catch (e) { return { content: [{ type: 'text', text: `Failed: ${e.message}` }] }; }
|
|
170
170
|
}
|
|
171
171
|
);
|
|
172
172
|
|
|
173
|
-
server.tool('localingos_remove_member', 'Remove a member from a
|
|
174
|
-
{
|
|
175
|
-
async ({
|
|
173
|
+
server.tool('localingos_remove_member', 'Remove a member from a project (admin only).',
|
|
174
|
+
{ projectId: z.string(), userId: z.string().describe('User ID to remove') },
|
|
175
|
+
async ({ projectId, userId }) => {
|
|
176
176
|
try {
|
|
177
|
-
await api('DELETE', `/
|
|
178
|
-
return { content: [{ type: 'text', text: `Removed ${userId} from
|
|
177
|
+
await api('DELETE', `/project/${projectId}/members/${userId}`, apiUrl, config.apiKey);
|
|
178
|
+
return { content: [{ type: 'text', text: `Removed ${userId} from project ${projectId}` }] };
|
|
179
179
|
} catch (e) { return { content: [{ type: 'text', text: `Failed: ${e.message}` }] }; }
|
|
180
180
|
}
|
|
181
181
|
);
|
|
182
182
|
|
|
183
|
-
server.tool('localingos_billing_status', 'Show billing status for a
|
|
184
|
-
{
|
|
185
|
-
async ({
|
|
183
|
+
server.tool('localingos_billing_status', 'Show billing status for a project — plan, usage, owner.',
|
|
184
|
+
{ projectId: z.string().describe('Project ID') },
|
|
185
|
+
async ({ projectId }) => {
|
|
186
186
|
try {
|
|
187
|
-
const b = await api('GET', `/
|
|
188
|
-
if (!b.attached) return { content: [{ type: 'text', text: 'No billing attached to this
|
|
187
|
+
const b = await api('GET', `/project/${projectId}/billing-status`, apiUrl, config.apiKey);
|
|
188
|
+
if (!b.attached) return { content: [{ type: 'text', text: 'No billing attached to this project.' }] };
|
|
189
189
|
const lines = [
|
|
190
190
|
`Status: ${b.accessMode}${b.planName ? ' · ' + b.planName : ''}`,
|
|
191
191
|
`Owner: ${b.ownerEmail || b.ownerUserId}`,
|
|
@@ -197,22 +197,22 @@ export async function mcpServeCommand() {
|
|
|
197
197
|
}
|
|
198
198
|
);
|
|
199
199
|
|
|
200
|
-
server.tool('localingos_attach_billing', 'Attach your billing account to a
|
|
201
|
-
{
|
|
202
|
-
async ({
|
|
200
|
+
server.tool('localingos_attach_billing', 'Attach your billing account to a project (admin only).',
|
|
201
|
+
{ projectId: z.string() },
|
|
202
|
+
async ({ projectId }) => {
|
|
203
203
|
try {
|
|
204
|
-
await api('POST', `/
|
|
205
|
-
return { content: [{ type: 'text', text: `Billing attached to
|
|
204
|
+
await api('POST', `/project/${projectId}/billing/attach`, apiUrl, config.apiKey);
|
|
205
|
+
return { content: [{ type: 'text', text: `Billing attached to project ${projectId}` }] };
|
|
206
206
|
} catch (e) { return { content: [{ type: 'text', text: `Failed: ${e.message}` }] }; }
|
|
207
207
|
}
|
|
208
208
|
);
|
|
209
209
|
|
|
210
|
-
server.tool('localingos_detach_billing', 'Detach billing from a
|
|
211
|
-
{
|
|
212
|
-
async ({
|
|
210
|
+
server.tool('localingos_detach_billing', 'Detach billing from a project (admin only). Project becomes read-only.',
|
|
211
|
+
{ projectId: z.string() },
|
|
212
|
+
async ({ projectId }) => {
|
|
213
213
|
try {
|
|
214
|
-
await api('POST', `/
|
|
215
|
-
return { content: [{ type: 'text', text: `Billing detached from
|
|
214
|
+
await api('POST', `/project/${projectId}/billing/detach`, apiUrl, config.apiKey);
|
|
215
|
+
return { content: [{ type: 'text', text: `Billing detached from project ${projectId}` }] };
|
|
216
216
|
} catch (e) { return { content: [{ type: 'text', text: `Failed: ${e.message}` }] }; }
|
|
217
217
|
}
|
|
218
218
|
);
|
package/src/commands/pull.js
CHANGED
|
@@ -9,12 +9,12 @@ export async function pullCommand(options) {
|
|
|
9
9
|
const apiUrl = resolveApiUrl(options.env);
|
|
10
10
|
const formatter = getFormatter(config.format);
|
|
11
11
|
|
|
12
|
-
console.log(chalk.blue(`⬇️ Pulling translations for
|
|
12
|
+
console.log(chalk.blue(`⬇️ Pulling translations for project ${config.projectId}...`));
|
|
13
13
|
const api = new LocalingosApi(apiUrl, config.apiKey);
|
|
14
14
|
|
|
15
15
|
let translations;
|
|
16
16
|
try {
|
|
17
|
-
translations = await api.getTranslations(config.
|
|
17
|
+
translations = await api.getTranslations(config.projectId);
|
|
18
18
|
} catch (e) {
|
|
19
19
|
console.error(chalk.red(`\nPull failed: ${e.message}`));
|
|
20
20
|
process.exit(1);
|
package/src/commands/push.js
CHANGED
|
@@ -56,11 +56,11 @@ export async function pushCommand(options) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
// 2. Push to API
|
|
59
|
-
console.log(chalk.blue(`\n⬆️ Pushing ${translatables.length} translatables to
|
|
59
|
+
console.log(chalk.blue(`\n⬆️ Pushing ${translatables.length} translatables to project ${config.projectId}...`));
|
|
60
60
|
const api = new LocalingosApi(apiUrl, config.apiKey);
|
|
61
61
|
|
|
62
62
|
try {
|
|
63
|
-
await api.postTranslatables(config.
|
|
63
|
+
await api.postTranslatables(config.projectId, translatables);
|
|
64
64
|
} catch (e) {
|
|
65
65
|
console.error(chalk.red(`\nPush failed: ${e.message}`));
|
|
66
66
|
process.exit(1);
|
package/src/commands/sync.js
CHANGED
|
@@ -70,12 +70,12 @@ export async function syncCommand(options) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
// 2. Call /sync endpoint (push + pull in one call)
|
|
73
|
-
console.log(chalk.blue(`\n🔄 Syncing with Localingos (
|
|
73
|
+
console.log(chalk.blue(`\n🔄 Syncing with Localingos (project: ${config.projectId})...`));
|
|
74
74
|
const api = new LocalingosApi(apiUrl, config.apiKey);
|
|
75
75
|
|
|
76
76
|
let result;
|
|
77
77
|
try {
|
|
78
|
-
result = await api.sync(config.
|
|
78
|
+
result = await api.sync(config.projectId, translatables, {
|
|
79
79
|
onProgress: (done, total, phase) => {
|
|
80
80
|
const label = phase === 'push' ? 'Pushed' : 'Pulled';
|
|
81
81
|
process.stdout.write(`\r ${label} ${done}/${total} strings...`);
|
|
@@ -94,11 +94,11 @@ export async function syncCommand(options) {
|
|
|
94
94
|
console.log(chalk.yellow(` ⏳ ${pending.length} keys pending translation: ${pending.slice(0, 5).join(', ')}${pending.length > 5 ? '...' : ''}`));
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
// 2b. Prune stale translatables from the remote
|
|
97
|
+
// 2b. Prune stale translatables from the remote project
|
|
98
98
|
if (options.prune) {
|
|
99
|
-
console.log(chalk.blue('\n🔍 Checking for stale translatables in the remote
|
|
99
|
+
console.log(chalk.blue('\n🔍 Checking for stale translatables in the remote project...'));
|
|
100
100
|
try {
|
|
101
|
-
const remoteTranslatables = await api.getTranslatables(config.
|
|
101
|
+
const remoteTranslatables = await api.getTranslatables(config.projectId);
|
|
102
102
|
const localIds = new Set(translatables.map(t => t.id));
|
|
103
103
|
const staleIds = remoteTranslatables
|
|
104
104
|
.filter(t => !localIds.has(t.foreignId || t.id))
|
|
@@ -111,7 +111,7 @@ export async function syncCommand(options) {
|
|
|
111
111
|
}
|
|
112
112
|
if (staleIds.length > 10) console.log(chalk.gray(` ... and ${staleIds.length - 10} more`));
|
|
113
113
|
|
|
114
|
-
const deleteResult = await api.deleteTranslatables(config.
|
|
114
|
+
const deleteResult = await api.deleteTranslatables(config.projectId, staleIds);
|
|
115
115
|
console.log(chalk.green(` 🗑️ Deleted ${deleteResult.deleted} stale translatable(s) from remote`));
|
|
116
116
|
} else {
|
|
117
117
|
console.log(chalk.green(' ✅ No stale translatables found'));
|
package/src/config.js
CHANGED
|
@@ -6,7 +6,7 @@ import chalk from 'chalk';
|
|
|
6
6
|
* Config resolution order:
|
|
7
7
|
*
|
|
8
8
|
* 1. localingos.config.json — committed to repo, shared with team/CI
|
|
9
|
-
* Contains:
|
|
9
|
+
* Contains: projectId, sourceLocale, format, sourceFile, outputDir, outputPattern, apiKeyEnv
|
|
10
10
|
*
|
|
11
11
|
* 2. .localingos.json — in .gitignore, local dev only
|
|
12
12
|
* Contains: apiKey (and can override any field from the committed config)
|
|
@@ -90,7 +90,7 @@ function resolveApiKey(config) {
|
|
|
90
90
|
config.apiKey = process.env[envVarName];
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
const required = ['apiKey', '
|
|
93
|
+
const required = ['apiKey', 'projectId', 'sourceLocale', 'format', 'sourceFile', 'outputDir'];
|
|
94
94
|
const missing = required.filter(field => !config[field]);
|
|
95
95
|
if (missing.length > 0) {
|
|
96
96
|
if (missing.length === 1 && missing[0] === 'apiKey') {
|