localship 0.1.0 → 0.3.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/README.md +54 -44
- package/dist/{chunk-OWT47JBL.js → chunk-GNL5EFNP.js} +1017 -517
- package/dist/cli.js +72 -41
- package/dist/{server-GZNS57EQ.js → server-5AITZXZV.js} +30 -18
- package/package.json +2 -1
- package/skills/localship/SKILL.md +66 -43
- package/skills/localship-extract/SKILL.md +123 -39
- package/skills/localship-extract/references/platform-localization.md +65 -0
- package/skills/localship-review/SKILL.md +11 -2
- package/skills/localship-setup/SKILL.md +50 -36
- package/skills/localship-translate/SKILL.md +32 -19
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
UserError,
|
|
5
5
|
addLocales,
|
|
6
6
|
approve,
|
|
7
|
+
buildUsageIndex,
|
|
7
8
|
computeStatus,
|
|
8
9
|
describeDetect,
|
|
9
10
|
describeNone,
|
|
@@ -29,13 +30,36 @@ import {
|
|
|
29
30
|
submit,
|
|
30
31
|
translateProject,
|
|
31
32
|
validateProject
|
|
32
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-GNL5EFNP.js";
|
|
33
34
|
|
|
34
35
|
// src/cli/main.ts
|
|
35
36
|
import path from "path";
|
|
36
37
|
import { createInterface } from "readline/promises";
|
|
37
38
|
import { Command, InvalidArgumentError } from "commander";
|
|
38
|
-
|
|
39
|
+
|
|
40
|
+
// src/cli/cancellation.ts
|
|
41
|
+
async function withCliCancellation(run) {
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
const interrupt = () => {
|
|
44
|
+
process.exitCode = 130;
|
|
45
|
+
controller.abort();
|
|
46
|
+
};
|
|
47
|
+
const terminate = () => {
|
|
48
|
+
process.exitCode = 143;
|
|
49
|
+
controller.abort();
|
|
50
|
+
};
|
|
51
|
+
process.on("SIGINT", interrupt);
|
|
52
|
+
process.on("SIGTERM", terminate);
|
|
53
|
+
try {
|
|
54
|
+
return await run(controller.signal);
|
|
55
|
+
} finally {
|
|
56
|
+
process.off("SIGINT", interrupt);
|
|
57
|
+
process.off("SIGTERM", terminate);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/cli/main.ts
|
|
62
|
+
var VERSION = true ? "0.3.0" : "dev";
|
|
39
63
|
var program = new Command().name("localship").description("Local-first, agent-native localization for apps").version(VERSION).option("-c, --config <file>", "path to the config (default: the nearest .localship/config.yaml)").option("-C, --cwd <dir>", "directory to run in", process.cwd());
|
|
40
64
|
var globals = () => program.opts();
|
|
41
65
|
var locales = (v) => v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -73,17 +97,24 @@ program.command("status").description("Translation progress per locale").option(
|
|
|
73
97
|
for (const n of codeText) console.log(`\u25B2 ${n.target}: ${n.message}`);
|
|
74
98
|
}
|
|
75
99
|
});
|
|
76
|
-
program.command("validate").description("Check source strings and translations; exits 1 on errors").option("-l, --locale <list>", "comma-separated locales", locales).option("--strict", "
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
100
|
+
program.command("validate").description("Check source strings and translations; exits 1 on errors").option("-l, --locale <list>", "comma-separated locales", locales).option("--strict", "missing translations, Android defaults and unsupported content are errors").option("--require-approved", "require current, approved translations (implies --strict)").option("--json", "machine-readable output").action(
|
|
101
|
+
async (opts) => {
|
|
102
|
+
const project = await openProject(nodeFs, globals());
|
|
103
|
+
const issues = validateProject(project, {
|
|
104
|
+
locales: opts.locale,
|
|
105
|
+
strict: opts.strict,
|
|
106
|
+
requireApproved: opts.requireApproved,
|
|
107
|
+
usages: await buildUsageIndex(project)
|
|
108
|
+
});
|
|
109
|
+
const errors = issues.filter((i) => i.severity === "error").length;
|
|
110
|
+
if (opts.json) {
|
|
111
|
+
console.log(JSON.stringify({ ok: errors === 0, issues }, null, 2));
|
|
112
|
+
} else {
|
|
113
|
+
printIssues(issues);
|
|
114
|
+
}
|
|
115
|
+
process.exitCode = errors > 0 ? 1 : 0;
|
|
84
116
|
}
|
|
85
|
-
|
|
86
|
-
});
|
|
117
|
+
);
|
|
87
118
|
program.command("detect").description("Find the translation files in this repo (writes nothing)").option("--json", "machine-readable output").action(async (opts) => {
|
|
88
119
|
const result = await detect(nodeFs, globals().cwd);
|
|
89
120
|
if (opts.json) {
|
|
@@ -104,9 +135,7 @@ program.command("plan").description("What it takes to translate this app: findin
|
|
|
104
135
|
if (opts.json) console.log(JSON.stringify(p, null, 2));
|
|
105
136
|
else printPlan(p);
|
|
106
137
|
});
|
|
107
|
-
program.command("start").description(
|
|
108
|
-
"Set up and translate in one go: config, default context, then translation with --model"
|
|
109
|
-
).argument("<languages...>", "languages to translate into, as codes or names").option(
|
|
138
|
+
program.command("start").description("Set up the config for these languages, then translate with --model if given").argument("<languages...>", "languages to translate into, as codes or names").option(
|
|
110
139
|
"-m, --model <provider:model>",
|
|
111
140
|
"translate with this model right away (default: models in the config)"
|
|
112
141
|
).option("--json", "machine-readable output").action(async (languages, opts) => {
|
|
@@ -115,25 +144,22 @@ program.command("start").description(
|
|
|
115
144
|
const setup = await start(nodeFs, g.cwd, { locales: locales2 });
|
|
116
145
|
const project = await openProject(nodeFs, g);
|
|
117
146
|
const canTranslate = !!(opts.model ?? project.config.models?.default);
|
|
118
|
-
const translated = canTranslate
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
147
|
+
const translated = canTranslate ? await withCliCancellation(
|
|
148
|
+
(signal) => translateProject(project, {
|
|
149
|
+
signal,
|
|
150
|
+
locales: setup.addedLocales.length ? setup.addedLocales : void 0,
|
|
151
|
+
model: opts.model,
|
|
152
|
+
log: (line) => console.error(line)
|
|
153
|
+
})
|
|
154
|
+
) : void 0;
|
|
123
155
|
if (opts.json) {
|
|
124
156
|
console.log(JSON.stringify({ setup, translated }, null, 2));
|
|
125
157
|
return;
|
|
126
158
|
}
|
|
127
159
|
console.log(setup.createdConfig ? `Set up ${setup.config}.` : `Using ${setup.config}.`);
|
|
128
160
|
if (setup.addedLocales.length) console.log(`Languages added: ${setup.addedLocales.join(", ")}`);
|
|
129
|
-
if (setup.wrote.length)
|
|
130
|
-
console.log(`Context written: ${setup.wrote.join(", ")} (edit them any time)`);
|
|
131
161
|
for (const w of setup.warnings) console.log(`\u25B2 ${w}`);
|
|
132
|
-
if (
|
|
133
|
-
console.log(
|
|
134
|
-
"Next: move the text into resources (the localship-extract skill), then translate."
|
|
135
|
-
);
|
|
136
|
-
} else if (translated) {
|
|
162
|
+
if (translated) {
|
|
137
163
|
console.log(`${translated.translated} strings translated as drafts.`);
|
|
138
164
|
for (const f of translated.failed)
|
|
139
165
|
console.log(` \u2716 ${f.locale} ${f.key}: ${f.errors.join("; ")}`);
|
|
@@ -256,20 +282,25 @@ program.command("prune").description("Remove translations of strings that were d
|
|
|
256
282
|
}
|
|
257
283
|
for (const f of result.files) console.log(` ${f}`);
|
|
258
284
|
});
|
|
259
|
-
program.command("translate").description("Translate missing and stale strings with a model API, as drafts").option("-l, --locale <list>", "comma-separated locales", locales).option("-t, --target <list>", "only these target ids", list).option("-k, --keys <list>", "only these keys", list).option("--states <list>", "which entries (default: missing,stale)", states).option("-m, --model <provider:model>", "model for every locale (default: models in the config)").option("-n, --batch-size <n>", "strings per model call", count, 30).option("-p, --parallel <n>", "model calls at once", count, 4).option("--force", "replace hand edits and reviewed or approved translations").option("--json", "machine-readable output").action(
|
|
285
|
+
program.command("translate").description("Translate missing and stale strings with a model API, as drafts").option("-l, --locale <list>", "comma-separated locales", locales).option("-t, --target <list>", "only these target ids", list).option("-k, --keys <list>", "only these keys", list).option("--states <list>", "which entries (default: missing,stale)", states).option("-m, --model <provider:model>", "model for every locale (default: models in the config)").option("-n, --batch-size <n>", "strings per model call", count, 30).option("-p, --parallel <n>", "model calls at once", count, 4).option("--max-prompt-bytes <n>", "maximum UTF-8 bytes of system and user prompt", count, 48e3).option("--timeout-ms <n>", "deadline per model request, including response body", count, 12e4).option("--force", "replace hand edits and reviewed or approved translations").option("--json", "machine-readable output").action(
|
|
260
286
|
async (opts) => {
|
|
261
287
|
const project = await openProject(nodeFs, globals());
|
|
262
|
-
const result = await
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
288
|
+
const result = await withCliCancellation(
|
|
289
|
+
(signal) => translateProject(project, {
|
|
290
|
+
signal,
|
|
291
|
+
maxPromptBytes: opts.maxPromptBytes,
|
|
292
|
+
timeoutMs: opts.timeoutMs,
|
|
293
|
+
locales: opts.locale,
|
|
294
|
+
targets: opts.target,
|
|
295
|
+
keys: opts.keys,
|
|
296
|
+
states: opts.states,
|
|
297
|
+
model: opts.model,
|
|
298
|
+
batchSize: opts.batchSize,
|
|
299
|
+
concurrency: opts.parallel,
|
|
300
|
+
force: opts.force,
|
|
301
|
+
log: (line) => console.error(line)
|
|
302
|
+
})
|
|
303
|
+
);
|
|
273
304
|
if (opts.json) {
|
|
274
305
|
console.log(JSON.stringify(result, null, 2));
|
|
275
306
|
} else {
|
|
@@ -291,7 +322,7 @@ program.command("translate").description("Translate missing and stale strings wi
|
|
|
291
322
|
}
|
|
292
323
|
);
|
|
293
324
|
program.command("mcp").description("Run the MCP server over stdio, for Claude, Cursor, Codex and other MCP clients").action(async () => {
|
|
294
|
-
const { runServer } = await import("./server-
|
|
325
|
+
const { runServer } = await import("./server-5AITZXZV.js");
|
|
295
326
|
const explicit = program.getOptionValueSource("cwd") !== "default";
|
|
296
327
|
const cwd = explicit ? globals().cwd : process.env.CLAUDE_PROJECT_DIR ?? globals().cwd;
|
|
297
328
|
await runServer({ cwd, version: VERSION });
|
|
@@ -328,7 +359,7 @@ program.command("init").description("Detect translation files and write .localsh
|
|
|
328
359
|
program.parseAsync().catch((e) => {
|
|
329
360
|
if (e instanceof UserError) {
|
|
330
361
|
console.error(e.message);
|
|
331
|
-
process.exitCode
|
|
362
|
+
process.exitCode ??= 2;
|
|
332
363
|
return;
|
|
333
364
|
}
|
|
334
365
|
throw e;
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
UserError,
|
|
8
8
|
addLocales,
|
|
9
9
|
approve,
|
|
10
|
+
buildUsageIndex,
|
|
10
11
|
computeStatus,
|
|
11
12
|
detect,
|
|
12
13
|
hardcodedNotes,
|
|
@@ -24,7 +25,7 @@ import {
|
|
|
24
25
|
translateProject,
|
|
25
26
|
validateProject,
|
|
26
27
|
writeContext
|
|
27
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-GNL5EFNP.js";
|
|
28
29
|
|
|
29
30
|
// src/mcp/server.ts
|
|
30
31
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -43,7 +44,7 @@ function createServer(opts) {
|
|
|
43
44
|
const server = new McpServer(
|
|
44
45
|
{ name: "localship", version: opts.version },
|
|
45
46
|
{
|
|
46
|
-
instructions: "Localization for app projects (Android XML, gettext PO, Apple String Catalogs). For
|
|
47
|
+
instructions: "Localization for app projects (Android XML, gettext PO, Apple String Catalogs). Do what the user asked and nothing more. For a request to translate, add a language or update translations: localship_plan to see what's there, localship_start if the languages aren't set up, then localship_next_batch \u2192 localship_submit until nothing remains, then localship_validate, and report. Go by what the user means, however it's worded: wanting the app in a language ('localize the app', 'I want this in Spanish') includes moving text still in the code into string resources first (the plan's notes); keeping translations up to date doesn't; a limit they state wins. If no language was named, ask once before starting. Otherwise stop only for blockers or warnings. Don't create branches, write context files or commit unless asked. For status questions use localship_status, or localship_detect when there's no config. Never edit translation files by hand when these tools can do it."
|
|
47
48
|
}
|
|
48
49
|
);
|
|
49
50
|
const dir = (cwd) => path.resolve(opts.cwd, cwd ?? ".");
|
|
@@ -52,9 +53,9 @@ function createServer(opts) {
|
|
|
52
53
|
name,
|
|
53
54
|
{ description, inputSchema: shape, annotations },
|
|
54
55
|
// biome-ignore lint/suspicious/noExplicitAny: the SDK's callback type is generic over the shape.
|
|
55
|
-
(async (args) => {
|
|
56
|
+
(async (args, extra) => {
|
|
56
57
|
try {
|
|
57
|
-
const out = await run(args);
|
|
58
|
+
const out = await run(args, extra.signal);
|
|
58
59
|
return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
|
|
59
60
|
} catch (e) {
|
|
60
61
|
if (!(e instanceof UserError)) throw e;
|
|
@@ -64,7 +65,7 @@ function createServer(opts) {
|
|
|
64
65
|
);
|
|
65
66
|
tool(
|
|
66
67
|
"localship_plan",
|
|
67
|
-
|
|
68
|
+
"Read-only look at a request (translate the app, add a language, update translations): the files, strings to translate per language, blockers, warnings (another tool manages translations: ask before writing), notes (text still in the code: move it first when the user wants the app in a language, otherwise just mention it), and what translators go by (voice per language, names kept). No confirmation needed: carry on with what the user asked unless there are blockers or warnings.",
|
|
68
69
|
{
|
|
69
70
|
cwd: cwdArg,
|
|
70
71
|
locales: z.array(z.string()).optional().describe("Languages the user asked for, as codes or names (hi, Hindi, pt-BR)")
|
|
@@ -74,12 +75,10 @@ function createServer(opts) {
|
|
|
74
75
|
);
|
|
75
76
|
tool(
|
|
76
77
|
"localship_start",
|
|
77
|
-
"
|
|
78
|
+
"Create .localship/config.yaml for these languages, or add them to the existing one. Writes nothing else.",
|
|
78
79
|
{
|
|
79
80
|
cwd: cwdArg,
|
|
80
|
-
locales: z.array(z.string()).min(1).describe("Languages, as codes or names")
|
|
81
|
-
voice: z.record(z.string(), z.string()).optional().describe("Voice per locale, if the user changed the plan's defaults"),
|
|
82
|
-
keepAsWritten: z.array(z.string()).optional().describe("Names to keep as written, if the user changed the plan's list")
|
|
81
|
+
locales: z.array(z.string()).min(1).describe("Languages, as codes or names")
|
|
83
82
|
},
|
|
84
83
|
async ({ cwd, ...o }) => start(fs, dir(cwd), o)
|
|
85
84
|
);
|
|
@@ -105,14 +104,23 @@ function createServer(opts) {
|
|
|
105
104
|
);
|
|
106
105
|
tool(
|
|
107
106
|
"localship_validate",
|
|
108
|
-
"Deterministic checks on source strings and translations: placeholders, plural forms, escaping, markup, glossary. ok is false when there are errors.",
|
|
107
|
+
"Deterministic checks on source strings and translations: placeholders, plural forms, escaping, markup, glossary. Use requireApproved for a release check that also requires current, approved translations. ok is false when there are errors.",
|
|
109
108
|
{
|
|
110
109
|
cwd: cwdArg,
|
|
111
110
|
locales: localesArg,
|
|
112
|
-
strict: z.boolean().optional().describe("
|
|
111
|
+
strict: z.boolean().optional().describe("Missing translations, Android defaults and unsupported content are errors"),
|
|
112
|
+
requireApproved: z.boolean().optional().describe(
|
|
113
|
+
"Require current, approved translations for every selected locale; implies strict"
|
|
114
|
+
)
|
|
113
115
|
},
|
|
114
|
-
async ({ cwd, locales, strict }) => {
|
|
115
|
-
const
|
|
116
|
+
async ({ cwd, locales, strict, requireApproved }) => {
|
|
117
|
+
const p = await project(cwd);
|
|
118
|
+
const issues = validateProject(p, {
|
|
119
|
+
locales,
|
|
120
|
+
strict,
|
|
121
|
+
requireApproved,
|
|
122
|
+
usages: await buildUsageIndex(p)
|
|
123
|
+
});
|
|
116
124
|
return { ok: !issues.some((i) => i.severity === "error"), issues };
|
|
117
125
|
},
|
|
118
126
|
{ readOnlyHint: true }
|
|
@@ -212,7 +220,7 @@ ${TRANSLATION_RULES}`,
|
|
|
212
220
|
);
|
|
213
221
|
tool(
|
|
214
222
|
"localship_submit",
|
|
215
|
-
"Check translations and write the ones without errors as drafts. Rejected ones come back with the errors to fix; resubmit just those. value is a string, an object of plural forms, or an array, matching the item's kind.",
|
|
223
|
+
"Check translations and write the ones without errors as drafts. Rejected ones come back with the errors to fix; resubmit just those. value is a string, an object of plural forms, or an array, matching the item's kind. Add a short note only when the source is ambiguous, saying how you read it.",
|
|
216
224
|
{
|
|
217
225
|
cwd: cwdArg,
|
|
218
226
|
translations: z.array(SubmissionSchema),
|
|
@@ -264,15 +272,19 @@ ${TRANSLATION_RULES}`,
|
|
|
264
272
|
);
|
|
265
273
|
tool(
|
|
266
274
|
"localship_translate",
|
|
267
|
-
"Translate everything missing with a separate model API (configured under models: in .localship/config.yaml, or pass model as provider:model).
|
|
275
|
+
"Translate everything missing with a separate model API (configured under models: in .localship/config.yaml, or pass model as provider:model). Only when the user asks for it: cloud providers bill per use and need their API key in the environment.",
|
|
268
276
|
{
|
|
269
277
|
cwd: cwdArg,
|
|
270
278
|
locales: localesArg,
|
|
271
279
|
targets: targetsArg,
|
|
272
280
|
keys: keysArg,
|
|
273
|
-
model: z.string().optional().describe("provider:model, e.g. anthropic:claude-sonnet-5-5")
|
|
281
|
+
model: z.string().optional().describe("provider:model, e.g. anthropic:claude-sonnet-5-5"),
|
|
282
|
+
batchSize: z.number().int().positive().optional().describe("Entries per call (default 30)"),
|
|
283
|
+
concurrency: z.number().int().positive().optional().describe("Calls at once (default 4)"),
|
|
284
|
+
maxPromptBytes: z.number().int().positive().optional().describe("UTF-8 prompt bytes (default 48000)"),
|
|
285
|
+
timeoutMs: z.number().int().positive().max(2147483647).optional().describe("Request deadline in milliseconds (default 120000)")
|
|
274
286
|
},
|
|
275
|
-
async ({ cwd, targets, ...o }) => translateProject(await project(cwd), { ...o, targets })
|
|
287
|
+
async ({ cwd, targets, ...o }, signal) => translateProject(await project(cwd), { ...o, targets, signal })
|
|
276
288
|
);
|
|
277
289
|
for (const name of [
|
|
278
290
|
"localship",
|
|
@@ -311,4 +323,4 @@ export {
|
|
|
311
323
|
createServer,
|
|
312
324
|
runServer
|
|
313
325
|
};
|
|
314
|
-
//# sourceMappingURL=server-
|
|
326
|
+
//# sourceMappingURL=server-5AITZXZV.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "localship",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Translate your app with the AI you already use. An MCP server, agent skills and CLI that check every translation before it's written. Android, Apple String Catalogs and gettext.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"localization",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"build": "tsup",
|
|
44
44
|
"dev": "tsx src/cli/main.ts",
|
|
45
45
|
"test": "vitest run",
|
|
46
|
+
"benchmark": "tsx benchmarks/localization.ts",
|
|
46
47
|
"typecheck": "tsc --noEmit",
|
|
47
48
|
"lint": "biome check .",
|
|
48
49
|
"format": "biome check --write .",
|
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: localship
|
|
3
|
-
description:
|
|
3
|
+
description: Use whenever the user wants their app in other languages, its translations updated, or its text made ready for translation, however they phrase it ("localize the app", "I want this in Spanish", "support Hindi users", "add French", "update the translations"). Works out what they mean, does it end to end and reports, stopping only when something is in the way. Use this first; the other localship-* skills are the detailed playbooks it follows.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Localize an app with Localship
|
|
6
|
+
# Localize an app with Localship
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
Do what the user asked, all of it, and nothing they didn't ask for. Don't ask for a go-ahead
|
|
9
|
+
on a normal request; stop only when something is genuinely in the way.
|
|
10
|
+
|
|
11
|
+
## What the user wants
|
|
12
|
+
|
|
13
|
+
Go by what the user means, not the words they use. Requests come in many forms; these are
|
|
14
|
+
examples, not phrases to match.
|
|
15
|
+
|
|
16
|
+
| What they want | Sounds like | Do |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| **The app in a language**, all of it | "localize the app", "I want this in Spanish", "support Hindi users", "add French", "make it work in Arabic", "go international" | Move text still in the code into string resources (the whole app), then translate |
|
|
19
|
+
| **Translations kept up to date** | "update the translations", "I changed some copy", "translate the new strings", "sync Hindi" | Translate what's missing or changed in the string resources; mention text still in the code |
|
|
20
|
+
| **Text ready for translation**, nothing translated | "make the app localizable", "find hardcoded strings", "move the text into strings.xml", "prepare for translation" | Move the text only (the `localship-extract` skill) |
|
|
21
|
+
|
|
22
|
+
A limit the user states wins: "just translate the strings file" or "don't touch the code" means
|
|
23
|
+
translate only. If you can't tell which they mean and the plan has `notes` (text still in the
|
|
24
|
+
code), ask once, together with the languages if those are missing too.
|
|
11
25
|
|
|
12
26
|
## Tools
|
|
13
27
|
|
|
@@ -15,53 +29,62 @@ Use the `localship_*` MCP tools when available. Otherwise the CLI: `localship pl
|
|
|
15
29
|
`localship start`, `localship next-batch`, `localship submit` (install once with
|
|
16
30
|
`npm install -g localship` if it's missing).
|
|
17
31
|
|
|
18
|
-
## 1.
|
|
32
|
+
## 1. Look
|
|
19
33
|
|
|
20
|
-
Call `localship_plan` with the languages from the request, as names or
|
|
21
|
-
(`["Hindi"]`, `["hi", "ar"]`).
|
|
34
|
+
Call `localship_plan` with the languages from the request, as names or
|
|
35
|
+
codes (`["Hindi"]`, `["hi", "ar"]`). It writes nothing.
|
|
22
36
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
If no language was named, ask which ones now, once, before any work, so the rest runs without
|
|
38
|
+
stopping. Otherwise stop and ask only when:
|
|
39
|
+
- **`blockers`**: a format Localship can't translate, or an empty String Catalog. "No
|
|
40
|
+
translation files" blocks keeping translations up to date, but not wanting the app in a
|
|
41
|
+
language: moving the text creates them. Say what's in the way and what would get past it.
|
|
42
|
+
- **`warnings`**: another tool (Crowdin, an over-the-air SDK…) already manages translations.
|
|
43
|
+
Writing files could clash with it; ask whether to go ahead.
|
|
30
44
|
|
|
31
|
-
|
|
32
|
-
|
|
45
|
+
Otherwise carry on, and tell the user in one line what you're about to do, e.g. "Moving about
|
|
46
|
+
300 pieces of text into string resources, then translating into Hindi." When only updating
|
|
47
|
+
translations, mention the plan's `notes` (text still in the code) in that line instead: those
|
|
48
|
+
strings won't be translated unless the user asks you to move them.
|
|
33
49
|
|
|
34
|
-
|
|
35
|
-
the user should be able to tell your changes from theirs.
|
|
50
|
+
## 2. Do it
|
|
36
51
|
|
|
37
|
-
|
|
52
|
+
1. **Move the text** (when they want the app in a language and the plan has `notes` or no
|
|
53
|
+
translation files): follow the `localship-extract` skill for the whole app, building as you go, with a
|
|
54
|
+
short progress update after each part. Then run `localship_plan` again for the new counts.
|
|
55
|
+
2. **Set up:** if the languages aren't configured yet, `localship_start` with them. It writes
|
|
56
|
+
only the config.
|
|
57
|
+
3. **Translate:** follow the `localship-translate` skill's loop (`localship_next_batch` →
|
|
58
|
+
translate → `localship_submit`, fix rejections) until `remaining` is 0 for every language.
|
|
59
|
+
4. **Check:** `localship_validate` must show no errors.
|
|
38
60
|
|
|
39
|
-
|
|
61
|
+
## 3. Report
|
|
40
62
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
review the drafts (the `localship-review` skill), then approve.
|
|
63
|
+
In a few lines:
|
|
64
|
+
- text moved into resources, and any naming convention you had to pick;
|
|
65
|
+
- strings translated per language, and anything that failed or was held for review;
|
|
66
|
+
- what translators went by: each language's voice (the project's, or the usual convention the
|
|
67
|
+
batch gave as `defaultVoice`, e.g. "addressed the user as आप") and names kept as written, so
|
|
68
|
+
the user can correct them;
|
|
69
|
+
- text still in the code that wasn't moved (when only updating translations);
|
|
70
|
+
- next step: review the drafts (the `localship-review` skill), then approve.
|
|
71
|
+
|
|
72
|
+
If the user asks for something different (another formality, a brief, a glossary term), write it
|
|
73
|
+
with `localship_update_context` or the files in `.localship/`, and redo what it affects.
|
|
74
|
+
|
|
75
|
+
## Only when the user asks
|
|
55
76
|
|
|
56
|
-
|
|
57
|
-
|
|
77
|
+
- moving hardcoded text into resources, unless they want the app in a language;
|
|
78
|
+
- creating a branch, committing or pushing;
|
|
79
|
+
- writing a brief, voice notes or glossary;
|
|
80
|
+
- translating through a model API (`localship_translate`): cloud providers bill per use;
|
|
81
|
+
- approving drafts, pruning, removing or `--force` (these change or delete people's work).
|
|
58
82
|
|
|
59
83
|
## Other requests
|
|
60
84
|
|
|
61
|
-
|
|
62
|
-
- **
|
|
63
|
-
|
|
64
|
-
- **
|
|
65
|
-
- **"Review" / "approve":** the `localship-review` skill.
|
|
85
|
+
Again by meaning, however it's worded:
|
|
86
|
+
- **Progress:** `localship_status` (or `localship_detect` without a config).
|
|
87
|
+
- **Checking or approving translations:** the `localship-review` skill.
|
|
88
|
+
- **Setting up Localship itself** (config, brief, voice, glossary): the `localship-setup` skill.
|
|
66
89
|
|
|
67
|
-
|
|
90
|
+
Hand-edited translations are always held for review, never replaced.
|