Skip to content

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

Open
amondnet wants to merge 12 commits intomainfrom
worktree-web-i18n
Open

feat(web): add i18n support with 4 locales (en, ko, ja, zh)#107
amondnet wants to merge 12 commits intomainfrom
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
Copy link
Copy Markdown

vercel bot commented Mar 26, 2026

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

Project Deployment Actions Updated (UTC)
claude-code-plugins Ready Ready Preview, Comment Mar 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.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays 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.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

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.

Comment thread apps/web/nuxt.config.ts
],
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
@amondnet amondnet marked this pull request as ready for review March 26, 2026 11:51
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

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>
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 26, 2026

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
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 26, 2026

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)
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

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()
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Mar 26, 2026

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 free to 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