@lingo.dev/compiler 0.2.0 → 0.3.1

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.
@@ -106,11 +106,11 @@ var MetadataManager = class {
106
106
  }
107
107
  const release = await proper_lockfile.default.lock(this.filePath, {
108
108
  retries: {
109
- retries: 10,
109
+ retries: 20,
110
110
  minTimeout: 50,
111
- maxTimeout: 1e3
111
+ maxTimeout: 2e3
112
112
  },
113
- stale: 2e3
113
+ stale: 5e3
114
114
  });
115
115
  try {
116
116
  const currentMetadata = await this.loadMetadata();
@@ -1,9 +1,9 @@
1
1
  import { logger } from "../utils/logger.mjs";
2
2
  import { DEFAULT_TIMEOUTS, withTimeout } from "../utils/timeout.mjs";
3
3
  import { getLingoDir } from "../utils/path-helpers.mjs";
4
- import fsPromises from "fs/promises";
4
+ import fs from "fs/promises";
5
5
  import path from "path";
6
- import fs from "fs";
6
+ import fs$1 from "fs";
7
7
  import lockfile from "proper-lockfile";
8
8
 
9
9
  //#region src/metadata/manager.ts
@@ -22,7 +22,7 @@ function loadMetadata(path$1) {
22
22
  function cleanupExistingMetadata(metadataFilePath) {
23
23
  logger.debug(`Attempting to cleanup metadata file: ${metadataFilePath}`);
24
24
  try {
25
- fs.unlinkSync(metadataFilePath);
25
+ fs$1.unlinkSync(metadataFilePath);
26
26
  logger.info(`🧹 Cleaned up build metadata file: ${metadataFilePath}`);
27
27
  } catch (error) {
28
28
  if (error.code === "ENOENT") logger.debug(`Metadata file already deleted or doesn't exist: ${metadataFilePath}`);
@@ -50,7 +50,7 @@ var MetadataManager = class {
50
50
  */
51
51
  async loadMetadata() {
52
52
  try {
53
- const content = await withTimeout(fsPromises.readFile(this.filePath, "utf-8"), DEFAULT_TIMEOUTS.METADATA, "Load metadata");
53
+ const content = await withTimeout(fs.readFile(this.filePath, "utf-8"), DEFAULT_TIMEOUTS.METADATA, "Load metadata");
54
54
  return JSON.parse(content);
55
55
  } catch (error) {
56
56
  if (error.code === "ENOENT") return createEmptyMetadata();
@@ -62,7 +62,7 @@ var MetadataManager = class {
62
62
  * Times out after 15 seconds to prevent indefinite hangs
63
63
  */
64
64
  async saveMetadata(metadata) {
65
- await withTimeout(fsPromises.mkdir(path.dirname(this.filePath), { recursive: true }), DEFAULT_TIMEOUTS.FILE_IO, "Create metadata directory");
65
+ await withTimeout(fs.mkdir(path.dirname(this.filePath), { recursive: true }), DEFAULT_TIMEOUTS.FILE_IO, "Create metadata directory");
66
66
  metadata.stats = {
67
67
  totalEntries: Object.keys(metadata.entries).length,
68
68
  lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
@@ -71,17 +71,17 @@ var MetadataManager = class {
71
71
  const base = path.basename(this.filePath);
72
72
  const tmpPath = path.join(dir, `.${base}.tmp-${process.pid}-${Date.now()}`);
73
73
  const json = JSON.stringify(metadata, null, 2);
74
- await withTimeout(fsPromises.writeFile(tmpPath, json, "utf-8"), DEFAULT_TIMEOUTS.METADATA, "Save metadata (tmp write)");
74
+ await withTimeout(fs.writeFile(tmpPath, json, "utf-8"), DEFAULT_TIMEOUTS.METADATA, "Save metadata (tmp write)");
75
75
  try {
76
- await withTimeout(fsPromises.rename(tmpPath, this.filePath), DEFAULT_TIMEOUTS.METADATA, "Save metadata (atomic rename)");
76
+ await withTimeout(fs.rename(tmpPath, this.filePath), DEFAULT_TIMEOUTS.METADATA, "Save metadata (atomic rename)");
77
77
  } catch (error) {
78
78
  if (error && typeof error === "object" && "code" in error && error.code === "EPERM") {
79
- await withTimeout(fsPromises.writeFile(this.filePath, json, "utf-8"), DEFAULT_TIMEOUTS.METADATA, "Save metadata (EPERM fallback direct write)");
79
+ await withTimeout(fs.writeFile(this.filePath, json, "utf-8"), DEFAULT_TIMEOUTS.METADATA, "Save metadata (EPERM fallback direct write)");
80
80
  return;
81
81
  }
82
82
  throw error;
83
83
  } finally {
84
- await fsPromises.unlink(tmpPath).catch(() => {});
84
+ await fs.unlink(tmpPath).catch(() => {});
85
85
  }
86
86
  }
87
87
  /**
@@ -93,19 +93,19 @@ var MetadataManager = class {
93
93
  */
94
94
  async saveMetadataWithEntries(entries) {
95
95
  const lockDir = path.dirname(this.filePath);
96
- await fsPromises.mkdir(lockDir, { recursive: true });
96
+ await fs.mkdir(lockDir, { recursive: true });
97
97
  try {
98
- await fsPromises.access(this.filePath);
98
+ await fs.access(this.filePath);
99
99
  } catch {
100
- await fsPromises.writeFile(this.filePath, JSON.stringify(createEmptyMetadata(), null, 2), "utf-8");
100
+ await fs.writeFile(this.filePath, JSON.stringify(createEmptyMetadata(), null, 2), "utf-8");
101
101
  }
102
102
  const release = await lockfile.lock(this.filePath, {
103
103
  retries: {
104
- retries: 10,
104
+ retries: 20,
105
105
  minTimeout: 50,
106
- maxTimeout: 1e3
106
+ maxTimeout: 2e3
107
107
  },
108
- stale: 2e3
108
+ stale: 5e3
109
109
  });
110
110
  try {
111
111
  const currentMetadata = await this.loadMetadata();
@@ -1 +1 @@
1
- {"version":3,"file":"manager.mjs","names":["path","error: any","filePath: string"],"sources":["../../src/metadata/manager.ts"],"sourcesContent":["import fsPromises from \"fs/promises\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport lockfile from \"proper-lockfile\";\nimport type { MetadataSchema, PathConfig, TranslationEntry } from \"../types\";\nimport { DEFAULT_TIMEOUTS, withTimeout } from \"../utils/timeout\";\nimport { getLingoDir } from \"../utils/path-helpers\";\nimport { logger } from \"../utils/logger\";\n\nexport function createEmptyMetadata(): MetadataSchema {\n return {\n entries: {},\n stats: {\n totalEntries: 0,\n lastUpdated: new Date().toISOString(),\n },\n };\n}\n\nexport function loadMetadata(path: string) {\n return new MetadataManager(path).loadMetadata();\n}\n\nexport function cleanupExistingMetadata(metadataFilePath: string) {\n // General cleanup. Delete metadata and stop the server if any was started.\n logger.debug(`Attempting to cleanup metadata file: ${metadataFilePath}`);\n\n try {\n fs.unlinkSync(metadataFilePath);\n logger.info(`🧹 Cleaned up build metadata file: ${metadataFilePath}`);\n } catch (error: any) {\n // Ignore if file doesn't exist\n if (error.code === \"ENOENT\") {\n logger.debug(\n `Metadata file already deleted or doesn't exist: ${metadataFilePath}`,\n );\n } else {\n logger.warn(`Failed to cleanup metadata file: ${error.message}`);\n }\n }\n}\n\n/**\n * Get the absolute path to the metadata file\n *\n * @param config - Config with sourceRoot, lingoDir, and environment\n * @returns Absolute path to metadata file\n */\nexport function getMetadataPath(config: PathConfig): string {\n const filename =\n // Similar to next keeping dev build separate, let's keep the build metadata clean of any dev mode additions\n config.environment === \"development\"\n ? \"metadata-dev.json\"\n : \"metadata-build.json\";\n return path.join(getLingoDir(config), filename);\n}\n\nexport class MetadataManager {\n constructor(private readonly filePath: string) {}\n\n /**\n * Load metadata from disk\n * Creates empty metadata if file doesn't exist\n * Times out after 15 seconds to prevent indefinite hangs\n */\n async loadMetadata(): Promise<MetadataSchema> {\n try {\n const content = await withTimeout(\n fsPromises.readFile(this.filePath, \"utf-8\"),\n DEFAULT_TIMEOUTS.METADATA,\n \"Load metadata\",\n );\n return JSON.parse(content) as MetadataSchema;\n } catch (error: any) {\n if (error.code === \"ENOENT\") {\n // File doesn't exist, create new metadata\n return createEmptyMetadata();\n }\n throw error;\n }\n }\n\n /**\n * Save metadata to disk\n * Times out after 15 seconds to prevent indefinite hangs\n */\n private async saveMetadata(metadata: MetadataSchema): Promise<void> {\n await withTimeout(\n fsPromises.mkdir(path.dirname(this.filePath), { recursive: true }),\n DEFAULT_TIMEOUTS.FILE_IO,\n \"Create metadata directory\",\n );\n\n metadata.stats = {\n totalEntries: Object.keys(metadata.entries).length,\n lastUpdated: new Date().toISOString(),\n };\n\n // Per LLM writing to a file is not an atomic operation while rename is, so nobody should get partial content.\n // Sounds reasonable.\n const dir = path.dirname(this.filePath);\n const base = path.basename(this.filePath);\n\n // Keep temp file in the same directory to maximize chance that rename is atomic\n const tmpPath = path.join(dir, `.${base}.tmp-${process.pid}-${Date.now()}`);\n\n const json = JSON.stringify(metadata, null, 2);\n\n await withTimeout(\n fsPromises.writeFile(tmpPath, json, \"utf-8\"),\n DEFAULT_TIMEOUTS.METADATA,\n \"Save metadata (tmp write)\",\n );\n\n try {\n // TODO (AleksandrSl 14/12/2025): LLM says that we may want to remove older file first for windows, but it seems lo work fine as is.\n await withTimeout(\n fsPromises.rename(tmpPath, this.filePath),\n DEFAULT_TIMEOUTS.METADATA,\n \"Save metadata (atomic rename)\",\n );\n } catch (error) {\n // On Windows, rename() can fail with EPERM if something briefly holds the file.\n // As a fallback, try writing directly to the destination (not atomic).\n if (\n error &&\n typeof error === \"object\" &&\n \"code\" in error &&\n error.code === \"EPERM\"\n ) {\n await withTimeout(\n fsPromises.writeFile(this.filePath, json, \"utf-8\"),\n DEFAULT_TIMEOUTS.METADATA,\n \"Save metadata (EPERM fallback direct write)\",\n );\n return;\n }\n throw error;\n } finally {\n // Best-effort cleanup if rename failed for some reason\n await fsPromises.unlink(tmpPath).catch(() => {});\n }\n }\n\n /**\n * Thread-safe save operation that atomically updates metadata with new entries\n * Uses file locking to prevent concurrent write corruption\n *\n * @param entries - Translation entries to add/update\n * @returns The updated metadata schema\n */\n async saveMetadataWithEntries(\n entries: TranslationEntry[],\n ): Promise<MetadataSchema> {\n const lockDir = path.dirname(this.filePath);\n\n await fsPromises.mkdir(lockDir, { recursive: true });\n\n try {\n await fsPromises.access(this.filePath);\n } catch {\n await fsPromises.writeFile(\n this.filePath,\n JSON.stringify(createEmptyMetadata(), null, 2),\n \"utf-8\",\n );\n }\n\n const release = await lockfile.lock(this.filePath, {\n retries: {\n retries: 10,\n minTimeout: 50,\n maxTimeout: 1000,\n },\n stale: 2000,\n });\n\n try {\n // Re-load metadata inside lock to get latest state\n const currentMetadata = await this.loadMetadata();\n for (const entry of entries) {\n currentMetadata.entries[entry.hash] = entry;\n }\n await this.saveMetadata(currentMetadata);\n return currentMetadata;\n } finally {\n await release();\n }\n }\n}\n"],"mappings":";;;;;;;;;AASA,SAAgB,sBAAsC;AACpD,QAAO;EACL,SAAS,EAAE;EACX,OAAO;GACL,cAAc;GACd,8BAAa,IAAI,MAAM,EAAC,aAAa;GACtC;EACF;;AAGH,SAAgB,aAAa,QAAc;AACzC,QAAO,IAAI,gBAAgBA,OAAK,CAAC,cAAc;;AAGjD,SAAgB,wBAAwB,kBAA0B;AAEhE,QAAO,MAAM,wCAAwC,mBAAmB;AAExE,KAAI;AACF,KAAG,WAAW,iBAAiB;AAC/B,SAAO,KAAK,sCAAsC,mBAAmB;UAC9DC,OAAY;AAEnB,MAAI,MAAM,SAAS,SACjB,QAAO,MACL,mDAAmD,mBACpD;MAED,QAAO,KAAK,oCAAoC,MAAM,UAAU;;;;;;;;;AAWtE,SAAgB,gBAAgB,QAA4B;CAC1D,MAAM,WAEJ,OAAO,gBAAgB,gBACnB,sBACA;AACN,QAAO,KAAK,KAAK,YAAY,OAAO,EAAE,SAAS;;AAGjD,IAAa,kBAAb,MAA6B;CAC3B,YAAY,AAAiBC,UAAkB;EAAlB;;;;;;;CAO7B,MAAM,eAAwC;AAC5C,MAAI;GACF,MAAM,UAAU,MAAM,YACpB,WAAW,SAAS,KAAK,UAAU,QAAQ,EAC3C,iBAAiB,UACjB,gBACD;AACD,UAAO,KAAK,MAAM,QAAQ;WACnBD,OAAY;AACnB,OAAI,MAAM,SAAS,SAEjB,QAAO,qBAAqB;AAE9B,SAAM;;;;;;;CAQV,MAAc,aAAa,UAAyC;AAClE,QAAM,YACJ,WAAW,MAAM,KAAK,QAAQ,KAAK,SAAS,EAAE,EAAE,WAAW,MAAM,CAAC,EAClE,iBAAiB,SACjB,4BACD;AAED,WAAS,QAAQ;GACf,cAAc,OAAO,KAAK,SAAS,QAAQ,CAAC;GAC5C,8BAAa,IAAI,MAAM,EAAC,aAAa;GACtC;EAID,MAAM,MAAM,KAAK,QAAQ,KAAK,SAAS;EACvC,MAAM,OAAO,KAAK,SAAS,KAAK,SAAS;EAGzC,MAAM,UAAU,KAAK,KAAK,KAAK,IAAI,KAAK,OAAO,QAAQ,IAAI,GAAG,KAAK,KAAK,GAAG;EAE3E,MAAM,OAAO,KAAK,UAAU,UAAU,MAAM,EAAE;AAE9C,QAAM,YACJ,WAAW,UAAU,SAAS,MAAM,QAAQ,EAC5C,iBAAiB,UACjB,4BACD;AAED,MAAI;AAEF,SAAM,YACJ,WAAW,OAAO,SAAS,KAAK,SAAS,EACzC,iBAAiB,UACjB,gCACD;WACM,OAAO;AAGd,OACE,SACA,OAAO,UAAU,YACjB,UAAU,SACV,MAAM,SAAS,SACf;AACA,UAAM,YACJ,WAAW,UAAU,KAAK,UAAU,MAAM,QAAQ,EAClD,iBAAiB,UACjB,8CACD;AACD;;AAEF,SAAM;YACE;AAER,SAAM,WAAW,OAAO,QAAQ,CAAC,YAAY,GAAG;;;;;;;;;;CAWpD,MAAM,wBACJ,SACyB;EACzB,MAAM,UAAU,KAAK,QAAQ,KAAK,SAAS;AAE3C,QAAM,WAAW,MAAM,SAAS,EAAE,WAAW,MAAM,CAAC;AAEpD,MAAI;AACF,SAAM,WAAW,OAAO,KAAK,SAAS;UAChC;AACN,SAAM,WAAW,UACf,KAAK,UACL,KAAK,UAAU,qBAAqB,EAAE,MAAM,EAAE,EAC9C,QACD;;EAGH,MAAM,UAAU,MAAM,SAAS,KAAK,KAAK,UAAU;GACjD,SAAS;IACP,SAAS;IACT,YAAY;IACZ,YAAY;IACb;GACD,OAAO;GACR,CAAC;AAEF,MAAI;GAEF,MAAM,kBAAkB,MAAM,KAAK,cAAc;AACjD,QAAK,MAAM,SAAS,QAClB,iBAAgB,QAAQ,MAAM,QAAQ;AAExC,SAAM,KAAK,aAAa,gBAAgB;AACxC,UAAO;YACC;AACR,SAAM,SAAS"}
1
+ {"version":3,"file":"manager.mjs","names":["path","error: any","filePath: string","fsPromises"],"sources":["../../src/metadata/manager.ts"],"sourcesContent":["import fsPromises from \"fs/promises\";\nimport fs from \"fs\";\nimport path from \"path\";\nimport lockfile from \"proper-lockfile\";\nimport type { MetadataSchema, PathConfig, TranslationEntry } from \"../types\";\nimport { DEFAULT_TIMEOUTS, withTimeout } from \"../utils/timeout\";\nimport { getLingoDir } from \"../utils/path-helpers\";\nimport { logger } from \"../utils/logger\";\n\nexport function createEmptyMetadata(): MetadataSchema {\n return {\n entries: {},\n stats: {\n totalEntries: 0,\n lastUpdated: new Date().toISOString(),\n },\n };\n}\n\nexport function loadMetadata(path: string) {\n return new MetadataManager(path).loadMetadata();\n}\n\nexport function cleanupExistingMetadata(metadataFilePath: string) {\n // General cleanup. Delete metadata and stop the server if any was started.\n logger.debug(`Attempting to cleanup metadata file: ${metadataFilePath}`);\n\n try {\n fs.unlinkSync(metadataFilePath);\n logger.info(`🧹 Cleaned up build metadata file: ${metadataFilePath}`);\n } catch (error: any) {\n // Ignore if file doesn't exist\n if (error.code === \"ENOENT\") {\n logger.debug(\n `Metadata file already deleted or doesn't exist: ${metadataFilePath}`,\n );\n } else {\n logger.warn(`Failed to cleanup metadata file: ${error.message}`);\n }\n }\n}\n\n/**\n * Get the absolute path to the metadata file\n *\n * @param config - Config with sourceRoot, lingoDir, and environment\n * @returns Absolute path to metadata file\n */\nexport function getMetadataPath(config: PathConfig): string {\n const filename =\n // Similar to next keeping dev build separate, let's keep the build metadata clean of any dev mode additions\n config.environment === \"development\"\n ? \"metadata-dev.json\"\n : \"metadata-build.json\";\n return path.join(getLingoDir(config), filename);\n}\n\nexport class MetadataManager {\n constructor(private readonly filePath: string) {}\n\n /**\n * Load metadata from disk\n * Creates empty metadata if file doesn't exist\n * Times out after 15 seconds to prevent indefinite hangs\n */\n async loadMetadata(): Promise<MetadataSchema> {\n try {\n const content = await withTimeout(\n fsPromises.readFile(this.filePath, \"utf-8\"),\n DEFAULT_TIMEOUTS.METADATA,\n \"Load metadata\",\n );\n return JSON.parse(content) as MetadataSchema;\n } catch (error: any) {\n if (error.code === \"ENOENT\") {\n // File doesn't exist, create new metadata\n return createEmptyMetadata();\n }\n throw error;\n }\n }\n\n /**\n * Save metadata to disk\n * Times out after 15 seconds to prevent indefinite hangs\n */\n private async saveMetadata(metadata: MetadataSchema): Promise<void> {\n await withTimeout(\n fsPromises.mkdir(path.dirname(this.filePath), { recursive: true }),\n DEFAULT_TIMEOUTS.FILE_IO,\n \"Create metadata directory\",\n );\n\n metadata.stats = {\n totalEntries: Object.keys(metadata.entries).length,\n lastUpdated: new Date().toISOString(),\n };\n\n // Per LLM writing to a file is not an atomic operation while rename is, so nobody should get partial content.\n // Sounds reasonable.\n const dir = path.dirname(this.filePath);\n const base = path.basename(this.filePath);\n\n // Keep temp file in the same directory to maximize chance that rename is atomic\n const tmpPath = path.join(dir, `.${base}.tmp-${process.pid}-${Date.now()}`);\n\n const json = JSON.stringify(metadata, null, 2);\n\n await withTimeout(\n fsPromises.writeFile(tmpPath, json, \"utf-8\"),\n DEFAULT_TIMEOUTS.METADATA,\n \"Save metadata (tmp write)\",\n );\n\n try {\n // TODO (AleksandrSl 14/12/2025): LLM says that we may want to remove older file first for windows, but it seems lo work fine as is.\n await withTimeout(\n fsPromises.rename(tmpPath, this.filePath),\n DEFAULT_TIMEOUTS.METADATA,\n \"Save metadata (atomic rename)\",\n );\n } catch (error) {\n // On Windows, rename() can fail with EPERM if something briefly holds the file.\n // As a fallback, try writing directly to the destination (not atomic).\n if (\n error &&\n typeof error === \"object\" &&\n \"code\" in error &&\n error.code === \"EPERM\"\n ) {\n await withTimeout(\n fsPromises.writeFile(this.filePath, json, \"utf-8\"),\n DEFAULT_TIMEOUTS.METADATA,\n \"Save metadata (EPERM fallback direct write)\",\n );\n return;\n }\n throw error;\n } finally {\n // Best-effort cleanup if rename failed for some reason\n await fsPromises.unlink(tmpPath).catch(() => {});\n }\n }\n\n /**\n * Thread-safe save operation that atomically updates metadata with new entries\n * Uses file locking to prevent concurrent write corruption\n *\n * @param entries - Translation entries to add/update\n * @returns The updated metadata schema\n */\n async saveMetadataWithEntries(\n entries: TranslationEntry[],\n ): Promise<MetadataSchema> {\n const lockDir = path.dirname(this.filePath);\n\n await fsPromises.mkdir(lockDir, { recursive: true });\n\n try {\n await fsPromises.access(this.filePath);\n } catch {\n await fsPromises.writeFile(\n this.filePath,\n JSON.stringify(createEmptyMetadata(), null, 2),\n \"utf-8\",\n );\n }\n\n const release = await lockfile.lock(this.filePath, {\n retries: {\n retries: 20,\n minTimeout: 50,\n maxTimeout: 2000,\n },\n stale: 5000,\n });\n\n try {\n // Re-load metadata inside lock to get latest state\n const currentMetadata = await this.loadMetadata();\n for (const entry of entries) {\n currentMetadata.entries[entry.hash] = entry;\n }\n await this.saveMetadata(currentMetadata);\n return currentMetadata;\n } finally {\n await release();\n }\n }\n}\n"],"mappings":";;;;;;;;;AASA,SAAgB,sBAAsC;AACpD,QAAO;EACL,SAAS,EAAE;EACX,OAAO;GACL,cAAc;GACd,8BAAa,IAAI,MAAM,EAAC,aAAa;GACtC;EACF;;AAGH,SAAgB,aAAa,QAAc;AACzC,QAAO,IAAI,gBAAgBA,OAAK,CAAC,cAAc;;AAGjD,SAAgB,wBAAwB,kBAA0B;AAEhE,QAAO,MAAM,wCAAwC,mBAAmB;AAExE,KAAI;AACF,OAAG,WAAW,iBAAiB;AAC/B,SAAO,KAAK,sCAAsC,mBAAmB;UAC9DC,OAAY;AAEnB,MAAI,MAAM,SAAS,SACjB,QAAO,MACL,mDAAmD,mBACpD;MAED,QAAO,KAAK,oCAAoC,MAAM,UAAU;;;;;;;;;AAWtE,SAAgB,gBAAgB,QAA4B;CAC1D,MAAM,WAEJ,OAAO,gBAAgB,gBACnB,sBACA;AACN,QAAO,KAAK,KAAK,YAAY,OAAO,EAAE,SAAS;;AAGjD,IAAa,kBAAb,MAA6B;CAC3B,YAAY,AAAiBC,UAAkB;EAAlB;;;;;;;CAO7B,MAAM,eAAwC;AAC5C,MAAI;GACF,MAAM,UAAU,MAAM,YACpBC,GAAW,SAAS,KAAK,UAAU,QAAQ,EAC3C,iBAAiB,UACjB,gBACD;AACD,UAAO,KAAK,MAAM,QAAQ;WACnBF,OAAY;AACnB,OAAI,MAAM,SAAS,SAEjB,QAAO,qBAAqB;AAE9B,SAAM;;;;;;;CAQV,MAAc,aAAa,UAAyC;AAClE,QAAM,YACJE,GAAW,MAAM,KAAK,QAAQ,KAAK,SAAS,EAAE,EAAE,WAAW,MAAM,CAAC,EAClE,iBAAiB,SACjB,4BACD;AAED,WAAS,QAAQ;GACf,cAAc,OAAO,KAAK,SAAS,QAAQ,CAAC;GAC5C,8BAAa,IAAI,MAAM,EAAC,aAAa;GACtC;EAID,MAAM,MAAM,KAAK,QAAQ,KAAK,SAAS;EACvC,MAAM,OAAO,KAAK,SAAS,KAAK,SAAS;EAGzC,MAAM,UAAU,KAAK,KAAK,KAAK,IAAI,KAAK,OAAO,QAAQ,IAAI,GAAG,KAAK,KAAK,GAAG;EAE3E,MAAM,OAAO,KAAK,UAAU,UAAU,MAAM,EAAE;AAE9C,QAAM,YACJA,GAAW,UAAU,SAAS,MAAM,QAAQ,EAC5C,iBAAiB,UACjB,4BACD;AAED,MAAI;AAEF,SAAM,YACJA,GAAW,OAAO,SAAS,KAAK,SAAS,EACzC,iBAAiB,UACjB,gCACD;WACM,OAAO;AAGd,OACE,SACA,OAAO,UAAU,YACjB,UAAU,SACV,MAAM,SAAS,SACf;AACA,UAAM,YACJA,GAAW,UAAU,KAAK,UAAU,MAAM,QAAQ,EAClD,iBAAiB,UACjB,8CACD;AACD;;AAEF,SAAM;YACE;AAER,SAAMA,GAAW,OAAO,QAAQ,CAAC,YAAY,GAAG;;;;;;;;;;CAWpD,MAAM,wBACJ,SACyB;EACzB,MAAM,UAAU,KAAK,QAAQ,KAAK,SAAS;AAE3C,QAAMA,GAAW,MAAM,SAAS,EAAE,WAAW,MAAM,CAAC;AAEpD,MAAI;AACF,SAAMA,GAAW,OAAO,KAAK,SAAS;UAChC;AACN,SAAMA,GAAW,UACf,KAAK,UACL,KAAK,UAAU,qBAAqB,EAAE,MAAM,EAAE,EAC9C,QACD;;EAGH,MAAM,UAAU,MAAM,SAAS,KAAK,KAAK,UAAU;GACjD,SAAS;IACP,SAAS;IACT,YAAY;IACZ,YAAY;IACb;GACD,OAAO;GACR,CAAC;AAEF,MAAI;GAEF,MAAM,kBAAkB,MAAM,KAAK,cAAc;AACjD,QAAK,MAAM,SAAS,QAClB,iBAAgB,QAAQ,MAAM,QAAQ;AAExC,SAAM,KAAK,aAAa,gBAAgB;AACxC,UAAO;YACC;AACR,SAAM,SAAS"}
@@ -59,7 +59,7 @@ async function processBuildTranslations(options) {
59
59
  },
60
60
  config
61
61
  });
62
- const needsSourceLocale = config.pluralization?.enabled !== false;
62
+ const needsSourceLocale = config.pluralization?.enabled === true;
63
63
  const allLocales = needsSourceLocale ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
64
64
  require_logger.logger.info(`Processing translations for ${allLocales.length} locale(s)${needsSourceLocale ? " (including source locale for pluralization)" : ""}...`);
65
65
  const stats = {};
@@ -110,7 +110,7 @@ async function validateCache(config, metadata, cache) {
110
110
  const allHashes = Object.keys(metadata.entries);
111
111
  const missingLocales = [];
112
112
  const incompleteLocales = [];
113
- const allLocales = config.pluralization?.enabled !== false ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
113
+ const allLocales = config.pluralization?.enabled === true ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
114
114
  for (const locale of allLocales) try {
115
115
  const entries = await cache.get(locale);
116
116
  if (Object.keys(entries).length === 0) {
@@ -140,7 +140,7 @@ async function validateCache(config, metadata, cache) {
140
140
  function buildCacheStats(config, metadata) {
141
141
  const totalEntries = Object.keys(metadata.entries).length;
142
142
  const stats = {};
143
- const allLocales = config.pluralization?.enabled !== false ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
143
+ const allLocales = config.pluralization?.enabled === true ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
144
144
  for (const locale of allLocales) stats[locale] = {
145
145
  total: totalEntries,
146
146
  translated: totalEntries,
@@ -153,7 +153,7 @@ async function copyStaticFiles(config, publicOutputPath, metadata, cache) {
153
153
  await fs_promises.default.mkdir(publicOutputPath, { recursive: true });
154
154
  const usedHashes = new Set(Object.keys(metadata.entries));
155
155
  require_logger.logger.info(`📊 Filtering translations to ${usedHashes.size} used hash(es)`);
156
- const allLocales = config.pluralization?.enabled !== false ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
156
+ const allLocales = config.pluralization?.enabled === true ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
157
157
  for (const locale of allLocales) {
158
158
  const publicFilePath = path.default.join(publicOutputPath, `${locale}.json`);
159
159
  try {
@@ -4,7 +4,7 @@ import { createCache } from "../translators/cache-factory.mjs";
4
4
  import { TranslationService } from "../translators/translation-service.mjs";
5
5
  import { loadMetadata } from "../metadata/manager.mjs";
6
6
  import { startTranslationServer } from "../translation-server/translation-server.mjs";
7
- import fsPromises from "fs/promises";
7
+ import fs from "fs/promises";
8
8
  import path from "path";
9
9
 
10
10
  //#region src/plugin/build-translator.ts
@@ -56,7 +56,7 @@ async function processBuildTranslations(options) {
56
56
  },
57
57
  config
58
58
  });
59
- const needsSourceLocale = config.pluralization?.enabled !== false;
59
+ const needsSourceLocale = config.pluralization?.enabled === true;
60
60
  const allLocales = needsSourceLocale ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
61
61
  logger.info(`Processing translations for ${allLocales.length} locale(s)${needsSourceLocale ? " (including source locale for pluralization)" : ""}...`);
62
62
  const stats = {};
@@ -107,7 +107,7 @@ async function validateCache(config, metadata, cache) {
107
107
  const allHashes = Object.keys(metadata.entries);
108
108
  const missingLocales = [];
109
109
  const incompleteLocales = [];
110
- const allLocales = config.pluralization?.enabled !== false ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
110
+ const allLocales = config.pluralization?.enabled === true ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
111
111
  for (const locale of allLocales) try {
112
112
  const entries = await cache.get(locale);
113
113
  if (Object.keys(entries).length === 0) {
@@ -137,7 +137,7 @@ async function validateCache(config, metadata, cache) {
137
137
  function buildCacheStats(config, metadata) {
138
138
  const totalEntries = Object.keys(metadata.entries).length;
139
139
  const stats = {};
140
- const allLocales = config.pluralization?.enabled !== false ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
140
+ const allLocales = config.pluralization?.enabled === true ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
141
141
  for (const locale of allLocales) stats[locale] = {
142
142
  total: totalEntries,
143
143
  translated: totalEntries,
@@ -147,16 +147,16 @@ function buildCacheStats(config, metadata) {
147
147
  }
148
148
  async function copyStaticFiles(config, publicOutputPath, metadata, cache) {
149
149
  logger.info(`📦 Generating static translation files in ${publicOutputPath}`);
150
- await fsPromises.mkdir(publicOutputPath, { recursive: true });
150
+ await fs.mkdir(publicOutputPath, { recursive: true });
151
151
  const usedHashes = new Set(Object.keys(metadata.entries));
152
152
  logger.info(`📊 Filtering translations to ${usedHashes.size} used hash(es)`);
153
- const allLocales = config.pluralization?.enabled !== false ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
153
+ const allLocales = config.pluralization?.enabled === true ? [config.sourceLocale, ...config.targetLocales] : config.targetLocales;
154
154
  for (const locale of allLocales) {
155
155
  const publicFilePath = path.join(publicOutputPath, `${locale}.json`);
156
156
  try {
157
157
  const entries = await cache.get(locale, Array.from(usedHashes));
158
158
  const outputData = dictionaryFrom(locale, entries);
159
- await fsPromises.writeFile(publicFilePath, JSON.stringify(outputData, null, 2), "utf-8");
159
+ await fs.writeFile(publicFilePath, JSON.stringify(outputData, null, 2), "utf-8");
160
160
  logger.info(`✓ Generated ${locale}.json (${Object.keys(entries).length} translations)`);
161
161
  } catch (error) {
162
162
  logger.error(`❌ Failed to generate ${locale}.json:`, error);
@@ -1 +1 @@
1
- {"version":3,"file":"build-translator.mjs","names":["translationServer: TranslationServer | undefined","stats: BuildTranslationResult[\"stats\"]","errors: Array<{ locale: LocaleCode; error: string }>","missingLocales: string[]","incompleteLocales: Array<{\n locale: LocaleCode;\n missing: number;\n total: number;\n }>","fs"],"sources":["../../src/plugin/build-translator.ts"],"sourcesContent":["/**\n * Build-time translation processor\n *\n * Handles translation generation and validation at build time\n * Supports two modes:\n * - \"translate\": Generate all translations, fail if translation fails\n * - \"cache-only\": Validate cache completeness, fail if incomplete\n */\n// TODO (AleksandrSl 08/12/2025): Add ICU validation for messages? The problem is that we don't know which will be rendered as a simple text\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport type { LingoConfig, MetadataSchema } from \"../types\";\nimport { logger } from \"../utils/logger\";\nimport { startTranslationServer, type TranslationServer, } from \"../translation-server\";\nimport { loadMetadata } from \"../metadata/manager\";\nimport { createCache, type TranslationCache, TranslationService, } from \"../translators\";\nimport { dictionaryFrom } from \"../translators/api\";\nimport type { LocaleCode } from \"lingo.dev/spec\";\n\nexport interface BuildTranslationOptions {\n config: LingoConfig;\n publicOutputPath: string;\n metadataFilePath: string;\n}\n\nexport interface BuildTranslationResult {\n /**\n * Whether the build succeeded\n */\n success: boolean;\n\n /**\n * Error message if build failed\n */\n error?: string;\n\n /**\n * Translation statistics per locale\n */\n stats: Record<\n string,\n {\n total: number;\n translated: number;\n failed: number;\n }\n >;\n}\n\n/**\n * Process translations at build time\n *\n * @throws Error if validation or translation fails (causes build to fail)\n */\nexport async function processBuildTranslations(\n options: BuildTranslationOptions,\n): Promise<BuildTranslationResult> {\n const { config, publicOutputPath, metadataFilePath } = options;\n\n // Determine build mode (env var > options > config)\n const buildMode =\n (process.env.LINGO_BUILD_MODE as \"translate\" | \"cache-only\") ||\n config.buildMode;\n\n logger.info(`🌍 Build mode: ${buildMode}`);\n\n const metadata = await loadMetadata(metadataFilePath);\n\n if (!metadata || Object.keys(metadata.entries).length === 0) {\n logger.info(\"No translations to process (metadata is empty)\");\n return {\n success: true,\n stats: {},\n };\n }\n\n const totalEntries = Object.keys(metadata.entries).length;\n logger.info(`📊 Found ${totalEntries} translatable entries`);\n\n const cache = createCache(config);\n\n // Handle cache-only mode\n if (buildMode === \"cache-only\") {\n logger.info(\"🔍 Validating translation cache...\");\n await validateCache(config, metadata, cache);\n logger.info(\"✅ Cache validation passed\");\n\n if (publicOutputPath) {\n await copyStaticFiles(config, publicOutputPath, metadata, cache);\n }\n\n return {\n success: true,\n stats: buildCacheStats(config, metadata),\n };\n }\n\n // Handle translate mode\n logger.info(\"🔄 Generating translations...\");\n let translationServer: TranslationServer | undefined;\n\n try {\n translationServer = await startTranslationServer({\n translationService: new TranslationService(config, logger),\n onError: (err) => {\n logger.error(\"Translation server error:\", err);\n },\n config,\n });\n\n // When pluralization is enabled, we need to generate the source locale file too\n // because pluralization modifies the sourceText\n const needsSourceLocale = config.pluralization?.enabled !== false;\n const allLocales = needsSourceLocale\n ? [config.sourceLocale, ...config.targetLocales]\n : config.targetLocales;\n\n logger.info(\n `Processing translations for ${allLocales.length} locale(s)${needsSourceLocale ? \" (including source locale for pluralization)\" : \"\"}...`,\n );\n\n const stats: BuildTranslationResult[\"stats\"] = {};\n const errors: Array<{ locale: LocaleCode; error: string }> = [];\n\n // Translate all locales in parallel\n const localePromises = allLocales.map(async (locale) => {\n logger.info(`Translating to ${locale}...`);\n\n const result = await translationServer!.translateAll(locale);\n\n stats[locale] = {\n total: totalEntries,\n translated: Object.keys(result.translations).length,\n failed: result.errors.length,\n };\n\n if (result.errors.length > 0) {\n logger.warn(\n `⚠️ ${result.errors.length} translation error(s) for ${locale}`,\n );\n errors.push({\n locale,\n error: `${result.errors.length} translation(s) failed`,\n });\n } else {\n logger.info(`✅ ${locale} completed successfully`);\n }\n });\n\n await Promise.all(localePromises);\n\n // Fail build if any translations failed in translate mode\n if (errors.length > 0) {\n const errorMsg = formatTranslationErrors(errors);\n logger.error(errorMsg);\n process.exit(1);\n }\n\n // Copy cache to public directory if requested\n if (publicOutputPath) {\n await copyStaticFiles(config, publicOutputPath, metadata, cache);\n }\n\n logger.info(\"✅ Translation generation completed successfully\");\n\n return {\n success: true,\n stats,\n };\n } catch (error) {\n logger.error(\n \"❌ Translation generation failed:\\n\",\n error instanceof Error ? error.message : error,\n );\n process.exit(1);\n } finally {\n if (translationServer) {\n await translationServer.stop();\n logger.info(\"✅ Translation server stopped\");\n }\n }\n}\n\n/**\n * Validate that all required translations exist in cache\n * @throws Error if cache is incomplete or missing\n */\nasync function validateCache(\n config: LingoConfig,\n metadata: MetadataSchema,\n cache: TranslationCache,\n): Promise<void> {\n const allHashes = Object.keys(metadata.entries);\n const missingLocales: string[] = [];\n const incompleteLocales: Array<{\n locale: LocaleCode;\n missing: number;\n total: number;\n }> = [];\n\n // Include source locale if pluralization is enabled\n const needsSourceLocale = config.pluralization?.enabled !== false;\n const allLocales = needsSourceLocale\n ? [config.sourceLocale, ...config.targetLocales]\n : config.targetLocales;\n\n for (const locale of allLocales) {\n try {\n const entries = await cache.get(locale);\n\n if (Object.keys(entries).length === 0) {\n missingLocales.push(locale);\n logger.debug(`Cache file not found or empty for ${locale}`);\n continue;\n }\n\n const missingHashes = allHashes.filter((hash) => !entries[hash]);\n\n if (missingHashes.length > 0) {\n incompleteLocales.push({\n locale,\n missing: missingHashes.length,\n total: allHashes.length,\n });\n\n // Log first few missing hashes for debugging\n logger.debug(\n `Missing hashes in ${locale}: ${missingHashes.slice(0, 5).join(\", \")}${\n missingHashes.length > 5 ? \"...\" : \"\"\n }`,\n );\n }\n } catch (error) {\n missingLocales.push(locale);\n logger.debug(`Failed to read cache for ${locale}:`, error);\n }\n }\n\n if (missingLocales.length > 0 || incompleteLocales.length > 0) {\n const errorMsg = formatCacheValidationError(\n missingLocales,\n incompleteLocales,\n );\n logger.error(errorMsg);\n process.exit(1);\n }\n}\n\nfunction buildCacheStats(\n config: LingoConfig,\n metadata: MetadataSchema,\n): BuildTranslationResult[\"stats\"] {\n const totalEntries = Object.keys(metadata.entries).length;\n const stats: BuildTranslationResult[\"stats\"] = {};\n\n // Include source locale if pluralization is enabled\n const needsSourceLocale = config.pluralization?.enabled !== false;\n const allLocales = needsSourceLocale\n ? [config.sourceLocale, ...config.targetLocales]\n : config.targetLocales;\n\n for (const locale of allLocales) {\n stats[locale] = {\n total: totalEntries,\n translated: totalEntries, // Assumed complete if validation passed\n failed: 0,\n };\n }\n\n return stats;\n}\n\nasync function copyStaticFiles(\n config: LingoConfig,\n publicOutputPath: string,\n metadata: MetadataSchema,\n cache: TranslationCache,\n): Promise<void> {\n logger.info(`📦 Generating static translation files in ${publicOutputPath}`);\n\n await fs.mkdir(publicOutputPath, { recursive: true });\n\n const usedHashes = new Set(Object.keys(metadata.entries));\n logger.info(`📊 Filtering translations to ${usedHashes.size} used hash(es)`);\n\n // Include source locale if pluralization is enabled\n const needsSourceLocale = config.pluralization?.enabled !== false;\n const allLocales = needsSourceLocale\n ? [config.sourceLocale, ...config.targetLocales]\n : config.targetLocales;\n\n for (const locale of allLocales) {\n const publicFilePath = path.join(publicOutputPath, `${locale}.json`);\n\n try {\n const entries = await cache.get(locale, Array.from(usedHashes));\n const outputData = dictionaryFrom(locale, entries);\n\n await fs.writeFile(\n publicFilePath,\n JSON.stringify(outputData, null, 2),\n \"utf-8\",\n );\n\n logger.info(\n `✓ Generated ${locale}.json (${Object.keys(entries).length} translations)`,\n );\n } catch (error) {\n logger.error(`❌ Failed to generate ${locale}.json:`, error);\n process.exit(1);\n }\n }\n}\n\nfunction formatCacheValidationError(\n missingLocales: string[],\n incompleteLocales: Array<{\n locale: LocaleCode;\n missing: number;\n total: number;\n }>,\n): string {\n let msg = \"❌ Cache validation failed in cache-only mode:\\n\\n\";\n\n if (missingLocales.length > 0) {\n msg += ` 📁 Missing cache files:\\n`;\n msg += missingLocales.map((locale) => ` - ${locale}.json`).join(\"\\n\");\n msg += \"\\n\\n\";\n }\n\n if (incompleteLocales.length > 0) {\n msg += ` 📊 Incomplete cache:\\n`;\n msg += incompleteLocales\n .map(\n (item) =>\n ` - ${item.locale}: ${item.missing}/${item.total} translations missing`,\n )\n .join(\"\\n\");\n msg += \"\\n\\n\";\n }\n\n msg += ` 💡 To fix:\\n`;\n msg += ` 1. Set LINGO_BUILD_MODE=translate to generate translations\\n`;\n msg += ` 2. Commit the generated .lingo/cache/*.json files\\n`;\n msg += ` 3. Ensure translation API keys are available if generating translations`;\n\n return msg;\n}\n\nfunction formatTranslationErrors(\n errors: Array<{ locale: LocaleCode; error: string }>,\n): string {\n let msg = \"❌ Translation generation failed:\\n\\n\";\n\n msg += errors.map((err) => ` - ${err.locale}: ${err.error}`).join(\"\\n\");\n\n msg += \"\\n\\n\";\n msg += ` 💡 Translation errors must be resolved in \"translate\" mode.\\n`;\n msg += ` Check translation server logs for details.`;\n\n return msg;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAsDA,eAAsB,yBACpB,SACiC;CACjC,MAAM,EAAE,QAAQ,kBAAkB,qBAAqB;CAGvD,MAAM,YACH,QAAQ,IAAI,oBACb,OAAO;AAET,QAAO,KAAK,kBAAkB,YAAY;CAE1C,MAAM,WAAW,MAAM,aAAa,iBAAiB;AAErD,KAAI,CAAC,YAAY,OAAO,KAAK,SAAS,QAAQ,CAAC,WAAW,GAAG;AAC3D,SAAO,KAAK,iDAAiD;AAC7D,SAAO;GACL,SAAS;GACT,OAAO,EAAE;GACV;;CAGH,MAAM,eAAe,OAAO,KAAK,SAAS,QAAQ,CAAC;AACnD,QAAO,KAAK,YAAY,aAAa,uBAAuB;CAE5D,MAAM,QAAQ,YAAY,OAAO;AAGjC,KAAI,cAAc,cAAc;AAC9B,SAAO,KAAK,qCAAqC;AACjD,QAAM,cAAc,QAAQ,UAAU,MAAM;AAC5C,SAAO,KAAK,4BAA4B;AAExC,MAAI,iBACF,OAAM,gBAAgB,QAAQ,kBAAkB,UAAU,MAAM;AAGlE,SAAO;GACL,SAAS;GACT,OAAO,gBAAgB,QAAQ,SAAS;GACzC;;AAIH,QAAO,KAAK,gCAAgC;CAC5C,IAAIA;AAEJ,KAAI;AACF,sBAAoB,MAAM,uBAAuB;GAC/C,oBAAoB,IAAI,mBAAmB,QAAQ,OAAO;GAC1D,UAAU,QAAQ;AAChB,WAAO,MAAM,6BAA6B,IAAI;;GAEhD;GACD,CAAC;EAIF,MAAM,oBAAoB,OAAO,eAAe,YAAY;EAC5D,MAAM,aAAa,oBACf,CAAC,OAAO,cAAc,GAAG,OAAO,cAAc,GAC9C,OAAO;AAEX,SAAO,KACL,+BAA+B,WAAW,OAAO,YAAY,oBAAoB,iDAAiD,GAAG,KACtI;EAED,MAAMC,QAAyC,EAAE;EACjD,MAAMC,SAAuD,EAAE;EAG/D,MAAM,iBAAiB,WAAW,IAAI,OAAO,WAAW;AACtD,UAAO,KAAK,kBAAkB,OAAO,KAAK;GAE1C,MAAM,SAAS,MAAM,kBAAmB,aAAa,OAAO;AAE5D,SAAM,UAAU;IACd,OAAO;IACP,YAAY,OAAO,KAAK,OAAO,aAAa,CAAC;IAC7C,QAAQ,OAAO,OAAO;IACvB;AAED,OAAI,OAAO,OAAO,SAAS,GAAG;AAC5B,WAAO,KACL,OAAO,OAAO,OAAO,OAAO,4BAA4B,SACzD;AACD,WAAO,KAAK;KACV;KACA,OAAO,GAAG,OAAO,OAAO,OAAO;KAChC,CAAC;SAEF,QAAO,KAAK,KAAK,OAAO,yBAAyB;IAEnD;AAEF,QAAM,QAAQ,IAAI,eAAe;AAGjC,MAAI,OAAO,SAAS,GAAG;GACrB,MAAM,WAAW,wBAAwB,OAAO;AAChD,UAAO,MAAM,SAAS;AACtB,WAAQ,KAAK,EAAE;;AAIjB,MAAI,iBACF,OAAM,gBAAgB,QAAQ,kBAAkB,UAAU,MAAM;AAGlE,SAAO,KAAK,kDAAkD;AAE9D,SAAO;GACL,SAAS;GACT;GACD;UACM,OAAO;AACd,SAAO,MACL,sCACA,iBAAiB,QAAQ,MAAM,UAAU,MAC1C;AACD,UAAQ,KAAK,EAAE;WACP;AACR,MAAI,mBAAmB;AACrB,SAAM,kBAAkB,MAAM;AAC9B,UAAO,KAAK,+BAA+B;;;;;;;;AASjD,eAAe,cACb,QACA,UACA,OACe;CACf,MAAM,YAAY,OAAO,KAAK,SAAS,QAAQ;CAC/C,MAAMC,iBAA2B,EAAE;CACnC,MAAMC,oBAID,EAAE;CAIP,MAAM,aADoB,OAAO,eAAe,YAAY,QAExD,CAAC,OAAO,cAAc,GAAG,OAAO,cAAc,GAC9C,OAAO;AAEX,MAAK,MAAM,UAAU,WACnB,KAAI;EACF,MAAM,UAAU,MAAM,MAAM,IAAI,OAAO;AAEvC,MAAI,OAAO,KAAK,QAAQ,CAAC,WAAW,GAAG;AACrC,kBAAe,KAAK,OAAO;AAC3B,UAAO,MAAM,qCAAqC,SAAS;AAC3D;;EAGF,MAAM,gBAAgB,UAAU,QAAQ,SAAS,CAAC,QAAQ,MAAM;AAEhE,MAAI,cAAc,SAAS,GAAG;AAC5B,qBAAkB,KAAK;IACrB;IACA,SAAS,cAAc;IACvB,OAAO,UAAU;IAClB,CAAC;AAGF,UAAO,MACL,qBAAqB,OAAO,IAAI,cAAc,MAAM,GAAG,EAAE,CAAC,KAAK,KAAK,GAClE,cAAc,SAAS,IAAI,QAAQ,KAEtC;;UAEI,OAAO;AACd,iBAAe,KAAK,OAAO;AAC3B,SAAO,MAAM,4BAA4B,OAAO,IAAI,MAAM;;AAI9D,KAAI,eAAe,SAAS,KAAK,kBAAkB,SAAS,GAAG;EAC7D,MAAM,WAAW,2BACf,gBACA,kBACD;AACD,SAAO,MAAM,SAAS;AACtB,UAAQ,KAAK,EAAE;;;AAInB,SAAS,gBACP,QACA,UACiC;CACjC,MAAM,eAAe,OAAO,KAAK,SAAS,QAAQ,CAAC;CACnD,MAAMH,QAAyC,EAAE;CAIjD,MAAM,aADoB,OAAO,eAAe,YAAY,QAExD,CAAC,OAAO,cAAc,GAAG,OAAO,cAAc,GAC9C,OAAO;AAEX,MAAK,MAAM,UAAU,WACnB,OAAM,UAAU;EACd,OAAO;EACP,YAAY;EACZ,QAAQ;EACT;AAGH,QAAO;;AAGT,eAAe,gBACb,QACA,kBACA,UACA,OACe;AACf,QAAO,KAAK,6CAA6C,mBAAmB;AAE5E,OAAMI,WAAG,MAAM,kBAAkB,EAAE,WAAW,MAAM,CAAC;CAErD,MAAM,aAAa,IAAI,IAAI,OAAO,KAAK,SAAS,QAAQ,CAAC;AACzD,QAAO,KAAK,gCAAgC,WAAW,KAAK,gBAAgB;CAI5E,MAAM,aADoB,OAAO,eAAe,YAAY,QAExD,CAAC,OAAO,cAAc,GAAG,OAAO,cAAc,GAC9C,OAAO;AAEX,MAAK,MAAM,UAAU,YAAY;EAC/B,MAAM,iBAAiB,KAAK,KAAK,kBAAkB,GAAG,OAAO,OAAO;AAEpE,MAAI;GACF,MAAM,UAAU,MAAM,MAAM,IAAI,QAAQ,MAAM,KAAK,WAAW,CAAC;GAC/D,MAAM,aAAa,eAAe,QAAQ,QAAQ;AAElD,SAAMA,WAAG,UACP,gBACA,KAAK,UAAU,YAAY,MAAM,EAAE,EACnC,QACD;AAED,UAAO,KACL,eAAe,OAAO,SAAS,OAAO,KAAK,QAAQ,CAAC,OAAO,gBAC5D;WACM,OAAO;AACd,UAAO,MAAM,wBAAwB,OAAO,SAAS,MAAM;AAC3D,WAAQ,KAAK,EAAE;;;;AAKrB,SAAS,2BACP,gBACA,mBAKQ;CACR,IAAI,MAAM;AAEV,KAAI,eAAe,SAAS,GAAG;AAC7B,SAAO;AACP,SAAO,eAAe,KAAK,WAAW,SAAS,OAAO,OAAO,CAAC,KAAK,KAAK;AACxE,SAAO;;AAGT,KAAI,kBAAkB,SAAS,GAAG;AAChC,SAAO;AACP,SAAO,kBACJ,KACE,SACC,SAAS,KAAK,OAAO,IAAI,KAAK,QAAQ,GAAG,KAAK,MAAM,uBACvD,CACA,KAAK,KAAK;AACb,SAAO;;AAGT,QAAO;AACP,QAAO;AACP,QAAO;AACP,QAAO;AAEP,QAAO;;AAGT,SAAS,wBACP,QACQ;CACR,IAAI,MAAM;AAEV,QAAO,OAAO,KAAK,QAAQ,OAAO,IAAI,OAAO,IAAI,IAAI,QAAQ,CAAC,KAAK,KAAK;AAExE,QAAO;AACP,QAAO;AACP,QAAO;AAEP,QAAO"}
1
+ {"version":3,"file":"build-translator.mjs","names":["translationServer: TranslationServer | undefined","stats: BuildTranslationResult[\"stats\"]","errors: Array<{ locale: LocaleCode; error: string }>","missingLocales: string[]","incompleteLocales: Array<{\n locale: LocaleCode;\n missing: number;\n total: number;\n }>"],"sources":["../../src/plugin/build-translator.ts"],"sourcesContent":["/**\n * Build-time translation processor\n *\n * Handles translation generation and validation at build time\n * Supports two modes:\n * - \"translate\": Generate all translations, fail if translation fails\n * - \"cache-only\": Validate cache completeness, fail if incomplete\n */\n// TODO (AleksandrSl 08/12/2025): Add ICU validation for messages? The problem is that we don't know which will be rendered as a simple text\nimport fs from \"fs/promises\";\nimport path from \"path\";\nimport type { LingoConfig, MetadataSchema } from \"../types\";\nimport { logger } from \"../utils/logger\";\nimport { startTranslationServer, type TranslationServer, } from \"../translation-server\";\nimport { loadMetadata } from \"../metadata/manager\";\nimport { createCache, type TranslationCache, TranslationService, } from \"../translators\";\nimport { dictionaryFrom } from \"../translators/api\";\nimport type { LocaleCode } from \"lingo.dev/spec\";\n\nexport interface BuildTranslationOptions {\n config: LingoConfig;\n publicOutputPath: string;\n metadataFilePath: string;\n}\n\nexport interface BuildTranslationResult {\n /**\n * Whether the build succeeded\n */\n success: boolean;\n\n /**\n * Error message if build failed\n */\n error?: string;\n\n /**\n * Translation statistics per locale\n */\n stats: Record<\n string,\n {\n total: number;\n translated: number;\n failed: number;\n }\n >;\n}\n\n/**\n * Process translations at build time\n *\n * @throws Error if validation or translation fails (causes build to fail)\n */\nexport async function processBuildTranslations(\n options: BuildTranslationOptions,\n): Promise<BuildTranslationResult> {\n const { config, publicOutputPath, metadataFilePath } = options;\n\n // Determine build mode (env var > options > config)\n const buildMode =\n (process.env.LINGO_BUILD_MODE as \"translate\" | \"cache-only\") ||\n config.buildMode;\n\n logger.info(`🌍 Build mode: ${buildMode}`);\n\n const metadata = await loadMetadata(metadataFilePath);\n\n if (!metadata || Object.keys(metadata.entries).length === 0) {\n logger.info(\"No translations to process (metadata is empty)\");\n return {\n success: true,\n stats: {},\n };\n }\n\n const totalEntries = Object.keys(metadata.entries).length;\n logger.info(`📊 Found ${totalEntries} translatable entries`);\n\n const cache = createCache(config);\n\n // Handle cache-only mode\n if (buildMode === \"cache-only\") {\n logger.info(\"🔍 Validating translation cache...\");\n await validateCache(config, metadata, cache);\n logger.info(\"✅ Cache validation passed\");\n\n if (publicOutputPath) {\n await copyStaticFiles(config, publicOutputPath, metadata, cache);\n }\n\n return {\n success: true,\n stats: buildCacheStats(config, metadata),\n };\n }\n\n // Handle translate mode\n logger.info(\"🔄 Generating translations...\");\n let translationServer: TranslationServer | undefined;\n\n try {\n translationServer = await startTranslationServer({\n translationService: new TranslationService(config, logger),\n onError: (err) => {\n logger.error(\"Translation server error:\", err);\n },\n config,\n });\n\n // When pluralization is enabled, we need to generate the source locale file too\n // because pluralization modifies the sourceText\n const needsSourceLocale = config.pluralization?.enabled === true;\n const allLocales = needsSourceLocale\n ? [config.sourceLocale, ...config.targetLocales]\n : config.targetLocales;\n\n logger.info(\n `Processing translations for ${allLocales.length} locale(s)${needsSourceLocale ? \" (including source locale for pluralization)\" : \"\"}...`,\n );\n\n const stats: BuildTranslationResult[\"stats\"] = {};\n const errors: Array<{ locale: LocaleCode; error: string }> = [];\n\n // Translate all locales in parallel\n const localePromises = allLocales.map(async (locale) => {\n logger.info(`Translating to ${locale}...`);\n\n const result = await translationServer!.translateAll(locale);\n\n stats[locale] = {\n total: totalEntries,\n translated: Object.keys(result.translations).length,\n failed: result.errors.length,\n };\n\n if (result.errors.length > 0) {\n logger.warn(\n `⚠️ ${result.errors.length} translation error(s) for ${locale}`,\n );\n errors.push({\n locale,\n error: `${result.errors.length} translation(s) failed`,\n });\n } else {\n logger.info(`✅ ${locale} completed successfully`);\n }\n });\n\n await Promise.all(localePromises);\n\n // Fail build if any translations failed in translate mode\n if (errors.length > 0) {\n const errorMsg = formatTranslationErrors(errors);\n logger.error(errorMsg);\n process.exit(1);\n }\n\n // Copy cache to public directory if requested\n if (publicOutputPath) {\n await copyStaticFiles(config, publicOutputPath, metadata, cache);\n }\n\n logger.info(\"✅ Translation generation completed successfully\");\n\n return {\n success: true,\n stats,\n };\n } catch (error) {\n logger.error(\n \"❌ Translation generation failed:\\n\",\n error instanceof Error ? error.message : error,\n );\n process.exit(1);\n } finally {\n if (translationServer) {\n await translationServer.stop();\n logger.info(\"✅ Translation server stopped\");\n }\n }\n}\n\n/**\n * Validate that all required translations exist in cache\n * @throws Error if cache is incomplete or missing\n */\nasync function validateCache(\n config: LingoConfig,\n metadata: MetadataSchema,\n cache: TranslationCache,\n): Promise<void> {\n const allHashes = Object.keys(metadata.entries);\n const missingLocales: string[] = [];\n const incompleteLocales: Array<{\n locale: LocaleCode;\n missing: number;\n total: number;\n }> = [];\n\n // Include source locale if pluralization is enabled\n const needsSourceLocale = config.pluralization?.enabled === true;\n const allLocales = needsSourceLocale\n ? [config.sourceLocale, ...config.targetLocales]\n : config.targetLocales;\n\n for (const locale of allLocales) {\n try {\n const entries = await cache.get(locale);\n\n if (Object.keys(entries).length === 0) {\n missingLocales.push(locale);\n logger.debug(`Cache file not found or empty for ${locale}`);\n continue;\n }\n\n const missingHashes = allHashes.filter((hash) => !entries[hash]);\n\n if (missingHashes.length > 0) {\n incompleteLocales.push({\n locale,\n missing: missingHashes.length,\n total: allHashes.length,\n });\n\n // Log first few missing hashes for debugging\n logger.debug(\n `Missing hashes in ${locale}: ${missingHashes.slice(0, 5).join(\", \")}${\n missingHashes.length > 5 ? \"...\" : \"\"\n }`,\n );\n }\n } catch (error) {\n missingLocales.push(locale);\n logger.debug(`Failed to read cache for ${locale}:`, error);\n }\n }\n\n if (missingLocales.length > 0 || incompleteLocales.length > 0) {\n const errorMsg = formatCacheValidationError(\n missingLocales,\n incompleteLocales,\n );\n logger.error(errorMsg);\n process.exit(1);\n }\n}\n\nfunction buildCacheStats(\n config: LingoConfig,\n metadata: MetadataSchema,\n): BuildTranslationResult[\"stats\"] {\n const totalEntries = Object.keys(metadata.entries).length;\n const stats: BuildTranslationResult[\"stats\"] = {};\n\n // Include source locale if pluralization is enabled\n const needsSourceLocale = config.pluralization?.enabled === true;\n const allLocales = needsSourceLocale\n ? [config.sourceLocale, ...config.targetLocales]\n : config.targetLocales;\n\n for (const locale of allLocales) {\n stats[locale] = {\n total: totalEntries,\n translated: totalEntries, // Assumed complete if validation passed\n failed: 0,\n };\n }\n\n return stats;\n}\n\nasync function copyStaticFiles(\n config: LingoConfig,\n publicOutputPath: string,\n metadata: MetadataSchema,\n cache: TranslationCache,\n): Promise<void> {\n logger.info(`📦 Generating static translation files in ${publicOutputPath}`);\n\n await fs.mkdir(publicOutputPath, { recursive: true });\n\n const usedHashes = new Set(Object.keys(metadata.entries));\n logger.info(`📊 Filtering translations to ${usedHashes.size} used hash(es)`);\n\n // Include source locale if pluralization is enabled\n const needsSourceLocale = config.pluralization?.enabled === true;\n const allLocales = needsSourceLocale\n ? [config.sourceLocale, ...config.targetLocales]\n : config.targetLocales;\n\n for (const locale of allLocales) {\n const publicFilePath = path.join(publicOutputPath, `${locale}.json`);\n\n try {\n const entries = await cache.get(locale, Array.from(usedHashes));\n const outputData = dictionaryFrom(locale, entries);\n\n await fs.writeFile(\n publicFilePath,\n JSON.stringify(outputData, null, 2),\n \"utf-8\",\n );\n\n logger.info(\n `✓ Generated ${locale}.json (${Object.keys(entries).length} translations)`,\n );\n } catch (error) {\n logger.error(`❌ Failed to generate ${locale}.json:`, error);\n process.exit(1);\n }\n }\n}\n\nfunction formatCacheValidationError(\n missingLocales: string[],\n incompleteLocales: Array<{\n locale: LocaleCode;\n missing: number;\n total: number;\n }>,\n): string {\n let msg = \"❌ Cache validation failed in cache-only mode:\\n\\n\";\n\n if (missingLocales.length > 0) {\n msg += ` 📁 Missing cache files:\\n`;\n msg += missingLocales.map((locale) => ` - ${locale}.json`).join(\"\\n\");\n msg += \"\\n\\n\";\n }\n\n if (incompleteLocales.length > 0) {\n msg += ` 📊 Incomplete cache:\\n`;\n msg += incompleteLocales\n .map(\n (item) =>\n ` - ${item.locale}: ${item.missing}/${item.total} translations missing`,\n )\n .join(\"\\n\");\n msg += \"\\n\\n\";\n }\n\n msg += ` 💡 To fix:\\n`;\n msg += ` 1. Set LINGO_BUILD_MODE=translate to generate translations\\n`;\n msg += ` 2. Commit the generated .lingo/cache/*.json files\\n`;\n msg += ` 3. Ensure translation API keys are available if generating translations`;\n\n return msg;\n}\n\nfunction formatTranslationErrors(\n errors: Array<{ locale: LocaleCode; error: string }>,\n): string {\n let msg = \"❌ Translation generation failed:\\n\\n\";\n\n msg += errors.map((err) => ` - ${err.locale}: ${err.error}`).join(\"\\n\");\n\n msg += \"\\n\\n\";\n msg += ` 💡 Translation errors must be resolved in \"translate\" mode.\\n`;\n msg += ` Check translation server logs for details.`;\n\n return msg;\n}\n"],"mappings":"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"}
@@ -1,12 +1,12 @@
1
1
  import { LingoProviderProps } from "../shared/LingoProvider.cjs";
2
- import * as react_jsx_runtime2 from "react/jsx-runtime";
2
+ import * as react_jsx_runtime3 from "react/jsx-runtime";
3
3
 
4
4
  //#region src/react/server/ServerLingoProvider.d.ts
5
5
  declare function LingoProvider({
6
6
  initialLocale,
7
7
  initialTranslations,
8
8
  ...rest
9
- }: LingoProviderProps): Promise<react_jsx_runtime2.JSX.Element>;
9
+ }: LingoProviderProps): Promise<react_jsx_runtime3.JSX.Element>;
10
10
  //#endregion
11
11
  export { LingoProvider };
12
12
  //# sourceMappingURL=ServerLingoProvider.d.cts.map
@@ -1,5 +1,5 @@
1
1
  import { LocaleCode } from "lingo.dev/spec";
2
- import * as react_jsx_runtime1 from "react/jsx-runtime";
2
+ import * as react_jsx_runtime2 from "react/jsx-runtime";
3
3
  import { PropsWithChildren } from "react";
4
4
 
5
5
  //#region src/react/shared/LingoProvider.d.ts
@@ -70,7 +70,7 @@ declare function LingoProvider__Dev({
70
70
  router,
71
71
  devWidget,
72
72
  children
73
- }: LingoProviderProps): react_jsx_runtime1.JSX.Element;
73
+ }: LingoProviderProps): react_jsx_runtime2.JSX.Element;
74
74
  //#endregion
75
75
  export { LingoProvider, LingoProviderProps };
76
76
  //# sourceMappingURL=LingoProvider.d.cts.map
@@ -1,5 +1,5 @@
1
1
  import { LocaleCode } from "lingo.dev/spec";
2
- import * as react_jsx_runtime3 from "react/jsx-runtime";
2
+ import * as react_jsx_runtime1 from "react/jsx-runtime";
3
3
  import { CSSProperties } from "react";
4
4
 
5
5
  //#region src/react/shared/LocaleSwitcher.d.ts
@@ -65,7 +65,7 @@ declare function LocaleSwitcher({
65
65
  style,
66
66
  className,
67
67
  showLoadingState
68
- }: LocaleSwitcherProps): react_jsx_runtime3.JSX.Element;
68
+ }: LocaleSwitcherProps): react_jsx_runtime1.JSX.Element;
69
69
  //#endregion
70
70
  export { LocaleSwitcher };
71
71
  //# sourceMappingURL=LocaleSwitcher.d.cts.map
@@ -1,6 +1,6 @@
1
1
  import { dictionaryFrom } from "./api.mjs";
2
2
  import { DEFAULT_TIMEOUTS, withTimeout } from "../utils/timeout.mjs";
3
- import * as fs from "fs/promises";
3
+ import * as fs$1 from "fs/promises";
4
4
  import * as path$1 from "path";
5
5
 
6
6
  //#region src/translators/local-cache.ts
@@ -27,7 +27,7 @@ var LocalTranslationCache = class {
27
27
  async getDictionary(locale) {
28
28
  try {
29
29
  const cachePath = this.getCachePath(locale);
30
- const content = await withTimeout(fs.readFile(cachePath, "utf-8"), DEFAULT_TIMEOUTS.FILE_IO, `Read cache for ${locale}`);
30
+ const content = await withTimeout(fs$1.readFile(cachePath, "utf-8"), DEFAULT_TIMEOUTS.FILE_IO, `Read cache for ${locale}`);
31
31
  return JSON.parse(content);
32
32
  } catch {
33
33
  return null;
@@ -41,8 +41,8 @@ var LocalTranslationCache = class {
41
41
  try {
42
42
  const cachePath = this.getCachePath(locale);
43
43
  const cacheDir = path$1.dirname(cachePath);
44
- await withTimeout(fs.mkdir(cacheDir, { recursive: true }), DEFAULT_TIMEOUTS.FILE_IO, `Create cache directory for ${locale}`);
45
- await withTimeout(fs.writeFile(cachePath, JSON.stringify(dictionary, null, 2), "utf-8"), DEFAULT_TIMEOUTS.FILE_IO, `Write cache for ${locale}`);
44
+ await withTimeout(fs$1.mkdir(cacheDir, { recursive: true }), DEFAULT_TIMEOUTS.FILE_IO, `Create cache directory for ${locale}`);
45
+ await withTimeout(fs$1.writeFile(cachePath, JSON.stringify(dictionary, null, 2), "utf-8"), DEFAULT_TIMEOUTS.FILE_IO, `Write cache for ${locale}`);
46
46
  } catch (error) {
47
47
  this.logger.error(`Failed to write cache for locale ${locale}:`, error);
48
48
  throw error;
@@ -82,7 +82,7 @@ var LocalTranslationCache = class {
82
82
  async has(locale) {
83
83
  try {
84
84
  const cachePath = this.getCachePath(locale);
85
- await fs.access(cachePath);
85
+ await fs$1.access(cachePath);
86
86
  return true;
87
87
  } catch {
88
88
  return false;
@@ -94,7 +94,7 @@ var LocalTranslationCache = class {
94
94
  async clear(locale) {
95
95
  try {
96
96
  const cachePath = this.getCachePath(locale);
97
- await fs.unlink(cachePath);
97
+ await fs$1.unlink(cachePath);
98
98
  } catch {}
99
99
  }
100
100
  /**
@@ -102,8 +102,8 @@ var LocalTranslationCache = class {
102
102
  */
103
103
  async clearAll() {
104
104
  try {
105
- const files = await fs.readdir(this.config.cacheDir);
106
- await Promise.all(files.filter((file) => file.endsWith(".json")).map((file) => fs.unlink(path$1.join(this.config.cacheDir, file))));
105
+ const files = await fs$1.readdir(this.config.cacheDir);
106
+ await Promise.all(files.filter((file) => file.endsWith(".json")).map((file) => fs$1.unlink(path$1.join(this.config.cacheDir, file))));
107
107
  } catch {}
108
108
  }
109
109
  };
@@ -1 +1 @@
1
- {"version":3,"file":"local-cache.mjs","names":["logger: Logger","path"],"sources":["../../src/translators/local-cache.ts"],"sourcesContent":["/**\n * Local disk-based translation cache implementation\n */\n\nimport * as fs from \"fs/promises\";\nimport * as path from \"path\";\nimport type { LocalCacheConfig, TranslationCache } from \"./cache\";\nimport { dictionaryFrom, type DictionarySchema } from \"./api\";\nimport { DEFAULT_TIMEOUTS, withTimeout } from \"../utils/timeout\";\nimport type { Logger } from \"../utils/logger\";\nimport type { LocaleCode } from \"lingo.dev/spec\";\n\n/**\n * Local file system cache for translations\n * Stores translations as JSON files in .lingo/cache/\n */\nexport class LocalTranslationCache implements TranslationCache {\n private config: LocalCacheConfig;\n\n constructor(\n config: LocalCacheConfig,\n private logger: Logger,\n ) {\n this.config = config;\n }\n\n private getCachePath(locale: LocaleCode): string {\n return path.join(this.config.cacheDir, `${locale}.json`);\n }\n\n /**\n * Read dictionary file from disk\n * Times out after 10 seconds to prevent indefinite hangs\n */\n async getDictionary(locale: LocaleCode): Promise<DictionarySchema | null> {\n try {\n const cachePath = this.getCachePath(locale);\n const content = await withTimeout(\n fs.readFile(cachePath, \"utf-8\"),\n DEFAULT_TIMEOUTS.FILE_IO,\n `Read cache for ${locale}`,\n );\n return JSON.parse(content);\n } catch {\n return null;\n }\n }\n\n /**\n * Write dictionary file to disk\n * Times out after 10 seconds to prevent indefinite hangs\n */\n private async setDictionary(\n locale: LocaleCode,\n dictionary: DictionarySchema,\n ): Promise<void> {\n try {\n const cachePath = this.getCachePath(locale);\n const cacheDir = path.dirname(cachePath);\n\n // Ensure cache directory exists\n await withTimeout(\n fs.mkdir(cacheDir, { recursive: true }),\n DEFAULT_TIMEOUTS.FILE_IO,\n `Create cache directory for ${locale}`,\n );\n\n // Write cache file\n await withTimeout(\n fs.writeFile(cachePath, JSON.stringify(dictionary, null, 2), \"utf-8\"),\n DEFAULT_TIMEOUTS.FILE_IO,\n `Write cache for ${locale}`,\n );\n } catch (error) {\n this.logger.error(`Failed to write cache for locale ${locale}:`, error);\n throw error;\n }\n }\n\n /**\n * Get cached translations for a locale\n */\n async get(\n locale: LocaleCode,\n hashes?: string[],\n ): Promise<Record<string, string>> {\n const dictionary = await this.getDictionary(locale);\n if (!dictionary) {\n return {};\n }\n if (hashes) {\n return hashes.reduce(\n (acc, hash) => ({ ...acc, [hash]: dictionary.entries[hash] }),\n {},\n );\n }\n return dictionary.entries || {};\n }\n\n /**\n * Update cache with new translations (merge)\n */\n async update(\n locale: LocaleCode,\n translations: Record<string, string>,\n ): Promise<void> {\n const existing = await this.get(locale);\n\n const merged = { ...existing, ...translations };\n\n await this.set(locale, merged);\n }\n\n /**\n * Replace entire cache for a locale\n */\n async set(\n locale: LocaleCode,\n translations: Record<string, string>,\n ): Promise<void> {\n await this.setDictionary(locale, dictionaryFrom(locale, translations));\n }\n\n /**\n * Check if cache exists for a locale\n */\n async has(locale: LocaleCode): Promise<boolean> {\n try {\n const cachePath = this.getCachePath(locale);\n await fs.access(cachePath);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Clear cache for a specific locale\n */\n async clear(locale: LocaleCode): Promise<void> {\n try {\n const cachePath = this.getCachePath(locale);\n await fs.unlink(cachePath);\n } catch {\n // Ignore errors if file doesn't exist\n }\n }\n\n /**\n * Clear all cached translations\n */\n async clearAll(): Promise<void> {\n try {\n const files = await fs.readdir(this.config.cacheDir);\n\n await Promise.all(\n files\n .filter((file) => file.endsWith(\".json\"))\n .map((file) => fs.unlink(path.join(this.config.cacheDir, file))),\n );\n } catch {\n // Ignore errors if directory doesn't exist\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAgBA,IAAa,wBAAb,MAA+D;CAC7D,AAAQ;CAER,YACE,QACA,AAAQA,QACR;EADQ;AAER,OAAK,SAAS;;CAGhB,AAAQ,aAAa,QAA4B;AAC/C,SAAOC,OAAK,KAAK,KAAK,OAAO,UAAU,GAAG,OAAO,OAAO;;;;;;CAO1D,MAAM,cAAc,QAAsD;AACxE,MAAI;GACF,MAAM,YAAY,KAAK,aAAa,OAAO;GAC3C,MAAM,UAAU,MAAM,YACpB,GAAG,SAAS,WAAW,QAAQ,EAC/B,iBAAiB,SACjB,kBAAkB,SACnB;AACD,UAAO,KAAK,MAAM,QAAQ;UACpB;AACN,UAAO;;;;;;;CAQX,MAAc,cACZ,QACA,YACe;AACf,MAAI;GACF,MAAM,YAAY,KAAK,aAAa,OAAO;GAC3C,MAAM,WAAWA,OAAK,QAAQ,UAAU;AAGxC,SAAM,YACJ,GAAG,MAAM,UAAU,EAAE,WAAW,MAAM,CAAC,EACvC,iBAAiB,SACjB,8BAA8B,SAC/B;AAGD,SAAM,YACJ,GAAG,UAAU,WAAW,KAAK,UAAU,YAAY,MAAM,EAAE,EAAE,QAAQ,EACrE,iBAAiB,SACjB,mBAAmB,SACpB;WACM,OAAO;AACd,QAAK,OAAO,MAAM,oCAAoC,OAAO,IAAI,MAAM;AACvE,SAAM;;;;;;CAOV,MAAM,IACJ,QACA,QACiC;EACjC,MAAM,aAAa,MAAM,KAAK,cAAc,OAAO;AACnD,MAAI,CAAC,WACH,QAAO,EAAE;AAEX,MAAI,OACF,QAAO,OAAO,QACX,KAAK,UAAU;GAAE,GAAG;IAAM,OAAO,WAAW,QAAQ;GAAO,GAC5D,EAAE,CACH;AAEH,SAAO,WAAW,WAAW,EAAE;;;;;CAMjC,MAAM,OACJ,QACA,cACe;EAGf,MAAM,SAAS;GAAE,GAFA,MAAM,KAAK,IAAI,OAAO;GAET,GAAG;GAAc;AAE/C,QAAM,KAAK,IAAI,QAAQ,OAAO;;;;;CAMhC,MAAM,IACJ,QACA,cACe;AACf,QAAM,KAAK,cAAc,QAAQ,eAAe,QAAQ,aAAa,CAAC;;;;;CAMxE,MAAM,IAAI,QAAsC;AAC9C,MAAI;GACF,MAAM,YAAY,KAAK,aAAa,OAAO;AAC3C,SAAM,GAAG,OAAO,UAAU;AAC1B,UAAO;UACD;AACN,UAAO;;;;;;CAOX,MAAM,MAAM,QAAmC;AAC7C,MAAI;GACF,MAAM,YAAY,KAAK,aAAa,OAAO;AAC3C,SAAM,GAAG,OAAO,UAAU;UACpB;;;;;CAQV,MAAM,WAA0B;AAC9B,MAAI;GACF,MAAM,QAAQ,MAAM,GAAG,QAAQ,KAAK,OAAO,SAAS;AAEpD,SAAM,QAAQ,IACZ,MACG,QAAQ,SAAS,KAAK,SAAS,QAAQ,CAAC,CACxC,KAAK,SAAS,GAAG,OAAOA,OAAK,KAAK,KAAK,OAAO,UAAU,KAAK,CAAC,CAAC,CACnE;UACK"}
1
+ {"version":3,"file":"local-cache.mjs","names":["logger: Logger","path","fs"],"sources":["../../src/translators/local-cache.ts"],"sourcesContent":["/**\n * Local disk-based translation cache implementation\n */\n\nimport * as fs from \"fs/promises\";\nimport * as path from \"path\";\nimport type { LocalCacheConfig, TranslationCache } from \"./cache\";\nimport { dictionaryFrom, type DictionarySchema } from \"./api\";\nimport { DEFAULT_TIMEOUTS, withTimeout } from \"../utils/timeout\";\nimport type { Logger } from \"../utils/logger\";\nimport type { LocaleCode } from \"lingo.dev/spec\";\n\n/**\n * Local file system cache for translations\n * Stores translations as JSON files in .lingo/cache/\n */\nexport class LocalTranslationCache implements TranslationCache {\n private config: LocalCacheConfig;\n\n constructor(\n config: LocalCacheConfig,\n private logger: Logger,\n ) {\n this.config = config;\n }\n\n private getCachePath(locale: LocaleCode): string {\n return path.join(this.config.cacheDir, `${locale}.json`);\n }\n\n /**\n * Read dictionary file from disk\n * Times out after 10 seconds to prevent indefinite hangs\n */\n async getDictionary(locale: LocaleCode): Promise<DictionarySchema | null> {\n try {\n const cachePath = this.getCachePath(locale);\n const content = await withTimeout(\n fs.readFile(cachePath, \"utf-8\"),\n DEFAULT_TIMEOUTS.FILE_IO,\n `Read cache for ${locale}`,\n );\n return JSON.parse(content);\n } catch {\n return null;\n }\n }\n\n /**\n * Write dictionary file to disk\n * Times out after 10 seconds to prevent indefinite hangs\n */\n private async setDictionary(\n locale: LocaleCode,\n dictionary: DictionarySchema,\n ): Promise<void> {\n try {\n const cachePath = this.getCachePath(locale);\n const cacheDir = path.dirname(cachePath);\n\n // Ensure cache directory exists\n await withTimeout(\n fs.mkdir(cacheDir, { recursive: true }),\n DEFAULT_TIMEOUTS.FILE_IO,\n `Create cache directory for ${locale}`,\n );\n\n // Write cache file\n await withTimeout(\n fs.writeFile(cachePath, JSON.stringify(dictionary, null, 2), \"utf-8\"),\n DEFAULT_TIMEOUTS.FILE_IO,\n `Write cache for ${locale}`,\n );\n } catch (error) {\n this.logger.error(`Failed to write cache for locale ${locale}:`, error);\n throw error;\n }\n }\n\n /**\n * Get cached translations for a locale\n */\n async get(\n locale: LocaleCode,\n hashes?: string[],\n ): Promise<Record<string, string>> {\n const dictionary = await this.getDictionary(locale);\n if (!dictionary) {\n return {};\n }\n if (hashes) {\n return hashes.reduce(\n (acc, hash) => ({ ...acc, [hash]: dictionary.entries[hash] }),\n {},\n );\n }\n return dictionary.entries || {};\n }\n\n /**\n * Update cache with new translations (merge)\n */\n async update(\n locale: LocaleCode,\n translations: Record<string, string>,\n ): Promise<void> {\n const existing = await this.get(locale);\n\n const merged = { ...existing, ...translations };\n\n await this.set(locale, merged);\n }\n\n /**\n * Replace entire cache for a locale\n */\n async set(\n locale: LocaleCode,\n translations: Record<string, string>,\n ): Promise<void> {\n await this.setDictionary(locale, dictionaryFrom(locale, translations));\n }\n\n /**\n * Check if cache exists for a locale\n */\n async has(locale: LocaleCode): Promise<boolean> {\n try {\n const cachePath = this.getCachePath(locale);\n await fs.access(cachePath);\n return true;\n } catch {\n return false;\n }\n }\n\n /**\n * Clear cache for a specific locale\n */\n async clear(locale: LocaleCode): Promise<void> {\n try {\n const cachePath = this.getCachePath(locale);\n await fs.unlink(cachePath);\n } catch {\n // Ignore errors if file doesn't exist\n }\n }\n\n /**\n * Clear all cached translations\n */\n async clearAll(): Promise<void> {\n try {\n const files = await fs.readdir(this.config.cacheDir);\n\n await Promise.all(\n files\n .filter((file) => file.endsWith(\".json\"))\n .map((file) => fs.unlink(path.join(this.config.cacheDir, file))),\n );\n } catch {\n // Ignore errors if directory doesn't exist\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAgBA,IAAa,wBAAb,MAA+D;CAC7D,AAAQ;CAER,YACE,QACA,AAAQA,QACR;EADQ;AAER,OAAK,SAAS;;CAGhB,AAAQ,aAAa,QAA4B;AAC/C,SAAOC,OAAK,KAAK,KAAK,OAAO,UAAU,GAAG,OAAO,OAAO;;;;;;CAO1D,MAAM,cAAc,QAAsD;AACxE,MAAI;GACF,MAAM,YAAY,KAAK,aAAa,OAAO;GAC3C,MAAM,UAAU,MAAM,YACpBC,KAAG,SAAS,WAAW,QAAQ,EAC/B,iBAAiB,SACjB,kBAAkB,SACnB;AACD,UAAO,KAAK,MAAM,QAAQ;UACpB;AACN,UAAO;;;;;;;CAQX,MAAc,cACZ,QACA,YACe;AACf,MAAI;GACF,MAAM,YAAY,KAAK,aAAa,OAAO;GAC3C,MAAM,WAAWD,OAAK,QAAQ,UAAU;AAGxC,SAAM,YACJC,KAAG,MAAM,UAAU,EAAE,WAAW,MAAM,CAAC,EACvC,iBAAiB,SACjB,8BAA8B,SAC/B;AAGD,SAAM,YACJA,KAAG,UAAU,WAAW,KAAK,UAAU,YAAY,MAAM,EAAE,EAAE,QAAQ,EACrE,iBAAiB,SACjB,mBAAmB,SACpB;WACM,OAAO;AACd,QAAK,OAAO,MAAM,oCAAoC,OAAO,IAAI,MAAM;AACvE,SAAM;;;;;;CAOV,MAAM,IACJ,QACA,QACiC;EACjC,MAAM,aAAa,MAAM,KAAK,cAAc,OAAO;AACnD,MAAI,CAAC,WACH,QAAO,EAAE;AAEX,MAAI,OACF,QAAO,OAAO,QACX,KAAK,UAAU;GAAE,GAAG;IAAM,OAAO,WAAW,QAAQ;GAAO,GAC5D,EAAE,CACH;AAEH,SAAO,WAAW,WAAW,EAAE;;;;;CAMjC,MAAM,OACJ,QACA,cACe;EAGf,MAAM,SAAS;GAAE,GAFA,MAAM,KAAK,IAAI,OAAO;GAET,GAAG;GAAc;AAE/C,QAAM,KAAK,IAAI,QAAQ,OAAO;;;;;CAMhC,MAAM,IACJ,QACA,cACe;AACf,QAAM,KAAK,cAAc,QAAQ,eAAe,QAAQ,aAAa,CAAC;;;;;CAMxE,MAAM,IAAI,QAAsC;AAC9C,MAAI;GACF,MAAM,YAAY,KAAK,aAAa,OAAO;AAC3C,SAAMA,KAAG,OAAO,UAAU;AAC1B,UAAO;UACD;AACN,UAAO;;;;;;CAOX,MAAM,MAAM,QAAmC;AAC7C,MAAI;GACF,MAAM,YAAY,KAAK,aAAa,OAAO;AAC3C,SAAMA,KAAG,OAAO,UAAU;UACpB;;;;;CAQV,MAAM,WAA0B;AAC9B,MAAI;GACF,MAAM,QAAQ,MAAMA,KAAG,QAAQ,KAAK,OAAO,SAAS;AAEpD,SAAM,QAAQ,IACZ,MACG,QAAQ,SAAS,KAAK,SAAS,QAAQ,CAAC,CACxC,KAAK,SAASA,KAAG,OAAOD,OAAK,KAAK,KAAK,OAAO,UAAU,KAAK,CAAC,CAAC,CACnE;UACK"}
@@ -8,7 +8,7 @@ type PluralizationConfig = {
8
8
  /**
9
9
  * LLM provider for pluralization detection
10
10
  * Format: "provider:model" (e.g., "groq:llama3-8b-8192")
11
- * @default "groq:llama3-8b-8192"
11
+ * If omitted in user config, the compiler can infer it from translation models.
12
12
  */
13
13
  model: string;
14
14
  };
@@ -8,7 +8,7 @@ type PluralizationConfig = {
8
8
  /**
9
9
  * LLM provider for pluralization detection
10
10
  * Format: "provider:model" (e.g., "groq:llama3-8b-8192")
11
- * @default "groq:llama3-8b-8192"
11
+ * If omitted in user config, the compiler can infer it from translation models.
12
12
  */
13
13
  model: string;
14
14
  };
package/build/types.d.cts CHANGED
@@ -31,11 +31,13 @@ type LocalePersistenceConfig = {
31
31
  */
32
32
  type LingoConfigRequiredFields = "sourceLocale" | "targetLocales";
33
33
  type LingoInternalFields = "environment" | "cacheType";
34
+ type PartialPluralizationConfig = Partial<Omit<PluralizationConfig, "sourceLocale">>;
34
35
  /**
35
36
  * Configuration for the Lingo compiler
36
37
  */
37
- type PartialLingoConfig = Pick<LingoConfig, LingoConfigRequiredFields> & Partial<Omit<LingoConfig, LingoConfigRequiredFields | "dev" | LingoInternalFields> & {
38
+ type PartialLingoConfig = Pick<LingoConfig, LingoConfigRequiredFields> & Partial<Omit<LingoConfig, LingoConfigRequiredFields | "dev" | LingoInternalFields | "pluralization"> & {
38
39
  dev: Partial<LingoConfig["dev"]>;
40
+ pluralization: PartialPluralizationConfig;
39
41
  }>;
40
42
  type LingoEnvironment = "development" | "production";
41
43
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.cts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;;AA4BA;AAKA;AAEA;AAKY,UA9BK,YAAA,CA8Ba;EAAQ;;;;EAIhC,IAAA,EAAA,MAAA;EAAoC;;;;EAHxC,MAAA,EAAA,MAAA;;AASF;AAKA;;;AAoDiB,KA/EL,uBAAA,GA+EK;EAkBO,IAAA,EAAA,QAAA;EAYF,MAAA,EA7G0C,YA6G1C;CAAL;;;;KAxGL,yBAAA;KAEA,mBAAA;;;;KAKA,kBAAA,GAAqB,KAAK,aAAa,6BACjD,QACE,KACE,aACA,oCAAoC;OAE/B,QAAQ;;KAIP,gBAAA;;;;KAKA,WAAA;;;;;;;;;;;;;;;;eAkBG;;;;;;;;;;;;;;;;;;;;gBAsBC;;;;;;;;;;;iBAYC;;;;;;;;;;;;;;;;wBAkBO;;;;;;;;;;iBAYP,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAkCD"}
1
+ {"version":3,"file":"types.d.cts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;;AA4BA;AAKA;AAEA;AAEY,UA3BK,YAAA,CA2BL;EACL;;;;EAMK,IAAA,EAAA,MAAA;EAA0B;;;;EAIhC,MAAA,EAAA,MAAA;;;;;;AAHJ,KAjBU,uBAAA,GAiBV;EAAO,IAAA,EAAA,QAAA;EAUG,MAAA,EA3BoD,YA2BpC;AAK5B,CAAA;;;;AAsEwB,KAjGZ,yBAAA,GAiGY,cAAA,GAAA,eAAA;AAYF,KA3GV,mBAAA,GA2GU,aAAA,GAAA,WAAA;AAAL,KAzGL,0BAAA,GAA6B,OAyGxB,CAxGf,IAwGe,CAxGV,mBAwGU,EAAA,cAAA,CAAA,CAAA;;;;KAlGL,kBAAA,GAAqB,KAAK,aAAa,6BACjD,QACE,KACE,aACA,oCAAoC;OAE/B,QAAQ;iBACE;;KAIT,gBAAA;;;;KAKA,WAAA;;;;;;;;;;;;;;;;eAkBG;;;;;;;;;;;;;;;;;;;;gBAsBC;;;;;;;;;;;iBAYC;;;;;;;;;;;;;;;;wBAkBO;;;;;;;;;;iBAYP,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAkCD"}
package/build/types.d.mts CHANGED
@@ -31,11 +31,13 @@ type LocalePersistenceConfig = {
31
31
  */
32
32
  type LingoConfigRequiredFields = "sourceLocale" | "targetLocales";
33
33
  type LingoInternalFields = "environment" | "cacheType";
34
+ type PartialPluralizationConfig = Partial<Omit<PluralizationConfig, "sourceLocale">>;
34
35
  /**
35
36
  * Configuration for the Lingo compiler
36
37
  */
37
- type PartialLingoConfig = Pick<LingoConfig, LingoConfigRequiredFields> & Partial<Omit<LingoConfig, LingoConfigRequiredFields | "dev" | LingoInternalFields> & {
38
+ type PartialLingoConfig = Pick<LingoConfig, LingoConfigRequiredFields> & Partial<Omit<LingoConfig, LingoConfigRequiredFields | "dev" | LingoInternalFields | "pluralization"> & {
38
39
  dev: Partial<LingoConfig["dev"]>;
40
+ pluralization: PartialPluralizationConfig;
39
41
  }>;
40
42
  type LingoEnvironment = "development" | "production";
41
43
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;;AA4BA;AAKA;AAEA;AAKY,UA9BK,YAAA,CA8Ba;EAAQ;;;;EAIhC,IAAA,EAAA,MAAA;EAAoC;;;;EAHxC,MAAA,EAAA,MAAA;;AASF;AAKA;;;AAoDiB,KA/EL,uBAAA,GA+EK;EAkBO,IAAA,EAAA,QAAA;EAYF,MAAA,EA7G0C,YA6G1C;CAAL;;;;KAxGL,yBAAA;KAEA,mBAAA;;;;KAKA,kBAAA,GAAqB,KAAK,aAAa,6BACjD,QACE,KACE,aACA,oCAAoC;OAE/B,QAAQ;;KAIP,gBAAA;;;;KAKA,WAAA;;;;;;;;;;;;;;;;eAkBG;;;;;;;;;;;;;;;;;;;;gBAsBC;;;;;;;;;;;iBAYC;;;;;;;;;;;;;;;;wBAkBO;;;;;;;;;;iBAYP,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAkCD"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;;AA4BA;AAKA;AAEA;AAEY,UA3BK,YAAA,CA2BL;EACL;;;;EAMK,IAAA,EAAA,MAAA;EAA0B;;;;EAIhC,MAAA,EAAA,MAAA;;;;;;AAHJ,KAjBU,uBAAA,GAiBV;EAAO,IAAA,EAAA,QAAA;EAUG,MAAA,EA3BoD,YA2BpC;AAK5B,CAAA;;;;AAsEwB,KAjGZ,yBAAA,GAiGY,cAAA,GAAA,eAAA;AAYF,KA3GV,mBAAA,GA2GU,aAAA,GAAA,WAAA;AAAL,KAzGL,0BAAA,GAA6B,OAyGxB,CAxGf,IAwGe,CAxGV,mBAwGU,EAAA,cAAA,CAAA,CAAA;;;;KAlGL,kBAAA,GAAqB,KAAK,aAAa,6BACjD,QACE,KACE,aACA,oCAAoC;OAE/B,QAAQ;iBACE;;KAIT,gBAAA;;;;KAKA,WAAA;;;;;;;;;;;;;;;;eAkBG;;;;;;;;;;;;;;;;;;;;gBAsBC;;;;;;;;;;;iBAYC;;;;;;;;;;;;;;;;wBAkBO;;;;;;;;;;iBAYP,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBAkCD"}
@@ -17,11 +17,27 @@ const DEFAULT_CONFIG = {
17
17
  },
18
18
  models: "lingo.dev",
19
19
  pluralization: {
20
- enabled: true,
20
+ enabled: false,
21
21
  model: "groq:llama-3.1-8b-instant"
22
22
  },
23
23
  buildMode: "translate"
24
24
  };
25
+ function getModelStringForLocales(models, sourceLocale, targetLocale) {
26
+ const modelKey = (targetLocale ? [
27
+ `${sourceLocale}:${targetLocale}`,
28
+ `*:${targetLocale}`,
29
+ `${sourceLocale}:*`,
30
+ "*:*"
31
+ ] : [`${sourceLocale}:*`, "*:*"]).find((key) => key in models);
32
+ if (modelKey) return models[modelKey];
33
+ const sortedKeys = Object.keys(models).sort();
34
+ if (sortedKeys.length === 0) return;
35
+ return models[sortedKeys[0]];
36
+ }
37
+ function inferPluralizationModel(models, sourceLocale, targetLocales) {
38
+ if (models === "lingo.dev") return;
39
+ return getModelStringForLocales(models, sourceLocale, targetLocales[0]);
40
+ }
25
41
  /**
26
42
  * Create a LoaderConfig with defaults applied
27
43
  *
@@ -30,7 +46,7 @@ const DEFAULT_CONFIG = {
30
46
  *
31
47
  */
32
48
  function createLingoConfig(options) {
33
- return {
49
+ const config = {
34
50
  ...DEFAULT_CONFIG,
35
51
  ...options,
36
52
  environment: options.environment ?? (process.env.NODE_ENV === "development" ? "development" : "production"),
@@ -52,6 +68,22 @@ function createLingoConfig(options) {
52
68
  }
53
69
  }
54
70
  };
71
+ const explicitEnabled = options.pluralization?.enabled;
72
+ const explicitModel = options.pluralization?.model;
73
+ const hasExplicitModel = typeof explicitModel === "string" && explicitModel.trim().length > 0;
74
+ const pluralizationEnabled = typeof explicitEnabled === "boolean" ? explicitEnabled : hasExplicitModel;
75
+ let pluralizationModel = hasExplicitModel ? explicitModel.trim() : config.pluralization.model;
76
+ if (pluralizationEnabled && !hasExplicitModel) {
77
+ const inferredModel = inferPluralizationModel(config.models, config.sourceLocale, config.targetLocales);
78
+ if (!inferredModel) throw new Error("Pluralization is enabled but no \"pluralization.model\" is configured. Please set \"pluralization.model\" explicitly or use direct LLM models (not \"lingo.dev\") so the model can be inferred.");
79
+ pluralizationModel = inferredModel;
80
+ }
81
+ config.pluralization = {
82
+ ...config.pluralization,
83
+ enabled: pluralizationEnabled,
84
+ model: pluralizationModel
85
+ };
86
+ return config;
55
87
  }
56
88
 
57
89
  //#endregion
@@ -16,11 +16,27 @@ const DEFAULT_CONFIG = {
16
16
  },
17
17
  models: "lingo.dev",
18
18
  pluralization: {
19
- enabled: true,
19
+ enabled: false,
20
20
  model: "groq:llama-3.1-8b-instant"
21
21
  },
22
22
  buildMode: "translate"
23
23
  };
24
+ function getModelStringForLocales(models, sourceLocale, targetLocale) {
25
+ const modelKey = (targetLocale ? [
26
+ `${sourceLocale}:${targetLocale}`,
27
+ `*:${targetLocale}`,
28
+ `${sourceLocale}:*`,
29
+ "*:*"
30
+ ] : [`${sourceLocale}:*`, "*:*"]).find((key) => key in models);
31
+ if (modelKey) return models[modelKey];
32
+ const sortedKeys = Object.keys(models).sort();
33
+ if (sortedKeys.length === 0) return;
34
+ return models[sortedKeys[0]];
35
+ }
36
+ function inferPluralizationModel(models, sourceLocale, targetLocales) {
37
+ if (models === "lingo.dev") return;
38
+ return getModelStringForLocales(models, sourceLocale, targetLocales[0]);
39
+ }
24
40
  /**
25
41
  * Create a LoaderConfig with defaults applied
26
42
  *
@@ -29,7 +45,7 @@ const DEFAULT_CONFIG = {
29
45
  *
30
46
  */
31
47
  function createLingoConfig(options) {
32
- return {
48
+ const config = {
33
49
  ...DEFAULT_CONFIG,
34
50
  ...options,
35
51
  environment: options.environment ?? (process.env.NODE_ENV === "development" ? "development" : "production"),
@@ -51,6 +67,22 @@ function createLingoConfig(options) {
51
67
  }
52
68
  }
53
69
  };
70
+ const explicitEnabled = options.pluralization?.enabled;
71
+ const explicitModel = options.pluralization?.model;
72
+ const hasExplicitModel = typeof explicitModel === "string" && explicitModel.trim().length > 0;
73
+ const pluralizationEnabled = typeof explicitEnabled === "boolean" ? explicitEnabled : hasExplicitModel;
74
+ let pluralizationModel = hasExplicitModel ? explicitModel.trim() : config.pluralization.model;
75
+ if (pluralizationEnabled && !hasExplicitModel) {
76
+ const inferredModel = inferPluralizationModel(config.models, config.sourceLocale, config.targetLocales);
77
+ if (!inferredModel) throw new Error("Pluralization is enabled but no \"pluralization.model\" is configured. Please set \"pluralization.model\" explicitly or use direct LLM models (not \"lingo.dev\") so the model can be inferred.");
78
+ pluralizationModel = inferredModel;
79
+ }
80
+ config.pluralization = {
81
+ ...config.pluralization,
82
+ enabled: pluralizationEnabled,
83
+ model: pluralizationModel
84
+ };
85
+ return config;
54
86
  }
55
87
 
56
88
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"config-factory.mjs","names":[],"sources":["../../src/utils/config-factory.ts"],"sourcesContent":["/**\n * Config factory for creating LoaderConfig instances\n */\nimport type {\n LingoConfig,\n LingoConfigRequiredFields,\n LingoInternalFields,\n PartialLingoConfig,\n} from \"../types\";\n\n/**\n * Default configuration values\n */\nexport const DEFAULT_CONFIG = {\n sourceRoot: \"src\",\n lingoDir: \"lingo\",\n useDirective: false,\n dev: {\n translationServerStartPort: 60000,\n },\n localePersistence: {\n type: \"cookie\" as const,\n config: {\n name: \"locale\",\n maxAge: 31536000,\n },\n },\n models: \"lingo.dev\",\n pluralization: {\n enabled: true,\n model: \"groq:llama-3.1-8b-instant\",\n },\n buildMode: \"translate\",\n} satisfies Omit<\n LingoConfig,\n // Looks like we can use LingoInternalFields, but it's only a coincidence that the types match, we may want to provide default for internal fields\n LingoConfigRequiredFields | \"environment\" | \"cacheType\"\n>;\n\n/**\n * Create a LoaderConfig with defaults applied\n *\n * @param options - Partial config to override defaults\n * @returns Complete LoaderConfig with all defaults applied\n *\n */\nexport function createLingoConfig(\n options: PartialLingoConfig & Partial<Pick<LingoConfig, LingoInternalFields>>,\n): LingoConfig {\n return {\n ...DEFAULT_CONFIG,\n ...options,\n environment:\n options.environment ??\n (process.env.NODE_ENV === \"development\" ? \"development\" : \"production\"),\n cacheType: options.cacheType ?? \"local\",\n dev: {\n ...DEFAULT_CONFIG.dev,\n ...options.dev,\n },\n pluralization: {\n ...DEFAULT_CONFIG.pluralization,\n ...options.pluralization,\n },\n localePersistence: {\n ...DEFAULT_CONFIG.localePersistence,\n ...options.localePersistence,\n config: {\n ...DEFAULT_CONFIG.localePersistence.config,\n ...options.localePersistence?.config,\n },\n },\n };\n}\n"],"mappings":";;;;AAaA,MAAa,iBAAiB;CAC5B,YAAY;CACZ,UAAU;CACV,cAAc;CACd,KAAK,EACH,4BAA4B,KAC7B;CACD,mBAAmB;EACjB,MAAM;EACN,QAAQ;GACN,MAAM;GACN,QAAQ;GACT;EACF;CACD,QAAQ;CACR,eAAe;EACb,SAAS;EACT,OAAO;EACR;CACD,WAAW;CACZ;;;;;;;;AAaD,SAAgB,kBACd,SACa;AACb,QAAO;EACL,GAAG;EACH,GAAG;EACH,aACE,QAAQ,gBACP,QAAQ,IAAI,aAAa,gBAAgB,gBAAgB;EAC5D,WAAW,QAAQ,aAAa;EAChC,KAAK;GACH,GAAG,eAAe;GAClB,GAAG,QAAQ;GACZ;EACD,eAAe;GACb,GAAG,eAAe;GAClB,GAAG,QAAQ;GACZ;EACD,mBAAmB;GACjB,GAAG,eAAe;GAClB,GAAG,QAAQ;GACX,QAAQ;IACN,GAAG,eAAe,kBAAkB;IACpC,GAAG,QAAQ,mBAAmB;IAC/B;GACF;EACF"}
1
+ {"version":3,"file":"config-factory.mjs","names":["config: LingoConfig"],"sources":["../../src/utils/config-factory.ts"],"sourcesContent":["/**\n * Config factory for creating LoaderConfig instances\n */\nimport type {\n LingoConfig,\n LingoConfigRequiredFields,\n LingoInternalFields,\n PartialLingoConfig,\n} from \"../types\";\n\n/**\n * Default configuration values\n */\nexport const DEFAULT_CONFIG = {\n sourceRoot: \"src\",\n lingoDir: \"lingo\",\n useDirective: false,\n dev: {\n translationServerStartPort: 60000,\n },\n localePersistence: {\n type: \"cookie\" as const,\n config: {\n name: \"locale\",\n maxAge: 31536000,\n },\n },\n models: \"lingo.dev\",\n pluralization: {\n enabled: false,\n model: \"groq:llama-3.1-8b-instant\",\n },\n buildMode: \"translate\",\n} satisfies Omit<\n LingoConfig,\n // Looks like we can use LingoInternalFields, but it's only a coincidence that the types match, we may want to provide default for internal fields\n LingoConfigRequiredFields | \"environment\" | \"cacheType\"\n>;\n\nfunction getModelStringForLocales(\n models: Record<string, string>,\n sourceLocale: string,\n targetLocale: string | undefined,\n): string | undefined {\n const localeKeys = targetLocale\n ? [\n `${sourceLocale}:${targetLocale}`,\n `*:${targetLocale}`,\n `${sourceLocale}:*`,\n \"*:*\",\n ]\n : [`${sourceLocale}:*`, \"*:*\"];\n\n const modelKey = localeKeys.find((key) => key in models);\n if (modelKey) {\n return models[modelKey];\n }\n\n const sortedKeys = Object.keys(models).sort();\n if (sortedKeys.length === 0) {\n return undefined;\n }\n\n return models[sortedKeys[0]];\n}\n\nfunction inferPluralizationModel(\n models: \"lingo.dev\" | Record<string, string>,\n sourceLocale: string,\n targetLocales: string[],\n): string | undefined {\n if (models === \"lingo.dev\") {\n return undefined;\n }\n\n return getModelStringForLocales(\n models,\n sourceLocale,\n targetLocales[0],\n );\n}\n\n/**\n * Create a LoaderConfig with defaults applied\n *\n * @param options - Partial config to override defaults\n * @returns Complete LoaderConfig with all defaults applied\n *\n */\nexport function createLingoConfig(\n options: PartialLingoConfig & Partial<Pick<LingoConfig, LingoInternalFields>>,\n): LingoConfig {\n const config: LingoConfig = {\n ...DEFAULT_CONFIG,\n ...options,\n environment:\n options.environment ??\n (process.env.NODE_ENV === \"development\" ? \"development\" : \"production\"),\n cacheType: options.cacheType ?? \"local\",\n dev: {\n ...DEFAULT_CONFIG.dev,\n ...options.dev,\n },\n pluralization: {\n ...DEFAULT_CONFIG.pluralization,\n ...options.pluralization,\n },\n localePersistence: {\n ...DEFAULT_CONFIG.localePersistence,\n ...options.localePersistence,\n config: {\n ...DEFAULT_CONFIG.localePersistence.config,\n ...options.localePersistence?.config,\n },\n },\n };\n\n const explicitEnabled = options.pluralization?.enabled;\n const explicitModel = options.pluralization?.model;\n const hasExplicitModel =\n typeof explicitModel === \"string\" && explicitModel.trim().length > 0;\n const hasExplicitEnabled = typeof explicitEnabled === \"boolean\";\n\n const pluralizationEnabled = hasExplicitEnabled\n ? explicitEnabled\n : hasExplicitModel;\n\n let pluralizationModel = hasExplicitModel\n ? explicitModel!.trim()\n : config.pluralization.model;\n\n if (pluralizationEnabled && !hasExplicitModel) {\n const inferredModel = inferPluralizationModel(\n config.models,\n config.sourceLocale,\n config.targetLocales,\n );\n\n if (!inferredModel) {\n throw new Error(\n 'Pluralization is enabled but no \"pluralization.model\" is configured. Please set \"pluralization.model\" explicitly or use direct LLM models (not \"lingo.dev\") so the model can be inferred.',\n );\n }\n\n pluralizationModel = inferredModel;\n }\n\n config.pluralization = {\n ...config.pluralization,\n enabled: pluralizationEnabled,\n model: pluralizationModel,\n };\n\n return config;\n}\n"],"mappings":";;;;AAaA,MAAa,iBAAiB;CAC5B,YAAY;CACZ,UAAU;CACV,cAAc;CACd,KAAK,EACH,4BAA4B,KAC7B;CACD,mBAAmB;EACjB,MAAM;EACN,QAAQ;GACN,MAAM;GACN,QAAQ;GACT;EACF;CACD,QAAQ;CACR,eAAe;EACb,SAAS;EACT,OAAO;EACR;CACD,WAAW;CACZ;AAMD,SAAS,yBACP,QACA,cACA,cACoB;CAUpB,MAAM,YATa,eACf;EACE,GAAG,aAAa,GAAG;EACnB,KAAK;EACL,GAAG,aAAa;EAChB;EACD,GACD,CAAC,GAAG,aAAa,KAAK,MAAM,EAEJ,MAAM,QAAQ,OAAO,OAAO;AACxD,KAAI,SACF,QAAO,OAAO;CAGhB,MAAM,aAAa,OAAO,KAAK,OAAO,CAAC,MAAM;AAC7C,KAAI,WAAW,WAAW,EACxB;AAGF,QAAO,OAAO,WAAW;;AAG3B,SAAS,wBACP,QACA,cACA,eACoB;AACpB,KAAI,WAAW,YACb;AAGF,QAAO,yBACL,QACA,cACA,cAAc,GACf;;;;;;;;;AAUH,SAAgB,kBACd,SACa;CACb,MAAMA,SAAsB;EAC1B,GAAG;EACH,GAAG;EACH,aACE,QAAQ,gBACP,QAAQ,IAAI,aAAa,gBAAgB,gBAAgB;EAC5D,WAAW,QAAQ,aAAa;EAChC,KAAK;GACH,GAAG,eAAe;GAClB,GAAG,QAAQ;GACZ;EACD,eAAe;GACb,GAAG,eAAe;GAClB,GAAG,QAAQ;GACZ;EACD,mBAAmB;GACjB,GAAG,eAAe;GAClB,GAAG,QAAQ;GACX,QAAQ;IACN,GAAG,eAAe,kBAAkB;IACpC,GAAG,QAAQ,mBAAmB;IAC/B;GACF;EACF;CAED,MAAM,kBAAkB,QAAQ,eAAe;CAC/C,MAAM,gBAAgB,QAAQ,eAAe;CAC7C,MAAM,mBACJ,OAAO,kBAAkB,YAAY,cAAc,MAAM,CAAC,SAAS;CAGrE,MAAM,uBAFqB,OAAO,oBAAoB,YAGlD,kBACA;CAEJ,IAAI,qBAAqB,mBACrB,cAAe,MAAM,GACrB,OAAO,cAAc;AAEzB,KAAI,wBAAwB,CAAC,kBAAkB;EAC7C,MAAM,gBAAgB,wBACpB,OAAO,QACP,OAAO,cACP,OAAO,cACR;AAED,MAAI,CAAC,cACH,OAAM,IAAI,MACR,kMACD;AAGH,uBAAqB;;AAGvB,QAAO,gBAAgB;EACrB,GAAG,OAAO;EACV,SAAS;EACT,OAAO;EACR;AAED,QAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lingo.dev/compiler",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Lingo.dev Compiler",
5
5
  "private": false,
6
6
  "repository": {
@@ -162,7 +162,7 @@
162
162
  "posthog-node": "5.14.0",
163
163
  "proper-lockfile": "4.1.2",
164
164
  "ws": "8.18.3",
165
- "lingo.dev": "^0.125.4"
165
+ "lingo.dev": "^0.125.5"
166
166
  },
167
167
  "peerDependencies": {
168
168
  "next": "^15.0.0 || ^16.0.4",