i18n-email 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -9
- package/dist/index.cjs +52 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -5
- package/dist/index.d.ts +10 -5
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -1
- package/package.json +6 -1
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
|
136
|
-
|
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
139
|
-
| `
|
|
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
|
@@ -229,6 +229,48 @@ async function translateStringsWithAi(model, strings, locale) {
|
|
|
229
229
|
return { detectedLocale, translations };
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
+
// src/translate-tanstack.ts
|
|
233
|
+
var OUTPUT_SCHEMA = {
|
|
234
|
+
type: "object",
|
|
235
|
+
properties: {
|
|
236
|
+
detectedLocale: {
|
|
237
|
+
type: "string",
|
|
238
|
+
description: "The ISO locale code of the source language"
|
|
239
|
+
},
|
|
240
|
+
translations: {
|
|
241
|
+
type: "array",
|
|
242
|
+
items: { type: "string" },
|
|
243
|
+
description: "Translated strings in the exact same order as the input"
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
required: ["detectedLocale", "translations"],
|
|
247
|
+
additionalProperties: false
|
|
248
|
+
};
|
|
249
|
+
async function translateStringsWithTanstack(adapter, strings, locale) {
|
|
250
|
+
let chat;
|
|
251
|
+
try {
|
|
252
|
+
chat = (await import("@tanstack/ai")).chat;
|
|
253
|
+
} catch {
|
|
254
|
+
throw new Error(
|
|
255
|
+
'i18n-email: The "@tanstack/ai" package is required when using a TanStack adapter. Install it with: npm install @tanstack/ai'
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
const result = await chat({
|
|
259
|
+
adapter,
|
|
260
|
+
systemPrompts: [buildSystemPrompt(locale)],
|
|
261
|
+
messages: [{ role: "user", content: buildUserPrompt(strings) }],
|
|
262
|
+
outputSchema: OUTPUT_SCHEMA,
|
|
263
|
+
stream: false
|
|
264
|
+
});
|
|
265
|
+
const { detectedLocale, translations } = result;
|
|
266
|
+
if (translations.length !== strings.length) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`i18n-email: Translation count mismatch. Expected ${strings.length}, got ${translations.length}`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
return { detectedLocale, translations };
|
|
272
|
+
}
|
|
273
|
+
|
|
232
274
|
// src/hash.ts
|
|
233
275
|
var import_node_crypto = require("crypto");
|
|
234
276
|
function createCacheKey(html, subject, locale) {
|
|
@@ -270,6 +312,12 @@ function isAiLanguageModel(value) {
|
|
|
270
312
|
function isAiSdkConfig(config) {
|
|
271
313
|
return isAiLanguageModel(config.model);
|
|
272
314
|
}
|
|
315
|
+
function isTanstackAiAdapter(value) {
|
|
316
|
+
return typeof value === "object" && value !== null && value["kind"] === "text" && typeof value["name"] === "string";
|
|
317
|
+
}
|
|
318
|
+
function isTanstackAiAdapterConfig(config) {
|
|
319
|
+
return isTanstackAiAdapter(config.adapter);
|
|
320
|
+
}
|
|
273
321
|
async function createOpenAIClient(config) {
|
|
274
322
|
let OpenAI;
|
|
275
323
|
try {
|
|
@@ -321,6 +369,7 @@ function injectRtlDir(html) {
|
|
|
321
369
|
var DEFAULT_BATCH_SIZE = 50;
|
|
322
370
|
function createI18nEmail(config) {
|
|
323
371
|
const aiSdk = isAiSdkConfig(config);
|
|
372
|
+
const tanstackAi = isTanstackAiAdapterConfig(config);
|
|
324
373
|
let clientPromise;
|
|
325
374
|
function getClient() {
|
|
326
375
|
if (!clientPromise) {
|
|
@@ -331,6 +380,9 @@ function createI18nEmail(config) {
|
|
|
331
380
|
return clientPromise;
|
|
332
381
|
}
|
|
333
382
|
async function translateBatch(strings, locale) {
|
|
383
|
+
if (tanstackAi) {
|
|
384
|
+
return translateStringsWithTanstack(config.adapter, strings, locale);
|
|
385
|
+
}
|
|
334
386
|
if (aiSdk) {
|
|
335
387
|
return translateStringsWithAi(config.model, strings, locale);
|
|
336
388
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -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 } from \"@react-email/render\";\nimport type { ReactElement } from \"react\";\n\nexport async function renderReactEmail(\n component: ReactElement,\n): Promise<string> {\n try {\n return await render(component);\n } catch (error) {\n throw new Error(\n `i18n-email: Failed to render React component: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n}\n","import { parse, NodeType } from \"node-html-parser\";\nimport type { HTMLElement as ParsedElement, Node } from \"node-html-parser\";\n\nconst TRANSLATABLE_ATTRS = [\"alt\", \"title\"];\nconst SKIP_TAGS = new Set([\"style\", \"script\", \"head\"]);\n\nexport interface TextEntry {\n type: \"text\";\n nodes: Node[];\n text: string;\n}\n\nexport interface AttributeEntry {\n type: \"attribute\";\n element: ParsedElement;\n attrName: string;\n text: string;\n}\n\nexport type ExtractionEntry = TextEntry | AttributeEntry;\n\nexport interface ExtractionResult {\n root: ParsedElement;\n entries: ExtractionEntry[];\n uniqueStrings: string[];\n}\n\nexport function extractStrings(html: string): ExtractionResult {\n const root = parse(html);\n const entries: ExtractionEntry[] = [];\n\n function walk(element: ParsedElement): void {\n const tag = element.tagName?.toLowerCase();\n if (tag && SKIP_TAGS.has(tag)) return;\n\n for (const attr of TRANSLATABLE_ATTRS) {\n const value = element.getAttribute(attr);\n if (value && value.trim()) {\n entries.push({\n type: \"attribute\",\n element,\n attrName: attr,\n text: value,\n });\n }\n }\n\n const children = element.childNodes;\n let i = 0;\n\n while (i < children.length) {\n const child = children[i]!;\n\n if (child.nodeType === NodeType.TEXT_NODE) {\n const textNodes: Node[] = [];\n while (\n i < children.length &&\n children[i]?.nodeType === NodeType.TEXT_NODE\n ) {\n textNodes.push(children[i]!);\n i++;\n }\n const merged = textNodes.map((n) => n.rawText).join(\"\");\n const trimmed = merged.trim();\n if (trimmed && !trimmed.startsWith(\"<!DOCTYPE\")) {\n entries.push({\n type: \"text\",\n nodes: textNodes,\n text: merged,\n });\n }\n } else if (child.nodeType === NodeType.ELEMENT_NODE) {\n walk(child as ParsedElement);\n i++;\n } else {\n i++;\n }\n }\n }\n\n walk(root);\n\n const seen = new Set<string>();\n const uniqueStrings: string[] = [];\n for (const entry of entries) {\n if (!seen.has(entry.text)) {\n seen.add(entry.text);\n uniqueStrings.push(entry.text);\n }\n }\n\n return { root, entries, uniqueStrings };\n}\n","import type { ExtractionEntry } from \"./extract\";\nimport type { HTMLElement as ParsedElement } from \"node-html-parser\";\n\nexport function injectTranslations(\n root: ParsedElement,\n entries: ExtractionEntry[],\n translationMap: Map<string, string>,\n): string {\n for (const entry of entries) {\n const translated = translationMap.get(entry.text);\n if (translated === undefined) continue;\n\n if (entry.type === \"attribute\") {\n entry.element.setAttribute(entry.attrName, translated);\n } else {\n const firstNode = entry.nodes[0];\n if (!firstNode) continue;\n\n firstNode.rawText = translated;\n for (let i = 1; i < entry.nodes.length; i++) {\n const node = entry.nodes[i];\n if (node) node.rawText = \"\";\n }\n }\n }\n\n return root.toString();\n}\n","export function buildSystemPrompt(locale: string): string {\n return [\n `You are translating email content to ${locale}.`,\n \"Rules:\",\n \"- Preserve dynamic values like names, URLs, amounts, dates, and codes exactly as they appear\",\n \"- Do not translate brand names or product names\",\n \"- Preserve tone: professional but friendly\",\n ].join(\"\\n\");\n}\n\nexport function buildUserPrompt(strings: string[]): string {\n return JSON.stringify(strings);\n}\n","import type OpenAI from \"openai\";\nimport type { TranslationResponse } from \"./types\";\nimport { buildSystemPrompt, buildUserPrompt } from \"./prompt\";\n\nexport async function translateStrings(\n client: OpenAI,\n strings: string[],\n locale: string,\n model: string,\n): Promise<TranslationResponse> {\n const response = await client.chat.completions.create({\n model,\n response_format: { type: \"json_object\" },\n messages: [\n {\n role: \"system\",\n content: [\n buildSystemPrompt(locale),\n \"- Return a JSON object with two fields:\",\n ' 1. \"detectedLocale\": the ISO locale code of the source language',\n ' 2. \"translations\": a JSON array of translated strings in the exact same order as the input',\n \"- Return only the JSON object, no explanation\",\n ].join(\"\\n\"),\n },\n {\n role: \"user\",\n content: buildUserPrompt(strings),\n },\n ],\n });\n\n const content = response.choices[0]?.message?.content;\n if (!content) {\n throw new Error(\"i18n-email: OpenAI returned an empty response\");\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch {\n throw new Error(`i18n-email: OpenAI returned malformed JSON: ${content}`);\n }\n\n const result = parsed as TranslationResponse;\n if (!result.detectedLocale || !Array.isArray(result.translations)) {\n throw new Error(\n `i18n-email: Unexpected response shape from OpenAI: ${content}`,\n );\n }\n\n if (result.translations.length !== strings.length) {\n throw new Error(\n `i18n-email: Translation count mismatch. ` +\n `Expected ${strings.length}, ` +\n `got ${result.translations.length}`,\n );\n }\n\n return result;\n}\n","import type { AiLanguageModel, TranslationResponse } from \"./types\";\nimport { buildSystemPrompt, buildUserPrompt } from \"./prompt\";\n\nexport async function translateStringsWithAi(\n model: AiLanguageModel,\n strings: string[],\n locale: string,\n): Promise<TranslationResponse> {\n let generateObject: typeof import(\"ai\").generateObject;\n let jsonSchema: typeof import(\"ai\").jsonSchema;\n\n try {\n const ai = await import(\"ai\");\n generateObject = ai.generateObject;\n jsonSchema = ai.jsonSchema;\n } catch {\n throw new Error(\n 'i18n-email: The \"ai\" package is required when using an AI SDK model. ' +\n \"Install it with: npm install ai\",\n );\n }\n\n const result = await generateObject({\n model: model as Parameters<typeof generateObject>[0][\"model\"],\n schema: jsonSchema<TranslationResponse>({\n type: \"object\",\n properties: {\n detectedLocale: {\n type: \"string\",\n description: \"The ISO locale code of the source language\",\n },\n translations: {\n type: \"array\",\n items: { type: \"string\" },\n description:\n \"Translated strings in the exact same order as the input\",\n },\n },\n required: [\"detectedLocale\", \"translations\"],\n additionalProperties: false,\n }),\n system: buildSystemPrompt(locale),\n prompt: buildUserPrompt(strings),\n });\n\n const { detectedLocale, translations } = result.object;\n\n if (translations.length !== strings.length) {\n throw new Error(\n `i18n-email: Translation count mismatch. ` +\n `Expected ${strings.length}, ` +\n `got ${translations.length}`,\n );\n }\n\n return { detectedLocale, translations };\n}\n","import type { TanstackAiTextAdapter, TranslationResponse } from \"./types\";\nimport { buildSystemPrompt, buildUserPrompt } from \"./prompt\";\n\nconst OUTPUT_SCHEMA = {\n type: \"object\",\n properties: {\n detectedLocale: {\n type: \"string\",\n description: \"The ISO locale code of the source language\",\n },\n translations: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Translated strings in the exact same order as the input\",\n },\n },\n required: [\"detectedLocale\", \"translations\"],\n additionalProperties: false,\n};\n\nexport async function translateStringsWithTanstack(\n adapter: TanstackAiTextAdapter,\n strings: string[],\n locale: string,\n): Promise<TranslationResponse> {\n let chat: typeof import(\"@tanstack/ai\").chat;\n try {\n chat = (await import(\"@tanstack/ai\")).chat;\n } catch {\n throw new Error(\n 'i18n-email: The \"@tanstack/ai\" package is required when using a TanStack adapter. ' +\n \"Install it with: npm install @tanstack/ai\",\n );\n }\n\n const result = await chat({\n adapter,\n systemPrompts: [buildSystemPrompt(locale)],\n messages: [{ role: \"user\", content: buildUserPrompt(strings) }],\n outputSchema: OUTPUT_SCHEMA,\n stream: false,\n });\n\n const { detectedLocale, translations } = result as TranslationResponse;\n\n if (translations.length !== strings.length) {\n throw new Error(\n `i18n-email: Translation count mismatch. ` +\n `Expected ${strings.length}, ` +\n `got ${translations.length}`,\n );\n }\n\n return { detectedLocale, translations };\n}\n","import { createHash } from \"node:crypto\";\n\nexport function createCacheKey(\n html: string,\n subject: string,\n locale: string,\n): string {\n return createHash(\"sha256\")\n .update([html, subject, locale].join(\"\\0\"))\n .digest(\"hex\");\n}\n","import type { CacheProvider, TranslateResult } from \"./types\";\nimport { createCacheKey } from \"./hash\";\n\nfunction buildKey(cache: CacheProvider, hash: string): string {\n return cache.prefix ? `${cache.prefix}${hash}` : hash;\n}\n\nexport async function getCachedResult(\n cache: CacheProvider,\n html: string,\n subject: string,\n locale: string,\n): Promise<TranslateResult | null> {\n const key = buildKey(cache, createCacheKey(html, subject, locale));\n const cached = await cache.get(key);\n if (!cached) return null;\n return JSON.parse(cached) as TranslateResult;\n}\n\nexport async function setCachedResult(\n cache: CacheProvider,\n html: string,\n subject: string,\n locale: string,\n result: TranslateResult,\n): Promise<void> {\n const key = buildKey(cache, createCacheKey(html, subject, locale));\n await cache.set(key, JSON.stringify(result));\n}\n","import { parse, NodeType } from \"node-html-parser\";\nimport type { HTMLElement as ParsedElement } from \"node-html-parser\";\nimport { baseLocale } from \"./utils\";\n\nconst RTL_LOCALES = new Set([\n \"ar\",\n \"he\",\n \"fa\",\n \"ur\",\n \"ps\",\n \"sd\",\n \"ug\",\n \"yi\",\n \"dv\",\n]);\n\nexport function isRtlLocale(locale: string): boolean {\n return RTL_LOCALES.has(baseLocale(locale));\n}\n\nexport function injectRtlDir(html: string): string {\n const root = parse(html);\n const htmlEl = root.querySelector(\"html\");\n\n if (htmlEl) {\n htmlEl.setAttribute(\"dir\", \"rtl\");\n return root.toString();\n }\n\n for (const child of root.childNodes) {\n if (child.nodeType === NodeType.ELEMENT_NODE) {\n (child as ParsedElement).setAttribute(\"dir\", \"rtl\");\n break;\n }\n }\n\n return root.toString();\n}\n","import type {\n AiLanguageModel,\n AiSdkConfig,\n I18nEmailConfig,\n TanstackAiTextAdapter,\n TanstackAiAdapterConfig,\n} from \"./types\";\n\nexport function baseLocale(tag: string): string {\n return tag.split(\"-\")[0]!.toLowerCase();\n}\n\nexport function chunk<T>(arr: T[], size: number): T[][] {\n const chunks: T[][] = [];\n for (let i = 0; i < arr.length; i += size) {\n chunks.push(arr.slice(i, i + size));\n }\n return chunks;\n}\n\nexport function isAiLanguageModel(value: unknown): value is AiLanguageModel {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"modelId\" in value &&\n \"provider\" in value\n );\n}\n\nexport function isAiSdkConfig(config: I18nEmailConfig): config is AiSdkConfig {\n return isAiLanguageModel(config.model);\n}\n\nexport function isTanstackAiAdapter(\n value: unknown,\n): value is TanstackAiTextAdapter {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as Record<string, unknown>)[\"kind\"] === \"text\" &&\n typeof (value as Record<string, unknown>)[\"name\"] === \"string\"\n );\n}\n\nexport function isTanstackAiAdapterConfig(\n config: I18nEmailConfig,\n): config is TanstackAiAdapterConfig {\n return isTanstackAiAdapter(config.adapter);\n}\n\nexport async function createOpenAIClient(\n config: import(\"./types\").OpenAIConfig,\n): Promise<import(\"openai\").default> {\n let OpenAI: typeof import(\"openai\").default;\n try {\n OpenAI = (await import(\"openai\")).default;\n } catch {\n throw new Error(\n 'i18n-email: The \"openai\" package is required when using a string model name. ' +\n \"Install it with: npm install openai\",\n );\n }\n return new OpenAI({\n apiKey: config.openaiApiKey,\n baseURL: config.baseURL,\n maxRetries: config.maxRetries ?? 2,\n });\n}\n","import type {\n I18nEmailConfig,\n TranslateOptions,\n TranslateResult,\n TranslationResponse,\n} from \"./types\";\nimport { renderReactEmail } from \"./render\";\nimport { extractStrings } from \"./extract\";\nimport { injectTranslations } from \"./inject\";\nimport { translateStrings } from \"./translate\";\nimport { translateStringsWithAi } from \"./translate-ai\";\nimport { translateStringsWithTanstack } from \"./translate-tanstack\";\nimport { getCachedResult, setCachedResult } from \"./cache\";\nimport { isRtlLocale, injectRtlDir } from \"./rtl\";\nimport {\n baseLocale,\n chunk,\n createOpenAIClient,\n isAiSdkConfig,\n isTanstackAiAdapterConfig,\n} from \"./utils\";\n\nconst DEFAULT_BATCH_SIZE = 50;\n\nexport function createI18nEmail(config: I18nEmailConfig) {\n const aiSdk = isAiSdkConfig(config);\n const tanstackAi = isTanstackAiAdapterConfig(config);\n\n let clientPromise: Promise<import(\"openai\").default> | undefined;\n\n function getClient(): Promise<import(\"openai\").default> {\n if (!clientPromise) {\n clientPromise = createOpenAIClient(\n config as import(\"./types\").OpenAIConfig,\n );\n }\n return clientPromise;\n }\n\n async function translateBatch(\n strings: string[],\n locale: string,\n ): Promise<TranslationResponse> {\n if (tanstackAi) {\n return translateStringsWithTanstack(config.adapter, strings, locale);\n }\n\n if (aiSdk) {\n return translateStringsWithAi(config.model, strings, locale);\n }\n\n const client = await getClient();\n return translateStrings(client, strings, locale, config.model ?? \"gpt-4o\");\n }\n\n async function translate(\n options: TranslateOptions,\n ): Promise<TranslateResult> {\n const { locale, subject } = options;\n\n const html = options.react\n ? await renderReactEmail(options.react)\n : options.html;\n\n if (config.cache) {\n const cached = await getCachedResult(config.cache, html, subject, locale);\n if (cached) {\n config.onTranslate?.({\n locale,\n detectedLocale: locale,\n strings: [],\n cacheHit: true,\n });\n return cached;\n }\n }\n\n const { root, entries, uniqueStrings } = extractStrings(html);\n\n const allStrings = [subject, ...uniqueStrings];\n const batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;\n const batches = chunk(allStrings, batchSize);\n\n const firstBatch = batches[0];\n if (!firstBatch || firstBatch.length === 0) {\n return { subject, html };\n }\n\n const firstResponse = await translateBatch(firstBatch, locale);\n\n if (baseLocale(firstResponse.detectedLocale) === baseLocale(locale)) {\n const result: TranslateResult = { subject, html };\n if (config.cache) {\n await setCachedResult(config.cache, html, subject, locale, result);\n }\n config.onTranslate?.({\n locale,\n detectedLocale: firstResponse.detectedLocale,\n strings: allStrings,\n cacheHit: false,\n });\n return result;\n }\n\n const allTranslations = [...firstResponse.translations];\n\n for (let i = 1; i < batches.length; i++) {\n const batch = batches[i]!;\n const response = await translateBatch(batch, locale);\n allTranslations.push(...response.translations);\n }\n\n const translatedSubject = allTranslations[0]!;\n const translationMap = new Map<string, string>();\n uniqueStrings.forEach((original, i) => {\n translationMap.set(original, allTranslations[i + 1]!);\n });\n\n let translatedHtml = injectTranslations(root, entries, translationMap);\n\n if (isRtlLocale(locale)) {\n translatedHtml = injectRtlDir(translatedHtml);\n }\n\n const result: TranslateResult = {\n subject: translatedSubject,\n html: translatedHtml,\n };\n\n if (config.cache) {\n await setCachedResult(config.cache, html, subject, locale, result);\n }\n\n config.onTranslate?.({\n locale,\n detectedLocale: firstResponse.detectedLocale,\n strings: allStrings,\n cacheHit: false,\n });\n\n return result;\n }\n\n return { translate };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,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;;;AC3BO,SAAS,kBAAkB,QAAwB;AACxD,SAAO;AAAA,IACL,wCAAwC,MAAM;AAAA,IAC9C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AACb;AAEO,SAAS,gBAAgB,SAA2B;AACzD,SAAO,KAAK,UAAU,OAAO;AAC/B;;;ACRA,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,kBAAkB,MAAM;AAAA,UACxB;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF,EAAE,KAAK,IAAI;AAAA,MACb;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,SAAS,gBAAgB,OAAO;AAAA,MAClC;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;;;ACxDA,eAAsB,uBACpB,OACA,SACA,QAC8B;AAC9B,MAAI;AACJ,MAAI;AAEJ,MAAI;AACF,UAAM,KAAK,MAAM,OAAO,IAAI;AAC5B,qBAAiB,GAAG;AACpB,iBAAa,GAAG;AAAA,EAClB,QAAQ;AACN,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AAEA,QAAM,SAAS,MAAM,eAAe;AAAA,IAClC;AAAA,IACA,QAAQ,WAAgC;AAAA,MACtC,MAAM;AAAA,MACN,YAAY;AAAA,QACV,gBAAgB;AAAA,UACd,MAAM;AAAA,UACN,aAAa;AAAA,QACf;AAAA,QACA,cAAc;AAAA,UACZ,MAAM;AAAA,UACN,OAAO,EAAE,MAAM,SAAS;AAAA,UACxB,aACE;AAAA,QACJ;AAAA,MACF;AAAA,MACA,UAAU,CAAC,kBAAkB,cAAc;AAAA,MAC3C,sBAAsB;AAAA,IACxB,CAAC;AAAA,IACD,QAAQ,kBAAkB,MAAM;AAAA,IAChC,QAAQ,gBAAgB,OAAO;AAAA,EACjC,CAAC;AAED,QAAM,EAAE,gBAAgB,aAAa,IAAI,OAAO;AAEhD,MAAI,aAAa,WAAW,QAAQ,QAAQ;AAC1C,UAAM,IAAI;AAAA,MACR,oDACc,QAAQ,MAAM,SACnB,aAAa,MAAM;AAAA,IAC9B;AAAA,EACF;AAEA,SAAO,EAAE,gBAAgB,aAAa;AACxC;;;ACrDA,IAAM,gBAAgB;AAAA,EACpB,MAAM;AAAA,EACN,YAAY;AAAA,IACV,gBAAgB;AAAA,MACd,MAAM;AAAA,MACN,aAAa;AAAA,IACf;AAAA,IACA,cAAc;AAAA,MACZ,MAAM;AAAA,MACN,OAAO,EAAE,MAAM,SAAS;AAAA,MACxB,aAAa;AAAA,IACf;AAAA,EACF;AAAA,EACA,UAAU,CAAC,kBAAkB,cAAc;AAAA,EAC3C,sBAAsB;AACxB;AAEA,eAAsB,6BACpB,SACA,SACA,QAC8B;AAC9B,MAAI;AACJ,MAAI;AACF,YAAQ,MAAM,OAAO,cAAc,GAAG;AAAA,EACxC,QAAQ;AACN,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AAEA,QAAM,SAAS,MAAM,KAAK;AAAA,IACxB;AAAA,IACA,eAAe,CAAC,kBAAkB,MAAM,CAAC;AAAA,IACzC,UAAU,CAAC,EAAE,MAAM,QAAQ,SAAS,gBAAgB,OAAO,EAAE,CAAC;AAAA,IAC9D,cAAc;AAAA,IACd,QAAQ;AAAA,EACV,CAAC;AAED,QAAM,EAAE,gBAAgB,aAAa,IAAI;AAEzC,MAAI,aAAa,WAAW,QAAQ,QAAQ;AAC1C,UAAM,IAAI;AAAA,MACR,oDACc,QAAQ,MAAM,SACnB,aAAa,MAAM;AAAA,IAC9B;AAAA,EACF;AAEA,SAAO,EAAE,gBAAgB,aAAa;AACxC;;;ACtDA,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;;;ACQzB,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;AAEO,SAAS,kBAAkB,OAA0C;AAC1E,SACE,OAAO,UAAU,YACjB,UAAU,QACV,aAAa,SACb,cAAc;AAElB;AAEO,SAAS,cAAc,QAAgD;AAC5E,SAAO,kBAAkB,OAAO,KAAK;AACvC;AAEO,SAAS,oBACd,OACgC;AAChC,SACE,OAAO,UAAU,YACjB,UAAU,QACT,MAAkC,MAAM,MAAM,UAC/C,OAAQ,MAAkC,MAAM,MAAM;AAE1D;AAEO,SAAS,0BACd,QACmC;AACnC,SAAO,oBAAoB,OAAO,OAAO;AAC3C;AAEA,eAAsB,mBACpB,QACmC;AACnC,MAAI;AACJ,MAAI;AACF,cAAU,MAAM,OAAO,QAAQ,GAAG;AAAA,EACpC,QAAQ;AACN,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AACA,SAAO,IAAI,OAAO;AAAA,IAChB,QAAQ,OAAO;AAAA,IACf,SAAS,OAAO;AAAA,IAChB,YAAY,OAAO,cAAc;AAAA,EACnC,CAAC;AACH;;;AD/DA,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;;;AEfA,IAAM,qBAAqB;AAEpB,SAAS,gBAAgB,QAAyB;AACvD,QAAM,QAAQ,cAAc,MAAM;AAClC,QAAM,aAAa,0BAA0B,MAAM;AAEnD,MAAI;AAEJ,WAAS,YAA+C;AACtD,QAAI,CAAC,eAAe;AAClB,sBAAgB;AAAA,QACd;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,iBAAe,eACb,SACA,QAC8B;AAC9B,QAAI,YAAY;AACd,aAAO,6BAA6B,OAAO,SAAS,SAAS,MAAM;AAAA,IACrE;AAEA,QAAI,OAAO;AACT,aAAO,uBAAuB,OAAO,OAAO,SAAS,MAAM;AAAA,IAC7D;AAEA,UAAM,SAAS,MAAM,UAAU;AAC/B,WAAO,iBAAiB,QAAQ,SAAS,QAAQ,OAAO,SAAS,QAAQ;AAAA,EAC3E;AAEA,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,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,eAAe,YAAY,MAAM;AAE7D,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,eAAe,OAAO,MAAM;AACnD,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","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
|
-
|
|
31
|
-
baseURL?: never;
|
|
32
|
-
maxRetries?: never;
|
|
33
|
+
adapter?: never;
|
|
33
34
|
}
|
|
34
|
-
|
|
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;
|
|
@@ -64,4 +69,4 @@ declare function createI18nEmail(config: I18nEmailConfig): {
|
|
|
64
69
|
translate: (options: TranslateOptions) => Promise<TranslateResult>;
|
|
65
70
|
};
|
|
66
71
|
|
|
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 };
|
|
72
|
+
export { type AiLanguageModel, type AiSdkConfig, type CacheProvider, type I18nEmailConfig, type OpenAIConfig, type TanstackAiAdapterConfig, type TanstackAiTextAdapter, type TranslateCallbackInfo, type TranslateOptions, type TranslateOptionsHtml, type TranslateOptionsReact, type TranslateResult, type TranslationResponse, createI18nEmail };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,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
|
-
|
|
31
|
-
baseURL?: never;
|
|
32
|
-
maxRetries?: never;
|
|
33
|
+
adapter?: never;
|
|
33
34
|
}
|
|
34
|
-
|
|
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;
|
|
@@ -64,4 +69,4 @@ declare function createI18nEmail(config: I18nEmailConfig): {
|
|
|
64
69
|
translate: (options: TranslateOptions) => Promise<TranslateResult>;
|
|
65
70
|
};
|
|
66
71
|
|
|
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 };
|
|
72
|
+
export { type AiLanguageModel, type AiSdkConfig, type CacheProvider, type I18nEmailConfig, type OpenAIConfig, type TanstackAiAdapterConfig, type TanstackAiTextAdapter, type TranslateCallbackInfo, type TranslateOptions, type TranslateOptionsHtml, type TranslateOptionsReact, type TranslateResult, type TranslationResponse, createI18nEmail };
|
package/dist/index.js
CHANGED
|
@@ -193,6 +193,48 @@ async function translateStringsWithAi(model, strings, locale) {
|
|
|
193
193
|
return { detectedLocale, translations };
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
// src/translate-tanstack.ts
|
|
197
|
+
var OUTPUT_SCHEMA = {
|
|
198
|
+
type: "object",
|
|
199
|
+
properties: {
|
|
200
|
+
detectedLocale: {
|
|
201
|
+
type: "string",
|
|
202
|
+
description: "The ISO locale code of the source language"
|
|
203
|
+
},
|
|
204
|
+
translations: {
|
|
205
|
+
type: "array",
|
|
206
|
+
items: { type: "string" },
|
|
207
|
+
description: "Translated strings in the exact same order as the input"
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
required: ["detectedLocale", "translations"],
|
|
211
|
+
additionalProperties: false
|
|
212
|
+
};
|
|
213
|
+
async function translateStringsWithTanstack(adapter, strings, locale) {
|
|
214
|
+
let chat;
|
|
215
|
+
try {
|
|
216
|
+
chat = (await import("@tanstack/ai")).chat;
|
|
217
|
+
} catch {
|
|
218
|
+
throw new Error(
|
|
219
|
+
'i18n-email: The "@tanstack/ai" package is required when using a TanStack adapter. Install it with: npm install @tanstack/ai'
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const result = await chat({
|
|
223
|
+
adapter,
|
|
224
|
+
systemPrompts: [buildSystemPrompt(locale)],
|
|
225
|
+
messages: [{ role: "user", content: buildUserPrompt(strings) }],
|
|
226
|
+
outputSchema: OUTPUT_SCHEMA,
|
|
227
|
+
stream: false
|
|
228
|
+
});
|
|
229
|
+
const { detectedLocale, translations } = result;
|
|
230
|
+
if (translations.length !== strings.length) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
`i18n-email: Translation count mismatch. Expected ${strings.length}, got ${translations.length}`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return { detectedLocale, translations };
|
|
236
|
+
}
|
|
237
|
+
|
|
196
238
|
// src/hash.ts
|
|
197
239
|
import { createHash } from "crypto";
|
|
198
240
|
function createCacheKey(html, subject, locale) {
|
|
@@ -234,6 +276,12 @@ function isAiLanguageModel(value) {
|
|
|
234
276
|
function isAiSdkConfig(config) {
|
|
235
277
|
return isAiLanguageModel(config.model);
|
|
236
278
|
}
|
|
279
|
+
function isTanstackAiAdapter(value) {
|
|
280
|
+
return typeof value === "object" && value !== null && value["kind"] === "text" && typeof value["name"] === "string";
|
|
281
|
+
}
|
|
282
|
+
function isTanstackAiAdapterConfig(config) {
|
|
283
|
+
return isTanstackAiAdapter(config.adapter);
|
|
284
|
+
}
|
|
237
285
|
async function createOpenAIClient(config) {
|
|
238
286
|
let OpenAI;
|
|
239
287
|
try {
|
|
@@ -285,6 +333,7 @@ function injectRtlDir(html) {
|
|
|
285
333
|
var DEFAULT_BATCH_SIZE = 50;
|
|
286
334
|
function createI18nEmail(config) {
|
|
287
335
|
const aiSdk = isAiSdkConfig(config);
|
|
336
|
+
const tanstackAi = isTanstackAiAdapterConfig(config);
|
|
288
337
|
let clientPromise;
|
|
289
338
|
function getClient() {
|
|
290
339
|
if (!clientPromise) {
|
|
@@ -295,6 +344,9 @@ function createI18nEmail(config) {
|
|
|
295
344
|
return clientPromise;
|
|
296
345
|
}
|
|
297
346
|
async function translateBatch(strings, locale) {
|
|
347
|
+
if (tanstackAi) {
|
|
348
|
+
return translateStringsWithTanstack(config.adapter, strings, locale);
|
|
349
|
+
}
|
|
298
350
|
if (aiSdk) {
|
|
299
351
|
return translateStringsWithAi(config.model, strings, locale);
|
|
300
352
|
}
|
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 } from \"@react-email/render\";\nimport type { ReactElement } from \"react\";\n\nexport async function renderReactEmail(\n component: ReactElement,\n): Promise<string> {\n try {\n return await render(component);\n } catch (error) {\n throw new Error(\n `i18n-email: Failed to render React component: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n }\n}\n","import { parse, NodeType } from \"node-html-parser\";\nimport type { HTMLElement as ParsedElement, Node } from \"node-html-parser\";\n\nconst TRANSLATABLE_ATTRS = [\"alt\", \"title\"];\nconst SKIP_TAGS = new Set([\"style\", \"script\", \"head\"]);\n\nexport interface TextEntry {\n type: \"text\";\n nodes: Node[];\n text: string;\n}\n\nexport interface AttributeEntry {\n type: \"attribute\";\n element: ParsedElement;\n attrName: string;\n text: string;\n}\n\nexport type ExtractionEntry = TextEntry | AttributeEntry;\n\nexport interface ExtractionResult {\n root: ParsedElement;\n entries: ExtractionEntry[];\n uniqueStrings: string[];\n}\n\nexport function extractStrings(html: string): ExtractionResult {\n const root = parse(html);\n const entries: ExtractionEntry[] = [];\n\n function walk(element: ParsedElement): void {\n const tag = element.tagName?.toLowerCase();\n if (tag && SKIP_TAGS.has(tag)) return;\n\n for (const attr of TRANSLATABLE_ATTRS) {\n const value = element.getAttribute(attr);\n if (value && value.trim()) {\n entries.push({\n type: \"attribute\",\n element,\n attrName: attr,\n text: value,\n });\n }\n }\n\n const children = element.childNodes;\n let i = 0;\n\n while (i < children.length) {\n const child = children[i]!;\n\n if (child.nodeType === NodeType.TEXT_NODE) {\n const textNodes: Node[] = [];\n while (\n i < children.length &&\n children[i]?.nodeType === NodeType.TEXT_NODE\n ) {\n textNodes.push(children[i]!);\n i++;\n }\n const merged = textNodes.map((n) => n.rawText).join(\"\");\n const trimmed = merged.trim();\n if (trimmed && !trimmed.startsWith(\"<!DOCTYPE\")) {\n entries.push({\n type: \"text\",\n nodes: textNodes,\n text: merged,\n });\n }\n } else if (child.nodeType === NodeType.ELEMENT_NODE) {\n walk(child as ParsedElement);\n i++;\n } else {\n i++;\n }\n }\n }\n\n walk(root);\n\n const seen = new Set<string>();\n const uniqueStrings: string[] = [];\n for (const entry of entries) {\n if (!seen.has(entry.text)) {\n seen.add(entry.text);\n uniqueStrings.push(entry.text);\n }\n }\n\n return { root, entries, uniqueStrings };\n}\n","import type { ExtractionEntry } from \"./extract\";\nimport type { HTMLElement as ParsedElement } from \"node-html-parser\";\n\nexport function injectTranslations(\n root: ParsedElement,\n entries: ExtractionEntry[],\n translationMap: Map<string, string>,\n): string {\n for (const entry of entries) {\n const translated = translationMap.get(entry.text);\n if (translated === undefined) continue;\n\n if (entry.type === \"attribute\") {\n entry.element.setAttribute(entry.attrName, translated);\n } else {\n const firstNode = entry.nodes[0];\n if (!firstNode) continue;\n\n firstNode.rawText = translated;\n for (let i = 1; i < entry.nodes.length; i++) {\n const node = entry.nodes[i];\n if (node) node.rawText = \"\";\n }\n }\n }\n\n return root.toString();\n}\n","export function buildSystemPrompt(locale: string): string {\n return [\n `You are translating email content to ${locale}.`,\n \"Rules:\",\n \"- Preserve dynamic values like names, URLs, amounts, dates, and codes exactly as they appear\",\n \"- Do not translate brand names or product names\",\n \"- Preserve tone: professional but friendly\",\n ].join(\"\\n\");\n}\n\nexport function buildUserPrompt(strings: string[]): string {\n return JSON.stringify(strings);\n}\n","import type OpenAI from \"openai\";\nimport type { TranslationResponse } from \"./types\";\nimport { buildSystemPrompt, buildUserPrompt } from \"./prompt\";\n\nexport async function translateStrings(\n client: OpenAI,\n strings: string[],\n locale: string,\n model: string,\n): Promise<TranslationResponse> {\n const response = await client.chat.completions.create({\n model,\n response_format: { type: \"json_object\" },\n messages: [\n {\n role: \"system\",\n content: [\n buildSystemPrompt(locale),\n \"- Return a JSON object with two fields:\",\n ' 1. \"detectedLocale\": the ISO locale code of the source language',\n ' 2. \"translations\": a JSON array of translated strings in the exact same order as the input',\n \"- Return only the JSON object, no explanation\",\n ].join(\"\\n\"),\n },\n {\n role: \"user\",\n content: buildUserPrompt(strings),\n },\n ],\n });\n\n const content = response.choices[0]?.message?.content;\n if (!content) {\n throw new Error(\"i18n-email: OpenAI returned an empty response\");\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch {\n throw new Error(`i18n-email: OpenAI returned malformed JSON: ${content}`);\n }\n\n const result = parsed as TranslationResponse;\n if (!result.detectedLocale || !Array.isArray(result.translations)) {\n throw new Error(\n `i18n-email: Unexpected response shape from OpenAI: ${content}`,\n );\n }\n\n if (result.translations.length !== strings.length) {\n throw new Error(\n `i18n-email: Translation count mismatch. ` +\n `Expected ${strings.length}, ` +\n `got ${result.translations.length}`,\n );\n }\n\n return result;\n}\n","import type { AiLanguageModel, TranslationResponse } from \"./types\";\nimport { buildSystemPrompt, buildUserPrompt } from \"./prompt\";\n\nexport async function translateStringsWithAi(\n model: AiLanguageModel,\n strings: string[],\n locale: string,\n): Promise<TranslationResponse> {\n let generateObject: typeof import(\"ai\").generateObject;\n let jsonSchema: typeof import(\"ai\").jsonSchema;\n\n try {\n const ai = await import(\"ai\");\n generateObject = ai.generateObject;\n jsonSchema = ai.jsonSchema;\n } catch {\n throw new Error(\n 'i18n-email: The \"ai\" package is required when using an AI SDK model. ' +\n \"Install it with: npm install ai\",\n );\n }\n\n const result = await generateObject({\n model: model as Parameters<typeof generateObject>[0][\"model\"],\n schema: jsonSchema<TranslationResponse>({\n type: \"object\",\n properties: {\n detectedLocale: {\n type: \"string\",\n description: \"The ISO locale code of the source language\",\n },\n translations: {\n type: \"array\",\n items: { type: \"string\" },\n description:\n \"Translated strings in the exact same order as the input\",\n },\n },\n required: [\"detectedLocale\", \"translations\"],\n additionalProperties: false,\n }),\n system: buildSystemPrompt(locale),\n prompt: buildUserPrompt(strings),\n });\n\n const { detectedLocale, translations } = result.object;\n\n if (translations.length !== strings.length) {\n throw new Error(\n `i18n-email: Translation count mismatch. ` +\n `Expected ${strings.length}, ` +\n `got ${translations.length}`,\n );\n }\n\n return { detectedLocale, translations };\n}\n","import type { TanstackAiTextAdapter, TranslationResponse } from \"./types\";\nimport { buildSystemPrompt, buildUserPrompt } from \"./prompt\";\n\nconst OUTPUT_SCHEMA = {\n type: \"object\",\n properties: {\n detectedLocale: {\n type: \"string\",\n description: \"The ISO locale code of the source language\",\n },\n translations: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Translated strings in the exact same order as the input\",\n },\n },\n required: [\"detectedLocale\", \"translations\"],\n additionalProperties: false,\n};\n\nexport async function translateStringsWithTanstack(\n adapter: TanstackAiTextAdapter,\n strings: string[],\n locale: string,\n): Promise<TranslationResponse> {\n let chat: typeof import(\"@tanstack/ai\").chat;\n try {\n chat = (await import(\"@tanstack/ai\")).chat;\n } catch {\n throw new Error(\n 'i18n-email: The \"@tanstack/ai\" package is required when using a TanStack adapter. ' +\n \"Install it with: npm install @tanstack/ai\",\n );\n }\n\n const result = await chat({\n adapter,\n systemPrompts: [buildSystemPrompt(locale)],\n messages: [{ role: \"user\", content: buildUserPrompt(strings) }],\n outputSchema: OUTPUT_SCHEMA,\n stream: false,\n });\n\n const { detectedLocale, translations } = result as TranslationResponse;\n\n if (translations.length !== strings.length) {\n throw new Error(\n `i18n-email: Translation count mismatch. ` +\n `Expected ${strings.length}, ` +\n `got ${translations.length}`,\n );\n }\n\n return { detectedLocale, translations };\n}\n","import { createHash } from \"node:crypto\";\n\nexport function createCacheKey(\n html: string,\n subject: string,\n locale: string,\n): string {\n return createHash(\"sha256\")\n .update([html, subject, locale].join(\"\\0\"))\n .digest(\"hex\");\n}\n","import type { CacheProvider, TranslateResult } from \"./types\";\nimport { createCacheKey } from \"./hash\";\n\nfunction buildKey(cache: CacheProvider, hash: string): string {\n return cache.prefix ? `${cache.prefix}${hash}` : hash;\n}\n\nexport async function getCachedResult(\n cache: CacheProvider,\n html: string,\n subject: string,\n locale: string,\n): Promise<TranslateResult | null> {\n const key = buildKey(cache, createCacheKey(html, subject, locale));\n const cached = await cache.get(key);\n if (!cached) return null;\n return JSON.parse(cached) as TranslateResult;\n}\n\nexport async function setCachedResult(\n cache: CacheProvider,\n html: string,\n subject: string,\n locale: string,\n result: TranslateResult,\n): Promise<void> {\n const key = buildKey(cache, createCacheKey(html, subject, locale));\n await cache.set(key, JSON.stringify(result));\n}\n","import { parse, NodeType } from \"node-html-parser\";\nimport type { HTMLElement as ParsedElement } from \"node-html-parser\";\nimport { baseLocale } from \"./utils\";\n\nconst RTL_LOCALES = new Set([\n \"ar\",\n \"he\",\n \"fa\",\n \"ur\",\n \"ps\",\n \"sd\",\n \"ug\",\n \"yi\",\n \"dv\",\n]);\n\nexport function isRtlLocale(locale: string): boolean {\n return RTL_LOCALES.has(baseLocale(locale));\n}\n\nexport function injectRtlDir(html: string): string {\n const root = parse(html);\n const htmlEl = root.querySelector(\"html\");\n\n if (htmlEl) {\n htmlEl.setAttribute(\"dir\", \"rtl\");\n return root.toString();\n }\n\n for (const child of root.childNodes) {\n if (child.nodeType === NodeType.ELEMENT_NODE) {\n (child as ParsedElement).setAttribute(\"dir\", \"rtl\");\n break;\n }\n }\n\n return root.toString();\n}\n","import type {\n AiLanguageModel,\n AiSdkConfig,\n I18nEmailConfig,\n TanstackAiTextAdapter,\n TanstackAiAdapterConfig,\n} from \"./types\";\n\nexport function baseLocale(tag: string): string {\n return tag.split(\"-\")[0]!.toLowerCase();\n}\n\nexport function chunk<T>(arr: T[], size: number): T[][] {\n const chunks: T[][] = [];\n for (let i = 0; i < arr.length; i += size) {\n chunks.push(arr.slice(i, i + size));\n }\n return chunks;\n}\n\nexport function isAiLanguageModel(value: unknown): value is AiLanguageModel {\n return (\n typeof value === \"object\" &&\n value !== null &&\n \"modelId\" in value &&\n \"provider\" in value\n );\n}\n\nexport function isAiSdkConfig(config: I18nEmailConfig): config is AiSdkConfig {\n return isAiLanguageModel(config.model);\n}\n\nexport function isTanstackAiAdapter(\n value: unknown,\n): value is TanstackAiTextAdapter {\n return (\n typeof value === \"object\" &&\n value !== null &&\n (value as Record<string, unknown>)[\"kind\"] === \"text\" &&\n typeof (value as Record<string, unknown>)[\"name\"] === \"string\"\n );\n}\n\nexport function isTanstackAiAdapterConfig(\n config: I18nEmailConfig,\n): config is TanstackAiAdapterConfig {\n return isTanstackAiAdapter(config.adapter);\n}\n\nexport async function createOpenAIClient(\n config: import(\"./types\").OpenAIConfig,\n): Promise<import(\"openai\").default> {\n let OpenAI: typeof import(\"openai\").default;\n try {\n OpenAI = (await import(\"openai\")).default;\n } catch {\n throw new Error(\n 'i18n-email: The \"openai\" package is required when using a string model name. ' +\n \"Install it with: npm install openai\",\n );\n }\n return new OpenAI({\n apiKey: config.openaiApiKey,\n baseURL: config.baseURL,\n maxRetries: config.maxRetries ?? 2,\n });\n}\n","import type {\n I18nEmailConfig,\n TranslateOptions,\n TranslateResult,\n TranslationResponse,\n} from \"./types\";\nimport { renderReactEmail } from \"./render\";\nimport { extractStrings } from \"./extract\";\nimport { injectTranslations } from \"./inject\";\nimport { translateStrings } from \"./translate\";\nimport { translateStringsWithAi } from \"./translate-ai\";\nimport { translateStringsWithTanstack } from \"./translate-tanstack\";\nimport { getCachedResult, setCachedResult } from \"./cache\";\nimport { isRtlLocale, injectRtlDir } from \"./rtl\";\nimport {\n baseLocale,\n chunk,\n createOpenAIClient,\n isAiSdkConfig,\n isTanstackAiAdapterConfig,\n} from \"./utils\";\n\nconst DEFAULT_BATCH_SIZE = 50;\n\nexport function createI18nEmail(config: I18nEmailConfig) {\n const aiSdk = isAiSdkConfig(config);\n const tanstackAi = isTanstackAiAdapterConfig(config);\n\n let clientPromise: Promise<import(\"openai\").default> | undefined;\n\n function getClient(): Promise<import(\"openai\").default> {\n if (!clientPromise) {\n clientPromise = createOpenAIClient(\n config as import(\"./types\").OpenAIConfig,\n );\n }\n return clientPromise;\n }\n\n async function translateBatch(\n strings: string[],\n locale: string,\n ): Promise<TranslationResponse> {\n if (tanstackAi) {\n return translateStringsWithTanstack(config.adapter, strings, locale);\n }\n\n if (aiSdk) {\n return translateStringsWithAi(config.model, strings, locale);\n }\n\n const client = await getClient();\n return translateStrings(client, strings, locale, config.model ?? \"gpt-4o\");\n }\n\n async function translate(\n options: TranslateOptions,\n ): Promise<TranslateResult> {\n const { locale, subject } = options;\n\n const html = options.react\n ? await renderReactEmail(options.react)\n : options.html;\n\n if (config.cache) {\n const cached = await getCachedResult(config.cache, html, subject, locale);\n if (cached) {\n config.onTranslate?.({\n locale,\n detectedLocale: locale,\n strings: [],\n cacheHit: true,\n });\n return cached;\n }\n }\n\n const { root, entries, uniqueStrings } = extractStrings(html);\n\n const allStrings = [subject, ...uniqueStrings];\n const batchSize = config.batchSize ?? DEFAULT_BATCH_SIZE;\n const batches = chunk(allStrings, batchSize);\n\n const firstBatch = batches[0];\n if (!firstBatch || firstBatch.length === 0) {\n return { subject, html };\n }\n\n const firstResponse = await translateBatch(firstBatch, locale);\n\n if (baseLocale(firstResponse.detectedLocale) === baseLocale(locale)) {\n const result: TranslateResult = { subject, html };\n if (config.cache) {\n await setCachedResult(config.cache, html, subject, locale, result);\n }\n config.onTranslate?.({\n locale,\n detectedLocale: firstResponse.detectedLocale,\n strings: allStrings,\n cacheHit: false,\n });\n return result;\n }\n\n const allTranslations = [...firstResponse.translations];\n\n for (let i = 1; i < batches.length; i++) {\n const batch = batches[i]!;\n const response = await translateBatch(batch, locale);\n allTranslations.push(...response.translations);\n }\n\n const translatedSubject = allTranslations[0]!;\n const translationMap = new Map<string, string>();\n uniqueStrings.forEach((original, i) => {\n translationMap.set(original, allTranslations[i + 1]!);\n });\n\n let translatedHtml = injectTranslations(root, entries, translationMap);\n\n if (isRtlLocale(locale)) {\n translatedHtml = injectRtlDir(translatedHtml);\n }\n\n const result: TranslateResult = {\n subject: translatedSubject,\n html: translatedHtml,\n };\n\n if (config.cache) {\n await setCachedResult(config.cache, html, subject, locale, result);\n }\n\n config.onTranslate?.({\n locale,\n detectedLocale: firstResponse.detectedLocale,\n strings: allStrings,\n cacheHit: false,\n });\n\n return result;\n }\n\n return { translate };\n}\n"],"mappings":"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","names":["parse","NodeType","parse","NodeType","result"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "i18n-email",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Translate transactional emails into any language using AI models.",
|
|
6
6
|
"author": "Dan Zabrotski",
|
|
@@ -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",
|