feat(rtl): basic rtl layout for rtl languages

This commit is contained in:
kolaente 2025-07-21 12:40:38 +02:00
parent 5e65f223bc
commit 654e397638
12 changed files with 71 additions and 36 deletions

View File

@ -74,7 +74,7 @@
>
<span class="username">{{ authStore.userDisplayName }}</span>
<span
class="ml-1 dropdown-icon icon is-small"
class="ms-1 dropdown-icon icon is-small"
:style="{
transform: open ? 'rotate(180deg)' : 'rotate(0)',
}"
@ -167,7 +167,7 @@ $user-dropdown-width-mobile: 5rem;
background: var(--site-background);
@media screen and (min-width: $tablet) {
padding-left: 2rem;
padding-inline-start: 2rem;
align-items: stretch;
}
@ -193,17 +193,17 @@ $user-dropdown-width-mobile: 5rem;
align-self: stretch;
display: flex;
align-items: center;
margin-right: .5rem;
margin-inline-end: .5rem;
}
}
.menu-button {
margin-right: auto;
margin-inline-end: auto;
align-self: stretch;
flex: 0 0 auto;
@media screen and (max-width: $tablet) {
margin-left: 1rem;
margin-inline-start: 1rem;
}
}
@ -252,7 +252,7 @@ $user-dropdown-width-mobile: 5rem;
}
.navbar-end {
margin-left: auto;
margin-inline-start: auto;
flex: 0 0 auto;
display: flex;
align-items: stretch;
@ -263,18 +263,18 @@ $user-dropdown-width-mobile: 5rem;
}
.username-dropdown-trigger {
padding-left: .75rem;
padding-inline-start: .75rem;
display: inline-flex;
align-items: center;
font-size: .85rem;
font-weight: 700;
@media screen and (max-width: $tablet) {
padding-right: .5rem;
padding-inline-end: .5rem;
}
@media screen and (min-width: $tablet) {
padding-right: .75rem;
padding-inline-end: .75rem;
}
}
@ -294,6 +294,6 @@ $user-dropdown-width-mobile: 5rem;
border-radius: 100%;
vertical-align: middle;
height: 40px;
margin-right: .5rem;
margin-inline-end: .5rem;
}
</style>

View File

@ -135,6 +135,11 @@ projectStore.loadAllProjects()
top: 0.5rem;
right: 0.5rem;
z-index: 31;
[dir="rtl"] & {
right: auto;
left: 0.5rem;
}
width: 3rem;
height: 3rem;
display: flex;
@ -170,9 +175,14 @@ projectStore.loadAllProjects()
padding: 1.5rem 0.5rem 0;
// TODO refactor: DRY `transition-timing-function` with `./Navigation.vue`.
transition: margin-left $transition-duration;
[dir="rtl"] & {
transition: margin-right $transition-duration;
}
@media screen and (max-width: $tablet) {
margin-left: 0;
margin-right: 0;
min-height: calc(100vh - 4rem);
}
@ -184,6 +194,13 @@ projectStore.loadAllProjects()
@media screen and (min-width: $tablet) {
margin-left: $navbar-width;
}
[dir="rtl"] & {
@media screen and (min-width: $tablet) {
margin-left: 0;
margin-right: $navbar-width;
}
}
}
// Used to make sure the spinner is always in the middle while loading
@ -222,6 +239,11 @@ projectStore.loadAllProjects()
bottom: calc(1rem - 4px);
right: 1rem;
z-index: 4500; // The modal has a z-index of 4000
[dir="rtl"] & {
right: auto;
left: 1rem;
}
color: var(--grey-500);
transition: color $transition;

View File

@ -139,8 +139,8 @@ const savedFilterProjects = computed(() => projectStore.savedFilterProjects)
.logo {
display: block;
padding-left: 1rem;
margin-right: 1rem;
padding-inline-start: 1rem;
margin-inline-end: 1rem;
margin-bottom: 1rem;
@media screen and (min-width: $tablet) {
@ -163,6 +163,12 @@ const savedFilterProjects = computed(() => projectStore.savedFilterProjects)
width: $navbar-width;
overflow-y: auto;
[dir="rtl"] & {
left: auto;
right: 0;
transform: translateX(100%);
}
@media screen and (max-width: $tablet) {
top: 0;
width: 70vw;
@ -183,7 +189,7 @@ const savedFilterProjects = computed(() => projectStore.savedFilterProjects)
.list-menu-link,
li > a {
padding-left: 2rem;
padding-inline-start: 2rem;
display: inline-block;
.icon {

View File

@ -18,7 +18,7 @@
<ColorBubble
v-if="!showProjectSeparately && projectColor !== '' && currentProject?.id !== task.projectId"
:color="projectColor"
class="mr-1"
class="me-1"
/>
<div
@ -30,8 +30,8 @@
v-if="showProject && typeof project !== 'undefined'"
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
:to="{ name: 'project.index', params: { projectId: task.projectId } }"
class="task-project mr-1"
:class="{'mr-2': task.hexColor !== ''}"
class="task-project me-1"
:class="{'me-2': task.hexColor !== ''}"
@click.stop
>
{{ project.title }}
@ -40,13 +40,13 @@
<ColorBubble
v-if="task.hexColor !== ''"
:color="getHexColor(task.hexColor)"
class="mr-1"
class="me-1"
/>
<PriorityLabel
:priority="task.priority"
:done="task.done"
class="pr-2"
class="pe-2"
/>
<RouterLink
@ -61,7 +61,7 @@
<Labels
v-if="task.labels.length > 0"
class="labels ml-2 mr-1"
class="labels ms-2 me-1"
:labels="task.labels"
/>
@ -69,7 +69,7 @@
v-if="task.assignees.length > 0"
:assignees="task.assignees"
:avatar-size="25"
class="ml-1"
class="ms-1"
:inline="true"
/>
@ -134,7 +134,7 @@
<ColorBubble
v-if="showProjectSeparately && projectColor !== '' && currentProject?.id !== task.projectId"
:color="projectColor"
class="mr-1"
class="me-1"
/>
<RouterLink

View File

@ -49,6 +49,12 @@ export const DEFAULT_LANGUAGE: SupportedLocale= 'en'
export type ISOLanguage = string
const RTL_LANGUAGES = ['ar-SA', 'he-IL'] as const
export function isRTLLanguage(locale: SupportedLocale): boolean {
return RTL_LANGUAGES.includes(locale as typeof RTL_LANGUAGES[number])
}
// we load all messages async
export const i18n = createI18n({
fallbackLocale: DEFAULT_LANGUAGE,
@ -102,6 +108,7 @@ export async function setLanguage(lang: SupportedLocale): Promise<SupportedLocal
i18n.global.locale.value = lang
document.documentElement.lang = lang
document.documentElement.dir = isRTLLanguage(lang) ? 'rtl' : 'ltr'
return lang
}

View File

@ -2,7 +2,7 @@
// very hard to untangle
// they have many overwrites at different positions
.tasks {
text-align: left;
text-align: start;
@media screen and (max-width: $tablet) {

View File

@ -15,7 +15,7 @@
}
td.actions {
text-align: right;
text-align: end;
}
}
@ -26,5 +26,5 @@
.content blockquote {
background-color: var(--grey-200);
border-left: .25rem solid var(--grey-300);
border-inline-start: .25rem solid var(--grey-300);
}

View File

@ -1,10 +1,10 @@
.field.has-addons .button {
height: 2.5rem;
margin-left: 0 !important;
margin-inline-start: 0 !important;
}
.field.has-addons .select select {
margin-right: 0;
margin-inline-end: 0;
}
.input,
@ -67,7 +67,7 @@
// Buttons icons
.button .icon.is-small {
margin-right: 0.05rem !important;
margin-inline-end: 0.05rem !important;
}
// FIXME: used for

View File

@ -33,7 +33,7 @@
@include loader;
width: 2rem;
height: 2rem;
margin-left: calc(50% - 1rem);
margin-inline-start: calc(50% - 1rem);
margin-top: 1rem;
border-width: 0.25rem;
}

View File

@ -111,7 +111,7 @@
.icon {
height: 1rem;
vertical-align: middle;
padding-right: 0.5rem;
padding-inline-end: 0.5rem;
}
&.router-link-exact-active .icon:not(.handle) {
@ -134,7 +134,7 @@
}
menu {
border-left: 0;
border-inline-start: 0;
}
}
}

View File

@ -26,7 +26,7 @@
<ImportHint v-if="tasksLoaded" />
<div
v-if="projectHistory.length > 0"
class="is-max-width-desktop has-text-left mt-4"
class="is-max-width-desktop has-text-start mt-4"
>
<h3>{{ $t('home.lastViewed') }}</h3>
<ProjectCardGrid

View File

@ -186,7 +186,7 @@ function toggleGroupPermissionsFromChild(group: string, checked: boolean) {
<th>{{ $t('user.settings.apiTokens.attributes.permissions') }}</th>
<th>{{ $t('user.settings.apiTokens.attributes.expiresAt') }}</th>
<th>{{ $t('misc.created') }}</th>
<th class="has-text-right">
<th class="has-text-end">
{{ $t('misc.actions') }}
</th>
</tr>
@ -216,7 +216,7 @@ function toggleGroupPermissionsFromChild(group: string, checked: boolean) {
</p>
</td>
<td>{{ formatDisplayDate(tk.created) }}</td>
<td class="has-text-right">
<td class="has-text-end">
<XButton
variant="secondary"
@click="() => {tokenToDelete = tk; showDeleteModal = true}"
@ -290,7 +290,7 @@ function toggleGroupPermissionsFromChild(group: string, checked: boolean) {
<flat-pickr
v-if="newTokenExpiry === 'custom'"
v-model="newTokenExpiryCustom"
class="ml-2"
class="ms-2"
:config="flatPickerConfig"
/>
</div>
@ -310,7 +310,7 @@ function toggleGroupPermissionsFromChild(group: string, checked: boolean) {
>
<FancyCheckbox
v-model="newTokenPermissionsGroup[group]"
class="mr-2 is-capitalized has-text-weight-bold"
class="me-2 is-capitalized has-text-weight-bold"
@update:modelValue="checked => selectPermissionGroup(group, checked)"
>
{{ formatPermissionTitle(group) }}
@ -323,7 +323,7 @@ function toggleGroupPermissionsFromChild(group: string, checked: boolean) {
>
<FancyCheckbox
v-model="newTokenPermissions[group][route]"
class="ml-4 mr-2 is-capitalized"
class="ms-4 me-2 is-capitalized"
@update:modelValue="checked => toggleGroupPermissionsFromChild(group, checked)"
>
{{ formatPermissionTitle(route) }}