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

feat: Languages with country codes

This commit is contained in:
Maksim Eltyshev 2024-07-21 19:33:57 +02:00
parent 79ad1836a8
commit 07e1903bb5
83 changed files with 211 additions and 99 deletions

View file

@ -12,7 +12,7 @@
"dequal": "^2.0.3", "dequal": "^2.0.3",
"easymde": "^2.18.0", "easymde": "^2.18.0",
"history": "^5.3.0", "history": "^5.3.0",
"i18next": "^23.11.5", "i18next": "^23.12.2",
"i18next-browser-languagedetector": "^8.0.0", "i18next-browser-languagedetector": "^8.0.0",
"initials": "^3.1.2", "initials": "^3.1.2",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
@ -30,7 +30,7 @@
"react-datepicker": "^4.25.0", "react-datepicker": "^4.25.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
"react-i18next": "^14.1.2", "react-i18next": "^15.0.0",
"react-input-mask": "^2.0.4", "react-input-mask": "^2.0.4",
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-photoswipe-gallery": "^2.2.7", "react-photoswipe-gallery": "^2.2.7",
@ -2007,9 +2007,9 @@
"integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==" "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA=="
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.24.6", "version": "7.24.8",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.6.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.8.tgz",
"integrity": "sha512-Ja18XcETdEl5mzzACGd+DKgaGJzPTCow7EglgwTmHdwokzDFYh/MHua6lU6DV/hjF2IaOJ4oX2nqnjG7RElKOw==", "integrity": "sha512-5F7SDGs1T72ZczbRwbGO9lQi0NLjQxzl6i4lJxLxfW9U5UluCSyEJeniWvnhl3/euNiqQVbo8zruhsDfid0esA==",
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
}, },
@ -11975,9 +11975,9 @@
} }
}, },
"node_modules/i18next": { "node_modules/i18next": {
"version": "23.11.5", "version": "23.12.2",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.11.5.tgz", "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.12.2.tgz",
"integrity": "sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA==", "integrity": "sha512-XIeh5V+bi8SJSWGL3jqbTEBW5oD6rbP5L+E7dVQh1MNTxxYef0x15rhJVcRb7oiuq4jLtgy2SD8eFlf6P2cmqg==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@ -22084,11 +22084,11 @@
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
}, },
"node_modules/react-i18next": { "node_modules/react-i18next": {
"version": "14.1.2", "version": "15.0.0",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-14.1.2.tgz", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.0.0.tgz",
"integrity": "sha512-FSIcJy6oauJbGEXfhUgVeLzvWBhIBIS+/9c6Lj4niwKZyGaGb4V4vUbATXSlsHJDXXB+ociNxqFNiFuV1gmoqg==", "integrity": "sha512-2O3IgF4zivg57Q6p6i+ChDgJ371IDcEWbuWC6gvoh5NbkDMs0Q+O7RPr4v61+Se32E0V+LmtwePAeqWZW0bi6g==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.23.9", "@babel/runtime": "^7.24.8",
"html-parse-stringify": "^3.0.1" "html-parse-stringify": "^3.0.1"
}, },
"peerDependencies": { "peerDependencies": {

View file

@ -65,7 +65,7 @@
"dequal": "^2.0.3", "dequal": "^2.0.3",
"easymde": "^2.18.0", "easymde": "^2.18.0",
"history": "^5.3.0", "history": "^5.3.0",
"i18next": "^23.11.5", "i18next": "^23.12.2",
"i18next-browser-languagedetector": "^8.0.0", "i18next-browser-languagedetector": "^8.0.0",
"initials": "^3.1.2", "initials": "^3.1.2",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
@ -83,7 +83,7 @@
"react-datepicker": "^4.25.0", "react-datepicker": "^4.25.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-dropzone": "^14.2.3", "react-dropzone": "^14.2.3",
"react-i18next": "^14.1.2", "react-i18next": "^15.0.0",
"react-input-mask": "^2.0.4", "react-input-mask": "^2.0.4",
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-photoswipe-gallery": "^2.2.7", "react-photoswipe-gallery": "^2.2.7",

View file

@ -58,9 +58,9 @@ i18n
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
resources: embeddedLocales, resources: embeddedLocales,
fallbackLng: 'en', fallbackLng: 'en-US',
supportedLngs: languages, supportedLngs: languages,
load: 'languageOnly', load: 'currentOnly',
interpolation: { interpolation: {
escapeValue: false, escapeValue: false,
format(value, format, language) { format(value, format, language) {
@ -80,7 +80,7 @@ i18n
}); });
i18n.loadCoreLocale = async (language = i18n.resolvedLanguage) => { i18n.loadCoreLocale = async (language = i18n.resolvedLanguage) => {
if (language === 'en') { if (language === i18n.options.fallbackLng[0]) {
return; return;
} }

View file

@ -1,7 +1,7 @@
import login from './login'; import login from './login';
export default { export default {
language: 'bg', language: 'bg-BG',
country: 'bg', country: 'bg',
name: 'Български', name: 'Български',
embeddedLocale: login, embeddedLocale: login,

View file

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

View file

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

View file

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

View file

@ -4,8 +4,8 @@ import login from './login';
import core from './core'; import core from './core';
export default { export default {
language: 'en', language: 'en-US',
country: 'gb', country: 'us',
name: 'English', name: 'English',
embeddedLocale: merge(login, core), embeddedLocale: merge(login, core),
}; };

View file

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

View file

@ -1,7 +1,7 @@
import login from './login'; import login from './login';
export default { export default {
language: 'fa', language: 'fa-IR',
country: 'ir', country: 'ir',
name: 'فارسی', name: 'فارسی',
embeddedLocale: login, embeddedLocale: login,

View file

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

View file

@ -1,7 +1,7 @@
import login from './login'; import login from './login';
export default { export default {
language: 'hu', language: 'hu-HU',
country: 'hu', country: 'hu',
name: 'Magyar', name: 'Magyar',
embeddedLocale: login, embeddedLocale: login,

View file

@ -1,7 +1,7 @@
import login from './login'; import login from './login';
export default { export default {
language: 'id', language: 'id-ID',
country: 'id', country: 'id',
name: 'Bahasa Indonesia', name: 'Bahasa Indonesia',
embeddedLocale: login, embeddedLocale: login,

View file

@ -1,53 +1,53 @@
import bg from './bg'; import bgBG from './bg-BG';
import cs from './cs'; import csCZ from './cs-CZ';
import da from './da'; import daDK from './da-DK';
import de from './de'; import deDE from './de-DE';
import en from './en'; import enUS from './en-US';
import es from './es'; import esES from './es-ES';
import fa from './fa'; import faIR from './fa-IR';
import fr from './fr'; import frFR from './fr-FR';
import hu from './hu'; import huHU from './hu-HU';
import id from './id'; import idID from './id-ID';
import it from './it'; import itIT from './it-IT';
import ja from './ja'; import jaJP from './ja-JP';
import ko from './ko'; import koKR from './ko-KR';
import nl from './nl'; import nlNL from './nl-NL';
import pl from './pl'; import plPL from './pl-PL';
import pt from './pt'; import ptBR from './pt-BR';
import ro from './ro'; import roRO from './ro-RO';
import ru from './ru'; import ruRU from './ru-RU';
import sk from './sk'; import skSK from './sk-SK';
import sv from './sv'; import svSE from './sv-SE';
import tr from './tr'; import trTR from './tr-TR';
import ua from './ua'; import ukUA from './uk-UA';
import uz from './uz'; import uzUZ from './uz-UZ';
import zh from './zh'; import zhCN from './zh-CN';
const locales = [ const locales = [
bg, bgBG,
cs, csCZ,
da, daDK,
de, deDE,
en, enUS,
es, esES,
fa, faIR,
fr, frFR,
hu, huHU,
id, idID,
it, itIT,
ja, jaJP,
ko, koKR,
nl, nlNL,
pl, plPL,
pt, ptBR,
ro, roRO,
ru, ruRU,
sk, skSK,
sv, svSE,
tr, trTR,
ua, ukUA,
uz, uzUZ,
zh, zhCN,
]; ];
export default locales; export default locales;

View file

@ -1,4 +1,8 @@
import dateFns from 'date-fns/locale/it';
export default { export default {
dateFns,
format: { format: {
date: 'd/M/yyyy', date: 'd/M/yyyy',
time: 'p', time: 'p',

View file

@ -1,7 +1,7 @@
import login from './login'; import login from './login';
export default { export default {
language: 'it', language: 'it-IT',
country: 'it', country: 'it',
name: 'Italiano', name: 'Italiano',
embeddedLocale: login, embeddedLocale: login,

View file

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

View file

@ -1,7 +1,7 @@
import login from './login'; import login from './login';
export default { export default {
language: 'ko', language: 'ko-KR',
country: 'kr', country: 'kr',
name: '한국어', name: '한국어',
embeddedLocale: login, embeddedLocale: login,

View file

@ -1,7 +1,7 @@
import login from './login'; import login from './login';
export default { export default {
language: 'nl', language: 'nl-NL',
country: 'nl', country: 'nl',
name: 'Nederlands', name: 'Nederlands',
embeddedLocale: login, embeddedLocale: login,

View file

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

View file

@ -1,7 +1,7 @@
import login from './login'; import login from './login';
export default { export default {
language: 'pt', language: 'pt-BR',
country: 'br', country: 'br',
name: 'Português', name: 'Português',
embeddedLocale: login, embeddedLocale: login,

View file

@ -1,7 +1,7 @@
import login from './login'; import login from './login';
export default { export default {
language: 'ro', language: 'ro-RO',
country: 'ro', country: 'ro',
name: 'Română', name: 'Română',
embeddedLocale: login, embeddedLocale: login,

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import login from './login'; import login from './login';
export default { export default {
language: 'tr', language: 'tr-TR',
country: 'tr', country: 'tr',
name: 'Türkçe', name: 'Türkçe',
embeddedLocale: login, embeddedLocale: login,

View file

@ -1,4 +1,8 @@
import dateFns from 'date-fns/locale/uk';
export default { export default {
dateFns,
format: { format: {
date: 'd/M/yyyy', date: 'd/M/yyyy',
time: 'p', time: 'p',

View file

@ -1,7 +1,7 @@
import login from './login'; import login from './login';
export default { export default {
language: 'ua', language: 'uk-UA',
country: 'ua', country: 'ua',
name: 'Українська', name: 'Українська',
embeddedLocale: login, embeddedLocale: login,

View file

@ -1,4 +1,8 @@
import dateFns from 'date-fns/locale/uz';
export default { export default {
dateFns,
format: { format: {
date: 'M/d/yyyy', date: 'M/d/yyyy',
time: 'p', time: 'p',

View file

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

View file

@ -1,4 +1,8 @@
import dateFns from 'date-fns/locale/zh-CN';
export default { export default {
dateFns,
format: { format: {
date: 'M/d/yyyy', date: 'M/d/yyyy',
time: 'p', time: 'p',

View file

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

View file

@ -50,7 +50,7 @@ module.exports = {
}, },
language: { language: {
type: 'string', type: 'string',
isNotEmptyString: true, isIn: User.LANGUAGES,
allowNull: true, allowNull: true,
}, },
subscribeToOwnCards: { subscribeToOwnCards: {

View file

@ -36,7 +36,7 @@ module.exports = {
}, },
language: { language: {
type: 'string', type: 'string',
isNotEmptyString: true, isIn: User.LANGUAGES,
allowNull: true, allowNull: true,
}, },
subscribeToOwnCards: { subscribeToOwnCards: {

View file

@ -5,11 +5,39 @@
* @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models * @docs :: https://sailsjs.com/docs/concepts/models-and-orm/models
*/ */
const LANGUAGES = [
'bg-BG',
'cs-CZ',
'da-DK',
'de-DE',
'en-US',
'es-ES',
'fa-IR',
'fr-FR',
'hu-HU',
'id-ID',
'it-IT',
'ja-JP',
'ko-KR',
'nl-NL',
'pl-PL',
'pt-BR',
'ro-RO',
'ru-RU',
'sk-SK',
'sv-SE',
'tr-TR',
'uk-UA',
'uz-UZ',
'zh-CN',
];
const OIDC = { const OIDC = {
id: '_oidc', id: '_oidc',
}; };
module.exports = { module.exports = {
LANGUAGES,
OIDC, OIDC,
attributes: { attributes: {
@ -62,7 +90,7 @@ module.exports = {
}, },
language: { language: {
type: 'string', type: 'string',
isNotEmptyString: true, isIn: LANGUAGES,
allowNull: true, allowNull: true,
}, },
subscribeToOwnCards: { subscribeToOwnCards: {

View file

@ -15,7 +15,7 @@ module.exports.up = async (knex) => {
const attachments = await knex('attachment'); const attachments = await knex('attachment');
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (attachment of attachments) { for (const attachment of attachments) {
if (attachment.is_image) { if (attachment.is_image) {
const image = sharp( const image = sharp(
path.join(config.custom.attachmentsPath, attachment.dirname, attachment.filename), path.join(config.custom.attachmentsPath, attachment.dirname, attachment.filename),
@ -54,7 +54,7 @@ module.exports.down = async (knex) => {
const attachments = await knex('attachment'); const attachments = await knex('attachment');
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (attachment of attachments) { for (const attachment of attachments) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await knex('attachment') await knex('attachment')
.update({ .update({

View file

@ -93,7 +93,7 @@ module.exports.up = async (knex) => {
const attachments = await knex('attachment').whereNotNull('image'); const attachments = await knex('attachment').whereNotNull('image');
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (attachment of attachments) { for (const attachment of attachments) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const image = await processAttachmentImage(attachment, config.custom.attachmentsPath); const image = await processAttachmentImage(attachment, config.custom.attachmentsPath);

View file

@ -113,7 +113,7 @@ module.exports.up = async (knex) => {
const users = await knex('user_account').whereNotNull('avatar'); const users = await knex('user_account').whereNotNull('avatar');
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (user of users) { for (const user of users) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await processUserAvatar(user, config.custom.userAvatarsPath); await processUserAvatar(user, config.custom.userAvatarsPath);
} }
@ -121,7 +121,7 @@ module.exports.up = async (knex) => {
const projects = await knex('project').whereNotNull('background_image'); const projects = await knex('project').whereNotNull('background_image');
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (project of projects) { for (const project of projects) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await processProjectBackgroundImage(project, config.custom.projectBackgroundImagesPath); await processProjectBackgroundImage(project, config.custom.projectBackgroundImagesPath);
} }
@ -129,7 +129,7 @@ module.exports.up = async (knex) => {
const attachments = await knex('attachment').whereNotNull('image'); const attachments = await knex('attachment').whereNotNull('image');
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (attachment of attachments) { for (const attachment of attachments) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await processAttachmentImage(attachment, config.custom.attachmentsPath); await processAttachmentImage(attachment, config.custom.attachmentsPath);
} }

View file

@ -0,0 +1,68 @@
const _ = require('lodash');
const LANGUAGES = [
'bg-BG',
'cs-CZ',
'da-DK',
'de-DE',
'en-US',
'es-ES',
'fa-IR',
'fr-FR',
'hu-HU',
'id-ID',
'it-IT',
'ja-JP',
'ko-KR',
'nl-NL',
'pl-PL',
'pt-BR',
'ro-RO',
'ru-RU',
'sk-SK',
'sv-SE',
'tr-TR',
'uz-UZ',
'zh-CN',
];
const LANGUAGE_BY_PREV_LANGUAGE = LANGUAGES.reduce(
(result, language) => ({
...result,
[language.split('-')[0]]: language,
}),
{},
);
LANGUAGE_BY_PREV_LANGUAGE.ua = 'uk-UA';
const PREV_LANGUAGE_BY_LANGUAGE = _.invert(LANGUAGE_BY_PREV_LANGUAGE);
module.exports.up = async (knex) => {
const users = await knex('user_account').whereNotNull('language');
const prevLanguages = [...new Set(users.map((user) => user.language))];
// eslint-disable-next-line no-restricted-syntax
for (const prevLanguage of prevLanguages) {
// eslint-disable-next-line no-await-in-loop
await knex('user_account')
.update({
language: LANGUAGE_BY_PREV_LANGUAGE[prevLanguage],
})
.where('language', prevLanguage);
}
};
module.exports.down = async (knex) => {
const users = await knex('user_account').whereNotNull('language');
const languages = [...new Set(users.map((user) => user.language))];
// eslint-disable-next-line no-restricted-syntax
for (const language of languages) {
// eslint-disable-next-line no-await-in-loop
await knex('user_account')
.update({
language: PREV_LANGUAGE_BY_LANGUAGE[language],
})
.where('language', language);
}
};