From 8cf36b896b5df8aefebb8a60c3c40657a4b1c2bd Mon Sep 17 00:00:00 2001 From: konstantin krivlenia Date: Mon, 17 Feb 2020 18:55:45 +0300 Subject: [PATCH] #10 add localization --- frontend/.gitignore | 1 + frontend/app/common/config-types.ts | 2 + .../components/auth-panel/auth-panel.test.tsx | 83 +- .../app/components/auth-panel/auth-panel.tsx | 7 +- .../components/comment-form/comment-form.tsx | 18 +- .../app/components/comment/comment.test.tsx | 31 +- frontend/app/components/comment/comment.tsx | 42 +- .../components/comment/connected-comment.tsx | 4 +- .../list-comments/list-comments.tsx | 39 +- frontend/app/components/root/root.tsx | 7 +- frontend/app/components/thread/thread.tsx | 3 + .../user-info/last-comments-list.tsx | 4 +- frontend/app/last-comments.tsx | 12 +- frontend/app/locales/en.json | 12 + frontend/app/locales/ru.json | 12 + frontend/app/remark.tsx | 28 +- frontend/app/utils/loadLocale.ts | 6 + frontend/index.ejs | 3 +- frontend/package-lock.json | 719 +++++++++++++++++- frontend/package.json | 8 +- frontend/tasks/generateDictionary.js | 30 + frontend/tsconfig.json | 1 + 22 files changed, 972 insertions(+), 100 deletions(-) create mode 100644 frontend/app/locales/en.json create mode 100644 frontend/app/locales/ru.json create mode 100644 frontend/app/utils/loadLocale.ts create mode 100644 frontend/tasks/generateDictionary.js diff --git a/frontend/.gitignore b/frontend/.gitignore index 4c49bd78f1..355e175cec 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1 +1,2 @@ .env +extracted-messages diff --git a/frontend/app/common/config-types.ts b/frontend/app/common/config-types.ts index 078d1e3e21..5ee93b5bb1 100644 --- a/frontend/app/common/config-types.ts +++ b/frontend/app/common/config-types.ts @@ -16,10 +16,12 @@ export interface CommentsConfig { theme?: Theme; page_title?: string; node?: string; + locale?: string; } export interface LastCommentsConfig { host: string; site_id: string; max_last_comments: number; + locale?: string; } diff --git a/frontend/app/components/auth-panel/auth-panel.test.tsx b/frontend/app/components/auth-panel/auth-panel.test.tsx index c1e41a01a6..3e36edaca4 100644 --- a/frontend/app/components/auth-panel/auth-panel.test.tsx +++ b/frontend/app/components/auth-panel/auth-panel.test.tsx @@ -6,6 +6,7 @@ import { Button } from '@app/components/button'; import { User, PostInfo } from '@app/common/types'; import { Props, AuthPanel } from './auth-panel'; +import { IntlProvider } from 'react-intl'; const DefaultProps: Partial = { sort: '-score', @@ -22,7 +23,11 @@ const DefaultProps: Partial = { describe('', () => { describe('For not authorized user', () => { it('should render login form with google and github provider', () => { - const element = mount(); + const element = mount( + + + + ); const authPanelColumn = element.find('.auth-panel__column'); @@ -41,12 +46,14 @@ describe('', () => { describe('sorting', () => { it('should place selected provider first', () => { const element = mount( - + + + ); const providerLinks = element @@ -61,12 +68,14 @@ describe('', () => { it('should do nothing if provider not found', () => { const element = mount( - + + + ); const providerLinks = element @@ -82,11 +91,13 @@ describe('', () => { it('should render login form with google and github provider for read-only post', () => { const element = mount( - + + + ); const authPanelColumn = element.find('.auth-panel__column'); @@ -105,11 +116,13 @@ describe('', () => { it('should not render settings if there is no hidden users', () => { const element = mount( - + + + ); const adminAction = element.find('.auth-panel__admin-action'); @@ -119,12 +132,14 @@ describe('', () => { it('should render settings if there is some hidden users', () => { const element = mount( - + + + ); const adminAction = element.find('.auth-panel__admin-action'); @@ -134,7 +149,11 @@ describe('', () => { }); describe('For authorized user', () => { it('should render info about current user', () => { - const element = mount(); + const element = mount( + + + + ); const authPanelColumn = element.find('.auth-panel__column'); @@ -148,7 +167,9 @@ describe('', () => { describe('For admin user', () => { it('should render admin action', () => { const element = mount( - + + {' '} + ); const adminAction = element.find('.auth-panel__admin-action').first(); diff --git a/frontend/app/components/auth-panel/auth-panel.tsx b/frontend/app/components/auth-panel/auth-panel.tsx index 88cf212647..8a38f9ab60 100644 --- a/frontend/app/components/auth-panel/auth-panel.tsx +++ b/frontend/app/components/auth-panel/auth-panel.tsx @@ -13,6 +13,7 @@ import { StoreState } from '@app/store'; import { ProviderState } from '@app/store/provider/reducers'; import { Dropdown, DropdownItem } from '@app/components/dropdown'; import { Button } from '@app/components/button'; +import { FormattedMessage } from 'react-intl'; import { AnonymousLoginForm } from './__anonymous-login-form'; import { EmailLoginFormConnected } from './__email-login-form'; @@ -155,7 +156,7 @@ export class AuthPanel extends Component { return (
- You logged in as{' '} + {' '}
{ )} {' '}
); @@ -263,7 +264,7 @@ export class AuthPanel extends Component { return (
- {'Login: '} + {' '} {!isAboveThreshold && sortedProviders.map((provider, i) => { const comma = i === 0 ? '' : i === sortedProviders.length - 1 ? ' or ' : ', '; diff --git a/frontend/app/components/comment-form/comment-form.tsx b/frontend/app/components/comment-form/comment-form.tsx index 0fb4a736c8..4e2c6ef31c 100644 --- a/frontend/app/components/comment-form/comment-form.tsx +++ b/frontend/app/components/comment-form/comment-form.tsx @@ -1,5 +1,6 @@ /** @jsx createElement */ import { createElement, Component, createRef, Fragment } from 'preact'; +import { FormattedMessage } from 'react-intl'; import b, { Mix } from 'bem-react-helper'; import { User, Theme, Image, ApiError } from '@app/common/types'; @@ -51,12 +52,6 @@ interface State { buttonText: null | string; } -const Labels = { - main: 'Send', - edit: 'Save', - reply: 'Reply', -}; - const ImageMimeRegex = /image\//i; export class CommentForm extends Component { @@ -343,6 +338,11 @@ export class CommentForm extends Component { render(props: Props, { isDisabled, isErrorShown, errorMessage, preview, maxLength, text, buttonText }: State) { const charactersLeft = maxLength - text.length; errorMessage = props.errorMessage || errorMessage; + const Labels = { + main: , + edit: , + reply: , + }; const label = buttonText || Labels[props.mode || 'main']; return ( @@ -406,7 +406,7 @@ export class CommentForm extends Component { disabled={isDisabled} onClick={this.getPreview} > - Preview + )} ); } @@ -476,7 +489,7 @@ export class Comment extends Component { : props.data.delete ? 'This comment was deleted' : props.data.text, - time: formatTime(new Date(props.data.time)), + time: new Date(props.data.time), orig: isEditing ? props.data.orig && props.data.orig.replace(/&[#A-Za-z0-9]+;/gi, entity => { @@ -602,7 +615,7 @@ export class Comment extends Component { )} - {o.time} + {!!props.level && props.level > 0 && props.view === 'main' && ( @@ -764,13 +777,16 @@ function getTextSnippet(html: string) { return snippet.length === LENGTH && result.length !== LENGTH ? `${snippet}...` : snippet; } -function formatTime(time: Date) { - // 'ru-RU' adds a dot as a separator - const date = time.toLocaleDateString(['ru-RU'], { day: '2-digit', month: '2-digit', year: '2-digit' }); - - // do it manually because Intl API doesn't add leading zeros to hours; idk why - const hours = `0${time.getHours()}`.slice(-2); - const mins = `0${time.getMinutes()}`.slice(-2); - - return `${date} at ${hours}:${mins}`; +function FormatTime({ time }: { time: Date }) { + const intl = useIntl(); + return ( + + ); } diff --git a/frontend/app/components/comment/connected-comment.tsx b/frontend/app/components/comment/connected-comment.tsx index 6ff516b3d8..172bcd6d0b 100644 --- a/frontend/app/components/comment/connected-comment.tsx +++ b/frontend/app/components/comment/connected-comment.tsx @@ -30,6 +30,7 @@ import { uploadImage, getPreview } from '@app/common/api'; import { getThreadIsCollapsed } from '@app/store/thread/getters'; import { bindActions } from '@app/utils/actionBinder'; import { useActions } from '@app/hooks/useAction'; +import { useIntl } from 'react-intl'; type ProvidedProps = Pick< Props, @@ -75,5 +76,6 @@ export const boundActions = bindActions({ export const ConnectedComment: FunctionComponent> = props => { const providedProps = mapStateToProps(useStore().getState(), props); const actions = useActions(boundActions); - return ; + const intl = useIntl(); + return ; }; diff --git a/frontend/app/components/list-comments/list-comments.tsx b/frontend/app/components/list-comments/list-comments.tsx index e0717d3e77..477ae80c8f 100644 --- a/frontend/app/components/list-comments/list-comments.tsx +++ b/frontend/app/components/list-comments/list-comments.tsx @@ -5,26 +5,31 @@ import { NODE_ID } from '@app/common/constants'; import { Comment as CommentType } from '@app/common/types'; import { Comment } from '@app/components/comment'; +import { useIntl } from 'react-intl'; interface Props { comments: CommentType[]; } -export const ListComments = ({ comments = [] }: Props) => ( -
-
- {comments.map(comment => ( - - ))} +export const ListComments = ({ comments = [] }: Props) => { + const intl = useIntl(); + return ( +
+
+ {comments.map(comment => ( + + ))} +
-
-); + ); +}; diff --git a/frontend/app/components/root/root.tsx b/frontend/app/components/root/root.tsx index 5e6cc68e1a..864e56b903 100644 --- a/frontend/app/components/root/root.tsx +++ b/frontend/app/components/root/root.tsx @@ -2,6 +2,7 @@ import { createElement, Component, FunctionComponent } from 'preact'; import { useSelector } from 'react-redux'; import b from 'bem-react-helper'; +import { IntlShape, useIntl } from 'react-intl'; import { User, Sorting, AuthProvider } from '@app/common/types'; import { @@ -79,7 +80,7 @@ const boundActions = bindActions({ updateComment, }); -type Props = ReturnType & typeof boundActions; +type Props = ReturnType & typeof boundActions & { intl: IntlShape }; interface State { isLoaded: boolean; @@ -258,6 +259,7 @@ export class Root extends Component {
{this.props.pinnedComments.map(comment => ( { export const ConnectedRoot: FunctionComponent = () => { const props = useSelector(mapStateToProps); const actions = useActions(boundActions); - return ; + const intl = useIntl(); + return ; }; diff --git a/frontend/app/components/thread/thread.tsx b/frontend/app/components/thread/thread.tsx index fc36911df1..84176343e1 100644 --- a/frontend/app/components/thread/thread.tsx +++ b/frontend/app/components/thread/thread.tsx @@ -11,6 +11,7 @@ import { setCollapse } from '@app/store/thread/actions'; import { getThreadIsCollapsed } from '@app/store/thread/getters'; import { InView } from '@app/components/root/in-view/in-view'; import { ConnectedComment as Comment } from '@app/components/comment/connected-comment'; +import { useIntl } from 'react-intl'; interface Props { id: CommentInterface['id']; @@ -32,6 +33,7 @@ const commentSelector = (id: string) => (state: StoreState) => { export const Thread: FunctionComponent = ({ id, level, mix, getPreview }) => { const dispatch = useDispatch(); + const intl = useIntl(); const { collapsed, comment, childs, theme } = useSelector(commentSelector(id), shallowEqual); const collapse = useCallback(() => { dispatch(setCollapse(id, !collapsed)); @@ -54,6 +56,7 @@ export const Thread: FunctionComponent = ({ id, level, mix, getPreview }) ref={ref => inviewProps.ref(ref)} key={`comment-${id}`} view="main" + intl={intl} data={comment} repliesCount={repliesCount} level={level} diff --git a/frontend/app/components/user-info/last-comments-list.tsx b/frontend/app/components/user-info/last-comments-list.tsx index 2dc5876709..16d002593f 100644 --- a/frontend/app/components/user-info/last-comments-list.tsx +++ b/frontend/app/components/user-info/last-comments-list.tsx @@ -5,18 +5,20 @@ import { Comment as CommentType } from '@app/common/types'; import { Comment } from '@app/components/comment'; import { Preloader } from '@app/components/preloader'; +import { useIntl } from 'react-intl'; const LastCommentsList = ({ comments, isLoading }: { comments: CommentType[]; isLoading: boolean }) => { + const intl = useIntl(); if (isLoading) { return ; } - return (
{comments.map(comment => ( { (node.dataset.max && parseInt(node.dataset.max, 10)) || remark_config.max_last_comments || DEFAULT_LAST_COMMENTS_MAX; - getLastComments(remark_config.site_id!, max).then(comments => { + const locale = remark_config.locale || 'en'; + Promise.all([getLastComments(remark_config.site_id!, max), loadLocale(locale)]).then(([comments, messages]) => { try { - render(, node); + render( + + + , + node + ); } catch (e) { console.error('Remark42: Something went wrong with last comments rendering'); console.error(e); diff --git a/frontend/app/locales/en.json b/frontend/app/locales/en.json new file mode 100644 index 0000000000..15874d59d2 --- /dev/null +++ b/frontend/app/locales/en.json @@ -0,0 +1,12 @@ +{ + "commentForm.send": "Send", + "commentForm.save": "Save", + "commentForm.replay": "Replay", + "comment.hide": "Hide", + "comment.time": "{day} at {time}", + "comment.delete": "Do you want to delete this comment?", + "commentForm.preview": "Preview", + "authPanel.logout": "Logout?", + "authPanel.login": "Login:", + "authPanel.logged-as": "You logged in as" +} \ No newline at end of file diff --git a/frontend/app/locales/ru.json b/frontend/app/locales/ru.json new file mode 100644 index 0000000000..11d4e13f42 --- /dev/null +++ b/frontend/app/locales/ru.json @@ -0,0 +1,12 @@ +{ + "commentForm.send": "Отправить", + "commentForm.save": "Сохранить", + "commentForm.replay": "Ответить", + "comment.hide": "Спрятать", + "comment.time": "{day} в {time}", + "comment.delete": "Удалить комментарий?", + "commentForm.preview": "Предпросмотр", + "authPanel.logout": "Выйти?", + "authPanel.login": "Вход:", + "authPanel.logged-as": "Вы вошли как" +} diff --git a/frontend/app/remark.tsx b/frontend/app/remark.tsx index 0b4cccc22a..78ed928b85 100644 --- a/frontend/app/remark.tsx +++ b/frontend/app/remark.tsx @@ -1,6 +1,9 @@ /** @jsx createElement */ import loadPolyfills from '@app/common/polyfills'; +import { IntlProvider } from 'react-intl'; +import { loadLocale } from './utils/loadLocale'; + import { createElement, render } from 'preact'; import 'preact/debug'; import { bindActionCreators } from 'redux'; @@ -56,26 +59,31 @@ async function init(): Promise { } return memo; }, {}); - + const locale = params.locale || `en`; + const messages = await loadLocale(locale).catch(() => ({})); StaticStore.config = await api.getConfig(); if (params.page === 'user-info') { return render( -
-
- - - + +
+
+ + + +
-
, + , node ); } render( - - - , + + + + + , node ); } diff --git a/frontend/app/utils/loadLocale.ts b/frontend/app/utils/loadLocale.ts new file mode 100644 index 0000000000..c7619bb6a0 --- /dev/null +++ b/frontend/app/utils/loadLocale.ts @@ -0,0 +1,6 @@ +export async function loadLocale(locale: string): Promise> { + if (locale === 'ru') { + return import(/* webpackChunkName: "ru" */ `../locales/ru.json`).then(res => res.default); + } + return {}; +} diff --git a/frontend/index.ejs b/frontend/index.ejs index 1950b09c70..1050e8a6a9 100644 --- a/frontend/index.ejs +++ b/frontend/index.ejs @@ -126,7 +126,8 @@ host: window.location.origin, url: 'https://remark42.com/demo/', components: ['embed', 'counter'], - theme: theme + theme: theme, + locale: "ru" }; (function(c, d) { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 1335f6af7f..6988d06f2f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -171,6 +171,164 @@ } } }, + "@babel/helper-create-class-features-plugin": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.8.3.tgz", + "integrity": "sha512-qmp4pD7zeTxsv0JNecSBsEmG1ei2MqwJq4YQcK3ZWm/0t07QstWfvuV/vm3Qt5xNMFETn2SZqpMx2MQzbtq+KA==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-member-expression-to-functions": "^7.8.3", + "@babel/helper-optimise-call-expression": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/helper-replace-supers": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/generator": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.4.tgz", + "integrity": "sha512-PwhclGdRpNAf3IxZb0YVuITPZmmrXz9zf6fH8lT4XbrmfQKr6ryBzhv593P5C6poJRciFCL/eHGW2NuGrgEyxA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz", + "integrity": "sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz", + "integrity": "sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "dev": true + }, + "@babel/helper-replace-supers": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.8.3.tgz", + "integrity": "sha512-xOUssL6ho41U81etpLoT2RTdvdus4VfHamCuAm4AHxGr+0it5fnwoVdwUJ7GFEqCsQYzJUhcbsN9wB9apcYKFA==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.8.3", + "@babel/helper-optimise-call-expression": "^7.8.3", + "@babel/traverse": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz", + "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==", + "dev": true + }, + "@babel/template": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", + "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/traverse": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.4.tgz", + "integrity": "sha512-NGLJPZwnVEyBPLI+bl9y9aSnxMhsKz42so7ApAv9D+b4vAFPpY013FTS9LdKxcABoIYFU52HcYga1pPlx454mg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.4", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.4", + "@babel/types": "^7.8.3", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + } + } + }, "@babel/helper-define-map": { "version": "7.5.5", "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.5.5.tgz", @@ -507,6 +665,24 @@ "@babel/plugin-syntax-async-generators": "^7.2.0" } }, + "@babel/plugin-proposal-class-properties": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.8.3.tgz", + "integrity": "sha512-EqFhbo7IosdgPgZggHaNObkmO1kNUe3slaKu54d5OWvy+p9QIKOzK1GAEpAIsZtWVtPXUHSMcT4smvDrCfY4AA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "dev": true + } + } + }, "@babel/plugin-proposal-dynamic-import": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz", @@ -547,6 +723,24 @@ "@babel/plugin-syntax-optional-catch-binding": "^7.2.0" } }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.8.3.tgz", + "integrity": "sha512-QIoIR9abkVn+seDE3OjA08jWcs3eZ9+wJCKSRgo3WdEU2csFYgdScb+8qHB3+WXsGJD55u+5hWCISI7ejXS+kg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.0" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "dev": true + } + } + }, "@babel/plugin-proposal-unicode-property-regex": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.6.2.tgz", @@ -612,6 +806,40 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "dev": true + } + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.8.3.tgz", + "integrity": "sha512-GO1MQ/SGGGoiEXY0e0bSpHimJvxqB7lktLLIq2pv8xG7WZ8IMEle74jIe1FhprHBWjwjZtXHkycDLZXIWM5Wfg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "dev": true + } + } + }, "@babel/plugin-transform-arrow-functions": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz", @@ -949,6 +1177,25 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-transform-typescript": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.8.3.tgz", + "integrity": "sha512-Ebj230AxcrKGZPKIp4g4TdQLrqX95TobLUWKd/CwG7X1XHUH1ZpkpFvXuXqWbtGRWb7uuEWNlrl681wsOArAdQ==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.8.3", + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-typescript": "^7.8.3" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "dev": true + } + } + }, "@babel/plugin-transform-unicode-regex": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.6.2.tgz", @@ -1044,6 +1291,24 @@ "@babel/plugin-transform-react-jsx-source": "^7.0.0" } }, + "@babel/preset-typescript": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.8.3.tgz", + "integrity": "sha512-qee5LgPGui9zQ0jR1TeU5/fP9L+ovoArklEqY12ek8P/wV5ZeM/VYSQYwICeoT6FfpJTekG9Ilay5PhwsOpMHA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-transform-typescript": "^7.8.3" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "dev": true + } + } + }, "@babel/runtime": { "version": "7.4.5", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz", @@ -1137,6 +1402,93 @@ "minimist": "^1.2.0" } }, + "@formatjs/cli": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/@formatjs/cli/-/cli-1.1.20.tgz", + "integrity": "sha512-0seqDTfTTzXikRYic5arhKI27b4sDpdtGsbA/WG5BMPUYRdGZ38xBUD7V8pueFXlGiIR+SVKqY2kHP0X3Y5b3Q==", + "dev": true, + "requires": { + "@babel/plugin-proposal-class-properties": "^7.5.5", + "@babel/plugin-proposal-optional-chaining": "7", + "@babel/preset-env": "^7.6.0", + "@babel/preset-typescript": "^7.6.0", + "@types/babel__core": "^7.1.3", + "@types/loader-utils": "^1.1.3", + "@types/lodash": "^4.14.138", + "babel-plugin-const-enum": "^0.0.2", + "babel-plugin-react-intl": "^5.1.18", + "commander": "4.0.0-1", + "fs-extra": "^8.1.0", + "glob": "^7.1.6", + "loader-utils": "^1.2.3", + "lodash": "^4.17.15", + "loud-rejection": "^2.1.0" + }, + "dependencies": { + "commander": { + "version": "4.0.0-1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.0.0-1.tgz", + "integrity": "sha512-6UCnFDyJnZfk4ZugvEhl8hYtlViGJNVki5J+3x/zNd8nLDpBcDHZShvBYWKIIktsiBUuRDttP/qG1yF+bhhVnQ==", + "dev": true + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "@formatjs/intl-displaynames": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-1.2.0.tgz", + "integrity": "sha512-mUGI2sc6OABkrMj42HlOpK1h96EVrN+gOhzbyCTMH9SVH/gPPLr/zFRH3KFWtBwxqhYsDghvUwm8xkdFOK0kTg==", + "requires": { + "@formatjs/intl-utils": "^2.2.0" + } + }, + "@formatjs/intl-listformat": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-1.4.1.tgz", + "integrity": "sha512-AX0o1y5xXyMY4ebZOO+UujMcDhniYDs50KpwGzjUPV+bBILwRYqH/6IprZZG/V8YSOtetZlalZiwzJ50dH6PuQ==", + "requires": { + "@formatjs/intl-utils": "^2.2.0" + } + }, + "@formatjs/intl-relativetimeformat": { + "version": "4.5.9", + "resolved": "https://registry.npmjs.org/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-4.5.9.tgz", + "integrity": "sha512-6rgPXQl5MrPPbCuNiHxolzO6xNCHphCVEWW6RWGy7t/Mek70gD7nq1erW8fbQJ0XL/UeAC0Cz/+ggh7vaSsKNA==", + "requires": { + "@formatjs/intl-utils": "^2.2.0" + } + }, + "@formatjs/intl-unified-numberformat": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-unified-numberformat/-/intl-unified-numberformat-3.2.0.tgz", + "integrity": "sha512-SZMTV/tR0h7nYhS2x69S7zhHXaBmE0ZTR2OIiakt8W7uYWVgcRhu/LgUeVtGzpwPI2ChcOjNMtX/k6y1M9aDNA==", + "requires": { + "@formatjs/intl-utils": "^2.2.0" + } + }, + "@formatjs/intl-utils": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@formatjs/intl-utils/-/intl-utils-2.2.0.tgz", + "integrity": "sha512-+Az7tR1av1DHZu9668D8uh9atT6vp+FFmEF8BrEssv0OqzpVjpVBGVmcgPzQP8k2PQjVlm/h2w8cTt0knn132w==" + }, + "@formatjs/macro": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@formatjs/macro/-/macro-0.2.6.tgz", + "integrity": "sha512-DfdnLJf8+PwLHzJECZ1Xfa8+sI9akQnUuLN2UdkaExTQmlY0Vs36rMzEP0JoVDBMk+KdQbJNt72rPeZkBNcKWg==" + }, "@github/combobox-nav": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@github/combobox-nav/-/combobox-nav-1.0.1.tgz", @@ -1831,12 +2183,16 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz", "integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==", - "dev": true, "requires": { "@types/react": "*", "hoist-non-react-statics": "^3.3.0" } }, + "@types/invariant": { + "version": "2.2.31", + "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.31.tgz", + "integrity": "sha512-jMlgg9pIURvy9jgBHCjQp/CyBjYHUwj91etVcDdXkFl2CwTFiQlB+8tcsMeXpXf2PFE5X2pjk4Gm43hQSMHAdA==" + }, "@types/istanbul-lib-coverage": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", @@ -1883,6 +2239,16 @@ "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==", "dev": true }, + "@types/loader-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/loader-utils/-/loader-utils-1.1.3.tgz", + "integrity": "sha512-euKGFr2oCB3ASBwG39CYJMR3N9T0nanVqXdiH7Zu/Nqddt6SmFRxytq/i2w9LQYNQekEtGBz+pE3qG6fQTNvRg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/webpack": "*" + } + }, "@types/lodash": { "version": "4.14.144", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.144.tgz", @@ -1927,14 +2293,12 @@ "@types/prop-types": { "version": "15.7.3", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", - "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==", - "dev": true + "integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==" }, "@types/react": { "version": "16.9.11", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.11.tgz", "integrity": "sha512-UBT4GZ3PokTXSWmdgC/GeCGEJXE5ofWyibCcecRLUVN2ZBpXQGVgQGtG2foS7CrTKFKlQVVswLvf7Js6XA/CVQ==", - "dev": true, "requires": { "@types/prop-types": "*", "csstype": "^2.2.0" @@ -1961,6 +2325,12 @@ "redux": "^4.0.0" } }, + "@types/schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-YesPanU1+WCigC/Aj1Mga8UCOjHIfMNHZ3zzDsUY7lI8GlKnh/Kv2QwJOQ+jNQ36Ru7IfzSedlG14hppYaN13A==", + "dev": true + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", @@ -2519,6 +2889,12 @@ "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", "dev": true }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -2937,6 +3313,16 @@ } } }, + "babel-plugin-const-enum": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-const-enum/-/babel-plugin-const-enum-0.0.2.tgz", + "integrity": "sha512-jTa4A/b2sTM++neJnNZRi6CL7imluFkepD7mB+IpndQy/5LKwPpuoIfSY0nC94Y/nnxjhNRAW2fdBgT5dI4/+w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-syntax-typescript": "^7.3.3" + } + }, "babel-plugin-dynamic-import-node": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz", @@ -2967,6 +3353,206 @@ "@types/babel__traverse": "^7.0.6" } }, + "babel-plugin-react-intl": { + "version": "5.1.18", + "resolved": "https://registry.npmjs.org/babel-plugin-react-intl/-/babel-plugin-react-intl-5.1.18.tgz", + "integrity": "sha512-tzzZoGDNQOiHmGFh+NPQJDpC10RbKlfw1CBVfALulqRa6UGkAv5eMs9sirxjhD3HryHPbYZ4x5FNdbzOyG2GJw==", + "dev": true, + "requires": { + "@babel/core": "^7.7.2", + "@babel/helper-plugin-utils": "^7.0.0", + "@types/babel__core": "^7.1.3", + "@types/schema-utils": "^1.0.0", + "fs-extra": "^8.1.0", + "intl-messageformat-parser": "^3.6.4", + "schema-utils": "^2.2.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", + "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.8.3" + } + }, + "@babel/core": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.8.4.tgz", + "integrity": "sha512-0LiLrB2PwrVI+a2/IEskBopDYSd8BCb3rOvH7D5tzoWd696TBEduBvuLVm4Nx6rltrLZqvI3MCalB2K2aVzQjA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.4", + "@babel/helpers": "^7.8.4", + "@babel/parser": "^7.8.4", + "@babel/template": "^7.8.3", + "@babel/traverse": "^7.8.4", + "@babel/types": "^7.8.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.0", + "lodash": "^4.17.13", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + } + }, + "@babel/generator": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.8.4.tgz", + "integrity": "sha512-PwhclGdRpNAf3IxZb0YVuITPZmmrXz9zf6fH8lT4XbrmfQKr6ryBzhv593P5C6poJRciFCL/eHGW2NuGrgEyxA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3", + "jsesc": "^2.5.1", + "lodash": "^4.17.13", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz", + "integrity": "sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.8.3", + "@babel/template": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz", + "integrity": "sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz", + "integrity": "sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA==", + "dev": true, + "requires": { + "@babel/types": "^7.8.3" + } + }, + "@babel/helpers": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.8.4.tgz", + "integrity": "sha512-VPbe7wcQ4chu4TDQjimHv/5tj73qz88o12EPkO2ValS2QiQS/1F2SsjyIGNnAD0vF/nZS6Cf9i+vW6HIlnaR8w==", + "dev": true, + "requires": { + "@babel/template": "^7.8.3", + "@babel/traverse": "^7.8.4", + "@babel/types": "^7.8.3" + } + }, + "@babel/highlight": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", + "dev": true, + "requires": { + "chalk": "^2.0.0", + "esutils": "^2.0.2", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.8.4.tgz", + "integrity": "sha512-0fKu/QqildpXmPVaRBoXOlyBb3MC+J0A66x97qEfLOMkn3u6nfY5esWogQwi/K0BjASYy4DbnsEWnpNL6qT5Mw==", + "dev": true + }, + "@babel/template": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.8.3.tgz", + "integrity": "sha512-04m87AcQgAFdvuoyiQ2kgELr2tV8B4fP/xJAVUL3Yb3bkNdMedD3d0rlSQr3PegP0cms3eHjl1F7PWlvWbU8FQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/parser": "^7.8.3", + "@babel/types": "^7.8.3" + } + }, + "@babel/traverse": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.8.4.tgz", + "integrity": "sha512-NGLJPZwnVEyBPLI+bl9y9aSnxMhsKz42so7ApAv9D+b4vAFPpY013FTS9LdKxcABoIYFU52HcYga1pPlx454mg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.8.3", + "@babel/generator": "^7.8.4", + "@babel/helper-function-name": "^7.8.3", + "@babel/helper-split-export-declaration": "^7.8.3", + "@babel/parser": "^7.8.4", + "@babel/types": "^7.8.3", + "debug": "^4.1.0", + "globals": "^11.1.0", + "lodash": "^4.17.13" + } + }, + "@babel/types": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.8.3.tgz", + "integrity": "sha512-jBD+G8+LWpMBBWvVcdr4QysjUE4mU/syrhN17o1u3gx0/WzJB1kwiVZAXRtWbsIPOwW8pF/YJV5+nmetPzepXg==", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "lodash": "^4.17.13", + "to-fast-properties": "^2.0.0" + } + }, + "ajv": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", + "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.1.tgz", + "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", + "dev": true + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, + "schema-utils": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.4.tgz", + "integrity": "sha512-VNjcaUxVnEeun6B2fiiUDjXXBtD4ZSH7pdbfIu1pOFwgptDPLMo/z9jr4sUfsjFVPqDCEin/F7IYlq7/E6yDbQ==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1" + } + } + } + }, "babel-polyfill": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", @@ -4518,8 +5104,7 @@ "csstype": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.7.tgz", - "integrity": "sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ==", - "dev": true + "integrity": "sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ==" }, "cuint": { "version": "0.2.2", @@ -4527,6 +5112,15 @@ "integrity": "sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs=", "dev": true }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "^1.0.1" + } + }, "cycle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", @@ -6239,6 +6833,25 @@ "readable-stream": "^2.0.0" } }, + "fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz", + "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", + "dev": true + } + } + }, "fs-write-stream-atomic": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", @@ -6841,6 +7454,12 @@ "lodash.padstart": "^4.1.0" } }, + "gensync": { + "version": "1.0.0-beta.1", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", + "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", + "dev": true + }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -7793,6 +8412,33 @@ "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.7.0.tgz", "integrity": "sha512-Id0Fij0HsB/vKWGeBe9PxeY45ttRiBmhFyyt/geBdDHBYNctMRTE3dC1U3ujzz3lap+hVXlEcVaB56kZP/eEUg==" }, + "intl-format-cache": { + "version": "4.2.21", + "resolved": "https://registry.npmjs.org/intl-format-cache/-/intl-format-cache-4.2.21.tgz", + "integrity": "sha512-6pZlBdqTRUuuwRWywPItHY1JQwzQxWcpBHv6w4M8T6bGzAsiL/QmI+XsdOhsqJLaL4ZmTATn1kIkNlMk4VzSLQ==" + }, + "intl-locales-supported": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/intl-locales-supported/-/intl-locales-supported-1.8.4.tgz", + "integrity": "sha512-wO0JhDqhshhkq8Pa9CLcstqd1aCXjfMgfMzjD6mDreS3mTSDbjGiMU+07O8BdJGxed7Q0Wf3TFVjGq0W3Y0n1w==" + }, + "intl-messageformat": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-7.8.4.tgz", + "integrity": "sha512-yS0cLESCKCYjseCOGXuV4pxJm/buTfyCJ1nzQjryHmSehlptbZbn9fnlk1I9peLopZGGbjj46yHHiTAEZ1qOTA==", + "requires": { + "intl-format-cache": "^4.2.21", + "intl-messageformat-parser": "^3.6.4" + } + }, + "intl-messageformat-parser": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/intl-messageformat-parser/-/intl-messageformat-parser-3.6.4.tgz", + "integrity": "sha512-RgPGwue0mJtoX2Ax8EmMzJzttxjnva7gx0Q7mKJ4oALrTZvtmCeAw5Msz2PcjW4dtCh/h7vN/8GJCxZO1uv+OA==", + "requires": { + "@formatjs/intl-unified-numberformat": "^3.2.0" + } + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -9892,6 +10538,15 @@ "minimist": "^1.2.0" } }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, "jsonify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", @@ -10695,6 +11350,16 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "loud-rejection": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-2.2.0.tgz", + "integrity": "sha512-S0FayMXku80toa5sZ6Ro4C+s+EtFDCsyJNG/AzFMfX3AxD5Si4dZsgzm/kKnbOxHl5Cv8jBlno8+3XYIh2pNjQ==", + "dev": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.2" + } + }, "lower-case": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", @@ -13094,6 +13759,37 @@ "unpipe": "1.0.0" } }, + "react-intl": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-3.12.0.tgz", + "integrity": "sha512-VQWkFYSKKoi85p3gOXgG80KkBImdBJXwJxssO9gqdelW/fuVnxQLXgYOKuOqWrUz5beXK+qBve6bTpblh1ep2g==", + "requires": { + "@formatjs/intl-displaynames": "^1.2.0", + "@formatjs/intl-listformat": "^1.3.7", + "@formatjs/intl-relativetimeformat": "^4.5.7", + "@formatjs/intl-unified-numberformat": "^3.0.4", + "@formatjs/intl-utils": "^2.0.4", + "@formatjs/macro": "^0.2.6", + "@types/hoist-non-react-statics": "^3.3.1", + "@types/invariant": "^2.2.31", + "hoist-non-react-statics": "^3.3.1", + "intl-format-cache": "^4.2.19", + "intl-locales-supported": "^1.8.4", + "intl-messageformat": "^7.8.2", + "intl-messageformat-parser": "^3.6.2", + "shallow-equal": "^1.2.1" + }, + "dependencies": { + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + } + } + }, "react-is": { "version": "16.8.6", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", @@ -13906,6 +14602,11 @@ "safe-buffer": "^5.0.1" } }, + "shallow-equal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", + "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==" + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -15955,6 +16656,12 @@ "unist-util-is": "^3.0.0" } }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true + }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index ca0b27d131..2a05237054 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,10 +6,12 @@ "start": "webpack-dev-server --mode development", "check": "tsc -p tsconfig.json --noEmit --skipLibCheck", "lint": "eslint --max-warnings=0 --ext=.ts,.tsx,.js,.jsx .", - "lint:style": "stylelint \"**/*.scss\" \"**/*.pcss\" \"**/*.css\" \"iframe.html\"", + "lint:style": "stylelint '**/*.scss' '**/*.pcss' '**/*.css' 'iframe.html'", "test": "jest", "test:coverage": "jest --coverage", - "prettier": "prettier --write \"./**/*.{js,jsx,ts,tsx,scss}\"" + "prettier": "prettier --write './**/*.{js,jsx,ts,tsx,scss}'", + "extract-messages": "formatjs extract --out-file=./extracted-messages/messages.json './app/**/*.js' './app/**/*.jsx' './app/**/*.tsx' './app/**/*.ts'", + "generate-lang": "npm run extract-messages && node ./tasks/generateDictionary.js" }, "husky": { "hooks": { @@ -24,6 +26,7 @@ "@babel/plugin-transform-react-jsx": "^7.3.0", "@babel/preset-env": "^7.6.3", "@babel/preset-react": "^7.6.3", + "@formatjs/cli": "^1.1.20", "@types/cheerio": "^0.22.13", "@types/core-js": "^2.5.2", "@types/enzyme": "^3.10.4", @@ -102,6 +105,7 @@ "intersection-observer": "^0.7.0", "node-emoji": "^1.10.0", "preact": "^10.0.1", + "react-intl": "^3.12.0", "react-redux": "^7.1.1", "redux": "^4.0.4", "redux-thunk": "^2.3.0" diff --git a/frontend/tasks/generateDictionary.js b/frontend/tasks/generateDictionary.js new file mode 100644 index 0000000000..677bb5e3cb --- /dev/null +++ b/frontend/tasks/generateDictionary.js @@ -0,0 +1,30 @@ +const fs = require('fs'); +const path = require('path'); +const defaultMessages = require('../extracted-messages/messages'); + +const locales = ['en', 'ru']; + +const keyMessagePairs = []; +const keysSet = new Set(); +defaultMessages.forEach(({ id, defaultMessage }) => { + keyMessagePairs.push([id, defaultMessage]); + keysSet.add(id); +}); + +function removeAbandonedKeys(existKeys, dictionary) { + return Object.fromEntries(Object.entries(dictionary).filter(([key]) => existKeys.has(key))); +} + +locales.forEach(locale => { + let currentDict = {}; + const pathToDict = path.resolve(__dirname, `../app/locales/${locale}.json`); + if (fs.existsSync(pathToDict)) { + currentDict = require(pathToDict); + } + keyMessagePairs.forEach(([key, defaultMessage]) => { + if (!currentDict[key]) { + currentDict[key] = defaultMessage; + } + }); + fs.writeFileSync(pathToDict, JSON.stringify(removeAbandonedKeys(keysSet, currentDict), null, 2)); +}); diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index a831df3c2e..46053048ae 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -16,6 +16,7 @@ "baseUrl": "./", "alwaysStrict": true, "sourceMap": true, + "resolveJsonModule": true, "forceConsistentCasingInFileNames": true, "paths": { "*": ["@types/*"],