diff --git a/package-lock.json b/package-lock.json index 8959f8609..d0377fe32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "eventemitter3": "4.0.7", "filter-invalid-dom-props": "2.1.0", "hat": "0.0.3", + "i18next": "^22.4.3", "langs": "^2.0.0", "lodash.debounce": "4.0.8", "lodash.intersection": "4.4.0", @@ -32,8 +33,10 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-focus-lock": "2.9.1", + "react-i18next": "^12.1.1", "react-is": "18.2.0", "spatial-navigation-polyfill": "git+https://git@github.com/Stremio/spatial-navigation.git#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", + "stremio-translations": "git+https://git@github.com/Stremio/stremio-translations.git#ef047329f5bcb0a8f96008fca05c68b449b34cb1", "url": "0.11.0" }, "devDependencies": { @@ -7009,6 +7012,14 @@ "node": ">= 12" } }, + "node_modules/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==", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-webpack-plugin": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", @@ -7153,6 +7164,39 @@ "node": ">=10.17.0" } }, + "node_modules/i18next": { + "version": "22.4.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.3.tgz", + "integrity": "sha512-rnAabD3+i/rMzdg85Eq4VkZjy0Uxe33J1069IQ4R6+cpcM+wL4lWMRClfSweINA0QEfqzSdsfsyLO7SnGAF4fg==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.20.6" + } + }, + "node_modules/i18next/node_modules/@babel/runtime": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz", + "integrity": "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==", + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -11920,6 +11964,27 @@ } } }, + "node_modules/react-i18next": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.1.1.tgz", + "integrity": "sha512-mFdieOI0LDy84q3JuZU6Aou1DoWW2fhapcTGeBS8+vWSJuViuoCLQAMYSb0QoHhXS8B0WKUOPpx4cffAP7r/aA==", + "dependencies": { + "@babel/runtime": "^7.14.5", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 19.0.0", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -11982,9 +12047,9 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "node_modules/regenerator-transform": { "version": "0.14.5", @@ -12781,6 +12846,12 @@ "node": ">= 0.6" } }, + "node_modules/stremio-translations": { + "version": "1.43.15", + "resolved": "git+https://git@github.com/Stremio/stremio-translations.git#ef047329f5bcb0a8f96008fca05c68b449b34cb1", + "integrity": "sha512-cG1GZIbCy2xWQpyYXJvlvnkWLtMBypL+BhMFJmC9LRHteAmJU++t37b5Wc6Sm1IbWoXqG508Tc9YfQ28bHTzaQ==", + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -13702,6 +13773,14 @@ "resolved": "https://registry.npmjs.org/video-name-parser/-/video-name-parser-1.4.6.tgz", "integrity": "sha512-ZdeYjh8X4ms1EzjY/UoiTZ6JWbi8SYyOPGY0jESSLq2BAmdc5sZHi+F8J19Qz0y7H1WSpaltojsCkO1p2dH4YA==" }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/vtt.js": { "version": "0.13.0", "resolved": "git+ssh://git@github.com/jaruba/vtt.js.git#e4f5f5603730866bacb174a93f51b734c9f29e6a", @@ -20111,6 +20190,14 @@ } } }, + "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": "5.5.0", "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz", @@ -20212,6 +20299,24 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, + "i18next": { + "version": "22.4.3", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.3.tgz", + "integrity": "sha512-rnAabD3+i/rMzdg85Eq4VkZjy0Uxe33J1069IQ4R6+cpcM+wL4lWMRClfSweINA0QEfqzSdsfsyLO7SnGAF4fg==", + "requires": { + "@babel/runtime": "^7.20.6" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.6.tgz", + "integrity": "sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + } + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -23691,6 +23796,15 @@ "use-sidecar": "^1.1.2" } }, + "react-i18next": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-12.1.1.tgz", + "integrity": "sha512-mFdieOI0LDy84q3JuZU6Aou1DoWW2fhapcTGeBS8+vWSJuViuoCLQAMYSb0QoHhXS8B0WKUOPpx4cffAP7r/aA==", + "requires": { + "@babel/runtime": "^7.14.5", + "html-parse-stringify": "^3.0.1" + } + }, "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -23741,9 +23855,9 @@ } }, "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, "regenerator-transform": { "version": "0.14.5", @@ -24393,6 +24507,11 @@ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", "dev": true }, + "stremio-translations": { + "version": "git+https://git@github.com/Stremio/stremio-translations.git#ef047329f5bcb0a8f96008fca05c68b449b34cb1", + "integrity": "sha512-cG1GZIbCy2xWQpyYXJvlvnkWLtMBypL+BhMFJmC9LRHteAmJU++t37b5Wc6Sm1IbWoXqG508Tc9YfQ28bHTzaQ==", + "from": "stremio-translations@git+https://git@github.com/Stremio/stremio-translations.git#ef047329f5bcb0a8f96008fca05c68b449b34cb1" + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -25067,6 +25186,11 @@ "resolved": "https://registry.npmjs.org/video-name-parser/-/video-name-parser-1.4.6.tgz", "integrity": "sha512-ZdeYjh8X4ms1EzjY/UoiTZ6JWbi8SYyOPGY0jESSLq2BAmdc5sZHi+F8J19Qz0y7H1WSpaltojsCkO1p2dH4YA==" }, + "void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==" + }, "vtt.js": { "version": "git+ssh://git@github.com/jaruba/vtt.js.git#e4f5f5603730866bacb174a93f51b734c9f29e6a", "integrity": "sha512-RXV60lPGrmjuRcV/jRuydLC2thMaMlmK4Vc3DtBmVSotFA3986sgW0H5AH9IUmHzQo4bFR2gELYLcfwVh7Dqow==", diff --git a/package.json b/package.json index f570411c1..e01a49d35 100755 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "eventemitter3": "4.0.7", "filter-invalid-dom-props": "2.1.0", "hat": "0.0.3", + "i18next": "^22.4.3", "langs": "^2.0.0", "lodash.debounce": "4.0.8", "lodash.intersection": "4.4.0", @@ -35,8 +36,10 @@ "react": "18.2.0", "react-dom": "18.2.0", "react-focus-lock": "2.9.1", + "react-i18next": "^12.1.1", "react-is": "18.2.0", "spatial-navigation-polyfill": "git+https://git@github.com/Stremio/spatial-navigation.git#64871b1422466f5f45d24ebc8bbd315b2ebab6a6", + "stremio-translations": "git+https://git@github.com/Stremio/stremio-translations.git#ef047329f5bcb0a8f96008fca05c68b449b34cb1", "url": "0.11.0" }, "devDependencies": { diff --git a/src/App/App.js b/src/App/App.js index 1735cee0d..3344d5645 100644 --- a/src/App/App.js +++ b/src/App/App.js @@ -2,6 +2,7 @@ require('spatial-navigation-polyfill'); const React = require('react'); +const { useTranslation } = require('react-i18next'); const { Router } = require('stremio-router'); const { Core, Shell, Chromecast, DragAndDrop, KeyboardShortcuts, ServicesProvider } = require('stremio/services'); const { NotFound } = require('stremio/routes'); @@ -13,6 +14,7 @@ const routerViewsConfig = require('./routerViewsConfig'); const styles = require('./styles'); const App = () => { + const { i18n } = useTranslation(); const onPathNotMatch = React.useCallback(() => { return NotFound; }, []); @@ -90,6 +92,21 @@ const App = () => { }; }, []); React.useEffect(() => { + const onCoreEvent = ({ event, args }) => { + switch (event) { + case 'SettingsUpdated': { + if (args && args.settings && typeof args.settings.interfaceLanguage === 'string') { + i18n.changeLanguage(args.settings.interfaceLanguage); + } + break; + } + } + }; + const onCtxState = (state) => { + if (state && state.profile && state.profile.settings && typeof state.profile.settings.interfaceLanguage === 'string') { + i18n.changeLanguage(state.profile.settings.interfaceLanguage); + } + }; const onWindowFocus = () => { services.core.transport.dispatch({ action: 'Ctx', @@ -113,8 +130,16 @@ const App = () => { if (services.core.active) { onWindowFocus(); window.addEventListener('focus', onWindowFocus); + services.core.transport.on('CoreEvent', onCoreEvent); + services.core.transport + .getState('ctx') + .then(onCtxState) + .catch((e) => console.error(e)); } - return () => window.removeEventListener('focus', onWindowFocus); + return () => { + window.removeEventListener('focus', onWindowFocus); + services.core.transport.off('CoreEvent', onCoreEvent); + }; }, [initialized]); return ( diff --git a/src/common/ColorInput/ColorInput.js b/src/common/ColorInput/ColorInput.js index 1c5d450bc..c60f02a23 100644 --- a/src/common/ColorInput/ColorInput.js +++ b/src/common/ColorInput/ColorInput.js @@ -4,6 +4,7 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const AColorPicker = require('a-color-picker'); +const { useTranslation } = require('react-i18next'); const Button = require('stremio/common/Button'); const ModalDialog = require('stremio/common/ModalDialog'); const useBinaryState = require('stremio/common/useBinaryState'); @@ -16,6 +17,7 @@ const parseColor = (value) => { }; const ColorInput = ({ className, value, dataset, onChange, ...props }) => { + const { t } = useTranslation(); const [modalOpen, openModal, closeModal] = useBinaryState(false); const [tempValue, setTempValue] = React.useState(() => { return parseColor(value); @@ -69,11 +71,11 @@ const ColorInput = ({ className, value, dataset, onChange, ...props }) => { setTempValue(parseColor(value)); }, [value, modalOpen]); return ( - ))} diff --git a/src/common/MetaPreview/MetaPreview.js b/src/common/MetaPreview/MetaPreview.js index f9ec1bef3..a0c3d9fd8 100644 --- a/src/common/MetaPreview/MetaPreview.js +++ b/src/common/MetaPreview/MetaPreview.js @@ -4,6 +4,7 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); const UrlUtils = require('url'); +const { useTranslation } = require('react-i18next'); const Icon = require('@stremio/stremio-icons/dom'); const Button = require('stremio/common/Button'); const Image = require('stremio/common/Image'); @@ -24,6 +25,7 @@ const ALLOWED_LINK_REDIRECTS = [ ]; const MetaPreview = ({ className, compact, name, logo, background, runtime, releaseInfo, released, description, deepLinks, links, trailerStreams, inLibrary, toggleInLibrary }) => { + const { t } = useTranslation(); const [shareModalOpen, openShareModal, closeShareModal] = useBinaryState(false); const linksGroups = React.useMemo(() => { return Array.isArray(links) ? @@ -192,7 +194,7 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele @@ -204,7 +206,7 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele @@ -216,7 +218,7 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele @@ -229,13 +231,13 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele { shareModalOpen ? - + { + const { t } = useTranslation(); return (
{ @@ -24,8 +26,8 @@ const MetaRow = ({ className, title, message, items, itemComponent, deepLinks }) } { deepLinks && (typeof deepLinks.discover === 'string' || typeof deepLinks.library === 'string') ? - : diff --git a/src/common/MetaRow/MetaRowPlaceholder/MetaRowPlaceholder.js b/src/common/MetaRow/MetaRowPlaceholder/MetaRowPlaceholder.js index f49c84c17..469faa349 100644 --- a/src/common/MetaRow/MetaRowPlaceholder/MetaRowPlaceholder.js +++ b/src/common/MetaRow/MetaRowPlaceholder/MetaRowPlaceholder.js @@ -3,12 +3,14 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); +const { useTranslation } = require('react-i18next'); const Icon = require('@stremio/stremio-icons/dom'); const Button = require('stremio/common/Button'); const CONSTANTS = require('stremio/common/CONSTANTS'); const styles = require('./styles'); const MetaRowPlaceholder = ({ className, title, deepLinks }) => { + const { t } = useTranslation(); return (
@@ -17,8 +19,8 @@ const MetaRowPlaceholder = ({ className, title, deepLinks }) => {
{ deepLinks && typeof deepLinks.discover === 'string' ? - : diff --git a/src/common/NavBar/HorizontalNavBar/HorizontalNavBar.js b/src/common/NavBar/HorizontalNavBar/HorizontalNavBar.js index 2d27f36fe..006983a9e 100644 --- a/src/common/NavBar/HorizontalNavBar/HorizontalNavBar.js +++ b/src/common/NavBar/HorizontalNavBar/HorizontalNavBar.js @@ -10,6 +10,7 @@ const useFullscreen = require('stremio/common/useFullscreen'); const SearchBar = require('./SearchBar'); const NavMenu = require('./NavMenu'); const styles = require('./styles'); +const { t } = require('i18next'); const HorizontalNavBar = React.memo(({ className, route, query, title, backButton, searchBar, addonsButton, fullscreenButton, navMenu, ...props }) => { const backButtonOnClick = React.useCallback(() => { @@ -54,7 +55,7 @@ const HorizontalNavBar = React.memo(({ className, route, query, title, backButto
{ addonsButton ? - : @@ -62,7 +63,7 @@ const HorizontalNavBar = React.memo(({ className, route, query, title, backButto } { fullscreenButton ? - : diff --git a/src/common/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js b/src/common/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js index efcef4feb..a6c095224 100644 --- a/src/common/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js +++ b/src/common/NavBar/HorizontalNavBar/NavMenu/NavMenuContent.js @@ -3,6 +3,7 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); +const { useTranslation } = require('react-i18next'); const Icon = require('@stremio/stremio-icons/dom'); const { useServices } = require('stremio/services'); const Button = require('stremio/common/Button'); @@ -13,6 +14,7 @@ const { withCoreSuspender } = require('stremio/common/CoreSuspender'); const styles = require('./styles'); const NavMenuContent = ({ onClick }) => { + const { t } = useTranslation(); const { core } = useServices(); const profile = useProfile(); const { createTorrentFromMagnet } = useTorrent(); @@ -46,45 +48,45 @@ const NavMenuContent = ({ onClick }) => { }} />
-
{profile.auth === null ? 'Anonymous user' : profile.auth.user.email}
+
{profile.auth === null ? t('ANONYMOUS_USER') : profile.auth.user.email}
-
-
- - - -
- - -
diff --git a/src/common/NavBar/HorizontalNavBar/SearchBar/SearchBar.js b/src/common/NavBar/HorizontalNavBar/SearchBar/SearchBar.js index dd76c8e84..34a38e050 100644 --- a/src/common/NavBar/HorizontalNavBar/SearchBar/SearchBar.js +++ b/src/common/NavBar/HorizontalNavBar/SearchBar/SearchBar.js @@ -3,6 +3,7 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); +const { useTranslation } = require('react-i18next'); const Icon = require('@stremio/stremio-icons/dom'); const { useRouteFocused } = require('stremio-router'); const Button = require('stremio/common/Button'); @@ -12,6 +13,7 @@ const { withCoreSuspender } = require('stremio/common/CoreSuspender'); const styles = require('./styles'); const SearchBar = ({ className, query, active }) => { + const { t } = useTranslation(); const routeFocused = useRouteFocused(); const { createTorrentFromMagnet } = useTorrent(); const searchInputRef = React.useRef(null); @@ -46,7 +48,7 @@ const SearchBar = ({ className, query, active }) => { ref={searchInputRef} className={styles['search-input']} type={'text'} - placeholder={'Search or paste link'} + placeholder={t('SEARCH_OR_PASTE_LINK')} defaultValue={query} tabIndex={-1} onChange={queryInputOnChange} @@ -54,7 +56,7 @@ const SearchBar = ({ className, query, active }) => { /> :
-
Search or paste link
+
{ t('SEARCH_OR_PASTE_LINK') }
} - -); +const SearchBarFallback = ({ className }) => { + const { t } = useTranslation(); + return ( + + ); +}; SearchBarFallback.propTypes = SearchBar.propTypes; diff --git a/src/common/NavBar/VerticalNavBar/VerticalNavBar.js b/src/common/NavBar/VerticalNavBar/VerticalNavBar.js index 4c0ef099c..6e48963d2 100644 --- a/src/common/NavBar/VerticalNavBar/VerticalNavBar.js +++ b/src/common/NavBar/VerticalNavBar/VerticalNavBar.js @@ -3,10 +3,12 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); +const { useTranslation } = require('react-i18next'); const NavTabButton = require('./NavTabButton'); const styles = require('./styles'); const VerticalNavBar = React.memo(({ className, selected, tabs }) => { + const { t } = useTranslation(); return (
diff --git a/src/common/StreamingServerWarning/StreamingServerWarning.js b/src/common/StreamingServerWarning/StreamingServerWarning.js index b0775b2e6..f095ce1d1 100644 --- a/src/common/StreamingServerWarning/StreamingServerWarning.js +++ b/src/common/StreamingServerWarning/StreamingServerWarning.js @@ -4,12 +4,14 @@ const React = require('react'); const { useServices } = require('stremio/services'); const PropTypes = require('prop-types'); const classnames = require('classnames'); +const { useTranslation } = require('react-i18next'); const Button = require('stremio/common/Button'); const useProfile = require('stremio/common/useProfile'); const { withCoreSuspender } = require('stremio/common/CoreSuspender'); const styles = require('./styles'); const StreamingServerWarning = ({ className }) => { + const { t } = useTranslation(); const { core } = useServices(); const profile = useProfile(); const onLaterClick = React.useCallback(() => { @@ -48,12 +50,12 @@ const StreamingServerWarning = ({ className }) => { return (
-
Streaming server is not available.
- -
); diff --git a/src/common/index.js b/src/common/index.js index 22dcad52a..82fcd579e 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -27,8 +27,10 @@ const comparatorWithPriorities = require('./comparatorWithPriorities'); const CONSTANTS = require('./CONSTANTS'); const { withCoreSuspender, useCoreSuspender } = require('./CoreSuspender'); const getVisibleChildrenRange = require('./getVisibleChildrenRange'); +const interfaceLanguages = require('./interfaceLanguages'); const languageNames = require('./languageNames'); const routesRegexp = require('./routesRegexp'); +const translateOption = require('./translateOption'); const useAnimationFrame = require('./useAnimationFrame'); const useBinaryState = require('./useBinaryState'); const useFullscreen = require('./useFullscreen'); @@ -70,8 +72,10 @@ module.exports = { withCoreSuspender, useCoreSuspender, getVisibleChildrenRange, + interfaceLanguages, languageNames, routesRegexp, + translateOption, useAnimationFrame, useBinaryState, useFullscreen, diff --git a/src/common/interfaceLanguages.json b/src/common/interfaceLanguages.json new file mode 100644 index 000000000..235193f0d --- /dev/null +++ b/src/common/interfaceLanguages.json @@ -0,0 +1,150 @@ +[ + { + "name": "العربية", + "codes": ["ar-AR", "ara"] + }, + { + "name": "български език", + "codes": ["bg-BG", "bul"] + }, + { + "name": "català", + "codes": ["ca-CA", "cat"] + }, + { + "name": "čeština", + "codes": ["cs-CZ", "ces"] + }, + { + "name": "dansk", + "codes": ["da-DK", "dan"] + }, + { + "name": "Deutsch", + "codes": ["de-DE", "deu"] + }, + { + "name": "ελληνικά", + "codes": ["el-GR", "ell"] + }, + { + "name": "English", + "codes": ["en-US", "eng"] + }, + { + "name": "Esperanto", + "codes": ["eo-EO", "epo"] + }, + { + "name": "español", + "codes": ["es-ES", "spa"] + }, + { + "name": "euskara", + "codes": ["eu-ES", "eus"] + }, + { + "name": "فارسی", + "codes": ["fa-IR", "fas"] + }, + { + "name": "Français", + "codes": ["fr-FR", "fre"] + }, + { + "name": "עברית", + "codes": ["he-IL", "heb"] + }, + { + "name": "हिन्दी", + "codes": ["hi-IN", "hin"] + }, + { + "name": "hrvatski jezik", + "codes": ["hr-HR", "hrv"] + }, + { + "name": "magyar", + "codes": ["hu-HU", "hun"] + }, + { + "name": "Bahasa Indonesia", + "codes": ["id-ID", "ind"] + }, + { + "name": "italiano", + "codes": ["it-IT", "ita"] + }, + { + "name": "македонски јазик", + "codes": ["mk-MK", "mkd"] + }, + { + "name": "ဗမာစာ", + "codes": ["my-BM", "mya"] + }, + { + "name": "Norsk bokmål", + "codes": ["nb-NO", "nob"] + }, + { + "name": "Nederlands", + "codes": ["nl-NL", "nld"] + }, + { + "name": "Norsk nynorsk", + "codes": ["nn-NO", "nno"] + }, + { + "name": "język polski", + "codes": ["pl-PL", "pol"] + }, + { + "name": "português Brazil", + "codes": ["pt-BR", "por"] + }, + { + "name": "português", + "codes": ["pt-PT", "por"] + }, + { + "name": "русский язык", + "codes": ["ru-RU", "rus"] + }, + { + "name": "Svenska", + "codes": ["sv-SE", "swe"] + }, + { + "name": "slovenski jezik", + "codes": ["sl-SL", "slv"] + }, + { + "name": "српски језик", + "codes": ["sr-RS", "srp"] + }, + { + "name": "తెలుగు", + "codes": ["te-IN", "tel"] + }, + { + "name": "Türkçe", + "codes": ["tr-TR", "tur"] + }, + { + "name": "українська мова", + "codes": ["uk-UA", "ukr"] + }, + { + "name": "中文(中华人民共和国)", + "codes": ["zh-CN", "zho"] + }, + { + "name": "中文(香港特别行政區)", + "codes": ["zh-HK", "zho"] + }, + { + "name": "中文(台灣)", + "codes": ["zh-TW", "zho"] + } +] \ No newline at end of file diff --git a/src/common/translateOption.js b/src/common/translateOption.js new file mode 100644 index 000000000..085306515 --- /dev/null +++ b/src/common/translateOption.js @@ -0,0 +1,15 @@ +// Copyright (C) 2017-2022 Smart code 203358507 + +const { t } = require('i18next'); + +const translateOption = (option, translateKeyPrefix = '') => { + const translateKey = `${translateKeyPrefix}${option}`; + const translateValue = t(translateKey, { + defaultValue: t(translateKey.toUpperCase(), { + defaultValue: null + }) + }); + return translateValue ?? option.charAt(0).toUpperCase() + option.slice(1); +}; + +module.exports = translateOption; diff --git a/src/index.js b/src/index.js index 27840b0d0..007665568 100755 --- a/src/index.js +++ b/src/index.js @@ -13,8 +13,26 @@ if (browser?.platform?.type === 'desktop') { const React = require('react'); const ReactDOM = require('react-dom/client'); +const i18n = require('i18next'); +const { initReactI18next } = require('react-i18next'); +const stremioTranslations = require('stremio-translations'); const App = require('./App'); +const translations = Object.fromEntries(Object.entries(stremioTranslations()).map(([key, value]) => [key, { + translation: value +}])); + +i18n + .use(initReactI18next) + .init({ + resources: translations, + lng: 'en-US', + fallbackLng: 'en-US', + interpolation: { + escapeValue: false + } + }); + const root = ReactDOM.createRoot(document.getElementById('app')); root.render(); diff --git a/src/routes/Addons/Addon/Addon.js b/src/routes/Addons/Addon/Addon.js index a330f9d8d..fed54fcfb 100644 --- a/src/routes/Addons/Addon/Addon.js +++ b/src/routes/Addons/Addon/Addon.js @@ -3,11 +3,13 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); +const { useTranslation } = require('react-i18next'); const Icon = require('@stremio/stremio-icons/dom'); const { Button, Image } = require('stremio/common'); const styles = require('./styles'); const Addon = ({ className, id, name, version, logo, description, types, installed, onToggle, onShare, dataset }) => { + const { t } = useTranslation(); const toggleButtonOnClick = React.useCallback((event) => { if (typeof onToggle === 'function') { onToggle({ @@ -82,12 +84,12 @@ const Addon = ({ className, id, name, version, logo, description, types, install }
- -
diff --git a/src/routes/Addons/Addons.js b/src/routes/Addons/Addons.js index aa513daaf..cddeb980a 100644 --- a/src/routes/Addons/Addons.js +++ b/src/routes/Addons/Addons.js @@ -3,6 +3,7 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); +const { useTranslation } = require('react-i18next'); const Icon = require('@stremio/stremio-icons/dom'); const { AddonDetailsModal, Button, Image, Multiselect, MainNavBars, TextInput, SearchBar, SharePrompt, ModalDialog, useBinaryState, withCoreSuspender } = require('stremio/common'); const Addon = require('./Addon'); @@ -13,6 +14,7 @@ const useSelectableInputs = require('./useSelectableInputs'); const styles = require('./styles'); const Addons = ({ urlParams, queryParams }) => { + const { t } = useTranslation(); const installedAddons = useInstalledAddons(urlParams); const remoteAddons = useRemoteAddons(urlParams); const [addonDetailsTransportUrl, setAddonDetailsTransportUrl] = useAddonDetailsTransportUrl(urlParams, queryParams); @@ -29,13 +31,13 @@ const Addons = ({ urlParams, queryParams }) => { return [ { className: styles['cancel-button'], - label: 'Cancel', + label: t('BUTTON_CANCEL'), props: { onClick: closeAddAddonModal } }, { - label: 'Add', + label: t('ADDON_ADD'), props: { onClick: addAddonOnSubmit } @@ -78,9 +80,9 @@ const Addons = ({ urlParams, queryParams }) => {
- {selectInputs.map((selectInput, index) => ( {
@@ -192,15 +194,15 @@ const Addons = ({ urlParams, queryParams }) => { addAddonModalOpen ? -
You can add an addon via an external link, which will appear under Installed addons.
+
{ t('ADD_ADDON_DESCRIPTION') }
@@ -212,7 +214,7 @@ const Addons = ({ urlParams, queryParams }) => { sharedAddon !== null ?
{ const catalogSelect = { - title: 'Select catalog', + title: t('SELECT_CATALOG'), options: remoteAddons.selectable.catalogs .concat(installedAddons.selectable.catalogs) .map(({ name, deepLinks }) => ({ value: deepLinks.addons, - label: name, - title: name + label: translateOption(name, 'ADDON_'), + title: translateOption(name, 'ADDON_'), })), selected: remoteAddons.selectable.catalogs .concat(installedAddons.selectable.catalogs) @@ -20,7 +22,7 @@ const mapSelectableInputs = (installedAddons, remoteAddons) => { () => { const selectableCatalog = remoteAddons.selectable.catalogs .find(({ id }) => id === remoteAddons.selected.request.path.id); - return selectableCatalog ? selectableCatalog.name : remoteAddons.selected.request.path.id; + return selectableCatalog ? translateOption(selectableCatalog.name, 'ADDON_') : remoteAddons.selected.request.path.id; } : null, @@ -29,16 +31,16 @@ const mapSelectableInputs = (installedAddons, remoteAddons) => { } }; const typeSelect = { - title: 'Select type', + title: t('SELECT_TYPE'), options: installedAddons.selected !== null ? installedAddons.selectable.types.map(({ type, deepLinks }) => ({ value: deepLinks.addons, - label: type !== null ? type : 'All' + label: type !== null ? translateOption(type, 'TYPE_') : t('TYPE_ALL') })) : remoteAddons.selectable.types.map(({ type, deepLinks }) => ({ value: deepLinks.addons, - label: type + label: translateOption(type, 'TYPE_') })), selected: installedAddons.selected !== null ? installedAddons.selectable.types @@ -51,12 +53,12 @@ const mapSelectableInputs = (installedAddons, remoteAddons) => { renderLabelText: () => { return installedAddons.selected !== null ? installedAddons.selected.request.type === null ? - 'All' + t('TYPE_ALL') : - installedAddons.selected.request.type + translateOption(installedAddons.selected.request.type, 'TYPE_') : remoteAddons.selected !== null ? - remoteAddons.selected.request.path.type + translateOption(remoteAddons.selected.request.path.type, 'TYPE_') : typeSelect.title; }, diff --git a/src/routes/Board/Board.js b/src/routes/Board/Board.js index d01666448..02c708c5c 100644 --- a/src/routes/Board/Board.js +++ b/src/routes/Board/Board.js @@ -3,6 +3,7 @@ const React = require('react'); const classnames = require('classnames'); const debounce = require('lodash.debounce'); +const { useTranslation } = require('react-i18next'); const { MainNavBars, MetaRow, LibItem, MetaItem, StreamingServerWarning, useStreamingServer, withCoreSuspender, getVisibleChildrenRange } = require('stremio/common'); const useBoard = require('./useBoard'); const useContinueWatchingPreview = require('./useContinueWatchingPreview'); @@ -11,6 +12,7 @@ const styles = require('./styles'); const THRESHOLD = 5; const Board = () => { + const { t } = useTranslation(); const streamingServer = useStreamingServer(); const continueWatchingPreview = useContinueWatchingPreview(); const [board, loadBoardRows] = useBoard(); @@ -42,7 +44,7 @@ const Board = () => { continueWatchingPreview.libraryItems.length > 0 ? { +const mapSelectableInputs = (discover, t) => { const typeSelect = { - title: 'Select type', + title: t('SELECT_TYPE'), options: discover.selectable.types .map(({ type, deepLinks }) => ({ value: deepLinks.discover, - label: type + label: translateOption(type, 'TYPE_') })), selected: discover.selectable.types .filter(({ selected }) => selected) .map(({ deepLinks }) => deepLinks.discover), renderLabelText: discover.selected !== null ? - () => discover.selected.request.path.type + () => translateOption(discover.selected.request.path.type, 'TYPE_') : null, onSelect: (event) => { @@ -22,7 +24,7 @@ const mapSelectableInputs = (discover) => { } }; const catalogSelect = { - title: 'Select catalog', + title: t('SELECT_CATALOG'), options: discover.selectable.catalogs .map(({ name, addon, deepLinks }) => ({ value: deepLinks.discover, @@ -45,10 +47,10 @@ const mapSelectableInputs = (discover) => { } }; const extraSelects = discover.selectable.extra.map(({ name, isRequired, options }) => ({ - title: `Select ${name}`, + title: translateOption(name, 'SELECT_'), isRequired: isRequired, options: options.map(({ value, deepLinks }) => ({ - label: typeof value === 'string' ? value : 'None', + label: typeof value === 'string' ? translateOption(value) : t('NONE'), value: JSON.stringify({ href: deepLinks.discover, value @@ -61,7 +63,7 @@ const mapSelectableInputs = (discover) => { value })), renderLabelText: options.some(({ selected, value }) => selected && value === null) ? - () => `Select ${name}` + () => translateOption(name, 'SELECT_') : null, onSelect: (event) => { @@ -73,8 +75,9 @@ const mapSelectableInputs = (discover) => { }; const useSelectableInputs = (discover) => { + const { t } = useTranslation(); const selectableInputs = React.useMemo(() => { - return mapSelectableInputs(discover); + return mapSelectableInputs(discover, t); }, [discover.selected, discover.selectable]); return selectableInputs; }; diff --git a/src/routes/Library/useSelectableInputs.js b/src/routes/Library/useSelectableInputs.js index 6f3d58d7e..e63ad811b 100644 --- a/src/routes/Library/useSelectableInputs.js +++ b/src/routes/Library/useSelectableInputs.js @@ -1,14 +1,16 @@ // Copyright (C) 2017-2022 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); +const { translateOption } = require('stremio/common'); -const mapSelectableInputs = (library) => { +const mapSelectableInputs = (library, t) => { const typeSelect = { - title: 'Select type', + title: t('SELECT_TYPE'), options: library.selectable.types .map(({ type, deepLinks }) => ({ value: deepLinks.library, - label: type === null ? 'All' : type + label: type === null ? t('TYPE_ALL') : translateOption(type, 'TYPE_') })), selected: library.selectable.types .filter(({ selected }) => selected) @@ -18,11 +20,11 @@ const mapSelectableInputs = (library) => { } }; const sortSelect = { - title: 'Select sort', + title: t('SELECT_SORT'), options: library.selectable.sorts .map(({ sort, deepLinks }) => ({ value: deepLinks.library, - label: sort + label: translateOption(sort, 'SORT_') })), selected: library.selectable.sorts .filter(({ selected }) => selected) @@ -49,8 +51,9 @@ const mapSelectableInputs = (library) => { }; const useSelectableInputs = (library) => { + const { t } = useTranslation(); const selectableInputs = React.useMemo(() => { - return mapSelectableInputs(library); + return mapSelectableInputs(library, t); }, [library]); return selectableInputs; }; diff --git a/src/routes/MetaDetails/StreamsList/StreamsList.js b/src/routes/MetaDetails/StreamsList/StreamsList.js index 4c061ef73..853e337b3 100644 --- a/src/routes/MetaDetails/StreamsList/StreamsList.js +++ b/src/routes/MetaDetails/StreamsList/StreamsList.js @@ -3,6 +3,7 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); +const { useTranslation } = require('react-i18next'); const Icon = require('@stremio/stremio-icons/dom'); const { Button, Image, Multiselect } = require('stremio/common'); const { useServices } = require('stremio/services'); @@ -12,6 +13,7 @@ const styles = require('./styles'); const ALL_ADDONS_KEY = 'ALL'; const StreamsList = ({ className, ...props }) => { + const { t } = useTranslation(); const { core } = useServices(); const [selectedAddon, setSelectedAddon] = React.useState(ALL_ADDONS_KEY); const onAddonSelected = React.useCallback((event) => { @@ -55,8 +57,8 @@ const StreamsList = ({ className, ...props }) => { options: [ { value: ALL_ADDONS_KEY, - label: 'All', - title: 'All' + label: t('ALL_ADDONS'), + title: t('ALL_ADDONS') }, ...Object.keys(streamsByAddon).map((transportUrl) => ({ value: transportUrl, @@ -80,7 +82,7 @@ const StreamsList = ({ className, ...props }) => { props.streams.every((streams) => streams.content.type === 'Err') ?
{' -
No streams were found!
+
{t('NO_STREAM')}
: filteredStreams.length === 0 ? @@ -115,9 +117,9 @@ const StreamsList = ({ className, ...props }) => {
} -
); diff --git a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js index 59141db05..14a4004f5 100644 --- a/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js +++ b/src/routes/MetaDetails/VideosList/SeasonsBar/SeasonsBar.js @@ -3,6 +3,7 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); +const { t } = require('i18next'); const Icon = require('@stremio/stremio-icons/dom'); const { Button, Multiselect } = require('stremio/common'); const SeasonsBarPlaceholder = require('./SeasonsBarPlaceholder'); @@ -12,7 +13,7 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => { const options = React.useMemo(() => { return seasons.map((season) => ({ value: String(season), - label: season > 0 ? `Season ${season}` : 'Specials' + label: season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL') })); }, [seasons]); const selected = React.useMemo(() => { @@ -53,7 +54,7 @@ const SeasonsBar = ({ className, seasons, season, onSelect }) => { 0 ? `Season ${season}` : 'Specials'} + title={season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')} direction={'bottom-left'} options={options} selected={selected} diff --git a/src/routes/MetaDetails/VideosList/VideosList.js b/src/routes/MetaDetails/VideosList/VideosList.js index 478324d28..e67d7660c 100644 --- a/src/routes/MetaDetails/VideosList/VideosList.js +++ b/src/routes/MetaDetails/VideosList/VideosList.js @@ -3,6 +3,7 @@ const React = require('react'); const PropTypes = require('prop-types'); const classnames = require('classnames'); +const { t } = require('i18next'); const Image = require('stremio/common/Image'); const SearchBar = require('stremio/common/SearchBar'); const SeasonsBar = require('./SeasonsBar'); @@ -62,7 +63,7 @@ const VideosList = ({ className, metaItem, season, seasonOnSelect }) => { !metaItem || metaItem.content.type === 'Loading' ? - +
@@ -92,7 +93,7 @@ const VideosList = ({ className, metaItem, season, seasonOnSelect }) => { } diff --git a/src/routes/Player/ControlBar/ControlBar.js b/src/routes/Player/ControlBar/ControlBar.js index 9a37ce990..2ffc02c7d 100644 --- a/src/routes/Player/ControlBar/ControlBar.js +++ b/src/routes/Player/ControlBar/ControlBar.js @@ -10,6 +10,7 @@ const SeekBar = require('./SeekBar'); const VolumeSlider = require('./VolumeSlider'); const styles = require('./styles'); const { useBinaryState } = require('stremio/common'); +const { t } = require('i18next'); const ControlBar = ({ className, @@ -131,18 +132,18 @@ const ControlBar = ({ onSeekRequested={onSeekRequested} />
- { nextVideo !== null ? - : null } -
-
Subtitles Variants
+
{ t('PLAYER_SUBTITLES_VARIANTS') }
{ subtitlesTracksForLanguage.length > 0 ?
@@ -216,7 +217,7 @@ const SubtitlesMenu = React.memo((props) => { :
- Subtitles are disabled + { t('PLAYER_SUBTITLES_DISABLED') }
} @@ -225,14 +226,14 @@ const SubtitlesMenu = React.memo((props) => {
Subtitles Settings
{ /> { + const { t } = useTranslation(); const [search, loadSearchRows] = useSearch(queryParams); const query = React.useMemo(() => { return search.selected !== null ? @@ -50,11 +52,11 @@ const Search = ({ queryParams }) => {
-
Search for movies, series, YouTube and TV channels
+
{ t('SEARCH_EXPLANATION_CONTENT') }
-
Search for actors, directors and writers
+
{ t('SEARCH_EXPLANATION_PEOPLE') }
: @@ -65,7 +67,7 @@ const Search = ({ queryParams }) => { src={require('/images/empty.png')} alt={' '} /> -
No addons were requested for catalogs!
+
{ t('STREMIO_TV_SEARCH_NO_ADDONS') }
: search.catalogs.map((catalog, index) => { diff --git a/src/routes/Settings/Settings.js b/src/routes/Settings/Settings.js index 5dcf697fa..81f147e2e 100644 --- a/src/routes/Settings/Settings.js +++ b/src/routes/Settings/Settings.js @@ -3,6 +3,7 @@ const React = require('react'); const classnames = require('classnames'); const throttle = require('lodash.throttle'); +const { useTranslation } = require('react-i18next'); const Icon = require('@stremio/stremio-icons/dom'); const { useRouteFocused } = require('stremio-router'); const { useServices } = require('stremio/services'); @@ -18,6 +19,7 @@ const STREAMING_SECTION = 'streaming'; const SHORTCUTS_SECTION = 'shortcuts'; const Settings = () => { + const { t } = useTranslation(); const { core } = useServices(); const { routeFocused } = useRouteFocused(); const profile = useProfile(); @@ -176,17 +178,17 @@ const Settings = () => {
- - - -
App Version: {process.env.VERSION}
@@ -199,7 +201,7 @@ const Settings = () => {
-
General
+
{ t('SETTINGS_NAV_GENERAL') }
{
{ profile.auth !== null ? - : null }
{ profile.auth === null ?
-
: @@ -241,11 +243,10 @@ const Settings = () => { }
-
Interface language
+
{ t('SETTINGS_INTERFACE_LANGUAGE') }
@@ -257,27 +258,27 @@ const Settings = () => {
-
Calendar
+
{ t('Calendar') }
-
-
@@ -286,21 +287,21 @@ const Settings = () => {
-
-
-
Player
+
{ t('SETTINGS_NAV_PLAYER') }
-
Subtitles language
+
{ t('SETTINGS_SUBTITLES_LANGUAGE') }
{
-
Subtitles size
+
{ t('SETTINGS_SUBTITLES_SIZE') }
{
-
Subtitles text color
+
{ t('SETTINGS_SUBTITLES_COLOR') }
{
-
Subtitles background color
+
{ t('SETTINGS_SUBTITLES_COLOR_BACKGROUND') }
{
-
Subtitles outline color
+
{ t('SETTINGS_SUBTITLES_COLOR_OUTLINE') }
{
-
Audio Language
+
{ t('SETTINGS_DEFAULT_AUDIO_TRACK') }
{
-
Rewind & Fast-forward duration
+
{ t('SETTINGS_REWIND_FAST_FORWARD_DURATION') }
{
-
Auto-play next episode
+
{ t('AUTO_PLAY') }
{
-
Next video popup duration
+
{ t('SETTINGS_NEXT_VIDEO_POPUP_DURATION') }
{
-
Play in background
+
{ t('SETTINGS_PLAY_IN_BACKGROUND') }
{
-
Play in external player
+
{ t('SETTINGS_PLAY_IN_EXTERNAL_PLAYER') }
{
-
Hardware-accelerated decoding
+
{ t('SETTINGS_HWDEC') }
{
-
Streaming Server
+
{ t('SETTINGS_NAV_STREAMING') }
-
Status
+
{ t('STATUS') }
@@ -432,10 +433,10 @@ const Settings = () => { 'NotLoaded' : streamingServer.settings.type === 'Ready' ? - 'Online' + t('SETTINGS_SERVER_STATUS_ONLINE') : streamingServer.settings.type === 'Error' ? - `Error: (${streamingServer.settings.content})` + `${t('SETTINGS_SERVER_STATUS_ERROR')}: (${streamingServer.settings.content})` : streamingServer.settings.type } @@ -457,7 +458,7 @@ const Settings = () => { cacheSizeSelect !== null ?
-
Cache size
+
{ t('SETTINGS_SERVER_CACHE_SIZE') }
{ torrentProfileSelect !== null ?
-
Torrent profile
+
{ t('SETTINGS_SERVER_TORRENT_PROFILE') }
{ }
-
Shortcuts
+
{ t('SETTINGS_NAV_SHORTCUTS') }
-
Play / Pause
+
{ t('SETTINGS_SHORTCUT_PLAY_PAUSE') }
- Space + { t('SETTINGS_SHORTCUT_SPACE') }
-
Seek Forward
+
{ t('SETTINGS_SHORTCUT_SEEK_FORWARD') }
-
or
- ⇧ Shift +
{ t('SETTINGS_SHORTCUT_OR') }
+ ⇧ { t('SETTINGS_SHORTCUT_SHIFT') }
+
-
Seek Backward
+
{ t('SETTINGS_SHORTCUT_SEEK_BACKWARD') }
-
or
- ⇧ Shift +
{ t('SETTINGS_SHORTCUT_OR') }
+ ⇧ { t('SETTINGS_SHORTCUT_SHIFT') }
+
-
Volume Up
+
{ t('SETTINGS_SHORTCUT_VOLUME_UP') }
@@ -526,7 +527,7 @@ const Settings = () => {
-
Volume Down
+
{ t('SETTINGS_SHORTCUT_VOLUME_DOWN') }
@@ -534,7 +535,7 @@ const Settings = () => {
-
Toggle Subtitles Menu
+
{ t('SETTINGS_SHORTCUT_MENU_SUBTITLES') }
S @@ -542,7 +543,7 @@ const Settings = () => {
-
Toggle Info Menu
+
{ t('SETTINGS_SHORTCUT_MENU_INFO') }
I @@ -550,7 +551,7 @@ const Settings = () => {
-
Toggle Fullscreen
+
{ t('SETTINGS_SHORTCUT_FULLSCREEN') }
F @@ -558,17 +559,17 @@ const Settings = () => {
-
Navigate Between Menus
+
{ t('SETTINGS_SHORTCUT_NAVIGATE_MENUS') }
1 -
to
+
{ t('SETTINGS_SHORTCUT_TO') }
5
-
Go to Search
+
{ t('SETTINGS_SHORTCUT_GO_TO_SEARCH') }
0 @@ -576,10 +577,10 @@ const Settings = () => {
-
Close Menu or Modal
+
{ t('SETTINGS_SHORTCUT_EXIT_BACK') }
- Esc + { t('SETTINGS_SHORTCUT_ESC') }
@@ -589,7 +590,7 @@ const Settings = () => { configureServerUrlModalOpen ? { className={styles['server-url-input']} type={'text'} defaultValue={streamingServerUrlInput.value} - placeholder={'Enter a streaming server url'} + placeholder={t('SETTINGS_SERVER_CONFIGURE_INPUT')} onSubmit={configureServerUrlOnSubmit} /> diff --git a/src/routes/Settings/useProfileSettingsInputs.js b/src/routes/Settings/useProfileSettingsInputs.js index e861f15ed..3c4c00bfc 100644 --- a/src/routes/Settings/useProfileSettingsInputs.js +++ b/src/routes/Settings/useProfileSettingsInputs.js @@ -1,18 +1,22 @@ // Copyright (C) 2017-2022 Smart code 203358507 const React = require('react'); +const { useTranslation } = require('react-i18next'); const { useServices } = require('stremio/services'); -const { CONSTANTS, languageNames } = require('stremio/common'); +const { CONSTANTS, interfaceLanguages, languageNames } = require('stremio/common'); const useProfileSettingsInputs = (profile) => { + const { t } = useTranslation(); const { core } = useServices(); // TODO combine those useMemo in one const interfaceLanguageSelect = React.useMemo(() => ({ - options: Object.keys(languageNames).map((code) => ({ - value: code, - label: languageNames[code] + options: interfaceLanguages.map(({ name, codes }) => ({ + value: codes[0], + label: name, })), - selected: [profile.settings.interfaceLanguage], + selected: [ + interfaceLanguages.find(({ codes }) => codes[1] === profile.settings.interfaceLanguage)?.codes?.[0] || profile.settings.interfaceLanguage + ], onSelect: (event) => { core.transport.dispatch({ action: 'Ctx', @@ -134,11 +138,11 @@ const useProfileSettingsInputs = (profile) => { const seekTimeDurationSelect = React.useMemo(() => ({ options: CONSTANTS.SEEK_TIME_DURATIONS.map((size) => ({ value: `${size}`, - label: `${size / 1000} seconds` + label: `${size / 1000} ${t('SECONDS')}` })), selected: [`${profile.settings.seekTimeDuration}`], renderLabelText: () => { - return `${profile.settings.seekTimeDuration / 1000} seconds`; + return `${profile.settings.seekTimeDuration / 1000} ${t('SECONDS')}`; }, onSelect: (event) => { core.transport.dispatch({ @@ -156,14 +160,14 @@ const useProfileSettingsInputs = (profile) => { const nextVideoPopupDurationSelect = React.useMemo(() => ({ options: CONSTANTS.NEXT_VIDEO_POPUP_DURATIONS.map((duration) => ({ value: `${duration}`, - label: duration === 0 ? 'Disabled' : `${duration / 1000} seconds` + label: duration === 0 ? 'Disabled' : `${duration / 1000} ${t('SECONDS')}` })), selected: [`${profile.settings.nextVideoNotificationDuration}`], renderLabelText: () => { return profile.settings.nextVideoNotificationDuration === 0 ? 'Disabled' : - `${profile.settings.nextVideoNotificationDuration / 1000} seconds`; + `${profile.settings.nextVideoNotificationDuration / 1000} ${t('SECONDS')}`; }, onSelect: (event) => { core.transport.dispatch({