Skip to content

feat: replace static locale bundles with dynamic worker translations#1884

Open
riderx wants to merge 19 commits intomainfrom
codex/dynamic-ai-translation
Open

feat: replace static locale bundles with dynamic worker translations#1884
riderx wants to merge 19 commits intomainfrom
codex/dynamic-ai-translation

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Apr 1, 2026

Summary (AI generated)

  • replace the repo-managed locale bundle system with a single English source catalog and a lightweight vue-i18n compatibility shim
  • add a public Cloudflare worker translation endpoint backed by Workers AI and edge cache for the top 30 supported languages
  • translate approved page text and common accessibility attributes at runtime on language change, while restoring original English source content when users switch back
  • add safety guards so non-Wrangler/Supabase-only environments disable runtime translation without breaking the UI, and so only approved source strings leave the browser by default
  • remove obsolete static locale assets and inlang config, and add focused unit coverage for language normalization and translation payload handling

Motivation (AI generated)

Maintaining large static translation catalogs in the repo is expensive and quickly drifts from the actual UI. This change moves translation to the edge so Capgo can ship English source text once, translate on demand with Workers AI, and cache the result close to the user instead of versioning locale files in the application.

Business Impact (AI generated)

This reduces translation maintenance cost, expands language coverage to the highest-priority developer languages without adding more repo weight, and makes localized experiences available immediately after cache warmup. It also keeps translation latency and cost bounded by reusing Cloudflare cache instead of translating every request from origin, while preserving a safe English fallback when the worker route is unavailable.

Test Plan (AI generated)

  • bun run lint
  • bun run lint:backend
  • bun run typecheck
  • bunx vitest run tests/dynamic-translation.unit.test.ts
  • bun run build
  • Verified the local fallback path keeps the app in English when /translation/page is unavailable instead of breaking navigation or rendering.
  • Verified runtime requests now send route.path only and do not include query-string or hash tokens.
  • Verified the browser-side translation scanner only sends approved source-catalog strings by default, with optional opt-in support for additional static chrome via data-capgo-translate.
  • Verified transient translation failures back off and stop retrying after the configured retry table is exhausted.

Screenshots (AI generated)

  • N/A. This change is primarily runtime translation, routing, and worker/cache behavior rather than a new visual workflow.

Generated with AI

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a page-translation system: client-side DOM translation composable invoked on app mount, a Supabase worker translation API using Workers AI with caching and rate-limits, a new /translation route and AI binding, a lightweight replacement for vue-i18n/shims, tests, and related config/templating updates.

Changes

Cohort / File(s) Summary
Cloudflare / API routing
cloudflare_workers/api/index.ts, cloudflare_workers/api/wrangler.jsonc
Registered translation route on the public Hono app (/translation) and added Wrangler ai.binding config (AI).
Translation backend (Supabase Worker)
supabase/functions/_backend/public/translation.ts, supabase/functions/_backend/utils/cloudflare.ts
New POST /page handler: input normalization/validation, token protection/restoration, batching, Workers AI calls, deterministic request hashing + cache read/write, IP rate-limiting; added optional AI binding type.
Client-page translation
src/composables/usePageTranslation.ts, src/App.vue
Added usePageTranslation() composable (DOM scanning, candidate extraction, dedupe, token handling, debounced/cancellable requests, MutationObserver, apply/restore translations); called from App.vue.
i18n replacement & shims
src/modules/i18n.ts, src/shims/vueI18n.ts, tsconfig.json, vite.config.mts
Replaced vue-i18n runtime with static message catalog and language-normalization utilities; added shim src/shims/vueI18n.ts, aliased vue-i18n to shim, removed Vite i18n plugin and filesystem locale discovery.
Tests
tests/dynamic-translation.unit.test.ts
Unit tests for language normalization, worker language mapping, string normalization/deduping, token protect/restore, and segmented translation parsing.
UI & typings
src/components/LangSelector.vue, src/pages/accountDisabled.vue, src/services/i18n.ts, src/components.d.ts, src/auto-imports.d.ts
Minor UI/attribute tweaks (data-capgo-no-translate, type="button"), replaced i18n-t usage with composed strings, changed emoji mapping and changeLanguage return, added/removed global type declarations.
FormKit & locale files
formkit.config.ts, messages/en.json, messages/README.md, project.inlang/settings.json, package.json
Hardcoded FormKit locale to en; added account-deletion keys in en.json; removed i18n docs and Inlang config; removed vue-i18n and @intlify/unplugin-vue-i18n deps.
Supabase templates & worktree
supabase/config.toml, scripts/supabase-worktree.ts, templates/*
Updated Supabase email template content_path entries to ./templates/*.html, added templates symlink in worktree script, and added templates directory.
Build & TS config
tsconfig.json, vite.config.mts
Added vue-i18n path alias to shim, removed plugin-based locale discovery and plugin config, and removed intlify message types from TS config.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Browser/Client
    participant App as Vue App
    participant Composable as usePageTranslation
    participant Cache as Cache Storage
    participant API as /translation Endpoint
    participant AI as Workers AI Model

    Browser->>App: Load / Navigate
    App->>Composable: init usePageTranslation()
    Composable->>Composable: Scan DOM → extract & normalize strings
    Composable->>Composable: Deduplicate, compute requestHash
    Composable->>Cache: GET translations by requestHash
    alt Cache Hit
        Cache-->>Composable: cached translations
    else Cache Miss
        Composable->>API: POST /translation/page {pagePath, targetLanguage, strings}
        API->>API: Protect tokens, segment & batch
        API->>AI: POST translate (model, batch)
        AI-->>API: Return translated_text
        API->>API: Restore tokens, map segments → translations
        API->>Cache: Store translations with TTL
        API-->>Composable: Return translations + requestHash
    end
    Composable->>Composable: Apply translations to DOM nodes/attributes
    rect rgba(200,150,255,0.5)
    Note over Composable,Browser: MutationObserver watches DOM and retriggers translation
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

enhancement

Poem

🐰 I hopped through strings both near and far,
I nudged the DOM and chased each star,
Tokens tucked in careful rows,
Workers hummed and language flows,
A rabbit’s patch to make pages spar.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the primary change: replacing static locale bundles with dynamic worker translations.
Description check ✅ Passed PR description is comprehensive, including summary, motivation, business impact, detailed test plan with checkmarks, and screenshots note. All required template sections are present and filled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/dynamic-ai-translation

Comment @coderabbitai help to get the list of available commands and usage tips.

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq bot commented Apr 1, 2026

Merging this PR will not alter performance

✅ 28 untouched benchmarks


Comparing codex/dynamic-ai-translation (50da775) with main (0d071b2)

Open in CodSpeed

@riderx riderx marked this pull request as ready for review April 1, 2026 21:18
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f3528351e6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

segment.element.setAttribute(segment.attr, translated)
}

const attrRecord = getAttributeRecord(segment.element, segment.attr, lang)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve attribute source before writing translated value

Calling getAttributeRecord() after setAttribute() makes the helper treat the translated attribute as a manual change and overwrite record.source with the translated text. That breaks language switching for attribute-based strings (placeholder, title, aria-label, and input value): when users switch back to English (or to another locale), restoreSourceContent() no longer has the original source value to restore, so attributes remain stuck in the previous translation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (7)
tests/dynamic-translation.unit.test.ts (1)

54-67: Prefer explicit typing over as any type assertion.

The as any cast on line 67 bypasses TypeScript's validation of the ProtectedEntry interface structure. Since the test entries already match the expected interface (per context snippet 4), you can type them properly.

♻️ Proposed fix
+import type { ProtectedEntry } from '../supabase/functions/_backend/public/translation.ts'
+
     const entries = [
       {
         marker: '[[CAPGO_SEGMENT_0000]]',
         protectedText: first.protectedText,
         source: 'Open settings for {app}',
         tokens: first.tokens,
       },
       {
         marker: '[[CAPGO_SEGMENT_0001]]',
         protectedText: second.protectedText,
         source: 'Bundle uploads',
         tokens: second.tokens,
       },
-    ] as any
+    ] satisfies ProtectedEntry[]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/dynamic-translation.unit.test.ts` around lines 54 - 67, The test uses a
loose `as any` on the entries array which hides type errors; replace it with an
explicit type annotation (e.g., declare `entries` as `ProtectedEntry[]`) and
remove the `as any` cast so TypeScript validates the shape matches the
`ProtectedEntry` interface; ensure you reference the same `first` and `second`
values and that `tokens`/`protectedText` fields conform to `ProtectedEntry`
(import or reference `ProtectedEntry` if needed).
src/pages/accountDisabled.vue (1)

113-122: Inconsistent translation approach within the same component.

This paragraph now uses hardcoded English text while the rest of the component still uses t() translation keys (e.g., lines 111, 125, 128, 140). This creates a mixed approach where some strings go through the i18n shim and others rely solely on dynamic page translation.

Consider either:

  1. Converting this text to use a translation key for consistency
  2. Documenting that hardcoded English is intentional for dynamic translation
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/accountDisabled.vue` around lines 113 - 122, The paragraph contains
hardcoded English while the rest of accountDisabled.vue uses the i18n helper
t(); replace the hardcoded strings with translation keys and call t() (for
example use t('accountDisabled.contactSupport') for the whole sentence and
t('accountDisabled.capgoSupport') for the link text) so the component is
consistent, or alternatively document intentional English usage — locate the
paragraph containing the anchor to https://support.capgo.app/ and update it to
use t() around both the surrounding sentence and the anchor text (keeping the
href and target attributes unchanged).
src/services/i18n.ts (1)

8-21: Hard page reload on language change affects UX.

The window.location.reload() call triggers a full page reload after changing the language. This is a deliberate design choice for the dynamic translation system, but it's a notable UX change from an in-place language switch.

A few observations:

  • The return value nextLanguage will never be used in browser environments since reload() interrupts execution
  • Any unsaved form state will be lost on language change

If this is the intended behavior for the dynamic translation architecture, consider adding a brief comment explaining the design rationale.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/i18n.ts` around lines 8 - 21, The changeLanguage function uses a
hard page reload (window.location.reload()) which interrupts execution and will
discard unsaved state and make the returned nextLanguage unused in the browser;
if this is intentional, add a brief comment above the reload explaining the
design rationale (e.g., dynamic translation requires a full reload to rehydrate
bundles/replace i18n context, and the trade-off of lost in-memory form state),
mention that getSelectedLanguage/normalizeLanguage/loadLanguageAsync are still
used before reload, and optionally document that callers should persist any
unsaved data before invoking changeLanguage.
src/modules/i18n.ts (2)

72-85: String params bypass interpolation - document this behavior.

When params is a string (line 73-74), interpolateMessage returns it directly, bypassing the message catalog entirely. This appears to be intentional for compatibility, but could be surprising. Consider adding a brief comment:

 function interpolateMessage(message: string, params?: MessageParams): string {
+  // String params override the message entirely (legacy compatibility)
   if (typeof params === 'string')
     return params
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/i18n.ts` around lines 72 - 85, The function interpolateMessage
currently treats a string params specially (if typeof params === 'string') and
returns params directly, bypassing the original message and any placeholder
interpolation; add a short comment above the typeof params === 'string' check
explaining this intentional compatibility behavior (that passing a string opts
out of interpolation and returns the string as-is), and ensure the comment
references interpolateMessage and the params parameter so future readers
understand why the code returns params instead of processing message
placeholders.

144-151: Fallback key transformation may produce unexpected results.

getSourceMessage transforms missing keys by replacing hyphens with spaces (line 150): key.replaceAll('-', ' '). For a key like error-not-found, this produces "error not found" which may be acceptable, but for keys like api-key-invalid, it produces "api key invalid" which lacks proper capitalization.

Consider whether this fallback behavior is intentional or if returning the raw key would be more appropriate for debugging:

-  return key.replaceAll('-', ' ')
+  return key  // Return raw key to make missing translations obvious
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/i18n.ts` around lines 144 - 151, The fallback in getSourceMessage
currently transforms missing keys with key.replaceAll('-', ' '), which can
produce misleading, uncapitalized strings; change the fallback to return the raw
key instead. Update the getSourceMessage function (which reads from
messageCatalog) so that if no resolved value and no defaultMessage exist it
returns key unchanged (remove the replaceAll transformation) to preserve the
original identifier for clearer debugging and consistency.
supabase/functions/_backend/public/translation.ts (2)

208-223: Sequential batch processing is acceptable but consider parallel execution for large payloads.

The current sequential for...of loop processes batches one at a time. For pages with many strings hitting multiple batches, this adds latency. If Workers AI supports concurrent requests, consider parallel execution with Promise.all:

-  for (const batch of batches) {
-    const segmentedText = buildSegmentedText(batch)
-    const result = await ai.run(TRANSLATION_MODEL, {
-      text: segmentedText,
-      source_lang: 'en',
-      target_lang: targetLanguage,
-    }) as { translated_text?: string }
-
-    const translatedText = typeof result?.translated_text === 'string'
-      ? result.translated_text
-      : ''
-
-    parseSegmentedTranslation(translatedText, batch).forEach((value, key) => {
-      translations.set(key, value)
-    })
-  }
+  const batchResults = await Promise.all(batches.map(async (batch) => {
+    const segmentedText = buildSegmentedText(batch)
+    const result = await ai.run(TRANSLATION_MODEL, {
+      text: segmentedText,
+      source_lang: 'en',
+      target_lang: targetLanguage,
+    }) as { translated_text?: string }
+
+    const translatedText = typeof result?.translated_text === 'string'
+      ? result.translated_text
+      : ''
+
+    return parseSegmentedTranslation(translatedText, batch)
+  }))
+
+  batchResults.forEach(batchTranslations => {
+    batchTranslations.forEach((value, key) => {
+      translations.set(key, value)
+    })
+  })

However, sequential may be intentional to respect rate limits. If so, a brief comment would help future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/public/translation.ts` around lines 208 - 223,
The current loop over batches performs sequential AI calls which increases
latency; either (A) parallelize the per-batch translation calls by mapping
batches to promises that call buildSegmentedText(...) and
ai.run(TRANSLATION_MODEL, {...}) then await Promise.all and merge results into
translations using parseSegmentedTranslation(...), or (B) if sequential behavior
is intentional to respect rate limits, add a short clarifying comment above the
for...of loop referencing batches, buildSegmentedText, ai.run,
TRANSLATION_MODEL, parseSegmentedTranslation, and translations to explain why
requests must be serialized; apply one of these two changes so maintainers know
whether concurrency is safe.

171-193: Segment marker parsing may silently fail on reordered or missing markers.

If the AI model reorders segments or omits markers in the translated output, parseSegmentedTranslation falls back to the source text (line 180), which is a safe default. However, when end < 0 (next marker not found), the logic at line 185 slices to translatedText.length, which could capture content from subsequent segments that were also missing their markers.

Consider adding logging when markers are missing to help diagnose translation quality issues:

    if (start < 0) {
+     // Marker not found in translated output - model may have dropped it
      translations.set(entry.source, entry.source)
      return
    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/public/translation.ts` around lines 171 - 193,
parseSegmentedTranslation silently grabs too much text when a next marker is
missing; update parseSegmentedTranslation to detect and handle missing or
reordered markers: instead of unconditionally slicing to translatedText.length
when end < 0, search for the earliest occurrence of any other entry.marker after
start to compute a safer end boundary, and if no marker is found log a warning
(including entry.marker and index) and treat the segment as missing (use
entry.source or skip restoring) rather than risking swallowing subsequent
segments; add a warning log near the start<0 and end<0 branches and keep
restoreTranslationTokens/translations.set calls unchanged otherwise.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@formkit.config.ts`:
- Around line 17-18: Replace the hardcoded FormKit locale with the app's
reactive language value: import selectedLanguage from your i18n module (the
exported ref used by changeLanguage) in formkit.config.ts and replace locale:
'en' with locale: selectedLanguage.value so FormKit initializes with the current
selected language; if you need live updates without reload, also watch
selectedLanguage and call FormKit's locale setter (or reinitialize FormKit) when
selectedLanguage changes.

In `@src/composables/usePageTranslation.ts`:
- Line 269: The module-level flag translationDisabled is set to true on certain
errors (e.g., in usePageTranslation) with no recovery; update the logic around
translationDisabled to allow recovery by resetting it on route/navigation
changes or by implementing a retry/backoff before marking it permanent: identify
where translationDisabled is toggled (the translationDisabled variable and the
error-handling branches around the response status checks and TypeError handlers
referenced in the diff) and modify them to (1) perform a limited number of
retries with exponential backoff when fetch/translate fails, and only set
translationDisabled to true after retries are exhausted, or (2) clear/reset
translationDisabled when the route changes (hook into the router/navigation
event in usePageTranslation) so transient errors don’t permanently disable
translations.

---

Nitpick comments:
In `@src/modules/i18n.ts`:
- Around line 72-85: The function interpolateMessage currently treats a string
params specially (if typeof params === 'string') and returns params directly,
bypassing the original message and any placeholder interpolation; add a short
comment above the typeof params === 'string' check explaining this intentional
compatibility behavior (that passing a string opts out of interpolation and
returns the string as-is), and ensure the comment references interpolateMessage
and the params parameter so future readers understand why the code returns
params instead of processing message placeholders.
- Around line 144-151: The fallback in getSourceMessage currently transforms
missing keys with key.replaceAll('-', ' '), which can produce misleading,
uncapitalized strings; change the fallback to return the raw key instead. Update
the getSourceMessage function (which reads from messageCatalog) so that if no
resolved value and no defaultMessage exist it returns key unchanged (remove the
replaceAll transformation) to preserve the original identifier for clearer
debugging and consistency.

In `@src/pages/accountDisabled.vue`:
- Around line 113-122: The paragraph contains hardcoded English while the rest
of accountDisabled.vue uses the i18n helper t(); replace the hardcoded strings
with translation keys and call t() (for example use
t('accountDisabled.contactSupport') for the whole sentence and
t('accountDisabled.capgoSupport') for the link text) so the component is
consistent, or alternatively document intentional English usage — locate the
paragraph containing the anchor to https://support.capgo.app/ and update it to
use t() around both the surrounding sentence and the anchor text (keeping the
href and target attributes unchanged).

In `@src/services/i18n.ts`:
- Around line 8-21: The changeLanguage function uses a hard page reload
(window.location.reload()) which interrupts execution and will discard unsaved
state and make the returned nextLanguage unused in the browser; if this is
intentional, add a brief comment above the reload explaining the design
rationale (e.g., dynamic translation requires a full reload to rehydrate
bundles/replace i18n context, and the trade-off of lost in-memory form state),
mention that getSelectedLanguage/normalizeLanguage/loadLanguageAsync are still
used before reload, and optionally document that callers should persist any
unsaved data before invoking changeLanguage.

In `@supabase/functions/_backend/public/translation.ts`:
- Around line 208-223: The current loop over batches performs sequential AI
calls which increases latency; either (A) parallelize the per-batch translation
calls by mapping batches to promises that call buildSegmentedText(...) and
ai.run(TRANSLATION_MODEL, {...}) then await Promise.all and merge results into
translations using parseSegmentedTranslation(...), or (B) if sequential behavior
is intentional to respect rate limits, add a short clarifying comment above the
for...of loop referencing batches, buildSegmentedText, ai.run,
TRANSLATION_MODEL, parseSegmentedTranslation, and translations to explain why
requests must be serialized; apply one of these two changes so maintainers know
whether concurrency is safe.
- Around line 171-193: parseSegmentedTranslation silently grabs too much text
when a next marker is missing; update parseSegmentedTranslation to detect and
handle missing or reordered markers: instead of unconditionally slicing to
translatedText.length when end < 0, search for the earliest occurrence of any
other entry.marker after start to compute a safer end boundary, and if no marker
is found log a warning (including entry.marker and index) and treat the segment
as missing (use entry.source or skip restoring) rather than risking swallowing
subsequent segments; add a warning log near the start<0 and end<0 branches and
keep restoreTranslationTokens/translations.set calls unchanged otherwise.

In `@tests/dynamic-translation.unit.test.ts`:
- Around line 54-67: The test uses a loose `as any` on the entries array which
hides type errors; replace it with an explicit type annotation (e.g., declare
`entries` as `ProtectedEntry[]`) and remove the `as any` cast so TypeScript
validates the shape matches the `ProtectedEntry` interface; ensure you reference
the same `first` and `second` values and that `tokens`/`protectedText` fields
conform to `ProtectedEntry` (import or reference `ProtectedEntry` if needed).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bd4e621f-30d8-4eb5-8652-09e529ba8def

📥 Commits

Reviewing files that changed from the base of the PR and between 57ba748 and f352835.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (37)
  • cloudflare_workers/api/index.ts
  • cloudflare_workers/api/wrangler.jsonc
  • formkit.config.ts
  • messages/README.md
  • messages/de.json
  • messages/es.json
  • messages/fr.json
  • messages/hi.json
  • messages/id.json
  • messages/it.json
  • messages/ja.json
  • messages/ko.json
  • messages/pl.json
  • messages/pt-br.json
  • messages/ru.json
  • messages/tr.json
  • messages/vi.json
  • messages/zh-cn.json
  • package.json
  • project.inlang/settings.json
  • scripts/supabase-worktree.ts
  • src/App.vue
  • src/auto-imports.d.ts
  • src/components.d.ts
  • src/components/LangSelector.vue
  • src/composables/usePageTranslation.ts
  • src/modules/i18n.ts
  • src/pages/accountDisabled.vue
  • src/services/i18n.ts
  • src/shims/vueI18n.ts
  • supabase/config.toml
  • supabase/functions/_backend/public/translation.ts
  • supabase/functions/_backend/utils/cloudflare.ts
  • templates
  • tests/dynamic-translation.unit.test.ts
  • tsconfig.json
  • vite.config.mts
💤 Files with no reviewable changes (3)
  • messages/README.md
  • project.inlang/settings.json
  • package.json

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bac49ed259

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +79 to +80
const text = normalizeWhitespace(entry)
if (!text)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep original whitespace when normalizing translation inputs

This normalization collapses internal whitespace before strings become translation keys, but the frontend later applies responses by exact segment.source lookup. For any source text that contains embedded newlines or repeated spaces (a common shape for formatted Vue text nodes), the response key no longer matches and that segment is silently left untranslated. Keep the original string as the response key (or carry an explicit original→normalized mapping) while using normalized text only for dedupe/batching.

Useful? React with 👍 / 👎.

Comment on lines +307 to +309
if (response.status === 404 || response.status === 501 || response.status === 503) {
translationDisabled = true
return {}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retry after transient 503 instead of disabling translations

Treating HTTP 503 as a permanent disable condition sets translationDisabled for the lifetime of the app session, so one temporary worker outage causes all later translation attempts to be skipped until a full page reload. Because 503 is a transient service-unavailable signal, this turns a short incident into persistent translation loss for active users.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 458c8e9238

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

async function fetchTranslations(strings: string[], lang: string, pagePath: string, signal: AbortSignal) {
const response = await fetch(`${defaultApiHost}/translation/page`, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Expose translation route in Supabase function runtime

The frontend now always posts page strings to defaultApiHost + '/translation/page', but this commit only wires /translation in the Cloudflare API router; a repo-wide check of supabase/functions/*/index.ts shows no translation function entrypoint. In environments where defaultApiHost points to Supabase Functions (notably local localhost:54321/functions/v1), requests return 404, translationDisabled is set, and users are stuck with English-only UI after static locale bundles were removed.

Useful? React with 👍 / 👎.


app.use('*', useCors)

app.post('/page', async (c) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add abuse controls to translation AI endpoint

This new endpoint executes Workers AI translation for arbitrary caller-provided text but is mounted with open CORS and no auth/rate-limit middleware, so any third party can automate calls and consume model quota/cost. Because each request can trigger multiple ai.run(...) calls, unauthenticated public access can degrade service for real users and create avoidable billing spikes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
src/modules/i18n.ts (1)

72-76: Don't preserve inline English fallback copy in the core t() path.

Treating a string params value or defaultMessage as source copy keeps the forbidden t('key', 'English fallback') pattern alive and hides missing messages/en.json entries instead of surfacing them during development. If you still need a migration shim, keep it in a temporary adapter rather than the default translator.

As per coding guidelines, "messages/en.json: Update messages/en.json when text is missing instead of using inline fallback text in translation calls" and "**/*.{ts,tsx,vue,js}: Never pass inline fallback text as the second argument to translation calls; use translation keys only."

Also applies to: 146-156

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/i18n.ts` around lines 72 - 76, Remove the special-case that
treats a string second argument as inline English fallback in the core
translator: in interpolateMessage, delete the branch that returns params when
typeof params === 'string' and instead treat string inputs as invalid/missing
params so missing translations surface; similarly remove or change any analogous
handling in the related translation path referenced at lines ~146-156 so
defaultMessage/string fallbacks are not honored by t(). If you need a temporary
shim for legacy callers, implement it outside the core functions (e.g., a
migration adapter) and update callers to pass proper MessageParams or add keys
to messages/en.json.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/auto-imports.d.ts`:
- Line 48: The auto-import types were emitted with relative paths (e.g., the
declaration for isAdminRole: typeof import('./stores/organization').isAdminRole)
but our frontend standard requires src imports to use the ~/ alias; update the
auto-import generation configuration (the script or tool that produces
src/auto-imports.d.ts) so it rewrites or emits imports using the ~/ prefix
instead of ./ relative paths, regenerate the auto-imports file, and verify
declarations such as isAdminRole (and the other listed entries) now reference
typeof import('~/stores/organization').isAdminRole.

In `@src/services/i18n.ts`:
- Around line 4-5: The current change repurposed getEmoji(lang) to look up a
language->country mapping, which breaks callers that pass an already-normalized
country code; revert getEmoji to its original country-code behavior and add a
new helper for language-based lookups: implement getLanguageEmoji(lang) that
returns countryCodeToFlagEmoji(getLanguageConfig(lang).countryCode), and restore
or keep getEmoji(countryCode) to call
countryCodeToFlagEmoji(normalizeCountryCode(countryCode)) so existing callers
using normalizeCountryCode(...) keep correct flags; update exports accordingly
and replace any new calls that intended a language lookup to use
getLanguageEmoji.

In `@supabase/functions/_backend/public/translation.ts`:
- Around line 177-193: The code in parseSegmentedTranslation uses console.warn
(lines handling missing marker/boundary) which CI rejects; change these to
structured backend logging by accepting a Hono Context (or a logger/requestId)
into parseSegmentedTranslation and replace console.warn calls with cloudlog()
including c.get('requestId') and context metadata; update all callers of
parseSegmentedTranslation to pass the Context<MiddlewareKeyVariables> (or a
small logger object) so you can call cloudlog({ msg: 'Translation segment marker
missing...', requestId: c.get('requestId'), index, marker: entry.marker }) and
similarly for the boundary warning, and keep translations.set behavior the same.
- Around line 247-290: The /page route currently allows unauthenticated callers
to trigger translateStrings (and thus c.env.AI.run) on cache misses; add quota
control early in the handler (immediately after parsing body and before
computing requestHash or invoking CacheHelper/translateStrings) by validating a
signed token or enforcing a per-IP rate limit; implement or call a helper like
verifySignedToken(req) or rateLimiter.check(c.ip) and return a 401/429
quickError on failure so only authorized/limited callers can reach
translateStrings and the CacheHelper.putJson path.

In `@tests/dynamic-translation.unit.test.ts`:
- Around line 3-9: The import mixes a value import with a type, violating the
ESLint rule; remove ProtectedEntry from the combined import of
normalizeTranslationStrings, parseSegmentedTranslation,
protectTranslationTokens, restoreTranslationTokens and instead add a separate
type-only import: use "import type { ProtectedEntry } from '.../translation.ts'"
while keeping the other named imports as value imports so the tests follow the
project's consistent type-import style.

---

Nitpick comments:
In `@src/modules/i18n.ts`:
- Around line 72-76: Remove the special-case that treats a string second
argument as inline English fallback in the core translator: in
interpolateMessage, delete the branch that returns params when typeof params ===
'string' and instead treat string inputs as invalid/missing params so missing
translations surface; similarly remove or change any analogous handling in the
related translation path referenced at lines ~146-156 so defaultMessage/string
fallbacks are not honored by t(). If you need a temporary shim for legacy
callers, implement it outside the core functions (e.g., a migration adapter) and
update callers to pass proper MessageParams or add keys to messages/en.json.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e76a8b98-1246-4c52-9b33-dc010032984f

📥 Commits

Reviewing files that changed from the base of the PR and between bac49ed and fcbaf9e.

📒 Files selected for processing (10)
  • formkit.config.ts
  • messages/en.json
  • src/auto-imports.d.ts
  • src/components.d.ts
  • src/composables/usePageTranslation.ts
  • src/modules/i18n.ts
  • src/pages/accountDisabled.vue
  • src/services/i18n.ts
  • supabase/functions/_backend/public/translation.ts
  • tests/dynamic-translation.unit.test.ts
💤 Files with no reviewable changes (1)
  • src/components.d.ts
✅ Files skipped from review due to trivial changes (1)
  • src/pages/accountDisabled.vue
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/composables/usePageTranslation.ts
  • formkit.config.ts

const ignorableWatch: typeof import('@vueuse/core').ignorableWatch
const inject: typeof import('vue').inject
const injectLocal: typeof import('@vueuse/core').injectLocal
const isAdminRole: typeof import('./stores/organization').isAdminRole
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.

⚠️ Potential issue | 🟡 Minor

Use ~/ alias in new auto-import type paths.

Line 48, Line 55, Line 101, Line 231, Line 396, Line 403, Line 448, and Line 578 add new relative import paths (./...). Please update the auto-import generation config so these emit ~/... paths, then regenerate this file to keep frontend imports aligned with repo standards.

As per coding guidelines: Import using \~/` alias for `src/` directory in frontend code instead of relative paths`.

Also applies to: 55-55, 101-101, 231-231, 396-396, 403-403, 448-448, 578-578

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/auto-imports.d.ts` at line 48, The auto-import types were emitted with
relative paths (e.g., the declaration for isAdminRole: typeof
import('./stores/organization').isAdminRole) but our frontend standard requires
src imports to use the ~/ alias; update the auto-import generation configuration
(the script or tool that produces src/auto-imports.d.ts) so it rewrites or emits
imports using the ~/ prefix instead of ./ relative paths, regenerate the
auto-imports file, and verify declarations such as isAdminRole (and the other
listed entries) now reference typeof
import('~/stores/organization').isAdminRole.

Comment on lines +177 to +193
if (start < 0) {
console.warn('Translation segment marker missing from model output', { index, marker: entry.marker })
translations.set(entry.source, entry.source)
return
}

const contentStart = start + entry.marker.length
const markerBoundaries = entries
.slice(index + 1)
.map(nextEntry => translatedText.indexOf(nextEntry.marker, contentStart))
.filter(boundary => boundary >= 0)
.sort((left, right) => left - right)

const end = markerBoundaries[0] ?? translatedText.length
if (entries[index + 1] && markerBoundaries.length === 0) {
console.warn('Translation segment boundary missing from model output', { index, marker: entry.marker })
translations.set(entry.source, entry.source)
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.

⚠️ Potential issue | 🟠 Major

Replace console.warn with backend logging.

CI is already rejecting console.* under supabase/functions, so these warnings block the merge. Bubble the anomaly back to the route or pass a logger/requestId into parseSegmentedTranslation() so it can emit through cloudlog() instead.

As per coding guidelines, "All endpoints must receive Hono Context<MiddlewareKeyVariables> object and use c.get('requestId') for structured logging with cloudlog()."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/public/translation.ts` around lines 177 - 193,
The code in parseSegmentedTranslation uses console.warn (lines handling missing
marker/boundary) which CI rejects; change these to structured backend logging by
accepting a Hono Context (or a logger/requestId) into parseSegmentedTranslation
and replace console.warn calls with cloudlog() including c.get('requestId') and
context metadata; update all callers of parseSegmentedTranslation to pass the
Context<MiddlewareKeyVariables> (or a small logger object) so you can call
cloudlog({ msg: 'Translation segment marker missing...', requestId:
c.get('requestId'), index, marker: entry.marker }) and similarly for the
boundary warning, and keep translations.set behavior the same.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de647fe04d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return

const uniqueSources = [...new Set(segments.map(segment => segment.source))]
const requestedPath = route.fullPath
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid sending full route URLs to translation endpoint

route.fullPath includes query strings and hash fragments, and this value is forwarded as pagePath in every translation request. In this codebase several auth flows carry sensitive one-time tokens in query/hash (for example login/recovery/sso routes), so a non-English session can unintentionally transmit those secrets to /translation/page even though translation only needs a stable page identifier. Use a sanitized path (e.g., route.path) or strip query/hash before building the request.

Useful? React with 👍 / 👎.

Comment on lines +417 to +419
if (!controller.signal.aborted && error instanceof TypeError) {
translationDisabled = true
transientRetryCount = 0
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retry transient fetch TypeErrors instead of disabling translation

A TypeError from fetch is usually a transient transport failure (temporary offline, DNS hiccup, brief network outage), but this branch permanently flips translationDisabled for the current page session. After one transient failure, later mutation-driven translation attempts will keep short-circuiting until the user changes route/language or reloads, so translation does not recover automatically when connectivity returns.

Useful? React with 👍 / 👎.

@socket-security
Copy link
Copy Markdown

socket-security bot commented Apr 1, 2026

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
tests/dynamic-translation.unit.test.ts (1)

3-9: ⚠️ Potential issue | 🟡 Minor

Split ProtectedEntry into a top-level type import.

This still triggers import/consistent-type-specifier-style and keeps the test file red in lint.

♻️ Proposed fix
+import type { ProtectedEntry } from '../supabase/functions/_backend/public/translation.ts'
 import {
   normalizeTranslationStrings,
   parseSegmentedTranslation,
-  type ProtectedEntry,
   protectTranslationTokens,
   restoreTranslationTokens,
 } from '../supabase/functions/_backend/public/translation.ts'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/dynamic-translation.unit.test.ts` around lines 3 - 9, The import line
includes ProtectedEntry alongside value imports which triggers the
import/consistent-type-specifier-style lint rule; change the import from
'../supabase/functions/_backend/public/translation.ts' to import the runtime
symbols (normalizeTranslationStrings, parseSegmentedTranslation,
protectTranslationTokens, restoreTranslationTokens) as normal imports and pull
ProtectedEntry as a top-level type-only import (import type { ProtectedEntry }
from ...), ensuring ProtectedEntry is imported with the "import type" specifier
to satisfy the linter.
supabase/functions/_backend/public/translation.ts (1)

248-290: ⚠️ Potential issue | 🟠 Major

/translation/page still needs quota protection.

This route remains public and unthrottled, so callers can generate arbitrary cache-miss bodies and force c.env.AI.run() on demand. Please add a signed caller check or at least per-IP/API-key rate limiting before the cache/translation path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/public/translation.ts` around lines 248 - 290,
Add an access-control check before computing/serving cache-miss translations in
the app.post('/page') handler: validate a signed caller token (e.g., verify a
signature/header using an env secret) or enforce per-IP/API-key rate limiting
(use CF-Connecting-IP or an API-Key header and a RateLimiter helper) before the
cache lookup and before calling translateStrings / c.env.AI.run(); if
validation/rate limit fails return 401/429 and short-circuit. Update the handler
around the start of the route (before sha256Hex, CacheHelper.buildRequest, and
translateStrings) and reuse/introduce helpers like verifySignature(...) or
RateLimiter.check(...) so legitimate cached hits still work but cache-miss AI
runs are protected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/composables/usePageTranslation.ts`:
- Around line 410-414: The retry loop for RetryableTranslationError currently
keeps scheduling retries forever once the TRANSIENT_RETRY_DELAYS_MS table is
exhausted; modify the logic in the block that checks controller.signal.aborted
and error instanceof RetryableTranslationError to cap retries by using
transientRetryCount and TRANSIENT_RETRY_DELAYS_MS.length: if transientRetryCount
is greater than or equal to TRANSIENT_RETRY_DELAYS_MS.length, do not call
scheduleTranslation again—mark the translation as failed (or emit/dispatch a
final failure state) and stop retrying; otherwise compute retryDelay as now,
increment transientRetryCount, and call scheduleTranslation as before. Ensure
you reference controller.signal.aborted, RetryableTranslationError,
TRANSIENT_RETRY_DELAYS_MS, transientRetryCount, and scheduleTranslation when
making the change.
- Around line 330-373: The translatePage function currently scrapes
document.body via collectSegments(root, lang), which risks sending sensitive
data; change the root selection to opt-in only (e.g., query for a specific
container or attribute) and abort if not found: replace using document.body with
a scoped selector such as document.querySelector('[data-capgo-translate]') (or
respect an existing configuration flag) so only marked subtrees are collected by
collectSegments; ensure the early-return logic (restoreSourceContent,
lastRequestHash, transientRetryCount) still runs if no opt-in root exists, and
update any tests or callers that assumed body-level behavior.

---

Duplicate comments:
In `@supabase/functions/_backend/public/translation.ts`:
- Around line 248-290: Add an access-control check before computing/serving
cache-miss translations in the app.post('/page') handler: validate a signed
caller token (e.g., verify a signature/header using an env secret) or enforce
per-IP/API-key rate limiting (use CF-Connecting-IP or an API-Key header and a
RateLimiter helper) before the cache lookup and before calling translateStrings
/ c.env.AI.run(); if validation/rate limit fails return 401/429 and
short-circuit. Update the handler around the start of the route (before
sha256Hex, CacheHelper.buildRequest, and translateStrings) and reuse/introduce
helpers like verifySignature(...) or RateLimiter.check(...) so legitimate cached
hits still work but cache-miss AI runs are protected.

In `@tests/dynamic-translation.unit.test.ts`:
- Around line 3-9: The import line includes ProtectedEntry alongside value
imports which triggers the import/consistent-type-specifier-style lint rule;
change the import from '../supabase/functions/_backend/public/translation.ts' to
import the runtime symbols (normalizeTranslationStrings,
parseSegmentedTranslation, protectTranslationTokens, restoreTranslationTokens)
as normal imports and pull ProtectedEntry as a top-level type-only import
(import type { ProtectedEntry } from ...), ensuring ProtectedEntry is imported
with the "import type" specifier to satisfy the linter.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3492e4e9-5f3f-4bf1-ba61-249fb18ce4df

📥 Commits

Reviewing files that changed from the base of the PR and between fcbaf9e and de647fe.

📒 Files selected for processing (3)
  • src/composables/usePageTranslation.ts
  • supabase/functions/_backend/public/translation.ts
  • tests/dynamic-translation.unit.test.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
src/services/i18n.ts (1)

8-10: Reuse getEmoji() in getLanguageEmoji() to avoid normalization drift.

getLanguageEmoji currently bypasses the normalization path in getEmoji. Reusing the helper keeps behavior centralized.

♻️ Proposed refactor
 export function getLanguageEmoji(lang: string) {
-  return countryCodeToFlagEmoji(getLanguageConfig(lang).countryCode)
+  return getEmoji(getLanguageConfig(lang).countryCode)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/services/i18n.ts` around lines 8 - 10, getLanguageEmoji should call the
centralized normalization helper getEmoji instead of directly using
countryCodeToFlagEmoji to avoid divergent normalization logic; update
getLanguageEmoji(lang) to retrieve the country code via
getLanguageConfig(lang).countryCode and pass that into getEmoji so the emoji
generation and normalization remain centralized in getEmoji rather than
duplicating behaviour in getLanguageEmoji.
supabase/functions/_backend/public/translation.ts (3)

190-225: Logging in parseSegmentedTranslation lacks request context.

The cloudlog() calls at lines 197 and 211 don't include requestId. Since this function is exported and doesn't receive the Hono Context, consider either:

  1. Passing an optional requestId parameter for correlation, or
  2. Accepting this limitation since the marker/index info provides sufficient debugging context.

This is a minor observability improvement, not blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/public/translation.ts` around lines 190 - 225,
The cloudlog calls inside parseSegmentedTranslation are missing request
correlation; update the exported function parseSegmentedTranslation to accept an
optional requestId parameter (e.g., parseSegmentedTranslation(translatedText:
string, entries: ProtectedEntry[], requestId?: string)) and include that
requestId in the two cloudlog calls (the one for missing marker and the one for
missing boundary) so logs include { message, index, marker, requestId }; ensure
the function signature change is propagated to callers and keep existing
behavior when requestId is undefined, and continue to use
restoreTranslationTokens and entries as before.

280-305: Consider adding a timeout to Workers AI calls.

The ai.run() calls have no timeout protection. If the Workers AI service becomes slow or unresponsive, requests will hang indefinitely until the worker execution limit is reached. Consider wrapping with Promise.race or using AbortController with a reasonable timeout (e.g., 30 seconds per batch).

♻️ Proposed timeout pattern
+const AI_TIMEOUT_MS = 30_000
+
+async function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
+  const timeout = new Promise<never>((_, reject) =>
+    setTimeout(() => reject(new Error(message)), ms),
+  )
+  return Promise.race([promise, timeout])
+}
+
 async function translateStrings(ai: { run: (model: string, input: unknown) => Promise<unknown> }, strings: string[], targetLanguage: string) {
   // ...
   for (const batch of batches) {
     const segmentedText = buildSegmentedText(batch)
-    const result = await ai.run(TRANSLATION_MODEL, {
+    const result = await withTimeout(ai.run(TRANSLATION_MODEL, {
       text: segmentedText,
       source_lang: 'en',
       target_lang: targetLanguage,
-    }) as { translated_text?: string }
+    }), AI_TIMEOUT_MS, 'Translation timed out') as { translated_text?: string }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/public/translation.ts` around lines 280 - 305,
translateStrings currently calls ai.run(TRANSLATION_MODEL, ...) for each batch
with no timeout, which can hang; update translateStrings to enforce a per-batch
timeout (e.g., 30s) by wrapping the ai.run call in a timeout strategy (either
Promise.race with a delay-reject promise or using AbortController if ai.run
supports signal) so that slow/unresponsive Workers AI requests are aborted and
handled; ensure you propagate or handle the timeout error (reject/log and
continue or retry as appropriate) where parseSegmentedTranslation is called, and
keep the existing batching logic (buildBatches, buildSegmentedText,
parseSegmentedTranslation) intact.

247-258: Race condition in rate limit increment allows brief bursts past limit.

The read-modify-write pattern on lines 249-255 is not atomic. Under concurrent requests, multiple callers can read the same count, each increment it, and all pass the limit check before any write completes. This is acceptable for an edge-cache-based rate limiter where eventual consistency is expected, but be aware that actual throughput can briefly exceed the configured limit under burst traffic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@supabase/functions/_backend/public/translation.ts` around lines 247 - 258,
The current read-modify-write using CacheHelper.matchJson + putJson on
TranslationRateLimitEntry is vulnerable to race conditions; change to an atomic
increment or compare-and-set loop so concurrent callers cannot overshoot the
limit. Specifically, replace the matchJson/putJson pattern around cacheKey with
either (1) an atomic increment operation provided by the cache (e.g., a
CacheHelper.increment or Redis INCR-style call) and only set/reset the TTL when
the counter is created, or (2) a CAS loop that reads the entry along with a
version/etag and retries putJson until successful, then evaluate
getTranslationIpRateLimit(limit) against the result; keep references to
CacheHelper, buildRequest/TRANSLATION_IP_RATE_PATH,
matchJson/putJson/TranslationRateLimitEntry, TRANSLATION_IP_RATE_TTL_SECONDS and
getTranslationIpRateLimit when applying the change.
tests/dynamic-translation.unit.test.ts (1)

1-9: Fix import ordering per ESLint rules.

ESLint flags that the type import from the backend should come before the value import from the frontend (parent-type before parent). The type import separation from the past review comment has been addressed.

♻️ Proposed fix for import order
 import { describe, expect, it } from 'vitest'
-import { getWorkerLanguageCode, normalizeLanguage } from '../src/modules/i18n'
 import type { ProtectedEntry } from '../supabase/functions/_backend/public/translation.ts'
 import {
   normalizeTranslationStrings,
   parseSegmentedTranslation,
   protectTranslationTokens,
   restoreTranslationTokens,
 } from '../supabase/functions/_backend/public/translation.ts'
+import { getWorkerLanguageCode, normalizeLanguage } from '../src/modules/i18n'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/dynamic-translation.unit.test.ts` around lines 1 - 9, Import ordering
violates ESLint: move the type-only import for ProtectedEntry so it appears
before the value imports from the frontend and keep the other named imports
together; specifically, place "import type { ProtectedEntry } from
'../supabase/functions/_backend/public/translation.ts'" above the imports of
normalizeTranslationStrings, parseSegmentedTranslation,
protectTranslationTokens, and restoreTranslationTokens, ensuring
getWorkerLanguageCode and normalizeLanguage imports remain grouped and the file
imports follow ESLint's parent-type-before-parent ordering.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/services/i18n.ts`:
- Around line 23-24: The unconditional window.location.reload() call in
src/services/i18n.ts should be removed from the shared service and made
caller-controlled: change the function that currently invokes
window.location.reload() (the language change / setter function in this file) to
accept an optional parameter (e.g., reload?: boolean) or an optional callback
(e.g., onReload?: () => void), defaulting to false/no-op, and only perform the
reload when that parameter is true or the callback is provided; alternatively
emit a 'languageChanged' event (or return a status) so callers can decide to
call window.location.reload() themselves — update all internal callers to pass
the desired behavior and add tests for both reload and no-reload flows.

---

Nitpick comments:
In `@src/services/i18n.ts`:
- Around line 8-10: getLanguageEmoji should call the centralized normalization
helper getEmoji instead of directly using countryCodeToFlagEmoji to avoid
divergent normalization logic; update getLanguageEmoji(lang) to retrieve the
country code via getLanguageConfig(lang).countryCode and pass that into getEmoji
so the emoji generation and normalization remain centralized in getEmoji rather
than duplicating behaviour in getLanguageEmoji.

In `@supabase/functions/_backend/public/translation.ts`:
- Around line 190-225: The cloudlog calls inside parseSegmentedTranslation are
missing request correlation; update the exported function
parseSegmentedTranslation to accept an optional requestId parameter (e.g.,
parseSegmentedTranslation(translatedText: string, entries: ProtectedEntry[],
requestId?: string)) and include that requestId in the two cloudlog calls (the
one for missing marker and the one for missing boundary) so logs include {
message, index, marker, requestId }; ensure the function signature change is
propagated to callers and keep existing behavior when requestId is undefined,
and continue to use restoreTranslationTokens and entries as before.
- Around line 280-305: translateStrings currently calls
ai.run(TRANSLATION_MODEL, ...) for each batch with no timeout, which can hang;
update translateStrings to enforce a per-batch timeout (e.g., 30s) by wrapping
the ai.run call in a timeout strategy (either Promise.race with a delay-reject
promise or using AbortController if ai.run supports signal) so that
slow/unresponsive Workers AI requests are aborted and handled; ensure you
propagate or handle the timeout error (reject/log and continue or retry as
appropriate) where parseSegmentedTranslation is called, and keep the existing
batching logic (buildBatches, buildSegmentedText, parseSegmentedTranslation)
intact.
- Around line 247-258: The current read-modify-write using CacheHelper.matchJson
+ putJson on TranslationRateLimitEntry is vulnerable to race conditions; change
to an atomic increment or compare-and-set loop so concurrent callers cannot
overshoot the limit. Specifically, replace the matchJson/putJson pattern around
cacheKey with either (1) an atomic increment operation provided by the cache
(e.g., a CacheHelper.increment or Redis INCR-style call) and only set/reset the
TTL when the counter is created, or (2) a CAS loop that reads the entry along
with a version/etag and retries putJson until successful, then evaluate
getTranslationIpRateLimit(limit) against the result; keep references to
CacheHelper, buildRequest/TRANSLATION_IP_RATE_PATH,
matchJson/putJson/TranslationRateLimitEntry, TRANSLATION_IP_RATE_TTL_SECONDS and
getTranslationIpRateLimit when applying the change.

In `@tests/dynamic-translation.unit.test.ts`:
- Around line 1-9: Import ordering violates ESLint: move the type-only import
for ProtectedEntry so it appears before the value imports from the frontend and
keep the other named imports together; specifically, place "import type {
ProtectedEntry } from '../supabase/functions/_backend/public/translation.ts'"
above the imports of normalizeTranslationStrings, parseSegmentedTranslation,
protectTranslationTokens, and restoreTranslationTokens, ensuring
getWorkerLanguageCode and normalizeLanguage imports remain grouped and the file
imports follow ESLint's parent-type-before-parent ordering.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3bb31a79-88e8-4076-b203-a64543640b8f

📥 Commits

Reviewing files that changed from the base of the PR and between de647fe and 102c457.

📒 Files selected for processing (5)
  • src/components/LangSelector.vue
  • src/composables/usePageTranslation.ts
  • src/services/i18n.ts
  • supabase/functions/_backend/public/translation.ts
  • tests/dynamic-translation.unit.test.ts
✅ Files skipped from review due to trivial changes (1)
  • src/components/LangSelector.vue
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/composables/usePageTranslation.ts

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f5a27dd096

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +323 to +324
const rateLimitStatus = await recordTranslationRequest(c)
if (rateLimitStatus.limited) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Check cache before enforcing translation IP rate limit

The handler increments/checks the per-IP limiter before reading the translation cache, so even cache hits consume quota and can start returning 429 once the shared IP crosses the threshold. In practice, repeated requests for already-cached pages (for example, multiple users behind one NAT) can get blocked even though no Workers AI call would be made, which turns a cost-control guard into a user-visible availability regression.

Useful? React with 👍 / 👎.

const knownSourceTexts = new Set([
...Object.values(messageCatalog),
...languageOptions.map(option => option.label),
].map(value => normalizeKnownSourceText(value)).filter(value => value && !value.includes('{')))
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep placeholder-based messages in source text allowlist

Filtering out every catalog entry containing {...} prevents interpolated UI copy from ever being considered translatable. usePageTranslation only sends text when isKnownSourceText(...) is true (outside explicit opt-in zones), so messages rendered from keys like "... {count} ..." become values such as "2 minutes ago" that no longer match the allowlist and remain stuck in English.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d83379d0c1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const strings = normalizeTranslationStrings(body.strings)
const requestHash = await sha256Hex(JSON.stringify({
model: TRANSLATION_MODEL,
pagePath: body.pagePath ?? '',
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove concrete page paths from translation cache hashing

Including pagePath in the server-side requestHash makes cache entries depend on concrete URLs instead of just the text/language payload. Because the client sends route.path (which contains route params on pages like /app/:app/...), identical strings on different app/device/bundle pages miss the cache and trigger fresh Workers AI calls, increasing latency/cost and causing avoidable rate-limit pressure. Hash against stable content inputs (or a route template/name) instead of the concrete path.

Useful? React with 👍 / 👎.

Comment on lines +414 to +415
if (controller.signal.aborted)
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retry timeout aborts instead of silently dropping translation

Requests are force-aborted after REQUEST_TIMEOUT_MS, but handleTranslationError returns immediately whenever controller.signal.aborted is true. That also swallows timeout-driven aborts, so a temporary slow response can leave the page untranslated with no retry/backoff unless a later DOM mutation or navigation happens. Timeout AbortError should be handled as a transient failure path rather than ignored.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50da775e35

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +19 to +22
locales: { en },
// FormKit keeps English source strings and the runtime page translator localizes
// the rendered validation copy, so we do not ship per-locale FormKit bundles.
locale: SOURCE_FORMKIT_LOCALE,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve non-English FormKit validation messages

This hard-codes FormKit to English-only (locales: { en }, locale: 'en'), but the new page translator only sends allowlisted source-catalog text (isKnownSourceText) for translation. FormKit’s built-in validation copy (required/email/min/etc.) is not in the app catalog, so those errors now remain English in non-English sessions across login/register/settings forms. This is a user-facing localization regression introduced by removing the bundled FormKit locales without an equivalent translation path for validation strings.

Useful? React with 👍 / 👎.

Comment on lines +190 to +191
if (element.closest(NO_TRANSLATE_SELECTOR) || SKIP_TAGS.has(element.tagName))
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Translate textarea attributes instead of skipping TEXTAREA

The attribute collector exits early for every TEXTAREA element, which also drops translatable attributes like placeholder/title/aria-label on textareas. With this commit’s English-source runtime translation flow, textarea placeholders (for example in role assignment reason fields) stay stuck in English for non-English locales. The skip should only avoid translating textarea content/user input, not its UI attributes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 43d5cb62ce

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +283 to +285
function getTranslationRoot() {
return document.querySelector<HTMLElement>(TRANSLATION_ROOT_SELECTOR)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Include teleported UI in translation scan scope

getTranslationRoot() limits scanning to the element marked data-capgo-translation-root, but several critical surfaces (for example DialogV2 uses <Teleport to="body">) render outside that subtree. Since this commit also makes t() always return English source text, modal titles/buttons/descriptions shown in teleported overlays will never be post-translated for non-English users. Expand the observed root (or add a second observer/root) so body-level teleports are translated too.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7b3c5cd6b0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +377 to +380
function shouldSkipTranslation(root: TranslationRoot, lang: string) {
if (isEnglishLocale(lang) || translationDisabled) {
resetTranslationState(root)
return true
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid full DOM restore on every English mutation

When selectedLanguage is English (the default path), shouldSkipTranslation() still calls resetTranslationState(root), which runs restoreSourceContent() over the entire translation root. Because the mutation observer is always active and schedules translation on every relevant DOM change, this turns normal UI updates (typing, reactive rerenders, list updates) into repeated full-tree walks for English sessions, creating a significant performance regression for the primary locale. Skip the restore walk unless we are actually reverting previously translated content.

Useful? React with 👍 / 👎.

Comment on lines +196 to +199
if (start < 0) {
cloudlog({ message: 'Translation segment marker missing from model output', index, marker: entry.marker })
translations.set(entry.source, entry.source)
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not cache fallback text after marker parse failures

If Workers AI output drops a segment marker, parseSegmentedTranslation() silently maps that entry back to entry.source and continues. The caller then treats this as a successful translation and caches the payload for 7 days, so one malformed model response can pin untranslated English text for all matching requests until cache expiry. Marker-loss should be treated as a retriable failure (or at least excluded from cache writes) instead of being cached as final output.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7bb414eace

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +277 to +281
if (response.status === 404 || response.status === 501 || response.status === 503) {
// Supabase-only/local runtimes intentionally do not expose translation
// bundles, and workers without AI bindings should keep the source English UI.
messageCatalogTranslationDisabled = true
return messageCatalog
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Retry catalog translation after transient 503 responses

This branch treats 503 Service Unavailable as a permanent disable signal by setting messageCatalogTranslationDisabled, and later calls short-circuit at line 261 to English forever in the current tab. If Workers AI has a brief outage or warmup delay, users who selected a non-English locale will not recover translated message catalogs until a full reload, so 503 should be handled as transient instead of disabling the feature.

Useful? React with 👍 / 👎.

Comment on lines +320 to +321
translatedMessageCatalogs.set(normalized, messages)
if (messages !== messageCatalog)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not memoize fallback English as locale catalog

fetchTranslatedMessageCatalog returns messageCatalog on network/transient failures, and this code stores that fallback under the target locale. After one temporary fetch failure (offline blip, DNS error, timeout), ensureMessageCatalogLoaded sees an existing catalog and never retries, so that locale stays English for the whole session even after connectivity recovers.

Useful? React with 👍 / 👎.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 2, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot

See analysis details on SonarQube Cloud

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fa94bde8fd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +178 to +179
if (typeof params === 'string')
return params
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep fallback-string calls from bypassing translations

Returning params directly when it is a string changes t(key, defaultMsg) behavior into an unconditional override, so translated catalogs are ignored for those calls. In this repo, src/utils/invites.ts calls t('too-recent-invitation-cancelation', options.cancelledFallback), which now always renders the English fallback text in non-English sessions even when a translated key is available. Keep key-based lookup as the primary path and use the string only as a fallback when the key is actually missing.

Useful? React with 👍 / 👎.

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