i18n-email 0.2.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 CHANGED
@@ -5,13 +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 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.)
15
16
 
16
17
  ## Install
17
18
 
@@ -23,6 +24,12 @@ npm install i18n-email
23
24
 
24
25
  Requires `react >= 18` as a peer dependency.
25
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
+
26
33
  ## Usage
27
34
 
28
35
  ### With a React Email component
@@ -51,6 +58,35 @@ const { subject, html } = await i18nEmail.translate({
51
58
  });
52
59
  ```
53
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
+
54
90
  ### With caching (Upstash Redis example)
55
91
 
56
92
  ```ts
@@ -92,15 +128,15 @@ const i18nEmail = createI18nEmail({
92
128
 
93
129
  ### `createI18nEmail(config)`
94
130
 
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 |
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 |
104
140
 
105
141
  Returns `{ translate }`.
106
142
 
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,49 @@ 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
+
182
232
  // src/hash.ts
183
233
  var import_node_crypto = require("crypto");
184
234
  function createCacheKey(html, subject, locale) {
@@ -214,6 +264,27 @@ function chunk(arr, size) {
214
264
  }
215
265
  return chunks;
216
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
+ }
217
288
 
218
289
  // src/rtl.ts
219
290
  var RTL_LOCALES = /* @__PURE__ */ new Set([
@@ -249,11 +320,23 @@ function injectRtlDir(html) {
249
320
  // src/client.ts
250
321
  var DEFAULT_BATCH_SIZE = 50;
251
322
  function createI18nEmail(config) {
252
- const client = new import_openai.default({
253
- apiKey: config.openaiApiKey,
254
- baseURL: config.baseURL,
255
- maxRetries: config.maxRetries ?? 2
256
- });
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
+ }
257
340
  async function translate(options) {
258
341
  const { locale, subject } = options;
259
342
  const html = options.react ? await renderReactEmail(options.react) : options.html;
@@ -271,19 +354,13 @@ function createI18nEmail(config) {
271
354
  }
272
355
  const { root, entries, uniqueStrings } = extractStrings(html);
273
356
  const allStrings = [subject, ...uniqueStrings];
274
- const model = config.model ?? "gpt-4o";
275
357
  const batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
276
358
  const batches = chunk(allStrings, batchSize);
277
359
  const firstBatch = batches[0];
278
360
  if (!firstBatch || firstBatch.length === 0) {
279
361
  return { subject, html };
280
362
  }
281
- const firstResponse = await translateStrings(
282
- client,
283
- firstBatch,
284
- locale,
285
- model
286
- );
363
+ const firstResponse = await translateBatch(firstBatch, locale);
287
364
  if (baseLocale(firstResponse.detectedLocale) === baseLocale(locale)) {
288
365
  const result2 = { subject, html };
289
366
  if (config.cache) {
@@ -300,7 +377,7 @@ function createI18nEmail(config) {
300
377
  const allTranslations = [...firstResponse.translations];
301
378
  for (let i = 1; i < batches.length; i++) {
302
379
  const batch = batches[i];
303
- const response = await translateStrings(client, batch, locale, model);
380
+ const response = await translateBatch(batch, locale);
304
381
  allTranslations.push(...response.translations);
305
382
  }
306
383
  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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,oBAAmB;;;ACAnB,oBAAuB;AAGvB,eAAsB,iBACpB,WACiB;AACjB,MAAI;AACF,WAAO,UAAM,sBAAO,SAAS;AAAA,EAC/B,SAAS,OAAO;AACd,UAAM,IAAI;AAAA,MACR,iDACE,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CACvD;AAAA,IACF;AAAA,EACF;AACF;;;ACfA,8BAAgC;AAGhC,IAAM,qBAAqB,CAAC,OAAO,OAAO;AAC1C,IAAM,YAAY,oBAAI,IAAI,CAAC,SAAS,UAAU,MAAM,CAAC;AAuB9C,SAAS,eAAe,MAAgC;AAC7D,QAAM,WAAO,+BAAM,IAAI;AACvB,QAAM,UAA6B,CAAC;AAEpC,WAAS,KAAK,SAA8B;AAC1C,UAAM,MAAM,QAAQ,SAAS,YAAY;AACzC,QAAI,OAAO,UAAU,IAAI,GAAG,EAAG;AAE/B,eAAW,QAAQ,oBAAoB;AACrC,YAAM,QAAQ,QAAQ,aAAa,IAAI;AACvC,UAAI,SAAS,MAAM,KAAK,GAAG;AACzB,gBAAQ,KAAK;AAAA,UACX,MAAM;AAAA,UACN;AAAA,UACA,UAAU;AAAA,UACV,MAAM;AAAA,QACR,CAAC;AAAA,MACH;AAAA,IACF;AAEA,UAAM,WAAW,QAAQ;AACzB,QAAI,IAAI;AAER,WAAO,IAAI,SAAS,QAAQ;AAC1B,YAAM,QAAQ,SAAS,CAAC;AAExB,UAAI,MAAM,aAAa,iCAAS,WAAW;AACzC,cAAM,YAAoB,CAAC;AAC3B,eACE,IAAI,SAAS,UACb,SAAS,CAAC,GAAG,aAAa,iCAAS,WACnC;AACA,oBAAU,KAAK,SAAS,CAAC,CAAE;AAC3B;AAAA,QACF;AACA,cAAM,SAAS,UAAU,IAAI,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE;AACtD,cAAM,UAAU,OAAO,KAAK;AAC5B,YAAI,WAAW,CAAC,QAAQ,WAAW,WAAW,GAAG;AAC/C,kBAAQ,KAAK;AAAA,YACX,MAAM;AAAA,YACN,OAAO;AAAA,YACP,MAAM;AAAA,UACR,CAAC;AAAA,QACH;AAAA,MACF,WAAW,MAAM,aAAa,iCAAS,cAAc;AACnD,aAAK,KAAsB;AAC3B;AAAA,MACF,OAAO;AACL;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,OAAK,IAAI;AAET,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,gBAA0B,CAAC;AACjC,aAAW,SAAS,SAAS;AAC3B,QAAI,CAAC,KAAK,IAAI,MAAM,IAAI,GAAG;AACzB,WAAK,IAAI,MAAM,IAAI;AACnB,oBAAc,KAAK,MAAM,IAAI;AAAA,IAC/B;AAAA,EACF;AAEA,SAAO,EAAE,MAAM,SAAS,cAAc;AACxC;;;ACzFO,SAAS,mBACd,MACA,SACA,gBACQ;AACR,aAAW,SAAS,SAAS;AAC3B,UAAM,aAAa,eAAe,IAAI,MAAM,IAAI;AAChD,QAAI,eAAe,OAAW;AAE9B,QAAI,MAAM,SAAS,aAAa;AAC9B,YAAM,QAAQ,aAAa,MAAM,UAAU,UAAU;AAAA,IACvD,OAAO;AACL,YAAM,YAAY,MAAM,MAAM,CAAC;AAC/B,UAAI,CAAC,UAAW;AAEhB,gBAAU,UAAU;AACpB,eAAS,IAAI,GAAG,IAAI,MAAM,MAAM,QAAQ,KAAK;AAC3C,cAAM,OAAO,MAAM,MAAM,CAAC;AAC1B,YAAI,KAAM,MAAK,UAAU;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AAEA,SAAO,KAAK,SAAS;AACvB;;;ACxBA,eAAsB,iBACpB,QACA,SACA,QACA,OAC8B;AAC9B,QAAM,WAAW,MAAM,OAAO,KAAK,YAAY,OAAO;AAAA,IACpD;AAAA,IACA,iBAAiB,EAAE,MAAM,cAAc;AAAA,IACvC,UAAU;AAAA,MACR;AAAA,QACE,MAAM;AAAA,QACN,SAAS;AAAA,UACP,wCAAwC,MAAM;AAAA,UAC9C;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF,EAAE,KAAK,IAAI;AAAA,MACb;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,SAAS,KAAK,UAAU,OAAO;AAAA,MACjC;AAAA,IACF;AAAA,EACF,CAAC;AAED,QAAM,UAAU,SAAS,QAAQ,CAAC,GAAG,SAAS;AAC9C,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,MAAM,+CAA+C;AAAA,EACjE;AAEA,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,OAAO;AAAA,EAC7B,QAAQ;AACN,UAAM,IAAI,MAAM,+CAA+C,OAAO,EAAE;AAAA,EAC1E;AAEA,QAAM,SAAS;AACf,MAAI,CAAC,OAAO,kBAAkB,CAAC,MAAM,QAAQ,OAAO,YAAY,GAAG;AACjE,UAAM,IAAI;AAAA,MACR,sDAAsD,OAAO;AAAA,IAC/D;AAAA,EACF;AAEA,MAAI,OAAO,aAAa,WAAW,QAAQ,QAAQ;AACjD,UAAM,IAAI;AAAA,MACR,oDACc,QAAQ,MAAM,SACnB,OAAO,aAAa,MAAM;AAAA,IACrC;AAAA,EACF;AAEA,SAAO;AACT;;;AC9DA,yBAA2B;AAEpB,SAAS,eACd,MACA,SACA,QACQ;AACR,aAAO,+BAAW,QAAQ,EACvB,OAAO,CAAC,MAAM,SAAS,MAAM,EAAE,KAAK,IAAI,CAAC,EACzC,OAAO,KAAK;AACjB;;;ACPA,SAAS,SAAS,OAAsB,MAAsB;AAC5D,SAAO,MAAM,SAAS,GAAG,MAAM,MAAM,GAAG,IAAI,KAAK;AACnD;AAEA,eAAsB,gBACpB,OACA,MACA,SACA,QACiC;AACjC,QAAM,MAAM,SAAS,OAAO,eAAe,MAAM,SAAS,MAAM,CAAC;AACjE,QAAM,SAAS,MAAM,MAAM,IAAI,GAAG;AAClC,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAO,KAAK,MAAM,MAAM;AAC1B;AAEA,eAAsB,gBACpB,OACA,MACA,SACA,QACA,QACe;AACf,QAAM,MAAM,SAAS,OAAO,eAAe,MAAM,SAAS,MAAM,CAAC;AACjE,QAAM,MAAM,IAAI,KAAK,KAAK,UAAU,MAAM,CAAC;AAC7C;;;AC5BA,IAAAA,2BAAgC;;;ACAzB,SAAS,WAAW,KAAqB;AAC9C,SAAO,IAAI,MAAM,GAAG,EAAE,CAAC,EAAG,YAAY;AACxC;AAEO,SAAS,MAAS,KAAU,MAAqB;AACtD,QAAM,SAAgB,CAAC;AACvB,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK,MAAM;AACzC,WAAO,KAAK,IAAI,MAAM,GAAG,IAAI,IAAI,CAAC;AAAA,EACpC;AACA,SAAO;AACT;;;ADNA,IAAM,cAAc,oBAAI,IAAI;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAEM,SAAS,YAAY,QAAyB;AACnD,SAAO,YAAY,IAAI,WAAW,MAAM,CAAC;AAC3C;AAEO,SAAS,aAAa,MAAsB;AACjD,QAAM,WAAO,gCAAM,IAAI;AACvB,QAAM,SAAS,KAAK,cAAc,MAAM;AAExC,MAAI,QAAQ;AACV,WAAO,aAAa,OAAO,KAAK;AAChC,WAAO,KAAK,SAAS;AAAA,EACvB;AAEA,aAAW,SAAS,KAAK,YAAY;AACnC,QAAI,MAAM,aAAa,kCAAS,cAAc;AAC5C,MAAC,MAAwB,aAAa,OAAO,KAAK;AAClD;AAAA,IACF;AAAA,EACF;AAEA,SAAO,KAAK,SAAS;AACvB;;;APvBA,IAAM,qBAAqB;AAEpB,SAAS,gBAAgB,QAAyB;AACvD,QAAM,SAAS,IAAI,cAAAC,QAAO;AAAA,IACxB,QAAQ,OAAO;AAAA,IACf,SAAS,OAAO;AAAA,IAChB,YAAY,OAAO,cAAc;AAAA,EACnC,CAAC;AAED,iBAAe,UACb,SAC0B;AAC1B,UAAM,EAAE,QAAQ,QAAQ,IAAI;AAE5B,UAAM,OAAO,QAAQ,QACjB,MAAM,iBAAiB,QAAQ,KAAK,IACpC,QAAQ;AAEZ,QAAI,OAAO,OAAO;AAChB,YAAM,SAAS,MAAM,gBAAgB,OAAO,OAAO,MAAM,SAAS,MAAM;AACxE,UAAI,QAAQ;AACV,eAAO,cAAc;AAAA,UACnB;AAAA,UACA,gBAAgB;AAAA,UAChB,SAAS,CAAC;AAAA,UACV,UAAU;AAAA,QACZ,CAAC;AACD,eAAO;AAAA,MACT;AAAA,IACF;AAEA,UAAM,EAAE,MAAM,SAAS,cAAc,IAAI,eAAe,IAAI;AAE5D,UAAM,aAAa,CAAC,SAAS,GAAG,aAAa;AAC7C,UAAM,QAAQ,OAAO,SAAS;AAC9B,UAAM,YAAY,OAAO,aAAa;AACtC,UAAM,UAAU,MAAM,YAAY,SAAS;AAE3C,UAAM,aAAa,QAAQ,CAAC;AAC5B,QAAI,CAAC,cAAc,WAAW,WAAW,GAAG;AAC1C,aAAO,EAAE,SAAS,KAAK;AAAA,IACzB;AAEA,UAAM,gBAAgB,MAAM;AAAA,MAC1B;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,QAAI,WAAW,cAAc,cAAc,MAAM,WAAW,MAAM,GAAG;AACnE,YAAMC,UAA0B,EAAE,SAAS,KAAK;AAChD,UAAI,OAAO,OAAO;AAChB,cAAM,gBAAgB,OAAO,OAAO,MAAM,SAAS,QAAQA,OAAM;AAAA,MACnE;AACA,aAAO,cAAc;AAAA,QACnB;AAAA,QACA,gBAAgB,cAAc;AAAA,QAC9B,SAAS;AAAA,QACT,UAAU;AAAA,MACZ,CAAC;AACD,aAAOA;AAAA,IACT;AAEA,UAAM,kBAAkB,CAAC,GAAG,cAAc,YAAY;AAEtD,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,YAAM,QAAQ,QAAQ,CAAC;AACvB,YAAM,WAAW,MAAM,iBAAiB,QAAQ,OAAO,QAAQ,KAAK;AACpE,sBAAgB,KAAK,GAAG,SAAS,YAAY;AAAA,IAC/C;AAEA,UAAM,oBAAoB,gBAAgB,CAAC;AAC3C,UAAM,iBAAiB,oBAAI,IAAoB;AAC/C,kBAAc,QAAQ,CAAC,UAAU,MAAM;AACrC,qBAAe,IAAI,UAAU,gBAAgB,IAAI,CAAC,CAAE;AAAA,IACtD,CAAC;AAED,QAAI,iBAAiB,mBAAmB,MAAM,SAAS,cAAc;AAErE,QAAI,YAAY,MAAM,GAAG;AACvB,uBAAiB,aAAa,cAAc;AAAA,IAC9C;AAEA,UAAM,SAA0B;AAAA,MAC9B,SAAS;AAAA,MACT,MAAM;AAAA,IACR;AAEA,QAAI,OAAO,OAAO;AAChB,YAAM,gBAAgB,OAAO,OAAO,MAAM,SAAS,QAAQ,MAAM;AAAA,IACnE;AAEA,WAAO,cAAc;AAAA,MACnB;AAAA,MACA,gBAAgB,cAAc;AAAA,MAC9B,SAAS;AAAA,MACT,UAAU;AAAA,IACZ,CAAC;AAED,WAAO;AAAA,EACT;AAEA,SAAO,EAAE,UAAU;AACrB;","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,15 +5,33 @@ interface CacheProvider {
5
5
  get: (key: string) => Promise<string | null>;
6
6
  set: (key: string, value: string) => Promise<void>;
7
7
  }
8
- interface I18nEmailConfig {
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;
12
26
  maxRetries?: number;
13
- batchSize?: number;
14
- cache?: CacheProvider;
15
- onTranslate?: (info: TranslateCallbackInfo) => void;
16
27
  }
28
+ interface AiSdkConfig extends SharedConfig {
29
+ model: AiLanguageModel;
30
+ openaiApiKey?: never;
31
+ baseURL?: never;
32
+ maxRetries?: never;
33
+ }
34
+ type I18nEmailConfig = OpenAIConfig | AiSdkConfig;
17
35
  interface TranslateCallbackInfo {
18
36
  locale: string;
19
37
  detectedLocale: string;
@@ -46,4 +64,4 @@ declare function createI18nEmail(config: I18nEmailConfig): {
46
64
  translate: (options: TranslateOptions) => Promise<TranslateResult>;
47
65
  };
48
66
 
49
- export { type CacheProvider, type I18nEmailConfig, type TranslateCallbackInfo, type TranslateOptions, type TranslateOptionsHtml, type TranslateOptionsReact, type TranslateResult, type TranslationResponse, 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,15 +5,33 @@ interface CacheProvider {
5
5
  get: (key: string) => Promise<string | null>;
6
6
  set: (key: string, value: string) => Promise<void>;
7
7
  }
8
- interface I18nEmailConfig {
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;
12
26
  maxRetries?: number;
13
- batchSize?: number;
14
- cache?: CacheProvider;
15
- onTranslate?: (info: TranslateCallbackInfo) => void;
16
27
  }
28
+ interface AiSdkConfig extends SharedConfig {
29
+ model: AiLanguageModel;
30
+ openaiApiKey?: never;
31
+ baseURL?: never;
32
+ maxRetries?: never;
33
+ }
34
+ type I18nEmailConfig = OpenAIConfig | AiSdkConfig;
17
35
  interface TranslateCallbackInfo {
18
36
  locale: string;
19
37
  detectedLocale: string;
@@ -46,4 +64,4 @@ declare function createI18nEmail(config: I18nEmailConfig): {
46
64
  translate: (options: TranslateOptions) => Promise<TranslateResult>;
47
65
  };
48
66
 
49
- export { type CacheProvider, type I18nEmailConfig, type TranslateCallbackInfo, type TranslateOptions, type TranslateOptionsHtml, type TranslateOptionsReact, type TranslateResult, type TranslationResponse, 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) {
@@ -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,49 @@ 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
+
146
196
  // src/hash.ts
147
197
  import { createHash } from "crypto";
148
198
  function createCacheKey(html, subject, locale) {
@@ -178,6 +228,27 @@ function chunk(arr, size) {
178
228
  }
179
229
  return chunks;
180
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
+ }
181
252
 
182
253
  // src/rtl.ts
183
254
  var RTL_LOCALES = /* @__PURE__ */ new Set([
@@ -213,11 +284,23 @@ function injectRtlDir(html) {
213
284
  // src/client.ts
214
285
  var DEFAULT_BATCH_SIZE = 50;
215
286
  function createI18nEmail(config) {
216
- const client = new OpenAI({
217
- apiKey: config.openaiApiKey,
218
- baseURL: config.baseURL,
219
- maxRetries: config.maxRetries ?? 2
220
- });
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
+ }
221
304
  async function translate(options) {
222
305
  const { locale, subject } = options;
223
306
  const html = options.react ? await renderReactEmail(options.react) : options.html;
@@ -235,19 +318,13 @@ function createI18nEmail(config) {
235
318
  }
236
319
  const { root, entries, uniqueStrings } = extractStrings(html);
237
320
  const allStrings = [subject, ...uniqueStrings];
238
- const model = config.model ?? "gpt-4o";
239
321
  const batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;
240
322
  const batches = chunk(allStrings, batchSize);
241
323
  const firstBatch = batches[0];
242
324
  if (!firstBatch || firstBatch.length === 0) {
243
325
  return { subject, html };
244
326
  }
245
- const firstResponse = await translateStrings(
246
- client,
247
- firstBatch,
248
- locale,
249
- model
250
- );
327
+ const firstResponse = await translateBatch(firstBatch, locale);
251
328
  if (baseLocale(firstResponse.detectedLocale) === baseLocale(locale)) {
252
329
  const result2 = { subject, html };
253
330
  if (config.cache) {
@@ -264,7 +341,7 @@ function createI18nEmail(config) {
264
341
  const allTranslations = [...firstResponse.translations];
265
342
  for (let i = 1; i < batches.length; i++) {
266
343
  const batch = batches[i];
267
- const response = await translateStrings(client, batch, locale, model);
344
+ const response = await translateBatch(batch, locale);
268
345
  allTranslations.push(...response.translations);
269
346
  }
270
347
  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/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.2.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",
@@ -46,15 +46,27 @@
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
+ "ai": ">=4",
53
+ "openai": ">=4",
53
54
  "react": ">=18"
54
55
  },
56
+ "peerDependenciesMeta": {
57
+ "ai": {
58
+ "optional": true
59
+ },
60
+ "openai": {
61
+ "optional": true
62
+ }
63
+ },
55
64
  "devDependencies": {
65
+ "@ai-sdk/openai": "^3",
56
66
  "@types/bun": "latest",
57
67
  "@types/react": "^19",
68
+ "ai": "^6",
69
+ "openai": "^4",
58
70
  "tsup": "^8",
59
71
  "typescript": "^5"
60
72
  }