Better I18NBetter I18N
Cli

content:types

Generate TypeScript types from your content models

Generate TypeScript type definitions from your content models — like supabase gen types typescript.

Usage

better-i18n content:types                           # Auto-detect project from i18n.config.ts
better-i18n content:types --project acme/landing    # Explicit project
better-i18n content:types --output types/cms.ts     # Custom output path

Options

OptionDescription
--project <org/name>Project identifier. Default: reads from i18n.config.ts
--api-key <key>Content API key. Default: BETTER_I18N_API_KEY env var
--output <path>Output file path. Default: src/content-types.ts
--dir <path>Directory to scan for config. Default: current directory

Setup

1. Add your API key

The CLI auto-loads .env files from your project root. Create a .env or .env.local file:

.env.local
BETTER_I18N_API_KEY=bi_pub_your_api_key_here

You can get your Content API key from the dashboard → Project Settings → API Keys.

Shell/CI environment variables always take precedence over .env files.

2. Run the generator

npx better-i18n content:types
✓ Project: acme/landing
✓ Found 3 content model(s)
✓ Content types generated
  Output: src/content-types.ts
  Models: blog-posts, changelog, pricing-plans

  Usage:
    import type { BlogPosts } from "./content-types";
    const { data } = await client.from<BlogPostsFields>("blog-posts").execute();

Generated Output

The generated file includes typed interfaces for each content model's custom fields, plus convenience type aliases:

src/content-types.ts
/**
 * Auto-generated by @better-i18n/cli — do not edit manually.
 */
import type { ContentEntry, ContentEntryListItem } from "@better-i18n/sdk";

/** Blog Posts — Localized blog posts for the landing site */
export interface BlogPostsFields extends Record<string, string | null> {
  author: RelationValue | null;
  category: RelationValue | null;
  featured: string | null;
  read_time: string;
}
export type BlogPosts = ContentEntry<BlogPostsFields>;
export type BlogPostsListItem = ContentEntryListItem<BlogPostsFields>;

/** Content model: pricing-plans */
export interface PricingPlansFields extends Record<string, string | null> {
  plan_id: "free" | "pro" | "enterprise";
  name: string;
  description: string;
  monthly_prices: string | null;
  // ... more fields
}
export type PricingPlans = ContentEntry<PricingPlansFields>;
export type PricingPlansListItem = ContentEntryListItem<PricingPlansFields>;

/** All content model slugs. */
export type ContentModelSlug = "blog-posts" | "pricing-plans" | "changelog";

/** Map from model slug to its custom fields type. */
export interface ContentTypeMap {
  "blog-posts": BlogPostsFields;
  "pricing-plans": PricingPlansFields;
  "changelog": ChangelogFields;
}

Using Generated Types

With the SDK client

import { createClient } from "@better-i18n/sdk";
import type { BlogPostsFields, BlogPostsListItem } from "./content-types";

const client = createClient({
  project: "acme/landing",
  apiKey: process.env.BETTER_I18N_API_KEY!,
});

// Typed list query
const { data } = await client
  .from<BlogPostsFields>("blog-posts")
  .eq("featured", "true")
  .execute();

// data is BlogPostsListItem[] — fully typed!

With getEntries (legacy)

const { items } = await client.getEntries<BlogPostsFields>("blog-posts", {
  status: "published",
  language: "en",
});

// items[0].author — typed as RelationValue | null
// items[0].read_time — typed as string

Field Type Mapping

The generator maps content model field types to TypeScript:

CMS Field TypeTypeScript TypeExample
text, textarea, richtextstring | nullauthor: string | null
numberstring | nullread_time: string | null
booleanstring | nullfeatured: string | null
date, datetimestring | nullrelease_date: string | null
enumLiteral unionstatus: "active" | "draft" | null
mediastring | nullcover: string | null
relationRelationValue | nullauthor: RelationValue | null

Required fields omit | null from the type.

.env Auto-Loading

The CLI automatically reads environment variables from .env files at startup:

FilePriorityUse case
Shell/CI varsHighestexport BETTER_I18N_API_KEY=xxx
.env.localHighLocal overrides (git-ignored)
.envNormalShared defaults

Variables already set in the shell are never overridden.

CI/CD Integration

Add type generation to your CI pipeline:

.github/workflows/types.yml
- name: Generate content types
  run: npx better-i18n content:types --output src/content-types.ts
  env:
    BETTER_I18N_API_KEY: ${{ secrets.BETTER_I18N_API_KEY }}

- name: Check for uncommitted changes
  run: git diff --exit-code src/content-types.ts

Or add it as a package.json script:

package.json
{
  "scripts": {
    "types:content": "better-i18n content:types"
  }
}

On this page