From 4fb1fd606736f238bc02c6a2d026fad8b213fe18 Mon Sep 17 00:00:00 2001 From: Thibaut Sardan <33178835+Tbaut@users.noreply.github.com> Date: Wed, 30 Sep 2020 21:19:29 +0200 Subject: [PATCH] Add i18n language change dropdown (#467) * add dropdown and make it work * clean up * Update packages/extension-ui/src/util/languages.ts * lint * remove lang detection * rename and clean up getLanguage options util --- packages/extension-ui/package.json | 1 - packages/extension-ui/src/createView.tsx | 2 +- packages/extension-ui/src/i18n/Backend.ts | 47 +++++++++++++++++++ packages/extension-ui/src/i18n/cache.ts | 6 +++ packages/extension-ui/src/{ => i18n}/i18n.ts | 22 ++++++--- .../src/partials/MenuSettings.tsx | 24 +++++++++- .../src/util/getLanguageOptions.ts | 24 ++++++++++ yarn.lock | 10 ---- 8 files changed, 117 insertions(+), 19 deletions(-) create mode 100644 packages/extension-ui/src/i18n/Backend.ts create mode 100644 packages/extension-ui/src/i18n/cache.ts rename packages/extension-ui/src/{ => i18n}/i18n.ts (55%) create mode 100644 packages/extension-ui/src/util/getLanguageOptions.ts diff --git a/packages/extension-ui/package.json b/packages/extension-ui/package.json index 474b765d..c48fcdc6 100644 --- a/packages/extension-ui/package.json +++ b/packages/extension-ui/package.json @@ -14,7 +14,6 @@ "@polkadot/util": "^3.4.1", "@polkadot/util-crypto": "^3.4.1", "i18next": "^19.7.0", - "i18next-browser-languagedetector": "^5.0.1", "react": "^16.13.1", "react-dom": "^16.13.1", "react-dropzone": "^11.1.0", diff --git a/packages/extension-ui/src/createView.tsx b/packages/extension-ui/src/createView.tsx index 7dc452f7..86ca8b10 100644 --- a/packages/extension-ui/src/createView.tsx +++ b/packages/extension-ui/src/createView.tsx @@ -7,7 +7,7 @@ import { HashRouter } from 'react-router-dom'; import { Fonts, View } from './components'; -import './i18n'; +import './i18n/i18n'; export default function createView (Entry: React.ComponentType, rootId = 'root'): void { const rootElement = document.getElementById(rootId); diff --git a/packages/extension-ui/src/i18n/Backend.ts b/packages/extension-ui/src/i18n/Backend.ts new file mode 100644 index 00000000..29e3b42e --- /dev/null +++ b/packages/extension-ui/src/i18n/Backend.ts @@ -0,0 +1,47 @@ +// Copyright 2017-2020 @polkadot/react-components authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import languageCache from './cache'; + +type Callback = (error: string | null, data: unknown) => void; + +type LoadResult = [string | null, Record | boolean]; + +const loaders: Record> = {}; + +export default class Backend { + type = 'backend' + + static type: 'backend' = 'backend' + + async read (lng: string, _namespace: string, responder: Callback): Promise { + if (languageCache[lng]) { + return responder(null, languageCache[lng]); + } + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + if (!loaders[lng]) { + loaders[lng] = this.createLoader(lng); + } + + const [error, data] = await loaders[lng]; + + return responder(error, data); + } + + async createLoader (lng: string): Promise { + try { + const response = await fetch(`locales/${lng}/translation.json`, {}); + + if (!response.ok) { + return [`i18n: failed loading ${lng}`, response.status >= 500 && response.status < 600]; + } else { + languageCache[lng] = await response.json() as Record; + + return [null, languageCache[lng]]; + } + } catch (error) { + return [(error as Error).message, false]; + } + } +} diff --git a/packages/extension-ui/src/i18n/cache.ts b/packages/extension-ui/src/i18n/cache.ts new file mode 100644 index 00000000..0264ab89 --- /dev/null +++ b/packages/extension-ui/src/i18n/cache.ts @@ -0,0 +1,6 @@ +// Copyright 2017-2020 @polkadot/react-components authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +const languageCache: Record> = {}; + +export default languageCache; diff --git a/packages/extension-ui/src/i18n.ts b/packages/extension-ui/src/i18n/i18n.ts similarity index 55% rename from packages/extension-ui/src/i18n.ts rename to packages/extension-ui/src/i18n/i18n.ts index 36b11eaa..862c89cd 100644 --- a/packages/extension-ui/src/i18n.ts +++ b/packages/extension-ui/src/i18n/i18n.ts @@ -1,13 +1,15 @@ // Copyright 2019-2020 @polkadot/extension-ui authors & contributors // SPDX-License-Identifier: Apache-2.0 -import i18n from 'i18next'; -import LanguageDetector from 'i18next-browser-languagedetector'; +import i18next from 'i18next'; import { initReactI18next } from 'react-i18next'; +import uiSettings from '@polkadot/ui-settings'; -i18n - .use(LanguageDetector) +import Backend from './Backend'; + +i18next .use(initReactI18next) + .use(Backend) .init({ backend: {}, debug: false, @@ -16,6 +18,7 @@ i18n escapeValue: false }, keySeparator: false, + lng: uiSettings.i18nLang, load: 'languageOnly', nsSeparator: false, react: { @@ -24,6 +27,13 @@ i18n returnEmptyString: false, returnNull: false }) - .catch(console.error); + .catch((error: Error): void => + console.log('i18n: failure', error) + ); -export default i18n; +uiSettings.on('change', (settings): void => { + i18next.changeLanguage(settings.i18nLang + ).catch(console.error); +}); + +export default i18next; diff --git a/packages/extension-ui/src/partials/MenuSettings.tsx b/packages/extension-ui/src/partials/MenuSettings.tsx index d4d11f66..d41f3642 100644 --- a/packages/extension-ui/src/partials/MenuSettings.tsx +++ b/packages/extension-ui/src/partials/MenuSettings.tsx @@ -3,7 +3,7 @@ import { Theme, ThemeProps } from '../types'; -import React, { useCallback, useContext, useState, useEffect } from 'react'; +import React, { useCallback, useContext, useEffect, useState, useMemo } from 'react'; import styled, { ThemeContext } from 'styled-components'; import settings from '@polkadot/ui-settings'; @@ -11,6 +11,7 @@ import FullScreenIcon from '../assets/fullscreen.svg'; import { ActionText, Checkbox, Dropdown, Menu, MenuDivider, MenuItem, Svg, Switch, ThemeSwitchContext, themes } from '../components'; import useTranslation from '../hooks/useTranslation'; import { windowOpen } from '../messaging'; +import getLanguageOptions from '../util/getLanguageOptions'; interface Option { text: string; @@ -33,6 +34,7 @@ function MenuSettings ({ className, reference }: Props): React.ReactElement(ThemeContext); const setTheme = useContext(ThemeSwitchContext); + const languageOptions = useMemo(() => getLanguageOptions(), []); useEffect(() => { settings.set({ camera: camera ? 'on' : 'off' }); @@ -51,6 +53,13 @@ function MenuSettings ({ className, reference }: Props): React.ReactElement { + settings.set({ i18nLang: value }); + }, + [] + ); + return ( + + ('Language')} + > + + {isPopup && ( <> diff --git a/packages/extension-ui/src/util/getLanguageOptions.ts b/packages/extension-ui/src/util/getLanguageOptions.ts new file mode 100644 index 00000000..1ee6cfaf --- /dev/null +++ b/packages/extension-ui/src/util/getLanguageOptions.ts @@ -0,0 +1,24 @@ +// Copyright 2019-2020 @polkadot/extension-ui authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +interface Option { + info?: string; + isDisabled?: boolean; + isHeader?: boolean; + text: React.ReactNode; + value: string | number; +} + +export default function getLanguageOptions (): Option[] { + return [ + // default/native + { + text: 'English', + value: 'en' + }, + { + text: '汉语', + value: 'zh' + } + ]; +} diff --git a/yarn.lock b/yarn.lock index c67033b3..ff83a326 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3073,7 +3073,6 @@ __metadata: enzyme: ^3.11.0 enzyme-adapter-react-16: ^1.15.4 i18next: ^19.7.0 - i18next-browser-languagedetector: ^5.0.1 react: ^16.13.1 react-dom: ^16.13.1 react-dropzone: ^11.1.0 @@ -11913,15 +11912,6 @@ fsevents@^1.2.7: languageName: node linkType: hard -"i18next-browser-languagedetector@npm:^5.0.1": - version: 5.0.1 - resolution: "i18next-browser-languagedetector@npm:5.0.1" - dependencies: - "@babel/runtime": ^7.5.5 - checksum: 4e80cc054cb1d1600c421d53153b7752aaca07a3ca9f394d64962aa34f901c3e61d2a409d76436c1d76ea8bb1e6ab99a4888dcfbfc3a8de0a489abcd2bb6563d - languageName: node - linkType: hard - "i18next-scanner@npm:^2.11.0": version: 2.11.0 resolution: "i18next-scanner@npm:2.11.0"