i18n-email 0.2.0 → 0.4.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 CHANGED
@@ -5,13 +5,15 @@ 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 OpenAI call
8
+ - Translates the **subject line and body** in a single API call
9
9
  - **Batches large emails** — splits strings into chunks to stay within model limits
10
10
  - Skips `<style>`, `<script>`, and `<head>` — only visible text is sent
11
11
  - Injects `dir="rtl"` automatically for Arabic, Hebrew, Persian, Urdu, and more
12
12
  - Optional **cache layer** with a key prefix to avoid redundant API calls
13
13
  - **`onTranslate` hook** for logging and analytics
14
14
  - Supports **any OpenAI-compatible API** via `baseURL`
15
+ - **AI SDK support** — pass any Vercel AI SDK model (`openai()`, `anthropic()`, `google()`, etc.)
16
+ - **TanStack AI support** — pass any TanStack AI adapter (`openai("gpt-4o")`, `anthropic("claude-4-sonnet")`, etc.)
15
17
 
16
18
  ## Install
17
19
 
@@ -23,6 +25,19 @@ npm install i18n-email
23
25
 
24
26
  Requires `react >= 18` as a peer dependency.
25
27
 
28
+ To use the Vercel AI SDK instead of the OpenAI client directly:
29
+
30
+ ```bash
31
+ bun add ai @ai-sdk/openai
32
+ ```
33
+
34
+ To use TanStack AI adapters:
35
+
36
+ ```bash
37
+ bun add @tanstack/ai @tanstack/ai-openai
38
+ # or for other providers: @tanstack/ai-anthropic, etc.
39
+ ```
40
+
26
41
  ## Usage
27
42
 
28
43
  ### With a React Email component
@@ -51,6 +66,54 @@ const { subject, html } = await i18nEmail.translate({
51
66
  });
52
67
  ```
53
68
 
69
+ ### With AI SDK (Vercel)
70
+
71
+ Use any AI SDK provider — OpenAI, Anthropic, Google, Mistral, and more:
72
+
73
+ ```ts
74
+ import { createI18nEmail } from "i18n-email";
75
+ import { openai } from "@ai-sdk/openai";
76
+
77
+ const i18nEmail = createI18nEmail({
78
+ model: openai("gpt-4o"),
79
+ });
80
+
81
+ const { subject, html } = await i18nEmail.translate({
82
+ locale: "ja",
83
+ subject: "Welcome!",
84
+ html: "<h1>Welcome!</h1>",
85
+ });
86
+ ```
87
+
88
+ Works with any provider:
89
+
90
+ ```ts
91
+ import { anthropic } from "@ai-sdk/anthropic";
92
+
93
+ const i18nEmail = createI18nEmail({
94
+ model: anthropic("claude-4-sonnet"),
95
+ });
96
+ ```
97
+
98
+ ### With TanStack AI
99
+
100
+ Pass a TanStack AI adapter as `adapter` — no `openaiApiKey` needed:
101
+
102
+ ```ts
103
+ import { createI18nEmail } from "i18n-email";
104
+ import { createOpenaiChat } from "@tanstack/ai-openai";
105
+
106
+ const openai = createOpenaiChat("gpt-4o", process.env.OPENAI_API_KEY!);
107
+
108
+ const i18nEmail = createI18nEmail({ adapter: openai });
109
+
110
+ const { subject, html } = await i18nEmail.translate({
111
+ locale: "ja",
112
+ subject: "Welcome!",
113
+ html: "<h1>Welcome!</h1>",
114
+ });
115
+ ```
116
+
54
117
  ### With caching (Upstash Redis example)
55
118
 
56
119
  ```ts
@@ -92,15 +155,28 @@ const i18nEmail = createI18nEmail({
92
155
 
93
156
  ### `createI18nEmail(config)`
94
157
 
95
- | Option | Type | Default | Description |
96
- | -------------- | --------------------------------------- | ---------- | --------------------------------------------- |
97
- | `openaiApiKey` | `string` | — | OpenAI API key |
98
- | `model` | `string` | `"gpt-4o"` | Any OpenAI chat model |
99
- | `baseURL` | `string` | | Override the API base URL (Azure, Groq, etc.) |
100
- | `maxRetries` | `number` | `2` | Retries on transient OpenAI errors |
101
- | `batchSize` | `number` | `50` | Max strings per OpenAI request |
102
- | `cache` | `CacheProvider` | | Cache adapter to avoid redundant API calls |
103
- | `onTranslate` | `(info: TranslateCallbackInfo) => void` | — | Hook called after every translate call |
158
+ The config is a discriminated union — pick one backend:
159
+
160
+ **OpenAI (default)**
161
+
162
+ | Option | Type | Default | Description |
163
+ | -------------- | -------- | ---------- | ---------------------------------------- |
164
+ | `openaiApiKey` | `string` | | OpenAI API key (required) |
165
+ | `model` | `string` | `"gpt-4o"` | OpenAI model name |
166
+ | `baseURL` | `string` | — | Override the API base URL (Azure, Groq…) |
167
+ | `maxRetries` | `number` | `2` | Retries on transient errors |
168
+
169
+ **Vercel AI SDK** — set `model` to an AI SDK `LanguageModel` (e.g. `openai("gpt-4o")`)
170
+
171
+ **TanStack AI** — set `adapter` to a TanStack `TextAdapter` (e.g. `createOpenaiChat(...)` from `@tanstack/ai-openai`)
172
+
173
+ **Shared options**
174
+
175
+ | Option | Type | Default | Description |
176
+ | ------------- | --------------------------------------- | ------- | -------------------------------------- |
177
+ | `batchSize` | `number` | `50` | Max strings per API request |
178
+ | `cache` | `CacheProvider` | — | Cache adapter to skip redundant calls |
179
+ | `onTranslate` | `(info: TranslateCallbackInfo) => void` | — | Hook called after every translate call |
104
180
 
105
181
  Returns `{ translate }`.
106
182
 
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) {
@@ -129,6 +126,20 @@ function injectTranslations(root, entries, translationMap) {
129
126
  return root.toString();
130
127
  }
131
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
+
132
143
  // src/translate.ts
133
144
  async function translateStrings(client, strings, locale, model) {
134
145
  const response = await client.chat.completions.create({
@@ -138,20 +149,16 @@ async function translateStrings(client, strings, locale, model) {
138
149
  {
139
150
  role: "system",
140
151
  content: [
141
- `You are translating email content to ${locale}.`,
142
- "Rules:",
152
+ buildSystemPrompt(locale),
143
153
  "- Return a JSON object with two fields:",
144
154
  ' 1. "detectedLocale": the ISO locale code of the source language',
145
155
  ' 2. "translations": a JSON array of translated strings in the exact same order as the input',
146
- "- Preserve dynamic values like names, URLs, amounts, dates, and codes exactly as they appear",
147
- "- Do not translate brand names or product names",
148
- "- Preserve tone: professional but friendly",
149
156
  "- Return only the JSON object, no explanation"
150
157
  ].join("\n")
151
158
  },
152
159
  {
153
160
  role: "user",
154
- content: JSON.stringify(strings)
161
+ content: buildUserPrompt(strings)
155
162
  }
156
163
  ]
157
164
  });
@@ -179,6 +186,91 @@ async function translateStrings(client, strings, locale, model) {
179
186
  return result;
180
187
  }
181
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
+
232
+ // src/translate-tanstack.ts
233
+ var OUTPUT_SCHEMA = {
234
+ type: "object",
235
+ properties: {
236
+ detectedLocale: {
237
+ type: "string",
238
+ description: "The ISO locale code of the source language"
239
+ },
240
+ translations: {
241
+ type: "array",
242
+ items: { type: "string" },
243
+ description: "Translated strings in the exact same order as the input"
244
+ }
245
+ },
246
+ required: ["detectedLocale", "translations"],
247
+ additionalProperties: false
248
+ };
249
+ async function translateStringsWithTanstack(adapter, strings, locale) {
250
+ let chat;
251
+ try {
252
+ chat = (await import("@tanstack/ai")).chat;
253
+ } catch {
254
+ throw new Error(
255
+ 'i18n-email: The "@tanstack/ai" package is required when using a TanStack adapter. Install it with: npm install @tanstack/ai'
256
+ );
257
+ }
258
+ const result = await chat({
259
+ adapter,
260
+ systemPrompts: [buildSystemPrompt(locale)],
261
+ messages: [{ role: "user", content: buildUserPrompt(strings) }],
262
+ outputSchema: OUTPUT_SCHEMA,
263
+ stream: false
264
+ });
265
+ const { detectedLocale, translations } = result;
266
+ if (translations.length !== strings.length) {
267
+ throw new Error(
268
+ `i18n-email: Translation count mismatch. Expected ${strings.length}, got ${translations.length}`
269
+ );
270
+ }
271
+ return { detectedLocale, translations };
272
+ }
273
+
182
274
  // src/hash.ts
183
275
  var import_node_crypto = require("crypto");
184
276
  function createCacheKey(html, subject, locale) {
@@ -214,6 +306,33 @@ function chunk(arr, size) {
214
306
  }
215
307
  return chunks;
216
308
  }
309
+ function isAiLanguageModel(value) {
310
+ return typeof value === "object" && value !== null && "modelId" in value && "provider" in value;
311
+ }
312
+ function isAiSdkConfig(config) {
313
+ return isAiLanguageModel(config.model);
314
+ }
315
+ function isTanstackAiAdapter(value) {
316
+ return typeof value === "object" && value !== null && value["kind"] === "text" && typeof value["name"] === "string";
317
+ }
318
+ function isTanstackAiAdapterConfig(config) {
319
+ return isTanstackAiAdapter(config.adapter);
320
+ }
321
+ async function createOpenAIClient(config) {
322
+ let OpenAI;
323
+ try {
324
+ OpenAI = (await import("openai")).default;
325
+ } catch {
326
+ throw new Error(
327
+ 'i18n-email: The "openai" package is required when using a string model name. Install it with: npm install openai'
328
+ );
329
+ }
330
+ return new OpenAI({
331
+ apiKey: config.openaiApiKey,
332
+ baseURL: config.baseURL,
333
+ maxRetries: config.maxRetries ?? 2
334
+ });
335
+ }
217
336
 
218
337
  // src/rtl.ts
219
338
  var RTL_LOCALES = /* @__PURE__ */ new Set([
@@ -249,11 +368,27 @@ function injectRtlDir(html) {
249
368
  // src/client.ts
250
369
  var DEFAULT_BATCH_SIZE = 50;
251
370
  function createI18nEmail(config) {
252
- const client = new import_openai.default({
253
- apiKey: config.openaiApiKey,
254
- baseURL: config.baseURL,
255
- maxRetries: config.maxRetries ?? 2
256
- });
371
+ const aiSdk = isAiSdkConfig(config);
372
+ const tanstackAi = isTanstackAiAdapterConfig(config);
373
+ let clientPromise;
374
+ function getClient() {
375
+ if (!clientPromise) {
376
+ clientPromise = createOpenAIClient(
377
+ config
378
+ );
379
+ }
380
+ return clientPromise;
381
+ }
382
+ async function translateBatch(strings, locale) {
383
+ if (tanstackAi) {
384
+ return translateStringsWithTanstack(config.adapter, strings, locale);
385
+ }
386
+ if (aiSdk) {
387
+ return translateStringsWithAi(config.model, strings, locale);
388
+ }
389
+ const client = await getClient();
390
+ return translateStrings(client, strings, locale, config.model ?? "gpt-4o");
391
+ }
257
392
  async function translate(options) {
258
393
  const { locale, subject } = options;
259
394
  const html = options.react ? await renderReactEmail(options.react) : options.html;
@@ -271,19 +406,13 @@ function createI18nEmail(config) {
271
406
  }
272
407
  const { root, entries, uniqueStrings } = extractStrings(html);
273
408
  const allStrings = [subject, ...uniqueStrings];
274
- const model = config.model ?? "gpt-4o";
275
409
  const batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
276
410
  const batches = chunk(allStrings, batchSize);
277
411
  const firstBatch = batches[0];
278
412
  if (!firstBatch || firstBatch.length === 0) {
279
413
  return { subject, html };
280
414
  }
281
- const firstResponse = await translateStrings(
282
- client,
283
- firstBatch,
284
- locale,
285
- model
286
- );
415
+ const firstResponse = await translateBatch(firstBatch, locale);
287
416
  if (baseLocale(firstResponse.detectedLocale) === baseLocale(locale)) {
288
417
  const result2 = { subject, html };
289
418
  if (config.cache) {
@@ -300,7 +429,7 @@ function createI18nEmail(config) {
300
429
  const allTranslations = [...firstResponse.translations];
301
430
  for (let i = 1; i < batches.length; i++) {
302
431
  const batch = batches[i];
303
- const response = await translateStrings(client, batch, locale, model);
432
+ const response = await translateBatch(batch, locale);
304
433
  allTranslations.push(...response.translations);
305
434
  }
306
435
  const translatedSubject = allTranslations[0];
@@ -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","../src/utils.ts"],"sourcesContent":["export { createI18nEmail } from \"./client\";\nexport type {\n I18nEmailConfig,\n CacheProvider,\n TranslateOptions,\n TranslateOptionsReact,\n TranslateOptionsHtml,\n TranslateResult,\n TranslationResponse,\n TranslateCallbackInfo,\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\";\nimport { baseLocale, chunk } from \"./utils\";\n\nconst DEFAULT_BATCH_SIZE = 50;\n\nexport function createI18nEmail(config: I18nEmailConfig) {\n const client = new OpenAI({\n apiKey: config.openaiApiKey,\n baseURL: config.baseURL,\n maxRetries: config.maxRetries ?? 2,\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 model = config.model ?? \"gpt-4o\";\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 translateStrings(\n client,\n firstBatch,\n locale,\n model,\n );\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 translateStrings(client, batch, locale, model);\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","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","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].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","export 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"],"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/translate-tanstack.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 TanstackAiTextAdapter,\n TanstackAiAdapterConfig,\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 type { TanstackAiTextAdapter, TranslationResponse } from \"./types\";\nimport { buildSystemPrompt, buildUserPrompt } from \"./prompt\";\n\nconst OUTPUT_SCHEMA = {\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: \"Translated strings in the exact same order as the input\",\n },\n },\n required: [\"detectedLocale\", \"translations\"],\n additionalProperties: false,\n};\n\nexport async function translateStringsWithTanstack(\n adapter: TanstackAiTextAdapter,\n strings: string[],\n locale: string,\n): Promise<TranslationResponse> {\n let chat: typeof import(\"@tanstack/ai\").chat;\n try {\n chat = (await import(\"@tanstack/ai\")).chat;\n } catch {\n throw new Error(\n 'i18n-email: The \"@tanstack/ai\" package is required when using a TanStack adapter. ' +\n \"Install it with: npm install @tanstack/ai\",\n );\n }\n\n const result = await chat({\n adapter,\n systemPrompts: [buildSystemPrompt(locale)],\n messages: [{ role: \"user\", content: buildUserPrompt(strings) }],\n outputSchema: OUTPUT_SCHEMA,\n stream: false,\n });\n\n const { detectedLocale, translations } = result as TranslationResponse;\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 {\n AiLanguageModel,\n AiSdkConfig,\n I18nEmailConfig,\n TanstackAiTextAdapter,\n TanstackAiAdapterConfig,\n} 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 function isTanstackAiAdapter(\n value: unknown,\n): value is TanstackAiTextAdapter {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as Record<string, unknown>)[\"kind\"] === \"text\" &&\n typeof (value as Record<string, unknown>)[\"name\"] === \"string\"\n );\n}\n\nexport function isTanstackAiAdapterConfig(\n config: I18nEmailConfig,\n): config is TanstackAiAdapterConfig {\n return isTanstackAiAdapter(config.adapter);\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 { translateStringsWithTanstack } from \"./translate-tanstack\";\nimport { getCachedResult, setCachedResult } from \"./cache\";\nimport { isRtlLocale, injectRtlDir } from \"./rtl\";\nimport {\n baseLocale,\n chunk,\n createOpenAIClient,\n isAiSdkConfig,\n isTanstackAiAdapterConfig,\n} from \"./utils\";\n\nconst DEFAULT_BATCH_SIZE = 50;\n\nexport function createI18nEmail(config: I18nEmailConfig) {\n const aiSdk = isAiSdkConfig(config);\n const tanstackAi = isTanstackAiAdapterConfig(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 (tanstackAi) {\n return translateStringsWithTanstack(config.adapter, strings, locale);\n }\n\n if (aiSdk) {\n return translateStringsWithAi(config.model, strings, locale);\n }\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
@@ -1,19 +1,42 @@
1
1
  import { ReactElement } from 'react';
2
+ import { AnyTextAdapter } from '@tanstack/ai';
2
3
 
3
4
  interface CacheProvider {
4
5
  prefix?: string;
5
6
  get: (key: string) => Promise<string | null>;
6
7
  set: (key: string, value: string) => Promise<void>;
7
8
  }
8
- interface I18nEmailConfig {
9
+ /**
10
+ * Minimal duck type for an AI SDK language model.
11
+ * Compatible with any provider: `openai("gpt-4o")`, `anthropic("claude-4-sonnet")`, etc.
12
+ */
13
+ interface AiLanguageModel {
14
+ readonly specificationVersion: string;
15
+ readonly modelId: string;
16
+ readonly provider: string;
17
+ }
18
+ type TanstackAiTextAdapter = AnyTextAdapter;
19
+ interface SharedConfig {
20
+ batchSize?: number;
21
+ cache?: CacheProvider;
22
+ onTranslate?: (info: TranslateCallbackInfo) => void;
23
+ }
24
+ interface OpenAIConfig extends SharedConfig {
9
25
  openaiApiKey: string;
10
26
  model?: string;
11
27
  baseURL?: string;
12
28
  maxRetries?: number;
13
- batchSize?: number;
14
- cache?: CacheProvider;
15
- onTranslate?: (info: TranslateCallbackInfo) => void;
29
+ adapter?: never;
30
+ }
31
+ interface AiSdkConfig extends SharedConfig {
32
+ model: AiLanguageModel;
33
+ adapter?: never;
34
+ }
35
+ interface TanstackAiAdapterConfig extends SharedConfig {
36
+ adapter: TanstackAiTextAdapter;
37
+ model?: never;
16
38
  }
39
+ type I18nEmailConfig = OpenAIConfig | AiSdkConfig | TanstackAiAdapterConfig;
17
40
  interface TranslateCallbackInfo {
18
41
  locale: string;
19
42
  detectedLocale: string;
@@ -46,4 +69,4 @@ declare function createI18nEmail(config: I18nEmailConfig): {
46
69
  translate: (options: TranslateOptions) => Promise<TranslateResult>;
47
70
  };
48
71
 
49
- export { type CacheProvider, type I18nEmailConfig, type TranslateCallbackInfo, type TranslateOptions, type TranslateOptionsHtml, type TranslateOptionsReact, type TranslateResult, type TranslationResponse, createI18nEmail };
72
+ export { type AiLanguageModel, type AiSdkConfig, type CacheProvider, type I18nEmailConfig, type OpenAIConfig, type TanstackAiAdapterConfig, type TanstackAiTextAdapter, type TranslateCallbackInfo, type TranslateOptions, type TranslateOptionsHtml, type TranslateOptionsReact, type TranslateResult, type TranslationResponse, createI18nEmail };
package/dist/index.d.ts CHANGED
@@ -1,19 +1,42 @@
1
1
  import { ReactElement } from 'react';
2
+ import { AnyTextAdapter } from '@tanstack/ai';
2
3
 
3
4
  interface CacheProvider {
4
5
  prefix?: string;
5
6
  get: (key: string) => Promise<string | null>;
6
7
  set: (key: string, value: string) => Promise<void>;
7
8
  }
8
- interface I18nEmailConfig {
9
+ /**
10
+ * Minimal duck type for an AI SDK language model.
11
+ * Compatible with any provider: `openai("gpt-4o")`, `anthropic("claude-4-sonnet")`, etc.
12
+ */
13
+ interface AiLanguageModel {
14
+ readonly specificationVersion: string;
15
+ readonly modelId: string;
16
+ readonly provider: string;
17
+ }
18
+ type TanstackAiTextAdapter = AnyTextAdapter;
19
+ interface SharedConfig {
20
+ batchSize?: number;
21
+ cache?: CacheProvider;
22
+ onTranslate?: (info: TranslateCallbackInfo) => void;
23
+ }
24
+ interface OpenAIConfig extends SharedConfig {
9
25
  openaiApiKey: string;
10
26
  model?: string;
11
27
  baseURL?: string;
12
28
  maxRetries?: number;
13
- batchSize?: number;
14
- cache?: CacheProvider;
15
- onTranslate?: (info: TranslateCallbackInfo) => void;
29
+ adapter?: never;
30
+ }
31
+ interface AiSdkConfig extends SharedConfig {
32
+ model: AiLanguageModel;
33
+ adapter?: never;
34
+ }
35
+ interface TanstackAiAdapterConfig extends SharedConfig {
36
+ adapter: TanstackAiTextAdapter;
37
+ model?: never;
16
38
  }
39
+ type I18nEmailConfig = OpenAIConfig | AiSdkConfig | TanstackAiAdapterConfig;
17
40
  interface TranslateCallbackInfo {
18
41
  locale: string;
19
42
  detectedLocale: string;
@@ -46,4 +69,4 @@ declare function createI18nEmail(config: I18nEmailConfig): {
46
69
  translate: (options: TranslateOptions) => Promise<TranslateResult>;
47
70
  };
48
71
 
49
- export { type CacheProvider, type I18nEmailConfig, type TranslateCallbackInfo, type TranslateOptions, type TranslateOptionsHtml, type TranslateOptionsReact, type TranslateResult, type TranslationResponse, createI18nEmail };
72
+ export { type AiLanguageModel, type AiSdkConfig, type CacheProvider, type I18nEmailConfig, type OpenAIConfig, type TanstackAiAdapterConfig, type TanstackAiTextAdapter, 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) {
@@ -93,6 +90,20 @@ function injectTranslations(root, entries, translationMap) {
93
90
  return root.toString();
94
91
  }
95
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
+
96
107
  // src/translate.ts
97
108
  async function translateStrings(client, strings, locale, model) {
98
109
  const response = await client.chat.completions.create({
@@ -102,20 +113,16 @@ async function translateStrings(client, strings, locale, model) {
102
113
  {
103
114
  role: "system",
104
115
  content: [
105
- `You are translating email content to ${locale}.`,
106
- "Rules:",
116
+ buildSystemPrompt(locale),
107
117
  "- Return a JSON object with two fields:",
108
118
  ' 1. "detectedLocale": the ISO locale code of the source language',
109
119
  ' 2. "translations": a JSON array of translated strings in the exact same order as the input',
110
- "- Preserve dynamic values like names, URLs, amounts, dates, and codes exactly as they appear",
111
- "- Do not translate brand names or product names",
112
- "- Preserve tone: professional but friendly",
113
120
  "- Return only the JSON object, no explanation"
114
121
  ].join("\n")
115
122
  },
116
123
  {
117
124
  role: "user",
118
- content: JSON.stringify(strings)
125
+ content: buildUserPrompt(strings)
119
126
  }
120
127
  ]
121
128
  });
@@ -143,6 +150,91 @@ async function translateStrings(client, strings, locale, model) {
143
150
  return result;
144
151
  }
145
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
+
196
+ // src/translate-tanstack.ts
197
+ var OUTPUT_SCHEMA = {
198
+ type: "object",
199
+ properties: {
200
+ detectedLocale: {
201
+ type: "string",
202
+ description: "The ISO locale code of the source language"
203
+ },
204
+ translations: {
205
+ type: "array",
206
+ items: { type: "string" },
207
+ description: "Translated strings in the exact same order as the input"
208
+ }
209
+ },
210
+ required: ["detectedLocale", "translations"],
211
+ additionalProperties: false
212
+ };
213
+ async function translateStringsWithTanstack(adapter, strings, locale) {
214
+ let chat;
215
+ try {
216
+ chat = (await import("@tanstack/ai")).chat;
217
+ } catch {
218
+ throw new Error(
219
+ 'i18n-email: The "@tanstack/ai" package is required when using a TanStack adapter. Install it with: npm install @tanstack/ai'
220
+ );
221
+ }
222
+ const result = await chat({
223
+ adapter,
224
+ systemPrompts: [buildSystemPrompt(locale)],
225
+ messages: [{ role: "user", content: buildUserPrompt(strings) }],
226
+ outputSchema: OUTPUT_SCHEMA,
227
+ stream: false
228
+ });
229
+ const { detectedLocale, translations } = result;
230
+ if (translations.length !== strings.length) {
231
+ throw new Error(
232
+ `i18n-email: Translation count mismatch. Expected ${strings.length}, got ${translations.length}`
233
+ );
234
+ }
235
+ return { detectedLocale, translations };
236
+ }
237
+
146
238
  // src/hash.ts
147
239
  import { createHash } from "crypto";
148
240
  function createCacheKey(html, subject, locale) {
@@ -178,6 +270,33 @@ function chunk(arr, size) {
178
270
  }
179
271
  return chunks;
180
272
  }
273
+ function isAiLanguageModel(value) {
274
+ return typeof value === "object" && value !== null && "modelId" in value && "provider" in value;
275
+ }
276
+ function isAiSdkConfig(config) {
277
+ return isAiLanguageModel(config.model);
278
+ }
279
+ function isTanstackAiAdapter(value) {
280
+ return typeof value === "object" && value !== null && value["kind"] === "text" && typeof value["name"] === "string";
281
+ }
282
+ function isTanstackAiAdapterConfig(config) {
283
+ return isTanstackAiAdapter(config.adapter);
284
+ }
285
+ async function createOpenAIClient(config) {
286
+ let OpenAI;
287
+ try {
288
+ OpenAI = (await import("openai")).default;
289
+ } catch {
290
+ throw new Error(
291
+ 'i18n-email: The "openai" package is required when using a string model name. Install it with: npm install openai'
292
+ );
293
+ }
294
+ return new OpenAI({
295
+ apiKey: config.openaiApiKey,
296
+ baseURL: config.baseURL,
297
+ maxRetries: config.maxRetries ?? 2
298
+ });
299
+ }
181
300
 
182
301
  // src/rtl.ts
183
302
  var RTL_LOCALES = /* @__PURE__ */ new Set([
@@ -213,11 +332,27 @@ function injectRtlDir(html) {
213
332
  // src/client.ts
214
333
  var DEFAULT_BATCH_SIZE = 50;
215
334
  function createI18nEmail(config) {
216
- const client = new OpenAI({
217
- apiKey: config.openaiApiKey,
218
- baseURL: config.baseURL,
219
- maxRetries: config.maxRetries ?? 2
220
- });
335
+ const aiSdk = isAiSdkConfig(config);
336
+ const tanstackAi = isTanstackAiAdapterConfig(config);
337
+ let clientPromise;
338
+ function getClient() {
339
+ if (!clientPromise) {
340
+ clientPromise = createOpenAIClient(
341
+ config
342
+ );
343
+ }
344
+ return clientPromise;
345
+ }
346
+ async function translateBatch(strings, locale) {
347
+ if (tanstackAi) {
348
+ return translateStringsWithTanstack(config.adapter, strings, locale);
349
+ }
350
+ if (aiSdk) {
351
+ return translateStringsWithAi(config.model, strings, locale);
352
+ }
353
+ const client = await getClient();
354
+ return translateStrings(client, strings, locale, config.model ?? "gpt-4o");
355
+ }
221
356
  async function translate(options) {
222
357
  const { locale, subject } = options;
223
358
  const html = options.react ? await renderReactEmail(options.react) : options.html;
@@ -235,19 +370,13 @@ function createI18nEmail(config) {
235
370
  }
236
371
  const { root, entries, uniqueStrings } = extractStrings(html);
237
372
  const allStrings = [subject, ...uniqueStrings];
238
- const model = config.model ?? "gpt-4o";
239
373
  const batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
240
374
  const batches = chunk(allStrings, batchSize);
241
375
  const firstBatch = batches[0];
242
376
  if (!firstBatch || firstBatch.length === 0) {
243
377
  return { subject, html };
244
378
  }
245
- const firstResponse = await translateStrings(
246
- client,
247
- firstBatch,
248
- locale,
249
- model
250
- );
379
+ const firstResponse = await translateBatch(firstBatch, locale);
251
380
  if (baseLocale(firstResponse.detectedLocale) === baseLocale(locale)) {
252
381
  const result2 = { subject, html };
253
382
  if (config.cache) {
@@ -264,7 +393,7 @@ function createI18nEmail(config) {
264
393
  const allTranslations = [...firstResponse.translations];
265
394
  for (let i = 1; i < batches.length; i++) {
266
395
  const batch = batches[i];
267
- const response = await translateStrings(client, batch, locale, model);
396
+ const response = await translateBatch(batch, locale);
268
397
  allTranslations.push(...response.translations);
269
398
  }
270
399
  const translatedSubject = allTranslations[0];
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","../src/utils.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\";\nimport { baseLocale, chunk } from \"./utils\";\n\nconst DEFAULT_BATCH_SIZE = 50;\n\nexport function createI18nEmail(config: I18nEmailConfig) {\n const client = new OpenAI({\n apiKey: config.openaiApiKey,\n baseURL: config.baseURL,\n maxRetries: config.maxRetries ?? 2,\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 model = config.model ?? \"gpt-4o\";\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 translateStrings(\n client,\n firstBatch,\n locale,\n model,\n );\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 translateStrings(client, batch, locale, model);\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","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","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].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","export 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"],"mappings":"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","names":["parse","NodeType","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/translate-tanstack.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 type { TanstackAiTextAdapter, TranslationResponse } from \"./types\";\nimport { buildSystemPrompt, buildUserPrompt } from \"./prompt\";\n\nconst OUTPUT_SCHEMA = {\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: \"Translated strings in the exact same order as the input\",\n },\n },\n required: [\"detectedLocale\", \"translations\"],\n additionalProperties: false,\n};\n\nexport async function translateStringsWithTanstack(\n adapter: TanstackAiTextAdapter,\n strings: string[],\n locale: string,\n): Promise<TranslationResponse> {\n let chat: typeof import(\"@tanstack/ai\").chat;\n try {\n chat = (await import(\"@tanstack/ai\")).chat;\n } catch {\n throw new Error(\n 'i18n-email: The \"@tanstack/ai\" package is required when using a TanStack adapter. ' +\n \"Install it with: npm install @tanstack/ai\",\n );\n }\n\n const result = await chat({\n adapter,\n systemPrompts: [buildSystemPrompt(locale)],\n messages: [{ role: \"user\", content: buildUserPrompt(strings) }],\n outputSchema: OUTPUT_SCHEMA,\n stream: false,\n });\n\n const { detectedLocale, translations } = result as TranslationResponse;\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 {\n AiLanguageModel,\n AiSdkConfig,\n I18nEmailConfig,\n TanstackAiTextAdapter,\n TanstackAiAdapterConfig,\n} 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 function isTanstackAiAdapter(\n value: unknown,\n): value is TanstackAiTextAdapter {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as Record<string, unknown>)[\"kind\"] === \"text\" &&\n typeof (value as Record<string, unknown>)[\"name\"] === \"string\"\n );\n}\n\nexport function isTanstackAiAdapterConfig(\n config: I18nEmailConfig,\n): config is TanstackAiAdapterConfig {\n return isTanstackAiAdapter(config.adapter);\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 { translateStringsWithTanstack } from \"./translate-tanstack\";\nimport { getCachedResult, setCachedResult } from \"./cache\";\nimport { isRtlLocale, injectRtlDir } from \"./rtl\";\nimport {\n baseLocale,\n chunk,\n createOpenAIClient,\n isAiSdkConfig,\n isTanstackAiAdapterConfig,\n} from \"./utils\";\n\nconst DEFAULT_BATCH_SIZE = 50;\n\nexport function createI18nEmail(config: I18nEmailConfig) {\n const aiSdk = isAiSdkConfig(config);\n const tanstackAi = isTanstackAiAdapterConfig(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 (tanstackAi) {\n return translateStringsWithTanstack(config.adapter, strings, locale);\n }\n\n if (aiSdk) {\n return translateStringsWithAi(config.model, strings, locale);\n }\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.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "description": "Translate transactional emails into any language using AI models.",
6
6
  "author": "Dan Zabrotski",
@@ -46,15 +46,32 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@react-email/render": "^1",
49
- "node-html-parser": "^6",
50
- "openai": "^4"
49
+ "node-html-parser": "^6"
51
50
  },
52
51
  "peerDependencies": {
52
+ "@tanstack/ai": ">=0.8",
53
+ "ai": ">=4",
54
+ "openai": ">=4",
53
55
  "react": ">=18"
54
56
  },
57
+ "peerDependenciesMeta": {
58
+ "@tanstack/ai": {
59
+ "optional": true
60
+ },
61
+ "ai": {
62
+ "optional": true
63
+ },
64
+ "openai": {
65
+ "optional": true
66
+ }
67
+ },
55
68
  "devDependencies": {
69
+ "@ai-sdk/openai": "^3",
70
+ "@tanstack/ai": "^0.8.1",
56
71
  "@types/bun": "latest",
57
72
  "@types/react": "^19",
73
+ "ai": "^6",
74
+ "openai": "^4",
58
75
  "tsup": "^8",
59
76
  "typescript": "^5"
60
77
  }