i18n-email 0.3.0 → 0.5.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
@@ -13,6 +13,7 @@ Translate transactional emails into any language using AI models. Works with Rea
13
13
  - **`onTranslate` hook** for logging and analytics
14
14
  - Supports **any OpenAI-compatible API** via `baseURL`
15
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.)
16
17
 
17
18
  ## Install
18
19
 
@@ -30,6 +31,13 @@ To use the Vercel AI SDK instead of the OpenAI client directly:
30
31
  bun add ai @ai-sdk/openai
31
32
  ```
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
+
33
41
  ## Usage
34
42
 
35
43
  ### With a React Email component
@@ -87,6 +95,25 @@ const i18nEmail = createI18nEmail({
87
95
  });
88
96
  ```
89
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
+
90
117
  ### With caching (Upstash Redis example)
91
118
 
92
119
  ```ts
@@ -128,15 +155,28 @@ const i18nEmail = createI18nEmail({
128
155
 
129
156
  ### `createI18nEmail(config)`
130
157
 
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 |
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 |
140
180
 
141
181
  Returns `{ translate }`.
142
182
 
package/dist/index.cjs CHANGED
@@ -45,6 +45,9 @@ async function renderReactEmail(component) {
45
45
  );
46
46
  }
47
47
  }
48
+ function renderPlainText(html) {
49
+ return (0, import_render.toPlainText)(html);
50
+ }
48
51
 
49
52
  // src/extract.ts
50
53
  var import_node_html_parser = require("node-html-parser");
@@ -229,6 +232,48 @@ async function translateStringsWithAi(model, strings, locale) {
229
232
  return { detectedLocale, translations };
230
233
  }
231
234
 
235
+ // src/translate-tanstack.ts
236
+ var OUTPUT_SCHEMA = {
237
+ type: "object",
238
+ properties: {
239
+ detectedLocale: {
240
+ type: "string",
241
+ description: "The ISO locale code of the source language"
242
+ },
243
+ translations: {
244
+ type: "array",
245
+ items: { type: "string" },
246
+ description: "Translated strings in the exact same order as the input"
247
+ }
248
+ },
249
+ required: ["detectedLocale", "translations"],
250
+ additionalProperties: false
251
+ };
252
+ async function translateStringsWithTanstack(adapter, strings, locale) {
253
+ let chat;
254
+ try {
255
+ chat = (await import("@tanstack/ai")).chat;
256
+ } catch {
257
+ throw new Error(
258
+ 'i18n-email: The "@tanstack/ai" package is required when using a TanStack adapter. Install it with: npm install @tanstack/ai'
259
+ );
260
+ }
261
+ const result = await chat({
262
+ adapter,
263
+ systemPrompts: [buildSystemPrompt(locale)],
264
+ messages: [{ role: "user", content: buildUserPrompt(strings) }],
265
+ outputSchema: OUTPUT_SCHEMA,
266
+ stream: false
267
+ });
268
+ const { detectedLocale, translations } = result;
269
+ if (translations.length !== strings.length) {
270
+ throw new Error(
271
+ `i18n-email: Translation count mismatch. Expected ${strings.length}, got ${translations.length}`
272
+ );
273
+ }
274
+ return { detectedLocale, translations };
275
+ }
276
+
232
277
  // src/hash.ts
233
278
  var import_node_crypto = require("crypto");
234
279
  function createCacheKey(html, subject, locale) {
@@ -270,6 +315,12 @@ function isAiLanguageModel(value) {
270
315
  function isAiSdkConfig(config) {
271
316
  return isAiLanguageModel(config.model);
272
317
  }
318
+ function isTanstackAiAdapter(value) {
319
+ return typeof value === "object" && value !== null && value["kind"] === "text" && typeof value["name"] === "string";
320
+ }
321
+ function isTanstackAiAdapterConfig(config) {
322
+ return isTanstackAiAdapter(config.adapter);
323
+ }
273
324
  async function createOpenAIClient(config) {
274
325
  let OpenAI;
275
326
  try {
@@ -321,6 +372,7 @@ function injectRtlDir(html) {
321
372
  var DEFAULT_BATCH_SIZE = 50;
322
373
  function createI18nEmail(config) {
323
374
  const aiSdk = isAiSdkConfig(config);
375
+ const tanstackAi = isTanstackAiAdapterConfig(config);
324
376
  let clientPromise;
325
377
  function getClient() {
326
378
  if (!clientPromise) {
@@ -331,6 +383,9 @@ function createI18nEmail(config) {
331
383
  return clientPromise;
332
384
  }
333
385
  async function translateBatch(strings, locale) {
386
+ if (tanstackAi) {
387
+ return translateStringsWithTanstack(config.adapter, strings, locale);
388
+ }
334
389
  if (aiSdk) {
335
390
  return translateStringsWithAi(config.model, strings, locale);
336
391
  }
@@ -339,6 +394,9 @@ function createI18nEmail(config) {
339
394
  }
340
395
  async function translate(options) {
341
396
  const { locale, subject } = options;
397
+ if (options.text != void 0 && options.react == void 0 && options.html == void 0) {
398
+ return translateTextOnly(options);
399
+ }
342
400
  const html = options.react ? await renderReactEmail(options.react) : options.html;
343
401
  if (config.cache) {
344
402
  const cached = await getCachedResult(config.cache, html, subject, locale);
@@ -358,11 +416,15 @@ function createI18nEmail(config) {
358
416
  const batches = chunk(allStrings, batchSize);
359
417
  const firstBatch = batches[0];
360
418
  if (!firstBatch || firstBatch.length === 0) {
361
- return { subject, html };
419
+ return { subject, html, text: renderPlainText(html) };
362
420
  }
363
421
  const firstResponse = await translateBatch(firstBatch, locale);
364
422
  if (baseLocale(firstResponse.detectedLocale) === baseLocale(locale)) {
365
- const result2 = { subject, html };
423
+ const result2 = {
424
+ subject,
425
+ html,
426
+ text: renderPlainText(html)
427
+ };
366
428
  if (config.cache) {
367
429
  await setCachedResult(config.cache, html, subject, locale, result2);
368
430
  }
@@ -391,7 +453,8 @@ function createI18nEmail(config) {
391
453
  }
392
454
  const result = {
393
455
  subject: translatedSubject,
394
- html: translatedHtml
456
+ html: translatedHtml,
457
+ text: renderPlainText(translatedHtml)
395
458
  };
396
459
  if (config.cache) {
397
460
  await setCachedResult(config.cache, html, subject, locale, result);
@@ -404,6 +467,51 @@ function createI18nEmail(config) {
404
467
  });
405
468
  return result;
406
469
  }
470
+ async function translateTextOnly(options) {
471
+ const { locale, subject, text } = options;
472
+ if (config.cache) {
473
+ const cached = await getCachedResult(config.cache, text, subject, locale);
474
+ if (cached) {
475
+ config.onTranslate?.({
476
+ locale,
477
+ detectedLocale: locale,
478
+ strings: [],
479
+ cacheHit: true
480
+ });
481
+ return cached;
482
+ }
483
+ }
484
+ const allStrings = [subject, text];
485
+ const firstResponse = await translateBatch(allStrings, locale);
486
+ if (baseLocale(firstResponse.detectedLocale) === baseLocale(locale)) {
487
+ const result2 = { subject, html: void 0, text };
488
+ if (config.cache) {
489
+ await setCachedResult(config.cache, text, subject, locale, result2);
490
+ }
491
+ config.onTranslate?.({
492
+ locale,
493
+ detectedLocale: firstResponse.detectedLocale,
494
+ strings: allStrings,
495
+ cacheHit: false
496
+ });
497
+ return result2;
498
+ }
499
+ const result = {
500
+ subject: firstResponse.translations[0],
501
+ html: void 0,
502
+ text: firstResponse.translations[1]
503
+ };
504
+ if (config.cache) {
505
+ await setCachedResult(config.cache, text, subject, locale, result);
506
+ }
507
+ config.onTranslate?.({
508
+ locale,
509
+ detectedLocale: firstResponse.detectedLocale,
510
+ strings: allStrings,
511
+ cacheHit: false
512
+ });
513
+ return result;
514
+ }
407
515
  return { translate };
408
516
  }
409
517
  // Annotate the CommonJS export names for ESM import in node:
@@ -1 +1 @@
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"]}
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, toPlainText } 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\nexport function renderPlainText(html: string): string {\n return toPlainText(html);\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, renderPlainText } 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 if (\n options.text != undefined &&\n options.react == undefined &&\n options.html == undefined\n ) {\n return translateTextOnly(options);\n }\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, text: renderPlainText(html) };\n }\n\n const firstResponse = await translateBatch(firstBatch, locale);\n\n if (baseLocale(firstResponse.detectedLocale) === baseLocale(locale)) {\n const result: TranslateResult = {\n subject,\n html,\n text: renderPlainText(html),\n };\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 text: renderPlainText(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 async function translateTextOnly(options: {\n locale: string;\n subject: string;\n text: string;\n }): Promise<TranslateResult> {\n const { locale, subject, text } = options;\n\n if (config.cache) {\n const cached = await getCachedResult(config.cache, text, 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 allStrings = [subject, text];\n const firstResponse = await translateBatch(allStrings, locale);\n\n if (baseLocale(firstResponse.detectedLocale) === baseLocale(locale)) {\n const result: TranslateResult = { subject, html: undefined, text };\n if (config.cache) {\n await setCachedResult(config.cache, text, 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 result: TranslateResult = {\n subject: firstResponse.translations[0]!,\n html: undefined,\n text: firstResponse.translations[1]!,\n };\n\n if (config.cache) {\n await setCachedResult(config.cache, text, 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,4 +1,5 @@
1
1
  import { ReactElement } from 'react';
2
+ import { AnyTextAdapter } from '@tanstack/ai';
2
3
 
3
4
  interface CacheProvider {
4
5
  prefix?: string;
@@ -14,6 +15,7 @@ interface AiLanguageModel {
14
15
  readonly modelId: string;
15
16
  readonly provider: string;
16
17
  }
18
+ type TanstackAiTextAdapter = AnyTextAdapter;
17
19
  interface SharedConfig {
18
20
  batchSize?: number;
19
21
  cache?: CacheProvider;
@@ -24,14 +26,17 @@ interface OpenAIConfig extends SharedConfig {
24
26
  model?: string;
25
27
  baseURL?: string;
26
28
  maxRetries?: number;
29
+ adapter?: never;
27
30
  }
28
31
  interface AiSdkConfig extends SharedConfig {
29
32
  model: AiLanguageModel;
30
- openaiApiKey?: never;
31
- baseURL?: never;
32
- maxRetries?: never;
33
+ adapter?: never;
33
34
  }
34
- type I18nEmailConfig = OpenAIConfig | AiSdkConfig;
35
+ interface TanstackAiAdapterConfig extends SharedConfig {
36
+ adapter: TanstackAiTextAdapter;
37
+ model?: never;
38
+ }
39
+ type I18nEmailConfig = OpenAIConfig | AiSdkConfig | TanstackAiAdapterConfig;
35
40
  interface TranslateCallbackInfo {
36
41
  locale: string;
37
42
  detectedLocale: string;
@@ -43,17 +48,27 @@ interface TranslateOptionsReact {
43
48
  subject: string;
44
49
  react: ReactElement;
45
50
  html?: never;
51
+ text?: never;
46
52
  }
47
53
  interface TranslateOptionsHtml {
48
54
  locale: string;
49
55
  subject: string;
50
56
  html: string;
51
57
  react?: never;
58
+ text?: never;
52
59
  }
53
- type TranslateOptions = TranslateOptionsReact | TranslateOptionsHtml;
60
+ interface TranslateOptionsText {
61
+ locale: string;
62
+ subject: string;
63
+ text: string;
64
+ react?: never;
65
+ html?: never;
66
+ }
67
+ type TranslateOptions = TranslateOptionsReact | TranslateOptionsHtml | TranslateOptionsText;
54
68
  interface TranslateResult {
55
69
  subject: string;
56
- html: string;
70
+ html: string | undefined;
71
+ text: string;
57
72
  }
58
73
  interface TranslationResponse {
59
74
  detectedLocale: string;
@@ -64,4 +79,4 @@ declare function createI18nEmail(config: I18nEmailConfig): {
64
79
  translate: (options: TranslateOptions) => Promise<TranslateResult>;
65
80
  };
66
81
 
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 };
82
+ 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,4 +1,5 @@
1
1
  import { ReactElement } from 'react';
2
+ import { AnyTextAdapter } from '@tanstack/ai';
2
3
 
3
4
  interface CacheProvider {
4
5
  prefix?: string;
@@ -14,6 +15,7 @@ interface AiLanguageModel {
14
15
  readonly modelId: string;
15
16
  readonly provider: string;
16
17
  }
18
+ type TanstackAiTextAdapter = AnyTextAdapter;
17
19
  interface SharedConfig {
18
20
  batchSize?: number;
19
21
  cache?: CacheProvider;
@@ -24,14 +26,17 @@ interface OpenAIConfig extends SharedConfig {
24
26
  model?: string;
25
27
  baseURL?: string;
26
28
  maxRetries?: number;
29
+ adapter?: never;
27
30
  }
28
31
  interface AiSdkConfig extends SharedConfig {
29
32
  model: AiLanguageModel;
30
- openaiApiKey?: never;
31
- baseURL?: never;
32
- maxRetries?: never;
33
+ adapter?: never;
33
34
  }
34
- type I18nEmailConfig = OpenAIConfig | AiSdkConfig;
35
+ interface TanstackAiAdapterConfig extends SharedConfig {
36
+ adapter: TanstackAiTextAdapter;
37
+ model?: never;
38
+ }
39
+ type I18nEmailConfig = OpenAIConfig | AiSdkConfig | TanstackAiAdapterConfig;
35
40
  interface TranslateCallbackInfo {
36
41
  locale: string;
37
42
  detectedLocale: string;
@@ -43,17 +48,27 @@ interface TranslateOptionsReact {
43
48
  subject: string;
44
49
  react: ReactElement;
45
50
  html?: never;
51
+ text?: never;
46
52
  }
47
53
  interface TranslateOptionsHtml {
48
54
  locale: string;
49
55
  subject: string;
50
56
  html: string;
51
57
  react?: never;
58
+ text?: never;
52
59
  }
53
- type TranslateOptions = TranslateOptionsReact | TranslateOptionsHtml;
60
+ interface TranslateOptionsText {
61
+ locale: string;
62
+ subject: string;
63
+ text: string;
64
+ react?: never;
65
+ html?: never;
66
+ }
67
+ type TranslateOptions = TranslateOptionsReact | TranslateOptionsHtml | TranslateOptionsText;
54
68
  interface TranslateResult {
55
69
  subject: string;
56
- html: string;
70
+ html: string | undefined;
71
+ text: string;
57
72
  }
58
73
  interface TranslationResponse {
59
74
  detectedLocale: string;
@@ -64,4 +79,4 @@ declare function createI18nEmail(config: I18nEmailConfig): {
64
79
  translate: (options: TranslateOptions) => Promise<TranslateResult>;
65
80
  };
66
81
 
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 };
82
+ 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,5 +1,5 @@
1
1
  // src/render.ts
2
- import { render } from "@react-email/render";
2
+ import { render, toPlainText } from "@react-email/render";
3
3
  async function renderReactEmail(component) {
4
4
  try {
5
5
  return await render(component);
@@ -9,6 +9,9 @@ async function renderReactEmail(component) {
9
9
  );
10
10
  }
11
11
  }
12
+ function renderPlainText(html) {
13
+ return toPlainText(html);
14
+ }
12
15
 
13
16
  // src/extract.ts
14
17
  import { parse, NodeType } from "node-html-parser";
@@ -193,6 +196,48 @@ async function translateStringsWithAi(model, strings, locale) {
193
196
  return { detectedLocale, translations };
194
197
  }
195
198
 
199
+ // src/translate-tanstack.ts
200
+ var OUTPUT_SCHEMA = {
201
+ type: "object",
202
+ properties: {
203
+ detectedLocale: {
204
+ type: "string",
205
+ description: "The ISO locale code of the source language"
206
+ },
207
+ translations: {
208
+ type: "array",
209
+ items: { type: "string" },
210
+ description: "Translated strings in the exact same order as the input"
211
+ }
212
+ },
213
+ required: ["detectedLocale", "translations"],
214
+ additionalProperties: false
215
+ };
216
+ async function translateStringsWithTanstack(adapter, strings, locale) {
217
+ let chat;
218
+ try {
219
+ chat = (await import("@tanstack/ai")).chat;
220
+ } catch {
221
+ throw new Error(
222
+ 'i18n-email: The "@tanstack/ai" package is required when using a TanStack adapter. Install it with: npm install @tanstack/ai'
223
+ );
224
+ }
225
+ const result = await chat({
226
+ adapter,
227
+ systemPrompts: [buildSystemPrompt(locale)],
228
+ messages: [{ role: "user", content: buildUserPrompt(strings) }],
229
+ outputSchema: OUTPUT_SCHEMA,
230
+ stream: false
231
+ });
232
+ const { detectedLocale, translations } = result;
233
+ if (translations.length !== strings.length) {
234
+ throw new Error(
235
+ `i18n-email: Translation count mismatch. Expected ${strings.length}, got ${translations.length}`
236
+ );
237
+ }
238
+ return { detectedLocale, translations };
239
+ }
240
+
196
241
  // src/hash.ts
197
242
  import { createHash } from "crypto";
198
243
  function createCacheKey(html, subject, locale) {
@@ -234,6 +279,12 @@ function isAiLanguageModel(value) {
234
279
  function isAiSdkConfig(config) {
235
280
  return isAiLanguageModel(config.model);
236
281
  }
282
+ function isTanstackAiAdapter(value) {
283
+ return typeof value === "object" && value !== null && value["kind"] === "text" && typeof value["name"] === "string";
284
+ }
285
+ function isTanstackAiAdapterConfig(config) {
286
+ return isTanstackAiAdapter(config.adapter);
287
+ }
237
288
  async function createOpenAIClient(config) {
238
289
  let OpenAI;
239
290
  try {
@@ -285,6 +336,7 @@ function injectRtlDir(html) {
285
336
  var DEFAULT_BATCH_SIZE = 50;
286
337
  function createI18nEmail(config) {
287
338
  const aiSdk = isAiSdkConfig(config);
339
+ const tanstackAi = isTanstackAiAdapterConfig(config);
288
340
  let clientPromise;
289
341
  function getClient() {
290
342
  if (!clientPromise) {
@@ -295,6 +347,9 @@ function createI18nEmail(config) {
295
347
  return clientPromise;
296
348
  }
297
349
  async function translateBatch(strings, locale) {
350
+ if (tanstackAi) {
351
+ return translateStringsWithTanstack(config.adapter, strings, locale);
352
+ }
298
353
  if (aiSdk) {
299
354
  return translateStringsWithAi(config.model, strings, locale);
300
355
  }
@@ -303,6 +358,9 @@ function createI18nEmail(config) {
303
358
  }
304
359
  async function translate(options) {
305
360
  const { locale, subject } = options;
361
+ if (options.text != void 0 && options.react == void 0 && options.html == void 0) {
362
+ return translateTextOnly(options);
363
+ }
306
364
  const html = options.react ? await renderReactEmail(options.react) : options.html;
307
365
  if (config.cache) {
308
366
  const cached = await getCachedResult(config.cache, html, subject, locale);
@@ -322,11 +380,15 @@ function createI18nEmail(config) {
322
380
  const batches = chunk(allStrings, batchSize);
323
381
  const firstBatch = batches[0];
324
382
  if (!firstBatch || firstBatch.length === 0) {
325
- return { subject, html };
383
+ return { subject, html, text: renderPlainText(html) };
326
384
  }
327
385
  const firstResponse = await translateBatch(firstBatch, locale);
328
386
  if (baseLocale(firstResponse.detectedLocale) === baseLocale(locale)) {
329
- const result2 = { subject, html };
387
+ const result2 = {
388
+ subject,
389
+ html,
390
+ text: renderPlainText(html)
391
+ };
330
392
  if (config.cache) {
331
393
  await setCachedResult(config.cache, html, subject, locale, result2);
332
394
  }
@@ -355,7 +417,8 @@ function createI18nEmail(config) {
355
417
  }
356
418
  const result = {
357
419
  subject: translatedSubject,
358
- html: translatedHtml
420
+ html: translatedHtml,
421
+ text: renderPlainText(translatedHtml)
359
422
  };
360
423
  if (config.cache) {
361
424
  await setCachedResult(config.cache, html, subject, locale, result);
@@ -368,6 +431,51 @@ function createI18nEmail(config) {
368
431
  });
369
432
  return result;
370
433
  }
434
+ async function translateTextOnly(options) {
435
+ const { locale, subject, text } = options;
436
+ if (config.cache) {
437
+ const cached = await getCachedResult(config.cache, text, subject, locale);
438
+ if (cached) {
439
+ config.onTranslate?.({
440
+ locale,
441
+ detectedLocale: locale,
442
+ strings: [],
443
+ cacheHit: true
444
+ });
445
+ return cached;
446
+ }
447
+ }
448
+ const allStrings = [subject, text];
449
+ const firstResponse = await translateBatch(allStrings, locale);
450
+ if (baseLocale(firstResponse.detectedLocale) === baseLocale(locale)) {
451
+ const result2 = { subject, html: void 0, text };
452
+ if (config.cache) {
453
+ await setCachedResult(config.cache, text, subject, locale, result2);
454
+ }
455
+ config.onTranslate?.({
456
+ locale,
457
+ detectedLocale: firstResponse.detectedLocale,
458
+ strings: allStrings,
459
+ cacheHit: false
460
+ });
461
+ return result2;
462
+ }
463
+ const result = {
464
+ subject: firstResponse.translations[0],
465
+ html: void 0,
466
+ text: firstResponse.translations[1]
467
+ };
468
+ if (config.cache) {
469
+ await setCachedResult(config.cache, text, subject, locale, result);
470
+ }
471
+ config.onTranslate?.({
472
+ locale,
473
+ detectedLocale: firstResponse.detectedLocale,
474
+ strings: allStrings,
475
+ cacheHit: false
476
+ });
477
+ return result;
478
+ }
371
479
  return { translate };
372
480
  }
373
481
  export {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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"]}
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, toPlainText } 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\nexport function renderPlainText(html: string): string {\n return toPlainText(html);\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, renderPlainText } 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 if (\n options.text != undefined &&\n options.react == undefined &&\n options.html == undefined\n ) {\n return translateTextOnly(options);\n }\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, text: renderPlainText(html) };\n }\n\n const firstResponse = await translateBatch(firstBatch, locale);\n\n if (baseLocale(firstResponse.detectedLocale) === baseLocale(locale)) {\n const result: TranslateResult = {\n subject,\n html,\n text: renderPlainText(html),\n };\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 text: renderPlainText(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 async function translateTextOnly(options: {\n locale: string;\n subject: string;\n text: string;\n }): Promise<TranslateResult> {\n const { locale, subject, text } = options;\n\n if (config.cache) {\n const cached = await getCachedResult(config.cache, text, 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 allStrings = [subject, text];\n const firstResponse = await translateBatch(allStrings, locale);\n\n if (baseLocale(firstResponse.detectedLocale) === baseLocale(locale)) {\n const result: TranslateResult = { subject, html: undefined, text };\n if (config.cache) {\n await setCachedResult(config.cache, text, 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 result: TranslateResult = {\n subject: firstResponse.translations[0]!,\n html: undefined,\n text: firstResponse.translations[1]!,\n };\n\n if (config.cache) {\n await setCachedResult(config.cache, text, subject, locale, result);\n }\n\n config.onTranslate?.({\n locale,\n detectedLocale: firstResponse.detectedLocale,\n strings: allStrings,\n cacheHit: false,\n });\n\n return result;\n }\n\n return { translate };\n}\n"],"mappings":"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","names":["parse","NodeType","parse","NodeType","result"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18n-email",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Translate transactional emails into any language using AI models.",
6
6
  "author": "Dan Zabrotski",
@@ -49,11 +49,15 @@
49
49
  "node-html-parser": "^6"
50
50
  },
51
51
  "peerDependencies": {
52
+ "@tanstack/ai": ">=0.8",
52
53
  "ai": ">=4",
53
54
  "openai": ">=4",
54
55
  "react": ">=18"
55
56
  },
56
57
  "peerDependenciesMeta": {
58
+ "@tanstack/ai": {
59
+ "optional": true
60
+ },
57
61
  "ai": {
58
62
  "optional": true
59
63
  },
@@ -63,6 +67,7 @@
63
67
  },
64
68
  "devDependencies": {
65
69
  "@ai-sdk/openai": "^3",
70
+ "@tanstack/ai": "^0.8.1",
66
71
  "@types/bun": "latest",
67
72
  "@types/react": "^19",
68
73
  "ai": "^6",