@better-i18n/server 0.2.1 → 0.3.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/README.md +177 -0
- package/dist/node.d.ts +4 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js.map +1 -1
- package/dist/providers/better-auth.d.ts +101 -0
- package/dist/providers/better-auth.d.ts.map +1 -0
- package/dist/providers/better-auth.js +187 -0
- package/dist/providers/better-auth.js.map +1 -0
- package/package.json +11 -3
package/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# @better-i18n/server
|
|
2
|
+
|
|
3
|
+
Framework-agnostic server-side i18n for [Better i18n](https://better-i18n.com). Built-in support for Hono, Express, Fastify, and any Node.js HTTP server.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@better-i18n/server)
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Framework Agnostic** — Core API uses Web Standards `Headers`, works everywhere
|
|
10
|
+
- **Hono Middleware** — First-class middleware with typed context variables
|
|
11
|
+
- **Express/Fastify Middleware** — Node.js adapter with `req.locale` and `req.t`
|
|
12
|
+
- **Accept-Language Detection** — RFC 5646 compliant locale matching
|
|
13
|
+
- **CDN-Cached** — Singleton pattern with shared TtlCache across all requests
|
|
14
|
+
- **Type-Safe Translations** — Built on `use-intl/core` for consistent formatting
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @better-i18n/server
|
|
20
|
+
# or
|
|
21
|
+
bun add @better-i18n/server
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### 1. Create a singleton instance
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// i18n.ts (module scope — shared across all requests)
|
|
30
|
+
import { createServerI18n } from "@better-i18n/server";
|
|
31
|
+
|
|
32
|
+
export const i18n = createServerI18n({
|
|
33
|
+
project: "acme/api",
|
|
34
|
+
defaultLocale: "en",
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 2. Use with your framework
|
|
39
|
+
|
|
40
|
+
#### Hono
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { Hono } from "hono";
|
|
44
|
+
import { betterI18n } from "@better-i18n/server/hono";
|
|
45
|
+
import type { Translator } from "@better-i18n/server";
|
|
46
|
+
import { i18n } from "./i18n";
|
|
47
|
+
|
|
48
|
+
const app = new Hono<{
|
|
49
|
+
Variables: {
|
|
50
|
+
locale: string;
|
|
51
|
+
t: Translator;
|
|
52
|
+
};
|
|
53
|
+
}>();
|
|
54
|
+
|
|
55
|
+
app.use("*", betterI18n(i18n));
|
|
56
|
+
|
|
57
|
+
app.get("/users/:id", (c) => {
|
|
58
|
+
const t = c.get("t");
|
|
59
|
+
const locale = c.get("locale");
|
|
60
|
+
return c.json({ message: t("users.welcome"), locale });
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
#### Express
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import express from "express";
|
|
68
|
+
import { betterI18nMiddleware } from "@better-i18n/server/node";
|
|
69
|
+
import { i18n } from "./i18n";
|
|
70
|
+
|
|
71
|
+
const app = express();
|
|
72
|
+
app.use(betterI18nMiddleware(i18n));
|
|
73
|
+
|
|
74
|
+
app.get("/users/:id", (req, res) => {
|
|
75
|
+
res.json({ message: req.t("users.welcome"), locale: req.locale });
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
TypeScript augmentation (add to a `.d.ts` file):
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import type { Translator } from "@better-i18n/server";
|
|
83
|
+
|
|
84
|
+
declare global {
|
|
85
|
+
namespace Express {
|
|
86
|
+
interface Request {
|
|
87
|
+
locale: string;
|
|
88
|
+
t: Translator;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
#### Fastify / Koa / Custom
|
|
95
|
+
|
|
96
|
+
Use `fromNodeHeaders` to convert Node.js headers to Web Standards `Headers`:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { fromNodeHeaders } from "@better-i18n/server/node";
|
|
100
|
+
import { i18n } from "./i18n";
|
|
101
|
+
|
|
102
|
+
// In any request handler
|
|
103
|
+
const headers = fromNodeHeaders(req.headers);
|
|
104
|
+
const locale = await i18n.detectLocaleFromHeaders(headers);
|
|
105
|
+
const t = await i18n.getTranslator(locale);
|
|
106
|
+
|
|
107
|
+
t("errors.notFound"); // → "Bulunamadı"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### Direct API (no middleware)
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { i18n } from "./i18n";
|
|
114
|
+
|
|
115
|
+
// Translate with any locale
|
|
116
|
+
const t = await i18n.getTranslator("tr");
|
|
117
|
+
t("errors.notFound"); // → "Bulunamadı"
|
|
118
|
+
|
|
119
|
+
// With namespace
|
|
120
|
+
const tAuth = await i18n.getTranslator("tr", "auth");
|
|
121
|
+
tAuth("loginRequired"); // → "Giriş yapmanız gerekiyor"
|
|
122
|
+
|
|
123
|
+
// Get available locales
|
|
124
|
+
const locales = await i18n.getLocales();
|
|
125
|
+
// ["en", "tr", "de"]
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## API
|
|
129
|
+
|
|
130
|
+
### `createServerI18n(config)`
|
|
131
|
+
|
|
132
|
+
Creates a singleton i18n instance. Call once at module scope.
|
|
133
|
+
|
|
134
|
+
| Option | Type | Default | Description |
|
|
135
|
+
|--------|------|---------|-------------|
|
|
136
|
+
| `project` | `string` | — | Project identifier (`"org/project"`) |
|
|
137
|
+
| `defaultLocale` | `string` | — | Fallback locale |
|
|
138
|
+
| `cdnBaseUrl` | `string` | CDN default | CDN base URL override |
|
|
139
|
+
| `debug` | `boolean` | `false` | Enable debug logging |
|
|
140
|
+
|
|
141
|
+
### Instance Methods
|
|
142
|
+
|
|
143
|
+
| Method | Returns | Description |
|
|
144
|
+
|--------|---------|-------------|
|
|
145
|
+
| `getTranslator(locale, namespace?)` | `Promise<Translator>` | Get a translator function for a locale |
|
|
146
|
+
| `detectLocaleFromHeaders(headers)` | `Promise<string>` | Detect locale from Web Standards `Headers` |
|
|
147
|
+
| `getLocales()` | `Promise<string[]>` | Get available locale codes |
|
|
148
|
+
| `getLanguages()` | `Promise<LanguageOption[]>` | Get languages with metadata |
|
|
149
|
+
|
|
150
|
+
### Entry Points
|
|
151
|
+
|
|
152
|
+
| Import | Use Case |
|
|
153
|
+
|--------|----------|
|
|
154
|
+
| `@better-i18n/server` | Core `createServerI18n`, types |
|
|
155
|
+
| `@better-i18n/server/hono` | `betterI18n()` Hono middleware |
|
|
156
|
+
| `@better-i18n/server/node` | `betterI18nMiddleware()` for Express, `fromNodeHeaders()` for Fastify/Koa |
|
|
157
|
+
|
|
158
|
+
## How It Works
|
|
159
|
+
|
|
160
|
+
1. **Singleton pattern** — `createServerI18n` creates one `@better-i18n/core` instance with shared TtlCache
|
|
161
|
+
2. **Locale detection** — `detectLocaleFromHeaders` parses Accept-Language, matches against CDN manifest locales
|
|
162
|
+
3. **Translation** — `getTranslator` fetches messages from CDN (cached), creates a `use-intl/core` translator
|
|
163
|
+
4. **Match strategy** — Exact match (`tr-TR`) → base language (`tr`) → region expansion (`tr` → `tr-TR`)
|
|
164
|
+
|
|
165
|
+
## Peer Dependencies
|
|
166
|
+
|
|
167
|
+
| Package | Required | Version |
|
|
168
|
+
|---------|----------|---------|
|
|
169
|
+
| `hono` | Optional | `>=4.0.0` |
|
|
170
|
+
|
|
171
|
+
## Documentation
|
|
172
|
+
|
|
173
|
+
Full documentation at [docs.better-i18n.com/frameworks/server-sdk](https://docs.better-i18n.com/frameworks/server-sdk)
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT © [Better i18n](https://better-i18n.com)
|
package/dist/node.d.ts
CHANGED
|
@@ -49,6 +49,9 @@ export declare function fromNodeHeaders(nodeHeaders: IncomingHttpHeaders): Heade
|
|
|
49
49
|
* }
|
|
50
50
|
* ```
|
|
51
51
|
*/
|
|
52
|
-
export declare function betterI18nMiddleware(i18n: ServerI18n): (req:
|
|
52
|
+
export declare function betterI18nMiddleware(i18n: ServerI18n): (req: {
|
|
53
|
+
headers: IncomingHttpHeaders;
|
|
54
|
+
[key: string]: unknown;
|
|
55
|
+
}, _res: unknown, next: () => void) => Promise<void>;
|
|
53
56
|
export type { Translator };
|
|
54
57
|
//# sourceMappingURL=node.d.ts.map
|
package/dist/node.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEzD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,mBAAmB,GAAG,OAAO,CASzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAEzD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,mBAAmB,GAAG,OAAO,CASzE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,IAEjD,KAAK;IAAE,OAAO,EAAE,mBAAmB,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,EAC7D,MAAM,OAAO,EACb,MAAM,MAAM,IAAI,mBAWnB;AAED,YAAY,EAAE,UAAU,EAAE,CAAC"}
|
package/dist/node.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.js","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,eAAe,CAAC,WAAgC;IAC9D,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAE9B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;QACvD,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAClC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACpE,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAgB;IACnD,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"node.js","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,eAAe,CAAC,WAAgC;IAC9D,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAE9B,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;QACvD,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QAClC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACpE,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAgB;IACnD,OAAO,KAAK,EACV,GAA6D,EAC7D,IAAa,EACb,IAAgB,EAChB,EAAE;QACF,MAAM,OAAO,GAAG,eAAe,CAAC,GAAG,CAAC,OAA8B,CAAC,CAAC;QACpE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,uBAAuB,CAAC,OAAO,CAAC,CAAC;QAC3D,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QAE3C,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC;QACpB,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;QAEV,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { ServerI18n } from "../types.js";
|
|
2
|
+
/** @internal Duck-typed subset of Better Auth's HookEndpointContext */
|
|
3
|
+
interface HookContext {
|
|
4
|
+
context: {
|
|
5
|
+
returned?: unknown;
|
|
6
|
+
responseHeaders?: Headers;
|
|
7
|
+
};
|
|
8
|
+
headers?: Headers;
|
|
9
|
+
path?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Options for the Better Auth localization provider.
|
|
13
|
+
*/
|
|
14
|
+
export interface BetterAuthProviderOptions {
|
|
15
|
+
/**
|
|
16
|
+
* CDN namespace where auth error translations are stored.
|
|
17
|
+
* Maps to the namespace in your Better i18n project.
|
|
18
|
+
* @default "auth"
|
|
19
|
+
*/
|
|
20
|
+
namespace?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Custom locale resolver. Overrides the default Accept-Language detection.
|
|
23
|
+
*
|
|
24
|
+
* Useful when locale is stored in a cookie, database, or user profile.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```ts
|
|
28
|
+
* createBetterAuthProvider(i18n, {
|
|
29
|
+
* getLocale: async ({ headers }) => {
|
|
30
|
+
* const cookie = headers?.get("cookie");
|
|
31
|
+
* return parseCookie(cookie)?.locale ?? "en";
|
|
32
|
+
* },
|
|
33
|
+
* })
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
getLocale?: (context: {
|
|
37
|
+
headers?: Headers;
|
|
38
|
+
}) => string | Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* Log a warning when a translation key is missing in the namespace.
|
|
41
|
+
* Helps identify untranslated error codes during development.
|
|
42
|
+
* @default true
|
|
43
|
+
*/
|
|
44
|
+
warnOnMissingKeys?: boolean;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Creates a Better Auth plugin that translates error messages using your
|
|
48
|
+
* Better i18n project's CDN translations.
|
|
49
|
+
*
|
|
50
|
+
* Unlike bundled localization packages, translations are fetched from the CDN
|
|
51
|
+
* at runtime — add or update translations from the dashboard, no redeployment
|
|
52
|
+
* needed. Missing keys log a warning and fall back to the original English
|
|
53
|
+
* message, so auth never breaks.
|
|
54
|
+
*
|
|
55
|
+
* @param i18n - The `ServerI18n` singleton from `createServerI18n()`
|
|
56
|
+
* @param options - Optional configuration (namespace, locale resolver, etc.)
|
|
57
|
+
* @returns A Better Auth plugin — pass it to `betterAuth({ plugins: [...] })`
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* import { createServerI18n } from "@better-i18n/server";
|
|
62
|
+
* import { createBetterAuthProvider } from "@better-i18n/server/providers/better-auth";
|
|
63
|
+
*
|
|
64
|
+
* const i18n = createServerI18n({
|
|
65
|
+
* project: "acme/dashboard",
|
|
66
|
+
* defaultLocale: "en",
|
|
67
|
+
* });
|
|
68
|
+
*
|
|
69
|
+
* export const auth = betterAuth({
|
|
70
|
+
* plugins: [
|
|
71
|
+
* createBetterAuthProvider(i18n),
|
|
72
|
+
* ],
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare function createBetterAuthProvider(i18n: ServerI18n, options?: BetterAuthProviderOptions): {
|
|
77
|
+
id: "better-i18n";
|
|
78
|
+
hooks: {
|
|
79
|
+
after: {
|
|
80
|
+
matcher: () => boolean;
|
|
81
|
+
handler: (ctx: HookContext) => Promise<void>;
|
|
82
|
+
}[];
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* All Better Auth core error codes with their default English messages.
|
|
87
|
+
* Use these to seed your project's "auth" namespace.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* import { DEFAULT_AUTH_KEYS } from "@better-i18n/server/providers/better-auth";
|
|
92
|
+
*
|
|
93
|
+
* // Seed via MCP createKeys tool
|
|
94
|
+
* for (const [key, message] of Object.entries(DEFAULT_AUTH_KEYS)) {
|
|
95
|
+
* await createKey({ namespace: "auth", key, translations: { en: message } });
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export declare const DEFAULT_AUTH_KEYS: Record<string, string>;
|
|
100
|
+
export {};
|
|
101
|
+
//# sourceMappingURL=better-auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"better-auth.d.ts","sourceRoot":"","sources":["../../src/providers/better-auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAQ9C,uEAAuE;AACvE,UAAU,WAAW;IACnB,OAAO,EAAE;QACP,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,eAAe,CAAC,EAAE,OAAO,CAAC;KAC3B,CAAC;IACF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;;;;;;;;;;;;OAcG;IACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAEzE;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AA6BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,UAAU,EAChB,OAAO,CAAC,EAAE,yBAAyB;;;;;2BAWN,WAAW;;;EAuDzC;AAaD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAuEpD,CAAC"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
function isAPIErrorLike(value) {
|
|
2
|
+
if (!value || typeof value !== "object")
|
|
3
|
+
return false;
|
|
4
|
+
const obj = value;
|
|
5
|
+
if (obj.name !== "APIError" || typeof obj.statusCode !== "number")
|
|
6
|
+
return false;
|
|
7
|
+
const body = obj.body;
|
|
8
|
+
if (!body || typeof body !== "object")
|
|
9
|
+
return false;
|
|
10
|
+
return typeof body.code === "string";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Creates a Better Auth plugin that translates error messages using your
|
|
14
|
+
* Better i18n project's CDN translations.
|
|
15
|
+
*
|
|
16
|
+
* Unlike bundled localization packages, translations are fetched from the CDN
|
|
17
|
+
* at runtime — add or update translations from the dashboard, no redeployment
|
|
18
|
+
* needed. Missing keys log a warning and fall back to the original English
|
|
19
|
+
* message, so auth never breaks.
|
|
20
|
+
*
|
|
21
|
+
* @param i18n - The `ServerI18n` singleton from `createServerI18n()`
|
|
22
|
+
* @param options - Optional configuration (namespace, locale resolver, etc.)
|
|
23
|
+
* @returns A Better Auth plugin — pass it to `betterAuth({ plugins: [...] })`
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { createServerI18n } from "@better-i18n/server";
|
|
28
|
+
* import { createBetterAuthProvider } from "@better-i18n/server/providers/better-auth";
|
|
29
|
+
*
|
|
30
|
+
* const i18n = createServerI18n({
|
|
31
|
+
* project: "acme/dashboard",
|
|
32
|
+
* defaultLocale: "en",
|
|
33
|
+
* });
|
|
34
|
+
*
|
|
35
|
+
* export const auth = betterAuth({
|
|
36
|
+
* plugins: [
|
|
37
|
+
* createBetterAuthProvider(i18n),
|
|
38
|
+
* ],
|
|
39
|
+
* });
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function createBetterAuthProvider(i18n, options) {
|
|
43
|
+
const namespace = options?.namespace ?? "auth";
|
|
44
|
+
const warnOnMissing = options?.warnOnMissingKeys ?? true;
|
|
45
|
+
return {
|
|
46
|
+
id: "better-i18n",
|
|
47
|
+
hooks: {
|
|
48
|
+
after: [
|
|
49
|
+
{
|
|
50
|
+
matcher: () => true,
|
|
51
|
+
handler: async (ctx) => {
|
|
52
|
+
const returned = ctx.context.returned;
|
|
53
|
+
// Only intercept APIError responses that carry an error code
|
|
54
|
+
if (!isAPIErrorLike(returned))
|
|
55
|
+
return;
|
|
56
|
+
const { body } = returned;
|
|
57
|
+
const errorCode = body.code;
|
|
58
|
+
// ── Detect locale ──────────────────────────────────────
|
|
59
|
+
let locale;
|
|
60
|
+
try {
|
|
61
|
+
if (options?.getLocale) {
|
|
62
|
+
locale = await options.getLocale({ headers: ctx.headers });
|
|
63
|
+
}
|
|
64
|
+
else if (ctx.headers) {
|
|
65
|
+
locale = await i18n.detectLocaleFromHeaders(ctx.headers);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
locale = i18n.config.defaultLocale;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Locale detection failed — keep original message
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// ── Translate ──────────────────────────────────────────
|
|
76
|
+
try {
|
|
77
|
+
const t = await i18n.getTranslator(locale, namespace);
|
|
78
|
+
const translated = t(errorCode);
|
|
79
|
+
// use-intl returns "namespace.key" for missing keys (default
|
|
80
|
+
// getMessageFallback). Detect this to avoid replacing a
|
|
81
|
+
// readable message with a raw fallback string.
|
|
82
|
+
const isFallback = translated === errorCode ||
|
|
83
|
+
translated === `${namespace}.${errorCode}`;
|
|
84
|
+
if (!isFallback && translated) {
|
|
85
|
+
body.message = translated;
|
|
86
|
+
// Also update Error.message for consistent logging
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- APIError.message is writable
|
|
88
|
+
returned.message = translated;
|
|
89
|
+
}
|
|
90
|
+
else if (warnOnMissing) {
|
|
91
|
+
console.warn(`[better-i18n] Missing auth translation: "${errorCode}" (locale: ${locale}, namespace: ${namespace})`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// CDN fetch failed or namespace missing — keep original message.
|
|
96
|
+
// Auth must never break because of i18n.
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Default English keys for Better Auth core error codes.
|
|
106
|
+
//
|
|
107
|
+
// Seed your Better i18n project's "auth" namespace with these keys,
|
|
108
|
+
// then translate them from the dashboard. Use with MCP `createKeys` tool
|
|
109
|
+
// or import in your seed script.
|
|
110
|
+
//
|
|
111
|
+
// These cover Better Auth core only. Plugin-specific error codes
|
|
112
|
+
// (admin, two-factor, email-otp, etc.) can be added as separate keys.
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
/**
|
|
115
|
+
* All Better Auth core error codes with their default English messages.
|
|
116
|
+
* Use these to seed your project's "auth" namespace.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* import { DEFAULT_AUTH_KEYS } from "@better-i18n/server/providers/better-auth";
|
|
121
|
+
*
|
|
122
|
+
* // Seed via MCP createKeys tool
|
|
123
|
+
* for (const [key, message] of Object.entries(DEFAULT_AUTH_KEYS)) {
|
|
124
|
+
* await createKey({ namespace: "auth", key, translations: { en: message } });
|
|
125
|
+
* }
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export const DEFAULT_AUTH_KEYS = {
|
|
129
|
+
// ── Authentication ─────────────────────────────────────────────────
|
|
130
|
+
INVALID_EMAIL_OR_PASSWORD: "Invalid email or password",
|
|
131
|
+
INVALID_PASSWORD: "Invalid password",
|
|
132
|
+
INVALID_EMAIL: "Invalid email",
|
|
133
|
+
INVALID_TOKEN: "Invalid token",
|
|
134
|
+
TOKEN_EXPIRED: "Token expired",
|
|
135
|
+
EMAIL_NOT_VERIFIED: "Email not verified",
|
|
136
|
+
EMAIL_ALREADY_VERIFIED: "Email is already verified",
|
|
137
|
+
EMAIL_MISMATCH: "Email mismatch",
|
|
138
|
+
// ── User management ────────────────────────────────────────────────
|
|
139
|
+
USER_NOT_FOUND: "User not found",
|
|
140
|
+
USER_ALREADY_EXISTS: "User already exists.",
|
|
141
|
+
USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL: "User already exists. Use another email.",
|
|
142
|
+
INVALID_USER: "Invalid user",
|
|
143
|
+
USER_EMAIL_NOT_FOUND: "User email not found",
|
|
144
|
+
USER_ALREADY_HAS_PASSWORD: "User already has a password. Provide that to delete the account.",
|
|
145
|
+
FAILED_TO_CREATE_USER: "Failed to create user",
|
|
146
|
+
FAILED_TO_UPDATE_USER: "Failed to update user",
|
|
147
|
+
FAILED_TO_GET_USER_INFO: "Failed to get user info",
|
|
148
|
+
// ── Session ────────────────────────────────────────────────────────
|
|
149
|
+
SESSION_EXPIRED: "Session expired. Re-authenticate to perform this action.",
|
|
150
|
+
SESSION_NOT_FRESH: "Session is not fresh",
|
|
151
|
+
FAILED_TO_CREATE_SESSION: "Failed to create session",
|
|
152
|
+
FAILED_TO_GET_SESSION: "Failed to get session",
|
|
153
|
+
// ── Password ───────────────────────────────────────────────────────
|
|
154
|
+
PASSWORD_TOO_SHORT: "Password too short",
|
|
155
|
+
PASSWORD_TOO_LONG: "Password too long",
|
|
156
|
+
PASSWORD_ALREADY_SET: "User already has a password set",
|
|
157
|
+
CREDENTIAL_ACCOUNT_NOT_FOUND: "Credential account not found",
|
|
158
|
+
// ── Account & Social ──────────────────────────────────────────────
|
|
159
|
+
ACCOUNT_NOT_FOUND: "Account not found",
|
|
160
|
+
SOCIAL_ACCOUNT_ALREADY_LINKED: "Social account already linked",
|
|
161
|
+
LINKED_ACCOUNT_ALREADY_EXISTS: "Linked account already exists",
|
|
162
|
+
FAILED_TO_UNLINK_LAST_ACCOUNT: "You can't unlink your last account",
|
|
163
|
+
PROVIDER_NOT_FOUND: "Provider not found",
|
|
164
|
+
ID_TOKEN_NOT_SUPPORTED: "id_token not supported",
|
|
165
|
+
// ── Email verification ─────────────────────────────────────────────
|
|
166
|
+
VERIFICATION_EMAIL_NOT_ENABLED: "Verification email isn't enabled",
|
|
167
|
+
EMAIL_CAN_NOT_BE_UPDATED: "Email can not be updated",
|
|
168
|
+
FAILED_TO_CREATE_VERIFICATION: "Unable to create verification",
|
|
169
|
+
// ── URL validation ─────────────────────────────────────────────────
|
|
170
|
+
INVALID_ORIGIN: "Invalid origin",
|
|
171
|
+
INVALID_CALLBACK_URL: "Invalid callbackURL",
|
|
172
|
+
INVALID_REDIRECT_URL: "Invalid redirectURL",
|
|
173
|
+
INVALID_ERROR_CALLBACK_URL: "Invalid errorCallbackURL",
|
|
174
|
+
INVALID_NEW_USER_CALLBACK_URL: "Invalid newUserCallbackURL",
|
|
175
|
+
MISSING_OR_NULL_ORIGIN: "Missing or null Origin",
|
|
176
|
+
CALLBACK_URL_REQUIRED: "callbackURL is required",
|
|
177
|
+
// ── Security ───────────────────────────────────────────────────────
|
|
178
|
+
CROSS_SITE_NAVIGATION_LOGIN_BLOCKED: "Cross-site navigation login blocked. This request appears to be a CSRF attack.",
|
|
179
|
+
// ── Validation ─────────────────────────────────────────────────────
|
|
180
|
+
VALIDATION_ERROR: "Validation Error",
|
|
181
|
+
MISSING_FIELD: "Field is required",
|
|
182
|
+
FIELD_NOT_ALLOWED: "Field not allowed to be set",
|
|
183
|
+
ASYNC_VALIDATION_NOT_SUPPORTED: "Async validation is not supported",
|
|
184
|
+
BODY_MUST_BE_AN_OBJECT: "Body must be an object",
|
|
185
|
+
METHOD_NOT_ALLOWED_DEFER_SESSION_REQUIRED: "POST method requires deferSessionRefresh to be enabled in session config",
|
|
186
|
+
};
|
|
187
|
+
//# sourceMappingURL=better-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"better-auth.js","sourceRoot":"","sources":["../../src/providers/better-auth.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-i18n/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Framework-agnostic server-side i18n for Better i18n (Hono, Express, Fastify)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -31,6 +31,11 @@
|
|
|
31
31
|
"bun": "./src/node.ts",
|
|
32
32
|
"default": "./dist/node.js"
|
|
33
33
|
},
|
|
34
|
+
"./providers/better-auth": {
|
|
35
|
+
"types": "./dist/providers/better-auth.d.ts",
|
|
36
|
+
"bun": "./src/providers/better-auth.ts",
|
|
37
|
+
"default": "./dist/providers/better-auth.js"
|
|
38
|
+
},
|
|
34
39
|
"./package.json": "./package.json"
|
|
35
40
|
},
|
|
36
41
|
"files": [
|
|
@@ -49,7 +54,10 @@
|
|
|
49
54
|
"express",
|
|
50
55
|
"fastify",
|
|
51
56
|
"server",
|
|
52
|
-
"middleware"
|
|
57
|
+
"middleware",
|
|
58
|
+
"better-auth",
|
|
59
|
+
"auth",
|
|
60
|
+
"localization"
|
|
53
61
|
],
|
|
54
62
|
"scripts": {
|
|
55
63
|
"build": "tsc",
|
|
@@ -59,7 +67,7 @@
|
|
|
59
67
|
"test": "vitest run"
|
|
60
68
|
},
|
|
61
69
|
"dependencies": {
|
|
62
|
-
"@better-i18n/core": "0.
|
|
70
|
+
"@better-i18n/core": "0.6.1",
|
|
63
71
|
"use-intl": ">=4.0.0"
|
|
64
72
|
},
|
|
65
73
|
"peerDependencies": {
|