Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/docs/administration/contributing/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,23 @@ Push the tag:
```
git push origin latest
```

## Localization

The UI is translated with [vue-i18n](https://vue-i18n.intlify.dev/). The English string is the
message key, so templates read `{{ $t("Report incident") }}` or `:label="$t('Search')"` and English
needs no message file: a key without a translation renders as itself. Translations live in
`src/dispatch/static/dispatch/src/locales/<locale>.json` as a flat map from the English string to
the translated one.

- Keys may contain dots and punctuation; `src/i18n/index.js` looks the whole key up before treating
dots as a nested path (Vuetify's own `$vuetify.*` messages still use the nested form).
- Table headers and other strings built in `data()` or plain functions must move to a `computed`
property (Options API) or call `t()` from `useI18n()` (`<script setup>`), otherwise they do not
follow a language switch.
- Adding a language: import its Vuetify messages from `vuetify/locale`, add the JSON file and an
entry to `SUPPORTED_LOCALES` in `src/i18n/index.js`.

Users pick the language in the profile menu (avatar → Language); the choice is stored both in the
browser and in the user's settings (`locale` in `dispatch_user_settings`), so it follows them across
devices. Without a saved choice the UI follows the browser language.
5 changes: 4 additions & 1 deletion src/dispatch/auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import bcrypt
from jose import jwt
from pydantic import field_validator
from pydantic import EmailStr
from pydantic import EmailStr, Field

from sqlalchemy import DateTime, Column, String, LargeBinary, Integer, Boolean
from sqlalchemy.dialects.postgresql import UUID
Expand Down Expand Up @@ -140,6 +140,8 @@ class DispatchUserSettings(Base, TimeStampMixin):
dispatch_user = relationship(DispatchUser, backref="settings")

auto_add_to_incident_bridges = Column(Boolean, default=True)
# UI language (BCP 47 tag such as "en" or "ru"); None means "follow the browser".
locale = Column(String, nullable=True)


class UserProject(DispatchBase):
Expand Down Expand Up @@ -343,6 +345,7 @@ class UserSettingsBase(DispatchBase):
"""Base Pydantic model for user settings."""

auto_add_to_incident_bridges: bool = True
locale: str | None = Field(None, max_length=16)


class UserSettingsRead(UserSettingsBase):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Add locale to dispatch_user_settings

Stores the UI language a user picked in their profile. NULL means the
frontend follows the browser language.

Revision ID: c6baea6d5ae8
Revises: b7e42c1d9a35
Create Date: 2026-10-26 00:00:00.000000

"""

from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = "c6baea6d5ae8"
down_revision = "b7e42c1d9a35"
branch_labels = None
depends_on = None


def upgrade():
op.add_column(
"dispatch_user_settings",
sa.Column("locale", sa.String(), nullable=True),
schema="dispatch_core",
)


def downgrade():
op.drop_column("dispatch_user_settings", "locale", schema="dispatch_core")
83 changes: 83 additions & 0 deletions src/dispatch/static/dispatch/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/dispatch/static/dispatch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"sortablejs": "^1.15.7",
"swrv": "^1.3.0",
"vue": "^3.5.44",
"vue-i18n": "^11.4.16",
"vue-router": "^5.4.0",
"vue3-apexcharts": "~1.7.0",
"vue3-markdown-it": "^1.0.10",
Expand Down
12 changes: 6 additions & 6 deletions src/dispatch/static/dispatch/src/auth/Login.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<template>
<v-form @submit.prevent="basicLogin({ email, password })" v-slot="{ isValid }">
<v-card class="mx-auto ma-4" max-width="600" variant="outlined" :loading="loading">
<v-card-title> Login </v-card-title>
<v-card-title> {{ $t("Login") }} </v-card-title>
<v-card-text>
<v-container>
<v-row>
<v-col cols="12" md="12">
<v-text-field
v-model="email"
label="Email"
:label="$t('Email')"
name="Email"
:rules="[rules.required, rules.email]"
/>
Expand All @@ -17,7 +17,7 @@
<v-text-field
v-model="password"
type="password"
label="Password"
:label="$t('Password')"
name="Password"
:rules="[rules.required]"
/>
Expand All @@ -27,8 +27,8 @@
</v-card-text>
<v-card-actions>
<div v-if="registrationEnabled" class="text-subheader-2 pl-4 text-medium-emphasis">
Don't have a account?
<router-link :to="{ name: 'BasicRegister' }"> Register </router-link>
{{ $t("Don't have an account?") }}
<router-link :to="{ name: 'BasicRegister' }"> {{ $t("Register") }} </router-link>
</div>
<v-spacer />
<v-btn
Expand All @@ -38,7 +38,7 @@
:loading="loading"
:disabled="!isValid"
>
Login
{{ $t("Login") }}
<template #loader>
<v-progress-linear indeterminate color="white" />
</template>
Expand Down
11 changes: 6 additions & 5 deletions src/dispatch/static/dispatch/src/auth/Register.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
<template>
<v-form @submit.prevent="register({ email, password })" v-slot="{ isValid }">
<v-card class="mx-auto ma-4" variant="outlined" max-width="600" flat :loading="loading">
<v-card-title> Register </v-card-title>
<v-card-title> {{ $t("Register") }} </v-card-title>
<v-card-text>
<v-container>
<v-row>
<v-col cols="12" md="12">
<v-text-field
v-model="email"
label="Email"
:label="$t('Email')"
name="Email"
:rules="[rules.required, rules.email]"
/>
Expand All @@ -17,7 +17,7 @@
<v-text-field
v-model="password"
type="password"
label="Password"
:label="$t('Password')"
name="Password"
:rules="[rules.required]"
/>
Expand All @@ -27,7 +27,8 @@
</v-card-text>
<v-card-actions>
<div class="text-subheader-2 pl-4 text-medium-emphasis">
Have an account? <router-link :to="{ name: 'BasicLogin' }"> Login </router-link>
{{ $t("Have an account?") }}
<router-link :to="{ name: 'BasicLogin' }"> {{ $t("Login") }} </router-link>
</div>
<v-spacer />
<v-btn
Expand All @@ -37,7 +38,7 @@
:loading="loading"
:disabled="!isValid"
>
Register
{{ $t("Register") }}
<template #loader>
<v-progress-linear indeterminate color="white" />
</template>
Expand Down
2 changes: 2 additions & 0 deletions src/dispatch/static/dispatch/src/auth/userSettings.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import store from "@/store"
import UserApi from "./api"
import { setLocale } from "@/i18n"

function load() {
return UserApi.getUserInfo().then(function (response) {
// Update the full current user data including settings
store.commit("auth/SET_CURRENT_USER", response.data)
if (response.data.settings?.locale) setLocale(response.data.settings.locale)
// Also update projects for backward compatibility
return store.commit("auth/SET_USER_PROJECTS", response.data.projects)
})
Expand Down
Loading
Loading