DEV Community

Cover image for Building a CMS Translation Pipeline: Developer's Guide to i18n Architecture
Diogo Heleno
Diogo Heleno

Posted on • Originally published at m21global.com

Building a CMS Translation Pipeline: Developer's Guide to i18n Architecture

Building a CMS Translation Pipeline: Developer's Guide to i18n Architecture

While project managers focus on workflow coordination, developers face the technical challenge of building systems that handle multilingual content efficiently. A well-architected translation pipeline reduces manual work, prevents data loss, and scales with your content volume.

This guide covers the technical implementation side: API integrations, automated workflows, and database design patterns that make CMS localization manageable for development teams.

Database Schema Considerations for Multilingual Content

Your database design determines how smoothly translations flow through your system. Two main patterns dominate CMS internationalization:

Separate tables per language (WordPress WPML approach):

CREATE TABLE posts_en (
  id INT PRIMARY KEY,
  title VARCHAR(255),
  content TEXT,
  slug VARCHAR(255)
);

CREATE TABLE posts_es (
  id INT PRIMARY KEY,
  title VARCHAR(255),
  content TEXT,
  slug VARCHAR(255),
  source_id INT -- references posts_en.id
);
Enter fullscreen mode Exit fullscreen mode

Single table with language columns (more common in headless CMS):

CREATE TABLE posts (
  id INT PRIMARY KEY,
  language_code VARCHAR(5),
  title VARCHAR(255),
  content TEXT,
  slug VARCHAR(255),
  translation_group_id INT
);
Enter fullscreen mode Exit fullscreen mode

The single-table approach scales better with multiple languages and simplifies queries, but requires careful indexing on language_code and translation_group_id.

Automated Export/Import Workflows

Manual file exports create bottlenecks. Most translation management systems (TMS) offer APIs that integrate directly with your CMS.

Contentful + Phrase Integration

// Export content for translation
const contentful = require('contentful-management');
const phrase = require('phrase-api');

async function exportForTranslation(entryId, targetLocale) {
  const entry = await contentfulClient.getEntry(entryId);

  // Extract translatable fields
  const translatable = {
    title: entry.fields.title['en-US'],
    body: entry.fields.body['en-US'],
    metaDescription: entry.fields.metaDescription['en-US']
  };

  // Create translation job in Phrase
  const job = await phraseClient.createJob({
    name: `Entry ${entryId} - ${targetLocale}`,
    sourceLocale: 'en',
    targetLocales: [targetLocale],
    content: translatable
  });

  return job.id;
}
Enter fullscreen mode Exit fullscreen mode

Strapi Custom Plugin

Strapi's plugin system lets you build translation workflows directly into the admin interface:

// strapi-plugin-translations/server/controllers/translation.js
module.exports = {
  async exportContent(ctx) {
    const { contentType, id, targetLocale } = ctx.request.body;

    const entity = await strapi.entityService.findOne(
      contentType, 
      id, 
      { populate: '*' }
    );

    // Generate XLIFF format
    const xliff = generateXLIFF(entity, targetLocale);

    // Send to translation service
    const jobId = await translationService.createJob(xliff);

    ctx.body = { success: true, jobId };
  }
};
Enter fullscreen mode Exit fullscreen mode

Handling Complex Content Structures

Modern CMS platforms use nested objects, arrays, and references that don't translate cleanly to flat key-value pairs.

JSON Field Translation

// Original content
const content = {
  hero: {
    title: "Welcome to our platform",
    subtitle: "Build amazing applications",
    cta: { text: "Get Started", url: "/signup" }
  },
  features: [
    { name: "Fast", description: "Lightning quick" },
    { name: "Secure", description: "Bank-grade security" }
  ]
};

// Flatten for translation
function flattenForTranslation(obj, prefix = '') {
  const flattened = {};

  Object.keys(obj).forEach(key => {
    const value = obj[key];
    const newKey = prefix ? `${prefix}.${key}` : key;

    if (typeof value === 'string') {
      flattened[newKey] = value;
    } else if (Array.isArray(value)) {
      value.forEach((item, index) => {
        Object.assign(flattened, flattenForTranslation(item, `${newKey}.${index}`));
      });
    } else if (typeof value === 'object') {
      Object.assign(flattened, flattenForTranslation(value, newKey));
    }
  });

  return flattened;
}
Enter fullscreen mode Exit fullscreen mode

API Design for Multilingual Content

Your API structure affects how frontend applications consume translated content. Consider language-aware endpoints:

// Language-specific routes
app.get('/api/:lang/posts', getPosts);
app.get('/api/:lang/posts/:slug', getPost);

// Or header-based
app.get('/api/posts', (req, res) => {
  const lang = req.headers['accept-language'] || 'en';
  const posts = getPostsByLanguage(lang);
  res.json(posts);
});

// GraphQL with locale argument
const typeDefs = `
  type Query {
    posts(locale: String = "en"): [Post]
    post(slug: String!, locale: String = "en"): Post
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    slug: String!
    locale: String!
  }
`;
Enter fullscreen mode Exit fullscreen mode

Translation Memory Integration

Translation memories (TM) reduce costs by reusing previous translations. Most TMS platforms provide APIs to query existing translations:

async function checkTranslationMemory(sourceText, sourceLang, targetLang) {
  const response = await fetch(`${TM_API_URL}/matches`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${TM_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      source_text: sourceText,
      source_language: sourceLang,
      target_language: targetLang,
      min_match_percentage: 85
    })
  });

  const matches = await response.json();
  return matches.length > 0 ? matches[0].target_text : null;
}
Enter fullscreen mode Exit fullscreen mode

Webhook-Driven Updates

Set up webhooks to automatically import completed translations:

// Express webhook handler
app.post('/webhooks/translation-complete', async (req, res) => {
  const { jobId, targetLocale, translations } = req.body;

  try {
    // Validate webhook signature
    if (!validateSignature(req)) {
      return res.status(401).send('Invalid signature');
    }

    // Import translations back to CMS
    await importTranslations(jobId, targetLocale, translations);

    // Trigger cache invalidation
    await invalidateCache(`/api/${targetLocale}/*`);

    res.status(200).send('OK');
  } catch (error) {
    console.error('Translation import failed:', error);
    res.status(500).send('Import failed');
  }
});
Enter fullscreen mode Exit fullscreen mode

Performance Considerations

Multilingual sites can quickly become slow without proper optimization:

  • Database indexing: Index on language_code and translation_group_id
  • CDN configuration: Serve language-specific content from edge locations
  • Lazy loading: Only load the active language's content
  • Caching strategy: Cache per language and invalidate selectively
// Redis caching by language
const cacheKey = `posts:${language}:${page}`;
const cached = await redis.get(cacheKey);

if (!cached) {
  const posts = await db.getPosts({ language, page });
  await redis.setex(cacheKey, 3600, JSON.stringify(posts));
  return posts;
}

return JSON.parse(cached);
Enter fullscreen mode Exit fullscreen mode

Testing Multilingual Features

Automated testing becomes crucial with multiple languages:

// Jest test for translation endpoints
describe('Multilingual API', () => {
  test('returns content in requested language', async () => {
    const response = await request(app)
      .get('/api/posts')
      .set('Accept-Language', 'es');

    expect(response.status).toBe(200);
    expect(response.body[0].locale).toBe('es');
    expect(response.body[0].title).not.toContain('Hello'); // English word
  });

  test('falls back to default language', async () => {
    const response = await request(app)
      .get('/api/posts')
      .set('Accept-Language', 'unsupported-lang');

    expect(response.body[0].locale).toBe('en');
  });
});
Enter fullscreen mode Exit fullscreen mode

Next Steps

The technical foundation described here supports the project management practices outlined in M21Global's CMS localization guide. Focus on building automated workflows early. Manual processes don't scale, and technical debt in internationalization systems is expensive to fix later.

Start with a solid database schema, add API endpoints that handle language parameters cleanly, and integrate with translation management platforms through webhooks rather than file uploads. Your future self (and your project managers) will thank you.

Top comments (0)