Merge pull request #329 from Stremio/interface-language

Interface language
This commit is contained in:
Tim 2023-04-13 17:11:11 +02:00 committed by GitHub
commit 5705233705
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 618 additions and 218 deletions

136
package-lock.json generated
View file

@ -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==",

View file

@ -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": {

View file

@ -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 (
<React.StrictMode>

View file

@ -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 (
<Button title={isTransparent ? 'Transparent' : value} {...props} style={labelButtonStyle} className={classnames(className, styles['color-input-container'])} onClick={labelButtonOnClick}>
<Button title={isTransparent ? t('BUTTON_COLOR_TRANSPARENT') : value} {...props} style={labelButtonStyle} className={classnames(className, styles['color-input-container'])} onClick={labelButtonOnClick}>
{
isTransparent ?
<div className={styles['transparent-label-container']}>
<div className={styles['transparent-label']}>Transparent</div>
<div className={styles['transparent-label']}>{ t('BUTTON_COLOR_TRANSPARENT') }</div>
</div>
:
null

View file

@ -4,29 +4,35 @@ const React = require('react');
const { useServices } = require('stremio/services');
const PropTypes = require('prop-types');
const MetaItem = require('stremio/common/MetaItem');
const { t } = require('i18next');
const OPTIONS = [
{ label: 'Play', value: 'play' },
{ label: 'Details', value: 'details' },
{ label: 'Dismiss', value: 'dismiss' },
{ label: 'Remove', value: 'remove' },
{ label: 'LIBRARY_PLAY', value: 'play' },
{ label: 'LIBRARY_DETAILS', value: 'details' },
{ label: 'LIBRARY_RESUME_DISMISS', value: 'dismiss' },
{ label: 'LIBRARY_REMOVE', value: 'remove' },
];
const LibItem = ({ _id, removable, ...props }) => {
const { core } = useServices();
const options = React.useMemo(() => {
return OPTIONS.filter(({ value }) => {
switch (value) {
case 'play':
return props.deepLinks && typeof props.deepLinks.player === 'string';
case 'details':
return props.deepLinks && (typeof props.deepLinks.metaDetailsVideos === 'string' || typeof props.deepLinks.metaDetailsStreams === 'string');
case 'dismiss':
return typeof _id === 'string' && props.progress !== null && !isNaN(props.progress);
case 'remove':
return typeof _id === 'string' && removable;
}
});
return OPTIONS
.filter(({ value }) => {
switch (value) {
case 'play':
return props.deepLinks && typeof props.deepLinks.player === 'string';
case 'details':
return props.deepLinks && (typeof props.deepLinks.metaDetailsVideos === 'string' || typeof props.deepLinks.metaDetailsStreams === 'string');
case 'dismiss':
return typeof _id === 'string' && props.progress !== null && !isNaN(props.progress);
case 'remove':
return typeof _id === 'string' && removable;
}
})
.map((option) => ({
...option,
label: t(option.label)
}));
}, [_id, removable, props.progress, props.deepLinks]);
const optionOnSelect = React.useCallback((event) => {
if (typeof props.optionOnSelect === 'function') {

View file

@ -10,8 +10,8 @@ const TABS = [
{ id: 'board', label: 'Board', icon: 'ic_board', href: '#/' },
{ id: 'discover', label: 'Discover', icon: 'ic_discover', href: '#/discover' },
{ id: 'library', label: 'Library', icon: 'ic_library', href: '#/library' },
{ id: 'settings', label: 'Settings', icon: 'ic_settings', href: '#/settings' },
{ id: 'addons', label: 'Addons', icon: 'ic_addons', href: '#/addons' }
{ id: 'settings', label: 'SETTINGS', icon: 'ic_settings', href: '#/settings' },
{ id: 'addons', label: 'ADDONS', icon: 'ic_addons', href: '#/addons' }
];
const MainNavBars = React.memo(({ className, route, query, children }) => {

View file

@ -3,15 +3,19 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const Button = require('stremio/common/Button');
const styles = require('./styles');
const MetaLinks = ({ className, label, links }) => {
const { t } = useTranslation();
return (
<div className={classnames(className, styles['meta-links-container'])}>
{
typeof label === 'string' && label.length > 0 ?
<div className={styles['label-container']}>{label}</div>
<div className={styles['label-container']}>
{t(`LINKS_${label.toUpperCase()}`)}
</div>
:
null
}
@ -20,7 +24,7 @@ const MetaLinks = ({ className, label, links }) => {
<div className={styles['links-container']}>
{links.map(({ label, href }, index) => (
<Button key={index} className={styles['link-container']} title={label} href={href}>
{label}
{ t(label) }
</Button>
))}
</div>

View file

@ -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
<ActionButton
className={styles['action-button']}
icon={inLibrary ? 'ic_removelib' : 'ic_addlib'}
label={inLibrary ? 'Remove from Library' : 'Add to library'}
label={inLibrary ? t('REMOVE_FROM_LIB') : t('ADD_TO_LIB')}
tabIndex={compact ? -1 : 0}
onClick={toggleInLibrary}
/>
@ -204,7 +206,7 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
<ActionButton
className={styles['action-button']}
icon={'ic_movies'}
label={'Trailer'}
label={t('TRAILER')}
tabIndex={compact ? -1 : 0}
href={trailerHref}
/>
@ -216,7 +218,7 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
<ActionButton
className={styles['action-button']}
icon={'ic_play'}
label={'Show'}
label={t('SHOW')}
tabIndex={compact ? -1 : 0}
href={showHref}
/>
@ -229,13 +231,13 @@ const MetaPreview = ({ className, compact, name, logo, background, runtime, rele
<ActionButton
className={styles['action-button']}
icon={'ic_share'}
label={'Share'}
label={t('CTX_SHARE')}
tabIndex={compact ? -1 : 0}
onClick={openShareModal}
/>
{
shareModalOpen ?
<ModalDialog title={'Share'} onCloseRequest={closeShareModal}>
<ModalDialog title={t('CTX_SHARE')} onCloseRequest={closeShareModal}>
<SharePrompt
className={styles['share-prompt']}
url={linksGroups.get(CONSTANTS.SHARE_LINK_CATEGORY).href}

View file

@ -4,6 +4,7 @@ const React = require('react');
const ReactIs = require('react-is');
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');
@ -11,6 +12,7 @@ const MetaRowPlaceholder = require('./MetaRowPlaceholder');
const styles = require('./styles');
const MetaRow = ({ className, title, message, items, itemComponent, deepLinks }) => {
const { t } = useTranslation();
return (
<div className={classnames(className, styles['meta-row-container'])}>
{
@ -24,8 +26,8 @@ const MetaRow = ({ className, title, message, items, itemComponent, deepLinks })
}
{
deepLinks && (typeof deepLinks.discover === 'string' || typeof deepLinks.library === 'string') ?
<Button className={styles['see-all-container']} title={'SEE ALL'} href={deepLinks.discover || deepLinks.library} tabIndex={-1}>
<div className={styles['label']}>SEE ALL</div>
<Button className={styles['see-all-container']} title={t('BUTTON_SEE_ALL')} href={deepLinks.discover || deepLinks.library} tabIndex={-1}>
<div className={styles['label']}>{ t('BUTTON_SEE_ALL') }</div>
<Icon className={styles['icon']} icon={'ic_arrow_thin_right'} />
</Button>
:

View file

@ -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 (
<div className={classnames(className, styles['meta-row-placeholder-container'])}>
<div className={styles['header-container']}>
@ -17,8 +19,8 @@ const MetaRowPlaceholder = ({ className, title, deepLinks }) => {
</div>
{
deepLinks && typeof deepLinks.discover === 'string' ?
<Button className={styles['see-all-container']} title={'SEE ALL'} href={deepLinks.discover} tabIndex={-1}>
<div className={styles['label']}>SEE ALL</div>
<Button className={styles['see-all-container']} title={t('BUTTON_SEE_ALL')} href={deepLinks.discover} tabIndex={-1}>
<div className={styles['label']}>{ t('BUTTON_SEE_ALL') }</div>
<Icon className={styles['icon']} icon={'ic_arrow_thin_right'} />
</Button>
:

View file

@ -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
<div className={styles['spacing']} />
{
addonsButton ?
<Button className={styles['button-container']} href={'#/addons'} title={'Addons'} tabIndex={-1}>
<Button className={styles['button-container']} href={'#/addons'} title={t('ADDONS')} tabIndex={-1}>
<Icon className={styles['icon']} icon={'ic_addons'} />
</Button>
:
@ -62,7 +63,7 @@ const HorizontalNavBar = React.memo(({ className, route, query, title, backButto
}
{
fullscreenButton ?
<Button className={styles['button-container']} title={fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'} tabIndex={-1} onClick={fullscreen ? exitFullscreen : requestFullscreen}>
<Button className={styles['button-container']} title={fullscreen ? t('EXIT_FULLSCREEN') : t('ENTER_FULLSCREEN')} tabIndex={-1} onClick={fullscreen ? exitFullscreen : requestFullscreen}>
<Icon className={styles['icon']} icon={fullscreen ? 'ic_exit_fullscreen' : 'ic_fullscreen'} />
</Button>
:

View file

@ -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 }) => {
}}
/>
<div className={styles['email-container']}>
<div className={styles['email-label']}>{profile.auth === null ? 'Anonymous user' : profile.auth.user.email}</div>
<div className={styles['email-label']}>{profile.auth === null ? t('ANONYMOUS_USER') : profile.auth.user.email}</div>
</div>
<Button className={styles['logout-button-container']} title={profile.auth === null ? 'Log in / Sign up' : 'Log out'} href={'#/intro'} onClick={logoutButtonOnClick}>
<div className={styles['logout-label']}>{profile.auth === null ? 'Log in / Sign up' : 'Log out'}</div>
<Button className={styles['logout-button-container']} title={profile.auth === null ? `${t('LOG_IN')} / ${t('SIGN_UP')}` : t('LOG_OUT')} href={'#/intro'} onClick={logoutButtonOnClick}>
<div className={styles['logout-label']}>{profile.auth === null ? `${t('LOG_IN')} / ${t('SIGN_UP')}` : t('LOG_OUT')}</div>
</Button>
</div>
<div className={styles['nav-menu-section']}>
<Button className={styles['nav-menu-option-container']} title={fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'} onClick={fullscreen ? exitFullscreen : requestFullscreen}>
<Button className={styles['nav-menu-option-container']} title={fullscreen ? t('EXIT_FULLSCREEN') : t('ENTER_FULLSCREEN')} onClick={fullscreen ? exitFullscreen : requestFullscreen}>
<Icon className={styles['icon']} icon={fullscreen ? 'ic_exit_fullscreen' : 'ic_fullscreen'} />
<div className={styles['nav-menu-option-label']}>{fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen'}</div>
<div className={styles['nav-menu-option-label']}>{fullscreen ? t('EXIT_FULLSCREEN') : t('ENTER_FULLSCREEN')}</div>
</Button>
</div>
<div className={styles['nav-menu-section']}>
<Button className={styles['nav-menu-option-container']} title={'Settings'} href={'#/settings'}>
<Button className={styles['nav-menu-option-container']} title={ t('SETTINGS') } href={'#/settings'}>
<Icon className={styles['icon']} icon={'ic_settings'} />
<div className={styles['nav-menu-option-label']}>Settings</div>
<div className={styles['nav-menu-option-label']}>{ t('SETTINGS') }</div>
</Button>
<Button className={styles['nav-menu-option-container']} title={'Addons'} href={'#/addons'}>
<Button className={styles['nav-menu-option-container']} title={ t('ADDONS') } href={'#/addons'}>
<Icon className={styles['icon']} icon={'ic_addons'} />
<div className={styles['nav-menu-option-label']}>Addons</div>
<div className={styles['nav-menu-option-label']}>{ t('ADDONS') }</div>
</Button>
<Button className={styles['nav-menu-option-container']} title={'Play Magnet Link'} onClick={onPlayMagnetLinkClick}>
<Button className={styles['nav-menu-option-container']} title={ t('PLAY_URL_MAGNET_LINK') } onClick={onPlayMagnetLinkClick}>
<Icon className={styles['icon']} icon={'ic_magnet'} />
<div className={styles['nav-menu-option-label']}>Play Magnet Link</div>
<div className={styles['nav-menu-option-label']}>{ t('PLAY_URL_MAGNET_LINK') }</div>
</Button>
<Button className={styles['nav-menu-option-container']} title={'Help & Feedback'} href={'https://stremio.zendesk.com/'} target={'_blank'}>
<Button className={styles['nav-menu-option-container']} title={ t('HELP_FEEDBACK') } href={'https://stremio.zendesk.com/'} target={'_blank'}>
<Icon className={styles['icon']} icon={'ic_help'} />
<div className={styles['nav-menu-option-label']}>Help & Feedback</div>
<div className={styles['nav-menu-option-label']}>{ t('HELP_FEEDBACK') }</div>
</Button>
</div>
<div className={styles['nav-menu-section']}>
<Button className={styles['nav-menu-option-container']} title={'Terms of Service'} href={'https://www.stremio.com/tos'} target={'_blank'}>
<div className={styles['nav-menu-option-label']}>Terms of Service</div>
<Button className={styles['nav-menu-option-container']} title={ t('TERMS_OF_SERVICE') } href={'https://www.stremio.com/tos'} target={'_blank'}>
<div className={styles['nav-menu-option-label']}>{ t('TERMS_OF_SERVICE') }</div>
</Button>
<Button className={styles['nav-menu-option-container']} title={'Privacy Policy'} href={'https://www.stremio.com/privacy'} target={'_blank'}>
<div className={styles['nav-menu-option-label']}>Privacy Policy</div>
<Button className={styles['nav-menu-option-container']} title={ t('PRIVACY_POLICY') } href={'https://www.stremio.com/privacy'} target={'_blank'}>
<div className={styles['nav-menu-option-label']}>{ t('PRIVACY_POLICY') }</div>
</Button>
<Button className={styles['nav-menu-option-container']} title={'About Stremio'} href={'https://www.stremio.com/'} target={'_blank'}>
<div className={styles['nav-menu-option-label']}>About Stremio</div>
<Button className={styles['nav-menu-option-container']} title={ t('ABOUT_STREMIO') } href={'https://www.stremio.com/'} target={'_blank'}>
<div className={styles['nav-menu-option-label']}>{ t('ABOUT_STREMIO') }</div>
</Button>
</div>
</div>

View file

@ -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 }) => {
/>
:
<div className={styles['search-input']}>
<div className={styles['placeholder-label']}>Search or paste link</div>
<div className={styles['placeholder-label']}>{ t('SEARCH_OR_PASTE_LINK') }</div>
</div>
}
<Button className={styles['submit-button-container']} tabIndex={-1} onClick={queryInputOnSubmit}>
@ -70,16 +72,19 @@ SearchBar.propTypes = {
active: PropTypes.bool
};
const SearchBarFallback = ({ className }) => (
<label className={classnames(className, styles['search-bar-container'])}>
<div className={styles['search-input']}>
<div className={styles['placeholder-label']}>Search or paste link</div>
</div>
<Button className={styles['submit-button-container']} tabIndex={-1}>
<Icon className={styles['icon']} icon={'ic_search_link'} />
</Button>
</label>
);
const SearchBarFallback = ({ className }) => {
const { t } = useTranslation();
return (
<label className={classnames(className, styles['search-bar-container'])}>
<div className={styles['search-input']}>
<div className={styles['placeholder-label']}>{ t('SEARCH_OR_PASTE_LINK') }</div>
</div>
<Button className={styles['submit-button-container']} tabIndex={-1}>
<Icon className={styles['icon']} icon={'ic_search_link'} />
</Button>
</label>
);
};
SearchBarFallback.propTypes = SearchBar.propTypes;

View file

@ -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 (
<nav className={classnames(className, styles['vertical-nav-bar-container'])}>
{
@ -19,7 +21,7 @@ const VerticalNavBar = React.memo(({ className, selected, tabs }) => {
href={tab.href}
logo={tab.logo}
icon={tab.icon}
label={tab.label}
label={t(tab.label)}
onClick={tab.onClick}
/>
))

View file

@ -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 { useServices } = require('stremio/services');
@ -11,6 +12,7 @@ const TextInput = require('stremio/common/TextInput');
const styles = require('./styles');
const SharePrompt = ({ className, url }) => {
const { t } = useTranslation();
const { core } = useServices();
const inputRef = React.useRef(null);
const routeFocused = useRouteFocused();
@ -62,7 +64,7 @@ const SharePrompt = ({ className, url }) => {
/>
<Button className={styles['copy-button']} title={'Copy to clipboard'} onClick={copyToClipboard}>
<Icon className={styles['icon']} icon={'ic_link'} />
<div className={styles['label']}>Copy</div>
<div className={styles['label']}>{ t('COPY') }</div>
</Button>
</div>
</div>

View file

@ -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 (
<div className={classnames(className, styles['warning-container'])}>
<div className={styles['warning-statement']}>Streaming server is not available.</div>
<Button className={styles['warning-button']} title={'Later'} onClick={onLaterClick} tabIndex={-1}>
<div className={styles['warning-label']}>Later</div>
<div className={styles['warning-statement']}>{ t('SETTINGS_SERVER_UNAVAILABLE') }</div>
<Button className={styles['warning-button']} title={t('WARNING_STREAMING_SERVER_LATER')} onClick={onLaterClick} tabIndex={-1}>
<div className={styles['warning-label']}>{ t('WARNING_STREAMING_SERVER_LATER') }</div>
</Button>
<Button className={styles['warning-button']} title={'Dismiss'} onClick={onDismissClick} tabIndex={-1}>
<div className={styles['warning-label']}>Dismiss</div>
<Button className={styles['warning-button']} title={t('WARNING_STREAMING_SERVER_DISMISS')} onClick={onDismissClick} tabIndex={-1}>
<div className={styles['warning-label']}>{ t('WARNING_STREAMING_SERVER_DISMISS') }</div>
</Button>
</div>
);

View file

@ -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,

View file

@ -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"]
}
]

View file

@ -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;

View file

@ -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(<App />);

View file

@ -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
}
</div>
<div className={styles['buttons-container']}>
<Button className={installed ? styles['uninstall-button-container'] : styles['install-button-container']} title={installed ? 'Uninstall' : 'Install'} tabIndex={-1} onClick={toggleButtonOnClick}>
<div className={styles['label']}>{installed ? 'Uninstall' : 'Install'}</div>
<Button className={installed ? styles['uninstall-button-container'] : styles['install-button-container']} title={installed ? t('ADDON_UNINSTALL') : t('ADDON_INSTALL')} tabIndex={-1} onClick={toggleButtonOnClick}>
<div className={styles['label']}>{installed ? t('ADDON_UNINSTALL') : t('ADDON_INSTALL')}</div>
</Button>
<Button className={styles['share-button-container']} title={'SHARE ADDON'} tabIndex={-1} onClick={shareButtonOnClick}>
<Button className={styles['share-button-container']} title={t('SHARE_ADDON')} tabIndex={-1} onClick={shareButtonOnClick}>
<Icon className={styles['icon']} icon={'ic_share'} />
<div className={styles['label']}>SHARE ADDON</div>
<div className={styles['label']}>{ t('SHARE_ADDON') }</div>
</Button>
</div>
</Button>

View file

@ -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 }) => {
<MainNavBars className={styles['addons-container']} route={'addons'}>
<div className={styles['addons-content']}>
<div className={styles['selectable-inputs-container']}>
<Button className={styles['add-button-container']} title={'Add addon'} onClick={openAddAddonModal}>
<Button className={styles['add-button-container']} title={t('ADD_ADDON')} onClick={openAddAddonModal}>
<Icon className={styles['icon']} icon={'ic_plus'} />
<div className={styles['add-button-label']}>Add addon</div>
<div className={styles['add-button-label']}>{ t('ADD_ADDON') }</div>
</Button>
{selectInputs.map((selectInput, index) => (
<Multiselect
@ -92,7 +94,7 @@ const Addons = ({ urlParams, queryParams }) => {
<div className={styles['spacing']} />
<SearchBar
className={styles['search-bar']}
title={'Search addons'}
title={t('ADDON_SEARCH')}
value={search}
onChange={searchInputOnChange}
/>
@ -192,15 +194,15 @@ const Addons = ({ urlParams, queryParams }) => {
addAddonModalOpen ?
<ModalDialog
className={styles['add-addon-modal-container']}
title={'Add addon'}
title={t('ADD_ADDON')}
buttons={addAddonModalButtons}
onCloseRequest={closeAddAddonModal}>
<div className={styles['notice']}>You can add an addon via an external link, which will appear under Installed addons.</div>
<div className={styles['notice']}>{ t('ADD_ADDON_DESCRIPTION') }</div>
<TextInput
ref={addAddonUrlInputRef}
className={styles['addon-url-input']}
type={'text'}
placeholder={'Paste addon URL'}
placeholder={t('PASTE_ADDON_URL')}
autoFocus={true}
onSubmit={addAddonOnSubmit}
/>
@ -212,7 +214,7 @@ const Addons = ({ urlParams, queryParams }) => {
sharedAddon !== null ?
<ModalDialog
className={styles['share-modal-container']}
title={'Share Addon'}
title={t('SHARE_ADDON')}
onCloseRequest={clearSharedAddon}>
<div className={styles['title-container']}>
<Image

View file

@ -1,16 +1,18 @@
// Copyright (C) 2017-2022 Smart code 203358507
const React = require('react');
const { t } = require('i18next');
const { translateOption } = require('stremio/common');
const mapSelectableInputs = (installedAddons, remoteAddons) => {
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;
},

View file

@ -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 ?
<MetaRow
className={classnames(styles['board-row'], styles['continue-watching-row'], 'animation-fade-in')}
title={'Continue Watching'}
title={t('BOARD_CONTINUE_WATCHING')}
items={continueWatchingPreview.libraryItems}
itemComponent={LibItem}
deepLinks={continueWatchingPreview.deepLinks}

View file

@ -1,20 +1,22 @@
// Copyright (C) 2017-2022 Smart code 203358507
const React = require('react');
const { useTranslation } = require('react-i18next');
const { translateOption } = require('stremio/common');
const mapSelectableInputs = (discover) => {
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;
};

View file

@ -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;
};

View file

@ -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') ?
<div className={styles['message-container']}>
<Image className={styles['image']} src={require('/images/empty.png')} alt={' '} />
<div className={styles['label']}>No streams were found!</div>
<div className={styles['label']}>{t('NO_STREAM')}</div>
</div>
:
filteredStreams.length === 0 ?
@ -115,9 +117,9 @@ const StreamsList = ({ className, ...props }) => {
</div>
</React.Fragment>
}
<Button className={styles['install-button-container']} title={'Install Addons'} href={'#/addons'}>
<Button className={styles['install-button-container']} title={t('ADDON_CATALOGUE_MORE')} href={'#/addons'}>
<Icon className={styles['icon']} icon={'ic_addons'} />
<div className={styles['label']}>Install Addons</div>
<div className={styles['label']}>{ t('ADDON_CATALOGUE_MORE') }</div>
</Button>
</div>
);

View file

@ -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 }) => {
</Button>
<Multiselect
className={styles['seasons-popup-label-container']}
title={season > 0 ? `Season ${season}` : 'Specials'}
title={season > 0 ? `${t('SEASON')} ${season}` : t('SPECIAL')}
direction={'bottom-left'}
options={options}
selected={selected}

View file

@ -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' ?
<React.Fragment>
<SeasonsBar.Placeholder className={styles['seasons-bar']} />
<SearchBar.Placeholder className={styles['search-bar']} title={'Search videos'} />
<SearchBar.Placeholder className={styles['search-bar']} title={t('SEARCH_VIDEOS')} />
<div className={styles['videos-scroll-container']}>
<Video.Placeholder />
<Video.Placeholder />
@ -92,7 +93,7 @@ const VideosList = ({ className, metaItem, season, seasonOnSelect }) => {
}
<SearchBar
className={styles['search-bar']}
title={'Search videos'}
title={t('SEARCH_VIDEOS')}
value={search}
onChange={searchInputOnChange}
/>

View file

@ -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}
/>
<div className={styles['control-bar-buttons-container']}>
<Button className={classnames(styles['control-bar-button'], { 'disabled': typeof paused !== 'boolean' })} title={paused ? 'Play' : 'Pause'} tabIndex={-1} onClick={onPlayPauseButtonClick}>
<Button className={classnames(styles['control-bar-button'], { 'disabled': typeof paused !== 'boolean' })} title={paused ? t('PLAYER_PLAY') : t('PLAYER_PAUSE')} tabIndex={-1} onClick={onPlayPauseButtonClick}>
<Icon className={styles['icon']} icon={typeof paused !== 'boolean' || paused ? 'ic_play' : 'ic_pause'} />
</Button>
{
nextVideo !== null ?
<Button className={classnames(styles['control-bar-button'])} title={'Next Video'} tabIndex={-1} onClick={onNextVideoButtonClick}>
<Button className={classnames(styles['control-bar-button'])} title={t('PLAYER_NEXT_VIDEO')} tabIndex={-1} onClick={onNextVideoButtonClick}>
<Icon className={styles['icon']} icon={'ic_play_next'} />
</Button>
:
null
}
<Button className={classnames(styles['control-bar-button'], { 'disabled': typeof muted !== 'boolean' })} title={muted ? 'Unmute' : 'Mute'} tabIndex={-1} onClick={onMuteButtonClick}>
<Button className={classnames(styles['control-bar-button'], { 'disabled': typeof muted !== 'boolean' })} title={muted ? t('PLAYER_UNMUTE') : t('PLAYER_MUTE')} tabIndex={-1} onClick={onMuteButtonClick}>
<Icon
className={styles['icon']}
icon={

View file

@ -3,12 +3,14 @@
const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const { useTranslation } = require('react-i18next');
const Option = require('./Option');
const styles = require('./styles');
const RATES = Array.from(Array(8).keys(), (n) => n * 0.25 + 0.25).reverse();
const SpeedMenu = ({ className, playbackSpeed, onPlaybackSpeedChanged }) => {
const { t } = useTranslation();
const onMouseDown = React.useCallback((event) => {
event.nativeEvent.speedMenuClosePrevented = true;
}, []);
@ -20,7 +22,7 @@ const SpeedMenu = ({ className, playbackSpeed, onPlaybackSpeedChanged }) => {
return (
<div className={classnames(className, styles['speed-menu-container'])} onMouseDown={onMouseDown}>
<div className={styles['title']}>
Playback Speed
{ t('PLAYBACK_SPEED') }
</div>
<div className={styles['options-container']}>
{

View file

@ -6,6 +6,7 @@ const classnames = require('classnames');
const { Button, CONSTANTS, comparatorWithPriorities, languageNames } = require('stremio/common');
const DiscreteSelectInput = require('./DiscreteSelectInput');
const styles = require('./styles');
const { t } = require('i18next');
const ORIGIN_PRIORITIES = {
'EMBEDDED': 2,
@ -172,10 +173,10 @@ const SubtitlesMenu = React.memo((props) => {
null
}
<div className={styles['languages-container']}>
<div className={styles['languages-header']}>Subtitles Languages</div>
<div className={styles['languages-header']}>{ t('PLAYER_SUBTITLES_LANGUAGES') }</div>
<div className={styles['languages-list']}>
<Button title={'Off'} className={classnames(styles['language-option'], { 'selected': selectedSubtitlesLanguage === null })} onClick={subtitlesLanguageOnClick}>
<div className={styles['language-label']}>Off</div>
<Button title={t('OFF')} className={classnames(styles['language-option'], { 'selected': selectedSubtitlesLanguage === null })} onClick={subtitlesLanguageOnClick}>
<div className={styles['language-label']}>{ t('OFF') }</div>
{
selectedSubtitlesLanguage === null ?
<div className={styles['icon']} />
@ -197,7 +198,7 @@ const SubtitlesMenu = React.memo((props) => {
</div>
</div>
<div className={styles['variants-container']}>
<div className={styles['variants-header']}>Subtitles Variants</div>
<div className={styles['variants-header']}>{ t('PLAYER_SUBTITLES_VARIANTS') }</div>
{
subtitlesTracksForLanguage.length > 0 ?
<div className={styles['variants-list']}>
@ -216,7 +217,7 @@ const SubtitlesMenu = React.memo((props) => {
:
<div className={styles['no-variants-container']}>
<div className={styles['no-variants-label']}>
Subtitles are disabled
{ t('PLAYER_SUBTITLES_DISABLED') }
</div>
</div>
}
@ -225,14 +226,14 @@ const SubtitlesMenu = React.memo((props) => {
<div className={styles['settings-header']}>Subtitles Settings</div>
<DiscreteSelectInput
className={styles['discrete-input']}
label={'Delay'}
label={t('DELAY')}
value={typeof props.selectedExtraSubtitlesTrackId === 'string' && props.extraSubtitlesDelay !== null && !isNaN(props.extraSubtitlesDelay) ? `${(props.extraSubtitlesDelay / 1000).toFixed(2)}s` : '--'}
disabled={typeof props.selectedExtraSubtitlesTrackId !== 'string' || props.extraSubtitlesDelay === null || isNaN(props.extraSubtitlesDelay)}
onChange={onSubtitlesDelayChanged}
/>
<DiscreteSelectInput
className={styles['discrete-input']}
label={'Size'}
label={t('SIZE')}
value={
typeof props.selectedSubtitlesTrackId === 'string' ?
props.subtitlesSize !== null && !isNaN(props.subtitlesSize) ? `${props.subtitlesSize}%` : '--'
@ -255,7 +256,7 @@ const SubtitlesMenu = React.memo((props) => {
/>
<DiscreteSelectInput
className={styles['discrete-input']}
label={'Vertical position'}
label={t('PLAYER_SUBTITLES_VERTICAL_POSIITON')}
value={
typeof props.selectedSubtitlesTrackId === 'string' ?
props.subtitlesOffset !== null && !isNaN(props.subtitlesOffset) ? `${props.subtitlesOffset}%` : '--'

View file

@ -4,6 +4,7 @@ const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const debounce = require('lodash.debounce');
const { useTranslation } = require('react-i18next');
const Icon = require('@stremio/stremio-icons/dom');
const { Image, MainNavBars, MetaRow, MetaItem, withCoreSuspender, getVisibleChildrenRange } = require('stremio/common');
const useSearch = require('./useSearch');
@ -12,6 +13,7 @@ const styles = require('./styles');
const THRESHOLD = 100;
const Search = ({ queryParams }) => {
const { t } = useTranslation();
const [search, loadSearchRows] = useSearch(queryParams);
const query = React.useMemo(() => {
return search.selected !== null ?
@ -50,11 +52,11 @@ const Search = ({ queryParams }) => {
<div className={classnames(styles['search-hints-container'], 'animation-fade-in')}>
<div className={styles['search-hint-container']}>
<Icon className={styles['icon']} icon={'ic_movies'} />
<div className={styles['label']}>Search for movies, series, YouTube and TV channels</div>
<div className={styles['label']}>{ t('SEARCH_EXPLANATION_CONTENT') }</div>
</div>
<div className={styles['search-hint-container']}>
<Icon className={styles['icon']} icon={'ic_actor'} />
<div className={styles['label']}>Search for actors, directors and writers</div>
<div className={styles['label']}>{ t('SEARCH_EXPLANATION_PEOPLE') }</div>
</div>
</div>
:
@ -65,7 +67,7 @@ const Search = ({ queryParams }) => {
src={require('/images/empty.png')}
alt={' '}
/>
<div className={styles['message-label']}>No addons were requested for catalogs!</div>
<div className={styles['message-label']}>{ t('STREMIO_TV_SEARCH_NO_ADDONS') }</div>
</div>
:
search.catalogs.map((catalog, index) => {

View file

@ -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 = () => {
<MainNavBars className={styles['settings-container']} route={'settings'}>
<div className={classnames(styles['settings-content'], 'animation-fade-in')}>
<div className={styles['side-menu-container']}>
<Button className={classnames(styles['side-menu-button'], { [styles['selected']]: selectedSectionId === GENERAL_SECTION })} title={'General'} data-section={GENERAL_SECTION} onClick={sideMenuButtonOnClick}>
General
<Button className={classnames(styles['side-menu-button'], { [styles['selected']]: selectedSectionId === GENERAL_SECTION })} title={ t('SETTINGS_NAV_GENERAL') } data-section={GENERAL_SECTION} onClick={sideMenuButtonOnClick}>
{ t('SETTINGS_NAV_GENERAL') }
</Button>
<Button className={classnames(styles['side-menu-button'], { [styles['selected']]: selectedSectionId === PLAYER_SECTION })} title={'Player'} data-section={PLAYER_SECTION} onClick={sideMenuButtonOnClick}>
Player
<Button className={classnames(styles['side-menu-button'], { [styles['selected']]: selectedSectionId === PLAYER_SECTION })} title={ t('SETTINGS_NAV_PLAYER') }data-section={PLAYER_SECTION} onClick={sideMenuButtonOnClick}>
{ t('SETTINGS_NAV_PLAYER') }
</Button>
<Button className={classnames(styles['side-menu-button'], { [styles['selected']]: selectedSectionId === STREAMING_SECTION })} title={'Streaming server'} data-section={STREAMING_SECTION} onClick={sideMenuButtonOnClick}>
Streaming server
<Button className={classnames(styles['side-menu-button'], { [styles['selected']]: selectedSectionId === STREAMING_SECTION })} title={ t('SETTINGS_NAV_STREAMING') } data-section={STREAMING_SECTION} onClick={sideMenuButtonOnClick}>
{ t('SETTINGS_NAV_STREAMING') }
</Button>
<Button className={classnames(styles['side-menu-button'], { [styles['selected']]: selectedSectionId === SHORTCUTS_SECTION })} title={'Shortcuts'} data-section={SHORTCUTS_SECTION} onClick={sideMenuButtonOnClick}>
Shortcuts
<Button className={classnames(styles['side-menu-button'], { [styles['selected']]: selectedSectionId === SHORTCUTS_SECTION })} title={ t('SETTINGS_NAV_SHORTCUTS') } data-section={SHORTCUTS_SECTION} onClick={sideMenuButtonOnClick}>
{ t('SETTINGS_NAV_SHORTCUTS') }
</Button>
<div className={styles['spacing']} />
<div className={styles['version-info-label']} title={process.env.VERSION}>App Version: {process.env.VERSION}</div>
@ -199,7 +201,7 @@ const Settings = () => {
</div>
<div ref={sectionsContainerRef} className={styles['sections-container']} onScroll={sectionsContainerOnScorll}>
<div ref={generalSectionRef} className={styles['section-container']}>
<div className={styles['section-title']}>General</div>
<div className={styles['section-title']}>{ t('SETTINGS_NAV_GENERAL') }</div>
<div className={classnames(styles['option-container'], styles['user-info-option-container'])}>
<div
className={styles['avatar-container']}
@ -218,22 +220,22 @@ const Settings = () => {
</div>
{
profile.auth !== null ?
<Button className={styles['logout-button-container']} title={'Log out'} href={'#/intro'} onClick={logoutButtonOnClick}>
<div className={styles['logout-label']}>Log out</div>
<Button className={styles['logout-button-container']} title={ t('LOG_OUT') } href={'#/intro'} onClick={logoutButtonOnClick}>
<div className={styles['logout-label']}>{ t('LOG_OUT') }</div>
</Button>
:
null
}
</div>
<Button className={styles['user-panel-container']} title={'User panel'} target={'_blank'} href={'https://www.stremio.com/acc-settings'}>
<div className={styles['user-panel-label']}>User Panel</div>
<div className={styles['user-panel-label']}>{ t('USER_PANEL') }</div>
</Button>
</div>
{
profile.auth === null ?
<div className={styles['option-container']}>
<Button className={classnames(styles['option-input-container'], styles['button-container'])} title={'Log in / Sign up'} href={'#/intro'} onClick={logoutButtonOnClick}>
<div className={styles['label']}>Log in / Sign up</div>
<Button className={classnames(styles['option-input-container'], styles['button-container'])} title={`${t('LOG_IN')} / ${t('SIGN_UP')}`} href={'#/intro'} onClick={logoutButtonOnClick}>
<div className={styles['label']}>{ t('LOG_IN') } / { t('SIGN_UP') }</div>
</Button>
</div>
:
@ -241,11 +243,10 @@ const Settings = () => {
}
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Interface language</div>
<div className={styles['label']}>{ t('SETTINGS_INTERFACE_LANGUAGE') }</div>
</div>
<Multiselect
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
disabled={true}
tabIndex={-1}
{...interfaceLanguageSelect}
/>
@ -257,27 +258,27 @@ const Settings = () => {
<Button className={classnames(styles['option-input-container'], styles['button-container'])} title={'Authenticate'} disabled={profile.auth === null} tabIndex={-1} onClick={toggleTraktOnClick}>
<Icon className={styles['icon']} icon={'ic_trakt'} />
<div className={styles['label']}>
{ profile.auth !== null && profile.auth.user !== null && profile.auth.user.trakt !== null ? 'Log out' : 'Authenticate' }
{ profile.auth !== null && profile.auth.user !== null && profile.auth.user.trakt !== null ? t('LOG_OUT') : t('SETTINGS_TRAKT_AUTHENTICATE') }
</div>
</Button>
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Calendar</div>
<div className={styles['label']}>{ t('Calendar') }</div>
</div>
<Button className={classnames(styles['option-input-container'], styles['button-container'])} title={'Subscribe'} disabled={!(profile.auth && profile.auth.user && profile.auth.user._id)} tabIndex={-1} onClick={subscribeCalendarOnClick}>
<Icon className={styles['icon']} icon={'ic_calendar'} />
<div className={styles['label']}>Subscribe</div>
<div className={styles['label']}>{ t('SETTINGS_CALENDAR_SUBSCRIBE') }</div>
</Button>
</div>
<div className={styles['option-container']}>
<Button className={classnames(styles['option-input-container'], styles['link-container'])} title={'Export user data'} tabIndex={-1} onClick={exportDataOnClick}>
<div className={styles['label']}>Export user data</div>
<Button className={classnames(styles['option-input-container'], styles['link-container'])} title={t('SETTINGS_DATA_EXPORT')} tabIndex={-1} onClick={exportDataOnClick}>
<div className={styles['label']}>{ t('SETTINGS_DATA_EXPORT') }</div>
</Button>
</div>
<div className={styles['option-container']}>
<Button className={classnames(styles['option-input-container'], styles['link-container'])} title={'Contact support'} target={'_blank'} href={'https://stremio.zendesk.com/hc/en-us'}>
<div className={styles['label']}>Contact support</div>
<Button className={classnames(styles['option-input-container'], styles['link-container'])} title={t('SETTINGS_SUPPORT')} target={'_blank'} href={'https://stremio.zendesk.com/hc/en-us'}>
<div className={styles['label']}>{ t('SETTINGS_SUPPORT') }</div>
</Button>
</div>
<div className={styles['option-container']}>
@ -286,21 +287,21 @@ const Settings = () => {
</Button>
</div>
<div className={styles['option-container']}>
<Button className={classnames(styles['option-input-container'], styles['link-container'])} title={'Terms of Service'} target={'_blank'} href={'https://www.stremio.com/tos'}>
<div className={styles['label']}>Terms of Service</div>
<Button className={classnames(styles['option-input-container'], styles['link-container'])} title={t('TERMS_OF_SERVICE')} target={'_blank'} href={'https://www.stremio.com/tos'}>
<div className={styles['label']}>{ t('TERMS_OF_SERVICE') }</div>
</Button>
</div>
<div className={styles['option-container']}>
<Button className={classnames(styles['option-input-container'], styles['link-container'])} title={'Privacy Policy'} target={'_blank'} href={'https://www.stremio.com/privacy'}>
<div className={styles['label']}>Privacy Policy</div>
<Button className={classnames(styles['option-input-container'], styles['link-container'])} title={t('PRIVACY_POLICY')} target={'_blank'} href={'https://www.stremio.com/privacy'}>
<div className={styles['label']}>{ t('PRIVACY_POLICY') }</div>
</Button>
</div>
</div>
<div ref={playerSectionRef} className={styles['section-container']}>
<div className={styles['section-title']}>Player</div>
<div className={styles['section-title']}>{ t('SETTINGS_NAV_PLAYER') }</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Subtitles language</div>
<div className={styles['label']}>{ t('SETTINGS_SUBTITLES_LANGUAGE') }</div>
</div>
<Multiselect
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
@ -309,7 +310,7 @@ const Settings = () => {
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Subtitles size</div>
<div className={styles['label']}>{ t('SETTINGS_SUBTITLES_SIZE') }</div>
</div>
<Multiselect
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
@ -318,7 +319,7 @@ const Settings = () => {
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Subtitles text color</div>
<div className={styles['label']}>{ t('SETTINGS_SUBTITLES_COLOR') }</div>
</div>
<ColorInput
className={classnames(styles['option-input-container'], styles['color-input-container'])}
@ -327,7 +328,7 @@ const Settings = () => {
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Subtitles background color</div>
<div className={styles['label']}>{ t('SETTINGS_SUBTITLES_COLOR_BACKGROUND') }</div>
</div>
<ColorInput
className={classnames(styles['option-input-container'], styles['color-input-container'])}
@ -336,7 +337,7 @@ const Settings = () => {
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Subtitles outline color</div>
<div className={styles['label']}>{ t('SETTINGS_SUBTITLES_COLOR_OUTLINE') }</div>
</div>
<ColorInput
className={classnames(styles['option-input-container'], styles['color-input-container'])}
@ -345,7 +346,7 @@ const Settings = () => {
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Audio Language</div>
<div className={styles['label']}>{ t('SETTINGS_DEFAULT_AUDIO_TRACK') }</div>
</div>
<Multiselect
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
@ -354,7 +355,7 @@ const Settings = () => {
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Rewind & Fast-forward duration</div>
<div className={styles['label']}>{ t('SETTINGS_REWIND_FAST_FORWARD_DURATION') }</div>
</div>
<Multiselect
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
@ -363,7 +364,7 @@ const Settings = () => {
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Auto-play next episode</div>
<div className={styles['label']}>{ t('AUTO_PLAY') }</div>
</div>
<Checkbox
className={classnames(styles['option-input-container'], styles['checkbox-container'])}
@ -372,7 +373,7 @@ const Settings = () => {
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Next video popup duration</div>
<div className={styles['label']}>{ t('SETTINGS_NEXT_VIDEO_POPUP_DURATION') }</div>
</div>
<Multiselect
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
@ -382,7 +383,7 @@ const Settings = () => {
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Play in background</div>
<div className={styles['label']}>{ t('SETTINGS_PLAY_IN_BACKGROUND') }</div>
</div>
<Checkbox
className={classnames(styles['option-input-container'], styles['checkbox-container'])}
@ -393,7 +394,7 @@ const Settings = () => {
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Play in external player</div>
<div className={styles['label']}>{ t('SETTINGS_PLAY_IN_EXTERNAL_PLAYER') }</div>
</div>
<Checkbox
className={classnames(styles['option-input-container'], styles['checkbox-container'])}
@ -404,7 +405,7 @@ const Settings = () => {
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Hardware-accelerated decoding</div>
<div className={styles['label']}>{ t('SETTINGS_HWDEC') }</div>
</div>
<Checkbox
className={classnames(styles['option-input-container'], styles['checkbox-container'])}
@ -415,15 +416,15 @@ const Settings = () => {
</div>
</div>
<div ref={streamingServerSectionRef} className={styles['section-container']}>
<div className={styles['section-title']}>Streaming Server</div>
<div className={styles['section-title']}>{ t('SETTINGS_NAV_STREAMING') }</div>
<div className={styles['option-container']}>
<Button className={classnames(styles['option-input-container'], styles['button-container'])} title={'Reload'} onClick={reloadStreamingServer}>
<div className={styles['label']}>Reload</div>
<div className={styles['label']}>{ t('RELOAD') }</div>
</Button>
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Status</div>
<div className={styles['label']}>{ t('STATUS') }</div>
</div>
<div className={classnames(styles['option-input-container'], styles['info-container'])}>
<div className={styles['label']}>
@ -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 ?
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Cache size</div>
<div className={styles['label']}>{ t('SETTINGS_SERVER_CACHE_SIZE') }</div>
</div>
<Multiselect
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
@ -471,7 +472,7 @@ const Settings = () => {
torrentProfileSelect !== null ?
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Torrent profile</div>
<div className={styles['label']}>{ t('SETTINGS_SERVER_TORRENT_PROFILE') }</div>
</div>
<Multiselect
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
@ -483,42 +484,42 @@ const Settings = () => {
}
</div>
<div ref={shortcutsSectionRef} className={styles['section-container']}>
<div className={styles['section-title']}>Shortcuts</div>
<div className={styles['section-title']}>{ t('SETTINGS_NAV_SHORTCUTS') }</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Play / Pause</div>
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_PLAY_PAUSE') }</div>
</div>
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
<kbd>Space</kbd>
<kbd>{ t('SETTINGS_SHORTCUT_SPACE') }</kbd>
</div>
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Seek Forward</div>
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_SEEK_FORWARD') }</div>
</div>
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
<kbd></kbd>
<div className={styles['label']}>or</div>
<kbd> Shift</kbd>
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_OR') }</div>
<kbd> { t('SETTINGS_SHORTCUT_SHIFT') }</kbd>
<div className={styles['label']}>+</div>
<kbd></kbd>
</div>
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Seek Backward</div>
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_SEEK_BACKWARD') }</div>
</div>
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
<kbd></kbd>
<div className={styles['label']}>or</div>
<kbd> Shift</kbd>
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_OR') }</div>
<kbd> { t('SETTINGS_SHORTCUT_SHIFT') }</kbd>
<div className={styles['label']}>+</div>
<kbd></kbd>
</div>
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Volume Up</div>
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_VOLUME_UP') }</div>
</div>
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
<kbd></kbd>
@ -526,7 +527,7 @@ const Settings = () => {
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Volume Down</div>
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_VOLUME_DOWN') }</div>
</div>
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
<kbd></kbd>
@ -534,7 +535,7 @@ const Settings = () => {
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Toggle Subtitles Menu</div>
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_MENU_SUBTITLES') }</div>
</div>
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
<kbd>S</kbd>
@ -542,7 +543,7 @@ const Settings = () => {
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Toggle Info Menu</div>
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_MENU_INFO') }</div>
</div>
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
<kbd>I</kbd>
@ -550,7 +551,7 @@ const Settings = () => {
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Toggle Fullscreen</div>
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_FULLSCREEN') }</div>
</div>
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
<kbd>F</kbd>
@ -558,17 +559,17 @@ const Settings = () => {
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Navigate Between Menus</div>
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_NAVIGATE_MENUS') }</div>
</div>
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
<kbd>1</kbd>
<div className={styles['label']}>to</div>
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_TO') }</div>
<kbd>5</kbd>
</div>
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Go to Search</div>
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_GO_TO_SEARCH') }</div>
</div>
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
<kbd>0</kbd>
@ -576,10 +577,10 @@ const Settings = () => {
</div>
<div className={styles['option-container']}>
<div className={styles['option-name-container']}>
<div className={styles['label']}>Close Menu or Modal</div>
<div className={styles['label']}>{ t('SETTINGS_SHORTCUT_EXIT_BACK') }</div>
</div>
<div className={classnames(styles['option-input-container'], styles['shortcut-container'])}>
<kbd>Esc</kbd>
<kbd>{ t('SETTINGS_SHORTCUT_ESC') }</kbd>
</div>
</div>
</div>
@ -589,7 +590,7 @@ const Settings = () => {
configureServerUrlModalOpen ?
<ModalDialog
className={styles['configure-server-url-modal-container']}
title={'Configure streaming server url'}
title={t('SETTINGS_SERVER_CONFIGURE_TITLE')}
buttons={configureServerUrlModalButtons}
onCloseRequest={closeConfigureServerUrlModal}>
<TextInput
@ -598,7 +599,7 @@ const Settings = () => {
className={styles['server-url-input']}
type={'text'}
defaultValue={streamingServerUrlInput.value}
placeholder={'Enter a streaming server url'}
placeholder={t('SETTINGS_SERVER_CONFIGURE_INPUT')}
onSubmit={configureServerUrlOnSubmit}
/>
</ModalDialog>

View file

@ -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({