Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions .claude/agents/agent-translation-reviewer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
---
name: translation-reviewer
description: Native-level review of translated documentation against its English source. Reports findings only, never edits.
tools: Read, Grep, Glob
model: opus
---

# Translation Reviewer Agent

Reviews translated documentation as a native speaker of the target language who also
writes React and TypeScript for a living.

## Role

**Read-only.** Report findings; never edit a file. The author applies the fixes.
This separation is deliberate — a translation reviewed by whoever wrote it is not reviewed.

## Input contract

The dispatching prompt must supply:

- Target language and its locale directory (e.g. Japanese, `docs/ja/`)
- Each translated file path, paired with its English source path

If any of these are missing, say so and stop. Do not guess which source a translation came from.

## Finding categories

| Category | Meaning |
| ----------- | ---------------------------------------------------------------------------------- |
| `meaning` | The technical claim differs from the English source |
| `glossary` | A term does not match the glossary for this language |
| `fluency` | Grammatical, but not how a native writes it |
| `register` | Politeness or formality inconsistent with the rest of the document |
| `code` | Code, identifier, import path, prop name, or VitePress code marker was altered |
| `link` | Internal link missing the locale prefix, or pointing at a page that does not exist |
| `structure` | Heading level or order diverges from the English source |

## Output format

One block per finding, nothing else:

```
<file>:<line> [<category>]
source: <the English sentence or snippet>
current: <the translated text>
suggest: <replacement>
why: <one sentence>
```

Last line, always:

```
VERDICT: approve
```

or

```
VERDICT: revise (<n> findings)
```

No preamble, no document summary, no praise, no closing remarks.

## Rules

1. Never report a finding you cannot anchor to a file and a line number.
2. In code blocks, only comments and string literals are translatable. Anything else
that differs from the source is a `code` finding — including `// [!code --]` markers.
3. Do not propose stylistic rewrites of sentences that are already accurate and natural.
Report only what a maintainer would actually change.
4. If the English source is itself ambiguous, report it under `meaning` rather than
inventing a reading.
5. Frontmatter keys are not translatable; their values are. A translated key is a `code` finding.

## Japanese (`ja`)

**Register:** です・ます体 for prose. 体言止め is acceptable in parameter descriptions,
return-value descriptions, and table cells.

| English | 日本語 |
| --------------------- | -------------------------- |
| Hook | フック |
| Component | コンポーネント |
| Utility | ユーティリティ |
| Guide | ガイド |
| Reference | リファレンス |
| Introduction | 紹介 |
| Installation | インストール |
| Design Principles | 設計原則 |
| Contributing | 貢献ガイド |
| Roadmap | ロードマップ |
| Parameters | パラメータ |
| Return Value | 戻り値 |
| Example | 使用例 |
| Bundle size | バンドルサイズ |
| Dependency | 依存関係 |
| Zero dependencies | 依存関係ゼロ |
| Server-side rendering | サーバーサイドレンダリング |
| Callback | コールバック |
| State | 状態 |
| Rendering | レンダリング |
| Cleanup | クリーンアップ |
| Deprecated | 非推奨 |
| Test coverage | テストカバレッジ |
| Type-safe | 型安全 |

**Keep in the original script:** `react-simplikit`, `React`, `TypeScript`, `npm`, `yarn`,
`pnpm`, `ref`, `props`, `JSDoc`, `SSR`, `MIT`, and every hook/component/util name.

**Recurring failure modes in JA technical translation — check these explicitly:**

- 「〜することができます」 where 「〜できます」 reads better
- Overuse of の-chains (「Reactのフックのテストのカバレッジ」)
- English word order preserved through a relative clause that Japanese would front
- 半角/全角 inconsistency around parentheses and colons
- Translating a UI label that the source deliberately left in English

## Adding a language

Add a section like the Japanese one above: register, glossary table, do-not-translate list,
and recurring failure modes. The categories, output format, and rules stay language-agnostic.
87 changes: 53 additions & 34 deletions .scripts/verifyDocsI18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,14 @@ for (const requiredText of ['release:', 'changesets/action@', 'changeset:publish
assert.equal(releaseWorkflow.includes(requiredText), true, `release workflow must contain ${requiredText}`);
}

assert.deepEqual(Object.keys(localeDefinitions), ['root', 'ko']);
assert.deepEqual(Object.keys(localeDefinitions), ['root', 'ko', 'ja']);
assert.equal(rewrites['docs/index.md'], 'index.md');
assert.equal(rewrites['docs/ko/index.md'], 'ko/index.md');
assert.equal(rewrites['packages/core/src/hooks/:hook/ko/:hook.md'], 'ko/core/hooks/:hook.md');
assert.equal(rewrites['docs/ja/index.md'], 'ja/index.md');
assert.equal(rewrites['packages/core/src/hooks/:hook/ja/:hook.md'], 'ja/core/hooks/:hook.md');
assert.equal(generatedRewrites['generated-locales/docs/ko/index.md'], 'ko/index.md');
assert.equal(generatedRewrites['generated-locales/docs/ja/index.md'], 'ja/index.md');
assert.equal(packageJson.scripts['docs:prepare'], 'tsx .scripts/index.ts prepare-localized-fallbacks');
assert.equal(packageJson.scripts['docs:dev'], 'yarn docs:prepare && vitepress dev');
assert.equal(packageJson.scripts['docs:build'], 'yarn docs:prepare && vitepress build');
Expand Down Expand Up @@ -65,8 +68,9 @@ const hookFixtureName = 'useUntranslatedFallbackFixture';
const hookFixtureDirectory = path.join(root, 'packages/core/src/hooks', hookFixtureName);
const buildOutputDirectory = await fs.mkdtemp(path.join(os.tmpdir(), 'react-simplikit-docs-'));

// The repository currently has a Korean translation for every routed English document, so the
// fallback path only has a route to render on while these English-only fixtures exist.
// Korean translates every routed English document, so its fallback path only has a route to
// render on while these English-only fixtures exist. Japanese ships without translated API
// reference pages, so those routes render from generated fallbacks on every build.
await fs.writeFile(guideFixturePath, `# ${guideFixtureTitle}\n`);
await fs.mkdir(hookFixtureDirectory, { recursive: true });
await fs.writeFile(path.join(hookFixtureDirectory, `${hookFixtureName}.md`), `# ${hookFixtureName}\n`);
Expand Down Expand Up @@ -110,49 +114,49 @@ try {
await fs.rm(buildOutputDirectory, { force: true, recursive: true });
}

const jaFixtureDefinition = {
label: '日本語',
lang: 'ja',
path: 'ja',
untranslatedNotice: 'このページは翻訳準備中のため、英語の原文を表示しています。',
const unregisteredLocaleFixture = {
label: '简体中文',
lang: 'zh-Hans',
path: 'zh-Hans',
untranslatedNotice: '此页面的翻译正在准备中,暂时显示英文原文。',
themeStrings: {
homeNavLabel: 'ホーム',
guideLabel: 'ガイド',
referenceLabel: 'リファレンス',
componentsLabel: 'コンポーネント',
hooksLabel: 'フック',
utilsLabel: 'ユーティリティ',
homeNavLabel: '首页',
guideLabel: '指南',
referenceLabel: '参考',
componentsLabel: '组件',
hooksLabel: 'Hooks',
utilsLabel: '工具函数',
guidePages: {
core: {
intro: '紹介',
whyReactSimplikitMatters: 'なぜ react-simplikit なのか',
installation: 'インストール',
designPrinciples: '設計原則',
contributing: '貢献ガイド',
intro: '介绍',
whyReactSimplikitMatters: '为什么选择 react-simplikit',
installation: '安装',
designPrinciples: '设计原则',
contributing: '贡献指南',
},
mobile: {
intro: '紹介',
roadmap: 'ロードマップ',
installation: 'インストール',
designPrinciples: '設計原則',
contributing: '貢献ガイド',
intro: '介绍',
roadmap: '路线图',
installation: '安装',
designPrinciples: '设计原则',
contributing: '贡献指南',
},
},
editLinkText: 'GitHub で編集する',
footerMessage: 'MIT ライセンスの下で配布されています。',
editLinkText: 'GitHub 上编辑此页',
footerMessage: '基于 MIT 许可证发布。',
},
} satisfies Parameters<typeof buildLocaleConfig>[0];

const jaConfig = buildLocaleConfig(jaFixtureDefinition);
const unregisteredConfig = buildLocaleConfig(unregisteredLocaleFixture);

assert.equal(jaConfig.lang, 'ja');
assert.deepEqual(jaConfig.themeConfig?.nav, [
{ text: 'ホーム', link: '/ja/' },
{ text: 'Mobile', link: '/ja/mobile/intro' },
{ text: 'Core', link: '/ja/core/intro' },
assert.equal(unregisteredConfig.lang, 'zh-Hans');
assert.deepEqual(unregisteredConfig.themeConfig?.nav, [
{ text: '首页', link: '/zh-Hans/' },
{ text: 'Mobile', link: '/zh-Hans/mobile/intro' },
{ text: 'Core', link: '/zh-Hans/core/intro' },
]);
assert.deepEqual(Object.keys(jaConfig.themeConfig?.sidebar ?? {}), ['/ja/core/', '/ja/mobile/']);
assert.equal(jaConfig.themeConfig?.editLink?.text, 'GitHub で編集する');
assert.deepEqual(Object.keys(unregisteredConfig.themeConfig?.sidebar ?? {}), ['/zh-Hans/core/', '/zh-Hans/mobile/']);
assert.equal(unregisteredConfig.themeConfig?.editLink?.text, 'GitHub 上编辑此页');

const koConfig = buildLocaleConfig(localeDefinitions.ko);
const rootConfig = buildLocaleConfig(localeDefinitions.root);
Expand Down Expand Up @@ -183,6 +187,21 @@ assert.deepEqual(rootConfig.themeConfig?.nav, [
assert.equal(rootConfig.lang, 'en');
assert.equal(rootConfig.themeConfig?.editLink?.text, 'Edit this page on GitHub');

const jaConfig = buildLocaleConfig(localeDefinitions.ja);

assert.equal(jaConfig.lang, 'ja');
assert.deepEqual(jaConfig.themeConfig?.nav, [
{ text: 'ホーム', link: '/ja/' },
{ text: 'Mobile', link: '/ja/mobile/intro' },
{ text: 'Core', link: '/ja/core/intro' },
]);
assert.equal(jaConfig.themeConfig?.editLink?.text, 'GitHub で編集する');
assert.notEqual(
localeDefinitions.ja.themeStrings.search,
undefined,
'Japanese must ship search translations, or the search UI silently renders in English'
);

for (const retiredLocaleFile of ['.vitepress/en.mts', '.vitepress/ko.mts']) {
await assert.rejects(
fs.access(path.join(root, retiredLocaleFile)),
Expand Down
10 changes: 9 additions & 1 deletion .vitepress/locales.mts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { DefaultTheme } from 'vitepress';

import { en } from './locales/en.mts';
import { ja } from './locales/ja.mts';
import { ko } from './locales/ko.mts';

export type LocaleCode = 'root' | 'ko';
export type LocaleCode = 'root' | 'ko' | 'ja';

type GuidePageTitles = {
core: {
Expand Down Expand Up @@ -65,6 +66,13 @@ export const localeDefinitions: Record<LocaleCode, LocaleDefinition> = {
untranslatedNotice: '이 페이지는 번역을 준비하는 동안 영어 원문으로 보여드려요.',
themeStrings: ko,
},
ja: {
label: '日本語',
lang: 'ja',
path: 'ja',
untranslatedNotice: 'このページは翻訳の準備中のため、英語の原文を表示しています。',
themeStrings: ja,
},
};

export const localeDirectories = Object.values(localeDefinitions)
Expand Down
51 changes: 51 additions & 0 deletions .vitepress/locales/ja.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { LocaleThemeStrings } from '../locales.mts';

export const ja: LocaleThemeStrings = {
homeNavLabel: 'ホーム',
guideLabel: 'ガイド',
referenceLabel: 'リファレンス',
componentsLabel: 'コンポーネント',
hooksLabel: 'フック',
utilsLabel: 'ユーティリティ',
guidePages: {
core: {
intro: '紹介',
whyReactSimplikitMatters: 'なぜ react-simplikit なのか',
installation: 'インストール',
designPrinciples: '設計原則',
contributing: '貢献ガイド',
},
mobile: {
intro: '紹介',
roadmap: 'ロードマップ',
installation: 'インストール',
designPrinciples: '設計原則',
contributing: '貢献ガイド',
},
},
editLinkText: 'GitHub で編集する',
footerMessage: 'MIT ライセンスの下で配布されています。',
search: {
translations: {
button: {
buttonText: '検索',
buttonAriaLabel: '検索',
},
modal: {
backButtonTitle: '戻る',
displayDetails: '詳細を表示',
footer: {
closeKeyAriaLabel: '閉じる',
closeText: '閉じる',
navigateDownKeyAriaLabel: '下へ',
navigateText: '移動',
navigateUpKeyAriaLabel: '上へ',
selectKeyAriaLabel: '選択',
selectText: '選択',
},
noResultsText: '検索結果が見つかりませんでした。',
resetButtonTitle: 'すべて消去',
},
},
},
};
Loading
Loading