Skip to content

feat(i18n): Hebrew + RTL localization (English/Hebrew, full RTL) - #84

Merged
hilash merged 40 commits into
mainfrom
feat/i18n-rtl-hebrew
May 14, 2026
Merged

feat(i18n): Hebrew + RTL localization (English/Hebrew, full RTL)#84
hilash merged 40 commits into
mainfrom
feat/i18n-rtl-hebrew

Conversation

@hilash

@hilash hilash commented May 12, 2026

Copy link
Copy Markdown
Collaborator

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 in layout.tsx sets <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 the cabinet logo 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 DOM dir mutates reactively on toolbar toggle so lists / tables / alignment all flip per-document. border-inline-start for blockquote + callouts.

Agent locale (P4)createConversation auto-injects user locale on every POST. buildManualConversationPrompt + buildEditorConversationPrompt prepend "Respond in {{language}}" to the system prompt. writePage auto-detects Hebrew Unicode (U+0590–U+05FF, >50% of letters in first 600 chars) → sets dir: rtl in frontmatter automatically when unset.

Foundation (P5)i18next-parser config + npm run i18n:extract. Regex-based hardcoded-string detector + npm run i18n:report (557 remaining, down from 595, grouped by file). docs/CONTRIBUTING_I18N.md documents namespace structure, t() patterns, how to add a new locale, RTL polish patterns, agent locale propagation, and the intentionally-untranslated list. Hebrew seed content in resources/getting-started-he/ (5 docs, all dir: 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

  • Settings → Appearance → switch to עברית → entire UI flips RTL; cabinet logo renders in Cardo italic; sidebar moves to the right
  • Switch back to English without restart; everything reverts
  • Refresh — locale persists from localStorage cabinet-locale
  • Open a page, click "Switch to RTL" in editor toolbar → ProseMirror lists/tables flip per-document
  • Run the tour: arrows mirror, ArrowLeft on keyboard advances, marquee scrolls the other way
  • Send a message to an agent with locale=he → response is Hebrew, saved note has dir: rtl
  • Fresh bootstrap with locale=he → seed pages under getting-started/ are Hebrew
  • Run npm run i18n:report — see the budget of remaining hardcoded strings (currently 557 across 118 files)
  • tsc --noEmit clean
  • next build succeeds (verified locally — confirm in CI)
  • No regression in English UX (sidebar, themes, agents, editor)

Phasing

The 15 commits map to the 5 phases in docs/I18N_RTL_HEBREW_PRD.md:

  • P1 infra — 1 commit
  • P2 string extraction (highest-traffic surfaces) — 4 commits
  • P3 RTL polish — 3 commits
  • P4 agent locale — 1 commit
  • P5 foundation + status bar + seed content + search/home — 4 commits

What is intentionally not translated

Documented in docs/CONTRIBUTING_I18N.md §"What is intentionally not translated". Highlights: the cabinet brand 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

    • Multi-language support with English and Hebrew
    • Language selection in Settings with ability to request additional locales
    • RTL (right-to-left) layout support for Hebrew and Arabic-script languages
    • Automatic text direction detection based on content
  • Documentation

    • Added comprehensive internationalization guides and Hebrew implementation documentation

hilash added 15 commits May 12, 2026 15:02
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
@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 048ec94d-2623-4873-9765-28111c1165bb

📥 Commits

Reviewing files that changed from the base of the PR and between 76f1924 and 074a882.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (149)
  • PROGRESS.md
  • docs/CONTRIBUTING_I18N.md
  • docs/I18N_RTL_HEBREW_PRD.md
  • package.json
  • resources/getting-started-he/apps-and-repos/index.md
  • resources/getting-started-he/delegating-between-agents/index.md
  • resources/getting-started-he/index.md
  • resources/getting-started-he/skills/index.md
  • resources/getting-started-he/symlinks-and-load-knowledge/index.md
  • scripts/i18n-extract.mjs
  • scripts/i18n-find-hardcoded.mjs
  • src/app/api/agents/conversations/route.ts
  • src/app/api/cabinets/create/route.ts
  • src/app/api/onboarding/setup/route.ts
  • src/app/globals.css
  • src/app/layout.tsx
  • src/components/agents/agent-detail-v2.tsx
  • src/components/agents/agent-list.tsx
  • src/components/agents/agent-live-panel.tsx
  • src/components/agents/agents-workspace.tsx
  • src/components/agents/conversation-live-view.tsx
  • src/components/agents/conversation-result-view.tsx
  • src/components/agents/edit-agent-identity-dialog.tsx
  • src/components/agents/new-routine-dialog.tsx
  • src/components/agents/pending-actions-panel.tsx
  • src/components/agents/schedule-row.tsx
  • src/components/ai-panel/ai-panel.tsx
  • src/components/cabinets/activity-feed.tsx
  • src/components/cabinets/agent-status-card.tsx
  • src/components/cabinets/cabinet-scheduler-controls.tsx
  • src/components/cabinets/cabinet-task-composer.tsx
  • src/components/cabinets/compact-org-chart.tsx
  • src/components/cabinets/depth-dropdown.tsx
  • src/components/cabinets/schedules-panel.tsx
  • src/components/composer/attachment-picker-button.tsx
  • src/components/composer/mention-chips.tsx
  • src/components/composer/new-task-button.tsx
  • src/components/composer/scheduling-fields.tsx
  • src/components/composer/start-work-dialog.tsx
  • src/components/composer/task-runtime-picker.tsx
  • src/components/editor/bubble-menu.tsx
  • src/components/editor/csv-viewer.tsx
  • src/components/editor/editor-toolbar.tsx
  • src/components/editor/editor.tsx
  • src/components/editor/embed-popover.tsx
  • src/components/editor/extensions.ts
  • src/components/editor/extensions/auto-direction.ts
  • src/components/editor/extensions/resizable-image.tsx
  • src/components/editor/folder-index.tsx
  • src/components/editor/google-doc-viewer.tsx
  • src/components/editor/link-popover.tsx
  • src/components/editor/mermaid-viewer.tsx
  • src/components/editor/notebook-viewer.tsx
  • src/components/editor/office/office-chrome.tsx
  • src/components/editor/source-viewer.tsx
  • src/components/editor/table-menu.tsx
  • src/components/editor/version-history.tsx
  • src/components/editor/website-viewer.tsx
  • src/components/help/demo-modal.tsx
  • src/components/help/demos/ai-team-demo.tsx
  • src/components/help/demos/api-keys-demo.tsx
  • src/components/help/demos/cabinets-demo.tsx
  • src/components/help/demos/knowledge-demo.tsx
  • src/components/help/demos/shortcuts-demo.tsx
  • src/components/help/demos/skills-demo.tsx
  • src/components/help/demos/task-board-demo.tsx
  • src/components/help/help-page.tsx
  • src/components/help/help-visuals.tsx
  • src/components/help/keyboard-shortcuts-modal.tsx
  • src/components/help/whats-new-card.tsx
  • src/components/home/home-screen.tsx
  • src/components/layout/breaking-changes-warning.tsx
  • src/components/layout/daemon-health-banner.tsx
  • src/components/layout/header-actions.tsx
  • src/components/layout/header.tsx
  • src/components/layout/locale-initializer.tsx
  • src/components/layout/narrow-viewport-hint.tsx
  • src/components/layout/nav-arrows.tsx
  • src/components/layout/status-bar.tsx
  • src/components/layout/theme-picker.tsx
  • src/components/layout/viewer-breadcrumb.tsx
  • src/components/mission-control/agent-card.tsx
  • src/components/mission-control/agent-detail-panel.tsx
  • src/components/mission-control/create-agent-dialog.tsx
  • src/components/mission-control/edit-agent-dialog.tsx
  • src/components/mission-control/mission-control.tsx
  • src/components/mission-control/pulse-strip.tsx
  • src/components/mission-control/schedule-picker.tsx
  • src/components/mission-control/slack-panel.tsx
  • src/components/mission-control/workspace-gallery.tsx
  • src/components/onboarding/feedback-popup.tsx
  • src/components/onboarding/onboarding-wizard.tsx
  • src/components/onboarding/tour/mockup-sidebar.tsx
  • src/components/onboarding/tour/slide-agents.tsx
  • src/components/onboarding/tour/slide-tasks.tsx
  • src/components/onboarding/tour/tour-modal.tsx
  • src/components/registry/registry-browser.tsx
  • src/components/search/search-palette.tsx
  • src/components/settings/api-keys-section.tsx
  • src/components/settings/connected-integrations-card.tsx
  • src/components/settings/data-locations-section.tsx
  • src/components/settings/edit-user-avatar-dialog.tsx
  • src/components/settings/settings-page.tsx
  • src/components/settings/storage-backend-section.tsx
  • src/components/settings/uninstall-section.tsx
  • src/components/sidebar/link-repo-dialog.tsx
  • src/components/sidebar/new-cabinet-dialog.tsx
  • src/components/sidebar/new-page-dialog.tsx
  • src/components/sidebar/recent-tasks.tsx
  • src/components/sidebar/sidebar.tsx
  • src/components/sidebar/tree-node.tsx
  • src/components/sidebar/tree-view.tsx
  • src/components/skills/skill-add-dialog.tsx
  • src/components/skills/skill-catalog-browser.tsx
  • src/components/skills/skill-detail.tsx
  • src/components/skills/skill-library.tsx
  • src/components/skills/skills-offered-footer.tsx
  • src/components/system/update-summary.tsx
  • src/components/tasks/board/board-skeletons.tsx
  • src/components/tasks/board/detail-panel.tsx
  • src/components/tasks/board/filter-bar.tsx
  • src/components/tasks/board/kanban-view.tsx
  • src/components/tasks/board/row-actions.tsx
  • src/components/tasks/board/task-card.tsx
  • src/components/tasks/board/tasks-board.tsx
  • src/components/tasks/board/undo-toast.tsx
  • src/components/tasks/conversation/claude-transcript-view.tsx
  • src/components/tasks/conversation/diff-panel.tsx
  • src/components/tasks/conversation/logs-panel.tsx
  • src/components/tasks/conversation/task-composer-panel.tsx
  • src/components/tasks/conversation/task-conversation-page.tsx
  • src/components/tasks/conversation/turn-block.tsx
  • src/components/tasks/task-detail-panel.tsx
  • src/components/terminal/terminal-exited-view.tsx
  • src/components/terminal/terminal-tabs.tsx
  • src/components/ui/confirm-dialog-host.tsx
  • src/components/ui/dialog.tsx
  • src/components/ui/dir-icon.tsx
  • src/i18n/formatters.ts
  • src/i18n/index.ts
  • src/i18n/locales/en.json
  • src/i18n/locales/he.json
  • src/i18n/use-locale.ts
  • src/lib/agents/conversation-client.ts
  • src/lib/agents/conversation-runner.ts
  • src/lib/storage/cabinet-scaffold.ts
  • src/lib/storage/page-io.ts
  • src/lib/telemetry/language-request-client.ts
  • src/types/conversations.ts

📝 Walkthrough

Walkthrough

This 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.

Changes

Internationalization and RTL rollout

Layer / File(s) Summary
I18n contracts, resources, and tooling
src/i18n/*, src/i18n/locales/*, scripts/i18n-*.mjs, package.json, docs/I18N_RTL_HEBREW_PRD.md, docs/CONTRIBUTING_I18N.md, PROGRESS.md
Adds i18n dependencies, locale constants/hooks/formatters, English and Hebrew translation catalogs, extraction/report scripts, and implementation documentation.
Locale propagation into bootstrap, prompts, and seeded content
src/app/layout.tsx, src/components/layout/locale-initializer.tsx, src/app/api/..., src/lib/agents/..., src/lib/storage/..., src/types/conversations.ts
Bootstraps html locale before hydration, threads locale through conversation and cabinet creation APIs, adds locale-aware prompt instructions, selects locale-specific seed content, and infers page direction from text.
RTL rendering and direction-aware behavior
src/app/globals.css, src/components/ui/dir-icon.tsx, src/components/editor/..., src/components/sidebar/..., src/components/home/home-screen.tsx, src/components/onboarding/tour/tour-modal.tsx
Switches several layouts and controls to logical direction-aware behavior, adds direction-sensitive icons, and updates editor and viewer direction handling.
Settings language flow, onboarding, and Hebrew seed docs
src/components/settings/settings-page.tsx, src/lib/telemetry/language-request-client.ts, src/components/onboarding/*, src/components/sidebar/new-cabinet-dialog.tsx, resources/getting-started-he/*
Adds a settings language section with request submission and local persistence, localizes onboarding and cabinet creation flows, and adds Hebrew getting-started content used for seeded cabinets.
Localized strings across app surfaces
src/components/agents/*, src/components/cabinets/*, src/components/composer/*, src/components/help/*, src/components/layout/*, src/components/mission-control/*, src/components/registry/*, src/components/search/*, src/components/skills/*, src/components/tasks/*, src/components/terminal/*, src/components/ui/*, src/components/editor/*, src/components/settings/*
Replaces hardcoded labels, tooltips, placeholders, headings, and accessibility text with translation lookups across existing product surfaces.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~110 minutes

Possibly related PRs

  • hilash/cabinet#53: Both PRs touch RTL/i18n-related UI text and direction-sensitive components such as agent and cabinet interface surfaces.

Poem

🐇 I tucked two tongues into one bright burrow,
with right-to-left moonbeams for tomorrow.
Buttons now whisper in Hebrew and English,
sidebars and prompts no longer languish.
I thumped once in joy: “Locale, hop on through!”

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/i18n-rtl-hebrew

hilash added 14 commits May 12, 2026 23:41
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
hilash added 11 commits May 14, 2026 16:49
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
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