Creation:2026-03-23Last update:2026-05-06

    Translate your Vite and Vanilla JS website using Intlayer | Internationalisation (i18n)

    ide.intlayer.org

    Table of Contents

    What is Intlayer?

    Intlayer is an innovative, open-source internationalisation (i18n) library designed to simplify multilingual support in modern web applications.

    With Intlayer, you can:

    • Easily manage translations using declarative dictionaries at the component level.
    • Dynamically localise metadata, routes, and content.
    • Ensure TypeScript support with autogenerated types, improving autocompletion and error detection.
    • Benefit from advanced features, like dynamic locale detection and switching.

    Step-by-Step Guide to Set Up Intlayer in a Vite and Vanilla JS Application

    Step 1: Install Dependencies

    Install the necessary packages using npm:

    bash
    npm install intlayer vanilla-intlayernpm install vite-intlayer --save-devnpx intlayer init
    • intlayer The core package that provides internationalisation tools for configuration management, translation, content declaration, transpilation, and CLI commands.

    • vanilla-intlayer The package that integrates Intlayer with plain JavaScript / TypeScript applications. It provides a pub/sub singleton (IntlayerClient) and callback-based helpers (useIntlayer, useLocale, etc.) so any part of your app can react to locale changes without depending on a UI framework.

    • vite-intlayer Includes the Vite plugin for integrating Intlayer with the Vite bundler, as well as middleware for detecting the user's preferred locale, managing cookies, and handling URL redirection.

    Step 2: Configuration of your project

    Create a config file to configure the languages of your application:

    intlayer.config.ts
    import { Locales, type IntlayerConfig } from "intlayer";
    
    const config: IntlayerConfig = {
      internationalization: {
        locales: [
          Locales.ENGLISH,
          Locales.FRENCH,
          Locales.SPANISH,
          // Your other locales
        ],
        defaultLocale: Locales.ENGLISH,
      },
    };
    
    export default config;
    Through this configuration file, you can set up localised URLs, middleware redirection, cookie names, the location and extension of your content declarations, disable Intlayer logs in the console, and more. For a complete list of available parameters, refer to the configuration documentation.

    Step 3: Integrate Intlayer in Your Vite Configuration

    Add the intlayer plugin into your configuration.

    vite.config.ts
    import { defineConfig } from "vite";
    import { intlayer } from "vite-intlayer";
    
    // https://vitejs.dev/config/
    export default defineConfig({
      plugins: [intlayer()],
    });
    The intlayer() Vite plugin is used to integrate Intlayer with Vite. It ensures the building of content declaration files and monitors them in development mode. It defines Intlayer environment variables within the Vite application. Additionally, it provides aliases to optimise performance.

    Step 4: Bootstrap Intlayer in your entry point

    Call installIntlayer() before any content is rendered so that the global locale singleton is ready.

    src/main.ts
    import { installIntlayer } from "vanilla-intlayer";// Must be called before rendering any i18n content.installIntlayer();// Import and run your app modules.import "./app.js";

    If you also use md() content declarations (Markdown), install the markdown renderer as well:

    src/main.ts
    import { installIntlayer, installIntlayerMarkdown } from "vanilla-intlayer";installIntlayer();installIntlayerMarkdown();import "./app.js";

    Step 5: Declare Your Content

    Create and manage your content declarations to store translations:

    src/app.content.ts
    import { insert, t, type Dictionary } from "intlayer";
    
    const appContent = {
      key: "app",
      content: {
        title: "Vite + Vanilla",
    
        viteLogoLabel: t({
          en: "Vite Logo",
          fr: "Logo Vite",
          es: "Logo Vite",
        }),
    
        count: insert(
          t({
            en: "count is {{count}}",
            fr: "le compte est {{count}}",
            es: "el recuento es {{count}}",
          })
        ),
    
        readTheDocs: t({
          en: "Click on the Vite logo to learn more",
          fr: "Cliquez sur le logo Vite pour en savoir plus",
          es: "Haga clic en el logotipo de Vite para obtener más información",
        }),
      },
    } satisfies Dictionary;
    
    export default appContent;

    Your content declarations can be defined anywhere in your application as soon as they are included in the contentDir directory (by default, ./src). And match the content declaration file extension (by default, .content.{json,ts,tsx,js,jsx,mjs,cjs}).

    For more details, refer to the content declaration documentation.

    Step 6: Use Intlayer in Your JavaScript

    vanilla-intlayer mirrors the react-intlayer surface API: useIntlayer(key, locale?) returns the translated content directly. Chain .onChange() on the result to subscribe to locale changes - the explicit equivalent of a React re-render.

    src/main.ts
    import { installIntlayer, useIntlayer } from "vanilla-intlayer";installIntlayer();// Get the initial content for the current locale.// Chain .onChange() to be notified whenever the locale changes.const content = useIntlayer("app").onChange((newContent) => {  // Re-render or patch only the affected DOM nodes  document.querySelector<HTMLHeadingElement>("h1")!.textContent = String(    newContent.title  );  document.querySelector<HTMLParagraphElement>(".read-the-docs")!.textContent =    String(newContent.readTheDocs);});// Initial renderdocument.querySelector<HTMLHeadingElement>("h1")!.textContent = String(  content.title);document.querySelector<HTMLParagraphElement>(".read-the-docs")!.textContent =  String(content.readTheDocs);

    Access leaf values as strings by wrapping them in String(), which calls the node's toString() method and returns the translated text.

    When you need the value for a native HTML attribute (e.g. alt, aria-label), use .value directly:

    typescript
    img.alt = content.viteLogoLabel.value;

    (Optional) Step 7: Change the language of your content

    To change the language of your content, use the setLocale function exposed by useLocale.

    src/locale-switcher.ts
    import { getLocaleName } from "intlayer";import { useLocale } from "vanilla-intlayer";export function setupLocaleSwitcher(container: HTMLElement): () => void {  const { locale, availableLocales, setLocale, subscribe } = useLocale();  const select = document.createElement("select");  select.setAttribute("aria-label", "Language");  const render = (currentLocale: string) => {    select.innerHTML = availableLocales      .map(        (loc) =>          `<option value="${loc}"${loc === currentLocale ? " selected" : ""}>            ${getLocaleName(loc)}          </option>`      )      .join("");  };  render(locale);  container.appendChild(select);  select.addEventListener("change", () => setLocale(select.value as any));  // Keep the dropdown in sync when locale changes from elsewhere  return subscribe((newLocale) => render(newLocale));}

    (Optional) Step 8: Render Markdown and HTML content

    Intlayer supports md() and html() content declarations. In vanilla JS, compiled output is inserted as raw HTML via innerHTML.

    Compile and inject the HTML:

    src/main.ts
    import {  compileMarkdown,  installIntlayerMarkdown,  useIntlayer,} from "vanilla-intlayer";installIntlayerMarkdown();const content = useIntlayer("app").onChange((newContent) => {  const el = document.querySelector<HTMLDivElement>(".edit-note")!;  el.innerHTML = compileMarkdown(String(newContent.editNote));});document.querySelector<HTMLDivElement>(".edit-note")!.innerHTML =  compileMarkdown(String(content.editNote));
    TIP
    String(content.editNote) calls toString() on the IntlayerNode which returns the raw Markdown string. Pass it to compileMarkdown to get an HTML string, then set it via innerHTML.
    WARNING

    Only use innerHTML with trusted content. If the markdown comes from user input, sanitise it first (e.g. with DOMPurify). You can install a sanitising renderer dynamically:

    typescript
    import { installIntlayerMarkdownDynamic } from "vanilla-intlayer";await installIntlayerMarkdownDynamic(async () => {  const DOMPurify = await import("dompurify");  return (markdown) => DOMPurify.sanitise(compileMarkdown(markdown));});

    (Optional) Step 9: Add localised Routing to your application

    To make unique routes for each language (useful for SEO), you can use intlayerProxy in your Vite config for server-side locale detection.

    First, add intlayerProxy to your Vite config:

    Note that to use intlayerProxy in production, you need to move vite-intlayer from devDependencies to dependencies.
    vite.config.ts
    import { defineConfig } from "vite";
    import { intlayer, intlayerProxy } from "vite-intlayer";
    
    export default defineConfig({
      plugins: [
        intlayerProxy(), // should be placed first
        intlayer(),
      ],
    });

    (Optional) Step 10: Change the URL when the locale changes

    To update the browser URL when the locale changes, call useRewriteURL() after installing Intlayer:

    src/main.ts
    import { installIntlayer, useRewriteURL } from "vanilla-intlayer";installIntlayer();// Rewrites the URL immediately and on every subsequent locale change.// Returns an unsubscribe function for cleanup.const stopRewriteURL = useRewriteURL();

    (Optional) Step 11: Switch the HTML Language and Direction Attributes

    Update the <html> tag's lang and dir attributes to match the current locale for accessibility and SEO.

    src/main.ts
    import { getHTMLTextDir } from "intlayer";import { installIntlayer, useLocale } from "vanilla-intlayer";installIntlayer();useLocale({  onLocaleChange: (locale) => {    document.documentElement.lang = locale;    document.documentElement.dir = getHTMLTextDir(locale);  },});

    (Optional) Step 12: Lazy-load dictionaries per locale

    For large apps you may want to split each locale's dictionary into its own chunk. Use useDictionaryDynamic alongside Vite's dynamic import():

    src/app.ts
    import { installIntlayer, useDictionaryDynamic } from "vanilla-intlayer";installIntlayer();const unsubscribe = useDictionaryDynamic(  {    en: () => import("../.intlayer/dictionaries/en/app.mjs"),    fr: () => import("../.intlayer/dictionaries/fr/app.mjs"),    es: () => import("../.intlayer/dictionaries/es/app.mjs"),  },  "app").onChange((content) => {  document.querySelector("h1")!.textContent = String(content.title);});
    Each locale's bundle is fetched only when that locale becomes active and the result is cached - subsequent switches to the same locale are instant.

    (Optional) Step 13: Extract the content of your components

    If you have an existing codebase, transforming thousands of files can be time-consuming.

    To ease this process, Intlayer proposes a compiler / extractor to transform your components and extract the content.

    To set it up, you can add a compiler section in your intlayer.config.ts file:

    intlayer.config.ts
    import { type IntlayerConfig } from "intlayer";const config: IntlayerConfig = {  // ... Rest of your config  compiler: {    /**     * Indicates if the compiler should be enabled.     */    enabled: true,    /**     * Defines the output files path     */    output: ({ fileName, extension }) => `./${fileName}${extension}`,    /**     * Indicates if the components should be saved after being transformed.     *     * - If `true`, the compiler will rewrite the component file in the disk. So the transformation will be permanent, and the compiler will skip the transformation for the next process. That way, the compiler can transform the app, and then it can be removed.     *     * - If `false`, the compiler will inject the `useIntlayer()` function call into the code in the build output only, and keep the base codebase intact. The transformation will be done only in memory.     */    saveComponents: false,    /**     * Dictionary key prefix     */    dictionaryKeyPrefix: "",  },};export default config;

    Run the extractor to transform your components and extract the content

    bash
    npx intlayer extract

    (Optional) Sitemap and robots.txt (build-time)

    Intlayer includes formatters such as generateSitemap and getMultilingualUrls that produce crawler-ready multilingual sitemap.xml and robots.txt output you can write into your project’s public/ folder. In practice you run a small Node script before Vite (for example predev / prebuild npm hooks) so those files exist when you build or serve the app.

    Sitemap

    Intlayer’s sitemap generator respects your locale setup and includes the usual metadata for crawlers.

    The generated sitemap supports the xhtml:link namespace (hreflang XML extensions). Unlike basic generators that only emit flat URLs, Intlayer wires bidirectional links between every localized variant of each page (for example /about, /fr/about, or /about?lang=fr, depending on your routing mode), which helps search engines relate localized URLs.

    Robots.txt

    Use getMultilingualUrls so Disallow entries cover every localized spelling of sensitive paths.

    1. Add generate-seo.mjs at the project root

    generate-seo.mjs
    import fs from "fs";import path from "path";import { fileURLToPath } from "url";import { generateSitemap, getMultilingualUrls } from "intlayer";const __dirname = path.dirname(fileURLToPath(import.meta.url));const SITE_URL = (process.env.SITE_URL || "http://localhost:5173").replace(  /\/$/,  "");const pathList = [  { path: "/", changefreq: "daily", priority: 1.0 },  { path: "/about", changefreq: "monthly", priority: 0.7 },];const sitemapXml = generateSitemap(pathList, { siteUrl: SITE_URL });fs.writeFileSync(path.join(__dirname, "public", "sitemap.xml"), sitemapXml);const getAllMultilingualUrls = (urls) =>  urls.flatMap((url) => Object.values(getMultilingualUrls(url)));const disallowedPaths = getAllMultilingualUrls(["/admin", "/private"]);const robotsTxt = [  "User-agent: *",  "Allow: /",  ...disallowedPaths.map((path) => `Disallow: ${path}`),  "",  `Sitemap: ${SITE_URL}/sitemap.xml`,].join("\n");fs.writeFileSync(path.join(__dirname, "public", "robots.txt"), robotsTxt);console.log("SEO files generated successfully.");

    intlayer must be installed so the script can import it. Set SITE_URL in the environment for production (for example in CI).

    Prefer generate-seo.mjs for Node ESM. If you use generate-seo.js instead, ensure "type": "module" is set in package.json, or run Node with ESM enabled.

    2. Run the script before Vite

    package.json
    {  "scripts": {    "dev": "vite",    "prebuild": "node generate-seo.mjs",    "build": "vite build",    "preview": "vite preview"  }}

    Adjust if you use pnpm or yarn. You can also invoke the same script from CI or another step if that fits your workflow.

    Configure TypeScript

    Ensure your TypeScript configuration includes the autogenerated types.

    tsconfig.json
    {  "compilerOptions": {    // ...  },  "include": ["src", ".intlayer/**/*.ts"],}

    Git Configuration

    It is recommended to ignore the files generated by Intlayer. This allows you to avoid committing them to your Git repository.

    To do this, you can add the following instructions to your .gitignore file:

    bash
    # Ignore the files generated by Intlayer.intlayer

    VS Code Extension

    To improve your development experience with Intlayer, you can install the official Intlayer VS Code Extension.

    Install from the VS Code Marketplace

    This extension provides:

    • Autocompletion for translation keys.
    • Real-time error detection for missing translations.
    • Inline previews of translated content.
    • Quick actions to easily create and update translations.

    For more details on how to use the extension, refer to the Intlayer VS Code Extension documentation.


    Go Further

    To go further, you can implement the visual editor or externalise your content using the CMS.