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
2 changes: 1 addition & 1 deletion echo/frontend/lingui.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const config: LinguiConfig = {
fallbackLocales: {
default: "en-US",
},
locales: ["en-US", "nl-NL", "de-DE", "fr-FR", "es-ES", "it-IT"],
locales: ["en-US", "nl-NL", "de-DE", "fr-FR", "es-ES", "it-IT", "uk-UA"],
sourceLocale: "en-US",
};

Expand Down
3 changes: 2 additions & 1 deletion echo/frontend/src/components/announcement/utils/dateUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { formatRelative } from "date-fns";
import { de, enUS, es, fr, it, nl } from "date-fns/locale";
import { de, enUS, es, fr, it, nl, uk } from "date-fns/locale";
import { useLanguage } from "@/hooks/useLanguage";

// Map of supported locales to date-fns locales
Expand All @@ -10,6 +10,7 @@ const localeMap = {
"fr-FR": fr,
"it-IT": it,
"nl-NL": nl,
"uk-UA": uk,
} as const;

type SupportedLocale = keyof typeof localeMap;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const LANGUAGE_TO_LOCALE: Record<string, string> = {
fr: "fr-FR",
it: "it-IT",
nl: "nl-NL",
uk: "uk-UA",
};

export const VerifiedArtefactsSection = ({
Expand Down
6 changes: 6 additions & 0 deletions echo/frontend/src/components/language/LanguagePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ const data: Array<{
label: "Español",
language: "es-ES",
},
{
flag: "🇺🇦",
iso639_1: "uk",
label: "Ukrainian",
language: "uk-UA",
},
];

export const languageOptions = data.map((d) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,29 @@ const ParticipantOnboardingCards = ({
],
},
],
"uk-UA": [
...getSystemCards("uk-UA", tutorialSlug, legalBasis, privacyPolicyUrl),
{
section: "Перевірка мікрофона",
slides: [
{
component: MicrophoneTestComponent,
content: "Переконаймось, що ми вас чуємо.",
title: "Перевірка мікрофона",
type: "microphone",
},
],
},
{
section: "Почати",
slides: [
{
component: InitiateFormComponent,
title: "Готові розпочати?",
},
],
},
],
};

// Add this check to ensure we have valid data
Expand Down
213 changes: 213 additions & 0 deletions echo/frontend/src/components/participant/hooks/useOnboardingCards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,69 @@ export const useOnboardingCards = () => {
],
},
],
"uk-UA": [
{
section: "Ласкаво просимо",
slides: [
{
content:
"Запишіть свій голос, щоб відповісти на запитання та зробити свій внесок.",
cta: "Почнімо!",
extraHelp:
"Це міні-інструкція. Використовуйте кнопки «назад» і «далі» для навігації. Після завершення ви потрапите на портал запису.",
title: "Ласкаво просимо до Dembrane!",
},
{
content:
"Dembrane допомагає людям легко збирати думки великих груп.",
cta: "Розкажіть більше",
extraHelp:
"Чи то зворотний зв'язок для місцевої громади, чи думки в робочому середовищі, чи участь у дослідженні — ваш голос має значення!",
title: "Що таке Dembrane?",
},
{
content:
"Відповідайте на запитання у зручному темпі, говорячи або друкуючи.",
cta: "Далі",
extraHelp:
"Голосовий ввід — наш основний режим, який дозволяє давати більш природні та детальні відповіді. Друкування завжди доступне як альтернатива.",
title: "Просто скажіть, що думаєте",
},
{
content: "Dembrane цікавіше у групі!",
cta: "Далі",
extraHelp:
"Dembrane цікавіше, коли ви знаходите когось, щоб обговорити запитання разом і записати вашу розмову. Ми не можемо визначити, хто що сказав, лише які ідеї були висловлені.",
title: "Наодинці чи в групі",
},
],
},
{
section: "Як це працює",
slides: [
{
content:
"Ви отримаєте запитання, коли потрапите на портал запису.",
cta: "Зрозуміло",
extraHelp:
"Запитання залежать від потреб організатора. Вони можуть стосуватися громадських питань, робочого досвіду або дослідницьких тем. Якщо конкретних запитань немає, ви можете вільно поділитися будь-якими думками чи проблемами.",
title: "Час запитань",
},
],
},
{
section: "Конфіденційність",
slides: [
{
content: "Як записувач, ви контролюєте те, чим ділитесь.",
cta: "Розкажіть більше",
extraHelp:
"Уникайте обміну деталями, які ви не хочете повідомляти організатору. Будьте уважні та не записуйте інших без їхньої згоди.",
title: "Конфіденційність важлива",
},
],
},
],
};

// Fallback to English if language not found
Expand Down Expand Up @@ -1003,6 +1066,100 @@ export const useOnboardingCards = () => {
],
},
],
"uk-UA": [
{
section: "Ласкаво просимо",
slides: [
{
content:
"Запишіть свій голос, щоб відповісти на запитання та зробити свій внесок.",
cta: "Почнімо!",
extraHelp:
"Це міні-інструкція. Використовуйте кнопки «назад» і «далі» для навігації. Після завершення ви потрапите на портал запису.",
title: "Ласкаво просимо до Dembrane!",
},
{
content:
"Dembrane допомагає людям легко збирати думки великих груп.",
cta: "Розкажіть більше",
extraHelp:
"Чи то зворотний зв'язок для місцевої громади, чи думки в робочому середовищі, чи участь у дослідженні — ваш голос має значення!",
title: "Що таке Dembrane?",
},
{
content:
"Відповідайте на запитання у зручному темпі, говорячи або друкуючи.",
cta: "Далі",
extraHelp:
"Голосовий ввід — наш основний режим, який дозволяє давати більш природні та детальні відповіді. Друкування завжди доступне як альтернатива.",
title: "Просто скажіть, що думаєте",
},
{
content: "Dembrane цікавіше у групі!",
cta: "Далі",
extraHelp:
"Dembrane цікавіше, коли ви знаходите когось, щоб обговорити запитання разом і записати вашу розмову. Ми не можемо визначити, хто що сказав, лише які ідеї були висловлені.",
title: "Наодинці чи в групі",
},
],
},
{
section: "Як це працює",
slides: [
{
content:
"Ви отримаєте запитання, коли потрапите на портал запису.",
cta: "Зрозуміло",
extraHelp:
"Запитання залежать від потреб організатора. Вони можуть стосуватися громадських питань, робочого досвіду або дослідницьких тем. Якщо конкретних запитань немає, ви можете вільно поділитися будь-якими думками чи проблемами.",
title: "Час запитань",
},
],
},
{
section: "Конфіденційність",
slides: [
{
content: "Як записувач, ви контролюєте те, чим ділитесь.",
cta: "Розкажіть більше",
extraHelp:
"Уникайте обміну деталями, які ви не хочете повідомляти організатору. Будьте уважні та не записуйте інших без їхньої згоди.",
title: "Конфіденційність важлива",
},
...(getPrivacyCard("uk-UA", legalBasis, privacyPolicyUrl)?.slides ||
[]),
],
},
{
section: "Корисні поради",
slides: [
{
content:
"Уявіть, що Dembrane на гучному зв'язку з вами. Якщо ви чуєте себе — все добре.",
cta: "Зрозуміло",
extraHelp:
"Невеликий фоновий шум — це нормально, головне, щоб було зрозуміло, хто говорить.",
title: "Зменшіть фоновий шум",
},
{
content:
"Забезпечте стабільне з'єднання для безперебійного запису.",
cta: "Готово!",
extraHelp:
"Wi-Fi або хороший мобільний інтернет працюють найкраще. Якщо з'єднання обірветься, не хвилюйтесь. Ви завжди можете продовжити з того місця, де зупинились.",
title: "Надійне інтернет-з'єднання",
},
{
content:
"Запобігайте перервам, тримаючи пристрій розблокованим. Якщо він заблокується, просто розблокуйте і продовжуйте.",
cta: "Добре",
extraHelp:
"Dembrane намагається тримати ваш пристрій активним, але іноді пристрої можуть це перевизначити. Ви можете налаштувати параметри пристрою, щоб він залишався розблокованим довше, якщо потрібно.",
title: "Не блокуйте пристрій!",
},
],
},
],
};

// Fallback to English if language not found
Expand Down Expand Up @@ -1105,6 +1262,19 @@ export const useOnboardingCards = () => {
},
],
},
"uk-UA": {
section: "Конфіденційність",
slides: [
{
content:
"Організатор відповідає за те, як використовуються ваші дані в цій сесії. dembrane обробляє вашу розмову від його імені.",
cta: "Я розумію",
extraHelp:
"Записи транскрибуються та аналізуються для отримання висновків. Ваші дані зберігаються на захищених серверах у Європі, не використовуються для навчання моделей ШІ та видаляються протягом 30 днів після завершення проєкту.\n\nПитання щодо вашої конфіденційності? Зверніться безпосередньо до організатора.",
title: "Контролер даних, використання та безпека.",
},
],
},
};

return cards[lang] || cards["en-US"] || null;
Expand Down Expand Up @@ -1275,6 +1445,32 @@ export const useOnboardingCards = () => {
},
],
},
"uk-UA": {
section: "Конфіденційність",
slides: [
{
checkbox: {
label: "Я даю згоду на запис і обробку моєї розмови.",
required: true,
},
content:
"Організатор відповідає за те, як використовуються ваші дані в цій сесії. dembrane обробляє вашу розмову від його імені.",
cta: "Я розумію",
extraHelp:
"Записи транскрибуються та аналізуються для отримання висновків. Ваші дані зберігаються на захищених серверах у Європі, не використовуються для навчання моделей ШІ та видаляються протягом 30 днів після завершення проєкту.\n\nПитання щодо вашої конфіденційності? Зверніться безпосередньо до організатора.",
...(policyUrl
? {
link: {
label:
"Ознайомтесь з політикою конфіденційності організатора",
url: policyUrl,
},
}
: {}),
title: "Контролер даних, використання та безпека.",
},
],
},
};

return cards[lang] || cards["en-US"] || null;
Expand Down Expand Up @@ -1389,6 +1585,23 @@ export const useOnboardingCards = () => {
},
],
},
"uk-UA": {
section: "Конфіденційність",
slides: [
{
content:
"dembrane записує та аналізує цю розмову на основі нашого законного інтересу: точно фіксувати обговорення, надавати надійні висновки та розвивати нашу платформу.",
cta: "Я розумію",
extraHelp:
"Записи та транскрипції видаляються протягом 30 днів після закриття сесії. Дані зберігаються на захищених серверах у Європі та не використовуються для навчання моделей ШІ.\n\nПитання або бажаєте заперечити? Зв'яжіться з нами за адресою info@dembrane.com або ознайомтесь з нашою політикою конфіденційності.",
link: {
label: "Ознайомтесь з повною політикою конфіденційності",
url: dembranePrivacyUrl,
},
title: "Використання даних та безпека",
},
],
},
};

return cards[lang] || cards["en-US"] || null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const VerifiedArtefactsList = ({
fr: "fr-FR",
it: "it-IT",
nl: "nl-NL",
uk: "uk-UA",
};

const locale =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
} from "./hooks";
import { VerifyInstructions } from "./VerifyInstructions";

type LanguageCode = "de" | "en" | "es" | "fr" | "nl" | "it";
type LanguageCode = "de" | "en" | "es" | "fr" | "nl" | "it" | "uk";

const LANGUAGE_TO_LOCALE: Record<LanguageCode, string> = {
de: "de-DE",
Expand All @@ -26,6 +26,7 @@ const LANGUAGE_TO_LOCALE: Record<LanguageCode, string> = {
fr: "fr-FR",
it: "it-IT",
nl: "nl-NL",
uk: "uk-UA",
};

const localeFromLanguage = (language?: string) => {
Expand Down
1 change: 1 addition & 0 deletions echo/frontend/src/components/project/CustomTopicModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const SUPPORTED_LANGUAGES = [
{ code: "fr-FR", label: "Français" },
{ code: "es-ES", label: "Español" },
{ code: "it-IT", label: "Italiano" },
{ code: "uk-UA", label: "Ukrainian" },
] as const;

type CustomTopicModalProps = {
Expand Down
6 changes: 4 additions & 2 deletions echo/frontend/src/components/project/ProjectPortalEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,13 @@ const FormSchema = z.object({
is_project_notification_subscription_allowed: z.boolean(),
is_verify_enabled: z.boolean(),
is_verify_on_finish_enabled: z.boolean(),
language: z.enum(["en", "nl", "de", "fr", "es", "it"]),
language: z.enum(["en", "nl", "de", "fr", "es", "it", "uk"]),
verification_topics: z.array(z.string()),
});

type ProjectPortalFormValues = z.infer<typeof FormSchema>;

type LanguageCode = "de" | "en" | "es" | "fr" | "nl" | "it";
type LanguageCode = "de" | "en" | "es" | "fr" | "nl" | "it" | "uk";

const LANGUAGE_TO_LOCALE: Record<LanguageCode, string> = {
de: "de-DE",
Expand All @@ -96,6 +96,7 @@ const LANGUAGE_TO_LOCALE: Record<LanguageCode, string> = {
fr: "fr-FR",
it: "it-IT",
nl: "nl-NL",
uk: "uk-UA",
};

const localeFromIso = (iso?: string) =>
Expand Down Expand Up @@ -642,6 +643,7 @@ const ProjectPortalEditorComponent: React.FC<ProjectPortalEditorProps> = ({
{ label: t`Spanish`, value: "es" },
{ label: t`French`, value: "fr" },
{ label: t`Italian`, value: "it" },
{ label: t`Ukrainian`, value: "uk" },
]}
{...field}
{...testId("portal-editor-language-select")}
Expand Down
Loading
Loading