From 1a6704f8da5a7521cbf8d280dcf1c804b745270a Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Thu, 26 Mar 2026 17:18:01 +0900 Subject: [PATCH 01/11] docs(track): add web-i18n-20260326 track --- .../active/web-i18n-20260326/metadata.json | 9 ++ .../tracks/active/web-i18n-20260326/plan.md | 114 ++++++++++++++++++ .../tracks/active/web-i18n-20260326/spec.md | 47 ++++++++ .please/docs/tracks/index.md | 1 + 4 files changed, 171 insertions(+) create mode 100644 .please/docs/tracks/active/web-i18n-20260326/metadata.json create mode 100644 .please/docs/tracks/active/web-i18n-20260326/plan.md create mode 100644 .please/docs/tracks/active/web-i18n-20260326/spec.md diff --git a/.please/docs/tracks/active/web-i18n-20260326/metadata.json b/.please/docs/tracks/active/web-i18n-20260326/metadata.json new file mode 100644 index 00000000..6d753367 --- /dev/null +++ b/.please/docs/tracks/active/web-i18n-20260326/metadata.json @@ -0,0 +1,9 @@ +{ + "track_id": "web-i18n-20260326", + "type": "feature", + "status": "planned", + "created_at": "2026-03-26T16:42:56+09:00", + "updated_at": "2026-03-26T16:50:00+09:00", + "issue": "", + "pr": "" +} diff --git a/.please/docs/tracks/active/web-i18n-20260326/plan.md b/.please/docs/tracks/active/web-i18n-20260326/plan.md new file mode 100644 index 00000000..1e8491af --- /dev/null +++ b/.please/docs/tracks/active/web-i18n-20260326/plan.md @@ -0,0 +1,114 @@ +# Plan: Web i18n Support + +> Track: web-i18n-20260326 +> Spec: [spec.md](./spec.md) + +## Overview + +- **Source**: .please/docs/tracks/active/web-i18n-20260326/spec.md +- **Issue**: TBD +- **Created**: 2026-03-26 +- **Approach**: Pragmatic + +## Purpose + +After this change, marketplace visitors will see the UI in their preferred language (English, Korean, Japanese, or Chinese). They can verify it works by visiting `/ko`, `/ja`, or `/zh` and seeing the full UI translated, or by using the language switcher in the header. + +## Context + +The Claude Code Plugin Marketplace (`apps/web/`) is a Nuxt 4 application with all UI strings hardcoded in English across 5 Vue files (~35 translatable strings). The site serves an international audience including Korean, Japanese, and Chinese developers. Adding i18n support using `@nuxtjs/i18n` — the standard Nuxt i18n module — enables locale-aware routing, browser language detection, and SEO-friendly hreflang tags. The site uses Vercel ISR with route rules that must be extended to cover locale-prefixed paths. Plugin names and descriptions come from marketplace API data and remain untranslated. + +**Non-goals**: Translating plugin data from the API, translating Nuxt Content pages, RTL support. + +## Architecture Decision + +Using `@nuxtjs/i18n` with `prefix_except_default` routing strategy. English remains at `/` (no prefix) for backward compatibility, while `/ko`, `/ja`, `/zh` serve localized versions. Locale messages are stored as lazy-loaded JSON files under `apps/web/app/locales/`. Browser language detection uses the built-in `detectBrowserLanguage` feature with cookie persistence. A new `LanguageSwitcher.vue` component is placed in the hero section alongside the existing GitHub and color-mode buttons. + +## Tasks + +### Phase 1: Foundation + +- [ ] T001 Install and configure @nuxtjs/i18n module (file: apps/web/nuxt.config.ts) +- [ ] T002 Create English locale file with all extracted UI strings (file: apps/web/app/locales/en.json) +- [ ] T003 [P] Create Korean locale file (file: apps/web/app/locales/ko.json) (depends on T002) +- [ ] T004 [P] Create Japanese locale file (file: apps/web/app/locales/ja.json) (depends on T002) +- [ ] T005 [P] Create Chinese locale file (file: apps/web/app/locales/zh.json) (depends on T002) + +### Phase 2: Component Integration + +- [ ] T006 Replace hardcoded strings in index.vue with $t() calls (file: apps/web/app/pages/index.vue) (depends on T001, T002) +- [ ] T007 [P] Replace hardcoded strings in PluginCard.vue (file: apps/web/app/components/PluginCard.vue) (depends on T001, T002) +- [ ] T008 [P] Replace hardcoded strings in PluginSearch.vue (file: apps/web/app/components/PluginSearch.vue) (depends on T001, T002) +- [ ] T009 [P] Replace hardcoded strings in InstallModal.vue (file: apps/web/app/components/InstallModal.vue) (depends on T001, T002) + +### Phase 3: UX & SEO + +- [ ] T010 Build LanguageSwitcher component (file: apps/web/app/components/LanguageSwitcher.vue) (depends on T001) +- [ ] T011 Integrate LanguageSwitcher into page layout (file: apps/web/app/pages/index.vue) (depends on T010, T006) +- [ ] T012 Update ISR route rules for locale-prefixed paths (file: apps/web/nuxt.config.ts) (depends on T001) +- [ ] T013 Add CJK font stack entries for Japanese and Chinese (file: apps/web/app/assets/css/main.css) (depends on T001) + +## Key Files + +### Create + +- `apps/web/app/locales/en.json` — English locale messages +- `apps/web/app/locales/ko.json` — Korean locale messages +- `apps/web/app/locales/ja.json` — Japanese locale messages +- `apps/web/app/locales/zh.json` — Chinese locale messages +- `apps/web/app/components/LanguageSwitcher.vue` — Language switcher component + +### Modify + +- `apps/web/nuxt.config.ts` — Add i18n module config and ISR route rules +- `apps/web/package.json` — Add @nuxtjs/i18n dependency +- `apps/web/app/pages/index.vue` — Replace hardcoded strings, integrate switcher +- `apps/web/app/components/PluginCard.vue` — Replace hardcoded strings +- `apps/web/app/components/PluginSearch.vue` — Replace hardcoded strings +- `apps/web/app/components/InstallModal.vue` — Replace hardcoded strings +- `apps/web/app/assets/css/main.css` — Add CJK font families + +### Reuse + +- `apps/web/app/app.vue` — No changes needed (NuxtPage handles i18n routing) +- `apps/web/app/types/marketplace.ts` — No changes needed + +## Verification + +### Automated Tests + +- [ ] i18n module loads without errors in Nuxt config +- [ ] All locale JSON files parse correctly and contain the same keys +- [ ] $t() calls return correct translations for each locale +- [ ] LanguageSwitcher renders locale options and triggers navigation + +### Observable Outcomes + +- After visiting `/ko`, all UI text appears in Korean +- After visiting `/ja`, all UI text appears in Japanese +- After visiting `/zh`, all UI text appears in Chinese +- After visiting `/` or `/en`, UI remains in English +- Running `bun run build` in `apps/web/` completes without errors + +### Manual Testing + +- [ ] Language switcher in header changes locale and URL +- [ ] Browser language detection redirects first-time visitors +- [ ] ISR cached pages serve correct locale content +- [ ] Plugin names/descriptions remain in original language across all locales + +### Acceptance Criteria Check + +- [ ] AC-1: `/ko` renders Korean UI +- [ ] AC-2: `/ja` renders Japanese UI +- [ ] AC-3: `/zh` renders Chinese UI +- [ ] AC-4: `/` renders English UI +- [ ] AC-5: Language switcher visible and functional +- [ ] AC-6: Browser language auto-detection works +- [ ] AC-7: All UI text translated (hero, search, filters, alerts, empty states, footer) + +## Decision Log + +- Decision: Use `prefix_except_default` strategy over `prefix` + Rationale: Maintains backward compatibility — existing `/` URLs continue working as English without redirects + Date/Author: 2026-03-26 / Claude diff --git a/.please/docs/tracks/active/web-i18n-20260326/spec.md b/.please/docs/tracks/active/web-i18n-20260326/spec.md new file mode 100644 index 00000000..284e39c2 --- /dev/null +++ b/.please/docs/tracks/active/web-i18n-20260326/spec.md @@ -0,0 +1,47 @@ +# Web i18n Support + +> Track: web-i18n-20260326 + +## Overview + +Add internationalization (i18n) support to the Claude Code Plugin Marketplace web application (`apps/web/`). The site currently has all UI strings hardcoded in English. This feature will enable multi-language support using `@nuxtjs/i18n` with prefix-based URL routing. + +## Requirements + +### Functional Requirements + +- [ ] FR-1: Integrate `@nuxtjs/i18n` module into the Nuxt 4 web application +- [ ] FR-2: Support 4 locales — English (en, default), Korean (ko), Japanese (ja), Chinese Simplified (zh) +- [ ] FR-3: Use prefix routing strategy (e.g., `/ko`, `/ja`, `/zh`; `/` or `/en` for English) +- [ ] FR-4: Extract all hardcoded UI strings from Vue components into locale message files +- [ ] FR-5: Provide a language switcher component in the site header for users to change locale +- [ ] FR-6: Detect browser language preference and redirect to the appropriate locale on first visit + +### Non-functional Requirements + +- [ ] NFR-1: Locale message files should use JSON format organized by locale (`locales/en.json`, `locales/ko.json`, etc.) +- [ ] NFR-2: No impact on ISR/SSR performance — locale switching must work with current Vercel ISR configuration +- [ ] NFR-3: Maintain existing SEO quality — `hreflang` tags should be auto-generated for all supported locales + +## Acceptance Criteria + +- [ ] AC-1: Visiting `/ko` renders the marketplace UI in Korean +- [ ] AC-2: Visiting `/ja` renders the marketplace UI in Japanese +- [ ] AC-3: Visiting `/zh` renders the marketplace UI in Chinese +- [ ] AC-4: Visiting `/` or `/en` renders the marketplace UI in English (same as current behavior) +- [ ] AC-5: Language switcher is visible and functional in the site header +- [ ] AC-6: Browser language auto-detection redirects new visitors to their preferred locale +- [ ] AC-7: All existing UI text (hero section, search, filters, alerts, empty states, footer) is translated + +## Out of Scope + +- Plugin names and descriptions (sourced from marketplace data) are NOT translated +- SEO meta tag translation per locale +- Right-to-left (RTL) language support +- Content pages managed by Nuxt Content + +## Assumptions + +- `@nuxtjs/i18n` is compatible with Nuxt 4 and Nuxt UI v4 +- Translation strings will be provided by the development team (machine translation acceptable for initial implementation) +- The number of UI strings is small (~30-50 keys) given the single-page marketplace layout diff --git a/.please/docs/tracks/index.md b/.please/docs/tracks/index.md index d4f6df77..36bc22f5 100644 --- a/.please/docs/tracks/index.md +++ b/.please/docs/tracks/index.md @@ -6,6 +6,7 @@ | Track | Feature | Type | Issue | Started | Status | |-------|---------|------|-------|---------|--------| +| [web-i18n-20260326](active/web-i18n-20260326/plan.md) | Web i18n Support | feature | TBD | 2026-03-26 | planned | ## Recently Completed From 2c8c0db650dfbef870deadd4c991c414f2264067 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Thu, 26 Mar 2026 17:22:59 +0900 Subject: [PATCH 02/11] chore(track): web-i18n-20260326 start implementation --- .please/docs/tracks/active/web-i18n-20260326/metadata.json | 2 +- .please/docs/tracks/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.please/docs/tracks/active/web-i18n-20260326/metadata.json b/.please/docs/tracks/active/web-i18n-20260326/metadata.json index 6d753367..52b27254 100644 --- a/.please/docs/tracks/active/web-i18n-20260326/metadata.json +++ b/.please/docs/tracks/active/web-i18n-20260326/metadata.json @@ -1,7 +1,7 @@ { "track_id": "web-i18n-20260326", "type": "feature", - "status": "planned", + "status": "in_progress", "created_at": "2026-03-26T16:42:56+09:00", "updated_at": "2026-03-26T16:50:00+09:00", "issue": "", diff --git a/.please/docs/tracks/index.md b/.please/docs/tracks/index.md index 36bc22f5..faa5812e 100644 --- a/.please/docs/tracks/index.md +++ b/.please/docs/tracks/index.md @@ -6,7 +6,7 @@ | Track | Feature | Type | Issue | Started | Status | |-------|---------|------|-------|---------|--------| -| [web-i18n-20260326](active/web-i18n-20260326/plan.md) | Web i18n Support | feature | TBD | 2026-03-26 | planned | +| [web-i18n-20260326](active/web-i18n-20260326/plan.md) | Web i18n Support | feature | TBD | 2026-03-26 | in_progress | ## Recently Completed From 9fb8831e4f034f7f8d3f40740f54bfffd2077d40 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Thu, 26 Mar 2026 17:27:15 +0900 Subject: [PATCH 03/11] feat(web): add @nuxtjs/i18n module with prefix_except_default strategy Configure 4 locales (en, ko, ja, zh) with lazy-loaded JSON files, browser language detection with cookie persistence, and ISR route rules for locale-prefixed paths. --- apps/web/nuxt.config.ts | 25 +++++++++++++++++++++++-- apps/web/package.json | 1 + 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/apps/web/nuxt.config.ts b/apps/web/nuxt.config.ts index 1d9d303f..1e56ab79 100644 --- a/apps/web/nuxt.config.ts +++ b/apps/web/nuxt.config.ts @@ -11,7 +11,10 @@ export default defineNuxtConfig({ // Page uses server-side data fetching (useAsyncData), so only page ISR is needed // Data is fetched on the server and included in the cached HTML routeRules: { - '/': { isr: 3600 }, // Main page with embedded data: revalidate every 1 hour + '/': { isr: 3600 }, // Main page (English default): revalidate every 1 hour + '/ko': { isr: 3600 }, // Korean locale + '/ja': { isr: 3600 }, // Japanese locale + '/zh': { isr: 3600 }, // Chinese locale '/api/marketplaces': { isr: 3600 }, // Keep API endpoint cached for direct API access if needed }, @@ -19,8 +22,26 @@ export default defineNuxtConfig({ '@nuxt/ui', '@nuxt/content', '@nuxt/eslint', - + '@nuxtjs/i18n', ], + + i18n: { + locales: [ + { code: 'en', name: 'English', file: 'en.json' }, + { code: 'ko', name: '한국어', file: 'ko.json' }, + { code: 'ja', name: '日本語', file: 'ja.json' }, + { code: 'zh', name: '中文', file: 'zh.json' }, + ], + defaultLocale: 'en', + strategy: 'prefix_except_default', + langDir: 'locales', + lazy: true, + detectBrowserLanguage: { + useCookie: true, + cookieKey: 'i18n_redirected', + redirectOn: 'root', + }, + }, css: ['~/assets/css/main.css'], eslint: { config: { diff --git a/apps/web/package.json b/apps/web/package.json index 370fb8d8..cc05f6cd 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -17,6 +17,7 @@ }, "dependencies": { "@nuxt/content": "^3.7.1", + "@nuxtjs/i18n": "^9.5.3", "@nuxt/ui": "^4.0.1", "better-sqlite3": "^12.4.1", "nuxt": "^4.1.3", From 294216c35cde2e4cf62cf8956bf79e24d6ee3c5f Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Thu, 26 Mar 2026 17:27:51 +0900 Subject: [PATCH 04/11] feat(web): create English locale file with extracted UI strings --- apps/web/app/locales/en.json | 78 ++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 apps/web/app/locales/en.json diff --git a/apps/web/app/locales/en.json b/apps/web/app/locales/en.json new file mode 100644 index 00000000..29fd5abb --- /dev/null +++ b/apps/web/app/locales/en.json @@ -0,0 +1,78 @@ +{ + "hero": { + "title": "Claude Code Plugin Marketplace", + "description": "Discover and install plugins to extend Claude Code's capabilities. Browse our collection of community-contributed plugins.", + "headline": "Explore Plugins" + }, + "search": { + "placeholder": "Search by name, description, or repository...", + "filterByMarketplace": "Filter by Marketplace:", + "allMarketplaces": "All Marketplaces", + "filteringBy": "Filtering by:", + "result": "result", + "results": "results", + "clearSearch": "Clear search" + }, + "plugin": { + "noDescription": "No description available", + "viewSource": "View Source", + "install": "Install", + "loading": "Loading...", + "viewInstallInstructions": "View installation instructions", + "badge": { + "fromMarketplace": "From {name} marketplace", + "developedByGoogle": "Developed by Google", + "developedByAnthropic": "Developed by Anthropic", + "includesContextFile": "Includes Context File", + "includesMcpServer": "Includes MCP Server", + "githubStars": "{count} GitHub stars" + } + }, + "installModal": { + "title": "Installation Instructions", + "description": "Run these two commands in order", + "step1": "Add Marketplace", + "step2": "Install Plugin", + "copyCommand": "Copy command", + "copied": "Copied!", + "copyAllCommands": "Copy All Commands", + "allCommandsCopied": "All Commands Copied!", + "close": "Close", + "clipboardNotAvailable": "Clipboard Not Available", + "clipboardWarning": "Your browser doesn't support automatic copying. Please copy the commands manually using Ctrl+C (Cmd+C on Mac).", + "tip": "Tip: You can also select and copy the commands manually (Ctrl+C / Cmd+C)" + }, + "alert": { + "communityPlugins": "Community Plugins", + "communityPluginsDescription": "These plugins are community-contributed. Please review the source code and documentation before installation." + }, + "loading": { + "loadingPlugins": "Loading plugins..." + }, + "error": { + "failedToLoadPlugins": "Failed to load plugins", + "pluginNotFound": "Plugin Not Found", + "pluginNotFoundDescription": "The plugin \"{name}\" could not be found. It may have been removed or the link may be incorrect.", + "navigationIssue": "Navigation Issue", + "navigationIssueDescription": "The plugin \"{name}\" was found but could not be displayed. Try refreshing the page.", + "copyFailed": "Copy Failed", + "copyFailedDescription": "Could not copy to clipboard. Please copy the command manually.", + "copyAllFailedDescription": "Could not copy commands to clipboard. Please copy them manually from above." + }, + "empty": { + "noPluginsFound": "No plugins found", + "noPluginsMatchQuery": "No plugins match your search query: '{query}'" + }, + "footer": { + "maintainedBy": "Marketplace maintained by", + "maintainerName": "Passion Factory" + }, + "seo": { + "title": "Claude Code Plugin Marketplace", + "description": "Discover and install plugins to extend Claude Code capabilities" + }, + "a11y": { + "viewOnGithub": "View on GitHub", + "clearSearch": "Clear search" + } +} From fc4baecacd0219287c8aedd81a080463fc34347c Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Thu, 26 Mar 2026 17:28:43 +0900 Subject: [PATCH 05/11] feat(web): add Korean, Japanese, and Chinese locale files --- apps/web/app/locales/ja.json | 78 ++++++++++++++++++++++++++++++++++++ apps/web/app/locales/ko.json | 78 ++++++++++++++++++++++++++++++++++++ apps/web/app/locales/zh.json | 78 ++++++++++++++++++++++++++++++++++++ 3 files changed, 234 insertions(+) create mode 100644 apps/web/app/locales/ja.json create mode 100644 apps/web/app/locales/ko.json create mode 100644 apps/web/app/locales/zh.json diff --git a/apps/web/app/locales/ja.json b/apps/web/app/locales/ja.json new file mode 100644 index 00000000..77603ce9 --- /dev/null +++ b/apps/web/app/locales/ja.json @@ -0,0 +1,78 @@ +{ + "hero": { + "title": "Claude Code プラグインマーケットプレイス", + "description": "Claude Codeの機能を拡張するプラグインを見つけてインストールしましょう。コミュニティ提供のプラグインコレクションをご覧ください。", + "headline": "プラグインを探す" + }, + "search": { + "placeholder": "名前、説明、またはリポジトリで検索...", + "filterByMarketplace": "マーケットプレイスで絞り込み:", + "allMarketplaces": "すべてのマーケットプレイス", + "filteringBy": "絞り込み:", + "result": "件", + "results": "件", + "clearSearch": "検索をクリア" + }, + "plugin": { + "noDescription": "説明はありません", + "viewSource": "ソースを見る", + "install": "インストール", + "loading": "読み込み中...", + "viewInstallInstructions": "インストール手順を見る", + "badge": { + "fromMarketplace": "{name}マーケットプレイス提供", + "developedByGoogle": "Google開発", + "developedByAnthropic": "Anthropic開発", + "includesContextFile": "コンテキストファイル付き", + "includesMcpServer": "MCPサーバー付き", + "githubStars": "GitHubスター{count}個" + } + }, + "installModal": { + "title": "インストール手順", + "description": "以下の2つのコマンドを順番に実行してください", + "step1": "マーケットプレイスを追加", + "step2": "プラグインをインストール", + "copyCommand": "コマンドをコピー", + "copied": "コピーしました!", + "copyAllCommands": "すべてのコマンドをコピー", + "allCommandsCopied": "すべてのコマンドをコピーしました!", + "close": "閉じる", + "clipboardNotAvailable": "クリップボード使用不可", + "clipboardWarning": "ブラウザが自動コピーに対応していません。Ctrl+C(Macの場合はCmd+C)で手動コピーしてください。", + "tip": "ヒント:Ctrl+C / Cmd+Cでコマンドを手動で選択してコピーすることもできます" + }, + "alert": { + "communityPlugins": "コミュニティプラグイン", + "communityPluginsDescription": "これらのプラグインはコミュニティの貢献によるものです。インストール前にソースコードとドキュメントをご確認ください。" + }, + "loading": { + "loadingPlugins": "プラグインを読み込み中..." + }, + "error": { + "failedToLoadPlugins": "プラグインの読み込みに失敗しました", + "pluginNotFound": "プラグインが見つかりません", + "pluginNotFoundDescription": "プラグイン「{name}」が見つかりませんでした。削除されたか、リンクが正しくない可能性があります。", + "navigationIssue": "ナビゲーションの問題", + "navigationIssueDescription": "プラグイン「{name}」は見つかりましたが、表示できませんでした。ページを更新してみてください。", + "copyFailed": "コピー失敗", + "copyFailedDescription": "クリップボードにコピーできませんでした。コマンドを手動でコピーしてください。", + "copyAllFailedDescription": "コマンドをクリップボードにコピーできませんでした。上記から手動でコピーしてください。" + }, + "empty": { + "noPluginsFound": "プラグインが見つかりません", + "noPluginsMatchQuery": "検索クエリ「{query}」に一致するプラグインはありません" + }, + "footer": { + "maintainedBy": "マーケットプレイス管理:", + "maintainerName": "Passion Factory" + }, + "seo": { + "title": "Claude Code プラグインマーケットプレイス", + "description": "Claude Codeの機能を拡張するプラグインを見つけてインストール" + }, + "a11y": { + "viewOnGithub": "GitHubで見る", + "clearSearch": "検索をクリア" + } +} diff --git a/apps/web/app/locales/ko.json b/apps/web/app/locales/ko.json new file mode 100644 index 00000000..a6b888ab --- /dev/null +++ b/apps/web/app/locales/ko.json @@ -0,0 +1,78 @@ +{ + "hero": { + "title": "Claude Code 플러그인 마켓플레이스", + "description": "Claude Code의 기능을 확장하는 플러그인을 탐색하고 설치하세요. 커뮤니티 기여 플러그인 컬렉션을 둘러보세요.", + "headline": "플러그인 탐색" + }, + "search": { + "placeholder": "이름, 설명 또는 저장소로 검색...", + "filterByMarketplace": "마켓플레이스 필터:", + "allMarketplaces": "전체 마켓플레이스", + "filteringBy": "필터 적용:", + "result": "개 결과", + "results": "개 결과", + "clearSearch": "검색 초기화" + }, + "plugin": { + "noDescription": "설명이 없습니다", + "viewSource": "소스 보기", + "install": "설치", + "loading": "로딩 중...", + "viewInstallInstructions": "설치 방법 보기", + "badge": { + "fromMarketplace": "{name} 마켓플레이스 제공", + "developedByGoogle": "Google 개발", + "developedByAnthropic": "Anthropic 개발", + "includesContextFile": "컨텍스트 파일 포함", + "includesMcpServer": "MCP 서버 포함", + "githubStars": "GitHub 스타 {count}개" + } + }, + "installModal": { + "title": "설치 안내", + "description": "아래 두 명령어를 순서대로 실행하세요", + "step1": "마켓플레이스 추가", + "step2": "플러그인 설치", + "copyCommand": "명령어 복사", + "copied": "복사됨!", + "copyAllCommands": "전체 명령어 복사", + "allCommandsCopied": "전체 명령어 복사 완료!", + "close": "닫기", + "clipboardNotAvailable": "클립보드 사용 불가", + "clipboardWarning": "브라우저에서 자동 복사를 지원하지 않습니다. Ctrl+C (Mac: Cmd+C)로 직접 복사해주세요.", + "tip": "팁: Ctrl+C / Cmd+C로 명령어를 직접 선택하여 복사할 수도 있습니다" + }, + "alert": { + "communityPlugins": "커뮤니티 플러그인", + "communityPluginsDescription": "이 플러그인은 커뮤니티에서 기여한 것입니다. 설치 전에 소스 코드와 문서를 검토해주세요." + }, + "loading": { + "loadingPlugins": "플러그인 로딩 중..." + }, + "error": { + "failedToLoadPlugins": "플러그인 로드 실패", + "pluginNotFound": "플러그인을 찾을 수 없음", + "pluginNotFoundDescription": "플러그인 \"{name}\"을(를) 찾을 수 없습니다. 삭제되었거나 링크가 올바르지 않을 수 있습니다.", + "navigationIssue": "탐색 문제", + "navigationIssueDescription": "플러그인 \"{name}\"을(를) 찾았지만 표시할 수 없습니다. 페이지를 새로고침해보세요.", + "copyFailed": "복사 실패", + "copyFailedDescription": "클립보드에 복사할 수 없습니다. 명령어를 직접 복사해주세요.", + "copyAllFailedDescription": "명령어를 클립보드에 복사할 수 없습니다. 위에서 직접 복사해주세요." + }, + "empty": { + "noPluginsFound": "플러그인을 찾을 수 없음", + "noPluginsMatchQuery": "검색어 '{query}'에 일치하는 플러그인이 없습니다" + }, + "footer": { + "maintainedBy": "마켓플레이스 관리:", + "maintainerName": "Passion Factory" + }, + "seo": { + "title": "Claude Code 플러그인 마켓플레이스", + "description": "Claude Code 기능을 확장하는 플러그인을 탐색하고 설치하세요" + }, + "a11y": { + "viewOnGithub": "GitHub에서 보기", + "clearSearch": "검색 초기화" + } +} diff --git a/apps/web/app/locales/zh.json b/apps/web/app/locales/zh.json new file mode 100644 index 00000000..c86041b6 --- /dev/null +++ b/apps/web/app/locales/zh.json @@ -0,0 +1,78 @@ +{ + "hero": { + "title": "Claude Code 插件市场", + "description": "发现并安装扩展 Claude Code 功能的插件。浏览我们的社区贡献插件集合。", + "headline": "探索插件" + }, + "search": { + "placeholder": "按名称、描述或仓库搜索...", + "filterByMarketplace": "按市场筛选:", + "allMarketplaces": "所有市场", + "filteringBy": "筛选条件:", + "result": "个结果", + "results": "个结果", + "clearSearch": "清除搜索" + }, + "plugin": { + "noDescription": "暂无描述", + "viewSource": "查看源码", + "install": "安装", + "loading": "加载中...", + "viewInstallInstructions": "查看安装说明", + "badge": { + "fromMarketplace": "来自 {name} 市场", + "developedByGoogle": "Google 开发", + "developedByAnthropic": "Anthropic 开发", + "includesContextFile": "包含上下文文件", + "includesMcpServer": "包含 MCP 服务器", + "githubStars": "{count} 个 GitHub 星标" + } + }, + "installModal": { + "title": "安装说明", + "description": "按顺序运行以下两个命令", + "step1": "添加市场", + "step2": "安装插件", + "copyCommand": "复制命令", + "copied": "已复制!", + "copyAllCommands": "复制所有命令", + "allCommandsCopied": "所有命令已复制!", + "close": "关闭", + "clipboardNotAvailable": "剪贴板不可用", + "clipboardWarning": "您的浏览器不支持自动复制。请使用 Ctrl+C(Mac 上为 Cmd+C)手动复制。", + "tip": "提示:您也可以手动选择并复制命令(Ctrl+C / Cmd+C)" + }, + "alert": { + "communityPlugins": "社区插件", + "communityPluginsDescription": "这些插件由社区贡献。安装前请查看源代码和文档。" + }, + "loading": { + "loadingPlugins": "正在加载插件..." + }, + "error": { + "failedToLoadPlugins": "加载插件失败", + "pluginNotFound": "未找到插件", + "pluginNotFoundDescription": "无法找到插件"{name}"。该插件可能已被移除或链接不正确。", + "navigationIssue": "导航问题", + "navigationIssueDescription": "找到了插件"{name}"但无法显示。请尝试刷新页面。", + "copyFailed": "复制失败", + "copyFailedDescription": "无法复制到剪贴板。请手动复制命令。", + "copyAllFailedDescription": "无法将命令复制到剪贴板。请从上方手动复制。" + }, + "empty": { + "noPluginsFound": "未找到插件", + "noPluginsMatchQuery": "没有与搜索词"{query}"匹配的插件" + }, + "footer": { + "maintainedBy": "市场维护方:", + "maintainerName": "Passion Factory" + }, + "seo": { + "title": "Claude Code 插件市场", + "description": "发现并安装扩展 Claude Code 功能的插件" + }, + "a11y": { + "viewOnGithub": "在 GitHub 上查看", + "clearSearch": "清除搜索" + } +} From cbbbf949240fb8d0c983344616aa3a9c173cff2a Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Thu, 26 Mar 2026 17:36:07 +0900 Subject: [PATCH 06/11] feat(web): replace hardcoded strings with i18n $t() calls Extract all UI strings from index.vue, PluginCard.vue, PluginSearch.vue, and InstallModal.vue to use i18n translation function calls referencing locale JSON files. --- apps/web/app/components/InstallModal.vue | 32 +++++++++--------- apps/web/app/components/PluginCard.vue | 24 +++++++------ apps/web/app/components/PluginSearch.vue | 8 ++--- apps/web/app/pages/index.vue | 43 ++++++++++++------------ 4 files changed, 56 insertions(+), 51 deletions(-) diff --git a/apps/web/app/components/InstallModal.vue b/apps/web/app/components/InstallModal.vue index f4cb8f94..c23633d3 100644 --- a/apps/web/app/components/InstallModal.vue +++ b/apps/web/app/components/InstallModal.vue @@ -1,4 +1,6 @@ + + diff --git a/apps/web/app/pages/index.vue b/apps/web/app/pages/index.vue index 813ad3e7..e596d6f1 100644 --- a/apps/web/app/pages/index.vue +++ b/apps/web/app/pages/index.vue @@ -247,6 +247,7 @@ useHead({ variant="ghost" :aria-label="$t('a11y.viewOnGithub')" /> + From 31c3c8fe0587a29a75b45b0cabade94fadb2b3d2 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Thu, 26 Mar 2026 17:41:42 +0900 Subject: [PATCH 08/11] fix(web): move locale files to i18n/locales/ for @nuxtjs/i18n compat The @nuxtjs/i18n module resolves langDir relative to its own i18n/ base directory. Also fix zh.json unicode quote issue that caused build parser failure. --- apps/web/app/locales/zh.json | 78 -------------------------- apps/web/{app => i18n}/locales/en.json | 0 apps/web/{app => i18n}/locales/ja.json | 0 apps/web/{app => i18n}/locales/ko.json | 0 apps/web/i18n/locales/zh.json | 78 ++++++++++++++++++++++++++ 5 files changed, 78 insertions(+), 78 deletions(-) delete mode 100644 apps/web/app/locales/zh.json rename apps/web/{app => i18n}/locales/en.json (100%) rename apps/web/{app => i18n}/locales/ja.json (100%) rename apps/web/{app => i18n}/locales/ko.json (100%) create mode 100644 apps/web/i18n/locales/zh.json diff --git a/apps/web/app/locales/zh.json b/apps/web/app/locales/zh.json deleted file mode 100644 index c86041b6..00000000 --- a/apps/web/app/locales/zh.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "hero": { - "title": "Claude Code 插件市场", - "description": "发现并安装扩展 Claude Code 功能的插件。浏览我们的社区贡献插件集合。", - "headline": "探索插件" - }, - "search": { - "placeholder": "按名称、描述或仓库搜索...", - "filterByMarketplace": "按市场筛选:", - "allMarketplaces": "所有市场", - "filteringBy": "筛选条件:", - "result": "个结果", - "results": "个结果", - "clearSearch": "清除搜索" - }, - "plugin": { - "noDescription": "暂无描述", - "viewSource": "查看源码", - "install": "安装", - "loading": "加载中...", - "viewInstallInstructions": "查看安装说明", - "badge": { - "fromMarketplace": "来自 {name} 市场", - "developedByGoogle": "Google 开发", - "developedByAnthropic": "Anthropic 开发", - "includesContextFile": "包含上下文文件", - "includesMcpServer": "包含 MCP 服务器", - "githubStars": "{count} 个 GitHub 星标" - } - }, - "installModal": { - "title": "安装说明", - "description": "按顺序运行以下两个命令", - "step1": "添加市场", - "step2": "安装插件", - "copyCommand": "复制命令", - "copied": "已复制!", - "copyAllCommands": "复制所有命令", - "allCommandsCopied": "所有命令已复制!", - "close": "关闭", - "clipboardNotAvailable": "剪贴板不可用", - "clipboardWarning": "您的浏览器不支持自动复制。请使用 Ctrl+C(Mac 上为 Cmd+C)手动复制。", - "tip": "提示:您也可以手动选择并复制命令(Ctrl+C / Cmd+C)" - }, - "alert": { - "communityPlugins": "社区插件", - "communityPluginsDescription": "这些插件由社区贡献。安装前请查看源代码和文档。" - }, - "loading": { - "loadingPlugins": "正在加载插件..." - }, - "error": { - "failedToLoadPlugins": "加载插件失败", - "pluginNotFound": "未找到插件", - "pluginNotFoundDescription": "无法找到插件"{name}"。该插件可能已被移除或链接不正确。", - "navigationIssue": "导航问题", - "navigationIssueDescription": "找到了插件"{name}"但无法显示。请尝试刷新页面。", - "copyFailed": "复制失败", - "copyFailedDescription": "无法复制到剪贴板。请手动复制命令。", - "copyAllFailedDescription": "无法将命令复制到剪贴板。请从上方手动复制。" - }, - "empty": { - "noPluginsFound": "未找到插件", - "noPluginsMatchQuery": "没有与搜索词"{query}"匹配的插件" - }, - "footer": { - "maintainedBy": "市场维护方:", - "maintainerName": "Passion Factory" - }, - "seo": { - "title": "Claude Code 插件市场", - "description": "发现并安装扩展 Claude Code 功能的插件" - }, - "a11y": { - "viewOnGithub": "在 GitHub 上查看", - "clearSearch": "清除搜索" - } -} diff --git a/apps/web/app/locales/en.json b/apps/web/i18n/locales/en.json similarity index 100% rename from apps/web/app/locales/en.json rename to apps/web/i18n/locales/en.json diff --git a/apps/web/app/locales/ja.json b/apps/web/i18n/locales/ja.json similarity index 100% rename from apps/web/app/locales/ja.json rename to apps/web/i18n/locales/ja.json diff --git a/apps/web/app/locales/ko.json b/apps/web/i18n/locales/ko.json similarity index 100% rename from apps/web/app/locales/ko.json rename to apps/web/i18n/locales/ko.json diff --git a/apps/web/i18n/locales/zh.json b/apps/web/i18n/locales/zh.json new file mode 100644 index 00000000..a958afaa --- /dev/null +++ b/apps/web/i18n/locales/zh.json @@ -0,0 +1,78 @@ +{ + "hero": { + "title": "Claude Code \u63d2\u4ef6\u5e02\u573a", + "description": "\u53d1\u73b0\u5e76\u5b89\u88c5\u6269\u5c55 Claude Code \u529f\u80fd\u7684\u63d2\u4ef6\u3002\u6d4f\u89c8\u6211\u4eec\u7684\u793e\u533a\u8d21\u732e\u63d2\u4ef6\u96c6\u5408\u3002", + "headline": "\u63a2\u7d22\u63d2\u4ef6" + }, + "search": { + "placeholder": "\u6309\u540d\u79f0\u3001\u63cf\u8ff0\u6216\u4ed3\u5e93\u641c\u7d22...", + "filterByMarketplace": "\u6309\u5e02\u573a\u7b5b\u9009\uff1a", + "allMarketplaces": "\u6240\u6709\u5e02\u573a", + "filteringBy": "\u7b5b\u9009\u6761\u4ef6\uff1a", + "result": "\u4e2a\u7ed3\u679c", + "results": "\u4e2a\u7ed3\u679c", + "clearSearch": "\u6e05\u9664\u641c\u7d22" + }, + "plugin": { + "noDescription": "\u6682\u65e0\u63cf\u8ff0", + "viewSource": "\u67e5\u770b\u6e90\u7801", + "install": "\u5b89\u88c5", + "loading": "\u52a0\u8f7d\u4e2d...", + "viewInstallInstructions": "\u67e5\u770b\u5b89\u88c5\u8bf4\u660e", + "badge": { + "fromMarketplace": "\u6765\u81ea {name} \u5e02\u573a", + "developedByGoogle": "Google \u5f00\u53d1", + "developedByAnthropic": "Anthropic \u5f00\u53d1", + "includesContextFile": "\u5305\u542b\u4e0a\u4e0b\u6587\u6587\u4ef6", + "includesMcpServer": "\u5305\u542b MCP \u670d\u52a1\u5668", + "githubStars": "{count} \u4e2a GitHub \u661f\u6807" + } + }, + "installModal": { + "title": "\u5b89\u88c5\u8bf4\u660e", + "description": "\u6309\u987a\u5e8f\u8fd0\u884c\u4ee5\u4e0b\u4e24\u4e2a\u547d\u4ee4", + "step1": "\u6dfb\u52a0\u5e02\u573a", + "step2": "\u5b89\u88c5\u63d2\u4ef6", + "copyCommand": "\u590d\u5236\u547d\u4ee4", + "copied": "\u5df2\u590d\u5236\uff01", + "copyAllCommands": "\u590d\u5236\u6240\u6709\u547d\u4ee4", + "allCommandsCopied": "\u6240\u6709\u547d\u4ee4\u5df2\u590d\u5236\uff01", + "close": "\u5173\u95ed", + "clipboardNotAvailable": "\u526a\u8d34\u677f\u4e0d\u53ef\u7528", + "clipboardWarning": "\u60a8\u7684\u6d4f\u89c8\u5668\u4e0d\u652f\u6301\u81ea\u52a8\u590d\u5236\u3002\u8bf7\u4f7f\u7528 Ctrl+C\uff08Mac \u4e0a\u4e3a Cmd+C\uff09\u624b\u52a8\u590d\u5236\u3002", + "tip": "\u63d0\u793a\uff1a\u60a8\u4e5f\u53ef\u4ee5\u624b\u52a8\u9009\u62e9\u5e76\u590d\u5236\u547d\u4ee4\uff08Ctrl+C / Cmd+C\uff09" + }, + "alert": { + "communityPlugins": "\u793e\u533a\u63d2\u4ef6", + "communityPluginsDescription": "\u8fd9\u4e9b\u63d2\u4ef6\u7531\u793e\u533a\u8d21\u732e\u3002\u5b89\u88c5\u524d\u8bf7\u67e5\u770b\u6e90\u4ee3\u7801\u548c\u6587\u6863\u3002" + }, + "loading": { + "loadingPlugins": "\u6b63\u5728\u52a0\u8f7d\u63d2\u4ef6..." + }, + "error": { + "failedToLoadPlugins": "\u52a0\u8f7d\u63d2\u4ef6\u5931\u8d25", + "pluginNotFound": "\u672a\u627e\u5230\u63d2\u4ef6", + "pluginNotFoundDescription": "\u65e0\u6cd5\u627e\u5230\u63d2\u4ef6\u300c{name}\u300d\u3002\u8be5\u63d2\u4ef6\u53ef\u80fd\u5df2\u88ab\u79fb\u9664\u6216\u94fe\u63a5\u4e0d\u6b63\u786e\u3002", + "navigationIssue": "\u5bfc\u822a\u95ee\u9898", + "navigationIssueDescription": "\u627e\u5230\u4e86\u63d2\u4ef6\u300c{name}\u300d\u4f46\u65e0\u6cd5\u663e\u793a\u3002\u8bf7\u5c1d\u8bd5\u5237\u65b0\u9875\u9762\u3002", + "copyFailed": "\u590d\u5236\u5931\u8d25", + "copyFailedDescription": "\u65e0\u6cd5\u590d\u5236\u5230\u526a\u8d34\u677f\u3002\u8bf7\u624b\u52a8\u590d\u5236\u547d\u4ee4\u3002", + "copyAllFailedDescription": "\u65e0\u6cd5\u5c06\u547d\u4ee4\u590d\u5236\u5230\u526a\u8d34\u677f\u3002\u8bf7\u4ece\u4e0a\u65b9\u624b\u52a8\u590d\u5236\u3002" + }, + "empty": { + "noPluginsFound": "\u672a\u627e\u5230\u63d2\u4ef6", + "noPluginsMatchQuery": "\u6ca1\u6709\u4e0e\u641c\u7d22\u8bcd\u300c{query}\u300d\u5339\u914d\u7684\u63d2\u4ef6" + }, + "footer": { + "maintainedBy": "\u5e02\u573a\u7ef4\u62a4\u65b9\uff1a", + "maintainerName": "Passion Factory" + }, + "seo": { + "title": "Claude Code \u63d2\u4ef6\u5e02\u573a", + "description": "\u53d1\u73b0\u5e76\u5b89\u88c5\u6269\u5c55 Claude Code \u529f\u80fd\u7684\u63d2\u4ef6" + }, + "a11y": { + "viewOnGithub": "\u5728 GitHub \u4e0a\u67e5\u770b", + "clearSearch": "\u6e05\u9664\u641c\u7d22" + } +} From 8739f62693b60e32ea89c23f1eb4230efdef9851 Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Thu, 26 Mar 2026 17:42:04 +0900 Subject: [PATCH 09/11] docs(track): web-i18n-20260326 sync tech-stack.md --- .please/docs/knowledge/tech-stack.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.please/docs/knowledge/tech-stack.md b/.please/docs/knowledge/tech-stack.md index 4fce7911..ca0dc17b 100644 --- a/.please/docs/knowledge/tech-stack.md +++ b/.please/docs/knowledge/tech-stack.md @@ -20,6 +20,7 @@ - **Vue** 3.5.x + **Vue Router** 5.x - **Nuxt UI** v4 (component library) - **Nuxt Content** (markdown content management) +- **@nuxtjs/i18n** 9.x (internationalization — en, ko, ja, zh) - **better-sqlite3** (local database) ## Testing From 5a509a7c3a78af961d31d52ad175448d2a6d8dac Mon Sep 17 00:00:00 2001 From: Minsu Lee Date: Thu, 26 Mar 2026 18:14:47 +0900 Subject: [PATCH 10/11] fix(web): apply review suggestions for i18n best practices - Use {count} interpolation for result count instead of manual singular/plural ternary (CJK-safe) - Use i18n-t component interpolation for footer text to support proper word order across locales - Wrap error.message in localized description template --- apps/web/app/components/PluginSearch.vue | 2 +- apps/web/app/pages/index.vue | 10 ++++++---- apps/web/i18n/locales/en.json | 7 +++---- apps/web/i18n/locales/ja.json | 7 +++---- apps/web/i18n/locales/ko.json | 7 +++---- apps/web/i18n/locales/zh.json | 7 +++---- 6 files changed, 19 insertions(+), 21 deletions(-) diff --git a/apps/web/app/components/PluginSearch.vue b/apps/web/app/components/PluginSearch.vue index 2131cc16..c93cf411 100644 --- a/apps/web/app/components/PluginSearch.vue +++ b/apps/web/app/components/PluginSearch.vue @@ -25,7 +25,7 @@ const searchQuery = defineModel({ default: '' }) variant="soft" size="sm" > - {{ filteredCount }} {{ filteredCount === 1 ? $t('search.result') : $t('search.results') }} + {{ $t('search.resultCount', { count: filteredCount }) }} @@ -336,9 +336,11 @@ useHead({
-

- {{ $t('footer.maintainedBy') }} {{ $t('footer.maintainerName') }} -

+ + + Date: Thu, 26 Mar 2026 21:01:10 +0900 Subject: [PATCH 11/11] feat(web): add hreflang SEO tags and locale key parity test - Add useLocaleHead() for automatic hreflang link generation (NFR-3) - Add i18n.test.ts verifying all locale files share identical keys (AC-7) --- apps/web/app/pages/index.vue | 6 ++++- apps/web/tests/i18n.test.ts | 47 ++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 apps/web/tests/i18n.test.ts diff --git a/apps/web/app/pages/index.vue b/apps/web/app/pages/index.vue index 98db0f86..68a25970 100644 --- a/apps/web/app/pages/index.vue +++ b/apps/web/app/pages/index.vue @@ -217,14 +217,18 @@ onBeforeUnmount(() => { pendingScrollTimer.value?.stop() }) -// SEO Meta +// SEO Meta + hreflang tags +const i18nHead = useLocaleHead({ addSeoAttributes: true }) useHead({ title: () => t('seo.title'), + htmlAttrs: { lang: i18nHead.value.htmlAttrs?.lang }, + link: [...(i18nHead.value.link ?? [])], meta: [ { name: 'description', content: () => t('seo.description'), }, + ...(i18nHead.value.meta ?? []), ], }) diff --git a/apps/web/tests/i18n.test.ts b/apps/web/tests/i18n.test.ts new file mode 100644 index 00000000..2985f7c3 --- /dev/null +++ b/apps/web/tests/i18n.test.ts @@ -0,0 +1,47 @@ +import { readFileSync, readdirSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' + +const localesDir = join(__dirname, '../i18n/locales') + +function getKeys(obj: Record, prefix = ''): string[] { + return Object.entries(obj).flatMap(([key, value]) => { + const fullKey = prefix ? `${prefix}.${key}` : key + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + return getKeys(value as Record, fullKey) + } + return [fullKey] + }) +} + +function loadLocale(filename: string): Record { + const content = readFileSync(join(localesDir, filename), 'utf-8') + return JSON.parse(content) +} + +describe('i18n locale files', () => { + const files = readdirSync(localesDir).filter(f => f.endsWith('.json')) + const locales = Object.fromEntries( + files.map(f => [f.replace('.json', ''), loadLocale(f)]), + ) + const referenceKeys = getKeys(locales.en!).sort() + + it('should have all expected locale files', () => { + expect(files.sort()).toEqual(['en.json', 'ja.json', 'ko.json', 'zh.json']) + }) + + it('should parse all locale files as valid JSON', () => { + for (const file of files) { + expect(() => loadLocale(file)).not.toThrow() + } + }) + + for (const [locale, data] of Object.entries(locales)) { + if (locale === 'en') continue + + it(`${locale}.json should have the same keys as en.json`, () => { + const keys = getKeys(data).sort() + expect(keys).toEqual(referenceKeys) + }) + } +})