From ec92fd830fd799ab5753f4e6ece6e5b7e4ccd58b Mon Sep 17 00:00:00 2001 From: Marcus Ortiz Date: Mon, 7 Jul 2025 15:58:42 -0700 Subject: [PATCH] Start to use CSS logical properties for docs pages. This transition to logical properties and values (as opposed to physical/dimensional ones) will allow for UI elements and associated spacing to better accomodate languages with a right-to-left writing direction. Although DocC doesn't yet support right-to-left localized content, this change will better future-proof the renderer if it does, allowing it to do things like flip the orientation of the sidebar and strings of text for languages that read from right to left instead of left to right. https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_logical_properties_and_values --- src/components/AdjustableSidebarWidth.vue | 5 ++- src/components/Badge.vue | 2 +- src/components/CodeBlock.vue | 4 +++ src/components/ContentNode/Aside.vue | 6 ++-- .../ContentNode/LinkableHeading.vue | 6 ++-- src/components/ContentNode/TabNavigator.vue | 4 +-- src/components/DocumentationLayout.vue | 11 +++---- src/components/DocumentationTopic.vue | 2 +- .../DecoratedTopicTitle.vue | 6 ++++ .../DocumentationTopic/DocumentationNav.vue | 8 ++--- .../DocumentationNav/LanguageToggle.vue | 20 ++++++------ .../Hero/DocumentationHero.vue | 11 +++---- .../Hero/HierarchyCollapsedItems.vue | 2 +- .../DocumentationTopic/Hero/HierarchyItem.vue | 5 +-- .../DocumentationTopic/Hero/Title.vue | 2 +- .../OnThisPageStickyContainer.vue | 4 +-- .../PrimaryContent/Mention.vue | 2 +- .../PrimaryContent/Parameters.vue | 10 +++--- .../DocumentationTopic/RelationshipsList.vue | 4 +-- .../Summary/Availability.vue | 6 ++-- .../DocumentationTopic/TopicLinkBlockIcon.vue | 2 +- .../DocumentationTopic/TopicsLinkBlock.vue | 6 ++-- src/components/Filter/FilterInput.vue | 16 +++++----- .../Icons/InlineChevronRightIcon.vue | 6 ++++ src/components/Icons/SidenavIcon.vue | 6 ++++ src/components/Icons/TwoLetterSymbolIcon.vue | 9 ++++++ src/components/NavMenuItemBase.vue | 4 +-- .../Navigator/BaseNavigatorCard.vue | 2 +- .../Navigator/BaseNavigatorCardItem.vue | 16 ++++++++-- src/components/Navigator/NavigatorCard.vue | 2 +- .../Navigator/NavigatorCardItem.vue | 2 +- .../Navigator/QuickNavigationModal.vue | 8 ++--- src/components/TabnavItem.vue | 6 ++-- src/lang/index.js | 4 ++- src/lang/locales.json | 5 +++ src/lang/locales/ar.json | 1 + src/styles/_base.scss | 8 +++++ src/styles/base/_reset.scss | 2 +- src/styles/base/_typography.scss | 5 ++- src/utils/metadata.js | 31 ++++++++++++++++++- 40 files changed, 173 insertions(+), 88 deletions(-) create mode 100644 src/lang/locales/ar.json diff --git a/src/components/AdjustableSidebarWidth.vue b/src/components/AdjustableSidebarWidth.vue index 61a0a4ef5..02dd90209 100644 --- a/src/components/AdjustableSidebarWidth.vue +++ b/src/components/AdjustableSidebarWidth.vue @@ -461,11 +461,10 @@ export default { position: fixed; top: var(--top-offset-mobile); bottom: 0; - left: 0; + inset-inline-start: 0; z-index: $nav-z-index + 1; transform: translateX(-100%); transition: transform var(--nav-transition-duration) ease-in; - left: 0; :deep(.aside-animated-child) { opacity: 0; @@ -502,7 +501,7 @@ export default { cursor: col-resize; top: 0; bottom: 0; - right: 0; + inset-inline-end: 0; width: 5px; height: 100%; user-select: none; diff --git a/src/components/Badge.vue b/src/components/Badge.vue index 623504e0d..4635b3606 100644 --- a/src/components/Badge.vue +++ b/src/components/Badge.vue @@ -59,7 +59,7 @@ export default { border-style: var(--badge-border-style, none); border-width: var(--badge-border-width, 1px); margin: auto; - margin-left: 5px; + margin-inline-start: 5px; color: var(--colors-badge-text, var(--color-badge-text)); background-color: var(--badge-color); @include prefers-dark { diff --git a/src/components/CodeBlock.vue b/src/components/CodeBlock.vue index 2b2dd23e7..159633ff8 100644 --- a/src/components/CodeBlock.vue +++ b/src/components/CodeBlock.vue @@ -29,6 +29,10 @@ export default { @import 'docc-render/styles/_core.scss'; code { + /* enforce "ltr" direction for text in code blocks right now since code is + likely to remain as English and not translated */ + direction: ltr; + &::before { content: attr(data-before-code); } diff --git a/src/components/ContentNode/Aside.vue b/src/components/ContentNode/Aside.vue index 46f968071..f9b9ffec6 100644 --- a/src/components/ContentNode/Aside.vue +++ b/src/components/ContentNode/Aside.vue @@ -53,11 +53,13 @@ aside { break-inside: avoid; border-radius: var(--aside-border-radius, $border-radius); border-style: var(--aside-border-style, solid); - border-width: var(--aside-border-width, + /*border-width: var(--aside-border-width, $aside-width-border $aside-width-border $aside-width-border - $aside-width-left-border); + $aside-width-left-border);*/ + border-block-width: $aside-width-border; + border-inline-width: $aside-width-left-border 0; padding: rem(16px); text-align: start; diff --git a/src/components/ContentNode/LinkableHeading.vue b/src/components/ContentNode/LinkableHeading.vue index 1a1352419..25945ff9c 100644 --- a/src/components/ContentNode/LinkableHeading.vue +++ b/src/components/ContentNode/LinkableHeading.vue @@ -78,7 +78,7 @@ $icon-margin: 7px; color: inherit; text-decoration: none; position: relative; - padding-right: $icon-size-default + $icon-margin; + padding-inline-end: $icon-size-default + $icon-margin; display: inline-block; &::after { @@ -88,11 +88,11 @@ $icon-margin: 7px; .icon { position: absolute; - right: 0; + inset-inline-end: 0; bottom: .2em; display: none; height: $icon-size-default; - margin-left: $icon-margin; + margin-inline-start: $icon-margin; } &:hover, &:focus { diff --git a/src/components/ContentNode/TabNavigator.vue b/src/components/ContentNode/TabNavigator.vue index 06b285580..f703f4415 100644 --- a/src/components/ContentNode/TabNavigator.vue +++ b/src/components/ContentNode/TabNavigator.vue @@ -130,9 +130,9 @@ export default { .tabs-content { flex: 1 1 auto; min-width: 0; - padding-right: var(--spacing-stacked-margin-xlarge); + padding-inline-end: var(--spacing-stacked-margin-xlarge); @include breakpoint(small) { - padding-right: 0; + padding-inline-end: 0; padding-bottom: var(--spacing-stacked-margin-large); } } diff --git a/src/components/DocumentationLayout.vue b/src/components/DocumentationLayout.vue index 6f2fdf9a9..b3eb520e2 100644 --- a/src/components/DocumentationLayout.vue +++ b/src/components/DocumentationLayout.vue @@ -248,7 +248,7 @@ export default { } .navigator-filter .quick-navigation-open { - margin-left: var(--nav-filter-horizontal-padding); + margin-inline-start: var(--nav-filter-horizontal-padding); width: calc(var(--nav-filter-horizontal-padding) * 2); } } @@ -268,14 +268,14 @@ export default { .documentation-layout-aside { height: 100%; box-sizing: border-box; - border-right: $generic-border-style; + border-inline-end: $generic-border-style; @include breakpoint(medium, nav) { background: var(--color-fill); - border-right: none; + border-inline-end: none; .sidebar-transitioning & { - border-right: $generic-border-style; + border-inline-end: $generic-border-style; } } } @@ -293,8 +293,7 @@ export default { @include inTargetWeb { @include breakpoint-full-width-container(); @include breakpoints-from(xlarge) { - border-left: $generic-border-style; - border-right: $generic-border-style; + border-inline: $generic-border-style; box-sizing: border-box; } } diff --git a/src/components/DocumentationTopic.vue b/src/components/DocumentationTopic.vue index 888d8018e..23c110f9d 100644 --- a/src/components/DocumentationTopic.vue +++ b/src/components/DocumentationTopic.vue @@ -816,7 +816,7 @@ $space-size: 15px; small { font-size: 1rem; - padding-left: 0.416rem; + padding-inline-start: 0.416rem; } } diff --git a/src/components/DocumentationTopic/DecoratedTopicTitle.vue b/src/components/DocumentationTopic/DecoratedTopicTitle.vue index f2a3be6e1..faa853a51 100644 --- a/src/components/DocumentationTopic/DecoratedTopicTitle.vue +++ b/src/components/DocumentationTopic/DecoratedTopicTitle.vue @@ -75,6 +75,12 @@ export default { diff --git a/src/components/DocumentationTopic/PrimaryContent/Parameters.vue b/src/components/DocumentationTopic/PrimaryContent/Parameters.vue index 6528e93b9..b3e542757 100644 --- a/src/components/DocumentationTopic/PrimaryContent/Parameters.vue +++ b/src/components/DocumentationTopic/PrimaryContent/Parameters.vue @@ -55,7 +55,7 @@ export default { .param-name { font-weight: $font-weight-semibold; - padding-left: 1rem; + padding-inline-start: 1rem; padding-top: var(--spacing-param); &:first-child { @@ -63,15 +63,15 @@ export default { } @include breakpoint(small) { - padding-left: 0; + padding-inline-start: 0; } } .param-content { - padding-left: 2rem; + padding-inline-start: 2rem; @include breakpoint(small) { - padding-left: 0; + padding-inline-start: 0; } :deep(dt) { @@ -79,7 +79,7 @@ export default { } :deep(dd) { - margin-left: 1em; + margin-inline-start: 1em; } } diff --git a/src/components/DocumentationTopic/RelationshipsList.vue b/src/components/DocumentationTopic/RelationshipsList.vue index c42c2bab1..a0f006a44 100644 --- a/src/components/DocumentationTopic/RelationshipsList.vue +++ b/src/components/DocumentationTopic/RelationshipsList.vue @@ -131,7 +131,7 @@ export default { list-style: none; &.column { - margin-left: 0; + margin-inline-start: 0; margin-top: 15px; } @@ -144,7 +144,7 @@ export default { flex-direction: row; flex-wrap: wrap; margin-top: 15px; - margin-left: 0; + margin-inline-start: 0; li:not(:last-child)::after { content: ",\00a0" diff --git a/src/components/DocumentationTopic/Summary/Availability.vue b/src/components/DocumentationTopic/Summary/Availability.vue index 8fe18ec59..1f320bbe2 100644 --- a/src/components/DocumentationTopic/Summary/Availability.vue +++ b/src/components/DocumentationTopic/Summary/Availability.vue @@ -113,7 +113,7 @@ $availability-info-spacing: 10px; .changed { $-coin-spacer: 5px; - padding-left: $icon-size-default - $-coin-spacer + 2; + padding-inline-start: $icon-size-default - $-coin-spacer + 2; border: none; &::after { @@ -124,7 +124,7 @@ $availability-info-spacing: 10px; &::before { @include coin($modified-svg, $icon-size-default); margin: 0; - left: -$-coin-spacer; + inset-inline-start: -$-coin-spacer; @include prefers-dark { background-image: $modified-dark-svg; @@ -162,7 +162,7 @@ $availability-info-spacing: 10px; width: 1px; height: 1em; background: currentColor; - margin-left: $availability-info-spacing; + margin-inline-start: $availability-info-spacing; } &:last-child::after { diff --git a/src/components/DocumentationTopic/TopicLinkBlockIcon.vue b/src/components/DocumentationTopic/TopicLinkBlockIcon.vue index 5cfa7df53..eef0b3a9f 100644 --- a/src/components/DocumentationTopic/TopicLinkBlockIcon.vue +++ b/src/components/DocumentationTopic/TopicLinkBlockIcon.vue @@ -64,7 +64,7 @@ export default { height: rem(25px); flex: 0 0 $topic-link-icon-width; width: $topic-link-icon-width; - margin-right: $topic-link-icon-spacing; + margin-inline-end: $topic-link-icon-spacing; } .topic-icon { diff --git a/src/components/DocumentationTopic/TopicsLinkBlock.vue b/src/components/DocumentationTopic/TopicsLinkBlock.vue index f790a6e91..d9e4b4b84 100644 --- a/src/components/DocumentationTopic/TopicsLinkBlock.vue +++ b/src/components/DocumentationTopic/TopicsLinkBlock.vue @@ -216,11 +216,11 @@ export default { .abstract, .link-block :deep(.badge) { - margin-left: calc(#{$topic-link-icon-spacing} + #{$topic-link-icon-width}); + margin-inline-start: calc(#{$topic-link-icon-spacing} + #{$topic-link-icon-width}); } .link-block .badge + .badge { - margin-left: 1rem; + margin-inline-start: 1rem; } .link { @@ -238,7 +238,7 @@ export default { flex-flow: row wrap; .badge { - margin-left: 1rem; + margin-inline-start: 1rem; margin-top: 0; } } diff --git a/src/components/Filter/FilterInput.vue b/src/components/Filter/FilterInput.vue index dcdf72334..9c0614500 100644 --- a/src/components/Filter/FilterInput.vue +++ b/src/components/Filter/FilterInput.vue @@ -456,11 +456,10 @@ $input-height: rem(28px); position: relative; z-index: 1; cursor: text; - margin-left: var(--input-horizontal-spacing); - margin-right: rem(3px); + margin-inline: var(--input-horizontal-spacing) rem(3px); @include breakpoint(small) { - margin-right: rem(7px); + margin-inline-end: rem(7px); } .svg-icon { @@ -522,11 +521,11 @@ $input-height: rem(28px); &__selected-tags { z-index: 1; - padding-left: $tag-outline-padding; + padding-inline-start: $tag-outline-padding; margin: -$tag-outline-padding 0; @include breakpoint(small) { - padding-left: 0; + padding-inline-start: 0; } :deep() { @@ -534,11 +533,11 @@ $input-height: rem(28px); padding: $tag-outline-padding; @include breakpoint(small) { - padding-right: rem(7px); + padding-inline-end: rem(7px); } .tag:last-child { - padding-right: 0; + padding-inline-end: 0; } } } @@ -562,8 +561,7 @@ $input-height: rem(28px); &__delete-button-wrapper { display: flex; align-items: center; - padding-right: var(--input-horizontal-spacing); - padding-left: rem(3px); + padding-inline: rem(3px) var(--input-horizontal-spacing); border-top-right-radius: $small-border-radius; border-bottom-right-radius: $small-border-radius; } diff --git a/src/components/Icons/InlineChevronRightIcon.vue b/src/components/Icons/InlineChevronRightIcon.vue index 8e0d9846c..9f4d09198 100644 --- a/src/components/Icons/InlineChevronRightIcon.vue +++ b/src/components/Icons/InlineChevronRightIcon.vue @@ -22,3 +22,9 @@ export default { components: { SVGIcon }, }; + + diff --git a/src/components/Icons/SidenavIcon.vue b/src/components/Icons/SidenavIcon.vue index cc0121c56..79ef28c5e 100644 --- a/src/components/Icons/SidenavIcon.vue +++ b/src/components/Icons/SidenavIcon.vue @@ -24,3 +24,9 @@ export default { components: { SVGIcon }, }; + + diff --git a/src/components/Icons/TwoLetterSymbolIcon.vue b/src/components/Icons/TwoLetterSymbolIcon.vue index bdf63028c..7a5a137de 100644 --- a/src/components/Icons/TwoLetterSymbolIcon.vue +++ b/src/components/Icons/TwoLetterSymbolIcon.vue @@ -46,3 +46,12 @@ export default { }, }; + + diff --git a/src/components/NavMenuItemBase.vue b/src/components/NavMenuItemBase.vue index 5a8d94ddb..c0ea398f1 100644 --- a/src/components/NavMenuItemBase.vue +++ b/src/components/NavMenuItemBase.vue @@ -36,12 +36,12 @@ export default { @import "docc-render/styles/_core.scss"; .nav-menu-item { - margin-left: $nav-menu-item-left-margin; + margin-inline-start: $nav-menu-item-left-margin; list-style: none; min-width: 0; @include nav-in-breakpoint { - margin-left: 0; + margin-inline-start: 0; width: 100%; min-height: rem(42px); // remove the first border of the first element diff --git a/src/components/Navigator/BaseNavigatorCard.vue b/src/components/Navigator/BaseNavigatorCard.vue index 9ef7bdd0c..cf573acce 100644 --- a/src/components/Navigator/BaseNavigatorCard.vue +++ b/src/components/Navigator/BaseNavigatorCard.vue @@ -134,7 +134,7 @@ $close-icon-padding: 5px; display: flex; flex-direction: column; // right padding is added by the items, so visually the scroller is stuck to the side - padding-right: 0; + padding-inline-end: 0; flex: 1 1 auto; min-height: 0; height: 100%; diff --git a/src/components/Navigator/BaseNavigatorCardItem.vue b/src/components/Navigator/BaseNavigatorCardItem.vue index efe29b47f..a45102571 100644 --- a/src/components/Navigator/BaseNavigatorCardItem.vue +++ b/src/components/Navigator/BaseNavigatorCardItem.vue @@ -67,7 +67,9 @@ $nesting-spacing: $nav-card-horizontal-spacing + $nav-card-horizontal-spacing-sm align-items: stretch; min-height: $item-height; box-sizing: border-box; - padding: 0 var(--nav-head-wrapper-right-space) 0 var(--nav-head-wrapper-left-space); + padding-block: 0; + padding-inline-start: var(--nav-head-wrapper-left-space); + padding-inline-end: var(--nav-head-wrapper-right-space); &.active { .head-wrapper { @@ -99,7 +101,7 @@ $nesting-spacing: $nav-card-horizontal-spacing + $nav-card-horizontal-spacing-sm } .navigator-icon-wrapper { - margin-right: 7px; + margin-inline-end: 7px; } .head-wrapper { @@ -119,3 +121,13 @@ $nesting-spacing: $nav-card-horizontal-spacing + $nav-card-horizontal-spacing-sm @include safe-area-right-set(padding-right, var(--nav-head-wrapper-right-space)); } + + diff --git a/src/components/Navigator/NavigatorCard.vue b/src/components/Navigator/NavigatorCard.vue index 5b4199382..04df6767d 100644 --- a/src/components/Navigator/NavigatorCard.vue +++ b/src/components/Navigator/NavigatorCard.vue @@ -1020,7 +1020,7 @@ $navigator-card-vertical-spacing: 8px !default; @include safe-area-left-set(margin-left, var(--card-horizontal-spacing)); @include safe-area-right-set(margin-right, var(--card-horizontal-spacing)); padding: $navigator-card-vertical-spacing $nav-card-horizontal-spacing; - padding-left: $nav-card-horizontal-spacing * 2; + padding-inline-start: $nav-card-horizontal-spacing * 2; background: $technology-title-background; border-radius: $nano-border-radius; display: flex; diff --git a/src/components/Navigator/NavigatorCardItem.vue b/src/components/Navigator/NavigatorCardItem.vue index c7c950692..73a6b0ba1 100644 --- a/src/components/Navigator/NavigatorCardItem.vue +++ b/src/components/Navigator/NavigatorCardItem.vue @@ -366,7 +366,7 @@ $chevron-width: $nav-card-horizontal-spacing; position: absolute; width: 100%; height: 100%; - padding-right: $tree-toggle-padding; + padding-inline-end: $tree-toggle-padding; box-sizing: border-box; z-index: 1; display: flex; diff --git a/src/components/Navigator/QuickNavigationModal.vue b/src/components/Navigator/QuickNavigationModal.vue index 0fd0c90c4..7b9a8be81 100644 --- a/src/components/Navigator/QuickNavigationModal.vue +++ b/src/components/Navigator/QuickNavigationModal.vue @@ -531,7 +531,7 @@ $input-horizontal-spacing: rem(15px); overflow: auto; } &__preview { - border-left: $base-border-width solid var(--color-grid); + border-inline-start: $base-border-width solid var(--color-grid); flex: 0 0 61.8%; overflow: auto; position: sticky; @@ -560,7 +560,7 @@ $input-horizontal-spacing: rem(15px); margin: auto; width: 100%; .navigator-icon { - margin-right: rem(10px); + margin-inline-end: rem(10px); } .symbol-name { display: flex; @@ -572,11 +572,11 @@ $input-horizontal-spacing: rem(15px); @include font-styles(body-reduced-tight); color: var(--color-figure-gray-secondary); display: flex; - margin-left: rem(27px); + margin-inline-start: rem(27px); overflow: hidden; white-space: nowrap; .parent-path { - padding-right: rem(5px); + padding-inline-end: rem(5px); } } } diff --git a/src/components/TabnavItem.vue b/src/components/TabnavItem.vue index 394e1e3bb..35666fa1f 100644 --- a/src/components/TabnavItem.vue +++ b/src/components/TabnavItem.vue @@ -58,12 +58,12 @@ $tabnav-item-gutter: rem(30px); display: flex; list-style: none; - padding-left: $tabnav-item-gutter; + padding-inline-start: $tabnav-item-gutter; margin: 0; outline: none; &:first-child { - padding-left: 0; + padding-inline-start: 0; } // hack to make sure item margin is not overwritten by external css @@ -97,7 +97,7 @@ $tabnav-item-gutter: rem(30px); content: ''; position: absolute; bottom: -1 * ($tabnav-margin + 1); - left: 0; + inset-inline-start: 0; width: 100%; border: 1px solid transparent; } diff --git a/src/lang/index.js b/src/lang/index.js index 0eb5a1122..fa1429769 100644 --- a/src/lang/index.js +++ b/src/lang/index.js @@ -9,15 +9,17 @@ */ /* eslint-disable camelcase */ +import ar from './locales/ar.json'; import en_US from './locales/en-US.json'; import zh_CN from './locales/zh-CN.json'; import ja_JP from './locales/ja-JP.json'; import ko_KR from './locales/ko-KR.json'; // default locale -export const defaultLocale = 'en-US'; +export const defaultLocale = process.env.VUE_APP_DEFAULT_LOCALE ?? 'en-US'; // translated locales export const messages = { + ar, 'en-US': en_US, 'zh-CN': zh_CN, 'ja-JP': ja_JP, diff --git a/src/lang/locales.json b/src/lang/locales.json index 4c340feb7..079525d84 100644 --- a/src/lang/locales.json +++ b/src/lang/locales.json @@ -1,4 +1,9 @@ [ + { + "code": "ar", + "name": "Arabic", + "slug": "ar" + }, { "code": "en-US", "name": "English", diff --git a/src/lang/locales/ar.json b/src/lang/locales/ar.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/src/lang/locales/ar.json @@ -0,0 +1 @@ +{} diff --git a/src/styles/_base.scss b/src/styles/_base.scss index ec8e3b97e..f07b584d0 100644 --- a/src/styles/_base.scss +++ b/src/styles/_base.scss @@ -28,3 +28,11 @@ :root { --app-height: 100vh; } + +html { + --scale-inline: 1; +} + +html[dir="rtl"] { + --scale-inline: -1; +} diff --git a/src/styles/base/_reset.scss b/src/styles/base/_reset.scss index e5589cccb..4e6c5d44b 100644 --- a/src/styles/base/_reset.scss +++ b/src/styles/base/_reset.scss @@ -80,7 +80,7 @@ img { //============================================================ caption, th { - text-align: left; + text-align: start; } //============================================================ diff --git a/src/styles/base/_typography.scss b/src/styles/base/_typography.scss index b33d872e5..53a9e8d01 100644 --- a/src/styles/base/_typography.scss +++ b/src/styles/base/_typography.scss @@ -63,8 +63,7 @@ button { -moz-font-feature-settings: 'kern'; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - direction: ltr; - text-align: left; + text-align: start; } // @@ -105,7 +104,7 @@ ol { // ul, ol { - margin-left: em(20px); + margin-inline-start: em(20px); // remove the top margin when nesting lists ul, diff --git a/src/utils/metadata.js b/src/utils/metadata.js index 2b8aeb3b7..b318ab563 100644 --- a/src/utils/metadata.js +++ b/src/utils/metadata.js @@ -122,10 +122,39 @@ export function addOrUpdateMetadata({ ); } +// these are hardcoded constants needed for manually determining the +// directionality of locales in Firefox +// +// this is very incomplete and will need to be manually updated to support other +// rtl languages until the `getTextInfo().direction` API is supported in FF — +// for now, this is just a basic set of example rtl languages +const RtlLocales = new Set([ + 'ar', // Arabic + 'he', // Hebrew + 'ur', // Urdu +]); + +const Direction = { + ltr: 'ltf', + rtl: 'rtl', +}; + +function getDirection(localeName) { + const locale = new Intl.Locale(localeName); + if ((typeof locale.getTextInfo) === 'function') { + return locale.getTextInfo()?.direction ?? Direction.ltr; + } + + // only needed for Firefox, which doesn't support `Intl.Locale.getTextInfo` + return RtlLocales.has(localeName) ? Direction.rtl : Direction.ltr; +} + /** * It updates the document setting a new lang attribute with the iso code or fallback on the locale * @param {String} locale */ export function updateLangTag(locale) { - document.querySelector('html').setAttribute('lang', locale); + const htmlElement = document.querySelector('html'); + htmlElement.setAttribute('lang', locale); + htmlElement.setAttribute('dir', getDirection(locale)); }