Use locale/get response to suggest homepage and language switch (#839)

* Use locale/get response to suggest homepage and language switch

* Fix language modal condition

* Fixes from review

* Fixes from review

* Fix gdpr

* string

* Fix multiple options behavior

* Fix gdpr and use only one fetch

* Only show if no languages set or loaded

* pt-br

* Fix ad

* Fix homepage select

* Fix zh langs
This commit is contained in:
saltrafael 2022-03-02 11:44:01 -03:00 committed by GitHub
parent f839e0c35d
commit 712e02db16
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 684 additions and 99 deletions

View file

@ -18,6 +18,7 @@ SOCKETY_SERVER_API=wss://sockety.odysee.com/ws
RECSYS_ENDPOINT=https://recsys.odysee.tv/v1/lvv RECSYS_ENDPOINT=https://recsys.odysee.tv/v1/lvv
THUMBNAIL_CDN_URL=https://thumbnails.odycdn.com/optimize/ THUMBNAIL_CDN_URL=https://thumbnails.odycdn.com/optimize/
THUMBNAIL_CARDS_CDN_URL=https://cards.odycdn.com/ THUMBNAIL_CARDS_CDN_URL=https://cards.odycdn.com/
LOCALE_API=https://api.odysee.com/locale/get
THUMBNAIL_HEIGHT=220 THUMBNAIL_HEIGHT=220
THUMBNAIL_WIDTH=390 THUMBNAIL_WIDTH=390
THUMBNAIL_QUALITY=85 THUMBNAIL_QUALITY=85

View file

@ -31,6 +31,7 @@ module.name_mapper='^rewards\(.*\)$' -> '<PROJECT_ROOT>/ui/rewards\1'
module.name_mapper='^i18n\(.*\)$' -> '<PROJECT_ROOT>/ui/i18n\1' module.name_mapper='^i18n\(.*\)$' -> '<PROJECT_ROOT>/ui/i18n\1'
module.name_mapper='^effects\(.*\)$' -> '<PROJECT_ROOT>/ui/effects\1' module.name_mapper='^effects\(.*\)$' -> '<PROJECT_ROOT>/ui/effects\1'
module.name_mapper='^comments\(.*\)$' -> '<PROJECT_ROOT>/ui/comments\1' module.name_mapper='^comments\(.*\)$' -> '<PROJECT_ROOT>/ui/comments\1'
module.name_mapper='^locale\(.*\)$' -> '<PROJECT_ROOT>/ui/locale\1'
module.name_mapper='^config\(.*\)$' -> '<PROJECT_ROOT>/config\1' module.name_mapper='^config\(.*\)$' -> '<PROJECT_ROOT>/config\1'
module.name_mapper='^web\/component\(.*\)$' -> '<PROJECT_ROOT>/web/component\1' module.name_mapper='^web\/component\(.*\)$' -> '<PROJECT_ROOT>/web/component\1'
module.name_mapper='^web\/effects\(.*\)$' -> '<PROJECT_ROOT>/web/effects\1' module.name_mapper='^web\/effects\(.*\)$' -> '<PROJECT_ROOT>/web/effects\1'

View file

@ -16,6 +16,7 @@ const config = {
SEARCH_SERVER_API_ALT: process.env.SEARCH_SERVER_API_ALT, SEARCH_SERVER_API_ALT: process.env.SEARCH_SERVER_API_ALT,
COMMENT_SERVER_API: process.env.COMMENT_SERVER_API, COMMENT_SERVER_API: process.env.COMMENT_SERVER_API,
SOCKETY_SERVER_API: process.env.SOCKETY_SERVER_API, SOCKETY_SERVER_API: process.env.SOCKETY_SERVER_API,
LOCALE_API: process.env.LOCALE_API,
WELCOME_VERSION: process.env.WELCOME_VERSION, WELCOME_VERSION: process.env.WELCOME_VERSION,
DOMAIN: process.env.DOMAIN, DOMAIN: process.env.DOMAIN,
SHARE_DOMAIN_URL: process.env.SHARE_DOMAIN_URL, SHARE_DOMAIN_URL: process.env.SHARE_DOMAIN_URL,

View file

@ -2209,5 +2209,12 @@
"The minimum duration must not exceed Feb 8th, 2022.": "The minimum duration must not exceed Feb 8th, 2022.", "The minimum duration must not exceed Feb 8th, 2022.": "The minimum duration must not exceed Feb 8th, 2022.",
"No limit": "No limit", "No limit": "No limit",
"Search results are being filtered by language. Click here to change the setting.": "Search results are being filtered by language. Click here to change the setting.", "Search results are being filtered by language. Click here to change the setting.": "Search results are being filtered by language. Click here to change the setting.",
"There are language translations available for your location! Do you want to switch from English?": "There are language translations available for your location! Do you want to switch from English?",
"A homepage and language translations are available for your location! Do you want to switch?": "A homepage and language translations are available for your location! Do you want to switch?",
"Switch Now": "Switch Now",
"Both": "Both",
"Only Language": "Only Language",
"Only Homepage": "Only Homepage",
"Choose Your Preference": "Choose Your Preference",
"--end--": "--end--" "--end--": "--end--"
} }

View file

@ -34,9 +34,13 @@ import {
} from 'web/effects/use-degraded-performance'; } from 'web/effects/use-degraded-performance';
import LANGUAGE_MIGRATIONS from 'constants/language-migrations'; import LANGUAGE_MIGRATIONS from 'constants/language-migrations';
import { useIsMobile } from 'effects/use-screensize'; import { useIsMobile } from 'effects/use-screensize';
import { fetchLocaleApi } from 'locale';
import getLanguagesForCountry from 'constants/country_languages';
import SUPPORTED_LANGUAGES from 'constants/supported_languages';
const FileDrop = lazyImport(() => import('component/fileDrop' /* webpackChunkName: "fileDrop" */)); const FileDrop = lazyImport(() => import('component/fileDrop' /* webpackChunkName: "fileDrop" */));
const NagContinueFirstRun = lazyImport(() => import('component/nagContinueFirstRun' /* webpackChunkName: "nagCFR" */)); const NagContinueFirstRun = lazyImport(() => import('component/nagContinueFirstRun' /* webpackChunkName: "nagCFR" */));
const NagLocaleSwitch = lazyImport(() => import('component/nagLocaleSwitch' /* webpackChunkName: "nagLocaleSwitch" */));
const OpenInAppLink = lazyImport(() => import('web/component/openInAppLink' /* webpackChunkName: "openInAppLink" */)); const OpenInAppLink = lazyImport(() => import('web/component/openInAppLink' /* webpackChunkName: "openInAppLink" */));
const NagDataCollection = lazyImport(() => import('web/component/nag-data-collection' /* webpackChunkName: "nagDC" */)); const NagDataCollection = lazyImport(() => import('web/component/nag-data-collection' /* webpackChunkName: "nagDC" */));
const NagDegradedPerformance = lazyImport(() => const NagDegradedPerformance = lazyImport(() =>
@ -132,6 +136,10 @@ function App(props: Props) {
const previousHasVerifiedEmail = usePrevious(hasVerifiedEmail); const previousHasVerifiedEmail = usePrevious(hasVerifiedEmail);
const previousRewardApproved = usePrevious(isRewardApproved); const previousRewardApproved = usePrevious(isRewardApproved);
const [gdprRequired, setGdprRequired] = usePersistedState('gdprRequired');
const [localeLangs, setLocaleLangs] = React.useState();
const [localeSwitchDismissed] = usePersistedState('locale-switch-dismissed', false);
const [showAnalyticsNag, setShowAnalyticsNag] = usePersistedState('analytics-nag', true); const [showAnalyticsNag, setShowAnalyticsNag] = usePersistedState('analytics-nag', true);
const [lbryTvApiStatus, setLbryTvApiStatus] = useState(STATUS_OK); const [lbryTvApiStatus, setLbryTvApiStatus] = useState(STATUS_OK);
@ -212,6 +220,11 @@ function App(props: Props) {
/> />
); );
} }
if (localeLangs) {
const noLanguageSet = language === 'en' && languages.length === 1;
return <NagLocaleSwitch localeLangs={localeLangs} noLanguageSet={noLanguageSet} />;
}
} }
useEffect(() => { useEffect(() => {
@ -312,6 +325,7 @@ function App(props: Props) {
fetchModBlockedList(); fetchModBlockedList();
fetchModAmIList(); fetchModAmIList();
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasMyChannels, hasNoChannels, hasActiveChannelClaim, setActiveChannelIfNotSet, setIncognito]); }, [hasMyChannels, hasNoChannels, hasActiveChannelClaim, setActiveChannelIfNotSet, setIncognito]);
useEffect(() => { useEffect(() => {
@ -396,40 +410,42 @@ function App(props: Props) {
// OneTrust asks to add this // OneTrust asks to add this
secondScript.innerHTML = 'function OptanonWrapper() { }'; secondScript.innerHTML = 'function OptanonWrapper() { }';
const getLocaleEndpoint = 'https://api.odysee.com/locale/get';
let gdprRequired;
try {
gdprRequired = localStorage.getItem('gdprRequired');
} catch (err) {
if (err) return;
}
// gdpr is known to be required, add script // gdpr is known to be required, add script
if (gdprRequired === 'true') { if (gdprRequired) {
// $FlowFixMe // $FlowFixMe
document.head.appendChild(script); document.head.appendChild(script);
// $FlowFixMe // $FlowFixMe
document.head.appendChild(secondScript); document.head.appendChild(secondScript);
} }
// haven't done a gdpr check, do it now fetchLocaleApi().then((response) => {
if (gdprRequired === null) { if (!localeLangs && !localeSwitchDismissed) {
(async function () { const countryCode = response?.data?.country;
const response = await fetch(getLocaleEndpoint); const langs = getLanguagesForCountry(countryCode);
const json = await response.json();
const gdprRequiredBasedOnLocation = json.data.gdpr_required; const supportedLangs = [];
langs.forEach((lang) => lang !== 'en' && SUPPORTED_LANGUAGES[lang] && supportedLangs.push(lang));
if (supportedLangs.length > 0) setLocaleLangs(supportedLangs);
}
// haven't done a gdpr check, do it now
if (gdprRequired === null || gdprRequired === undefined) {
const gdprRequiredBasedOnLocation = response?.data?.gdpr_required;
// note we need gdpr and load script // note we need gdpr and load script
if (gdprRequiredBasedOnLocation) { if (gdprRequiredBasedOnLocation) {
localStorage.setItem('gdprRequired', 'true'); setGdprRequired(true);
// $FlowFixMe // $FlowFixMe
document.head.appendChild(script); document.head.appendChild(script);
// $FlowFixMe // $FlowFixMe
document.head.appendChild(secondScript); document.head.appendChild(secondScript);
// note we don't need gdpr, save to session // note we don't need gdpr, save to session
} else if (gdprRequiredBasedOnLocation === false) { } else if (gdprRequiredBasedOnLocation === false) {
localStorage.setItem('gdprRequired', 'false'); setGdprRequired(false);
} }
})(); }
} });
return () => { return () => {
try { try {
@ -438,9 +454,11 @@ function App(props: Props) {
// $FlowFixMe // $FlowFixMe
document.head.removeChild(secondScript); document.head.removeChild(secondScript);
} catch (err) { } catch (err) {
// eslint-disable-next-line no-console
console.log(err); console.log(err);
} }
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
// ready for sync syncs, however after signin when hasVerifiedEmail, that syncs too. // ready for sync syncs, however after signin when hasVerifiedEmail, that syncs too.

View file

@ -7,6 +7,8 @@ import Button from 'component/button';
type Props = { type Props = {
message: string | Node, message: string | Node,
action?: Node,
closeTitle?: string,
actionText?: string, actionText?: string,
href?: string, href?: string,
type?: string, type?: string,
@ -17,9 +19,20 @@ type Props = {
}; };
export default function Nag(props: Props) { export default function Nag(props: Props) {
const { message, actionText, href, onClick, onClose, type, inline, relative } = props; const {
message,
action: customAction,
closeTitle,
actionText,
href,
onClick,
onClose,
type,
inline,
relative,
} = props;
const buttonProps = onClick ? { onClick } : { href }; const buttonProps = onClick ? { onClick } : href ? { href } : null;
return ( return (
<div <div
@ -31,7 +44,10 @@ export default function Nag(props: Props) {
})} })}
> >
<div className="nag__message">{message}</div> <div className="nag__message">{message}</div>
{(href || onClick) && (
{customAction}
{buttonProps && (
<Button <Button
className={classnames('nag__button', { className={classnames('nag__button', {
'nag__button--helpful': type === 'helpful', 'nag__button--helpful': type === 'helpful',
@ -42,12 +58,14 @@ export default function Nag(props: Props) {
{actionText} {actionText}
</Button> </Button>
)} )}
{onClose && ( {onClose && (
<Button <Button
className={classnames('nag__button nag__close', { className={classnames('nag__button nag__close', {
'nag__button--helpful': type === 'helpful', 'nag__button--helpful': type === 'helpful',
'nag__button--error': type === 'error', 'nag__button--error': type === 'error',
})} })}
title={closeTitle}
icon={ICONS.REMOVE} icon={ICONS.REMOVE}
onClick={onClose} onClick={onClose}
/> />

View file

@ -2,7 +2,7 @@
import React from 'react'; import React from 'react';
// $FlowFixMe // $FlowFixMe
import homepages from 'homepages'; import homepages from 'homepages';
import LANGUAGES from 'constants/languages'; import { getLanguageEngName } from 'constants/languages';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
import { getDefaultHomepageKey } from 'util/default-languages'; import { getDefaultHomepageKey } from 'util/default-languages';
@ -31,7 +31,7 @@ function SelectHomepage(props: Props) {
> >
{Object.keys(homepages).map((hp) => ( {Object.keys(homepages).map((hp) => (
<option key={'hp' + hp} value={hp}> <option key={'hp' + hp} value={hp}>
{`${LANGUAGES[hp][1]}`} {`${getLanguageEngName(hp)}`}
</option> </option>
))} ))}
</FormField> </FormField>

View file

@ -0,0 +1,12 @@
import { connect } from 'react-redux';
import { doSetLanguage, doSetHomepage } from 'redux/actions/settings';
import { doOpenModal } from 'redux/actions/app';
import NagLocaleSwitch from './view';
const perform = {
doSetLanguage,
doSetHomepage,
doOpenModal,
};
export default connect(null, perform)(NagLocaleSwitch);

View file

@ -0,0 +1,194 @@
// @flow
import { FormField } from 'component/common/form';
import * as MODALS from 'constants/modal_types';
import HOMEPAGE_LANGUAGES, { getHomepageLanguage } from 'constants/homepage_languages';
import Nag from 'component/common/nag';
import React from 'react';
import usePersistedState from 'effects/use-persisted-state';
import { getLanguageEngName, getLanguageName } from 'constants/languages';
const LOCALE_OPTIONS = {
BOTH: 'both',
LANG: 'lang',
HOME: 'home',
};
type Props = {
localeLangs: Array<string>,
noLanguageSet: boolean,
// redux
doSetLanguage: (string) => void,
doSetHomepage: (string) => void,
doOpenModal: (string, {}) => void,
};
export default function NagLocaleSwitch(props: Props) {
const { localeLangs, noLanguageSet, doSetLanguage, doSetHomepage, doOpenModal } = props;
const [switchOption, setSwitchOption] = React.useState(LOCALE_OPTIONS.BOTH);
const [localeSwitchDismissed, setLocaleSwitchDismissed] = usePersistedState('locale-switch-dismissed', false);
const hasHomepageForLang = localeLangs.some((lang) => getHomepageLanguage(lang));
const message = __(
// If no homepage, only suggest language switch
!hasHomepageForLang
? 'There are language translations available for your location! Do you want to switch from English?'
: 'A homepage and language translations are available for your location! Do you want to switch?'
);
if (localeSwitchDismissed || (!noLanguageSet && !hasHomepageForLang)) return null;
function dismissNag() {
setLocaleSwitchDismissed(true);
}
function handleSwitch() {
const homepages = [];
localeLangs.forEach((lang) => {
const homepageLanguage = getHomepageLanguage(lang);
if (homepageLanguage && !homepages.includes(homepageLanguage)) {
homepages.push(homepageLanguage);
}
});
const homeSwitchSelected = switchOption === LOCALE_OPTIONS.BOTH || switchOption === LOCALE_OPTIONS.HOME;
const multipleHomepages = homeSwitchSelected && homepages.length > 1;
const langSwitchSelected = switchOption === LOCALE_OPTIONS.BOTH || switchOption === LOCALE_OPTIONS.LANG;
const multipleLangs = langSwitchSelected && localeLangs.length > 1;
// if language or homepage has more than 1 option, modal for selection
// if some has only one option, still show the selection for confirmation of what's being switched
if (multipleHomepages || multipleLangs) {
doOpenModal(MODALS.CONFIRM, {
title: __('Choose Your Preference'),
body: (
<>
{langSwitchSelected && <LanguageSelect langs={localeLangs} />}
{homeSwitchSelected && <HomepageSelect homepages={homepages} />}
</>
),
onConfirm: (closeModal) => {
if (langSwitchSelected) {
// $FlowFixMe
const selection = document.querySelector('.language-switch.checked').id.split(' ')[1];
doSetLanguage(selection);
}
if (homeSwitchSelected) {
// $FlowFixMe
const selection = document.querySelector('.homepage-switch.checked').id.split(' ')[1];
let homepageSelection = '';
Object.values(HOMEPAGE_LANGUAGES).some((lang, index) => {
if (lang === selection) {
homepageSelection = Object.keys(HOMEPAGE_LANGUAGES)[index];
return true;
}
});
doSetHomepage(homepageSelection);
}
dismissNag();
closeModal();
},
});
// if selected switch has only one option, just make the switch
} else {
const onlyLanguage = localeLangs[0];
if (langSwitchSelected) doSetLanguage(onlyLanguage);
if (homeSwitchSelected) doSetHomepage(onlyLanguage);
dismissNag();
}
}
return (
<Nag
message={message}
type="helpful"
action={
// Menu field only needed if there is a homepage + language to choose, otherwise
// there is only 1 option to switch, so use the nag button
hasHomepageForLang && (
<FormField
className="nag__select"
type="select"
value={switchOption}
onChange={(e) => setSwitchOption(e.target.value)}
>
<option value={LOCALE_OPTIONS.BOTH}>{__('Both')}</option>
<option value={LOCALE_OPTIONS.LANG}>{__('Only Language')}</option>
<option value={LOCALE_OPTIONS.HOME}>{__('Only Homepage')}</option>
</FormField>
)
}
actionText={__('Switch Now')}
onClick={handleSwitch}
onClose={dismissNag}
closeTitle={__('Dismiss')}
/>
);
}
type HomepageProps = {
homepages: Array<string>,
};
const HomepageSelect = (props: HomepageProps) => {
const { homepages } = props;
const [selection, setSelection] = React.useState(homepages[0]);
return (
<>
<h1>{__('Homepage')}</h1>
{homepages.map((homepage) => (
<FormField
type="radio"
className={`homepage-switch ${selection === homepage ? 'checked' : ''}`}
name={`homepage_switch ${homepage}`}
key={homepage}
label={homepage}
checked={selection === homepage}
onChange={() => setSelection(homepage)}
/>
))}
</>
);
};
type LangProps = {
langs: Array<string>,
};
const LanguageSelect = (props: LangProps) => {
const { langs } = props;
const [selection, setSelection] = React.useState(langs[0]);
return (
<>
<h1>{__('Language')}</h1>
{langs.map((lang) => {
const language = getLanguageEngName(lang);
const languageName = getLanguageName(lang);
const label = language === languageName ? language : `${language} - ${languageName}`;
return (
<FormField
type="radio"
className={`language-switch ${selection === lang ? 'checked' : ''}`}
name={`language_switch ${lang}`}
key={lang}
label={label}
checked={selection === lang}
onChange={() => setSelection(lang)}
/>
);
})}
</>
);
};

View file

@ -0,0 +1,279 @@
/*
* Upstream source: https://wiki.openstreetmap.org/wiki/Nominatim/Country_Codes
*/
const COUNTRY_LANGUAGES = {
ad: 'ca',
ae: 'ar',
af: 'fa,ps',
ag: 'en',
ai: 'en',
al: 'sq',
am: 'hy',
ao: 'pt',
aq: '',
ar: 'es',
as: 'en,sm',
at: 'de',
au: 'en',
aw: 'nl,pap',
ax: 'sv',
az: 'az',
ba: 'bs,hr,sr',
bb: 'en',
bd: 'bn',
be: 'nl,fr,de',
bf: 'fr',
bg: 'bg',
bh: 'ar',
bi: 'fr',
bj: 'fr',
bl: 'fr',
bm: 'en',
bn: 'ms',
bo: 'es,qu,ay',
bq: 'nl',
br: 'pt',
bs: 'en',
bt: 'dz',
bv: 'no',
bw: 'en,tn',
by: 'be,ru',
bz: 'en',
ca: 'en,fr',
cc: 'en',
cd: 'fr',
cf: 'fr',
cg: 'fr',
ch: 'de,fr,it,rm',
ci: 'fr',
ck: 'en,rar',
cl: 'es',
cm: 'fr,en',
cn: 'zh',
co: 'es',
cr: 'es',
cu: 'es',
cv: 'pt',
cw: 'nl,en',
cx: 'en',
cy: 'el,tr',
cz: 'cs',
de: 'de',
dj: 'fr,ar',
dk: 'da',
dm: 'en',
do: 'es',
dz: 'ar,ber',
ec: 'es',
ee: 'et',
eg: 'ar',
eh: 'ar,es,fr',
er: 'ti,ar,en',
es: 'es',
et: 'am,om',
fi: 'fi,sv,se',
fj: 'en',
fk: 'en',
fm: 'en',
fo: 'fo',
fr: 'fr',
ga: 'fr',
gb: 'en',
gd: 'en',
ge: 'ka',
gf: 'fr',
gg: 'en',
gh: 'en',
gi: 'en',
gl: 'kl,da',
gm: 'en',
gn: 'fr',
gp: 'fr',
gq: 'es',
gr: 'el',
gs: 'en',
gt: 'es',
gu: 'en,ch',
gw: 'pt',
gy: 'en',
hk: 'zh,en',
hm: 'en',
hn: 'es',
hr: 'hr',
ht: 'fr,ht',
hu: 'hu',
id: 'id',
ie: 'en,ga',
il: 'he',
im: 'en',
in: 'hi,en',
io: 'en',
iq: 'ar,ku',
ir: 'fa',
is: 'is',
it: 'it',
je: 'en',
jm: 'en',
jo: 'ar',
jp: 'ja',
ke: 'sw,en',
kg: 'ky,ru',
kh: 'km',
ki: 'en',
km: 'bnt,ar,fr',
kn: 'en',
kp: 'ko',
kr: 'ko',
kw: 'ar',
ky: 'en',
kz: 'kk,ru',
la: 'lo',
lb: 'ar,fr',
lc: 'en',
li: 'de',
lk: 'si,ta',
lr: 'en',
ls: 'en,st',
lt: 'lt',
lu: 'lb,fr,de',
lv: 'lv',
ly: 'ar,ber',
ma: 'ar,ber',
mc: 'fr',
md: 'ru,uk,ro',
me: 'sr,sh',
mf: 'fr',
mg: 'mg,fr',
mh: 'en,mh',
mk: 'mk',
ml: 'fr',
mm: 'my',
mn: 'mn',
mo: 'zh,pt',
mp: 'ch',
mq: 'fr',
mr: 'ar,fr',
ms: 'en',
mt: 'mt,en',
mu: 'mfe,fr,en',
mv: 'dv',
mw: 'en,ny',
mx: 'es',
my: 'en,ms,zh,ta',
mz: 'pt',
na: 'en,sf,de',
nc: 'fr',
ne: 'fr',
nf: 'en,pih',
ng: 'en',
ni: 'es',
nl: 'nl',
no: 'nb,nn,no',
np: 'ne',
nr: 'na,en',
nu: 'niu,en',
nz: 'mi,en',
om: 'ar',
pa: 'es',
pe: 'es',
pf: 'fr',
pg: 'en,tpi,ho',
ph: 'en,tl',
pk: 'en,ur',
pl: 'pl',
pm: 'fr',
pn: 'en,pih',
pr: 'es,en',
ps: 'ar,he',
pt: 'pt',
pw: 'en,pau,ja,sov,tox',
py: 'es,gn',
qa: 'ar',
re: 'fr',
ro: 'ro',
rs: 'sr',
ru: 'ru',
rw: 'rw,fr,en',
sa: 'ar',
sb: 'en',
sc: 'fr,en,crs',
sd: 'ar,en',
se: 'sv',
sg: 'en,ms,zh,ta',
sh: 'en',
si: 'sl',
sj: 'no',
sk: 'sk',
sl: 'en',
sm: 'it',
sn: 'fr',
so: 'so,ar',
sr: 'nl',
st: 'pt',
ss: 'en',
sv: 'es',
sx: 'nl,en',
sy: 'ar',
sz: 'en,ss',
tc: 'en',
td: 'fr,ar',
tf: 'fr',
tg: 'fr',
th: 'th',
tj: 'tg,ru',
tk: 'tkl,en,sm',
tl: 'pt,tet',
tm: 'tk',
tn: 'ar',
to: 'en',
tr: 'tr',
tt: 'en',
tv: 'en',
tw: 'zh',
tz: 'sw,en',
ua: 'uk',
ug: 'en,sw',
um: 'en',
us: 'en',
uy: 'es',
uz: 'uz,kaa',
va: 'it',
vc: 'en',
ve: 'es',
vg: 'en',
vi: 'en',
vn: 'vi',
vu: 'bi,en,fr',
wf: 'fr',
ws: 'sm,en',
ye: 'ar',
yt: 'fr',
za: 'af,zu,xh',
zm: 'en',
zw: 'en,sn,nd',
};
export default function getLanguagesForCountry(countryCode) {
const country = countryCode.toLowerCase();
const countryLanguages = COUNTRY_LANGUAGES[country];
if (!countryLanguages || countryLanguages.length === 0) return null;
const languages = countryLanguages.split(',');
// ----overrides----
if (country === 'br') return ['pt-BR'];
const zhCountries = ['cn', 'hk', 'tw'];
const zhLangs = ['zh-Hans', 'zh-Hant'];
if (zhCountries.includes(country)) return zhLangs;
if (languages.includes('zh')) {
languages.filter((lang) => lang === 'zh');
languages.push(...zhLangs);
}
// -----------------
return languages;
}

View file

@ -0,0 +1,21 @@
import { getLanguageEngName } from 'constants/languages';
const HOMEPAGE_LANGUAGES = {
en: getLanguageEngName('en'),
fr: getLanguageEngName('fr'),
es: getLanguageEngName('es'),
de: getLanguageEngName('de'),
zh: getLanguageEngName('zh'),
ru: getLanguageEngName('ru'),
'pt-BR': getLanguageEngName('pt-BR'),
};
export function getHomepageLanguage(code) {
// -----override-----
if (code === 'zh-Hans' || code === 'zh-Hant') return HOMEPAGE_LANGUAGES.zh;
// ------------------
return HOMEPAGE_LANGUAGES[code] || null;
}
export default HOMEPAGE_LANGUAGES;

View file

@ -187,4 +187,12 @@ const LANGUAGES = {
zu: ['Zulu', 'isiZulu'], zu: ['Zulu', 'isiZulu'],
}; };
export function getLanguageEngName(code) {
return LANGUAGES[code][0];
}
export function getLanguageName(code) {
return LANGUAGES[code][1];
}
export default LANGUAGES; export default LANGUAGES;

View file

@ -1,18 +1,18 @@
import LANGUAGES from './languages'; import { getLanguageName } from './languages';
const SEARCHABLE_LANGUAGES = { const SEARCHABLE_LANGUAGES = {
en: LANGUAGES.en[1], en: getLanguageName('en'),
hr: LANGUAGES.hr[1], hr: getLanguageName('hr'),
nl: LANGUAGES.nl[1], nl: getLanguageName('nl'),
fr: LANGUAGES.fr[1], fr: getLanguageName('fr'),
de: LANGUAGES.de[1], de: getLanguageName('de'),
it: LANGUAGES.it[1], it: getLanguageName('it'),
pl: LANGUAGES.pl[1], pl: getLanguageName('pl'),
pt: LANGUAGES.pt[1], pt: getLanguageName('pt'),
ru: LANGUAGES.ru[1], ru: getLanguageName('ru'),
es: LANGUAGES.es[1], es: getLanguageName('es'),
tr: LANGUAGES.tr[1], tr: getLanguageName('tr'),
cs: LANGUAGES.cs[1], cs: getLanguageName('cs'),
}; };
// Properties: language code (e.g. 'ja') // Properties: language code (e.g. 'ja')

View file

@ -1,47 +1,48 @@
import LANGUAGES from './languages'; import { getLanguageName } from './languages';
// supported_browser_languages // supported_browser_languages
const SUPPORTED_LANGUAGES = { const SUPPORTED_LANGUAGES = {
af: LANGUAGES.af[1], af: getLanguageName('af'),
en: LANGUAGES.en[1], en: getLanguageName('en'),
da: LANGUAGES.da[1], da: getLanguageName('da'),
'zh-Hans': LANGUAGES['zh-Hans'][1], 'zh-Hans': getLanguageName('zh-Hans'),
'zh-Hant': LANGUAGES['zh-Hant'][1], 'zh-Hant': getLanguageName('zh-Hant'),
hr: LANGUAGES.hr[1], hr: getLanguageName('hr'),
nl: LANGUAGES.nl[1], nl: getLanguageName('nl'),
no: LANGUAGES.no[1], no: getLanguageName('no'),
fi: LANGUAGES.fi[1], fi: getLanguageName('fi'),
fr: LANGUAGES.fr[1], fr: getLanguageName('fr'),
de: LANGUAGES.de[1], de: getLanguageName('de'),
gu: LANGUAGES.gu[1], gu: getLanguageName('gu'),
hi: LANGUAGES.hi[1], hi: getLanguageName('hi'),
hu: LANGUAGES.hu[1], hu: getLanguageName('hu'),
id: LANGUAGES.id[1], id: getLanguageName('id'),
ja: LANGUAGES.ja[1], ja: getLanguageName('ja'),
jv: LANGUAGES.jv[1], jv: getLanguageName('jv'),
it: LANGUAGES.it[1], it: getLanguageName('it'),
ms: LANGUAGES.ms[1], ms: getLanguageName('ms'),
ml: LANGUAGES.ml[1], ml: getLanguageName('ml'),
mr: LANGUAGES.mr[1], mr: getLanguageName('mr'),
pa: LANGUAGES.pa[1], pa: getLanguageName('pa'),
pl: LANGUAGES.pl[1], pl: getLanguageName('pl'),
pt: LANGUAGES.pt[1], pt: getLanguageName('pt'),
'pt-BR': LANGUAGES['pt-BR'][1], 'pt-BR': getLanguageName('pt-BR'),
ro: LANGUAGES.ro[1], ro: getLanguageName('ro'),
ru: LANGUAGES.ru[1], ru: getLanguageName('ru'),
sr: LANGUAGES.sr[1], sr: getLanguageName('sr'),
sk: LANGUAGES.sk[1], sk: getLanguageName('sk'),
th: LANGUAGES.th[1], th: getLanguageName('th'),
ur: LANGUAGES.ur[1], ur: getLanguageName('ur'),
ca: LANGUAGES.ca[1], ca: getLanguageName('ca'),
es: LANGUAGES.es[1], es: getLanguageName('es'),
sv: LANGUAGES.sv[1], sv: getLanguageName('sv'),
tl: LANGUAGES.tl[1], tl: getLanguageName('tl'),
tr: LANGUAGES.tr[1], tr: getLanguageName('tr'),
cs: LANGUAGES.cs[1], cs: getLanguageName('cs'),
kn: LANGUAGES.kn[1], kn: getLanguageName('kn'),
uk: LANGUAGES.uk[1], uk: getLanguageName('uk'),
vi: LANGUAGES.vi[1], vi: getLanguageName('vi'),
ar: LANGUAGES.ar[1], ar: getLanguageName('ar'),
}; };
// Properties: language code (e.g. 'ja') // Properties: language code (e.g. 'ja')

6
ui/locale.js Normal file
View file

@ -0,0 +1,6 @@
// @flow
import { LOCALE_API } from 'config';
export async function fetchLocaleApi() {
return fetch(LOCALE_API).then((res) => res.json());
}

View file

@ -1,10 +1,9 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app'; import { doHideModal } from 'redux/actions/app';
import ModalConfirm from './view'; import ModalConfirm from './view';
const perform = (dispatch) => ({ const perform = {
doHideModal: () => dispatch(doHideModal()), doHideModal,
}); };
export default connect(null, perform)(ModalConfirm); export default connect(null, perform)(ModalConfirm);

View file

@ -12,14 +12,15 @@ type Props = {
body?: string | Node, body?: string | Node,
labelOk?: string, labelOk?: string,
labelCancel?: string, labelCancel?: string,
onConfirm: (closeModal: () => void, setIsBusy: (boolean) => void) => void,
hideCancel?: boolean, hideCancel?: boolean,
onConfirm: (closeModal: () => void, setIsBusy: (boolean) => void) => void,
// --- perform --- // --- perform ---
doHideModal: () => void, doHideModal: () => void,
}; };
export default function ModalConfirm(props: Props) { export default function ModalConfirm(props: Props) {
const { title, subtitle, body, labelOk, labelCancel, onConfirm, hideCancel, doHideModal } = props; const { title, subtitle, body, labelOk, labelCancel, hideCancel, onConfirm, doHideModal } = props;
const [isBusy, setIsBusy] = React.useState(false); const [isBusy, setIsBusy] = React.useState(false);
function handleOnClick() { function handleOnClick() {
@ -28,14 +29,6 @@ export default function ModalConfirm(props: Props) {
} }
} }
function getOkLabel() {
return isBusy ? <Spinner type="small" /> : labelOk || __('OK');
}
function getCancelLabel() {
return labelCancel || __('Cancel');
}
return ( return (
<Modal isOpen type="card" onAborted={doHideModal}> <Modal isOpen type="card" onAborted={doHideModal}>
<Card <Card
@ -43,12 +36,18 @@ export default function ModalConfirm(props: Props) {
subtitle={subtitle} subtitle={subtitle}
body={body} body={body}
actions={ actions={
<> <div className="section__actions">
<div className="section__actions"> <Button
<Button button="primary" label={getOkLabel()} disabled={isBusy} onClick={handleOnClick} /> button="primary"
{!hideCancel && <Button button="link" label={getCancelLabel()} disabled={isBusy} onClick={doHideModal} />} label={isBusy ? <Spinner type="small" /> : labelOk || __('OK')}
</div> disabled={isBusy}
</> onClick={handleOnClick}
/>
{!hideCancel && (
<Button button="link" label={labelCancel || __('Cancel')} disabled={isBusy} onClick={doHideModal} />
)}
</div>
} }
/> />
</Modal> </Modal>

View file

@ -176,7 +176,9 @@ function DiscoverPage(props: Props) {
} }
React.useEffect(() => { React.useEffect(() => {
if (isAuthenticated || !SHOW_ADS || window.location.pathname === `/$/${PAGES.WILD_WEST}`) { const hasAdOnPage = document.querySelector('.homepageAdContainer');
if (hasAdOnPage || isAuthenticated || !SHOW_ADS || window.location.pathname === `/$/${PAGES.WILD_WEST}`) {
return; return;
} }
injectAd(); injectAd();

View file

@ -109,6 +109,11 @@
z-index: 10000; z-index: 10000;
margin-top: var(--spacing-s); margin-top: var(--spacing-s);
} }
.radio,
.radio + h1 {
margin-top: var(--spacing-s);
}
} }
.modal--card-internal { .modal--card-internal {

View file

@ -112,3 +112,16 @@ $nag-error-z-index: 999;
stroke-width: 4px; stroke-width: 4px;
} }
} }
.nag__select {
display: inline;
color: var(--color-text);
select {
width: unset;
min-width: 10rem;
padding-right: unset;
margin: 0px var(--spacing-s);
height: var(--height-button-mobile);
}
}