@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.
Files changed (2) hide show
  1. package/dist/index.cjs +234 -0
  2. 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
+ }