diff --git a/public/index.css b/public/index.css index c16d2e5c89200..7cfb9a488983f 100644 --- a/public/index.css +++ b/public/index.css @@ -267,7 +267,7 @@ article > section li > :is(p, pre, .code-snippet, blockquote):not(:first-child) } article > section nav :is(ul, ol) { - padding-left: inherit; + padding-inline-start: inherit; } article > section nav { @@ -334,7 +334,7 @@ code { } /*RTL Fix Arrows*/ -[dir="rtl"] .toc-mobile-container svg, [dir="rtl"] [rel="next"] svg { +[dir="rtl"] .toc-mobile-container svg { transform: rotateY(180deg); } @@ -344,7 +344,7 @@ pre { --padding-block: 1rem; --padding-inline: 2rem; padding: var(--padding-block) var(--padding-inline); - padding-right: calc(var(--padding-inline) * 2); + padding-inline-end: calc(var(--padding-inline) * 2); margin-left: calc(var(--padding-inline) * -1); margin-right: calc(var(--padding-inline) * -1); font-family: var(--font-mono); @@ -392,7 +392,7 @@ th { td, th { padding: 6px; - text-align: left; + text-align: start; } blockquote code { @@ -402,7 +402,7 @@ blockquote code { blockquote { margin: 2rem 0 2rem 0; padding: 1.25em 1.5rem; - border-left: 8px solid var(--theme-divider); + border-inline-start: 8px solid var(--theme-divider); background-color: var(--theme-bg-offset); border-radius: 0 0.25rem 0.25rem 0; line-height: 1.7; @@ -503,6 +503,7 @@ h2.heading { font: inherit; color: var(--theme-text-lighter); text-decoration: none; + unicode-bidi: plaintext; } .header-link:hover, @@ -545,7 +546,7 @@ h2.heading { /* Apply different TOC styling for wide viewports showing the right sidebar */ @media (min-width: 72em) { h2.heading { - padding-left: calc(1rem + 4px); + padding-inline-start: calc(1rem + 4px); } .header-link { diff --git a/scripts/add-language.mjs b/scripts/add-language.mjs index 9162837a8809f..122fa91140164 100644 --- a/scripts/add-language.mjs +++ b/scripts/add-language.mjs @@ -23,6 +23,8 @@ class LanguageScaffolder { #tag = ''; /** Language name (e.g. English, Português do Brasil, etc.) */ #name = ''; + /** Language writing direction (`'ltr' | 'rtl'`) */ + #dir = ''; /** Track whether this instance has made any changes. */ #dirty = false; @@ -69,6 +71,15 @@ class LanguageScaffolder { validate: (name) => (name ? true : kleur.reset('[Press Enter to resubmit] ') + kleur.red().italic('Please enter a language name.')), format: (value) => value.trim(), }, + { + type: 'select', + name: 'dir', + message: 'Writing direction', + choices: [ + { title: 'Left-to-right', description: '(e.g. English, Russian, etc.)', value: 'ltr' }, + { title: 'Right-to-left', description: '(e.g. Arabic, Hebrew, etc.)', value: 'rtl' }, + ], + }, { type: 'confirm', name: 'confirm', @@ -77,11 +88,12 @@ class LanguageScaffolder { }, ]; - const { tag, name, confirm } = await prompts(questions); + const { tag, name, dir, confirm } = await prompts(questions); console.log(); // Add newline after questions summary. this.#tag = tag; this.#name = name; + this.#dir = dir; if (!confirm) process.exit(0); } @@ -136,6 +148,22 @@ class LanguageScaffolder { defaultExport.properties.push(newProperty); } }, + // Handle the set of RTL languages. + ExportNamedDeclaration: (path) => { + if (this.#dir !== 'rtl') return; + + const namedExport = path.node.declaration; + if (!t.isVariableDeclaration(namedExport)) return; + + const declarator = namedExport.declarations[0]; + if (declarator.id.name !== 'rtlLanguages') return; + + const langArray = declarator.init.arguments[0]; + if (!t.isArrayExpression(langArray)) return; + + const langAlreadyInList = langArray.elements.some(({ value }) => value === this.#tag); + if (!langAlreadyInList) langArray.elements.push(t.stringLiteral(this.#tag)); + }, }); if (!langAlreadyInList) { diff --git a/src/components/Button.astro b/src/components/Button.astro index 7644056309786..6addcf638efdd 100644 --- a/src/components/Button.astro +++ b/src/components/Button.astro @@ -3,16 +3,13 @@ const { class: className = '', style, href } = Astro.props; // Wrap in <span> because Houdini is disabled for a[href] for security const { variant = 'primary' } = Astro.props; -const { before, after } = Astro.slots; --- -<span class={`link pixel variant-${variant} ${before ? 'has-before' : ''} ${after ? 'has-after' : ''} ${className}`.trim()} {style}> +<span class:list={[`link pixel variant-${variant}`, className]} {style}> <a {href}> - <slot name="before" /> <span><slot /></span> - <slot name="after" /> </a> </span> @@ -108,14 +105,6 @@ const { before, after } = Astro.slots; .link:active { transform: translateY(0); } - .has-before a :first-child { - margin-left: -1rem; - margin-right: 0.25rem; - } - .has-before a :last-child { - margin-left: 0.25rem; - margin-right: -1rem; - } a { display: flex; align-items: center; @@ -136,7 +125,7 @@ const { before, after } = Astro.slots; } a > :global(* + *) { - margin-left: 0.25rem; + margin-inline-start: 0.25rem; } .variant-primary { diff --git a/src/components/DeployGuidesNav.astro b/src/components/DeployGuidesNav.astro index 66aa4220b07ff..903b24b390bb5 100644 --- a/src/components/DeployGuidesNav.astro +++ b/src/components/DeployGuidesNav.astro @@ -79,6 +79,7 @@ const services: Service[] = [ user-select: none; font-weight: bold; cursor: pointer; + unicode-bidi: plaintext; } .filter-text { diff --git a/src/components/LeftSidebar/LeftSidebar.astro b/src/components/LeftSidebar/LeftSidebar.astro index cac5d1c7805ca..00954e0df98d8 100644 --- a/src/components/LeftSidebar/LeftSidebar.astro +++ b/src/components/LeftSidebar/LeftSidebar.astro @@ -82,7 +82,6 @@ for (const section of sidebarSections) { position: fixed; top: calc(var(--theme-navbar-height) + 3rem); bottom: 0; - right: unset; width: calc(var(--theme-left-sidebar-width) - var(--min-spacing-inline) * 1.6); } } diff --git a/src/components/LeftSidebar/SidebarContent.astro b/src/components/LeftSidebar/SidebarContent.astro index 3aa69586ca23d..4be0e9d87d2f4 100644 --- a/src/components/LeftSidebar/SidebarContent.astro +++ b/src/components/LeftSidebar/SidebarContent.astro @@ -165,6 +165,10 @@ const lang = getLanguageFromURL(Astro.url.pathname); vertical-align: middle; } + :global([dir="rtl"]) svg { + transform: rotate(180deg); + } + svg path { fill: currentColor; } diff --git a/src/components/LeftSidebar/Sponsors.astro b/src/components/LeftSidebar/Sponsors.astro index 3943b385f86e3..b578cbec6be16 100644 --- a/src/components/LeftSidebar/Sponsors.astro +++ b/src/components/LeftSidebar/Sponsors.astro @@ -36,11 +36,6 @@ } .sponsor { margin-bottom: -0.375rem; - /* display: grid; - padding-left: 1rem; - padding: 0.25rem; - grid-gap: 0.5rem; - grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); */ } svg { color: var(--theme-text); diff --git a/src/components/PageContent/ArticleNavigationButton.astro b/src/components/PageContent/ArticleNavigationButton.astro index 83cab95e2027d..635ab32843a83 100644 --- a/src/components/PageContent/ArticleNavigationButton.astro +++ b/src/components/PageContent/ArticleNavigationButton.astro @@ -59,4 +59,7 @@ const { item, rel } = Astro.props as Props; font-weight: bold; color: var(--theme-text); } + :global([dir="rtl"]) svg { + transform: rotateY(180deg); + } </style> diff --git a/src/components/PageContent/PageContent.astro b/src/components/PageContent/PageContent.astro index 79bb4ef0d9b97..14d32d99d04de 100644 --- a/src/components/PageContent/PageContent.astro +++ b/src/components/PageContent/PageContent.astro @@ -4,7 +4,13 @@ import TableOfContents from '../RightSidebar/TableOfContents'; import { getNav, useTranslations } from "../../i18n/util"; import { getLanguageFromURL } from '../../util'; -const { content, currentPage } = Astro.props; +export interface Props { + content: Record<string, any>; + currentPage: string; + layoutDir: 'ltr' | 'rtl'; +} + +const { content, currentPage, layoutDir } = Astro.props as Props; // We wrap `@astrojs/` in a span to style it separately on integration pages. const title = content.title.replace('@astrojs/', '<span class="scope">@astrojs/</span>'); const headings = content.astro?.headings; @@ -23,7 +29,7 @@ const t = useTranslations(Astro); // inside elements that hide any overflow axis. The article content hides `overflow-x`, // so we must place the mobile TOC here. headings && ( - <nav class="mobile-toc"> + <nav class="mobile-toc" dir={layoutDir}> <TableOfContents client:media="(max-width: 72em)" headings={headings} @@ -78,14 +84,13 @@ const t = useTranslations(Astro); .mobile-toc { display: block; position: fixed; - left: 0; + inset-inline: 0; top: calc(var(--theme-navbar-height)); - right: 0; z-index: 2; } @media (min-width: 50em) { .mobile-toc { - left: var(--theme-left-sidebar-width); + inset-inline-start: var(--theme-left-sidebar-width); margin-top: 0; } } diff --git a/src/components/RightSidebar/CommunityMenu.astro b/src/components/RightSidebar/CommunityMenu.astro index 8d6fd7c368938..9d16b515cbadc 100644 --- a/src/components/RightSidebar/CommunityMenu.astro +++ b/src/components/RightSidebar/CommunityMenu.astro @@ -20,7 +20,7 @@ const HeadingWrapper = hideOnLargerScreens <HeadingWrapper> <h2 class="heading"><UIString key="rightSidebar.community" /></h2> { hideOnLargerScreens && - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 1 16 16" width="16" height="16" aria-hidden="true"> + <svg class="chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 1 16 16" width="16" height="16" aria-hidden="true"> <path fill-rule="evenodd" d="M6.22 3.22a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 010-1.06z"> </path> @@ -78,16 +78,16 @@ const HeadingWrapper = hideOnLargerScreens <style> h2, ul { - padding-left: 2rem; + padding-inline-start: 2rem; } @media (min-width: 50em) { h2 { - padding-left: 1rem; + padding-inline-start: 1rem; } ul { - padding-left: 0; + padding-inline-start: 0; } } @@ -97,7 +97,7 @@ const HeadingWrapper = hideOnLargerScreens } h2 { - padding-left: calc(1rem + 4px); + padding-inline-start: calc(1rem + 4px); } } @@ -114,7 +114,7 @@ const HeadingWrapper = hideOnLargerScreens display: none; } - details[open] > summary svg { + details[open] > summary .chevron { transform: rotate(90deg); } @@ -128,12 +128,16 @@ const HeadingWrapper = hideOnLargerScreens display: inline; } - svg { + .chevron { transform: rotate(0); transition: .15s transform ease; vertical-align: middle; } + :global([dir="rtl"]) .chevron { + transform: rotate(180deg); + } + svg path { fill: currentColor; } diff --git a/src/components/RightSidebar/TableOfContents.css b/src/components/RightSidebar/TableOfContents.css index 50c54134cc3ae..4df99d48e92f3 100644 --- a/src/components/RightSidebar/TableOfContents.css +++ b/src/components/RightSidebar/TableOfContents.css @@ -49,13 +49,19 @@ margin-inline-end: 0.5rem; border-radius: 0.5rem; border: 1px solid var(--theme-shade-subtle); - padding: 0.25rem 0.5rem 0.25rem 0.75rem; + padding: 0.25rem 0.75rem; + padding-inline-end: 0.5rem; +} + +.toc-toggle svg { + margin-inline-start: .25rem; } .toc-current-heading { text-overflow: ellipsis; overflow: hidden; color: var(--theme-text-light); + unicode-bidi: plaintext; } .toc-mobile-container[open] .toc-toggle { @@ -68,7 +74,7 @@ } .toc-mobile-header span { - margin-left: 0.2rem; + margin-inline-start: 0.2rem; } .toc-mobile-header svg { diff --git a/src/i18n/languages.ts b/src/i18n/languages.ts index 8fceecee14d23..37280ba3b12d1 100644 --- a/src/i18n/languages.ts +++ b/src/i18n/languages.ts @@ -14,3 +14,5 @@ export default { ja: '日本語', ru: 'Русский', }; + +export const rtlLanguages = new Set(['ar']); diff --git a/src/layouts/MainLayout.astro b/src/layouts/MainLayout.astro index 960a7acba61b2..ea8db1414aa14 100644 --- a/src/layouts/MainLayout.astro +++ b/src/layouts/MainLayout.astro @@ -9,11 +9,13 @@ import RightSidebar from '../components/RightSidebar/RightSidebar.astro'; import { getLanguageFromURL } from '../util'; import { normalizeLangTag } from '../i18n/bcp-normalize'; import { useTranslations } from '../i18n/util'; +import { rtlLanguages } from '../i18n/languages'; const { content = {}, hideRightSidebar = false } = Astro.props; const isFallback = !!Astro.params.fallback; const url = new URL(Astro.request.url); const lang = getLanguageFromURL(url.pathname); +const direction = rtlLanguages.has(lang) ? 'rtl' : 'ltr'; const bcpLang = normalizeLangTag(lang); const currentPage = url.pathname; const filePath = `src/pages${currentPage.replace(/\/$/, '')}.md`; @@ -28,7 +30,7 @@ const canonicalURL = new URL(Astro.url.pathname.replace(/([^/])$/, '$1/'), Astro if (isFallback) canonicalURL.pathname = canonicalURL.pathname.replace(`/${lang}/`, '/en/'); --- -<html dir={content.dir ?? 'ltr'} lang={bcpLang} class="initial"> +<html dir={direction} lang={bcpLang} class="initial"> <head> <HeadCommon /> <HeadSEO {content} {canonicalURL} /> @@ -44,12 +46,12 @@ if (isFallback) canonicalURL.pathname = canonicalURL.pathname.replace(`/${lang}/ display: none; background: var(--theme-bg-gradient); z-index: 10; - left: 0; + inset-inline-start: 0; } #right-sidebar { display: none; top: var(--theme-navbar-height); - right: 0; + inset-inline-end: 0; width: var(--theme-right-sidebar-width); } #main-content { @@ -65,7 +67,7 @@ if (isFallback) canonicalURL.pathname = canonicalURL.pathname.replace(`/${lang}/ :global(.mobile-sidebar-toggle #left-sidebar) { display: block; top: var(--theme-navbar-height); - right: 0; + inset-inline-end: 0; } /* Try to prevent the rest of the page from scrolling, @@ -89,7 +91,7 @@ if (isFallback) canonicalURL.pathname = canonicalURL.pathname.replace(`/${lang}/ @media (min-width: 50em) { #main-content { - margin-left: var(--theme-left-sidebar-width); + margin-inline-start: var(--theme-left-sidebar-width); } #left-sidebar { display: flex; @@ -103,7 +105,7 @@ if (isFallback) canonicalURL.pathname = canonicalURL.pathname.replace(`/${lang}/ @media (min-width: 72em) { #main-content { - margin-right: var(--theme-right-sidebar-width); + margin-inline-end: var(--theme-right-sidebar-width); } #right-sidebar { display: flex; @@ -140,11 +142,13 @@ if (isFallback) canonicalURL.pathname = canonicalURL.pathname.replace(`/${lang}/ {!hideRightSidebar && <RightSidebar content={content} githubEditUrl={githubEditUrl} />} </aside> <div id="main-content" lang={isFallback && 'en'}> - <PageContent {content} {githubEditUrl} {currentPage}> - <slot name="header" /> - {isFallback && <FallbackNotice />} - <slot /> - </PageContent> + <div dir={isFallback ? 'ltr' : direction}> + <PageContent {content} {githubEditUrl} {currentPage} layoutDir={direction}> + <slot name="header" /> + {isFallback && <div lang={bcpLang} dir={direction}><FallbackNotice /></div>} + <slot /> + </PageContent> + </div> </div> </main> </body> diff --git a/src/pages/ar/editor-setup.md b/src/pages/ar/editor-setup.md index 020c7c683a33c..751bd9c21497f 100644 --- a/src/pages/ar/editor-setup.md +++ b/src/pages/ar/editor-setup.md @@ -4,7 +4,6 @@ setup: | import Badge from '~/components/Badge.astro'; title: إعداد البيئة البرمجية description: أعِد محرر الشفرة لبناء المشاريع مع Astro. -dir: rtl --- خصص محرر الكود لتحسين تجربة التطوير مع أسترو وفتح ميزات جديدة diff --git a/src/pages/ar/getting-started.md b/src/pages/ar/getting-started.md index c16c0989b74ad..e80c218368fbe 100644 --- a/src/pages/ar/getting-started.md +++ b/src/pages/ar/getting-started.md @@ -5,7 +5,6 @@ setup: | import ContributorList from '../../components/ContributorList.astro' import PackageManagerTabs from '~/components/tabs/PackageManagerTabs.astro' title: باشر بالبدأ -dir: rtl --- #### ما هو أسترو؟