@plantagoai/i18n
Multi-language support combining two patterns: UI translations (i18next, static, bundled) and content translations (Gemini AI, dynamic, lazy-cached in Firestore).
Consumers: HerbPulse (extracted), MarketHub
Installation
"@plantagoai/i18n": "file:../../shared/packages/i18n"
Exports
| Entry Point | Description |
|---|---|
@plantagoai/i18n |
Client-side: i18next setup, React hooks, RTL support |
@plantagoai/i18n/middleware |
Server-side: Gemini content translation, batch translate, audit |
Peer Dependencies
| Dependency | Required For |
|---|---|
i18next |
UI translations |
react-i18next |
React integration |
react |
React hooks |
firebase-admin |
Firestore translation cache |
@google/generative-ai |
AI content translation |
Pattern A: UI Translations (Client)
Static translations bundled with the app. Uses i18next under the hood.
Initialize
import { initI18n } from "@plantagoai/i18n";
await initI18n({
fallbackLang: "en",
supported: ["en", "fr", "he", "ar", "es", "de", "pt", "it", "ro", "zh"],
rtlLanguages: ["he", "ar"],
namespace: "common",
resources: {
en: { greeting: "Hello", vote: "Vote", proposal: "Proposal" },
fr: { greeting: "Bonjour", vote: "Voter", proposal: "Proposition" },
he: { greeting: "שלום", vote: "הצבע", proposal: "הצעה" },
ar: { greeting: "مرحبا", vote: "صوّت", proposal: "اقتراح" },
},
});
React Usage
import { useTranslation } from "@plantagoai/i18n";
function VoteButton() {
const { t } = useTranslation();
return <button>{t("vote")}</button>;
}
Language Management Hook
import { useLanguage } from "@plantagoai/i18n";
function LanguagePicker() {
const { language, setLanguage, dir, isRTL, supported } = useLanguage();
return (
<select value={language} onChange={(e) => setLanguage(e.target.value)}>
{supported.map((lang) => (
<option key={lang} value={lang}>{lang}</option>
))}
</select>
);
}
RTL Direction
setDirection() is called automatically when the language changes. It sets document.dir and document.documentElement.lang:
import { setDirection } from "@plantagoai/i18n";
setDirection("he"); // sets dir="rtl", lang="he"
setDirection("en"); // sets dir="ltr", lang="en"
I18nConfig
| Field | Type | Default | Description |
|---|---|---|---|
fallbackLang |
string |
"en" |
Fallback language code |
supported |
string[] |
required | Supported language codes |
rtlLanguages |
string[] |
[] |
Languages that use RTL direction |
resources |
Record<string, Record<string, string>> |
{} |
Translation key-value pairs per language |
namespace |
string |
"translation" |
i18next namespace |
LanguageInfo
interface LanguageInfo {
language: string;
setLanguage: (lang: string) => Promise<void>;
dir: "ltr" | "rtl";
isRTL: boolean;
supported: string[];
}
Pattern B: Content Translation (Server)
AI-powered translation using Gemini, with Firestore caching. Designed for large content blocks (product descriptions, articles, herb monographs) where static JSON bundles aren't practical.
Translate Single Content
import { translateContent } from "@plantagoai/i18n/middleware";
const translated = await translateContent(db, "Hello and welcome to Foundation", "fr", {
sourceLang: "en", // default: "en"
cacheCollection: "translations", // Firestore collection for cache
forceRefresh: false, // bypass cache
});
// "Bonjour et bienvenue sur Foundation"
Translation flow:
- Hash the source text
- Check Firestore cache (
translations/{hash}/fr) - If cached, return cached translation
- If not, call Gemini to translate
- Store in Firestore cache
- Return translated text
Batch Translate Collection
Translate specific fields across all documents in a collection:
import { batchTranslate } from "@plantagoai/i18n/middleware";
const count = await batchTranslate(db, "products", ["name", "description"], "fr", {
sourceLang: "en",
batchSize: 50, // docs per batch
});
// Returns number of documents translated
Each document gets a _translations map field:
{
"name": "Widget",
"description": "A great widget",
"_translations": {
"fr": { "name": "Gadget", "description": "Un super gadget" },
"he": { "name": "וידג'ט", "description": "וידג'ט מעולה" }
}
}
Audit Missing Translations
Scan for missing translations across configured languages:
import { auditTranslations } from "@plantagoai/i18n/middleware";
const report = await auditTranslations(db, "products", ["name", "description"], ["fr", "he", "ar"]);
console.log(`Total gaps: ${report.total}`);
for (const gap of report.missing) {
console.log(`${gap.docId}: missing ${gap.field} in ${gap.language}`);
}
AuditReport
interface AuditReport {
total: number;
missing: TranslationGap[];
}
interface TranslationGap {
docId: string;
field: string;
language: string;
}
Current Project Usage
| Project | Pattern A (UI) | Pattern B (Content) | Languages |
|---|---|---|---|
| HerbPulse | i18next, 10 languages, RTL | Herb monographs via Gemini | en, fr, he, ar, es, de, pt, it, ro, zh |
| MarketHub | Planned | Product names/descriptions via Gemini | Dynamic per tenant |