@better-i18n/sdk 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # @better-i18n/sdk
2
+
3
+ Content SDK for [Better i18n](https://better-i18n.com). A lightweight, typed client for fetching content models and entries from the headless CMS.
4
+
5
+ ## Features
6
+
7
+ - 📦 **Zero Dependencies** — Runs anywhere with `fetch()`
8
+ - 🔒 **Type-Safe** — Full TypeScript types with generic custom fields
9
+ - 🌍 **Language-Aware** — Fetch localized content by language code
10
+ - 📄 **Pagination Built-in** — Paginated listing with total count and `hasMore`
11
+ - 🔍 **Filtering & Sorting** — Filter by status, sort by date or title
12
+ - ⚡ **Lightweight** — Thin wrapper over REST API
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @better-i18n/sdk
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ```typescript
23
+ import { createClient } from "@better-i18n/sdk";
24
+
25
+ const client = createClient({
26
+ org: "acme",
27
+ project: "web-app",
28
+ apiKey: process.env.BETTER_I18N_API_KEY!,
29
+ });
30
+
31
+ // List content models
32
+ const models = await client.getModels();
33
+
34
+ // List published blog posts
35
+ const { items, total, hasMore } = await client.getEntries("blog-posts", {
36
+ status: "published",
37
+ sort: "publishedAt",
38
+ order: "desc",
39
+ language: "en",
40
+ limit: 10,
41
+ });
42
+
43
+ // Get a single entry with localized content
44
+ const post = await client.getEntry("blog-posts", "hello-world", {
45
+ language: "fr",
46
+ });
47
+ console.log(post.title, post.bodyMarkdown);
48
+ ```
49
+
50
+ ## API
51
+
52
+ | Method | Description |
53
+ | --- | --- |
54
+ | `getModels()` | List all content models with entry counts |
55
+ | `getEntries(modelSlug, options?)` | Paginated list of entries for a model |
56
+ | `getEntry(modelSlug, entrySlug, options?)` | Full content entry with all fields |
57
+
58
+ ### `getEntries` Options
59
+
60
+ | Option | Type | Default | Description |
61
+ | --- | --- | --- | --- |
62
+ | `language` | `string` | source language | Language code for localized content |
63
+ | `status` | `"draft" \| "published" \| "archived"` | all | Filter by entry status |
64
+ | `sort` | `"publishedAt" \| "createdAt" \| "updatedAt" \| "title"` | `"updatedAt"` | Sort field |
65
+ | `order` | `"asc" \| "desc"` | `"desc"` | Sort direction |
66
+ | `page` | `number` | `1` | Page number (1-based) |
67
+ | `limit` | `number` | `50` | Entries per page (1-100) |
68
+
69
+ ### `getEntry` Options
70
+
71
+ | Option | Type | Default | Description |
72
+ | --- | --- | --- | --- |
73
+ | `language` | `string` | source language | Language code for localized content |
74
+
75
+ ### Response Types
76
+
77
+ **`getEntries` returns `PaginatedResponse<ContentEntryListItem>`:**
78
+
79
+ ```typescript
80
+ {
81
+ items: ContentEntryListItem[]; // slug, title, excerpt, publishedAt, tags, author
82
+ total: number; // total matching entries
83
+ hasMore: boolean; // more pages available
84
+ }
85
+ ```
86
+
87
+ **`getEntry` returns `ContentEntry<CF>`:**
88
+
89
+ ```typescript
90
+ {
91
+ id, slug, status, publishedAt, sourceLanguage, availableLanguages,
92
+ featuredImage, tags, author, customFields,
93
+ title, excerpt, body, bodyHtml, bodyMarkdown,
94
+ metaTitle, metaDescription
95
+ }
96
+ ```
97
+
98
+ ## Typed Custom Fields
99
+
100
+ Use the generic type parameter for type-safe custom fields:
101
+
102
+ ```typescript
103
+ interface BlogFields {
104
+ readingTime: string | null;
105
+ category: string | null;
106
+ }
107
+
108
+ const post = await client.getEntry<BlogFields>("blog-posts", "hello-world");
109
+ post.customFields.readingTime; // string | null (typed!)
110
+ post.customFields.category; // string | null (typed!)
111
+ ```
112
+
113
+ ## Configuration
114
+
115
+ | Option | Required | Description |
116
+ | --- | --- | --- |
117
+ | `org` | Yes | Organization slug |
118
+ | `project` | Yes | Project slug |
119
+ | `apiKey` | Yes | API key from [dashboard](https://dash.better-i18n.com) |
120
+ | `apiBase` | No | API base URL (default: `https://api.better-i18n.com`) |
121
+
122
+ ## Documentation
123
+
124
+ Full documentation at [docs.better-i18n.com/sdk](https://docs.better-i18n.com/sdk)
125
+
126
+ ## License
127
+
128
+ MIT © [Better i18n](https://better-i18n.com)
package/package.json CHANGED
@@ -1,14 +1,12 @@
1
1
  {
2
2
  "name": "@better-i18n/sdk",
3
- "version": "0.1.1",
4
- "description": "SDK for fetching content and translations from Better i18n",
3
+ "version": "0.3.0",
4
+ "description": "Content SDK for Better i18n - headless CMS client for fetching content models and entries",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "types": "src/index.ts",
8
8
  "exports": {
9
- ".": "./src/index.ts",
10
- "./content": "./src/content.ts",
11
- "./translations": "./src/translations.ts"
9
+ ".": "./src/index.ts"
12
10
  },
13
11
  "files": [
14
12
  "src",
@@ -18,11 +16,11 @@
18
16
  "typecheck": "tsc --noEmit"
19
17
  },
20
18
  "keywords": [
21
- "i18n",
22
- "translations",
23
19
  "content",
24
20
  "sdk",
25
- "headless-cms"
21
+ "headless-cms",
22
+ "better-i18n",
23
+ "cms"
26
24
  ],
27
25
  "devDependencies": {
28
26
  "typescript": "~5.9.3"
package/src/client.ts CHANGED
@@ -1,15 +1,13 @@
1
- import type { ClientConfig, BetterI18nClient } from "./types";
1
+ import type { ClientConfig, ContentClient } from "./types";
2
2
  import { createContentAPIClient } from "./content-api";
3
- import { createTranslationsCDNClient } from "./translations-cdn";
4
3
 
5
- const DEFAULT_CDN_BASE = "https://cdn.better-i18n.com";
6
4
  const DEFAULT_API_BASE = "https://api.better-i18n.com";
7
5
 
8
6
  /**
9
- * Creates a Better i18n client with content and translation sub-clients.
7
+ * Creates a Better i18n content client.
10
8
  *
11
- * Content is always fetched from the REST API and requires an API key.
12
- * Translations are served from the CDN for performance (no API key needed).
9
+ * Fetches content models and entries from the REST API.
10
+ * Requires an API key for authentication.
13
11
  *
14
12
  * @example
15
13
  * ```typescript
@@ -19,31 +17,22 @@ const DEFAULT_API_BASE = "https://api.better-i18n.com";
19
17
  * apiKey: "bi18n_...",
20
18
  * });
21
19
  *
22
- * // Content (API mode, requires apiKey)
23
- * const posts = await client.content.getEntries("blog-posts", { language: "fr" });
24
- * const post = await client.content.getEntry("blog-posts", "hello-world");
25
- * const models = await client.content.getModels();
26
- *
27
- * // Translations (CDN mode, no API key needed)
28
- * const strings = await client.translations.get("common", { language: "fr" });
20
+ * const models = await client.getModels();
21
+ * const posts = await client.getEntries("blog-posts", { language: "fr" });
22
+ * const post = await client.getEntry("blog-posts", "hello-world");
29
23
  * ```
30
24
  */
31
- export function createClient(config: ClientConfig): BetterI18nClient {
25
+ export function createClient(config: ClientConfig): ContentClient {
32
26
  const { org, project } = config;
33
- const cdnBase = (config.cdnBase || DEFAULT_CDN_BASE).replace(/\/$/, "");
34
27
  const apiBase = (config.apiBase || DEFAULT_API_BASE).replace(/\/$/, "");
35
28
 
36
29
  if (!config.apiKey) {
37
30
  throw new Error(
38
- "API key is required. Content is served via the REST API.\n" +
31
+ "API key is required for content API access.\n" +
39
32
  "Set apiKey in your client config:\n\n" +
40
33
  ' createClient({ org: "...", project: "...", apiKey: "bi18n_..." })',
41
34
  );
42
35
  }
43
36
 
44
- return {
45
- content: createContentAPIClient(apiBase, org, project, config.apiKey),
46
- // Translations always served from CDN for performance
47
- translations: createTranslationsCDNClient(cdnBase, org, project),
48
- };
37
+ return createContentAPIClient(apiBase, org, project, config.apiKey);
49
38
  }
@@ -5,6 +5,7 @@ import type {
5
5
  ContentModel,
6
6
  ListEntriesOptions,
7
7
  GetEntryOptions,
8
+ PaginatedResponse,
8
9
  } from "./types";
9
10
 
10
11
  /**
@@ -42,9 +43,12 @@ export function createContentAPIClient(
42
43
  async getEntries(
43
44
  modelSlug: string,
44
45
  options?: ListEntriesOptions,
45
- ): Promise<ContentEntryListItem[]> {
46
+ ): Promise<PaginatedResponse<ContentEntryListItem>> {
46
47
  const params = new URLSearchParams();
47
48
  if (options?.language) params.set("language", options.language);
49
+ if (options?.status) params.set("status", options.status);
50
+ if (options?.sort) params.set("sort", options.sort);
51
+ if (options?.order) params.set("order", options.order);
48
52
  if (options?.page) params.set("page", String(options.page));
49
53
  if (options?.limit) params.set("limit", String(options.limit));
50
54
  const qs = params.toString() ? `?${params}` : "";
@@ -58,16 +62,19 @@ export function createContentAPIClient(
58
62
  `API error fetching entries for ${modelSlug}: ${res.status}`,
59
63
  );
60
64
  }
61
- const data: { items?: ContentEntryListItem[] } | ContentEntryListItem[] =
62
- await res.json();
63
- return Array.isArray(data) ? data : (data.items ?? []);
65
+ const data = await res.json() as { items: ContentEntryListItem[]; total: number; hasMore: boolean };
66
+ return {
67
+ items: data.items,
68
+ total: data.total,
69
+ hasMore: data.hasMore,
70
+ };
64
71
  },
65
72
 
66
- async getEntry(
73
+ async getEntry<CF extends Record<string, string | null> = Record<string, string | null>>(
67
74
  modelSlug: string,
68
75
  entrySlug: string,
69
76
  options?: GetEntryOptions,
70
- ): Promise<ContentEntry> {
77
+ ): Promise<ContentEntry<CF>> {
71
78
  const params = new URLSearchParams();
72
79
  if (options?.language) params.set("language", options.language);
73
80
  const qs = params.toString() ? `?${params}` : "";
package/src/index.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  export { createClient } from "./client";
2
+ export { createContentAPIClient } from "./content-api";
2
3
  export type {
3
4
  ClientConfig,
4
- BetterI18nClient,
5
5
  ContentClient,
6
- TranslationsClient,
7
6
  ContentEntry,
8
7
  ContentEntryListItem,
8
+ ContentEntryStatus,
9
+ ContentEntrySortField,
9
10
  ContentModel,
10
- TranslationManifest,
11
11
  ListEntriesOptions,
12
12
  GetEntryOptions,
13
- GetTranslationsOptions,
13
+ PaginatedResponse,
14
14
  } from "./types";
package/src/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // ─── Client Configuration ────────────────────────────────────────────
2
2
 
3
- /** Configuration for creating a Better i18n client. */
3
+ /** Configuration for creating a Better i18n content client. */
4
4
  export interface ClientConfig {
5
5
  /** Organization slug (e.g., "acme-corp"). */
6
6
  org: string;
@@ -8,26 +8,36 @@ export interface ClientConfig {
8
8
  project: string;
9
9
  /** API key for authenticating content requests. Required. */
10
10
  apiKey: string;
11
- /** CDN base URL for translations. Defaults to `https://cdn.better-i18n.com`. */
12
- cdnBase?: string;
13
- /** REST API base URL for content. Defaults to `https://api.better-i18n.com`. */
11
+ /** REST API base URL. Defaults to `https://api.better-i18n.com`. */
14
12
  apiBase?: string;
15
13
  }
16
14
 
17
15
  // ─── Content Types ───────────────────────────────────────────────────
18
16
 
19
- /** A full content entry with all localized fields. */
20
- export interface ContentEntry {
17
+ /**
18
+ * A full content entry with all localized fields.
19
+ *
20
+ * @typeParam CF - Custom fields shape. Defaults to `Record<string, string | null>`.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * // Typed custom fields
25
+ * interface BlogFields { readingTime: string | null; category: string | null }
26
+ * const post = await client.getEntry<BlogFields>("blog", "hello-world");
27
+ * post.customFields.readingTime; // string | null (typed!)
28
+ * ```
29
+ */
30
+ export interface ContentEntry<CF extends Record<string, string | null> = Record<string, string | null>> {
21
31
  id: string;
22
32
  slug: string;
23
- status: string;
33
+ status: "draft" | "published" | "archived";
24
34
  publishedAt: string | null;
25
35
  sourceLanguage: string;
26
36
  availableLanguages: string[];
27
37
  featuredImage: string | null;
28
38
  tags: string[];
29
39
  author: { name: string; image: string | null } | null;
30
- customFields: Record<string, string | null>;
40
+ customFields: CF;
31
41
  // Localized content
32
42
  title: string;
33
43
  excerpt: string | null;
@@ -38,6 +48,9 @@ export interface ContentEntry {
38
48
  metaDescription: string | null;
39
49
  }
40
50
 
51
+ /** Entry status filter values. */
52
+ export type ContentEntryStatus = "draft" | "published" | "archived";
53
+
41
54
  /** A summary item for content entry lists. */
42
55
  export interface ContentEntryListItem {
43
56
  slug: string;
@@ -49,6 +62,13 @@ export interface ContentEntryListItem {
49
62
  author: { name: string; image: string | null } | null;
50
63
  }
51
64
 
65
+ /** Paginated response wrapper. */
66
+ export interface PaginatedResponse<T> {
67
+ items: T[];
68
+ total: number;
69
+ hasMore: boolean;
70
+ }
71
+
52
72
  /** A content model definition. */
53
73
  export interface ContentModel {
54
74
  slug: string;
@@ -58,43 +78,30 @@ export interface ContentModel {
58
78
  entryCount: number;
59
79
  }
60
80
 
61
- // ─── Translation Types ───────────────────────────────────────────────
81
+ // ─── Client Interface ───────────────────────────────────────────────
62
82
 
63
- /** Manifest describing project languages and translation coverage. */
64
- export interface TranslationManifest {
65
- projectSlug: string;
66
- sourceLanguage: string;
67
- languages: Array<{
68
- code: string;
69
- name: string;
70
- nativeName: string;
71
- isSource: boolean;
72
- keyCount: number;
73
- }>;
74
- updatedAt: string;
75
- }
76
-
77
- // ─── Client Interfaces ──────────────────────────────────────────────
83
+ /** Sortable fields for content entries. */
84
+ export type ContentEntrySortField = "publishedAt" | "createdAt" | "updatedAt" | "title";
78
85
 
79
86
  /** Options for listing content entries. */
80
87
  export interface ListEntriesOptions {
81
- /** Language code for localized content. Defaults to `"en"`. */
88
+ /** Language code for localized content. Defaults to source language. */
82
89
  language?: string;
90
+ /** Filter by entry status. */
91
+ status?: ContentEntryStatus;
92
+ /** Field to sort by. Defaults to `"updatedAt"`. */
93
+ sort?: ContentEntrySortField;
94
+ /** Sort direction. Defaults to `"desc"`. */
95
+ order?: "asc" | "desc";
83
96
  /** Page number (1-based). */
84
97
  page?: number;
85
- /** Max entries per page. */
98
+ /** Max entries per page (1-100). Defaults to 50. */
86
99
  limit?: number;
87
100
  }
88
101
 
89
102
  /** Options for fetching a single content entry. */
90
103
  export interface GetEntryOptions {
91
- /** Language code for localized content. Defaults to `"en"`. */
92
- language?: string;
93
- }
94
-
95
- /** Options for fetching translations. */
96
- export interface GetTranslationsOptions {
97
- /** Language code. Defaults to `"en"`. */
104
+ /** Language code for localized content. Defaults to source language. */
98
105
  language?: string;
99
106
  }
100
107
 
@@ -102,34 +109,15 @@ export interface GetTranslationsOptions {
102
109
  export interface ContentClient {
103
110
  /** List all content models in the project. */
104
111
  getModels(): Promise<ContentModel[]>;
105
- /** List entries for a content model. */
112
+ /** List entries for a content model with pagination. */
106
113
  getEntries(
107
114
  modelSlug: string,
108
115
  options?: ListEntriesOptions,
109
- ): Promise<ContentEntryListItem[]>;
116
+ ): Promise<PaginatedResponse<ContentEntryListItem>>;
110
117
  /** Fetch a single content entry by slug. */
111
- getEntry(
118
+ getEntry<CF extends Record<string, string | null> = Record<string, string | null>>(
112
119
  modelSlug: string,
113
120
  entrySlug: string,
114
121
  options?: GetEntryOptions,
115
- ): Promise<ContentEntry>;
116
- }
117
-
118
- /** Client for fetching translation strings. */
119
- export interface TranslationsClient {
120
- /** Fetch translations for a namespace. */
121
- get(
122
- namespace: string,
123
- options?: GetTranslationsOptions,
124
- ): Promise<Record<string, string>>;
125
- /** Fetch the project translation manifest. */
126
- getManifest(): Promise<TranslationManifest>;
127
- }
128
-
129
- /** The unified Better i18n client. */
130
- export interface BetterI18nClient {
131
- /** Content sub-client for headless CMS operations. */
132
- content: ContentClient;
133
- /** Translations sub-client for i18n string fetching. */
134
- translations: TranslationsClient;
122
+ ): Promise<ContentEntry<CF>>;
135
123
  }
package/src/content.ts DELETED
@@ -1,9 +0,0 @@
1
- export { createContentAPIClient } from "./content-api";
2
- export type {
3
- ContentClient,
4
- ContentEntry,
5
- ContentEntryListItem,
6
- ContentModel,
7
- ListEntriesOptions,
8
- GetEntryOptions,
9
- } from "./types";
@@ -1,51 +0,0 @@
1
- import type {
2
- TranslationsClient,
3
- TranslationManifest,
4
- GetTranslationsOptions,
5
- } from "./types";
6
-
7
- /**
8
- * Creates a translations client that fetches from the CDN.
9
- *
10
- * Translation files are pre-built JSON served from Cloudflare R2.
11
- * No authentication required.
12
- *
13
- * URL patterns:
14
- * - Namespace: `{cdnBase}/{org}/{project}/{lang}/{namespace}.json`
15
- * - Manifest: `{cdnBase}/{org}/{project}/manifest.json`
16
- */
17
- export function createTranslationsCDNClient(
18
- cdnBase: string,
19
- org: string,
20
- project: string,
21
- ): TranslationsClient {
22
- const base = `${cdnBase}/${org}/${project}`;
23
-
24
- return {
25
- async get(
26
- namespace: string,
27
- options?: GetTranslationsOptions,
28
- ): Promise<Record<string, string>> {
29
- const lang = options?.language || "en";
30
- const ns = namespace || "default";
31
- const res = await fetch(`${base}/${lang}/${ns}.json`);
32
- if (!res.ok) {
33
- if (res.status === 404) return {};
34
- throw new Error(
35
- `Failed to fetch translations for ${ns} (${lang}): ${res.status}`,
36
- );
37
- }
38
- return res.json();
39
- },
40
-
41
- async getManifest(): Promise<TranslationManifest> {
42
- const res = await fetch(`${base}/manifest.json`);
43
- if (!res.ok) {
44
- throw new Error(
45
- `Failed to fetch translation manifest: ${res.status}`,
46
- );
47
- }
48
- return res.json();
49
- },
50
- };
51
- }
@@ -1,6 +0,0 @@
1
- export { createTranslationsCDNClient } from "./translations-cdn";
2
- export type {
3
- TranslationsClient,
4
- TranslationManifest,
5
- GetTranslationsOptions,
6
- } from "./types";