Takazudo Modular Docs

Type to search...

to open search from anywhere

Language Routing & i18n

How the bilingual (JA/EN) routing, locale detection, and content translation system works.

Language Routing & i18n

This document explains how the bilingual (Japanese / English) routing system works across the site. Japanese is the default locale served at the root, and English pages are served under the /en/ prefix.

Astro i18n Configuration

Astro’s built-in i18n routing handles the two locales. Configuration is in astro.config.ts:

// astro.config.ts
i18n: {
  defaultLocale: 'ja',
  locales: ['ja', 'en'],
  routing: {
    prefixDefaultLocale: false,
  },
},

This means:

  • Japanese pages are served at the root (/notes/, /products/, etc.)
  • English pages are served under /en/ (/en/notes/, /en/products/, etc.)
  • No /ja/ prefix for the default locale

Page Structure

src/astro/pages/
├── notes/[slug].astro       # → /notes/[slug]/
├── guides/[slug].astro      # → /guides/[slug]/
├── products/[slug].astro    # → /products/[slug]/
├── brands/[slug].astro      # → /brands/[slug]/
├── categories/[slug].astro  # → /categories/[slug]/
├── tags/[slug].astro        # → /tags/[slug]/
├── s/[slug].astro           # → /s/[slug]/
├── en/
│   ├── notes/[slug].astro   # → /en/notes/[slug]/
│   ├── guides/[slug].astro  # → /en/guides/[slug]/
│   ├── products/[slug].astro # → /en/products/[slug]/
│   ├── brands/[slug].astro  # → /en/brands/[slug]/
│   ├── categories/[slug].astro # → /en/categories/[slug]/
│   ├── tags/[slug].astro    # → /en/tags/[slug]/
│   └── s/[slug].astro       # → /en/s/[slug]/
└── ...

Locale Detection

Locale Type

export const LOCALES = ['ja', 'en'] as const;
export type Locale = (typeof LOCALES)[number];
export const DEFAULT_LOCALE: Locale = 'ja';

Layout-Level Context

Astro layouts detect the locale from the URL and pass it to components:

---
const locale = Astro.currentLocale ?? 'ja';
const dictionary = await getDictionary(locale);
---
<html lang={locale}>
  <!-- layout content -->
</html>

Accessing Locale in Components

Astro components use Astro.currentLocale:

---
const locale = Astro.currentLocale ?? 'ja';
---

React components (client islands) receive locale as a prop:

function MyComponent({ locale = 'ja' }: { locale?: Locale }) {
  const name = getProductName(product, locale);
}

MDX File Convention

File Naming

Content directories contain paired MDX files:

src/mdx/notes/
├── col003-mixers.mdx       # Japanese article
├── col003-mixers.en.mdx    # English article
├── first-post.mdx
└── first-post.en.mdx
FileLocaleSlugURL
col003-mixers.mdxJAcol003-mixers/notes/col003-mixers/
col003-mixers.en.mdxENcol003-mixers/en/notes/col003-mixers/

Both files share the same slug. The .en.mdx suffix is stripped to derive the slug.

Content Directories

DirectoryJA URLEN URL
src/mdx/notes//notes/[slug]//en/notes/[slug]/
src/mdx/highlights//notes/[slug]//en/notes/[slug]/
src/mdx/guides//guides/[slug]//en/guides/[slug]/
src/mdx/products//products/[slug]//en/products/[slug]/
src/mdx/s//s/[slug]//en/s/[slug]/

Article Loading via Content Collections

Astro content collections handle article loading. Collections are defined in src/astro/content.config.ts:

Collection Structure

// JA collections — load from src/mdx/
const notes = defineCollection({
  loader: glob({ pattern: '**/*.mdx', base: './src/mdx/notes' }),
});

// EN collections — load .en.mdx files with slug stripping
const notesEn = defineCollection({
  loader: glob({
    pattern: '**/*.en.mdx',
    base: './src/mdx/notes',
    generateId: ({ entry }) => entry.replace(/\.en\.mdx$/, ''),
  }),
});

There are 5 JA collections (products, guides, notes, highlights, standalone) and 5 corresponding EN collections (products-en, guides-en, notes-en, highlights-en, standalone-en).

Querying Content

import { getCollection, getEntry } from 'astro:content';

// Get all JA notes
const jaArticles = await getCollection('notes');

// Get all EN notes
const enArticles = await getCollection('notes-en');

// Get a specific article
const article = await getEntry('notes', 'col003-mixers');

Static Page Generation

Astro pages use getStaticPaths() to generate pages from collections:

---
export async function getStaticPaths() {
  const articles = await getCollection('notes');
  return articles.map((article) => ({
    params: { slug: article.id },
  }));
}
---
---
export async function getStaticPaths() {
  const articles = await getCollection('notes-en');
  return articles.map((article) => ({
    params: { slug: article.id },
  }));
}
---

Product & Brand i18n

Master Data Fields

Product and brand data store both JA and EN fields side by side:

{
  slug: 'iromihon-acr-glasscyan-s',
  name: 'ブランクパネル: Iromihon-ACR GlassCyan-S',         // JA
  nameEn: 'Blank Panel: Iromihon-ACR GlassCyan-S',           // EN
  description: 'アクリル製カラーブランクパネル...',            // JA
  descriptionEn: 'Acrylic color blank panel...',              // EN
}
{
  slug: 'addac',
  description: '音による表現のための楽器...',                   // JA
  descriptionEn: 'Instruments for sonic expression...',        // EN
  descriptionMore: 'ポルトガル・リスボン発の...',               // JA
  descriptionMoreEn: 'ADDAC System is a modular synth...',     // EN
}

Helper Functions

// Returns nameEn for 'en' locale, falls back to name
getProductName(product, locale)

// Returns descriptionEn for 'en' locale, falls back to description
getProductDescription(product, locale)

// Same pattern for brands
getBrandDescription(brand, locale)
getBrandDescriptionMore(brand, locale)

All exported from lib/i18n/index.ts.

Component Usage

Components receive locale and use the helpers:

function ProductNavItem({ product, locale = 'ja' }) {
  const pathPrefix = locale === 'en' ? '/en' : '';
  const name = getProductName(product, locale);
  const description = getProductDescription(product, locale);
  const href = `${pathPrefix}${product.detailHref}`;
  // ...
}

Tag & Category System

Dual Labels

Tags and categories have ASCII keys with localized display labels:

const CATEGORY_MAPPINGS = [
  { key: 'news', label: 'お知らせ', labelEn: 'News' },
  { key: 'products-intro', label: '商品紹介', labelEn: 'Product Introduction' },
  { key: 'guide', label: 'ガイド', labelEn: 'Guides' },
  { key: 'column', label: 'コラム', labelEn: 'Columns' },
];

MDX frontmatter always uses the ASCII key:

categories:
  - products-intro
tags:
  - how-to-build

Labels are resolved at render time via getTaxonomyLabel(key, locale).

URL Path Helpers

// Build locale-aware paths
localePath('/products/', 'ja')  // → '/products/'
localePath('/products/', 'en')  // → '/en/products/'

// Language switcher (alternate locale path)
getAlternatePath('/products/', 'ja')  // → '/en/products/'
getAlternatePath('/en/products/', 'en')  // → '/products/'

getAlternatePath() includes a whitelist of known EN top-level pages to avoid linking to 404s for pages without English translations.

SEO: hreflang Alternates

Pages include <link rel="alternate" hreflang="..."> tags in the HTML head for cross-locale linking:

<link rel="alternate" hreflang="ja" href="/notes/" />
<link rel="alternate" hreflang="en" href="/en/notes/" />

These are generated from layout or head components based on the current locale and the existence of a corresponding translation.

Translation Workflow

To translate JA content into EN, the project provides Claude Code automation:

en-translator agent

A custom Claude Code agent (.claude/agents/en-translator.md) that translates a JA .mdx file into an .en.mdx file. It has built-in synth terminology awareness and preserves all MDX components, links, and frontmatter structure. Uses the opus model for translation quality.

/l-create-en-implementation skill

A skill (.claude/skills/l-create-en-implementation/SKILL.md) that automates the full EN implementation workflow. Invoke it while checked out on the JA feature branch — it auto-detects the current branch’s PR:

  1. Auto-detect the current branch’s PR and identify changed files
  2. Categorize changes (MDX content, routes, components, data)
  3. Create an EN branch from the JA PR’s head
  4. Translate MDX files using the en-translator agent
  5. Scaffold EN routes if new JA routes were added
  6. Update enComponents and data fields as needed
  7. Validate with pnpm check
  8. Create a PR targeting the JA PR’s head branch

This ensures JA and EN changes merge together when the JA PR lands on main.

Summary of Key Principles

  1. Japanese is default — no URL prefix, served at root
  2. English is prefixed — explicit /en/ in URLs
  3. Dual file namingarticle.mdx (JA) + article.en.mdx (EN) share the same slug
  4. Fallback to JA — English helpers fall back to Japanese content when no EN field exists
  5. Astro = Astro.currentLocale, React = props — locale via Astro API for .astro files, props for React islands
  6. All i18n in lib/i18n/ — centralized types, paths, helpers
  7. Separate content collections — JA and EN articles use parallel collections (notes / notes-en)