lingo.dev 0.80.0 → 0.81.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/build/cli.mjs CHANGED
@@ -621,9 +621,9 @@ function makeGitlabInitializer(spinner) {
621
621
 
622
622
  // src/cli/cmd/init.ts
623
623
  import open2 from "open";
624
- var openUrl = (path15) => {
624
+ var openUrl = (path18) => {
625
625
  const settings = getSettings(void 0);
626
- open2(`${settings.auth.webUrl}${path15}`, { wait: false });
626
+ open2(`${settings.auth.webUrl}${path18}`, { wait: false });
627
627
  };
628
628
  var throwHelpError = (option, value) => {
629
629
  if (value === "help") {
@@ -967,8 +967,8 @@ var files_default = new Command4().command("files").description("Print out the l
967
967
  } else if (type.target) {
968
968
  result.push(...targetPaths);
969
969
  }
970
- result.forEach((path15) => {
971
- console.log(path15);
970
+ result.forEach((path18) => {
971
+ console.log(path18);
972
972
  });
973
973
  }
974
974
  }
@@ -990,8 +990,9 @@ var show_default = new Command5().command("show").description("Prints out the cu
990
990
  // src/cli/cmd/i18n.ts
991
991
  import { bucketTypeSchema, localeCodeSchema, resolveOverriddenLocale as resolveOverriddenLocale3 } from "@lingo.dev/_spec";
992
992
  import { Command as Command6 } from "interactive-commander";
993
- import Z4 from "zod";
993
+ import Z3 from "zod";
994
994
  import _20 from "lodash";
995
+ import * as path15 from "path";
995
996
  import Ora5 from "ora";
996
997
 
997
998
  // src/cli/loaders/_utils.ts
@@ -1198,7 +1199,7 @@ function createTextFileLoader(pathPattern) {
1198
1199
  const trimmedResult = result.trim();
1199
1200
  return trimmedResult;
1200
1201
  },
1201
- async push(locale, data, _22, originalLocale) {
1202
+ async push(locale, data, _23, originalLocale) {
1202
1203
  const draftPath = pathPattern.replaceAll("[locale]", locale);
1203
1204
  const finalPath = path10.resolve(draftPath);
1204
1205
  const dirPath = path10.dirname(finalPath);
@@ -1535,9 +1536,9 @@ function createHtmlLoader() {
1535
1536
  const bDepth = b.split("/").length;
1536
1537
  return aDepth - bDepth;
1537
1538
  });
1538
- paths.forEach((path15) => {
1539
- const value = data[path15];
1540
- const [nodePath, attribute] = path15.split("#");
1539
+ paths.forEach((path18) => {
1540
+ const value = data[path18];
1541
+ const [nodePath, attribute] = path18.split("#");
1541
1542
  const [rootTag, ...indices] = nodePath.split("/");
1542
1543
  let parent = rootTag === "head" ? document.head : document.body;
1543
1544
  let current = parent;
@@ -1640,7 +1641,7 @@ function createPropertiesLoader() {
1640
1641
  return result;
1641
1642
  },
1642
1643
  async push(locale, payload) {
1643
- const result = Object.entries(payload).filter(([_22, value]) => value != null).map(([key, value]) => `${key}=${value}`).join("\n");
1644
+ const result = Object.entries(payload).filter(([_23, value]) => value != null).map(([key, value]) => `${key}=${value}`).join("\n");
1644
1645
  return result;
1645
1646
  }
1646
1647
  });
@@ -1889,10 +1890,10 @@ function createUnlocalizableLoader(isCacheRestore = false, returnUnlocalizedKeys
1889
1890
  }
1890
1891
  }
1891
1892
  return false;
1892
- }).map(([key, _22]) => key);
1893
- const result = _10.omitBy(input2, (_22, key) => passthroughKeys.includes(key));
1893
+ }).map(([key, _23]) => key);
1894
+ const result = _10.omitBy(input2, (_23, key) => passthroughKeys.includes(key));
1894
1895
  if (returnUnlocalizedKeys) {
1895
- result.unlocalizable = _10.omitBy(input2, (_22, key) => !passthroughKeys.includes(key));
1896
+ result.unlocalizable = _10.omitBy(input2, (_23, key) => !passthroughKeys.includes(key));
1896
1897
  }
1897
1898
  return result;
1898
1899
  },
@@ -2583,18 +2584,18 @@ function createRawDatoValue(parsedDatoValue, originalRawDatoValue, isClean = fal
2583
2584
  }
2584
2585
  function serializeStructuredText(rawStructuredText) {
2585
2586
  return serializeStructuredTextNode(rawStructuredText);
2586
- function serializeStructuredTextNode(node, path15 = [], acc = {}) {
2587
+ function serializeStructuredTextNode(node, path18 = [], acc = {}) {
2587
2588
  if ("document" in node) {
2588
- return serializeStructuredTextNode(node.document, [...path15, "document"], acc);
2589
+ return serializeStructuredTextNode(node.document, [...path18, "document"], acc);
2589
2590
  }
2590
2591
  if (!_15.isNil(node.value)) {
2591
- acc[[...path15, "value"].join(".")] = node.value;
2592
+ acc[[...path18, "value"].join(".")] = node.value;
2592
2593
  } else if (_15.get(node, "type") === "block") {
2593
- acc[[...path15, "item"].join(".")] = serializeBlock(node.item);
2594
+ acc[[...path18, "item"].join(".")] = serializeBlock(node.item);
2594
2595
  }
2595
2596
  if (node.children) {
2596
2597
  for (let i = 0; i < node.children.length; i++) {
2597
- serializeStructuredTextNode(node.children[i], [...path15, i.toString()], acc);
2598
+ serializeStructuredTextNode(node.children[i], [...path18, i.toString()], acc);
2598
2599
  }
2599
2600
  }
2600
2601
  return acc;
@@ -2653,8 +2654,8 @@ function deserializeBlockList(parsedBlockList, originalRawBlockList, isClean = f
2653
2654
  }
2654
2655
  function deserializeStructuredText(parsedStructuredText, originalRawStructuredText) {
2655
2656
  const result = _15.cloneDeep(originalRawStructuredText);
2656
- for (const [path15, value] of _15.entries(parsedStructuredText)) {
2657
- const realPath = _15.chain(path15.split(".")).flatMap((s) => !_15.isNaN(_15.toNumber(s)) ? ["children", s] : s).value();
2657
+ for (const [path18, value] of _15.entries(parsedStructuredText)) {
2658
+ const realPath = _15.chain(path18.split(".")).flatMap((s) => !_15.isNaN(_15.toNumber(s)) ? ["children", s] : s).value();
2658
2659
  const deserializedValue = createRawDatoValue(value, _15.get(originalRawStructuredText, realPath), true);
2659
2660
  _15.set(result, realPath, deserializedValue);
2660
2661
  }
@@ -3179,75 +3180,6 @@ function createBucketLoader(bucketType, bucketPathPattern, options) {
3179
3180
  }
3180
3181
  }
3181
3182
 
3182
- // src/cli/utils/lockfile.ts
3183
- import fs10 from "fs";
3184
- import path12 from "path";
3185
- import Z3 from "zod";
3186
- import YAML3 from "yaml";
3187
- import { MD5 } from "object-hash";
3188
- import _19 from "lodash";
3189
- function createLockfileHelper() {
3190
- return {
3191
- isLockfileExists: () => {
3192
- const lockfilePath = _getLockfilePath();
3193
- return fs10.existsSync(lockfilePath);
3194
- },
3195
- registerSourceData: (pathPattern, sourceData) => {
3196
- const lockfile = _loadLockfile();
3197
- const sectionKey = MD5(pathPattern);
3198
- const sectionChecksums = _19.mapValues(sourceData, (value) => MD5(value));
3199
- lockfile.checksums[sectionKey] = sectionChecksums;
3200
- _saveLockfile(lockfile);
3201
- },
3202
- registerPartialSourceData: (pathPattern, partialSourceData) => {
3203
- const lockfile = _loadLockfile();
3204
- const sectionKey = MD5(pathPattern);
3205
- const sectionChecksums = _19.mapValues(partialSourceData, (value) => MD5(value));
3206
- lockfile.checksums[sectionKey] = _19.merge({}, lockfile.checksums[sectionKey] ?? {}, sectionChecksums);
3207
- _saveLockfile(lockfile);
3208
- },
3209
- extractUpdatedData: (pathPattern, sourceData) => {
3210
- const lockfile = _loadLockfile();
3211
- const sectionKey = MD5(pathPattern);
3212
- const currentChecksums = _19.mapValues(sourceData, (value) => MD5(value));
3213
- const savedChecksums = lockfile.checksums[sectionKey] || {};
3214
- const updatedData = _19.pickBy(sourceData, (value, key) => savedChecksums[key] !== currentChecksums[key]);
3215
- return updatedData;
3216
- }
3217
- };
3218
- function _loadLockfile() {
3219
- const lockfilePath = _getLockfilePath();
3220
- if (!fs10.existsSync(lockfilePath)) {
3221
- return LockfileSchema.parse({});
3222
- }
3223
- const content = fs10.readFileSync(lockfilePath, "utf-8");
3224
- const result = LockfileSchema.parse(YAML3.parse(content));
3225
- return result;
3226
- }
3227
- function _saveLockfile(lockfile) {
3228
- const lockfilePath = _getLockfilePath();
3229
- const content = YAML3.stringify(lockfile);
3230
- fs10.writeFileSync(lockfilePath, content);
3231
- }
3232
- function _getLockfilePath() {
3233
- return path12.join(process.cwd(), "i18n.lock");
3234
- }
3235
- }
3236
- var LockfileSchema = Z3.object({
3237
- version: Z3.literal(1).default(1),
3238
- checksums: Z3.record(
3239
- Z3.string(),
3240
- // localizable files' keys
3241
- Z3.record(
3242
- // checksums hashmap
3243
- Z3.string(),
3244
- // key
3245
- Z3.string()
3246
- // checksum of the key's value in the source locale
3247
- ).default({})
3248
- ).default({})
3249
- });
3250
-
3251
3183
  // src/cli/cmd/i18n.ts
3252
3184
  import chalk from "chalk";
3253
3185
  import { createTwoFilesPatch } from "diff";
@@ -3255,8 +3187,8 @@ import inquirer2 from "inquirer";
3255
3187
  import externalEditor from "external-editor";
3256
3188
 
3257
3189
  // src/cli/utils/cache.ts
3258
- import path13 from "path";
3259
- import fs11 from "fs";
3190
+ import path12 from "path";
3191
+ import fs10 from "fs";
3260
3192
  var cacheChunk = (targetLocale, sourceChunk, processedChunk) => {
3261
3193
  const rows = Object.entries(sourceChunk).map(([key, source]) => ({
3262
3194
  targetLocale,
@@ -3286,26 +3218,26 @@ function getNormalizedCache() {
3286
3218
  function deleteCache() {
3287
3219
  const cacheFilePath = _getCacheFilePath();
3288
3220
  try {
3289
- fs11.unlinkSync(cacheFilePath);
3221
+ fs10.unlinkSync(cacheFilePath);
3290
3222
  } catch (e) {
3291
3223
  }
3292
3224
  }
3293
3225
  function _loadCache() {
3294
3226
  const cacheFilePath = _getCacheFilePath();
3295
- if (!fs11.existsSync(cacheFilePath)) {
3227
+ if (!fs10.existsSync(cacheFilePath)) {
3296
3228
  return [];
3297
3229
  }
3298
- const content = fs11.readFileSync(cacheFilePath, "utf-8");
3230
+ const content = fs10.readFileSync(cacheFilePath, "utf-8");
3299
3231
  const result = _parseJSONLines(content);
3300
3232
  return result;
3301
3233
  }
3302
3234
  function _appendToCache(rows) {
3303
3235
  const cacheFilePath = _getCacheFilePath();
3304
3236
  const lines = _buildJSONLines(rows);
3305
- fs11.appendFileSync(cacheFilePath, lines);
3237
+ fs10.appendFileSync(cacheFilePath, lines);
3306
3238
  }
3307
3239
  function _getCacheFilePath() {
3308
- return path13.join(process.cwd(), "i18n.cache");
3240
+ return path12.join(process.cwd(), "i18n.cache");
3309
3241
  }
3310
3242
  function _buildJSONLines(rows) {
3311
3243
  return rows.map((row) => JSON.stringify(row)).join("\n") + "\n";
@@ -3325,6 +3257,9 @@ function _tryParseJSON(line) {
3325
3257
  import { LingoDotDevEngine } from "@lingo.dev/_sdk";
3326
3258
  function createLingoLocalizer(params) {
3327
3259
  return async (input2, onProgress) => {
3260
+ if (!Object.keys(input2.processableData).length) {
3261
+ return input2.processableData;
3262
+ }
3328
3263
  const lingo = new LingoDotDevEngine({
3329
3264
  apiKey: params.apiKey,
3330
3265
  apiUrl: params.apiUrl
@@ -3345,10 +3280,13 @@ function createLingoLocalizer(params) {
3345
3280
  };
3346
3281
  }
3347
3282
 
3348
- // src/cli/processor/openai.ts
3283
+ // src/cli/processor/basic.ts
3349
3284
  import { generateText } from "ai";
3350
3285
  function createBasicTranslator(model, systemPrompt) {
3351
3286
  return async (input2, onProgress) => {
3287
+ if (!Object.keys(input2.processableData).length) {
3288
+ return input2.processableData;
3289
+ }
3352
3290
  if (!process.env.OPENAI_API_KEY) {
3353
3291
  throw new Error("OPENAI_API_KEY is not set");
3354
3292
  }
@@ -3393,7 +3331,7 @@ function createBasicTranslator(model, systemPrompt) {
3393
3331
  ]
3394
3332
  });
3395
3333
  const result = JSON.parse(response.text);
3396
- return result;
3334
+ return result?.data || {};
3397
3335
  };
3398
3336
  }
3399
3337
 
@@ -3462,12 +3400,127 @@ async function trackEvent(distinctId, event, properties) {
3462
3400
  await posthog.capture({
3463
3401
  distinctId,
3464
3402
  event,
3465
- properties
3403
+ properties: {
3404
+ ...properties,
3405
+ meta: {
3406
+ version: process.env.npm_package_version,
3407
+ isCi: process.env.CI === "true"
3408
+ }
3409
+ }
3466
3410
  });
3467
3411
  await posthog.shutdown();
3468
3412
  }
3469
3413
 
3414
+ // src/cli/utils/delta.ts
3415
+ import _19 from "lodash";
3416
+ import z from "zod";
3417
+ import { MD5 } from "object-hash";
3418
+
3419
+ // src/cli/utils/fs.ts
3420
+ import * as fs11 from "fs";
3421
+ import * as path13 from "path";
3422
+ function tryReadFile(filePath, defaultValue = null) {
3423
+ try {
3424
+ const content = fs11.readFileSync(filePath, "utf-8");
3425
+ return content;
3426
+ } catch (error) {
3427
+ return defaultValue;
3428
+ }
3429
+ }
3430
+ function writeFile(filePath, content) {
3431
+ const dir = path13.dirname(filePath);
3432
+ if (!fs11.existsSync(dir)) {
3433
+ fs11.mkdirSync(dir, { recursive: true });
3434
+ }
3435
+ fs11.writeFileSync(filePath, content);
3436
+ }
3437
+ function checkIfFileExists(filePath) {
3438
+ return fs11.existsSync(filePath);
3439
+ }
3440
+
3441
+ // src/cli/utils/delta.ts
3442
+ import * as path14 from "path";
3443
+ import YAML3 from "yaml";
3444
+ var LockSchema = z.object({
3445
+ version: z.literal(1).default(1),
3446
+ checksums: z.record(
3447
+ z.string(),
3448
+ // localizable files' keys
3449
+ // checksums hashmap
3450
+ z.record(
3451
+ // key
3452
+ z.string(),
3453
+ // checksum of the key's value in the source locale
3454
+ z.string()
3455
+ ).default({})
3456
+ ).default({})
3457
+ });
3458
+ function createDeltaProcessor(fileKey) {
3459
+ const lockfilePath = path14.join(process.cwd(), "i18n.lock");
3460
+ return {
3461
+ async checkIfLockExists() {
3462
+ return checkIfFileExists(lockfilePath);
3463
+ },
3464
+ async calculateDelta(params) {
3465
+ let added = _19.difference(Object.keys(params.sourceData), Object.keys(params.targetData));
3466
+ let removed = _19.difference(Object.keys(params.targetData), Object.keys(params.sourceData));
3467
+ const updated = _19.filter(Object.keys(params.sourceData), (key) => {
3468
+ return MD5(params.sourceData[key]) !== params.checksums[key] && params.checksums[key];
3469
+ });
3470
+ const renamed = [];
3471
+ for (const addedKey of added) {
3472
+ const addedHash = MD5(params.sourceData[addedKey]);
3473
+ for (const removedKey of removed) {
3474
+ if (params.checksums[removedKey] === addedHash) {
3475
+ renamed.push([removedKey, addedKey]);
3476
+ break;
3477
+ }
3478
+ }
3479
+ }
3480
+ added = added.filter((key) => !renamed.some(([oldKey, newKey]) => newKey === key));
3481
+ removed = removed.filter((key) => !renamed.some(([oldKey, newKey]) => oldKey === key));
3482
+ const hasChanges = [added.length > 0, removed.length > 0, updated.length > 0, renamed.length > 0].some((v) => v);
3483
+ return {
3484
+ added,
3485
+ removed,
3486
+ updated,
3487
+ renamed,
3488
+ hasChanges
3489
+ };
3490
+ },
3491
+ async loadLock() {
3492
+ const lockfileContent = tryReadFile(lockfilePath, null);
3493
+ const lockfileYaml = lockfileContent ? YAML3.parse(lockfileContent) : null;
3494
+ const lockfileData = lockfileYaml ? LockSchema.parse(lockfileYaml) : {
3495
+ version: 1,
3496
+ checksums: {}
3497
+ };
3498
+ return lockfileData;
3499
+ },
3500
+ async saveLock(lockData) {
3501
+ const lockfileYaml = YAML3.stringify(lockData);
3502
+ writeFile(lockfilePath, lockfileYaml);
3503
+ },
3504
+ async loadChecksums() {
3505
+ const id = MD5(fileKey);
3506
+ const lockfileData = await this.loadLock();
3507
+ return lockfileData.checksums[id] || {};
3508
+ },
3509
+ async saveChecksums(checksums) {
3510
+ const id = MD5(fileKey);
3511
+ const lockfileData = await this.loadLock();
3512
+ lockfileData.checksums[id] = checksums;
3513
+ await this.saveLock(lockfileData);
3514
+ },
3515
+ async createChecksums(sourceData) {
3516
+ const checksums = _19.mapValues(sourceData, (value) => MD5(value));
3517
+ return checksums;
3518
+ }
3519
+ };
3520
+ }
3521
+
3470
3522
  // src/cli/cmd/i18n.ts
3523
+ import { flatten as flatten2, unflatten as unflatten2 } from "flat";
3471
3524
  var i18n_default = new Command6().command("i18n").description("Run Localization engine").helpOption("-h, --help", "Show help").option("--locale <locale>", "Locale to process", (val, prev) => prev ? [...prev, val] : [val]).option("--bucket <bucket>", "Bucket to process", (val, prev) => prev ? [...prev, val] : [val]).option(
3472
3525
  "--key <key>",
3473
3526
  "Key to process. Process only a specific translation key, useful for debugging or updating a single entry"
@@ -3515,7 +3568,7 @@ var i18n_default = new Command6().command("i18n").description("Run Localization
3515
3568
  ora.succeed("Buckets retrieved");
3516
3569
  if (flags.file?.length) {
3517
3570
  buckets = buckets.map((bucket) => {
3518
- const paths = bucket.paths.filter((path15) => flags.file.find((file) => path15.pathPattern?.match(file)));
3571
+ const paths = bucket.paths.filter((path18) => flags.file.find((file) => path18.pathPattern?.match(file)));
3519
3572
  return { ...bucket, paths };
3520
3573
  }).filter((bucket) => bucket.paths.length > 0);
3521
3574
  if (buckets.length === 0) {
@@ -3525,16 +3578,17 @@ var i18n_default = new Command6().command("i18n").description("Run Localization
3525
3578
  ora.info(`\x1B[36mProcessing only filtered buckets:\x1B[0m`);
3526
3579
  buckets.map((bucket) => {
3527
3580
  ora.info(` ${bucket.type}:`);
3528
- bucket.paths.forEach((path15) => {
3529
- ora.info(` - ${path15.pathPattern}`);
3581
+ bucket.paths.forEach((path18) => {
3582
+ ora.info(` - ${path18.pathPattern}`);
3530
3583
  });
3531
3584
  });
3532
3585
  }
3533
3586
  }
3534
3587
  const targetLocales = flags.locale?.length ? flags.locale : i18nConfig.locale.targets;
3535
- const lockfileHelper = createLockfileHelper();
3536
- ora.start("Ensuring i18n.lock exists...");
3537
- if (!lockfileHelper.isLockfileExists()) {
3588
+ ora.start("Setting up localization cache...");
3589
+ const checkLockfileProcessor = createDeltaProcessor("");
3590
+ const lockfileExists = await checkLockfileProcessor.checkIfLockExists();
3591
+ if (!lockfileExists) {
3538
3592
  ora.start("Creating i18n.lock...");
3539
3593
  for (const bucket of buckets) {
3540
3594
  for (const bucketPath of bucket.paths) {
@@ -3547,12 +3601,66 @@ var i18n_default = new Command6().command("i18n").description("Run Localization
3547
3601
  bucketLoader.setDefaultLocale(sourceLocale);
3548
3602
  await bucketLoader.init();
3549
3603
  const sourceData = await bucketLoader.pull(i18nConfig.locale.source);
3550
- lockfileHelper.registerSourceData(bucketPath.pathPattern, sourceData);
3604
+ const deltaProcessor = createDeltaProcessor(bucketPath.pathPattern);
3605
+ const checksums = await deltaProcessor.createChecksums(sourceData);
3606
+ await deltaProcessor.saveChecksums(checksums);
3551
3607
  }
3552
3608
  }
3553
- ora.succeed("i18n.lock created");
3609
+ ora.succeed("Localization cache initialized");
3554
3610
  } else {
3555
- ora.succeed("i18n.lock loaded");
3611
+ ora.succeed("Localization cache loaded");
3612
+ }
3613
+ for (const bucket of buckets) {
3614
+ if (bucket.type !== "json") {
3615
+ continue;
3616
+ }
3617
+ ora.start("Validating localization state...");
3618
+ for (const bucketPath of bucket.paths) {
3619
+ const sourceLocale = resolveOverriddenLocale3(i18nConfig.locale.source, bucketPath.delimiter);
3620
+ const deltaProcessor = createDeltaProcessor(bucketPath.pathPattern);
3621
+ const sourcePath = path15.join(process.cwd(), bucketPath.pathPattern.replace("[locale]", sourceLocale));
3622
+ const sourceContent = tryReadFile(sourcePath, null);
3623
+ const sourceData = JSON.parse(sourceContent || "{}");
3624
+ const sourceFlattenedData = flatten2(sourceData, {
3625
+ delimiter: "/",
3626
+ transformKey(key) {
3627
+ return encodeURIComponent(key);
3628
+ }
3629
+ });
3630
+ for (const _targetLocale of targetLocales) {
3631
+ const targetLocale = resolveOverriddenLocale3(_targetLocale, bucketPath.delimiter);
3632
+ const targetPath = path15.join(process.cwd(), bucketPath.pathPattern.replace("[locale]", targetLocale));
3633
+ const targetContent = tryReadFile(targetPath, null);
3634
+ const targetData = JSON.parse(targetContent || "{}");
3635
+ const targetFlattenedData = flatten2(targetData, {
3636
+ delimiter: "/",
3637
+ transformKey(key) {
3638
+ return encodeURIComponent(key);
3639
+ }
3640
+ });
3641
+ const checksums = await deltaProcessor.loadChecksums();
3642
+ const delta = await deltaProcessor.calculateDelta({
3643
+ sourceData: sourceFlattenedData,
3644
+ targetData: targetFlattenedData,
3645
+ checksums
3646
+ });
3647
+ if (!delta.hasChanges) {
3648
+ continue;
3649
+ }
3650
+ for (const [oldKey, newKey] of delta.renamed) {
3651
+ targetFlattenedData[newKey] = targetFlattenedData[oldKey];
3652
+ delete targetFlattenedData[oldKey];
3653
+ }
3654
+ const updatedTargetData = unflatten2(targetFlattenedData, {
3655
+ delimiter: "/",
3656
+ transformKey(key) {
3657
+ return decodeURIComponent(key);
3658
+ }
3659
+ });
3660
+ await writeFile(targetPath, JSON.stringify(updatedTargetData, null, 2));
3661
+ }
3662
+ }
3663
+ ora.succeed("Localization state check completed");
3556
3664
  }
3557
3665
  const cache = getNormalizedCache();
3558
3666
  if (cache) {
@@ -3584,7 +3692,9 @@ var i18n_default = new Command6().command("i18n").description("Run Localization
3584
3692
  }
3585
3693
  }
3586
3694
  await bucketLoader.push(targetLocale, targetData);
3587
- lockfileHelper.registerPartialSourceData(bucketPath.pathPattern, cachedSourceData);
3695
+ const deltaProcessor = createDeltaProcessor(bucketPath.pathPattern);
3696
+ const checksums = await deltaProcessor.createChecksums(cachedSourceData);
3697
+ await deltaProcessor.saveChecksums(checksums);
3588
3698
  bucketOra.succeed(
3589
3699
  `[${sourceLocale} -> ${targetLocale}] Recovered ${Object.keys(cachedSourceData).length} entries from cache`
3590
3700
  );
@@ -3615,7 +3725,13 @@ var i18n_default = new Command6().command("i18n").description("Run Localization
3615
3725
  const { unlocalizable: sourceUnlocalizable, ...sourceData } = await bucketLoader.pull(
3616
3726
  i18nConfig.locale.source
3617
3727
  );
3618
- const updatedSourceData = lockfileHelper.extractUpdatedData(bucketPath.pathPattern, sourceData);
3728
+ const deltaProcessor = createDeltaProcessor(bucketPath.pathPattern);
3729
+ const sourceChecksums = await deltaProcessor.createChecksums(sourceData);
3730
+ const savedChecksums = await deltaProcessor.loadChecksums();
3731
+ const updatedSourceData = _20.pickBy(
3732
+ sourceData,
3733
+ (value, key) => sourceChecksums[key] !== savedChecksums[key]
3734
+ );
3619
3735
  if (Object.keys(updatedSourceData).length > 0) {
3620
3736
  requiresUpdate = "updated";
3621
3737
  break bucketLoop;
@@ -3675,15 +3791,17 @@ var i18n_default = new Command6().command("i18n").description("Run Localization
3675
3791
  try {
3676
3792
  bucketOra.start(`[${sourceLocale} -> ${targetLocale}] (0%) Localization in progress...`);
3677
3793
  sourceData = await bucketLoader.pull(sourceLocale);
3678
- const updatedSourceData = flags.force ? sourceData : lockfileHelper.extractUpdatedData(bucketPath.pathPattern, sourceData);
3679
3794
  const targetData = await bucketLoader.pull(targetLocale);
3680
- let processableData = calculateDataDelta({
3795
+ const deltaProcessor2 = createDeltaProcessor(bucketPath.pathPattern);
3796
+ const checksums2 = await deltaProcessor2.loadChecksums();
3797
+ const delta = await deltaProcessor2.calculateDelta({
3681
3798
  sourceData,
3682
- updatedSourceData,
3683
- targetData
3799
+ targetData,
3800
+ checksums: checksums2
3684
3801
  });
3802
+ let processableData = _20.chain(sourceData).entries().filter(([key, value]) => delta.added.includes(key) || delta.updated.includes(key) || !!flags.force).fromPairs().value();
3685
3803
  if (flags.key) {
3686
- processableData = _20.pickBy(processableData, (_22, key) => key === flags.key);
3804
+ processableData = _20.pickBy(processableData, (_23, key) => key === flags.key);
3687
3805
  }
3688
3806
  if (flags.verbose) {
3689
3807
  bucketOra.info(JSON.stringify(processableData, null, 2));
@@ -3751,7 +3869,9 @@ var i18n_default = new Command6().command("i18n").description("Run Localization
3751
3869
  }
3752
3870
  }
3753
3871
  }
3754
- lockfileHelper.registerSourceData(bucketPath.pathPattern, sourceData);
3872
+ const deltaProcessor = createDeltaProcessor(bucketPath.pathPattern);
3873
+ const checksums = await deltaProcessor.createChecksums(sourceData);
3874
+ await deltaProcessor.saveChecksums(checksums);
3755
3875
  }
3756
3876
  } catch (_error) {
3757
3877
  const error = new Error(`Failed to process bucket ${bucket.type}: ${_error.message}`);
@@ -3786,25 +3906,19 @@ var i18n_default = new Command6().command("i18n").description("Run Localization
3786
3906
  process.exit(1);
3787
3907
  }
3788
3908
  });
3789
- function calculateDataDelta(args) {
3790
- const newKeys = _20.difference(Object.keys(args.sourceData), Object.keys(args.targetData));
3791
- const updatedKeys = Object.keys(args.updatedSourceData);
3792
- const result = _20.chain(args.sourceData).pickBy((value, key) => newKeys.includes(key) || updatedKeys.includes(key)).value();
3793
- return result;
3794
- }
3795
3909
  function parseFlags(options) {
3796
- return Z4.object({
3797
- apiKey: Z4.string().optional(),
3798
- locale: Z4.array(localeCodeSchema).optional(),
3799
- bucket: Z4.array(bucketTypeSchema).optional(),
3800
- force: Z4.boolean().optional(),
3801
- frozen: Z4.boolean().optional(),
3802
- verbose: Z4.boolean().optional(),
3803
- strict: Z4.boolean().optional(),
3804
- key: Z4.string().optional(),
3805
- file: Z4.array(Z4.string()).optional(),
3806
- interactive: Z4.boolean().default(false),
3807
- debug: Z4.boolean().default(false)
3910
+ return Z3.object({
3911
+ apiKey: Z3.string().optional(),
3912
+ locale: Z3.array(localeCodeSchema).optional(),
3913
+ bucket: Z3.array(bucketTypeSchema).optional(),
3914
+ force: Z3.boolean().optional(),
3915
+ frozen: Z3.boolean().optional(),
3916
+ verbose: Z3.boolean().optional(),
3917
+ strict: Z3.boolean().optional(),
3918
+ key: Z3.string().optional(),
3919
+ file: Z3.array(Z3.string()).optional(),
3920
+ interactive: Z3.boolean().default(false),
3921
+ debug: Z3.boolean().default(false)
3808
3922
  }).parse(options);
3809
3923
  }
3810
3924
  async function validateAuth(settings) {
@@ -3950,6 +4064,77 @@ Editing value for: ${chalk.cyan(key)}`);
3950
4064
  import { Command as Command7 } from "interactive-commander";
3951
4065
  import Z5 from "zod";
3952
4066
  import Ora6 from "ora";
4067
+
4068
+ // src/cli/utils/lockfile.ts
4069
+ import fs12 from "fs";
4070
+ import path16 from "path";
4071
+ import Z4 from "zod";
4072
+ import YAML4 from "yaml";
4073
+ import { MD5 as MD52 } from "object-hash";
4074
+ import _21 from "lodash";
4075
+ function createLockfileHelper() {
4076
+ return {
4077
+ isLockfileExists: () => {
4078
+ const lockfilePath = _getLockfilePath();
4079
+ return fs12.existsSync(lockfilePath);
4080
+ },
4081
+ registerSourceData: (pathPattern, sourceData) => {
4082
+ const lockfile = _loadLockfile();
4083
+ const sectionKey = MD52(pathPattern);
4084
+ const sectionChecksums = _21.mapValues(sourceData, (value) => MD52(value));
4085
+ lockfile.checksums[sectionKey] = sectionChecksums;
4086
+ _saveLockfile(lockfile);
4087
+ },
4088
+ registerPartialSourceData: (pathPattern, partialSourceData) => {
4089
+ const lockfile = _loadLockfile();
4090
+ const sectionKey = MD52(pathPattern);
4091
+ const sectionChecksums = _21.mapValues(partialSourceData, (value) => MD52(value));
4092
+ lockfile.checksums[sectionKey] = _21.merge({}, lockfile.checksums[sectionKey] ?? {}, sectionChecksums);
4093
+ _saveLockfile(lockfile);
4094
+ },
4095
+ extractUpdatedData: (pathPattern, sourceData) => {
4096
+ const lockfile = _loadLockfile();
4097
+ const sectionKey = MD52(pathPattern);
4098
+ const currentChecksums = _21.mapValues(sourceData, (value) => MD52(value));
4099
+ const savedChecksums = lockfile.checksums[sectionKey] || {};
4100
+ const updatedData = _21.pickBy(sourceData, (value, key) => savedChecksums[key] !== currentChecksums[key]);
4101
+ return updatedData;
4102
+ }
4103
+ };
4104
+ function _loadLockfile() {
4105
+ const lockfilePath = _getLockfilePath();
4106
+ if (!fs12.existsSync(lockfilePath)) {
4107
+ return LockfileSchema.parse({});
4108
+ }
4109
+ const content = fs12.readFileSync(lockfilePath, "utf-8");
4110
+ const result = LockfileSchema.parse(YAML4.parse(content));
4111
+ return result;
4112
+ }
4113
+ function _saveLockfile(lockfile) {
4114
+ const lockfilePath = _getLockfilePath();
4115
+ const content = YAML4.stringify(lockfile);
4116
+ fs12.writeFileSync(lockfilePath, content);
4117
+ }
4118
+ function _getLockfilePath() {
4119
+ return path16.join(process.cwd(), "i18n.lock");
4120
+ }
4121
+ }
4122
+ var LockfileSchema = Z4.object({
4123
+ version: Z4.literal(1).default(1),
4124
+ checksums: Z4.record(
4125
+ Z4.string(),
4126
+ // localizable files' keys
4127
+ Z4.record(
4128
+ // checksums hashmap
4129
+ Z4.string(),
4130
+ // key
4131
+ Z4.string()
4132
+ // checksum of the key's value in the source locale
4133
+ ).default({})
4134
+ ).default({})
4135
+ });
4136
+
4137
+ // src/cli/cmd/lockfile.ts
3953
4138
  import { resolveOverriddenLocale as resolveOverriddenLocale4 } from "@lingo.dev/_spec";
3954
4139
  var lockfile_default = new Command7().command("lockfile").description("Create a lockfile if it does not exist").helpOption("-h, --help", "Show help").option("-f, --force", "Force create a lockfile").action(async (options) => {
3955
4140
  const flags = flagsSchema.parse(options);
@@ -3982,7 +4167,7 @@ var flagsSchema = Z5.object({
3982
4167
  // src/cli/cmd/cleanup.ts
3983
4168
  import { resolveOverriddenLocale as resolveOverriddenLocale5 } from "@lingo.dev/_spec";
3984
4169
  import { Command as Command8 } from "interactive-commander";
3985
- import _21 from "lodash";
4170
+ import _22 from "lodash";
3986
4171
  import Ora7 from "ora";
3987
4172
  var cleanup_default = new Command8().command("cleanup").description("Remove keys from target files that do not exist in the source file").helpOption("-h, --help", "Show help").option("--locale <locale>", "Specific locale to cleanup").option("--bucket <bucket>", "Specific bucket to cleanup").option("--dry-run", "Show what would be removed without making changes").option(
3988
4173
  "--verbose",
@@ -4018,7 +4203,7 @@ var cleanup_default = new Command8().command("cleanup").description("Remove keys
4018
4203
  try {
4019
4204
  const targetData = await bucketLoader.pull(targetLocale);
4020
4205
  const targetKeys = Object.keys(targetData);
4021
- const keysToRemove = _21.difference(targetKeys, sourceKeys);
4206
+ const keysToRemove = _22.difference(targetKeys, sourceKeys);
4022
4207
  if (keysToRemove.length === 0) {
4023
4208
  bucketOra.succeed(`[${targetLocale}] No keys to remove`);
4024
4209
  continue;
@@ -4027,7 +4212,7 @@ var cleanup_default = new Command8().command("cleanup").description("Remove keys
4027
4212
  bucketOra.info(`[${targetLocale}] Keys to remove: ${JSON.stringify(keysToRemove, null, 2)}`);
4028
4213
  }
4029
4214
  if (!options.dryRun) {
4030
- const cleanedData = _21.pick(targetData, sourceKeys);
4215
+ const cleanedData = _22.pick(targetData, sourceKeys);
4031
4216
  await bucketLoader.push(targetLocale, cleanedData);
4032
4217
  bucketOra.succeed(`[${targetLocale}] Removed ${keysToRemove.length} keys`);
4033
4218
  } else {
@@ -4082,7 +4267,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4082
4267
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4083
4268
  import Z6 from "zod";
4084
4269
  import { ReplexicaEngine } from "@lingo.dev/_sdk";
4085
- var mcp_default = new Command9().command("mcp").description("Use Lingo.dev model context provider with your AI agent").helpOption("-h, --help", "Show help").action(async (_22, program) => {
4270
+ var mcp_default = new Command9().command("mcp").description("Use Lingo.dev model context provider with your AI agent").helpOption("-h, --help", "Show help").action(async (_23, program) => {
4086
4271
  const apiKey = program.args[0];
4087
4272
  const settings = getSettings(apiKey);
4088
4273
  if (!settings.auth.apiKey) {
@@ -4140,7 +4325,7 @@ import { execSync as execSync2 } from "child_process";
4140
4325
 
4141
4326
  // ../../action/src/flows/in-branch.ts
4142
4327
  import { execSync } from "child_process";
4143
- import path14 from "path";
4328
+ import path17 from "path";
4144
4329
 
4145
4330
  // ../../action/src/flows/_base.ts
4146
4331
  var IntegrationFlow = class {
@@ -4218,7 +4403,7 @@ var InBranchFlow = class extends IntegrationFlow {
4218
4403
  return false;
4219
4404
  }
4220
4405
  }
4221
- const workingDir = path14.resolve(process.cwd(), this.platformKit.config.workingDir);
4406
+ const workingDir = path17.resolve(process.cwd(), this.platformKit.config.workingDir);
4222
4407
  if (workingDir !== process.cwd()) {
4223
4408
  this.ora.info(`Changing to working directory: ${this.platformKit.config.workingDir}`);
4224
4409
  process.chdir(workingDir);
@@ -4387,15 +4572,21 @@ Hey team,
4387
4572
  };
4388
4573
 
4389
4574
  // ../../action/src/platforms/bitbucket.ts
4390
- import { execSync as execSync3 } from "child_process";
4575
+ import { execSync as execSync4 } from "child_process";
4391
4576
  import bbLib from "bitbucket";
4392
4577
  import Z8 from "zod";
4393
4578
 
4394
4579
  // ../../action/src/platforms/_base.ts
4580
+ import { execSync as execSync3 } from "child_process";
4395
4581
  import Z7 from "zod";
4396
4582
  var defaultMessage = "feat: update translations via @lingodotdev";
4397
4583
  var PlatformKit = class {
4398
- gitConfig() {
4584
+ gitConfig(token, repoUrl) {
4585
+ if (token && repoUrl) {
4586
+ execSync3(`git remote set-url origin ${repoUrl}`, {
4587
+ stdio: "inherit"
4588
+ });
4589
+ }
4399
4590
  }
4400
4591
  get config() {
4401
4592
  const env = Z7.object({
@@ -4479,10 +4670,10 @@ var BitbucketPlatformKit = class extends PlatformKit {
4479
4670
  });
4480
4671
  }
4481
4672
  async gitConfig() {
4482
- execSync3("git config --unset http.${BITBUCKET_GIT_HTTP_ORIGIN}.proxy", {
4673
+ execSync4("git config --unset http.${BITBUCKET_GIT_HTTP_ORIGIN}.proxy", {
4483
4674
  stdio: "inherit"
4484
4675
  });
4485
- execSync3("git config http.${BITBUCKET_GIT_HTTP_ORIGIN}.proxy http://host.docker.internal:29418/", {
4676
+ execSync4("git config http.${BITBUCKET_GIT_HTTP_ORIGIN}.proxy http://host.docker.internal:29418/", {
4486
4677
  stdio: "inherit"
4487
4678
  });
4488
4679
  }
@@ -4509,7 +4700,6 @@ var BitbucketPlatformKit = class extends PlatformKit {
4509
4700
  // ../../action/src/platforms/github.ts
4510
4701
  import { Octokit } from "octokit";
4511
4702
  import Z9 from "zod";
4512
- import { execSync as execSync4 } from "child_process";
4513
4703
  var GitHubPlatformKit = class extends PlatformKit {
4514
4704
  _octokit;
4515
4705
  get octokit() {
@@ -4566,9 +4756,7 @@ var GitHubPlatformKit = class extends PlatformKit {
4566
4756
  if (ghToken && processOwnCommits) {
4567
4757
  console.log("Using provided GH_TOKEN. This will trigger your CI/CD pipeline to run again.");
4568
4758
  const url = `https://${ghToken}@github.com/${repositoryOwner}/${repositoryName}.git`;
4569
- execSync4(`git remote set-url origin ${url}`, {
4570
- stdio: "inherit"
4571
- });
4759
+ super.gitConfig(ghToken, url);
4572
4760
  }
4573
4761
  }
4574
4762
  get platformConfig() {
@@ -4596,7 +4784,6 @@ var GitHubPlatformKit = class extends PlatformKit {
4596
4784
  // ../../action/src/platforms/gitlab.ts
4597
4785
  import { Gitlab } from "@gitbeaker/rest";
4598
4786
  import Z10 from "zod";
4599
- import { execSync as execSync5 } from "child_process";
4600
4787
  var gl = new Gitlab({ token: "" });
4601
4788
  var GitlabPlatformKit = class extends PlatformKit {
4602
4789
  _gitlab;
@@ -4671,10 +4858,9 @@ var GitlabPlatformKit = class extends PlatformKit {
4671
4858
  await this.gitlab.MergeRequestNotes.create(this.platformConfig.gitlabProjectId, pullRequestNumber, body);
4672
4859
  }
4673
4860
  gitConfig() {
4674
- const url = `https://oauth2:${this.platformConfig.glToken}@gitlab.com/${this.platformConfig.repositoryOwner}/${this.platformConfig.repositoryName}.git`;
4675
- execSync5(`git remote set-url origin ${url}`, {
4676
- stdio: "inherit"
4677
- });
4861
+ const glToken = this.platformConfig.glToken;
4862
+ const url = `https://oauth2:${glToken}@gitlab.com/${this.platformConfig.repositoryOwner}/${this.platformConfig.repositoryName}.git`;
4863
+ super.gitConfig(glToken, url);
4678
4864
  }
4679
4865
  buildPullRequestUrl(pullRequestNumber) {
4680
4866
  return `https://gitlab.com/${this.platformConfig.repositoryOwner}/${this.platformConfig.repositoryName}/-/merge_requests/${pullRequestNumber}`;
@@ -4745,7 +4931,7 @@ var ci_default = new Command10().command("ci").description("Run Lingo.dev CI/CD
4745
4931
  // package.json
4746
4932
  var package_default = {
4747
4933
  name: "lingo.dev",
4748
- version: "0.80.0",
4934
+ version: "0.81.0",
4749
4935
  description: "Lingo.dev CLI",
4750
4936
  private: false,
4751
4937
  publishConfig: {
@@ -4901,7 +5087,9 @@ ${vice(
4901
5087
  })
4902
5088
  )}
4903
5089
 
4904
- Website: https://lingo.dev
5090
+ \u26A1\uFE0F AI-powered open-source CLI for web & mobile localization.
5091
+
5092
+ Star the the repo :) https://github.com/LingoDotDev/lingo.dev
4905
5093
  `
4906
5094
  ).version(`v${package_default.version}`, "-v, --version", "Show version").addCommand(init_default).interactive("-y, --no-interactive", "Disable interactive mode").addCommand(i18n_default).addCommand(auth_default).addCommand(show_default).addCommand(lockfile_default).addCommand(cleanup_default).addCommand(mcp_default).addCommand(ci_default).exitOverride((err) => {
4907
5095
  if (err.code === "commander.helpDisplayed" || err.code === "commander.version" || err.code === "commander.help") {