diff --git a/package-lock.json b/package-lock.json index ef6320fc5..9b86afa50 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#e47383bbbb3da8708bb727acd73a093604cdf5c8", "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#e47383bbbb3da8708bb727acd73a093604cdf5c8", + "integrity": "sha512-33qlTmLytSgTAf0fFO242pq2Ir52xREhMs0AGCBmaft4maceuM1cRf/lRJswwu/yMkEBH92C2hGkLGpd9qtw2A==", + "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#e47383bbbb3da8708bb727acd73a093604cdf5c8", + "integrity": "sha512-33qlTmLytSgTAf0fFO242pq2Ir52xREhMs0AGCBmaft4maceuM1cRf/lRJswwu/yMkEBH92C2hGkLGpd9qtw2A==", + "from": "stremio-translations@git+https://git@github.com/Stremio/stremio-translations.git#e47383bbbb3da8708bb727acd73a093604cdf5c8" + }, "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 2242f5f80..e0add18d5 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#e47383bbbb3da8708bb727acd73a093604cdf5c8", "url": "0.11.0" }, "devDependencies": { diff --git a/src/App/App.js b/src/App/App.js index c0a79b727..97e32b920 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); + } + }; if (services.core.active) { services.core.transport.dispatch({ action: 'Ctx', @@ -109,7 +126,15 @@ const App = () => { action: 'SyncLibraryWithAPI' } }); + services.core.transport.on('CoreEvent', onCoreEvent); + services.core.transport + .getState('ctx') + .then(onCtxState) + .catch((e) => console.error(e)); } + return () => { + services.core.transport.off('CoreEvent', onCoreEvent); + }; }, [initialized]); return ( diff --git a/src/common/index.js b/src/common/index.js index 22dcad52a..5110dd669 100644 --- a/src/common/index.js +++ b/src/common/index.js @@ -27,6 +27,7 @@ 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 useAnimationFrame = require('./useAnimationFrame'); @@ -70,6 +71,7 @@ module.exports = { withCoreSuspender, useCoreSuspender, getVisibleChildrenRange, + interfaceLanguages, languageNames, routesRegexp, useAnimationFrame, 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/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/Settings/Settings.js b/src/routes/Settings/Settings.js index 3b99b20fb..d8942b804 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'); @@ -17,6 +18,7 @@ const STREAMING_SECTION = 'streaming'; const SHORTCUTS_SECTION = 'shortcuts'; const Settings = () => { + const { t } = useTranslation(); const { core } = useServices(); const { routeFocused } = useRouteFocused(); const profile = useProfile(); @@ -138,17 +140,17 @@ const Settings = () => {
- - - -
App Version: {process.env.VERSION}
@@ -161,7 +163,7 @@ const Settings = () => {
-
General
+
{ t('SETTINGS_NAV_GENERAL') }
{
@@ -266,7 +267,7 @@ const Settings = () => {
-
Player
+
{ t('SETTINGS_NAV_PLAYER') }
Subtitles language
@@ -384,15 +385,15 @@ const Settings = () => {
-
Streaming Server
+
{ t('SETTINGS_NAV_STREAMING') }
-
Status
+
{ t('STATUS') }
@@ -452,10 +453,10 @@ const Settings = () => { }
-
Shortcuts
+
{ t('SETTINGS_NAV_SHORTCUTS') }
-
Play / Pause
+
{ t('SETTINGS_SHORTCUT_PLAY_PAUSE') }
Space @@ -487,7 +488,7 @@ const Settings = () => {
-
Volume Up
+
{ t('SETTINGS_SHORTCUT_VOLUME_UP') }
@@ -495,7 +496,7 @@ const Settings = () => {
-
Volume Down
+
{ t('SETTINGS_SHORTCUT_VOLUME_DOWN') }
@@ -545,7 +546,7 @@ const Settings = () => {
-
Close Menu or Modal
+
{ t('SETTINGS_SHORTCUT_EXIT_BACK') }
Esc diff --git a/src/routes/Settings/useProfileSettingsInputs.js b/src/routes/Settings/useProfileSettingsInputs.js index e861f15ed..417e4ec89 100644 --- a/src/routes/Settings/useProfileSettingsInputs.js +++ b/src/routes/Settings/useProfileSettingsInputs.js @@ -2,17 +2,19 @@ const React = require('react'); const { useServices } = require('stremio/services'); -const { CONSTANTS, languageNames } = require('stremio/common'); +const { CONSTANTS, interfaceLanguages, languageNames } = require('stremio/common'); const useProfileSettingsInputs = (profile) => { 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',