The CRUD API is what makes building i18n tools easy. Instead of parsing and writing translation files in different formats (JSON, XLIFF, YAML, etc.), you query and modify messages directly. Plugins handle the file format conversion at the boundary.

Inlang uses Kysely for type-safe database queries. Access it via project.db.

import { loadProjectFromDirectory } from "@inlang/sdk";

const project = await loadProjectFromDirectory({
  path: "./project.inlang",
  fs: fs,
});

// project.db is a Kysely instance
const bundles = await project.db
  .selectFrom("bundle")
  .selectAll()
  .execute();

Create

Insert a bundle

await project.db
  .insertInto("bundle")
  .values({
    id: "greeting",
    declarations: []
  })
  .execute();

Insert a message

await project.db
  .insertInto("message")
  .values({
    id: crypto.randomUUID(),
    bundleId: "greeting",
    locale: "en",
    selectors: []
  })
  .execute();

Insert a variant

await project.db
  .insertInto("variant")
  .values({
    id: crypto.randomUUID(),
    messageId: messageId,
    matches: [],
    pattern: [{ type: "text", value: "Hello world!" }]
  })
  .execute();

Insert nested (bundle + messages + variants)

import { insertBundleNested } from "@inlang/sdk";

await insertBundleNested(project.db, {
  id: "greeting",
  declarations: [],
  messages: [
    {
      id: crypto.randomUUID(),
      bundleId: "greeting",
      locale: "en",
      selectors: [],
      variants: [
        {
          id: crypto.randomUUID(),
          messageId: messageId,
          matches: [],
          pattern: [{ type: "text", value: "Hello!" }]
        }
      ]
    }
  ]
});

Read

Get all bundles

const bundles = await project.db
  .selectFrom("bundle")
  .selectAll()
  .execute();

Get bundle by ID

const bundle = await project.db
  .selectFrom("bundle")
  .selectAll()
  .where("id", "=", "greeting")
  .executeTakeFirst();

Get messages by locale

const messages = await project.db
  .selectFrom("message")
  .selectAll()
  .where("locale", "=", "en")
  .execute();

Get messages for a bundle

const messages = await project.db
  .selectFrom("message")
  .selectAll()
  .where("bundleId", "=", "greeting")
  .execute();

Get variants for a message

const variants = await project.db
  .selectFrom("variant")
  .selectAll()
  .where("messageId", "=", messageId)
  .execute();

Get nested (bundle with messages and variants)

import { selectBundleNested } from "@inlang/sdk";

const bundle = await selectBundleNested(project.db)
  .where("bundle.id", "=", "greeting")
  .executeTakeFirst();

// Returns:
// {
//   id: "greeting",
//   declarations: [],
//   messages: [
//     {
//       id: "...",
//       locale: "en",
//       variants: [{ id: "...", pattern: [...] }]
//     }
//   ]
// }

Join bundles and messages

const results = await project.db
  .selectFrom("bundle")
  .leftJoin("message", "message.bundleId", "bundle.id")
  .selectAll()
  .execute();

Find missing translations

const missingGerman = await project.db
  .selectFrom("bundle")
  .where((eb) =>
    eb.not(
      eb.exists(
        eb.selectFrom("message")
          .where("message.bundleId", "=", eb.ref("bundle.id"))
          .where("message.locale", "=", "de")
      )
    )
  )
  .selectAll()
  .execute();

Update

Update a bundle

await project.db
  .updateTable("bundle")
  .set({
    declarations: [{ type: "input-variable", name: "count" }]
  })
  .where("id", "=", "greeting")
  .execute();

Update a variant's text

await project.db
  .updateTable("variant")
  .set({
    pattern: [{ type: "text", value: "Updated text" }]
  })
  .where("id", "=", variantId)
  .execute();

Update nested

import { updateBundleNested } from "@inlang/sdk";

await updateBundleNested(project.db, {
  id: "greeting",
  declarations: [],
  messages: [
    {
      id: messageId,
      locale: "en",
      selectors: [],
      variants: [
        {
          id: variantId,
          matches: [],
          pattern: [{ type: "text", value: "Updated!" }]
        }
      ]
    }
  ]
});

Delete

Delete a bundle

await project.db
  .deleteFrom("bundle")
  .where("id", "=", "greeting")
  .execute();

// Cascades: all messages and variants are deleted

Delete a message

await project.db
  .deleteFrom("message")
  .where("id", "=", messageId)
  .execute();

// Cascades: all variants are deleted

Delete a variant

await project.db
  .deleteFrom("variant")
  .where("id", "=", variantId)
  .execute();

Upsert

Insert or update based on whether the record exists.

Upsert a bundle

await project.db
  .insertInto("bundle")
  .values({
    id: "greeting",
    declarations: []
  })
  .onConflict((oc) =>
    oc.column("id").doUpdateSet({
      declarations: []
    })
  )
  .execute();

Upsert nested

import { upsertBundleNested } from "@inlang/sdk";

await upsertBundleNested(project.db, {
  id: "greeting",
  declarations: [],
  messages: [...]
});

Next steps