Frontend Implementation of the Translation Management System

2024年12月12日 作成者 モリッツ・トーマス

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.

Architecture Overview

Runtime vs Build-time Integration

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:

  1. Translation updates require new deployments

  2. Changes can take weeks to propagate through development cycles

  3. Engineering resources are needed for routine translation updates

  4. Version control becomes complex with multiple translation branches

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

Import Graph Analysis

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

Developer Workflow

Automatic Key Detection

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:

  1. Automatically includes keys from shared components

  2. Handles dynamic imports correctly

  3. Optimizes loading by only including necessary keys

  4. Scales efficiently with application size

Local Development Flow

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"
  );
}

Production Deployment

Moving translations to production is seamless:

  1. Developers can test translations locally first

  2. AI translation features can be used for initial translations

  3. Import/export functionality moves translations to production

  4. No deployment needed - changes are live within minutes

Technical Implementation

Translation Provider Setup

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>
  );
}

Dynamic Content and Formatting

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(),
  }}
/>

Performance Considerations

Optimized Loading Pattern

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

Conclusion

This frontend implementation achieves several key goals:

  1. Minimal developer overhead through automation

  2. Optimal performance through smart key mapping

  3. Immediate translation updates without deployments

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

コメント

まだコメントはありません。最初にコメントを:

© 2024年 モリッツ・トーマス