Skip to content

i18n Build Strategies — Multi-Build and Single-Build Approaches

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about i18n build strategies. We cover key concepts, practical examples, and best practices to help you master this topic.

i18n build strategies include multi-build (separate deployment per locale) and single-build (runtime locale switching) approaches for multilingual apps.

What You'll Learn

By the end of this tutorial, you'll understand the trade-offs between multi-build and single-build strategies, how to implement each approach, when to use subdomain vs path-based builds, and how to optimize build performance for multiple locales.

Why It Matters

The build Strategy determines your deployment complexity, build time, CDN Caching behavior, and runtime performance. A single-build approach is simpler but serves unused translations to every user. A multi-build approach optimizes per-locale delivery but increases build and deployment complexity. The right choice depends on your number of locales, page count, and performance requirements.

Real-World Use

A documentation site with 8 languages uses multi-build strategy. Each locale is a separate build with only that locale's translations embedded. CDN caching is per-locale, and users never download translations for other languages. Building 8 locales takes 8 minutes (1 minute each) and can be parallelized. The total output is 8 independent sites, each optimized for one language.

Strategy Comparison

graph LR
    A[i18n Build
Strategies] --> B[Single Build
Runtime locale switching] A --> C[Multi Build
Separate build per locale] A --> D[Hybrid
Single build, lazy locales] B --> E[One deployment] B --> F[All translations in bundle] B --> G[Simple infrastructure] C --> H[One deployment per locale] C --> I[Only current locale's data] C --> J[Optimal per-locale perf] D --> K[One deployment] D --> L[Lazy load translations] D --> M[Good perf + simple infra] style A fill:#4a90d9,color:#fff style C fill:#27ae60,color:#fff style D fill:#f39c12,color:#fff

Multi-Build Strategy

// build/i18n-multi-build.js — Multi-build strategy implementation
const fs = require('fs-extra');
const path = require('path');
const { execSync } = require('child_process');
const locales = ['en', 'es', 'fr', 'de', 'ar', 'ja'];
const baseUrl = 'https://example.com';

async function buildLocale(locale) {
    console.log(`Building locale: ${locale}`);

    // Set environment variables for this locale
    process.env.LOCALE = locale;
    process.env.BASE_URL = `${baseUrl}/${locale}`;

    // Run the build command (e.g., Next.js, Vite, Webpack)
    execSync('npm run build', {
        env: {
            ...process.env,
            NEXT_PUBLIC_LOCALE: locale,
            NEXT_PUBLIC_BASE_URL: `${baseUrl}/${locale}`
        },
        stdio: 'inherit'
    });

    // Move output to locale-specific directory
    const outputDir = path.join('dist', locale);
    await fs.move('out', outputDir, { overwrite: true });

    // Generate locale-specific sitemap
    await generateSitemap(locale, outputDir);

    // Generate locale-specific robots.txt
    await fs.writeFile(
        path.join(outputDir, 'robots.txt'),
        generateRobots(locale)
    );

    console.log(`Completed locale: ${locale}`);
}

async function generateSitemap(locale, outputDir) {
    const urls = [
        `${baseUrl}/${locale}/`,
        `${baseUrl}/${locale}/about`,
        `${baseUrl}/${locale}/products`,
        `${baseUrl}/${locale}/contact`
    ];

    const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls.map(url => `  <url><loc>${url}</loc></url>`).join('\n')}
</urlset>`;

    await fs.writeFile(path.join(outputDir, 'sitemap.xml'), sitemap);
}

function generateRobots(locale) {
    return `User-agent: *
Allow: /
Sitemap: ${baseUrl}/${locale}/sitemap.xml`;
}

// Build all locales (in parallel)
async function buildAll() {
    await Promise.all(locales.map(buildLocale));
    console.log('All locales built successfully');
}

// Build all locales with progress
async function buildAllSequential() {
    for (const locale of locales) {
        const start = Date.now();
        await buildLocale(locale);
        const elapsed = ((Date.now() - start) / 1000).toFixed(1);
        console.log(`Built ${locale} in ${elapsed}s`);
    }
}

// Generate root index page that redirects based on Accept-Language
async function generateRootIndex() {
    const html = `<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Example</title>
    <script>
        const locales = ${JSON.stringify(locales)};
        const lang = navigator.language.split('-')[0];
        const matchedLang = locales.includes(lang) ? lang : 'en';
        window.location.href = '/' + matchedLang + '/';
    </script>
</head>
<body>
    <a href="/en/">English</a>
    <a href="/es/">Espanol</a>
    <a href="/fr/">Francais</a>
</body>
</html>`;

    await fs.writeFile(path.join('dist', 'index.html'), html);
}

// Run: node build/i18n-multi-build.js

Single-Build Strategy

// build/i18n-single-build.js — Single-build with runtime locale switching
const fs = require('fs-extra');
const path = require('path');

// Webpack/Vite config for single build
// All translations are bundled, locale is determined at runtime

/*
Webpack config:
module.exports = {
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js',
    },
    plugins: [
        // i18n plugin that includes all translation files
        new I18nPlugin({
            locales: ['en', 'es', 'fr', 'de', 'ar', 'ja'],
            // Include ALL translations in bundle
            bundleAllLocales: true
        })
    ]
};
*/

// For Vite:
// vite.config.js
// import { defineConfig } from 'vite';
// export default defineConfig({
//     plugins: [
//         {
//             name: 'i18n-include-all',
//             transform(code, id) {
//                 // Transform to include all locale files
//                 // This is simplified — real implementation uses i18n plugins
//                 return code;
//             }
//         }
//     ]
// });

// Single build output structure:
// dist/
//   index.html
//   bundle.js       (includes all translations)
//   styles.css
//   locales/
//     en.json
//     es.json
//     fr.json       (can be lazy loaded)
//     ...

async function singleBuild() {
    console.log('Building single output for all locales');

    // Build once — all locales included
    execSync('npm run build', { stdio: 'inherit' });

    // Generate root-level sitemap with hreflang entries
    await generateSitemap();

    console.log('Single build complete');
}

async function generateSitemap() {
    const locales = ['en', 'es', 'fr', 'de', 'ar', 'ja'];
    const baseUrl = 'https://example.com';

    // Each URL has hreflang alternates for all locales
    const pages = ['/', '/about', '/products', '/contact'];

    const entries = pages.map(page => {
        const alternates = locales.map(locale =>
            `        <xhtml:link rel="alternate" hreflang="${locale}" href="${baseUrl}/${locale}${page}" />`
        ).join('\n');

        return `    <url>
        <loc>${baseUrl}/en${page}</loc>
${alternates}
        <xhtml:link rel="alternate" hreflang="x-default" href="${baseUrl}/en${page}" />
    </url>`;
    }).join('\n');

    const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:xhtml="http://www.w3.org/1999/xhtml">
${entries}
</urlset>`;

    await fs.writeFile(path.join('dist', 'sitemap.xml'), sitemap);
}

Hybrid Strategy (Lazy Locales)

// build/i18n-hybrid.js — Hybrid strategy with lazy loaded locales

// Build configuration:
// 1. Common bundle (core app code) — built once
// 2. Per-locale translation chunks — built per locale
// 3. Locale chunks are loaded at runtime based on detection

// Webpack config example:
// {
//     entry: './src/index.js',
//     output: {
//         filename: 'bundle.js',
//     },
//     plugins: [
//         new webpack.ProvidePlugin({
//             // Core i18n library in main bundle
//         })
//     ]
// }

// Translation chunk loading:
// Each locale is a separate async chunk loaded on demand
// /locales/en.chunk.js
// /locales/es.chunk.js
// /locales/fr.chunk.js

async function buildHybrid() {
    console.log('Building hybrid: common + locale chunks');

    // 1. Build common bundle (core app)
    execSync('npm run build:core', { stdio: 'inherit' });

    // 2. Build locale chunks in parallel
    const locales = ['en', 'es', 'fr', 'de', 'ar', 'ja'];
    await Promise.all(locales.map(async (locale) => {
        execSync(`npm run build:locale -- --locale=${locale}`, {
            stdio: 'inherit'
        });
    }));

    console.log('Hybrid build complete');
}

Deployment Strategies

// deploy/i18n-deploy.js — Deployment strategies for multi-locale builds

// Strategy 1: Separate deployments (multi-build)
// Each locale is deployed independently
// en.example.com → build/en/
// es.example.com → build/es/
// Or:
// example.com/en/ → build/en/  (path-based on same CDN)

// Strategy 2: Single deployment (single-build or hybrid)
// All locales served from one deployment
// Runtime code handles locale routing

// Strategy 3: CDN with lambda@edge (for SSR locales)
// CloudFront + Lambda@Edge to detect locale and serve correct build

async function deployMultiBuild(buildDir, locales) {
    for (const locale of locales) {
        console.log(`Deploying locale: ${locale}`);

        // Deploy to locale-specific path
        // execSync(`aws s3 sync ${buildDir}/${locale} s3://bucket/${locale}/ --delete`, {
        //     stdio: 'inherit'
        // });

        // Or with Netlify:
        // execSync(`npx netlify-cli deploy --dir=${buildDir}/${locale} --alias=${locale}`);
    }
}

async function deploySingleBuild(buildDir) {
    console.log('Deploying single build');

    // Single deployment — locale resolution happens at runtime
    // execSync(`aws s3 sync ${buildDir} s3://bucket/ --delete`, {
    //     stdio: 'inherit'
    // });
}

Common Mistakes

  1. Using single-build for 20+ locales. Bundling all translations for 20+ languages creates massive JS bundles. Use multi-build or hybrid strategies to keep per-locale payload small.
  2. Not parallelizing multi-builds. Building 10 locales sequentially takes 10x the build time. Use Promise.all or CI matrix builds to build locales in parallel.
  3. Forgetting locale-specific sitemaps and robots.txt. Each locale build needs its own sitemap.xml and robots.txt. Without them, search engines can't discover all locale versions.
  4. Inconsistent base URLs between build and deployment. The base URL used during build must match the deployment URL. A locale built for /en/ but deployed to /fr/ will have broken hreflang, canonical, and asset paths.
  5. Not testing the root URL redirect. The root domain (example.com) must redirect users to the correct locale. Test with different browser languages to ensure the redirect works correctly.

Practice Questions

  1. What are the trade-offs between single-build and multi-build strategies?
  2. How does the hybrid strategy balance build complexity and performance?
  3. Why would you choose multi-build for sites with 20+ locales?
  4. How do CDN caching strategies differ between single-build and multi-build?
  5. How do you handle locale-specific SEO metadata in a multi-build setup?

Challenge: Given a site with 12 locales and 500 pages each, design and implement a build strategy. Create benchmarks for single-build, multi-build (parallel), and hybrid approaches. Measure: total build time, per-locale JS bundle size, CDN storage requirements, and deployment time for each strategy.

FAQ

Which build strategy is best for performance?

Multi-build is best for performance. Each user only downloads the translations and assets for their locale. Single-build serves all translations to all users, increasing bundle size by ~2-5KB per additional locale.

Can I use a CDN with multi-build?

Yes, and it's the recommended approach. Each locale is a separate directory on the CDN. Cache invalidation is per-locale, so updating one language doesn't affect others.

How do I handle SSR with multi-build?

Each locale needs its own server instance (or a single server that determines locale from the URL path). Use environment variables per locale build to set the correct base URL, locale, and translations.

Does multi-build work with static site generators?

Yes. Run the build once per locale with the locale as a build argument. Output each locale to a separate directory. Deploy all locales under the same domain with path-based routing.

How do I handle locale-specific API endpoints?

Include the locale in the API URL: /api/en/products. The server reads the locale from the URL and returns localized data. Multi-build doesn't affect API structure — it only affects the frontend build.

Mini Project

Build both a single-build and multi-build version of a multilingual static site with 4 locales and 10 pages each. Compare: build time, output size per locale, total storage, deployment complexity, and runtime performance (bundle size, page load time). Document which strategy is better for different scale scenarios.

What's Next

You've mastered i18n build strategies. Next, learn about i18n Testing for testing internationalized applications.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro