localingos 0.1.33 → 0.1.35

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "localingos",
3
- "version": "0.1.33",
3
+ "version": "0.1.35",
4
4
  "description": "CLI tool to sync translations with Localingos",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/api.js CHANGED
@@ -6,11 +6,30 @@ export class LocalingosApi {
6
6
  this.apiKey = apiKey;
7
7
  }
8
8
 
9
- async sync(familyId, translatables) {
10
- return this._post('/sync', {
11
- familyId,
12
- translatables
13
- });
9
+ async sync(familyId, translatables, { batchSize = 100, onProgress } = {}) {
10
+ // Push in batches to avoid API timeout
11
+ const batches = [];
12
+ for (let i = 0; i < translatables.length; i += batchSize) {
13
+ batches.push(translatables.slice(i, i + batchSize));
14
+ }
15
+
16
+ for (let i = 0; i < batches.length; i++) {
17
+ await this._post('/translatable', {
18
+ familyId,
19
+ translatableList: batches[i]
20
+ });
21
+ if (onProgress) onProgress(Math.min((i + 1) * batchSize, translatables.length), translatables.length);
22
+ }
23
+
24
+ // Pull translations separately
25
+ const translations = await this._get(`/translation/${familyId}`);
26
+ const pushedIds = new Set(translatables.map(t => t.id));
27
+ const available = Array.isArray(translations) ? translations : [];
28
+ const pending = translatables
29
+ .filter(t => !available.some(tr => tr.foreignId === t.id || tr.translatableId === t.id))
30
+ .map(t => t.id);
31
+
32
+ return { translations: available, pending };
14
33
  }
15
34
 
16
35
  async getTranslatables(familyId) {
@@ -28,6 +47,36 @@ export class LocalingosApi {
28
47
  });
29
48
  }
30
49
 
50
+ async deleteTranslatables(familyId, foreignIds) {
51
+ return this._request('DELETE', `/translatable/${familyId}`, { foreignIds });
52
+ }
53
+
54
+ async _request(method, path, body) {
55
+ const url = `${this.apiUrl}${path}`;
56
+ let response;
57
+ try {
58
+ const opts = {
59
+ method,
60
+ headers: {
61
+ 'X-API-Key': this.apiKey,
62
+ 'Content-Type': 'application/json',
63
+ 'Accept': 'application/json'
64
+ },
65
+ };
66
+ if (body) opts.body = JSON.stringify(body);
67
+ response = await fetch(url, opts);
68
+ } catch (networkError) {
69
+ this._throwConnectionError(method, url, networkError);
70
+ }
71
+ if (!response.ok) {
72
+ const responseBody = await response.text().catch(() => '');
73
+ throw new Error(`API ${method} ${path} failed (${response.status}): ${responseBody}`);
74
+ }
75
+ const text = await response.text();
76
+ if (!text) return {};
77
+ return JSON.parse(text);
78
+ }
79
+
31
80
  async _get(path) {
32
81
  const url = `${this.apiUrl}${path}`;
33
82
  let response;
@@ -75,7 +75,12 @@ export async function syncCommand(options) {
75
75
 
76
76
  let result;
77
77
  try {
78
- result = await api.sync(config.familyId, translatables);
78
+ result = await api.sync(config.familyId, translatables, {
79
+ onProgress: (done, total) => {
80
+ process.stdout.write(`\r Pushed ${done}/${total} strings...`);
81
+ }
82
+ });
83
+ process.stdout.write('\n');
79
84
  } catch (e) {
80
85
  console.error(chalk.red(`\nSync failed: ${e.message}`));
81
86
  process.exit(1);
@@ -88,6 +93,33 @@ export async function syncCommand(options) {
88
93
  console.log(chalk.yellow(` ā³ ${pending.length} keys pending translation: ${pending.slice(0, 5).join(', ')}${pending.length > 5 ? '...' : ''}`));
89
94
  }
90
95
 
96
+ // 2b. Prune stale translatables from the remote family
97
+ if (options.prune) {
98
+ console.log(chalk.blue('\nšŸ” Checking for stale translatables in the remote family...'));
99
+ try {
100
+ const remoteTranslatables = await api.getTranslatables(config.familyId);
101
+ const localIds = new Set(translatables.map(t => t.id));
102
+ const staleIds = remoteTranslatables
103
+ .filter(t => !localIds.has(t.foreignId || t.id))
104
+ .map(t => t.foreignId || t.id);
105
+
106
+ if (staleIds.length > 0) {
107
+ console.log(chalk.yellow(` Found ${staleIds.length} stale translatable(s) not in source file:`));
108
+ for (const id of staleIds.slice(0, 10)) {
109
+ console.log(chalk.gray(` - ${id}`));
110
+ }
111
+ if (staleIds.length > 10) console.log(chalk.gray(` ... and ${staleIds.length - 10} more`));
112
+
113
+ const deleteResult = await api.deleteTranslatables(config.familyId, staleIds);
114
+ console.log(chalk.green(` šŸ—‘ļø Deleted ${deleteResult.deleted} stale translatable(s) from remote`));
115
+ } else {
116
+ console.log(chalk.green(' āœ… No stale translatables found'));
117
+ }
118
+ } catch (e) {
119
+ console.error(chalk.yellow(` āš ļø Could not prune remote translatables: ${e.message}`));
120
+ }
121
+ }
122
+
91
123
  // 3. Write translation files (skip source locale to avoid overwriting the source file)
92
124
  const targetTranslations = translations.filter(t => t.locale !== config.sourceLocale);
93
125