Localization: Better platform support (#5136)

This commit is contained in:
zubiden 2024-11-09 15:40:11 +04:00 committed by Alexander Zinchuk
parent 6babbae9f9
commit e678824a10
64 changed files with 1105 additions and 886 deletions

View File

@ -3,38 +3,42 @@ import { readFileSync, writeFileSync } from 'fs';
import readStrings from '../src/util/data/readStrings'; 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 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 data = readFileSync('./src/assets/localization/fallback.strings', 'utf8');
const parsed = readStrings(data); const parsed = readStrings(data);
const keysWithVars = Object.entries(parsed).reduce((acc, [keyWithSuffix, value]) => { const regularKeysWithVars: Record<string, string[]> = {};
const key = keyWithSuffix.split('_')[0]; const pluralKeysWithVars: Record<string, string[]> = {};
Object.entries(parsed).forEach(([keyWithSuffix, value]) => {
const [key, pluralSuffix] = keyWithSuffix.split('_');
const variables = extractVariables(value); const variables = extractVariables(value);
const acc = pluralSuffix ? pluralKeysWithVars : regularKeysWithVars;
const previousVariables = acc[key] || []; const previousVariables = acc[key] || [];
if (!previousVariables.length) { previousVariables.push(...variables);
acc[key] = variables; acc[key] = previousVariables;
} 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}`;
}); });
const langPair = `export interface LangPair {\n${entries}\n}\n`; const regularTypes = formatKeyWithVariables(false, regularKeysWithVars);
const text = `${TOP_COMMENT}\n${langPair}\n${LANG_KEY_TYPE}\n`; 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'); writeFileSync('./src/types/language.d.ts', text, 'utf8');
// eslint-disable-next-line no-console // 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) { function extractVariables(value: string) {
const matches = value.match(/(?<!\\){[^{}]+}/g); const matches = value.match(/(?<!\\){[^{}]+}/g);
@ -45,3 +49,23 @@ function extractVariables(value: string) {
function wrapInQuotes(value: string) { function wrapInQuotes(value: string) {
return `'${value}'`; 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`;
}

View File

@ -16,7 +16,7 @@ import type {
import { import {
APP_CODE_NAME, APP_CODE_NAME,
DEBUG, DEBUG_GRAMJS, IS_TEST, UPLOAD_WORKERS, DEBUG, DEBUG_GRAMJS, IS_TEST, LANG_PACK, UPLOAD_WORKERS,
} from '../../../config'; } from '../../../config';
import { pause } from '../../../util/schedulers'; import { pause } from '../../../util/schedulers';
import { import {
@ -95,7 +95,9 @@ export async function init(initialArgs: ApiInitialArgs) {
shouldForceHttpTransport, shouldForceHttpTransport,
shouldAllowHttpTransport, shouldAllowHttpTransport,
dcId, dcId,
langPack: LANG_PACK,
langCode, langCode,
systemLangCode: navigator.language,
isTestServerRequested, isTestServerRequested,
} as any, } as any,
); );

View File

@ -2,7 +2,7 @@ import BigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs'; import { Api as GramJs } from '../../../lib/gramjs';
import type { LANG_PACKS } from '../../../config'; import type { LANG_PACKS } from '../../../config';
import type { ApiInputPrivacyRules, ApiPrivacyKey, LangCode } from '../../../types'; import type { ApiInputPrivacyRules, ApiPrivacyKey } from '../../../types';
import type { import type {
ApiAppConfig, ApiAppConfig,
ApiConfig, ApiConfig,
@ -14,7 +14,10 @@ import type {
} from '../../types'; } from '../../types';
import { 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'; } from '../../../config';
import { buildCollectionByKey } from '../../../util/iteratees'; import { buildCollectionByKey } from '../../../util/iteratees';
import { getServerTime } from '../../../util/serverTime'; import { getServerTime } from '../../../util/serverTime';
@ -466,7 +469,7 @@ export async function fetchLangDifference({
export async function fetchLanguages(): Promise<ApiLanguage[] | undefined> { export async function fetchLanguages(): Promise<ApiLanguage[] | undefined> {
const result = await invokeRequest(new GramJs.langpack.GetLanguages({ const result = await invokeRequest(new GramJs.langpack.GetLanguages({
langPack: DEFAULT_LANG_PACK, langPack: OLD_DEFAULT_LANG_PACK,
})); }));
if (!result) { if (!result) {
return undefined; 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({ const countryList = await invokeRequest(new GramJs.help.GetCountriesList({
langCode, langCode,
})); }));

View File

@ -47,6 +47,7 @@ import {
import { import {
buildApiNotifyException, buildApiNotifyException,
buildApiNotifyExceptionTopic, buildApiNotifyExceptionTopic,
buildLangStrings,
buildPrivacyKey, buildPrivacyKey,
} from '../apiBuilders/misc'; } from '../apiBuilders/misc';
import { buildApiEmojiStatus, buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; import { buildApiEmojiStatus, buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
@ -1050,7 +1051,20 @@ export function updater(update: Update) {
'@type': 'updatePaidReactionPrivacy', '@type': 'updatePaidReactionPrivacy',
isPrivate: update.private, 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({ sendApiUpdate({
'@type': 'updatePremiumFloodWait', '@type': 'updatePremiumFloodWait',
isUpload: update.isUpload, isUpload: update.isUpload,

View File

@ -1,5 +1,8 @@
import type { TeactNode } from '../../lib/teact/teact';
import type { ApiLimitType, ApiPremiumSection, CallbackAction } from '../../global/types'; import type { ApiLimitType, ApiPremiumSection, CallbackAction } from '../../global/types';
import type { IconName } from '../../types/icons'; import type { IconName } from '../../types/icons';
import type { LangFnParameters } from '../../util/localization';
import type { ApiDocument, ApiPhoto, ApiReaction } from './messages'; import type { ApiDocument, ApiPhoto, ApiReaction } from './messages';
import type { ApiUser } from './users'; import type { ApiUser } from './users';
@ -111,10 +114,11 @@ export type ApiNotifyException = {
export type ApiNotification = { export type ApiNotification = {
localId: string; localId: string;
title?: string; containerSelector?: string;
message: string; title?: string | LangFnParameters;
message: TeactNode | LangFnParameters;
cacheBreaker?: string; cacheBreaker?: string;
actionText?: string; actionText?: string | LangFnParameters;
action?: CallbackAction | CallbackAction[]; action?: CallbackAction | CallbackAction[];
className?: string; className?: string;
duration?: number; duration?: number;

View File

@ -34,6 +34,7 @@ import type {
import type { import type {
ApiEmojiInteraction, ApiError, ApiNotifyException, ApiSessionData, ApiEmojiInteraction, ApiError, ApiNotifyException, ApiSessionData,
} from './misc'; } from './misc';
import type { LangPackStringValue } from './settings';
import type { ApiStealthMode, ApiStory, ApiStorySkipped } from './stories'; import type { ApiStealthMode, ApiStory, ApiStorySkipped } from './stories';
import type { import type {
ApiEmojiStatus, ApiUser, ApiUserFullInfo, ApiUserStatus, ApiEmojiStatus, ApiUser, ApiUserFullInfo, ApiUserStatus,
@ -774,6 +775,18 @@ export type ApiUpdatePaidReactionPrivacy = {
isPrivate: boolean; 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 = ( export type ApiUpdate = (
ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate | ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate |
ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser | ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser |
@ -806,7 +819,8 @@ export type ApiUpdate = (
ApiUpdateViewForumAsMessages | ApiUpdateSavedDialogPinned | ApiUpdatePinnedSavedDialogIds | ApiUpdateChatLastMessage | ApiUpdateViewForumAsMessages | ApiUpdateSavedDialogPinned | ApiUpdatePinnedSavedDialogIds | ApiUpdateChatLastMessage |
ApiUpdateDeleteSavedHistory | ApiUpdatePremiumFloodWait | ApiUpdateStarsBalance | ApiUpdateDeleteSavedHistory | ApiUpdatePremiumFloodWait | ApiUpdateStarsBalance |
ApiUpdateQuickReplyMessage | ApiUpdateQuickReplies | ApiDeleteQuickReply | ApiUpdateDeleteQuickReplyMessages | ApiUpdateQuickReplyMessage | ApiUpdateQuickReplies | ApiDeleteQuickReply | ApiUpdateDeleteQuickReplyMessages |
ApiUpdateDeleteProfilePhoto | ApiUpdateNewProfilePhoto | ApiUpdateEntities | ApiUpdatePaidReactionPrivacy ApiUpdateDeleteProfilePhoto | ApiUpdateNewProfilePhoto | ApiUpdateEntities | ApiUpdatePaidReactionPrivacy |
ApiUpdateLangPackTooLong | ApiUpdateLangPack
); );
export type OnApiUpdate = (update: ApiUpdate) => void; export type OnApiUpdate = (update: ApiUpdate) => void;

View File

@ -668,7 +668,6 @@
"DiscussChannel" = "channel"; "DiscussChannel" = "channel";
"ForwardedMessage" = "Forwarded message"; "ForwardedMessage" = "Forwarded message";
"ContextForwardMsg" = "Forward"; "ContextForwardMsg" = "Forward";
"ShareLinkCopied" = "Copied to Clipboard";
"MessageScheduleSend" = "Send Now"; "MessageScheduleSend" = "Send Now";
"MessageScheduleEditTime" = "Reschedule"; "MessageScheduleEditTime" = "Reschedule";
"Reply" = "Reply"; "Reply" = "Reply";

View File

@ -7,7 +7,6 @@ import { getActions, withGlobal } from '../../global';
import type { ApiCountryCode } from '../../api/types'; import type { ApiCountryCode } from '../../api/types';
import type { GlobalState } from '../../global/types'; import type { GlobalState } from '../../global/types';
import type { LangCode } from '../../types';
import { requestMeasure } from '../../lib/fasterdom/fasterdom'; import { requestMeasure } from '../../lib/fasterdom/fasterdom';
import { preloadImage } from '../../util/files'; import { preloadImage } from '../../util/files';
@ -36,7 +35,7 @@ type StateProps = Pick<GlobalState, (
'authIsLoadingQrCode' | 'authError' | 'authIsLoadingQrCode' | 'authError' |
'authRememberMe' | 'authNearestCountry' 'authRememberMe' | 'authNearestCountry'
)> & { )> & {
language?: LangCode; language?: string;
phoneCodeList: ApiCountryCode[]; phoneCodeList: ApiCountryCode[];
}; };

View File

@ -5,7 +5,6 @@ import React, {
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { GlobalState } from '../../global/types'; import type { GlobalState } from '../../global/types';
import type { LangCode } from '../../types';
import { DEFAULT_LANG_CODE, STRICTERDOM_ENABLED } from '../../config'; import { DEFAULT_LANG_CODE, STRICTERDOM_ENABLED } from '../../config';
import { disableStrict, enableStrict } from '../../lib/fasterdom/stricterdom'; import { disableStrict, enableStrict } from '../../lib/fasterdom/stricterdom';
@ -29,7 +28,7 @@ import blankUrl from '../../assets/blank.png';
type StateProps = type StateProps =
Pick<GlobalState, 'connectionState' | 'authState' | 'authQrCode'> Pick<GlobalState, 'connectionState' | 'authState' | 'authQrCode'>
& { language?: LangCode }; & { language?: string };
const DATA_PREFIX = 'tg://login?token='; const DATA_PREFIX = 'tg://login?token=';
const QR_SIZE = 280; const QR_SIZE = 280;

View File

@ -6,7 +6,7 @@ import { getActions } from '../../global';
import type { ApiAudio, ApiMessage, ApiVoice } from '../../api/types'; import type { ApiAudio, ApiMessage, ApiVoice } from '../../api/types';
import type { BufferedRange } from '../../hooks/useBuffering'; import type { BufferedRange } from '../../hooks/useBuffering';
import type { LangFn } from '../../hooks/useOldLang'; import type { OldLangFn } from '../../hooks/useOldLang';
import type { ISettings } from '../../types'; import type { ISettings } from '../../types';
import { ApiMediaFormat } from '../../api/types'; import { ApiMediaFormat } from '../../api/types';
import { AudioOrigin } from '../../types'; import { AudioOrigin } from '../../types';
@ -495,7 +495,7 @@ function getSeeklineSpikeAmounts(isMobile?: boolean, withAvatar?: boolean) {
} }
function renderAudio( function renderAudio(
lang: LangFn, lang: OldLangFn,
audio: ApiAudio, audio: ApiAudio,
duration: number, duration: number,
isPlaying: boolean, isPlaying: boolean,

View File

@ -3,7 +3,7 @@ import React, {
memo, useCallback, useEffect, useMemo, useState, memo, useCallback, useEffect, useMemo, useState,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import type { LangFn } from '../../hooks/useOldLang'; import type { OldLangFn } from '../../hooks/useOldLang';
import { MAX_INT_32 } from '../../config'; import { MAX_INT_32 } from '../../config';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
@ -401,7 +401,7 @@ function formatDay(year: number, month: number, day: number) {
return `${year}-${month + 1}-${day}`; return `${year}-${month + 1}-${day}`;
} }
function formatSubmitLabel(lang: LangFn, date: Date) { function formatSubmitLabel(lang: OldLangFn, date: Date) {
const day = formatDateToString(date, lang.code); const day = formatDateToString(date, lang.code);
const today = formatDateToString(new Date(), lang.code); const today = formatDateToString(new Date(), lang.code);

View File

@ -43,7 +43,9 @@ const InviteLink: FC<OwnProps> = ({
const copyLink = useLastCallback(() => { const copyLink = useLastCallback(() => {
copyTextToClipboard(link); copyTextToClipboard(link);
showNotification({ showNotification({
message: lang('LinkCopied'), message: {
key: 'LinkCopied',
},
}); });
}); });

View File

@ -4,7 +4,7 @@ import type {
ApiChat, ApiGroupCall, ApiMessage, ApiTopic, ApiUser, ApiChat, ApiGroupCall, ApiMessage, ApiTopic, ApiUser,
} from '../../../api/types'; } from '../../../api/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { LangFn } from '../../../hooks/useOldLang'; import type { OldLangFn } from '../../../hooks/useOldLang';
import type { TextPart } from '../../../types'; import type { TextPart } from '../../../types';
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config'; import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
@ -36,7 +36,7 @@ const MAX_LENGTH = 32;
const NBSP = '\u00A0'; const NBSP = '\u00A0';
export function renderActionMessageText( export function renderActionMessageText(
oldLang: LangFn, oldLang: OldLangFn,
message: ApiMessage, message: ApiMessage,
actionOriginUser?: ApiUser, actionOriginUser?: ApiUser,
actionOriginChat?: ApiChat, actionOriginChat?: ApiChat,
@ -269,7 +269,7 @@ function renderProductContent(message: ApiMessage) {
} }
function renderMessageContent( function renderMessageContent(
lang: LangFn, lang: OldLangFn,
message: ApiMessage, message: ApiMessage,
options: RenderOptions = {}, options: RenderOptions = {},
observeIntersectionForLoading?: ObserveFn, 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>; 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); const text = trimText(getChatTitle(lang, chat), MAX_LENGTH);
if (noLinks) { if (noLinks) {

View File

@ -1,7 +1,7 @@
import { getGlobal } from '../../../global'; import { getGlobal } from '../../../global';
import type { ApiMessage, ApiSponsoredMessage } from '../../../api/types'; 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 type { TextPart } from '../../../types';
import { ApiMessageEntityTypes } from '../../../api/types'; import { ApiMessageEntityTypes } from '../../../api/types';
@ -65,7 +65,7 @@ export function renderMessageText({
// TODO Use Message Summary component instead // TODO Use Message Summary component instead
export function renderMessageSummary( export function renderMessageSummary(
lang: LangFn, lang: OldLangFn,
message: ApiMessage, message: ApiMessage,
noEmoji = false, noEmoji = false,
highlight?: string, highlight?: string,

View File

@ -62,6 +62,7 @@ type OwnProps = {
noScrollRestore?: boolean; noScrollRestore?: boolean;
isViewOnly?: boolean; isViewOnly?: boolean;
withDefaultPadding?: boolean; withDefaultPadding?: boolean;
forceRenderAllItems?: boolean;
onFilterChange?: (value: string) => void; onFilterChange?: (value: string) => void;
onDisabledClick?: (value: string, isSelected: boolean) => void; onDisabledClick?: (value: string, isSelected: boolean) => void;
onLoadMore?: () => void; onLoadMore?: () => void;
@ -86,6 +87,7 @@ const ItemPicker = ({
itemInputType, itemInputType,
itemClassName, itemClassName,
withDefaultPadding, withDefaultPadding,
forceRenderAllItems,
onFilterChange, onFilterChange,
onDisabledClick, onDisabledClick,
onLoadMore, onLoadMore,
@ -163,7 +165,7 @@ const ItemPicker = ({
}); });
const [viewportValuesList, getMore] = useInfiniteScroll( const [viewportValuesList, getMore] = useInfiniteScroll(
onLoadMore, sortedItemValuesList, Boolean(filterValue), onLoadMore, sortedItemValuesList, Boolean(forceRenderAllItems || filterValue),
); );
const handleFilterChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => { const handleFilterChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {

View File

@ -6,7 +6,7 @@ import type {
ApiChat, ApiMessage, ApiMessageOutgoingStatus, ApiChat, ApiMessage, ApiMessageOutgoingStatus,
ApiUser, ApiUser,
} from '../../../api/types'; } from '../../../api/types';
import type { LangFn } from '../../../hooks/useOldLang'; import type { OldLangFn } from '../../../hooks/useOldLang';
import { import {
getMessageIsSpoiler, getMessageIsSpoiler,
@ -111,7 +111,7 @@ const ChatMessage: FC<OwnProps & StateProps> = ({
}; };
function renderSummary( function renderSummary(
lang: LangFn, message: ApiMessage, blobUrl?: string, searchQuery?: string, isRoundVideo?: boolean, lang: OldLangFn, message: ApiMessage, blobUrl?: string, searchQuery?: string, isRoundVideo?: boolean,
) { ) {
if (!blobUrl) { if (!blobUrl) {
return renderMessageSummary(lang, message, undefined, searchQuery); return renderMessageSummary(lang, message, undefined, searchQuery);

View File

@ -1,5 +1,5 @@
import type { ApiChat, ApiMessage, ApiUser } from '../../../../api/types'; import type { ApiChat, ApiMessage, ApiUser } from '../../../../api/types';
import type { LangFn } from '../../../../hooks/useOldLang'; import type { OldLangFn } from '../../../../hooks/useOldLang';
import { import {
getChatTitle, getChatTitle,
@ -9,7 +9,7 @@ import {
} from '../../../../global/helpers'; } from '../../../../global/helpers';
export function getSenderName( 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; const { senderId } = message;
if (!senderId) { if (!senderId) {

View File

@ -4,6 +4,7 @@ import React, {
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiLanguage } from '../../../api/types';
import type { ISettings, LangCode } from '../../../types'; import type { ISettings, LangCode } from '../../../types';
import { SettingsScreens } from '../../../types'; import { SettingsScreens } from '../../../types';
@ -29,7 +30,8 @@ type OwnProps = {
type StateProps = { type StateProps = {
isCurrentUserPremium: boolean; isCurrentUserPremium: boolean;
} & Pick<ISettings, 'languages' | 'language' | 'canTranslate' | 'canTranslateChats' | 'doNotTranslate'>; languages?: ApiLanguage[];
} & Pick<ISettings, | 'language' | 'canTranslate' | 'canTranslateChats' | 'doNotTranslate'>;
const SettingsLanguage: FC<OwnProps & StateProps> = ({ const SettingsLanguage: FC<OwnProps & StateProps> = ({
isActive, isActive,
@ -44,7 +46,6 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
}) => { }) => {
const { const {
loadLanguages, loadLanguages,
loadAttachBots,
setSettingOption, setSettingOption,
openPremiumModal, openPremiumModal,
} = getActions(); } = getActions();
@ -70,8 +71,6 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
unmarkIsLoading(); unmarkIsLoading();
setSettingOption({ language: langCode as LangCode }); setSettingOption({ language: langCode as LangCode });
loadAttachBots(); // Should be refetched every language change
}); });
}); });
@ -167,6 +166,7 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
<ItemPicker <ItemPicker
items={options} items={options}
selectedValue={selectedLanguage} selectedValue={selectedLanguage}
forceRenderAllItems
onSelectedValueChange={handleChange} onSelectedValueChange={handleChange}
itemInputType="radio" itemInputType="radio"
className="settings-picker" className="settings-picker"
@ -182,8 +182,9 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global): StateProps => { (global): StateProps => {
const { const {
language, languages, canTranslate, canTranslateChats, doNotTranslate, language, canTranslate, canTranslateChats, doNotTranslate,
} = global.settings.byKey; } = global.settings.byKey;
const languages = global.settings.languages;
const isCurrentUserPremium = selectIsCurrentUserPremium(global); const isCurrentUserPremium = selectIsCurrentUserPremium(global);

View File

@ -10,7 +10,6 @@ import { getActions, getGlobal, withGlobal } from '../../global';
import type { ApiChatFolder, ApiMessage, ApiUser } from '../../api/types'; import type { ApiChatFolder, ApiMessage, ApiUser } from '../../api/types';
import type { ApiLimitTypeWithModal, TabState } from '../../global/types'; import type { ApiLimitTypeWithModal, TabState } from '../../global/types';
import type { LangCode } from '../../types';
import { ElectronEvent } from '../../types/electron'; import { ElectronEvent } from '../../types/electron';
import { BASE_EMOJI_KEYWORD_LANG, DEBUG, INACTIVE_MARKER } from '../../config'; 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 useTimeout from '../../hooks/schedulers/useTimeout';
import useAppLayout from '../../hooks/useAppLayout'; import useAppLayout from '../../hooks/useAppLayout';
import useForceUpdate from '../../hooks/useForceUpdate'; import useForceUpdate from '../../hooks/useForceUpdate';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import usePreventPinchZoomGesture from '../../hooks/usePreventPinchZoomGesture'; import usePreventPinchZoomGesture from '../../hooks/usePreventPinchZoomGesture';
import useShowTransition from '../../hooks/useShowTransition'; import useShowTransition from '../../hooks/useShowTransition';
@ -115,7 +115,6 @@ type StateProps = {
openedCustomEmojiSetIds?: string[]; openedCustomEmojiSetIds?: string[];
activeGroupCallId?: string; activeGroupCallId?: string;
isServiceChatReady?: boolean; isServiceChatReady?: boolean;
language?: LangCode;
wasTimeFormatSetManually?: boolean; wasTimeFormatSetManually?: boolean;
isPhoneCallActive?: boolean; isPhoneCallActive?: boolean;
addedSetIds?: string[]; addedSetIds?: string[];
@ -170,7 +169,6 @@ const Main = ({
openedCustomEmojiSetIds, openedCustomEmojiSetIds,
isServiceChatReady, isServiceChatReady,
withInterfaceAnimations, withInterfaceAnimations,
language,
wasTimeFormatSetManually, wasTimeFormatSetManually,
addedSetIds, addedSetIds,
addedCustomEmojiIds, addedCustomEmojiIds,
@ -257,6 +255,8 @@ const Main = ({
console.log('>>> RENDER MAIN'); console.log('>>> RENDER MAIN');
} }
const lang = useLang();
// Preload Calls bundle to initialize sounds for iOS // Preload Calls bundle to initialize sounds for iOS
useTimeout(() => { useTimeout(() => {
void loadBundle(Bundles.Calls); void loadBundle(Bundles.Calls);
@ -349,13 +349,15 @@ const Main = ({
// Language-based API calls // Language-based API calls
useEffect(() => { useEffect(() => {
if (isMasterTab) { if (isMasterTab) {
if (language !== BASE_EMOJI_KEYWORD_LANG) { if (lang.code !== BASE_EMOJI_KEYWORD_LANG) {
loadEmojiKeywords({ language: language! }); loadEmojiKeywords({ language: lang.code });
} }
loadCountryList({ langCode: language }); loadCountryList({ langCode: lang.code });
loadAttachBots();
} }
}, [language, isMasterTab]); }, [lang, isMasterTab]);
// Re-fetch cached saved emoji for `localDb` // Re-fetch cached saved emoji for `localDb`
useEffect(() => { useEffect(() => {
@ -596,7 +598,7 @@ export default memo(withGlobal<OwnProps>(
const { const {
settings: { settings: {
byKey: { byKey: {
language, wasTimeFormatSetManually, wasTimeFormatSetManually,
}, },
}, },
currentUserId, currentUserId,
@ -660,7 +662,6 @@ export default memo(withGlobal<OwnProps>(
isServiceChatReady: selectIsServiceChatReady(global), isServiceChatReady: selectIsServiceChatReady(global),
activeGroupCallId: isMasterTab ? global.groupCalls.activeGroupCallId : undefined, activeGroupCallId: isMasterTab ? global.groupCalls.activeGroupCallId : undefined,
withInterfaceAnimations: selectCanAnimateInterface(global), withInterfaceAnimations: selectCanAnimateInterface(global),
language,
wasTimeFormatSetManually, wasTimeFormatSetManually,
isPhoneCallActive: isMasterTab ? Boolean(global.phoneCall) : undefined, isPhoneCallActive: isMasterTab ? Boolean(global.phoneCall) : undefined,
addedSetIds: global.stickers.added.setIds, addedSetIds: global.stickers.added.setIds,

View File

@ -1,12 +1,11 @@
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../lib/teact/teact';
import React, { memo } 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 type { ApiNotification } from '../../api/types';
import { selectTabState } from '../../global/selectors'; import { selectTabState } from '../../global/selectors';
import { pick } from '../../util/iteratees'; import { pick } from '../../util/iteratees';
import renderText from '../common/helpers/renderText';
import Notification from '../ui/Notification'; import Notification from '../ui/Notification';
@ -15,8 +14,6 @@ type StateProps = {
}; };
const Notifications: FC<StateProps> = ({ notifications }) => { const Notifications: FC<StateProps> = ({ notifications }) => {
const { dismissNotification } = getActions();
if (!notifications.length) { if (!notifications.length) {
return undefined; return undefined;
} }
@ -24,23 +21,7 @@ const Notifications: FC<StateProps> = ({ notifications }) => {
return ( return (
<div id="Notifications"> <div id="Notifications">
{notifications.map((notification) => ( {notifications.map((notification) => (
<Notification <Notification 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 })}
/>
))} ))}
</div> </div>
); );

View File

@ -3,7 +3,7 @@ import React, { memo, useCallback, useEffect } from '../../../../lib/teact/teact
import { getActions, withGlobal } from '../../../../global'; import { getActions, withGlobal } from '../../../../global';
import type { ApiLimitTypeWithModal } from '../../../../global/types'; 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 type { IconName } from '../../../../types/icons';
import { MAX_UPLOAD_FILEPART_SIZE } from '../../../../config'; 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>> = { 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 // The real size is not exactly 4gb, so we need to round it
if (value === 8000) return lang('FileSize.GB', '4'); if (value === 8000) return lang('FileSize.GB', '4');
if (value === 4000) return lang('FileSize.GB', '2'); if (value === 4000) return lang('FileSize.GB', '2');
@ -88,7 +88,7 @@ function getLimiterDescription({
premiumValue, premiumValue,
valueFormatter, valueFormatter,
}: { }: {
lang: LangFn; lang: OldLangFn;
limitType?: ApiLimitTypeWithModal; limitType?: ApiLimitTypeWithModal;
isPremium?: boolean; isPremium?: boolean;
canBuyPremium?: boolean; canBuyPremium?: boolean;

View File

@ -3,7 +3,7 @@ import React, { memo } from '../../lib/teact/teact';
import type { ApiTopic } from '../../api/types'; import type { ApiTopic } from '../../api/types';
import type { MessageListType } from '../../global/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 { REM } from '../common/helpers/mediaDimensions';
import renderText from '../common/helpers/renderText'; 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 ( return (
<div className="NoMessages"> <div className="NoMessages">
<div className="wrapper"> <div className="wrapper">
@ -69,13 +69,13 @@ function renderTopic(lang: LangFn, topic: ApiTopic) {
); );
} }
function renderScheduled(lang: LangFn) { function renderScheduled(lang: OldLangFn) {
return ( return (
<div className="empty"><span>{lang('ScheduledMessages.EmptyPlaceholder')}</span></div> <div className="empty"><span>{lang('ScheduledMessages.EmptyPlaceholder')}</span></div>
); );
} }
function renderSavedMessages(lang: LangFn) { function renderSavedMessages(lang: OldLangFn) {
return ( return (
<div className="NoMessages"> <div className="NoMessages">
<div className="wrapper"> <div className="wrapper">
@ -92,7 +92,7 @@ function renderSavedMessages(lang: LangFn) {
); );
} }
function renderGroup(lang: LangFn) { function renderGroup(lang: OldLangFn) {
return ( return (
<div className="NoMessages"> <div className="NoMessages">
<div className="wrapper" dir={lang.isRtl ? 'rtl' : undefined}> <div className="wrapper" dir={lang.isRtl ? 'rtl' : undefined}>

View File

@ -117,7 +117,7 @@ const DropArea: FC<OwnProps> = ({
); );
return ( return (
<Portal containerId="#middle-column-portals"> <Portal containerSelector="#middle-column-portals">
<div <div
className={className} className={className}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}

View File

@ -6,11 +6,11 @@ import { STARS_ICON_PLACEHOLDER } from '../../../../config';
import { replaceWithTeact } from '../../../../util/replaceWithTeact'; import { replaceWithTeact } from '../../../../util/replaceWithTeact';
import renderText from '../../../common/helpers/renderText'; import renderText from '../../../common/helpers/renderText';
import { type LangFn } from '../../../../hooks/useOldLang'; import { type OldLangFn } from '../../../../hooks/useOldLang';
import Icon from '../../../common/icons/Icon'; 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') { if (button.type === 'receipt') {
return lang('PaymentReceipt'); return lang('PaymentReceipt');
} }

View File

@ -13,10 +13,11 @@ import type {
ApiMessage, ApiPeer, ApiPoll, ApiPollAnswer, ApiMessage, ApiPeer, ApiPoll, ApiPollAnswer,
} from '../../../api/types'; } from '../../../api/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { LangFn } from '../../../hooks/useOldLang'; import type { OldLangFn } from '../../../hooks/useOldLang';
import { selectPeer } from '../../../global/selectors'; import { selectPeer } from '../../../global/selectors';
import { formatMediaDuration } from '../../../util/dates/dateFormat'; import { formatMediaDuration } from '../../../util/dates/dateFormat';
import { getMessageKey } from '../../../util/keys/messageKey';
import { getServerTime } from '../../../util/serverTime'; import { getServerTime } from '../../../util/serverTime';
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities'; import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
@ -26,7 +27,6 @@ import useOldLang from '../../../hooks/useOldLang';
import AvatarList from '../../common/AvatarList'; import AvatarList from '../../common/AvatarList';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import CheckboxGroup from '../../ui/CheckboxGroup'; import CheckboxGroup from '../../ui/CheckboxGroup';
import Notification from '../../ui/Notification';
import RadioGroup from '../../ui/RadioGroup'; import RadioGroup from '../../ui/RadioGroup';
import PollOption from './PollOption'; import PollOption from './PollOption';
@ -54,13 +54,14 @@ const Poll: FC<OwnProps> = ({
observeIntersectionForPlaying, observeIntersectionForPlaying,
onSendVote, onSendVote,
}) => { }) => {
const { loadMessage, openPollResults, requestConfetti } = getActions(); const {
loadMessage, openPollResults, requestConfetti, showNotification,
} = getActions();
const { id: messageId, chatId } = message; const { id: messageId, chatId } = message;
const { summary, results } = poll; const { summary, results } = poll;
const [isSubmitting, setIsSubmitting] = useState<boolean>(false); const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const [chosenOptions, setChosenOptions] = useState<string[]>([]); const [chosenOptions, setChosenOptions] = useState<string[]>([]);
const [isSolutionShown, setIsSolutionShown] = useState<boolean>(false);
const [wasSubmitted, setWasSubmitted] = useState<boolean>(false); const [wasSubmitted, setWasSubmitted] = useState<boolean>(false);
const [closePeriod, setClosePeriod] = useState<number>( const [closePeriod, setClosePeriod] = useState<number>(
!summary.closed && summary.closeDate && summary.closeDate > 0 !summary.closed && summary.closeDate && summary.closeDate > 0
@ -176,13 +177,13 @@ const Poll: FC<OwnProps> = ({
openPollResults({ chatId, messageId }); openPollResults({ chatId, messageId });
}); });
const handleSolutionShow = useLastCallback(() => { const showSolution = useLastCallback(() => {
setIsSolutionShown(true); showNotification({
}); localId: getMessageKey(message),
message: renderTextWithEntities({ text: poll.results.solution!, entities: poll.results.solutionEntities }),
const handleSolutionHide = useLastCallback(() => { duration: SOLUTION_DURATION,
setIsSolutionShown(false); containerSelector: SOLUTION_CONTAINER_ID,
setWasSubmitted(false); });
}); });
// Show the solution to quiz if the answer was incorrect // 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) { if (wasSubmitted && hasVoted && summary.quiz && results.results && poll.results.solution) {
const correctResult = results.results.find((r) => r.isChosen && r.isCorrect); const correctResult = results.results.find((r) => r.isChosen && r.isCorrect);
if (!correctResult) { if (!correctResult) {
setIsSolutionShown(true); showSolution();
} }
} }
}, [hasVoted, wasSubmitted, results.results, summary.quiz, poll.results.solution]); }, [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 ( return (
<div className="Poll" dir={lang.isRtl ? 'auto' : 'ltr'}> <div className="Poll" dir={lang.isRtl ? 'auto' : 'ltr'}>
{renderSolution()}
<div className="poll-question"> <div className="poll-question">
{renderTextWithEntities({ {renderTextWithEntities({
text: summary.question.text, text: summary.question.text,
@ -274,8 +261,7 @@ const Poll: FC<OwnProps> = ({
size="tiny" size="tiny"
color="translucent" color="translucent"
className="poll-quiz-help" className="poll-quiz-help"
disabled={isSolutionShown} onClick={showSolution}
onClick={handleSolutionShow}
ariaLabel="Show Solution" ariaLabel="Show Solution"
> >
<i className="icon icon-lamp" /> <i className="icon icon-lamp" />
@ -353,7 +339,7 @@ function getPollTypeString(summary: ApiPoll['summary']) {
return summary.isPublic ? 'PublicPoll' : 'AnonymousPoll'; 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) { if (!count) {
return lang(isQuiz ? 'Chat.Quiz.TotalVotesEmpty' : 'Chat.Poll.TotalVotesResultEmpty'); return lang(isQuiz ? 'Chat.Quiz.TotalVotesEmpty' : 'Chat.Poll.TotalVotesResultEmpty');
} }

View File

@ -4,7 +4,7 @@ import { getActions } from '../../../../global';
import type { import type {
ApiMessage, ApiPeer, ApiStory, ApiTopic, ApiUser, ApiMessage, ApiPeer, ApiStory, ApiTopic, ApiUser,
} from '../../../../api/types'; } from '../../../../api/types';
import type { LangFn } from '../../../../hooks/useOldLang'; import type { OldLangFn } from '../../../../hooks/useOldLang';
import type { IAlbum, ThreadId } from '../../../../types'; import type { IAlbum, ThreadId } from '../../../../types';
import { MAIN_THREAD_ID } from '../../../../api/types'; import { MAIN_THREAD_ID } from '../../../../api/types';
import { MediaViewerOrigin } from '../../../../types'; import { MediaViewerOrigin } from '../../../../types';
@ -33,7 +33,7 @@ export default function useInnerHandlers({
isRepliesChat, isRepliesChat,
isSavedMessages, isSavedMessages,
}: { }: {
lang: LangFn; lang: OldLangFn;
selectMessage: (e: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => void; selectMessage: (e: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => void;
message: ApiMessage; message: ApiMessage;
chatId: string; chatId: string;

View File

@ -7,17 +7,15 @@ import { getActions, withGlobal } from '../../../global';
import type { ApiMessage, ApiUser } from '../../../api/types'; import type { ApiMessage, ApiUser } from '../../../api/types';
import type { GiftOption } from './GiftModal'; 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 { getUserFullName } from '../../../global/helpers';
import { selectTabState, selectTheme, selectUser } from '../../../global/selectors'; import { selectTabState, selectTheme, selectUser } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { formatCurrency } from '../../../util/formatCurrency'; import { formatCurrency } from '../../../util/formatCurrency';
import { formatInteger } from '../../../util/textFormat';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import Icon from '../../common/icons/Icon';
import PremiumProgress from '../../common/PremiumProgress'; import PremiumProgress from '../../common/PremiumProgress';
import ActionMessage from '../../middle/ActionMessage'; import ActionMessage from '../../middle/ActionMessage';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
@ -175,16 +173,9 @@ function GiftComposer({
function renderFooter() { function renderFooter() {
const userFullName = getUserFullName(user)!; const userFullName = getUserFullName(user)!;
const amount = isStarGift ? ( const amount = isStarGift
lang('StarsAmount', { ? formatCurrency(gift.stars, STARS_CURRENCY_CODE, lang.code, { iconClassName: 'star-amount-icon' })
amount: formatInteger(gift.stars), : formatCurrency(gift.amount, gift.currency);
}, {
withNodes: true,
specialReplacement: {
[STARS_ICON_PLACEHOLDER]: <Icon className="star-amount-icon" name="star" />,
},
})
) : formatCurrency(gift.amount, gift.currency);
return ( return (
<div className={styles.footer}> <div className={styles.footer}>
@ -201,9 +192,9 @@ function GiftComposer({
isPrimary isPrimary
progress={gift.availabilityRemains / gift.availabilityTotal!} progress={gift.availabilityRemains / gift.availabilityTotal!}
rightText={lang('GiftSoldCount', { 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} className={styles.limited}
/> />
)} )}

View File

@ -52,7 +52,9 @@ function GiftItemPremium({
: undefined; : undefined;
function renderMonths() { 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 ( return (
<div className={styles.monthsDescription}> <div className={styles.monthsDescription}>
{caption} {caption}

View File

@ -4,11 +4,11 @@ import { getActions, withGlobal } from '../../../../global';
import type { ApiSticker, ApiUser } from '../../../../api/types'; import type { ApiSticker, ApiUser } from '../../../../api/types';
import type { TabState } from '../../../../global/types'; import type { TabState } from '../../../../global/types';
import { STARS_ICON_PLACEHOLDER } from '../../../../config';
import { getUserFullName } from '../../../../global/helpers'; import { getUserFullName } from '../../../../global/helpers';
import { selectStarGiftSticker, selectUser } from '../../../../global/selectors'; import { selectStarGiftSticker, selectUser } from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName'; import buildClassName from '../../../../util/buildClassName';
import { formatDateTimeToString } from '../../../../util/dates/dateFormat'; import { formatDateTimeToString } from '../../../../util/dates/dateFormat';
import { formatStarsAsIcon, formatStarsAsText } from '../../../../util/localization/format';
import { CUSTOM_PEER_HIDDEN } from '../../../../util/objects/customPeer'; import { CUSTOM_PEER_HIDDEN } from '../../../../util/objects/customPeer';
import { formatInteger } from '../../../../util/textFormat'; import { formatInteger } from '../../../../util/textFormat';
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities'; import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
@ -69,6 +69,7 @@ const GiftInfoModal = ({
const canConvertDifference = (userGift && starGiftMaxConvertPeriod && ( const canConvertDifference = (userGift && starGiftMaxConvertPeriod && (
userGift.date + starGiftMaxConvertPeriod - Date.now() / 1000 userGift.date + starGiftMaxConvertPeriod - Date.now() / 1000
)) || 0; )) || 0;
const conversionLeft = Math.ceil(canConvertDifference / 60 / 60 / 24);
const handleClose = useLastCallback(() => { const handleClose = useLastCallback(() => {
closeGiftInfoModal(); closeGiftInfoModal();
@ -131,17 +132,19 @@ const GiftInfoModal = ({
return canUpdate return canUpdate
? lang('GiftInfoDescription', { ? lang('GiftInfoDescription', {
amount: formatInteger(starsToConvert!), amount: starsToConvert,
}, { }, {
withNodes: true, withNodes: true,
withMarkdown: true, withMarkdown: true,
pluralValue: starsToConvert,
}) })
: lang('GiftInfoDescriptionOut', { : lang('GiftInfoDescriptionOut', {
amount: formatInteger(starsToConvert!), amount: starsToConvert,
user: getUserFullName(targetUser)!, user: getUserFullName(targetUser)!,
}, { }, {
withNodes: true, withNodes: true,
withMarkdown: true, withMarkdown: true,
pluralValue: starsToConvert,
}); });
})(); })();
@ -205,14 +208,7 @@ const GiftInfoModal = ({
tableData.push([ tableData.push([
lang('GiftInfoValue'), lang('GiftInfoValue'),
<div className={styles.giftValue}> <div className={styles.giftValue}>
{lang('StarsAmount', { {formatStarsAsIcon(lang, gift.stars)}
amount: formatInteger(gift.stars),
}, {
withNodes: true,
specialReplacement: {
[STARS_ICON_PLACEHOLDER]: <StarIcon type="gold" size="small" />,
},
})}
{canUpdate && canConvertDifference > 0 && Boolean(starsToConvert) && ( {canUpdate && canConvertDifference > 0 && Boolean(starsToConvert) && (
<BadgeButton onClick={openConvertConfirm}> <BadgeButton onClick={openConvertConfirm}>
{lang('GiftInfoConvert', { amount: starsToConvert }, { pluralValue: starsToConvert })} {lang('GiftInfoConvert', { amount: starsToConvert }, { pluralValue: starsToConvert })}
@ -225,8 +221,10 @@ const GiftInfoModal = ({
tableData.push([ tableData.push([
lang('GiftInfoAvailability'), lang('GiftInfoAvailability'),
lang('GiftInfoAvailabilityValue', { lang('GiftInfoAvailabilityValue', {
count: formatInteger(gift.availabilityRemains!), count: gift.availabilityRemains || 0,
total: formatInteger(gift.availabilityTotal), total: gift.availabilityTotal,
}, {
pluralValue: gift.availabilityRemains || 0,
}), }),
]); ]);
} }
@ -295,7 +293,7 @@ const GiftInfoModal = ({
> >
<div> <div>
{lang('GiftInfoConvertDescription1', { {lang('GiftInfoConvertDescription1', {
amount: lang('StarsAmountText', { amount: formatInteger(userGift.starsToConvert!) }), amount: formatStarsAsText(lang, userGift.starsToConvert!),
user: getUserFullName(userFrom)!, user: getUserFullName(userFrom)!,
}, { }, {
withNodes: true, withNodes: true,
@ -305,10 +303,11 @@ const GiftInfoModal = ({
{canConvertDifference > 0 && ( {canConvertDifference > 0 && (
<div> <div>
{lang('GiftInfoConvertDescriptionPeriod', { {lang('GiftInfoConvertDescriptionPeriod', {
count: formatInteger(Math.ceil(canConvertDifference / 60 / 60 / 24)), count: conversionLeft,
}, { }, {
withNodes: true, withNodes: true,
withMarkdown: true, withMarkdown: true,
pluralValue: conversionLeft,
})} })}
</div> </div>
)} )}

View File

@ -94,11 +94,11 @@ const StarPaymentModal = ({
if (subscriptionInfo) { if (subscriptionInfo) {
return lang('StarsSubscribeText', { return lang('StarsSubscribeText', {
chat: subscriptionInfo.title, chat: subscriptionInfo.title,
amount: amount!, amount,
}, { }, {
withNodes: true, withNodes: true,
withMarkdown: true, withMarkdown: true,
pluralValue: amount, pluralValue: amount!,
}); });
} }

View File

@ -1,9 +1,9 @@
import type { ApiStarsTransaction } from '../../../../api/types'; import type { ApiStarsTransaction } from '../../../../api/types';
import type { LangFn } from '../../../../hooks/useOldLang'; import type { OldLangFn } from '../../../../hooks/useOldLang';
import { buildStarsTransactionCustomPeer } from '../../../../global/helpers/payments'; 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.extendedMedia) return lang('StarMediaPurchase');
if (transaction.subscriptionPeriod) return lang('StarSubscriptionPurchase'); if (transaction.subscriptionPeriod) return lang('StarSubscriptionPurchase');
if (transaction.isReaction) return lang('StarsReactionsSent'); if (transaction.isReaction) return lang('StarsReactionsSent');

View File

@ -73,6 +73,9 @@ const MinimizedWebAppModal = ({
{ {
botName: activeTabName, botName: activeTabName,
count: openedTabsCount - 1, count: openedTabsCount - 1,
},
{
pluralValue: openedTabsCount - 1,
})}` })}`
: activeTabName; : activeTabName;

View File

@ -182,10 +182,6 @@ const useWebAppFrame = (
data: null, data: null,
}, },
}); });
// showNotification({
// message: 'Clipboard access is not supported in this client yet',
// });
} }
if (eventType === 'web_app_open_scan_qr_popup') { if (eventType === 'web_app_open_scan_qr_popup') {

View File

@ -9,7 +9,6 @@ import type {
ApiWebDocument, ApiWebDocument,
} from '../../api/types'; } from '../../api/types';
import type { FormEditDispatch } from '../../hooks/reducers/usePaymentReducer'; import type { FormEditDispatch } from '../../hooks/reducers/usePaymentReducer';
import type { LangCode } from '../../types';
import type { IconName } from '../../types/icons'; import type { IconName } from '../../types/icons';
import { PaymentStep } from '../../types'; import { PaymentStep } from '../../types';
@ -238,7 +237,7 @@ const Checkout: FC<OwnProps> = ({
export default memo(Checkout); export default memo(Checkout);
function renderPaymentItem( 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 ( return (
<div className={buildClassName(styles.priceInfoItem, main && styles.priceInfoItemMain)}> <div className={buildClassName(styles.priceInfoItem, main && styles.priceInfoItemMain)}>

View File

@ -3,7 +3,7 @@ import React, { memo, useCallback } from '../../../lib/teact/teact';
import { getActions } from '../../../global'; import { getActions } from '../../../global';
import type { ApiMessage, StatisticsMessageInteractionCounter } from '../../../api/types'; import type { ApiMessage, StatisticsMessageInteractionCounter } from '../../../api/types';
import type { LangFn } from '../../../hooks/useOldLang'; import type { OldLangFn } from '../../../hooks/useOldLang';
import { import {
getMessageMediaHash, 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) { if (!blobUrl) {
return renderMessageSummary(lang, message); return renderMessageSummary(lang, message);
} }

View File

@ -6,7 +6,7 @@ import type {
ApiTypeStory, ApiTypeStory,
StatisticsStoryInteractionCounter, StatisticsStoryInteractionCounter,
} from '../../../api/types'; } from '../../../api/types';
import type { LangFn } from '../../../hooks/useOldLang'; import type { OldLangFn } from '../../../hooks/useOldLang';
import { getStoryMediaHash } from '../../../global/helpers'; import { getStoryMediaHash } from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName'; 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 ( return (
<span> <span>
{blobUrl ? ( {blobUrl ? (

View File

@ -9,6 +9,9 @@ const storedParameter: LangFnParameters = {
variables: { variables: {
count: 42, count: 42,
}, },
options: {
pluralValue: 42,
},
}; };
const storedAdvancedParameter: LangFnParameters = { const storedAdvancedParameter: LangFnParameters = {
@ -39,7 +42,7 @@ const TestLocale = () => {
withMarkdown: true, withMarkdown: true,
})} })}
</p> </p>
<p>{lang('Participants', { count: 42 })}</p> <p>{lang('Participants', { count: 42 }, { pluralValue: 42 })}</p>
<p> <p>
{lang('ChatServiceGroupUpdatedPinnedMessage1', { {lang('ChatServiceGroupUpdatedPinnedMessage1', {
message: 'Some message', message: 'Some message',

View File

@ -1,19 +1,21 @@
import type { FC, TeactNode } from '../../lib/teact/teact'; import type { FC } from '../../lib/teact/teact';
import React, { import React, {
useCallback,
useEffect, useEffect,
useMemo,
useRef, useRef,
useState, useState,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions } from '../../global'; import { getActions } from '../../global';
import type { CallbackAction } from '../../global/types'; import type { ApiNotification } from '../../api/types';
import type { IconName } from '../../types/icons'; import { isLangFnParam } from '../../util/localization/types';
import { ANIMATION_END_DELAY } from '../../config'; import { ANIMATION_END_DELAY } from '../../config';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import captureEscKeyListener from '../../util/captureEscKeyListener'; import captureEscKeyListener from '../../util/captureEscKeyListener';
import renderText from '../common/helpers/renderText';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated'; import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated';
@ -25,57 +27,55 @@ import RoundTimer from './RoundTimer';
import './Notification.scss'; import './Notification.scss';
type OwnProps = { type OwnProps = {
title?: TeactNode; notification: ApiNotification;
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;
}; };
const DEFAULT_DURATION = 3000; const DEFAULT_DURATION = 3000;
const ANIMATION_DURATION = 150; const ANIMATION_DURATION = 150;
const Notification: FC<OwnProps> = ({ const Notification: FC<OwnProps> = ({
title, notification,
className,
message,
duration = DEFAULT_DURATION,
containerId,
icon,
action,
actionText,
shouldDisableClickDismiss,
dismissAction,
shouldShowTimer,
cacheBreaker,
onDismiss,
}) => { }) => {
const actions = getActions(); 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); const [isOpen, setIsOpen] = useState(true);
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const timerRef = useRef<number | undefined>(null); const timerRef = useRef<number | undefined>(null);
const { transitionClassNames } = useShowTransitionDeprecated(isOpen); const { transitionClassNames } = useShowTransitionDeprecated(isOpen);
const handleDismiss = useLastCallback(() => {
actions.dismissNotification({ localId });
});
const closeAndDismiss = useLastCallback((force?: boolean) => { const closeAndDismiss = useLastCallback((force?: boolean) => {
if (!force && shouldDisableClickDismiss) return; if (!force && disableClickDismiss) return;
setIsOpen(false); setIsOpen(false);
setTimeout(onDismiss, ANIMATION_DURATION + ANIMATION_END_DELAY); setTimeout(handleDismiss, ANIMATION_DURATION + ANIMATION_END_DELAY);
if (dismissAction) { if (dismissAction) {
// @ts-ignore // @ts-ignore
actions[dismissAction.action](dismissAction.payload); actions[dismissAction.action](dismissAction.payload);
} }
}); });
const handleClick = useCallback(() => { const handleClick = useLastCallback(() => {
if (action) { if (action) {
if (Array.isArray(action)) { if (Array.isArray(action)) {
// @ts-ignore // @ts-ignore
@ -86,7 +86,7 @@ const Notification: FC<OwnProps> = ({
} }
} }
closeAndDismiss(); closeAndDismiss();
}, [action, actions, closeAndDismiss]); });
useEffect(() => (isOpen ? captureEscKeyListener(closeAndDismiss) : undefined), [isOpen, closeAndDismiss]); useEffect(() => (isOpen ? captureEscKeyListener(closeAndDismiss) : undefined), [isOpen, closeAndDismiss]);
@ -102,7 +102,7 @@ const Notification: FC<OwnProps> = ({
}, [duration, cacheBreaker]); // Reset timer if `cacheBreaker` changes }, [duration, cacheBreaker]); // Reset timer if `cacheBreaker` changes
const handleMouseEnter = useLastCallback(() => { const handleMouseEnter = useLastCallback(() => {
if (shouldDisableClickDismiss) return; if (disableClickDismiss) return;
if (timerRef.current) { if (timerRef.current) {
clearTimeout(timerRef.current); clearTimeout(timerRef.current);
timerRef.current = undefined; timerRef.current = undefined;
@ -110,15 +110,45 @@ const Notification: FC<OwnProps> = ({
}); });
const handleMouseLeave = useLastCallback(() => { const handleMouseLeave = useLastCallback(() => {
if (shouldDisableClickDismiss) return; if (disableClickDismiss) return;
if (timerRef.current) { if (timerRef.current) {
clearTimeout(timerRef.current); clearTimeout(timerRef.current);
} }
timerRef.current = window.setTimeout(closeAndDismiss, duration); 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 ( return (
<Portal className="Notification-container" containerId={containerId}> <Portal className="Notification-container" containerSelector={containerSelector}>
<div <div
className={buildClassName('Notification', transitionClassNames, className)} className={buildClassName('Notification', transitionClassNames, className)}
onClick={handleClick} onClick={handleClick}
@ -127,16 +157,18 @@ const Notification: FC<OwnProps> = ({
> >
<Icon name={icon || 'info-filled'} className="notification-icon" /> <Icon name={icon || 'info-filled'} className="notification-icon" />
<div className="content"> <div className="content">
{title && <div className="notification-title">{title}</div>} {renderedTitle && (
{message} <div className="notification-title">{renderedTitle}</div>
)}
{renderedMessage}
</div> </div>
{action && actionText && ( {action && renderedActionText && (
<Button <Button
color="translucent-white" color="translucent-white"
onClick={handleClick} onClick={handleClick}
className="notification-button" className="notification-button"
> >
{actionText} {renderedActionText}
</Button> </Button>
)} )}
{shouldShowTimer && ( {shouldShowTimer && (

View File

@ -3,19 +3,19 @@ import { useLayoutEffect, useRef } from '../../lib/teact/teact';
import TeactDOM from '../../lib/teact/teact-dom'; import TeactDOM from '../../lib/teact/teact-dom';
type OwnProps = { type OwnProps = {
containerId?: string; containerSelector?: string;
className?: string; className?: string;
children: VirtualElement; children: VirtualElement;
}; };
const Portal: FC<OwnProps> = ({ containerId, className, children }) => { const Portal: FC<OwnProps> = ({ containerSelector, className, children }) => {
const elementRef = useRef<HTMLDivElement>(); const elementRef = useRef<HTMLDivElement>();
if (!elementRef.current) { if (!elementRef.current) {
elementRef.current = document.createElement('div'); elementRef.current = document.createElement('div');
} }
useLayoutEffect(() => { useLayoutEffect(() => {
const container = document.querySelector<HTMLDivElement>(containerId || '#portals'); const container = document.querySelector<HTMLDivElement>(containerSelector || '#portals');
if (!container) { if (!container) {
return undefined; return undefined;
} }
@ -31,7 +31,7 @@ const Portal: FC<OwnProps> = ({ containerId, className, children }) => {
TeactDOM.render(undefined, element); TeactDOM.render(undefined, element);
container.removeChild(element); container.removeChild(element);
}; };
}, [className, containerId]); }, [className, containerSelector]);
return TeactDOM.render(children, elementRef.current); return TeactDOM.render(children, elementRef.current);
}; };

View File

@ -294,6 +294,7 @@ export const PURCHASE_USERNAME = 'auction';
export const ACCEPTABLE_USERNAME_ERRORS = new Set([USERNAME_PURCHASE_ERROR, 'USERNAME_INVALID']); 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 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 WEB_APP_PLATFORM = 'weba';
export const LANG_PACK = 'weba';
// eslint-disable-next-line max-len // 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']); 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 MAX_ACTIVE_PINNED_CHATS = 5;
export const SCHEDULED_WHEN_ONLINE = 0x7FFFFFFE; export const SCHEDULED_WHEN_ONLINE = 0x7FFFFFFE;
export const DEFAULT_LANG_CODE = 'en'; 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 LANG_PACKS = ['android', 'ios', 'tdesktop', 'macos'] as const;
export const FEEDBACK_URL = 'https://bugs.telegram.org/?tag_ids=41&sort=time'; export const FEEDBACK_URL = 'https://bugs.telegram.org/?tag_ids=41&sort=time';
export const FAQ_URL = 'https://telegram.org/faq'; export const FAQ_URL = 'https://telegram.org/faq';

View File

@ -373,7 +373,13 @@ addActionHandler('loadLanguages', async (global): Promise<void> => {
} }
global = getGlobal(); global = getGlobal();
global = replaceSettings(global, { languages: result }); global = {
...global,
settings: {
...global.settings,
languages: result,
},
};
setGlobal(global); setGlobal(global);
}); });

View File

@ -6,6 +6,7 @@ import type {
ApiUpdateServerTimeOffset, ApiUpdateServerTimeOffset,
ApiUpdateSession, ApiUpdateSession,
} from '../../../api/types'; } from '../../../api/types';
import type { LangCode } from '../../../types';
import type { RequiredGlobalActions } from '../../index'; import type { RequiredGlobalActions } from '../../index';
import type { ActionReturnType, GlobalState } from '../../types'; import type { ActionReturnType, GlobalState } from '../../types';
@ -97,7 +98,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
}); });
function onUpdateApiReady<T extends GlobalState>(global: T) { 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) { function onUpdateAuthorizationState<T extends GlobalState>(global: T, update: ApiUpdateAuthorizationState) {

View File

@ -1,6 +1,7 @@
import type { ActionReturnType } from '../../types'; import type { ActionReturnType } from '../../types';
import { PaymentStep } from '../../../types'; import { PaymentStep } from '../../../types';
import { applyLangPackDifference, requestLangPackDifference } from '../../../util/localization';
import { addActionHandler, setGlobal } from '../../index'; import { addActionHandler, setGlobal } from '../../index';
import { import {
addBlockedUser, addBlockedUser,
@ -194,6 +195,15 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
setGlobal(global); setGlobal(global);
break; break;
} }
case 'updateLangPackTooLong': {
requestLangPackDifference(update.langCode);
break;
}
case 'updateLangPack': {
applyLangPackDifference(update.version, update.strings, update.keysToRemove);
}
} }
return undefined; return undefined;

View File

@ -199,7 +199,9 @@ addActionHandler('createGroupCallInviteLink', async (global, actions, payload):
copyTextToClipboard(inviteLink); copyTextToClipboard(inviteLink);
actions.showNotification({ actions.showNotification({
message: 'Link copied to clipboard', message: {
key: 'LinkCopied',
},
tabId, tabId,
}); });
}); });

View File

@ -1,5 +1,6 @@
import { addCallback } from '../../../lib/teact/teactn'; import { addCallback } from '../../../lib/teact/teactn';
import type { LangCode } from '../../../types';
import type { ActionReturnType, GlobalState } from '../../types'; import type { ActionReturnType, GlobalState } from '../../types';
import { requestMutation } from '../../../lib/fasterdom/fasterdom'; import { requestMutation } from '../../../lib/fasterdom/fasterdom';
@ -134,7 +135,7 @@ addCallback((global: GlobalState) => {
const performanceType = selectPerformanceSettings(global); const performanceType = selectPerformanceSettings(global);
void oldSetLanguage(language, undefined, true); void oldSetLanguage(language as LangCode, undefined, true);
requestMutation(() => { requestMutation(() => {
document.documentElement.style.setProperty( document.documentElement.style.setProperty(

View File

@ -1,7 +1,7 @@
import { addCallback } from '../../../lib/teact/teactn'; import { addCallback } from '../../../lib/teact/teactn';
import type { ActionReturnType, GlobalState } from '../../types'; import type { ActionReturnType, GlobalState } from '../../types';
import { SettingsScreens } from '../../../types'; import { type LangCode, SettingsScreens } from '../../../types';
import { requestMutation } from '../../../lib/fasterdom/fasterdom'; import { requestMutation } from '../../../lib/fasterdom/fasterdom';
import { disableDebugConsole, initDebugConsole } from '../../../util/debugConsole'; import { disableDebugConsole, initDebugConsole } from '../../../util/debugConsole';
@ -53,7 +53,7 @@ addCallback((global: GlobalState) => {
} }
if (settings.language !== prevSettings.language) { if (settings.language !== prevSettings.language) {
oldSetLanguage(settings.language); oldSetLanguage(settings.language as LangCode);
} }
if (settings.timeFormat !== prevSettings.timeFormat) { if (settings.timeFormat !== prevSettings.timeFormat) {

View File

@ -9,7 +9,7 @@ import type {
ApiTopic, ApiTopic,
ApiUser, ApiUser,
} from '../../api/types'; } from '../../api/types';
import type { LangFn } from '../../hooks/useOldLang'; import type { OldLangFn } from '../../hooks/useOldLang';
import type { import type {
CustomPeer, NotifyException, NotifySettings, ThreadId, CustomPeer, NotifyException, NotifySettings, ThreadId,
} from '../../types'; } from '../../types';
@ -101,7 +101,7 @@ export function getPrivateChatUserId(chat: ApiChat) {
return chat.id; return chat.id;
} }
export function getChatTitle(lang: LangFn, chat: ApiChat, isSelf = false) { export function getChatTitle(lang: OldLangFn, chat: ApiChat, isSelf = false) {
if (isSelf) { if (isSelf) {
return lang('SavedMessages'); return lang('SavedMessages');
} }
@ -247,7 +247,7 @@ export function getAllowedAttachmentOptions(
} }
export function getMessageSendingRestrictionReason( export function getMessageSendingRestrictionReason(
lang: LangFn, lang: OldLangFn,
currentUserBannedRights?: ApiChatBannedRights, currentUserBannedRights?: ApiChatBannedRights,
defaultBannedRights?: ApiChatBannedRights, defaultBannedRights?: ApiChatBannedRights,
) { ) {
@ -272,7 +272,7 @@ export function getMessageSendingRestrictionReason(
} }
export function getForumComposerPlaceholder( export function getForumComposerPlaceholder(
lang: LangFn, lang: OldLangFn,
chat?: ApiChat, chat?: ApiChat,
threadId: ThreadId = MAIN_THREAD_ID, threadId: ThreadId = MAIN_THREAD_ID,
topics?: Record<number, ApiTopic>, topics?: Record<number, ApiTopic>,
@ -341,7 +341,7 @@ export function getCanDeleteChat(chat: ApiChat) {
return isChatBasicGroup(chat) || ((isChatSuperGroup(chat) || isChatChannel(chat)) && chat.isCreator); 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 { const {
excludedChatIds, includedChatIds, excludedChatIds, includedChatIds,
bots, groups, contacts, nonContacts, channels, 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)) { if (!sender || isUserId(chatId)) {
return undefined; return undefined;
} }
@ -395,7 +395,7 @@ export function getMessageSenderName(lang: LangFn, chatId: string, sender?: ApiP
} }
export function filterChatsByName( export function filterChatsByName(
lang: LangFn, lang: OldLangFn,
chatIds: string[], chatIds: string[],
chatsById: Record<string, ApiChat>, chatsById: Record<string, ApiChat>,
query?: string, query?: string,
@ -475,7 +475,7 @@ export function getIsSavedDialog(chatId: string, threadId: ThreadId | undefined,
return chatId === currentUserId && threadId !== MAIN_THREAD_ID; 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 chatTypeString = lang(getChatTypeString(chat));
const { membersCount } = chat; const { membersCount } = chat;

View File

@ -3,7 +3,7 @@ import type { TeactNode } from '../../lib/teact/teact';
import type { import type {
ApiMediaExtendedPreview, ApiMessage, MediaContent, StatefulMediaContent, ApiMediaExtendedPreview, ApiMessage, MediaContent, StatefulMediaContent,
} from '../../api/types'; } from '../../api/types';
import type { LangFn } from '../../hooks/useOldLang'; import type { OldLangFn } from '../../hooks/useOldLang';
import { ApiMessageEntityTypes } from '../../api/types'; import { ApiMessageEntityTypes } from '../../api/types';
import { CONTENT_NOT_SUPPORTED } from '../../config'; import { CONTENT_NOT_SUPPORTED } from '../../config';
@ -17,7 +17,7 @@ const SPOILER_CHARS = ['⠺', '⠵', '⠞', '⠟'];
export const TRUNCATED_SUMMARY_LENGTH = 80; export const TRUNCATED_SUMMARY_LENGTH = 80;
export function getMessageSummaryText( export function getMessageSummaryText(
lang: LangFn, lang: OldLangFn,
message: ApiMessage, message: ApiMessage,
statefulContent: StatefulMediaContent | undefined, statefulContent: StatefulMediaContent | undefined,
noEmoji = false, noEmoji = false,
@ -106,12 +106,12 @@ export function getMessageSummaryEmoji(message: ApiMessage) {
} }
export function getMediaContentTypeDescription( export function getMediaContentTypeDescription(
lang: LangFn, content: MediaContent, statefulContent: StatefulMediaContent | undefined, lang: OldLangFn, content: MediaContent, statefulContent: StatefulMediaContent | undefined,
) { ) {
return getSummaryDescription(lang, content, statefulContent); return getSummaryDescription(lang, content, statefulContent);
} }
export function getMessageSummaryDescription( export function getMessageSummaryDescription(
lang: LangFn, lang: OldLangFn,
message: ApiMessage, message: ApiMessage,
statefulContent: StatefulMediaContent | undefined, statefulContent: StatefulMediaContent | undefined,
truncatedText?: string | TeactNode, truncatedText?: string | TeactNode,
@ -120,7 +120,7 @@ export function getMessageSummaryDescription(
return getSummaryDescription(lang, message.content, statefulContent, message, truncatedText, isExtended); return getSummaryDescription(lang, message.content, statefulContent, message, truncatedText, isExtended);
} }
function getSummaryDescription( function getSummaryDescription(
lang: LangFn, lang: OldLangFn,
mediaContent: MediaContent, mediaContent: MediaContent,
statefulContent: StatefulMediaContent | undefined, statefulContent: StatefulMediaContent | undefined,
message?: ApiMessage, message?: ApiMessage,

View File

@ -9,7 +9,7 @@ import type {
import type { import type {
ApiPoll, MediaContainer, MediaContent, StatefulMediaContent, ApiPoll, MediaContainer, MediaContent, StatefulMediaContent,
} from '../../api/types/messages'; } from '../../api/types/messages';
import type { LangFn } from '../../hooks/useOldLang'; import type { OldLangFn } from '../../hooks/useOldLang';
import type { ThreadId } from '../../types'; import type { ThreadId } from '../../types';
import type { GlobalState } from '../types'; import type { GlobalState } from '../types';
import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/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); 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); return isPeerUser(sender) ? getUserFullName(sender) : getChatTitle(lang, sender);
} }
@ -344,10 +344,10 @@ export function extractMessageText(message: ApiMessage | ApiStory, inChatList =
return { text, entities }; 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); 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; const { isExpiredVoice, isExpiredRoundVideo } = mediaContent;
if (isExpiredVoice) { if (isExpiredVoice) {
return langFn('Message.VoiceMessageExpired'); return langFn('Message.VoiceMessageExpired');

View File

@ -1,5 +1,5 @@
import type { ApiMessage } from '../../api/types'; 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 { renderMessageText } from '../../components/common/helpers/renderMessageText';
import { getGlobal } from '..'; import { getGlobal } from '..';
@ -7,7 +7,7 @@ import { getMessageStatefulContent } from './messages';
import { getMessageSummaryDescription, getMessageSummaryEmoji } from './messageSummary'; import { getMessageSummaryDescription, getMessageSummaryEmoji } from './messageSummary';
export function renderMessageSummaryHtml( export function renderMessageSummaryHtml(
lang: LangFn, lang: OldLangFn,
message: ApiMessage, message: ApiMessage,
) { ) {
const global = getGlobal(); const global = getGlobal();

View File

@ -1,5 +1,5 @@
import type { ApiPeer, ApiUser, ApiUserStatus } from '../../api/types'; 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 { ANONYMOUS_USER_ID, SERVICE_NOTIFICATIONS_USER_ID } from '../../config';
import { formatFullDate, formatTime } from '../../util/dates/dateFormat'; import { formatFullDate, formatTime } from '../../util/dates/dateFormat';
@ -66,7 +66,7 @@ export function getUserFullName(user?: ApiUser) {
} }
export function getUserStatus( export function getUserStatus(
lang: LangFn, user: ApiUser, userStatus: ApiUserStatus | undefined, lang: OldLangFn, user: ApiUser, userStatus: ApiUserStatus | undefined,
) { ) {
if (user.id === SERVICE_NOTIFICATIONS_USER_ID) { if (user.id === SERVICE_NOTIFICATIONS_USER_ID) {
return lang('ServiceNotifications'); return lang('ServiceNotifications');

View File

@ -36,6 +36,7 @@ import type {
ApiInputInvoiceStarGift, ApiInputInvoiceStarGift,
ApiInputMessageReplyInfo, ApiInputMessageReplyInfo,
ApiKeyboardButton, ApiKeyboardButton,
ApiLanguage,
ApiMediaFormat, ApiMediaFormat,
ApiMessage, ApiMessage,
ApiMessageEntity, ApiMessageEntity,
@ -115,7 +116,6 @@ import type {
InlineBotSettings, InlineBotSettings,
ISettings, ISettings,
IThemeSettings, IThemeSettings,
LangCode,
LoadMoreDirection, LoadMoreDirection,
ManagementProgress, ManagementProgress,
ManagementScreens, ManagementScreens,
@ -1199,7 +1199,7 @@ export type GlobalState = {
defaultTopicIconsId?: string; defaultTopicIconsId?: string;
defaultStatusIconsId?: string; defaultStatusIconsId?: string;
premiumGifts?: ApiStickerSet; premiumGifts?: ApiStickerSet;
emojiKeywords: Partial<Record<LangCode, EmojiKeywords>>; emojiKeywords: Record<string, EmojiKeywords | undefined>;
gifs: { gifs: {
saved: { saved: {
@ -1243,6 +1243,7 @@ export type GlobalState = {
notifyExceptions?: Record<number, NotifyException>; notifyExceptions?: Record<number, NotifyException>;
lastPremiumBandwithNotificationDate?: number; lastPremiumBandwithNotificationDate?: number;
paidReactionPrivacy?: boolean; paidReactionPrivacy?: boolean;
languages?: ApiLanguage[];
}; };
push?: { push?: {
@ -1377,7 +1378,7 @@ export interface ActionPayloads {
faveSticker: { sticker: ApiSticker } & WithTabId; faveSticker: { sticker: ApiSticker } & WithTabId;
unfaveSticker: { sticker: ApiSticker }; unfaveSticker: { sticker: ApiSticker };
toggleStickerSet: { stickerSetId: string }; toggleStickerSet: { stickerSetId: string };
loadEmojiKeywords: { language: LangCode }; loadEmojiKeywords: { language: string };
// groups // groups
togglePreHistoryHidden: { togglePreHistoryHidden: {
@ -1484,7 +1485,7 @@ export interface ActionPayloads {
updateContentSettings: boolean; updateContentSettings: boolean;
loadCountryList: { loadCountryList: {
langCode?: LangCode; langCode?: string;
}; };
ensureTimeFormat: WithTabId | undefined; ensureTimeFormat: WithTabId | undefined;

View File

@ -1,5 +1,5 @@
import type { GlobalState } from '../global/types'; import type { GlobalState } from '../global/types';
import type { LangFn } from './useOldLang'; import type { OldLangFn } from './useOldLang';
import useBrowserOnline from './window/useBrowserOnline'; import useBrowserOnline from './window/useBrowserOnline';
@ -16,7 +16,7 @@ type ConnectionStatusPosition =
| 'none'; | 'none';
export default function useConnectionStatus( export default function useConnectionStatus(
lang: LangFn, lang: OldLangFn,
connectionState: GlobalState['connectionState'], connectionState: GlobalState['connectionState'],
isSyncing: boolean | undefined, isSyncing: boolean | undefined,
hasMiddleHeader: boolean, hasMiddleHeader: boolean,

View File

@ -2,11 +2,11 @@ import * as langProvider from '../util/oldLangProvider';
import useEffectOnce from './useEffectOnce'; import useEffectOnce from './useEffectOnce';
import useForceUpdate from './useForceUpdate'; import useForceUpdate from './useForceUpdate';
export type LangFn = langProvider.LangFn; export type OldLangFn = langProvider.LangFn;
/** /**
* @deprecated * @deprecated
*/ */
const useOldLang = (): LangFn => { const useOldLang = (): OldLangFn => {
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
useEffectOnce(() => { useEffectOnce(() => {

View File

@ -73,6 +73,7 @@ class TelegramClient {
systemVersion: undefined, systemVersion: undefined,
appVersion: undefined, appVersion: undefined,
langCode: 'en', langCode: 'en',
langPack: 'weba',
systemLangCode: 'en', systemLangCode: 'en',
baseLogger: 'gramjs', baseLogger: 'gramjs',
useWSS: false, useWSS: false,
@ -158,7 +159,7 @@ class TelegramClient {
.toString() || '1.0', .toString() || '1.0',
appVersion: args.appVersion || '1.0', appVersion: args.appVersion || '1.0',
langCode: args.langCode, langCode: args.langCode,
langPack: 'weba', langPack: args.langPack,
systemLangCode: args.systemLangCode, systemLangCode: args.systemLangCode,
query: x, query: x,
proxy: undefined, // no proxies yet. proxy: undefined, // no proxies yet.

View File

@ -11,7 +11,6 @@ import type {
ApiExportedInvite, ApiExportedInvite,
ApiFakeType, ApiFakeType,
ApiLabeledPrice, ApiLabeledPrice,
ApiLanguage,
ApiMessage, ApiMessage,
ApiPhoto, ApiPhoto,
ApiReaction, ApiReaction,
@ -113,8 +112,7 @@ export interface ISettings extends NotifySettings, Record<string, any> {
shouldSuggestCustomEmoji: boolean; shouldSuggestCustomEmoji: boolean;
shouldUpdateStickerSetOrder: boolean; shouldUpdateStickerSetOrder: boolean;
hasPassword?: boolean; hasPassword?: boolean;
languages?: ApiLanguage[]; language: string;
language: LangCode;
isSensitiveEnabled?: boolean; isSensitiveEnabled?: boolean;
canChangeSensitive?: boolean; canChangeSensitive?: boolean;
timeFormat: TimeFormat; timeFormat: TimeFormat;
@ -481,8 +479,8 @@ export type NotifyException = {
export type EmojiKeywords = { export type EmojiKeywords = {
isLoading?: boolean; isLoading?: boolean;
version: number; version?: number;
keywords: Record<string, string[]>; keywords?: Record<string, string[]>;
}; };
export type InlineBotSettings = { export type InlineBotSettings = {

1123
src/types/language.d.ts vendored

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
import type { LangFn } from '../../hooks/useOldLang'; import type { OldLangFn } from '../../hooks/useOldLang';
import type { TimeFormat } from '../../types'; import type { TimeFormat } from '../../types';
import withCache from '../withCache'; import withCache from '../withCache';
@ -39,7 +39,7 @@ function toIsoString(date: Date) {
} }
// @optimization `toLocaleTimeString` is avoided because of bad performance // @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 date = typeof datetime === 'number' ? new Date(datetime) : datetime;
const timeFormat = lang.timeFormat || '24h'; 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}`; 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 date = typeof datetime === 'number' ? new Date(datetime) : datetime;
const time = formatTime(lang, date); 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; 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'); 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); return formatDateToString(date, lang.code, false, isShort ? 'short' : 'long', true);
} }
export function formatCountdown( export function formatCountdown(
lang: LangFn, lang: OldLangFn,
msLeft: number, msLeft: number,
) { ) {
const days = Math.floor(msLeft / MILLISECONDS_IN_DAY); 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) { if (msLeft < 60 * 1000) {
return Math.ceil(msLeft / 1000).toString(); return Math.ceil(msLeft / 1000).toString();
} else if (msLeft < 60 * 60 * 1000) { } 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; const seconds = currentTime - lastUpdated;
if (seconds < 60) { if (seconds < 60) {
return lang('LiveLocationUpdated.JustNow'); 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; const seconds = currentTime - lastUpdated;
if (seconds < 60) { if (seconds < 60) {
@ -156,7 +156,7 @@ export function formatRelativeTime(lang: LangFn, currentTime: number, lastUpdate
type DurationType = 'Seconds' | 'Minutes' | 'Hours' | 'Days' | 'Weeks'; 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) { if (!duration) {
return undefined; return undefined;
} }
@ -196,7 +196,7 @@ export function formatTimeDuration(lang: LangFn, duration: number, showLast = 2)
} }
export function formatHumanDate( export function formatHumanDate(
lang: LangFn, lang: OldLangFn,
datetime: number | Date, datetime: number | Date,
isShort = false, isShort = false,
noWeekdays = false, noWeekdays = false,
@ -237,13 +237,13 @@ export function formatHumanDate(
* Returns weekday name * Returns weekday name
* @param day 0 - Sunday, 1 - Monday, ... * @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]; const weekDay = WEEKDAYS_FULL[day];
return isShort ? lang(`Weekday.Short${weekDay}`) : lang(`Weekday.${weekDay}`); return isShort ? lang(`Weekday.Short${weekDay}`) : lang(`Weekday.${weekDay}`);
} }
export function formatMediaDateTime( export function formatMediaDateTime(
lang: LangFn, lang: OldLangFn,
datetime: number | Date, datetime: number | Date,
isUpperFirst?: boolean, isUpperFirst?: boolean,
) { ) {
@ -350,7 +350,7 @@ export function formatDateTimeToString(
} }
export function formatDateAtTime( export function formatDateAtTime(
lang: LangFn, lang: OldLangFn,
datetime: number | Date, datetime: number | Date,
) { ) {
const date = typeof datetime === 'number' ? new Date(datetime) : datetime; const date = typeof datetime === 'number' ? new Date(datetime) : datetime;
@ -375,7 +375,7 @@ export function formatDateAtTime(
} }
export function formatDateInFuture( export function formatDateInFuture(
lang: LangFn, lang: OldLangFn,
currentTime: number, currentTime: number,
datetime: number, datetime: number,
) { ) {

View File

@ -1,7 +1,5 @@
import React, { type TeactNode } from '../lib/teact/teact'; import React, { type TeactNode } from '../lib/teact/teact';
import type { LangCode } from '../types';
import { STARS_CURRENCY_CODE } from '../config'; import { STARS_CURRENCY_CODE } from '../config';
import StarIcon from '../components/common/icons/StarIcon'; import StarIcon from '../components/common/icons/StarIcon';
@ -9,7 +7,7 @@ import StarIcon from '../components/common/icons/StarIcon';
export function formatCurrency( export function formatCurrency(
totalPrice: number, totalPrice: number,
currency: string, currency: string,
locale: LangCode = 'en', locale: string = 'en',
options?: { options?: {
shouldOmitFractions?: boolean; shouldOmitFractions?: boolean;
iconClassName?: string; iconClassName?: string;
@ -27,7 +25,7 @@ export function formatCurrency(
export function formatCurrencyAsString( export function formatCurrencyAsString(
totalPrice: number, totalPrice: number,
currency: string, currency: string,
locale: LangCode = 'en', locale: string = 'en',
options?: { options?: {
shouldOmitFractions?: boolean; shouldOmitFractions?: boolean;
}, },

View 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" />,
},
});
}

View File

@ -4,21 +4,23 @@ import type {
ApiLanguage, ApiLanguage,
CachedLangData, CachedLangData,
LangPack, LangPack,
LangPackStringValue,
} from '../../api/types'; } from '../../api/types';
import type { LangKey } from '../../types/language'; import type { LangKey, LangVariable } from '../../types/language';
import { import {
type AdvancedLangFnOptions, type AdvancedLangFnOptions,
type AdvancedLangFnOptionsWithPlural,
areAdvancedLangFnOptions, areAdvancedLangFnOptions,
isDeletedLangString, isDeletedLangString,
isPluralLangString, isPluralLangString,
type LangFn, type LangFn,
type LangFnOptions, type LangFnOptions,
type LangFnOptionsWithPlural,
type LangFnParameters, type LangFnParameters,
type LangFnWithFunction,
type LangFormatters, type LangFormatters,
} from './types'; } from './types';
import { DEBUG } from '../../config'; import { DEBUG, LANG_PACK } from '../../config';
import { callApi } from '../../api/gramjs'; import { callApi } from '../../api/gramjs';
import renderText, { type TextFilter } from '../../components/common/helpers/renderText'; import renderText, { type TextFilter } from '../../components/common/helpers/renderText';
import { MAIN_IDB_STORE } from '../browser/idb'; import { MAIN_IDB_STORE } from '../browser/idb';
@ -37,7 +39,6 @@ import LimitedMap from '../primitives/LimitedMap';
import initialStrings from '../../assets/localization/initialStrings'; import initialStrings from '../../assets/localization/initialStrings';
const LANG_PACK = 'weba';
const LANGPACK_STORE_PREFIX = 'langpack-'; const LANGPACK_STORE_PREFIX = 'langpack-';
const FORMATTERS_FALLBACK_LANG = 'en'; const FORMATTERS_FALLBACK_LANG = 'en';
@ -115,14 +116,22 @@ async function fetchDifference() {
langCode: langPack.langCode, langCode: langPack.langCode,
fromVersion: langPack.version, 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 = { const newLangPack = {
...langPack, ...langPack,
version: result.version, version,
strings: { strings: {
...omit(langPack.strings, result.keysToRemove), ...omit(langPack.strings, keysToRemove),
...result.strings, ...strings,
}, },
}; };
updateLangPack(newLangPack); updateLangPack(newLangPack);
@ -242,6 +251,11 @@ export async function loadAndChangeLanguage(langCode: string, shouldCheckCache?:
return changeLanguage(remoteLanguage); return changeLanguage(remoteLanguage);
} }
export function requestLangPackDifference(langCode: string) {
if (language?.langCode !== langCode) return;
fetchDifference();
}
export async function changeLanguage(newLanguage: ApiLanguage) { export async function changeLanguage(newLanguage: ApiLanguage) {
if (langPack && language?.langCode === newLanguage.langCode) return; if (langPack && language?.langCode === newLanguage.langCode) return;
@ -300,10 +314,10 @@ function createTranslationFn(): LangFn {
fn.pluralCode = language?.pluralCode || FORMATTERS_FALLBACK_LANG; fn.pluralCode = language?.pluralCode || FORMATTERS_FALLBACK_LANG;
fn.with = (({ key, variables, options }: LangFnParameters) => { fn.with = (({ key, variables, options }: LangFnParameters) => {
if (options && areAdvancedLangFnOptions(options)) { 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); return processTranslation(key, variables as Record<string, LangVariable>, options);
}) as LangFnWithFunction; });
fn.region = (code: string) => formatters?.region.of(code); fn.region = (code: string) => formatters?.region.of(code);
fn.conjunction = (list: string[]) => formatters?.conjunction.format(list) || list.join(', '); fn.conjunction = (list: string[]) => formatters?.conjunction.format(list) || list.join(', ');
fn.disjunction = (list: string[]) => formatters?.disjunction.format(list) || list.join(', '); fn.disjunction = (list: string[]) => formatters?.disjunction.format(list) || list.join(', ');
@ -315,7 +329,7 @@ export function getTranslationFn(): LangFn {
return translationFn; return translationFn;
} }
function getString(langKey: LangKey, count: number, options?: Pick<LangFnOptions, 'pluralValue'>) { function getString(langKey: LangKey, count: number) {
let langPackStringValue = langPack?.strings[langKey]; let langPackStringValue = langPack?.strings[langKey];
if (!langPackStringValue && !fallbackLangPack) { if (!langPackStringValue && !fallbackLangPack) {
@ -327,7 +341,7 @@ function getString(langKey: LangKey, count: number, options?: Pick<LangFnOptions
if (!langPackStringValue || isDeletedLangString(langPackStringValue)) return undefined; 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) const string = isPluralLangString(langPackStringValue)
? (langPackStringValue[pluralSuffix] || langPackStringValue.other) ? (langPackStringValue[pluralSuffix] || langPackStringValue.other)
@ -337,20 +351,26 @@ function getString(langKey: LangKey, count: number, options?: Pick<LangFnOptions
} }
function processTranslation( function processTranslation(
langKey: LangKey, variables?: Record<string, string | number>, options?: LangFnOptions, langKey: LangKey,
variables?: Record<string, LangVariable>,
options?: LangFnOptions | LangFnOptionsWithPlural,
): string { ): string {
const cacheKey = `${langKey}-${JSON.stringify(variables)}-${JSON.stringify(options)}`; const cacheKey = `${langKey}-${JSON.stringify(variables)}-${JSON.stringify(options)}`;
if (TRANSLATION_CACHE.has(cacheKey)) { if (TRANSLATION_CACHE.has(cacheKey)) {
return TRANSLATION_CACHE.get(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; if (!string) return langKey;
const variableEntries = variables ? Object.entries(variables) : []; const variableEntries = variables ? Object.entries(variables) : [];
const finalString = variableEntries.reduce((result, [key, value]) => { 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); }, string);
TRANSLATION_CACHE.set(cacheKey, finalString); TRANSLATION_CACHE.set(cacheKey, finalString);
@ -359,9 +379,12 @@ function processTranslation(
} }
function processTranslationAdvanced( function processTranslationAdvanced(
langKey: LangKey, variables?: Record<string, TeactNode>, options?: AdvancedLangFnOptions, langKey: LangKey,
variables?: Record<string, TeactNode | undefined>,
options?: AdvancedLangFnOptions | AdvancedLangFnOptionsWithPlural,
): TeactNode { ): 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; if (!string) return langKey;
const variableEntries = variables ? Object.entries(variables) : []; const variableEntries = variables ? Object.entries(variables) : [];
@ -389,7 +412,10 @@ function processTranslationAdvanced(
return renderText(curr, filters, { return renderText(curr, filters, {
markdownPostProcessor: (part: string) => { markdownPostProcessor: (part: string) => {
return variableEntries.reduce((result, [key, value]): TeactNode[] => { 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[]); }, [part] as TeactNode[]);
}, },
}); });
@ -397,7 +423,10 @@ function processTranslationAdvanced(
} }
return variableEntries.reduce((result, [key, value]): TeactNode[] => { 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); }, tempResult);
} }

View File

@ -7,30 +7,51 @@ import type {
LangPackStringValueRegular, LangPackStringValueRegular,
} from '../../api/types'; } from '../../api/types';
import type { TextFilter } from '../../components/common/helpers/renderText'; 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> = { export interface LangFnOptionsRegular {
[K in keyof T]: R;
};
export interface LangFnOptions {
pluralValue?: number;
withNodes?: never; withNodes?: never;
withMarkdown?: never;
pluralValue?: never;
} }
export interface AdvancedLangFnOptions { export type LangFnOptionsWithPlural = Omit<LangFnOptionsRegular, 'pluralValue'> & {
pluralValue?: number; pluralValue: number;
};
export type LangFnOptions = LangFnOptionsRegular | LangFnOptionsWithPlural;
export interface AdvancedLangFnOptionsRegular {
withNodes: true; withNodes: true;
withMarkdown?: boolean; withMarkdown?: boolean;
pluralValue?: never;
renderTextFilters?: TextFilter[]; renderTextFilters?: TextFilter[];
specialReplacement?: Record<string, TeactNode>; specialReplacement?: Record<string, TeactNode>;
} }
type LangPairWithNodes = { export type AdvancedLangFnOptionsWithPlural = Omit<AdvancedLangFnOptionsRegular, 'pluralValue'> & {
[K in keyof LangPair]: LangPair[K] extends object ? ReplaceTypeValues<LangPair[K], TeactNode> : LangPair[K]; 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]: { [K in keyof T]: {
key: K; key: K;
variables: T[K]; variables: T[K];
@ -38,7 +59,33 @@ type RegularLangFnParameters<T = LangPair> = {
} }
}[keyof T]; }[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]: { [K in keyof T]: {
key: K; key: K;
variables: T[K]; variables: T[K];
@ -46,21 +93,56 @@ type AdvancedLangFnParameters<T = LangPairWithNodes> = {
} }
}[keyof T]; }[keyof T];
export type LangFnParameters = RegularLangFnParameters | AdvancedLangFnParameters; type AdvancedLangFnPluralParameters = {
key: PluralLangKey;
export type LangFnWithFunction = { variables?: undefined;
(params: RegularLangFnParameters): string; options: AdvancedLangFnOptionsWithPlural;
(params: AdvancedLangFnParameters): TeactNode;
}; };
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 = { export type LangFn = {
<K extends LangKey = LangKey, V extends LangPair[K] = LangPair[K]>( <K = RegularLangKey>(
key: K, variables?: V, options?: LangFnOptions, key: K, variables?: undefined, options?: LangFnOptions,
): string; ): 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, key: K, variables: V, options: AdvancedLangFnOptions,
): TeactNode; ): 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; region: (code: string) => string | undefined;
conjunction: (list: string[]) => string; conjunction: (list: string[]) => string;
disjunction: (list: string[]) => string; disjunction: (list: string[]) => string;
@ -101,5 +183,5 @@ export function isLangFnParam(object: unknown): object is LangFnParameters {
export function areAdvancedLangFnOptions( export function areAdvancedLangFnOptions(
params: LangFnOptions | AdvancedLangFnOptions, params: LangFnOptions | AdvancedLangFnOptions,
): params is AdvancedLangFnOptions { ): params is AdvancedLangFnOptions {
return 'withNodes' in params; return 'withNodes' in params && Boolean(params.withNodes);
} }

View File

@ -4,7 +4,7 @@ import type { ApiOldLangPack, ApiOldLangString } from '../api/types';
import type { LangCode, TimeFormat } from '../types'; import type { LangCode, TimeFormat } from '../types';
import { 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'; } from '../config';
import { callApi } from '../api/gramjs'; import { callApi } from '../api/gramjs';
import * as cacheApi from './cacheApi'; import * as cacheApi from './cacheApi';
@ -155,14 +155,14 @@ export async function getTranslationForLangString(langCode: string, key: string)
let translateString: ApiOldLangString | undefined; let translateString: ApiOldLangString | undefined;
const cachedValue = await cacheApi.fetch( const cachedValue = await cacheApi.fetch(
LANG_CACHE_NAME, LANG_CACHE_NAME,
`${DEFAULT_LANG_PACK}_${langCode}_${key}`, `${OLD_DEFAULT_LANG_PACK}_${langCode}_${key}`,
cacheApi.Type.Json, cacheApi.Type.Json,
); );
if (cachedValue) { if (cachedValue) {
translateString = cachedValue.value; translateString = cachedValue.value;
} else { } else {
translateString = await fetchRemoteString(DEFAULT_LANG_PACK, langCode, key); translateString = await fetchRemoteString(OLD_DEFAULT_LANG_PACK, langCode, key);
} }
return processTranslation(translateString, key); return processTranslation(translateString, key);
@ -199,7 +199,9 @@ export async function oldSetLanguage(langCode: LangCode, callback?: NoneToVoidFu
langPack = newLangPack; langPack = newLangPack;
document.documentElement.lang = langCode; 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); const langInfo = languages?.find((lang) => lang.langCode === langCode);
translationFn = createLangFn(); translationFn = createLangFn();
translationFn.isRtl = Boolean(langInfo?.isRtl); translationFn.isRtl = Boolean(langInfo?.isRtl);

View File

@ -1,4 +1,4 @@
import type { LangFn } from '../hooks/useOldLang'; import type { OldLangFn } from '../hooks/useOldLang';
import EMOJI_REGEX from '../lib/twemojiRegex'; import EMOJI_REGEX from '../lib/twemojiRegex';
import fixNonStandardEmoji from './emoji/fixNonStandardEmoji'; 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']; 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) { if (bytes === 0) {
return lang('FileSize.B', 0); return lang('FileSize.B', 0);
} }