Skip to content

feat(app): rtl header for Hebrew and Arabic systems - #54898

Closed
arielfrja wants to merge 1 commit into
anomalyco:devfrom
arielfrja:rtl-header-v2
Closed

arielfrja wants to merge 1 commit into
anomalyco:devfrom
arielfrja:rtl-header-v2

Conversation

@arielfrja

@arielfrja arielfrja commented Oct 12, 2026 •

Copy link
Copy Markdown

Issue for this PR

Follow-up to #48856 (closed: V1 branch upgraded to V2). This ports that change onto the V2 codebase.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

V2 already ships full Hebrew locale support (including RTL), which covers the common case. What is still missing is tying the desktop titlebar to the OS language specifically:

  • systemPrefersRtl() in packages/app/src/runtime/i18n/desktop-native.ts detects RTL OS languages from navigator.languages (the OS language in Electron): he/iw/ar/fa/ur/dv/yi/ps.
  • New language.systemDirection(), consumed only by the titlebar. The <header> in packages/app/src/shell/titlebar/titlebar.tsx gets dir from it, so a Hebrew/Arabic system gets an RTL header even when the UI language is LTR.
  • Caption-button clearance uses the Window Controls Overlay geometry (left: env(titlebar-area-x, 0px)), converging with fix(app): respect RTL Windows caption controls #50661 — no language guessing for positioning, and the old margin-right: auto assumption is gone.

Out of scope for this PR (will propose separately): a direction.toggle command/hotkey for flipping content direction.

How did you verify your code works?

  • bun test src/runtime/i18n/desktop-native.test.ts: 32 pass, 0 fail (extends the existing test.each tables with systemPrefersRtl cases: he-IL, iw-IL, ar-EG, LTR negatives, tag normalization).
  • bunx oxlint on all touched files: 0 errors.
  • bun run typecheck in packages/app: no errors in any file this PR touches. Repo-wide there are pre-existing latent errors (previously masked behind the custom-elements.d.ts symlink issue below); none point at lines added here.

Screenshots / recordings

Not yet re-verified visually on the V2 base (the V1 verification with the mirrored header on Hebrew Windows is in #48856). Will run the V2 desktop dev build and attach.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@opencode-agent opencode-agent Bot left a comment

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.

Thanks for porting this to V2. I couldn't check the header placement itself: it needs the Windows desktop app with an RTL system language. I ran the new tests (32 pass) and bun run typecheck in packages/app, which passes both with and without your custom-elements.d.ts change. A few things need fixing before this can go in.

Public API: unchanged. This adds an internal language.systemDirection() accessor and a direction.toggle app command bound to mod+shift+d. Nothing in the SDK, server, plugin or config surface is affected.

Concerns, most important first:

  1. The custom-elements.d.ts commit breaks both files. They are git symlinks (mode 120000) to ../../ui/src/custom-elements.d.ts. The commit keeps them as symlinks but sets the link target to the text /// <reference path=... />, so the link now points nowhere on any system that supports symlinks. The TS1128 error you saw comes from a checkout with symlinks turned off (for example Windows with core.symlinks=false), where git writes the target path as plain text. It isn't a bug on dev: typecheck passes there with the original links. Please drop this commit, and enable symlinks in your checkout instead (git config core.symlinks true with Developer Mode, then re-checkout).
  2. Overlap with #50661. That open PR fixes the same problem, keeping the titlebar clear of Windows caption buttons when they're mirrored to the left. It positions the header from env(titlebar-area-x) / env(titlebar-area-width), which is where Chromium actually draws the overlay. This PR guesses the side from navigator.languages. In Electron that list reflects Chromium's locale and accept-languages, which can differ from the OS UI language, and whether the controls are mirrored follows Chromium's locale, not the user's language list. If the guess and the real position disagree, the header covers the caption buttons or leaves a gap on the wrong side. Please compare with #50661. The env-based approach doesn't need any language detection.
  3. Scope. Setting dir on the whole <header> mirrors every titlebar control (tabs, menus, buttons) for users whose UI language is LTR, which goes beyond reserving space for the caption buttons. The new direction.toggle command and its global shortcut also aren't needed for the header fix. I'd suggest proposing them separately, if at all.

You also mentioned you haven't yet checked this visually on V2. Before/after screenshots on RTL Windows would help once the points above are addressed.

Comment thread packages/app/src/custom-elements.d.ts Outdated
@@ -1 +1 @@
../../ui/src/custom-elements.d.ts No newline at end of file
/// <reference path="../../ui/src/custom-elements.d.ts" /> No newline at end of file

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.

This file is still a symlink (mode 120000), and its target is now the literal string /// <reference path="../../ui/src/custom-elements.d.ts" />, so it's a broken link. The same applies to packages/enterprise/src/custom-elements.d.ts. On dev the link works and bun run typecheck passes. The TS1128 error comes from a checkout with symlinks turned off. Please revert this commit.

const RTL_LANGUAGE_CODES: ReadonlySet<string> = new Set(["ar", "he", "iw", "fa", "ur", "dv", "yi", "ps"])

export function systemPrefersRtl(languages: readonly string[]): boolean {
return languages.some((tag) => {

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.

.some over every preferred language means ["en-US", "he-IL"] counts as RTL (your test asserts exactly this). An English-Windows user who has also added Hebrew or Arabic as a secondary language would get an RTL header with margin-left: auto, while the caption buttons stay on the right. If you keep language detection at all, only the first entry should count. Reading the real overlay position (see #50661) avoids the guess entirely.

// and on the right in LTR ones. The header reserves space on that side.
// Its direction follows the OS language only, never the UI language or content.
"margin-right": windows() && language.systemDirection() === "ltr" ? "auto" : undefined,
"margin-left": windows() && language.systemDirection() === "rtl" ? "auto" : undefined,

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.

Where the Windows caption controls sit follows Chromium's locale, not navigator.languages, and env(titlebar-area-x) already says exactly where they are. #50661 uses that to position the header. Choosing margin-left/margin-right from a language guess can disagree with the real overlay and make the header cover the caption buttons.

},
])

command.register("direction", () => [

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.

This adds a user-facing command and a global mod+shift+d shortcut for what is currently a debug-bar switch. The header fix doesn't need it. I'd drop it from this PR and propose it separately if it's wanted.

@arielfrja

Copy link
Copy Markdown
Author

Thanks for the detailed review — all three points addressed in the latest push (single commit now, daa1262):

  1. Symlink commit dropped. Verified mode 120000 on both files — you were right, my checkout has core.symlinks=false so git materialized the target as text and I \u201cfixed\u201d the symptom. Sorry about that. The files are untouched now.
  2. Converged with fix(app): respect RTL Windows caption controls #50661 for clearance. The margin-guessing (margin-right/margin-left from
    avigator.languages) is gone; the header now uses left: env(titlebar-area-x, 0px) exactly as fix(app): respect RTL Windows caption controls #50661 does, so positioning follows the real overlay geometry. systemDirection remains only as the source for the header \dir.
  3. Scope trimmed. The \direction.toggle\ command, its keybind, and the i18n key are out of this PR — I\u2019ll propose them separately. What remains is: RTL header direction from the OS language + overlay-based clearance.

On the \dir\ scope question: the explicit ask behind this PR is an RTL header on RTL systems even when the UI language is LTR (e.g. English UI on Hebrew Windows). Happy to narrow further if you\u2019d rather the header inherit document direction, but then Hebrew-OS/English-UI goes back to an LTR header over left-side caption buttons.

Screenshots on V2 + Hebrew Windows to follow.

@github-actions

Copy link
Copy Markdown
Contributor

Automated PR Cleanup

Thank you for contributing to opencode.

Due to the high volume of PRs from users and AI agents, we periodically close older PRs using automated criteria so maintainers can focus review time on the most active and community-supported contributions.

This PR was closed because it matched the following cleanup criteria:

  • The PR was created more than 1 month ago
  • The PR had fewer than 2 positive reactions
  • Positive reactions are counted as thumbs-up, heart, celebration, or rocket reactions on the PR

PRs created within the last month are not affected by this cleanup.

If you believe this PR was closed incorrectly, or if you are still actively working on it, please leave a comment explaining why it should be reopened. A maintainer can review and reopen it if appropriate.

Thanks again for taking the time to contribute.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant