@ownlate/cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +234 -0
- package/package.json +38 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
let commander = require("commander");
|
|
3
|
+
let node_readline_promises = require("node:readline/promises");
|
|
4
|
+
let node_fs_promises = require("node:fs/promises");
|
|
5
|
+
let node_path = require("node:path");
|
|
6
|
+
|
|
7
|
+
//#region src/commands/init.ts
|
|
8
|
+
async function ask(rl, question, fallback = "") {
|
|
9
|
+
return (await rl.question(question)).trim() || fallback;
|
|
10
|
+
}
|
|
11
|
+
async function initCommand(options) {
|
|
12
|
+
const configPath = (0, node_path.resolve)(options.config);
|
|
13
|
+
try {
|
|
14
|
+
await (0, node_fs_promises.access)(configPath);
|
|
15
|
+
console.error(`Config already exists: ${configPath}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
} catch {}
|
|
18
|
+
const rl = (0, node_readline_promises.createInterface)({
|
|
19
|
+
input: process.stdin,
|
|
20
|
+
output: process.stdout
|
|
21
|
+
});
|
|
22
|
+
try {
|
|
23
|
+
console.log("Initializing ownlate config...\n");
|
|
24
|
+
const apiUrl = await ask(rl, "API URL [http://localhost:3000]: ", "http://localhost:3000");
|
|
25
|
+
const apiKey = await ask(rl, "API Key: ");
|
|
26
|
+
const projectId = await ask(rl, "Project ID: ");
|
|
27
|
+
const sourceLanguage = await ask(rl, "Source language [en]: ", "en");
|
|
28
|
+
const targetLangsRaw = await ask(rl, "Target languages, comma-separated [fr,de]: ", "fr,de");
|
|
29
|
+
const sourceFile = await ask(rl, "Source file path [locales/en.json]: ", "locales/en.json");
|
|
30
|
+
const outputPattern = await ask(rl, "Output pattern [locales/{{language}}.json]: ", "locales/{{language}}.json");
|
|
31
|
+
const config = {
|
|
32
|
+
apiUrl,
|
|
33
|
+
apiKey,
|
|
34
|
+
projectId,
|
|
35
|
+
sourceLanguage,
|
|
36
|
+
targetLanguages: targetLangsRaw.split(",").map((l) => l.trim()).filter(Boolean),
|
|
37
|
+
files: [{
|
|
38
|
+
source: sourceFile,
|
|
39
|
+
output: outputPattern
|
|
40
|
+
}]
|
|
41
|
+
};
|
|
42
|
+
await (0, node_fs_promises.writeFile)(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
43
|
+
console.log(`\nCreated ${configPath}`);
|
|
44
|
+
console.log("\nNext steps:");
|
|
45
|
+
console.log(" ownlate push — upload source strings to ownlate");
|
|
46
|
+
console.log(" ownlate pull — download translations to local files");
|
|
47
|
+
} finally {
|
|
48
|
+
rl.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/lib/config.ts
|
|
54
|
+
async function readConfig(configPath) {
|
|
55
|
+
const absPath = (0, node_path.resolve)(configPath);
|
|
56
|
+
let raw;
|
|
57
|
+
try {
|
|
58
|
+
raw = await (0, node_fs_promises.readFile)(absPath, "utf-8");
|
|
59
|
+
} catch {
|
|
60
|
+
throw new Error(`Config file not found: ${absPath}\nRun "ownlate init" to create one.`);
|
|
61
|
+
}
|
|
62
|
+
let config;
|
|
63
|
+
try {
|
|
64
|
+
config = JSON.parse(raw);
|
|
65
|
+
} catch {
|
|
66
|
+
throw new Error(`Config file is not valid JSON: ${absPath}`);
|
|
67
|
+
}
|
|
68
|
+
config.apiUrl = process.env.OWNLATE_API_URL ?? config.apiUrl;
|
|
69
|
+
config.apiKey = process.env.OWNLATE_API_KEY ?? config.apiKey;
|
|
70
|
+
for (const field of [
|
|
71
|
+
"apiUrl",
|
|
72
|
+
"apiKey",
|
|
73
|
+
"projectId",
|
|
74
|
+
"sourceLanguage",
|
|
75
|
+
"targetLanguages",
|
|
76
|
+
"files"
|
|
77
|
+
]) if (!config[field]) throw new Error(`Missing required config field: "${field}"`);
|
|
78
|
+
return config;
|
|
79
|
+
}
|
|
80
|
+
async function writeConfig(configPath, config) {
|
|
81
|
+
await (0, node_fs_promises.writeFile)((0, node_path.resolve)(configPath), JSON.stringify(config, null, 2) + "\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
//#endregion
|
|
85
|
+
//#region src/lib/api-client.ts
|
|
86
|
+
var ApiClient = class {
|
|
87
|
+
constructor(baseUrl, apiKey) {
|
|
88
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
89
|
+
this.apiKey = apiKey;
|
|
90
|
+
}
|
|
91
|
+
authHeaders() {
|
|
92
|
+
return { Authorization: `Bearer ${this.apiKey}` };
|
|
93
|
+
}
|
|
94
|
+
async upload(dto) {
|
|
95
|
+
const res = await fetch(`${this.baseUrl}/v1/translation-files/upload`, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: {
|
|
98
|
+
...this.authHeaders(),
|
|
99
|
+
"Content-Type": "application/json"
|
|
100
|
+
},
|
|
101
|
+
body: JSON.stringify(dto)
|
|
102
|
+
});
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
const text = await res.text();
|
|
105
|
+
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
106
|
+
}
|
|
107
|
+
return res.json();
|
|
108
|
+
}
|
|
109
|
+
async exportTranslations(params) {
|
|
110
|
+
const query = new URLSearchParams({
|
|
111
|
+
projectId: params.projectId,
|
|
112
|
+
language: params.language,
|
|
113
|
+
format: params.format
|
|
114
|
+
});
|
|
115
|
+
if (params.fileId) query.set("fileId", params.fileId);
|
|
116
|
+
if (params.sourceLanguage) query.set("sourceLanguage", params.sourceLanguage);
|
|
117
|
+
const res = await fetch(`${this.baseUrl}/v1/segments/export?${query}`, { headers: this.authHeaders() });
|
|
118
|
+
if (!res.ok) {
|
|
119
|
+
const text = await res.text();
|
|
120
|
+
throw new Error(`HTTP ${res.status}: ${text}`);
|
|
121
|
+
}
|
|
122
|
+
return res.text();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/commands/push.ts
|
|
128
|
+
function detectFormat$1(filePath) {
|
|
129
|
+
const ext = (0, node_path.extname)(filePath).toLowerCase();
|
|
130
|
+
if (ext === ".json") return "json";
|
|
131
|
+
if (ext === ".yaml" || ext === ".yml") return "yaml";
|
|
132
|
+
if (ext === ".po") return "po";
|
|
133
|
+
throw new Error(`Unsupported extension "${ext}". Use .json, .yaml, .yml or .po`);
|
|
134
|
+
}
|
|
135
|
+
async function pushCommand(options) {
|
|
136
|
+
const config = await readConfig(options.config);
|
|
137
|
+
const client = new ApiClient(config.apiUrl, config.apiKey);
|
|
138
|
+
let configUpdated = false;
|
|
139
|
+
console.log(`Pushing to ${config.apiUrl} (project: ${config.projectId})\n`);
|
|
140
|
+
for (const file of config.files) {
|
|
141
|
+
const absPath = (0, node_path.resolve)(file.source);
|
|
142
|
+
let content;
|
|
143
|
+
try {
|
|
144
|
+
content = await (0, node_fs_promises.readFile)(absPath, "utf-8");
|
|
145
|
+
} catch {
|
|
146
|
+
console.error(` [skip] ${file.source} — file not found`);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const format = detectFormat$1(file.source);
|
|
150
|
+
const name = (0, node_path.basename)(file.source);
|
|
151
|
+
const dir = (0, node_path.dirname)(file.source);
|
|
152
|
+
const filePath = dir === "." ? "" : dir;
|
|
153
|
+
process.stdout.write(` ${file.source} ...`);
|
|
154
|
+
try {
|
|
155
|
+
const result = await client.upload({
|
|
156
|
+
projectId: config.projectId,
|
|
157
|
+
name,
|
|
158
|
+
path: filePath,
|
|
159
|
+
format,
|
|
160
|
+
content,
|
|
161
|
+
fileId: file.fileId
|
|
162
|
+
});
|
|
163
|
+
if (!file.fileId) {
|
|
164
|
+
file.fileId = result.id;
|
|
165
|
+
configUpdated = true;
|
|
166
|
+
}
|
|
167
|
+
console.log(` ${result.segmentsCreated} segments`);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.log(" failed");
|
|
170
|
+
console.error(` [!] ${err.message}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (configUpdated) {
|
|
174
|
+
await writeConfig(options.config, config);
|
|
175
|
+
console.log(`\nSaved file IDs to ${options.config}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/commands/pull.ts
|
|
181
|
+
function detectFormat(pattern) {
|
|
182
|
+
const ext = (0, node_path.extname)(pattern).toLowerCase();
|
|
183
|
+
if (ext === ".json") return "json";
|
|
184
|
+
if (ext === ".yaml" || ext === ".yml") return "yaml";
|
|
185
|
+
if (ext === ".po") return "po";
|
|
186
|
+
throw new Error(`Cannot detect format from output pattern "${pattern}"`);
|
|
187
|
+
}
|
|
188
|
+
async function pullCommand(options) {
|
|
189
|
+
const config = await readConfig(options.config);
|
|
190
|
+
const client = new ApiClient(config.apiUrl, config.apiKey);
|
|
191
|
+
const languages = options.lang ? [options.lang] : config.targetLanguages;
|
|
192
|
+
console.log(`Pulling from ${config.apiUrl} (project: ${config.projectId})\n`);
|
|
193
|
+
for (const file of config.files) {
|
|
194
|
+
const format = detectFormat(file.output);
|
|
195
|
+
for (const language of languages) {
|
|
196
|
+
const localPath = file.output.replace("{{language}}", language);
|
|
197
|
+
const absPath = (0, node_path.resolve)(localPath);
|
|
198
|
+
process.stdout.write(` ${language} → ${localPath} ...`);
|
|
199
|
+
try {
|
|
200
|
+
const content = await client.exportTranslations({
|
|
201
|
+
projectId: config.projectId,
|
|
202
|
+
language,
|
|
203
|
+
format,
|
|
204
|
+
fileId: file.fileId
|
|
205
|
+
});
|
|
206
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(absPath), { recursive: true });
|
|
207
|
+
await (0, node_fs_promises.writeFile)(absPath, content);
|
|
208
|
+
console.log(" done");
|
|
209
|
+
} catch (err) {
|
|
210
|
+
console.log(" failed");
|
|
211
|
+
console.error(` [!] ${err.message}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region src/index.ts
|
|
219
|
+
const program = new commander.Command();
|
|
220
|
+
program.name("ownlate").description("Sync translation files with your ownlate project").version("1.0.0");
|
|
221
|
+
const configOpt = [
|
|
222
|
+
"-c, --config <path>",
|
|
223
|
+
"path to config file",
|
|
224
|
+
".ownlate.json"
|
|
225
|
+
];
|
|
226
|
+
program.command("init").description("Create .ownlate.json config interactively").option(...configOpt).action(initCommand);
|
|
227
|
+
program.command("push").description("Upload source files — creates/updates segments in ownlate").option(...configOpt).action(pushCommand);
|
|
228
|
+
program.command("pull").description("Download translated files from ownlate to local filesystem").option(...configOpt).option("-l, --lang <code>", "pull a single language only (e.g. fr)").action(pullCommand);
|
|
229
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
230
|
+
console.error(err.message);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
//#endregion
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ownlate/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Ownlate CLI — push source files and pull translations",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"bin": {
|
|
10
|
+
"ownlate": "./dist/index.cjs"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18.0.0"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsdown",
|
|
17
|
+
"dev": "tsdown --watch"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"ownlate",
|
|
21
|
+
"i18n",
|
|
22
|
+
"localization",
|
|
23
|
+
"translations",
|
|
24
|
+
"cli"
|
|
25
|
+
],
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"commander": "^12.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@ownlate/typescript-config": "workspace:*",
|
|
34
|
+
"@types/node": "catalog:dev",
|
|
35
|
+
"tsdown": "catalog:dev",
|
|
36
|
+
"typescript": "catalog:dev"
|
|
37
|
+
}
|
|
38
|
+
}
|