Auth: Support switching to system language (#1324)

This commit is contained in:
Alexander Zinchuk 2021-07-26 17:36:06 +03:00
parent 752d9a4df7
commit c52ee0574f
20 changed files with 353 additions and 97 deletions

View File

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

View File

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

View File

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

View File

@ -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"
/>

View File

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

View File

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

View File

@ -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&nbsp;<b>Settings</b>&nbsp;&gt;&nbsp;<b>Devices</b>&nbsp;&gt;&nbsp;<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));

View File

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

View File

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

View File

@ -0,0 +1,9 @@
export function getSuggestedLanguage() {
let suggestedLanguage = navigator.language;
if (suggestedLanguage && suggestedLanguage !== 'pt-br') {
suggestedLanguage = suggestedLanguage.substr(0, 2);
}
return suggestedLanguage;
}

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
import './ui/initial';
import './ui/settings';
import './api/initial';
import './api/settings';
import './apiUpdaters/initial';

View File

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

View File

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