Skip to main content
Product Builders Landing comes with a built-in internationalization (i18n) system supporting multiple languages. The template includes Spanish and English translations configured out of the box, with a Russian dictionary file available for activation.

How It Works

The i18n implementation uses Next.js dynamic routes with language-specific dictionaries stored as JSON files. The system provides:
  • Server-side translations with zero client-side JavaScript overhead
  • Type-safe dictionary access
  • Dynamic locale switching
  • SEO-friendly URLs with language prefixes

Configuration

The i18n configuration is defined in src/i18n-config.ts:
export const i18n = {
  defaultLocale: "es",
  locales: ["es", "en"],
} as const;

export type Locale = (typeof i18n)["locales"][number];

Available Options

  • defaultLocale: The fallback language when no locale is specified
  • locales: Array of supported language codes

Dictionary Structure

Translations are stored in src/dictionaries/ as JSON files. The project includes:
  • src/dictionaries/en.json - English translations (configured)
  • src/dictionaries/es.json - Spanish translations (configured, default)
  • src/dictionaries/ru.json - Russian translations (available, not configured)
The Russian dictionary file exists but is not currently configured in i18n-config.ts. See Adding a New Language below to enable it.

Configured Dictionaries

The following dictionaries are active in the i18n configuration:
{
  "metadata": {
    "title": "Product Builders",
    "description": "Consultoría y mentorías para llevar tu producto digital al siguiente nivel."
  },
  "header": {
    "contact": "Hablemos"
  },
  "hero": {
    "title": "Construimos y escalamos productos digitales de alto impacto.",
    "subtitle": "En Product Builders te ayudamos a transformar ideas en productos exitosos a través de consultoría estratégica y mentoría especializada.",
    "button": "Ver servicios"
  }
}

Loading Dictionaries

The getDictionary function in src/lib/dictionaries.ts loads the appropriate dictionary based on the current locale:
import "server-only";
import type { Locale } from "@/i18n-config";

const dictionaries = {
  en: () => import("@/dictionaries/en.json").then((module) => module.default),
  es: () => import("@/dictionaries/es.json").then((module) => module.default),
};

export const getDictionary = async (locale: Locale) =>
  dictionaries[locale]?.() ?? dictionaries.es();
This function:
  • Uses dynamic imports for code splitting
  • Falls back to Spanish if the requested locale doesn’t exist
  • Is marked with "server-only" to prevent accidental client-side usage

Using Translations in Components

Server Components

Load translations in server components using getDictionary:
import { getDictionary } from "@/lib/dictionaries";
import type { Locale } from "@/i18n-config";

export default async function Page({
  params: { lang },
}: {
  params: { lang: Locale };
}) {
  const dict = await getDictionary(lang);

  return (
    <div>
      <h1>{dict.hero.title}</h1>
      <p>{dict.hero.subtitle}</p>
    </div>
  );
}

Metadata Generation

Generate localized metadata in layout.tsx:
export async function generateMetadata({
  params: { lang },
}: {
  params: { lang: Locale };
}): Promise<Metadata> {
  const dictionary = await getDictionary(lang);
  return {
    title: dictionary.metadata.title,
    description: dictionary.metadata.description,
  };
}

Language Switcher

The LocaleSwitcher component allows users to change languages:
"use client";

import { usePathname } from "next/navigation";
import Link from "next/link";
import { i18n } from "@/i18n-config";
import { Button } from "@/components/ui/button";
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Globe } from "lucide-react";

export function LocaleSwitcher() {
  const pathName = usePathname();

  const redirectedPathName = (locale: string) => {
    if (!pathName) return "/";
    const segments = pathName.split("/");
    segments[1] = locale;
    return segments.join("/");
  };

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="icon">
          <Globe className="h-[1.2rem] w-[1.2rem]" />
          <span className="sr-only">Change language</span>
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        {i18n.locales.map((locale) => (
          <DropdownMenuItem key={locale} asChild>
            <Link href={redirectedPathName(locale)}>
              {locale.toUpperCase()}
            </Link>
          </DropdownMenuItem>
        ))}
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

Enabling Russian

The project includes a complete Russian dictionary (src/dictionaries/ru.json) that is not currently enabled. To activate it:
1

Update i18n configuration

Add "ru" to the locales array in src/i18n-config.ts:
export const i18n = {
  defaultLocale: "es",
  locales: ["es", "en", "ru"], // Added "ru"
} as const;
2

Update dictionary loader

Add the Russian import in src/lib/dictionaries.ts:
const dictionaries = {
  en: () => import("@/dictionaries/en.json").then((module) => module.default),
  es: () => import("@/dictionaries/es.json").then((module) => module.default),
  ru: () => import("@/dictionaries/ru.json").then((module) => module.default),
};
3

Test Russian locale

Navigate to /ru to see the Russian version of your landing page.

Adding a New Language

To add a language that doesn’t have a dictionary file yet:

Step 1: Create Dictionary File

Create a new JSON file in src/dictionaries/ (e.g., fr.json for French):
{
  "metadata": {
    "title": "Product Builders",
    "description": "Conseil et mentorat pour faire passer votre produit numérique au niveau supérieur."
  },
  "header": {
    "contact": "Parlons-en"
  },
  "hero": {
    "title": "Nous construisons et développons des produits numériques à fort impact.",
    "subtitle": "Chez Product Builders, nous vous aidons à transformer vos idées en produits réussis.",
    "button": "Voir les services"
  }
}

Step 2: Update i18n Configuration

Add the new locale to src/i18n-config.ts:
export const i18n = {
  defaultLocale: "es",
  locales: ["es", "en", "fr"], // Add "fr"
} as const;

Step 3: Update Dictionary Loader

Add the import in src/lib/dictionaries.ts:
const dictionaries = {
  en: () => import("@/dictionaries/en.json").then((module) => module.default),
  es: () => import("@/dictionaries/es.json").then((module) => module.default),
  fr: () => import("@/dictionaries/fr.json").then((module) => module.default),
};

Step 4: Test

Navigate to /fr to see your new language in action.

URL Structure

The i18n system uses locale-prefixed URLs:
  • /es - Spanish (default)
  • /en - English
  • /fr - Example for adding French (not configured by default)

Static Generation

All locale routes are statically generated at build time using generateStaticParams:
export async function generateStaticParams() {
  return i18n.locales.map((locale) => ({ lang: locale }));
}
This ensures optimal performance with pre-rendered pages for each language.

Build docs developers (and LLMs) love