Localization: Better platform support (#5136)
This commit is contained in:
parent
6babbae9f9
commit
e678824a10
@ -3,38 +3,42 @@ import { readFileSync, writeFileSync } from 'fs';
|
||||
import readStrings from '../src/util/data/readStrings';
|
||||
|
||||
const TOP_COMMENT = '// This file is generated by dev/generateLangTypes.ts. Do not edit it manually.\n';
|
||||
const LANG_KEY_TYPE = 'export type LangKey = keyof LangPair;';
|
||||
const LANG_KEY_TYPE = `
|
||||
export type RegularLangKey = keyof LangPair;
|
||||
export type RegularLangKeyWithVariables = keyof LangPairWithVariables;
|
||||
export type PluralLangKey = keyof LangPairPlural;
|
||||
export type PluralLangKeyWithVariables = keyof LangPairPluralWithVariables;
|
||||
export type LangKey = RegularLangKey | RegularLangKeyWithVariables | PluralLangKey | PluralLangKeyWithVariables;
|
||||
type LangVariable = string | number | undefined;
|
||||
`.trim();
|
||||
|
||||
const data = readFileSync('./src/assets/localization/fallback.strings', 'utf8');
|
||||
|
||||
const parsed = readStrings(data);
|
||||
const keysWithVars = Object.entries(parsed).reduce((acc, [keyWithSuffix, value]) => {
|
||||
const key = keyWithSuffix.split('_')[0];
|
||||
const regularKeysWithVars: Record<string, string[]> = {};
|
||||
const pluralKeysWithVars: Record<string, string[]> = {};
|
||||
Object.entries(parsed).forEach(([keyWithSuffix, value]) => {
|
||||
const [key, pluralSuffix] = keyWithSuffix.split('_');
|
||||
const variables = extractVariables(value);
|
||||
|
||||
const acc = pluralSuffix ? pluralKeysWithVars : regularKeysWithVars;
|
||||
|
||||
const previousVariables = acc[key] || [];
|
||||
if (!previousVariables.length) {
|
||||
acc[key] = variables;
|
||||
} else {
|
||||
acc[key] = Array.from(new Set([...previousVariables, ...variables]));
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, string[]>);
|
||||
|
||||
let entries = '';
|
||||
|
||||
Object.entries(keysWithVars).forEach(([key, variables]) => {
|
||||
const varString = variables.length
|
||||
? `{\n ${variables.map((v) => `${wrapInQuotes(v)}: string | number;`).join('\n ')}\n };\n`
|
||||
: 'undefined;\n';
|
||||
entries += ` ${wrapInQuotes(key)}: ${varString}`;
|
||||
previousVariables.push(...variables);
|
||||
acc[key] = previousVariables;
|
||||
});
|
||||
|
||||
const langPair = `export interface LangPair {\n${entries}\n}\n`;
|
||||
const text = `${TOP_COMMENT}\n${langPair}\n${LANG_KEY_TYPE}\n`;
|
||||
const regularTypes = formatKeyWithVariables(false, regularKeysWithVars);
|
||||
const pluralTypes = formatKeyWithVariables(true, pluralKeysWithVars);
|
||||
|
||||
const text = `${TOP_COMMENT}\n${regularTypes}\n${pluralTypes}${LANG_KEY_TYPE}\n`;
|
||||
writeFileSync('./src/types/language.d.ts', text, 'utf8');
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Language types generated: ${Object.keys(keysWithVars).length} keys`);
|
||||
console.log(`Language types generated
|
||||
${Object.keys(regularKeysWithVars).length} simple keys
|
||||
${Object.keys(pluralKeysWithVars).length} plural keys
|
||||
`);
|
||||
|
||||
function extractVariables(value: string) {
|
||||
const matches = value.match(/(?<!\\){[^{}]+}/g);
|
||||
@ -45,3 +49,23 @@ function extractVariables(value: string) {
|
||||
function wrapInQuotes(value: string) {
|
||||
return `'${value}'`;
|
||||
}
|
||||
|
||||
function formatKeyWithVariables(isPlural: boolean, keysWithVars: Record<string, string[]>) {
|
||||
let entries = '';
|
||||
let variableEntries = '';
|
||||
|
||||
Object.entries(keysWithVars).forEach(([key, variables]) => {
|
||||
const uniqueVariables = variables.length ? Array.from(new Set(variables)) : undefined;
|
||||
if (uniqueVariables) {
|
||||
const type = `{\n ${uniqueVariables.map((v) => `${wrapInQuotes(v)}: V;`).join('\n ')}\n };\n`;
|
||||
variableEntries += ` ${wrapInQuotes(key)}: ${type}`;
|
||||
} else {
|
||||
entries += ` ${wrapInQuotes(key)}: undefined;\n`;
|
||||
}
|
||||
});
|
||||
|
||||
const typeName = isPlural ? 'LangPairPlural' : 'LangPair';
|
||||
|
||||
return `export interface ${typeName} {\n${entries}}\n
|
||||
export interface ${typeName}WithVariables<V extends unknown = LangVariable> {\n${variableEntries}}\n`;
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ import type {
|
||||
|
||||
import {
|
||||
APP_CODE_NAME,
|
||||
DEBUG, DEBUG_GRAMJS, IS_TEST, UPLOAD_WORKERS,
|
||||
DEBUG, DEBUG_GRAMJS, IS_TEST, LANG_PACK, UPLOAD_WORKERS,
|
||||
} from '../../../config';
|
||||
import { pause } from '../../../util/schedulers';
|
||||
import {
|
||||
@ -95,7 +95,9 @@ export async function init(initialArgs: ApiInitialArgs) {
|
||||
shouldForceHttpTransport,
|
||||
shouldAllowHttpTransport,
|
||||
dcId,
|
||||
langPack: LANG_PACK,
|
||||
langCode,
|
||||
systemLangCode: navigator.language,
|
||||
isTestServerRequested,
|
||||
} as any,
|
||||
);
|
||||
|
||||
@ -2,7 +2,7 @@ import BigInt from 'big-integer';
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type { LANG_PACKS } from '../../../config';
|
||||
import type { ApiInputPrivacyRules, ApiPrivacyKey, LangCode } from '../../../types';
|
||||
import type { ApiInputPrivacyRules, ApiPrivacyKey } from '../../../types';
|
||||
import type {
|
||||
ApiAppConfig,
|
||||
ApiConfig,
|
||||
@ -14,7 +14,10 @@ import type {
|
||||
} from '../../types';
|
||||
|
||||
import {
|
||||
ACCEPTABLE_USERNAME_ERRORS, BLOCKED_LIST_LIMIT, DEFAULT_LANG_PACK, MAX_INT_32,
|
||||
ACCEPTABLE_USERNAME_ERRORS,
|
||||
BLOCKED_LIST_LIMIT,
|
||||
MAX_INT_32,
|
||||
OLD_DEFAULT_LANG_PACK,
|
||||
} from '../../../config';
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
@ -466,7 +469,7 @@ export async function fetchLangDifference({
|
||||
|
||||
export async function fetchLanguages(): Promise<ApiLanguage[] | undefined> {
|
||||
const result = await invokeRequest(new GramJs.langpack.GetLanguages({
|
||||
langPack: DEFAULT_LANG_PACK,
|
||||
langPack: OLD_DEFAULT_LANG_PACK,
|
||||
}));
|
||||
if (!result) {
|
||||
return undefined;
|
||||
@ -646,7 +649,7 @@ export async function fetchTimezones(hash?: number) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchCountryList({ langCode = 'en' }: { langCode?: LangCode }) {
|
||||
export async function fetchCountryList({ langCode = 'en' }: { langCode?: string }) {
|
||||
const countryList = await invokeRequest(new GramJs.help.GetCountriesList({
|
||||
langCode,
|
||||
}));
|
||||
|
||||
@ -47,6 +47,7 @@ import {
|
||||
import {
|
||||
buildApiNotifyException,
|
||||
buildApiNotifyExceptionTopic,
|
||||
buildLangStrings,
|
||||
buildPrivacyKey,
|
||||
} from '../apiBuilders/misc';
|
||||
import { buildApiEmojiStatus, buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
|
||||
@ -1050,7 +1051,20 @@ export function updater(update: Update) {
|
||||
'@type': 'updatePaidReactionPrivacy',
|
||||
isPrivate: update.private,
|
||||
});
|
||||
} else if (update instanceof LocalUpdatePremiumFloodWait) {
|
||||
} else if (update instanceof GramJs.UpdateLangPackTooLong) {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateLangPackTooLong',
|
||||
langCode: update.langCode,
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdateLangPack) {
|
||||
const { strings, keysToRemove } = buildLangStrings(update.difference.strings);
|
||||
sendApiUpdate({
|
||||
'@type': 'updateLangPack',
|
||||
version: update.difference.version,
|
||||
strings,
|
||||
keysToRemove,
|
||||
});
|
||||
} else if (update instanceof LocalUpdatePremiumFloodWait) { // Local updates
|
||||
sendApiUpdate({
|
||||
'@type': 'updatePremiumFloodWait',
|
||||
isUpload: update.isUpload,
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import type { TeactNode } from '../../lib/teact/teact';
|
||||
|
||||
import type { ApiLimitType, ApiPremiumSection, CallbackAction } from '../../global/types';
|
||||
import type { IconName } from '../../types/icons';
|
||||
import type { LangFnParameters } from '../../util/localization';
|
||||
import type { ApiDocument, ApiPhoto, ApiReaction } from './messages';
|
||||
import type { ApiUser } from './users';
|
||||
|
||||
@ -111,10 +114,11 @@ export type ApiNotifyException = {
|
||||
|
||||
export type ApiNotification = {
|
||||
localId: string;
|
||||
title?: string;
|
||||
message: string;
|
||||
containerSelector?: string;
|
||||
title?: string | LangFnParameters;
|
||||
message: TeactNode | LangFnParameters;
|
||||
cacheBreaker?: string;
|
||||
actionText?: string;
|
||||
actionText?: string | LangFnParameters;
|
||||
action?: CallbackAction | CallbackAction[];
|
||||
className?: string;
|
||||
duration?: number;
|
||||
|
||||
@ -34,6 +34,7 @@ import type {
|
||||
import type {
|
||||
ApiEmojiInteraction, ApiError, ApiNotifyException, ApiSessionData,
|
||||
} from './misc';
|
||||
import type { LangPackStringValue } from './settings';
|
||||
import type { ApiStealthMode, ApiStory, ApiStorySkipped } from './stories';
|
||||
import type {
|
||||
ApiEmojiStatus, ApiUser, ApiUserFullInfo, ApiUserStatus,
|
||||
@ -774,6 +775,18 @@ export type ApiUpdatePaidReactionPrivacy = {
|
||||
isPrivate: boolean;
|
||||
};
|
||||
|
||||
export type ApiUpdateLangPackTooLong = {
|
||||
'@type': 'updateLangPackTooLong';
|
||||
langCode: string;
|
||||
};
|
||||
|
||||
export type ApiUpdateLangPack = {
|
||||
'@type': 'updateLangPack';
|
||||
version: number;
|
||||
strings: Record<string, LangPackStringValue>;
|
||||
keysToRemove: string[];
|
||||
};
|
||||
|
||||
export type ApiUpdate = (
|
||||
ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate |
|
||||
ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser |
|
||||
@ -806,7 +819,8 @@ export type ApiUpdate = (
|
||||
ApiUpdateViewForumAsMessages | ApiUpdateSavedDialogPinned | ApiUpdatePinnedSavedDialogIds | ApiUpdateChatLastMessage |
|
||||
ApiUpdateDeleteSavedHistory | ApiUpdatePremiumFloodWait | ApiUpdateStarsBalance |
|
||||
ApiUpdateQuickReplyMessage | ApiUpdateQuickReplies | ApiDeleteQuickReply | ApiUpdateDeleteQuickReplyMessages |
|
||||
ApiUpdateDeleteProfilePhoto | ApiUpdateNewProfilePhoto | ApiUpdateEntities | ApiUpdatePaidReactionPrivacy
|
||||
ApiUpdateDeleteProfilePhoto | ApiUpdateNewProfilePhoto | ApiUpdateEntities | ApiUpdatePaidReactionPrivacy |
|
||||
ApiUpdateLangPackTooLong | ApiUpdateLangPack
|
||||
);
|
||||
|
||||
export type OnApiUpdate = (update: ApiUpdate) => void;
|
||||
|
||||
@ -668,7 +668,6 @@
|
||||
"DiscussChannel" = "channel";
|
||||
"ForwardedMessage" = "Forwarded message";
|
||||
"ContextForwardMsg" = "Forward";
|
||||
"ShareLinkCopied" = "Copied to Clipboard";
|
||||
"MessageScheduleSend" = "Send Now";
|
||||
"MessageScheduleEditTime" = "Reschedule";
|
||||
"Reply" = "Reply";
|
||||
|
||||
@ -7,7 +7,6 @@ import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiCountryCode } from '../../api/types';
|
||||
import type { GlobalState } from '../../global/types';
|
||||
import type { LangCode } from '../../types';
|
||||
|
||||
import { requestMeasure } from '../../lib/fasterdom/fasterdom';
|
||||
import { preloadImage } from '../../util/files';
|
||||
@ -36,7 +35,7 @@ type StateProps = Pick<GlobalState, (
|
||||
'authIsLoadingQrCode' | 'authError' |
|
||||
'authRememberMe' | 'authNearestCountry'
|
||||
)> & {
|
||||
language?: LangCode;
|
||||
language?: string;
|
||||
phoneCodeList: ApiCountryCode[];
|
||||
};
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@ import React, {
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { GlobalState } from '../../global/types';
|
||||
import type { LangCode } from '../../types';
|
||||
|
||||
import { DEFAULT_LANG_CODE, STRICTERDOM_ENABLED } from '../../config';
|
||||
import { disableStrict, enableStrict } from '../../lib/fasterdom/stricterdom';
|
||||
@ -29,7 +28,7 @@ import blankUrl from '../../assets/blank.png';
|
||||
|
||||
type StateProps =
|
||||
Pick<GlobalState, 'connectionState' | 'authState' | 'authQrCode'>
|
||||
& { language?: LangCode };
|
||||
& { language?: string };
|
||||
|
||||
const DATA_PREFIX = 'tg://login?token=';
|
||||
const QR_SIZE = 280;
|
||||
|
||||
@ -6,7 +6,7 @@ import { getActions } from '../../global';
|
||||
|
||||
import type { ApiAudio, ApiMessage, ApiVoice } from '../../api/types';
|
||||
import type { BufferedRange } from '../../hooks/useBuffering';
|
||||
import type { LangFn } from '../../hooks/useOldLang';
|
||||
import type { OldLangFn } from '../../hooks/useOldLang';
|
||||
import type { ISettings } from '../../types';
|
||||
import { ApiMediaFormat } from '../../api/types';
|
||||
import { AudioOrigin } from '../../types';
|
||||
@ -495,7 +495,7 @@ function getSeeklineSpikeAmounts(isMobile?: boolean, withAvatar?: boolean) {
|
||||
}
|
||||
|
||||
function renderAudio(
|
||||
lang: LangFn,
|
||||
lang: OldLangFn,
|
||||
audio: ApiAudio,
|
||||
duration: number,
|
||||
isPlaying: boolean,
|
||||
|
||||
@ -3,7 +3,7 @@ import React, {
|
||||
memo, useCallback, useEffect, useMemo, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
|
||||
import type { LangFn } from '../../hooks/useOldLang';
|
||||
import type { OldLangFn } from '../../hooks/useOldLang';
|
||||
|
||||
import { MAX_INT_32 } from '../../config';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
@ -401,7 +401,7 @@ function formatDay(year: number, month: number, day: number) {
|
||||
return `${year}-${month + 1}-${day}`;
|
||||
}
|
||||
|
||||
function formatSubmitLabel(lang: LangFn, date: Date) {
|
||||
function formatSubmitLabel(lang: OldLangFn, date: Date) {
|
||||
const day = formatDateToString(date, lang.code);
|
||||
const today = formatDateToString(new Date(), lang.code);
|
||||
|
||||
|
||||
@ -43,7 +43,9 @@ const InviteLink: FC<OwnProps> = ({
|
||||
const copyLink = useLastCallback(() => {
|
||||
copyTextToClipboard(link);
|
||||
showNotification({
|
||||
message: lang('LinkCopied'),
|
||||
message: {
|
||||
key: 'LinkCopied',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import type {
|
||||
ApiChat, ApiGroupCall, ApiMessage, ApiTopic, ApiUser,
|
||||
} from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import type { LangFn } from '../../../hooks/useOldLang';
|
||||
import type { OldLangFn } from '../../../hooks/useOldLang';
|
||||
import type { TextPart } from '../../../types';
|
||||
|
||||
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
|
||||
@ -36,7 +36,7 @@ const MAX_LENGTH = 32;
|
||||
const NBSP = '\u00A0';
|
||||
|
||||
export function renderActionMessageText(
|
||||
oldLang: LangFn,
|
||||
oldLang: OldLangFn,
|
||||
message: ApiMessage,
|
||||
actionOriginUser?: ApiUser,
|
||||
actionOriginChat?: ApiChat,
|
||||
@ -269,7 +269,7 @@ function renderProductContent(message: ApiMessage) {
|
||||
}
|
||||
|
||||
function renderMessageContent(
|
||||
lang: LangFn,
|
||||
lang: OldLangFn,
|
||||
message: ApiMessage,
|
||||
options: RenderOptions = {},
|
||||
observeIntersectionForLoading?: ObserveFn,
|
||||
@ -318,7 +318,7 @@ function renderUserContent(sender: ApiUser, noLinks?: boolean): string | TextPar
|
||||
return <UserLink className="action-link" sender={sender}>{sender && renderText(text!)}</UserLink>;
|
||||
}
|
||||
|
||||
function renderChatContent(lang: LangFn, chat: ApiChat, noLinks?: boolean): string | TextPart | undefined {
|
||||
function renderChatContent(lang: OldLangFn, chat: ApiChat, noLinks?: boolean): string | TextPart | undefined {
|
||||
const text = trimText(getChatTitle(lang, chat), MAX_LENGTH);
|
||||
|
||||
if (noLinks) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { getGlobal } from '../../../global';
|
||||
|
||||
import type { ApiMessage, ApiSponsoredMessage } from '../../../api/types';
|
||||
import type { LangFn } from '../../../hooks/useOldLang';
|
||||
import type { OldLangFn } from '../../../hooks/useOldLang';
|
||||
import type { TextPart } from '../../../types';
|
||||
import { ApiMessageEntityTypes } from '../../../api/types';
|
||||
|
||||
@ -65,7 +65,7 @@ export function renderMessageText({
|
||||
|
||||
// TODO Use Message Summary component instead
|
||||
export function renderMessageSummary(
|
||||
lang: LangFn,
|
||||
lang: OldLangFn,
|
||||
message: ApiMessage,
|
||||
noEmoji = false,
|
||||
highlight?: string,
|
||||
|
||||
@ -62,6 +62,7 @@ type OwnProps = {
|
||||
noScrollRestore?: boolean;
|
||||
isViewOnly?: boolean;
|
||||
withDefaultPadding?: boolean;
|
||||
forceRenderAllItems?: boolean;
|
||||
onFilterChange?: (value: string) => void;
|
||||
onDisabledClick?: (value: string, isSelected: boolean) => void;
|
||||
onLoadMore?: () => void;
|
||||
@ -86,6 +87,7 @@ const ItemPicker = ({
|
||||
itemInputType,
|
||||
itemClassName,
|
||||
withDefaultPadding,
|
||||
forceRenderAllItems,
|
||||
onFilterChange,
|
||||
onDisabledClick,
|
||||
onLoadMore,
|
||||
@ -163,7 +165,7 @@ const ItemPicker = ({
|
||||
});
|
||||
|
||||
const [viewportValuesList, getMore] = useInfiniteScroll(
|
||||
onLoadMore, sortedItemValuesList, Boolean(filterValue),
|
||||
onLoadMore, sortedItemValuesList, Boolean(forceRenderAllItems || filterValue),
|
||||
);
|
||||
|
||||
const handleFilterChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
||||
@ -6,7 +6,7 @@ import type {
|
||||
ApiChat, ApiMessage, ApiMessageOutgoingStatus,
|
||||
ApiUser,
|
||||
} from '../../../api/types';
|
||||
import type { LangFn } from '../../../hooks/useOldLang';
|
||||
import type { OldLangFn } from '../../../hooks/useOldLang';
|
||||
|
||||
import {
|
||||
getMessageIsSpoiler,
|
||||
@ -111,7 +111,7 @@ const ChatMessage: FC<OwnProps & StateProps> = ({
|
||||
};
|
||||
|
||||
function renderSummary(
|
||||
lang: LangFn, message: ApiMessage, blobUrl?: string, searchQuery?: string, isRoundVideo?: boolean,
|
||||
lang: OldLangFn, message: ApiMessage, blobUrl?: string, searchQuery?: string, isRoundVideo?: boolean,
|
||||
) {
|
||||
if (!blobUrl) {
|
||||
return renderMessageSummary(lang, message, undefined, searchQuery);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { ApiChat, ApiMessage, ApiUser } from '../../../../api/types';
|
||||
import type { LangFn } from '../../../../hooks/useOldLang';
|
||||
import type { OldLangFn } from '../../../../hooks/useOldLang';
|
||||
|
||||
import {
|
||||
getChatTitle,
|
||||
@ -9,7 +9,7 @@ import {
|
||||
} from '../../../../global/helpers';
|
||||
|
||||
export function getSenderName(
|
||||
lang: LangFn, message: ApiMessage, chatsById: Record<string, ApiChat>, usersById: Record<string, ApiUser>,
|
||||
lang: OldLangFn, message: ApiMessage, chatsById: Record<string, ApiChat>, usersById: Record<string, ApiUser>,
|
||||
) {
|
||||
const { senderId } = message;
|
||||
if (!senderId) {
|
||||
|
||||
@ -4,6 +4,7 @@ import React, {
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiLanguage } from '../../../api/types';
|
||||
import type { ISettings, LangCode } from '../../../types';
|
||||
import { SettingsScreens } from '../../../types';
|
||||
|
||||
@ -29,7 +30,8 @@ type OwnProps = {
|
||||
|
||||
type StateProps = {
|
||||
isCurrentUserPremium: boolean;
|
||||
} & Pick<ISettings, 'languages' | 'language' | 'canTranslate' | 'canTranslateChats' | 'doNotTranslate'>;
|
||||
languages?: ApiLanguage[];
|
||||
} & Pick<ISettings, | 'language' | 'canTranslate' | 'canTranslateChats' | 'doNotTranslate'>;
|
||||
|
||||
const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
||||
isActive,
|
||||
@ -44,7 +46,6 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
||||
}) => {
|
||||
const {
|
||||
loadLanguages,
|
||||
loadAttachBots,
|
||||
setSettingOption,
|
||||
openPremiumModal,
|
||||
} = getActions();
|
||||
@ -70,8 +71,6 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
||||
unmarkIsLoading();
|
||||
|
||||
setSettingOption({ language: langCode as LangCode });
|
||||
|
||||
loadAttachBots(); // Should be refetched every language change
|
||||
});
|
||||
});
|
||||
|
||||
@ -167,6 +166,7 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
||||
<ItemPicker
|
||||
items={options}
|
||||
selectedValue={selectedLanguage}
|
||||
forceRenderAllItems
|
||||
onSelectedValueChange={handleChange}
|
||||
itemInputType="radio"
|
||||
className="settings-picker"
|
||||
@ -182,8 +182,9 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const {
|
||||
language, languages, canTranslate, canTranslateChats, doNotTranslate,
|
||||
language, canTranslate, canTranslateChats, doNotTranslate,
|
||||
} = global.settings.byKey;
|
||||
const languages = global.settings.languages;
|
||||
|
||||
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
||||
|
||||
|
||||
@ -10,7 +10,6 @@ import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiChatFolder, ApiMessage, ApiUser } from '../../api/types';
|
||||
import type { ApiLimitTypeWithModal, TabState } from '../../global/types';
|
||||
import type { LangCode } from '../../types';
|
||||
import { ElectronEvent } from '../../types/electron';
|
||||
|
||||
import { BASE_EMOJI_KEYWORD_LANG, DEBUG, INACTIVE_MARKER } from '../../config';
|
||||
@ -43,6 +42,7 @@ import useInterval from '../../hooks/schedulers/useInterval';
|
||||
import useTimeout from '../../hooks/schedulers/useTimeout';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useForceUpdate from '../../hooks/useForceUpdate';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import usePreventPinchZoomGesture from '../../hooks/usePreventPinchZoomGesture';
|
||||
import useShowTransition from '../../hooks/useShowTransition';
|
||||
@ -115,7 +115,6 @@ type StateProps = {
|
||||
openedCustomEmojiSetIds?: string[];
|
||||
activeGroupCallId?: string;
|
||||
isServiceChatReady?: boolean;
|
||||
language?: LangCode;
|
||||
wasTimeFormatSetManually?: boolean;
|
||||
isPhoneCallActive?: boolean;
|
||||
addedSetIds?: string[];
|
||||
@ -170,7 +169,6 @@ const Main = ({
|
||||
openedCustomEmojiSetIds,
|
||||
isServiceChatReady,
|
||||
withInterfaceAnimations,
|
||||
language,
|
||||
wasTimeFormatSetManually,
|
||||
addedSetIds,
|
||||
addedCustomEmojiIds,
|
||||
@ -257,6 +255,8 @@ const Main = ({
|
||||
console.log('>>> RENDER MAIN');
|
||||
}
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
// Preload Calls bundle to initialize sounds for iOS
|
||||
useTimeout(() => {
|
||||
void loadBundle(Bundles.Calls);
|
||||
@ -349,13 +349,15 @@ const Main = ({
|
||||
// Language-based API calls
|
||||
useEffect(() => {
|
||||
if (isMasterTab) {
|
||||
if (language !== BASE_EMOJI_KEYWORD_LANG) {
|
||||
loadEmojiKeywords({ language: language! });
|
||||
if (lang.code !== BASE_EMOJI_KEYWORD_LANG) {
|
||||
loadEmojiKeywords({ language: lang.code });
|
||||
}
|
||||
|
||||
loadCountryList({ langCode: language });
|
||||
loadCountryList({ langCode: lang.code });
|
||||
|
||||
loadAttachBots();
|
||||
}
|
||||
}, [language, isMasterTab]);
|
||||
}, [lang, isMasterTab]);
|
||||
|
||||
// Re-fetch cached saved emoji for `localDb`
|
||||
useEffect(() => {
|
||||
@ -596,7 +598,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const {
|
||||
settings: {
|
||||
byKey: {
|
||||
language, wasTimeFormatSetManually,
|
||||
wasTimeFormatSetManually,
|
||||
},
|
||||
},
|
||||
currentUserId,
|
||||
@ -660,7 +662,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
isServiceChatReady: selectIsServiceChatReady(global),
|
||||
activeGroupCallId: isMasterTab ? global.groupCalls.activeGroupCallId : undefined,
|
||||
withInterfaceAnimations: selectCanAnimateInterface(global),
|
||||
language,
|
||||
wasTimeFormatSetManually,
|
||||
isPhoneCallActive: isMasterTab ? Boolean(global.phoneCall) : undefined,
|
||||
addedSetIds: global.stickers.added.setIds,
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo } from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
import { withGlobal } from '../../global';
|
||||
|
||||
import type { ApiNotification } from '../../api/types';
|
||||
|
||||
import { selectTabState } from '../../global/selectors';
|
||||
import { pick } from '../../util/iteratees';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
|
||||
import Notification from '../ui/Notification';
|
||||
|
||||
@ -15,8 +14,6 @@ type StateProps = {
|
||||
};
|
||||
|
||||
const Notifications: FC<StateProps> = ({ notifications }) => {
|
||||
const { dismissNotification } = getActions();
|
||||
|
||||
if (!notifications.length) {
|
||||
return undefined;
|
||||
}
|
||||
@ -24,23 +21,7 @@ const Notifications: FC<StateProps> = ({ notifications }) => {
|
||||
return (
|
||||
<div id="Notifications">
|
||||
{notifications.map((notification) => (
|
||||
<Notification
|
||||
key={notification.localId}
|
||||
title={notification.title
|
||||
? renderText(notification.title, ['simple_markdown', 'emoji', 'br', 'links']) : undefined}
|
||||
action={notification.action}
|
||||
actionText={notification.actionText}
|
||||
className={notification.className}
|
||||
duration={notification.duration}
|
||||
icon={notification.icon}
|
||||
cacheBreaker={notification.cacheBreaker}
|
||||
message={renderText(notification.message, ['simple_markdown', 'emoji', 'br', 'links'])}
|
||||
shouldDisableClickDismiss={notification.disableClickDismiss}
|
||||
dismissAction={notification.dismissAction}
|
||||
shouldShowTimer={notification.shouldShowTimer}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onDismiss={() => dismissNotification({ localId: notification.localId })}
|
||||
/>
|
||||
<Notification notification={notification} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -3,7 +3,7 @@ import React, { memo, useCallback, useEffect } from '../../../../lib/teact/teact
|
||||
import { getActions, withGlobal } from '../../../../global';
|
||||
|
||||
import type { ApiLimitTypeWithModal } from '../../../../global/types';
|
||||
import type { LangFn } from '../../../../hooks/useOldLang';
|
||||
import type { OldLangFn } from '../../../../hooks/useOldLang';
|
||||
import type { IconName } from '../../../../types/icons';
|
||||
|
||||
import { MAX_UPLOAD_FILEPART_SIZE } from '../../../../config';
|
||||
@ -71,7 +71,7 @@ const LIMIT_ICON: Record<ApiLimitTypeWithModal, IconName> = {
|
||||
};
|
||||
|
||||
const LIMIT_VALUE_FORMATTER: Partial<Record<ApiLimitTypeWithModal, (...args: any[]) => string>> = {
|
||||
uploadMaxFileparts: (lang: LangFn, value: number) => {
|
||||
uploadMaxFileparts: (lang: OldLangFn, value: number) => {
|
||||
// The real size is not exactly 4gb, so we need to round it
|
||||
if (value === 8000) return lang('FileSize.GB', '4');
|
||||
if (value === 4000) return lang('FileSize.GB', '2');
|
||||
@ -88,7 +88,7 @@ function getLimiterDescription({
|
||||
premiumValue,
|
||||
valueFormatter,
|
||||
}: {
|
||||
lang: LangFn;
|
||||
lang: OldLangFn;
|
||||
limitType?: ApiLimitTypeWithModal;
|
||||
isPremium?: boolean;
|
||||
canBuyPremium?: boolean;
|
||||
|
||||
@ -3,7 +3,7 @@ import React, { memo } from '../../lib/teact/teact';
|
||||
|
||||
import type { ApiTopic } from '../../api/types';
|
||||
import type { MessageListType } from '../../global/types';
|
||||
import type { LangFn } from '../../hooks/useOldLang';
|
||||
import type { OldLangFn } from '../../hooks/useOldLang';
|
||||
|
||||
import { REM } from '../common/helpers/mediaDimensions';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
@ -53,7 +53,7 @@ const NoMessages: FC<OwnProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
function renderTopic(lang: LangFn, topic: ApiTopic) {
|
||||
function renderTopic(lang: OldLangFn, topic: ApiTopic) {
|
||||
return (
|
||||
<div className="NoMessages">
|
||||
<div className="wrapper">
|
||||
@ -69,13 +69,13 @@ function renderTopic(lang: LangFn, topic: ApiTopic) {
|
||||
);
|
||||
}
|
||||
|
||||
function renderScheduled(lang: LangFn) {
|
||||
function renderScheduled(lang: OldLangFn) {
|
||||
return (
|
||||
<div className="empty"><span>{lang('ScheduledMessages.EmptyPlaceholder')}</span></div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSavedMessages(lang: LangFn) {
|
||||
function renderSavedMessages(lang: OldLangFn) {
|
||||
return (
|
||||
<div className="NoMessages">
|
||||
<div className="wrapper">
|
||||
@ -92,7 +92,7 @@ function renderSavedMessages(lang: LangFn) {
|
||||
);
|
||||
}
|
||||
|
||||
function renderGroup(lang: LangFn) {
|
||||
function renderGroup(lang: OldLangFn) {
|
||||
return (
|
||||
<div className="NoMessages">
|
||||
<div className="wrapper" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
|
||||
@ -117,7 +117,7 @@ const DropArea: FC<OwnProps> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<Portal containerId="#middle-column-portals">
|
||||
<Portal containerSelector="#middle-column-portals">
|
||||
<div
|
||||
className={className}
|
||||
onDragLeave={handleDragLeave}
|
||||
|
||||
@ -6,11 +6,11 @@ import { STARS_ICON_PLACEHOLDER } from '../../../../config';
|
||||
import { replaceWithTeact } from '../../../../util/replaceWithTeact';
|
||||
import renderText from '../../../common/helpers/renderText';
|
||||
|
||||
import { type LangFn } from '../../../../hooks/useOldLang';
|
||||
import { type OldLangFn } from '../../../../hooks/useOldLang';
|
||||
|
||||
import Icon from '../../../common/icons/Icon';
|
||||
|
||||
export default function renderKeyboardButtonText(lang: LangFn, button: ApiKeyboardButton): TeactNode {
|
||||
export default function renderKeyboardButtonText(lang: OldLangFn, button: ApiKeyboardButton): TeactNode {
|
||||
if (button.type === 'receipt') {
|
||||
return lang('PaymentReceipt');
|
||||
}
|
||||
|
||||
@ -13,10 +13,11 @@ import type {
|
||||
ApiMessage, ApiPeer, ApiPoll, ApiPollAnswer,
|
||||
} from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import type { LangFn } from '../../../hooks/useOldLang';
|
||||
import type { OldLangFn } from '../../../hooks/useOldLang';
|
||||
|
||||
import { selectPeer } from '../../../global/selectors';
|
||||
import { formatMediaDuration } from '../../../util/dates/dateFormat';
|
||||
import { getMessageKey } from '../../../util/keys/messageKey';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
|
||||
|
||||
@ -26,7 +27,6 @@ import useOldLang from '../../../hooks/useOldLang';
|
||||
import AvatarList from '../../common/AvatarList';
|
||||
import Button from '../../ui/Button';
|
||||
import CheckboxGroup from '../../ui/CheckboxGroup';
|
||||
import Notification from '../../ui/Notification';
|
||||
import RadioGroup from '../../ui/RadioGroup';
|
||||
import PollOption from './PollOption';
|
||||
|
||||
@ -54,13 +54,14 @@ const Poll: FC<OwnProps> = ({
|
||||
observeIntersectionForPlaying,
|
||||
onSendVote,
|
||||
}) => {
|
||||
const { loadMessage, openPollResults, requestConfetti } = getActions();
|
||||
const {
|
||||
loadMessage, openPollResults, requestConfetti, showNotification,
|
||||
} = getActions();
|
||||
|
||||
const { id: messageId, chatId } = message;
|
||||
const { summary, results } = poll;
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
const [chosenOptions, setChosenOptions] = useState<string[]>([]);
|
||||
const [isSolutionShown, setIsSolutionShown] = useState<boolean>(false);
|
||||
const [wasSubmitted, setWasSubmitted] = useState<boolean>(false);
|
||||
const [closePeriod, setClosePeriod] = useState<number>(
|
||||
!summary.closed && summary.closeDate && summary.closeDate > 0
|
||||
@ -176,13 +177,13 @@ const Poll: FC<OwnProps> = ({
|
||||
openPollResults({ chatId, messageId });
|
||||
});
|
||||
|
||||
const handleSolutionShow = useLastCallback(() => {
|
||||
setIsSolutionShown(true);
|
||||
});
|
||||
|
||||
const handleSolutionHide = useLastCallback(() => {
|
||||
setIsSolutionShown(false);
|
||||
setWasSubmitted(false);
|
||||
const showSolution = useLastCallback(() => {
|
||||
showNotification({
|
||||
localId: getMessageKey(message),
|
||||
message: renderTextWithEntities({ text: poll.results.solution!, entities: poll.results.solutionEntities }),
|
||||
duration: SOLUTION_DURATION,
|
||||
containerSelector: SOLUTION_CONTAINER_ID,
|
||||
});
|
||||
});
|
||||
|
||||
// Show the solution to quiz if the answer was incorrect
|
||||
@ -190,7 +191,7 @@ const Poll: FC<OwnProps> = ({
|
||||
if (wasSubmitted && hasVoted && summary.quiz && results.results && poll.results.solution) {
|
||||
const correctResult = results.results.find((r) => r.isChosen && r.isCorrect);
|
||||
if (!correctResult) {
|
||||
setIsSolutionShown(true);
|
||||
showSolution();
|
||||
}
|
||||
}
|
||||
}, [hasVoted, wasSubmitted, results.results, summary.quiz, poll.results.solution]);
|
||||
@ -224,22 +225,8 @@ const Poll: FC<OwnProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
function renderSolution() {
|
||||
return (
|
||||
isSolutionShown && poll.results.solution && (
|
||||
<Notification
|
||||
message={renderTextWithEntities({ text: poll.results.solution, entities: poll.results.solutionEntities })}
|
||||
duration={SOLUTION_DURATION}
|
||||
onDismiss={handleSolutionHide}
|
||||
containerId={SOLUTION_CONTAINER_ID}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="Poll" dir={lang.isRtl ? 'auto' : 'ltr'}>
|
||||
{renderSolution()}
|
||||
<div className="poll-question">
|
||||
{renderTextWithEntities({
|
||||
text: summary.question.text,
|
||||
@ -274,8 +261,7 @@ const Poll: FC<OwnProps> = ({
|
||||
size="tiny"
|
||||
color="translucent"
|
||||
className="poll-quiz-help"
|
||||
disabled={isSolutionShown}
|
||||
onClick={handleSolutionShow}
|
||||
onClick={showSolution}
|
||||
ariaLabel="Show Solution"
|
||||
>
|
||||
<i className="icon icon-lamp" />
|
||||
@ -353,7 +339,7 @@ function getPollTypeString(summary: ApiPoll['summary']) {
|
||||
return summary.isPublic ? 'PublicPoll' : 'AnonymousPoll';
|
||||
}
|
||||
|
||||
function getReadableVotersCount(lang: LangFn, isQuiz: true | undefined, count?: number) {
|
||||
function getReadableVotersCount(lang: OldLangFn, isQuiz: true | undefined, count?: number) {
|
||||
if (!count) {
|
||||
return lang(isQuiz ? 'Chat.Quiz.TotalVotesEmpty' : 'Chat.Poll.TotalVotesResultEmpty');
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import { getActions } from '../../../../global';
|
||||
import type {
|
||||
ApiMessage, ApiPeer, ApiStory, ApiTopic, ApiUser,
|
||||
} from '../../../../api/types';
|
||||
import type { LangFn } from '../../../../hooks/useOldLang';
|
||||
import type { OldLangFn } from '../../../../hooks/useOldLang';
|
||||
import type { IAlbum, ThreadId } from '../../../../types';
|
||||
import { MAIN_THREAD_ID } from '../../../../api/types';
|
||||
import { MediaViewerOrigin } from '../../../../types';
|
||||
@ -33,7 +33,7 @@ export default function useInnerHandlers({
|
||||
isRepliesChat,
|
||||
isSavedMessages,
|
||||
}: {
|
||||
lang: LangFn;
|
||||
lang: OldLangFn;
|
||||
selectMessage: (e: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => void;
|
||||
message: ApiMessage;
|
||||
chatId: string;
|
||||
|
||||
@ -7,17 +7,15 @@ import { getActions, withGlobal } from '../../../global';
|
||||
import type { ApiMessage, ApiUser } from '../../../api/types';
|
||||
import type { GiftOption } from './GiftModal';
|
||||
|
||||
import { STARS_CURRENCY_CODE, STARS_ICON_PLACEHOLDER } from '../../../config';
|
||||
import { STARS_CURRENCY_CODE } from '../../../config';
|
||||
import { getUserFullName } from '../../../global/helpers';
|
||||
import { selectTabState, selectTheme, selectUser } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatCurrency } from '../../../util/formatCurrency';
|
||||
import { formatInteger } from '../../../util/textFormat';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import Icon from '../../common/icons/Icon';
|
||||
import PremiumProgress from '../../common/PremiumProgress';
|
||||
import ActionMessage from '../../middle/ActionMessage';
|
||||
import Button from '../../ui/Button';
|
||||
@ -175,16 +173,9 @@ function GiftComposer({
|
||||
function renderFooter() {
|
||||
const userFullName = getUserFullName(user)!;
|
||||
|
||||
const amount = isStarGift ? (
|
||||
lang('StarsAmount', {
|
||||
amount: formatInteger(gift.stars),
|
||||
}, {
|
||||
withNodes: true,
|
||||
specialReplacement: {
|
||||
[STARS_ICON_PLACEHOLDER]: <Icon className="star-amount-icon" name="star" />,
|
||||
},
|
||||
})
|
||||
) : formatCurrency(gift.amount, gift.currency);
|
||||
const amount = isStarGift
|
||||
? formatCurrency(gift.stars, STARS_CURRENCY_CODE, lang.code, { iconClassName: 'star-amount-icon' })
|
||||
: formatCurrency(gift.amount, gift.currency);
|
||||
|
||||
return (
|
||||
<div className={styles.footer}>
|
||||
@ -201,9 +192,9 @@ function GiftComposer({
|
||||
isPrimary
|
||||
progress={gift.availabilityRemains / gift.availabilityTotal!}
|
||||
rightText={lang('GiftSoldCount', {
|
||||
count: formatInteger(gift.availabilityTotal! - gift.availabilityRemains),
|
||||
count: gift.availabilityTotal! - gift.availabilityRemains,
|
||||
})}
|
||||
leftText={lang('GiftLeftCount', { count: formatInteger(gift.availabilityRemains) })}
|
||||
leftText={lang('GiftLeftCount', { count: gift.availabilityRemains })}
|
||||
className={styles.limited}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -52,7 +52,9 @@ function GiftItemPremium({
|
||||
: undefined;
|
||||
|
||||
function renderMonths() {
|
||||
const caption = months === 12 ? lang('Years', { count: 1 }) : lang('Months', { count: months });
|
||||
const caption = months === 12
|
||||
? lang('Years', { count: 1 }, { pluralValue: 1 })
|
||||
: lang('Months', { count: months }, { pluralValue: months });
|
||||
return (
|
||||
<div className={styles.monthsDescription}>
|
||||
{caption}
|
||||
|
||||
@ -4,11 +4,11 @@ import { getActions, withGlobal } from '../../../../global';
|
||||
import type { ApiSticker, ApiUser } from '../../../../api/types';
|
||||
import type { TabState } from '../../../../global/types';
|
||||
|
||||
import { STARS_ICON_PLACEHOLDER } from '../../../../config';
|
||||
import { getUserFullName } from '../../../../global/helpers';
|
||||
import { selectStarGiftSticker, selectUser } from '../../../../global/selectors';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { formatDateTimeToString } from '../../../../util/dates/dateFormat';
|
||||
import { formatStarsAsIcon, formatStarsAsText } from '../../../../util/localization/format';
|
||||
import { CUSTOM_PEER_HIDDEN } from '../../../../util/objects/customPeer';
|
||||
import { formatInteger } from '../../../../util/textFormat';
|
||||
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
|
||||
@ -69,6 +69,7 @@ const GiftInfoModal = ({
|
||||
const canConvertDifference = (userGift && starGiftMaxConvertPeriod && (
|
||||
userGift.date + starGiftMaxConvertPeriod - Date.now() / 1000
|
||||
)) || 0;
|
||||
const conversionLeft = Math.ceil(canConvertDifference / 60 / 60 / 24);
|
||||
|
||||
const handleClose = useLastCallback(() => {
|
||||
closeGiftInfoModal();
|
||||
@ -131,17 +132,19 @@ const GiftInfoModal = ({
|
||||
|
||||
return canUpdate
|
||||
? lang('GiftInfoDescription', {
|
||||
amount: formatInteger(starsToConvert!),
|
||||
amount: starsToConvert,
|
||||
}, {
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
pluralValue: starsToConvert,
|
||||
})
|
||||
: lang('GiftInfoDescriptionOut', {
|
||||
amount: formatInteger(starsToConvert!),
|
||||
amount: starsToConvert,
|
||||
user: getUserFullName(targetUser)!,
|
||||
}, {
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
pluralValue: starsToConvert,
|
||||
});
|
||||
})();
|
||||
|
||||
@ -205,14 +208,7 @@ const GiftInfoModal = ({
|
||||
tableData.push([
|
||||
lang('GiftInfoValue'),
|
||||
<div className={styles.giftValue}>
|
||||
{lang('StarsAmount', {
|
||||
amount: formatInteger(gift.stars),
|
||||
}, {
|
||||
withNodes: true,
|
||||
specialReplacement: {
|
||||
[STARS_ICON_PLACEHOLDER]: <StarIcon type="gold" size="small" />,
|
||||
},
|
||||
})}
|
||||
{formatStarsAsIcon(lang, gift.stars)}
|
||||
{canUpdate && canConvertDifference > 0 && Boolean(starsToConvert) && (
|
||||
<BadgeButton onClick={openConvertConfirm}>
|
||||
{lang('GiftInfoConvert', { amount: starsToConvert }, { pluralValue: starsToConvert })}
|
||||
@ -225,8 +221,10 @@ const GiftInfoModal = ({
|
||||
tableData.push([
|
||||
lang('GiftInfoAvailability'),
|
||||
lang('GiftInfoAvailabilityValue', {
|
||||
count: formatInteger(gift.availabilityRemains!),
|
||||
total: formatInteger(gift.availabilityTotal),
|
||||
count: gift.availabilityRemains || 0,
|
||||
total: gift.availabilityTotal,
|
||||
}, {
|
||||
pluralValue: gift.availabilityRemains || 0,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
@ -295,7 +293,7 @@ const GiftInfoModal = ({
|
||||
>
|
||||
<div>
|
||||
{lang('GiftInfoConvertDescription1', {
|
||||
amount: lang('StarsAmountText', { amount: formatInteger(userGift.starsToConvert!) }),
|
||||
amount: formatStarsAsText(lang, userGift.starsToConvert!),
|
||||
user: getUserFullName(userFrom)!,
|
||||
}, {
|
||||
withNodes: true,
|
||||
@ -305,10 +303,11 @@ const GiftInfoModal = ({
|
||||
{canConvertDifference > 0 && (
|
||||
<div>
|
||||
{lang('GiftInfoConvertDescriptionPeriod', {
|
||||
count: formatInteger(Math.ceil(canConvertDifference / 60 / 60 / 24)),
|
||||
count: conversionLeft,
|
||||
}, {
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
pluralValue: conversionLeft,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -94,11 +94,11 @@ const StarPaymentModal = ({
|
||||
if (subscriptionInfo) {
|
||||
return lang('StarsSubscribeText', {
|
||||
chat: subscriptionInfo.title,
|
||||
amount: amount!,
|
||||
amount,
|
||||
}, {
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
pluralValue: amount,
|
||||
pluralValue: amount!,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import type { ApiStarsTransaction } from '../../../../api/types';
|
||||
import type { LangFn } from '../../../../hooks/useOldLang';
|
||||
import type { OldLangFn } from '../../../../hooks/useOldLang';
|
||||
|
||||
import { buildStarsTransactionCustomPeer } from '../../../../global/helpers/payments';
|
||||
|
||||
export function getTransactionTitle(lang: LangFn, transaction: ApiStarsTransaction) {
|
||||
export function getTransactionTitle(lang: OldLangFn, transaction: ApiStarsTransaction) {
|
||||
if (transaction.extendedMedia) return lang('StarMediaPurchase');
|
||||
if (transaction.subscriptionPeriod) return lang('StarSubscriptionPurchase');
|
||||
if (transaction.isReaction) return lang('StarsReactionsSent');
|
||||
|
||||
@ -73,6 +73,9 @@ const MinimizedWebAppModal = ({
|
||||
{
|
||||
botName: activeTabName,
|
||||
count: openedTabsCount - 1,
|
||||
},
|
||||
{
|
||||
pluralValue: openedTabsCount - 1,
|
||||
})}`
|
||||
: activeTabName;
|
||||
|
||||
|
||||
@ -182,10 +182,6 @@ const useWebAppFrame = (
|
||||
data: null,
|
||||
},
|
||||
});
|
||||
|
||||
// showNotification({
|
||||
// message: 'Clipboard access is not supported in this client yet',
|
||||
// });
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_open_scan_qr_popup') {
|
||||
|
||||
@ -9,7 +9,6 @@ import type {
|
||||
ApiWebDocument,
|
||||
} from '../../api/types';
|
||||
import type { FormEditDispatch } from '../../hooks/reducers/usePaymentReducer';
|
||||
import type { LangCode } from '../../types';
|
||||
import type { IconName } from '../../types/icons';
|
||||
import { PaymentStep } from '../../types';
|
||||
|
||||
@ -238,7 +237,7 @@ const Checkout: FC<OwnProps> = ({
|
||||
export default memo(Checkout);
|
||||
|
||||
function renderPaymentItem(
|
||||
langCode: LangCode | undefined, title: string, value: number, currency: string, main = false,
|
||||
langCode: string | undefined, title: string, value: number, currency: string, main = false,
|
||||
) {
|
||||
return (
|
||||
<div className={buildClassName(styles.priceInfoItem, main && styles.priceInfoItemMain)}>
|
||||
|
||||
@ -3,7 +3,7 @@ import React, { memo, useCallback } from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { ApiMessage, StatisticsMessageInteractionCounter } from '../../../api/types';
|
||||
import type { LangFn } from '../../../hooks/useOldLang';
|
||||
import type { OldLangFn } from '../../../hooks/useOldLang';
|
||||
|
||||
import {
|
||||
getMessageMediaHash,
|
||||
@ -67,7 +67,7 @@ const StatisticsRecentMessage: FC<OwnProps> = ({ postStatistic, message }) => {
|
||||
);
|
||||
};
|
||||
|
||||
function renderSummary(lang: LangFn, message: ApiMessage, blobUrl?: string, isRoundVideo?: boolean) {
|
||||
function renderSummary(lang: OldLangFn, message: ApiMessage, blobUrl?: string, isRoundVideo?: boolean) {
|
||||
if (!blobUrl) {
|
||||
return renderMessageSummary(lang, message);
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import type {
|
||||
ApiTypeStory,
|
||||
StatisticsStoryInteractionCounter,
|
||||
} from '../../../api/types';
|
||||
import type { LangFn } from '../../../hooks/useOldLang';
|
||||
import type { OldLangFn } from '../../../hooks/useOldLang';
|
||||
|
||||
import { getStoryMediaHash } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
@ -65,7 +65,7 @@ function StatisticsRecentStory({ chat, story, postStatistic }: OwnProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function renderSummary(lang: LangFn, chat: ApiChat, blobUrl?: string) {
|
||||
function renderSummary(lang: OldLangFn, chat: ApiChat, blobUrl?: string) {
|
||||
return (
|
||||
<span>
|
||||
{blobUrl ? (
|
||||
|
||||
@ -9,6 +9,9 @@ const storedParameter: LangFnParameters = {
|
||||
variables: {
|
||||
count: 42,
|
||||
},
|
||||
options: {
|
||||
pluralValue: 42,
|
||||
},
|
||||
};
|
||||
|
||||
const storedAdvancedParameter: LangFnParameters = {
|
||||
@ -39,7 +42,7 @@ const TestLocale = () => {
|
||||
withMarkdown: true,
|
||||
})}
|
||||
</p>
|
||||
<p>{lang('Participants', { count: 42 })}</p>
|
||||
<p>{lang('Participants', { count: 42 }, { pluralValue: 42 })}</p>
|
||||
<p>
|
||||
{lang('ChatServiceGroupUpdatedPinnedMessage1', {
|
||||
message: 'Some message',
|
||||
|
||||
@ -1,19 +1,21 @@
|
||||
import type { FC, TeactNode } from '../../lib/teact/teact';
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions } from '../../global';
|
||||
|
||||
import type { CallbackAction } from '../../global/types';
|
||||
import type { IconName } from '../../types/icons';
|
||||
import type { ApiNotification } from '../../api/types';
|
||||
import { isLangFnParam } from '../../util/localization/types';
|
||||
|
||||
import { ANIMATION_END_DELAY } from '../../config';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated';
|
||||
|
||||
@ -25,57 +27,55 @@ import RoundTimer from './RoundTimer';
|
||||
import './Notification.scss';
|
||||
|
||||
type OwnProps = {
|
||||
title?: TeactNode;
|
||||
containerId?: string;
|
||||
message: TeactNode;
|
||||
duration?: number;
|
||||
action?: CallbackAction | CallbackAction[];
|
||||
actionText?: string;
|
||||
className?: string;
|
||||
icon?: IconName;
|
||||
shouldDisableClickDismiss?: boolean;
|
||||
dismissAction?: CallbackAction;
|
||||
shouldShowTimer?: boolean;
|
||||
cacheBreaker?: string;
|
||||
onDismiss: NoneToVoidFunction;
|
||||
notification: ApiNotification;
|
||||
};
|
||||
|
||||
const DEFAULT_DURATION = 3000;
|
||||
const ANIMATION_DURATION = 150;
|
||||
|
||||
const Notification: FC<OwnProps> = ({
|
||||
title,
|
||||
className,
|
||||
message,
|
||||
duration = DEFAULT_DURATION,
|
||||
containerId,
|
||||
icon,
|
||||
action,
|
||||
actionText,
|
||||
shouldDisableClickDismiss,
|
||||
dismissAction,
|
||||
shouldShowTimer,
|
||||
cacheBreaker,
|
||||
onDismiss,
|
||||
notification,
|
||||
}) => {
|
||||
const actions = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const {
|
||||
localId,
|
||||
message,
|
||||
action,
|
||||
actionText,
|
||||
cacheBreaker,
|
||||
className,
|
||||
disableClickDismiss,
|
||||
dismissAction,
|
||||
duration = DEFAULT_DURATION,
|
||||
icon,
|
||||
shouldShowTimer,
|
||||
title,
|
||||
containerSelector,
|
||||
} = notification;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const timerRef = useRef<number | undefined>(null);
|
||||
const { transitionClassNames } = useShowTransitionDeprecated(isOpen);
|
||||
|
||||
const handleDismiss = useLastCallback(() => {
|
||||
actions.dismissNotification({ localId });
|
||||
});
|
||||
|
||||
const closeAndDismiss = useLastCallback((force?: boolean) => {
|
||||
if (!force && shouldDisableClickDismiss) return;
|
||||
if (!force && disableClickDismiss) return;
|
||||
setIsOpen(false);
|
||||
setTimeout(onDismiss, ANIMATION_DURATION + ANIMATION_END_DELAY);
|
||||
setTimeout(handleDismiss, ANIMATION_DURATION + ANIMATION_END_DELAY);
|
||||
if (dismissAction) {
|
||||
// @ts-ignore
|
||||
actions[dismissAction.action](dismissAction.payload);
|
||||
}
|
||||
});
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
const handleClick = useLastCallback(() => {
|
||||
if (action) {
|
||||
if (Array.isArray(action)) {
|
||||
// @ts-ignore
|
||||
@ -86,7 +86,7 @@ const Notification: FC<OwnProps> = ({
|
||||
}
|
||||
}
|
||||
closeAndDismiss();
|
||||
}, [action, actions, closeAndDismiss]);
|
||||
});
|
||||
|
||||
useEffect(() => (isOpen ? captureEscKeyListener(closeAndDismiss) : undefined), [isOpen, closeAndDismiss]);
|
||||
|
||||
@ -102,7 +102,7 @@ const Notification: FC<OwnProps> = ({
|
||||
}, [duration, cacheBreaker]); // Reset timer if `cacheBreaker` changes
|
||||
|
||||
const handleMouseEnter = useLastCallback(() => {
|
||||
if (shouldDisableClickDismiss) return;
|
||||
if (disableClickDismiss) return;
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = undefined;
|
||||
@ -110,15 +110,45 @@ const Notification: FC<OwnProps> = ({
|
||||
});
|
||||
|
||||
const handleMouseLeave = useLastCallback(() => {
|
||||
if (shouldDisableClickDismiss) return;
|
||||
if (disableClickDismiss) return;
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
timerRef.current = window.setTimeout(closeAndDismiss, duration);
|
||||
});
|
||||
|
||||
const renderedTitle = useMemo(() => {
|
||||
if (!title) return undefined;
|
||||
if (isLangFnParam(title)) {
|
||||
return lang.with(title);
|
||||
}
|
||||
|
||||
return renderText(title, ['simple_markdown', 'emoji', 'br', 'links']);
|
||||
}, [lang, title]);
|
||||
|
||||
const renderedMessage = useMemo(() => {
|
||||
if (isLangFnParam(message)) {
|
||||
return lang.with(message);
|
||||
}
|
||||
|
||||
if (typeof message === 'string') {
|
||||
return renderText(message, ['simple_markdown', 'emoji', 'br', 'links']);
|
||||
}
|
||||
|
||||
return message;
|
||||
}, [lang, message]);
|
||||
|
||||
const renderedActionText = useMemo(() => {
|
||||
if (!actionText) return undefined;
|
||||
if (isLangFnParam(actionText)) {
|
||||
return lang.with(actionText);
|
||||
}
|
||||
|
||||
return actionText;
|
||||
}, [lang, actionText]);
|
||||
|
||||
return (
|
||||
<Portal className="Notification-container" containerId={containerId}>
|
||||
<Portal className="Notification-container" containerSelector={containerSelector}>
|
||||
<div
|
||||
className={buildClassName('Notification', transitionClassNames, className)}
|
||||
onClick={handleClick}
|
||||
@ -127,16 +157,18 @@ const Notification: FC<OwnProps> = ({
|
||||
>
|
||||
<Icon name={icon || 'info-filled'} className="notification-icon" />
|
||||
<div className="content">
|
||||
{title && <div className="notification-title">{title}</div>}
|
||||
{message}
|
||||
{renderedTitle && (
|
||||
<div className="notification-title">{renderedTitle}</div>
|
||||
)}
|
||||
{renderedMessage}
|
||||
</div>
|
||||
{action && actionText && (
|
||||
{action && renderedActionText && (
|
||||
<Button
|
||||
color="translucent-white"
|
||||
onClick={handleClick}
|
||||
className="notification-button"
|
||||
>
|
||||
{actionText}
|
||||
{renderedActionText}
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowTimer && (
|
||||
|
||||
@ -3,19 +3,19 @@ import { useLayoutEffect, useRef } from '../../lib/teact/teact';
|
||||
import TeactDOM from '../../lib/teact/teact-dom';
|
||||
|
||||
type OwnProps = {
|
||||
containerId?: string;
|
||||
containerSelector?: string;
|
||||
className?: string;
|
||||
children: VirtualElement;
|
||||
};
|
||||
|
||||
const Portal: FC<OwnProps> = ({ containerId, className, children }) => {
|
||||
const Portal: FC<OwnProps> = ({ containerSelector, className, children }) => {
|
||||
const elementRef = useRef<HTMLDivElement>();
|
||||
if (!elementRef.current) {
|
||||
elementRef.current = document.createElement('div');
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const container = document.querySelector<HTMLDivElement>(containerId || '#portals');
|
||||
const container = document.querySelector<HTMLDivElement>(containerSelector || '#portals');
|
||||
if (!container) {
|
||||
return undefined;
|
||||
}
|
||||
@ -31,7 +31,7 @@ const Portal: FC<OwnProps> = ({ containerId, className, children }) => {
|
||||
TeactDOM.render(undefined, element);
|
||||
container.removeChild(element);
|
||||
};
|
||||
}, [className, containerId]);
|
||||
}, [className, containerSelector]);
|
||||
|
||||
return TeactDOM.render(children, elementRef.current);
|
||||
};
|
||||
|
||||
@ -294,6 +294,7 @@ export const PURCHASE_USERNAME = 'auction';
|
||||
export const ACCEPTABLE_USERNAME_ERRORS = new Set([USERNAME_PURCHASE_ERROR, 'USERNAME_INVALID']);
|
||||
export const TME_WEB_DOMAINS = new Set(['t.me', 'web.t.me', 'a.t.me', 'k.t.me', 'z.t.me']);
|
||||
export const WEB_APP_PLATFORM = 'weba';
|
||||
export const LANG_PACK = 'weba';
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
export const COUNTRIES_WITH_12H_TIME_FORMAT = new Set(['AU', 'BD', 'CA', 'CO', 'EG', 'HN', 'IE', 'IN', 'JO', 'MX', 'MY', 'NI', 'NZ', 'PH', 'PK', 'SA', 'SV', 'US']);
|
||||
@ -321,7 +322,7 @@ 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 OLD_DEFAULT_LANG_PACK = 'android';
|
||||
export const LANG_PACKS = ['android', 'ios', 'tdesktop', 'macos'] as const;
|
||||
export const FEEDBACK_URL = 'https://bugs.telegram.org/?tag_ids=41&sort=time';
|
||||
export const FAQ_URL = 'https://telegram.org/faq';
|
||||
|
||||
@ -373,7 +373,13 @@ addActionHandler('loadLanguages', async (global): Promise<void> => {
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
global = replaceSettings(global, { languages: result });
|
||||
global = {
|
||||
...global,
|
||||
settings: {
|
||||
...global.settings,
|
||||
languages: result,
|
||||
},
|
||||
};
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import type {
|
||||
ApiUpdateServerTimeOffset,
|
||||
ApiUpdateSession,
|
||||
} from '../../../api/types';
|
||||
import type { LangCode } from '../../../types';
|
||||
import type { RequiredGlobalActions } from '../../index';
|
||||
import type { ActionReturnType, GlobalState } from '../../types';
|
||||
|
||||
@ -97,7 +98,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
});
|
||||
|
||||
function onUpdateApiReady<T extends GlobalState>(global: T) {
|
||||
void oldSetLanguage(global.settings.byKey.language);
|
||||
void oldSetLanguage(global.settings.byKey.language as LangCode);
|
||||
}
|
||||
|
||||
function onUpdateAuthorizationState<T extends GlobalState>(global: T, update: ApiUpdateAuthorizationState) {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { ActionReturnType } from '../../types';
|
||||
import { PaymentStep } from '../../../types';
|
||||
|
||||
import { applyLangPackDifference, requestLangPackDifference } from '../../../util/localization';
|
||||
import { addActionHandler, setGlobal } from '../../index';
|
||||
import {
|
||||
addBlockedUser,
|
||||
@ -194,6 +195,15 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
setGlobal(global);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'updateLangPackTooLong': {
|
||||
requestLangPackDifference(update.langCode);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'updateLangPack': {
|
||||
applyLangPackDifference(update.version, update.strings, update.keysToRemove);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@ -199,7 +199,9 @@ addActionHandler('createGroupCallInviteLink', async (global, actions, payload):
|
||||
|
||||
copyTextToClipboard(inviteLink);
|
||||
actions.showNotification({
|
||||
message: 'Link copied to clipboard',
|
||||
message: {
|
||||
key: 'LinkCopied',
|
||||
},
|
||||
tabId,
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { addCallback } from '../../../lib/teact/teactn';
|
||||
|
||||
import type { LangCode } from '../../../types';
|
||||
import type { ActionReturnType, GlobalState } from '../../types';
|
||||
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
@ -134,7 +135,7 @@ addCallback((global: GlobalState) => {
|
||||
|
||||
const performanceType = selectPerformanceSettings(global);
|
||||
|
||||
void oldSetLanguage(language, undefined, true);
|
||||
void oldSetLanguage(language as LangCode, undefined, true);
|
||||
|
||||
requestMutation(() => {
|
||||
document.documentElement.style.setProperty(
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { addCallback } from '../../../lib/teact/teactn';
|
||||
|
||||
import type { ActionReturnType, GlobalState } from '../../types';
|
||||
import { SettingsScreens } from '../../../types';
|
||||
import { type LangCode, SettingsScreens } from '../../../types';
|
||||
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { disableDebugConsole, initDebugConsole } from '../../../util/debugConsole';
|
||||
@ -53,7 +53,7 @@ addCallback((global: GlobalState) => {
|
||||
}
|
||||
|
||||
if (settings.language !== prevSettings.language) {
|
||||
oldSetLanguage(settings.language);
|
||||
oldSetLanguage(settings.language as LangCode);
|
||||
}
|
||||
|
||||
if (settings.timeFormat !== prevSettings.timeFormat) {
|
||||
|
||||
@ -9,7 +9,7 @@ import type {
|
||||
ApiTopic,
|
||||
ApiUser,
|
||||
} from '../../api/types';
|
||||
import type { LangFn } from '../../hooks/useOldLang';
|
||||
import type { OldLangFn } from '../../hooks/useOldLang';
|
||||
import type {
|
||||
CustomPeer, NotifyException, NotifySettings, ThreadId,
|
||||
} from '../../types';
|
||||
@ -101,7 +101,7 @@ export function getPrivateChatUserId(chat: ApiChat) {
|
||||
return chat.id;
|
||||
}
|
||||
|
||||
export function getChatTitle(lang: LangFn, chat: ApiChat, isSelf = false) {
|
||||
export function getChatTitle(lang: OldLangFn, chat: ApiChat, isSelf = false) {
|
||||
if (isSelf) {
|
||||
return lang('SavedMessages');
|
||||
}
|
||||
@ -247,7 +247,7 @@ export function getAllowedAttachmentOptions(
|
||||
}
|
||||
|
||||
export function getMessageSendingRestrictionReason(
|
||||
lang: LangFn,
|
||||
lang: OldLangFn,
|
||||
currentUserBannedRights?: ApiChatBannedRights,
|
||||
defaultBannedRights?: ApiChatBannedRights,
|
||||
) {
|
||||
@ -272,7 +272,7 @@ export function getMessageSendingRestrictionReason(
|
||||
}
|
||||
|
||||
export function getForumComposerPlaceholder(
|
||||
lang: LangFn,
|
||||
lang: OldLangFn,
|
||||
chat?: ApiChat,
|
||||
threadId: ThreadId = MAIN_THREAD_ID,
|
||||
topics?: Record<number, ApiTopic>,
|
||||
@ -341,7 +341,7 @@ export function getCanDeleteChat(chat: ApiChat) {
|
||||
return isChatBasicGroup(chat) || ((isChatSuperGroup(chat) || isChatChannel(chat)) && chat.isCreator);
|
||||
}
|
||||
|
||||
export function getFolderDescriptionText(lang: LangFn, folder: ApiChatFolder, chatsCount?: number) {
|
||||
export function getFolderDescriptionText(lang: OldLangFn, folder: ApiChatFolder, chatsCount?: number) {
|
||||
const {
|
||||
excludedChatIds, includedChatIds,
|
||||
bots, groups, contacts, nonContacts, channels,
|
||||
@ -376,7 +376,7 @@ export function getFolderDescriptionText(lang: LangFn, folder: ApiChatFolder, ch
|
||||
}
|
||||
}
|
||||
|
||||
export function getMessageSenderName(lang: LangFn, chatId: string, sender?: ApiPeer) {
|
||||
export function getMessageSenderName(lang: OldLangFn, chatId: string, sender?: ApiPeer) {
|
||||
if (!sender || isUserId(chatId)) {
|
||||
return undefined;
|
||||
}
|
||||
@ -395,7 +395,7 @@ export function getMessageSenderName(lang: LangFn, chatId: string, sender?: ApiP
|
||||
}
|
||||
|
||||
export function filterChatsByName(
|
||||
lang: LangFn,
|
||||
lang: OldLangFn,
|
||||
chatIds: string[],
|
||||
chatsById: Record<string, ApiChat>,
|
||||
query?: string,
|
||||
@ -475,7 +475,7 @@ export function getIsSavedDialog(chatId: string, threadId: ThreadId | undefined,
|
||||
return chatId === currentUserId && threadId !== MAIN_THREAD_ID;
|
||||
}
|
||||
|
||||
export function getGroupStatus(lang: LangFn, chat: ApiChat) {
|
||||
export function getGroupStatus(lang: OldLangFn, chat: ApiChat) {
|
||||
const chatTypeString = lang(getChatTypeString(chat));
|
||||
const { membersCount } = chat;
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import type { TeactNode } from '../../lib/teact/teact';
|
||||
import type {
|
||||
ApiMediaExtendedPreview, ApiMessage, MediaContent, StatefulMediaContent,
|
||||
} from '../../api/types';
|
||||
import type { LangFn } from '../../hooks/useOldLang';
|
||||
import type { OldLangFn } from '../../hooks/useOldLang';
|
||||
import { ApiMessageEntityTypes } from '../../api/types';
|
||||
|
||||
import { CONTENT_NOT_SUPPORTED } from '../../config';
|
||||
@ -17,7 +17,7 @@ const SPOILER_CHARS = ['⠺', '⠵', '⠞', '⠟'];
|
||||
export const TRUNCATED_SUMMARY_LENGTH = 80;
|
||||
|
||||
export function getMessageSummaryText(
|
||||
lang: LangFn,
|
||||
lang: OldLangFn,
|
||||
message: ApiMessage,
|
||||
statefulContent: StatefulMediaContent | undefined,
|
||||
noEmoji = false,
|
||||
@ -106,12 +106,12 @@ export function getMessageSummaryEmoji(message: ApiMessage) {
|
||||
}
|
||||
|
||||
export function getMediaContentTypeDescription(
|
||||
lang: LangFn, content: MediaContent, statefulContent: StatefulMediaContent | undefined,
|
||||
lang: OldLangFn, content: MediaContent, statefulContent: StatefulMediaContent | undefined,
|
||||
) {
|
||||
return getSummaryDescription(lang, content, statefulContent);
|
||||
}
|
||||
export function getMessageSummaryDescription(
|
||||
lang: LangFn,
|
||||
lang: OldLangFn,
|
||||
message: ApiMessage,
|
||||
statefulContent: StatefulMediaContent | undefined,
|
||||
truncatedText?: string | TeactNode,
|
||||
@ -120,7 +120,7 @@ export function getMessageSummaryDescription(
|
||||
return getSummaryDescription(lang, message.content, statefulContent, message, truncatedText, isExtended);
|
||||
}
|
||||
function getSummaryDescription(
|
||||
lang: LangFn,
|
||||
lang: OldLangFn,
|
||||
mediaContent: MediaContent,
|
||||
statefulContent: StatefulMediaContent | undefined,
|
||||
message?: ApiMessage,
|
||||
|
||||
@ -9,7 +9,7 @@ import type {
|
||||
import type {
|
||||
ApiPoll, MediaContainer, MediaContent, StatefulMediaContent,
|
||||
} from '../../api/types/messages';
|
||||
import type { LangFn } from '../../hooks/useOldLang';
|
||||
import type { OldLangFn } from '../../hooks/useOldLang';
|
||||
import type { ThreadId } from '../../types';
|
||||
import type { GlobalState } from '../types';
|
||||
import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types';
|
||||
@ -208,7 +208,7 @@ export function isAnonymousOwnMessage(message: ApiMessage) {
|
||||
return Boolean(message.senderId) && !isUserId(message.senderId) && isOwnMessage(message);
|
||||
}
|
||||
|
||||
export function getSenderTitle(lang: LangFn, sender: ApiPeer) {
|
||||
export function getSenderTitle(lang: OldLangFn, sender: ApiPeer) {
|
||||
return isPeerUser(sender) ? getUserFullName(sender) : getChatTitle(lang, sender);
|
||||
}
|
||||
|
||||
@ -344,10 +344,10 @@ export function extractMessageText(message: ApiMessage | ApiStory, inChatList =
|
||||
return { text, entities };
|
||||
}
|
||||
|
||||
export function getExpiredMessageDescription(langFn: LangFn, message: ApiMessage): string | undefined {
|
||||
export function getExpiredMessageDescription(langFn: OldLangFn, message: ApiMessage): string | undefined {
|
||||
return getExpiredMessageContentDescription(langFn, message.content);
|
||||
}
|
||||
export function getExpiredMessageContentDescription(langFn: LangFn, mediaContent: MediaContent): string | undefined {
|
||||
export function getExpiredMessageContentDescription(langFn: OldLangFn, mediaContent: MediaContent): string | undefined {
|
||||
const { isExpiredVoice, isExpiredRoundVideo } = mediaContent;
|
||||
if (isExpiredVoice) {
|
||||
return langFn('Message.VoiceMessageExpired');
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { ApiMessage } from '../../api/types';
|
||||
import type { LangFn } from '../../hooks/useOldLang';
|
||||
import type { OldLangFn } from '../../hooks/useOldLang';
|
||||
|
||||
import { renderMessageText } from '../../components/common/helpers/renderMessageText';
|
||||
import { getGlobal } from '..';
|
||||
@ -7,7 +7,7 @@ import { getMessageStatefulContent } from './messages';
|
||||
import { getMessageSummaryDescription, getMessageSummaryEmoji } from './messageSummary';
|
||||
|
||||
export function renderMessageSummaryHtml(
|
||||
lang: LangFn,
|
||||
lang: OldLangFn,
|
||||
message: ApiMessage,
|
||||
) {
|
||||
const global = getGlobal();
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { ApiPeer, ApiUser, ApiUserStatus } from '../../api/types';
|
||||
import type { LangFn } from '../../hooks/useOldLang';
|
||||
import type { OldLangFn } from '../../hooks/useOldLang';
|
||||
|
||||
import { ANONYMOUS_USER_ID, SERVICE_NOTIFICATIONS_USER_ID } from '../../config';
|
||||
import { formatFullDate, formatTime } from '../../util/dates/dateFormat';
|
||||
@ -66,7 +66,7 @@ export function getUserFullName(user?: ApiUser) {
|
||||
}
|
||||
|
||||
export function getUserStatus(
|
||||
lang: LangFn, user: ApiUser, userStatus: ApiUserStatus | undefined,
|
||||
lang: OldLangFn, user: ApiUser, userStatus: ApiUserStatus | undefined,
|
||||
) {
|
||||
if (user.id === SERVICE_NOTIFICATIONS_USER_ID) {
|
||||
return lang('ServiceNotifications');
|
||||
|
||||
@ -36,6 +36,7 @@ import type {
|
||||
ApiInputInvoiceStarGift,
|
||||
ApiInputMessageReplyInfo,
|
||||
ApiKeyboardButton,
|
||||
ApiLanguage,
|
||||
ApiMediaFormat,
|
||||
ApiMessage,
|
||||
ApiMessageEntity,
|
||||
@ -115,7 +116,6 @@ import type {
|
||||
InlineBotSettings,
|
||||
ISettings,
|
||||
IThemeSettings,
|
||||
LangCode,
|
||||
LoadMoreDirection,
|
||||
ManagementProgress,
|
||||
ManagementScreens,
|
||||
@ -1199,7 +1199,7 @@ export type GlobalState = {
|
||||
defaultTopicIconsId?: string;
|
||||
defaultStatusIconsId?: string;
|
||||
premiumGifts?: ApiStickerSet;
|
||||
emojiKeywords: Partial<Record<LangCode, EmojiKeywords>>;
|
||||
emojiKeywords: Record<string, EmojiKeywords | undefined>;
|
||||
|
||||
gifs: {
|
||||
saved: {
|
||||
@ -1243,6 +1243,7 @@ export type GlobalState = {
|
||||
notifyExceptions?: Record<number, NotifyException>;
|
||||
lastPremiumBandwithNotificationDate?: number;
|
||||
paidReactionPrivacy?: boolean;
|
||||
languages?: ApiLanguage[];
|
||||
};
|
||||
|
||||
push?: {
|
||||
@ -1377,7 +1378,7 @@ export interface ActionPayloads {
|
||||
faveSticker: { sticker: ApiSticker } & WithTabId;
|
||||
unfaveSticker: { sticker: ApiSticker };
|
||||
toggleStickerSet: { stickerSetId: string };
|
||||
loadEmojiKeywords: { language: LangCode };
|
||||
loadEmojiKeywords: { language: string };
|
||||
|
||||
// groups
|
||||
togglePreHistoryHidden: {
|
||||
@ -1484,7 +1485,7 @@ export interface ActionPayloads {
|
||||
updateContentSettings: boolean;
|
||||
|
||||
loadCountryList: {
|
||||
langCode?: LangCode;
|
||||
langCode?: string;
|
||||
};
|
||||
ensureTimeFormat: WithTabId | undefined;
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { GlobalState } from '../global/types';
|
||||
import type { LangFn } from './useOldLang';
|
||||
import type { OldLangFn } from './useOldLang';
|
||||
|
||||
import useBrowserOnline from './window/useBrowserOnline';
|
||||
|
||||
@ -16,7 +16,7 @@ type ConnectionStatusPosition =
|
||||
| 'none';
|
||||
|
||||
export default function useConnectionStatus(
|
||||
lang: LangFn,
|
||||
lang: OldLangFn,
|
||||
connectionState: GlobalState['connectionState'],
|
||||
isSyncing: boolean | undefined,
|
||||
hasMiddleHeader: boolean,
|
||||
|
||||
@ -2,11 +2,11 @@ import * as langProvider from '../util/oldLangProvider';
|
||||
import useEffectOnce from './useEffectOnce';
|
||||
import useForceUpdate from './useForceUpdate';
|
||||
|
||||
export type LangFn = langProvider.LangFn;
|
||||
export type OldLangFn = langProvider.LangFn;
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
const useOldLang = (): LangFn => {
|
||||
const useOldLang = (): OldLangFn => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
useEffectOnce(() => {
|
||||
|
||||
@ -73,6 +73,7 @@ class TelegramClient {
|
||||
systemVersion: undefined,
|
||||
appVersion: undefined,
|
||||
langCode: 'en',
|
||||
langPack: 'weba',
|
||||
systemLangCode: 'en',
|
||||
baseLogger: 'gramjs',
|
||||
useWSS: false,
|
||||
@ -158,7 +159,7 @@ class TelegramClient {
|
||||
.toString() || '1.0',
|
||||
appVersion: args.appVersion || '1.0',
|
||||
langCode: args.langCode,
|
||||
langPack: 'weba',
|
||||
langPack: args.langPack,
|
||||
systemLangCode: args.systemLangCode,
|
||||
query: x,
|
||||
proxy: undefined, // no proxies yet.
|
||||
|
||||
@ -11,7 +11,6 @@ import type {
|
||||
ApiExportedInvite,
|
||||
ApiFakeType,
|
||||
ApiLabeledPrice,
|
||||
ApiLanguage,
|
||||
ApiMessage,
|
||||
ApiPhoto,
|
||||
ApiReaction,
|
||||
@ -113,8 +112,7 @@ export interface ISettings extends NotifySettings, Record<string, any> {
|
||||
shouldSuggestCustomEmoji: boolean;
|
||||
shouldUpdateStickerSetOrder: boolean;
|
||||
hasPassword?: boolean;
|
||||
languages?: ApiLanguage[];
|
||||
language: LangCode;
|
||||
language: string;
|
||||
isSensitiveEnabled?: boolean;
|
||||
canChangeSensitive?: boolean;
|
||||
timeFormat: TimeFormat;
|
||||
@ -481,8 +479,8 @@ export type NotifyException = {
|
||||
|
||||
export type EmojiKeywords = {
|
||||
isLoading?: boolean;
|
||||
version: number;
|
||||
keywords: Record<string, string[]>;
|
||||
version?: number;
|
||||
keywords?: Record<string, string[]>;
|
||||
};
|
||||
|
||||
export type InlineBotSettings = {
|
||||
|
||||
1123
src/types/language.d.ts
vendored
1123
src/types/language.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
||||
import type { LangFn } from '../../hooks/useOldLang';
|
||||
import type { OldLangFn } from '../../hooks/useOldLang';
|
||||
import type { TimeFormat } from '../../types';
|
||||
|
||||
import withCache from '../withCache';
|
||||
@ -39,7 +39,7 @@ function toIsoString(date: Date) {
|
||||
}
|
||||
|
||||
// @optimization `toLocaleTimeString` is avoided because of bad performance
|
||||
export function formatTime(lang: LangFn, datetime: number | Date) {
|
||||
export function formatTime(lang: OldLangFn, datetime: number | Date) {
|
||||
const date = typeof datetime === 'number' ? new Date(datetime) : datetime;
|
||||
const timeFormat = lang.timeFormat || '24h';
|
||||
|
||||
@ -53,7 +53,7 @@ export function formatTime(lang: LangFn, datetime: number | Date) {
|
||||
return `${String(hours).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}${marker}`;
|
||||
}
|
||||
|
||||
export function formatPastTimeShort(lang: LangFn, datetime: number | Date, alwaysShowTime = false) {
|
||||
export function formatPastTimeShort(lang: OldLangFn, datetime: number | Date, alwaysShowTime = false) {
|
||||
const date = typeof datetime === 'number' ? new Date(datetime) : datetime;
|
||||
|
||||
const time = formatTime(lang, date);
|
||||
@ -76,16 +76,16 @@ export function formatPastTimeShort(lang: LangFn, datetime: number | Date, alway
|
||||
return alwaysShowTime ? lang('FullDateTimeFormat', [formattedDate, time]) : formattedDate;
|
||||
}
|
||||
|
||||
export function formatFullDate(lang: LangFn, datetime: number | Date) {
|
||||
export function formatFullDate(lang: OldLangFn, datetime: number | Date) {
|
||||
return formatDateToString(datetime, lang.code, false, 'numeric');
|
||||
}
|
||||
|
||||
export function formatMonthAndYear(lang: LangFn, date: Date, isShort = false) {
|
||||
export function formatMonthAndYear(lang: OldLangFn, date: Date, isShort = false) {
|
||||
return formatDateToString(date, lang.code, false, isShort ? 'short' : 'long', true);
|
||||
}
|
||||
|
||||
export function formatCountdown(
|
||||
lang: LangFn,
|
||||
lang: OldLangFn,
|
||||
msLeft: number,
|
||||
) {
|
||||
const days = Math.floor(msLeft / MILLISECONDS_IN_DAY);
|
||||
@ -104,7 +104,7 @@ export function formatCountdown(
|
||||
}
|
||||
}
|
||||
|
||||
export function formatCountdownShort(lang: LangFn, msLeft: number): string {
|
||||
export function formatCountdownShort(lang: OldLangFn, msLeft: number): string {
|
||||
if (msLeft < 60 * 1000) {
|
||||
return Math.ceil(msLeft / 1000).toString();
|
||||
} else if (msLeft < 60 * 60 * 1000) {
|
||||
@ -116,7 +116,7 @@ export function formatCountdownShort(lang: LangFn, msLeft: number): string {
|
||||
}
|
||||
}
|
||||
|
||||
export function formatLastUpdated(lang: LangFn, currentTime: number, lastUpdated = currentTime) {
|
||||
export function formatLastUpdated(lang: OldLangFn, currentTime: number, lastUpdated = currentTime) {
|
||||
const seconds = currentTime - lastUpdated;
|
||||
if (seconds < 60) {
|
||||
return lang('LiveLocationUpdated.JustNow');
|
||||
@ -127,7 +127,7 @@ export function formatLastUpdated(lang: LangFn, currentTime: number, lastUpdated
|
||||
}
|
||||
}
|
||||
|
||||
export function formatRelativeTime(lang: LangFn, currentTime: number, lastUpdated = currentTime) {
|
||||
export function formatRelativeTime(lang: OldLangFn, currentTime: number, lastUpdated = currentTime) {
|
||||
const seconds = currentTime - lastUpdated;
|
||||
|
||||
if (seconds < 60) {
|
||||
@ -156,7 +156,7 @@ export function formatRelativeTime(lang: LangFn, currentTime: number, lastUpdate
|
||||
|
||||
type DurationType = 'Seconds' | 'Minutes' | 'Hours' | 'Days' | 'Weeks';
|
||||
|
||||
export function formatTimeDuration(lang: LangFn, duration: number, showLast = 2) {
|
||||
export function formatTimeDuration(lang: OldLangFn, duration: number, showLast = 2) {
|
||||
if (!duration) {
|
||||
return undefined;
|
||||
}
|
||||
@ -196,7 +196,7 @@ export function formatTimeDuration(lang: LangFn, duration: number, showLast = 2)
|
||||
}
|
||||
|
||||
export function formatHumanDate(
|
||||
lang: LangFn,
|
||||
lang: OldLangFn,
|
||||
datetime: number | Date,
|
||||
isShort = false,
|
||||
noWeekdays = false,
|
||||
@ -237,13 +237,13 @@ export function formatHumanDate(
|
||||
* Returns weekday name
|
||||
* @param day 0 - Sunday, 1 - Monday, ...
|
||||
*/
|
||||
export function formatWeekday(lang: LangFn, day: number, isShort = false) {
|
||||
export function formatWeekday(lang: OldLangFn, day: number, isShort = false) {
|
||||
const weekDay = WEEKDAYS_FULL[day];
|
||||
return isShort ? lang(`Weekday.Short${weekDay}`) : lang(`Weekday.${weekDay}`);
|
||||
}
|
||||
|
||||
export function formatMediaDateTime(
|
||||
lang: LangFn,
|
||||
lang: OldLangFn,
|
||||
datetime: number | Date,
|
||||
isUpperFirst?: boolean,
|
||||
) {
|
||||
@ -350,7 +350,7 @@ export function formatDateTimeToString(
|
||||
}
|
||||
|
||||
export function formatDateAtTime(
|
||||
lang: LangFn,
|
||||
lang: OldLangFn,
|
||||
datetime: number | Date,
|
||||
) {
|
||||
const date = typeof datetime === 'number' ? new Date(datetime) : datetime;
|
||||
@ -375,7 +375,7 @@ export function formatDateAtTime(
|
||||
}
|
||||
|
||||
export function formatDateInFuture(
|
||||
lang: LangFn,
|
||||
lang: OldLangFn,
|
||||
currentTime: number,
|
||||
datetime: number,
|
||||
) {
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import React, { type TeactNode } from '../lib/teact/teact';
|
||||
|
||||
import type { LangCode } from '../types';
|
||||
|
||||
import { STARS_CURRENCY_CODE } from '../config';
|
||||
|
||||
import StarIcon from '../components/common/icons/StarIcon';
|
||||
@ -9,7 +7,7 @@ import StarIcon from '../components/common/icons/StarIcon';
|
||||
export function formatCurrency(
|
||||
totalPrice: number,
|
||||
currency: string,
|
||||
locale: LangCode = 'en',
|
||||
locale: string = 'en',
|
||||
options?: {
|
||||
shouldOmitFractions?: boolean;
|
||||
iconClassName?: string;
|
||||
@ -27,7 +25,7 @@ export function formatCurrency(
|
||||
export function formatCurrencyAsString(
|
||||
totalPrice: number,
|
||||
currency: string,
|
||||
locale: LangCode = 'en',
|
||||
locale: string = 'en',
|
||||
options?: {
|
||||
shouldOmitFractions?: boolean;
|
||||
},
|
||||
|
||||
20
src/util/localization/format.tsx
Normal file
20
src/util/localization/format.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from '../../lib/teact/teact';
|
||||
|
||||
import type { LangFn } from './types';
|
||||
|
||||
import { STARS_ICON_PLACEHOLDER } from '../../config';
|
||||
|
||||
import StarIcon from '../../components/common/icons/StarIcon';
|
||||
|
||||
export function formatStarsAsText(lang: LangFn, amount: number) {
|
||||
return lang('StarsAmountText', { amount }, { pluralValue: amount });
|
||||
}
|
||||
|
||||
export function formatStarsAsIcon(lang: LangFn, amount: number) {
|
||||
return lang('StarsAmount', { amount }, {
|
||||
withNodes: true,
|
||||
specialReplacement: {
|
||||
[STARS_ICON_PLACEHOLDER]: <StarIcon type="gold" className="star-amount-icon" size="adaptive" />,
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -4,21 +4,23 @@ import type {
|
||||
ApiLanguage,
|
||||
CachedLangData,
|
||||
LangPack,
|
||||
LangPackStringValue,
|
||||
} from '../../api/types';
|
||||
import type { LangKey } from '../../types/language';
|
||||
import type { LangKey, LangVariable } from '../../types/language';
|
||||
import {
|
||||
type AdvancedLangFnOptions,
|
||||
type AdvancedLangFnOptionsWithPlural,
|
||||
areAdvancedLangFnOptions,
|
||||
isDeletedLangString,
|
||||
isPluralLangString,
|
||||
type LangFn,
|
||||
type LangFnOptions,
|
||||
type LangFnOptionsWithPlural,
|
||||
type LangFnParameters,
|
||||
type LangFnWithFunction,
|
||||
type LangFormatters,
|
||||
} from './types';
|
||||
|
||||
import { DEBUG } from '../../config';
|
||||
import { DEBUG, LANG_PACK } from '../../config';
|
||||
import { callApi } from '../../api/gramjs';
|
||||
import renderText, { type TextFilter } from '../../components/common/helpers/renderText';
|
||||
import { MAIN_IDB_STORE } from '../browser/idb';
|
||||
@ -37,7 +39,6 @@ import LimitedMap from '../primitives/LimitedMap';
|
||||
|
||||
import initialStrings from '../../assets/localization/initialStrings';
|
||||
|
||||
const LANG_PACK = 'weba';
|
||||
const LANGPACK_STORE_PREFIX = 'langpack-';
|
||||
const FORMATTERS_FALLBACK_LANG = 'en';
|
||||
|
||||
@ -115,14 +116,22 @@ async function fetchDifference() {
|
||||
langCode: langPack.langCode,
|
||||
fromVersion: langPack.version,
|
||||
});
|
||||
if (!result || result.version === langPack.version) return;
|
||||
if (!result) return;
|
||||
|
||||
applyLangPackDifference(result.version, result.strings, result.keysToRemove);
|
||||
}
|
||||
|
||||
export function applyLangPackDifference(
|
||||
version: number, strings: Record<string, LangPackStringValue>, keysToRemove: string[],
|
||||
) {
|
||||
if (!langPack || !language || version === langPack.version) return;
|
||||
|
||||
const newLangPack = {
|
||||
...langPack,
|
||||
version: result.version,
|
||||
version,
|
||||
strings: {
|
||||
...omit(langPack.strings, result.keysToRemove),
|
||||
...result.strings,
|
||||
...omit(langPack.strings, keysToRemove),
|
||||
...strings,
|
||||
},
|
||||
};
|
||||
updateLangPack(newLangPack);
|
||||
@ -242,6 +251,11 @@ export async function loadAndChangeLanguage(langCode: string, shouldCheckCache?:
|
||||
return changeLanguage(remoteLanguage);
|
||||
}
|
||||
|
||||
export function requestLangPackDifference(langCode: string) {
|
||||
if (language?.langCode !== langCode) return;
|
||||
fetchDifference();
|
||||
}
|
||||
|
||||
export async function changeLanguage(newLanguage: ApiLanguage) {
|
||||
if (langPack && language?.langCode === newLanguage.langCode) return;
|
||||
|
||||
@ -300,10 +314,10 @@ function createTranslationFn(): LangFn {
|
||||
fn.pluralCode = language?.pluralCode || FORMATTERS_FALLBACK_LANG;
|
||||
fn.with = (({ key, variables, options }: LangFnParameters) => {
|
||||
if (options && areAdvancedLangFnOptions(options)) {
|
||||
return processTranslationAdvanced(key, variables as Record<string, TeactNode>, options);
|
||||
return processTranslationAdvanced(key, variables as Record<string, TeactNode | undefined>, options);
|
||||
}
|
||||
return processTranslation(key, variables as Record<string, string | number>, options);
|
||||
}) as LangFnWithFunction;
|
||||
return processTranslation(key, variables as Record<string, LangVariable>, options);
|
||||
});
|
||||
fn.region = (code: string) => formatters?.region.of(code);
|
||||
fn.conjunction = (list: string[]) => formatters?.conjunction.format(list) || list.join(', ');
|
||||
fn.disjunction = (list: string[]) => formatters?.disjunction.format(list) || list.join(', ');
|
||||
@ -315,7 +329,7 @@ export function getTranslationFn(): LangFn {
|
||||
return translationFn;
|
||||
}
|
||||
|
||||
function getString(langKey: LangKey, count: number, options?: Pick<LangFnOptions, 'pluralValue'>) {
|
||||
function getString(langKey: LangKey, count: number) {
|
||||
let langPackStringValue = langPack?.strings[langKey];
|
||||
|
||||
if (!langPackStringValue && !fallbackLangPack) {
|
||||
@ -327,7 +341,7 @@ function getString(langKey: LangKey, count: number, options?: Pick<LangFnOptions
|
||||
|
||||
if (!langPackStringValue || isDeletedLangString(langPackStringValue)) return undefined;
|
||||
|
||||
const pluralSuffix = formatters?.pluralRules.select(options?.pluralValue || count) || 'other';
|
||||
const pluralSuffix = formatters?.pluralRules.select(count) || 'other';
|
||||
|
||||
const string = isPluralLangString(langPackStringValue)
|
||||
? (langPackStringValue[pluralSuffix] || langPackStringValue.other)
|
||||
@ -337,20 +351,26 @@ function getString(langKey: LangKey, count: number, options?: Pick<LangFnOptions
|
||||
}
|
||||
|
||||
function processTranslation(
|
||||
langKey: LangKey, variables?: Record<string, string | number>, options?: LangFnOptions,
|
||||
langKey: LangKey,
|
||||
variables?: Record<string, LangVariable>,
|
||||
options?: LangFnOptions | LangFnOptionsWithPlural,
|
||||
): string {
|
||||
const cacheKey = `${langKey}-${JSON.stringify(variables)}-${JSON.stringify(options)}`;
|
||||
if (TRANSLATION_CACHE.has(cacheKey)) {
|
||||
return TRANSLATION_CACHE.get(cacheKey)!;
|
||||
}
|
||||
|
||||
const string = getString(langKey, options?.pluralValue || Number(variables?.count) || 0, options);
|
||||
const pluralValue = options && 'pluralValue' in options ? Number(options.pluralValue) : 0;
|
||||
const string = getString(langKey, pluralValue);
|
||||
|
||||
if (!string) return langKey;
|
||||
|
||||
const variableEntries = variables ? Object.entries(variables) : [];
|
||||
const finalString = variableEntries.reduce((result, [key, value]) => {
|
||||
return result.replace(`{${key}}`, String(value));
|
||||
if (!value) return result;
|
||||
|
||||
const valueAsString = Number.isInteger(value) ? formatters!.number.format(value as number) : String(value);
|
||||
return result.replace(`{${key}}`, valueAsString);
|
||||
}, string);
|
||||
|
||||
TRANSLATION_CACHE.set(cacheKey, finalString);
|
||||
@ -359,9 +379,12 @@ function processTranslation(
|
||||
}
|
||||
|
||||
function processTranslationAdvanced(
|
||||
langKey: LangKey, variables?: Record<string, TeactNode>, options?: AdvancedLangFnOptions,
|
||||
langKey: LangKey,
|
||||
variables?: Record<string, TeactNode | undefined>,
|
||||
options?: AdvancedLangFnOptions | AdvancedLangFnOptionsWithPlural,
|
||||
): TeactNode {
|
||||
const string = getString(langKey, options?.pluralValue || Number(variables?.count) || 0, options);
|
||||
const pluralValue = options && 'pluralValue' in options ? Number(options.pluralValue) : 0;
|
||||
const string = getString(langKey, pluralValue);
|
||||
if (!string) return langKey;
|
||||
|
||||
const variableEntries = variables ? Object.entries(variables) : [];
|
||||
@ -389,7 +412,10 @@ function processTranslationAdvanced(
|
||||
return renderText(curr, filters, {
|
||||
markdownPostProcessor: (part: string) => {
|
||||
return variableEntries.reduce((result, [key, value]): TeactNode[] => {
|
||||
return replaceInStringsWithTeact(result, `{${key}}`, value);
|
||||
if (!value) return result;
|
||||
|
||||
const preparedValue = Number.isInteger(value) ? formatters!.number.format(value as number) : value;
|
||||
return replaceInStringsWithTeact(result, `{${key}}`, preparedValue);
|
||||
}, [part] as TeactNode[]);
|
||||
},
|
||||
});
|
||||
@ -397,7 +423,10 @@ function processTranslationAdvanced(
|
||||
}
|
||||
|
||||
return variableEntries.reduce((result, [key, value]): TeactNode[] => {
|
||||
return replaceInStringsWithTeact(result, `{${key}}`, value);
|
||||
if (!value) return result;
|
||||
|
||||
const preparedValue = Number.isInteger(value) ? formatters!.number.format(value as number) : value;
|
||||
return replaceInStringsWithTeact(result, `{${key}}`, preparedValue);
|
||||
}, tempResult);
|
||||
}
|
||||
|
||||
|
||||
@ -7,30 +7,51 @@ import type {
|
||||
LangPackStringValueRegular,
|
||||
} from '../../api/types';
|
||||
import type { TextFilter } from '../../components/common/helpers/renderText';
|
||||
import type { LangKey, LangPair } from '../../types/language';
|
||||
import type {
|
||||
LangPairPluralWithVariables,
|
||||
LangPairWithVariables,
|
||||
PluralLangKey,
|
||||
PluralLangKeyWithVariables,
|
||||
RegularLangKey,
|
||||
RegularLangKeyWithVariables,
|
||||
} from '../../types/language';
|
||||
|
||||
type ReplaceTypeValues<T, R> = {
|
||||
[K in keyof T]: R;
|
||||
};
|
||||
|
||||
export interface LangFnOptions {
|
||||
pluralValue?: number;
|
||||
export interface LangFnOptionsRegular {
|
||||
withNodes?: never;
|
||||
withMarkdown?: never;
|
||||
pluralValue?: never;
|
||||
}
|
||||
|
||||
export interface AdvancedLangFnOptions {
|
||||
pluralValue?: number;
|
||||
export type LangFnOptionsWithPlural = Omit<LangFnOptionsRegular, 'pluralValue'> & {
|
||||
pluralValue: number;
|
||||
};
|
||||
|
||||
export type LangFnOptions = LangFnOptionsRegular | LangFnOptionsWithPlural;
|
||||
|
||||
export interface AdvancedLangFnOptionsRegular {
|
||||
withNodes: true;
|
||||
withMarkdown?: boolean;
|
||||
pluralValue?: never;
|
||||
renderTextFilters?: TextFilter[];
|
||||
specialReplacement?: Record<string, TeactNode>;
|
||||
}
|
||||
|
||||
type LangPairWithNodes = {
|
||||
[K in keyof LangPair]: LangPair[K] extends object ? ReplaceTypeValues<LangPair[K], TeactNode> : LangPair[K];
|
||||
export type AdvancedLangFnOptionsWithPlural = Omit<AdvancedLangFnOptionsRegular, 'pluralValue'> & {
|
||||
pluralValue: number;
|
||||
};
|
||||
|
||||
type RegularLangFnParameters<T = LangPair> = {
|
||||
export type AdvancedLangFnOptions = AdvancedLangFnOptionsRegular | AdvancedLangFnOptionsWithPlural;
|
||||
|
||||
type LangPairWithNodes = LangPairWithVariables<TeactNode | undefined>;
|
||||
type LangPairPluralWithNodes = LangPairPluralWithVariables<TeactNode | undefined>;
|
||||
|
||||
type RegularLangFnParametersWithoutVariables = {
|
||||
key: RegularLangKey;
|
||||
variables?: undefined;
|
||||
options?: LangFnOptions;
|
||||
};
|
||||
|
||||
type RegularLangFnParametersWithVariables<T = LangPairWithVariables> = {
|
||||
[K in keyof T]: {
|
||||
key: K;
|
||||
variables: T[K];
|
||||
@ -38,7 +59,33 @@ type RegularLangFnParameters<T = LangPair> = {
|
||||
}
|
||||
}[keyof T];
|
||||
|
||||
type AdvancedLangFnParameters<T = LangPairWithNodes> = {
|
||||
type RegularLangFnPluralParameters = {
|
||||
key: PluralLangKey;
|
||||
variables?: undefined;
|
||||
options: LangFnOptionsWithPlural;
|
||||
};
|
||||
|
||||
type RegularLangFnPluralParametersWithVariables<T = LangPairPluralWithVariables> = {
|
||||
[K in keyof T]: {
|
||||
key: K;
|
||||
variables: T[K];
|
||||
options: LangFnOptionsWithPlural;
|
||||
}
|
||||
}[keyof T];
|
||||
|
||||
type RegularLangFnParameters =
|
||||
| RegularLangFnParametersWithoutVariables
|
||||
| RegularLangFnParametersWithVariables
|
||||
| RegularLangFnPluralParameters
|
||||
| RegularLangFnPluralParametersWithVariables;
|
||||
|
||||
type AdvancedLangFnParametersWithoutVariables = {
|
||||
key: RegularLangKey;
|
||||
variables?: undefined;
|
||||
options: AdvancedLangFnOptions;
|
||||
};
|
||||
|
||||
type AdvancedLangFnParametersWithVariables<T = LangPairWithNodes> = {
|
||||
[K in keyof T]: {
|
||||
key: K;
|
||||
variables: T[K];
|
||||
@ -46,21 +93,56 @@ type AdvancedLangFnParameters<T = LangPairWithNodes> = {
|
||||
}
|
||||
}[keyof T];
|
||||
|
||||
export type LangFnParameters = RegularLangFnParameters | AdvancedLangFnParameters;
|
||||
|
||||
export type LangFnWithFunction = {
|
||||
(params: RegularLangFnParameters): string;
|
||||
(params: AdvancedLangFnParameters): TeactNode;
|
||||
type AdvancedLangFnPluralParameters = {
|
||||
key: PluralLangKey;
|
||||
variables?: undefined;
|
||||
options: AdvancedLangFnOptionsWithPlural;
|
||||
};
|
||||
|
||||
type AdvancedLangFnPluralParametersWithVariables<T = LangPairPluralWithNodes> = {
|
||||
[K in keyof T]: {
|
||||
key: K;
|
||||
variables: T[K];
|
||||
options: AdvancedLangFnOptionsWithPlural;
|
||||
}
|
||||
}[keyof T];
|
||||
|
||||
type AdvancedLangFnParameters =
|
||||
| AdvancedLangFnParametersWithoutVariables
|
||||
| AdvancedLangFnParametersWithVariables
|
||||
| AdvancedLangFnPluralParameters
|
||||
| AdvancedLangFnPluralParametersWithVariables;
|
||||
|
||||
export type LangFnParameters = RegularLangFnParameters | AdvancedLangFnParameters;
|
||||
|
||||
export type LangFn = {
|
||||
<K extends LangKey = LangKey, V extends LangPair[K] = LangPair[K]>(
|
||||
key: K, variables?: V, options?: LangFnOptions,
|
||||
<K = RegularLangKey>(
|
||||
key: K, variables?: undefined, options?: LangFnOptions,
|
||||
): string;
|
||||
<K extends LangKey = LangKey, V extends LangPairWithNodes[K] = LangPairWithNodes[K]>(
|
||||
<K = PluralLangKey>(
|
||||
key: K, variables: undefined, options: LangFnOptionsWithPlural,
|
||||
): string;
|
||||
<K extends RegularLangKeyWithVariables = RegularLangKeyWithVariables, V = LangPairWithVariables[K]>(
|
||||
key: K, variables: V, options?: LangFnOptions,
|
||||
): string;
|
||||
<K extends PluralLangKeyWithVariables = PluralLangKeyWithVariables, V = LangPairPluralWithVariables[K]>(
|
||||
key: K, variables: V, options: LangFnOptionsWithPlural,
|
||||
): string;
|
||||
|
||||
<K = RegularLangKey>(
|
||||
key: K, variables?: undefined, options?: AdvancedLangFnOptions,
|
||||
): TeactNode;
|
||||
<K = PluralLangKey>(
|
||||
key: K, variables: undefined, options: AdvancedLangFnOptionsWithPlural,
|
||||
): TeactNode;
|
||||
<K extends RegularLangKeyWithVariables = RegularLangKeyWithVariables, V = LangPairWithVariables[K]>(
|
||||
key: K, variables: V, options: AdvancedLangFnOptions,
|
||||
): TeactNode;
|
||||
with: LangFnWithFunction;
|
||||
<K extends PluralLangKeyWithVariables = PluralLangKeyWithVariables, V = LangPairPluralWithVariables[K]>(
|
||||
key: K, variables: V, options: AdvancedLangFnOptionsWithPlural,
|
||||
): TeactNode;
|
||||
|
||||
with: (params: LangFnParameters) => TeactNode;
|
||||
region: (code: string) => string | undefined;
|
||||
conjunction: (list: string[]) => string;
|
||||
disjunction: (list: string[]) => string;
|
||||
@ -101,5 +183,5 @@ export function isLangFnParam(object: unknown): object is LangFnParameters {
|
||||
export function areAdvancedLangFnOptions(
|
||||
params: LangFnOptions | AdvancedLangFnOptions,
|
||||
): params is AdvancedLangFnOptions {
|
||||
return 'withNodes' in params;
|
||||
return 'withNodes' in params && Boolean(params.withNodes);
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import type { ApiOldLangPack, ApiOldLangString } from '../api/types';
|
||||
import type { LangCode, TimeFormat } from '../types';
|
||||
|
||||
import {
|
||||
DEFAULT_LANG_CODE, DEFAULT_LANG_PACK, LANG_CACHE_NAME, LANG_PACKS,
|
||||
DEFAULT_LANG_CODE, LANG_CACHE_NAME, LANG_PACKS, OLD_DEFAULT_LANG_PACK,
|
||||
} from '../config';
|
||||
import { callApi } from '../api/gramjs';
|
||||
import * as cacheApi from './cacheApi';
|
||||
@ -155,14 +155,14 @@ export async function getTranslationForLangString(langCode: string, key: string)
|
||||
let translateString: ApiOldLangString | undefined;
|
||||
const cachedValue = await cacheApi.fetch(
|
||||
LANG_CACHE_NAME,
|
||||
`${DEFAULT_LANG_PACK}_${langCode}_${key}`,
|
||||
`${OLD_DEFAULT_LANG_PACK}_${langCode}_${key}`,
|
||||
cacheApi.Type.Json,
|
||||
);
|
||||
|
||||
if (cachedValue) {
|
||||
translateString = cachedValue.value;
|
||||
} else {
|
||||
translateString = await fetchRemoteString(DEFAULT_LANG_PACK, langCode, key);
|
||||
translateString = await fetchRemoteString(OLD_DEFAULT_LANG_PACK, langCode, key);
|
||||
}
|
||||
|
||||
return processTranslation(translateString, key);
|
||||
@ -199,7 +199,9 @@ export async function oldSetLanguage(langCode: LangCode, callback?: NoneToVoidFu
|
||||
langPack = newLangPack;
|
||||
document.documentElement.lang = langCode;
|
||||
|
||||
const { languages, timeFormat } = getGlobal().settings.byKey;
|
||||
const global = getGlobal();
|
||||
const { languages, byKey } = global.settings;
|
||||
const timeFormat = byKey?.timeFormat;
|
||||
const langInfo = languages?.find((lang) => lang.langCode === langCode);
|
||||
translationFn = createLangFn();
|
||||
translationFn.isRtl = Boolean(langInfo?.isRtl);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { LangFn } from '../hooks/useOldLang';
|
||||
import type { OldLangFn } from '../hooks/useOldLang';
|
||||
|
||||
import EMOJI_REGEX from '../lib/twemojiRegex';
|
||||
import fixNonStandardEmoji from './emoji/fixNonStandardEmoji';
|
||||
@ -48,7 +48,7 @@ export const getFirstLetters = withCache((phrase: string, count = 2) => {
|
||||
});
|
||||
|
||||
const FILE_SIZE_UNITS = ['B', 'KB', 'MB', 'GB'];
|
||||
export function formatFileSize(lang: LangFn, bytes: number, decimals = 1): string {
|
||||
export function formatFileSize(lang: OldLangFn, bytes: number, decimals = 1): string {
|
||||
if (bytes === 0) {
|
||||
return lang('FileSize.B', 0);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user