Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ Manage language options for your Election Event. The selected languages will be

- Use radio buttons to select the languages available.
- Set the default language by selecting **Default** next to the appropriate language.
- **Language Detection Policy**:
Affects the default language in the Voting Portal.
- **Browser Detect**: The default language will be determined by the browser.
- **Force Default**: The default language will be the one selected as **Default**.

## Ballot Design

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
id: languages
title: Languages
---

<!--
-- SPDX-FileCopyrightText: 2026 Sequent Tech Inc <legal@sequentech.io>
SPDX-License-Identifier: AGPL-3.0-only
-->
## Language Determination in the Voting Portal

In the voting portal, the system determines which language to display to voters based on a defined order. It respects user preferences while allowing administrators to enforce language policies when needed.

### Language Determination Priority

The language is determined in the following order of precedence (highest to lowest):

1. **URL Search Parameter (`lang`)** — Highest priority
2. **User Selected Locale in the login flow** — Saved in browser cookie
3. **Language Detection Policy** — Configured in election event Data tab
4. **Browser Settings** — Browser language preference (lowest priority)

### How Each Level Works

#### 1. URL Search Parameter (`lang`)

When a voter accesses the voting portal with a `lang` query parameter, it takes absolute precedence over all other settings. This allows you to direct voters to a specific language version via links.

**Example:**
```
https://myelection.sequent.vote/?lang=es
```

The `lang` parameter is checked during i18n initialization, ensuring the language is applied before any policies are evaluated.

#### 2. User Selected Locale

Once a voter selects a language in the voting portal, their choice is saved in a browser cookie. This cookie is stored for the duration of the session (until the browser is closed). On subsequent visits within the same session, the saved language preference is restored.

This allows voters to:
- Select their preferred language once
- Override the language detection policy for their individual session

#### 3. Language Detection Policy

The Language Detection Policy is configured at the election event level and determines how the system selects a language when no URL parameter or user preference cookie exists.

**Available Policies:**

- **`BROWSER_DETECT`** (default) — The voting portal automatically detects the voter's browser language and displays content in the closest available language
- **`FORCE_DEFAULT`** — All voters are shown the default language specified in the election settings, regardless of their browser language

**When this policy applies:**
- Only when no `lang` URL parameter is present
- Only when the user hasn’t manually changed the language.


### Example Scenarios

**Scenario 1: Multi-language election with browser detection**
- No language policy is set
- Voter 1 with Spanish browser settings sees Spanish immediately
- Voter 2 with English browser settings sees English immediately
- Both voters can manually select a different language and their choice is saved

**Scenario 2: Bilingual election with forced default**
- Language policy is set to `FORCE_DEFAULT` with `default_language_code: es`
- All voters see Spanish, regardless of their browser settings
- Voters can still choose a different language manually via the UI

**Scenario 3: Election with persistent user preference**
- No language policy is set
- Voter visits the portal in Spanish and selects English manually
- English preference is saved to a cookie
- The `lang` URL parameter still overrides this if provided

Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ title: Languages
SPDX-License-Identifier: AGPL-3.0-only
-->

Manage language options for the Tenant in the Admin Portal.



This is a placeholder page for the section: Languages.

Content will be added here soon.
- Use radio buttons to select the languages available.
- Set the default language by selecting **Default** next to the appropriate language.
- **Language Detection Policy**:
- **Browser Detect**: The default language will be determined by the browser.
- **Force Default**: The default language will be the one selected as **Default**.
Binary file modified packages/admin-portal/rust/sequent-core-0.1.0.tgz
Binary file not shown.
9 changes: 8 additions & 1 deletion packages/admin-portal/src/components/CustomLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {Sequent_Backend_Tenant} from "@/gql/graphql"
import {useGetOne} from "react-admin"
import cssInputLookAndFeel from "@/atoms/css-input-look-and-feel"
import {useAtomValue, useSetAtom} from "jotai"
import {ITenantTheme} from "@sequentech/ui-core"
import {applyLanguagePolicy, ITenantTheme} from "@sequentech/ui-core"
import {ImportDataDrawer} from "./election-event/import-data/ImportDataDrawer"
import {
CreateElectionEventProvider,
Expand All @@ -34,6 +34,13 @@ export const CustomCssReader: React.FC = () => {
}
}, [tenantData?.annotations?.css, setAtomValue, css])

useEffect(() => {
const languageConf = tenantData?.settings?.language_conf
if (languageConf) {
applyLanguagePolicy(languageConf)
}
}, [tenantData?.settings?.language_conf])

return <></>
}

Expand Down
53 changes: 53 additions & 0 deletions packages/admin-portal/src/components/SettingsLanguageSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: 2026 Sequent Tech Inc <legal@sequentech.io>
//
// SPDX-License-Identifier: AGPL-3.0-only
import {Box, Radio, Typography} from "@mui/material"
import {BooleanInput, useInput} from "react-admin"
import {useTranslation} from "react-i18next"

type SettingsLanguageSelectorProps = {
languageSettings: string[]
canEdit?: boolean
}

export const SettingsLanguageSelector = ({
languageSettings,
canEdit = true,
}: SettingsLanguageSelectorProps) => {
const {t} = useTranslation()

const {
field: {value: defaultLanguage, onChange: onDefaultLanguageChange},
} = useInput<string>({
source: "presentation.language_conf.default_language_code",
})

return (
<Box display="flex" flexDirection="column" gap={1}>
{languageSettings.map((lang) => (
<Box
key={lang}
display="grid"
gridTemplateColumns="11fr 1fr"
alignItems="flex-start"
columnGap={4}
>
<BooleanInput
disabled={!canEdit}
source={`enabled_languages.${lang}`}
label={String(t(`common.language.${lang}`))}
/>

<Box display="flex" alignItems="center">
<Radio
checked={defaultLanguage === lang}
onChange={() => onDefaultLanguageChange(lang)}
disabled={!canEdit}
/>
<Typography>{String(t("electionScreen.edit.default"))}</Typography>
</Box>
</Box>
))}
</Box>
)
}
39 changes: 5 additions & 34 deletions packages/admin-portal/src/resources/Election/ElectionDataForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
useRecordContext,
SimpleForm,
useGetOne,
RadioButtonGroupInput,
Toolbar,
SaveButton,
useNotify,
Expand Down Expand Up @@ -82,6 +81,7 @@ import {MANAGE_ELECTION_DATES} from "@/queries/ManageElectionDates"
import {JsonEditor, UpdateFunction} from "json-edit-react"
import {CustomFilter} from "@/types/filters"
import {useGetDocumentUrl} from "@/hooks/useGetDocumentUrl"
import {SettingsLanguageSelector} from "@/components/SettingsLanguageSelector"

const LangsWrapper = styled(Box)`
margin-top: 46px;
Expand Down Expand Up @@ -329,37 +329,6 @@ export const ElectionDataForm: React.FC = () => {
setValue(newValue)
}

const renderLangs = (parsedValue: Sequent_Backend_Election_Extended) => {
return (
<LangsWrapper>
{languageSettings.map((lang) => (
<BooleanInput
key={lang}
source={`enabled_languages.${lang}`}
label={String(t(`common.language.${lang}`))}
helperText={false}
/>
))}
</LangsWrapper>
)
}

const renderDefaultLangs = (_parsedValue: Sequent_Backend_Election_Extended) => {
let langNodes = languageSettings.map((lang) => ({
id: lang,
name: t(`electionScreen.edit.default`),
}))

return (
<RadioButtonGroupInput
label={false}
source="presentation.language_conf.default_language_code"
choices={langNodes}
row={true}
/>
)
}

const renderVotingChannels = (parsedValue: Sequent_Backend_Election_Extended) => {
let channelNodes = []
for (const channel in parsedValue?.voting_channels) {
Expand Down Expand Up @@ -652,8 +621,10 @@ export const ElectionDataForm: React.FC = () => {
<AccordionDetails>
<ElectionStyles.AccordionContainer>
<ElectionStyles.AccordionWrapper>
{renderLangs(parsedValue)}
{renderDefaultLangs(parsedValue)}
<SettingsLanguageSelector
languageSettings={languageSettings}
canEdit={canEdit}
/>
</ElectionStyles.AccordionWrapper>
</ElectionStyles.AccordionContainer>
</AccordionDetails>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ export const EditElectionEventData: React.FC = () => {
language_conf: {
...language_conf,
default_language_code: data?.presentation?.language_conf?.default_language_code,
language_detection_policy:
data?.presentation?.language_conf?.language_detection_policy,
},
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
Identifier,
useEditController,
useRecordContext,
RadioButtonGroupInput,
useNotify,
Button,
SelectInput,
Expand Down Expand Up @@ -58,6 +57,8 @@ import {
EElectionEventCeremoniesPolicy,
EElectionEventWeightedVotingPolicy,
EElectionEventDelegatedVotingPolicy,
ELanguageDetectionPolicy,
getDefaultLanguageDetectionPolicy,
} from "@sequentech/ui-core"
import {ListActions} from "@/components/ListActions"
import {ImportDataDrawer} from "@/components/election-event/import-data/ImportDataDrawer"
Expand Down Expand Up @@ -90,6 +91,7 @@ import {JsonEditor, UpdateFunction} from "json-edit-react"
import {CustomFilter} from "@/types/filters"
import {SET_VOTER_AOTHENTICATION} from "@/queries/SetVoterAuthentication"
import {GoogleMeetLinkGenerator} from "@/components/election-event/google-meet/GoogleMeetLinkGenerator"
import {SettingsLanguageSelector} from "../../components/SettingsLanguageSelector"

export type Sequent_Backend_Election_Event_Extended = RaRecord<Identifier> & {
enabled_languages?: {[key: string]: boolean}
Expand Down Expand Up @@ -297,37 +299,6 @@ export const EditElectionEventDataForm: React.FC = () => {
return errors
}

const renderDefaultLangs = (_parsedValue: Sequent_Backend_Election_Event_Extended) => {
let langNodes = languageSettings.map((lang) => ({
id: lang,
name: t(`electionScreen.edit.default`),
}))

return (
<RadioButtonGroupInput
label={false}
source="presentation.language_conf.default_language_code"
choices={langNodes}
row={true}
/>
)
}

const renderLangs = (parsedValue: Sequent_Backend_Election_Event_Extended) => {
return (
<Box>
{languageSettings.map((lang) => (
<BooleanInput
key={lang}
disabled={!canEdit}
source={`enabled_languages.${lang}`}
label={String(t(`common.language.${lang}`))}
/>
))}
</Box>
)
}

const renderVotingChannels = (parsedValue: Sequent_Backend_Election_Event_Extended) => {
let channelNodes = []
for (const channel in parsedValue?.voting_channels) {
Expand Down Expand Up @@ -574,6 +545,13 @@ export const EditElectionEventDataForm: React.FC = () => {
}))
}

const languageDetectionPolicyOptions = () => {
return Object.values(ELanguageDetectionPolicy).map((value) => ({
id: value,
name: t(`electionEventScreen.field.languageDetectionPolicy.options.${value}`),
}))
}

type UpdateFunctionProps = Parameters<UpdateFunction>[0]

const updateCustomFilters = (
Expand Down Expand Up @@ -704,6 +682,8 @@ export const EditElectionEventDataForm: React.FC = () => {
}

const onSave = async () => {
console.log(parsedValue.presentation)

await handleUpdateCustomUrls(
parsedValue.presentation as IElectionEventPresentation,
record?.id
Expand Down Expand Up @@ -742,9 +722,7 @@ export const EditElectionEventDataForm: React.FC = () => {
<Toolbar>
{canEdit && (
<SaveButton
onClick={() => {
onSave()
}}
onClick={onSave}
type="button"
alwaysEnable={activateSave}
/>
Expand Down Expand Up @@ -803,8 +781,23 @@ export const EditElectionEventDataForm: React.FC = () => {
<AccordionDetails>
<ElectionStyles.AccordionContainer>
<ElectionStyles.AccordionWrapper>
{renderLangs(parsedValue)}
{renderDefaultLangs(parsedValue)}
<Box sx={{display: "flex", flexDirection: "column", gap: 2}}>
<SettingsLanguageSelector languageSettings={languageSettings} />
<SelectInput
source={
"presentation.language_conf.language_detection_policy"
}
choices={languageDetectionPolicyOptions()}
label={String(
t(
"electionEventScreen.field.languageDetectionPolicy.policyLabel"
)
)}
defaultValue={getDefaultLanguageDetectionPolicy()}
emptyText={undefined}
validate={required()}
/>
</Box>
</ElectionStyles.AccordionWrapper>
</ElectionStyles.AccordionContainer>
</AccordionDetails>
Expand Down
Loading
Loading