Skip to content

feat(i18n): translate the app into Japanese - #649

Draft
alukach wants to merge 5 commits into
mainfrom
feat/i18n-japanese
Draft

alukach wants to merge 5 commits into
mainfrom
feat/i18n-japanese

Conversation

@alukach

@alukach alukach commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Part of #650.

A user asked for the app in Japanese. This adds next-intl and translates the whole UI (1,056 strings in 139 namespaces) into Japanese, with a language switcher in the footer. It also lays the groundwork so that each additional language is just one more JSON file.

How the locale is chosen

There is no /ja/... URL prefix. Accounts own the top level of the path (/{account}/{product}), so a locale prefix would collide with any account named ja, de or fr, and it would fight the redirects and rewrites in src/middleware.ts. Instead, src/i18n/request.ts resolves the locale for each request:

  1. The NEXT_LOCALE cookie, which the footer's LocaleSwitcher writes client-side before calling router.refresh(). No server action is involved.
  2. Otherwise, the first supported language in the browser's Accept-Language header.
  3. Otherwise, English.

A link can choose the language too: ?lang=ja on any URL is handled in src/middleware.ts. It sets the same cookie the switcher does and redirects to the same URL without the parameter, so the address bar stays clean and the parameter isn't passed along when the page is shared again. Unsupported values are dropped without changing anything. For example, https://source.coop/products?lang=ja opens the product listing in Japanese.

<html lang> follows the chosen locale. Search engines only index the English pages; if localized search results ever matter, ?hl= plus hreflang can be added then.

What changed

  • Plumbing. src/i18n/locales.ts holds the locale list and the negotiation logic (unit-tested). src/i18n/request.ts is next-intl's request config. src/global.d.ts types every t() key against en.json, so a typo or a missing key fails npm run type-check. A test fails if ja.json and en.json don't have exactly the same keys.
  • Strings. Every user-visible string moved into src/i18n/messages/{en,ja}.json: the app shell, marketing page, product pages, object browser, uploader, analytics, settings, memberships, service accounts, data connections, admin tools, page metadata, and the messages server actions return to forms. Sentences that used to be built from fragments are now single ICU messages, with placeholders, plurals and rich-text tags, because Japanese word order differs.
  • Dates and numbers. Formatting pinned to en-US now goes through next-intl's formatter in the reader's locale. Server and client format with the same locale and a fixed UTC time zone, so hydration stays deterministic.
  • Tests. jest.setup.ts mocks next-intl and next-intl/server with translators built from the real en.json, so existing assertions on English text keep passing unchanged.
  • Storybook. A Language toolbar switches every story between English and 日本語.

Visible changes in English

  • Dates read "Oct 8, 2026" (the en locale default) instead of the hand-built "8 Oct 2026".
  • The product count is pluralized: "1 product", not "1 products".
  • The "Copied!" tooltip is now "Copied".

Left untranslated on purpose

  • Not UI text: user content (product titles, descriptions, READMEs, account names), brand names, identifiers and code snippets.
  • Must stay English: HTTP API responses under src/app/api, zod schema messages in src/types (shared with the API), log messages, and OG images.
  • Quotes from real people: the case-study quotes on the landing page. Translating them would put words in people's mouths.
  • Shared with the API: serviceAccountGrantProblem() in src/lib/accounts/service-accounts.ts is shared by the server actions and the HTTP API, so its two rare service-account grant errors stay English in both. Since feat(api): operations layer, generated OpenAPI docs, and the memberships API #642, membership errors (invite, accept, reject, revoke) also come from the operations layer the API shares, so they stay English as well; only the success messages for invite and revoke are translated.
  • Byte sizes: formatBytes units (KB, MB) are unchanged.
  • Login: login and registration are Ory's hosted pages at auth.source.coop. Their language is configured in the Ory console, not in this repo.

Costs to know about

  • Every page now renders dynamically. Reading the cookie and Accept-Language opts every route out of static rendering. Most app routes were already dynamic through getPageSession(). The marketing homepage and /products were not.
  • Every page ships the whole catalog. NextIntlClientProvider sends all messages for the active locale in the page payload, about 21 KB gzipped for Japanese. Passing each client subtree only the namespaces it uses is the upgrade path if that shows up in page weight.

Translations

An LLM produced the Japanese against a fixed glossary (product = プロダクト, organization = 組織, data connection = データ接続, service account = サービスアカウント, restricted = 制限付き, …). It uses polite です/ます for sentences and concise noun forms for buttons and labels. A native speaker should review it before it's announced; the user who asked for Japanese is the obvious reviewer.

Stories

Language toolbar set to 日本語:

Service account API keys
Service account detail in Japanese API key list in Japanese
Usage panel New product
Usage panel in Japanese Product creation form in Japanese
Data connection Directory listing
Data connection form in Japanese Directory listing in Japanese

Testing

All checks were run against the current head of the branch.

  • Type-check: npm run type-check passes.
  • Jest: npx jest --forceExit passed 85 suites and 862 tests at 44d2c1c, including the locale negotiation, en/ja key-parity and ?lang= middleware tests. After merging main (feat(api): operations layer, generated OpenAPI docs, and the memberships API #642), I re-ran type-check plus the membership action and i18n suites, which pass. I haven't re-run the full suite since that merge.
  • Lint: npm run lint reports no errors. Twelve touched files have warnings (unused vars, hook deps, no-explicit-any). I spot-checked the unused vars against main, and they were unused there too.
  • Storybook: storybook build succeeds. The stories above were rendered headless with the toolbar set to 日本語; those renders are the screenshots.
  • Not run: I didn't run the app against the dev server, which belongs to whoever is at the keyboard. To check the real app, open any page with ?lang=ja, pick 日本語 in the footer, or set the browser's language to Japanese with no cookie.

Docs and ADRs

  • docs.source.coop: I searched it for the UI language and found nothing that describes it, so no page goes stale. A short "Changing the display language" page under docs/using-source/ would be a natural follow-up once the translation has been reviewed.
  • public/llms.txt: checked. It lists docs and API tools for agents, which use the English API, so it has no UI-language line to add.
  • Data-proxy ADRs: I checked the list (001 to 014 and rfc-001). They cover credentials, runtimes, authorization, outbound storage, analytics, identity providers, API keys and service accounts. None of them concerns UI language, and this PR moves none of those decisions.

🤖 Generated with Claude Code

alukach and others added 2 commits October 8, 2026 23:20
…panese catalog

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Moves every user-visible string in the app shell, product pages, object browser, uploader, analytics, settings, service accounts, data connections, admin tools and server-action responses into next-intl catalogs, with a Japanese translation for each. Dates and numbers that were pinned to en-US now format in the reader's locale. User content, brand names, API responses and log messages stay as they are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
source-coop-ui Ready Ready Preview Oct 9, 2026 8:11pm UTC
source-cooperative Ready Ready Preview Oct 9, 2026 8:11pm UTC

Request Review

@claude

claude Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @alukach's task in 20s —— View job


✅ No blocking issues — safe to merge.

I read the locale plumbing (src/i18n/locales.ts, src/i18n/request.ts), the root layout, metadata, DateText, the ?lang= middleware handler and LocaleSwitcher. I did not read the other ~160 changed files line by line, and I did not run tests or the type-check.

Notes (non-blocking)

  • src/middleware.ts handleLocaleParam redirects any non-/api request that carries a lang query parameter, including unsupported values, which are dropped. A page or product route that uses ?lang= for something else would lose it. This is unlikely today, but a short comment saying the parameter is reserved would make it deliberate.
  • src/i18n/locales.ts negotiateLocale reads Accept-Language by splitting on ;q=. A header written en; q=0.8 (with a space) gives Number(" 0.8"), which is still 0.8, so it is fine. A malformed q gives NaN, and NaN > 0 is false, so that entry is skipped. This is safe.
  • The cookie is written both in LocaleSwitcher and in the middleware with duplicated attributes (max-age=31536000, lax). If the two drift apart, behaviour will be inconsistent. A shared constant in locales.ts would prevent that.
  • src/app/metadata.ts keeps the OpenGraph and Twitter cards in English while only description is localized. This is documented in the JSDoc and is a reasonable choice.

Simplify (ponytail)

  • src/middleware.ts / LocaleSwitcher.tsx: cookie max-age and options are written in two places. Export a LOCALE_COOKIE_MAX_AGE constant from src/i18n/locales.ts and use it in both.

Docs

  • The PR description names the docs it checked: docs.source.coop (no page on UI language; a follow-up page is suggested) and the data-proxy ADRs 001–014 and rfc-001 (none moved). Nothing is missing there.
  • public/llms.txt is not mentioned in the description. Language selection via ?lang=ja is a link-level feature an agent could use. Consider a line in llms.txt, or a note in the description saying it was checked and still holds.

💰 Estimated review cost: $0.16 · 0m20s · 6 turns

A ?lang=ja query parameter on any URL sets the locale cookie the footer switcher writes and redirects to the same URL without the parameter, so a sender can choose the language a link opens in and the parameter isn't passed along when the page is shared again. Unsupported values are dropped without changing anything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	jest.config.cjs
#	src/lib/actions/memberships.ts

This branch was successfully deployed

2 active deployments
Preview – source-cooperative — 0b281d3f Deployed Oct 9, 2026 by vercel[bot]
Preview – source-coop-ui — 0b281d3f Deployed Oct 9, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant