The Trinity Beast — Multi-Lingual Communications

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.

Languages: 12 Live Translation: AWS Translate (support, map) + Bedrock (newsletters) Static i18n: JSON i18n Engine (v5) Email Templates: SES + Aurora + Valkey Updated: August 2026

Table of Contents

List of Diagrams

1. Overview — Design Philosophy

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:

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.

2. Supported Languages

All 12 languages are supported by AWS Translate, ensuring both static and dynamic translation coverage.

🇺🇸
English
en
🇪🇸
Español
es
🇧🇷
Português
pt
🇫🇷
Français
fr
🇩🇪
Deutsch
de
🇷🇺
Русский
ru
🇮🇳
हिन्दी
hi
🇵🇰
اردو
ur
🇮🇹
Italiano
it
🇸🇦
العربية
ar
🇯🇵
日本語
ja
🇨🇳
中文
zh
LanguageStatic i18n KeysEmail TemplatesAWS TranslateRTL Support
English (en)1,000+All templatesSource languageNo
Spanish (es)1,000+All templatesNo
Portuguese (pt)1,000+All templatesNo
French (fr)1,000+All templatesNo
German (de)1,000+All templatesNo
Russian (ru)1,000+All templatesNo
Hindi (hi)1,000+All templatesNo
Urdu (ur)1,000+All templatesYes
Italian (it)1,000+All templatesNo
Arabic (ar)1,000+All templatesYes
Japanese (ja)1,000+All templatesNo
Chinese (zh)1,000+All templatesNo

3. Static Website Translation (JSON i18n Engine)

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.

Architecture

ComponentLocationPurpose
i18n Enginejs/i18n.jsLoads JSON, applies translations, manages language switching, browser detection, cpmp_site localStorage object
Language Files (S3)lang/{code}.json12 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 Stylesheetcss/rtl.cssRight-to-left layout for Arabic, Urdu
Flag Iconsicons/flag-{code}.svg12 SVG flags for the language dropdown
Language Dropdownincludes/header.htmlSite-wide language switcher in the header

How It Works

Key Coverage

SectionApprox. KeysPages Covered
header / footer30All pages (shared)
home80Homepage
give45Give page
subscribeListener250+Subscription page
docLibrary142Document Library
support27Support page
map25Impact Map
Impact pages (7)350+Freedom, Water, Medical, etc.
team / origin100+Team, Origin Story
optout (2)20Newsletter opt-out pages

4. Email Communication System

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.

Correspondence Language Resolution

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_langapi_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:

  1. users.preferred_lang, when the caller already has it in hand for this account
  2. newsletter_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 signup
  3. en — final fallback

Known 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 Types

Email TypeTriggerTranslation MethodSender
Subscription ReceiptStripe checkoutValkey frames (email:frames) — zero AICPMP Mission
Donation ReceiptStripe checkoutValkey frames + impact messagesCPMP Mission
LRS Add-On ReceiptStripe checkoutValkey framesCPMP Mission
Webhook ReceiptStripe checkoutValkey framesCPMP Mission
Plan Switch ConfirmationStripe subscription updateValkey frames + tier comparison tableCPMP Mission
Refund ConfirmationStripe charge refundedValkey framesCPMP Mission
Support ConfirmationTicket submittedValkey templates (email:tpl:support-confirmation)Support@CPMP-Site.org
Support ReplyAdmin replies to ticketValkey templates (email:tpl:support-reply) + AWS Translate for reply bodySupport@CPMP-Site.org
Translation RefundAdmin or customer refundValkey templates (email:tpl:refund)The Trinity Beast
TBTS WelcomeTranslation service signupValkey templates (email:tpl:tbts-welcome)CPMP Mission
Dashboard Magic LinkDashboard login requestValkey templates (email:tpl:magic-link)The Trinity Beast
ReactivationSubscription reactivatedValkey templates (email:tpl:reactivation)CPMP Mission
CPMP Newsletter WelcomeNewsletter subscriptionSES Template + email_translations tableCPMP Mission
LPO Newsletter WelcomeLPO newsletter subscriptionSES Template + email_translations tableThe Trinity Beast
Newsletter BroadcastAdmin sends newsletterTBCC 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.

Receipt Email Architecture (Zero Bedrock)

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:

  1. Receipt Lambda builds the complete localized email by reading email:frames (12 languages × 69 keys) from Valkey and combining frame labels with dynamic values (name, amount, API key, date).
  2. SQS — the finished email is enqueued to trinity-beast-email-queue (~20 ms, durable).
  3. Email-sender Lambda picks up the message (batch 6, 3s window) and delivers via SES.
  4. Customer inbox — fully localized email arrives within 3–6 seconds of checkout.

Pre-Translated Data Sources

Valkey KeyContentSource of TruthRefresh
email:frames12 langs × 69 keys (subjects, headings, labels, footers, link text)s3://trinity-beast-website-east2/data/email-frames.jsonNightly (BeastReconciler syncEmailFrames())
impact:messages12 langs × 10 impact types (personalized donation messages)s3://trinity-beast-website-east2/data/impact-messages.jsonNightly (BeastReconciler syncImpactMessages())

Language Preference Storage

Every user interaction stores the language preference:

Centralized Email Templates (Valkey 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.

Template Pipeline

S3 JSON (source of truth)kcc push-email-templatesValkey 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.

The Registry — 6 Templates × 12 Languages = 72 Entries

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 KeyKeys per LanguageConsumer
email:tpl:magic-link7Dashboard passwordless login
email:tpl:reactivation8Subscription reactivation confirmation
email:tpl:refund22Translation and subscription refund confirmations
email:tpl:support-confirmation12Ticket receipt acknowledgment
email:tpl:support-reply9Admin or Rhema reply notification
email:tpl:tbts-welcome20TBTS 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.

The Loader — internal/email/loader.go

A 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:

  1. In-memory cache — a sync.Map keyed by template:lang with a 5-minute TTL. Repeat sends in the same window never touch the network.
  2. Valkey readHGET email:tpl:{template} {lang}. On a cache miss for a non-English language, the loader automatically retries with en.
  3. Hardcoded Go fallback — if Valkey is unreachable or the template is absent entirely, the original hardcoded string functions still ship in the binary and serve the email. The customer always receives their message.

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.

Loader Integration Pattern

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

Two Template Systems, Two Owners

The platform runs two distinct localization stores. They are not duplicates — they serve different processes with different constraints:

StoreOwnerShapeCovers
email:framesReceipt LambdaOne blob, 12 langs × 69 keysStripe checkout receipts — subscription, donation, LRS, webhook, plan switch
email:tpl:*LPO server (EmailLoader)Six hashes, field = langServer-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.

Keeping the Three Stores in Sync

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

Updating Copy

# 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

Email Template Translation (Newsletters)

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).

TemplateFields per LanguageLanguagesTotal Rows
CPMPNewsletterWelcome1112132
LPONewsletterWelcome61272

5. AWS Translate Integration

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.

Integration Points

Use CaseDirectionTriggerCaching
Support ticket (inbound)Customer lang → EnglishTicket submission (lang ≠ en)Stored in message_en column
Support reply (outbound)English → Customer langAdmin reply (lang ≠ en)Stored in message_translated column
Map pin captionsEnglish → User's langPin tap (lang ≠ en)ElastiCache (30-day TTL)

Technical Implementation

Diagram 5.1 — AWS Translate Integration Flow
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

6. Support Ticket Translation Flow

Inbound (Customer → Admin)

  1. Customer selects their language via the flag dropdown (stored in cpmp_site.lang in localStorage)
  2. Support form renders in their language (static i18n)
  3. Customer writes message using Quill rich text editor
  4. Form submits with preferred_lang read from cpmp_site.lang
  5. Server stores original message in support_tickets.message
  6. If preferred_lang != "en": AWS Translate converts to English, stored in support_tickets.message_en
  7. Admin views ticket in TBCC — sees English translation + original

Outbound (Admin → Customer)

  1. Admin writes reply in English via TBCC Support Console — or Rhema (Bedrock, Qwen 3 235B) auto-drafts a reply already written directly in the customer's language, and posts it through the same reply endpoint
  2. Server stores the reply text as submitted in support_replies.message
  3. If preferred_lang != "en": AWS Translate converts en → preferred_lang, stored in support_replies.message_translated
  4. Email notification sent with the translated reply body + translated chrome (heading, labels, buttons)
  5. Customer receives email entirely in their language

Fixed 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.

Database Schema

TableColumnPurpose
support_ticketsmessageOriginal message (customer's language)
support_ticketsmessage_enEnglish translation (NULL if submitted in English)
support_ticketspreferred_langCustomer's language code (e.g., "ur", "pt")
support_repliesmessageOriginal reply (admin's English)
support_repliesmessage_translatedTranslated reply (customer's language, NULL if en)

7. Impact Map — Real-Time Caption Translation

The Impact Map translates pin captions on-the-fly when a user taps a pin in a non-English language.

Static Elements (i18n JSON)

Dynamic Elements (AWS Translate)

Translation Endpoint

EndpointMethodBodyResponse
/translatePOST{"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.

8. Newsletter System — Multi-Lingual Delivery

Welcome Emails

When a user subscribes to either newsletter (CPMP Mission or LPO), a welcome email is sent in their preferred language using SES templated sending.

Newsletter Broadcasts — Pre-Translated via Bedrock, Not AWS Translate

Newsletter content is composed in the TBCC Newsletter Console using a Quill rich text editor. The admin writes in English. Translation is a separate, explicit admin action — POST /newsletter/{id}/translate — that runs once per send, not once per recipient, and stores its output rather than calling an AI model on delivery.

  1. The English newsletter is saved as the lang='en' row in newsletter_translations.
  2. The handler reads newsletter_subscribers for the distinct non-English preferred_lang values actually in use by opted-in subscribers of that newsletter type — it never translates into a language nobody has selected. If every subscriber is English, translation is skipped entirely and nothing is sent to Bedrock.
  3. The request returns immediately (202 Accepted); translation runs in a background goroutine so the admin isn't blocked.
  4. For each target language, the subject and HTML body are translated together in a single Bedrock call (qwen.qwen3-235b-a22b-2507-v1:0) — the same model used for Rhema and AutoOps threat analysis elsewhere in this platform. The prompt preserves every HTML tag, inline style, and bgcolor attribute exactly, and protects the same brand-term list as the document translation engine (The Trinity Beast, CPMP, exchange names, AWS service names, node names).
  5. Each translated result is stored as its own row in newsletter_translations (auto_translated=true, reviewed=false).
  6. At send time, NewsletterSendHandler loads every stored translation into memory once and picks each recipient's row by their preferred_lang, falling back to the English row if their language was never translated. Zero AI calls happen at send time — the same pre-translate-and-store pattern the rest of this document describes for email.

Right-sized for real usage. As of this writing, non-English newsletter subscribers are a small, entirely Urdu-speaking group — translation runs into exactly the languages subscribers have actually chosen, which today means one language, not all eleven. The mechanism scales to more languages the moment subscribers in those languages exist; it does not spend Bedrock tokens translating into languages with zero recipients.

Opt-Out Pages

Both newsletter opt-out pages (newsletter-optout-lpo.html and newsletter-optout-cpmp.html) are fully multi-lingual via the static i18n engine. The user sees the unsubscribe confirmation in their selected language.

9. Full Architecture Diagram

Complete view of how language flows through every layer of The Trinity Beast communications system.

Diagram 9.1 — Full Multi-Lingual Communications Architecture
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

10. Translation Caching Strategy

Cache LayerKey PatternTTLPurpose
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 daysMap caption translations — avoid repeat API calls
Aurora (column)support_tickets.message_enPermanentInbound ticket translation — audit trail
Aurora (column)support_replies.message_translatedPermanentOutbound reply translation — audit trail
Browser (localStorage)cpmp_sitePermanentWebsite preferences JSON object — lang (language code), theme (light/dark), lang_notice_dismissed (boolean). Written by i18n.js v5 and theme.js.
Browser (localStorage)cpmp_userSession (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-viewPermanentDaily reports index preferences — JSON object with view (list or card) and sort (desc or asc)
Browser (memory)cpmpI18n._cacheSessionLoaded 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.

11. Cost Analysis

ComponentMonthly CostNotes
AWS Translate (support tickets)< $0.50~30 tickets/month × avg 200 chars × $15/million chars
AWS Translate (map captions)< $0.05One-time cost, then cached. ~100 pins × 11 langs = 1,100 calls max
Bedrock (newsletter translation)< $0.05Live, 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.00Included 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.00Negligible 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.