# Blog i18n Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add English/Korean i18n to the Robota blog with Content Collections, built-in Astro i18n routing, and language switcher.

**Architecture:** Use Astro's built-in i18n routing (no external library needed — Astro 6 has native i18n). Content Collections with `en/` and `ko/` subdirectories. Dynamic routes render posts per language. Header language switcher links to translated version or other language's index.

**Tech Stack:** Astro 6 built-in i18n, Content Collections, Zod schema, `getRelativeLocaleUrl()` helper

---

## File Map

| File                                                      | Action        | Responsibility                                             |
| --------------------------------------------------------- | ------------- | ---------------------------------------------------------- |
| `apps/blog/astro.config.mjs`                              | Modify        | Add i18n config (defaultLocale: en, locales: [en, ko])     |
| `apps/blog/src/content/config.ts`                         | Create        | Content Collections Zod schema with lang field             |
| `apps/blog/src/content/blog/en/how-coding-agent-works.md` | Create (move) | English blog post (translated from current Korean)         |
| `apps/blog/src/content/blog/ko/how-coding-agent-works.md` | Create (move) | Korean blog post (current content)                         |
| `apps/blog/src/i18n/ui.ts`                                | Create        | UI translation strings (en/ko)                             |
| `apps/blog/src/i18n/utils.ts`                             | Create        | Helper: getLangFromUrl, useTranslations, getTranslatedPost |
| `apps/blog/src/pages/blog/[...slug].astro`                | Create        | English dynamic post route                                 |
| `apps/blog/src/pages/ko/blog/[...slug].astro`             | Create        | Korean dynamic post route                                  |
| `apps/blog/src/pages/index.astro`                         | Modify        | English index (query en posts)                             |
| `apps/blog/src/pages/ko/index.astro`                      | Create        | Korean index                                               |
| `apps/blog/src/layouts/BlogPost.astro`                    | Modify        | Accept lang prop, dynamic html lang, hreflang tags         |
| `apps/blog/src/layouts/Base.astro`                        | Modify        | Accept lang prop, dynamic html lang, language switcher     |
| `apps/blog/src/pages/blog/how-coding-agent-works.md`      | Delete        | Replaced by Content Collections                            |

---

### Task 1: Configure Astro i18n

**Files:**

- Modify: `apps/blog/astro.config.mjs`

- [ ] **Step 1: Add i18n config to astro.config.mjs**

```js
// @ts-check
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
import rehypePrettyCode from 'rehype-pretty-code';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import remarkToc from 'remark-toc';
import remarkMermaid from './src/plugins/remark-mermaid.mjs';

export default defineConfig({
  site: 'https://blog.robota.io',
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'ko'],
    routing: {
      prefixDefaultLocale: false,
    },
  },
  integrations: [sitemap()],
  markdown: {
    syntaxHighlight: false,
    remarkPlugins: [remarkMermaid, remarkToc],
    rehypePlugins: [
      [rehypePrettyCode, { theme: 'github-dark' }],
      [rehypeAutolinkHeadings, { behavior: 'wrap' }],
    ],
  },
});
```

- [ ] **Step 2: Build to verify config is valid**

Run: `pnpm --filter robota-blog build`
Expected: Build succeeds (pages may be empty, that's OK at this step)

- [ ] **Step 3: Commit**

```bash
git add apps/blog/astro.config.mjs
git commit -m "feat(blog): configure Astro i18n routing (en default, ko)"
```

---

### Task 2: Create Content Collections schema

**Files:**

- Create: `apps/blog/src/content/config.ts`

- [ ] **Step 1: Create content config with Zod schema**

```ts
// apps/blog/src/content/config.ts
import { defineCollection, z } from 'astro:content';

const blog = defineCollection({
  type: 'content',
  schema: z.object({
    title: z.string(),
    subtitle: z.string().optional(),
    date: z.string(),
    author: z.string().optional(),
    authorUrl: z.string().url().optional(),
    image: z.string().url().optional(),
    lang: z.enum(['en', 'ko']),
  }),
});

export const collections = { blog };
```

- [ ] **Step 2: Commit**

```bash
git add apps/blog/src/content/config.ts
git commit -m "feat(blog): add Content Collections schema with lang field"
```

---

### Task 3: Migrate blog post to Content Collections

**Files:**

- Create: `apps/blog/src/content/blog/ko/how-coding-agent-works.md`
- Create: `apps/blog/src/content/blog/en/how-coding-agent-works.md`
- Delete: `apps/blog/src/pages/blog/how-coding-agent-works.md`

- [ ] **Step 1: Copy current post to Korean content directory**

```bash
mkdir -p apps/blog/src/content/blog/ko
cp apps/blog/src/pages/blog/how-coding-agent-works.md apps/blog/src/content/blog/ko/how-coding-agent-works.md
```

- [ ] **Step 2: Update Korean post frontmatter**

Remove `layout` field (Content Collections don't use it), add `lang: 'ko'`:

```yaml
---
title: '코딩 에이전트 CLI는 어떻게 만들까'
subtitle: '직접 만들어본 코딩 에이전트 CLI'
date: '2026-03-28'
author: '황정연'
authorUrl: 'https://github.com/woojubb'
lang: 'ko'
---
```

- [ ] **Step 3: Create English content directory and post**

```bash
mkdir -p apps/blog/src/content/blog/en
```

Create `apps/blog/src/content/blog/en/how-coding-agent-works.md` with English frontmatter:

```yaml
---
title: 'How to Build a Coding Agent CLI'
subtitle: 'A coding agent CLI built from scratch'
date: '2026-03-28'
author: 'Jung Youn Hwang'
authorUrl: 'https://github.com/woojubb'
lang: 'en'
---
```

Body: translate the Korean post to English (or use placeholder text initially and translate later).

- [ ] **Step 4: Delete old pages-based post**

```bash
rm apps/blog/src/pages/blog/how-coding-agent-works.md
```

- [ ] **Step 5: Commit**

```bash
git add apps/blog/src/content/blog/ apps/blog/src/pages/blog/how-coding-agent-works.md
git commit -m "feat(blog): migrate post to Content Collections (en + ko)"
```

---

### Task 4: Create i18n UI translations and helpers

**Files:**

- Create: `apps/blog/src/i18n/ui.ts`
- Create: `apps/blog/src/i18n/utils.ts`

- [ ] **Step 1: Create UI translation strings**

```ts
// apps/blog/src/i18n/ui.ts
export const languages = {
  en: 'English',
  ko: '한국어',
} as const;

export const defaultLang = 'en' as const;

export const ui = {
  en: {
    'nav.blog': 'blog',
    'site.title': 'Robota Blog',
    'site.description': 'Coding agents, AI, and development',
    'theme.toggle': 'Toggle theme',
    'lang.switch': '한국어',
  },
  ko: {
    'nav.blog': '블로그',
    'site.title': 'Robota Blog',
    'site.description': '코딩 에이전트, AI, 개발 이야기',
    'theme.toggle': '테마 전환',
    'lang.switch': 'English',
  },
} as const;
```

- [ ] **Step 2: Create i18n utility functions**

```ts
// apps/blog/src/i18n/utils.ts
import { ui, defaultLang, languages } from './ui';

export type Lang = keyof typeof languages;

export function getLangFromUrl(url: URL): Lang {
  const [, lang] = url.pathname.split('/');
  if (lang in languages) return lang as Lang;
  return defaultLang;
}

export function useTranslations(lang: Lang) {
  return function t(key: keyof (typeof ui)[typeof defaultLang]): string {
    return ui[lang][key] || ui[defaultLang][key];
  };
}

export function getLocalePath(lang: Lang, path: string): string {
  if (lang === defaultLang) return path;
  return `/${lang}${path}`;
}

export function getSwitchLangPath(currentLang: Lang, currentPath: string): string {
  const targetLang = currentLang === 'en' ? 'ko' : 'en';
  if (currentLang === defaultLang) {
    // Currently on /blog/slug → switch to /ko/blog/slug
    return `/${targetLang}${currentPath}`;
  } else {
    // Currently on /ko/blog/slug → switch to /blog/slug
    return currentPath.replace(`/${currentLang}`, '');
  }
}
```

- [ ] **Step 3: Commit**

```bash
git add apps/blog/src/i18n/
git commit -m "feat(blog): add i18n UI translations and helper functions"
```

---

### Task 5: Create dynamic post routes

**Files:**

- Create: `apps/blog/src/pages/blog/[...slug].astro`
- Create: `apps/blog/src/pages/ko/blog/[...slug].astro`

- [ ] **Step 1: Create English post route**

```astro
---
// apps/blog/src/pages/blog/[...slug].astro
import { getCollection } from 'astro:content';
import BlogPost from '../../layouts/BlogPost.astro';

export async function getStaticPaths() {
  const posts = await getCollection('blog', ({ data }) => data.lang === 'en');
  return posts.map((post) => ({
    params: { slug: post.slug.replace('en/', '') },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content } = await post.render();

// Check if Korean translation exists
const allPosts = await getCollection('blog', ({ data }) => data.lang === 'ko');
const koSlug = post.slug.replace('en/', '');
const hasTranslation = allPosts.some((p) => p.slug.replace('ko/', '') === koSlug);
const translationUrl = hasTranslation ? `/ko/blog/${koSlug}/` : '/ko/';
---

<BlogPost
  frontmatter={post.data}
  lang="en"
  translationUrl={translationUrl}
>
  <Content />
</BlogPost>
```

- [ ] **Step 2: Create Korean post route**

```astro
---
// apps/blog/src/pages/ko/blog/[...slug].astro
import { getCollection } from 'astro:content';
import BlogPost from '../../../layouts/BlogPost.astro';

export async function getStaticPaths() {
  const posts = await getCollection('blog', ({ data }) => data.lang === 'ko');
  return posts.map((post) => ({
    params: { slug: post.slug.replace('ko/', '') },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content } = await post.render();

// Check if English translation exists
const allPosts = await getCollection('blog', ({ data }) => data.lang === 'en');
const enSlug = post.slug.replace('ko/', '');
const hasTranslation = allPosts.some((p) => p.slug.replace('en/', '') === enSlug);
const translationUrl = hasTranslation ? `/blog/${enSlug}/` : '/';
---

<BlogPost
  frontmatter={post.data}
  lang="ko"
  translationUrl={translationUrl}
>
  <Content />
</BlogPost>
```

- [ ] **Step 3: Build to verify routes work**

Run: `pnpm --filter robota-blog build`
Expected: Build succeeds, generates `/blog/how-coding-agent-works/index.html` and `/ko/blog/how-coding-agent-works/index.html`

- [ ] **Step 4: Commit**

```bash
git add apps/blog/src/pages/blog/\[...slug\].astro apps/blog/src/pages/ko/blog/\[...slug\].astro
git commit -m "feat(blog): add dynamic post routes for en and ko"
```

---

### Task 6: Update index pages

**Files:**

- Modify: `apps/blog/src/pages/index.astro`
- Create: `apps/blog/src/pages/ko/index.astro`

- [ ] **Step 1: Update English index to use Content Collections**

```astro
---
// apps/blog/src/pages/index.astro
import { getCollection } from 'astro:content';
import Base from '../layouts/Base.astro';

const posts = await getCollection('blog', ({ data }) => data.lang === 'en');
const sortedPosts = posts.sort(
  (a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()
);
---

<Base title="Robota Blog" description="Coding agents, AI, and development" lang="en" translationUrl="/ko/">
  <h1>blog</h1>
  <p class="intro">Coding agents, AI, and development</p>

  <ul class="post-list">
    {sortedPosts.map((post) => {
      const slug = post.slug.replace('en/', '');
      const date = new Date(post.data.date).toLocaleDateString('en-US', {
        year: 'numeric',
        month: 'long',
        day: 'numeric',
      });
      return (
        <li>
          <a href={`/blog/${slug}/`}>
            <time>{date}</time>
            <span class="post-title">{post.data.title}</span>
            {post.data.subtitle && (
              <span class="post-subtitle">{post.data.subtitle}</span>
            )}
          </a>
        </li>
      );
    })}
  </ul>
</Base>
```

Keep the existing `<style>` block unchanged.

- [ ] **Step 2: Create Korean index**

```astro
---
// apps/blog/src/pages/ko/index.astro
import { getCollection } from 'astro:content';
import Base from '../../layouts/Base.astro';

const posts = await getCollection('blog', ({ data }) => data.lang === 'ko');
const sortedPosts = posts.sort(
  (a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()
);
---

<Base title="Robota Blog" description="코딩 에이전트, AI, 개발 이야기" lang="ko" translationUrl="/">
  <h1>블로그</h1>
  <p class="intro">코딩 에이전트, AI, 개발 이야기</p>

  <ul class="post-list">
    {sortedPosts.map((post) => {
      const slug = post.slug.replace('ko/', '');
      const date = new Date(post.data.date).toLocaleDateString('ko-KR', {
        year: 'numeric',
        month: 'long',
        day: 'numeric',
      });
      return (
        <li>
          <a href={`/ko/blog/${slug}/`}>
            <time>{date}</time>
            <span class="post-title">{post.data.title}</span>
            {post.data.subtitle && (
              <span class="post-subtitle">{post.data.subtitle}</span>
            )}
          </a>
        </li>
      );
    })}
  </ul>
</Base>
```

Copy the `<style>` block from the English index.

- [ ] **Step 3: Commit**

```bash
git add apps/blog/src/pages/index.astro apps/blog/src/pages/ko/index.astro
git commit -m "feat(blog): update index pages for i18n (en + ko)"
```

---

### Task 7: Update layouts with lang, hreflang, and language switcher

**Files:**

- Modify: `apps/blog/src/layouts/Base.astro`
- Modify: `apps/blog/src/layouts/BlogPost.astro`

- [ ] **Step 1: Update Base.astro**

Key changes:

- Accept `lang` and `translationUrl` props
- Change `<html lang="ko">` → `<html lang={lang}>`
- Add language switcher button in header nav (before theme toggle)
- Language switcher text: "한국어" when on English, "English" when on Korean

```astro
---
const { title = 'Robota Blog', description = '', lang = 'en', translationUrl = '' } = Astro.props;
const switchLabel = lang === 'en' ? '한국어' : 'English';
---

<html lang={lang}>
<head>
  <!-- existing head content unchanged -->
</head>
<body>
  <header class="site-header">
    <a href={lang === 'en' ? '/' : '/ko/'} class="logo">Robota</a>
    <nav>
      {translationUrl && <a href={translationUrl} class="lang-switch">{switchLabel}</a>}
      <button id="theme-toggle" class="theme-btn" aria-label="Toggle theme">☀</button>
      <a href={lang === 'en' ? '/' : '/ko/'}>{lang === 'en' ? 'blog' : '블로그'}</a>
    </nav>
  </header>
  <!-- rest unchanged -->
</body>
```

Add CSS for `.lang-switch`:

```css
.lang-switch {
  font-family: var(--mono);
  font-size: 0.85rem;
  color: var(--text-dim);
  text-decoration: none;
  margin-right: 10px;
}

.lang-switch:hover {
  color: var(--green);
}
```

- [ ] **Step 2: Update BlogPost.astro**

Key changes:

- Accept `lang` and `translationUrl` props
- Change `<html lang="ko">` → `<html lang={lang}>`
- Add `hreflang` alternate links
- Add language switcher in header
- Date format based on lang (`en-US` or `ko-KR`)

In the frontmatter section:

```astro
---
const { frontmatter, lang = 'en', translationUrl = '' } = Astro.props;
const { title, subtitle, date, image, author, authorUrl } = frontmatter;
const dateLocale = lang === 'ko' ? 'ko-KR' : 'en-US';
const formattedDate = new Date(date).toLocaleDateString(dateLocale, {
  year: 'numeric',
  month: 'long',
  day: 'numeric',
});
const switchLabel = lang === 'en' ? '한국어' : 'English';
const currentUrl = `https://blog.robota.io${Astro.url.pathname}`;
const altLang = lang === 'en' ? 'ko' : 'en';
---
```

In `<head>`, add hreflang tags:

```html
<link rel="alternate" hreflang={lang} href={currentUrl} />
{translationUrl && <link rel="alternate" hreflang={altLang} href={`https://blog.robota.io${translationUrl}`} />}
```

In header nav, add language switcher (same as Base.astro):

```html
<nav>
  {translationUrl && <a href={translationUrl} class="lang-switch">{switchLabel}</a>}
  <button id="theme-toggle" class="theme-btn" aria-label="Toggle theme">☀</button>
  <a href={lang === 'en' ? '/' : '/ko/'}>{lang === 'en' ? 'blog' : '블로그'}</a>
</nav>
```

- [ ] **Step 3: Build and verify**

Run: `pnpm --filter robota-blog build`
Expected: Build succeeds. Check output:

- `dist/blog/how-coding-agent-works/index.html` has `<html lang="en">` and hreflang tags
- `dist/ko/blog/how-coding-agent-works/index.html` has `<html lang="ko">` and hreflang tags
- Both have language switcher in header

- [ ] **Step 4: Commit**

```bash
git add apps/blog/src/layouts/Base.astro apps/blog/src/layouts/BlogPost.astro
git commit -m "feat(blog): add lang prop, hreflang tags, language switcher to layouts"
```

---

### Task 8: Verify and clean up

**Files:**

- Modify: `apps/blog/public/styles/global.css` (sync from src)
- Delete: `apps/blog/src/pages/blog/` directory (old static pages, if any remain)

- [ ] **Step 1: Sync CSS**

```bash
cp apps/blog/src/styles/global.css apps/blog/public/styles/global.css
```

- [ ] **Step 2: Full build and preview**

```bash
pnpm --filter robota-blog build
pnpm --filter robota-blog preview
```

Verify in browser:

- `/` shows English index with English post
- `/ko/` shows Korean index with Korean post
- `/blog/how-coding-agent-works/` shows English post with `<html lang="en">`
- `/ko/blog/how-coding-agent-works/` shows Korean post with `<html lang="ko">`
- Language switcher links work correctly
- Theme toggle still works
- hreflang tags present in page source

- [ ] **Step 3: Clean up any remaining old files**

```bash
# Remove old static blog page directory if empty
rmdir apps/blog/src/pages/blog 2>/dev/null || true
```

- [ ] **Step 4: Final commit**

```bash
git add -A apps/blog/
git commit -m "feat(blog): complete i18n setup — Content Collections, routing, language switcher"
```

---

### Task 9: Push and deploy

- [ ] **Step 1: Push to release branch**

```bash
git push origin release/v3.0.0
```

- [ ] **Step 2: Create PR**

```bash
gh pr create --base main --head release/v3.0.0 --title "feat: blog i18n (en/ko)" --body "..."
```

- [ ] **Step 3: Verify Cloudflare Pages build succeeds**

Check CF dashboard for successful deployment.

## 검증

- 구현 완료 후 관련 패키지 빌드 성공 확인
- 연관 유닛 테스트 통과 확인
- typecheck 및 lint 에러 없음 확인
