From 9577ada17138dccfc60d8de7cea6082509ee8c10 Mon Sep 17 00:00:00 2001 From: Jeremy Letto Date: Sat, 26 Feb 2022 21:36:53 -0600 Subject: [PATCH] Webatrice: i18n (#4562) * implement i18n capability * reset package.lock file * remove custom fallback * fix relative path for i18n files * check for language support before fetch request * add LanguageDropdown component, es translation file to prove functionality * remove boilerplate * bundle default english translation with app * add missing file * rollup component-level i18n files * cleanup Co-authored-by: Jeremy Letto --- webclient/package-lock.json | 51 +++++- webclient/package.json | 3 + webclient/prebuild.js | 47 +++++- .../public/locales/es-ES/translation.json | 4 + webclient/src/common.i18n.json | 6 + .../CountryDropdown/CountryDropdown.css | 3 +- .../LanguageDropdown/LanguageDropdown.css | 19 +++ .../LanguageDropdown/LanguageDropdown.tsx | 51 ++++++ webclient/src/components/index.ts | 1 + webclient/src/containers/Account/Account.css | 8 +- webclient/src/containers/Account/Account.tsx | 153 +++++++++--------- webclient/src/containers/App/AppShell.tsx | 28 ++-- webclient/src/containers/Login/Login.css | 6 +- .../src/containers/Login/Login.i18n.json | 6 + webclient/src/containers/Login/Login.tsx | 16 +- webclient/src/i18n-backend.ts | 21 +++ webclient/src/i18n-default.json | 1 + webclient/src/i18n.ts | 30 ++++ webclient/src/index.tsx | 7 +- webclient/src/translations/en.json | 3 - webclient/src/types/index.ts | 1 + webclient/src/types/languages.ts | 11 ++ 22 files changed, 365 insertions(+), 111 deletions(-) create mode 100644 webclient/public/locales/es-ES/translation.json create mode 100644 webclient/src/common.i18n.json create mode 100644 webclient/src/components/LanguageDropdown/LanguageDropdown.css create mode 100644 webclient/src/components/LanguageDropdown/LanguageDropdown.tsx create mode 100644 webclient/src/containers/Login/Login.i18n.json create mode 100644 webclient/src/i18n-backend.ts create mode 100644 webclient/src/i18n-default.json create mode 100644 webclient/src/i18n.ts delete mode 100644 webclient/src/translations/en.json create mode 100644 webclient/src/types/languages.ts diff --git a/webclient/package-lock.json b/webclient/package-lock.json index 9759dcd9..7d7cc621 100644 --- a/webclient/package-lock.json +++ b/webclient/package-lock.json @@ -7352,9 +7352,9 @@ } }, "follow-redirects": { - "version": "1.14.8", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", - "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==" + "version": "1.14.7", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", + "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==" }, "for-in": { "version": "1.0.2", @@ -7929,6 +7929,14 @@ "terser": "^4.6.3" } }, + "html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "requires": { + "void-elements": "3.1.0" + } + }, "html-webpack-plugin": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.5.0.tgz", @@ -8193,6 +8201,22 @@ "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz", "integrity": "sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==" }, + "i18next": { + "version": "21.6.12", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-21.6.12.tgz", + "integrity": "sha512-xlGTPdu2g5PZEUIE6TA1mQ9EIAAv9nMFONzgwAIrKL/KTmYYWufQNGgOmp5Og1PvgUji+6i1whz0rMdsz1qaKw==", + "requires": { + "@babel/runtime": "^7.12.0" + } + }, + "i18next-browser-languagedetector": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-6.1.3.tgz", + "integrity": "sha512-T+oGXHXtrur14CGnZZ7qQ07X38XJQEI00b/4ILrtO6xPbwTlQ1wtMZC2H+tBULixHuVUXv8LKbxfjyITJkezUg==", + "requires": { + "@babel/runtime": "^7.14.6" + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -13198,6 +13222,16 @@ "@babel/runtime": "^7.12.5" } }, + "react-i18next": { + "version": "11.15.5", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.15.5.tgz", + "integrity": "sha512-vBWuVEQgrhZrGKpyv8FmJ7Zs5jRQWl794Tte7yzJ0okZqqi3jd6j2pLYNg441WcREsbIOvWdiDXbY7W6E93p1A==", + "requires": { + "@babel/runtime": "^7.14.5", + "html-escaper": "^2.0.2", + "html-parse-stringify": "^3.0.1" + } + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -15740,9 +15774,9 @@ } }, "url-parse": { - "version": "1.5.7", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.7.tgz", - "integrity": "sha512-HxWkieX+STA38EDk7CE9MEryFeHCKzgagxlGvsdS7WBImq9Mk+PGwiT56w82WI3aicwJA8REp42Cxo98c8FZMA==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz", + "integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==", "requires": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" @@ -15851,6 +15885,11 @@ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" }, + "void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=" + }, "w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", diff --git a/webclient/package.json b/webclient/package.json index 643a7ef4..8c077478 100644 --- a/webclient/package.json +++ b/webclient/package.json @@ -13,6 +13,8 @@ "dexie": "^3.0.3", "final-form": "^4.20.4", "final-form-set-field-touched": "^1.0.1", + "i18next": "^21.6.10", + "i18next-browser-languagedetector": "^6.1.3", "jquery": "^3.6.0", "lodash": "^4.17.21", "prop-types": "^15.7.2", @@ -21,6 +23,7 @@ "react-dom": "^17.0.2", "react-final-form": "^6.5.7", "react-final-form-listeners": "^1.0.3", + "react-i18next": "^11.15.3", "react-redux": "^7.2.6", "react-router-dom": "^5.3.0", "react-scripts": "4.0.3", diff --git a/webclient/prebuild.js b/webclient/prebuild.js index fde45996..46f511e6 100644 --- a/webclient/prebuild.js +++ b/webclient/prebuild.js @@ -1,16 +1,23 @@ const fse = require('fs-extra'); +const path = require('path'); const util = require('util'); const exec = util.promisify(require('child_process').exec); -const protoFilesDir = './public/pb'; -const serverPropsFile = './src/server-props.json'; -const masterProtoFile = './src/proto-files.json'; +const ROOT_DIR = './src'; +const PUBLIC_DIR = './public'; + +const protoFilesDir = `${PUBLIC_DIR}/pb`; +const i18nDefaultFile = `${ROOT_DIR}/i18n-default.json`; +const serverPropsFile = `${ROOT_DIR}/server-props.json`; +const masterProtoFile = `${ROOT_DIR}/proto-files.json`; const sharedFiles = [ ['../common/pb', protoFilesDir], - ['../cockatrice/resources/countries', './src/images/countries'], + ['../cockatrice/resources/countries', `${ROOT_DIR}/images/countries`], ]; +const i18nFileRegex = /\.i18n\.json$/; + (async () => { // make sure these files finish copying before master file is created @@ -18,6 +25,7 @@ const sharedFiles = [ createMasterProtoFile(); createServerProps(); + createI18NDefault(); })(); async function copySharedFiles() { @@ -53,6 +61,37 @@ async function createServerProps() { } } +async function createI18NDefault() { + try { + const files = getAllFiles(ROOT_DIR, i18nFileRegex); + const allJson = await Promise.all(files.map(file => fse.readJson(file))); + + const rollup = allJson.reduce((acc, json) => ({ + ...acc, + ...json, + }), {}); + + fse.outputFile(i18nDefaultFile, JSON.stringify(rollup)); + } catch (e) { + console.error(e); + process.exitCode = 1; + } +} + async function getCommitHash() { return (await exec('git rev-parse HEAD')).stdout.trim(); } + +function getAllFiles(dirPath, regex = /./, allFiles = []) { + return fse.readdirSync(dirPath).reduce((files, file) => { + const filePath = dirPath + "/" + file; + + if (fse.statSync(filePath).isDirectory()) { + files.concat(getAllFiles(filePath, regex, files)); + } else if (regex.test(file)) { + files.push(path.join(__dirname, filePath)); + } + + return files; + }, allFiles); +} diff --git a/webclient/public/locales/es-ES/translation.json b/webclient/public/locales/es-ES/translation.json new file mode 100644 index 00000000..887d24a1 --- /dev/null +++ b/webclient/public/locales/es-ES/translation.json @@ -0,0 +1,4 @@ +{ + "Common": { "disconnect": "Desconectar"}, + "LoginContainer": { "title": "Acceso" } +} diff --git a/webclient/src/common.i18n.json b/webclient/src/common.i18n.json new file mode 100644 index 00000000..019ea3cd --- /dev/null +++ b/webclient/src/common.i18n.json @@ -0,0 +1,6 @@ +{ + "Common": { + "language": "Translate into English.", + "disconnect": "Disconnect" + } +} diff --git a/webclient/src/components/CountryDropdown/CountryDropdown.css b/webclient/src/components/CountryDropdown/CountryDropdown.css index c9537add..9b167951 100644 --- a/webclient/src/components/CountryDropdown/CountryDropdown.css +++ b/webclient/src/components/CountryDropdown/CountryDropdown.css @@ -4,9 +4,10 @@ .CountryDropdown-item { display: flex; + align-items: center; } .CountryDropdown-item__image { - width: 1.1em; + width: 1.5em; margin-right: 1em; } diff --git a/webclient/src/components/LanguageDropdown/LanguageDropdown.css b/webclient/src/components/LanguageDropdown/LanguageDropdown.css new file mode 100644 index 00000000..031e1f5f --- /dev/null +++ b/webclient/src/components/LanguageDropdown/LanguageDropdown.css @@ -0,0 +1,19 @@ +.LanguageDropdown { +} + +.LanguageDropdown-item { + display: flex; + align-items: center; +} + +.LanguageDropdown-item__image { + width: 1.5em; +} + +.MuiSelect-root .LanguageDropdown-item__label { + display: none; +} + +.MuiListItem-root .LanguageDropdown-item__image { + margin-right: 1em; +} diff --git a/webclient/src/components/LanguageDropdown/LanguageDropdown.tsx b/webclient/src/components/LanguageDropdown/LanguageDropdown.tsx new file mode 100644 index 00000000..002f2695 --- /dev/null +++ b/webclient/src/components/LanguageDropdown/LanguageDropdown.tsx @@ -0,0 +1,51 @@ +// eslint-disable-next-line +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Select, MenuItem } from '@material-ui/core'; +import FormControl from '@material-ui/core/FormControl'; +import InputLabel from '@material-ui/core/InputLabel'; + +import { Images } from 'images/Images'; +import { CountryLabel, Language, LanguageCountry } from 'types'; + +import './LanguageDropdown.css'; + +const LanguageDropdown = () => { + const { i18n } = useTranslation(); + const [language, setLanguage] = useState(i18n.resolvedLanguage); + + useEffect(() => { + if (language !== i18n.resolvedLanguage) { + i18n.changeLanguage(language); + } + }, [language]); + + return ( + + + + ) +}; + +export default LanguageDropdown; diff --git a/webclient/src/components/index.ts b/webclient/src/components/index.ts index 3526176f..031e19b4 100644 --- a/webclient/src/components/index.ts +++ b/webclient/src/components/index.ts @@ -6,6 +6,7 @@ export { default as Header } from './Header/Header'; export { default as InputField } from './InputField/InputField'; export { default as InputAction } from './InputAction/InputAction'; export { default as KnownHosts } from './KnownHosts/KnownHosts'; +export { default as LanguageDropdown } from './LanguageDropdown/LanguageDropdown'; export { default as Message } from './Message/Message'; export { default as VirtualList } from './VirtualList/VirtualList'; export { default as UserDisplay } from './UserDisplay/UserDisplay'; diff --git a/webclient/src/containers/Account/Account.css b/webclient/src/containers/Account/Account.css index 70455dd7..6b5c995a 100644 --- a/webclient/src/containers/Account/Account.css +++ b/webclient/src/containers/Account/Account.css @@ -43,7 +43,11 @@ font-size: 10px; } -.account-details img { +.account-details > img { width: 100%; margin-bottom: 20px; -} \ No newline at end of file +} + +.account-details__lang { + margin-top: 20px; +} diff --git a/webclient/src/containers/Account/Account.tsx b/webclient/src/containers/Account/Account.tsx index 03a62720..395b10d5 100644 --- a/webclient/src/containers/Account/Account.tsx +++ b/webclient/src/containers/Account/Account.tsx @@ -1,12 +1,13 @@ // eslint-disable-next-line import React, { Component } from "react"; +import { useTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import Button from '@material-ui/core/Button'; import ListItem from '@material-ui/core/ListItem'; import Paper from '@material-ui/core/Paper'; -import { UserDisplay, VirtualList, AuthGuard } from 'components'; +import { UserDisplay, VirtualList, AuthGuard, LanguageDropdown } from 'components'; import { AuthenticationService, SessionService } from 'api'; import { ServerSelectors } from 'store'; import { User } from 'types'; @@ -16,85 +17,87 @@ import AddToIgnore from './AddToIgnore'; import './Account.css'; -class Account extends Component { - handleAddToBuddies({ userName }) { +const Account = (props: AccountProps) => { + const { buddyList, ignoreList, serverName, serverVersion, user } = props; + const { country, realName, name, userLevel, accountageSecs, avatarBmp } = user; + let url = URL.createObjectURL(new Blob([avatarBmp], { 'type': 'image/png' })); + + const { t } = useTranslation(); + + const handleAddToBuddies = ({ userName }) => { SessionService.addToBuddyList(userName); - } + }; - handleAddToIgnore({ userName }) { + const handleAddToIgnore = ({ userName }) => { SessionService.addToIgnoreList(userName); - } + }; - render() { - console.log(this.props); - - const { buddyList, ignoreList, serverName, serverVersion, user } = this.props; - const { country, realName, name, userLevel, accountageSecs, avatarBmp } = user; - - let url = URL.createObjectURL(new Blob([avatarBmp], { 'type': 'image/png' })); - - return ( -
- -
- -
- Buddies Online: ?/{buddyList.length} -
- buddyList[index].name } - items={ buddyList.map(user => ( - - - - )) } - /> -
- -
-
-
-
- -
- Ignored Users Online: ?/{ignoreList.length} -
- ignoreList[index].name } - items={ ignoreList.map(user => ( - - - - )) } - /> -
- -
-
-
-
- - {name} -

{name}

-

Location: ({country?.toUpperCase()})

-

User Level: {userLevel}

-

Account Age: {accountageSecs}

-

Real Name: {realName}

-
- - - -
-
- -

Server Name: {serverName}

-

Server Version: {serverVersion}

- -
-
+ return ( +
+ +
+ +
+ Buddies Online: ?/{buddyList.length} +
+ buddyList[index].name } + items={ buddyList.map(user => ( + + + + )) } + /> +
+ +
+
- ) - } +
+ +
+ Ignored Users Online: ?/{ignoreList.length} +
+ ignoreList[index].name } + items={ ignoreList.map(user => ( + + + + )) } + /> +
+ +
+
+
+
+ + {name} +

{name}

+

Location: ({country?.toUpperCase()})

+

User Level: {userLevel}

+

Account Age: {accountageSecs}

+

Real Name: {realName}

+
+ + + +
+ +
+ +

Server Name: {serverName}

+

Server Version: {serverVersion}

+ + +
+ +
+
+
+
+ ) } interface AccountProps { diff --git a/webclient/src/containers/App/AppShell.tsx b/webclient/src/containers/App/AppShell.tsx index 14ee0077..beec032e 100644 --- a/webclient/src/containers/App/AppShell.tsx +++ b/webclient/src/containers/App/AppShell.tsx @@ -1,4 +1,4 @@ -import { Component } from 'react'; +import { Component, Suspense } from 'react'; import { Provider } from 'react-redux'; import { MemoryRouter as Router } from 'react-router-dom'; import CssBaseline from '@material-ui/core/CssBaseline'; @@ -23,19 +23,21 @@ class AppShell extends Component { render() { return ( - - - -
- -
+ + + + +
+ +
- - - -
-
-
+ + + +
+
+
+ ); } } diff --git a/webclient/src/containers/Login/Login.css b/webclient/src/containers/Login/Login.css index 3bb97690..7166187a 100644 --- a/webclient/src/containers/Login/Login.css +++ b/webclient/src/containers/Login/Login.css @@ -185,11 +185,15 @@ margin-top: 30px; } -.login-footer_register { +.login-footer__register { margin-bottom: 10px; font-weight: bold; } +.login-footer__language { + margin-top: 20px; +} + .login-content__connectionStatus { text-align: center; margin: 20px 0; diff --git a/webclient/src/containers/Login/Login.i18n.json b/webclient/src/containers/Login/Login.i18n.json new file mode 100644 index 00000000..fecbd3f8 --- /dev/null +++ b/webclient/src/containers/Login/Login.i18n.json @@ -0,0 +1,6 @@ +{ + "LoginContainer": { + "title": "Login", + "subtitle": "A cross-platform virtual tabletop for multiplayer card games." + } +} diff --git a/webclient/src/containers/Login/Login.tsx b/webclient/src/containers/Login/Login.tsx index f107feb8..04153c98 100644 --- a/webclient/src/containers/Login/Login.tsx +++ b/webclient/src/containers/Login/Login.tsx @@ -1,4 +1,5 @@ import { useState, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { connect } from 'react-redux'; import { Redirect } from 'react-router-dom'; import { makeStyles } from '@material-ui/core/styles'; @@ -6,9 +7,9 @@ import Button from '@material-ui/core/Button'; import Paper from '@material-ui/core/Paper'; import Typography from '@material-ui/core/Typography'; - import { AuthenticationService } from 'api'; import { RegistrationDialog, RequestPasswordResetDialog, ResetPasswordDialog, AccountActivationDialog } from 'dialogs'; +import { LanguageDropdown } from 'components'; import { LoginForm } from 'forms'; import { useReduxEffect, useFireOnce } from 'hooks'; import { Images } from 'images'; @@ -58,6 +59,8 @@ const useStyles = makeStyles(theme => ({ const Login = ({ state, description }: LoginProps) => { const classes = useStyles(); + const { t } = useTranslation(); + const isConnected = AuthenticationService.isConnected(state); const [hostIdToRemember, setHostIdToRemember] = useState(null); @@ -239,8 +242,8 @@ const Login = ({ state, description }: LoginProps) => { logo COCKATRICE
- Login - A cross-platform virtual tabletop for multiplayer card games. + { t('LoginContainer.title') } + { t('LoginContainer.subtitle') }
{ }
-
+
Not registered yet?
Cockatrice is an open source project. { new Date().getUTCFullYear() } + { serverProps.REACT_APP_VERSION && ( @@ -272,6 +276,10 @@ const Login = ({ state, description }: LoginProps) => { ) } + +
+ +
diff --git a/webclient/src/i18n-backend.ts b/webclient/src/i18n-backend.ts new file mode 100644 index 00000000..d270b350 --- /dev/null +++ b/webclient/src/i18n-backend.ts @@ -0,0 +1,21 @@ +import { ModuleType } from 'i18next'; + +import { Language } from 'types'; + +class I18nBackend { + static type: ModuleType = 'backend'; + static BASE_URL = `${process.env.PUBLIC_URL}/locales`; + + read(language, namespace, callback) { + if (!Language[language]) { + callback(true, null); + return; + } + + fetch(`${I18nBackend.BASE_URL}/${Language[language]}/${namespace}.json`) + .then(resp => resp.json().then(json => callback(null, json))) + .catch(error => callback(error, null)); + } +} + +export default I18nBackend; diff --git a/webclient/src/i18n-default.json b/webclient/src/i18n-default.json new file mode 100644 index 00000000..690e5780 --- /dev/null +++ b/webclient/src/i18n-default.json @@ -0,0 +1 @@ +{"Common":{"language":"Translate into English.","disconnect":"Disconnect"},"LoginContainer":{"title":"Login","subtitle":"A cross-platform virtual tabletop for multiplayer card games."}} \ No newline at end of file diff --git a/webclient/src/i18n.ts b/webclient/src/i18n.ts new file mode 100644 index 00000000..8c805354 --- /dev/null +++ b/webclient/src/i18n.ts @@ -0,0 +1,30 @@ +import i18n from 'i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import { initReactI18next } from 'react-i18next'; + +import { Language } from 'types'; + +import I18nBackend from './i18n-backend'; + +// Bundle default translation with application +import translation from './i18n-default.json'; + +i18n + .use(I18nBackend) + .use(LanguageDetector) + .use(initReactI18next) + // for all options read: https://www.i18next.com/overview/configuration-options + .init({ + fallbackLng: Language['en-US'], + resources: { + [Language['en-US']]: { translation }, + }, + partialBundledLanguages: true, + + interpolation: { + // not needed for react as it escapes by default + escapeValue: false, + }, + }); + +export default i18n; diff --git a/webclient/src/index.tsx b/webclient/src/index.tsx index e165d57f..8cf04158 100644 --- a/webclient/src/index.tsx +++ b/webclient/src/index.tsx @@ -1,12 +1,15 @@ import { ThemeProvider } from '@material-ui/styles'; import React from 'react'; import ReactDOM from 'react-dom'; -import './index.css'; -import { materialTheme } from './material-theme'; import { AppShell } from 'containers'; +import { materialTheme } from './material-theme'; +import './i18n'; + +import './index.css'; + const appWithMaterialTheme = () => ( diff --git a/webclient/src/translations/en.json b/webclient/src/translations/en.json deleted file mode 100644 index 544b7b4d..00000000 --- a/webclient/src/translations/en.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} \ No newline at end of file diff --git a/webclient/src/types/index.ts b/webclient/src/types/index.ts index 16889eed..24b3c27a 100644 --- a/webclient/src/types/index.ts +++ b/webclient/src/types/index.ts @@ -11,3 +11,4 @@ export * from './sort'; export * from './forms'; export * from './message'; export * from './settings'; +export * from './languages'; diff --git a/webclient/src/types/languages.ts b/webclient/src/types/languages.ts new file mode 100644 index 00000000..4b3cb06a --- /dev/null +++ b/webclient/src/types/languages.ts @@ -0,0 +1,11 @@ +import { CountryLabel } from './countries'; + +export enum Language { + 'en-US' = 'en-US', + 'es-ES' = 'es-ES', +} + +export enum LanguageCountry { + 'en-US' = 'us', + 'es-ES' = 'es', +}