@better-i18n/next 0.1.0 → 0.1.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
@@ -1,6 +1,6 @@
1
1
  # @better-i18n/next
2
2
 
3
- Headless Next.js integration for Better i18n. This package owns your `next-intl` wiring, middleware/proxy, and CDN fetching.
3
+ Next.js integration for [Better i18n](https://better-i18n.com). Handles `next-intl` wiring, middleware, and CDN-based translation fetching.
4
4
 
5
5
  ## Install
6
6
 
@@ -8,20 +8,23 @@ Headless Next.js integration for Better i18n. This package owns your `next-intl`
8
8
  npm i @better-i18n/next next-intl
9
9
  ```
10
10
 
11
- ## Quick start
11
+ ## Quick Start
12
+
13
+ ### 1. Create i18n config
12
14
 
13
15
  ```ts
14
16
  // i18n.ts
15
17
  import { createI18n } from "@better-i18n/next";
16
18
 
17
19
  export const i18n = createI18n({
18
- workspaceId: "ws_123",
19
- projectSlug: "landing",
20
+ project: "your-org/your-project", // Format: "org-slug/project-slug"
20
21
  defaultLocale: "en",
21
22
  debug: process.env.NODE_ENV !== "production",
22
23
  });
23
24
  ```
24
25
 
26
+ ### 2. Setup request config
27
+
25
28
  ```ts
26
29
  // app/i18n/request.ts
27
30
  import { i18n } from "./i18n";
@@ -29,25 +32,20 @@ import { i18n } from "./i18n";
29
32
  export default i18n.requestConfig;
30
33
  ```
31
34
 
35
+ ### 3. Add middleware
36
+
32
37
  ```ts
33
- // middleware.ts (Next 15.x)
38
+ // middleware.ts
34
39
  import { i18n } from "./i18n";
35
40
 
36
41
  export default i18n.middleware;
37
42
 
38
43
  export const config = {
39
- matcher: ["/", "/((?!api|_next|_vercel|.*\\..*).*)"],
44
+ matcher: ["/", "/((?!api|_next|_vercel|.*\\..*).*)" ],
40
45
  };
41
46
  ```
42
47
 
43
- ```ts
44
- // proxy.ts (Next 16+)
45
- import { i18n } from "./i18n";
46
-
47
- export default i18n.proxy;
48
- ```
49
-
50
- ## Client hook
48
+ ## Client Hook
51
49
 
52
50
  ```tsx
53
51
  "use client";
@@ -56,8 +54,7 @@ import { useManifestLanguages } from "@better-i18n/next/client";
56
54
 
57
55
  export function LanguageSwitcher() {
58
56
  const { languages, isLoading, error } = useManifestLanguages({
59
- workspaceId: "ws_123",
60
- projectSlug: "landing",
57
+ project: "your-org/your-project",
61
58
  defaultLocale: "en",
62
59
  });
63
60
 
@@ -74,27 +71,33 @@ export function LanguageSwitcher() {
74
71
  }
75
72
  ```
76
73
 
77
- ## Server helpers
74
+ ## Server Helpers
78
75
 
79
76
  ```ts
80
77
  import { getLocales, getMessages } from "@better-i18n/next/server";
81
78
 
82
- const locales = await getLocales({ workspaceId, projectSlug, defaultLocale });
83
- const messages = await getMessages({ workspaceId, projectSlug, defaultLocale }, "tr");
79
+ const config = { project: "your-org/your-project", defaultLocale: "en" };
80
+
81
+ const locales = await getLocales(config);
82
+ const messages = await getMessages(config, "tr");
84
83
  ```
85
84
 
86
- ## Debug logging
85
+ ## Configuration Options
87
86
 
88
- - `BETTER_I18N_DEBUG=1`
89
- - `BETTER_I18N_LOG_LEVEL=debug|info|warn|error|silent`
87
+ | Option | Type | Description |
88
+ |--------|------|-------------|
89
+ | `project` | `string` | **Required.** Format: `"org-slug/project-slug"` |
90
+ | `defaultLocale` | `string` | **Required.** Default locale code (e.g., `"en"`) |
91
+ | `cdnBaseUrl` | `string` | CDN URL (default: `https://cdn.better-i18n.com`) |
92
+ | `debug` | `boolean` | Enable verbose logging |
93
+ | `localePrefix` | `"as-needed" \| "always" \| "never"` | URL prefix strategy |
90
94
 
91
- `debug` in config also enables verbose logs.
95
+ ## Debug Logging
92
96
 
93
- ## Notes
94
-
95
- - Fail-fast: CDN errors throw (no local fallback).
96
- - For Next apps, ensure `transpilePackages: ["@better-i18n/next"]` if your bundler doesn’t compile TS in node_modules.
97
+ Enable with environment variables:
98
+ - `BETTER_I18N_DEBUG=1`
99
+ - `BETTER_I18N_LOG_LEVEL=debug|info|warn|error|silent`
97
100
 
98
101
  ## License
99
102
 
100
- MIT
103
+ MIT © [Better i18n](https://better-i18n.com)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-i18n/next",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Better-i18n Next.js integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,6 +32,10 @@
32
32
  "types": "./src/middleware.ts",
33
33
  "default": "./src/middleware.ts"
34
34
  },
35
+ "./proxy": {
36
+ "types": "./src/proxy.ts",
37
+ "default": "./src/proxy.ts"
38
+ },
35
39
  "./package.json": "./package.json"
36
40
  },
37
41
  "files": [
@@ -52,6 +56,9 @@
52
56
  "scripts": {
53
57
  "typecheck": "tsc --noEmit"
54
58
  },
59
+ "dependencies": {
60
+ "@better-i18n/core": "workspace:*"
61
+ },
55
62
  "peerDependencies": {
56
63
  "next": ">=15.0.0",
57
64
  "next-intl": ">=4.0.0",
@@ -61,7 +68,7 @@
61
68
  "devDependencies": {
62
69
  "next": "^15.4.4",
63
70
  "next-intl": "^4.5.8",
64
- "@repo/typescript-config": "workspace:*",
71
+ "@better-i18n/typescript-config": "workspace:*",
65
72
  "@types/node": "^20.0.0",
66
73
  "@types/react": "^19.0.0",
67
74
  "typescript": "~5.9.2"
package/src/client.ts CHANGED
@@ -26,7 +26,7 @@ type ClientCacheEntry = {
26
26
  const clientCache = new Map<string, ClientCacheEntry>();
27
27
 
28
28
  const getCacheKey = (config: I18nConfig) =>
29
- `${config.cdnBaseUrl || "https://cdn.better-i18n.com"}|${config.workspaceId}|${config.projectSlug}`;
29
+ `${config.cdnBaseUrl || "https://cdn.better-i18n.com"}|${config.project}`;
30
30
 
31
31
  const fetchManifestLanguages = async (
32
32
  config: I18nConfig,
@@ -62,8 +62,7 @@ export const useManifestLanguages = (
62
62
  const normalized = useMemo(
63
63
  () => normalizeConfig(config),
64
64
  [
65
- config.workspaceId,
66
- config.projectSlug,
65
+ config.project,
67
66
  config.defaultLocale,
68
67
  config.cdnBaseUrl,
69
68
  config.localePrefix,
package/src/config.ts CHANGED
@@ -1,27 +1,39 @@
1
+ import {
2
+ parseProject,
3
+ getProjectBaseUrl as coreGetProjectBaseUrl,
4
+ normalizeConfig as coreNormalizeConfig,
5
+ } from "@better-i18n/core";
1
6
  import type { I18nConfig, NormalizedConfig } from "./types";
2
7
 
3
- const DEFAULT_CDN_BASE_URL = "https://cdn.better-i18n.com";
8
+ // Re-export parseProject from core
9
+ export { parseProject };
4
10
 
11
+ /**
12
+ * Normalize Next.js i18n config with defaults
13
+ */
5
14
  export const normalizeConfig = (config: I18nConfig): NormalizedConfig => {
6
- if (!config.workspaceId?.trim()) {
7
- throw new Error("[better-i18n] workspaceId is required");
8
- }
9
- if (!config.projectSlug?.trim()) {
10
- throw new Error("[better-i18n] projectSlug is required");
11
- }
12
- if (!config.defaultLocale?.trim()) {
13
- throw new Error("[better-i18n] defaultLocale is required");
14
- }
15
+ // Use core normalization for base config
16
+ const coreConfig = coreNormalizeConfig({
17
+ project: config.project,
18
+ defaultLocale: config.defaultLocale,
19
+ cdnBaseUrl: config.cdnBaseUrl,
20
+ manifestCacheTtlMs: config.manifestCacheTtlMs,
21
+ debug: config.debug,
22
+ logLevel: config.logLevel,
23
+ fetch: config.fetch,
24
+ });
15
25
 
26
+ // Add Next.js-specific defaults
16
27
  return {
17
- ...config,
18
- cdnBaseUrl: config.cdnBaseUrl?.replace(/\/$/, "") || DEFAULT_CDN_BASE_URL,
28
+ ...coreConfig,
19
29
  localePrefix: config.localePrefix ?? "as-needed",
20
- manifestCacheTtlMs: config.manifestCacheTtlMs ?? 5 * 60 * 1000,
21
30
  manifestRevalidateSeconds: config.manifestRevalidateSeconds ?? 3600,
22
31
  messagesRevalidateSeconds: config.messagesRevalidateSeconds ?? 30,
23
32
  };
24
33
  };
25
34
 
26
- export const getProjectBaseUrl = (config: NormalizedConfig) =>
27
- `${config.cdnBaseUrl}/${config.workspaceId}/${config.projectSlug}`;
35
+ /**
36
+ * Get the project base URL on CDN
37
+ */
38
+ export const getProjectBaseUrl = (config: NormalizedConfig): string =>
39
+ coreGetProjectBaseUrl(config);
package/src/core.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { normalizeConfig, getProjectBaseUrl } from "./config";
1
+ import { getProjectBaseUrl, normalizeConfig } from "./config";
2
2
  import { createLogger } from "./logger";
3
3
  import { extractLanguages } from "./manifest";
4
4
  import type {
@@ -18,7 +18,7 @@ type ManifestCacheEntry = {
18
18
  const manifestCache = new Map<string, ManifestCacheEntry>();
19
19
 
20
20
  const buildCacheKey = (config: NormalizedConfig) =>
21
- `${config.cdnBaseUrl}|${config.workspaceId}|${config.projectSlug}`;
21
+ `${config.cdnBaseUrl}|${config.project}`;
22
22
 
23
23
  const withRevalidate = (
24
24
  init: NextFetchRequestInit,
package/src/logger.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { I18nConfig, Logger, LogLevel } from "./types";
1
+ import type { Logger, LogLevel } from "@better-i18n/core";
2
+ import type { I18nConfig } from "./types";
2
3
 
3
4
  const LEVEL_RANK: Record<LogLevel, number> = {
4
5
  debug: 0,
package/src/manifest.ts CHANGED
@@ -1,28 +1,2 @@
1
- import type {
2
- LanguageOption,
3
- ManifestLanguage,
4
- ManifestResponse,
5
- } from "./types";
6
-
7
- const normalizeLanguage = (language: ManifestLanguage): LanguageOption => ({
8
- code: language.code,
9
- name: language.name,
10
- nativeName:
11
- language.nativeName || language.name || language.code.toUpperCase(),
12
- flagUrl: language.flagUrl ?? null,
13
- });
14
-
15
- export const extractLanguages = (
16
- manifest: ManifestResponse,
17
- ): LanguageOption[] => {
18
- const languages = Array.isArray(manifest.languages)
19
- ? manifest.languages
20
- : [];
21
-
22
- return languages
23
- .filter(
24
- (language): language is ManifestLanguage =>
25
- !!language && typeof language.code === "string" && language.code.length > 0,
26
- )
27
- .map(normalizeLanguage);
28
- };
1
+ // Re-export from i18n-core
2
+ export { extractLanguages } from "@better-i18n/core";
package/src/middleware.ts CHANGED
@@ -1,11 +1,18 @@
1
1
  import createMiddleware from "next-intl/middleware";
2
- import type { NextRequest } from "next/server";
2
+ import { NextRequest, NextResponse } from "next/server";
3
+ // @ts-ignore - internal workspace dependency
4
+ import { detectLocale, getLocales as getCDNLocales } from "@better-i18n/core";
5
+ // @ts-ignore - internal workspace dependency
6
+ import type { I18nMiddlewareConfig } from "@better-i18n/core";
3
7
 
4
8
  import { normalizeConfig } from "./config";
5
9
  import { createLogger } from "./logger";
6
10
  import { getLocales } from "./core";
7
11
  import type { I18nConfig } from "./types";
8
12
 
13
+ /**
14
+ * Legacy Next-intl based middleware
15
+ */
9
16
  export const createI18nMiddleware = (config: I18nConfig) => {
10
17
  const normalized = normalizeConfig(config);
11
18
  const logger = createLogger(normalized, "middleware");
@@ -25,3 +32,75 @@ export const createI18nMiddleware = (config: I18nConfig) => {
25
32
  };
26
33
 
27
34
  export const createI18nProxy = createI18nMiddleware;
35
+
36
+ /**
37
+ * Modern composable middleware for Better i18n
38
+ */
39
+ export function createBetterI18nMiddleware(config: I18nMiddlewareConfig) {
40
+ const { project, defaultLocale, detection = {} } = config;
41
+
42
+ const {
43
+ cookie = true,
44
+ browserLanguage = true,
45
+ cookieName = "locale",
46
+ cookieMaxAge = 31536000,
47
+ } = detection;
48
+
49
+ return async (request: NextRequest): Promise<NextResponse> => {
50
+ // 1. Fetch available locales from CDN
51
+ const availableLocales = await getCDNLocales({ project });
52
+
53
+ // 2. Extract locale indicators
54
+ const pathLocale = request.nextUrl.pathname.split("/")[1];
55
+ const cookieLocale = cookie ? request.cookies.get(cookieName)?.value : null;
56
+ const headerLocale = browserLanguage
57
+ ? request.headers.get("accept-language")?.split(",")[0]?.split("-")[0]
58
+ : null;
59
+
60
+ // 3. Detect locale using core logic
61
+ const result = detectLocale({
62
+ project,
63
+ defaultLocale,
64
+ pathLocale,
65
+ cookieLocale: cookieLocale || null,
66
+ headerLocale,
67
+ availableLocales,
68
+ });
69
+
70
+ // 4. Create response
71
+ const response = NextResponse.next();
72
+
73
+ // 5. Set cookie if needed
74
+ if (cookie && result.shouldSetCookie) {
75
+ response.cookies.set(cookieName, result.locale, {
76
+ path: "/",
77
+ maxAge: cookieMaxAge,
78
+ sameSite: "lax",
79
+ });
80
+ }
81
+
82
+ return response;
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Helper to compose multiple Next.js middleware
88
+ */
89
+ export function composeMiddleware(
90
+ ...middlewares: Array<(req: NextRequest) => Promise<NextResponse>>
91
+ ) {
92
+ return async (request: NextRequest): Promise<NextResponse> => {
93
+ let response = NextResponse.next();
94
+
95
+ for (const middleware of middlewares) {
96
+ response = await middleware(request);
97
+
98
+ // Short-circuit on redirect/rewrite (status >= 300)
99
+ if (response.status >= 300) {
100
+ break;
101
+ }
102
+ }
103
+
104
+ return response;
105
+ };
106
+ }
package/src/proxy.ts ADDED
@@ -0,0 +1 @@
1
+ export { createI18nProxy } from "./middleware";
package/src/types.ts CHANGED
@@ -1,68 +1,57 @@
1
- export type Locale = string;
1
+ // Re-export common types from i18n-core
2
+ export type {
3
+ Locale,
4
+ LogLevel,
5
+ ManifestLanguage,
6
+ ManifestFile,
7
+ ManifestResponse,
8
+ LanguageOption,
9
+ Messages,
10
+ Logger,
11
+ ParsedProject,
12
+ } from "@better-i18n/core";
13
+
14
+ import type { I18nCoreConfig, NormalizedConfig as CoreNormalizedConfig } from "@better-i18n/core";
15
+
16
+ // Next.js-specific types
2
17
 
3
18
  export type LocalePrefix = "as-needed" | "always" | "never";
4
19
 
5
- export type LogLevel = "debug" | "info" | "warn" | "error" | "silent";
6
-
7
- export interface I18nConfig {
8
- workspaceId: string;
9
- projectSlug: string;
10
- defaultLocale: string;
11
- cdnBaseUrl?: string;
20
+ /**
21
+ * Next.js i18n configuration (extends core config)
22
+ */
23
+ export interface I18nConfig extends I18nCoreConfig {
24
+ /**
25
+ * URL locale prefix behavior
26
+ * @default "as-needed"
27
+ */
12
28
  localePrefix?: LocalePrefix;
13
- debug?: boolean;
14
- logLevel?: LogLevel;
15
- manifestCacheTtlMs?: number;
29
+
30
+ /**
31
+ * Next.js ISR revalidation time for manifest (seconds)
32
+ * @default 3600
33
+ */
16
34
  manifestRevalidateSeconds?: number;
35
+
36
+ /**
37
+ * Next.js ISR revalidation time for messages (seconds)
38
+ * @default 30
39
+ */
17
40
  messagesRevalidateSeconds?: number;
18
- fetch?: typeof fetch;
19
41
  }
20
42
 
21
- export interface NormalizedConfig extends I18nConfig {
22
- cdnBaseUrl: string;
43
+ /**
44
+ * Normalized Next.js config with defaults
45
+ */
46
+ export interface NormalizedConfig extends CoreNormalizedConfig, Omit<I18nConfig, keyof I18nCoreConfig> {
23
47
  localePrefix: LocalePrefix;
48
+ manifestRevalidateSeconds: number;
49
+ messagesRevalidateSeconds: number;
24
50
  }
25
51
 
26
- export interface ManifestLanguage {
27
- code: string;
28
- name?: string;
29
- nativeName?: string;
30
- flagUrl?: string | null;
31
- isSource?: boolean;
32
- lastUpdated?: string | null;
33
- keyCount?: number;
34
- }
35
-
36
- export interface ManifestFile {
37
- url: string;
38
- size: number;
39
- lastModified: string | null;
40
- }
41
-
42
- export interface ManifestResponse {
43
- projectSlug?: string;
44
- sourceLanguage?: string;
45
- languages: ManifestLanguage[];
46
- files?: Record<string, ManifestFile>;
47
- updatedAt?: string;
48
- }
49
-
50
- export interface LanguageOption {
51
- code: string;
52
- name?: string;
53
- nativeName?: string;
54
- flagUrl?: string | null;
55
- }
56
-
57
- export type Messages = Record<string, any>;
58
-
59
- export interface Logger {
60
- debug: (...args: unknown[]) => void;
61
- info: (...args: unknown[]) => void;
62
- warn: (...args: unknown[]) => void;
63
- error: (...args: unknown[]) => void;
64
- }
65
-
52
+ /**
53
+ * Next.js fetch request init with ISR options
54
+ */
66
55
  export type NextFetchRequestInit = RequestInit & {
67
56
  next?: {
68
57
  revalidate?: number;