Auth: Support switching to system language (#1324)
This commit is contained in:
parent
752d9a4df7
commit
c52ee0574f
@ -9,9 +9,9 @@ import {
|
||||
import { DEBUG } from '../../../config';
|
||||
|
||||
const ApiErrors: { [k: string]: string } = {
|
||||
PHONE_NUMBER_INVALID: 'Invalid Phone Number',
|
||||
PHONE_CODE_INVALID: 'Invalid Code',
|
||||
PASSWORD_HASH_INVALID: 'Invalid Password',
|
||||
PHONE_NUMBER_INVALID: 'PHONE_NUMBER_INVALID',
|
||||
PHONE_CODE_INVALID: 'PHONE_CODE_INVALID',
|
||||
PASSWORD_HASH_INVALID: 'PASSWORD_HASH_INVALID',
|
||||
PHONE_PASSWORD_FLOOD: 'You have tried logging in too many times',
|
||||
};
|
||||
|
||||
|
||||
@ -47,7 +47,7 @@ export {
|
||||
fetchAuthorizations, terminateAuthorization, terminateAllAuthorizations,
|
||||
fetchNotificationExceptions, fetchNotificationSettings, updateContactSignUpNotification, updateNotificationSettings,
|
||||
fetchLanguages, fetchLangPack, fetchPrivacySettings, setPrivacySettings, registerDevice, unregisterDevice,
|
||||
updateIsOnline, fetchContentSettings, updateContentSettings,
|
||||
updateIsOnline, fetchContentSettings, updateContentSettings, fetchLangStrings,
|
||||
} from './settings';
|
||||
|
||||
export {
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
} from '../../types';
|
||||
import { ApiPrivacyKey, IInputPrivacyRules } from '../../../types';
|
||||
|
||||
import { BLOCKED_LIST_LIMIT, DEFAULT_LANG_PACK } from '../../../config';
|
||||
import { BLOCKED_LIST_LIMIT, DEFAULT_LANG_PACK, LANG_PACKS } from '../../../config';
|
||||
import {
|
||||
buildApiWallpaper, buildApiSession, buildPrivacyRules, buildApiNotifyException,
|
||||
} from '../apiBuilders/misc';
|
||||
@ -278,7 +278,10 @@ export async function fetchLanguages(): Promise<ApiLanguage[] | undefined> {
|
||||
return result.map(omitVirtualClassFields);
|
||||
}
|
||||
|
||||
export async function fetchLangPack({ sourceLangPacks, langCode }: { sourceLangPacks: string[]; langCode: string }) {
|
||||
export async function fetchLangPack({ sourceLangPacks, langCode }: {
|
||||
sourceLangPacks: typeof LANG_PACKS;
|
||||
langCode: string;
|
||||
}) {
|
||||
const results = await Promise.all(sourceLangPacks.map((langPack) => {
|
||||
return invokeRequest(new GramJs.langpack.GetLangPack({
|
||||
langPack,
|
||||
@ -299,6 +302,22 @@ export async function fetchLangPack({ sourceLangPacks, langCode }: { sourceLangP
|
||||
return { langPack: Object.assign({}, ...collections.reverse()) };
|
||||
}
|
||||
|
||||
export async function fetchLangStrings({ langPack, langCode, keys }: {
|
||||
langPack: string; langCode: string; keys: string[];
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.langpack.GetStrings({
|
||||
langPack,
|
||||
langCode: BETA_LANG_CODES.includes(langCode) ? `${langCode}-raw` : langCode,
|
||||
keys,
|
||||
}));
|
||||
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return result.map(omitVirtualClassFields);
|
||||
}
|
||||
|
||||
export async function fetchPrivacySettings(privacyKey: ApiPrivacyKey) {
|
||||
const key = buildInputPrivacyKey(privacyKey);
|
||||
const result = await invokeRequest(new GramJs.account.GetPrivacy({ key }));
|
||||
|
||||
@ -7,7 +7,9 @@ import { GlobalState, GlobalActions } from '../../global/types';
|
||||
|
||||
import { IS_TOUCH_ENV } from '../../util/environment';
|
||||
import { pick } from '../../util/iteratees';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import InputText from '../ui/InputText';
|
||||
import Loading from '../ui/Loading';
|
||||
@ -29,6 +31,7 @@ const AuthCode: FC<StateProps & DispatchProps> = ({
|
||||
returnToAuthPhoneNumber,
|
||||
clearAuthError,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@ -91,31 +94,21 @@ const AuthCode: FC<StateProps & DispatchProps> = ({
|
||||
onClick={returnToAuthPhoneNumber}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
title="Sign In with another phone number"
|
||||
title={lang('WrongNumber')}
|
||||
>
|
||||
<i className="icon-edit" />
|
||||
</div>
|
||||
</h2>
|
||||
<p className="note">
|
||||
{authIsCodeViaApp ? (
|
||||
<>
|
||||
We have sent the code to the Telegram app
|
||||
<br />on your other device.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
We have sent you an SMS
|
||||
<br />with the code.
|
||||
</>
|
||||
)}
|
||||
{renderText(lang(authIsCodeViaApp ? 'SentAppCode' : 'Login.JustSentSms'), ['simple_markdown'])}
|
||||
</p>
|
||||
<InputText
|
||||
ref={inputRef}
|
||||
id="sign-in-code"
|
||||
label="Code"
|
||||
label={lang('Code')}
|
||||
onInput={onCodeChange}
|
||||
value={code}
|
||||
error={authError}
|
||||
error={authError && lang(authError)}
|
||||
autoComplete="off"
|
||||
inputMode="decimal"
|
||||
/>
|
||||
|
||||
@ -6,6 +6,7 @@ import { withGlobal } from '../../lib/teact/teactn';
|
||||
import { GlobalState, GlobalActions } from '../../global/types';
|
||||
|
||||
import { pick } from '../../util/iteratees';
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import MonkeyPassword from '../common/PasswordMonkey';
|
||||
import PasswordForm from '../common/PasswordForm';
|
||||
@ -16,6 +17,7 @@ type DispatchProps = Pick<GlobalActions, 'setAuthPassword' | 'clearAuthError'>;
|
||||
const AuthPassword: FC<StateProps & DispatchProps> = ({
|
||||
authIsLoading, authError, authHint, setAuthPassword, clearAuthError,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleChangePasswordVisibility = useCallback((isVisible) => {
|
||||
@ -30,14 +32,11 @@ const AuthPassword: FC<StateProps & DispatchProps> = ({
|
||||
<div id="auth-password-form" className="custom-scroll">
|
||||
<div className="auth-form">
|
||||
<MonkeyPassword isPasswordVisible={showPassword} />
|
||||
<h2>Enter Your Password</h2>
|
||||
<p className="note">
|
||||
Your account is protected with
|
||||
<br />an additional password.
|
||||
</p>
|
||||
<h2>{lang('Login.Header.Password')}</h2>
|
||||
<p className="note">{lang('Login.EnterPasswordDescription')}</p>
|
||||
<PasswordForm
|
||||
clearError={clearAuthError}
|
||||
error={authError}
|
||||
error={authError && lang(authError)}
|
||||
hint={authHint}
|
||||
isLoading={authIsLoading}
|
||||
isPasswordVisible={showPassword}
|
||||
|
||||
@ -3,18 +3,24 @@ import { ChangeEvent } from 'react';
|
||||
// @ts-ignore
|
||||
import monkeyPath from '../../assets/monkey.svg';
|
||||
|
||||
import { GlobalActions, GlobalState } from '../../global/types';
|
||||
import React, {
|
||||
FC, memo, useCallback, useEffect, useLayoutEffect, useRef, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { withGlobal } from '../../lib/teact/teactn';
|
||||
import {
|
||||
IS_SAFARI, IS_TOUCH_ENV,
|
||||
} from '../../util/environment';
|
||||
|
||||
import { GlobalActions, GlobalState } from '../../global/types';
|
||||
import { LangCode } from '../../types';
|
||||
|
||||
import { IS_SAFARI, IS_TOUCH_ENV } from '../../util/environment';
|
||||
import { preloadImage } from '../../util/files';
|
||||
import preloadFonts from '../../util/fonts';
|
||||
import { pick } from '../../util/iteratees';
|
||||
import { formatPhoneNumber, getCountryById, getCountryFromPhoneNumber } from '../../util/phoneNumber';
|
||||
import { setLanguage } from '../../util/langProvider';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useLangString from '../../hooks/useLangString';
|
||||
import { getSuggestedLanguage } from './helpers/getSuggestedLanguage';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
import Checkbox from '../ui/Checkbox';
|
||||
@ -25,9 +31,12 @@ import CountryCodeInput from './CountryCodeInput';
|
||||
type StateProps = Pick<GlobalState, (
|
||||
'connectionState' | 'authState' |
|
||||
'authPhoneNumber' | 'authIsLoading' | 'authIsLoadingQrCode' | 'authError' | 'authRememberMe' | 'authNearestCountry'
|
||||
)>;
|
||||
)> & {
|
||||
language?: LangCode;
|
||||
};
|
||||
type DispatchProps = Pick<GlobalActions, (
|
||||
'setAuthPhoneNumber' | 'setAuthRememberMe' | 'loadNearestCountry' | 'clearAuthError' | 'goToAuthQrCode'
|
||||
'setAuthPhoneNumber' | 'setAuthRememberMe' | 'loadNearestCountry' | 'clearAuthError' | 'goToAuthQrCode' |
|
||||
'setSettingOption'
|
||||
)>;
|
||||
|
||||
const MIN_NUMBER_LENGTH = 7;
|
||||
@ -43,19 +52,25 @@ const AuthPhoneNumber: FC<StateProps & DispatchProps> = ({
|
||||
authError,
|
||||
authRememberMe,
|
||||
authNearestCountry,
|
||||
language,
|
||||
setAuthPhoneNumber,
|
||||
setAuthRememberMe,
|
||||
loadNearestCountry,
|
||||
clearAuthError,
|
||||
goToAuthQrCode,
|
||||
setSettingOption,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const suggestedLanguage = getSuggestedLanguage();
|
||||
|
||||
const continueText = useLangString(suggestedLanguage, 'ContinueOnThisLanguage');
|
||||
const [country, setCountry] = useState<Country | undefined>();
|
||||
const [phoneNumber, setPhoneNumber] = useState<string | undefined>();
|
||||
const [isTouched, setIsTouched] = useState(false);
|
||||
const [lastSelection, setLastSelection] = useState<[number, number] | undefined>();
|
||||
const [isLoading, markIsLoading, unmarkIsLoading] = useFlag();
|
||||
|
||||
const fullNumber = country ? `${country.code} ${phoneNumber || ''}` : phoneNumber;
|
||||
const canSubmit = fullNumber && fullNumber.replace(/[^\d]+/g, '').length >= MIN_NUMBER_LENGTH;
|
||||
@ -99,6 +114,16 @@ const AuthPhoneNumber: FC<StateProps & DispatchProps> = ({
|
||||
setPhoneNumber(formatPhoneNumber(newFullNumber, selectedCountry));
|
||||
}, [country]);
|
||||
|
||||
const handleLangChange = useCallback(() => {
|
||||
markIsLoading();
|
||||
|
||||
setLanguage(suggestedLanguage!, () => {
|
||||
unmarkIsLoading();
|
||||
|
||||
setSettingOption({ language: suggestedLanguage });
|
||||
});
|
||||
}, [markIsLoading, setSettingOption, suggestedLanguage, unmarkIsLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (phoneNumber === undefined && authPhoneNumber) {
|
||||
parseFullNumber(authPhoneNumber);
|
||||
@ -169,11 +194,8 @@ const AuthPhoneNumber: FC<StateProps & DispatchProps> = ({
|
||||
<div id="auth-phone-number-form" className="custom-scroll">
|
||||
<div className="auth-form">
|
||||
<div id="logo" />
|
||||
<h2>Sign in to Telegram</h2>
|
||||
<p className="note">
|
||||
Please confirm your country and
|
||||
<br />enter your phone number.
|
||||
</p>
|
||||
<h2>Telegram</h2>
|
||||
<p className="note">{lang('StartText')}</p>
|
||||
<form action="" onSubmit={handleSubmit}>
|
||||
<CountryCodeInput
|
||||
id="sign-in-phone-code"
|
||||
@ -184,9 +206,9 @@ const AuthPhoneNumber: FC<StateProps & DispatchProps> = ({
|
||||
<InputText
|
||||
ref={inputRef}
|
||||
id="sign-in-phone-number"
|
||||
label="Phone Number"
|
||||
label={lang('Login.PhonePlaceholder')}
|
||||
value={fullNumber}
|
||||
error={authError}
|
||||
error={authError && lang(authError)}
|
||||
inputMode="tel"
|
||||
onChange={handlePhoneNumberChange}
|
||||
onPaste={IS_SAFARI ? handlePaste : undefined}
|
||||
@ -199,16 +221,19 @@ const AuthPhoneNumber: FC<StateProps & DispatchProps> = ({
|
||||
/>
|
||||
{canSubmit && (
|
||||
isAuthReady ? (
|
||||
<Button type="submit" ripple isLoading={authIsLoading}>Next</Button>
|
||||
<Button type="submit" ripple isLoading={authIsLoading}>{lang('Login.Next')}</Button>
|
||||
) : (
|
||||
<Loading />
|
||||
)
|
||||
)}
|
||||
{isAuthReady && (
|
||||
<Button isText ripple isLoading={authIsLoadingQrCode} onClick={goToAuthQrCode}>
|
||||
Log in by QR code
|
||||
{lang('Login.QR.Login')}
|
||||
</Button>
|
||||
)}
|
||||
{suggestedLanguage && suggestedLanguage !== language && continueText && (
|
||||
<Button isText isLoading={isLoading} onClick={handleLangChange}>{continueText}</Button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@ -216,21 +241,31 @@ const AuthPhoneNumber: FC<StateProps & DispatchProps> = ({
|
||||
};
|
||||
|
||||
export default memo(withGlobal(
|
||||
(global): StateProps => pick(global, [
|
||||
'connectionState',
|
||||
'authState',
|
||||
'authPhoneNumber',
|
||||
'authIsLoading',
|
||||
'authIsLoadingQrCode',
|
||||
'authError',
|
||||
'authRememberMe',
|
||||
'authNearestCountry',
|
||||
]),
|
||||
(global): StateProps => {
|
||||
const {
|
||||
settings: { byKey: { language } },
|
||||
} = global;
|
||||
|
||||
return {
|
||||
...pick(global, [
|
||||
'connectionState',
|
||||
'authState',
|
||||
'authPhoneNumber',
|
||||
'authIsLoading',
|
||||
'authIsLoadingQrCode',
|
||||
'authError',
|
||||
'authRememberMe',
|
||||
'authNearestCountry',
|
||||
]),
|
||||
language,
|
||||
};
|
||||
},
|
||||
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||
'setAuthPhoneNumber',
|
||||
'setAuthRememberMe',
|
||||
'clearAuthError',
|
||||
'loadNearestCountry',
|
||||
'goToAuthQrCode',
|
||||
'setSettingOption',
|
||||
]),
|
||||
)(AuthPhoneNumber));
|
||||
|
||||
@ -1,17 +1,29 @@
|
||||
import QrCreator from 'qr-creator';
|
||||
import React, {
|
||||
FC, useEffect, useRef, memo,
|
||||
FC, useEffect, useRef, memo, useCallback,
|
||||
} from '../../lib/teact/teact';
|
||||
import { withGlobal } from '../../lib/teact/teactn';
|
||||
|
||||
import { GlobalState, GlobalActions } from '../../global/types';
|
||||
import { LangCode } from '../../types';
|
||||
|
||||
import { pick } from '../../util/iteratees';
|
||||
import { setLanguage } from '../../util/langProvider';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
import useLangString from '../../hooks/useLangString';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import { getSuggestedLanguage } from './helpers/getSuggestedLanguage';
|
||||
|
||||
import Loading from '../ui/Loading';
|
||||
import Button from '../ui/Button';
|
||||
|
||||
type StateProps = Pick<GlobalState, 'connectionState' | 'authState' | 'authQrCode'>;
|
||||
type DispatchProps = Pick<GlobalActions, 'returnToAuthPhoneNumber'>;
|
||||
type StateProps = Pick<GlobalState, 'connectionState' | 'authState' | 'authQrCode'> & {
|
||||
language?: LangCode;
|
||||
};
|
||||
type DispatchProps = Pick<GlobalActions, (
|
||||
'returnToAuthPhoneNumber' | 'setSettingOption'
|
||||
)>;
|
||||
|
||||
const DATA_PREFIX = 'tg://login?token=';
|
||||
|
||||
@ -19,10 +31,16 @@ const AuthCode: FC<StateProps & DispatchProps> = ({
|
||||
connectionState,
|
||||
authState,
|
||||
authQrCode,
|
||||
language,
|
||||
returnToAuthPhoneNumber,
|
||||
setSettingOption,
|
||||
}) => {
|
||||
const suggestedLanguage = getSuggestedLanguage();
|
||||
const lang = useLang();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const qrCodeRef = useRef<HTMLDivElement>(null);
|
||||
const continueText = useLangString(suggestedLanguage, 'ContinueOnThisLanguage');
|
||||
const [isLoading, markIsLoading, unmarkIsLoading] = useFlag();
|
||||
|
||||
useEffect(() => {
|
||||
if (!authQrCode || connectionState !== 'connectionStateReady') {
|
||||
@ -43,6 +61,16 @@ const AuthCode: FC<StateProps & DispatchProps> = ({
|
||||
}, container);
|
||||
}, [connectionState, authQrCode]);
|
||||
|
||||
const handleLangChange = useCallback(() => {
|
||||
markIsLoading();
|
||||
|
||||
setLanguage(suggestedLanguage!, () => {
|
||||
unmarkIsLoading();
|
||||
|
||||
setSettingOption({ language: suggestedLanguage });
|
||||
});
|
||||
}, [markIsLoading, setSettingOption, suggestedLanguage, unmarkIsLoading]);
|
||||
|
||||
const isAuthReady = authState === 'authorizationStateWaitQrCode';
|
||||
|
||||
return (
|
||||
@ -53,14 +81,17 @@ const AuthCode: FC<StateProps & DispatchProps> = ({
|
||||
) : (
|
||||
<div key="qr-loading" className="qr-loading"><Loading /></div>
|
||||
)}
|
||||
<h3>Log in to Telegram by QR Code</h3>
|
||||
<h3>{lang('Login.QR.Title')}</h3>
|
||||
<ol>
|
||||
<li><span>Open Telegram on your phone</span></li>
|
||||
<li><span>Go to <b>Settings</b> > <b>Devices</b> > <b>Scan QR</b></span></li>
|
||||
<li><span>Point your phone at this screen to confirm login</span></li>
|
||||
<li><span>{lang('Login.QR.Help1')}</span></li>
|
||||
<li><span>{renderText(lang('Login.QR.Help2'), ['simple_markdown'])}</span></li>
|
||||
<li><span>{lang('Login.QR.Help3')}</span></li>
|
||||
</ol>
|
||||
{isAuthReady && (
|
||||
<Button isText onClick={returnToAuthPhoneNumber}>Log in by phone number</Button>
|
||||
<Button isText onClick={returnToAuthPhoneNumber}>{lang('Login.QR.Cancel')}</Button>
|
||||
)}
|
||||
{suggestedLanguage && suggestedLanguage !== language && continueText && (
|
||||
<Button isText isLoading={isLoading} onClick={handleLangChange}>{continueText}</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -68,6 +99,19 @@ const AuthCode: FC<StateProps & DispatchProps> = ({
|
||||
};
|
||||
|
||||
export default memo(withGlobal(
|
||||
(global): StateProps => pick(global, ['connectionState', 'authState', 'authQrCode']),
|
||||
(setGlobal, actions): DispatchProps => pick(actions, ['returnToAuthPhoneNumber']),
|
||||
(global): StateProps => {
|
||||
const {
|
||||
connectionState, authState, authQrCode, settings: { byKey: { language } },
|
||||
} = global;
|
||||
|
||||
return {
|
||||
connectionState,
|
||||
authState,
|
||||
authQrCode,
|
||||
language,
|
||||
};
|
||||
},
|
||||
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||
'returnToAuthPhoneNumber', 'setSettingOption',
|
||||
]),
|
||||
)(AuthCode));
|
||||
|
||||
@ -5,6 +5,7 @@ import { withGlobal } from '../../lib/teact/teactn';
|
||||
import { GlobalState, GlobalActions } from '../../global/types';
|
||||
|
||||
import { pick } from '../../util/iteratees';
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
import InputText from '../ui/InputText';
|
||||
@ -16,6 +17,7 @@ type DispatchProps = Pick<GlobalActions, 'signUp' | 'clearAuthError' | 'uploadPr
|
||||
const AuthRegister: FC<StateProps & DispatchProps> = ({
|
||||
authIsLoading, authError, signUp, clearAuthError, uploadProfilePhoto,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
const [isButtonShown, setIsButtonShown] = useState(false);
|
||||
const [croppedFile, setCroppedFile] = useState<File | undefined>();
|
||||
const [firstName, setFirstName] = useState('');
|
||||
@ -53,28 +55,25 @@ const AuthRegister: FC<StateProps & DispatchProps> = ({
|
||||
<div className="auth-form">
|
||||
<form action="" method="post" onSubmit={handleSubmit}>
|
||||
<AvatarEditable onChange={setCroppedFile} />
|
||||
<h2>Your Name</h2>
|
||||
<p className="note">
|
||||
Enter your name and add
|
||||
<br />a profile picture.
|
||||
</p>
|
||||
<h2>{lang('YourName')}</h2>
|
||||
<p className="note">{lang('Login.Register.Desc')}</p>
|
||||
<InputText
|
||||
id="registration-first-name"
|
||||
label="Name"
|
||||
label={lang('Login.Register.FirstName.Placeholder')}
|
||||
onChange={handleFirstNameChange}
|
||||
value={firstName}
|
||||
error={authError}
|
||||
error={authError && lang(authError)}
|
||||
autoComplete="given-name"
|
||||
/>
|
||||
<InputText
|
||||
id="registration-last-name"
|
||||
label="Last Name (optional)"
|
||||
label={lang('Login.Register.LastName.Placeholder')}
|
||||
onChange={handleLastNameChange}
|
||||
value={lastName}
|
||||
autoComplete="family-name"
|
||||
/>
|
||||
{isButtonShown && (
|
||||
<Button type="submit" ripple isLoading={authIsLoading}>Start Messaging</Button>
|
||||
<Button type="submit" ripple isLoading={authIsLoading}>{lang('Next')}</Button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -2,17 +2,18 @@ import React, {
|
||||
FC, useState, memo, useCallback, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
|
||||
import { ANIMATION_END_DELAY } from '../../config';
|
||||
import { countryList } from '../../util/phoneNumber';
|
||||
import searchWords from '../../util/searchWords';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import DropdownMenu from '../ui/DropdownMenu';
|
||||
import MenuItem from '../ui/MenuItem';
|
||||
import Spinner from '../ui/Spinner';
|
||||
|
||||
import './CountryCodeInput.scss';
|
||||
import { ANIMATION_END_DELAY } from '../../config';
|
||||
|
||||
type OwnProps = {
|
||||
id: string;
|
||||
@ -30,6 +31,7 @@ const CountryCodeInput: FC<OwnProps> = ({
|
||||
isLoading,
|
||||
onChange,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@ -103,7 +105,7 @@ const CountryCodeInput: FC<OwnProps> = ({
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
/>
|
||||
<label>Country</label>
|
||||
<label>{lang('Login.SelectCountry.Title')}</label>
|
||||
{isLoading ? (
|
||||
<Spinner color="black" />
|
||||
) : (
|
||||
@ -136,7 +138,7 @@ const CountryCodeInput: FC<OwnProps> = ({
|
||||
className="no-results"
|
||||
disabled
|
||||
>
|
||||
<span>No countries matched your filter.</span>
|
||||
<span>{lang('lng_country_none')}</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
|
||||
9
src/components/auth/helpers/getSuggestedLanguage.ts
Normal file
9
src/components/auth/helpers/getSuggestedLanguage.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export function getSuggestedLanguage() {
|
||||
let suggestedLanguage = navigator.language;
|
||||
|
||||
if (suggestedLanguage && suggestedLanguage !== 'pt-br') {
|
||||
suggestedLanguage = suggestedLanguage.substr(0, 2);
|
||||
}
|
||||
|
||||
return suggestedLanguage;
|
||||
}
|
||||
@ -181,7 +181,7 @@ const SettingsTwoFa: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
return (
|
||||
<SettingsTwoFaPassword
|
||||
screen={currentScreen}
|
||||
placeholder={lang('EnterPassword')}
|
||||
placeholder={lang('PleaseEnterPassword')}
|
||||
submitLabel={lang('Continue')}
|
||||
onSubmit={handleNewPassword}
|
||||
onScreenSelect={onScreenSelect}
|
||||
|
||||
@ -137,8 +137,9 @@ export const DELETED_COMMENTS_CHANNEL_ID = 777;
|
||||
export const MAX_MEDIA_FILES_FOR_ALBUM = 10;
|
||||
export const MAX_ACTIVE_PINNED_CHATS = 5;
|
||||
export const SCHEDULED_WHEN_ONLINE = 0x7FFFFFFE;
|
||||
export const DEFAULT_LANG_CODE = 'en';
|
||||
export const DEFAULT_LANG_PACK = 'android';
|
||||
export const LANG_PACKS = ['android', 'ios', 'tdesktop', 'macos'];
|
||||
export const LANG_PACKS = ['android', 'ios', 'tdesktop', 'macos'] as const;
|
||||
export const TIPS_USERNAME = 'TelegramTips';
|
||||
export const FEEDBACK_URL = 'https://bugs.telegram.org/?tag_ids=41&sort=time';
|
||||
export const LIGHT_THEME_BG_COLOR = '#A2AF8E';
|
||||
|
||||
14
src/hooks/useLangString.ts
Normal file
14
src/hooks/useLangString.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import * as langProvider from '../util/langProvider';
|
||||
import { useState } from '../lib/teact/teact';
|
||||
|
||||
export default (langCode: string | undefined, key: string): string | undefined => {
|
||||
const [translation, setTranslation] = useState<string>();
|
||||
|
||||
if (langCode) {
|
||||
langProvider
|
||||
.getTranslationForLangString(langCode, key)
|
||||
.then(setTranslation);
|
||||
}
|
||||
|
||||
return translation;
|
||||
};
|
||||
@ -1084,6 +1084,7 @@ payments.validateRequestedInfo#db103170 flags:# save:flags.0?true peer:InputPeer
|
||||
payments.sendPaymentForm#30c3bc9d flags:# form_id:long peer:InputPeer msg_id:int requested_info_id:flags.0?string shipping_option_id:flags.1?string credentials:InputPaymentCredentials tip_amount:flags.2?long = payments.PaymentResult;
|
||||
payments.getSavedInfo#227d824b = payments.SavedInfo;
|
||||
langpack.getLangPack#f2f2330a lang_pack:string lang_code:string = LangPackDifference;
|
||||
langpack.getStrings#efea3803 lang_pack:string lang_code:string keys:Vector<string> = Vector<LangPackString>;
|
||||
langpack.getLanguages#42c6978f lang_pack:string = Vector<LangPackLanguage>;
|
||||
folders.editPeerFolders#6847d0ab folder_peers:Vector<InputFolderPeer> = Updates;
|
||||
// LAYER 128
|
||||
|
||||
@ -1084,6 +1084,7 @@ payments.validateRequestedInfo#db103170 flags:# save:flags.0?true peer:InputPeer
|
||||
payments.sendPaymentForm#30c3bc9d flags:# form_id:long peer:InputPeer msg_id:int requested_info_id:flags.0?string shipping_option_id:flags.1?string credentials:InputPaymentCredentials tip_amount:flags.2?long = payments.PaymentResult;
|
||||
payments.getSavedInfo#227d824b = payments.SavedInfo;
|
||||
langpack.getLangPack#f2f2330a lang_pack:string lang_code:string = LangPackDifference;
|
||||
langpack.getStrings#efea3803 lang_pack:string lang_code:string keys:Vector<string> = Vector<LangPackString>;
|
||||
langpack.getLanguages#42c6978f lang_pack:string = Vector<LangPackLanguage>;
|
||||
folders.editPeerFolders#6847d0ab folder_peers:Vector<InputFolderPeer> = Updates;
|
||||
// LAYER 128
|
||||
|
||||
@ -2,6 +2,7 @@ import {
|
||||
addReducer, getDispatch, getGlobal, setGlobal,
|
||||
} from '../../../lib/teact/teactn';
|
||||
|
||||
import { initApi, callApi } from '../../../api/gramjs';
|
||||
import { GlobalState } from '../../../global/types';
|
||||
|
||||
import {
|
||||
@ -13,7 +14,6 @@ import {
|
||||
IS_TEST,
|
||||
} from '../../../config';
|
||||
import { PLATFORM_ENV } from '../../../util/environment';
|
||||
import { initApi, callApi } from '../../../api/gramjs';
|
||||
import { unsubscribe } from '../../../util/notifications';
|
||||
import * as cacheApi from '../../../util/cacheApi';
|
||||
import { updateAppBadge } from '../../../util/appBadge';
|
||||
|
||||
@ -3,7 +3,7 @@ import { addReducer, getGlobal, setGlobal } from '../../../lib/teact/teactn';
|
||||
import { GlobalState } from '../../../global/types';
|
||||
import {
|
||||
ApiPrivacyKey, PrivacyVisibility, ProfileEditProgress, IInputPrivacyRules, IInputPrivacyContact,
|
||||
UPLOADING_WALLPAPER_SLUG,
|
||||
UPLOADING_WALLPAPER_SLUG, LangCode,
|
||||
} from '../../../types';
|
||||
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import './ui/initial';
|
||||
import './ui/settings';
|
||||
import './api/initial';
|
||||
import './api/settings';
|
||||
import './apiUpdaters/initial';
|
||||
|
||||
@ -1705,4 +1705,104 @@ export default {
|
||||
key: 'lng_update_telegram',
|
||||
value: 'Update Telegram',
|
||||
},
|
||||
'Login.QR.Title': {
|
||||
key: 'Login.QR.Title',
|
||||
value: 'Log in to Telegram by QR Code',
|
||||
},
|
||||
PHONE_NUMBER_INVALID: {
|
||||
key: 'PHONE_NUMBER_INVALID',
|
||||
value: 'Invalid phone number',
|
||||
},
|
||||
PHONE_CODE_INVALID: {
|
||||
key: 'PHONE_CODE_INVALID',
|
||||
value: 'Invalid code',
|
||||
},
|
||||
PASSWORD_HASH_INVALID: {
|
||||
key: 'PASSWORD_HASH_INVALID',
|
||||
value: 'Incorrect password',
|
||||
},
|
||||
WrongNumber: {
|
||||
key: 'WrongNumber',
|
||||
value: 'Wrong number?',
|
||||
},
|
||||
SentAppCode: {
|
||||
key: 'SentAppCode',
|
||||
value: 'We\'ve sent the code to the **Telegram** app on your other device.',
|
||||
},
|
||||
'Login.JustSentSms': {
|
||||
key: 'Login.JustSentSms',
|
||||
value: 'We have sent you a code via SMS. Please enter it above.',
|
||||
},
|
||||
'Login.Header.Password': {
|
||||
key: 'Login.Header.Password',
|
||||
value: 'Enter Password',
|
||||
},
|
||||
'Login.EnterPasswordDescription': {
|
||||
key: 'Login.EnterPasswordDescription',
|
||||
value: 'You have Two-Step Verification enabled, so your account is protected with an additional password.',
|
||||
},
|
||||
StartText: {
|
||||
key: 'StartText',
|
||||
value: 'Please confirm your country code and enter your phone number.',
|
||||
},
|
||||
'Login.PhonePlaceholder': {
|
||||
key: 'Login.PhonePlaceholder',
|
||||
value: 'Your phone number',
|
||||
},
|
||||
'Login.Next': {
|
||||
key: 'Login.Next',
|
||||
value: 'Next',
|
||||
},
|
||||
'Login.QR.Login': {
|
||||
key: 'Login.QR.Login',
|
||||
value: 'Log in by QR Code',
|
||||
},
|
||||
ContinueOnThisLanguage: {
|
||||
key: 'ContinueOnThisLanguage',
|
||||
value: 'Continue in English',
|
||||
},
|
||||
'Login.QR.Help1': {
|
||||
key: 'Login.QR.Help1',
|
||||
value: 'Open Telegram on your phone',
|
||||
},
|
||||
'Login.QR.Help2': {
|
||||
key: 'Login.QR.Help2',
|
||||
value: 'Go to **Settings** > **Devices** > **Scan QR**',
|
||||
},
|
||||
'Login.QR.Help3': {
|
||||
key: 'Login.QR.Help3',
|
||||
value: 'Point your phone at this screen to confirm login',
|
||||
},
|
||||
'Login.QR.Cancel': {
|
||||
key: 'Login.QR.Cancel',
|
||||
value: 'Log in by phone Number',
|
||||
},
|
||||
YourName: {
|
||||
key: 'YourName',
|
||||
value: 'Your Name',
|
||||
},
|
||||
'Login.Register.Desc': {
|
||||
key: 'Login.Register.Desc',
|
||||
value: 'Enter your name and add a profile picture.',
|
||||
},
|
||||
'Login.Register.FirstName.Placeholder': {
|
||||
key: 'Login.Register.FirstName.Placeholder',
|
||||
value: 'First Name',
|
||||
},
|
||||
'Login.Register.LastName.Placeholder': {
|
||||
key: 'Login.Register.LastName.Placeholder',
|
||||
value: 'Last Name',
|
||||
},
|
||||
'Login.SelectCountry.Title': {
|
||||
key: 'Login.SelectCountry.Title',
|
||||
value: 'Country',
|
||||
},
|
||||
lng_country_none: {
|
||||
key: 'lng_country_none',
|
||||
value: 'Country not found',
|
||||
},
|
||||
PleaseEnterPassword: {
|
||||
key: 'PleaseEnterPassword',
|
||||
value: 'Enter your new password',
|
||||
},
|
||||
} as ApiLangPack;
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { ApiLangPack } from '../api/types';
|
||||
import { ApiLangPack, ApiLangString } from '../api/types';
|
||||
|
||||
import { LANG_CACHE_NAME, LANG_PACKS } from '../config';
|
||||
import {
|
||||
DEFAULT_LANG_CODE, DEFAULT_LANG_PACK, LANG_CACHE_NAME, LANG_PACKS,
|
||||
} from '../config';
|
||||
import * as cacheApi from './cacheApi';
|
||||
import { callApi } from '../api/gramjs';
|
||||
import { createCallbackManager } from './callbacks';
|
||||
@ -14,7 +16,6 @@ interface LangFn {
|
||||
code?: string;
|
||||
}
|
||||
|
||||
const FALLBACK_LANG_CODE = 'en';
|
||||
const SUBSTITUTION_REGEX = /%\d?\$?[sdf@]/g;
|
||||
const PLURAL_OPTIONS = ['value', 'zeroValue', 'oneValue', 'twoValue', 'fewValue', 'manyValue', 'otherValue'] as const;
|
||||
const PLURAL_RULES = {
|
||||
@ -77,24 +78,23 @@ export const getTranslation: LangFn = (key: string, value?: any, format?: 'i') =
|
||||
return key;
|
||||
}
|
||||
|
||||
const template = langString[typeof value === 'number' ? getPluralOption(value) : 'value'];
|
||||
if (!template || !template.trim()) {
|
||||
const parts = key.split('.');
|
||||
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
|
||||
if (value !== undefined) {
|
||||
const formattedValue = format === 'i' ? formatInteger(value) : value;
|
||||
const result = processTemplate(template, formattedValue);
|
||||
const cacheValue = Array.isArray(value) ? JSON.stringify(value) : value;
|
||||
cache.set(`${key}_${cacheValue}_${format}`, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
return template;
|
||||
return processTranslation(langString, key, value, format);
|
||||
};
|
||||
|
||||
export async function getTranslationForLangString(langCode: string, key: string) {
|
||||
let translateString = await cacheApi.fetch(
|
||||
LANG_CACHE_NAME,
|
||||
`${DEFAULT_LANG_PACK}_${langCode}_${key}`,
|
||||
cacheApi.Type.Json,
|
||||
);
|
||||
|
||||
if (!translateString) {
|
||||
translateString = await fetchRemoteString(DEFAULT_LANG_PACK, langCode, key);
|
||||
}
|
||||
|
||||
return processTranslation(translateString, key);
|
||||
}
|
||||
|
||||
export async function setLanguage(langCode: string, callback?: NoneToVoidFunction, withFallback = false) {
|
||||
if (langPack && langCode === currentLangCode) {
|
||||
if (callback) {
|
||||
@ -153,8 +153,26 @@ async function fetchRemote(langCode: string): Promise<ApiLangPack | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function fetchRemoteString(
|
||||
remoteLangPack: typeof LANG_PACKS[number], langCode: string, key: string,
|
||||
): Promise<string | undefined> {
|
||||
const remote = await callApi('fetchLangStrings', {
|
||||
langPack: remoteLangPack,
|
||||
langCode,
|
||||
keys: [key],
|
||||
});
|
||||
|
||||
if (remote && remote.length) {
|
||||
await cacheApi.save(LANG_CACHE_NAME, `${remoteLangPack}_${langCode}_${key}`, remote[0]);
|
||||
|
||||
return remote[0];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getPluralOption(amount: number) {
|
||||
const langCode = currentLangCode || FALLBACK_LANG_CODE;
|
||||
const langCode = currentLangCode || DEFAULT_LANG_CODE;
|
||||
const optionIndex = PLURAL_RULES[langCode as keyof typeof PLURAL_RULES]
|
||||
? PLURAL_RULES[langCode as keyof typeof PLURAL_RULES](amount)
|
||||
: 0;
|
||||
@ -171,3 +189,22 @@ function processTemplate(template: string, value: any) {
|
||||
return `${result}${String(value[index] || '')}${str}`;
|
||||
}, initialValue || '');
|
||||
}
|
||||
|
||||
function processTranslation(langString: ApiLangString | undefined, key: string, value?: any, format?: 'i') {
|
||||
const template = langString ? langString[typeof value === 'number' ? getPluralOption(value) : 'value'] : undefined;
|
||||
if (!template || !template.trim()) {
|
||||
const parts = key.split('.');
|
||||
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
|
||||
if (value !== undefined) {
|
||||
const formattedValue = format === 'i' ? formatInteger(value) : value;
|
||||
const result = processTemplate(template, formattedValue);
|
||||
const cacheValue = Array.isArray(value) ? JSON.stringify(value) : value;
|
||||
cache.set(`${key}_${cacheValue}_${format}`, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user