Language is not a barrier. 12 languages. Every one a first-class citizen. Static site translation, pre-translated email delivery, live support-ticket and map-caption translation — all integrated.
Language is incidental. A user in Lahore submits a support ticket in Urdu. The admin reads it in English. The admin replies in English. The user receives the reply in Urdu. Neither party thinks about translation — the system handles it transparently. This is the design principle that drives every communication layer in The Trinity Beast.
The Trinity Beast serves 12 languages across every touchpoint:
/docs/{lang}/.Two live translation engines do different jobs here, and the distinction matters: AWS Translate does literal, synchronous language conversion for support tickets and map captions — short text, translated on the exact request that needs it. Bedrock handles everything that benefits from understanding rather than literal conversion — drafting Rhema's support replies (Qwen 3 235B), translating full newsletter HTML once per send rather than per recipient, and translating the entire document library (also Qwen 3 235B, via the sentinel-preprocessing engine described in the TBI Translation Engine document). Neither approach is used for the other's job, and nothing customer-facing is English-only — the one exception, session reports, is covered live rather than pre-translated, for the reason above.
All 12 languages are supported by AWS Translate, ensuring both static and dynamic translation coverage.
| Language | Static i18n Keys | Email Templates | AWS Translate | RTL Support |
|---|---|---|---|---|
| English (en) | 1,000+ | All templates | Source language | No |
| Spanish (es) | 1,000+ | All templates | ✓ | No |
| Portuguese (pt) | 1,000+ | All templates | ✓ | No |
| French (fr) | 1,000+ | All templates | ✓ | No |
| German (de) | 1,000+ | All templates | ✓ | No |
| Russian (ru) | 1,000+ | All templates | ✓ | No |
| Hindi (hi) | 1,000+ | All templates | ✓ | No |
| Urdu (ur) | 1,000+ | All templates | ✓ | Yes |
| Italian (it) | 1,000+ | All templates | ✓ | No |
| Arabic (ar) | 1,000+ | All templates | ✓ | Yes |
| Japanese (ja) | 1,000+ | All templates | ✓ | No |
| Chinese (zh) | 1,000+ | All templates | ✓ | No |
The website uses a custom i18n engine (v5) that loads language-specific JSON files and applies translations to DOM elements via data-i18n attributes. Translation data is served from Valkey via the API first, with S3 as fallback — enabling instant updates without a deploy.
| Component | Location | Purpose |
|---|---|---|
| i18n Engine | js/i18n.js | Loads JSON, applies translations, manages language switching, browser detection, cpmp_site localStorage object |
| Language Files (S3) | lang/{code}.json | 12 files, ~1,000+ keys each — fallback source when API unavailable |
| Language API (Valkey) | GET /public/lang/{code} | Primary source — serves JSON from Valkey lang:{code} key. 5-min cache header. Updated instantly via POST /admin/lang/set. |
| RTL Stylesheet | css/rtl.css | Right-to-left layout for Arabic, Urdu |
| Flag Icons | icons/flag-{code}.svg | 12 SVG flags for the language dropdown |
| Language Dropdown | includes/header.html | Site-wide language switcher in the header |
cpmp_site.lang from localStorage (JSON object). Falls back to legacy cpmp-lang flat key (auto-migrated on first visit), then browser language detection.GET /public/lang/{code} from the API (Valkey-backed) first. Falls back to lang/{code}.json from S3 if the API returns empty or fails.data-i18n="section.key" have their text content replaced. data-i18n-html for HTML content. data-i18n-placeholder for form placeholders.cpmp_site.lang in localStorage.ar, ur, or pa, the dir="rtl" attribute is set and css/rtl.css activates mirrored layouts.window.cpmpI18n._cache) — no re-fetch on page navigation within the same session.| Section | Approx. Keys | Pages Covered |
|---|---|---|
| header / footer | 30 | All pages (shared) |
| home | 80 | Homepage |
| give | 45 | Give page |
subscribeListener | 250+ | Subscription page |
docLibrary | 142 | Document Library |
| support | 27 | Support page |
| map | 25 | Impact Map |
| Impact pages (7) | 350+ | Freedom, Water, Medical, etc. |
| team / origin | 100+ | Team, Origin Story |
| optout (2) | 20 | Newsletter opt-out pages |
All outbound email from The Trinity Beast is sent via Amazon SES and rendered in the recipient's preferred language. Nothing is translated at send time — every string is pre-translated and cached, so no email send ever waits on an AI call.
Two stores serve this, split by who originates the mail. Stripe checkout receipts (subscription, donation, LRS, webhook, plan switch) are assembled by the receipt Lambda from pre-translated frames in Valkey and delivered asynchronously through SQS. Server-originated mail (support, refund, TBTS welcome, magic link, reactivation) is assembled by the LPO server from the centralized email:tpl:* template registry. Both are zero-Bedrock at send time, and both fall back gracefully when their cache is unavailable.
Every server-originated email needs to pick a language before it can render, and that language is deliberately not the same thing as the customer's api_lang — api_lang governs API response messages only, while correspondence (emails, newsletters, support replies) uses preferred_lang. The resolver (correspondenceLang) walks a short, explicit fallback chain rather than assuming a single always-populated column:
users.preferred_lang, when the caller already has it in hand for this accountnewsletter_subscribers.preferred_lang, the most recent by subscribed_at, looked up by email — a customer may not have a users row at all (e.g. a donor with no API key) but still have a language on record from a newsletter signupen — final fallbackKnown gap: three checkout-time handlers in the receipt Lambda — donation, subscription, and webhook subscription activation — still read session.MetaLocale directly instead of calling the resolver. MetaLocale is sourced from whatever locale Stripe's checkout session or Payment Link metadata carried at checkout time, not from users.preferred_lang — so a customer's account-level language preference can be silently ignored on these three paths even though the resolver exists and is correct everywhere else it's called. Not yet fixed as of this writing.
| Email Type | Trigger | Translation Method | Sender |
|---|---|---|---|
| Subscription Receipt | Stripe checkout | Valkey frames (email:frames) — zero AI | CPMP Mission |
| Donation Receipt | Stripe checkout | Valkey frames + impact messages | CPMP Mission |
| LRS Add-On Receipt | Stripe checkout | Valkey frames | CPMP Mission |
| Webhook Receipt | Stripe checkout | Valkey frames | CPMP Mission |
| Plan Switch Confirmation | Stripe subscription update | Valkey frames + tier comparison table | CPMP Mission |
| Refund Confirmation | Stripe charge refunded | Valkey frames | CPMP Mission |
| Support Confirmation | Ticket submitted | Valkey templates (email:tpl:support-confirmation) | Support@CPMP-Site.org |
| Support Reply | Admin replies to ticket | Valkey templates (email:tpl:support-reply) + AWS Translate for reply body | Support@CPMP-Site.org |
| Translation Refund | Admin or customer refund | Valkey templates (email:tpl:refund) | The Trinity Beast |
| TBTS Welcome | Translation service signup | Valkey templates (email:tpl:tbts-welcome) | CPMP Mission |
| Dashboard Magic Link | Dashboard login request | Valkey templates (email:tpl:magic-link) | The Trinity Beast |
| Reactivation | Subscription reactivated | Valkey templates (email:tpl:reactivation) | CPMP Mission |
| CPMP Newsletter Welcome | Newsletter subscription | SES Template + email_translations table | CPMP Mission |
| LPO Newsletter Welcome | LPO newsletter subscription | SES Template + email_translations table | The Trinity Beast |
| Newsletter Broadcast | Admin sends newsletter | TBCC Newsletter Console (Quill editor) | CPMP Mission / The Trinity Beast |
Correspondence model change, 2026-08: transactional emails that are not themselves part of a reply-capable thread (receipts, refund confirmations, plan-switch notices) no longer invite a reply. The copy changed from "Reply to this email or submit a support ticket" to an anchor-linked "Open a support ticket" pointing at support.html, across all 12 languages, in every template store that carried the old phrasing (email:frames, email-frames.json, and email:tpl:refund). Support-ticket-confirmation and Rhema reply-notification emails are unaffected and remain genuinely reply-capable by design — those threads exist specifically to accept a reply.
All receipt emails (the first 6 types above) are assembled from pre-translated parts stored in Valkey. No AI translation at send time. The pipeline:
email:frames (12 languages × 69 keys) from Valkey and combining frame labels with dynamic values (name, amount, API key, date).trinity-beast-email-queue (~20 ms, durable).| Valkey Key | Content | Source of Truth | Refresh |
|---|---|---|---|
email:frames | 12 langs × 69 keys (subjects, headings, labels, footers, link text) | s3://trinity-beast-website-east2/data/email-frames.json | Nightly (BeastReconciler syncEmailFrames()) |
impact:messages | 12 langs × 10 impact types (personalized donation messages) | s3://trinity-beast-website-east2/data/impact-messages.json | Nightly (BeastReconciler syncImpactMessages()) |
Every user interaction stores the language preference:
support_tickets.preferred_lang — set from the form submission's cpmp_site.lang (read from localStorage)newsletter_subscribers.preferred_lang — set at subscription timeusers.preferred_lang — set from Stripe checkout locale (passed via client_reference_id on checkout URL)email:tpl:*)Emails sent by the LPO server — as opposed to the receipt Lambda — draw their localized strings from a centralized, Valkey-backed template registry. This replaced a set of hardcoded Go switch statements that had to be recompiled and redeployed for every copy change. Now the copy lives in JSON on S3, syncs to Valkey, and the server reads it at send time.
S3 JSON (source of truth) → kcc push-email-templates → Valkey hash email:tpl:{name} → EmailLoader (5-min cache) → rendered email
The BeastReconciler nightly sync also refreshes Valkey from S3, so the registry self-heals without manual intervention.
Each template is one Valkey hash. The hash field is the language code; the value is a JSON object of string keys. Source files live in cpmp-redesign/email-templates/.
| Valkey Key | Keys per Language | Consumer |
|---|---|---|
email:tpl:magic-link | 7 | Dashboard passwordless login |
email:tpl:reactivation | 8 | Subscription reactivation confirmation |
email:tpl:refund | 22 | Translation and subscription refund confirmations |
email:tpl:support-confirmation | 12 | Ticket receipt acknowledgment |
email:tpl:support-reply | 9 | Admin or Rhema reply notification |
email:tpl:tbts-welcome | 20 | TBTS translation service onboarding. v2 added a "Resources" link section — key_copy_hint (how to copy the API key), link_demo/link_demo_desc (live-demo link), alongside the existing guide/API/dashboard link fields. Language is now threaded from the signup request itself rather than hardcoded to en. |
internal/email/loader.goA single shared EmailLoader is constructed once at server startup and hung on HandlerDeps. Every email builder receives it and resolves its strings through the same three-step chain:
sync.Map keyed by template:lang with a 5-minute TTL. Repeat sends in the same window never touch the network.HGET email:tpl:{template} {lang}. On a cache miss for a non-English language, the loader automatically retries with en.Defense in depth by design. The hardcoded functions (GetSupportEmailStrings, GetRefundEmailStrings, getTBTSWelcomeStrings) were intentionally retained after the migration rather than deleted. They cost nothing at runtime, and they mean a Valkey outage degrades email localization rather than breaking email delivery. This mirrors the platform-wide Valkey dependency policy: the cache is a performance layer, never a persistence layer.
Every migrated builder follows an identical shape — try the loader, fall back to hardcoded, merge key by key. A missing individual key falls back independently, so a partially translated template still renders completely:
// Resolve strings with graceful degradation at every level
func getRefundStringsFromLoader(loader *email.Loader, lang string) refundEmailStrings {
if loader == nil {
return GetRefundEmailStrings(lang) // no loader wired (tests)
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
tpl := loader.GetStrings(ctx, "refund", lang)
if tpl == nil {
return GetRefundEmailStrings(lang) // Valkey miss or outage
}
fallback := GetRefundEmailStrings(lang)
getOr := func(key, def string) string { // per-key fallback
if v, ok := tpl[key]; ok && v != "" {
return v
}
return def
}
return refundEmailStrings{
subjectFull: getOr("subject_full", fallback.subjectFull),
headingFull: getOr("heading_full", fallback.headingFull),
// ... 20 more keys
}
}
Public builders accept the loader as a variadic trailing parameter, which keeps existing call sites and unit tests compiling unchanged while letting handlers pass h.EmailLoader:
func BuildTranslationRefundHTML(lang, docName string, /* ... */ loader ...*email.Loader) string
func BuildSubscriptionRefundHTML(lang, tier string, /* ... */ loader ...*email.Loader) string
func RefundSubject(lang string, isPartial bool, docName string, loader ...*email.Loader) string
func buildTBTSWelcomeEmailHTML(name, apiKey string, loader ...*email.Loader) string
The platform runs two distinct localization stores. They are not duplicates — they serve different processes with different constraints:
| Store | Owner | Shape | Covers |
|---|---|---|---|
email:frames | Receipt Lambda | One blob, 12 langs × 69 keys | Stripe checkout receipts — subscription, donation, LRS, webhook, plan switch |
email:tpl:* | LPO server (EmailLoader) | Six hashes, field = lang | Server-originated mail — support, refund, TBTS welcome, magic link, reactivation |
The receipt Lambda is a short-lived function outside the VPC that must assemble and enqueue an email in milliseconds, so it loads one blob and caches it for its container lifetime. The LPO server is long-running with many distinct email types, so per-template hashes let it fetch only what a given send needs.
There is a subtlety here that cost us two separate defects, and it is worth understanding precisely. The frames a receipt email needs actually live in three places that must agree: the keys the Go code asks for at send time (each frame(lang, "...") call in the receipt Lambda), the email-frames.json data file that defines them, and the Valkey email:frames blob the Lambda actually reads. Nothing in the build enforces that these three stay aligned — email:frames is a hand-maintained blob, edited and pushed by a human, not generated from the code. When a new email type ships, its keys have to be added to the data file and pushed to Valkey as separate, easily-forgotten steps.
Both failure shapes happened. First, an email family was coded and referenced but its keys were never added to the data — so the customer saw the raw key name (lifetime_heading) instead of a heading. Then, after the data file and S3 were corrected, the entire family was still missing from Valkey for all 12 languages — the file update never landed in the store the Lambda reads — so every language's email silently fell back to the binary's hardcoded English. A configured, committed, deployed set of translations was simply absent from the one place that mattered, and nothing reported it. This is the same lesson the logging layer taught: the presence of a definition says nothing about whether the running system can see it.
The guard is a reconciliation audit that reads no rendered email and instead asks the only questions that matter — does every key the code uses exist in the data, and does every language/key pair in the data exist, identical, in live Valkey? It also checks that every language defines the full key set, that translated values preserve their {name} / {refund} placeholders, and that the binary's English safety net actually covers every key the code can request. It is part of the standard hygiene sweep:
# Reconcile Go code keys ↔ email-frames.json ↔ live Valkey email:frames
bash scripts/kcc.sh frames-check
# Runs automatically as one of the five audits in:
bash scripts/kcc.sh audit-all
# 1. Edit the source JSON
vim cpmp-redesign/email-templates/refund.json
# 2. Push to S3 + Valkey (all templates, all 12 languages)
bash scripts/kcc.sh push-email-templates
# 3. Live within 5 minutes (loader cache TTL) — no redeploy
Newsletter welcome emails use SES templated sending with field values loaded from the email_translations Aurora table. Each template has 6-11 translatable fields per language (heading, subject, body, button labels, unsubscribe text).
| Template | Fields per Language | Languages | Total Rows |
|---|---|---|---|
| CPMPNewsletterWelcome | 11 | 12 | 132 |
| LPONewsletterWelcome | 6 | 12 | 72 |
AWS Translate provides real-time neural machine translation for dynamic content that cannot be pre-translated in static JSON files. It handles exactly two things in this system, both literal short-text conversion on a live request: support ticket messages (section 6) and impact map pin captions (section 7). Newsletter broadcast translation is a separate engine entirely — Bedrock, translated once at send time and stored rather than called per request — covered in section 8, not here.
| Use Case | Direction | Trigger | Caching |
|---|---|---|---|
| Support ticket (inbound) | Customer lang → English | Ticket submission (lang ≠ en) | Stored in message_en column |
| Support reply (outbound) | English → Customer lang | Admin reply (lang ≠ en) | Stored in message_translated column |
| Map pin captions | English → User's lang | Pin tap (lang ≠ en) | ElastiCache (30-day TTL) |
github.com/aws/aws-sdk-go-v2/service/translateecsTaskRole) with translate:TranslateText permission
flowchart LR
subgraph Customer["Customer (any language)"]
C1[Submits ticket in Urdu]
C2[Receives reply in Urdu]
end
subgraph Server["ECS Container"]
S1[Support Handler]
S2[translateText fn]
S3[Reply Handler]
end
subgraph AWS["AWS Services"]
T[Amazon Translate API]
EC[ElastiCache
Translation Cache]
end
subgraph Admin["Admin (English)"]
A1[Reads ticket in English]
A2[Replies in English]
end
C1 -->|"POST /support/submit"| S1
S1 -->|"ur → en"| S2
S2 --> T
S2 -->|"message_en"| A1
A2 -->|"POST /support/reply"| S3
S3 -->|"en → ur"| S2
S2 -->|"message_translated"| C2
style C1 fill:#10b981,stroke:#059669,color:#fff
style C2 fill:#10b981,stroke:#059669,color:#fff
style A1 fill:#FF9900,stroke:#cc7a00,color:#fff
style A2 fill:#FF9900,stroke:#cc7a00,color:#fff
style S1 fill:#1e293b,stroke:#334155,color:#e2e8f0
style S2 fill:#635bff,stroke:#4b44cc,color:#fff
style S3 fill:#1e293b,stroke:#334155,color:#e2e8f0
style T fill:#60a5fa,stroke:#3b82f6,color:#fff
style EC fill:#a855f7,stroke:#7c3aed,color:#fff
cpmp_site.lang in localStorage)preferred_lang read from cpmp_site.langsupport_tickets.messagepreferred_lang != "en": AWS Translate converts to English, stored in support_tickets.message_ensupport_replies.messagepreferred_lang != "en": AWS Translate converts en → preferred_lang, stored in support_replies.message_translatedFixed 2026-08-25: step 3 now branches on who actually wrote the message, not just whether the author is "not the customer." Rhema's reply payload carries author_type: "rhema", and the reply handler skips AWS Translate entirely for that author type — its draft is already written directly in the customer's language, per its own drafting prompt, so translating it again would have declared English as the source language for text that often wasn't. A human admin reply still translates en → preferred_lang exactly as before.
| Table | Column | Purpose |
|---|---|---|
support_tickets | message | Original message (customer's language) |
support_tickets | message_en | English translation (NULL if submitted in English) |
support_tickets | preferred_lang | Customer's language code (e.g., "ur", "pt") |
support_replies | message | Original reply (admin's English) |
support_replies | message_translated | Translated reply (customer's language, NULL if en) |
The Impact Map translates pin captions on-the-fly when a user taps a pin in a non-English language.
| Endpoint | Method | Body | Response |
|---|---|---|---|
/translate | POST | {"text":"...","target_lang":"ur"} | {"translated":"..."} |
The endpoint checks ElastiCache first (key: translate:{lang}:{text_hash}). On cache miss, calls AWS Translate and caches the result for 30 days. On cache hit, returns immediately with no API call.
Complete view of how language flows through every layer of The Trinity Beast communications system.
flowchart TD
subgraph User["User Browser"]
LS["localStorage
cpmp_site.lang (website)
cpmp_user.lang (dashboard)"]
DD[Flag Dropdown
Language Selector]
DD --> LS
end
subgraph Static["Static Translation Layer"]
JSON["lang/{code}.json
12 files × 1,000+ keys (S3 fallback)"]
VAPI["GET /public/lang/{code}
Valkey lang:{code} (primary)"]
I18N["i18n.js Engine v5"]
RTL["css/rtl.css
RTL Support"]
VAPI --> I18N
JSON -->|fallback| I18N
I18N --> Pages["All Pages
data-i18n attributes"]
LS --> I18N
end
subgraph Dynamic["Dynamic Translation Layer"]
API["/translate endpoint"]
EC["ElastiCache
translate:{lang}:{hash}
30-day TTL"]
AT["Amazon Translate
API"]
API --> EC
EC -->|miss| AT
AT --> EC
end
subgraph Email["Email Layer"]
SES["Amazon SES"]
ET["email_translations
Aurora Table
12 langs × 2 templates"]
GT["getSupportEmailStrings()
12 languages hardcoded"]
NT["newsletter_translations
Aurora Table
pre-translated per send"]
GT --> SES
ET --> SES
NT --> SES
AT -->|reply translation| SES
end
subgraph Bedrock["Bedrock (Qwen 3 235B)"]
RH["Rhema
drafts reply in
customer's language"]
NB["Newsletter translate
once per send,
stored not called live"]
end
subgraph Admin["TBCC Admin"]
NC["Newsletter Console
Quill Editor"]
SC["Support Console"]
NC -->|"POST /newsletter/{id}/translate"| NB
NB --> NT
SC -->|English reply| AT
RH -->|"draft, already in
target language"| SC
end
subgraph Map["Impact Map"]
Pins["Pin Captions
(English in Aurora)"]
Pins -->|tap| API
API -->|translated| Panel["Detail Panel"]
end
LS -->|preferred_lang| Email
style LS fill:#FF9900,stroke:#cc7a00,color:#fff
style DD fill:#FF9900,stroke:#cc7a00,color:#fff
style JSON fill:#10b981,stroke:#059669,color:#fff
style I18N fill:#10b981,stroke:#059669,color:#fff
style VAPI fill:#a855f7,stroke:#7c3aed,color:#fff
style AT fill:#60a5fa,stroke:#3b82f6,color:#fff
style EC fill:#a855f7,stroke:#7c3aed,color:#fff
style SES fill:#f59e0b,stroke:#d97706,color:#fff
style ET fill:#1e293b,stroke:#334155,color:#e2e8f0
style GT fill:#1e293b,stroke:#334155,color:#e2e8f0
style NT fill:#1e293b,stroke:#334155,color:#e2e8f0
style RH fill:#c026d3,stroke:#a21caf,color:#fff
style NB fill:#c026d3,stroke:#a21caf,color:#fff
style API fill:#635bff,stroke:#4b44cc,color:#fff
| Cache Layer | Key Pattern | TTL | Purpose |
|---|---|---|---|
| Valkey (ElastiCache) | lang:{code} | Permanent (no TTL) | Language JSON served by /public/lang/{code} API — updated instantly via POST /admin/lang/set without a deploy |
| ElastiCache (Valkey) | translate:{lang}:{text_hash} | 30 days | Map caption translations — avoid repeat API calls |
| Aurora (column) | support_tickets.message_en | Permanent | Inbound ticket translation — audit trail |
| Aurora (column) | support_replies.message_translated | Permanent | Outbound reply translation — audit trail |
Browser (localStorage) | cpmp_site | Permanent | Website preferences JSON object — lang (language code), theme (light/dark), lang_notice_dismissed (boolean). Written by i18n.js v5 and theme.js. |
Browser (localStorage) | cpmp_user | Session (cleared on logout) | Dashboard session JSON object — token (Bearer), email, name, roles, lang. Written on magic link auth. Lang flows from cpmp_site.lang at login time so dashboard respects the user's site language automatically. |
Browser (localStorage) | cpmp-reports-view | Permanent | Daily reports index preferences — JSON object with view (list or card) and sort (desc or asc) |
| Browser (memory) | cpmpI18n._cache | Session | Loaded JSON files — no re-fetch within session |
Legacy key migration: The old flat cpmp-lang localStorage key was replaced by cpmp_site.lang in i18n.js v5 (May 2026). On first page load after the upgrade, the engine automatically reads the old key, migrates the value into cpmp_site.lang, and removes the old key. Transparent to users — no action required.
Cost Efficiency: Each unique caption is translated only once per language. With ~100 map pins and 11 non-English languages, the maximum translation calls is ~1,100 — after which every subsequent tap is a sub-millisecond ElastiCache hit. Total one-time cost: approximately $0.02.
| Component | Monthly Cost | Notes |
|---|---|---|
| AWS Translate (support tickets) | < $0.50 | ~30 tickets/month × avg 200 chars × $15/million chars |
| AWS Translate (map captions) | < $0.05 | One-time cost, then cached. ~100 pins × 11 langs = 1,100 calls max |
| Bedrock (newsletter translation) | < $0.05 | Live, not projected — weekly send, currently one target language (Urdu, the only non-English language subscribers have chosen). Derived from real translated body sizes (~12–29 KB HTML) at the standing Qwen 3 235B rate ($0.22/1M input, $0.88/1M output tokens). Re-derive from GET /admin/translate/params rather than quoting this figure verbatim if the model or rate ever changes. |
| Static JSON files (S3 + CloudFront) | $0.00 | Included in existing S3/CloudFront costs |
| SES email sending | < $0.10 | $0.10 per 1,000 emails — well under 1,000/month |
| ElastiCache (translation cache) | $0.00 | Negligible additional memory on existing 52 GB node |
Total estimated monthly cost for full multi-lingual communications: < $2.00. Language accessibility for 12 languages, covering 600+ million native speakers, for less than the cost of a cup of coffee. This is what cloud-native architecture makes possible.