Modern web applications need to balance efficient translation management with excellent developer experience. This article explores how our Translation Management System achieves this through automated key management, runtime integration, and optimized loading patterns.
Traditional translation systems often rely on build-time integration, where translations are compiled into the application bundle. While this approach is simple, it creates significant operational overhead:
Translation updates require new deployments
Changes can take weeks to propagate through development cycles
Engineering resources are needed for routine translation updates
Version control becomes complex with multiple translation branches
Coordination between translators and engineering teams creates bottlenecks
Our system takes a different approach by using runtime integration. This means translations are loaded dynamically during page generation:
// Loading translations in getStaticProps
export async function getStaticProps({ locale }) {
const translations = await fetchUIStrings(
locale,
translationKeyMappings[`pages\\page\\[variants]\\[pageSlug].tsx`]
);
return {
props: {
messages: translations,
// ... other props
},
revalidate: 300 // Revalidate every 5 minutes
};
}
This runtime approach offers several advantages:
Translation updates are immediately available without deployments
Content managers can work independently of engineering teams
Changes propagate within minutes instead of weeks
The system remains flexible for future optimization
No git repository management for translation files
Clear separation between code and content
A key innovation in our system is the automatic detection of translation keys through static analysis. This process uses two main tools:
1. Skott for import graph generation:
const skott = await import("skott");
const { getStructure } = await skott.default({
entrypoint,
cwd: process.cwd(),
});
const { files } = getStructure();
2. Babel parser for AST analysis:
import { parse } from "@babel/parser";
import traverse from "@babel/traverse";
const ast = parse(content, {
sourceType: "module",
plugins: ["jsx", "typescript"],
});
traverse(ast, {
// For <FormattedMessage id="..."> pattern
JSXOpeningElement(path) {
if (t.isJSXIdentifier(path.node.name, { name: "FormattedMessage" })) {
const idAttribute = path.node.attributes.find(
(attr) => t.isJSXAttribute(attr) &&
t.isJSXIdentifier(attr.name, { name: "id" })
);
if (idAttribute && t.isStringLiteral(idAttribute.value)) {
keys.push(idAttribute.value.value);
}
}
},
// For intl.formatMessage({ id: "..." }) pattern
CallExpression(path) {
if (
t.isMemberExpression(path.node.callee) &&
t.isIdentifier(path.node.callee.object, { name: "intl" }) &&
t.isIdentifier(path.node.callee.property, { name: "formatMessage" })
) {
const firstArg = path.node.arguments[0];
if (t.isObjectExpression(firstArg)) {
const idProperty = firstArg.properties.find(
(prop) => t.isObjectProperty(prop) &&
t.isIdentifier(prop.key, { name: "id" })
);
if (idProperty && t.isStringLiteral(idProperty.value)) {
keys.push(idProperty.value.value);
}
}
}
},
});
This script:
Follows the complete import graph of each page
Detects both static and dynamic imports
Identifies translation keys in both JSX and JavaScript
Creates an optimal mapping of pages to required keys
The system generates a comprehensive mapping of pages to translation keys:
export const translationKeyMappings: Record<string, string[]> = {
"pages\\blog\\[slug].tsx": [
"blog-post-backButton",
"blog-subtitle-dataAndAuthor",
"comments-headline",
"comments-submit-button",
// ... other keys used in the blog template
],
"pages\\page\\[variants]\\[pageSlug].tsx": [
"contact-title",
"copyright",
"aria-link-home",
// ... other keys used in the page template
]
};
This mapping is crucial because it:
Automatically includes keys from shared components
Handles dynamic imports correctly
Optimizes loading by only including necessary keys
Scales efficiently with application size
The development workflow is streamlined through automation:
1. Developers use translation keys in components:
// Using FormattedMessage component
<Typography variant="h2">
<FormattedMessage id="comments-headline" />
</Typography>
// Using useIntl hook
const intl = useIntl();
const label = intl.formatMessage({ id: "comments-author-textfield-label" });
2. The key mapping script automatically detects usage.
3. New keys are automatically added to the local CMS:
export async function addNewTranslations() {
const existingUIStrings = await fetchAllUIStrings();
const existingIds = new Set(existingUIStrings.map((str) => str.id));
const allTranslationIds = new Set<string>();
for (const keys of Object.values(translationKeyMappings)) {
for (const key of keys) allTranslationIds.add(key);
}
const newTranslationIds = [...allTranslationIds]
.filter((id) => !existingIds.has(id));
for (const id of newTranslationIds) {
await createUIString({ id });
console.log(`Added translation: ${id}`);
}
console.log(
"Access new translations at: " +
"http://localhost:3001/admin/collections/ui-strings?" +
"where[text][exists]=false"
);
}
Moving translations to production is seamless:
Developers can test translations locally first
AI translation features can be used for initial translations
Import/export functionality moves translations to production
No deployment needed - changes are live within minutes
The system uses react-intl for translation rendering:
// _app.tsx - Application-wide translation provider
import { IntlProvider } from "react-intl";
function MyApp({ Component, pageProps, router }) {
return (
<IntlProvider
locale={router.locale ?? defaultLocale}
defaultLocale={defaultLocale}
messages={pageProps.messages}
>
<Layout>
<Component {...pageProps} />
</Layout>
</IntlProvider>
);
}
The system supports rich content formatting:
1. Basic variable substitution:
<FormattedMessage
id="welcome-message"
values={{
name: userName,
}}
/>
2. Rich text formatting:
<FormattedMessage
id="dot-calendar-rewards"
values={{
strong: (chunks) => <strong>{chunks}</strong>,
rewards: rewardCount,
}}
/>
3. Pluralization and date formatting through format.js:
<FormattedMessage
id="items-count"
values={{
count: items.length,
date: new Date(),
}}
/>
The key mapping system provides several performance benefits:
1. Efficient Loading
Only loads translations needed for each page
Includes keys from dynamic imports proactively
Supports code splitting without overhead
2. Caching Strategy
// Page component with optimized caching
export async function getStaticProps({ locale }) {
const translations = await fetchUIStrings(
locale,
translationKeyMappings[`pages\\${page}.tsx`]
);
return {
props: { messages: translations },
revalidate: 300, // 5-minute revalidation
};
}
3. CDN Integration
Works seamlessly with Next.js ISR
Supports global CDN distribution
Maintains high cache hit rates
This frontend implementation achieves several key goals:
Minimal developer overhead through automation
Optimal performance through smart key mapping
Immediate translation updates without deployments
Flexibility for future optimization
The combination of runtime integration and static analysis provides the best of both worlds - the immediacy of runtime updates with the robustness of build-time optimization. This approach has proven successful in production, significantly reducing both development effort and time-to-market for translation updates.
The system's architecture demonstrates that with careful design, it's possible to create a translation management system that scales efficiently while maintaining an excellent developer experience. The key innovations - automatic key mapping and runtime integration - solve the two biggest challenges in translation management: development overhead and deployment friction.
Noch keine Kommentare, sei der Erste:
© 2024 von Moritz Thomas