diff --git a/backend/helpers/appShell.ts b/backend/helpers/appShell.ts index 236a9dbf5..27c837e51 100644 --- a/backend/helpers/appShell.ts +++ b/backend/helpers/appShell.ts @@ -338,7 +338,19 @@ function bodyForPage(page: PageDescription): string { if (!page.render) { return '' } - return `
${stripActiveMarkup(page.render)}
` + return `
${stripActiveMarkup(page.render)}
` +} + +/** + * The `lang` and `dir` of the copy a page is served with, as the app puts them on the article. + * + * The shell's own `` is the interface's and says nothing about the page, which may be + * written in any of the site's locales — and a right-to-left page drawn left to right is punctuated at + * the wrong end of every line. + */ +function languageAttrs(locale: string): string { + const dir = WIKI.models.locales.isRTL(locale) ? 'rtl' : 'ltr' + return ` lang="${htmlEscape(locale)}" dir="${dir}"` } /** @@ -385,7 +397,7 @@ async function bodyForBlog(siteId: string, page: PageDescription): Promise${intro}` + return `
${intro}
` } /** diff --git a/backend/locales/en.json b/backend/locales/en.json index 75a9552a3..1a18ca2d1 100644 --- a/backend/locales/en.json +++ b/backend/locales/en.json @@ -2012,6 +2012,7 @@ "common.page.global": "Global", "common.page.id": "ID {id}", "common.page.lastEditedBy": "Last edited by", + "common.page.lastModified": "Last modified on {date}", "common.page.loading": "Loading Page...", "common.page.locked": "This page is password protected.", "common.page.lockedHint": "Enter the password to read it.", diff --git a/backend/models/locales.ts b/backend/models/locales.ts index fb0acdf92..fbc94ce94 100644 --- a/backend/models/locales.ts +++ b/backend/models/locales.ts @@ -568,6 +568,15 @@ class Locales { return (WIKI.cache?.get(`locale:${code}`) as any)?.displayCode ?? code } + /** + * Whether a locale's script runs right to left. Sync for the same reason as `shortCodeFor`: the + * document a page is served in is assembled without awaiting anything per locale. A code the cache + * does not hold reads as left to right. + */ + isRTL(code: string): boolean { + return Boolean((WIKI.cache?.get(`locale:${code}`) as any)?.isRTL) + } + /** * The locale a short code names, whichever of its codes was used. * diff --git a/frontend/index.html b/frontend/index.html index 3581a1293..54e6c55eb 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -116,7 +116,7 @@ } #wiki-prerender ul, #wiki-prerender ol { - padding-left: 2em; + padding-inline-start: 2em; list-style: revert; } #wiki-prerender pre, @@ -125,12 +125,13 @@ } #wiki-prerender pre { overflow-x: auto; + direction: ltr; padding: .75em 1em; background: #f4f4f4; } #wiki-prerender blockquote { - padding-left: 1em; - border-left: 4px solid #ccc; + padding-inline-start: 1em; + border-inline-start: 4px solid #ccc; color: #555; } #wiki-prerender img { diff --git a/frontend/src/assets/icons.generated.js b/frontend/src/assets/icons.generated.js index ed7a214f1..4c510a84c 100644 --- a/frontend/src/assets/icons.generated.js +++ b/frontend/src/assets/icons.generated.js @@ -5,10 +5,11 @@ never waits on (or depends on) the icon service. Regenerate with `npm run icons` after adding or removing an icon; `check-icons.mjs` fails the build if this drifts. - 283 icons. + 284 icons. */ export const BUNDLED_ICONS = { "la:angle-down": {"body":"","width":32,"height":32}, + "la:angle-left": {"body":"","width":32,"height":32}, "la:angle-right": {"body":"","width":32,"height":32}, "la:arrow-circle-left": {"body":"","width":32,"height":32}, "la:arrow-circle-right": {"body":"","width":32,"height":32}, diff --git a/frontend/src/components/EditorAsciidoc.vue b/frontend/src/components/EditorAsciidoc.vue index f45ddc69b..fc9aa4a0d 100644 --- a/frontend/src/components/EditorAsciidoc.vue +++ b/frontend/src/components/EditorAsciidoc.vue @@ -276,6 +276,8 @@ -->
diff --git a/frontend/src/components/EditorMarkdown.vue b/frontend/src/components/EditorMarkdown.vue index 17abfdf10..bbc43dd00 100644 --- a/frontend/src/components/EditorMarkdown.vue +++ b/frontend/src/components/EditorMarkdown.vue @@ -317,6 +317,8 @@ -->
diff --git a/frontend/src/components/EditorVisual.vue b/frontend/src/components/EditorVisual.vue index 564bc576d..026f9a693 100644 --- a/frontend/src/components/EditorVisual.vue +++ b/frontend/src/components/EditorVisual.vue @@ -342,7 +342,12 @@ @paste="onEditorPaste" @dragover="onEditorDragOver" @drop="onEditorDrop"> -
+ +
diff --git a/frontend/src/components/LocaleSelectorMenu.vue b/frontend/src/components/LocaleSelectorMenu.vue index 871ebfa83..8e851efc3 100644 --- a/frontend/src/components/LocaleSelectorMenu.vue +++ b/frontend/src/components/LocaleSelectorMenu.vue @@ -5,7 +5,16 @@ :anchor="props.anchor" :self="props.self" :offset="props.offset"> - + + - {{ lang.displayName }} + + {{ + lang.displayName + }} + diff --git a/frontend/src/components/NavBrowseMenu.vue b/frontend/src/components/NavBrowseMenu.vue index 67cde8f58..c33c471ed 100644 --- a/frontend/src/components/NavBrowseMenu.vue +++ b/frontend/src/components/NavBrowseMenu.vue @@ -8,10 +8,12 @@ @show="onShow"> -
+ +
@@ -33,7 +35,11 @@
{{ level.title }}
-
/{{ state.path }}
+ +
+ /{{ state.path }} +
@@ -70,7 +76,7 @@ {{ item.title }} - +
@@ -101,6 +107,7 @@ import { useI18n } from 'vue-i18n' import { notify } from '@/composables/notify' +import { useCommonStore } from '@/stores/common' import { usePageStore } from '@/stores/page' import { useSiteStore } from '@/stores/site' @@ -134,6 +141,7 @@ const props = defineProps({ // STORES +const commonStore = useCommonStore() const pageStore = usePageStore() const siteStore = useSiteStore() @@ -166,6 +174,12 @@ const state = reactive({ // COMPUTED +/** The interface's direction, which the panel has to state itself -- see the template. */ +const dir = computed(() => siteStore.localeDir(commonStore.locale)) + +/** The chevron into a folder points the way the line runs, which right to left is to the left. */ +const forwardIcon = computed(() => (dir.value === 'rtl' ? 'la:angle-left' : 'la:angle-right')) + const level = computed(() => state.levels[state.path] ?? EMPTY_LEVEL) const isRoot = computed(() => !state.path) @@ -259,8 +273,19 @@ function goUp() { diff --git a/frontend/src/components/shared/WBar.vue b/frontend/src/components/shared/WBar.vue index 7252c9129..7230cfbc7 100644 --- a/frontend/src/components/shared/WBar.vue +++ b/frontend/src/components/shared/WBar.vue @@ -48,8 +48,7 @@ defineProps({ /* -> After the two size rules, so it overrides whichever of them is in play */ .w-bar--flush { - padding-left: 0; - padding-right: 0; + padding-inline: 0; } /* @@ -65,6 +64,6 @@ defineProps({ } .w-bar :deep(> * + *) { - margin-left: 2px; + margin-inline-start: 2px; } diff --git a/frontend/src/components/shared/WBreadcrumbs.vue b/frontend/src/components/shared/WBreadcrumbs.vue index 32674eac0..69694de6d 100644 --- a/frontend/src/components/shared/WBreadcrumbs.vue +++ b/frontend/src/components/shared/WBreadcrumbs.vue @@ -13,7 +13,7 @@ v-if="item.icon" :name="item.icon" class="w-breadcrumbs__el-icon" - :class="item.label ? 'mr-2' : ''" /> + :class="item.label ? 'me-2' : ''" /> {{ item.label }} {{ item.tooltip }} diff --git a/frontend/src/components/shared/WBtnGroup.vue b/frontend/src/components/shared/WBtnGroup.vue index c4a53fe82..e8f40a7c6 100644 --- a/frontend/src/components/shared/WBtnGroup.vue +++ b/frontend/src/components/shared/WBtnGroup.vue @@ -28,23 +28,24 @@ defineProps({ `:deep` because the buttons come from the consumer's slot content and carry that component's scope attribute rather than this one's. */ +/* -> Logical sides throughout, so a group read right to left joins its buttons the same way */ .w-btn-group :deep(> .w-btn:not(:first-child)) { - border-top-left-radius: 0; - border-bottom-left-radius: 0; + border-start-start-radius: 0; + border-end-start-radius: 0; } .w-btn-group :deep(> .w-btn:not(:last-child)) { - border-top-right-radius: 0; - border-bottom-right-radius: 0; + border-start-end-radius: 0; + border-end-end-radius: 0; /* The seam is a border rather than an inset shadow so that `unelevated` -- which is about the drop shadow -- can switch the one off without also erasing the other. */ - border-right: 1px solid rgb(0 0 0 / 0.12); + border-inline-end: 1px solid rgb(0 0 0 / 0.12); } :global(body.body--dark .w-btn-group > .w-btn:not(:last-child)) { - border-right-color: rgb(255 255 255 / 0.15); + border-inline-end-color: rgb(255 255 255 / 0.15); } .w-btn-group--unelevated :deep(> .w-btn) { diff --git a/frontend/src/components/shared/WIcon.vue b/frontend/src/components/shared/WIcon.vue index 564798816..148ec77cf 100644 --- a/frontend/src/components/shared/WIcon.vue +++ b/frontend/src/components/shared/WIcon.vue @@ -143,8 +143,9 @@ const bundled = computed(() => { */ const colorClass = computed(() => [ props.color ? `text-${props.color}` : '', - props.left ? 'mr-2' : '', - props.right ? 'ml-2' : '' + // -> Logical, so "before the text" and "after it" stay true on a right-to-left interface + props.left ? 'me-2' : '', + props.right ? 'ms-2' : '' ]) /** diff --git a/frontend/src/components/shared/WItemSection.vue b/frontend/src/components/shared/WItemSection.vue index d1e4d6527..63e064346 100644 --- a/frontend/src/components/shared/WItemSection.vue +++ b/frontend/src/components/shared/WItemSection.vue @@ -61,19 +61,19 @@ const classes = computed(() => [ */ .w-item-section--side { align-items: flex-start; - padding-right: 16px; + padding-inline-end: 16px; } /* A side section that FOLLOWS the main one is trailing: padding and alignment flip. */ .w-item-section--main ~ .w-item-section--side { align-items: flex-end; - padding-left: 16px; - padding-right: 0; + padding-inline-start: 16px; + padding-inline-end: 0; } /* Two adjacent main sections get a small gutter, as they did before. */ .w-item-section--main + .w-item-section--main { - margin-left: 8px; + margin-inline-start: 8px; } .w-item-section--avatar { diff --git a/frontend/src/components/shared/WRating.vue b/frontend/src/components/shared/WRating.vue index f4fdf39c1..afeb68fef 100644 --- a/frontend/src/components/shared/WRating.vue +++ b/frontend/src/components/shared/WRating.vue @@ -11,7 +11,7 @@ :key="i" :ref="(el) => setContainer(el, i)" class="w-rating__icon-container flex h-[1em] items-center justify-center outline-0" - :class="[editable ? 'cursor-pointer' : '', i > 1 ? 'ml-0.5' : '']" + :class="[editable ? 'cursor-pointer' : '', i > 1 ? 'ms-0.5' : '']" role="radio" :tabindex="editable ? 0 : -1" :aria-checked="modelValue === i ? 'true' : 'false'" @@ -148,15 +148,19 @@ function set(value) { } function onKeyup(e, i) { + // -> The stars run the way the text does, so on a right-to-left page the next one is to the LEFT + const isRtl = getComputedStyle(e.currentTarget).direction === 'rtl' + const back = isRtl ? 'ArrowRight' : 'ArrowLeft' + const forward = isRtl ? 'ArrowLeft' : 'ArrowRight' if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() e.stopPropagation() set(i) - } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') { + } else if (e.key === back || e.key === 'ArrowDown') { e.preventDefault() e.stopPropagation() containers.value?.[i - 2]?.focus() - } else if (e.key === 'ArrowRight' || e.key === 'ArrowUp') { + } else if (e.key === forward || e.key === 'ArrowUp') { e.preventDefault() e.stopPropagation() containers.value?.[i]?.focus() diff --git a/frontend/src/components/shared/WSelect.vue b/frontend/src/components/shared/WSelect.vue index 6e3761ff0..2dbf98174 100644 --- a/frontend/src/components/shared/WSelect.vue +++ b/frontend/src/components/shared/WSelect.vue @@ -14,7 +14,7 @@ :for="selectId" class="mb-1 block text-caption text-black/60 dark:text-white/70"> {{ label }} - + - {{ lc.name }} + + {{ lc.name }} + + {{ t('admin.locale.rtl') }} + + {{ lc.nativeName }} ({{ lc.displayCode }}) +