1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-19 05:09:43 +02:00

feat: Add language selector

Closes #212
This commit is contained in:
Maksim Eltyshev 2022-07-26 12:26:42 +02:00
parent a1cb04ea8e
commit 1329da3fe5
31 changed files with 277 additions and 40 deletions

View file

@ -41,6 +41,13 @@ export const handleUserUpdate = (user) => ({
}, },
}); });
export const updateCurrentUserLanguage = (language) => ({
type: EntryActionTypes.CURRENT_USER_LANGUAGE_UPDATE,
payload: {
language,
},
});
export const updateUserEmail = (id, data) => ({ export const updateUserEmail = (id, data) => ({
type: EntryActionTypes.USER_EMAIL_UPDATE, type: EntryActionTypes.USER_EMAIL_UPDATE,
payload: { payload: {

View file

@ -1,8 +1,9 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button, Divider, Header, Tab } from 'semantic-ui-react'; import { Button, Divider, Dropdown, Header, Tab } from 'semantic-ui-react';
import locales from '../../../locales';
import AvatarEditPopup from './AvatarEditPopup'; import AvatarEditPopup from './AvatarEditPopup';
import User from '../../User'; import User from '../../User';
import UserInformationEdit from '../../UserInformationEdit'; import UserInformationEdit from '../../UserInformationEdit';
@ -20,12 +21,14 @@ const AccountPane = React.memo(
avatarUrl, avatarUrl,
phone, phone,
organization, organization,
language,
isAvatarUpdating, isAvatarUpdating,
usernameUpdateForm, usernameUpdateForm,
emailUpdateForm, emailUpdateForm,
passwordUpdateForm, passwordUpdateForm,
onUpdate, onUpdate,
onAvatarUpdate, onAvatarUpdate,
onLanguageUpdate,
onUsernameUpdate, onUsernameUpdate,
onUsernameUpdateMessageDismiss, onUsernameUpdateMessageDismiss,
onEmailUpdate, onEmailUpdate,
@ -41,6 +44,13 @@ const AccountPane = React.memo(
}); });
}, [onUpdate]); }, [onUpdate]);
const handleLanguageChange = useCallback(
(_, { value }) => {
onLanguageUpdate(value === 'auto' ? null : value); // FIXME: hack
},
[onLanguageUpdate],
);
return ( return (
<Tab.Pane attached={false} className={styles.wrapper}> <Tab.Pane attached={false} className={styles.wrapper}>
<AvatarEditPopup <AvatarEditPopup
@ -60,6 +70,32 @@ const AccountPane = React.memo(
}} }}
onUpdate={onUpdate} onUpdate={onUpdate}
/> />
<Divider horizontal section>
<Header as="h4">
{t('common.language', {
context: 'title',
})}
</Header>
</Divider>
<Dropdown
fluid
selection
options={[
{
key: 'auto',
value: 'auto',
text: t('common.detectAutomatically'),
},
...locales.map((locale) => ({
key: locale.language,
value: locale.language,
flag: locale.country,
text: locale.name,
})),
]}
value={language || 'auto'}
onChange={handleLanguageChange}
/>
<Divider horizontal section> <Divider horizontal section>
<Header as="h4"> <Header as="h4">
{t('common.authentication', { {t('common.authentication', {
@ -129,6 +165,7 @@ AccountPane.propTypes = {
avatarUrl: PropTypes.string, avatarUrl: PropTypes.string,
phone: PropTypes.string, phone: PropTypes.string,
organization: PropTypes.string, organization: PropTypes.string,
language: PropTypes.string,
isAvatarUpdating: PropTypes.bool.isRequired, isAvatarUpdating: PropTypes.bool.isRequired,
/* eslint-disable react/forbid-prop-types */ /* eslint-disable react/forbid-prop-types */
usernameUpdateForm: PropTypes.object.isRequired, usernameUpdateForm: PropTypes.object.isRequired,
@ -137,6 +174,7 @@ AccountPane.propTypes = {
/* eslint-enable react/forbid-prop-types */ /* eslint-enable react/forbid-prop-types */
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onAvatarUpdate: PropTypes.func.isRequired, onAvatarUpdate: PropTypes.func.isRequired,
onLanguageUpdate: PropTypes.func.isRequired,
onUsernameUpdate: PropTypes.func.isRequired, onUsernameUpdate: PropTypes.func.isRequired,
onUsernameUpdateMessageDismiss: PropTypes.func.isRequired, onUsernameUpdateMessageDismiss: PropTypes.func.isRequired,
onEmailUpdate: PropTypes.func.isRequired, onEmailUpdate: PropTypes.func.isRequired,
@ -150,6 +188,7 @@ AccountPane.defaultProps = {
avatarUrl: undefined, avatarUrl: undefined,
phone: undefined, phone: undefined,
organization: undefined, organization: undefined,
language: undefined,
}; };
export default AccountPane; export default AccountPane;

View file

@ -14,6 +14,7 @@ const UserSettingsModal = React.memo(
avatarUrl, avatarUrl,
phone, phone,
organization, organization,
language,
subscribeToOwnCards, subscribeToOwnCards,
isAvatarUpdating, isAvatarUpdating,
usernameUpdateForm, usernameUpdateForm,
@ -21,6 +22,7 @@ const UserSettingsModal = React.memo(
passwordUpdateForm, passwordUpdateForm,
onUpdate, onUpdate,
onAvatarUpdate, onAvatarUpdate,
onLanguageUpdate,
onUsernameUpdate, onUsernameUpdate,
onUsernameUpdateMessageDismiss, onUsernameUpdateMessageDismiss,
onEmailUpdate, onEmailUpdate,
@ -44,12 +46,14 @@ const UserSettingsModal = React.memo(
avatarUrl={avatarUrl} avatarUrl={avatarUrl}
phone={phone} phone={phone}
organization={organization} organization={organization}
language={language}
isAvatarUpdating={isAvatarUpdating} isAvatarUpdating={isAvatarUpdating}
usernameUpdateForm={usernameUpdateForm} usernameUpdateForm={usernameUpdateForm}
emailUpdateForm={emailUpdateForm} emailUpdateForm={emailUpdateForm}
passwordUpdateForm={passwordUpdateForm} passwordUpdateForm={passwordUpdateForm}
onUpdate={onUpdate} onUpdate={onUpdate}
onAvatarUpdate={onAvatarUpdate} onAvatarUpdate={onAvatarUpdate}
onLanguageUpdate={onLanguageUpdate}
onUsernameUpdate={onUsernameUpdate} onUsernameUpdate={onUsernameUpdate}
onUsernameUpdateMessageDismiss={onUsernameUpdateMessageDismiss} onUsernameUpdateMessageDismiss={onUsernameUpdateMessageDismiss}
onEmailUpdate={onEmailUpdate} onEmailUpdate={onEmailUpdate}
@ -92,6 +96,7 @@ UserSettingsModal.propTypes = {
avatarUrl: PropTypes.string, avatarUrl: PropTypes.string,
phone: PropTypes.string, phone: PropTypes.string,
organization: PropTypes.string, organization: PropTypes.string,
language: PropTypes.string,
subscribeToOwnCards: PropTypes.bool.isRequired, subscribeToOwnCards: PropTypes.bool.isRequired,
isAvatarUpdating: PropTypes.bool.isRequired, isAvatarUpdating: PropTypes.bool.isRequired,
/* eslint-disable react/forbid-prop-types */ /* eslint-disable react/forbid-prop-types */
@ -101,6 +106,7 @@ UserSettingsModal.propTypes = {
/* eslint-enable react/forbid-prop-types */ /* eslint-enable react/forbid-prop-types */
onUpdate: PropTypes.func.isRequired, onUpdate: PropTypes.func.isRequired,
onAvatarUpdate: PropTypes.func.isRequired, onAvatarUpdate: PropTypes.func.isRequired,
onLanguageUpdate: PropTypes.func.isRequired,
onUsernameUpdate: PropTypes.func.isRequired, onUsernameUpdate: PropTypes.func.isRequired,
onUsernameUpdateMessageDismiss: PropTypes.func.isRequired, onUsernameUpdateMessageDismiss: PropTypes.func.isRequired,
onEmailUpdate: PropTypes.func.isRequired, onEmailUpdate: PropTypes.func.isRequired,
@ -115,6 +121,7 @@ UserSettingsModal.defaultProps = {
avatarUrl: undefined, avatarUrl: undefined,
phone: undefined, phone: undefined,
organization: undefined, organization: undefined,
language: undefined,
}; };
export default UserSettingsModal; export default UserSettingsModal;

View file

@ -31,6 +31,7 @@ export default {
USER_UPDATE: `${PREFIX}/USER_UPDATE`, USER_UPDATE: `${PREFIX}/USER_UPDATE`,
CURRENT_USER_UPDATE: `${PREFIX}/CURRENT_USER_UPDATE`, CURRENT_USER_UPDATE: `${PREFIX}/CURRENT_USER_UPDATE`,
USER_UPDATE_HANDLE: `${PREFIX}/USER_UPDATE_HANDLE`, USER_UPDATE_HANDLE: `${PREFIX}/USER_UPDATE_HANDLE`,
CURRENT_USER_LANGUAGE_UPDATE: `${PREFIX}/CURRENT_USER_LANGUAGE_UPDATE`,
USER_EMAIL_UPDATE: `${PREFIX}/USER_EMAIL_UPDATE`, USER_EMAIL_UPDATE: `${PREFIX}/USER_EMAIL_UPDATE`,
CURRENT_USER_EMAIL_UPDATE: `${PREFIX}/CURRENT_USER_EMAIL_UPDATE`, CURRENT_USER_EMAIL_UPDATE: `${PREFIX}/CURRENT_USER_EMAIL_UPDATE`,
USER_EMAIL_UPDATE_ERROR_CLEAR: `${PREFIX}/USER_EMAIL_UPDATE_ERROR_CLEAR`, USER_EMAIL_UPDATE_ERROR_CLEAR: `${PREFIX}/USER_EMAIL_UPDATE_ERROR_CLEAR`,

View file

@ -10,6 +10,7 @@ import {
updateCurrentUser, updateCurrentUser,
updateCurrentUserAvatar, updateCurrentUserAvatar,
updateCurrentUserEmail, updateCurrentUserEmail,
updateCurrentUserLanguage,
updateCurrentUserPassword, updateCurrentUserPassword,
updateCurrentUserUsername, updateCurrentUserUsername,
} from '../actions/entry'; } from '../actions/entry';
@ -23,6 +24,7 @@ const mapStateToProps = (state) => {
avatarUrl, avatarUrl,
phone, phone,
organization, organization,
language,
subscribeToOwnCards, subscribeToOwnCards,
isAvatarUpdating, isAvatarUpdating,
emailUpdateForm, emailUpdateForm,
@ -37,6 +39,7 @@ const mapStateToProps = (state) => {
avatarUrl, avatarUrl,
phone, phone,
organization, organization,
language,
subscribeToOwnCards, subscribeToOwnCards,
isAvatarUpdating, isAvatarUpdating,
emailUpdateForm, emailUpdateForm,
@ -50,6 +53,7 @@ const mapDispatchToProps = (dispatch) =>
{ {
onUpdate: updateCurrentUser, onUpdate: updateCurrentUser,
onAvatarUpdate: updateCurrentUserAvatar, onAvatarUpdate: updateCurrentUserAvatar,
onLanguageUpdate: updateCurrentUserLanguage,
onUsernameUpdate: updateCurrentUserUsername, onUsernameUpdate: updateCurrentUserUsername,
onUsernameUpdateMessageDismiss: clearCurrentUserUsernameUpdateError, onUsernameUpdateMessageDismiss: clearCurrentUserUsernameUpdateError,
onEmailUpdate: updateCurrentUserEmail, onEmailUpdate: updateCurrentUserEmail,

View file

@ -1,11 +1,11 @@
import i18n from 'i18next'; import i18n from 'i18next';
import languageDetector from 'i18next-browser-languagedetector'; import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next'; import { initReactI18next } from 'react-i18next';
import formatDate from 'date-fns/format'; import formatDate from 'date-fns/format';
import parseDate from 'date-fns/parse'; import parseDate from 'date-fns/parse';
import { registerLocale, setDefaultLocale } from 'react-datepicker'; import { registerLocale, setDefaultLocale } from 'react-datepicker';
import { embedLocales, languages } from './locales'; import { embeddedLocales, languages } from './locales';
i18n.dateFns = { i18n.dateFns = {
locales: {}, locales: {},
@ -52,12 +52,12 @@ const parseDatePostProcessor = {
}; };
i18n i18n
.use(languageDetector) .use(LanguageDetector)
.use(formatDatePostProcessor) .use(formatDatePostProcessor)
.use(parseDatePostProcessor) .use(parseDatePostProcessor)
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
resources: embedLocales, resources: embeddedLocales,
fallbackLng: 'en', fallbackLng: 'en',
supportedLngs: languages, supportedLngs: languages,
load: 'languageOnly', load: 'languageOnly',
@ -74,7 +74,7 @@ i18n
}, },
}, },
react: { react: {
useSuspense: false, useSuspense: true,
}, },
debug: process.env.NODE_ENV !== 'production', debug: process.env.NODE_ENV !== 'production',
}); });
@ -95,4 +95,20 @@ i18n.loadCoreLocale = async (language = i18n.resolvedLanguage) => {
}); });
}; };
i18n.detectLanguage = () => {
const {
services: { languageDetector, languageUtils },
} = i18n;
localStorage.removeItem(languageDetector.options.lookupLocalStorage);
const detectedLanguages = languageDetector.detect();
i18n.language = languageUtils.getBestMatchFromCodes(detectedLanguages);
i18n.languages = languageUtils.toResolveHierarchy(i18n.language);
i18n.resolvedLanguage = undefined;
i18n.setResolvedLanguage(i18n.language);
};
export default i18n; export default i18n;

View file

@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'cs',
country: 'cz',
name: 'Čeština',
embeddedLocale: login,
};

View file

@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'da',
country: 'dk',
name: 'Dansk',
embeddedLocale: login,
};

View file

@ -72,6 +72,7 @@ export default {
deleteTask_title: 'Aufgabe löschen', deleteTask_title: 'Aufgabe löschen',
deleteUser_title: 'Benutzer löschen', deleteUser_title: 'Benutzer löschen',
description: 'Beschreibung', description: 'Beschreibung',
detectAutomatically: 'Automatische Erkennung',
dropFileToUpload: 'Datei ablegen, um hochzuladen', dropFileToUpload: 'Datei ablegen, um hochzuladen',
editAttachment_title: 'Anhang bearbieten', editAttachment_title: 'Anhang bearbieten',
editAvatar_title: 'Avatar bearbeiten', editAvatar_title: 'Avatar bearbeiten',
@ -97,6 +98,7 @@ export default {
hours: 'Stunden', hours: 'Stunden',
invalidCurrentPassword: 'Das aktuelle Passwort ist falsch', invalidCurrentPassword: 'Das aktuelle Passwort ist falsch',
labels: 'Labels', labels: 'Labels',
language: 'Sprache',
leaveBoard_title: 'Board verlassen', leaveBoard_title: 'Board verlassen',
leaveProject_title: 'Projekt verlassen', leaveProject_title: 'Projekt verlassen',
list: 'Listen', list: 'Listen',

View file

@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'de',
country: 'de',
name: 'Deutsch',
embeddedLocale: login,
};

View file

@ -64,6 +64,7 @@ export default {
deleteTask_title: 'Delete Task', deleteTask_title: 'Delete Task',
deleteUser_title: 'Delete User', deleteUser_title: 'Delete User',
description: 'Description', description: 'Description',
detectAutomatically: 'Detect automatically',
dropFileToUpload: 'Drop file to upload', dropFileToUpload: 'Drop file to upload',
editAttachment_title: 'Edit Attachment', editAttachment_title: 'Edit Attachment',
editAvatar_title: 'Edit Avatar', editAvatar_title: 'Edit Avatar',
@ -90,6 +91,7 @@ export default {
hours: 'Hours', hours: 'Hours',
invalidCurrentPassword: 'Invalid current password', invalidCurrentPassword: 'Invalid current password',
labels: 'Labels', labels: 'Labels',
language: 'Language',
leaveBoard_title: 'Leave Board', leaveBoard_title: 'Leave Board',
leaveProject_title: 'Leave Project', leaveProject_title: 'Leave Project',
list: 'List', list: 'List',

View file

@ -0,0 +1,11 @@
import merge from 'lodash/merge';
import login from './login';
import core from './core';
export default {
language: 'en',
country: 'us',
name: 'English',
embeddedLocale: merge(login, core),
};

View file

@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'es',
country: 'es',
name: 'Español',
embeddedLocale: login,
};

View file

@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'fr',
country: 'fr',
name: 'Français',
embeddedLocale: login,
};

View file

@ -1,37 +1,27 @@
import merge from 'lodash/merge'; import cs from './cs';
import fromPairs from 'lodash/fromPairs'; import da from './da';
import de from './de';
import en from './en';
import es from './es';
import fr from './fr';
import ja from './ja';
import pl from './pl';
import ru from './ru';
import sk from './sk';
import sv from './sv';
import uz from './uz';
import zh from './zh';
import csLogin from './cs/login'; const locales = [cs, da, de, en, es, fr, ja, pl, ru, sk, sv, uz, zh];
import daLogin from './da/login';
import deLogin from './de/login';
import enLogin from './en/login';
import enCore from './en/core';
import esLogin from './es/login';
import frLogin from './fr/login';
import jaLogin from './ja/login';
import plLogin from './pl/login';
import ruLogin from './ru/login';
import skLogin from './sk/login';
import svLogin from './sv/login';
import uzLogin from './uz/login';
import zhLogin from './zh/login';
const localePairs = [ export default locales;
['cs', csLogin],
['da', daLogin],
['de', deLogin],
['en', merge(enLogin, enCore)],
['es', esLogin],
['fr', frLogin],
['ja', jaLogin],
['pl', plLogin],
['ru', ruLogin],
['sk', skLogin],
['sv', svLogin],
['uz', uzLogin],
['zh', zhLogin],
];
export const languages = localePairs.map((locale) => locale[0]); export const languages = locales.map((locale) => locale.language);
export const embedLocales = fromPairs(localePairs); export const embeddedLocales = locales.reduce(
(result, locale) => ({
...result,
[locale.language]: locale.embeddedLocale,
}),
{},
);

View file

@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'ja',
country: 'jp',
name: '日本語',
embeddedLocale: login,
};

View file

@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'pl',
country: 'pl',
name: 'Polski',
embeddedLocale: login,
};

View file

@ -62,6 +62,7 @@ export default {
deleteTask: 'Удаление задачи', deleteTask: 'Удаление задачи',
deleteUser: 'Удаление пользователя', deleteUser: 'Удаление пользователя',
description: 'Описание', description: 'Описание',
detectAutomatically: 'Определить автоматически',
dropFileToUpload: 'Перетяните файл, чтобы загрузить', dropFileToUpload: 'Перетяните файл, чтобы загрузить',
editAttachment: 'Изменение вложения', editAttachment: 'Изменение вложения',
editAvatar: 'Изменение аватара', editAvatar: 'Изменение аватара',
@ -88,6 +89,7 @@ export default {
hours: 'Часы', hours: 'Часы',
invalidCurrentPassword: 'Неверный текущий пароль', invalidCurrentPassword: 'Неверный текущий пароль',
labels: 'Метки', labels: 'Метки',
language: 'Язык',
list: 'Список', list: 'Список',
listActions: 'Действия со списком', listActions: 'Действия со списком',
members: 'Участники', members: 'Участники',

View file

@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'ru',
country: 'ru',
name: 'Русский',
embeddedLocale: login,
};

View file

@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'sk',
country: 'sk',
name: 'Slovenčina',
embeddedLocale: login,
};

View file

@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'sv',
country: 'se',
name: 'Svenska',
embeddedLocale: login,
};

View file

@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'uz',
country: 'uz',
name: "O'zbek",
embeddedLocale: login,
};

View file

@ -0,0 +1,8 @@
import login from './login';
export default {
language: 'zh',
country: 'cn',
name: '中文',
embeddedLocale: login,
};

View file

@ -40,6 +40,7 @@ export default class extends Model {
avatarUrl: attr(), avatarUrl: attr(),
phone: attr(), phone: attr(),
organization: attr(), organization: attr(),
language: attr(),
subscribeToOwnCards: attr(), subscribeToOwnCards: attr(),
deletedAt: attr(), deletedAt: attr(),
isAdmin: attr({ isAdmin: attr({

View file

@ -4,7 +4,6 @@ import { fetchCoreRequest } from '../requests';
import { initializeCore } from '../../../actions'; import { initializeCore } from '../../../actions';
import i18n from '../../../i18n'; import i18n from '../../../i18n';
// eslint-disable-next-line import/prefer-default-export
export function* initializeCoreService() { export function* initializeCoreService() {
const { const {
user, user,
@ -25,6 +24,7 @@ export function* initializeCoreService() {
notifications, notifications,
} = yield call(fetchCoreRequest); // TODO: handle error } = yield call(fetchCoreRequest); // TODO: handle error
yield call(i18n.changeLanguage, user.language);
yield call(i18n.loadCoreLocale); yield call(i18n.loadCoreLocale);
yield put( yield put(
@ -48,3 +48,14 @@ export function* initializeCoreService() {
), ),
); );
} }
export function* changeCoreLanguageService(language) {
if (language === null) {
yield call(i18n.detectLanguage);
yield call(i18n.loadCoreLocale);
yield call(i18n.changeLanguage, i18n.resolvedLanguage);
} else {
yield call(i18n.loadCoreLocale, language);
yield call(i18n.changeLanguage, language);
}
}

View file

@ -1,6 +1,7 @@
import { call, put, select } from 'redux-saga/effects'; import { call, put, select } from 'redux-saga/effects';
import { logoutService } from './login'; import { logoutService } from './login';
import { changeCoreLanguageService } from './core';
import request from '../request'; import request from '../request';
import { currentUserIdSelector, currentUserSelector, pathSelector } from '../../../selectors'; import { currentUserIdSelector, currentUserSelector, pathSelector } from '../../../selectors';
import { import {
@ -81,6 +82,21 @@ export function* handleUserUpdateService(user) {
yield put(handleUserUpdate(user, users, isCurrent)); yield put(handleUserUpdate(user, users, isCurrent));
} }
// TODO: add loading state
export function* updateUserLanguageService(id, language) {
yield call(changeCoreLanguageService, language);
yield call(updateUserService, id, {
language,
});
}
export function* updateCurrentUserLanguageService(language) {
const id = yield select(currentUserIdSelector);
yield call(updateUserLanguageService, id, language);
}
export function* updateUserEmailService(id, data) { export function* updateUserEmailService(id, data) {
yield put(updateUserEmail(id, data)); yield put(updateUserEmail(id, data));

View file

@ -24,6 +24,7 @@ import {
updateUserService, updateUserService,
updateCurrentUserAvatarService, updateCurrentUserAvatarService,
updateCurrentUserEmailService, updateCurrentUserEmailService,
updateCurrentUserLanguageService,
updateCurrentUserPasswordService, updateCurrentUserPasswordService,
updateCurrentUserService, updateCurrentUserService,
updateCurrentUserUsernameService, updateCurrentUserUsernameService,
@ -49,6 +50,9 @@ export default function* userWatchers() {
takeEvery(EntryActionTypes.USER_UPDATE_HANDLE, ({ payload: { user } }) => takeEvery(EntryActionTypes.USER_UPDATE_HANDLE, ({ payload: { user } }) =>
handleUserUpdateService(user), handleUserUpdateService(user),
), ),
takeEvery(EntryActionTypes.CURRENT_USER_LANGUAGE_UPDATE, ({ payload: { language } }) =>
updateCurrentUserLanguageService(language),
),
takeEvery(EntryActionTypes.USER_EMAIL_UPDATE, ({ payload: { id, data } }) => takeEvery(EntryActionTypes.USER_EMAIL_UPDATE, ({ payload: { id, data } }) =>
updateUserEmailService(id, data), updateUserEmailService(id, data),
), ),

View file

@ -40,6 +40,11 @@ module.exports = {
isNotEmptyString: true, isNotEmptyString: true,
allowNull: true, allowNull: true,
}, },
language: {
type: 'string',
isNotEmptyString: true,
allowNull: true,
},
subscribeToOwnCards: { subscribeToOwnCards: {
type: 'boolean', type: 'boolean',
}, },
@ -62,6 +67,7 @@ module.exports = {
'username', 'username',
'phone', 'phone',
'organization', 'organization',
'language',
'subscribeToOwnCards', 'subscribeToOwnCards',
]); ]);

View file

@ -32,6 +32,11 @@ module.exports = {
isNotEmptyString: true, isNotEmptyString: true,
allowNull: true, allowNull: true,
}, },
language: {
type: 'string',
isNotEmptyString: true,
allowNull: true,
},
subscribeToOwnCards: { subscribeToOwnCards: {
type: 'boolean', type: 'boolean',
}, },
@ -66,6 +71,7 @@ module.exports = {
'avatarUrl', 'avatarUrl',
'phone', 'phone',
'organization', 'organization',
'language',
'subscribeToOwnCards', 'subscribeToOwnCards',
]); ]);

View file

@ -53,6 +53,11 @@ module.exports = {
isNotEmptyString: true, isNotEmptyString: true,
allowNull: true, allowNull: true,
}, },
language: {
type: 'string',
isNotEmptyString: true,
allowNull: true,
},
subscribeToOwnCards: { subscribeToOwnCards: {
type: 'boolean', type: 'boolean',
defaultsTo: false, defaultsTo: false,

View file

@ -0,0 +1,11 @@
module.exports.up = async (knex) =>
knex.schema.table('user_account', (table) => {
/* Columns */
table.text('language');
});
module.exports.down = async (knex) =>
knex.schema.table('user_account', (table) => {
table.dropColumn('language');
});