@better-i18n/sdk 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@better-i18n/sdk",
3
+ "version": "0.1.1",
4
+ "description": "SDK for fetching content and translations from Better i18n",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
+ "exports": {
9
+ ".": "./src/index.ts",
10
+ "./content": "./src/content.ts",
11
+ "./translations": "./src/translations.ts"
12
+ },
13
+ "files": [
14
+ "src",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "typecheck": "tsc --noEmit"
19
+ },
20
+ "keywords": [
21
+ "i18n",
22
+ "translations",
23
+ "content",
24
+ "sdk",
25
+ "headless-cms"
26
+ ],
27
+ "devDependencies": {
28
+ "typescript": "~5.9.3"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "license": "MIT"
34
+ }
package/src/client.ts ADDED
@@ -0,0 +1,49 @@
1
+ import type { ClientConfig, BetterI18nClient } from "./types";
2
+ import { createContentAPIClient } from "./content-api";
3
+ import { createTranslationsCDNClient } from "./translations-cdn";
4
+
5
+ const DEFAULT_CDN_BASE = "https://cdn.better-i18n.com";
6
+ const DEFAULT_API_BASE = "https://api.better-i18n.com";
7
+
8
+ /**
9
+ * Creates a Better i18n client with content and translation sub-clients.
10
+ *
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).
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const client = createClient({
17
+ * org: "acme",
18
+ * project: "web-app",
19
+ * apiKey: "bi18n_...",
20
+ * });
21
+ *
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" });
29
+ * ```
30
+ */
31
+ export function createClient(config: ClientConfig): BetterI18nClient {
32
+ const { org, project } = config;
33
+ const cdnBase = (config.cdnBase || DEFAULT_CDN_BASE).replace(/\/$/, "");
34
+ const apiBase = (config.apiBase || DEFAULT_API_BASE).replace(/\/$/, "");
35
+
36
+ if (!config.apiKey) {
37
+ throw new Error(
38
+ "API key is required. Content is served via the REST API.\n" +
39
+ "Set apiKey in your client config:\n\n" +
40
+ ' createClient({ org: "...", project: "...", apiKey: "bi18n_..." })',
41
+ );
42
+ }
43
+
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
+ };
49
+ }
@@ -0,0 +1,87 @@
1
+ import type {
2
+ ContentClient,
3
+ ContentEntry,
4
+ ContentEntryListItem,
5
+ ContentModel,
6
+ ListEntriesOptions,
7
+ GetEntryOptions,
8
+ } from "./types";
9
+
10
+ /**
11
+ * Creates a content client that fetches from the REST API.
12
+ *
13
+ * This mode supports filtering, pagination, and real-time data (no caching lag).
14
+ * Requires an API key for authentication via the `x-api-key` header.
15
+ *
16
+ * URL patterns:
17
+ * - Models: `{apiBase}/api/v1/content/{org}/{project}/models`
18
+ * - Entries: `{apiBase}/api/v1/content/{org}/{project}/models/{model}/entries`
19
+ * - Entry: `{apiBase}/api/v1/content/{org}/{project}/models/{model}/entries/{slug}`
20
+ */
21
+ export function createContentAPIClient(
22
+ apiBase: string,
23
+ org: string,
24
+ project: string,
25
+ apiKey: string,
26
+ ): ContentClient {
27
+ const base = `${apiBase}/api/v1/content/${org}/${project}`;
28
+ const headers: Record<string, string> = {
29
+ "x-api-key": apiKey,
30
+ "content-type": "application/json",
31
+ };
32
+
33
+ return {
34
+ async getModels(): Promise<ContentModel[]> {
35
+ const res = await fetch(`${base}/models`, { headers });
36
+ if (!res.ok) {
37
+ throw new Error(`API error fetching models: ${res.status}`);
38
+ }
39
+ return res.json();
40
+ },
41
+
42
+ async getEntries(
43
+ modelSlug: string,
44
+ options?: ListEntriesOptions,
45
+ ): Promise<ContentEntryListItem[]> {
46
+ const params = new URLSearchParams();
47
+ if (options?.language) params.set("language", options.language);
48
+ if (options?.page) params.set("page", String(options.page));
49
+ if (options?.limit) params.set("limit", String(options.limit));
50
+ const qs = params.toString() ? `?${params}` : "";
51
+
52
+ const res = await fetch(
53
+ `${base}/models/${modelSlug}/entries${qs}`,
54
+ { headers },
55
+ );
56
+ if (!res.ok) {
57
+ throw new Error(
58
+ `API error fetching entries for ${modelSlug}: ${res.status}`,
59
+ );
60
+ }
61
+ const data: { items?: ContentEntryListItem[] } | ContentEntryListItem[] =
62
+ await res.json();
63
+ return Array.isArray(data) ? data : (data.items ?? []);
64
+ },
65
+
66
+ async getEntry(
67
+ modelSlug: string,
68
+ entrySlug: string,
69
+ options?: GetEntryOptions,
70
+ ): Promise<ContentEntry> {
71
+ const params = new URLSearchParams();
72
+ if (options?.language) params.set("language", options.language);
73
+ const qs = params.toString() ? `?${params}` : "";
74
+
75
+ const res = await fetch(
76
+ `${base}/models/${modelSlug}/entries/${entrySlug}${qs}`,
77
+ { headers },
78
+ );
79
+ if (!res.ok) {
80
+ throw new Error(
81
+ `API error fetching entry ${modelSlug}/${entrySlug}: ${res.status}`,
82
+ );
83
+ }
84
+ return res.json();
85
+ },
86
+ };
87
+ }
package/src/content.ts ADDED
@@ -0,0 +1,9 @@
1
+ export { createContentAPIClient } from "./content-api";
2
+ export type {
3
+ ContentClient,
4
+ ContentEntry,
5
+ ContentEntryListItem,
6
+ ContentModel,
7
+ ListEntriesOptions,
8
+ GetEntryOptions,
9
+ } from "./types";
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export { createClient } from "./client";
2
+ export type {
3
+ ClientConfig,
4
+ BetterI18nClient,
5
+ ContentClient,
6
+ TranslationsClient,
7
+ ContentEntry,
8
+ ContentEntryListItem,
9
+ ContentModel,
10
+ TranslationManifest,
11
+ ListEntriesOptions,
12
+ GetEntryOptions,
13
+ GetTranslationsOptions,
14
+ } from "./types";
@@ -0,0 +1,51 @@
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
+ }
@@ -0,0 +1,6 @@
1
+ export { createTranslationsCDNClient } from "./translations-cdn";
2
+ export type {
3
+ TranslationsClient,
4
+ TranslationManifest,
5
+ GetTranslationsOptions,
6
+ } from "./types";
package/src/types.ts ADDED
@@ -0,0 +1,135 @@
1
+ // ─── Client Configuration ────────────────────────────────────────────
2
+
3
+ /** Configuration for creating a Better i18n client. */
4
+ export interface ClientConfig {
5
+ /** Organization slug (e.g., "acme-corp"). */
6
+ org: string;
7
+ /** Project slug (e.g., "web-app"). */
8
+ project: string;
9
+ /** API key for authenticating content requests. Required. */
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`. */
14
+ apiBase?: string;
15
+ }
16
+
17
+ // ─── Content Types ───────────────────────────────────────────────────
18
+
19
+ /** A full content entry with all localized fields. */
20
+ export interface ContentEntry {
21
+ id: string;
22
+ slug: string;
23
+ status: string;
24
+ publishedAt: string | null;
25
+ sourceLanguage: string;
26
+ availableLanguages: string[];
27
+ featuredImage: string | null;
28
+ tags: string[];
29
+ author: { name: string; image: string | null } | null;
30
+ customFields: Record<string, string | null>;
31
+ // Localized content
32
+ title: string;
33
+ excerpt: string | null;
34
+ body: unknown;
35
+ bodyHtml: string | null;
36
+ bodyMarkdown: string | null;
37
+ metaTitle: string | null;
38
+ metaDescription: string | null;
39
+ }
40
+
41
+ /** A summary item for content entry lists. */
42
+ export interface ContentEntryListItem {
43
+ slug: string;
44
+ title: string;
45
+ excerpt: string | null;
46
+ publishedAt: string | null;
47
+ featuredImage: string | null;
48
+ tags: string[];
49
+ author: { name: string; image: string | null } | null;
50
+ }
51
+
52
+ /** A content model definition. */
53
+ export interface ContentModel {
54
+ slug: string;
55
+ displayName: string;
56
+ description: string | null;
57
+ kind: string;
58
+ entryCount: number;
59
+ }
60
+
61
+ // ─── Translation Types ───────────────────────────────────────────────
62
+
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 ──────────────────────────────────────────────
78
+
79
+ /** Options for listing content entries. */
80
+ export interface ListEntriesOptions {
81
+ /** Language code for localized content. Defaults to `"en"`. */
82
+ language?: string;
83
+ /** Page number (1-based). */
84
+ page?: number;
85
+ /** Max entries per page. */
86
+ limit?: number;
87
+ }
88
+
89
+ /** Options for fetching a single content entry. */
90
+ 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"`. */
98
+ language?: string;
99
+ }
100
+
101
+ /** Client for fetching content entries and models. */
102
+ export interface ContentClient {
103
+ /** List all content models in the project. */
104
+ getModels(): Promise<ContentModel[]>;
105
+ /** List entries for a content model. */
106
+ getEntries(
107
+ modelSlug: string,
108
+ options?: ListEntriesOptions,
109
+ ): Promise<ContentEntryListItem[]>;
110
+ /** Fetch a single content entry by slug. */
111
+ getEntry(
112
+ modelSlug: string,
113
+ entrySlug: string,
114
+ 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;
135
+ }