Merge branch 'i18n'

This commit is contained in:
ErickSkrauch 2021-03-28 03:02:35 +02:00
commit 3c897845ab
No known key found for this signature in database
GPG Key ID: 669339FCBB30EE0E
55 changed files with 747 additions and 199 deletions

View File

@ -16,6 +16,6 @@ module.exports = {
filePath: 'accounts/site.json', filePath: 'accounts/site.json',
sourceLang: 'en', sourceLang: 'en',
basePath: `${__dirname}/packages/app/i18n`, basePath: `${__dirname}/packages/app/i18n`,
minApproved: 80, // Minimal ready percent before translation can be published minTranslated: 80, // Minimal ready percent before translation can be published
}, },
}; };

View File

@ -45,7 +45,8 @@
"build:dll": "babel-node --extensions '.ts,.d.ts' ./packages/scripts/build-dll.ts", "build:dll": "babel-node --extensions '.ts,.d.ts' ./packages/scripts/build-dll.ts",
"build:serve": "http-server --proxy https://dev.account.ely.by ./build", "build:serve": "http-server --proxy https://dev.account.ely.by ./build",
"sb": "APP_ENV=storybook start-storybook -p 9009 --ci", "sb": "APP_ENV=storybook start-storybook -p 9009 --ci",
"sb:build": "APP_ENV=storybook build-storybook" "sb:build": "APP_ENV=storybook build-storybook",
"postinstall": "node -e \"try{require('./postinstall.js')}catch(e){}\""
}, },
"lint-staged": { "lint-staged": {
"*.{json,scss,css,md}": [ "*.{json,scss,css,md}": [
@ -144,6 +145,7 @@
"loader-utils": "^2.0.0", "loader-utils": "^2.0.0",
"mini-css-extract-plugin": "^0.9.0", "mini-css-extract-plugin": "^0.9.0",
"node-sass": "^4.14.1", "node-sass": "^4.14.1",
"postcss-bidirection": "https://github.com/erickskrauch/postcss-bidirection.git#iss_23",
"postcss-import": "^12.0.1", "postcss-import": "^12.0.1",
"postcss-loader": "^3.0.0", "postcss-loader": "^3.0.0",
"postcss-scss": "^2.1.1", "postcss-scss": "^2.1.1",

View File

@ -119,6 +119,7 @@ interface State {
formsHeights: Record<PanelId, number>; formsHeights: Record<PanelId, number>;
} }
// TODO: completely broken for RTL languages
class PanelTransition extends React.PureComponent<Props, State> { class PanelTransition extends React.PureComponent<Props, State> {
state: State = { state: State = {
contextHeight: 0, contextHeight: 0,

View File

@ -16,7 +16,6 @@
display: block; display: block;
position: absolute; position: absolute;
left: 0;
bottom: 0; bottom: 0;
height: 3px; height: 3px;
width: 40px; width: 40px;

View File

@ -3,7 +3,7 @@
.accountSwitcher { .accountSwitcher {
background: $black; background: $black;
text-align: left; text-align: start;
} }
$border: 1px solid lighter($black); $border: 1px solid lighter($black);
@ -37,12 +37,12 @@ $border: 1px solid lighter($black);
.accountAvatar { .accountAvatar {
font-size: 35px; font-size: 35px;
margin-right: 15px; margin-inline-end: 15px;
} }
.accountInfo { .accountInfo {
flex-grow: 1; flex-grow: 1;
margin-right: 15px; margin-inline-end: 15px;
min-width: 0; // Fix for text-overflow. See https://stackoverflow.com/a/40612184 min-width: 0; // Fix for text-overflow. See https://stackoverflow.com/a/40612184
} }
@ -76,17 +76,25 @@ $border: 1px solid lighter($black);
composes: arrowRight from '~app/components/ui/icons.scss'; composes: arrowRight from '~app/components/ui/icons.scss';
position: relative; position: relative;
left: 0; inset-inline-start: 0;
font-size: 24px; font-size: 24px;
color: #4e4e4e; color: #4e4e4e;
line-height: 35px; line-height: 35px;
transition: color 0.25s, inset-inline-start 0.5s;
// TODO: right now transition property doesn't support the bidirectional value.
// See https://github.com/gasolin/postcss-bidirection/issues/25.
// noinspection CssOverwrittenProperties Graceful degradation
transition: color 0.25s, left 0.5s; transition: color 0.25s, left 0.5s;
html[dir='rtl'] & {
transition: color 0.25s, right 0.5s;
}
.item:hover & { .item:hover & {
color: #aaa; color: #aaa;
left: 5px; inset-inline-start: 5px;
} }
} }

View File

@ -16,7 +16,7 @@
position: relative; position: relative;
bottom: 1px; bottom: 1px;
padding-left: 3px; padding-inline-start: 3px;
color: #666666; color: #666666;
font-size: 10px; font-size: 10px;

View File

@ -4,18 +4,18 @@
.authInfo { .authInfo {
// Отступы сверху и снизу разные т.к. мы ужимаем высоту линии строки с логином на 2 пикселя и из-за этого теряем отступ снизу // Отступы сверху и снизу разные т.к. мы ужимаем высоту линии строки с логином на 2 пикселя и из-за этого теряем отступ снизу
padding: 5px 20px 7px; padding: 5px 20px 7px;
text-align: left; text-align: start;
} }
.authInfoAvatar { .authInfoAvatar {
$size: 30px; $size: 30px;
float: left; float: start;
height: $size; height: $size;
width: $size; width: $size;
font-size: $size; font-size: $size;
line-height: 1; line-height: 1;
margin-right: 10px; margin-inline-end: 10px;
margin-top: 2px; margin-top: 2px;
color: #aaa; color: #aaa;
@ -38,7 +38,7 @@
.permissionsContainer { .permissionsContainer {
padding: 15px 12px; padding: 15px 12px;
text-align: left; text-align: start;
} }
.permissionsTitle { .permissionsTitle {
@ -57,7 +57,7 @@
font-size: 14px; font-size: 14px;
line-height: 1.4; line-height: 1.4;
padding-bottom: 4px; padding-bottom: 4px;
padding-left: 17px; padding-inline-start: 17px;
position: relative; position: relative;
&:last-of-type { &:last-of-type {
@ -71,7 +71,7 @@
line-height: 9px; line-height: 9px;
position: absolute; position: absolute;
top: 6px; top: 6px;
left: -4px; inset-inline-start: -4px;
} }
} }
} }

View File

@ -37,7 +37,7 @@
width: 50%; width: 50%;
&:first-of-type { &:first-of-type {
margin-right: $popupPadding; margin-inline-end: $popupPadding;
} }
} }

View File

@ -160,7 +160,7 @@
composes: arrowRight from '~app/components/ui/icons.scss'; composes: arrowRight from '~app/components/ui/icons.scss';
position: relative; position: relative;
left: 0; inset-inline-start: 0;
font-size: 28px; font-size: 28px;
color: #ebe8e1; color: #ebe8e1;
@ -174,6 +174,10 @@
.appExpanded & { .appExpanded & {
color: #777; color: #777;
transform: rotate(360deg)!important; // Prevent it from hover rotating transform: rotate(360deg)!important; // Prevent it from hover rotating
html[dir='rtl'] & {
transform: rotate(0)!important;
}
} }
} }
@ -195,7 +199,7 @@ $appDetailsContainerRightLeftPadding: 30px;
.editAppLink { .editAppLink {
position: absolute; position: absolute;
top: 4px; top: 4px;
right: 0; inset-inline-end: 0;
font-size: 12px; font-size: 12px;
color: #9a9a9a; color: #9a9a9a;
@ -221,17 +225,19 @@ $appDetailsContainerRightLeftPadding: 30px;
} }
.appActionButton { .appActionButton {
margin: 0 10px 10px 0; margin-inline-end: 10px;
margin-bottom: 10px;
&:last-of-type { &:last-of-type {
margin-right: 0; margin-inline-end: 0;
} }
} }
.appActionContainer { .appActionContainer {
position: absolute; position: absolute;
width: calc(100% - #{$appDetailsContainerRightLeftPadding * 2});
top: 100%; top: 100%;
left: 0; inset-inline-start: 0;
padding: 0 $appDetailsContainerRightLeftPadding; padding: 0 $appDetailsContainerRightLeftPadding;
background: #f5f5f5; background: #f5f5f5;
} }
@ -244,7 +250,7 @@ $appDetailsContainerRightLeftPadding: 30px;
.continueActionButtonWrapper { .continueActionButtonWrapper {
display: inline-block; display: inline-block;
margin-left: 10px; margin-inline-start: 10px;
} }
.continueActionLink { .continueActionLink {

View File

@ -24,5 +24,5 @@
position: relative; position: relative;
bottom: 1px; bottom: 1px;
font-size: 11px; font-size: 11px;
margin-right: 3px; margin-inline-end: 3px;
} }

View File

@ -1,5 +1,8 @@
import React, { useState, useEffect, ComponentType } from 'react'; import React, { useState, useEffect, ComponentType } from 'react';
import { RawIntlProvider, IntlShape } from 'react-intl'; import { RawIntlProvider, IntlShape } from 'react-intl';
import { Helmet } from 'react-helmet-async';
import { getLangDir } from 'rtl-detect';
import i18n from 'app/services/i18n'; import i18n from 'app/services/i18n';
import { useReduxSelector } from 'app/functions'; import { useReduxSelector } from 'app/functions';
@ -26,7 +29,12 @@ const IntlProvider: ComponentType = ({ children }) => {
return null; return null;
} }
return <RawIntlProvider value={intl}>{children}</RawIntlProvider>; return (
<RawIntlProvider value={intl}>
<Helmet htmlAttributes={{ lang: locale, dir: getLangDir(locale) }} />
{children}
</RawIntlProvider>
);
}; };
export default IntlProvider; export default IntlProvider;

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="180" viewBox="0 0 63.5 47.625">
<path fill="#000" d="M0 0h63.5v15.875H0z"/>
<path fill="#00783b" d="M0 15.903h63.5v15.875H0z"/>
<path fill="#fff" d="M0 31.75h63.5v15.875H0z"/>
<path fill="#cc1025" d="m0 0 21.167 23.813A16486.171 16486.171 0 0 1 0 47.624V0z"/>
</svg>

After

Width:  |  Height:  |  Size: 343 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480">
<path d="M0 0h204.8v480H0z"/>
<path fill="#D21" d="M435.2 0H640v480H435.2z"/>
<path fill="#FFF" d="M204.8 0h230.4v480H204.8z"/>
<path fill="#D21" d="M286.667 140L420 273.333H220L353.333 140v200L220 206.667h200L286.667 340V140z"/>
</svg>

After

Width:  |  Height:  |  Size: 316 B

View File

@ -10,6 +10,8 @@ const localeToCountryCode: Record<string, string> = {
sr: 'rs', sr: 'rs',
zh: 'cn', zh: 'cn',
cs: 'cz', cs: 'cz',
fil: 'ph',
he: 'il',
}; };
const SUPPORTED_LANGUAGES: ReadonlyArray<string> = Object.keys(supportedLocales); const SUPPORTED_LANGUAGES: ReadonlyArray<string> = Object.keys(supportedLocales);

View File

@ -26,7 +26,7 @@
position: relative; position: relative;
top: 1px; top: 1px;
display: inline-block; display: inline-block;
margin-right: 4px; margin-inline-end: 4px;
height: $height; height: $height;
width: $height * 4 / 3; width: $height * 4 / 3;
box-shadow: 0 0 1px rgba(#000, 0.2); box-shadow: 0 0 1px rgba(#000, 0.2);

View File

@ -33,7 +33,7 @@
position: absolute; position: absolute;
top: 14px; top: 14px;
right: 12px; inset-inline-end: 12px;
font-size: 22px; font-size: 22px;
color: #edebe5; color: #edebe5;
pointer-events: none; // Иконка чисто декоративная, так что клик должен проходить сквозь неё pointer-events: none; // Иконка чисто декоративная, так что клик должен проходить сквозь неё
@ -94,7 +94,7 @@ $languageListBorderStyle: 1px solid $languageListBorderColor;
.languageIco { .languageIco {
display: inline-block; display: inline-block;
margin-right: 7px; margin-inline-end: 7px;
width: 40px; width: 40px;
height: 30px; height: 30px;
box-shadow: 0 0 1px rgba(#000, 0.2); box-shadow: 0 0 1px rgba(#000, 0.2);
@ -129,7 +129,7 @@ $languageListBorderStyle: 1px solid $languageListBorderColor;
box-sizing: border-box; box-sizing: border-box;
width: 22px; width: 22px;
height: 22px; height: 22px;
right: -32px; inset-inline-end: -32px;
font-size: 10px; font-size: 10px;
line-height: 18px; line-height: 18px;
@ -146,13 +146,13 @@ $languageListBorderStyle: 1px solid $languageListBorderColor;
} }
.languageItem:hover & { .languageItem:hover & {
right: 0; inset-inline-end: 0;
} }
.activeLanguageItem & { .activeLanguageItem & {
border-color: $green; border-color: $green;
background: $green; background: $green;
right: 0; inset-inline-end: 0;
&:before { &:before {
opacity: 1; opacity: 1;
@ -209,7 +209,7 @@ $languageListBorderStyle: 1px solid $languageListBorderColor;
color: lighter($blue); color: lighter($blue);
font-size: 22px; font-size: 22px;
margin-right: 10px; margin-inline-end: 10px;
} }
.improveTranslatesContent { .improveTranslatesContent {

View File

@ -19,12 +19,12 @@
li { li {
position: relative; position: relative;
padding-left: 11px; padding-inline-start: 11px;
&:before { &:before {
content: ''; content: '';
position: absolute; position: absolute;
left: 0; inset-inline-start: 0;
top: 2px; top: 2px;
} }

View File

@ -75,7 +75,7 @@ export default function OsInstruction({ os }: { os: OS }) {
<ul className={styles.appList}> <ul className={styles.appList}>
{linksByOs[os].featured.map((item) => ( {linksByOs[os].featured.map((item) => (
<li key={item.label}> <li key={item.label}>
<a href={item.link} target="_blank"> <a href={item.link} target="_blank" dir="ltr">
{item.label} {item.label}
</a> </a>
</li> </li>

View File

@ -22,7 +22,7 @@
.otherApps { .otherApps {
position: absolute; position: absolute;
right: 0; inset-inline-end: 0;
bottom: 5px; bottom: 5px;
font-size: 10px; font-size: 10px;
@ -96,7 +96,7 @@
.androidActive & { .androidActive & {
transform: translateX(0); transform: translateX(0);
left: 0; inset-inline-start: 0;
} }
.appleActive &, .appleActive &,
@ -111,21 +111,33 @@
$translateX: -51%; $translateX: -51%;
transform: translateX($translateX) scale(1); transform: translateX($translateX) scale(1);
left: 49%; inset-inline-start: 49%;
html[dir='rtl'] & {
transform: translateX(-$translateX) scale(1);
}
&:hover { &:hover {
transform: translateX($translateX) scale(1.1); transform: translateX($translateX) scale(1.1);
html[dir='rtl'] & {
transform: translateX(-$translateX) scale(1.1);
}
} }
.appleActive & { .appleActive & {
transform: translateX(0); transform: translateX(0)!important; // override dir='rtl'
left: 0; inset-inline-start: 0;
} }
.androidActive &, .androidActive &,
.windowsActive & { .windowsActive & {
transform: translateX($translateX) scale(0); transform: translateX($translateX) scale(0);
opacity: 0; opacity: 0;
html[dir='rtl'] & {
transform: translateX(-$translateX) scale(0);
}
} }
} }
@ -134,21 +146,33 @@
$translateX: -100%; $translateX: -100%;
transform: translateX($translateX) scale(1); transform: translateX($translateX) scale(1);
left: 100%; inset-inline-start: 100%;
html[dir='rtl'] & {
transform: translateX(-$translateX) scale(1);
}
&:hover { &:hover {
transform: translateX($translateX) scale(1.1); transform: translateX($translateX) scale(1.1);
html[dir='rtl'] & {
transform: translateX(-$translateX) scale(1.1);
}
} }
.windowsActive & { .windowsActive & {
transform: translateX(0); transform: translateX(0)!important; // override dir='rtl'
left: 0; inset-inline-start: 0;
} }
.appleActive &, .appleActive &,
.androidActive & { .androidActive & {
transform: translateX($translateX) scale(0); transform: translateX($translateX) scale(0);
opacity: 0; opacity: 0;
html[dir='rtl'] & {
transform: translateX(-$translateX) scale(0);
}
} }
} }
@ -166,8 +190,8 @@
position: relative; position: relative;
z-index: 1; z-index: 1;
margin: 15px; margin: 15px;
margin-left: 30%; margin-inline-start: 30%;
padding-left: 15px; padding-inline-start: 15px;
padding-bottom: 15px; padding-bottom: 15px;
min-height: $boxHeight; min-height: $boxHeight;
} }
@ -212,7 +236,7 @@
.osName { .osName {
font-family: $font-family-title; font-family: $font-family-title;
font-size: 16px; font-size: 16px;
margin-left: 10px; margin-inline-start: 10px;
} }
@mixin commonNonActiveTile() { @mixin commonNonActiveTile() {

View File

@ -16,8 +16,8 @@ $maxQrCodeSize: 242px;
} }
.or { .or {
position: absolute; position: absolute; // Use absolute positioning to put an element between margin
left: 0; inset-inline-start: 0; // TODO: doesn't have any effect. Probably can be removed
width: 100%; width: 100%;
margin-top: -18px; margin-top: -18px;

View File

@ -13,7 +13,8 @@ $formColumnWidth: 416px;
} }
.descriptionColumn { .descriptionColumn {
padding: 12px 20px 0 0; padding-top: 12px;
padding-inline-end: 20px;
box-sizing: border-box; box-sizing: border-box;
} }
@ -97,7 +98,8 @@ $formColumnWidth: 416px;
} }
.paramMessage { .paramMessage {
padding: 10px 40px 0 0; padding-top: 10px;
padding-inline-end: 40px;
color: $red; color: $red;
font-size: 11px; font-size: 11px;
@ -168,7 +170,7 @@ $formColumnWidth: 416px;
} }
.paramEditIcon { .paramEditIcon {
margin-left: 5px; margin-inline-start: 5px;
vertical-align: top; vertical-align: top;
} }

View File

@ -9,7 +9,7 @@
.backButton { .backButton {
position: absolute; position: absolute;
left: -60px; inset-inline-start: -60px;
top: 15px; top: 15px;
width: 25px; width: 25px;
height: 25px; height: 25px;
@ -60,7 +60,6 @@
display: block; display: block;
position: absolute; position: absolute;
left: 0;
bottom: 0; bottom: 0;
height: 3px; height: 3px;
width: 86px; width: 86px;
@ -110,7 +109,7 @@
.backButton { .backButton {
top: 29px; top: 29px;
left: 27px; inset-inline-start: 27px;
padding: 0; padding: 0;
margin: 0; margin: 0;
width: auto; width: auto;

View File

@ -29,7 +29,8 @@
} }
.iconWrapper { .iconWrapper {
margin: 4px 12px 0 0; margin-top: 4px;
margin-inline-end: 12px;
} }
.dbIcon { .dbIcon {
@ -68,14 +69,15 @@
font-family: $font-family-title; font-family: $font-family-title;
font-size: 14px; font-size: 14px;
color: #9a9a9a; color: #9a9a9a;
margin: 0 0 5px 1px; // Add 1px from the left to make icon look visually aligned with texts margin-bottom: 5px;
margin-inline-start: 1px; // Add 1px from the left to make icon look visually aligned with texts
} }
.githubIcon { .githubIcon {
composes: github from '~app/components/ui/icons.scss'; composes: github from '~app/components/ui/icons.scss';
font-size: 12px; font-size: 12px;
margin-right: 2px; margin-inline-end: 2px;
position: relative; position: relative;
bottom: 1px; bottom: 1px;
} }

View File

@ -1,20 +0,0 @@
.horizontalGroup {
display: flex;
}
.item {
// TODO: in some cases we do not need overflow hidden
// probably, it is better to create a separate class for children, that will
// enable overflow hidden and ellipsis
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
$borderConfig: 1px solid rgba(#fff, 0.15);
border-left: $borderConfig;
&:last-child {
border-right: $borderConfig;
}
}

View File

@ -1,31 +1,31 @@
import React from 'react'; import React, { ReactNode } from 'react';
import { MessageDescriptor } from 'react-intl'; import { MessageDescriptor } from 'react-intl';
import i18n from 'app/services/i18n'; import i18n from 'app/services/i18n';
function isMessageDescriptor(value: any): value is MessageDescriptor {
return typeof value === 'object' && value.id;
}
export default class FormComponent<P, S = {}> extends React.Component<P, S> { export default class FormComponent<P, S = {}> extends React.Component<P, S> {
/** /**
* Formats message resolving intl translations * Formats message resolving intl translations
* *
* @param {string|object} message - message string, or intl message descriptor with an `id` field * @param {string|object} message - message string, or intl message descriptor with an `id` field
* *
* @deprecated
* @returns {string} * @returns {string}
*/ */
formatMessage(message: string | MessageDescriptor): string { formatMessage<T extends ReactNode | MessageDescriptor>(message: T): T extends MessageDescriptor ? string : T {
if (!message) { if (isMessageDescriptor(message)) {
throw new Error('A message is required'); // @ts-ignore
}
if (typeof message === 'string') {
return message;
}
if (!message.id) {
throw new Error(`Invalid message format: ${JSON.stringify(message)}`);
}
return i18n.getIntl().formatMessage(message); return i18n.getIntl().formatMessage(message);
} }
// @ts-ignore
return message;
}
/** /**
* Focuses this field * Focuses this field
*/ */

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { ReactNode } from 'react';
import { MessageDescriptor } from 'react-intl'; import { MessageDescriptor } from 'react-intl';
import clsx from 'clsx'; import clsx from 'clsx';
import { uniqueId, omit } from 'app/functions'; import { uniqueId, omit } from 'app/functions';
@ -17,7 +17,7 @@ export default class Input extends FormInputComponent<
color: Color; color: Color;
center: boolean; center: boolean;
disabled: boolean; disabled: boolean;
label?: string | MessageDescriptor; label?: ReactNode | MessageDescriptor;
placeholder?: string | MessageDescriptor; placeholder?: string | MessageDescriptor;
icon?: string; icon?: string;
copy?: boolean; copy?: boolean;

View File

@ -25,8 +25,9 @@ $dropdownPadding: 15px;
display: inline-block; display: inline-block;
box-sizing: border-box; box-sizing: border-box;
height: 50px; height: 50px;
padding-inline-start: $dropdownPadding;
// 28px - ширина иконки при заданном размере шрифта // 28px - ширина иконки при заданном размере шрифта
padding: 0 ($dropdownPadding * 2 + 28px) 0 $dropdownPadding; padding-inline-end: $dropdownPadding * 2 + 28px;
position: relative; position: relative;
font-family: $font-family-title; font-family: $font-family-title;
@ -48,6 +49,7 @@ $dropdownPadding: 15px;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
text-align: start;
} }
.opened { .opened {
@ -57,18 +59,26 @@ $dropdownPadding: 15px;
composes: selecter from '~app/components/ui/icons.scss'; composes: selecter from '~app/components/ui/icons.scss';
position: absolute; position: absolute;
right: $dropdownPadding; inset-inline-end: $dropdownPadding;
top: 16px; top: 16px;
font-size: 17px; font-size: 17px;
transition: right 0.3s cubic-bezier(0.23, 1, 0.32, 1); // easeOutQuint transition: inset-inline-end 0.3s cubic-bezier(0.23, 1, 0.32, 1); // easeOutQuint
// TODO: right now transition property doesn't support the bidirectional value.
// See https://github.com/gasolin/postcss-bidirection/issues/25.
// noinspection CssOverwrittenProperties Graceful degradation
transition: right 0.3s cubic-bezier(0.23, 1, 0.32, 1);
html[dir='rtl'] & {
transition: left 0.3s cubic-bezier(0.23, 1, 0.32, 1);
}
.dropdown:hover & { .dropdown:hover & {
right: $dropdownPadding - 5px; inset-inline-end: $dropdownPadding - 5px;
} }
.dropdown:active &, .dropdown:active &,
.dropdown.opened & { .dropdown.opened & {
right: $dropdownPadding + 5px; inset-inline-end: $dropdownPadding + 5px;
} }
} }
@ -84,7 +94,7 @@ $dropdownPadding: 15px;
.menu { .menu {
position: absolute; position: absolute;
top: 60px; top: 60px;
left: 0; inset-inline-start: 0;
z-index: 10; z-index: 10;
width: 120%; width: 120%;

View File

@ -36,7 +36,7 @@
composes: formRow; composes: formRow;
.textField { .textField {
padding-left: 60px; padding-inline-start: 60px;
} }
} }
@ -80,7 +80,7 @@
.textFieldIcon { .textFieldIcon {
box-sizing: border-box; box-sizing: border-box;
position: absolute; position: absolute;
left: 0; inset-inline-start: 0;
top: 0; top: 0;
height: 50px; height: 50px;
width: 50px; width: 50px;
@ -95,7 +95,7 @@
.copyIcon { .copyIcon {
position: absolute; position: absolute;
right: 5px; inset-inline-end: 5px;
top: 10px; top: 10px;
padding: 5px; padding: 5px;
@ -175,6 +175,7 @@
font-family: $font-family-title; font-family: $font-family-title;
color: #666; color: #666;
font-size: 18px; font-size: 18px;
text-align: start;
} }
.fieldError { .fieldError {
@ -248,7 +249,7 @@
.markableContainer { .markableContainer {
display: inline-block; display: inline-block;
position: relative; position: relative;
padding-left: 27px; padding-inline-start: 27px;
font-family: $font-family-title; font-family: $font-family-title;
font-size: 16px; font-size: 16px;
@ -260,7 +261,7 @@
.markPosition { .markPosition {
position: absolute; position: absolute;
box-sizing: border-box; box-sizing: border-box;
left: 0; inset-inline-start: 0;
top: 0; top: 0;
margin: 0; margin: 0;

View File

@ -15,10 +15,30 @@
@extend .arrow; @extend .arrow;
transform: rotate(270deg); transform: rotate(270deg);
html[dir='rtl'] & {
transform: rotate(90deg);
}
} }
.arrowLeft { .arrowLeft {
@extend .arrow; @extend .arrow;
transform: rotate(90deg); transform: rotate(90deg);
html[dir='rtl'] & {
transform: rotate(270deg);
}
}
html[dir='rtl'] {
.brush,
.exit,
.key,
.pencil,
.search {
&:before {
transform: scaleX(-1);
}
}
} }

View File

@ -1,4 +1,6 @@
.loader-overlay { .loader-overlay {
direction: ltr;
background: rgba(255, 255, 255, 0.3); background: rgba(255, 255, 255, 0.3);
position: fixed; position: fixed;
top: 0; top: 0;

View File

@ -77,8 +77,8 @@ class SlideMotion extends React.PureComponent<Props, State> {
<div <div
className={styles.container} className={styles.container}
style={{ style={{
WebkitTransform: `translateX(-${interpolatingStyle.transform}%)`, // @ts-ignore see https://stackoverflow.com/a/52013197/5184751
transform: `translateX(-${interpolatingStyle.transform}%)`, '--transition-progress': `${interpolatingStyle.transform}%`,
}} }}
> >
{React.Children.map(children, (child, index) => ( {React.Children.map(children, (child, index) => (

View File

@ -1,5 +1,11 @@
.container { .container {
white-space: nowrap; white-space: nowrap;
transform: translate(var(--transition-progress));
html[dir='ltr'] & {
// noinspection CssInvalidFunction works fine in a browser (:
transform: translate(calc(var(--transition-progress) * -1));
}
} }
.item { .item {

View File

@ -25,12 +25,12 @@ $headerHeight: 60px;
.headerControl { .headerControl {
composes: black from '~app/components/ui/buttons.scss'; composes: black from '~app/components/ui/buttons.scss';
float: left; float: start;
overflow: hidden; overflow: hidden;
height: $headerHeight - 1px; height: $headerHeight - 1px;
width: 49px; width: 49px;
padding: 0; padding: 0;
border-right: 1px solid lighter($black); border-inline-end: 1px solid lighter($black);
line-height: 1; line-height: 1;
text-align: center; text-align: center;
@ -122,7 +122,7 @@ $bodyTopBottomPadding: 15px;
composes: close from '~app/components/ui/icons.scss'; composes: close from '~app/components/ui/icons.scss';
position: absolute; position: absolute;
right: 5px; inset-inline-end: 5px;
top: 5px; top: 5px;
font-size: 10px; font-size: 10px;

View File

@ -59,7 +59,7 @@ $popupMargin: 20px; // Outer popup margins
.popup { .popup {
white-space: normal; white-space: normal;
text-align: left; text-align: start;
background: #fff; background: #fff;
box-shadow: 0 0 10px rgba(#000, 0.2); box-shadow: 0 0 10px rgba(#000, 0.2);
@ -95,7 +95,7 @@ $popupMargin: 20px; // Outer popup margins
composes: close from '~app/components/ui/icons.scss'; composes: close from '~app/components/ui/icons.scss';
position: absolute; position: absolute;
right: 0; inset-inline-end: 0;
top: 0; top: 0;
padding: 15px; padding: 15px;
cursor: pointer; cursor: pointer;
@ -122,6 +122,10 @@ $popupMargin: 20px; // Outer popup margins
opacity: 0; opacity: 0;
transform: translate(100%); transform: translate(100%);
transition: 0s; transition: 0s;
html[dir='rtl'] & {
transform: translate(-100%);
}
} }
} }

View File

@ -47,7 +47,7 @@
position: absolute; position: absolute;
top: 0.16em; top: 0.16em;
left: -0.145em; inset-inline-start: -0.145em;
font-size: 0.7em; font-size: 0.7em;
color: rgba($red, 0.75); color: rgba($red, 0.75);
} }

View File

@ -8,7 +8,7 @@
.step { .step {
position: relative; position: relative;
text-align: right; text-align: end;
width: 100%; width: 100%;
height: 4px; height: 4px;
@ -24,8 +24,8 @@
position: absolute; position: absolute;
height: 4px; height: 4px;
left: 0; inset-inline-start: 0;
right: 100%; inset-inline-end: 100%;
top: 50%; top: 50%;
margin-top: -2px; margin-top: -2px;
@ -53,7 +53,7 @@
.activeStep { .activeStep {
&:before { &:before {
right: 0; inset-inline-end: 0;
transition-delay: unset; transition-delay: unset;
} }

View File

@ -47,7 +47,7 @@ const AccountSwitcher: ComponentType<Props> = ({
<div className={clsx(styles.accountSwitcher)} data-testid="account-switcher"> <div className={clsx(styles.accountSwitcher)} data-testid="account-switcher">
<div className={styles.item} data-testid="active-account"> <div className={styles.item} data-testid="active-account">
<PseudoAvatar className={styles.activeAccountIcon} /> <PseudoAvatar className={styles.activeAccountIcon} />
<div className={styles.activeAccountInfo}> <div>
<div className={styles.activeAccountUsername}>{activeAccount.username}</div> <div className={styles.activeAccountUsername}>{activeAccount.username}</div>
<div className={clsx(styles.accountEmail, styles.activeAccountEmail)}>{activeAccount.email}</div> <div className={clsx(styles.accountEmail, styles.activeAccountEmail)}>{activeAccount.email}</div>
<div className={styles.links}> <div className={styles.links}>
@ -81,16 +81,16 @@ const AccountSwitcher: ComponentType<Props> = ({
> >
<PseudoAvatar index={index + 1} deleted={account.isDeleted} className={styles.accountIcon} /> <PseudoAvatar index={index + 1} deleted={account.isDeleted} className={styles.accountIcon} />
<div className={styles.accountInfo}>
<div className={styles.accountUsername}>{account.username}</div>
<div className={styles.accountEmail}>{account.email}</div>
</div>
<div <div
className={styles.logoutIcon} className={styles.logoutIcon}
data-testid="logout-account" data-testid="logout-account"
onClick={onAccountRemoveCallback(account)} onClick={onAccountRemoveCallback(account)}
/> />
<div className={styles.accountInfo}>
<div className={styles.accountUsername}>{account.username}</div>
<div className={styles.accountEmail}>{account.email}</div>
</div>
</div> </div>
))} ))}
<Link to="/login" onClick={onLoginClick}> <Link to="/login" onClick={onLoginClick}>

View File

@ -8,8 +8,6 @@ $bodyLeftRightPadding: 20px;
$lightBorderColor: #eee; $lightBorderColor: #eee;
.accountSwitcher { .accountSwitcher {
text-align: left;
background: #fff; background: #fff;
color: #444; color: #444;
min-width: 205px; min-width: 205px;
@ -20,9 +18,6 @@ $lightBorderColor: #eee;
border-bottom: 7px solid darker($green); border-bottom: 7px solid darker($green);
} }
.accountInfo {
}
.accountUsername, .accountUsername,
.accountEmail { .accountEmail {
overflow: hidden; overflow: hidden;
@ -30,6 +25,7 @@ $lightBorderColor: #eee;
} }
.item { .item {
display: flex;
padding: 15px; padding: 15px;
border-bottom: 1px solid $lightBorderColor; border-bottom: 1px solid $lightBorderColor;
} }
@ -50,8 +46,7 @@ $lightBorderColor: #eee;
.accountIcon { .accountIcon {
font-size: 27px; font-size: 27px;
width: 20px; width: 20px;
text-align: center; margin-inline-end: 9px;
float: left;
} }
.activeAccountIcon { .activeAccountIcon {
@ -60,10 +55,6 @@ $lightBorderColor: #eee;
font-size: 40px; font-size: 40px;
} }
.activeAccountInfo {
margin-left: 29px;
}
.activeAccountUsername { .activeAccountUsername {
font-family: $font-family-title; font-family: $font-family-title;
font-size: 20px; font-size: 20px;
@ -88,8 +79,7 @@ $lightBorderColor: #eee;
} }
.accountInfo { .accountInfo {
margin-left: 29px; flex-grow: 1;
margin-right: 25px;
} }
.accountUsername { .accountUsername {
@ -117,15 +107,16 @@ $lightBorderColor: #eee;
color: $green; color: $green;
position: relative; position: relative;
bottom: 1px; bottom: 1px;
margin-right: 3px; margin-inline-end: 3px;
} }
.logoutIcon { .logoutIcon {
composes: exit from '~app/components/ui/icons.scss'; composes: exit from '~app/components/ui/icons.scss';
align-self: center;
margin-inline-start: 9px;
color: #cdcdcd; color: #cdcdcd;
float: right;
line-height: 27px;
transition: 0.25s; transition: 0.25s;
&:hover { &:hover {

View File

@ -27,6 +27,10 @@
.expandIcon { .expandIcon {
transform: rotate(-180deg); transform: rotate(-180deg);
html[dir='rtl'] & {
transform: rotate(180deg); // Fix spin direction
}
} }
} }
@ -36,13 +40,13 @@
position: relative; position: relative;
bottom: 2px; bottom: 2px;
padding-right: 5px; padding-inline-end: 5px;
} }
.expandIcon { .expandIcon {
composes: caret from '~app/components/ui/icons.scss'; composes: caret from '~app/components/ui/icons.scss';
margin-left: 4px; margin-inline-start: 4px;
font-size: 6px; font-size: 6px;
color: #ccc; color: #ccc;
transition: 0.2s; transition: 0.2s;
@ -54,7 +58,7 @@
.accountSwitcherContainer { .accountSwitcherContainer {
position: absolute; position: absolute;
top: 100%; top: 100%;
right: -2px; inset-inline-end: -2px;
cursor: auto; cursor: auto;
display: none; display: none;

View File

@ -30,6 +30,7 @@
"redux": "^4.0.5", "redux": "^4.0.5",
"redux-localstorage": "^0.4.1", "redux-localstorage": "^0.4.1",
"redux-thunk": "^2.0.0", "redux-thunk": "^2.0.0",
"rtl-detect": "^1.0.2",
"url-search-params-polyfill": "^8.1.0", "url-search-params-polyfill": "^8.1.0",
"webfontloader": "^1.6.26", "webfontloader": "^1.6.26",
"whatwg-fetch": "^3.0.0" "whatwg-fetch": "^3.0.0"
@ -45,6 +46,7 @@
"@types/react-helmet": "^6.0.0", "@types/react-helmet": "^6.0.0",
"@types/react-motion": "^0.0.29", "@types/react-motion": "^0.0.29",
"@types/react-transition-group": "^4.2.4", "@types/react-transition-group": "^4.2.4",
"@types/rtl-detect": "^1.0.0",
"@types/webfontloader": "^1.6.30", "@types/webfontloader": "^1.6.30",
"@types/webpack-env": "^1.15.2", "@types/webpack-env": "^1.15.2",
"utility-types": "^3.10.0" "utility-types": "^3.10.0"

View File

@ -26,7 +26,11 @@
width: 50px; width: 50px;
height: 50px; height: 50px;
background: white; background: white;
animation: cubeRotate 1s ease-out infinite; animation: cubeRotateLTR 1s ease-out infinite;
html[dir='rtl'] & {
animation-name: cubeRotateRTL;
}
} }
.road { .road {
@ -34,12 +38,12 @@
width: 100%; width: 100%;
height: 1px; height: 1px;
background: white; background: white;
left: 0; inset-inline-start: 0;
bottom: 0; bottom: 0;
animation: roadStab 1s ease-out infinite; animation: roadStab 1s ease-out infinite;
} }
@keyframes cubeRotate { @keyframes cubeRotateLTR {
0% { 0% {
transform: rotate(0deg) translate3D(0, 0, 0); transform: rotate(0deg) translate3D(0, 0, 0);
} }
@ -54,6 +58,21 @@
} }
} }
@keyframes cubeRotateRTL {
0% {
transform: rotate(0deg) translate3D(0, 0, 0);
}
65% {
transform: rotate(-45deg) translate3D(0, -13px, 0);
}
90% {
transform: rotate(-70deg) translate3D(0, -8px, 0);
}
100% {
transform: rotate(-90deg) translate3D(0, 0, 0);
}
}
@keyframes roadStab { @keyframes roadStab {
0% { 0% {
transform: translate3D(0, 0, 0); transform: translate3D(0, 0, 0);
@ -91,7 +110,7 @@
height: 100%; height: 100%;
position: absolute; position: absolute;
bottom: -50px; bottom: -50px;
left: 0; inset-inline-start: 0;
overflow: hidden; overflow: hidden;
animation: roadStab 1s ease-out infinite; animation: roadStab 1s ease-out infinite;
} }
@ -103,7 +122,7 @@
border-right: 2px solid transparent; border-right: 2px solid transparent;
border-bottom: 4px solid white; border-bottom: 4px solid white;
bottom: $bottom; bottom: $bottom;
right: -2%; inset-inline-end: -2%;
animation: rockTravelling 10s $delay ease-out infinite; animation: rockTravelling 10s $delay ease-out infinite;
} }
} }
@ -116,37 +135,37 @@
@keyframes rockTravelling { @keyframes rockTravelling {
0% { 0% {
right: -2%; inset-inline-end: -2%;
} }
10% { 10% {
right: 8%; inset-inline-end: 8%;
} }
20% { 20% {
right: 18%; inset-inline-end: 18%;
} }
30% { 30% {
right: 29%; inset-inline-end: 29%;
} }
40% { 40% {
right: 40%; inset-inline-end: 40%;
} }
50% { 50% {
right: 51%; inset-inline-end: 51%;
} }
60% { 60% {
right: 62%; inset-inline-end: 62%;
} }
70% { 70% {
right: 72%; inset-inline-end: 72%;
} }
80% { 80% {
right: 82%; inset-inline-end: 82%;
} }
90% { 90% {
right: 92%; inset-inline-end: 92%;
} }
100% { 100% {
right: 102%; inset-inline-end: 102%;
} }
} }
@ -158,7 +177,7 @@
animation: roadStab 1s ease-out infinite, cloudStab 1s ease-out infinite; animation: roadStab 1s ease-out infinite, cloudStab 1s ease-out infinite;
position: absolute; position: absolute;
bottom: -50px; bottom: -50px;
left: -50%; inset-inline-start: -50%;
overflow: hidden; overflow: hidden;
} }
@ -183,7 +202,7 @@
composes: cloud; composes: cloud;
top: 65px; top: 65px;
right: -30%; inset-inline-end: -30%;
width: 50px; width: 50px;
height: 16px; height: 16px;
animation: cloudTravelling 21s 5s linear infinite; animation: cloudTravelling 21s 5s linear infinite;
@ -193,7 +212,7 @@
composes: cloud; composes: cloud;
top: 40px; top: 40px;
right: -30%; inset-inline-end: -30%;
width: 70px; width: 70px;
height: 22px; height: 22px;
animation: cloudTravelling 26s 11s linear infinite; animation: cloudTravelling 26s 11s linear infinite;
@ -201,10 +220,10 @@
@keyframes cloudTravelling { @keyframes cloudTravelling {
0% { 0% {
right: -30%; inset-inline-end: -30%;
} }
100% { 100% {
right: 110%; inset-inline-end: 110%;
} }
} }

View File

@ -34,12 +34,10 @@ $sidebar-width: 320px;
@media (min-width: 720px) { @media (min-width: 720px) {
.content { .content {
padding: 55px 50px; padding: 55px 50px;
margin-left: $sidebar-width; margin-inline-start: $sidebar-width;
} }
.sidebar { .sidebar {
right: auto;
width: $sidebar-width; width: $sidebar-width;
} }

View File

@ -6,7 +6,7 @@
width: 100%; width: 100%;
position: absolute; position: absolute;
bottom: 10px; bottom: 10px;
left: 0; inset-inline-start: 0; // TODO: does nothing. Maybe should be reimplemented?
text-align: center; text-align: center;
} }

View File

@ -6,7 +6,7 @@
width: 100%; width: 100%;
position: absolute; position: absolute;
bottom: 10px; bottom: 10px;
left: 0; inset-inline-start: 0;
text-align: center; text-align: center;
} }

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { Route, Switch } from 'react-router-dom'; import { Route, Switch } from 'react-router-dom';
import { Helmet } from 'react-helmet-async';
import clsx from 'clsx'; import clsx from 'clsx';
import { connect } from 'app/functions'; import { connect } from 'app/functions';
@ -54,10 +53,6 @@ class RootPage extends React.PureComponent<{
return ( return (
<div className={styles.root}> <div className={styles.root}>
<Helmet>
<html lang={user.lang} />
</Helmet>
<ScrollIntoView top /> <ScrollIntoView top />
<div <div

View File

@ -32,7 +32,7 @@ $toolbarHeight: 50px;
.toolbarContent { .toolbarContent {
composes: wrapper; composes: wrapper;
position: relative; display: flex;
} }
.siteName { .siteName {
@ -44,15 +44,11 @@ $toolbarHeight: 50px;
font-family: $font-family-title; font-family: $font-family-title;
font-size: 33px; font-size: 33px;
color: #fff!important; // TODO: why? color: #fff!important; // Important to remove hover effect, which is inherited from the global <a> style
} }
.userBar { .userBar {
position: absolute; margin-inline-start: auto;
right: 0;
left: 115px;
top: 0;
text-align: right;
} }
.body { .body {

View File

@ -50,7 +50,7 @@
.rulesList { .rulesList {
padding: 0; padding: 0;
margin: 0; margin: 0;
padding-left: 20px; padding-inline-start: 20px;
} }
.rulesItem { .rulesItem {
@ -71,7 +71,7 @@
content: ''; content: '';
position: absolute; position: absolute;
top: -10px; top: -10px;
left: -40px; inset-inline-start: -40px;
width: calc(100% + 60px); width: calc(100% + 60px);
height: calc(100% + 20px); height: calc(100% + 20px);
background: $white; background: $white;

View File

@ -0,0 +1,250 @@
IntlPolyfill.__addLocaleData({
locale: "udm",
date: {
ca: ["gregory", "generic"],
hourNo0: true,
hour12: false,
formats: {
short: "{1}, {0}",
medium: "{1}, {0}",
full: "{1}, {0}",
long: "{1}, {0}",
availableFormats: {
d: "d",
E: "ccc",
Ed: "ccc, d",
Ehm: "E h:mm a",
EHm: "E HH:mm",
Ehms: "E h:mm:ss a",
EHms: "E HH:mm:ss",
Gy: "G y 'аре'",
GyMMM: "LLL G y",
GyMMMd: "d MMM G y 'аре'",
GyMMMEd: "E, d MMM G y 'аре'",
h: "h a",
H: "H",
hm: "h:mm a",
Hm: "H:mm",
hms: "h:mm:ss a",
Hms: "H:mm:ss",
hmsv: "h:mm:ss a v",
Hmsv: "H:mm:ss v",
hmv: "h:mm a v",
Hmv: "H:mm v",
M: "L",
Md: "dd.MM",
MEd: "E, dd.MM",
MMdd: "dd.MM",
MMM: "LLL",
MMMd: "d MMM",
MMMEd: "ccc, d MMM",
MMMMd: "d MMMM",
ms: "mm:ss",
y: "y",
yM: "MM.y",
yMd: "dd.MM.y",
yMEd: "ccc, d.MM.y 'аре'",
yMM: "MM.y",
yMMM: "LLL y 'аре'",
yMMMd: "d MMM y 'аре'",
yMMMEd: "E, d MMM y 'аре'",
yMMMM: "LLLL y 'аре'",
yQQQ: "QQQ y 'аре'",
yQQQQ: "QQQQ y 'аре'",
},
dateFormats: {
yMMMMEEEEd: "EEEE, d MMMM y 'аре'",
yMMMMd: "d MMMM y 'аре'",
yMMMd: "d MMM y 'аре'",
yMd: "dd.MM.yy",
},
timeFormats: {
hmmsszzzz: "H:mm:ss zzzz",
hmsz: "H:mm:ss z",
hms: "H:mm:ss",
hm: "H:mm",
},
},
calendars: {
generic: {
months: {
narrow: [
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"11",
"12",
],
short: [
"Т01",
"Т02",
"Т03",
"Т04",
"Т05",
"Т06",
"Т07",
"Т08",
"Т09",
"Т10",
"Т11",
"Т12",
],
long: [
"Т01",
"Т02",
"Т03",
"Т04",
"Т05",
"Т06",
"Т07",
"Т08",
"Т09",
"Т10",
"Т11",
"Т12",
],
},
days: {
narrow: ["А", "В", "П", "В", "П", "У", "К"],
short: ["ан", "вр", "пк", "вн", "па", "уд", "кн"],
long: [
"арнянунал",
"вордӥськон",
"пуксён",
"вирнунал",
"покчиарня",
"удмуртарня",
"кӧснунал",
],
},
eras: {
narrow: ["ERA0", "ERA1"],
short: ["ERA0", "ERA1"],
long: ["ERA0", "ERA1"],
},
dayPeriods: { am: "ЛА", pm: "ЛБ" },
},
gregory: {
months: {
narrow: ["Т", "Т", "М", "А", "М", "И", "И", "А", "С", "О", "Н", "Д"],
short: [
"тшт",
"тпт",
"южт",
"ошт",
"крт",
"ивт",
"пст",
"гкт",
"кут",
"квт",
"шкт",
"тст",
],
long: [
"толшоре",
"тулыспалэ",
"южтолэзе",
"оштолэзе",
"куартолэзе",
"инвожое",
"пӧсьтолэзе",
"гудырикошконэ",
"куарусёнэ",
"коньывуонэ",
"шуркынмонэ",
"толсуре",
],
},
days: {
narrow: ["А", "В", "П", "В", "П", "У", "К"],
short: ["ан", "вр", "пк", "вн", "па", "уд", "кн"],
long: [
"арнянунал",
"вордӥськон",
"пуксён",
"вирнунал",
"покчиарня",
"удмуртарня",
"кӧснунал",
],
},
eras: {
narrow: ["КА", "КБ", "ава", "ав"],
short: ["КА", "КБ", "а.в.а", "а.в."],
long: [
"Кристослэн вордскем азяз",
"Кристослэн вордскем бераз",
"асьме вакытлэсь азьло",
"асьме вакытэ",
],
},
dayPeriods: { am: "ЛА", pm: "ЛБ" },
},
},
},
number: {
nu: ["latn"],
patterns: {
decimal: {
positivePattern: "{number}",
negativePattern: "{minusSign}{number}",
},
currency: {
positivePattern: "{number} {currency}",
negativePattern: "{minusSign}{number} {currency}",
},
percent: {
positivePattern: "{number} {percentSign}",
negativePattern: "{minusSign}{number} {percentSign}",
},
},
symbols: {
latn: {
decimal: ",",
group: " ",
nan: "лыд ӧвӧл",
plusSign: "+",
minusSign: "-",
percentSign: "%",
infinity: "∞",
},
},
currencies: {
AUD: "A$",
BRL: "R$",
CAD: "CA$",
CNY: "CN¥",
EUR: "€",
GBP: "£",
HKD: "HK$",
ILS: "₪",
INR: "₹",
JPY: "¥",
KRW: "₩",
MXN: "MX$",
NZD: "NZ$",
RUB: "₽",
RUR: "р.",
THB: "฿",
TMT: "ТМТ",
TWD: "NT$",
UAH: "₴",
USD: "$",
VND: "₫",
XAF: "FCFA",
XCD: "EC$",
XOF: "CFA",
XPF: "CFPF",
XXX: "XXXX",
},
},
});

View File

@ -0,0 +1,12 @@
Intl.PluralRules && 'function' == typeof Intl.PluralRules.__addLocaleData && Intl.PluralRules.__addLocaleData({
data: {
udm: {
categories: {
cardinal: ['other'],
ordinal: ['other'],
}, fn: function(a, l) {
return 'other';
},
},
}, availableLocales: ['udm'], aliases: {}, parentLocales: {},
});

View File

@ -0,0 +1,159 @@
Intl.RelativeTimeFormat && 'function' == typeof Intl.RelativeTimeFormat.__addLocaleData && Intl.RelativeTimeFormat.__addLocaleData({
data: {
udm: {
nu: ['latn'],
year: {
0: 'таяз аре',
1: 'вуоно аре',
'-1': 'кылем аре',
past: { other: '{0} ар талэсь азьло' },
future: { other: '{0} ар ортчыса' },
},
'year-short': {
0: 'туэ',
1: 'вуоно аре',
'-1': 'кылем аре',
past: { other: '{0} ар талэсь азьло' },
future: { other: '{0} ар ортчыса' },
},
'year-narrow': {
0: 'туэ',
1: 'кайта',
'-1': 'мийым',
past: { other: '{0} ар талэсь азьло' },
future: { other: '{0} ар ортчыса' },
},
quarter: {
0: 'таяз арньыльмосэ',
1: 'вуоно арньыльмосэ',
'-1': 'ортчем арньыльмосэ',
past: { other: '{0} арньыльмос талэсь азьло' },
future: { other: '{0} арныльмос ортчыса' },
},
'quarter-short': {
0: 'таяз арньыльмосэ',
1: 'вуоно арньыльмосэ',
'-1': 'ортчем арньыльмосэ',
past: { other: '{0} арньыльмос талэсь азьло' },
future: { other: '{0} арныльмос ортчыса' },
},
'quarter-narrow': {
0: 'таяз арньыльмосэ',
1: 'вуоно арньыльмосэ',
'-1': 'ортчем арньыльмосэ',
past: { other: '{0} арньыльмос талэсь азьло' },
future: { other: '{0} арныльмос ортчыса' },
},
month: {
0: 'таяз толэзе',
1: 'вуоно толэзе',
'-1': 'ортчем толэзе',
past: { other: '{0} толэзь талэсь азьло' },
future: { other: '{0} толэзь ортчыса' },
},
'month-short': {
0: 'таяз толэзе',
1: 'вуоно толэзе',
'-1': 'ортчем толэзе',
past: { other: '{0} толэзь талэсь азьло' },
future: { other: '{0} толэзь ортчыса' },
},
'month-narrow': {
0: 'таяз толэзе',
1: 'вуоно толэзе',
'-1': 'ортчем толэзе',
past: { other: '{0} толэзь талэсь азьло' },
future: { other: '{0} толэзь ортчыса' },
},
week: {
0: 'таяз арняе',
1: 'вуоно арняе',
'-1': 'ортчем арняе',
past: { other: '{0} арня талэсь азьло' },
future: { other: '{0} арня ортчыса' },
},
'week-short': {
0: 'таяз арняе',
1: 'вуоно арняе',
'-1': 'ортчем арняе',
past: { other: '{0} арня талэсь азьло' },
future: { other: '{0} арня ортчыса' },
},
'week-narrow': {
0: 'таяз арняе',
1: 'вуоно арняе',
'-1': 'ортчем арняе',
past: { other: '{0} арня талэсь азьло' },
future: { other: '{0} арня ортчыса' },
},
day: {
0: 'туннэ',
1: 'ӵуказе',
2: 'ӵуказе улыса',
'-2': 'валлян',
'-1': 'толон',
past: { other: '{0} нунал талэсь азьло' },
future: { other: '{0} нунал ортчыса' },
},
'day-short': {
0: 'туннэ',
1: 'ӵуказе',
2: 'ӵуказе улыса',
'-2': 'валлян',
'-1': 'толон',
past: { other: '{0} нунал талэсь азьло' },
future: { other: '{0} нунал ортчыса' },
},
'day-narrow': {
0: 'туннэ',
1: 'ӵуказе',
2: 'усьсэ',
'-2': 'валлян',
'-1': 'толон',
past: { other: '{0} нунал талэсь азьло' },
future: { other: '{0} нунал ортчыса' },
},
hour: { 0: 'таяз часэ', past: { other: '{0} час талэсь азьло' }, future: { other: '{0} час ортчыса' } },
'hour-short': {
0: 'таяз часэ',
past: { other: '{0} час талэсь азьло' },
future: { other: '{0} час ортчыса' },
},
'hour-narrow': {
0: 'таяз часэ',
past: { other: '{0} час талэсь азьло' },
future: { other: '{0} час ортчыса' },
},
minute: {
0: 'таяз минутэ',
past: { other: '{0} минут талэсь азьло' },
future: { other: '{0} минут ортчыса' },
},
'minute-short': {
0: 'таяз минутэ',
past: { other: '{0} минут талэсь азьло' },
future: { other: '{0} минут ортчыса' },
},
'minute-narrow': {
0: 'таяз минутэ',
past: { other: '{0} минут талэсь азьло' },
future: { other: '{0} минут ортчыса' },
},
second: {
0: 'таяз секундэ',
past: { other: '{0} секунд талэзь азьло' },
future: { other: '{0} секунд ортчыса' },
},
'second-short': {
0: 'таяз секундэ',
past: { other: '{0} секунд талэзь азьло' },
future: { other: '{0} секунд ортчыса' },
},
'second-narrow': {
0: 'таяз секундэ',
past: { other: '{0} секунд талэзь азьло' },
future: { other: '{0} секунд ортчыса' },
},
},
}, availableLocales: ['udm'], aliases: {}, parentLocales: {},
});

View File

@ -28,7 +28,7 @@ const CROWDIN_FILE_PATH = config.filePath;
const SOURCE_LANG = config.sourceLang; const SOURCE_LANG = config.sourceLang;
const LANG_DIR = config.basePath; const LANG_DIR = config.basePath;
const INDEX_FILE_NAME = 'index.js'; const INDEX_FILE_NAME = 'index.js';
const MIN_RELEASE_PROGRESS = config.minApproved; const MIN_RELEASE_PROGRESS = config.minTranslated;
const crowdin = new Crowdin({ const crowdin = new Crowdin({
token: config.apiKey, token: config.apiKey,
@ -43,6 +43,7 @@ const releasedLocales: ReadonlyArray<string> = ['be', 'fr', 'id', 'pt', 'ru', 'u
* Map Crowdin locales into our internal locales representation * Map Crowdin locales into our internal locales representation
*/ */
const LOCALES_MAP: Record<string, string> = { const LOCALES_MAP: Record<string, string> = {
'es-ES': 'es',
'pt-BR': 'pt', 'pt-BR': 'pt',
'zh-CN': 'zh', 'zh-CN': 'zh',
}; };
@ -54,6 +55,7 @@ const LOCALES_MAP: Record<string, string> = {
const NATIVE_NAMES_MAP: Record<string, string> = { const NATIVE_NAMES_MAP: Record<string, string> = {
be: 'Беларуская', be: 'Беларуская',
cs: 'Čeština', cs: 'Čeština',
fil: 'Wikang Filipino',
id: 'Bahasa Indonesia', id: 'Bahasa Indonesia',
lt: 'Lietuvių', lt: 'Lietuvių',
pl: 'Polski', pl: 'Polski',
@ -67,6 +69,7 @@ const NATIVE_NAMES_MAP: Record<string, string> = {
* This arrays allows us to override Crowdin English languages names * This arrays allows us to override Crowdin English languages names
*/ */
const ENGLISH_NAMES_MAP: Record<string, string> = { const ENGLISH_NAMES_MAP: Record<string, string> = {
fil: 'Filipino',
pt: 'Portuguese, Brazilian', pt: 'Portuguese, Brazilian',
sr: 'Serbian', sr: 'Serbian',
zh: 'Simplified Chinese', zh: 'Simplified Chinese',
@ -225,7 +228,7 @@ async function pull(): Promise<void> {
} }
console.log('Pulling translation progress...'); console.log('Pulling translation progress...');
const { data: translationProgress } = await crowdin.translationStatusApi.getFileProgress(PROJECT_ID, fileId, 100); const { data: fileProgress } = await crowdin.translationStatusApi.getFileProgress(PROJECT_ID, fileId, 100);
const localesToPull: Array<string> = []; const localesToPull: Array<string> = [];
const indexFileEntries: Record<string, IndexFileEntry> = { const indexFileEntries: Record<string, IndexFileEntry> = {
@ -238,16 +241,16 @@ async function pull(): Promise<void> {
}, },
}; };
translationProgress.forEach(({ data: { languageId, approvalProgress } }) => { fileProgress.forEach(({ data: { languageId, translationProgress } }) => {
const locale = toInternalLocale(languageId); const locale = toInternalLocale(languageId);
if (releasedLocales.includes(locale) || approvalProgress >= MIN_RELEASE_PROGRESS) { if (releasedLocales.includes(locale) || translationProgress >= MIN_RELEASE_PROGRESS) {
localesToPull.push(languageId); localesToPull.push(languageId);
indexFileEntries[locale] = { indexFileEntries[locale] = {
code: locale, code: locale,
name: NATIVE_NAMES_MAP[locale] || iso639.getNativeName(locale), name: NATIVE_NAMES_MAP[locale] || iso639.getNativeName(locale),
englishName: ENGLISH_NAMES_MAP[locale] || iso639.getName(locale), englishName: ENGLISH_NAMES_MAP[locale] || iso639.getName(locale),
progress: approvalProgress, progress: translationProgress,
isReleased: releasedLocales.includes(locale), isReleased: releasedLocales.includes(locale),
}; };
} }
@ -268,7 +271,6 @@ async function pull(): Promise<void> {
data: { url }, data: { url },
} = await crowdin.translationsApi.buildProjectFileTranslation(PROJECT_ID, fileId, { } = await crowdin.translationsApi.buildProjectFileTranslation(PROJECT_ID, fileId, {
targetLanguageId: languageId, targetLanguageId: languageId,
exportApprovedOnly: true,
}); });
const { data: fileContents } = await axios.get(url, { const { data: fileContents } = await axios.get(url, {

View File

@ -47,6 +47,7 @@ module.exports = ({ webpack: loader }) => ({
return defaultLoad(filename, importOptions); return defaultLoad(filename, importOptions);
})(require('postcss-import/lib/load-content')), })(require('postcss-import/lib/load-content')),
}, },
'postcss-bidirection': {},
// TODO: for some reason cssnano strips out @mixin declarations // TODO: for some reason cssnano strips out @mixin declarations
// cssnano: { // cssnano: {
// /** // /**

19
postinstall.js Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env node
/* eslint-disable @typescript-eslint/no-var-requires */
const fs = require('fs');
const path = require('path');
const localeOverrides = {
'packages/app/services/i18n/overrides/intl': 'node_modules/intl/locale-data/jsonp',
'packages/app/services/i18n/overrides/pluralrules': 'node_modules/@formatjs/intl-pluralrules/dist/locale-data',
// eslint-disable-next-line prettier/prettier
'packages/app/services/i18n/overrides/relativetimeformat': 'node_modules/@formatjs/intl-relativetimeformat/dist/locale-data',
};
Object.entries(localeOverrides).forEach(([sourceDir, targetDir]) => {
fs.readdirSync(sourceDir).forEach((localeFile) => {
fs.copyFileSync(path.join(sourceDir, localeFile), path.join(targetDir, localeFile));
});
});

View File

@ -201,19 +201,6 @@ const webpackConfig = {
type: 'javascript/auto', type: 'javascript/auto',
use: ['fontgen-loader'], use: ['fontgen-loader'],
}, },
{
// Replace some locales provided by FormatJS with local ones
test: /@formatjs\/intl-\w+\/dist\/locale-data/,
loader: 'file-replace-loader',
options: {
condition: 'if-replacement-exists',
replacement: (resource) =>
resource.replace(
/node_modules\/@formatjs\/intl-(\w+)\/dist\/locale-data\/(\w+)\.js/,
'packages/app/services/i18n/overrides/$1/$2.js',
),
},
},
], ],
}, },

View File

@ -3439,6 +3439,11 @@
"@types/prop-types" "*" "@types/prop-types" "*"
csstype "^3.0.2" csstype "^3.0.2"
"@types/rtl-detect@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/rtl-detect/-/rtl-detect-1.0.0.tgz#5791e18a111f2b8b5b328160af97f3991a5697a5"
integrity sha512-lyYh44YgrejEK9/5rhASghvRUOxrSJyyyQmqK7L6F/V5qs6PY1RfCi1VbjSkY6kuDt7lzQyhd006slhda4Oypg==
"@types/sax@^1.2.1": "@types/sax@^1.2.1":
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/@types/sax/-/sax-1.2.1.tgz#e0248be936ece791a82db1a57f3fb5f7c87e8172" resolved "https://registry.yarnpkg.com/@types/sax/-/sax-1.2.1.tgz#e0248be936ece791a82db1a57f3fb5f7c87e8172"
@ -12535,6 +12540,12 @@ posix-character-classes@^0.1.0:
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
"postcss-bidirection@https://github.com/erickskrauch/postcss-bidirection.git#iss_23":
version "2.7.2"
resolved "https://github.com/erickskrauch/postcss-bidirection.git#feabf42c8ab5cf6b0e22f78de04984e22cf42ba4"
dependencies:
postcss "^7.0.13"
postcss-calc@^7.0.1: postcss-calc@^7.0.1:
version "7.0.1" version "7.0.1"
resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.1.tgz#36d77bab023b0ecbb9789d84dcb23c4941145436" resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-7.0.1.tgz#36d77bab023b0ecbb9789d84dcb23c4941145436"
@ -12917,6 +12928,15 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.2
source-map "^0.6.1" source-map "^0.6.1"
supports-color "^6.1.0" supports-color "^6.1.0"
postcss@^7.0.13:
version "7.0.35"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.35.tgz#d2be00b998f7f211d8a276974079f2e92b970e24"
integrity sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==
dependencies:
chalk "^2.4.2"
source-map "^0.6.1"
supports-color "^6.1.0"
postcss@^7.0.26: postcss@^7.0.26:
version "7.0.26" version "7.0.26"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.26.tgz#5ed615cfcab35ba9bbb82414a4fa88ea10429587" resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.26.tgz#5ed615cfcab35ba9bbb82414a4fa88ea10429587"
@ -14246,6 +14266,11 @@ rsvp@^4.8.4:
resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734"
integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
rtl-detect@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/rtl-detect/-/rtl-detect-1.0.2.tgz#8eca316f5c6563d54df4e406171dd7819adda67f"
integrity sha512-5X1422hvphzg2a/bo4tIDbjFjbJUOaPZwqE6dnyyxqwFqfR+tBcvfqapJr0o0VygATVCGKiODEewhZtKF+90AA==
run-async@^2.2.0, run-async@^2.4.0: run-async@^2.2.0, run-async@^2.4.0:
version "2.4.1" version "2.4.1"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"