Skip to content

feat(web): add i18n support with 4 locales (en, ko, ja, zh) - #107

Open
amondnet wants to merge 12 commits into
mainfrom
worktree-web-i18n
Open

feat(web): add i18n support with 4 locales (en, ko, ja, zh)#107
amondnet wants to merge 12 commits into
mainfrom
worktree-web-i18n

Conversation

@amondnet

Copy link
Copy Markdown
Contributor

Summary

  • Add @nuxtjs/i18n module with prefix_except_default routing strategy
  • Support 4 locales: English (default), Korean, Japanese, Chinese (Simplified)
  • Extract ~35 UI strings from Vue components into lazy-loaded JSON locale files
  • Add LanguageSwitcher component in hero section
  • Configure browser language auto-detection with cookie persistence
  • Update ISR route rules for locale-prefixed paths
  • Add CJK font stack entries (Japanese + Chinese system fonts)

Test plan

  • Visit /ko — UI renders in Korean
  • Visit /ja — UI renders in Japanese
  • Visit /zh — UI renders in Chinese
  • Visit / — UI renders in English (default)
  • Language switcher changes locale and URL
  • Browser language detection redirects first-time visitors
  • bun run build completes without errors
  • Plugin names/descriptions remain untranslated across all locales

Track

web-i18n-20260326spec | plan

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.
Extract all UI strings from index.vue, PluginCard.vue,
PluginSearch.vue, and InstallModal.vue to use i18n translation
function calls referencing locale JSON files.
Build LanguageSwitcher using USelectMenu with locale options,
integrate into hero section, and extend font stack with Japanese
(Hiragino, Yu Gothic) and Chinese (PingFang SC, Microsoft YaHei)
system fonts.
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.
@vercel

vercelBot commented Mar 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
claude-code-pluginsReadyReadyPreview, CommentMar 26, 2026 1:21pm

Request Review

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the web application's accessibility for a global audience by integrating comprehensive internationalization capabilities. It allows users to experience the interface in their native language, improving usability and reach without impacting performance due to optimized routing and lazy-loaded translations.

Highlights

  • Internationalization (i18n) Support: Added the @nuxtjs/i18n module to enable multi-language support for the web application.
  • Locale Support: Implemented support for four locales: English (default), Korean, Japanese, and Chinese (Simplified).
  • UI String Extraction: Extracted approximately 35 UI strings from Vue components into lazy-loaded JSON locale files for translation.
  • Language Switching: Introduced a new LanguageSwitcher component in the hero section, allowing users to easily change the display language.
  • Browser Language Detection: Configured automatic browser language detection with cookie persistence to redirect first-time visitors to their preferred locale.
  • ISR Route Rules: Updated ISR (Incremental Static Regeneration) route rules to support locale-prefixed paths, ensuring proper caching for translated pages.
  • CJK Font Support: Added CJK (Chinese, Japanese, Korean) font stack entries to the main CSS to ensure correct rendering of these languages.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

FeatureCommandDescription
Code Review/gemini reviewPerforms a code review for the current pull request in its current state.
Pull Request Summary/gemini summaryProvides a summary of the current pull request in its current state.
Comment@gemini-code-assistResponds in comments when explicitly tagged, both in pull request comments and review comments.
Help/gemini helpDisplays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assistgemini-code-assistBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces internationalization (i18n) support to the web application using @nuxtjs/i18n. It adds new locale files for English, Korean, Japanese, and Chinese, updates the Nuxt configuration for i18n and ISR rules, and modifies several Vue components to use translated strings. Documentation for the i18n feature has also been added. Feedback includes a critical issue with the langDir configuration in nuxt.config.ts which needs to be corrected to i18n/locales, and a suggestion to leverage @nuxtjs/i18n's built-in pluralization feature for the search results count in PluginSearch.vue for cleaner and more maintainable code.

],
defaultLocale: 'en',
strategy: 'prefix_except_default',
langDir: 'locales',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The langDir seems to be configured incorrectly. The translation files are located in apps/web/i18n/locales/, but langDir is set to 'locales'. Since langDir is relative to the Nuxt project root (apps/web/), it should be 'i18n/locales'. Without this change, Nuxt will look for translations in apps/web/locales/ and fail to find them.

Suggested change
langDir: 'locales',
langDir: 'i18n/locales',

size="sm"
>
{{ filteredCount }} {{ filteredCount === 1 ? 'result' : 'results' }}
{{ filteredCount }} {{ filteredCount === 1 ? $t('search.result') : $t('search.results') }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better internationalization support and to fully leverage the @nuxtjs/i18n library, I recommend using its built-in pluralization feature. This will make the code cleaner and more maintainable, especially if you add languages with more complex pluralization rules in the future.

You can update your locale files like this:

en.json

"search": {
..."results": "1 result | {count} results",
...
}

(The result key can be removed.)

ko.json

"search": {
..."results": "{count}개 결과",
...
}

(The result key can be removed.)

Then, you can simplify the template code.

 {{ $t('search.results', filteredCount) }}

- 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

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 17 files

Confidence score: 4/5

  • This PR is safe to merge with minimal risk: both findings are low severity (4/10 and 3/10) and mainly affect wording/clarity rather than core runtime behavior.
  • The most user-facing issue is in apps/web/app/components/PluginSearch.vue, where concatenated translated fragments may produce awkward or incorrect grammar in some locales; switching to a single i18n key with {query} improves localization quality.
  • .please/docs/tracks/active/web-i18n-20260326/plan.md has conflicting English-route expectations (/ vs / or /en), which could cause implementation or QA confusion if not clarified.
  • Pay close attention to apps/web/app/components/PluginSearch.vue and .please/docs/tracks/active/web-i18n-20260326/plan.md - ensure i18n sentence composition and route expectations are aligned.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/web/app/components/PluginSearch.vue">
<violation number="1" location="apps/web/app/components/PluginSearch.vue:45">
P2: Avoid concatenating translated fragments with dynamic text for user-facing sentences; use one i18n message with a `{query}` placeholder so each locale can control grammar and spacing.</violation>
</file>
<file name=".please/docs/tracks/active/web-i18n-20260326/plan.md">
<violation number="1" location=".please/docs/tracks/active/web-i18n-20260326/plan.md:90">
P3: The plan has conflicting English-route expectations (`/` only vs `/` or `/en`). Align this line with the routing strategy to avoid ambiguous implementation and QA validation.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Client as Browser (User)
participant Nuxt as Nuxt Server / ISR
participant i18n as @nuxtjs/i18n Module
participant Locales as Locale JSON Files
participant API as Marketplace API
Note over Client, API: Initial Visit (Browser Language Detection)
Client->>Nuxt: GET / (Accept-Language: ko)
Nuxt->>i18n: NEW: detectBrowserLanguage()
i18n->>i18n: Check 'i18n_redirected' cookie
alt Cookie missing & Locale is non-default
i18n-->>Nuxt: Redirect to /ko
Nuxt-->>Client: NEW: 302 Redirect to /ko + Set Cookie
end
Note over Client, API: Rendering localized page (ISR)
Client->>Nuxt: GET /ko
Nuxt->>i18n: Resolve locale context ('ko')
i18n->>Locales: NEW: Lazy-load ko.json
Locales-->>i18n: Locale keys ($t)
Nuxt->>API: fetch("/api/marketplaces")
API-->>Nuxt: Plugin data (Untranslated)
Nuxt->>Nuxt: CHANGED: Render components with $t()
Nuxt-->>Client: Localized HTML (Cached via ISR)
Note over Client, API: Manual Language Switching
Client->>Client: Click LanguageSwitcher.vue
Client->>i18n: NEW: useSwitchLocalePath('ja')
i18n-->>Client: Returns "/ja"
Client->>Nuxt: navigateTo("/ja")
Nuxt->>i18n: Update locale to 'ja'
i18n->>Locales: NEW: Lazy-load ja.json
Nuxt-->>Client: Updated UI in Japanese
Note over Client, API: Failure Path (Missing Translation)
Client->>Nuxt: GET /zh
Nuxt->>i18n: Request missing key
i18n-->>Nuxt: CHANGED: Fallback to 'en' key/string
Nuxt-->>Client: Rendered HTML with fallback text
Loading

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

<div v-if="searchQuery" class="mt-2 text-xs text-muted flex items-center gap-1">
<UIcon name="i-heroicons-funnel" />
<span>Filtering by: <span class="font-semibold">{{ searchQuery }}</span></span>
<span>{{ $t('search.filteringBy') }} <span class="font-semibold">{{ searchQuery }}</span></span>

@cubic-dev-aicubic-dev-aiBotMar 26, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Avoid concatenating translated fragments with dynamic text for user-facing sentences; use one i18n message with a {query} placeholder so each locale can control grammar and spacing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/app/components/PluginSearch.vue, line 45:
<comment>Avoid concatenating translated fragments with dynamic text for user-facing sentences; use one i18n message with a `{query}` placeholder so each locale can control grammar and spacing.</comment>
<file context>
@@ -42,7 +42,7 @@ const searchQuery = defineModel<string>({ default: '' })
<div v-if="searchQuery" class="mt-2 text-xs text-muted flex items-center gap-1">
<UIcon name="i-heroicons-funnel" />
- <span>Filtering by: <span class="font-semibold">{{ searchQuery }}</span></span>
+ <span>{{ $t('search.filteringBy') }} <span class="font-semibold">{{ searchQuery }}</span></span>
</div>
</div>
</file context>
Fix with Cubic

- 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

@cubic-dev-aicubic-dev-aiBotMar 26, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The plan has conflicting English-route expectations (/ only vs / or /en). Align this line with the routing strategy to avoid ambiguous implementation and QA validation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .please/docs/tracks/active/web-i18n-20260326/plan.md, line 90:
<comment>The plan has conflicting English-route expectations (`/` only vs `/` or `/en`). Align this line with the routing strategy to avoid ambiguous implementation and QA validation.</comment>
<file context>
@@ -0,0 +1,114 @@
+- 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
+
</file context>
Fix with Cubic

- Add useLocaleHead() for automatic hreflang link generation (NFR-3)
- Add i18n.test.ts verifying all locale files share identical keys (AC-7)

@cubic-dev-aicubic-dev-aiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/web/tests/i18n.test.ts">
<violation number="1" location="apps/web/tests/i18n.test.ts:27">
P2: Avoid computing `referenceKeys` with a non-null assertion before verifying `en.json` exists; a missing/invalid `en.json` will crash the suite before the file-existence test runs.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

const locales = Object.fromEntries(
files.map(f => [f.replace('.json', ''), loadLocale(f)]),
)
const referenceKeys = getKeys(locales.en!).sort()

@cubic-dev-aicubic-dev-aiBotMar 26, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Avoid computing referenceKeys with a non-null assertion before verifying en.json exists; a missing/invalid en.json will crash the suite before the file-existence test runs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/tests/i18n.test.ts, line 27:
<comment>Avoid computing `referenceKeys` with a non-null assertion before verifying `en.json` exists; a missing/invalid `en.json` will crash the suite before the file-existence test runs.</comment>
<file context>
@@ -0,0 +1,47 @@
+ 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', () => {
</file context>
Fix with Cubic

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@amondnet