localship 0.2.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 +23 -12
- package/dist/{chunk-QS3UG3KI.js → chunk-GNL5EFNP.js} +468 -248
- package/dist/cli.js +71 -45
- package/dist/{server-T6GQ6CV5.js → server-5AITZXZV.js} +27 -17
- 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 +9 -1
- package/skills/localship-setup/SKILL.md +50 -36
- package/skills/localship-translate/SKILL.md +26 -18
package/dist/cli.js
CHANGED
|
@@ -30,13 +30,36 @@ import {
|
|
|
30
30
|
submit,
|
|
31
31
|
translateProject,
|
|
32
32
|
validateProject
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-GNL5EFNP.js";
|
|
34
34
|
|
|
35
35
|
// src/cli/main.ts
|
|
36
36
|
import path from "path";
|
|
37
37
|
import { createInterface } from "readline/promises";
|
|
38
38
|
import { Command, InvalidArgumentError } from "commander";
|
|
39
|
-
|
|
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";
|
|
40
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());
|
|
41
64
|
var globals = () => program.opts();
|
|
42
65
|
var locales = (v) => v.split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -74,21 +97,24 @@ program.command("status").description("Translation progress per locale").option(
|
|
|
74
97
|
for (const n of codeText) console.log(`\u25B2 ${n.target}: ${n.message}`);
|
|
75
98
|
}
|
|
76
99
|
});
|
|
77
|
-
program.command("validate").description("Check source strings and translations; exits 1 on errors").option("-l, --locale <list>", "comma-separated locales", locales).option("--strict", "
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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;
|
|
89
116
|
}
|
|
90
|
-
|
|
91
|
-
});
|
|
117
|
+
);
|
|
92
118
|
program.command("detect").description("Find the translation files in this repo (writes nothing)").option("--json", "machine-readable output").action(async (opts) => {
|
|
93
119
|
const result = await detect(nodeFs, globals().cwd);
|
|
94
120
|
if (opts.json) {
|
|
@@ -109,9 +135,7 @@ program.command("plan").description("What it takes to translate this app: findin
|
|
|
109
135
|
if (opts.json) console.log(JSON.stringify(p, null, 2));
|
|
110
136
|
else printPlan(p);
|
|
111
137
|
});
|
|
112
|
-
program.command("start").description(
|
|
113
|
-
"Set up and translate in one go: config, default context, then translation with --model"
|
|
114
|
-
).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(
|
|
115
139
|
"-m, --model <provider:model>",
|
|
116
140
|
"translate with this model right away (default: models in the config)"
|
|
117
141
|
).option("--json", "machine-readable output").action(async (languages, opts) => {
|
|
@@ -120,25 +144,22 @@ program.command("start").description(
|
|
|
120
144
|
const setup = await start(nodeFs, g.cwd, { locales: locales2 });
|
|
121
145
|
const project = await openProject(nodeFs, g);
|
|
122
146
|
const canTranslate = !!(opts.model ?? project.config.models?.default);
|
|
123
|
-
const translated = canTranslate
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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;
|
|
128
155
|
if (opts.json) {
|
|
129
156
|
console.log(JSON.stringify({ setup, translated }, null, 2));
|
|
130
157
|
return;
|
|
131
158
|
}
|
|
132
159
|
console.log(setup.createdConfig ? `Set up ${setup.config}.` : `Using ${setup.config}.`);
|
|
133
160
|
if (setup.addedLocales.length) console.log(`Languages added: ${setup.addedLocales.join(", ")}`);
|
|
134
|
-
if (setup.wrote.length)
|
|
135
|
-
console.log(`Context written: ${setup.wrote.join(", ")} (edit them any time)`);
|
|
136
161
|
for (const w of setup.warnings) console.log(`\u25B2 ${w}`);
|
|
137
|
-
if (
|
|
138
|
-
console.log(
|
|
139
|
-
"Next: move the text into resources (the localship-extract skill), then translate."
|
|
140
|
-
);
|
|
141
|
-
} else if (translated) {
|
|
162
|
+
if (translated) {
|
|
142
163
|
console.log(`${translated.translated} strings translated as drafts.`);
|
|
143
164
|
for (const f of translated.failed)
|
|
144
165
|
console.log(` \u2716 ${f.locale} ${f.key}: ${f.errors.join("; ")}`);
|
|
@@ -261,20 +282,25 @@ program.command("prune").description("Remove translations of strings that were d
|
|
|
261
282
|
}
|
|
262
283
|
for (const f of result.files) console.log(` ${f}`);
|
|
263
284
|
});
|
|
264
|
-
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(
|
|
265
286
|
async (opts) => {
|
|
266
287
|
const project = await openProject(nodeFs, globals());
|
|
267
|
-
const result = await
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
+
);
|
|
278
304
|
if (opts.json) {
|
|
279
305
|
console.log(JSON.stringify(result, null, 2));
|
|
280
306
|
} else {
|
|
@@ -296,7 +322,7 @@ program.command("translate").description("Translate missing and stale strings wi
|
|
|
296
322
|
}
|
|
297
323
|
);
|
|
298
324
|
program.command("mcp").description("Run the MCP server over stdio, for Claude, Cursor, Codex and other MCP clients").action(async () => {
|
|
299
|
-
const { runServer } = await import("./server-
|
|
325
|
+
const { runServer } = await import("./server-5AITZXZV.js");
|
|
300
326
|
const explicit = program.getOptionValueSource("cwd") !== "default";
|
|
301
327
|
const cwd = explicit ? globals().cwd : process.env.CLAUDE_PROJECT_DIR ?? globals().cwd;
|
|
302
328
|
await runServer({ cwd, version: VERSION });
|
|
@@ -333,7 +359,7 @@ program.command("init").description("Detect translation files and write .localsh
|
|
|
333
359
|
program.parseAsync().catch((e) => {
|
|
334
360
|
if (e instanceof UserError) {
|
|
335
361
|
console.error(e.message);
|
|
336
|
-
process.exitCode
|
|
362
|
+
process.exitCode ??= 2;
|
|
337
363
|
return;
|
|
338
364
|
}
|
|
339
365
|
throw e;
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
translateProject,
|
|
26
26
|
validateProject,
|
|
27
27
|
writeContext
|
|
28
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-GNL5EFNP.js";
|
|
29
29
|
|
|
30
30
|
// src/mcp/server.ts
|
|
31
31
|
import { existsSync, readFileSync } from "fs";
|
|
@@ -44,7 +44,7 @@ function createServer(opts) {
|
|
|
44
44
|
const server = new McpServer(
|
|
45
45
|
{ name: "localship", version: opts.version },
|
|
46
46
|
{
|
|
47
|
-
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."
|
|
48
48
|
}
|
|
49
49
|
);
|
|
50
50
|
const dir = (cwd) => path.resolve(opts.cwd, cwd ?? ".");
|
|
@@ -53,9 +53,9 @@ function createServer(opts) {
|
|
|
53
53
|
name,
|
|
54
54
|
{ description, inputSchema: shape, annotations },
|
|
55
55
|
// biome-ignore lint/suspicious/noExplicitAny: the SDK's callback type is generic over the shape.
|
|
56
|
-
(async (args) => {
|
|
56
|
+
(async (args, extra) => {
|
|
57
57
|
try {
|
|
58
|
-
const out = await run(args);
|
|
58
|
+
const out = await run(args, extra.signal);
|
|
59
59
|
return { content: [{ type: "text", text: JSON.stringify(out, null, 2) }] };
|
|
60
60
|
} catch (e) {
|
|
61
61
|
if (!(e instanceof UserError)) throw e;
|
|
@@ -65,7 +65,7 @@ function createServer(opts) {
|
|
|
65
65
|
);
|
|
66
66
|
tool(
|
|
67
67
|
"localship_plan",
|
|
68
|
-
|
|
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.",
|
|
69
69
|
{
|
|
70
70
|
cwd: cwdArg,
|
|
71
71
|
locales: z.array(z.string()).optional().describe("Languages the user asked for, as codes or names (hi, Hindi, pt-BR)")
|
|
@@ -75,12 +75,10 @@ function createServer(opts) {
|
|
|
75
75
|
);
|
|
76
76
|
tool(
|
|
77
77
|
"localship_start",
|
|
78
|
-
"
|
|
78
|
+
"Create .localship/config.yaml for these languages, or add them to the existing one. Writes nothing else.",
|
|
79
79
|
{
|
|
80
80
|
cwd: cwdArg,
|
|
81
|
-
locales: z.array(z.string()).min(1).describe("Languages, as codes or names")
|
|
82
|
-
voice: z.record(z.string(), z.string()).optional().describe("Voice per locale, if the user changed the plan's defaults"),
|
|
83
|
-
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")
|
|
84
82
|
},
|
|
85
83
|
async ({ cwd, ...o }) => start(fs, dir(cwd), o)
|
|
86
84
|
);
|
|
@@ -106,15 +104,23 @@ function createServer(opts) {
|
|
|
106
104
|
);
|
|
107
105
|
tool(
|
|
108
106
|
"localship_validate",
|
|
109
|
-
"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.",
|
|
110
108
|
{
|
|
111
109
|
cwd: cwdArg,
|
|
112
110
|
locales: localesArg,
|
|
113
|
-
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
|
+
)
|
|
114
115
|
},
|
|
115
|
-
async ({ cwd, locales, strict }) => {
|
|
116
|
+
async ({ cwd, locales, strict, requireApproved }) => {
|
|
116
117
|
const p = await project(cwd);
|
|
117
|
-
const issues = validateProject(p, {
|
|
118
|
+
const issues = validateProject(p, {
|
|
119
|
+
locales,
|
|
120
|
+
strict,
|
|
121
|
+
requireApproved,
|
|
122
|
+
usages: await buildUsageIndex(p)
|
|
123
|
+
});
|
|
118
124
|
return { ok: !issues.some((i) => i.severity === "error"), issues };
|
|
119
125
|
},
|
|
120
126
|
{ readOnlyHint: true }
|
|
@@ -266,15 +272,19 @@ ${TRANSLATION_RULES}`,
|
|
|
266
272
|
);
|
|
267
273
|
tool(
|
|
268
274
|
"localship_translate",
|
|
269
|
-
"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.",
|
|
270
276
|
{
|
|
271
277
|
cwd: cwdArg,
|
|
272
278
|
locales: localesArg,
|
|
273
279
|
targets: targetsArg,
|
|
274
280
|
keys: keysArg,
|
|
275
|
-
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)")
|
|
276
286
|
},
|
|
277
|
-
async ({ cwd, targets, ...o }) => translateProject(await project(cwd), { ...o, targets })
|
|
287
|
+
async ({ cwd, targets, ...o }, signal) => translateProject(await project(cwd), { ...o, targets, signal })
|
|
278
288
|
);
|
|
279
289
|
for (const name of [
|
|
280
290
|
"localship",
|
|
@@ -313,4 +323,4 @@ export {
|
|
|
313
323
|
createServer,
|
|
314
324
|
runServer
|
|
315
325
|
};
|
|
316
|
-
//# 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.
|