feat(i18n): Hebrew + RTL localization (English/Hebrew, full RTL) - #84
Merged
Conversation
Approved plan for Hebrew/English locale support with full RTL when locale=he. Phased into 4 PRs (infra, string extraction, RTL polish, content + agent locale). References issue #79; Chinese deferred.
Adds the foundation for Hebrew + English locale support:
- react-i18next + i18next + browser-languagedetector deps
- src/i18n scaffold: index.ts (init), use-locale.ts (hook),
formatters.ts (Intl-based date/time/number), locales/{en,he}/
with common, settings, sidebar namespaces
- LocaleInitializer mirrors ThemeInitializer; pre-hydration inline
script in layout.tsx sets <html lang/dir> before first paint to
avoid an RTL flip flash
- Language section in Settings > Appearance with live switching
(English / עברית), no restart needed
Locale persists in localStorage as cabinet-locale (mirrors theme
persistence). User-facing strings still mostly English; P2 extracts
the remaining ~150 component files.
Refs #79
Adds editor + tour namespaces (en + he). Sidebar, tour-modal, and editor-toolbar now route all user-facing labels through t(). Toolbar tooltips, slide nav, refresh toasts, and the starter-task prompt all translate cleanly when switching to Hebrew. Note: starter-task in the tour is the prompt sent to the agent, so it's translated too — the user receives a response in their locale (P4 wires the agent itself; this just sources the prompt). P3 will handle the directional ArrowLeft/Right + PilcrowLeft/Right icons in these files. P2 stays strictly string-extraction. Refs #79
Adds common:nav.{goBack,goForward}. Keyboard shortcuts (⌘[/⌘]) stay
inline since they don't translate.
Refs #79
Translates the functional UI of the onboarding wizard: intro CTA + tagline, welcome paragraph (typewriter), welcome-back step, provider verification status labels (Passed / Not installed / Auth required / etc.), and provider readiness pills. Deferred (will not translate well or needs design judgment): - The dictionary card intro (creative English wordplay on "cabinet") - Starter team names / descriptions in STARTER_TEAMS (English SaaS jargon) - Community step cards (GitHub / Discord brand content) - Cabinet-creation animation tree ONBOARDING_VERIFY_META switched from literal `label` to `labelKey` so call sites can resolve via t() at render time. Refs #79
confirm-dialog-host defaults now translate (cancel/confirm fall through to common:actions). Page header export menu (Copy Markdown / for LLMs / HTML / Download .md / PDF + toast) and AI panel toggle now translated. Refs #79
Adds dialogs namespace. Translates the New Page dialog (trigger, title, placeholder, create button) and the New Cabinet overlay (title, subtitle, Import from Registry, Cabinet name label/placeholder, Select agents, selected count, error messages, footer buttons). The agent picker contents and registry import flow are still English — those live in separate components. Refs #79
Adds <DirIcon ltr={X} rtl={Y} /> for icons that flip meaning in RTL
(e.g. ChevronRight ⇄ ChevronLeft for expand/collapse). Use Tailwind's
rtl:rotate-180 modifier for purely-decorative arrows where it's enough.
Loads Cardo via next/font/google as --font-logo-rtl. CSS rule in
globals.css overrides .font-logo to use Cardo when html[dir="rtl"].
Picks up the cabinet logo in the sidebar header plus any other
.font-logo surface in Hebrew mode.
Refs #79
Direction-aware swaps using <DirIcon> in nav-arrows (back/forward), tour-modal (back arrow + next/finish arrows), and tree-node (chevron gets rtl:rotate-180 when collapsed). Panel collapse/expand icons get rtl:rotate-180 for visual consistency. Sidebar physical → logical Tailwind: mobile fixed position uses inset-y-0 start-0 (was left-0 top-0 bottom-0); resize handle uses -ms-px (was -ml-px); expand button uses start-2/start-3 (was left-). Tree-node chevron padding uses -ms-1 (was -ml-1). Marquee carousels in home-screen (RegistryCarousel) and onboarding wizard (TeamCarousel) flip translateX sign in RTL so items still emerge from the leading edge. CabinetCreatedScreen tree-line slide-in respects dir. Tour keyboard handler: ArrowRight = next in LTR, ArrowLeft = next in RTL (and vice versa for back). Refs #79
editorProps.attributes is read once at editor creation, so toggling frontmatter.dir wasn't reaching the ProseMirror contenteditable element after first render. Adds a useEffect that mutates the editor.view.dom dir attribute reactively. This makes list indentation, table cell alignment, and block direction all flip when the user toggles RTL via the toolbar. Also sets dir on initial editorProps so first paint is correct for documents that load with dir: rtl already in frontmatter. Refs #79
When the user's UI locale is Hebrew, the conversation client now reads
cabinet-locale from localStorage and adds it to every POST to
/api/agents/conversations. The API route threads locale into
buildManualConversationPrompt + buildEditorConversationPrompt, which
inject a 'Respond in {{language}}' instruction near the top of the
system prompt. Hebrew users get Hebrew replies + frontmatter dir:rtl
on generated notes.
CreateConversationRequest gets an optional locale field (auto-injected
by createConversation; callers don't usually set it).
writePage now auto-detects Hebrew content: if frontmatter.dir is
unset and >50% of letters in the first 600 chars fall in the Hebrew
unicode block (U+0590–U+05FF), dir defaults to rtl. Saves Hebrew users
from manually toggling RTL on every agent-saved note. Explicit dir
in frontmatter still wins.
Refs #79
Adds the tooling for ongoing translation work: - i18next-parser config (i18next-parser.config.cjs) extracts t() calls into locale JSON files. New English keys default to the key name so unfilled copy is obvious; new Hebrew keys default to empty so they flag in code review. Run: npm run i18n:extract - scripts/i18n-find-hardcoded.mjs walks src/components and reports user-facing English strings (JSX text + title/aria-label/placeholder props) that aren't wrapped in t(). Best-effort regex pass — false positives expected, but useful as a budget number that should trend down. First run reports ~595 hits across 121 files; we've translated the highest-traffic ~150 of those. Run: npm run i18n:report - docs/CONTRIBUTING_I18N.md explains namespace structure, how to add a string, how to add a new locale, RTL polish patterns, agent locale propagation, and what is intentionally not translated. Refs #79
Adds status namespace covering: server health states (Online / Offline / Degraded / Checking, plus the per-service popup with App Server / Daemon / Agent Providers + Running/Down badges + descriptive lines), save states (Auto-saving / Editing / Force save / retry), AI prompt input, git uncommitted status labels (Modified / New / Added / Deleted / Renamed), relative-time formatters (just now / Xs ago / Xm ago / Xh ago / Xd ago / never), and the Help & community popup (demos / Discord / GitHub / If you find it useful). describeUncommittedStatus, formatRelativeAgo, and formatRelativeSavedAgo now take TFunction so they can render translated strings. Callers updated. Refs #79
…lockquote/callout CSS
Adds a Hebrew variant of the getting-started seed corpus under
resources/getting-started-he/ (index, apps-and-repos, delegating-
between-agents, symlinks-and-load-knowledge, skills). Each file has
`dir: rtl` in frontmatter so the editor renders them RTL on load.
These are condensed Hebrew rewrites rather than line-by-line
translations — the English origin has heavy inline HTML and embedded
SVG icons that don't all translate cleanly. The Hebrew version covers
every concept and table from the original.
cabinet-scaffold's seedGettingStartedDir / scaffoldCabinet now accept
a locale parameter and try `resources/getting-started-{locale}` before
falling back to the English seed. /api/cabinets/create and
/api/onboarding/setup thread locale through from the client. New
Hebrew cabinets are now seeded with Hebrew docs on first run.
globals.css: blockquote + callout switched from border-left /
padding-left to logical border-inline-start / padding-inline-start
so they render correctly under <html dir="rtl">.
Win95/XP/Apple bevels use top/bottom asymmetry (universal light
direction) and don't need RTL flips — verified in audit, no changes
needed there.
Refs #79
Adds search and home namespaces. Search palette: dialog title, placeholder, clear button aria-label, scope tabs (All/Pages/Agents/ Tasks), group headers, and 'No commands match' empty state. Home screen: cabinet name placeholder + rename warning, quick-actions shuffle button. SCOPES const refactored from `label` literals to `labelKey`s so ResultList resolves via t() at render time without making the const itself component-scoped. Hardcoded-string budget: 595 → 557 (~40 strings translated, 3 files fully cleared). Run `npm run i18n:report` for the current list. Refs #79
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (149)
📝 WalkthroughWalkthroughThis PR adds app-wide i18n and Hebrew support, introduces locale persistence and prompt propagation, updates RTL rendering behavior, adds a language request flow in Settings, localizes many existing UI surfaces, and adds Hebrew getting-started seed documentation plus i18n tooling and docs. ChangesInternationalization and RTL rollout
Estimated code review effort🎯 5 (Critical) | ⏱️ ~110 minutes Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
Adds CSS rule to .tiptap and .registry-prose so every paragraph, heading, list item, blockquote, table cell, and figcaption picks its own reading direction from its first strong directional character. How it works: `unicode-bidi: plaintext` opts each block into the Unicode Bidi paragraph algorithm (rule P3) — the browser scans the inline content and infers direction. Combined with `text-align: start` the alignment follows the inferred direction, so a Hebrew paragraph right-aligns and an English paragraph left-aligns even within the same doc. This means mixed-language docs "just work" without the user toggling RTL per block. Frontmatter `dir` still controls scroll position and empty-paragraph cursor placement (the wrapper-level dir is the fallback for blocks without strong directional chars). Also fixes registry-prose ul/ol/li padding-left → padding-inline-start so list bullets land on the correct side when a list item is auto- detected as RTL. Refs #79
Layers semantic HTML `dir="auto"` on top of the previous CSS-only
auto-direction fix. The CSS (`unicode-bidi: plaintext`) handles visual
flow; `dir="auto"` on the actual DOM element handles everything else:
- Screen readers announce blocks with their inferred direction.
- List markers / bullets flip to the inferred side (CSS-only fix
didn't change `direction`, so markers could land wrong).
- Cursor positioning in mixed-bidi text uses the browser's tuned
paragraph path instead of the narrower unicode-bidi path.
- Tables: each <td>/<th> infers its own direction.
Mechanics: a Tiptap globalAttributes extension adds a `dir` attribute
to paragraph, heading, blockquote, listItem, taskItem, tableCell, and
tableHeader nodes. Default value is "auto"; existing explicit
"rtl"/"ltr" on incoming HTML is preserved. Code blocks are excluded
(always LTR monospace).
Round-trip note: turndown strips the `dir` attribute on markdown
export — that's fine, the "auto" default reapplies on reload so
behavior is stable across save/load cycles. The CSS rule is the
safety net for any block that doesn't get the attribute.
Refs #79
Bubble menu (17 strings): reuses existing editor.toolbar.* keys for Bold/Italic/Underline/Strikethrough/Inline code/Superscript/Subscript/ Text color/Highlight/Background/Link + new editor.toolbar.align key. Inline align popover (Left/Center/Right/Justify) uses the existing toolbar keys. Table menu (14 strings): new editor.toolbar.table.* keys for cell selection, row/column add/move/delete, header row/column toggle, and delete table. Refs #79
Translates the rest of Settings beyond Appearance + Language: - Sidebar: Show hidden files - Storage: Data Directory, Restart required - Updates: Checking for Cabinet updates… - Providers: Agent Providers, Default Model, Setup guide - Notifications: Channels, Alert Rules, Always on, Coming Soon, email placeholder - About: Cabinet, Version, Release, Framework, Storage, Privacy, telemetry - Profile: Name/Display name/Role + Avatar/Accent color/Fallback icon - Workspace: Name/Description/Team size - Settings categories aria-label, Copy to clipboard tooltip in TerminalCommand - Connect, Recommended (integrations) ProfileTab + TerminalCommand + AvatarPicker + IconPicker all get useLocale(). Search placeholders, Use icon instead title also wired. Refs #79
Adds agents namespace with dialog.* (createAgent, avatar, name/role placeholders, department, specialist/lead toggle, provider, runtime, prompt placeholder) and metric.* (name, target, floor, period, daily/ weekly/monthly + their placeholders). Both create-agent-dialog and edit-agent-dialog translated end-to-end. Refs #79
Adds tasks namespace with conversation.* keys: status action buttons (Restart/Mark done), tab labels (Terminal/Transcript/Details), transcript header (Back, Claude Code session JSONL hint), PTY mode indicators (copy prompt / graceful exit / SIGTERM / more actions / running in PTY mode / compact context), and session state labels (No summary yet / Session ended / Terminal live). StatusActionButton + TaskConversationPage both get useLocale(). Refs #79
Was: 12 namespace files per locale (24 files total).
Now: one src/i18n/locales/<locale>.json per language, with namespaces
nested as top-level keys.
Why: translators get a single en.json to translate end-to-end → produce
a single es.json / zh.json / ar.json without juggling per-namespace
fragments. Adding a new locale is one file in, one row added to
index.ts.
t() call sites are unchanged — i18next reads the same {namespace: {...}}
shape from the consolidated file. Build/typecheck/extract all green.
Bundled changes:
- Deleted src/i18n/locales/{en,he}/*.json (12+12 files)
- Created src/i18n/locales/{en,he}.json
- index.ts now imports the two files and derives the namespace list from
Object.keys(en) so adding a namespace requires zero index.ts edits
- Dropped i18next-parser (per-namespace output didn't fit the new layout)
- New scripts/i18n-extract.mjs walks t() calls and merges missing keys
into the consolidated files (idempotent)
- npm run i18n:check exits non-zero in CI if any t() keys aren't in
the locale files yet
- CONTRIBUTING_I18N.md rewritten for the single-file workflow
Refs #79
agents-workspace.tsx: ~37 strings under agents.workspace namespace (orgChart, jobDetails, editAgent, instructions/placeholder, newAgent, routines, browseLibrary, editYourOwn, runNow, delete, hideIntro, sharedAcrossCabinets, expandSchedule, etc.). Spans for Heartbeat / Workspace / Avatar / Department / Provider / Runtime now go through existing namespace keys. agent-detail-v2.tsx: ~29 strings under agents.detail (clickToTurnOff, color/customColor/preset, editIconColor, inbox, conversations, recentWork, schedule, heartbeat, runNow, editRoutine, addRoutine, details, editingGlobalAgent, displayName/role/department/type/ workspace/tags, openSkillsSettings, notInCatalog/recommendedForRole, saved). TopBar + StatusChip + AvatarEditorPopover + Hero + InboxSection + ConversationsSection + RecentWorkSection + ScheduleSection + DetailsSection + SkillsMultiSelect all get useLocale(). Budget: 426 → 358 (~68 strings translated). Refs #79
…, registry, tasks-board, data-locations
Bulk extraction across six high-traffic surfaces using a Python-driven
sweep:
- mission-control.tsx (11): Gallery, New Agent, All Goals, no-goals
state, From Scratch / Describe It / Import tabs, close dialog,
Describe Your Agent + 200-char placeholder
- pulse-strip.tsx (10): five PulseChip labels (Agents/Running/Goals/
Alerts/Cost), Running Plays popover (no-running state),
Cost Breakdown popover (no-usage / total estimated)
- new-routine-dialog.tsx (9): runNow / delete / prompt placeholder,
Job name + placeholder, Job id, Schedule, Model, Enabled
- registry-browser.tsx (9): Loading from registry, Ready to import,
Organization, Scheduled Jobs, About, Cabinet name + placeholder,
Search cabinets placeholder, no-results state
- tasks-board.tsx (10): page title Tasks, Manual/Jobs/Heartbeat
tooltips, Clear selection, create-task tooltip, More new item
types, New Task, New Routine
- data-locations-section.tsx (10): Where your data lives, Why this
matters, Location/Contains/Network headers, Reveal in file manager,
Stored, Reset onboarding
Keys consolidated under agents.{missionControl,pulseStrip,routine,
detail} + new top-level registry, tasksBoard, dataLocations
namespaces.
Budget: 370 → 306 (~64 strings, 6 files cleared).
Refs #79
…-panel, schedule-row, embed-popover Five more files extracted. New namespaces: - runtime (task-runtime-picker): noProvider, modeAriaLabel, native transcript title, OpenAI/Codex PTY warning - help.page (help-page): Cabinet brand, AI team / AI teams, Routines - agentDetailPanel: editConfig, exportBundle, Goals, Workspace - scheduleRow: deleteQ + confirmDelete/cancelDelete aria-labels, runNow - embedPopover: placeholder, notValidUrl, anyUrlWorks HELP_ITEMS module-level constant refactored to getHelpItems(t) so help.page.* keys resolve at render time (was a module-scope t() reference, which broke build). Budget: 306 → 287 (~19 strings, 5 files cleared). Refs #79
…y, scheduler, skill-library Five more files. New namespaces: apiKeys, updateSummary, editIdentity, cabinetScheduler, skillLibrary. Strings: Add key / Edit / Remove / New API key (ApiKeysSection + KeyRow + AddKeyForm), Cabinet Updates (UpdateSummary), Edit agent / Display name / Color / #hex (EditAgentIdentityDialog), Start/Stop all agents + subtitles (CabinetSchedulerControls), Read-only / no upstream / executable-warning / Skills (SkillLibrary's SkillCard). Budget: 287 → 270 (~17 strings, 5 files cleared). Refs #79
…duling-fields, update-summary data-dir Five more files. New namespaces: kanban, terminalTabs, newTaskButton, schedulingFields. Plus updateSummary.dataDirectory. Strings: stop-all/restart-all/new-task/collapse-column lane controls, new/close terminal tab, Create new / New task / New routine in the unified new-work button, RoutineFields scheduling form (job name placeholder, Schedule, Job id, Enabled toggle). Budget: 270 → 253 (~17 strings, 5 files cleared). Refs #79
…l-add, pulse-strip leftovers Knocks out remaining strings in task-runtime-picker (Native/Terminal tabs + PTY mode title + Providers aria), pulse-strip (5 chip labels that didn't match earlier regex), breaking-changes-warning (3 disclaimer lines + I-accept aria-label), skill-add-dialog (Add skill button + audit tooltips), update-summary (Backups label). Budget: 253 → 241. Refs #79
…, help, tasks Bulk pass across: - row-actions, schedules-panel, embed-popover plus, mermaid-viewer - help-page (Keyboard/Skills/API keys/MCP servers tiles), keyboard-shortcuts-modal (title + filter + close) - agent-detail-panel (Live Terminal / Running heartbeat / Memory / Recent Activity) - edit-user-avatar-dialog, update-summary (Local app-code changes) - logs-panel (Classified error / Session / no-events / no-transcript) - conversation-result-view (Prompt / Result / no summary) - schedule-row remaining (Run now / Delete), attachment-picker-button - pulse-strip MetricCard labels (5 — earlier regex matched PulseChip selector, real component is MetricCard) Budget: 241 → 191 (~50 strings, 12 files cleared, files-with-strings down from 109 → 97). Refs #79
Demos (ai-team + shortcuts), slide-agents, start-work-dialog, google-doc-viewer, feedback-popup, api-keys EditKeyForm + Cancel/ paste-here, storage-backend, link-repo-dialog, recent-tasks PTY + older buttons, tree-node (Open cabinet / Page title / Rename), board-skeletons loading aria. Budget: 191 → 155 (~36 strings, 10 files cleared). Refs #79
agent-list, agent-live-panel, conversation-live-view, edit-agent-identity plus Avatar/Use-icon, pending-actions-panel, ai-panel, scheduler restart, csv-viewer, resizable-image, folder-index, link-popover, office-chrome, source-viewer, version-history, demo-modal. Budget: 155 → 125 (~30 strings, 15 files cleared). Refs #79
Demos (knowledge/skills/task-board), narrow-viewport-hint, agent-card Run heartbeat, schedule-picker frequency/onDay, slack-panel create channel/thread, uninstall-section, tree-view drawers + page-title placeholder, skill-add Cancel, skill-catalog audit + search, skill-library refresh + no-skills, detail-panel close, row-actions delete, task-card PTY, undo-toast dismiss, terminal-exited details/ reconnect, terminal-tabs resize. Budget: 125 → 97 (~28 strings, 10 files cleared, files-with-strings 72 → 62). Refs #79
Cabinets (activity-feed / agent-status / task-composer / org-chart / depth-dropdown), composer (mention-chips / new-task title / scheduling Active / runtime PTY pick), editor (Edit with AI / jupyter / Exit app), demos (api-keys-demo / cabinets-demo), whats-new dismiss, breaking-changes I-understand, daemon-health dismiss. Budget: 97 → 81 (~16 strings, 16 files cleared). Refs #79
theme-picker Light, viewer-breadcrumb Home, workspace-gallery title, mockup-sidebar drawers aria, slide-tasks 'Saving to', connected- integrations Coming soon, skill-detail Delete skill, skills-offered- footer, filter-bar All agents, depth-dropdown Cabinet scope second occurrence, registry-browser Agents leftover. Budget: 81 → 72 (~9 strings, 9 files cleared, files-with-strings 46 → 37). Most of the remaining 72 are TypeScript Promise<T> false positives in the detector and brand/example labels in help-visuals + onboarding intro that intentionally stay English. Refs #79
…rn-block/dialog/etc Cleaned up the remaining real-string scatter: - onboarding-wizard: Copy to clipboard, Star Cabinet on GitHub, Help more people find the community, Join the Discord - diff-panel: No diff available - task-composer-panel: Heads up: - turn-block: Edit your avatar - task-detail-panel: Open full task viewer - ui/dialog: Close (sr-only) - claude-transcript-view: No transcript found Budget: 72 → 62 (10 real strings translated, 6 files cleared). Remaining 62 are mostly Promise<T> TypeScript false positives in the detector regex and intentionally-untranslated illustrative content in help-visuals + onboarding tour slide-data (sample file names, example department labels). Real user-facing strings are at single digits per file from here. Refs #79
…uals shortcuts Onboarding wizard: Chat with builders, Join Cabinet Cloud waitlist, email + name placeholders, I-have-read aria, I-understand text. Help-visuals ShortcutsVisual: Add to Inbox / Run task now / Search / AI panel / Drawers labels. Slide-tasks 'Saving to', depth-dropdown duplicate Cabinet scope, task-conversation Back tooltip. Budget: 62 → 49 (~13 strings, 8 files cleared). next build green. Refs #79
Settings → Language now shows the full board:
- Active row (English / עברית) — primary buttons, live switch
- 'More languages' grid: 80 locales sorted A–Z by english name,
rendered in their own native script with the right `dir`,
dense 3/4/5/6-col responsive grid with 11px text
- Click a disabled locale → POST to cabinet-backend
/language-requests with {locale, label, currentLocale} → toast
confirmation "Thanks! We've noted your request for {{language}}"
- Already-requested locales turn emerald, non-clickable, persist
in localStorage (cabinet-requested-locales) across sessions
- Failure path: error toast 'Couldn't reach the server'
LanguageSection moved from top of Appearance tab to below Sidebar
(Theme → Sidebar → Language order, separator-line above).
New file: src/lib/telemetry/language-request-client.ts — posts to
the cabinet-backend endpoint via the same origin convention as the
waitlist client.
Locale catalog: src/i18n/index.ts exports REQUESTABLE_LOCALES with
{code, label (native), englishName (sort key), dir}. RTL languages
(Arabic, Persian, Pashto, Urdu) tagged so we can wire those when
their translations land.
CONTRIBUTING_I18N.md: added the TL;DR 5-step recipe for adding
Spanish at the top of the 'Adding a new locale' section.
Refs #79
In RTL mode, all sidebar items were collapsed flush to the right edge with no visible hierarchy — depth indentation was applied via inline `paddingLeft` and `left:` styles, which always push from the physical left regardless of dir. Fix: switch to logical properties throughout the row. - paddingLeft → paddingInlineStart (main row indent per depth) - left: depth*16+8 → insetInlineStart (drop-indicator anchors) - right-1.5 → end-1.5 (drop-indicator outer anchor) - text-left → text-start (row label alignment) - before:left-0 / rounded-r-full → before:start-0 / rounded-e-full (active-row accent bar — flips to right edge in RTL, stays rounded on its inner side) - ml-auto → ms-auto (running spinner + status badge stay on the end side instead of jumping to the start in RTL) Now in RTL mode each depth level indents 16px from the right edge, matching LTR's left-edge behavior. Folder/file rows nest visibly under their parents. Refs #79
# Conflicts: # src/components/agents/agent-detail-v2.tsx
hilash
marked this pull request as ready for review
May 14, 2026 16:46
This was referenced May 14, 2026
agenticocho
pushed a commit
to agenticocho/cabinet
that referenced
this pull request
May 18, 2026
This was referenced May 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds Hebrew + English locale support with full RTL when locale=he. Refs #79
(scope narrowed: Hebrew first, Chinese deferred).
What ships
Infrastructure (P1) —
react-i18next+ browser language detector. Live Language toggle in Settings → Appearance (English / עברית), no restart. Pre-hydration inline script inlayout.tsxsets<html dir/lang>before first paint — no flash on Hebrew first load.RTL polish (P3) —
<DirIcon ltr={...} rtl={...} />helper applied to nav arrows / tour back-next / tree-node chevrons. Cardo italic for thecabinetlogo under<html dir="rtl">. Sidebar physical→logical Tailwind (fixed inset-y-0 start-0,-ms-px,start-2/3). Marquee carousels flip translateX sign in RTL. Tour keyboard ArrowLeft = forward in RTL. Tiptap ProseMirror DOMdirmutates reactively on toolbar toggle so lists / tables / alignment all flip per-document.border-inline-startfor blockquote + callouts.Agent locale (P4) —
createConversationauto-injects user locale on every POST.buildManualConversationPrompt+buildEditorConversationPromptprepend "Respond in {{language}}" to the system prompt.writePageauto-detects Hebrew Unicode (U+0590–U+05FF, >50% of letters in first 600 chars) → setsdir: rtlin frontmatter automatically when unset.Foundation (P5) —
i18next-parserconfig +npm run i18n:extract. Regex-based hardcoded-string detector +npm run i18n:report(557 remaining, down from 595, grouped by file).docs/CONTRIBUTING_I18N.mddocuments namespace structure, t() patterns, how to add a new locale, RTL polish patterns, agent locale propagation, and the intentionally-untranslated list. Hebrew seed content inresources/getting-started-he/(5 docs, alldir: rtl);seedGettingStartedDir(targetDir, locale)picks the right seed at bootstrap.What now translates in Hebrew
Settings appearance/language; sidebar (every tooltip + toast); tour modal (full); editor toolbar (33+ labels); page header export menu + AI panel toggle + confirm dialogs; new-page + new-cabinet dialogs; onboarding intro CTA + welcome paragraph + welcome-back + verify states + provider status; status bar (server health popup, save states, git uncommitted, Help menu, relative-time); search palette (placeholder, scope tabs, group headers); home screen quick-actions.
Agents reply in Hebrew when locale=he. Hebrew users get Hebrew getting-started seed pages on first cabinet creation.
Test plan
cabinet-localedir: rtlgetting-started/are Hebrewnpm run i18n:report— see the budget of remaining hardcoded strings (currently 557 across 118 files)tsc --noEmitcleannext buildsucceeds (verified locally — confirm in CI)Phasing
The 15 commits map to the 5 phases in
docs/I18N_RTL_HEBREW_PRD.md:What is intentionally not translated
Documented in
docs/CONTRIBUTING_I18N.md§"What is intentionally not translated". Highlights: thecabinetbrand mark stays Latin (renders in Cardo italic under RTL), keyboard shortcuts (⌘K, ⌘[) stay inline, provider identifiers, starter team brand names (English SaaS jargon), and the dictionary-card intro (English wordplay on "cabinet"). All open to revisiting.Remaining work (low priority, mechanical)
557 hardcoded strings across 118 files — visible via
npm run i18n:report. Top files:settings-page.tsx(49),agents-workspace.tsx(39),agent-detail-v2.tsx(29). Each follows the same pattern documented in CONTRIBUTING_I18N.md.Summary by CodeRabbit
New Features
Documentation