@inlang/sdk 2.9.1 → 2.9.3

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/README.md CHANGED
@@ -17,24 +17,38 @@
17
17
 
18
18
  ## Introduction
19
19
 
20
- The inlang SDK is the official specification and parser for `.inlang` files.
20
+ The inlang SDK is the reference implementation for reading and writing `.inlang` project files.
21
21
 
22
- `.inlang` files are designed to become the open standard for i18n and enable interoperability between i18n solutions. Such solutions involve apps like [Fink](https://inlang.com/m/tdozzpar/app-inlang-finkLocalizationEditor), libraries like [Paraglide JS](https://inlang.com/m/gerre34r/library-inlang-paraglideJs), or plugins that extend inlang.
22
+ `.inlang` files are designed to become the open standard for localization data and make i18n tools work together. Build editors, CLIs, runtimes, agents, and plugins on the same shared project format instead of inventing another file structure.
23
+
24
+ An `.inlang` project is canonically a single binary file: a SQLite database with version control via [lix](https://lix.dev). Like `.sqlite` for relational data, `.inlang` packages localization data into one file that tools can share.
25
+
26
+ For Git repositories, the binary file can be unpacked into a directory of plain files so changes can be reviewed alongside code. The packed file is the canonical format; the unpacked directory is the Git-friendly representation.
27
+
28
+ `.inlang` is the canonical project format. Plugins import and export formats like JSON, ICU MessageFormat v1, i18next, and XLIFF for compatibility with existing translation files and runtimes. Version control via lix adds file-level history, merging, and change proposals to `.inlang` projects.
29
+
30
+ Messages, variants, and locale data live in the `.inlang` database. External translation files such as `messages/en.json` are compatibility files outside `project.inlang/`, connected through plugins.
23
31
 
24
32
  ### Core Features
25
33
 
26
- - 📁 **File-based**: Interoperability without cloud integrations or lock-in.
27
- - 🖊️ **CRUD API**: Query messages with SQL.
28
- - 🧩 **Plugin System**: Extend the capabilities with plugins.
29
- - 📦 **Import/Export**: Import and export messages in different file formats.
30
- - [<img src="https://raw.githubusercontent.com/opral/inlang/refs/heads/main/lix/assets/lix-icon.svg" width="20" height="12" alt="Lix Icon">**Change control**](https://lix.dev/): Collaboration, change proposals, reviews, and automation.
34
+ - 📁 **File-based**: A portable project file, no cloud integrations or lock-in.
35
+ - 🖊️ **CRUD API**: Read, write, and query messages with SQL.
36
+ - 🧩 **Plugin System**: Connect external translation files to the shared message structure.
37
+ - 📦 **Import/Export**: Import and export messages in formats like JSON, XLIFF, and i18next.
38
+ - [<img src="https://raw.githubusercontent.com/opral/inlang/refs/heads/main/lix/assets/lix-icon.svg" width="20" height="12" alt="Lix Icon">**Version control via lix**](https://lix.dev/): File-level history, merging, change proposals, reviews, and automation.
39
+
40
+ ### Core data model
41
+
42
+ - **Bundle** — one translatable unit across locales
43
+ - **Message** — locale-specific translation for a bundle
44
+ - **Variant** — text pattern plus selector matches
31
45
 
32
46
 
33
47
 
34
48
  ## Getting Started
35
49
 
36
50
  > [!Note]
37
- > Inlang files can be unpacked and [stored as directories](#unpacked-inlang-files-directories). The long-term goal is to have portable `.inlang` files. Hence, the documentation refers to files instead of directories.
51
+ > Inlang files are single binary files. They can be unpacked and [stored as directories](#unpacked-inlang-files-directories) when you want to review changes in Git. The packed file remains the canonical format.
38
52
 
39
53
  ### Installation
40
54
 
@@ -55,6 +69,16 @@ const project = await loadProjectInMemory({
55
69
  project.*
56
70
  ```
57
71
 
72
+ ### Loading an unpacked project from Git
73
+
74
+ ```ts
75
+ import { loadProjectFromDirectory } from "@inlang/sdk";
76
+
77
+ const project = await loadProjectFromDirectory({
78
+ path: "./project.inlang",
79
+ });
80
+ ```
81
+
58
82
  ### Next steps
59
83
 
60
84
  Go to the [API reference](#api-reference) to learn how to query messages, changes, and save the project.
@@ -66,6 +90,19 @@ The inlang SDK supports plugins to extend its functionality.
66
90
 
67
91
  Plugins can be used to import/export messages in different formats, add custom validation rules, and implement specialized workflows.
68
92
 
93
+ ## For coding agents and tool builders
94
+
95
+ If you are building i18n tooling or generating localization code, target the `.inlang` file format instead of inventing your own JSON schema.
96
+
97
+ Why target `.inlang` instead of JSON?
98
+
99
+ - One file gives every tool the same data model for bundles, messages, variants, locales, metadata, and settings.
100
+ - CRUD operations are available through the SDK instead of custom parsing and rewriting.
101
+ - Version control via [lix](https://lix.dev) keeps localization changes reviewable and mergeable.
102
+ - Plugins handle JSON, ICU, i18next, XLIFF, and other formats for compatibility.
103
+
104
+ Do not invent custom translation-file schemas, create a second localization config when `.inlang` already exists, edit generated/cache files in unpacked projects, or bypass `@inlang/sdk`.
105
+
69
106
  ### Available Plugins
70
107
 
71
108
  Find available plugins on https://inlang.com/c/plugins.
@@ -155,7 +192,7 @@ console.log(messages);
155
192
  > [!NOTE]
156
193
  > The inlang plugin for lix is work in progress. If you stumble on issues, please open an issue on the [GitHub](https://github.com/opral/inlang).
157
194
 
158
- The inlang file format uses lix for change control. The lix APIs are exposed via `project.lix.*`. Visit the [lix documentation](https://lix.dev/) for more information on how to query changes.
195
+ The inlang file format uses version control via lix. The lix APIs are exposed via `project.lix.*`. Visit the [lix documentation](https://lix.dev/) for more information on how to query changes.
159
196
 
160
197
  ```typescript
161
198
  const changes = await project.lix.db
@@ -210,11 +247,11 @@ await project.settings.set(settings)
210
247
  ### Unpacked inlang files (directories)
211
248
 
212
249
  > [!NOTE]
213
- > Unpacked inlang files are a workaround to store inlang files in git.
250
+ > Unpacked inlang files are the Git-friendly representation of packed `.inlang` files.
214
251
  >
215
- > Git can't handle binary files. **If you don't intend to store the inlang file in git, do not use unpacked inlang files.**
252
+ > Git can store binary files, but plain-file review and merge workflows work better with the unpacked directory. **If you don't intend to store the inlang file in git, use the packed binary file.**
216
253
  >
217
- > Unpacked inlang files are not portable. They depent on plugins that and do not persist [lix change control](https://lix.dev/) data.
254
+ > Unpacked inlang files are not portable. They depend on plugins and do not persist [version control via lix](https://lix.dev/) data.
218
255
 
219
256
  ```typescript
220
257
  import {
@@ -77,7 +77,7 @@ async function handleForeignKeyViolation(args) {
77
77
  .selectAll()
78
78
  // heuristic that getting the last bundle value is fine
79
79
  // and using created_at is fine too. if the change is undesired
80
- // , a user can revert it with lix change control
80
+ // , a user can revert it with version control via lix
81
81
  .orderBy("created_at", "desc")
82
82
  .where("type", "=", type)
83
83
  .where((eb) => eb.ref("value", "->>").key("id"), "=", id)
@@ -1 +1 @@
1
- {"version":3,"file":"applyChanges.js","sourceRoot":"/","sources":["lix-plugin/applyChanges.ts"],"names":[],"mappings":"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","sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\n\nimport {\n\tgetLeafChange,\n\ttype Change,\n\ttype LixPlugin,\n\ttype LixReadonly,\n} from \"@lix-js/sdk\";\nimport { contentFromDatabase, loadDatabaseInMemory } from \"sqlite-wasm-kysely\";\nimport { initDb } from \"../database/initDb.js\";\nimport type { Kysely } from \"kysely\";\nimport type { InlangDatabaseSchema } from \"../database/schema.js\";\n\nexport const applyChanges: NonNullable<LixPlugin[\"applyChanges\"]> = async ({\n\tlix,\n\tfile,\n\tchanges,\n}) => {\n\tif (file.path?.endsWith(\"db.sqlite\") === false) {\n\t\tthrow new Error(\n\t\t\t\"Unimplemented. Only the db.sqlite file can be handled for now.\"\n\t\t);\n\t}\n\n\t// todo make transactional\n\n\tconst sqlite = await loadDatabaseInMemory(file.data);\n\tconst db = initDb({ sqlite });\n\n\t// the award for the most inefficient deduplication goes to...\n\tconst leafChanges = [\n\t\t...new Set(\n\t\t\tawait Promise.all(\n\t\t\t\tchanges.map(async (change) => {\n\t\t\t\t\tconst leafChange = await getLeafChange({ change, lix });\n\t\t\t\t\t// enable string comparison to avoid duplicates\n\t\t\t\t\treturn JSON.stringify(leafChange);\n\t\t\t\t})\n\t\t\t)\n\t\t),\n\t].map((v) => JSON.parse(v));\n\n\t// changes need to be applied in order of foreign keys to avoid constraint violations\n\t// 1. bundles\n\t// 2. messages\n\t// 3. variants\n\tconst applyOrder: Record<string, number> = {\n\t\tbundle: 1,\n\t\tmessage: 2,\n\t\tvariant: 3,\n\t};\n\n\t// future optimization potential here but sorting in one go\n\tconst orderedLeafChanges = [...leafChanges].sort((a, b) => {\n\t\tconst orderA = applyOrder[a.type];\n\t\tconst orderB = applyOrder[b.type];\n\n\t\tif (orderA === undefined || orderB === undefined) {\n\t\t\tthrow new Error(\n\t\t\t\t`Received an unknown entity type: ${a.type} && ${\n\t\t\t\t\tb.type\n\t\t\t\t}. Expected one of: ${Object.keys(applyOrder)}`\n\t\t\t);\n\t\t}\n\n\t\treturn orderA - orderB;\n\t});\n\tfor (const leafChange of orderedLeafChanges) {\n\t\t// deletion\n\t\tif (leafChange.value === undefined) {\n\t\t\tawait db\n\t\t\t\t.deleteFrom(leafChange.type as \"bundle\" | \"message\" | \"variant\")\n\t\t\t\t.where(\"id\", \"=\", leafChange.meta?.id)\n\t\t\t\t.execute();\n\t\t\tcontinue;\n\t\t}\n\n\t\t// upsert the value\n\t\tconst value = leafChange.value as any;\n\n\t\ttry {\n\t\t\tawait db\n\t\t\t\t.insertInto(leafChange.type as \"bundle\" | \"message\" | \"variant\")\n\t\t\t\t.values(value)\n\t\t\t\t.onConflict((c) => c.column(\"id\").doUpdateSet(value))\n\t\t\t\t.execute();\n\t\t} catch (e) {\n\t\t\t// 787 = SQLITE_CONSTRAINT_FOREIGNKEY\n\t\t\tif (e instanceof Error && (e as any)?.resultCode === 787) {\n\t\t\t\tawait handleForeignKeyViolation({ change: leafChange, lix, db });\n\t\t\t} else {\n\t\t\t\tthrow e;\n\t\t\t}\n\t\t}\n\t}\n\treturn { fileData: contentFromDatabase(sqlite) };\n};\n\n/**\n * Handles foreign key violations e.g. a change\n * doesn't exist in the target database but is referenced\n * by an entity.\n */\nasync function handleForeignKeyViolation(args: {\n\tchange: Change;\n\tlix: LixReadonly;\n\tdb: Kysely<InlangDatabaseSchema>;\n}) {\n\tconst lastKnown = async (\n\t\ttype: \"bundle\" | \"message\" | \"variant\",\n\t\tid: string\n\t) =>\n\t\tawait args.lix.db\n\t\t\t.selectFrom(\"change\")\n\t\t\t.selectAll()\n\t\t\t// heuristic that getting the last bundle value is fine\n\t\t\t// and using created_at is fine too. if the change is undesired\n\t\t\t// , a user can revert it with lix change control\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.where(\"type\", \"=\", type)\n\t\t\t.where((eb) => eb.ref(\"value\", \"->>\").key(\"id\"), \"=\", id)\n\t\t\t.where(\"operation\", \"in\", [\"create\", \"update\"])\n\t\t\t// TODO shouldn't throw. The API needs to be able to\n\t\t\t// report issues back to the app without throwing and potentially failing\n\t\t\t// to apply 1000 changes because 1 change is invalid\n\t\t\t// same requirement as in inlang, see https://github.com/opral/inlang/issues/213\n\t\t\t.executeTakeFirstOrThrow();\n\n\tif (args.change.type === \"message\") {\n\t\tconst lastKnownBundle = await lastKnown(\n\t\t\t\"bundle\",\n\t\t\targs.change.value?.bundleId\n\t\t);\n\t\tawait args.db\n\t\t\t.insertInto(\"bundle\")\n\t\t\t.values(lastKnownBundle.value as any)\n\t\t\t.execute();\n\t} else if (args.change.type === \"variant\") {\n\t\tconst lastKnownMessage = await lastKnown(\n\t\t\t\"message\",\n\t\t\targs.change.value?.messageId\n\t\t);\n\t\t// getting the bundle too out of precaution\n\t\tconst lastKnownBundle = await lastKnown(\n\t\t\t\"bundle\",\n\t\t\tlastKnownMessage.value?.bundleId\n\t\t);\n\t\tawait args.db\n\t\t\t.insertInto(\"bundle\")\n\t\t\t.values(lastKnownBundle.value as any)\n\t\t\t// the bundle exists, so we can ignore the conflict\n\t\t\t.onConflict((c) => c.doNothing())\n\t\t\t.execute();\n\t\tawait args.db\n\t\t\t.insertInto(\"message\")\n\t\t\t.values(lastKnownMessage.value as any)\n\t\t\t.execute();\n\t\tawait args.db\n\t\t\t.insertInto(args.change.type as \"bundle\" | \"message\" | \"variant\")\n\t\t\t.values(args.change.value as any)\n\t\t\t.onConflict((c) => c.column(\"id\").doUpdateSet(args.change.value as any))\n\t\t\t.execute();\n\t}\n\t// re-execute applying the change\n\tawait args.db\n\t\t.insertInto(args.change.type as \"bundle\" | \"message\" | \"variant\")\n\t\t.values(args.change.value as any)\n\t\t.onConflict((c) => c.column(\"id\").doUpdateSet(args.change.value as any))\n\t\t.execute();\n}\n"]}
1
+ {"version":3,"file":"applyChanges.js","sourceRoot":"/","sources":["lix-plugin/applyChanges.ts"],"names":[],"mappings":"AAAA,6DAA6D;AAC7D,cAAc;AAEd,OAAO,EACN,aAAa,GAIb,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,EAAE,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAI/C,MAAM,CAAC,MAAM,YAAY,GAA2C,KAAK,EAAE,EAC1E,GAAG,EACH,IAAI,EACJ,OAAO,GACP,EAAE,EAAE;IACJ,IAAI,IAAI,CAAC,IAAI,EAAE,QAAQ,CAAC,WAAW,CAAC,KAAK,KAAK,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CACd,gEAAgE,CAChE,CAAC;IACH,CAAC;IAED,0BAA0B;IAE1B,MAAM,MAAM,GAAG,MAAM,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrD,MAAM,EAAE,GAAG,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;IAE9B,8DAA8D;IAC9D,MAAM,WAAW,GAAG;QACnB,GAAG,IAAI,GAAG,CACT,MAAM,OAAO,CAAC,GAAG,CAChB,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE;YAC5B,MAAM,UAAU,GAAG,MAAM,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;YACxD,+CAA+C;YAC/C,OAAO,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QACnC,CAAC,CAAC,CACF,CACD;KACD,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAE5B,qFAAqF;IACrF,aAAa;IACb,cAAc;IACd,cAAc;IACd,MAAM,UAAU,GAA2B;QAC1C,MAAM,EAAE,CAAC;QACT,OAAO,EAAE,CAAC;QACV,OAAO,EAAE,CAAC;KACV,CAAC;IAEF,2DAA2D;IAC3D,MAAM,kBAAkB,GAAG,CAAC,GAAG,WAAW,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACzD,MAAM,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAClC,MAAM,MAAM,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAElC,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YAClD,MAAM,IAAI,KAAK,CACd,oCAAoC,CAAC,CAAC,IAAI,OACzC,CAAC,CAAC,IACH,sBAAsB,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAC/C,CAAC;QACH,CAAC;QAED,OAAO,MAAM,GAAG,MAAM,CAAC;IACxB,CAAC,CAAC,CAAC;IACH,KAAK,MAAM,UAAU,IAAI,kBAAkB,EAAE,CAAC;QAC7C,WAAW;QACX,IAAI,UAAU,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YACpC,MAAM,EAAE;iBACN,UAAU,CAAC,UAAU,CAAC,IAAwC,CAAC;iBAC/D,KAAK,CAAC,IAAI,EAAE,GAAG,EAAE,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC;iBACrC,OAAO,EAAE,CAAC;YACZ,SAAS;QACV,CAAC;QAED,mBAAmB;QACnB,MAAM,KAAK,GAAG,UAAU,CAAC,KAAY,CAAC;QAEtC,IAAI,CAAC;YACJ,MAAM,EAAE;iBACN,UAAU,CAAC,UAAU,CAAC,IAAwC,CAAC;iBAC/D,MAAM,CAAC,KAAK,CAAC;iBACb,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;iBACpD,OAAO,EAAE,CAAC;QACb,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACZ,qCAAqC;YACrC,IAAI,CAAC,YAAY,KAAK,IAAK,CAAS,EAAE,UAAU,KAAK,GAAG,EAAE,CAAC;gBAC1D,MAAM,yBAAyB,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;YAClE,CAAC;iBAAM,CAAC;gBACP,MAAM,CAAC,CAAC;YACT,CAAC;QACF,CAAC;IACF,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,mBAAmB,CAAC,MAAM,CAAC,EAAE,CAAC;AAClD,CAAC,CAAC;AAEF;;;;GAIG;AACH,KAAK,UAAU,yBAAyB,CAAC,IAIxC;IACA,MAAM,SAAS,GAAG,KAAK,EACtB,IAAsC,EACtC,EAAU,EACT,EAAE,CACH,MAAM,IAAI,CAAC,GAAG,CAAC,EAAE;SACf,UAAU,CAAC,QAAQ,CAAC;SACpB,SAAS,EAAE;QACZ,uDAAuD;QACvD,+DAA+D;QAC/D,sDAAsD;SACrD,OAAO,CAAC,YAAY,EAAE,MAAM,CAAC;SAC7B,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC;SACxB,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC;SACxD,KAAK,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC/C,oDAAoD;QACpD,yEAAyE;QACzE,oDAAoD;QACpD,gFAAgF;SAC/E,uBAAuB,EAAE,CAAC;IAE7B,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QACpC,MAAM,eAAe,GAAG,MAAM,SAAS,CACtC,QAAQ,EACR,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAC3B,CAAC;QACF,MAAM,IAAI,CAAC,EAAE;aACX,UAAU,CAAC,QAAQ,CAAC;aACpB,MAAM,CAAC,eAAe,CAAC,KAAY,CAAC;aACpC,OAAO,EAAE,CAAC;IACb,CAAC;SAAM,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC3C,MAAM,gBAAgB,GAAG,MAAM,SAAS,CACvC,SAAS,EACT,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,SAAS,CAC5B,CAAC;QACF,2CAA2C;QAC3C,MAAM,eAAe,GAAG,MAAM,SAAS,CACtC,QAAQ,EACR,gBAAgB,CAAC,KAAK,EAAE,QAAQ,CAChC,CAAC;QACF,MAAM,IAAI,CAAC,EAAE;aACX,UAAU,CAAC,QAAQ,CAAC;aACpB,MAAM,CAAC,eAAe,CAAC,KAAY,CAAC;YACrC,mDAAmD;aAClD,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;aAChC,OAAO,EAAE,CAAC;QACZ,MAAM,IAAI,CAAC,EAAE;aACX,UAAU,CAAC,SAAS,CAAC;aACrB,MAAM,CAAC,gBAAgB,CAAC,KAAY,CAAC;aACrC,OAAO,EAAE,CAAC;QACZ,MAAM,IAAI,CAAC,EAAE;aACX,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,IAAwC,CAAC;aAChE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAY,CAAC;aAChC,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,KAAY,CAAC,CAAC;aACvE,OAAO,EAAE,CAAC;IACb,CAAC;IACD,iCAAiC;IACjC,MAAM,IAAI,CAAC,EAAE;SACX,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,IAAwC,CAAC;SAChE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,KAAY,CAAC;SAChC,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,KAAY,CAAC,CAAC;SACvE,OAAO,EAAE,CAAC;AACb,CAAC","sourcesContent":["// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n// @ts-nocheck\n\nimport {\n\tgetLeafChange,\n\ttype Change,\n\ttype LixPlugin,\n\ttype LixReadonly,\n} from \"@lix-js/sdk\";\nimport { contentFromDatabase, loadDatabaseInMemory } from \"sqlite-wasm-kysely\";\nimport { initDb } from \"../database/initDb.js\";\nimport type { Kysely } from \"kysely\";\nimport type { InlangDatabaseSchema } from \"../database/schema.js\";\n\nexport const applyChanges: NonNullable<LixPlugin[\"applyChanges\"]> = async ({\n\tlix,\n\tfile,\n\tchanges,\n}) => {\n\tif (file.path?.endsWith(\"db.sqlite\") === false) {\n\t\tthrow new Error(\n\t\t\t\"Unimplemented. Only the db.sqlite file can be handled for now.\"\n\t\t);\n\t}\n\n\t// todo make transactional\n\n\tconst sqlite = await loadDatabaseInMemory(file.data);\n\tconst db = initDb({ sqlite });\n\n\t// the award for the most inefficient deduplication goes to...\n\tconst leafChanges = [\n\t\t...new Set(\n\t\t\tawait Promise.all(\n\t\t\t\tchanges.map(async (change) => {\n\t\t\t\t\tconst leafChange = await getLeafChange({ change, lix });\n\t\t\t\t\t// enable string comparison to avoid duplicates\n\t\t\t\t\treturn JSON.stringify(leafChange);\n\t\t\t\t})\n\t\t\t)\n\t\t),\n\t].map((v) => JSON.parse(v));\n\n\t// changes need to be applied in order of foreign keys to avoid constraint violations\n\t// 1. bundles\n\t// 2. messages\n\t// 3. variants\n\tconst applyOrder: Record<string, number> = {\n\t\tbundle: 1,\n\t\tmessage: 2,\n\t\tvariant: 3,\n\t};\n\n\t// future optimization potential here but sorting in one go\n\tconst orderedLeafChanges = [...leafChanges].sort((a, b) => {\n\t\tconst orderA = applyOrder[a.type];\n\t\tconst orderB = applyOrder[b.type];\n\n\t\tif (orderA === undefined || orderB === undefined) {\n\t\t\tthrow new Error(\n\t\t\t\t`Received an unknown entity type: ${a.type} && ${\n\t\t\t\t\tb.type\n\t\t\t\t}. Expected one of: ${Object.keys(applyOrder)}`\n\t\t\t);\n\t\t}\n\n\t\treturn orderA - orderB;\n\t});\n\tfor (const leafChange of orderedLeafChanges) {\n\t\t// deletion\n\t\tif (leafChange.value === undefined) {\n\t\t\tawait db\n\t\t\t\t.deleteFrom(leafChange.type as \"bundle\" | \"message\" | \"variant\")\n\t\t\t\t.where(\"id\", \"=\", leafChange.meta?.id)\n\t\t\t\t.execute();\n\t\t\tcontinue;\n\t\t}\n\n\t\t// upsert the value\n\t\tconst value = leafChange.value as any;\n\n\t\ttry {\n\t\t\tawait db\n\t\t\t\t.insertInto(leafChange.type as \"bundle\" | \"message\" | \"variant\")\n\t\t\t\t.values(value)\n\t\t\t\t.onConflict((c) => c.column(\"id\").doUpdateSet(value))\n\t\t\t\t.execute();\n\t\t} catch (e) {\n\t\t\t// 787 = SQLITE_CONSTRAINT_FOREIGNKEY\n\t\t\tif (e instanceof Error && (e as any)?.resultCode === 787) {\n\t\t\t\tawait handleForeignKeyViolation({ change: leafChange, lix, db });\n\t\t\t} else {\n\t\t\t\tthrow e;\n\t\t\t}\n\t\t}\n\t}\n\treturn { fileData: contentFromDatabase(sqlite) };\n};\n\n/**\n * Handles foreign key violations e.g. a change\n * doesn't exist in the target database but is referenced\n * by an entity.\n */\nasync function handleForeignKeyViolation(args: {\n\tchange: Change;\n\tlix: LixReadonly;\n\tdb: Kysely<InlangDatabaseSchema>;\n}) {\n\tconst lastKnown = async (\n\t\ttype: \"bundle\" | \"message\" | \"variant\",\n\t\tid: string\n\t) =>\n\t\tawait args.lix.db\n\t\t\t.selectFrom(\"change\")\n\t\t\t.selectAll()\n\t\t\t// heuristic that getting the last bundle value is fine\n\t\t\t// and using created_at is fine too. if the change is undesired\n\t\t\t// , a user can revert it with version control via lix\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.where(\"type\", \"=\", type)\n\t\t\t.where((eb) => eb.ref(\"value\", \"->>\").key(\"id\"), \"=\", id)\n\t\t\t.where(\"operation\", \"in\", [\"create\", \"update\"])\n\t\t\t// TODO shouldn't throw. The API needs to be able to\n\t\t\t// report issues back to the app without throwing and potentially failing\n\t\t\t// to apply 1000 changes because 1 change is invalid\n\t\t\t// same requirement as in inlang, see https://github.com/opral/inlang/issues/213\n\t\t\t.executeTakeFirstOrThrow();\n\n\tif (args.change.type === \"message\") {\n\t\tconst lastKnownBundle = await lastKnown(\n\t\t\t\"bundle\",\n\t\t\targs.change.value?.bundleId\n\t\t);\n\t\tawait args.db\n\t\t\t.insertInto(\"bundle\")\n\t\t\t.values(lastKnownBundle.value as any)\n\t\t\t.execute();\n\t} else if (args.change.type === \"variant\") {\n\t\tconst lastKnownMessage = await lastKnown(\n\t\t\t\"message\",\n\t\t\targs.change.value?.messageId\n\t\t);\n\t\t// getting the bundle too out of precaution\n\t\tconst lastKnownBundle = await lastKnown(\n\t\t\t\"bundle\",\n\t\t\tlastKnownMessage.value?.bundleId\n\t\t);\n\t\tawait args.db\n\t\t\t.insertInto(\"bundle\")\n\t\t\t.values(lastKnownBundle.value as any)\n\t\t\t// the bundle exists, so we can ignore the conflict\n\t\t\t.onConflict((c) => c.doNothing())\n\t\t\t.execute();\n\t\tawait args.db\n\t\t\t.insertInto(\"message\")\n\t\t\t.values(lastKnownMessage.value as any)\n\t\t\t.execute();\n\t\tawait args.db\n\t\t\t.insertInto(args.change.type as \"bundle\" | \"message\" | \"variant\")\n\t\t\t.values(args.change.value as any)\n\t\t\t.onConflict((c) => c.column(\"id\").doUpdateSet(args.change.value as any))\n\t\t\t.execute();\n\t}\n\t// re-execute applying the change\n\tawait args.db\n\t\t.insertInto(args.change.type as \"bundle\" | \"message\" | \"variant\")\n\t\t.values(args.change.value as any)\n\t\t.onConflict((c) => c.column(\"id\").doUpdateSet(args.change.value as any))\n\t\t.execute();\n}\n"]}
@@ -4,5 +4,5 @@
4
4
  * The goal is to help coding agents understand what this folder is
5
5
  * and how to use the inlang SDK to build tooling.
6
6
  */
7
- export declare const README_CONTENT = "\n## What is this folder?\n\nThis is an [unpacked (git-friendly)](https://inlang.com/docs/unpacked-project) inlang project.\n\n## At a glance\n\nPurpose:\n- This folder stores inlang project configuration and plugin cache data.\n- Translation files live outside this folder and are referenced from `settings.json`.\n\nSafe to edit:\n- `settings.json`\n\nDo not edit:\n- `cache/`\n- `.gitignore`\n\nKey files:\n- `settings.json` \u2014 locales, plugins, file patterns (source of truth)\n- `cache/` \u2014 plugin caches (safe to delete)\n- `.gitignore` \u2014 generated\n\n```\n*.inlang/\n\u251C\u2500\u2500 settings.json # Locales, plugins, and file patterns (source of truth)\n\u251C\u2500\u2500 cache/ # Plugin caches (gitignored)\n\u2514\u2500\u2500 .gitignore # Ignores everything except settings.json\n```\n\nTranslation files (like `messages/en.json`) live **outside** this folder and are referenced via plugins in `settings.json`.\n\n## What is inlang?\n\n[Inlang](https://inlang.com) is an open file format for building custom localization (i18n) tooling. It provides:\n\n- **CRUD API** \u2014 Read and write translations programmatically via SQL\n- **Plugin system** \u2014 Import/export any format (JSON, XLIFF, etc.)\n- **Version control** \u2014 Built-in version control via [lix](https://lix.dev)\n\n```\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 i18n lib \u2502 \u2502Translation\u2502 \u2502 CI/CD \u2502\n\u2502 \u2502 \u2502 Tool \u2502 \u2502 Automation \u2502\n\u2514\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502 \u2502 \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u25BC \u25BC \u25BC\n \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 *.inlang file \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n## Quick start\n\n```bash\nnpm install @inlang/sdk\n```\n\n```ts\nimport { loadProjectFromDirectory, saveProjectToDirectory } from \"@inlang/sdk\";\n\nconst project = await loadProjectFromDirectory({ path: \"./project.inlang\" });\n// Query messages with SQLite + [Kysely](https://kysely.dev/) under the hood.\nconst messages = await project.db.selectFrom(\"message\").selectAll().execute();\n\n// Use project.db to update messages.\nawait saveProjectToDirectory({ path: \"./project.inlang\", project });\n```\n\n## Ideas for custom tooling\n\n- Translation health dashboard (missing/empty/stale messages)\n- Locale coverage report in CI\n- Auto-PR for new keys with placeholders\n- Migration tool between file formats via plugins\n- Glossary/term consistency checker\n\n## Data model ([docs](https://inlang.com/docs/data-model))\n\n```\nbundle (a concept, e.g., \"welcome_header\")\n \u2514\u2500\u2500 message (per locale, e.g., \"en\", \"de\")\n \u2514\u2500\u2500 variant (plural forms, gender, etc.)\n```\n\n- **bundle**: Groups messages by ID (e.g., `welcome_header`)\n- **message**: A translation for a specific locale\n- **variant**: Handles pluralization/selectors (most messages have one variant)\n\n## Common tasks\n\n- List bundles: `project.db.selectFrom(\"bundle\").selectAll().execute()`\n- List messages for locale: `project.db.selectFrom(\"message\").where(\"locale\", \"=\", \"en\").selectAll().execute()`\n- Find missing translations: compare message counts across locales\n- Update a message: `project.db.updateTable(\"message\").set({ ... }).where(\"id\", \"=\", \"...\").execute()`\n\n## Links\n\n- [SDK documentation](https://inlang.com/docs)\n- [inlang.com](https://inlang.com)\n- [List of plugins](https://inlang.com/c/plugins)\n- [List of tools](https://inlang.com/c/tools)\n";
7
+ export declare const README_CONTENT = "\n## What is this folder?\n\nThis is an [unpacked (git-friendly)](https://inlang.com/docs/unpacked-project) inlang project.\n\n## At a glance\n\nPurpose:\n- This folder is the Git-friendly representation of an `.inlang` project.\n- The canonical `.inlang` format is a single binary file; this directory is the unpacked version for Git.\n- This folder stores project configuration and plugin cache data.\n- Translation files live outside this folder and are referenced from `settings.json`.\n\nSafe to edit:\n- `settings.json`\n\nDo not edit:\n- `cache/`\n- `.gitignore`\n\nKey files:\n- `settings.json` \u2014 locales, plugins, file patterns\n- `cache/` \u2014 plugin caches (safe to delete)\n- `.gitignore` \u2014 generated\n- `README.md` \u2014 generated, explains this folder\n- `.meta.json` \u2014 generated SDK metadata\n\n```\n*.inlang/\n\u251C\u2500\u2500 settings.json # Locales, plugins, and file patterns; kept in Git\n\u251C\u2500\u2500 .gitignore # Ignores everything except settings.json\n\u251C\u2500\u2500 README.md # Generated, explains this folder\n\u251C\u2500\u2500 .meta.json # Generated SDK metadata\n\u2514\u2500\u2500 cache/ # Plugin caches, usually cache/plugins/\n```\n\nTranslation files (like `messages/en.json`) live **outside** this folder and are referenced via plugins in `settings.json`.\n\n## What is inlang?\n\n[Inlang](https://inlang.com) is an open project file format for localization. An `.inlang` project is canonically a single binary file: a SQLite database with version control via [lix](https://lix.dev). Like `.sqlite` for relational data, `.inlang` packages localization data into one file that tools can share.\n\nFor Git repositories, that binary file can be unpacked into a directory of plain files. The packed file is the canonical format; this directory is the Git-friendly representation.\n\nUse inlang when multiple tools, teams, automations, or agents need to use the same localization data. The `@inlang/sdk` is the reference implementation for reading and writing `.inlang` projects.\n\n`.inlang` is the canonical project format. Plugins import and export external translation files for compatibility with existing runtimes and workflows. Messages, variants, and locale data live in the `.inlang` database; translation files such as `messages/en.json` live outside this folder and are connected through plugins. Version control via lix adds file-level history, merging, and change proposals to `.inlang` projects.\n\nIt provides:\n\n- **CRUD API** \u2014 Read and write translations programmatically via SQL\n- **Plugin system** \u2014 Import/export external translation files (JSON, XLIFF, etc.)\n- **Version control** \u2014 Version control via [lix](https://lix.dev)\n\nCore data model:\n\n- **Bundle** \u2014 one translatable unit across locales\n- **Message** \u2014 locale-specific translation for a bundle\n- **Variant** \u2014 text pattern plus selector matches\n\n```\n\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 i18n lib \u2502 \u2502Translation\u2502 \u2502 CI/CD \u2502\n\u2502 \u2502 \u2502 Tool \u2502 \u2502 Automation \u2502\n\u2514\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u2502 \u2502 \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u2502 \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n \u25BC \u25BC \u25BC\n \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 *.inlang file \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\n\n## Quick start\n\n```bash\nnpm install @inlang/sdk\n```\n\n```ts\nimport { loadProjectFromDirectory, saveProjectToDirectory } from \"@inlang/sdk\";\nimport fs from \"node:fs\";\n\nconst project = await loadProjectFromDirectory({\n path: \"./project.inlang\",\n fs,\n});\n// Query messages with the SDK.\nconst messages = await project.db.selectFrom(\"message\").selectAll().execute();\n\n// Use project.db to update messages.\nawait saveProjectToDirectory({\n path: \"./project.inlang\",\n fs,\n project,\n});\n\nawait project.close();\n```\n\n`loadProjectFromDirectory()` and `saveProjectToDirectory()` both accept `node:fs`. `saveProjectToDirectory()` writes translation files through import/export plugins. If no exporter plugin is configured, save the canonical packed file with `project.toBlob()` instead.\n\n## For coding agents and tool builders\n\nTarget `.inlang` instead of inventing a custom JSON schema.\n\nWhy target `.inlang`?\n\n- One file gives every tool the same data model for bundles, messages, variants, locales, metadata, and settings.\n- CRUD operations are available through the SDK instead of custom parsing and rewriting.\n- Version control via [lix](https://lix.dev) keeps localization changes reviewable and mergeable.\n- Plugins handle JSON, ICU, i18next, XLIFF, and other formats for compatibility.\n\nDo not:\n\n- Invent a custom `locales/en.json` schema as the project format.\n- Create a new localization config when a `.inlang` project already exists.\n- Edit generated or cached files in this directory.\n- Bypass `@inlang/sdk` when reading or writing `.inlang`.\n\n## Ideas for custom tooling\n\n- Translation health dashboard (missing/empty/stale messages)\n- Locale coverage report in CI\n- Auto-PR for new keys with placeholders\n- Migration tool between file formats via plugins\n- Glossary/term consistency checker\n\n## Data model ([docs](https://inlang.com/docs/data-model))\n\n```\nbundle (a concept, e.g., \"welcome_header\")\n \u2514\u2500\u2500 message (per locale, e.g., \"en\", \"de\")\n \u2514\u2500\u2500 variant (plural forms, gender, etc.)\n```\n\n- **bundle**: Groups messages by ID (e.g., `welcome_header`)\n- **message**: A translation for a specific locale\n- **variant**: Handles pluralization/selectors (most messages have one variant)\n\n## Common tasks\n\n- List bundles: `project.db.selectFrom(\"bundle\").selectAll().execute()`\n- List messages for locale: `project.db.selectFrom(\"message\").where(\"locale\", \"=\", \"en\").selectAll().execute()`\n- Find missing translations: compare message counts across locales\n- Update a message: `project.db.updateTable(\"message\").set({ ... }).where(\"id\", \"=\", \"...\").execute()`\n\n## Links\n\n- [SDK documentation](https://inlang.com/docs)\n- [inlang.com](https://inlang.com)\n- [List of plugins](https://inlang.com/c/plugins)\n- [List of tools](https://inlang.com/c/tools)\n";
8
8
  //# sourceMappingURL=README_CONTENT.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"README_CONTENT.d.ts","sourceRoot":"/","sources":["project/README_CONTENT.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,cAAc,wiJAuG1B,CAAC"}
1
+ {"version":3,"file":"README_CONTENT.d.ts","sourceRoot":"/","sources":["project/README_CONTENT.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,cAAc,0sOAyJ1B,CAAC"}
@@ -12,7 +12,9 @@ This is an [unpacked (git-friendly)](https://inlang.com/docs/unpacked-project) i
12
12
  ## At a glance
13
13
 
14
14
  Purpose:
15
- - This folder stores inlang project configuration and plugin cache data.
15
+ - This folder is the Git-friendly representation of an \`.inlang\` project.
16
+ - The canonical \`.inlang\` format is a single binary file; this directory is the unpacked version for Git.
17
+ - This folder stores project configuration and plugin cache data.
16
18
  - Translation files live outside this folder and are referenced from \`settings.json\`.
17
19
 
18
20
  Safe to edit:
@@ -23,26 +25,44 @@ Do not edit:
23
25
  - \`.gitignore\`
24
26
 
25
27
  Key files:
26
- - \`settings.json\` — locales, plugins, file patterns (source of truth)
28
+ - \`settings.json\` — locales, plugins, file patterns
27
29
  - \`cache/\` — plugin caches (safe to delete)
28
30
  - \`.gitignore\` — generated
31
+ - \`README.md\` — generated, explains this folder
32
+ - \`.meta.json\` — generated SDK metadata
29
33
 
30
34
  \`\`\`
31
35
  *.inlang/
32
- ├── settings.json # Locales, plugins, and file patterns (source of truth)
33
- ├── cache/ # Plugin caches (gitignored)
34
- └── .gitignore # Ignores everything except settings.json
36
+ ├── settings.json # Locales, plugins, and file patterns; kept in Git
37
+ ├── .gitignore # Ignores everything except settings.json
38
+ ├── README.md # Generated, explains this folder
39
+ ├── .meta.json # Generated SDK metadata
40
+ └── cache/ # Plugin caches, usually cache/plugins/
35
41
  \`\`\`
36
42
 
37
43
  Translation files (like \`messages/en.json\`) live **outside** this folder and are referenced via plugins in \`settings.json\`.
38
44
 
39
45
  ## What is inlang?
40
46
 
41
- [Inlang](https://inlang.com) is an open file format for building custom localization (i18n) tooling. It provides:
47
+ [Inlang](https://inlang.com) is an open project file format for localization. An \`.inlang\` project is canonically a single binary file: a SQLite database with version control via [lix](https://lix.dev). Like \`.sqlite\` for relational data, \`.inlang\` packages localization data into one file that tools can share.
48
+
49
+ For Git repositories, that binary file can be unpacked into a directory of plain files. The packed file is the canonical format; this directory is the Git-friendly representation.
50
+
51
+ Use inlang when multiple tools, teams, automations, or agents need to use the same localization data. The \`@inlang/sdk\` is the reference implementation for reading and writing \`.inlang\` projects.
52
+
53
+ \`.inlang\` is the canonical project format. Plugins import and export external translation files for compatibility with existing runtimes and workflows. Messages, variants, and locale data live in the \`.inlang\` database; translation files such as \`messages/en.json\` live outside this folder and are connected through plugins. Version control via lix adds file-level history, merging, and change proposals to \`.inlang\` projects.
54
+
55
+ It provides:
42
56
 
43
57
  - **CRUD API** — Read and write translations programmatically via SQL
44
- - **Plugin system** — Import/export any format (JSON, XLIFF, etc.)
45
- - **Version control** — Built-in version control via [lix](https://lix.dev)
58
+ - **Plugin system** — Import/export external translation files (JSON, XLIFF, etc.)
59
+ - **Version control** — Version control via [lix](https://lix.dev)
60
+
61
+ Core data model:
62
+
63
+ - **Bundle** — one translatable unit across locales
64
+ - **Message** — locale-specific translation for a bundle
65
+ - **Variant** — text pattern plus selector matches
46
66
 
47
67
  \`\`\`
48
68
  ┌──────────┐ ┌───────────┐ ┌────────────┐
@@ -65,15 +85,45 @@ npm install @inlang/sdk
65
85
 
66
86
  \`\`\`ts
67
87
  import { loadProjectFromDirectory, saveProjectToDirectory } from "@inlang/sdk";
88
+ import fs from "node:fs";
68
89
 
69
- const project = await loadProjectFromDirectory({ path: "./project.inlang" });
70
- // Query messages with SQLite + [Kysely](https://kysely.dev/) under the hood.
90
+ const project = await loadProjectFromDirectory({
91
+ path: "./project.inlang",
92
+ fs,
93
+ });
94
+ // Query messages with the SDK.
71
95
  const messages = await project.db.selectFrom("message").selectAll().execute();
72
96
 
73
97
  // Use project.db to update messages.
74
- await saveProjectToDirectory({ path: "./project.inlang", project });
98
+ await saveProjectToDirectory({
99
+ path: "./project.inlang",
100
+ fs,
101
+ project,
102
+ });
103
+
104
+ await project.close();
75
105
  \`\`\`
76
106
 
107
+ \`loadProjectFromDirectory()\` and \`saveProjectToDirectory()\` both accept \`node:fs\`. \`saveProjectToDirectory()\` writes translation files through import/export plugins. If no exporter plugin is configured, save the canonical packed file with \`project.toBlob()\` instead.
108
+
109
+ ## For coding agents and tool builders
110
+
111
+ Target \`.inlang\` instead of inventing a custom JSON schema.
112
+
113
+ Why target \`.inlang\`?
114
+
115
+ - One file gives every tool the same data model for bundles, messages, variants, locales, metadata, and settings.
116
+ - CRUD operations are available through the SDK instead of custom parsing and rewriting.
117
+ - Version control via [lix](https://lix.dev) keeps localization changes reviewable and mergeable.
118
+ - Plugins handle JSON, ICU, i18next, XLIFF, and other formats for compatibility.
119
+
120
+ Do not:
121
+
122
+ - Invent a custom \`locales/en.json\` schema as the project format.
123
+ - Create a new localization config when a \`.inlang\` project already exists.
124
+ - Edit generated or cached files in this directory.
125
+ - Bypass \`@inlang/sdk\` when reading or writing \`.inlang\`.
126
+
77
127
  ## Ideas for custom tooling
78
128
 
79
129
  - Translation health dashboard (missing/empty/stale messages)
@@ -1 +1 @@
1
- {"version":3,"file":"README_CONTENT.js","sourceRoot":"/","sources":["project/README_CONTENT.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuG7B,CAAC","sourcesContent":["/**\n * README content that gets written to every .inlang project folder.\n *\n * The goal is to help coding agents understand what this folder is\n * and how to use the inlang SDK to build tooling.\n */\nexport const README_CONTENT = `\n## What is this folder?\n\nThis is an [unpacked (git-friendly)](https://inlang.com/docs/unpacked-project) inlang project.\n\n## At a glance\n\nPurpose:\n- This folder stores inlang project configuration and plugin cache data.\n- Translation files live outside this folder and are referenced from \\`settings.json\\`.\n\nSafe to edit:\n- \\`settings.json\\`\n\nDo not edit:\n- \\`cache/\\`\n- \\`.gitignore\\`\n\nKey files:\n- \\`settings.json\\` — locales, plugins, file patterns (source of truth)\n- \\`cache/\\` — plugin caches (safe to delete)\n- \\`.gitignore\\` — generated\n\n\\`\\`\\`\n*.inlang/\n├── settings.json # Locales, plugins, and file patterns (source of truth)\n├── cache/ # Plugin caches (gitignored)\n└── .gitignore # Ignores everything except settings.json\n\\`\\`\\`\n\nTranslation files (like \\`messages/en.json\\`) live **outside** this folder and are referenced via plugins in \\`settings.json\\`.\n\n## What is inlang?\n\n[Inlang](https://inlang.com) is an open file format for building custom localization (i18n) tooling. It provides:\n\n- **CRUD API** — Read and write translations programmatically via SQL\n- **Plugin system** — Import/export any format (JSON, XLIFF, etc.)\n- **Version control** — Built-in version control via [lix](https://lix.dev)\n\n\\`\\`\\`\n┌──────────┐ ┌───────────┐ ┌────────────┐\n│ i18n lib │ │Translation│ │ CI/CD │\n│ │ │ Tool │ │ Automation │\n└────┬─────┘ └─────┬─────┘ └─────┬──────┘\n │ │ │\n └─────────┐ │ ┌──────────┘\n ▼ ▼ ▼\n ┌──────────────────────────────────┐\n │ *.inlang file │\n └──────────────────────────────────┘\n\\`\\`\\`\n\n## Quick start\n\n\\`\\`\\`bash\nnpm install @inlang/sdk\n\\`\\`\\`\n\n\\`\\`\\`ts\nimport { loadProjectFromDirectory, saveProjectToDirectory } from \"@inlang/sdk\";\n\nconst project = await loadProjectFromDirectory({ path: \"./project.inlang\" });\n// Query messages with SQLite + [Kysely](https://kysely.dev/) under the hood.\nconst messages = await project.db.selectFrom(\"message\").selectAll().execute();\n\n// Use project.db to update messages.\nawait saveProjectToDirectory({ path: \"./project.inlang\", project });\n\\`\\`\\`\n\n## Ideas for custom tooling\n\n- Translation health dashboard (missing/empty/stale messages)\n- Locale coverage report in CI\n- Auto-PR for new keys with placeholders\n- Migration tool between file formats via plugins\n- Glossary/term consistency checker\n\n## Data model ([docs](https://inlang.com/docs/data-model))\n\n\\`\\`\\`\nbundle (a concept, e.g., \"welcome_header\")\n └── message (per locale, e.g., \"en\", \"de\")\n └── variant (plural forms, gender, etc.)\n\\`\\`\\`\n\n- **bundle**: Groups messages by ID (e.g., \\`welcome_header\\`)\n- **message**: A translation for a specific locale\n- **variant**: Handles pluralization/selectors (most messages have one variant)\n\n## Common tasks\n\n- List bundles: \\`project.db.selectFrom(\"bundle\").selectAll().execute()\\`\n- List messages for locale: \\`project.db.selectFrom(\"message\").where(\"locale\", \"=\", \"en\").selectAll().execute()\\`\n- Find missing translations: compare message counts across locales\n- Update a message: \\`project.db.updateTable(\"message\").set({ ... }).where(\"id\", \"=\", \"...\").execute()\\`\n\n## Links\n\n- [SDK documentation](https://inlang.com/docs)\n- [inlang.com](https://inlang.com)\n- [List of plugins](https://inlang.com/c/plugins)\n- [List of tools](https://inlang.com/c/tools)\n`;\n"]}
1
+ {"version":3,"file":"README_CONTENT.js","sourceRoot":"/","sources":["project/README_CONTENT.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyJ7B,CAAC","sourcesContent":["/**\n * README content that gets written to every .inlang project folder.\n *\n * The goal is to help coding agents understand what this folder is\n * and how to use the inlang SDK to build tooling.\n */\nexport const README_CONTENT = `\n## What is this folder?\n\nThis is an [unpacked (git-friendly)](https://inlang.com/docs/unpacked-project) inlang project.\n\n## At a glance\n\nPurpose:\n- This folder is the Git-friendly representation of an \\`.inlang\\` project.\n- The canonical \\`.inlang\\` format is a single binary file; this directory is the unpacked version for Git.\n- This folder stores project configuration and plugin cache data.\n- Translation files live outside this folder and are referenced from \\`settings.json\\`.\n\nSafe to edit:\n- \\`settings.json\\`\n\nDo not edit:\n- \\`cache/\\`\n- \\`.gitignore\\`\n\nKey files:\n- \\`settings.json\\` — locales, plugins, file patterns\n- \\`cache/\\` — plugin caches (safe to delete)\n- \\`.gitignore\\` — generated\n- \\`README.md\\` — generated, explains this folder\n- \\`.meta.json\\` — generated SDK metadata\n\n\\`\\`\\`\n*.inlang/\n├── settings.json # Locales, plugins, and file patterns; kept in Git\n├── .gitignore # Ignores everything except settings.json\n├── README.md # Generated, explains this folder\n├── .meta.json # Generated SDK metadata\n└── cache/ # Plugin caches, usually cache/plugins/\n\\`\\`\\`\n\nTranslation files (like \\`messages/en.json\\`) live **outside** this folder and are referenced via plugins in \\`settings.json\\`.\n\n## What is inlang?\n\n[Inlang](https://inlang.com) is an open project file format for localization. An \\`.inlang\\` project is canonically a single binary file: a SQLite database with version control via [lix](https://lix.dev). Like \\`.sqlite\\` for relational data, \\`.inlang\\` packages localization data into one file that tools can share.\n\nFor Git repositories, that binary file can be unpacked into a directory of plain files. The packed file is the canonical format; this directory is the Git-friendly representation.\n\nUse inlang when multiple tools, teams, automations, or agents need to use the same localization data. The \\`@inlang/sdk\\` is the reference implementation for reading and writing \\`.inlang\\` projects.\n\n\\`.inlang\\` is the canonical project format. Plugins import and export external translation files for compatibility with existing runtimes and workflows. Messages, variants, and locale data live in the \\`.inlang\\` database; translation files such as \\`messages/en.json\\` live outside this folder and are connected through plugins. Version control via lix adds file-level history, merging, and change proposals to \\`.inlang\\` projects.\n\nIt provides:\n\n- **CRUD API** — Read and write translations programmatically via SQL\n- **Plugin system** — Import/export external translation files (JSON, XLIFF, etc.)\n- **Version control** — Version control via [lix](https://lix.dev)\n\nCore data model:\n\n- **Bundle** — one translatable unit across locales\n- **Message** — locale-specific translation for a bundle\n- **Variant** — text pattern plus selector matches\n\n\\`\\`\\`\n┌──────────┐ ┌───────────┐ ┌────────────┐\n│ i18n lib │ │Translation│ │ CI/CD │\n│ │ │ Tool │ │ Automation │\n└────┬─────┘ └─────┬─────┘ └─────┬──────┘\n │ │ │\n └─────────┐ │ ┌──────────┘\n ▼ ▼ ▼\n ┌──────────────────────────────────┐\n │ *.inlang file │\n └──────────────────────────────────┘\n\\`\\`\\`\n\n## Quick start\n\n\\`\\`\\`bash\nnpm install @inlang/sdk\n\\`\\`\\`\n\n\\`\\`\\`ts\nimport { loadProjectFromDirectory, saveProjectToDirectory } from \"@inlang/sdk\";\nimport fs from \"node:fs\";\n\nconst project = await loadProjectFromDirectory({\n path: \"./project.inlang\",\n fs,\n});\n// Query messages with the SDK.\nconst messages = await project.db.selectFrom(\"message\").selectAll().execute();\n\n// Use project.db to update messages.\nawait saveProjectToDirectory({\n path: \"./project.inlang\",\n fs,\n project,\n});\n\nawait project.close();\n\\`\\`\\`\n\n\\`loadProjectFromDirectory()\\` and \\`saveProjectToDirectory()\\` both accept \\`node:fs\\`. \\`saveProjectToDirectory()\\` writes translation files through import/export plugins. If no exporter plugin is configured, save the canonical packed file with \\`project.toBlob()\\` instead.\n\n## For coding agents and tool builders\n\nTarget \\`.inlang\\` instead of inventing a custom JSON schema.\n\nWhy target \\`.inlang\\`?\n\n- One file gives every tool the same data model for bundles, messages, variants, locales, metadata, and settings.\n- CRUD operations are available through the SDK instead of custom parsing and rewriting.\n- Version control via [lix](https://lix.dev) keeps localization changes reviewable and mergeable.\n- Plugins handle JSON, ICU, i18next, XLIFF, and other formats for compatibility.\n\nDo not:\n\n- Invent a custom \\`locales/en.json\\` schema as the project format.\n- Create a new localization config when a \\`.inlang\\` project already exists.\n- Edit generated or cached files in this directory.\n- Bypass \\`@inlang/sdk\\` when reading or writing \\`.inlang\\`.\n\n## Ideas for custom tooling\n\n- Translation health dashboard (missing/empty/stale messages)\n- Locale coverage report in CI\n- Auto-PR for new keys with placeholders\n- Migration tool between file formats via plugins\n- Glossary/term consistency checker\n\n## Data model ([docs](https://inlang.com/docs/data-model))\n\n\\`\\`\\`\nbundle (a concept, e.g., \"welcome_header\")\n └── message (per locale, e.g., \"en\", \"de\")\n └── variant (plural forms, gender, etc.)\n\\`\\`\\`\n\n- **bundle**: Groups messages by ID (e.g., \\`welcome_header\\`)\n- **message**: A translation for a specific locale\n- **variant**: Handles pluralization/selectors (most messages have one variant)\n\n## Common tasks\n\n- List bundles: \\`project.db.selectFrom(\"bundle\").selectAll().execute()\\`\n- List messages for locale: \\`project.db.selectFrom(\"message\").where(\"locale\", \"=\", \"en\").selectAll().execute()\\`\n- Find missing translations: compare message counts across locales\n- Update a message: \\`project.db.updateTable(\"message\").set({ ... }).where(\"id\", \"=\", \"...\").execute()\\`\n\n## Links\n\n- [SDK documentation](https://inlang.com/docs)\n- [inlang.com](https://inlang.com)\n- [List of plugins](https://inlang.com/c/plugins)\n- [List of tools](https://inlang.com/c/tools)\n`;\n"]}
@@ -1,5 +1,7 @@
1
+ import type nodeFs from "node:fs";
1
2
  import type fs from "node:fs/promises";
2
3
  import type { InlangProject } from "./api.js";
4
+ type SaveProjectFs = typeof fs | typeof nodeFs;
3
5
  /**
4
6
  * Saves a project to a directory.
5
7
  *
@@ -8,7 +10,7 @@ import type { InlangProject } from "./api.js";
8
10
  *
9
11
  * @example
10
12
  * await saveProjectToDirectory({
11
- * fs: await import("node:fs/promises"),
13
+ * fs: await import("node:fs"),
12
14
  * project,
13
15
  * path: "./project.inlang",
14
16
  * });
@@ -16,8 +18,10 @@ import type { InlangProject } from "./api.js";
16
18
  export declare function saveProjectToDirectory(args: {
17
19
  /**
18
20
  * The file system module to use for writing files.
21
+ *
22
+ * Accepts either `node:fs` or `node:fs/promises`.
19
23
  */
20
- fs: typeof fs;
24
+ fs: SaveProjectFs;
21
25
  /**
22
26
  * The inlang project to save.
23
27
  */
@@ -34,4 +38,5 @@ export declare function saveProjectToDirectory(args: {
34
38
  */
35
39
  skipExporting?: boolean;
36
40
  }): Promise<void>;
41
+ export {};
37
42
  //# sourceMappingURL=saveProjectToDirectory.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"saveProjectToDirectory.d.ts","sourceRoot":"/","sources":["project/saveProjectToDirectory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAmB9C;;;;;;;;;;;;GAYG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IAClD;;OAEG;IACH,EAAE,EAAE,OAAO,EAAE,CAAC;IACd;;OAEG;IACH,OAAO,EAAE,aAAa,CAAC;IACvB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACxB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoJhB"}
1
+ {"version":3,"file":"saveProjectToDirectory.d.ts","sourceRoot":"/","sources":["project/saveProjectToDirectory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,SAAS,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAmB9C,KAAK,aAAa,GAAG,OAAO,EAAE,GAAG,OAAO,MAAM,CAAC;AA2B/C;;;;;;;;;;;;GAYG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IAClD;;;;OAIG;IACH,EAAE,EAAE,aAAa,CAAC;IAClB;;OAEG;IACH,OAAO,EAAE,aAAa,CAAC;IACvB;;OAEG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACxB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsJhB"}
@@ -15,6 +15,24 @@ async function fileExists(fsModule, filePath) {
15
15
  return false;
16
16
  }
17
17
  }
18
+ function getPromisesFs(fsModule) {
19
+ return "promises" in fsModule ? fsModule.promises : fsModule;
20
+ }
21
+ async function assertTranslationDataCanBeExported(project) {
22
+ const plugins = await project.plugins.get();
23
+ const hasExporter = plugins.some((plugin) => plugin.exportFiles || plugin.saveMessages);
24
+ if (hasExporter) {
25
+ return;
26
+ }
27
+ const [bundle, message, variant] = await Promise.all([
28
+ project.db.selectFrom("bundle").select("id").limit(1).executeTakeFirst(),
29
+ project.db.selectFrom("message").select("id").limit(1).executeTakeFirst(),
30
+ project.db.selectFrom("variant").select("id").limit(1).executeTakeFirst(),
31
+ ]);
32
+ if (bundle || message || variant) {
33
+ throw new Error("saveProjectToDirectory cannot write bundles, messages, or variants without an import/export plugin. Add a plugin to settings.modules/providePlugins, or save the canonical .inlang file with project.toBlob().");
34
+ }
35
+ }
18
36
  /**
19
37
  * Saves a project to a directory.
20
38
  *
@@ -23,7 +41,7 @@ async function fileExists(fsModule, filePath) {
23
41
  *
24
42
  * @example
25
43
  * await saveProjectToDirectory({
26
- * fs: await import("node:fs/promises"),
44
+ * fs: await import("node:fs"),
27
45
  * project,
28
46
  * path: "./project.inlang",
29
47
  * });
@@ -32,13 +50,17 @@ export async function saveProjectToDirectory(args) {
32
50
  if (args.path.endsWith(".inlang") === false) {
33
51
  throw new Error("The path must end with .inlang");
34
52
  }
53
+ if (!args.skipExporting) {
54
+ await assertTranslationDataCanBeExported(args.project);
55
+ }
56
+ const fsModule = getPromisesFs(args.fs);
35
57
  const files = await args.project.lix.db
36
58
  .selectFrom("file")
37
59
  .selectAll()
38
60
  .execute();
39
61
  const gitignoreContent = new TextEncoder().encode("# IF GIT SHOWED THAT THIS FILE CHANGED\n#\n# 1. RUN THE FOLLOWING COMMAND\n#\n# ---\n# git rm --cached '**/*.inlang/.gitignore'\n# ---\n#\n# 2. COMMIT THE CHANGE\n#\n# ---\n# git commit -m \"fix: remove tracked .gitignore from inlang project\"\n# ---\n#\n# Inlang handles the gitignore itself starting with version ^2.5.\n#\n# everything is ignored except settings.json\n*\n!settings.json");
40
62
  const existingMeta = await readProjectMeta({
41
- fs: args.fs,
63
+ fs: fsModule,
42
64
  projectPath: args.path,
43
65
  });
44
66
  const highestSdkVersion = pickHighestVersion([
@@ -51,27 +73,27 @@ export async function saveProjectToDirectory(args) {
51
73
  })();
52
74
  const readmePath = path.join(args.path, "README.md");
53
75
  const gitignorePath = path.join(args.path, ".gitignore");
54
- const shouldWriteReadme = shouldWriteMetadata || !(await fileExists(args.fs, readmePath));
55
- const shouldWriteGitignore = shouldWriteMetadata || !(await fileExists(args.fs, gitignorePath));
76
+ const shouldWriteReadme = shouldWriteMetadata || !(await fileExists(fsModule, readmePath));
77
+ const shouldWriteGitignore = shouldWriteMetadata || !(await fileExists(fsModule, gitignorePath));
56
78
  // write all files to the directory
57
79
  for (const file of files) {
58
80
  if (file.path.endsWith("db.sqlite") || file.path === "/project_id") {
59
81
  continue;
60
82
  }
61
83
  const p = path.join(args.path, file.path);
62
- await args.fs.mkdir(path.dirname(p), { recursive: true });
63
- await args.fs.writeFile(p, new Uint8Array(file.data));
84
+ await fsModule.mkdir(path.dirname(p), { recursive: true });
85
+ await fsModule.writeFile(p, new Uint8Array(file.data));
64
86
  }
65
87
  if (shouldWriteGitignore) {
66
- await args.fs.writeFile(gitignorePath, gitignoreContent);
88
+ await fsModule.writeFile(gitignorePath, gitignoreContent);
67
89
  }
68
90
  if (shouldWriteReadme) {
69
91
  // Write README.md for coding agents
70
- await args.fs.writeFile(readmePath, new TextEncoder().encode(README_CONTENT));
92
+ await fsModule.writeFile(readmePath, new TextEncoder().encode(README_CONTENT));
71
93
  }
72
94
  if (shouldWriteMetadata) {
73
95
  const metaContent = JSON.stringify({ highestSdkVersion }, null, 2);
74
- await args.fs.writeFile(path.join(args.path, ".meta.json"), new TextEncoder().encode(metaContent));
96
+ await fsModule.writeFile(path.join(args.path, ".meta.json"), new TextEncoder().encode(metaContent));
75
97
  }
76
98
  if (args.skipExporting) {
77
99
  return;
@@ -110,24 +132,21 @@ export async function saveProjectToDirectory(args) {
110
132
  const p = pathPattern
111
133
  ? absolutePathFromProject(args.path, pathPattern.replace(/\{(languageTag|locale)\}/g, file.locale))
112
134
  : absolutePathFromProject(args.path, file.name);
113
- const dirname = path.dirname(p);
114
- if ((await args.fs.stat(dirname)).isDirectory() === false) {
115
- await args.fs.mkdir(dirname, { recursive: true });
116
- }
135
+ await fsModule.mkdir(path.dirname(p), { recursive: true });
117
136
  if (p.endsWith(".json")) {
118
137
  try {
119
- const existing = await args.fs.readFile(p, "utf-8");
138
+ const existing = await fsModule.readFile(p, "utf-8");
120
139
  const stringify = detectJsonFormatting(existing);
121
- await args.fs.writeFile(p, new TextEncoder().encode(stringify(JSON.parse(new TextDecoder().decode(file.content)))));
140
+ await fsModule.writeFile(p, new TextEncoder().encode(stringify(JSON.parse(new TextDecoder().decode(file.content)))));
122
141
  }
123
142
  catch {
124
143
  // write the file to disk (json doesn't exist yet)
125
144
  // yeah ugly duplication of write file but it works.
126
- await args.fs.writeFile(p, new Uint8Array(file.content));
145
+ await fsModule.writeFile(p, new Uint8Array(file.content));
127
146
  }
128
147
  }
129
148
  else {
130
- await args.fs.writeFile(p, new Uint8Array(file.content));
149
+ await fsModule.writeFile(p, new Uint8Array(file.content));
131
150
  }
132
151
  }
133
152
  }
@@ -140,7 +159,7 @@ export async function saveProjectToDirectory(args) {
140
159
  await plugin.saveMessages({
141
160
  messages: bundlesNested.map((b) => toMessageV1(b)),
142
161
  // @ts-expect-error - legacy
143
- nodeishFs: withAbsolutePaths(args.fs, args.path),
162
+ nodeishFs: withAbsolutePaths(fsModule, args.path),
144
163
  settings,
145
164
  });
146
165
  }
@@ -1 +1 @@
1
- {"version":3,"file":"saveProjectToDirectory.js","sourceRoot":"/","sources":["project/saveProjectToDirectory.ts"],"names":[],"mappings":"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","sourcesContent":["import type fs from \"node:fs/promises\";\nimport type { InlangProject } from \"./api.js\";\nimport path from \"node:path\";\nimport { toMessageV1 } from \"../json-schema/old-v1-message/toMessageV1.js\";\nimport { absolutePathFromProject, withAbsolutePaths } from \"./path-helpers.js\";\nimport { detectJsonFormatting } from \"../utilities/detectJsonFormatting.js\";\nimport { selectBundleNested } from \"../query-utilities/selectBundleNested.js\";\nimport { README_CONTENT } from \"./README_CONTENT.js\";\nimport { ENV_VARIABLES } from \"../services/env-variables/index.js\";\nimport { compareSemver, pickHighestVersion, readProjectMeta } from \"./meta.js\";\n\nasync function fileExists(fsModule: typeof fs, filePath: string) {\n\ttry {\n\t\tawait fsModule.stat(filePath);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * Saves a project to a directory.\n *\n * Writes all project files to disk and runs exporters to generate\n * resource files (e.g., JSON translation files).\n *\n * @example\n * await saveProjectToDirectory({\n * fs: await import(\"node:fs/promises\"),\n * project,\n * path: \"./project.inlang\",\n * });\n */\nexport async function saveProjectToDirectory(args: {\n\t/**\n\t * The file system module to use for writing files.\n\t */\n\tfs: typeof fs;\n\t/**\n\t * The inlang project to save.\n\t */\n\tproject: InlangProject;\n\t/**\n\t * The path to the inlang project directory. Must end with `.inlang`.\n\t */\n\tpath: string;\n\t/**\n\t * If `true`, skips running exporters and only writes internal project files.\n\t *\n\t * Useful when you only want to update project metadata without\n\t * regenerating resource files.\n\t */\n\tskipExporting?: boolean;\n}): Promise<void> {\n\tif (args.path.endsWith(\".inlang\") === false) {\n\t\tthrow new Error(\"The path must end with .inlang\");\n\t}\n\tconst files = await args.project.lix.db\n\t\t.selectFrom(\"file\")\n\t\t.selectAll()\n\t\t.execute();\n\n\tconst gitignoreContent = new TextEncoder().encode(\n\t\t\"# IF GIT SHOWED THAT THIS FILE CHANGED\\n#\\n# 1. RUN THE FOLLOWING COMMAND\\n#\\n# ---\\n# git rm --cached '**/*.inlang/.gitignore'\\n# ---\\n#\\n# 2. COMMIT THE CHANGE\\n#\\n# ---\\n# git commit -m \\\"fix: remove tracked .gitignore from inlang project\\\"\\n# ---\\n#\\n# Inlang handles the gitignore itself starting with version ^2.5.\\n#\\n# everything is ignored except settings.json\\n*\\n!settings.json\"\n\t);\n\n\tconst existingMeta = await readProjectMeta({\n\t\tfs: args.fs,\n\t\tprojectPath: args.path,\n\t});\n\tconst highestSdkVersion =\n\t\tpickHighestVersion([\n\t\t\texistingMeta?.highestSdkVersion,\n\t\t\tENV_VARIABLES.SDK_VERSION,\n\t\t]) ?? ENV_VARIABLES.SDK_VERSION;\n\tconst shouldWriteMetadata = (() => {\n\t\tconst comparison = compareSemver(\n\t\t\thighestSdkVersion,\n\t\t\tENV_VARIABLES.SDK_VERSION\n\t\t);\n\t\treturn comparison === null || comparison <= 0;\n\t})();\n\tconst readmePath = path.join(args.path, \"README.md\");\n\tconst gitignorePath = path.join(args.path, \".gitignore\");\n\tconst shouldWriteReadme =\n\t\tshouldWriteMetadata || !(await fileExists(args.fs, readmePath));\n\tconst shouldWriteGitignore =\n\t\tshouldWriteMetadata || !(await fileExists(args.fs, gitignorePath));\n\n\t// write all files to the directory\n\tfor (const file of files) {\n\t\tif (file.path.endsWith(\"db.sqlite\") || file.path === \"/project_id\") {\n\t\t\tcontinue;\n\t\t}\n\t\tconst p = path.join(args.path, file.path);\n\t\tawait args.fs.mkdir(path.dirname(p), { recursive: true });\n\t\tawait args.fs.writeFile(p, new Uint8Array(file.data));\n\t}\n\n\tif (shouldWriteGitignore) {\n\t\tawait args.fs.writeFile(gitignorePath, gitignoreContent);\n\t}\n\n\tif (shouldWriteReadme) {\n\t\t// Write README.md for coding agents\n\t\tawait args.fs.writeFile(\n\t\t\treadmePath,\n\t\t\tnew TextEncoder().encode(README_CONTENT)\n\t\t);\n\t}\n\n\tif (shouldWriteMetadata) {\n\t\tconst metaContent = JSON.stringify({ highestSdkVersion }, null, 2);\n\t\tawait args.fs.writeFile(\n\t\t\tpath.join(args.path, \".meta.json\"),\n\t\t\tnew TextEncoder().encode(metaContent)\n\t\t);\n\t}\n\n\tif (args.skipExporting) {\n\t\treturn;\n\t}\n\n\t// run exporters\n\tconst plugins = await args.project.plugins.get();\n\tconst settings = await args.project.settings.get();\n\n\tfor (const plugin of plugins) {\n\t\tif (plugin.exportFiles) {\n\t\t\tconst bundles = await args.project.db\n\t\t\t\t.selectFrom(\"bundle\")\n\t\t\t\t.selectAll()\n\t\t\t\t.execute();\n\t\t\tconst messages = await args.project.db\n\t\t\t\t.selectFrom(\"message\")\n\t\t\t\t.selectAll()\n\t\t\t\t.execute();\n\t\t\tconst variants = await args.project.db\n\t\t\t\t.selectFrom(\"variant\")\n\t\t\t\t.selectAll()\n\t\t\t\t.execute();\n\t\t\tconst files = await plugin.exportFiles({\n\t\t\t\tbundles,\n\t\t\t\tmessages,\n\t\t\t\tvariants,\n\t\t\t\tsettings,\n\t\t\t});\n\t\t\tfor (const file of files) {\n\t\t\t\tconst pathPattern = settings[plugin.key]?.pathPattern;\n\n\t\t\t\t// We need to check if pathPattern is a string or an array of strings\n\t\t\t\t// and handle both cases.\n\t\t\t\tconst formattedPathPatterns = Array.isArray(pathPattern)\n\t\t\t\t\t? pathPattern\n\t\t\t\t\t: [pathPattern];\n\n\t\t\t\tfor (const pathPattern of formattedPathPatterns) {\n\t\t\t\t\tconst p = pathPattern\n\t\t\t\t\t\t? absolutePathFromProject(\n\t\t\t\t\t\t\t\targs.path,\n\t\t\t\t\t\t\t\tpathPattern.replace(/\\{(languageTag|locale)\\}/g, file.locale)\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t: absolutePathFromProject(args.path, file.name);\n\t\t\t\t\tconst dirname = path.dirname(p);\n\t\t\t\t\tif ((await args.fs.stat(dirname)).isDirectory() === false) {\n\t\t\t\t\t\tawait args.fs.mkdir(dirname, { recursive: true });\n\t\t\t\t\t}\n\t\t\t\t\tif (p.endsWith(\".json\")) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst existing = await args.fs.readFile(p, \"utf-8\");\n\t\t\t\t\t\t\tconst stringify = detectJsonFormatting(existing);\n\t\t\t\t\t\t\tawait args.fs.writeFile(\n\t\t\t\t\t\t\t\tp,\n\t\t\t\t\t\t\t\tnew TextEncoder().encode(\n\t\t\t\t\t\t\t\t\tstringify(JSON.parse(new TextDecoder().decode(file.content)))\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// write the file to disk (json doesn't exist yet)\n\t\t\t\t\t\t\t// yeah ugly duplication of write file but it works.\n\t\t\t\t\t\t\tawait args.fs.writeFile(p, new Uint8Array(file.content));\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tawait args.fs.writeFile(p, new Uint8Array(file.content));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// old legacy remove with v3\n\t\telse if (plugin.saveMessages) {\n\t\t\t// in-efficient re-qeuery but it's a legacy function that will be removed.\n\t\t\t// the effort of adjusting the code to not re-query is not worth it.\n\t\t\tconst bundlesNested = await selectBundleNested(args.project.db).execute();\n\t\t\tawait plugin.saveMessages({\n\t\t\t\tmessages: bundlesNested.map((b) => toMessageV1(b)),\n\t\t\t\t// @ts-expect-error - legacy\n\t\t\t\tnodeishFs: withAbsolutePaths(args.fs, args.path),\n\t\t\t\tsettings,\n\t\t\t});\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"saveProjectToDirectory.js","sourceRoot":"/","sources":["project/saveProjectToDirectory.ts"],"names":[],"mappings":"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","sourcesContent":["import type nodeFs from \"node:fs\";\nimport type fs from \"node:fs/promises\";\nimport type { InlangProject } from \"./api.js\";\nimport path from \"node:path\";\nimport { toMessageV1 } from \"../json-schema/old-v1-message/toMessageV1.js\";\nimport { absolutePathFromProject, withAbsolutePaths } from \"./path-helpers.js\";\nimport { detectJsonFormatting } from \"../utilities/detectJsonFormatting.js\";\nimport { selectBundleNested } from \"../query-utilities/selectBundleNested.js\";\nimport { README_CONTENT } from \"./README_CONTENT.js\";\nimport { ENV_VARIABLES } from \"../services/env-variables/index.js\";\nimport { compareSemver, pickHighestVersion, readProjectMeta } from \"./meta.js\";\n\nasync function fileExists(fsModule: typeof fs, filePath: string) {\n\ttry {\n\t\tawait fsModule.stat(filePath);\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n}\n\ntype SaveProjectFs = typeof fs | typeof nodeFs;\n\nfunction getPromisesFs(fsModule: SaveProjectFs): typeof fs {\n\treturn \"promises\" in fsModule ? fsModule.promises : fsModule;\n}\n\nasync function assertTranslationDataCanBeExported(project: InlangProject) {\n\tconst plugins = await project.plugins.get();\n\tconst hasExporter = plugins.some(\n\t\t(plugin) => plugin.exportFiles || plugin.saveMessages\n\t);\n\tif (hasExporter) {\n\t\treturn;\n\t}\n\n\tconst [bundle, message, variant] = await Promise.all([\n\t\tproject.db.selectFrom(\"bundle\").select(\"id\").limit(1).executeTakeFirst(),\n\t\tproject.db.selectFrom(\"message\").select(\"id\").limit(1).executeTakeFirst(),\n\t\tproject.db.selectFrom(\"variant\").select(\"id\").limit(1).executeTakeFirst(),\n\t]);\n\tif (bundle || message || variant) {\n\t\tthrow new Error(\n\t\t\t\"saveProjectToDirectory cannot write bundles, messages, or variants without an import/export plugin. Add a plugin to settings.modules/providePlugins, or save the canonical .inlang file with project.toBlob().\"\n\t\t);\n\t}\n}\n\n/**\n * Saves a project to a directory.\n *\n * Writes all project files to disk and runs exporters to generate\n * resource files (e.g., JSON translation files).\n *\n * @example\n * await saveProjectToDirectory({\n * fs: await import(\"node:fs\"),\n * project,\n * path: \"./project.inlang\",\n * });\n */\nexport async function saveProjectToDirectory(args: {\n\t/**\n\t * The file system module to use for writing files.\n\t *\n\t * Accepts either `node:fs` or `node:fs/promises`.\n\t */\n\tfs: SaveProjectFs;\n\t/**\n\t * The inlang project to save.\n\t */\n\tproject: InlangProject;\n\t/**\n\t * The path to the inlang project directory. Must end with `.inlang`.\n\t */\n\tpath: string;\n\t/**\n\t * If `true`, skips running exporters and only writes internal project files.\n\t *\n\t * Useful when you only want to update project metadata without\n\t * regenerating resource files.\n\t */\n\tskipExporting?: boolean;\n}): Promise<void> {\n\tif (args.path.endsWith(\".inlang\") === false) {\n\t\tthrow new Error(\"The path must end with .inlang\");\n\t}\n\tif (!args.skipExporting) {\n\t\tawait assertTranslationDataCanBeExported(args.project);\n\t}\n\tconst fsModule = getPromisesFs(args.fs);\n\n\tconst files = await args.project.lix.db\n\t\t.selectFrom(\"file\")\n\t\t.selectAll()\n\t\t.execute();\n\n\tconst gitignoreContent = new TextEncoder().encode(\n\t\t\"# IF GIT SHOWED THAT THIS FILE CHANGED\\n#\\n# 1. RUN THE FOLLOWING COMMAND\\n#\\n# ---\\n# git rm --cached '**/*.inlang/.gitignore'\\n# ---\\n#\\n# 2. COMMIT THE CHANGE\\n#\\n# ---\\n# git commit -m \\\"fix: remove tracked .gitignore from inlang project\\\"\\n# ---\\n#\\n# Inlang handles the gitignore itself starting with version ^2.5.\\n#\\n# everything is ignored except settings.json\\n*\\n!settings.json\"\n\t);\n\n\tconst existingMeta = await readProjectMeta({\n\t\tfs: fsModule,\n\t\tprojectPath: args.path,\n\t});\n\tconst highestSdkVersion =\n\t\tpickHighestVersion([\n\t\t\texistingMeta?.highestSdkVersion,\n\t\t\tENV_VARIABLES.SDK_VERSION,\n\t\t]) ?? ENV_VARIABLES.SDK_VERSION;\n\tconst shouldWriteMetadata = (() => {\n\t\tconst comparison = compareSemver(\n\t\t\thighestSdkVersion,\n\t\t\tENV_VARIABLES.SDK_VERSION\n\t\t);\n\t\treturn comparison === null || comparison <= 0;\n\t})();\n\tconst readmePath = path.join(args.path, \"README.md\");\n\tconst gitignorePath = path.join(args.path, \".gitignore\");\n\tconst shouldWriteReadme =\n\t\tshouldWriteMetadata || !(await fileExists(fsModule, readmePath));\n\tconst shouldWriteGitignore =\n\t\tshouldWriteMetadata || !(await fileExists(fsModule, gitignorePath));\n\n\t// write all files to the directory\n\tfor (const file of files) {\n\t\tif (file.path.endsWith(\"db.sqlite\") || file.path === \"/project_id\") {\n\t\t\tcontinue;\n\t\t}\n\t\tconst p = path.join(args.path, file.path);\n\t\tawait fsModule.mkdir(path.dirname(p), { recursive: true });\n\t\tawait fsModule.writeFile(p, new Uint8Array(file.data));\n\t}\n\n\tif (shouldWriteGitignore) {\n\t\tawait fsModule.writeFile(gitignorePath, gitignoreContent);\n\t}\n\n\tif (shouldWriteReadme) {\n\t\t// Write README.md for coding agents\n\t\tawait fsModule.writeFile(\n\t\t\treadmePath,\n\t\t\tnew TextEncoder().encode(README_CONTENT)\n\t\t);\n\t}\n\n\tif (shouldWriteMetadata) {\n\t\tconst metaContent = JSON.stringify({ highestSdkVersion }, null, 2);\n\t\tawait fsModule.writeFile(\n\t\t\tpath.join(args.path, \".meta.json\"),\n\t\t\tnew TextEncoder().encode(metaContent)\n\t\t);\n\t}\n\n\tif (args.skipExporting) {\n\t\treturn;\n\t}\n\n\t// run exporters\n\tconst plugins = await args.project.plugins.get();\n\tconst settings = await args.project.settings.get();\n\n\tfor (const plugin of plugins) {\n\t\tif (plugin.exportFiles) {\n\t\t\tconst bundles = await args.project.db\n\t\t\t\t.selectFrom(\"bundle\")\n\t\t\t\t.selectAll()\n\t\t\t\t.execute();\n\t\t\tconst messages = await args.project.db\n\t\t\t\t.selectFrom(\"message\")\n\t\t\t\t.selectAll()\n\t\t\t\t.execute();\n\t\t\tconst variants = await args.project.db\n\t\t\t\t.selectFrom(\"variant\")\n\t\t\t\t.selectAll()\n\t\t\t\t.execute();\n\t\t\tconst files = await plugin.exportFiles({\n\t\t\t\tbundles,\n\t\t\t\tmessages,\n\t\t\t\tvariants,\n\t\t\t\tsettings,\n\t\t\t});\n\t\t\tfor (const file of files) {\n\t\t\t\tconst pathPattern = settings[plugin.key]?.pathPattern;\n\n\t\t\t\t// We need to check if pathPattern is a string or an array of strings\n\t\t\t\t// and handle both cases.\n\t\t\t\tconst formattedPathPatterns = Array.isArray(pathPattern)\n\t\t\t\t\t? pathPattern\n\t\t\t\t\t: [pathPattern];\n\n\t\t\t\tfor (const pathPattern of formattedPathPatterns) {\n\t\t\t\t\tconst p = pathPattern\n\t\t\t\t\t\t? absolutePathFromProject(\n\t\t\t\t\t\t\t\targs.path,\n\t\t\t\t\t\t\t\tpathPattern.replace(/\\{(languageTag|locale)\\}/g, file.locale)\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t: absolutePathFromProject(args.path, file.name);\n\t\t\t\t\tawait fsModule.mkdir(path.dirname(p), { recursive: true });\n\t\t\t\t\tif (p.endsWith(\".json\")) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst existing = await fsModule.readFile(p, \"utf-8\");\n\t\t\t\t\t\t\tconst stringify = detectJsonFormatting(existing);\n\t\t\t\t\t\t\tawait fsModule.writeFile(\n\t\t\t\t\t\t\t\tp,\n\t\t\t\t\t\t\t\tnew TextEncoder().encode(\n\t\t\t\t\t\t\t\t\tstringify(JSON.parse(new TextDecoder().decode(file.content)))\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// write the file to disk (json doesn't exist yet)\n\t\t\t\t\t\t\t// yeah ugly duplication of write file but it works.\n\t\t\t\t\t\t\tawait fsModule.writeFile(p, new Uint8Array(file.content));\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tawait fsModule.writeFile(p, new Uint8Array(file.content));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\t// old legacy remove with v3\n\t\telse if (plugin.saveMessages) {\n\t\t\t// in-efficient re-qeuery but it's a legacy function that will be removed.\n\t\t\t// the effort of adjusting the code to not re-query is not worth it.\n\t\t\tconst bundlesNested = await selectBundleNested(args.project.db).execute();\n\t\t\tawait plugin.saveMessages({\n\t\t\t\tmessages: bundlesNested.map((b) => toMessageV1(b)),\n\t\t\t\t// @ts-expect-error - legacy\n\t\t\t\tnodeishFs: withAbsolutePaths(fsModule, args.path),\n\t\t\t\tsettings,\n\t\t\t});\n\t\t}\n\t}\n}\n"]}