i18n-email

Caching

Avoid redundant OpenAI API calls by caching translation results.

Overview

Every translate call makes a request to OpenAI. For high-volume use cases, or for emails with static content, you can provide a cache adapter to store results and skip the API call on subsequent runs.

The cache key is a SHA-256 hash of the HTML, subject, and locale combined. If any of these change, a new translation is fetched automatically.

Cache interface

interface CacheProvider {
  prefix?: string;
  get: (key: string) => Promise<string | null>;
  set: (key: string, value: string) => Promise<void>;
}

Any async key-value store works — Redis, Upstash, DynamoDB, Cloudflare KV, or even a simple in-memory Map.

Key prefix

Set prefix to namespace your cache keys. This is useful when sharing a Redis instance across multiple services.

cache: {
  prefix: "i18n-email:",
  get: ...,
  set: ...,
}

Examples

Upstash Redis

import { createI18nEmail } from "i18n-email";
import { Redis } from "@upstash/redis";

const redis = Redis.fromEnv();

const i18nEmail = createI18nEmail({
  openaiApiKey: process.env.OPENAI_API_KEY!,
  cache: {
    prefix: "i18n-email:",
    get: async (key) => {
      const value = await redis.get(key);
      if (value === null) return null;
      // Upstash auto-deserializes JSON, so re-stringify if needed
      return typeof value === "string" ? value : JSON.stringify(value);
    },
    set: async (key, value) => {
      await redis.set(key, value);
    },
  },
});

Upstash Redis automatically deserializes stored JSON on get. The wrapper above re-stringifies the value so the cache layer always receives a plain string.

In-memory (development / testing)

const store = new Map<string, string>();

const i18nEmail = createI18nEmail({
  openaiApiKey: process.env.OPENAI_API_KEY!,
  cache: {
    get: async (key) => store.get(key) ?? null,
    set: async (key, value) => {
      store.set(key, value);
    },
  },
});

Node.js node:crypto + filesystem

import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";

const cacheDir = ".cache/i18n-email";

const i18nEmail = createI18nEmail({
  openaiApiKey: process.env.OPENAI_API_KEY!,
  cache: {
    get: async (key) => {
      try {
        return await readFile(join(cacheDir, key), "utf-8");
      } catch {
        return null;
      }
    },
    set: async (key, value) => {
      await writeFile(join(cacheDir, key), value, "utf-8");
    },
  },
});

Cache invalidation

Because the key is derived from content, cache invalidation is automatic:

  • Same email, same locale → cache hit, OpenAI is not called
  • Email content changes → new hash, new translation fetched
  • Different locale → different hash, translated independently

On this page