i18n-email 0.1.0 → 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 +76 -8
- package/dist/index.cjs +166 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -3
- package/dist/index.d.ts +35 -3
- package/dist/index.js +166 -29
- package/dist/index.js.map +1 -1
- package/package.json +17 -4
package/README.md
CHANGED
|
@@ -5,10 +5,14 @@ Translate transactional emails into any language using AI models. Works with Rea
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- Accepts a **React Email component** or a **raw HTML string**
|
|
8
|
-
- Translates the **subject line and body** in a single
|
|
8
|
+
- Translates the **subject line and body** in a single API call
|
|
9
|
+
- **Batches large emails** — splits strings into chunks to stay within model limits
|
|
9
10
|
- Skips `<style>`, `<script>`, and `<head>` — only visible text is sent
|
|
10
|
-
- Injects `dir="rtl"` automatically for Arabic, Hebrew, Persian, and
|
|
11
|
+
- Injects `dir="rtl"` automatically for Arabic, Hebrew, Persian, Urdu, and more
|
|
11
12
|
- Optional **cache layer** with a key prefix to avoid redundant API calls
|
|
13
|
+
- **`onTranslate` hook** for logging and analytics
|
|
14
|
+
- Supports **any OpenAI-compatible API** via `baseURL`
|
|
15
|
+
- **AI SDK support** — pass any Vercel AI SDK model (`openai()`, `anthropic()`, `google()`, etc.)
|
|
12
16
|
|
|
13
17
|
## Install
|
|
14
18
|
|
|
@@ -20,6 +24,12 @@ npm install i18n-email
|
|
|
20
24
|
|
|
21
25
|
Requires `react >= 18` as a peer dependency.
|
|
22
26
|
|
|
27
|
+
To use the Vercel AI SDK instead of the OpenAI client directly:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bun add ai @ai-sdk/openai
|
|
31
|
+
```
|
|
32
|
+
|
|
23
33
|
## Usage
|
|
24
34
|
|
|
25
35
|
### With a React Email component
|
|
@@ -48,6 +58,35 @@ const { subject, html } = await i18nEmail.translate({
|
|
|
48
58
|
});
|
|
49
59
|
```
|
|
50
60
|
|
|
61
|
+
### With AI SDK (Vercel)
|
|
62
|
+
|
|
63
|
+
Use any AI SDK provider — OpenAI, Anthropic, Google, Mistral, and more:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { createI18nEmail } from "i18n-email";
|
|
67
|
+
import { openai } from "@ai-sdk/openai";
|
|
68
|
+
|
|
69
|
+
const i18nEmail = createI18nEmail({
|
|
70
|
+
model: openai("gpt-4o"),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const { subject, html } = await i18nEmail.translate({
|
|
74
|
+
locale: "ja",
|
|
75
|
+
subject: "Welcome!",
|
|
76
|
+
html: "<h1>Welcome!</h1>",
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Works with any provider:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { anthropic } from "@ai-sdk/anthropic";
|
|
84
|
+
|
|
85
|
+
const i18nEmail = createI18nEmail({
|
|
86
|
+
model: anthropic("claude-4-sonnet"),
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
51
90
|
### With caching (Upstash Redis example)
|
|
52
91
|
|
|
53
92
|
```ts
|
|
@@ -71,15 +110,33 @@ const i18nEmail = createI18nEmail({
|
|
|
71
110
|
});
|
|
72
111
|
```
|
|
73
112
|
|
|
113
|
+
### With logging
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const i18nEmail = createI18nEmail({
|
|
117
|
+
openaiApiKey: process.env.OPENAI_API_KEY!,
|
|
118
|
+
onTranslate: ({ locale, detectedLocale, strings, cacheHit }) => {
|
|
119
|
+
console.log(
|
|
120
|
+
`Translated ${strings.length} strings to ${locale}` +
|
|
121
|
+
` (detected: ${detectedLocale}, cache: ${cacheHit})`,
|
|
122
|
+
);
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
74
127
|
## API
|
|
75
128
|
|
|
76
129
|
### `createI18nEmail(config)`
|
|
77
130
|
|
|
78
|
-
| Option | Type
|
|
79
|
-
| -------------- |
|
|
80
|
-
| `openaiApiKey` | `string`
|
|
81
|
-
| `model` | `string`
|
|
82
|
-
| `
|
|
131
|
+
| Option | Type | Default | Description |
|
|
132
|
+
| -------------- | --------------------------------------- | ---------- | ---------------------------------------------------------------- |
|
|
133
|
+
| `openaiApiKey` | `string` | — | OpenAI API key (required when `model` is a string or omitted) |
|
|
134
|
+
| `model` | `string \| AiLanguageModel` | `"gpt-4o"` | OpenAI model name or an AI SDK model instance |
|
|
135
|
+
| `baseURL` | `string` | — | Override the API base URL (Azure, Groq, etc.) — OpenAI path only |
|
|
136
|
+
| `maxRetries` | `number` | `2` | Retries on transient OpenAI errors — OpenAI path only |
|
|
137
|
+
| `batchSize` | `number` | `50` | Max strings per API request |
|
|
138
|
+
| `cache` | `CacheProvider` | — | Cache adapter to avoid redundant API calls |
|
|
139
|
+
| `onTranslate` | `(info: TranslateCallbackInfo) => void` | — | Hook called after every translate call |
|
|
83
140
|
|
|
84
141
|
Returns `{ translate }`.
|
|
85
142
|
|
|
@@ -94,7 +151,7 @@ Returns `{ translate }`.
|
|
|
94
151
|
|
|
95
152
|
Returns `Promise<{ subject: string; html: string }>`.
|
|
96
153
|
|
|
97
|
-
If the email is already in the target locale, the original `subject` and `html` are returned unchanged.
|
|
154
|
+
If the email is already in the target locale, the original `subject` and `html` are returned unchanged. Locale matching normalizes base tags, so `"en-US"` and `"en"` are treated as the same language.
|
|
98
155
|
|
|
99
156
|
### `CacheProvider`
|
|
100
157
|
|
|
@@ -107,3 +164,14 @@ interface CacheProvider {
|
|
|
107
164
|
```
|
|
108
165
|
|
|
109
166
|
The cache key is a SHA-256 hash of the HTML, subject, and locale. `prefix` is prepended to every key when provided.
|
|
167
|
+
|
|
168
|
+
### `TranslateCallbackInfo`
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
interface TranslateCallbackInfo {
|
|
172
|
+
locale: string; // requested target locale
|
|
173
|
+
detectedLocale: string; // source locale detected by OpenAI
|
|
174
|
+
strings: string[]; // all strings sent for translation (empty on cache hit)
|
|
175
|
+
cacheHit: boolean; // true when the result was served from cache
|
|
176
|
+
}
|
|
177
|
+
```
|
package/dist/index.cjs
CHANGED
|
@@ -34,9 +34,6 @@ __export(index_exports, {
|
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(index_exports);
|
|
36
36
|
|
|
37
|
-
// src/client.ts
|
|
38
|
-
var import_openai = __toESM(require("openai"), 1);
|
|
39
|
-
|
|
40
37
|
// src/render.ts
|
|
41
38
|
var import_render = require("@react-email/render");
|
|
42
39
|
async function renderReactEmail(component) {
|
|
@@ -74,10 +71,6 @@ function extractStrings(html) {
|
|
|
74
71
|
let i = 0;
|
|
75
72
|
while (i < children.length) {
|
|
76
73
|
const child = children[i];
|
|
77
|
-
if (!child) {
|
|
78
|
-
i++;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
74
|
if (child.nodeType === import_node_html_parser.NodeType.TEXT_NODE) {
|
|
82
75
|
const textNodes = [];
|
|
83
76
|
while (i < children.length && children[i]?.nodeType === import_node_html_parser.NodeType.TEXT_NODE) {
|
|
@@ -85,7 +78,8 @@ function extractStrings(html) {
|
|
|
85
78
|
i++;
|
|
86
79
|
}
|
|
87
80
|
const merged = textNodes.map((n) => n.rawText).join("");
|
|
88
|
-
|
|
81
|
+
const trimmed = merged.trim();
|
|
82
|
+
if (trimmed && !trimmed.startsWith("<!DOCTYPE")) {
|
|
89
83
|
entries.push({
|
|
90
84
|
type: "text",
|
|
91
85
|
nodes: textNodes,
|
|
@@ -113,9 +107,6 @@ function extractStrings(html) {
|
|
|
113
107
|
}
|
|
114
108
|
|
|
115
109
|
// src/inject.ts
|
|
116
|
-
function setRawText(node, text) {
|
|
117
|
-
node._rawText = text;
|
|
118
|
-
}
|
|
119
110
|
function injectTranslations(root, entries, translationMap) {
|
|
120
111
|
for (const entry of entries) {
|
|
121
112
|
const translated = translationMap.get(entry.text);
|
|
@@ -125,16 +116,30 @@ function injectTranslations(root, entries, translationMap) {
|
|
|
125
116
|
} else {
|
|
126
117
|
const firstNode = entry.nodes[0];
|
|
127
118
|
if (!firstNode) continue;
|
|
128
|
-
|
|
119
|
+
firstNode.rawText = translated;
|
|
129
120
|
for (let i = 1; i < entry.nodes.length; i++) {
|
|
130
121
|
const node = entry.nodes[i];
|
|
131
|
-
if (node)
|
|
122
|
+
if (node) node.rawText = "";
|
|
132
123
|
}
|
|
133
124
|
}
|
|
134
125
|
}
|
|
135
126
|
return root.toString();
|
|
136
127
|
}
|
|
137
128
|
|
|
129
|
+
// src/prompt.ts
|
|
130
|
+
function buildSystemPrompt(locale) {
|
|
131
|
+
return [
|
|
132
|
+
`You are translating email content to ${locale}.`,
|
|
133
|
+
"Rules:",
|
|
134
|
+
"- Preserve dynamic values like names, URLs, amounts, dates, and codes exactly as they appear",
|
|
135
|
+
"- Do not translate brand names or product names",
|
|
136
|
+
"- Preserve tone: professional but friendly"
|
|
137
|
+
].join("\n");
|
|
138
|
+
}
|
|
139
|
+
function buildUserPrompt(strings) {
|
|
140
|
+
return JSON.stringify(strings);
|
|
141
|
+
}
|
|
142
|
+
|
|
138
143
|
// src/translate.ts
|
|
139
144
|
async function translateStrings(client, strings, locale, model) {
|
|
140
145
|
const response = await client.chat.completions.create({
|
|
@@ -144,20 +149,16 @@ async function translateStrings(client, strings, locale, model) {
|
|
|
144
149
|
{
|
|
145
150
|
role: "system",
|
|
146
151
|
content: [
|
|
147
|
-
|
|
148
|
-
"Rules:",
|
|
152
|
+
buildSystemPrompt(locale),
|
|
149
153
|
"- Return a JSON object with two fields:",
|
|
150
154
|
' 1. "detectedLocale": the ISO locale code of the source language',
|
|
151
155
|
' 2. "translations": a JSON array of translated strings in the exact same order as the input',
|
|
152
|
-
"- Preserve dynamic values like names, URLs, amounts, dates, and codes exactly as they appear",
|
|
153
|
-
"- Do not translate brand names or product names",
|
|
154
|
-
"- Preserve tone: professional but friendly",
|
|
155
156
|
"- Return only the JSON object, no explanation"
|
|
156
157
|
].join("\n")
|
|
157
158
|
},
|
|
158
159
|
{
|
|
159
160
|
role: "user",
|
|
160
|
-
content:
|
|
161
|
+
content: buildUserPrompt(strings)
|
|
161
162
|
}
|
|
162
163
|
]
|
|
163
164
|
});
|
|
@@ -185,10 +186,53 @@ async function translateStrings(client, strings, locale, model) {
|
|
|
185
186
|
return result;
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
// src/translate-ai.ts
|
|
190
|
+
async function translateStringsWithAi(model, strings, locale) {
|
|
191
|
+
let generateObject;
|
|
192
|
+
let jsonSchema;
|
|
193
|
+
try {
|
|
194
|
+
const ai = await import("ai");
|
|
195
|
+
generateObject = ai.generateObject;
|
|
196
|
+
jsonSchema = ai.jsonSchema;
|
|
197
|
+
} catch {
|
|
198
|
+
throw new Error(
|
|
199
|
+
'i18n-email: The "ai" package is required when using an AI SDK model. Install it with: npm install ai'
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
const result = await generateObject({
|
|
203
|
+
model,
|
|
204
|
+
schema: jsonSchema({
|
|
205
|
+
type: "object",
|
|
206
|
+
properties: {
|
|
207
|
+
detectedLocale: {
|
|
208
|
+
type: "string",
|
|
209
|
+
description: "The ISO locale code of the source language"
|
|
210
|
+
},
|
|
211
|
+
translations: {
|
|
212
|
+
type: "array",
|
|
213
|
+
items: { type: "string" },
|
|
214
|
+
description: "Translated strings in the exact same order as the input"
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
required: ["detectedLocale", "translations"],
|
|
218
|
+
additionalProperties: false
|
|
219
|
+
}),
|
|
220
|
+
system: buildSystemPrompt(locale),
|
|
221
|
+
prompt: buildUserPrompt(strings)
|
|
222
|
+
});
|
|
223
|
+
const { detectedLocale, translations } = result.object;
|
|
224
|
+
if (translations.length !== strings.length) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`i18n-email: Translation count mismatch. Expected ${strings.length}, got ${translations.length}`
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return { detectedLocale, translations };
|
|
230
|
+
}
|
|
231
|
+
|
|
188
232
|
// src/hash.ts
|
|
189
233
|
var import_node_crypto = require("crypto");
|
|
190
234
|
function createCacheKey(html, subject, locale) {
|
|
191
|
-
return (0, import_node_crypto.createHash)("sha256").update(
|
|
235
|
+
return (0, import_node_crypto.createHash)("sha256").update([html, subject, locale].join("\0")).digest("hex");
|
|
192
236
|
}
|
|
193
237
|
|
|
194
238
|
// src/cache.ts
|
|
@@ -208,9 +252,54 @@ async function setCachedResult(cache, html, subject, locale, result) {
|
|
|
208
252
|
|
|
209
253
|
// src/rtl.ts
|
|
210
254
|
var import_node_html_parser2 = require("node-html-parser");
|
|
211
|
-
|
|
255
|
+
|
|
256
|
+
// src/utils.ts
|
|
257
|
+
function baseLocale(tag) {
|
|
258
|
+
return tag.split("-")[0].toLowerCase();
|
|
259
|
+
}
|
|
260
|
+
function chunk(arr, size) {
|
|
261
|
+
const chunks = [];
|
|
262
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
263
|
+
chunks.push(arr.slice(i, i + size));
|
|
264
|
+
}
|
|
265
|
+
return chunks;
|
|
266
|
+
}
|
|
267
|
+
function isAiLanguageModel(value) {
|
|
268
|
+
return typeof value === "object" && value !== null && "modelId" in value && "provider" in value;
|
|
269
|
+
}
|
|
270
|
+
function isAiSdkConfig(config) {
|
|
271
|
+
return isAiLanguageModel(config.model);
|
|
272
|
+
}
|
|
273
|
+
async function createOpenAIClient(config) {
|
|
274
|
+
let OpenAI;
|
|
275
|
+
try {
|
|
276
|
+
OpenAI = (await import("openai")).default;
|
|
277
|
+
} catch {
|
|
278
|
+
throw new Error(
|
|
279
|
+
'i18n-email: The "openai" package is required when using a string model name. Install it with: npm install openai'
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
return new OpenAI({
|
|
283
|
+
apiKey: config.openaiApiKey,
|
|
284
|
+
baseURL: config.baseURL,
|
|
285
|
+
maxRetries: config.maxRetries ?? 2
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/rtl.ts
|
|
290
|
+
var RTL_LOCALES = /* @__PURE__ */ new Set([
|
|
291
|
+
"ar",
|
|
292
|
+
"he",
|
|
293
|
+
"fa",
|
|
294
|
+
"ur",
|
|
295
|
+
"ps",
|
|
296
|
+
"sd",
|
|
297
|
+
"ug",
|
|
298
|
+
"yi",
|
|
299
|
+
"dv"
|
|
300
|
+
]);
|
|
212
301
|
function isRtlLocale(locale) {
|
|
213
|
-
return RTL_LOCALES.has(locale
|
|
302
|
+
return RTL_LOCALES.has(baseLocale(locale));
|
|
214
303
|
}
|
|
215
304
|
function injectRtlDir(html) {
|
|
216
305
|
const root = (0, import_node_html_parser2.parse)(html);
|
|
@@ -229,30 +318,72 @@ function injectRtlDir(html) {
|
|
|
229
318
|
}
|
|
230
319
|
|
|
231
320
|
// src/client.ts
|
|
321
|
+
var DEFAULT_BATCH_SIZE = 50;
|
|
232
322
|
function createI18nEmail(config) {
|
|
233
|
-
const
|
|
323
|
+
const aiSdk = isAiSdkConfig(config);
|
|
324
|
+
let clientPromise;
|
|
325
|
+
function getClient() {
|
|
326
|
+
if (!clientPromise) {
|
|
327
|
+
clientPromise = createOpenAIClient(
|
|
328
|
+
config
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
return clientPromise;
|
|
332
|
+
}
|
|
333
|
+
async function translateBatch(strings, locale) {
|
|
334
|
+
if (aiSdk) {
|
|
335
|
+
return translateStringsWithAi(config.model, strings, locale);
|
|
336
|
+
}
|
|
337
|
+
const client = await getClient();
|
|
338
|
+
return translateStrings(client, strings, locale, config.model ?? "gpt-4o");
|
|
339
|
+
}
|
|
234
340
|
async function translate(options) {
|
|
235
341
|
const { locale, subject } = options;
|
|
236
342
|
const html = options.react ? await renderReactEmail(options.react) : options.html;
|
|
237
343
|
if (config.cache) {
|
|
238
344
|
const cached = await getCachedResult(config.cache, html, subject, locale);
|
|
239
|
-
if (cached)
|
|
345
|
+
if (cached) {
|
|
346
|
+
config.onTranslate?.({
|
|
347
|
+
locale,
|
|
348
|
+
detectedLocale: locale,
|
|
349
|
+
strings: [],
|
|
350
|
+
cacheHit: true
|
|
351
|
+
});
|
|
352
|
+
return cached;
|
|
353
|
+
}
|
|
240
354
|
}
|
|
241
355
|
const { root, entries, uniqueStrings } = extractStrings(html);
|
|
242
356
|
const allStrings = [subject, ...uniqueStrings];
|
|
243
|
-
const
|
|
244
|
-
const
|
|
245
|
-
|
|
357
|
+
const batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
358
|
+
const batches = chunk(allStrings, batchSize);
|
|
359
|
+
const firstBatch = batches[0];
|
|
360
|
+
if (!firstBatch || firstBatch.length === 0) {
|
|
361
|
+
return { subject, html };
|
|
362
|
+
}
|
|
363
|
+
const firstResponse = await translateBatch(firstBatch, locale);
|
|
364
|
+
if (baseLocale(firstResponse.detectedLocale) === baseLocale(locale)) {
|
|
246
365
|
const result2 = { subject, html };
|
|
247
366
|
if (config.cache) {
|
|
248
367
|
await setCachedResult(config.cache, html, subject, locale, result2);
|
|
249
368
|
}
|
|
369
|
+
config.onTranslate?.({
|
|
370
|
+
locale,
|
|
371
|
+
detectedLocale: firstResponse.detectedLocale,
|
|
372
|
+
strings: allStrings,
|
|
373
|
+
cacheHit: false
|
|
374
|
+
});
|
|
250
375
|
return result2;
|
|
251
376
|
}
|
|
252
|
-
const
|
|
377
|
+
const allTranslations = [...firstResponse.translations];
|
|
378
|
+
for (let i = 1; i < batches.length; i++) {
|
|
379
|
+
const batch = batches[i];
|
|
380
|
+
const response = await translateBatch(batch, locale);
|
|
381
|
+
allTranslations.push(...response.translations);
|
|
382
|
+
}
|
|
383
|
+
const translatedSubject = allTranslations[0];
|
|
253
384
|
const translationMap = /* @__PURE__ */ new Map();
|
|
254
385
|
uniqueStrings.forEach((original, i) => {
|
|
255
|
-
translationMap.set(original,
|
|
386
|
+
translationMap.set(original, allTranslations[i + 1]);
|
|
256
387
|
});
|
|
257
388
|
let translatedHtml = injectTranslations(root, entries, translationMap);
|
|
258
389
|
if (isRtlLocale(locale)) {
|
|
@@ -265,6 +396,12 @@ function createI18nEmail(config) {
|
|
|
265
396
|
if (config.cache) {
|
|
266
397
|
await setCachedResult(config.cache, html, subject, locale, result);
|
|
267
398
|
}
|
|
399
|
+
config.onTranslate?.({
|
|
400
|
+
locale,
|
|
401
|
+
detectedLocale: firstResponse.detectedLocale,
|
|
402
|
+
strings: allStrings,
|
|
403
|
+
cacheHit: false
|
|
404
|
+
});
|
|
268
405
|
return result;
|
|
269
406
|
}
|
|
270
407
|
return { translate };
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/render.ts","../src/extract.ts","../src/inject.ts","../src/translate.ts","../src/hash.ts","../src/cache.ts","../src/rtl.ts"],"sourcesContent":["export { createI18nEmail } from \"./client\";\nexport type {\n I18nEmailConfig,\n CacheProvider,\n TranslateOptions,\n TranslateOptionsReact,\n TranslateOptionsHtml,\n TranslateResult,\n} from \"./types\";\n","import OpenAI from \"openai\";\nimport type {\n I18nEmailConfig,\n TranslateOptions,\n TranslateResult,\n} from \"./types\";\nimport { renderReactEmail } from \"./render\";\nimport { extractStrings } from \"./extract\";\nimport { injectTranslations } from \"./inject\";\nimport { translateStrings } from \"./translate\";\nimport { getCachedResult, setCachedResult } from \"./cache\";\nimport { isRtlLocale, injectRtlDir } from \"./rtl\";\n\nexport function createI18nEmail(config: I18nEmailConfig) {\n const client = new OpenAI({ apiKey: config.openaiApiKey });\n\n async function translate(\n options: TranslateOptions,\n ): Promise<TranslateResult> {\n const { locale, subject } = options;\n\n const html = options.react\n ? await renderReactEmail(options.react)\n : options.html;\n\n if (config.cache) {\n const cached = await getCachedResult(config.cache, html, subject, locale);\n if (cached) return cached;\n }\n\n const { root, entries, uniqueStrings } = extractStrings(html);\n\n const allStrings = [subject, ...uniqueStrings];\n const model = config.model ?? \"gpt-4o\";\n const response = await translateStrings(client, allStrings, locale, model);\n\n if (response.detectedLocale === locale) {\n const result: TranslateResult = { subject, html };\n if (config.cache) {\n await setCachedResult(config.cache, html, subject, locale, result);\n }\n return result;\n }\n\n const translatedSubject = response.translations[0]!;\n const translationMap = new Map<string, string>();\n uniqueStrings.forEach((original, i) => {\n translationMap.set(original, response.translations[i + 1]!);\n });\n\n let translatedHtml = injectTranslations(root, entries, translationMap);\n\n if (isRtlLocale(locale)) {\n translatedHtml = injectRtlDir(translatedHtml);\n }\n\n const result: TranslateResult = {\n subject: translatedSubject,\n html: translatedHtml,\n };\n\n if (config.cache) {\n await setCachedResult(config.cache, html, subject, locale, result);\n }\n\n return result;\n }\n\n return { translate };\n}\n","import { render } from \"@react-email/render\";\nimport type { ReactElement } from \"react\";\n\nexport async function renderReactEmail(\n component: ReactElement,\n): Promise<string> {\n try {\n return await render(component);\n } catch (error) {\n throw new Error(\n `i18n-email: Failed to render React component: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n}\n","import { parse, NodeType } from \"node-html-parser\";\nimport type { HTMLElement as ParsedElement, Node } from \"node-html-parser\";\n\nconst TRANSLATABLE_ATTRS = [\"alt\", \"title\"];\nconst SKIP_TAGS = new Set([\"style\", \"script\", \"head\"]);\n\nexport interface TextEntry {\n type: \"text\";\n nodes: Node[];\n text: string;\n}\n\nexport interface AttributeEntry {\n type: \"attribute\";\n element: ParsedElement;\n attrName: string;\n text: string;\n}\n\nexport type ExtractionEntry = TextEntry | AttributeEntry;\n\nexport interface ExtractionResult {\n root: ParsedElement;\n entries: ExtractionEntry[];\n uniqueStrings: string[];\n}\n\nexport function extractStrings(html: string): ExtractionResult {\n const root = parse(html);\n const entries: ExtractionEntry[] = [];\n\n function walk(element: ParsedElement): void {\n const tag = element.tagName?.toLowerCase();\n if (tag && SKIP_TAGS.has(tag)) return;\n\n for (const attr of TRANSLATABLE_ATTRS) {\n const value = element.getAttribute(attr);\n if (value && value.trim()) {\n entries.push({\n type: \"attribute\",\n element,\n attrName: attr,\n text: value,\n });\n }\n }\n\n const children = element.childNodes;\n let i = 0;\n\n while (i < children.length) {\n const child = children[i];\n if (!child) {\n i++;\n continue;\n }\n\n if (child.nodeType === NodeType.TEXT_NODE) {\n const textNodes: Node[] = [];\n while (\n i < children.length &&\n children[i]?.nodeType === NodeType.TEXT_NODE\n ) {\n textNodes.push(children[i]!);\n i++;\n }\n const merged = textNodes.map((n) => n.rawText).join(\"\");\n if (merged.trim()) {\n entries.push({\n type: \"text\",\n nodes: textNodes,\n text: merged,\n });\n }\n } else if (child.nodeType === NodeType.ELEMENT_NODE) {\n walk(child as ParsedElement);\n i++;\n } else {\n i++;\n }\n }\n }\n\n walk(root);\n\n const seen = new Set<string>();\n const uniqueStrings: string[] = [];\n for (const entry of entries) {\n if (!seen.has(entry.text)) {\n seen.add(entry.text);\n uniqueStrings.push(entry.text);\n }\n }\n\n return { root, entries, uniqueStrings };\n}\n","import type { ExtractionEntry } from \"./extract\";\nimport type { HTMLElement as ParsedElement } from \"node-html-parser\";\n\nfunction setRawText(node: unknown, text: string): void {\n (node as { _rawText: string })._rawText = text;\n}\n\nexport function injectTranslations(\n root: ParsedElement,\n entries: ExtractionEntry[],\n translationMap: Map<string, string>,\n): string {\n for (const entry of entries) {\n const translated = translationMap.get(entry.text);\n if (translated === undefined) continue;\n\n if (entry.type === \"attribute\") {\n entry.element.setAttribute(entry.attrName, translated);\n } else {\n const firstNode = entry.nodes[0];\n if (!firstNode) continue;\n\n setRawText(firstNode, translated);\n for (let i = 1; i < entry.nodes.length; i++) {\n const node = entry.nodes[i];\n if (node) setRawText(node, \"\");\n }\n }\n }\n\n return root.toString();\n}\n","import type OpenAI from \"openai\";\nimport type { TranslationResponse } from \"./types\";\n\nexport async function translateStrings(\n client: OpenAI,\n strings: string[],\n locale: string,\n model: string,\n): Promise<TranslationResponse> {\n const response = await client.chat.completions.create({\n model,\n response_format: { type: \"json_object\" },\n messages: [\n {\n role: \"system\",\n content: [\n `You are translating email content to ${locale}.`,\n \"Rules:\",\n \"- Return a JSON object with two fields:\",\n ' 1. \"detectedLocale\": the ISO locale code of the source language',\n ' 2. \"translations\": a JSON array of translated strings in the exact same order as the input',\n \"- Preserve dynamic values like names, URLs, amounts, dates, and codes exactly as they appear\",\n \"- Do not translate brand names or product names\",\n \"- Preserve tone: professional but friendly\",\n \"- Return only the JSON object, no explanation\",\n ].join(\"\\n\"),\n },\n {\n role: \"user\",\n content: JSON.stringify(strings),\n },\n ],\n });\n\n const content = response.choices[0]?.message?.content;\n if (!content) {\n throw new Error(\"i18n-email: OpenAI returned an empty response\");\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch {\n throw new Error(`i18n-email: OpenAI returned malformed JSON: ${content}`);\n }\n\n const result = parsed as TranslationResponse;\n if (!result.detectedLocale || !Array.isArray(result.translations)) {\n throw new Error(\n `i18n-email: Unexpected response shape from OpenAI: ${content}`,\n );\n }\n\n if (result.translations.length !== strings.length) {\n throw new Error(\n `i18n-email: Translation count mismatch. ` +\n `Expected ${strings.length}, ` +\n `got ${result.translations.length}`,\n );\n }\n\n return result;\n}\n","import { createHash } from \"node:crypto\";\n\nexport function createCacheKey(\n html: string,\n subject: string,\n locale: string,\n): string {\n return createHash(\"sha256\")\n .update(`${html}${subject}${locale}`)\n .digest(\"hex\");\n}\n","import type { CacheProvider, TranslateResult } from \"./types\";\nimport { createCacheKey } from \"./hash\";\n\nfunction buildKey(cache: CacheProvider, hash: string): string {\n return cache.prefix ? `${cache.prefix}${hash}` : hash;\n}\n\nexport async function getCachedResult(\n cache: CacheProvider,\n html: string,\n subject: string,\n locale: string,\n): Promise<TranslateResult | null> {\n const key = buildKey(cache, createCacheKey(html, subject, locale));\n const cached = await cache.get(key);\n if (!cached) return null;\n return JSON.parse(cached) as TranslateResult;\n}\n\nexport async function setCachedResult(\n cache: CacheProvider,\n html: string,\n subject: string,\n locale: string,\n result: TranslateResult,\n): Promise<void> {\n const key = buildKey(cache, createCacheKey(html, subject, locale));\n await cache.set(key, JSON.stringify(result));\n}\n","import { parse, NodeType } from \"node-html-parser\";\nimport type { HTMLElement as ParsedElement } from \"node-html-parser\";\n\nconst RTL_LOCALES = new Set([\"ar\", \"he\", \"fa\", \"ur\"]);\n\nexport function isRtlLocale(locale: string): boolean {\n return RTL_LOCALES.has(locale.toLowerCase());\n}\n\nexport function injectRtlDir(html: string): string {\n const root = parse(html);\n const htmlEl = root.querySelector(\"html\");\n\n if (htmlEl) {\n htmlEl.setAttribute(\"dir\", \"rtl\");\n return root.toString();\n }\n\n for (const child of root.childNodes) {\n if (child.nodeType === NodeType.ELEMENT_NODE) {\n (child as ParsedElement).setAttribute(\"dir\", \"rtl\");\n break;\n }\n }\n\n return root.toString();\n}\n"],"mappings":"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","names":["import_node_html_parser","OpenAI","result"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/render.ts","../src/extract.ts","../src/inject.ts","../src/prompt.ts","../src/translate.ts","../src/translate-ai.ts","../src/hash.ts","../src/cache.ts","../src/rtl.ts","../src/utils.ts","../src/client.ts"],"sourcesContent":["export { createI18nEmail } from \"./client\";\nexport type {\n AiLanguageModel,\n AiSdkConfig,\n OpenAIConfig,\n I18nEmailConfig,\n CacheProvider,\n TranslateOptions,\n TranslateOptionsReact,\n TranslateOptionsHtml,\n TranslateResult,\n TranslationResponse,\n TranslateCallbackInfo,\n} from \"./types\";\n","import { render } from \"@react-email/render\";\nimport type { ReactElement } from \"react\";\n\nexport async function renderReactEmail(\n component: ReactElement,\n): Promise<string> {\n try {\n return await render(component);\n } catch (error) {\n throw new Error(\n `i18n-email: Failed to render React component: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n}\n","import { parse, NodeType } from \"node-html-parser\";\nimport type { HTMLElement as ParsedElement, Node } from \"node-html-parser\";\n\nconst TRANSLATABLE_ATTRS = [\"alt\", \"title\"];\nconst SKIP_TAGS = new Set([\"style\", \"script\", \"head\"]);\n\nexport interface TextEntry {\n type: \"text\";\n nodes: Node[];\n text: string;\n}\n\nexport interface AttributeEntry {\n type: \"attribute\";\n element: ParsedElement;\n attrName: string;\n text: string;\n}\n\nexport type ExtractionEntry = TextEntry | AttributeEntry;\n\nexport interface ExtractionResult {\n root: ParsedElement;\n entries: ExtractionEntry[];\n uniqueStrings: string[];\n}\n\nexport function extractStrings(html: string): ExtractionResult {\n const root = parse(html);\n const entries: ExtractionEntry[] = [];\n\n function walk(element: ParsedElement): void {\n const tag = element.tagName?.toLowerCase();\n if (tag && SKIP_TAGS.has(tag)) return;\n\n for (const attr of TRANSLATABLE_ATTRS) {\n const value = element.getAttribute(attr);\n if (value && value.trim()) {\n entries.push({\n type: \"attribute\",\n element,\n attrName: attr,\n text: value,\n });\n }\n }\n\n const children = element.childNodes;\n let i = 0;\n\n while (i < children.length) {\n const child = children[i]!;\n\n if (child.nodeType === NodeType.TEXT_NODE) {\n const textNodes: Node[] = [];\n while (\n i < children.length &&\n children[i]?.nodeType === NodeType.TEXT_NODE\n ) {\n textNodes.push(children[i]!);\n i++;\n }\n const merged = textNodes.map((n) => n.rawText).join(\"\");\n const trimmed = merged.trim();\n if (trimmed && !trimmed.startsWith(\"<!DOCTYPE\")) {\n entries.push({\n type: \"text\",\n nodes: textNodes,\n text: merged,\n });\n }\n } else if (child.nodeType === NodeType.ELEMENT_NODE) {\n walk(child as ParsedElement);\n i++;\n } else {\n i++;\n }\n }\n }\n\n walk(root);\n\n const seen = new Set<string>();\n const uniqueStrings: string[] = [];\n for (const entry of entries) {\n if (!seen.has(entry.text)) {\n seen.add(entry.text);\n uniqueStrings.push(entry.text);\n }\n }\n\n return { root, entries, uniqueStrings };\n}\n","import type { ExtractionEntry } from \"./extract\";\nimport type { HTMLElement as ParsedElement } from \"node-html-parser\";\n\nexport function injectTranslations(\n root: ParsedElement,\n entries: ExtractionEntry[],\n translationMap: Map<string, string>,\n): string {\n for (const entry of entries) {\n const translated = translationMap.get(entry.text);\n if (translated === undefined) continue;\n\n if (entry.type === \"attribute\") {\n entry.element.setAttribute(entry.attrName, translated);\n } else {\n const firstNode = entry.nodes[0];\n if (!firstNode) continue;\n\n firstNode.rawText = translated;\n for (let i = 1; i < entry.nodes.length; i++) {\n const node = entry.nodes[i];\n if (node) node.rawText = \"\";\n }\n }\n }\n\n return root.toString();\n}\n","export function buildSystemPrompt(locale: string): string {\n return [\n `You are translating email content to ${locale}.`,\n \"Rules:\",\n \"- Preserve dynamic values like names, URLs, amounts, dates, and codes exactly as they appear\",\n \"- Do not translate brand names or product names\",\n \"- Preserve tone: professional but friendly\",\n ].join(\"\\n\");\n}\n\nexport function buildUserPrompt(strings: string[]): string {\n return JSON.stringify(strings);\n}\n","import type OpenAI from \"openai\";\nimport type { TranslationResponse } from \"./types\";\nimport { buildSystemPrompt, buildUserPrompt } from \"./prompt\";\n\nexport async function translateStrings(\n client: OpenAI,\n strings: string[],\n locale: string,\n model: string,\n): Promise<TranslationResponse> {\n const response = await client.chat.completions.create({\n model,\n response_format: { type: \"json_object\" },\n messages: [\n {\n role: \"system\",\n content: [\n buildSystemPrompt(locale),\n \"- Return a JSON object with two fields:\",\n ' 1. \"detectedLocale\": the ISO locale code of the source language',\n ' 2. \"translations\": a JSON array of translated strings in the exact same order as the input',\n \"- Return only the JSON object, no explanation\",\n ].join(\"\\n\"),\n },\n {\n role: \"user\",\n content: buildUserPrompt(strings),\n },\n ],\n });\n\n const content = response.choices[0]?.message?.content;\n if (!content) {\n throw new Error(\"i18n-email: OpenAI returned an empty response\");\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch {\n throw new Error(`i18n-email: OpenAI returned malformed JSON: ${content}`);\n }\n\n const result = parsed as TranslationResponse;\n if (!result.detectedLocale || !Array.isArray(result.translations)) {\n throw new Error(\n `i18n-email: Unexpected response shape from OpenAI: ${content}`,\n );\n }\n\n if (result.translations.length !== strings.length) {\n throw new Error(\n `i18n-email: Translation count mismatch. ` +\n `Expected ${strings.length}, ` +\n `got ${result.translations.length}`,\n );\n }\n\n return result;\n}\n","import type { AiLanguageModel, TranslationResponse } from \"./types\";\nimport { buildSystemPrompt, buildUserPrompt } from \"./prompt\";\n\nexport async function translateStringsWithAi(\n model: AiLanguageModel,\n strings: string[],\n locale: string,\n): Promise<TranslationResponse> {\n let generateObject: typeof import(\"ai\").generateObject;\n let jsonSchema: typeof import(\"ai\").jsonSchema;\n\n try {\n const ai = await import(\"ai\");\n generateObject = ai.generateObject;\n jsonSchema = ai.jsonSchema;\n } catch {\n throw new Error(\n 'i18n-email: The \"ai\" package is required when using an AI SDK model. ' +\n \"Install it with: npm install ai\",\n );\n }\n\n const result = await generateObject({\n model: model as Parameters<typeof generateObject>[0][\"model\"],\n schema: jsonSchema<TranslationResponse>({\n type: \"object\",\n properties: {\n detectedLocale: {\n type: \"string\",\n description: \"The ISO locale code of the source language\",\n },\n translations: {\n type: \"array\",\n items: { type: \"string\" },\n description:\n \"Translated strings in the exact same order as the input\",\n },\n },\n required: [\"detectedLocale\", \"translations\"],\n additionalProperties: false,\n }),\n system: buildSystemPrompt(locale),\n prompt: buildUserPrompt(strings),\n });\n\n const { detectedLocale, translations } = result.object;\n\n if (translations.length !== strings.length) {\n throw new Error(\n `i18n-email: Translation count mismatch. ` +\n `Expected ${strings.length}, ` +\n `got ${translations.length}`,\n );\n }\n\n return { detectedLocale, translations };\n}\n","import { createHash } from \"node:crypto\";\n\nexport function createCacheKey(\n html: string,\n subject: string,\n locale: string,\n): string {\n return createHash(\"sha256\")\n .update([html, subject, locale].join(\"\\0\"))\n .digest(\"hex\");\n}\n","import type { CacheProvider, TranslateResult } from \"./types\";\nimport { createCacheKey } from \"./hash\";\n\nfunction buildKey(cache: CacheProvider, hash: string): string {\n return cache.prefix ? `${cache.prefix}${hash}` : hash;\n}\n\nexport async function getCachedResult(\n cache: CacheProvider,\n html: string,\n subject: string,\n locale: string,\n): Promise<TranslateResult | null> {\n const key = buildKey(cache, createCacheKey(html, subject, locale));\n const cached = await cache.get(key);\n if (!cached) return null;\n return JSON.parse(cached) as TranslateResult;\n}\n\nexport async function setCachedResult(\n cache: CacheProvider,\n html: string,\n subject: string,\n locale: string,\n result: TranslateResult,\n): Promise<void> {\n const key = buildKey(cache, createCacheKey(html, subject, locale));\n await cache.set(key, JSON.stringify(result));\n}\n","import { parse, NodeType } from \"node-html-parser\";\nimport type { HTMLElement as ParsedElement } from \"node-html-parser\";\nimport { baseLocale } from \"./utils\";\n\nconst RTL_LOCALES = new Set([\n \"ar\",\n \"he\",\n \"fa\",\n \"ur\",\n \"ps\",\n \"sd\",\n \"ug\",\n \"yi\",\n \"dv\",\n]);\n\nexport function isRtlLocale(locale: string): boolean {\n return RTL_LOCALES.has(baseLocale(locale));\n}\n\nexport function injectRtlDir(html: string): string {\n const root = parse(html);\n const htmlEl = root.querySelector(\"html\");\n\n if (htmlEl) {\n htmlEl.setAttribute(\"dir\", \"rtl\");\n return root.toString();\n }\n\n for (const child of root.childNodes) {\n if (child.nodeType === NodeType.ELEMENT_NODE) {\n (child as ParsedElement).setAttribute(\"dir\", \"rtl\");\n break;\n }\n }\n\n return root.toString();\n}\n","import type { AiLanguageModel, AiSdkConfig, I18nEmailConfig } from \"./types\";\n\nexport function baseLocale(tag: string): string {\n return tag.split(\"-\")[0]!.toLowerCase();\n}\n\nexport function chunk<T>(arr: T[], size: number): T[][] {\n const chunks: T[][] = [];\n for (let i = 0; i < arr.length; i += size) {\n chunks.push(arr.slice(i, i + size));\n }\n return chunks;\n}\n\nexport function isAiLanguageModel(value: unknown): value is AiLanguageModel {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"modelId\" in value &&\n \"provider\" in value\n );\n}\n\nexport function isAiSdkConfig(config: I18nEmailConfig): config is AiSdkConfig {\n return isAiLanguageModel(config.model);\n}\n\nexport async function createOpenAIClient(\n config: import(\"./types\").OpenAIConfig,\n): Promise<import(\"openai\").default> {\n let OpenAI: typeof import(\"openai\").default;\n try {\n OpenAI = (await import(\"openai\")).default;\n } catch {\n throw new Error(\n 'i18n-email: The \"openai\" package is required when using a string model name. ' +\n \"Install it with: npm install openai\",\n );\n }\n return new OpenAI({\n apiKey: config.openaiApiKey,\n baseURL: config.baseURL,\n maxRetries: config.maxRetries ?? 2,\n });\n}\n","import type {\n I18nEmailConfig,\n TranslateOptions,\n TranslateResult,\n TranslationResponse,\n} from \"./types\";\nimport { renderReactEmail } from \"./render\";\nimport { extractStrings } from \"./extract\";\nimport { injectTranslations } from \"./inject\";\nimport { translateStrings } from \"./translate\";\nimport { translateStringsWithAi } from \"./translate-ai\";\nimport { getCachedResult, setCachedResult } from \"./cache\";\nimport { isRtlLocale, injectRtlDir } from \"./rtl\";\nimport { baseLocale, chunk, createOpenAIClient, isAiSdkConfig } from \"./utils\";\n\nconst DEFAULT_BATCH_SIZE = 50;\n\nexport function createI18nEmail(config: I18nEmailConfig) {\n const aiSdk = isAiSdkConfig(config);\n\n let clientPromise: Promise<import(\"openai\").default> | undefined;\n\n function getClient(): Promise<import(\"openai\").default> {\n if (!clientPromise) {\n clientPromise = createOpenAIClient(\n config as import(\"./types\").OpenAIConfig,\n );\n }\n return clientPromise;\n }\n\n async function translateBatch(\n strings: string[],\n locale: string,\n ): Promise<TranslationResponse> {\n if (aiSdk) {\n return translateStringsWithAi(config.model, strings, locale);\n }\n const client = await getClient();\n return translateStrings(client, strings, locale, config.model ?? \"gpt-4o\");\n }\n\n async function translate(\n options: TranslateOptions,\n ): Promise<TranslateResult> {\n const { locale, subject } = options;\n\n const html = options.react\n ? await renderReactEmail(options.react)\n : options.html;\n\n if (config.cache) {\n const cached = await getCachedResult(config.cache, html, subject, locale);\n if (cached) {\n config.onTranslate?.({\n locale,\n detectedLocale: locale,\n strings: [],\n cacheHit: true,\n });\n return cached;\n }\n }\n\n const { root, entries, uniqueStrings } = extractStrings(html);\n\n const allStrings = [subject, ...uniqueStrings];\n const batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;\n const batches = chunk(allStrings, batchSize);\n\n const firstBatch = batches[0];\n if (!firstBatch || firstBatch.length === 0) {\n return { subject, html };\n }\n\n const firstResponse = await translateBatch(firstBatch, locale);\n\n if (baseLocale(firstResponse.detectedLocale) === baseLocale(locale)) {\n const result: TranslateResult = { subject, html };\n if (config.cache) {\n await setCachedResult(config.cache, html, subject, locale, result);\n }\n config.onTranslate?.({\n locale,\n detectedLocale: firstResponse.detectedLocale,\n strings: allStrings,\n cacheHit: false,\n });\n return result;\n }\n\n const allTranslations = [...firstResponse.translations];\n\n for (let i = 1; i < batches.length; i++) {\n const batch = batches[i]!;\n const response = await translateBatch(batch, locale);\n allTranslations.push(...response.translations);\n }\n\n const translatedSubject = allTranslations[0]!;\n const translationMap = new Map<string, string>();\n uniqueStrings.forEach((original, i) => {\n translationMap.set(original, allTranslations[i + 1]!);\n });\n\n let translatedHtml = injectTranslations(root, entries, translationMap);\n\n if (isRtlLocale(locale)) {\n translatedHtml = injectRtlDir(translatedHtml);\n }\n\n const result: TranslateResult = {\n subject: translatedSubject,\n html: translatedHtml,\n };\n\n if (config.cache) {\n await setCachedResult(config.cache, html, subject, locale, result);\n }\n\n config.onTranslate?.({\n locale,\n detectedLocale: firstResponse.detectedLocale,\n strings: allStrings,\n cacheHit: false,\n });\n\n return result;\n }\n\n return { translate };\n}\n"],"mappings":"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","names":["import_node_html_parser","result"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -5,10 +5,38 @@ interface CacheProvider {
|
|
|
5
5
|
get: (key: string) => Promise<string | null>;
|
|
6
6
|
set: (key: string, value: string) => Promise<void>;
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Minimal duck type for an AI SDK language model.
|
|
10
|
+
* Compatible with any provider: `openai("gpt-4o")`, `anthropic("claude-4-sonnet")`, etc.
|
|
11
|
+
*/
|
|
12
|
+
interface AiLanguageModel {
|
|
13
|
+
readonly specificationVersion: string;
|
|
14
|
+
readonly modelId: string;
|
|
15
|
+
readonly provider: string;
|
|
16
|
+
}
|
|
17
|
+
interface SharedConfig {
|
|
18
|
+
batchSize?: number;
|
|
19
|
+
cache?: CacheProvider;
|
|
20
|
+
onTranslate?: (info: TranslateCallbackInfo) => void;
|
|
21
|
+
}
|
|
22
|
+
interface OpenAIConfig extends SharedConfig {
|
|
9
23
|
openaiApiKey: string;
|
|
10
24
|
model?: string;
|
|
11
|
-
|
|
25
|
+
baseURL?: string;
|
|
26
|
+
maxRetries?: number;
|
|
27
|
+
}
|
|
28
|
+
interface AiSdkConfig extends SharedConfig {
|
|
29
|
+
model: AiLanguageModel;
|
|
30
|
+
openaiApiKey?: never;
|
|
31
|
+
baseURL?: never;
|
|
32
|
+
maxRetries?: never;
|
|
33
|
+
}
|
|
34
|
+
type I18nEmailConfig = OpenAIConfig | AiSdkConfig;
|
|
35
|
+
interface TranslateCallbackInfo {
|
|
36
|
+
locale: string;
|
|
37
|
+
detectedLocale: string;
|
|
38
|
+
strings: string[];
|
|
39
|
+
cacheHit: boolean;
|
|
12
40
|
}
|
|
13
41
|
interface TranslateOptionsReact {
|
|
14
42
|
locale: string;
|
|
@@ -27,9 +55,13 @@ interface TranslateResult {
|
|
|
27
55
|
subject: string;
|
|
28
56
|
html: string;
|
|
29
57
|
}
|
|
58
|
+
interface TranslationResponse {
|
|
59
|
+
detectedLocale: string;
|
|
60
|
+
translations: string[];
|
|
61
|
+
}
|
|
30
62
|
|
|
31
63
|
declare function createI18nEmail(config: I18nEmailConfig): {
|
|
32
64
|
translate: (options: TranslateOptions) => Promise<TranslateResult>;
|
|
33
65
|
};
|
|
34
66
|
|
|
35
|
-
export { type CacheProvider, type I18nEmailConfig, type TranslateOptions, type TranslateOptionsHtml, type TranslateOptionsReact, type TranslateResult, createI18nEmail };
|
|
67
|
+
export { type AiLanguageModel, type AiSdkConfig, type CacheProvider, type I18nEmailConfig, type OpenAIConfig, type TranslateCallbackInfo, type TranslateOptions, type TranslateOptionsHtml, type TranslateOptionsReact, type TranslateResult, type TranslationResponse, createI18nEmail };
|
package/dist/index.d.ts
CHANGED
|
@@ -5,10 +5,38 @@ interface CacheProvider {
|
|
|
5
5
|
get: (key: string) => Promise<string | null>;
|
|
6
6
|
set: (key: string, value: string) => Promise<void>;
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Minimal duck type for an AI SDK language model.
|
|
10
|
+
* Compatible with any provider: `openai("gpt-4o")`, `anthropic("claude-4-sonnet")`, etc.
|
|
11
|
+
*/
|
|
12
|
+
interface AiLanguageModel {
|
|
13
|
+
readonly specificationVersion: string;
|
|
14
|
+
readonly modelId: string;
|
|
15
|
+
readonly provider: string;
|
|
16
|
+
}
|
|
17
|
+
interface SharedConfig {
|
|
18
|
+
batchSize?: number;
|
|
19
|
+
cache?: CacheProvider;
|
|
20
|
+
onTranslate?: (info: TranslateCallbackInfo) => void;
|
|
21
|
+
}
|
|
22
|
+
interface OpenAIConfig extends SharedConfig {
|
|
9
23
|
openaiApiKey: string;
|
|
10
24
|
model?: string;
|
|
11
|
-
|
|
25
|
+
baseURL?: string;
|
|
26
|
+
maxRetries?: number;
|
|
27
|
+
}
|
|
28
|
+
interface AiSdkConfig extends SharedConfig {
|
|
29
|
+
model: AiLanguageModel;
|
|
30
|
+
openaiApiKey?: never;
|
|
31
|
+
baseURL?: never;
|
|
32
|
+
maxRetries?: never;
|
|
33
|
+
}
|
|
34
|
+
type I18nEmailConfig = OpenAIConfig | AiSdkConfig;
|
|
35
|
+
interface TranslateCallbackInfo {
|
|
36
|
+
locale: string;
|
|
37
|
+
detectedLocale: string;
|
|
38
|
+
strings: string[];
|
|
39
|
+
cacheHit: boolean;
|
|
12
40
|
}
|
|
13
41
|
interface TranslateOptionsReact {
|
|
14
42
|
locale: string;
|
|
@@ -27,9 +55,13 @@ interface TranslateResult {
|
|
|
27
55
|
subject: string;
|
|
28
56
|
html: string;
|
|
29
57
|
}
|
|
58
|
+
interface TranslationResponse {
|
|
59
|
+
detectedLocale: string;
|
|
60
|
+
translations: string[];
|
|
61
|
+
}
|
|
30
62
|
|
|
31
63
|
declare function createI18nEmail(config: I18nEmailConfig): {
|
|
32
64
|
translate: (options: TranslateOptions) => Promise<TranslateResult>;
|
|
33
65
|
};
|
|
34
66
|
|
|
35
|
-
export { type CacheProvider, type I18nEmailConfig, type TranslateOptions, type TranslateOptionsHtml, type TranslateOptionsReact, type TranslateResult, createI18nEmail };
|
|
67
|
+
export { type AiLanguageModel, type AiSdkConfig, type CacheProvider, type I18nEmailConfig, type OpenAIConfig, type TranslateCallbackInfo, type TranslateOptions, type TranslateOptionsHtml, type TranslateOptionsReact, type TranslateResult, type TranslationResponse, createI18nEmail };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// src/client.ts
|
|
2
|
-
import OpenAI from "openai";
|
|
3
|
-
|
|
4
1
|
// src/render.ts
|
|
5
2
|
import { render } from "@react-email/render";
|
|
6
3
|
async function renderReactEmail(component) {
|
|
@@ -38,10 +35,6 @@ function extractStrings(html) {
|
|
|
38
35
|
let i = 0;
|
|
39
36
|
while (i < children.length) {
|
|
40
37
|
const child = children[i];
|
|
41
|
-
if (!child) {
|
|
42
|
-
i++;
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
38
|
if (child.nodeType === NodeType.TEXT_NODE) {
|
|
46
39
|
const textNodes = [];
|
|
47
40
|
while (i < children.length && children[i]?.nodeType === NodeType.TEXT_NODE) {
|
|
@@ -49,7 +42,8 @@ function extractStrings(html) {
|
|
|
49
42
|
i++;
|
|
50
43
|
}
|
|
51
44
|
const merged = textNodes.map((n) => n.rawText).join("");
|
|
52
|
-
|
|
45
|
+
const trimmed = merged.trim();
|
|
46
|
+
if (trimmed && !trimmed.startsWith("<!DOCTYPE")) {
|
|
53
47
|
entries.push({
|
|
54
48
|
type: "text",
|
|
55
49
|
nodes: textNodes,
|
|
@@ -77,9 +71,6 @@ function extractStrings(html) {
|
|
|
77
71
|
}
|
|
78
72
|
|
|
79
73
|
// src/inject.ts
|
|
80
|
-
function setRawText(node, text) {
|
|
81
|
-
node._rawText = text;
|
|
82
|
-
}
|
|
83
74
|
function injectTranslations(root, entries, translationMap) {
|
|
84
75
|
for (const entry of entries) {
|
|
85
76
|
const translated = translationMap.get(entry.text);
|
|
@@ -89,16 +80,30 @@ function injectTranslations(root, entries, translationMap) {
|
|
|
89
80
|
} else {
|
|
90
81
|
const firstNode = entry.nodes[0];
|
|
91
82
|
if (!firstNode) continue;
|
|
92
|
-
|
|
83
|
+
firstNode.rawText = translated;
|
|
93
84
|
for (let i = 1; i < entry.nodes.length; i++) {
|
|
94
85
|
const node = entry.nodes[i];
|
|
95
|
-
if (node)
|
|
86
|
+
if (node) node.rawText = "";
|
|
96
87
|
}
|
|
97
88
|
}
|
|
98
89
|
}
|
|
99
90
|
return root.toString();
|
|
100
91
|
}
|
|
101
92
|
|
|
93
|
+
// src/prompt.ts
|
|
94
|
+
function buildSystemPrompt(locale) {
|
|
95
|
+
return [
|
|
96
|
+
`You are translating email content to ${locale}.`,
|
|
97
|
+
"Rules:",
|
|
98
|
+
"- Preserve dynamic values like names, URLs, amounts, dates, and codes exactly as they appear",
|
|
99
|
+
"- Do not translate brand names or product names",
|
|
100
|
+
"- Preserve tone: professional but friendly"
|
|
101
|
+
].join("\n");
|
|
102
|
+
}
|
|
103
|
+
function buildUserPrompt(strings) {
|
|
104
|
+
return JSON.stringify(strings);
|
|
105
|
+
}
|
|
106
|
+
|
|
102
107
|
// src/translate.ts
|
|
103
108
|
async function translateStrings(client, strings, locale, model) {
|
|
104
109
|
const response = await client.chat.completions.create({
|
|
@@ -108,20 +113,16 @@ async function translateStrings(client, strings, locale, model) {
|
|
|
108
113
|
{
|
|
109
114
|
role: "system",
|
|
110
115
|
content: [
|
|
111
|
-
|
|
112
|
-
"Rules:",
|
|
116
|
+
buildSystemPrompt(locale),
|
|
113
117
|
"- Return a JSON object with two fields:",
|
|
114
118
|
' 1. "detectedLocale": the ISO locale code of the source language',
|
|
115
119
|
' 2. "translations": a JSON array of translated strings in the exact same order as the input',
|
|
116
|
-
"- Preserve dynamic values like names, URLs, amounts, dates, and codes exactly as they appear",
|
|
117
|
-
"- Do not translate brand names or product names",
|
|
118
|
-
"- Preserve tone: professional but friendly",
|
|
119
120
|
"- Return only the JSON object, no explanation"
|
|
120
121
|
].join("\n")
|
|
121
122
|
},
|
|
122
123
|
{
|
|
123
124
|
role: "user",
|
|
124
|
-
content:
|
|
125
|
+
content: buildUserPrompt(strings)
|
|
125
126
|
}
|
|
126
127
|
]
|
|
127
128
|
});
|
|
@@ -149,10 +150,53 @@ async function translateStrings(client, strings, locale, model) {
|
|
|
149
150
|
return result;
|
|
150
151
|
}
|
|
151
152
|
|
|
153
|
+
// src/translate-ai.ts
|
|
154
|
+
async function translateStringsWithAi(model, strings, locale) {
|
|
155
|
+
let generateObject;
|
|
156
|
+
let jsonSchema;
|
|
157
|
+
try {
|
|
158
|
+
const ai = await import("ai");
|
|
159
|
+
generateObject = ai.generateObject;
|
|
160
|
+
jsonSchema = ai.jsonSchema;
|
|
161
|
+
} catch {
|
|
162
|
+
throw new Error(
|
|
163
|
+
'i18n-email: The "ai" package is required when using an AI SDK model. Install it with: npm install ai'
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const result = await generateObject({
|
|
167
|
+
model,
|
|
168
|
+
schema: jsonSchema({
|
|
169
|
+
type: "object",
|
|
170
|
+
properties: {
|
|
171
|
+
detectedLocale: {
|
|
172
|
+
type: "string",
|
|
173
|
+
description: "The ISO locale code of the source language"
|
|
174
|
+
},
|
|
175
|
+
translations: {
|
|
176
|
+
type: "array",
|
|
177
|
+
items: { type: "string" },
|
|
178
|
+
description: "Translated strings in the exact same order as the input"
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
required: ["detectedLocale", "translations"],
|
|
182
|
+
additionalProperties: false
|
|
183
|
+
}),
|
|
184
|
+
system: buildSystemPrompt(locale),
|
|
185
|
+
prompt: buildUserPrompt(strings)
|
|
186
|
+
});
|
|
187
|
+
const { detectedLocale, translations } = result.object;
|
|
188
|
+
if (translations.length !== strings.length) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`i18n-email: Translation count mismatch. Expected ${strings.length}, got ${translations.length}`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
return { detectedLocale, translations };
|
|
194
|
+
}
|
|
195
|
+
|
|
152
196
|
// src/hash.ts
|
|
153
197
|
import { createHash } from "crypto";
|
|
154
198
|
function createCacheKey(html, subject, locale) {
|
|
155
|
-
return createHash("sha256").update(
|
|
199
|
+
return createHash("sha256").update([html, subject, locale].join("\0")).digest("hex");
|
|
156
200
|
}
|
|
157
201
|
|
|
158
202
|
// src/cache.ts
|
|
@@ -172,9 +216,54 @@ async function setCachedResult(cache, html, subject, locale, result) {
|
|
|
172
216
|
|
|
173
217
|
// src/rtl.ts
|
|
174
218
|
import { parse as parse2, NodeType as NodeType2 } from "node-html-parser";
|
|
175
|
-
|
|
219
|
+
|
|
220
|
+
// src/utils.ts
|
|
221
|
+
function baseLocale(tag) {
|
|
222
|
+
return tag.split("-")[0].toLowerCase();
|
|
223
|
+
}
|
|
224
|
+
function chunk(arr, size) {
|
|
225
|
+
const chunks = [];
|
|
226
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
227
|
+
chunks.push(arr.slice(i, i + size));
|
|
228
|
+
}
|
|
229
|
+
return chunks;
|
|
230
|
+
}
|
|
231
|
+
function isAiLanguageModel(value) {
|
|
232
|
+
return typeof value === "object" && value !== null && "modelId" in value && "provider" in value;
|
|
233
|
+
}
|
|
234
|
+
function isAiSdkConfig(config) {
|
|
235
|
+
return isAiLanguageModel(config.model);
|
|
236
|
+
}
|
|
237
|
+
async function createOpenAIClient(config) {
|
|
238
|
+
let OpenAI;
|
|
239
|
+
try {
|
|
240
|
+
OpenAI = (await import("openai")).default;
|
|
241
|
+
} catch {
|
|
242
|
+
throw new Error(
|
|
243
|
+
'i18n-email: The "openai" package is required when using a string model name. Install it with: npm install openai'
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
return new OpenAI({
|
|
247
|
+
apiKey: config.openaiApiKey,
|
|
248
|
+
baseURL: config.baseURL,
|
|
249
|
+
maxRetries: config.maxRetries ?? 2
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/rtl.ts
|
|
254
|
+
var RTL_LOCALES = /* @__PURE__ */ new Set([
|
|
255
|
+
"ar",
|
|
256
|
+
"he",
|
|
257
|
+
"fa",
|
|
258
|
+
"ur",
|
|
259
|
+
"ps",
|
|
260
|
+
"sd",
|
|
261
|
+
"ug",
|
|
262
|
+
"yi",
|
|
263
|
+
"dv"
|
|
264
|
+
]);
|
|
176
265
|
function isRtlLocale(locale) {
|
|
177
|
-
return RTL_LOCALES.has(locale
|
|
266
|
+
return RTL_LOCALES.has(baseLocale(locale));
|
|
178
267
|
}
|
|
179
268
|
function injectRtlDir(html) {
|
|
180
269
|
const root = parse2(html);
|
|
@@ -193,30 +282,72 @@ function injectRtlDir(html) {
|
|
|
193
282
|
}
|
|
194
283
|
|
|
195
284
|
// src/client.ts
|
|
285
|
+
var DEFAULT_BATCH_SIZE = 50;
|
|
196
286
|
function createI18nEmail(config) {
|
|
197
|
-
const
|
|
287
|
+
const aiSdk = isAiSdkConfig(config);
|
|
288
|
+
let clientPromise;
|
|
289
|
+
function getClient() {
|
|
290
|
+
if (!clientPromise) {
|
|
291
|
+
clientPromise = createOpenAIClient(
|
|
292
|
+
config
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
return clientPromise;
|
|
296
|
+
}
|
|
297
|
+
async function translateBatch(strings, locale) {
|
|
298
|
+
if (aiSdk) {
|
|
299
|
+
return translateStringsWithAi(config.model, strings, locale);
|
|
300
|
+
}
|
|
301
|
+
const client = await getClient();
|
|
302
|
+
return translateStrings(client, strings, locale, config.model ?? "gpt-4o");
|
|
303
|
+
}
|
|
198
304
|
async function translate(options) {
|
|
199
305
|
const { locale, subject } = options;
|
|
200
306
|
const html = options.react ? await renderReactEmail(options.react) : options.html;
|
|
201
307
|
if (config.cache) {
|
|
202
308
|
const cached = await getCachedResult(config.cache, html, subject, locale);
|
|
203
|
-
if (cached)
|
|
309
|
+
if (cached) {
|
|
310
|
+
config.onTranslate?.({
|
|
311
|
+
locale,
|
|
312
|
+
detectedLocale: locale,
|
|
313
|
+
strings: [],
|
|
314
|
+
cacheHit: true
|
|
315
|
+
});
|
|
316
|
+
return cached;
|
|
317
|
+
}
|
|
204
318
|
}
|
|
205
319
|
const { root, entries, uniqueStrings } = extractStrings(html);
|
|
206
320
|
const allStrings = [subject, ...uniqueStrings];
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
321
|
+
const batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
|
|
322
|
+
const batches = chunk(allStrings, batchSize);
|
|
323
|
+
const firstBatch = batches[0];
|
|
324
|
+
if (!firstBatch || firstBatch.length === 0) {
|
|
325
|
+
return { subject, html };
|
|
326
|
+
}
|
|
327
|
+
const firstResponse = await translateBatch(firstBatch, locale);
|
|
328
|
+
if (baseLocale(firstResponse.detectedLocale) === baseLocale(locale)) {
|
|
210
329
|
const result2 = { subject, html };
|
|
211
330
|
if (config.cache) {
|
|
212
331
|
await setCachedResult(config.cache, html, subject, locale, result2);
|
|
213
332
|
}
|
|
333
|
+
config.onTranslate?.({
|
|
334
|
+
locale,
|
|
335
|
+
detectedLocale: firstResponse.detectedLocale,
|
|
336
|
+
strings: allStrings,
|
|
337
|
+
cacheHit: false
|
|
338
|
+
});
|
|
214
339
|
return result2;
|
|
215
340
|
}
|
|
216
|
-
const
|
|
341
|
+
const allTranslations = [...firstResponse.translations];
|
|
342
|
+
for (let i = 1; i < batches.length; i++) {
|
|
343
|
+
const batch = batches[i];
|
|
344
|
+
const response = await translateBatch(batch, locale);
|
|
345
|
+
allTranslations.push(...response.translations);
|
|
346
|
+
}
|
|
347
|
+
const translatedSubject = allTranslations[0];
|
|
217
348
|
const translationMap = /* @__PURE__ */ new Map();
|
|
218
349
|
uniqueStrings.forEach((original, i) => {
|
|
219
|
-
translationMap.set(original,
|
|
350
|
+
translationMap.set(original, allTranslations[i + 1]);
|
|
220
351
|
});
|
|
221
352
|
let translatedHtml = injectTranslations(root, entries, translationMap);
|
|
222
353
|
if (isRtlLocale(locale)) {
|
|
@@ -229,6 +360,12 @@ function createI18nEmail(config) {
|
|
|
229
360
|
if (config.cache) {
|
|
230
361
|
await setCachedResult(config.cache, html, subject, locale, result);
|
|
231
362
|
}
|
|
363
|
+
config.onTranslate?.({
|
|
364
|
+
locale,
|
|
365
|
+
detectedLocale: firstResponse.detectedLocale,
|
|
366
|
+
strings: allStrings,
|
|
367
|
+
cacheHit: false
|
|
368
|
+
});
|
|
232
369
|
return result;
|
|
233
370
|
}
|
|
234
371
|
return { translate };
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/render.ts","../src/extract.ts","../src/inject.ts","../src/translate.ts","../src/hash.ts","../src/cache.ts","../src/rtl.ts"],"sourcesContent":["import OpenAI from \"openai\";\nimport type {\n I18nEmailConfig,\n TranslateOptions,\n TranslateResult,\n} from \"./types\";\nimport { renderReactEmail } from \"./render\";\nimport { extractStrings } from \"./extract\";\nimport { injectTranslations } from \"./inject\";\nimport { translateStrings } from \"./translate\";\nimport { getCachedResult, setCachedResult } from \"./cache\";\nimport { isRtlLocale, injectRtlDir } from \"./rtl\";\n\nexport function createI18nEmail(config: I18nEmailConfig) {\n const client = new OpenAI({ apiKey: config.openaiApiKey });\n\n async function translate(\n options: TranslateOptions,\n ): Promise<TranslateResult> {\n const { locale, subject } = options;\n\n const html = options.react\n ? await renderReactEmail(options.react)\n : options.html;\n\n if (config.cache) {\n const cached = await getCachedResult(config.cache, html, subject, locale);\n if (cached) return cached;\n }\n\n const { root, entries, uniqueStrings } = extractStrings(html);\n\n const allStrings = [subject, ...uniqueStrings];\n const model = config.model ?? \"gpt-4o\";\n const response = await translateStrings(client, allStrings, locale, model);\n\n if (response.detectedLocale === locale) {\n const result: TranslateResult = { subject, html };\n if (config.cache) {\n await setCachedResult(config.cache, html, subject, locale, result);\n }\n return result;\n }\n\n const translatedSubject = response.translations[0]!;\n const translationMap = new Map<string, string>();\n uniqueStrings.forEach((original, i) => {\n translationMap.set(original, response.translations[i + 1]!);\n });\n\n let translatedHtml = injectTranslations(root, entries, translationMap);\n\n if (isRtlLocale(locale)) {\n translatedHtml = injectRtlDir(translatedHtml);\n }\n\n const result: TranslateResult = {\n subject: translatedSubject,\n html: translatedHtml,\n };\n\n if (config.cache) {\n await setCachedResult(config.cache, html, subject, locale, result);\n }\n\n return result;\n }\n\n return { translate };\n}\n","import { render } from \"@react-email/render\";\nimport type { ReactElement } from \"react\";\n\nexport async function renderReactEmail(\n component: ReactElement,\n): Promise<string> {\n try {\n return await render(component);\n } catch (error) {\n throw new Error(\n `i18n-email: Failed to render React component: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n}\n","import { parse, NodeType } from \"node-html-parser\";\nimport type { HTMLElement as ParsedElement, Node } from \"node-html-parser\";\n\nconst TRANSLATABLE_ATTRS = [\"alt\", \"title\"];\nconst SKIP_TAGS = new Set([\"style\", \"script\", \"head\"]);\n\nexport interface TextEntry {\n type: \"text\";\n nodes: Node[];\n text: string;\n}\n\nexport interface AttributeEntry {\n type: \"attribute\";\n element: ParsedElement;\n attrName: string;\n text: string;\n}\n\nexport type ExtractionEntry = TextEntry | AttributeEntry;\n\nexport interface ExtractionResult {\n root: ParsedElement;\n entries: ExtractionEntry[];\n uniqueStrings: string[];\n}\n\nexport function extractStrings(html: string): ExtractionResult {\n const root = parse(html);\n const entries: ExtractionEntry[] = [];\n\n function walk(element: ParsedElement): void {\n const tag = element.tagName?.toLowerCase();\n if (tag && SKIP_TAGS.has(tag)) return;\n\n for (const attr of TRANSLATABLE_ATTRS) {\n const value = element.getAttribute(attr);\n if (value && value.trim()) {\n entries.push({\n type: \"attribute\",\n element,\n attrName: attr,\n text: value,\n });\n }\n }\n\n const children = element.childNodes;\n let i = 0;\n\n while (i < children.length) {\n const child = children[i];\n if (!child) {\n i++;\n continue;\n }\n\n if (child.nodeType === NodeType.TEXT_NODE) {\n const textNodes: Node[] = [];\n while (\n i < children.length &&\n children[i]?.nodeType === NodeType.TEXT_NODE\n ) {\n textNodes.push(children[i]!);\n i++;\n }\n const merged = textNodes.map((n) => n.rawText).join(\"\");\n if (merged.trim()) {\n entries.push({\n type: \"text\",\n nodes: textNodes,\n text: merged,\n });\n }\n } else if (child.nodeType === NodeType.ELEMENT_NODE) {\n walk(child as ParsedElement);\n i++;\n } else {\n i++;\n }\n }\n }\n\n walk(root);\n\n const seen = new Set<string>();\n const uniqueStrings: string[] = [];\n for (const entry of entries) {\n if (!seen.has(entry.text)) {\n seen.add(entry.text);\n uniqueStrings.push(entry.text);\n }\n }\n\n return { root, entries, uniqueStrings };\n}\n","import type { ExtractionEntry } from \"./extract\";\nimport type { HTMLElement as ParsedElement } from \"node-html-parser\";\n\nfunction setRawText(node: unknown, text: string): void {\n (node as { _rawText: string })._rawText = text;\n}\n\nexport function injectTranslations(\n root: ParsedElement,\n entries: ExtractionEntry[],\n translationMap: Map<string, string>,\n): string {\n for (const entry of entries) {\n const translated = translationMap.get(entry.text);\n if (translated === undefined) continue;\n\n if (entry.type === \"attribute\") {\n entry.element.setAttribute(entry.attrName, translated);\n } else {\n const firstNode = entry.nodes[0];\n if (!firstNode) continue;\n\n setRawText(firstNode, translated);\n for (let i = 1; i < entry.nodes.length; i++) {\n const node = entry.nodes[i];\n if (node) setRawText(node, \"\");\n }\n }\n }\n\n return root.toString();\n}\n","import type OpenAI from \"openai\";\nimport type { TranslationResponse } from \"./types\";\n\nexport async function translateStrings(\n client: OpenAI,\n strings: string[],\n locale: string,\n model: string,\n): Promise<TranslationResponse> {\n const response = await client.chat.completions.create({\n model,\n response_format: { type: \"json_object\" },\n messages: [\n {\n role: \"system\",\n content: [\n `You are translating email content to ${locale}.`,\n \"Rules:\",\n \"- Return a JSON object with two fields:\",\n ' 1. \"detectedLocale\": the ISO locale code of the source language',\n ' 2. \"translations\": a JSON array of translated strings in the exact same order as the input',\n \"- Preserve dynamic values like names, URLs, amounts, dates, and codes exactly as they appear\",\n \"- Do not translate brand names or product names\",\n \"- Preserve tone: professional but friendly\",\n \"- Return only the JSON object, no explanation\",\n ].join(\"\\n\"),\n },\n {\n role: \"user\",\n content: JSON.stringify(strings),\n },\n ],\n });\n\n const content = response.choices[0]?.message?.content;\n if (!content) {\n throw new Error(\"i18n-email: OpenAI returned an empty response\");\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch {\n throw new Error(`i18n-email: OpenAI returned malformed JSON: ${content}`);\n }\n\n const result = parsed as TranslationResponse;\n if (!result.detectedLocale || !Array.isArray(result.translations)) {\n throw new Error(\n `i18n-email: Unexpected response shape from OpenAI: ${content}`,\n );\n }\n\n if (result.translations.length !== strings.length) {\n throw new Error(\n `i18n-email: Translation count mismatch. ` +\n `Expected ${strings.length}, ` +\n `got ${result.translations.length}`,\n );\n }\n\n return result;\n}\n","import { createHash } from \"node:crypto\";\n\nexport function createCacheKey(\n html: string,\n subject: string,\n locale: string,\n): string {\n return createHash(\"sha256\")\n .update(`${html}${subject}${locale}`)\n .digest(\"hex\");\n}\n","import type { CacheProvider, TranslateResult } from \"./types\";\nimport { createCacheKey } from \"./hash\";\n\nfunction buildKey(cache: CacheProvider, hash: string): string {\n return cache.prefix ? `${cache.prefix}${hash}` : hash;\n}\n\nexport async function getCachedResult(\n cache: CacheProvider,\n html: string,\n subject: string,\n locale: string,\n): Promise<TranslateResult | null> {\n const key = buildKey(cache, createCacheKey(html, subject, locale));\n const cached = await cache.get(key);\n if (!cached) return null;\n return JSON.parse(cached) as TranslateResult;\n}\n\nexport async function setCachedResult(\n cache: CacheProvider,\n html: string,\n subject: string,\n locale: string,\n result: TranslateResult,\n): Promise<void> {\n const key = buildKey(cache, createCacheKey(html, subject, locale));\n await cache.set(key, JSON.stringify(result));\n}\n","import { parse, NodeType } from \"node-html-parser\";\nimport type { HTMLElement as ParsedElement } from \"node-html-parser\";\n\nconst RTL_LOCALES = new Set([\"ar\", \"he\", \"fa\", \"ur\"]);\n\nexport function isRtlLocale(locale: string): boolean {\n return RTL_LOCALES.has(locale.toLowerCase());\n}\n\nexport function injectRtlDir(html: string): string {\n const root = parse(html);\n const htmlEl = root.querySelector(\"html\");\n\n if (htmlEl) {\n htmlEl.setAttribute(\"dir\", \"rtl\");\n return root.toString();\n }\n\n for (const child of root.childNodes) {\n if (child.nodeType === NodeType.ELEMENT_NODE) {\n (child as ParsedElement).setAttribute(\"dir\", \"rtl\");\n break;\n }\n }\n\n return root.toString();\n}\n"],"mappings":"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","names":["parse","NodeType","result"]}
|
|
1
|
+
{"version":3,"sources":["../src/render.ts","../src/extract.ts","../src/inject.ts","../src/prompt.ts","../src/translate.ts","../src/translate-ai.ts","../src/hash.ts","../src/cache.ts","../src/rtl.ts","../src/utils.ts","../src/client.ts"],"sourcesContent":["import { render } from \"@react-email/render\";\nimport type { ReactElement } from \"react\";\n\nexport async function renderReactEmail(\n component: ReactElement,\n): Promise<string> {\n try {\n return await render(component);\n } catch (error) {\n throw new Error(\n `i18n-email: Failed to render React component: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n}\n","import { parse, NodeType } from \"node-html-parser\";\nimport type { HTMLElement as ParsedElement, Node } from \"node-html-parser\";\n\nconst TRANSLATABLE_ATTRS = [\"alt\", \"title\"];\nconst SKIP_TAGS = new Set([\"style\", \"script\", \"head\"]);\n\nexport interface TextEntry {\n type: \"text\";\n nodes: Node[];\n text: string;\n}\n\nexport interface AttributeEntry {\n type: \"attribute\";\n element: ParsedElement;\n attrName: string;\n text: string;\n}\n\nexport type ExtractionEntry = TextEntry | AttributeEntry;\n\nexport interface ExtractionResult {\n root: ParsedElement;\n entries: ExtractionEntry[];\n uniqueStrings: string[];\n}\n\nexport function extractStrings(html: string): ExtractionResult {\n const root = parse(html);\n const entries: ExtractionEntry[] = [];\n\n function walk(element: ParsedElement): void {\n const tag = element.tagName?.toLowerCase();\n if (tag && SKIP_TAGS.has(tag)) return;\n\n for (const attr of TRANSLATABLE_ATTRS) {\n const value = element.getAttribute(attr);\n if (value && value.trim()) {\n entries.push({\n type: \"attribute\",\n element,\n attrName: attr,\n text: value,\n });\n }\n }\n\n const children = element.childNodes;\n let i = 0;\n\n while (i < children.length) {\n const child = children[i]!;\n\n if (child.nodeType === NodeType.TEXT_NODE) {\n const textNodes: Node[] = [];\n while (\n i < children.length &&\n children[i]?.nodeType === NodeType.TEXT_NODE\n ) {\n textNodes.push(children[i]!);\n i++;\n }\n const merged = textNodes.map((n) => n.rawText).join(\"\");\n const trimmed = merged.trim();\n if (trimmed && !trimmed.startsWith(\"<!DOCTYPE\")) {\n entries.push({\n type: \"text\",\n nodes: textNodes,\n text: merged,\n });\n }\n } else if (child.nodeType === NodeType.ELEMENT_NODE) {\n walk(child as ParsedElement);\n i++;\n } else {\n i++;\n }\n }\n }\n\n walk(root);\n\n const seen = new Set<string>();\n const uniqueStrings: string[] = [];\n for (const entry of entries) {\n if (!seen.has(entry.text)) {\n seen.add(entry.text);\n uniqueStrings.push(entry.text);\n }\n }\n\n return { root, entries, uniqueStrings };\n}\n","import type { ExtractionEntry } from \"./extract\";\nimport type { HTMLElement as ParsedElement } from \"node-html-parser\";\n\nexport function injectTranslations(\n root: ParsedElement,\n entries: ExtractionEntry[],\n translationMap: Map<string, string>,\n): string {\n for (const entry of entries) {\n const translated = translationMap.get(entry.text);\n if (translated === undefined) continue;\n\n if (entry.type === \"attribute\") {\n entry.element.setAttribute(entry.attrName, translated);\n } else {\n const firstNode = entry.nodes[0];\n if (!firstNode) continue;\n\n firstNode.rawText = translated;\n for (let i = 1; i < entry.nodes.length; i++) {\n const node = entry.nodes[i];\n if (node) node.rawText = \"\";\n }\n }\n }\n\n return root.toString();\n}\n","export function buildSystemPrompt(locale: string): string {\n return [\n `You are translating email content to ${locale}.`,\n \"Rules:\",\n \"- Preserve dynamic values like names, URLs, amounts, dates, and codes exactly as they appear\",\n \"- Do not translate brand names or product names\",\n \"- Preserve tone: professional but friendly\",\n ].join(\"\\n\");\n}\n\nexport function buildUserPrompt(strings: string[]): string {\n return JSON.stringify(strings);\n}\n","import type OpenAI from \"openai\";\nimport type { TranslationResponse } from \"./types\";\nimport { buildSystemPrompt, buildUserPrompt } from \"./prompt\";\n\nexport async function translateStrings(\n client: OpenAI,\n strings: string[],\n locale: string,\n model: string,\n): Promise<TranslationResponse> {\n const response = await client.chat.completions.create({\n model,\n response_format: { type: \"json_object\" },\n messages: [\n {\n role: \"system\",\n content: [\n buildSystemPrompt(locale),\n \"- Return a JSON object with two fields:\",\n ' 1. \"detectedLocale\": the ISO locale code of the source language',\n ' 2. \"translations\": a JSON array of translated strings in the exact same order as the input',\n \"- Return only the JSON object, no explanation\",\n ].join(\"\\n\"),\n },\n {\n role: \"user\",\n content: buildUserPrompt(strings),\n },\n ],\n });\n\n const content = response.choices[0]?.message?.content;\n if (!content) {\n throw new Error(\"i18n-email: OpenAI returned an empty response\");\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch {\n throw new Error(`i18n-email: OpenAI returned malformed JSON: ${content}`);\n }\n\n const result = parsed as TranslationResponse;\n if (!result.detectedLocale || !Array.isArray(result.translations)) {\n throw new Error(\n `i18n-email: Unexpected response shape from OpenAI: ${content}`,\n );\n }\n\n if (result.translations.length !== strings.length) {\n throw new Error(\n `i18n-email: Translation count mismatch. ` +\n `Expected ${strings.length}, ` +\n `got ${result.translations.length}`,\n );\n }\n\n return result;\n}\n","import type { AiLanguageModel, TranslationResponse } from \"./types\";\nimport { buildSystemPrompt, buildUserPrompt } from \"./prompt\";\n\nexport async function translateStringsWithAi(\n model: AiLanguageModel,\n strings: string[],\n locale: string,\n): Promise<TranslationResponse> {\n let generateObject: typeof import(\"ai\").generateObject;\n let jsonSchema: typeof import(\"ai\").jsonSchema;\n\n try {\n const ai = await import(\"ai\");\n generateObject = ai.generateObject;\n jsonSchema = ai.jsonSchema;\n } catch {\n throw new Error(\n 'i18n-email: The \"ai\" package is required when using an AI SDK model. ' +\n \"Install it with: npm install ai\",\n );\n }\n\n const result = await generateObject({\n model: model as Parameters<typeof generateObject>[0][\"model\"],\n schema: jsonSchema<TranslationResponse>({\n type: \"object\",\n properties: {\n detectedLocale: {\n type: \"string\",\n description: \"The ISO locale code of the source language\",\n },\n translations: {\n type: \"array\",\n items: { type: \"string\" },\n description:\n \"Translated strings in the exact same order as the input\",\n },\n },\n required: [\"detectedLocale\", \"translations\"],\n additionalProperties: false,\n }),\n system: buildSystemPrompt(locale),\n prompt: buildUserPrompt(strings),\n });\n\n const { detectedLocale, translations } = result.object;\n\n if (translations.length !== strings.length) {\n throw new Error(\n `i18n-email: Translation count mismatch. ` +\n `Expected ${strings.length}, ` +\n `got ${translations.length}`,\n );\n }\n\n return { detectedLocale, translations };\n}\n","import { createHash } from \"node:crypto\";\n\nexport function createCacheKey(\n html: string,\n subject: string,\n locale: string,\n): string {\n return createHash(\"sha256\")\n .update([html, subject, locale].join(\"\\0\"))\n .digest(\"hex\");\n}\n","import type { CacheProvider, TranslateResult } from \"./types\";\nimport { createCacheKey } from \"./hash\";\n\nfunction buildKey(cache: CacheProvider, hash: string): string {\n return cache.prefix ? `${cache.prefix}${hash}` : hash;\n}\n\nexport async function getCachedResult(\n cache: CacheProvider,\n html: string,\n subject: string,\n locale: string,\n): Promise<TranslateResult | null> {\n const key = buildKey(cache, createCacheKey(html, subject, locale));\n const cached = await cache.get(key);\n if (!cached) return null;\n return JSON.parse(cached) as TranslateResult;\n}\n\nexport async function setCachedResult(\n cache: CacheProvider,\n html: string,\n subject: string,\n locale: string,\n result: TranslateResult,\n): Promise<void> {\n const key = buildKey(cache, createCacheKey(html, subject, locale));\n await cache.set(key, JSON.stringify(result));\n}\n","import { parse, NodeType } from \"node-html-parser\";\nimport type { HTMLElement as ParsedElement } from \"node-html-parser\";\nimport { baseLocale } from \"./utils\";\n\nconst RTL_LOCALES = new Set([\n \"ar\",\n \"he\",\n \"fa\",\n \"ur\",\n \"ps\",\n \"sd\",\n \"ug\",\n \"yi\",\n \"dv\",\n]);\n\nexport function isRtlLocale(locale: string): boolean {\n return RTL_LOCALES.has(baseLocale(locale));\n}\n\nexport function injectRtlDir(html: string): string {\n const root = parse(html);\n const htmlEl = root.querySelector(\"html\");\n\n if (htmlEl) {\n htmlEl.setAttribute(\"dir\", \"rtl\");\n return root.toString();\n }\n\n for (const child of root.childNodes) {\n if (child.nodeType === NodeType.ELEMENT_NODE) {\n (child as ParsedElement).setAttribute(\"dir\", \"rtl\");\n break;\n }\n }\n\n return root.toString();\n}\n","import type { AiLanguageModel, AiSdkConfig, I18nEmailConfig } from \"./types\";\n\nexport function baseLocale(tag: string): string {\n return tag.split(\"-\")[0]!.toLowerCase();\n}\n\nexport function chunk<T>(arr: T[], size: number): T[][] {\n const chunks: T[][] = [];\n for (let i = 0; i < arr.length; i += size) {\n chunks.push(arr.slice(i, i + size));\n }\n return chunks;\n}\n\nexport function isAiLanguageModel(value: unknown): value is AiLanguageModel {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"modelId\" in value &&\n \"provider\" in value\n );\n}\n\nexport function isAiSdkConfig(config: I18nEmailConfig): config is AiSdkConfig {\n return isAiLanguageModel(config.model);\n}\n\nexport async function createOpenAIClient(\n config: import(\"./types\").OpenAIConfig,\n): Promise<import(\"openai\").default> {\n let OpenAI: typeof import(\"openai\").default;\n try {\n OpenAI = (await import(\"openai\")).default;\n } catch {\n throw new Error(\n 'i18n-email: The \"openai\" package is required when using a string model name. ' +\n \"Install it with: npm install openai\",\n );\n }\n return new OpenAI({\n apiKey: config.openaiApiKey,\n baseURL: config.baseURL,\n maxRetries: config.maxRetries ?? 2,\n });\n}\n","import type {\n I18nEmailConfig,\n TranslateOptions,\n TranslateResult,\n TranslationResponse,\n} from \"./types\";\nimport { renderReactEmail } from \"./render\";\nimport { extractStrings } from \"./extract\";\nimport { injectTranslations } from \"./inject\";\nimport { translateStrings } from \"./translate\";\nimport { translateStringsWithAi } from \"./translate-ai\";\nimport { getCachedResult, setCachedResult } from \"./cache\";\nimport { isRtlLocale, injectRtlDir } from \"./rtl\";\nimport { baseLocale, chunk, createOpenAIClient, isAiSdkConfig } from \"./utils\";\n\nconst DEFAULT_BATCH_SIZE = 50;\n\nexport function createI18nEmail(config: I18nEmailConfig) {\n const aiSdk = isAiSdkConfig(config);\n\n let clientPromise: Promise<import(\"openai\").default> | undefined;\n\n function getClient(): Promise<import(\"openai\").default> {\n if (!clientPromise) {\n clientPromise = createOpenAIClient(\n config as import(\"./types\").OpenAIConfig,\n );\n }\n return clientPromise;\n }\n\n async function translateBatch(\n strings: string[],\n locale: string,\n ): Promise<TranslationResponse> {\n if (aiSdk) {\n return translateStringsWithAi(config.model, strings, locale);\n }\n const client = await getClient();\n return translateStrings(client, strings, locale, config.model ?? \"gpt-4o\");\n }\n\n async function translate(\n options: TranslateOptions,\n ): Promise<TranslateResult> {\n const { locale, subject } = options;\n\n const html = options.react\n ? await renderReactEmail(options.react)\n : options.html;\n\n if (config.cache) {\n const cached = await getCachedResult(config.cache, html, subject, locale);\n if (cached) {\n config.onTranslate?.({\n locale,\n detectedLocale: locale,\n strings: [],\n cacheHit: true,\n });\n return cached;\n }\n }\n\n const { root, entries, uniqueStrings } = extractStrings(html);\n\n const allStrings = [subject, ...uniqueStrings];\n const batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;\n const batches = chunk(allStrings, batchSize);\n\n const firstBatch = batches[0];\n if (!firstBatch || firstBatch.length === 0) {\n return { subject, html };\n }\n\n const firstResponse = await translateBatch(firstBatch, locale);\n\n if (baseLocale(firstResponse.detectedLocale) === baseLocale(locale)) {\n const result: TranslateResult = { subject, html };\n if (config.cache) {\n await setCachedResult(config.cache, html, subject, locale, result);\n }\n config.onTranslate?.({\n locale,\n detectedLocale: firstResponse.detectedLocale,\n strings: allStrings,\n cacheHit: false,\n });\n return result;\n }\n\n const allTranslations = [...firstResponse.translations];\n\n for (let i = 1; i < batches.length; i++) {\n const batch = batches[i]!;\n const response = await translateBatch(batch, locale);\n allTranslations.push(...response.translations);\n }\n\n const translatedSubject = allTranslations[0]!;\n const translationMap = new Map<string, string>();\n uniqueStrings.forEach((original, i) => {\n translationMap.set(original, allTranslations[i + 1]!);\n });\n\n let translatedHtml = injectTranslations(root, entries, translationMap);\n\n if (isRtlLocale(locale)) {\n translatedHtml = injectRtlDir(translatedHtml);\n }\n\n const result: TranslateResult = {\n subject: translatedSubject,\n html: translatedHtml,\n };\n\n if (config.cache) {\n await setCachedResult(config.cache, html, subject, locale, result);\n }\n\n config.onTranslate?.({\n locale,\n detectedLocale: firstResponse.detectedLocale,\n strings: allStrings,\n cacheHit: false,\n });\n\n return result;\n }\n\n return { translate };\n}\n"],"mappings":"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","names":["parse","NodeType","parse","NodeType","result"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "i18n-email",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Translate transactional emails into any language using AI models.",
|
|
6
6
|
"author": "Dan Zabrotski",
|
|
@@ -41,19 +41,32 @@
|
|
|
41
41
|
],
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "tsup",
|
|
44
|
-
"check-types": "tsc --noEmit"
|
|
44
|
+
"check-types": "tsc --noEmit",
|
|
45
|
+
"test": "bun test"
|
|
45
46
|
},
|
|
46
47
|
"dependencies": {
|
|
47
48
|
"@react-email/render": "^1",
|
|
48
|
-
"node-html-parser": "^6"
|
|
49
|
-
"openai": "^4"
|
|
49
|
+
"node-html-parser": "^6"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
+
"ai": ">=4",
|
|
53
|
+
"openai": ">=4",
|
|
52
54
|
"react": ">=18"
|
|
53
55
|
},
|
|
56
|
+
"peerDependenciesMeta": {
|
|
57
|
+
"ai": {
|
|
58
|
+
"optional": true
|
|
59
|
+
},
|
|
60
|
+
"openai": {
|
|
61
|
+
"optional": true
|
|
62
|
+
}
|
|
63
|
+
},
|
|
54
64
|
"devDependencies": {
|
|
65
|
+
"@ai-sdk/openai": "^3",
|
|
55
66
|
"@types/bun": "latest",
|
|
56
67
|
"@types/react": "^19",
|
|
68
|
+
"ai": "^6",
|
|
69
|
+
"openai": "^4",
|
|
57
70
|
"tsup": "^8",
|
|
58
71
|
"typescript": "^5"
|
|
59
72
|
}
|