From e678824a10dd8c7dcbdd47542f1b4c1c31e9401d Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Sat, 9 Nov 2024 15:40:11 +0400 Subject: [PATCH] Localization: Better platform support (#5136) --- dev/generateLangTypes.ts | 66 +- src/api/gramjs/methods/client.ts | 4 +- src/api/gramjs/methods/settings.ts | 11 +- src/api/gramjs/updates/mtpUpdateHandler.ts | 16 +- src/api/types/misc.ts | 10 +- src/api/types/updates.ts | 16 +- src/assets/localization/fallback.strings | 1 - src/components/auth/AuthPhoneNumber.tsx | 3 +- src/components/auth/AuthQrCode.tsx | 3 +- src/components/common/Audio.tsx | 4 +- src/components/common/CalendarModal.tsx | 4 +- src/components/common/LinkField.tsx | 4 +- .../helpers/renderActionMessageText.tsx | 8 +- .../common/helpers/renderMessageText.ts | 4 +- src/components/common/pickers/ItemPicker.tsx | 4 +- src/components/left/search/ChatMessage.tsx | 4 +- .../left/search/helpers/getSenderName.ts | 4 +- .../left/settings/SettingsLanguage.tsx | 11 +- src/components/main/Main.tsx | 19 +- src/components/main/Notifications.tsx | 23 +- .../common/PremiumLimitReachedModal.tsx | 6 +- src/components/middle/NoMessages.tsx | 10 +- src/components/middle/composer/DropArea.tsx | 2 +- .../helpers/renderKeyboardButtonText.tsx | 4 +- src/components/middle/message/Poll.tsx | 44 +- .../middle/message/hooks/useInnerHandlers.ts | 4 +- src/components/modals/gift/GiftComposer.tsx | 21 +- .../modals/gift/GiftItemPremium.tsx | 4 +- .../modals/gift/info/GiftInfoModal.tsx | 29 +- .../modals/stars/StarsPaymentModal.tsx | 4 +- .../modals/stars/helpers/transaction.ts | 4 +- .../modals/webApp/MinimizedWebAppModal.tsx | 3 + .../modals/webApp/hooks/useWebAppFrame.ts | 4 - src/components/payment/Checkout.tsx | 3 +- .../statistics/StatisticsRecentMessage.tsx | 4 +- .../statistics/StatisticsRecentStory.tsx | 4 +- src/components/test/TestLocale.tsx | 5 +- src/components/ui/Notification.tsx | 114 +- src/components/ui/Portal.ts | 8 +- src/config.ts | 3 +- src/global/actions/api/settings.ts | 8 +- src/global/actions/apiUpdaters/initial.ts | 3 +- src/global/actions/apiUpdaters/misc.ts | 10 + src/global/actions/ui/calls.ts | 4 +- src/global/actions/ui/initial.ts | 3 +- src/global/actions/ui/settings.ts | 4 +- src/global/helpers/chats.ts | 16 +- src/global/helpers/messageSummary.ts | 10 +- src/global/helpers/messages.ts | 8 +- .../helpers/renderMessageSummaryHtml.ts | 4 +- src/global/helpers/users.ts | 4 +- src/global/types.ts | 9 +- src/hooks/useConnectionStatus.ts | 4 +- src/hooks/useOldLang.ts | 4 +- src/lib/gramjs/client/TelegramClient.js | 3 +- src/types/index.ts | 8 +- src/types/language.d.ts | 1123 +++++++++-------- src/util/dates/dateFormat.ts | 30 +- src/util/formatCurrency.tsx | 6 +- src/util/localization/format.tsx | 20 + src/util/localization/index.ts | 69 +- src/util/localization/types.ts | 128 +- src/util/oldLangProvider.ts | 10 +- src/util/textFormat.ts | 4 +- 64 files changed, 1105 insertions(+), 886 deletions(-) create mode 100644 src/util/localization/format.tsx diff --git a/dev/generateLangTypes.ts b/dev/generateLangTypes.ts index 2b766287a..995935334 100644 --- a/dev/generateLangTypes.ts +++ b/dev/generateLangTypes.ts @@ -3,38 +3,42 @@ import { readFileSync, writeFileSync } from 'fs'; import readStrings from '../src/util/data/readStrings'; const TOP_COMMENT = '// This file is generated by dev/generateLangTypes.ts. Do not edit it manually.\n'; -const LANG_KEY_TYPE = 'export type LangKey = keyof LangPair;'; +const LANG_KEY_TYPE = ` +export type RegularLangKey = keyof LangPair; +export type RegularLangKeyWithVariables = keyof LangPairWithVariables; +export type PluralLangKey = keyof LangPairPlural; +export type PluralLangKeyWithVariables = keyof LangPairPluralWithVariables; +export type LangKey = RegularLangKey | RegularLangKeyWithVariables | PluralLangKey | PluralLangKeyWithVariables; +type LangVariable = string | number | undefined; +`.trim(); const data = readFileSync('./src/assets/localization/fallback.strings', 'utf8'); const parsed = readStrings(data); -const keysWithVars = Object.entries(parsed).reduce((acc, [keyWithSuffix, value]) => { - const key = keyWithSuffix.split('_')[0]; +const regularKeysWithVars: Record = {}; +const pluralKeysWithVars: Record = {}; +Object.entries(parsed).forEach(([keyWithSuffix, value]) => { + const [key, pluralSuffix] = keyWithSuffix.split('_'); const variables = extractVariables(value); + const acc = pluralSuffix ? pluralKeysWithVars : regularKeysWithVars; + const previousVariables = acc[key] || []; - if (!previousVariables.length) { - acc[key] = variables; - } else { - acc[key] = Array.from(new Set([...previousVariables, ...variables])); - } - return acc; -}, {} as Record); - -let entries = ''; - -Object.entries(keysWithVars).forEach(([key, variables]) => { - const varString = variables.length - ? `{\n ${variables.map((v) => `${wrapInQuotes(v)}: string | number;`).join('\n ')}\n };\n` - : 'undefined;\n'; - entries += ` ${wrapInQuotes(key)}: ${varString}`; + previousVariables.push(...variables); + acc[key] = previousVariables; }); -const langPair = `export interface LangPair {\n${entries}\n}\n`; -const text = `${TOP_COMMENT}\n${langPair}\n${LANG_KEY_TYPE}\n`; +const regularTypes = formatKeyWithVariables(false, regularKeysWithVars); +const pluralTypes = formatKeyWithVariables(true, pluralKeysWithVars); + +const text = `${TOP_COMMENT}\n${regularTypes}\n${pluralTypes}${LANG_KEY_TYPE}\n`; writeFileSync('./src/types/language.d.ts', text, 'utf8'); + // eslint-disable-next-line no-console -console.log(`Language types generated: ${Object.keys(keysWithVars).length} keys`); +console.log(`Language types generated +${Object.keys(regularKeysWithVars).length} simple keys +${Object.keys(pluralKeysWithVars).length} plural keys +`); function extractVariables(value: string) { const matches = value.match(/(?) { + 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 {\n${variableEntries}}\n`; +} diff --git a/src/api/gramjs/methods/client.ts b/src/api/gramjs/methods/client.ts index 3b543ea14..416978877 100644 --- a/src/api/gramjs/methods/client.ts +++ b/src/api/gramjs/methods/client.ts @@ -16,7 +16,7 @@ import type { import { APP_CODE_NAME, - DEBUG, DEBUG_GRAMJS, IS_TEST, UPLOAD_WORKERS, + DEBUG, DEBUG_GRAMJS, IS_TEST, LANG_PACK, UPLOAD_WORKERS, } from '../../../config'; import { pause } from '../../../util/schedulers'; import { @@ -95,7 +95,9 @@ export async function init(initialArgs: ApiInitialArgs) { shouldForceHttpTransport, shouldAllowHttpTransport, dcId, + langPack: LANG_PACK, langCode, + systemLangCode: navigator.language, isTestServerRequested, } as any, ); diff --git a/src/api/gramjs/methods/settings.ts b/src/api/gramjs/methods/settings.ts index 2163c2cbb..8cae5a1e3 100644 --- a/src/api/gramjs/methods/settings.ts +++ b/src/api/gramjs/methods/settings.ts @@ -2,7 +2,7 @@ import BigInt from 'big-integer'; import { Api as GramJs } from '../../../lib/gramjs'; import type { LANG_PACKS } from '../../../config'; -import type { ApiInputPrivacyRules, ApiPrivacyKey, LangCode } from '../../../types'; +import type { ApiInputPrivacyRules, ApiPrivacyKey } from '../../../types'; import type { ApiAppConfig, ApiConfig, @@ -14,7 +14,10 @@ import type { } from '../../types'; import { - ACCEPTABLE_USERNAME_ERRORS, BLOCKED_LIST_LIMIT, DEFAULT_LANG_PACK, MAX_INT_32, + ACCEPTABLE_USERNAME_ERRORS, + BLOCKED_LIST_LIMIT, + MAX_INT_32, + OLD_DEFAULT_LANG_PACK, } from '../../../config'; import { buildCollectionByKey } from '../../../util/iteratees'; import { getServerTime } from '../../../util/serverTime'; @@ -466,7 +469,7 @@ export async function fetchLangDifference({ export async function fetchLanguages(): Promise { const result = await invokeRequest(new GramJs.langpack.GetLanguages({ - langPack: DEFAULT_LANG_PACK, + langPack: OLD_DEFAULT_LANG_PACK, })); if (!result) { return undefined; @@ -646,7 +649,7 @@ export async function fetchTimezones(hash?: number) { }; } -export async function fetchCountryList({ langCode = 'en' }: { langCode?: LangCode }) { +export async function fetchCountryList({ langCode = 'en' }: { langCode?: string }) { const countryList = await invokeRequest(new GramJs.help.GetCountriesList({ langCode, })); diff --git a/src/api/gramjs/updates/mtpUpdateHandler.ts b/src/api/gramjs/updates/mtpUpdateHandler.ts index 44e2b70c6..a19668dc1 100644 --- a/src/api/gramjs/updates/mtpUpdateHandler.ts +++ b/src/api/gramjs/updates/mtpUpdateHandler.ts @@ -47,6 +47,7 @@ import { import { buildApiNotifyException, buildApiNotifyExceptionTopic, + buildLangStrings, buildPrivacyKey, } from '../apiBuilders/misc'; import { buildApiEmojiStatus, buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; @@ -1050,7 +1051,20 @@ export function updater(update: Update) { '@type': 'updatePaidReactionPrivacy', isPrivate: update.private, }); - } else if (update instanceof LocalUpdatePremiumFloodWait) { + } else if (update instanceof GramJs.UpdateLangPackTooLong) { + sendApiUpdate({ + '@type': 'updateLangPackTooLong', + langCode: update.langCode, + }); + } else if (update instanceof GramJs.UpdateLangPack) { + const { strings, keysToRemove } = buildLangStrings(update.difference.strings); + sendApiUpdate({ + '@type': 'updateLangPack', + version: update.difference.version, + strings, + keysToRemove, + }); + } else if (update instanceof LocalUpdatePremiumFloodWait) { // Local updates sendApiUpdate({ '@type': 'updatePremiumFloodWait', isUpload: update.isUpload, diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 955306b14..76b319e3b 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -1,5 +1,8 @@ +import type { TeactNode } from '../../lib/teact/teact'; + import type { ApiLimitType, ApiPremiumSection, CallbackAction } from '../../global/types'; import type { IconName } from '../../types/icons'; +import type { LangFnParameters } from '../../util/localization'; import type { ApiDocument, ApiPhoto, ApiReaction } from './messages'; import type { ApiUser } from './users'; @@ -111,10 +114,11 @@ export type ApiNotifyException = { export type ApiNotification = { localId: string; - title?: string; - message: string; + containerSelector?: string; + title?: string | LangFnParameters; + message: TeactNode | LangFnParameters; cacheBreaker?: string; - actionText?: string; + actionText?: string | LangFnParameters; action?: CallbackAction | CallbackAction[]; className?: string; duration?: number; diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 392a4e6e8..fe67d1c50 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -34,6 +34,7 @@ import type { import type { ApiEmojiInteraction, ApiError, ApiNotifyException, ApiSessionData, } from './misc'; +import type { LangPackStringValue } from './settings'; import type { ApiStealthMode, ApiStory, ApiStorySkipped } from './stories'; import type { ApiEmojiStatus, ApiUser, ApiUserFullInfo, ApiUserStatus, @@ -774,6 +775,18 @@ export type ApiUpdatePaidReactionPrivacy = { isPrivate: boolean; }; +export type ApiUpdateLangPackTooLong = { + '@type': 'updateLangPackTooLong'; + langCode: string; +}; + +export type ApiUpdateLangPack = { + '@type': 'updateLangPack'; + version: number; + strings: Record; + keysToRemove: string[]; +}; + export type ApiUpdate = ( ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate | ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser | @@ -806,7 +819,8 @@ export type ApiUpdate = ( ApiUpdateViewForumAsMessages | ApiUpdateSavedDialogPinned | ApiUpdatePinnedSavedDialogIds | ApiUpdateChatLastMessage | ApiUpdateDeleteSavedHistory | ApiUpdatePremiumFloodWait | ApiUpdateStarsBalance | ApiUpdateQuickReplyMessage | ApiUpdateQuickReplies | ApiDeleteQuickReply | ApiUpdateDeleteQuickReplyMessages | - ApiUpdateDeleteProfilePhoto | ApiUpdateNewProfilePhoto | ApiUpdateEntities | ApiUpdatePaidReactionPrivacy + ApiUpdateDeleteProfilePhoto | ApiUpdateNewProfilePhoto | ApiUpdateEntities | ApiUpdatePaidReactionPrivacy | + ApiUpdateLangPackTooLong | ApiUpdateLangPack ); export type OnApiUpdate = (update: ApiUpdate) => void; diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index dbfd97b4a..be15a68d3 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -668,7 +668,6 @@ "DiscussChannel" = "channel"; "ForwardedMessage" = "Forwarded message"; "ContextForwardMsg" = "Forward"; -"ShareLinkCopied" = "Copied to Clipboard"; "MessageScheduleSend" = "Send Now"; "MessageScheduleEditTime" = "Reschedule"; "Reply" = "Reply"; diff --git a/src/components/auth/AuthPhoneNumber.tsx b/src/components/auth/AuthPhoneNumber.tsx index 4615c3b96..5327ec7e4 100644 --- a/src/components/auth/AuthPhoneNumber.tsx +++ b/src/components/auth/AuthPhoneNumber.tsx @@ -7,7 +7,6 @@ import { getActions, withGlobal } from '../../global'; import type { ApiCountryCode } from '../../api/types'; import type { GlobalState } from '../../global/types'; -import type { LangCode } from '../../types'; import { requestMeasure } from '../../lib/fasterdom/fasterdom'; import { preloadImage } from '../../util/files'; @@ -36,7 +35,7 @@ type StateProps = Pick & { - language?: LangCode; + language?: string; phoneCodeList: ApiCountryCode[]; }; diff --git a/src/components/auth/AuthQrCode.tsx b/src/components/auth/AuthQrCode.tsx index 879acdc31..b2e9d4036 100644 --- a/src/components/auth/AuthQrCode.tsx +++ b/src/components/auth/AuthQrCode.tsx @@ -5,7 +5,6 @@ import React, { import { getActions, withGlobal } from '../../global'; import type { GlobalState } from '../../global/types'; -import type { LangCode } from '../../types'; import { DEFAULT_LANG_CODE, STRICTERDOM_ENABLED } from '../../config'; import { disableStrict, enableStrict } from '../../lib/fasterdom/stricterdom'; @@ -29,7 +28,7 @@ import blankUrl from '../../assets/blank.png'; type StateProps = Pick - & { language?: LangCode }; + & { language?: string }; const DATA_PREFIX = 'tg://login?token='; const QR_SIZE = 280; diff --git a/src/components/common/Audio.tsx b/src/components/common/Audio.tsx index b8b7a8add..3f218b560 100644 --- a/src/components/common/Audio.tsx +++ b/src/components/common/Audio.tsx @@ -6,7 +6,7 @@ import { getActions } from '../../global'; import type { ApiAudio, ApiMessage, ApiVoice } from '../../api/types'; import type { BufferedRange } from '../../hooks/useBuffering'; -import type { LangFn } from '../../hooks/useOldLang'; +import type { OldLangFn } from '../../hooks/useOldLang'; import type { ISettings } from '../../types'; import { ApiMediaFormat } from '../../api/types'; import { AudioOrigin } from '../../types'; @@ -495,7 +495,7 @@ function getSeeklineSpikeAmounts(isMobile?: boolean, withAvatar?: boolean) { } function renderAudio( - lang: LangFn, + lang: OldLangFn, audio: ApiAudio, duration: number, isPlaying: boolean, diff --git a/src/components/common/CalendarModal.tsx b/src/components/common/CalendarModal.tsx index a77081971..0f7bea477 100644 --- a/src/components/common/CalendarModal.tsx +++ b/src/components/common/CalendarModal.tsx @@ -3,7 +3,7 @@ import React, { memo, useCallback, useEffect, useMemo, useState, } from '../../lib/teact/teact'; -import type { LangFn } from '../../hooks/useOldLang'; +import type { OldLangFn } from '../../hooks/useOldLang'; import { MAX_INT_32 } from '../../config'; import buildClassName from '../../util/buildClassName'; @@ -401,7 +401,7 @@ function formatDay(year: number, month: number, day: number) { return `${year}-${month + 1}-${day}`; } -function formatSubmitLabel(lang: LangFn, date: Date) { +function formatSubmitLabel(lang: OldLangFn, date: Date) { const day = formatDateToString(date, lang.code); const today = formatDateToString(new Date(), lang.code); diff --git a/src/components/common/LinkField.tsx b/src/components/common/LinkField.tsx index a47179742..e5d70ff76 100644 --- a/src/components/common/LinkField.tsx +++ b/src/components/common/LinkField.tsx @@ -43,7 +43,9 @@ const InviteLink: FC = ({ const copyLink = useLastCallback(() => { copyTextToClipboard(link); showNotification({ - message: lang('LinkCopied'), + message: { + key: 'LinkCopied', + }, }); }); diff --git a/src/components/common/helpers/renderActionMessageText.tsx b/src/components/common/helpers/renderActionMessageText.tsx index 63a76c80c..a3691574c 100644 --- a/src/components/common/helpers/renderActionMessageText.tsx +++ b/src/components/common/helpers/renderActionMessageText.tsx @@ -4,7 +4,7 @@ import type { ApiChat, ApiGroupCall, ApiMessage, ApiTopic, ApiUser, } from '../../../api/types'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; -import type { LangFn } from '../../../hooks/useOldLang'; +import type { OldLangFn } from '../../../hooks/useOldLang'; import type { TextPart } from '../../../types'; import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config'; @@ -36,7 +36,7 @@ const MAX_LENGTH = 32; const NBSP = '\u00A0'; export function renderActionMessageText( - oldLang: LangFn, + oldLang: OldLangFn, message: ApiMessage, actionOriginUser?: ApiUser, actionOriginChat?: ApiChat, @@ -269,7 +269,7 @@ function renderProductContent(message: ApiMessage) { } function renderMessageContent( - lang: LangFn, + lang: OldLangFn, message: ApiMessage, options: RenderOptions = {}, observeIntersectionForLoading?: ObserveFn, @@ -318,7 +318,7 @@ function renderUserContent(sender: ApiUser, noLinks?: boolean): string | TextPar return {sender && renderText(text!)}; } -function renderChatContent(lang: LangFn, chat: ApiChat, noLinks?: boolean): string | TextPart | undefined { +function renderChatContent(lang: OldLangFn, chat: ApiChat, noLinks?: boolean): string | TextPart | undefined { const text = trimText(getChatTitle(lang, chat), MAX_LENGTH); if (noLinks) { diff --git a/src/components/common/helpers/renderMessageText.ts b/src/components/common/helpers/renderMessageText.ts index 06edb8a4f..24965ad16 100644 --- a/src/components/common/helpers/renderMessageText.ts +++ b/src/components/common/helpers/renderMessageText.ts @@ -1,7 +1,7 @@ import { getGlobal } from '../../../global'; import type { ApiMessage, ApiSponsoredMessage } from '../../../api/types'; -import type { LangFn } from '../../../hooks/useOldLang'; +import type { OldLangFn } from '../../../hooks/useOldLang'; import type { TextPart } from '../../../types'; import { ApiMessageEntityTypes } from '../../../api/types'; @@ -65,7 +65,7 @@ export function renderMessageText({ // TODO Use Message Summary component instead export function renderMessageSummary( - lang: LangFn, + lang: OldLangFn, message: ApiMessage, noEmoji = false, highlight?: string, diff --git a/src/components/common/pickers/ItemPicker.tsx b/src/components/common/pickers/ItemPicker.tsx index ebdb2064b..32a656df3 100644 --- a/src/components/common/pickers/ItemPicker.tsx +++ b/src/components/common/pickers/ItemPicker.tsx @@ -62,6 +62,7 @@ type OwnProps = { noScrollRestore?: boolean; isViewOnly?: boolean; withDefaultPadding?: boolean; + forceRenderAllItems?: boolean; onFilterChange?: (value: string) => void; onDisabledClick?: (value: string, isSelected: boolean) => void; onLoadMore?: () => void; @@ -86,6 +87,7 @@ const ItemPicker = ({ itemInputType, itemClassName, withDefaultPadding, + forceRenderAllItems, onFilterChange, onDisabledClick, onLoadMore, @@ -163,7 +165,7 @@ const ItemPicker = ({ }); const [viewportValuesList, getMore] = useInfiniteScroll( - onLoadMore, sortedItemValuesList, Boolean(filterValue), + onLoadMore, sortedItemValuesList, Boolean(forceRenderAllItems || filterValue), ); const handleFilterChange = useLastCallback((e: React.ChangeEvent) => { diff --git a/src/components/left/search/ChatMessage.tsx b/src/components/left/search/ChatMessage.tsx index 7a5c1a9f7..52741b439 100644 --- a/src/components/left/search/ChatMessage.tsx +++ b/src/components/left/search/ChatMessage.tsx @@ -6,7 +6,7 @@ import type { ApiChat, ApiMessage, ApiMessageOutgoingStatus, ApiUser, } from '../../../api/types'; -import type { LangFn } from '../../../hooks/useOldLang'; +import type { OldLangFn } from '../../../hooks/useOldLang'; import { getMessageIsSpoiler, @@ -111,7 +111,7 @@ const ChatMessage: FC = ({ }; function renderSummary( - lang: LangFn, message: ApiMessage, blobUrl?: string, searchQuery?: string, isRoundVideo?: boolean, + lang: OldLangFn, message: ApiMessage, blobUrl?: string, searchQuery?: string, isRoundVideo?: boolean, ) { if (!blobUrl) { return renderMessageSummary(lang, message, undefined, searchQuery); diff --git a/src/components/left/search/helpers/getSenderName.ts b/src/components/left/search/helpers/getSenderName.ts index 8c98f883e..716465da5 100644 --- a/src/components/left/search/helpers/getSenderName.ts +++ b/src/components/left/search/helpers/getSenderName.ts @@ -1,5 +1,5 @@ import type { ApiChat, ApiMessage, ApiUser } from '../../../../api/types'; -import type { LangFn } from '../../../../hooks/useOldLang'; +import type { OldLangFn } from '../../../../hooks/useOldLang'; import { getChatTitle, @@ -9,7 +9,7 @@ import { } from '../../../../global/helpers'; export function getSenderName( - lang: LangFn, message: ApiMessage, chatsById: Record, usersById: Record, + lang: OldLangFn, message: ApiMessage, chatsById: Record, usersById: Record, ) { const { senderId } = message; if (!senderId) { diff --git a/src/components/left/settings/SettingsLanguage.tsx b/src/components/left/settings/SettingsLanguage.tsx index caea44032..afa21ad03 100644 --- a/src/components/left/settings/SettingsLanguage.tsx +++ b/src/components/left/settings/SettingsLanguage.tsx @@ -4,6 +4,7 @@ import React, { } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; +import type { ApiLanguage } from '../../../api/types'; import type { ISettings, LangCode } from '../../../types'; import { SettingsScreens } from '../../../types'; @@ -29,7 +30,8 @@ type OwnProps = { type StateProps = { isCurrentUserPremium: boolean; -} & Pick; + languages?: ApiLanguage[]; +} & Pick; const SettingsLanguage: FC = ({ isActive, @@ -44,7 +46,6 @@ const SettingsLanguage: FC = ({ }) => { const { loadLanguages, - loadAttachBots, setSettingOption, openPremiumModal, } = getActions(); @@ -70,8 +71,6 @@ const SettingsLanguage: FC = ({ unmarkIsLoading(); setSettingOption({ language: langCode as LangCode }); - - loadAttachBots(); // Should be refetched every language change }); }); @@ -167,6 +166,7 @@ const SettingsLanguage: FC = ({ = ({ export default memo(withGlobal( (global): StateProps => { const { - language, languages, canTranslate, canTranslateChats, doNotTranslate, + language, canTranslate, canTranslateChats, doNotTranslate, } = global.settings.byKey; + const languages = global.settings.languages; const isCurrentUserPremium = selectIsCurrentUserPremium(global); diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index c09371178..367d40673 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -10,7 +10,6 @@ import { getActions, getGlobal, withGlobal } from '../../global'; import type { ApiChatFolder, ApiMessage, ApiUser } from '../../api/types'; import type { ApiLimitTypeWithModal, TabState } from '../../global/types'; -import type { LangCode } from '../../types'; import { ElectronEvent } from '../../types/electron'; import { BASE_EMOJI_KEYWORD_LANG, DEBUG, INACTIVE_MARKER } from '../../config'; @@ -43,6 +42,7 @@ import useInterval from '../../hooks/schedulers/useInterval'; import useTimeout from '../../hooks/schedulers/useTimeout'; import useAppLayout from '../../hooks/useAppLayout'; import useForceUpdate from '../../hooks/useForceUpdate'; +import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import usePreventPinchZoomGesture from '../../hooks/usePreventPinchZoomGesture'; import useShowTransition from '../../hooks/useShowTransition'; @@ -115,7 +115,6 @@ type StateProps = { openedCustomEmojiSetIds?: string[]; activeGroupCallId?: string; isServiceChatReady?: boolean; - language?: LangCode; wasTimeFormatSetManually?: boolean; isPhoneCallActive?: boolean; addedSetIds?: string[]; @@ -170,7 +169,6 @@ const Main = ({ openedCustomEmojiSetIds, isServiceChatReady, withInterfaceAnimations, - language, wasTimeFormatSetManually, addedSetIds, addedCustomEmojiIds, @@ -257,6 +255,8 @@ const Main = ({ console.log('>>> RENDER MAIN'); } + const lang = useLang(); + // Preload Calls bundle to initialize sounds for iOS useTimeout(() => { void loadBundle(Bundles.Calls); @@ -349,13 +349,15 @@ const Main = ({ // Language-based API calls useEffect(() => { if (isMasterTab) { - if (language !== BASE_EMOJI_KEYWORD_LANG) { - loadEmojiKeywords({ language: language! }); + if (lang.code !== BASE_EMOJI_KEYWORD_LANG) { + loadEmojiKeywords({ language: lang.code }); } - loadCountryList({ langCode: language }); + loadCountryList({ langCode: lang.code }); + + loadAttachBots(); } - }, [language, isMasterTab]); + }, [lang, isMasterTab]); // Re-fetch cached saved emoji for `localDb` useEffect(() => { @@ -596,7 +598,7 @@ export default memo(withGlobal( const { settings: { byKey: { - language, wasTimeFormatSetManually, + wasTimeFormatSetManually, }, }, currentUserId, @@ -660,7 +662,6 @@ export default memo(withGlobal( isServiceChatReady: selectIsServiceChatReady(global), activeGroupCallId: isMasterTab ? global.groupCalls.activeGroupCallId : undefined, withInterfaceAnimations: selectCanAnimateInterface(global), - language, wasTimeFormatSetManually, isPhoneCallActive: isMasterTab ? Boolean(global.phoneCall) : undefined, addedSetIds: global.stickers.added.setIds, diff --git a/src/components/main/Notifications.tsx b/src/components/main/Notifications.tsx index 42649746b..3a0dca812 100644 --- a/src/components/main/Notifications.tsx +++ b/src/components/main/Notifications.tsx @@ -1,12 +1,11 @@ import type { FC } from '../../lib/teact/teact'; import React, { memo } from '../../lib/teact/teact'; -import { getActions, withGlobal } from '../../global'; +import { withGlobal } from '../../global'; import type { ApiNotification } from '../../api/types'; import { selectTabState } from '../../global/selectors'; import { pick } from '../../util/iteratees'; -import renderText from '../common/helpers/renderText'; import Notification from '../ui/Notification'; @@ -15,8 +14,6 @@ type StateProps = { }; const Notifications: FC = ({ notifications }) => { - const { dismissNotification } = getActions(); - if (!notifications.length) { return undefined; } @@ -24,23 +21,7 @@ const Notifications: FC = ({ notifications }) => { return (
{notifications.map((notification) => ( - dismissNotification({ localId: notification.localId })} - /> + ))}
); diff --git a/src/components/main/premium/common/PremiumLimitReachedModal.tsx b/src/components/main/premium/common/PremiumLimitReachedModal.tsx index 950974540..6ef1e4aa0 100644 --- a/src/components/main/premium/common/PremiumLimitReachedModal.tsx +++ b/src/components/main/premium/common/PremiumLimitReachedModal.tsx @@ -3,7 +3,7 @@ import React, { memo, useCallback, useEffect } from '../../../../lib/teact/teact import { getActions, withGlobal } from '../../../../global'; import type { ApiLimitTypeWithModal } from '../../../../global/types'; -import type { LangFn } from '../../../../hooks/useOldLang'; +import type { OldLangFn } from '../../../../hooks/useOldLang'; import type { IconName } from '../../../../types/icons'; import { MAX_UPLOAD_FILEPART_SIZE } from '../../../../config'; @@ -71,7 +71,7 @@ const LIMIT_ICON: Record = { }; const LIMIT_VALUE_FORMATTER: Partial string>> = { - uploadMaxFileparts: (lang: LangFn, value: number) => { + uploadMaxFileparts: (lang: OldLangFn, value: number) => { // The real size is not exactly 4gb, so we need to round it if (value === 8000) return lang('FileSize.GB', '4'); if (value === 4000) return lang('FileSize.GB', '2'); @@ -88,7 +88,7 @@ function getLimiterDescription({ premiumValue, valueFormatter, }: { - lang: LangFn; + lang: OldLangFn; limitType?: ApiLimitTypeWithModal; isPremium?: boolean; canBuyPremium?: boolean; diff --git a/src/components/middle/NoMessages.tsx b/src/components/middle/NoMessages.tsx index 21534a971..55ca2d936 100644 --- a/src/components/middle/NoMessages.tsx +++ b/src/components/middle/NoMessages.tsx @@ -3,7 +3,7 @@ import React, { memo } from '../../lib/teact/teact'; import type { ApiTopic } from '../../api/types'; import type { MessageListType } from '../../global/types'; -import type { LangFn } from '../../hooks/useOldLang'; +import type { OldLangFn } from '../../hooks/useOldLang'; import { REM } from '../common/helpers/mediaDimensions'; import renderText from '../common/helpers/renderText'; @@ -53,7 +53,7 @@ const NoMessages: FC = ({ ); }; -function renderTopic(lang: LangFn, topic: ApiTopic) { +function renderTopic(lang: OldLangFn, topic: ApiTopic) { return (
@@ -69,13 +69,13 @@ function renderTopic(lang: LangFn, topic: ApiTopic) { ); } -function renderScheduled(lang: LangFn) { +function renderScheduled(lang: OldLangFn) { return (
{lang('ScheduledMessages.EmptyPlaceholder')}
); } -function renderSavedMessages(lang: LangFn) { +function renderSavedMessages(lang: OldLangFn) { return (
@@ -92,7 +92,7 @@ function renderSavedMessages(lang: LangFn) { ); } -function renderGroup(lang: LangFn) { +function renderGroup(lang: OldLangFn) { return (
diff --git a/src/components/middle/composer/DropArea.tsx b/src/components/middle/composer/DropArea.tsx index f2455d8f4..392ff15b2 100644 --- a/src/components/middle/composer/DropArea.tsx +++ b/src/components/middle/composer/DropArea.tsx @@ -117,7 +117,7 @@ const DropArea: FC = ({ ); return ( - +
= ({ observeIntersectionForPlaying, onSendVote, }) => { - const { loadMessage, openPollResults, requestConfetti } = getActions(); + const { + loadMessage, openPollResults, requestConfetti, showNotification, + } = getActions(); const { id: messageId, chatId } = message; const { summary, results } = poll; const [isSubmitting, setIsSubmitting] = useState(false); const [chosenOptions, setChosenOptions] = useState([]); - const [isSolutionShown, setIsSolutionShown] = useState(false); const [wasSubmitted, setWasSubmitted] = useState(false); const [closePeriod, setClosePeriod] = useState( !summary.closed && summary.closeDate && summary.closeDate > 0 @@ -176,13 +177,13 @@ const Poll: FC = ({ openPollResults({ chatId, messageId }); }); - const handleSolutionShow = useLastCallback(() => { - setIsSolutionShown(true); - }); - - const handleSolutionHide = useLastCallback(() => { - setIsSolutionShown(false); - setWasSubmitted(false); + const showSolution = useLastCallback(() => { + showNotification({ + localId: getMessageKey(message), + message: renderTextWithEntities({ text: poll.results.solution!, entities: poll.results.solutionEntities }), + duration: SOLUTION_DURATION, + containerSelector: SOLUTION_CONTAINER_ID, + }); }); // Show the solution to quiz if the answer was incorrect @@ -190,7 +191,7 @@ const Poll: FC = ({ if (wasSubmitted && hasVoted && summary.quiz && results.results && poll.results.solution) { const correctResult = results.results.find((r) => r.isChosen && r.isCorrect); if (!correctResult) { - setIsSolutionShown(true); + showSolution(); } } }, [hasVoted, wasSubmitted, results.results, summary.quiz, poll.results.solution]); @@ -224,22 +225,8 @@ const Poll: FC = ({ ); } - function renderSolution() { - return ( - isSolutionShown && poll.results.solution && ( - - ) - ); - } - return (
- {renderSolution()}
{renderTextWithEntities({ text: summary.question.text, @@ -274,8 +261,7 @@ const Poll: FC = ({ size="tiny" color="translucent" className="poll-quiz-help" - disabled={isSolutionShown} - onClick={handleSolutionShow} + onClick={showSolution} ariaLabel="Show Solution" > @@ -353,7 +339,7 @@ function getPollTypeString(summary: ApiPoll['summary']) { return summary.isPublic ? 'PublicPoll' : 'AnonymousPoll'; } -function getReadableVotersCount(lang: LangFn, isQuiz: true | undefined, count?: number) { +function getReadableVotersCount(lang: OldLangFn, isQuiz: true | undefined, count?: number) { if (!count) { return lang(isQuiz ? 'Chat.Quiz.TotalVotesEmpty' : 'Chat.Poll.TotalVotesResultEmpty'); } diff --git a/src/components/middle/message/hooks/useInnerHandlers.ts b/src/components/middle/message/hooks/useInnerHandlers.ts index 7c16a6789..ee2cee23a 100644 --- a/src/components/middle/message/hooks/useInnerHandlers.ts +++ b/src/components/middle/message/hooks/useInnerHandlers.ts @@ -4,7 +4,7 @@ import { getActions } from '../../../../global'; import type { ApiMessage, ApiPeer, ApiStory, ApiTopic, ApiUser, } from '../../../../api/types'; -import type { LangFn } from '../../../../hooks/useOldLang'; +import type { OldLangFn } from '../../../../hooks/useOldLang'; import type { IAlbum, ThreadId } from '../../../../types'; import { MAIN_THREAD_ID } from '../../../../api/types'; import { MediaViewerOrigin } from '../../../../types'; @@ -33,7 +33,7 @@ export default function useInnerHandlers({ isRepliesChat, isSavedMessages, }: { - lang: LangFn; + lang: OldLangFn; selectMessage: (e: React.MouseEvent, groupedId?: string) => void; message: ApiMessage; chatId: string; diff --git a/src/components/modals/gift/GiftComposer.tsx b/src/components/modals/gift/GiftComposer.tsx index 54429a8b5..3c9cdb326 100644 --- a/src/components/modals/gift/GiftComposer.tsx +++ b/src/components/modals/gift/GiftComposer.tsx @@ -7,17 +7,15 @@ import { getActions, withGlobal } from '../../../global'; import type { ApiMessage, ApiUser } from '../../../api/types'; import type { GiftOption } from './GiftModal'; -import { STARS_CURRENCY_CODE, STARS_ICON_PLACEHOLDER } from '../../../config'; +import { STARS_CURRENCY_CODE } from '../../../config'; import { getUserFullName } from '../../../global/helpers'; import { selectTabState, selectTheme, selectUser } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import { formatCurrency } from '../../../util/formatCurrency'; -import { formatInteger } from '../../../util/textFormat'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; -import Icon from '../../common/icons/Icon'; import PremiumProgress from '../../common/PremiumProgress'; import ActionMessage from '../../middle/ActionMessage'; import Button from '../../ui/Button'; @@ -175,16 +173,9 @@ function GiftComposer({ function renderFooter() { const userFullName = getUserFullName(user)!; - const amount = isStarGift ? ( - lang('StarsAmount', { - amount: formatInteger(gift.stars), - }, { - withNodes: true, - specialReplacement: { - [STARS_ICON_PLACEHOLDER]: , - }, - }) - ) : formatCurrency(gift.amount, gift.currency); + const amount = isStarGift + ? formatCurrency(gift.stars, STARS_CURRENCY_CODE, lang.code, { iconClassName: 'star-amount-icon' }) + : formatCurrency(gift.amount, gift.currency); return (
@@ -201,9 +192,9 @@ function GiftComposer({ isPrimary progress={gift.availabilityRemains / gift.availabilityTotal!} rightText={lang('GiftSoldCount', { - count: formatInteger(gift.availabilityTotal! - gift.availabilityRemains), + count: gift.availabilityTotal! - gift.availabilityRemains, })} - leftText={lang('GiftLeftCount', { count: formatInteger(gift.availabilityRemains) })} + leftText={lang('GiftLeftCount', { count: gift.availabilityRemains })} className={styles.limited} /> )} diff --git a/src/components/modals/gift/GiftItemPremium.tsx b/src/components/modals/gift/GiftItemPremium.tsx index 729bf67c1..a9d42a671 100644 --- a/src/components/modals/gift/GiftItemPremium.tsx +++ b/src/components/modals/gift/GiftItemPremium.tsx @@ -52,7 +52,9 @@ function GiftItemPremium({ : undefined; function renderMonths() { - const caption = months === 12 ? lang('Years', { count: 1 }) : lang('Months', { count: months }); + const caption = months === 12 + ? lang('Years', { count: 1 }, { pluralValue: 1 }) + : lang('Months', { count: months }, { pluralValue: months }); return (
{caption} diff --git a/src/components/modals/gift/info/GiftInfoModal.tsx b/src/components/modals/gift/info/GiftInfoModal.tsx index 935218b67..4749ddef0 100644 --- a/src/components/modals/gift/info/GiftInfoModal.tsx +++ b/src/components/modals/gift/info/GiftInfoModal.tsx @@ -4,11 +4,11 @@ import { getActions, withGlobal } from '../../../../global'; import type { ApiSticker, ApiUser } from '../../../../api/types'; import type { TabState } from '../../../../global/types'; -import { STARS_ICON_PLACEHOLDER } from '../../../../config'; import { getUserFullName } from '../../../../global/helpers'; import { selectStarGiftSticker, selectUser } from '../../../../global/selectors'; import buildClassName from '../../../../util/buildClassName'; import { formatDateTimeToString } from '../../../../util/dates/dateFormat'; +import { formatStarsAsIcon, formatStarsAsText } from '../../../../util/localization/format'; import { CUSTOM_PEER_HIDDEN } from '../../../../util/objects/customPeer'; import { formatInteger } from '../../../../util/textFormat'; import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities'; @@ -69,6 +69,7 @@ const GiftInfoModal = ({ const canConvertDifference = (userGift && starGiftMaxConvertPeriod && ( userGift.date + starGiftMaxConvertPeriod - Date.now() / 1000 )) || 0; + const conversionLeft = Math.ceil(canConvertDifference / 60 / 60 / 24); const handleClose = useLastCallback(() => { closeGiftInfoModal(); @@ -131,17 +132,19 @@ const GiftInfoModal = ({ return canUpdate ? lang('GiftInfoDescription', { - amount: formatInteger(starsToConvert!), + amount: starsToConvert, }, { withNodes: true, withMarkdown: true, + pluralValue: starsToConvert, }) : lang('GiftInfoDescriptionOut', { - amount: formatInteger(starsToConvert!), + amount: starsToConvert, user: getUserFullName(targetUser)!, }, { withNodes: true, withMarkdown: true, + pluralValue: starsToConvert, }); })(); @@ -205,14 +208,7 @@ const GiftInfoModal = ({ tableData.push([ lang('GiftInfoValue'),
- {lang('StarsAmount', { - amount: formatInteger(gift.stars), - }, { - withNodes: true, - specialReplacement: { - [STARS_ICON_PLACEHOLDER]: , - }, - })} + {formatStarsAsIcon(lang, gift.stars)} {canUpdate && canConvertDifference > 0 && Boolean(starsToConvert) && ( {lang('GiftInfoConvert', { amount: starsToConvert }, { pluralValue: starsToConvert })} @@ -225,8 +221,10 @@ const GiftInfoModal = ({ tableData.push([ lang('GiftInfoAvailability'), lang('GiftInfoAvailabilityValue', { - count: formatInteger(gift.availabilityRemains!), - total: formatInteger(gift.availabilityTotal), + count: gift.availabilityRemains || 0, + total: gift.availabilityTotal, + }, { + pluralValue: gift.availabilityRemains || 0, }), ]); } @@ -295,7 +293,7 @@ const GiftInfoModal = ({ >
{lang('GiftInfoConvertDescription1', { - amount: lang('StarsAmountText', { amount: formatInteger(userGift.starsToConvert!) }), + amount: formatStarsAsText(lang, userGift.starsToConvert!), user: getUserFullName(userFrom)!, }, { withNodes: true, @@ -305,10 +303,11 @@ const GiftInfoModal = ({ {canConvertDifference > 0 && (
{lang('GiftInfoConvertDescriptionPeriod', { - count: formatInteger(Math.ceil(canConvertDifference / 60 / 60 / 24)), + count: conversionLeft, }, { withNodes: true, withMarkdown: true, + pluralValue: conversionLeft, })}
)} diff --git a/src/components/modals/stars/StarsPaymentModal.tsx b/src/components/modals/stars/StarsPaymentModal.tsx index c5680d261..c6476b122 100644 --- a/src/components/modals/stars/StarsPaymentModal.tsx +++ b/src/components/modals/stars/StarsPaymentModal.tsx @@ -94,11 +94,11 @@ const StarPaymentModal = ({ if (subscriptionInfo) { return lang('StarsSubscribeText', { chat: subscriptionInfo.title, - amount: amount!, + amount, }, { withNodes: true, withMarkdown: true, - pluralValue: amount, + pluralValue: amount!, }); } diff --git a/src/components/modals/stars/helpers/transaction.ts b/src/components/modals/stars/helpers/transaction.ts index d6994ed7d..c16a7862a 100644 --- a/src/components/modals/stars/helpers/transaction.ts +++ b/src/components/modals/stars/helpers/transaction.ts @@ -1,9 +1,9 @@ import type { ApiStarsTransaction } from '../../../../api/types'; -import type { LangFn } from '../../../../hooks/useOldLang'; +import type { OldLangFn } from '../../../../hooks/useOldLang'; import { buildStarsTransactionCustomPeer } from '../../../../global/helpers/payments'; -export function getTransactionTitle(lang: LangFn, transaction: ApiStarsTransaction) { +export function getTransactionTitle(lang: OldLangFn, transaction: ApiStarsTransaction) { if (transaction.extendedMedia) return lang('StarMediaPurchase'); if (transaction.subscriptionPeriod) return lang('StarSubscriptionPurchase'); if (transaction.isReaction) return lang('StarsReactionsSent'); diff --git a/src/components/modals/webApp/MinimizedWebAppModal.tsx b/src/components/modals/webApp/MinimizedWebAppModal.tsx index 4953d1103..05bafb9d9 100644 --- a/src/components/modals/webApp/MinimizedWebAppModal.tsx +++ b/src/components/modals/webApp/MinimizedWebAppModal.tsx @@ -73,6 +73,9 @@ const MinimizedWebAppModal = ({ { botName: activeTabName, count: openedTabsCount - 1, + }, + { + pluralValue: openedTabsCount - 1, })}` : activeTabName; diff --git a/src/components/modals/webApp/hooks/useWebAppFrame.ts b/src/components/modals/webApp/hooks/useWebAppFrame.ts index 50e559b9c..a2be962da 100644 --- a/src/components/modals/webApp/hooks/useWebAppFrame.ts +++ b/src/components/modals/webApp/hooks/useWebAppFrame.ts @@ -182,10 +182,6 @@ const useWebAppFrame = ( data: null, }, }); - - // showNotification({ - // message: 'Clipboard access is not supported in this client yet', - // }); } if (eventType === 'web_app_open_scan_qr_popup') { diff --git a/src/components/payment/Checkout.tsx b/src/components/payment/Checkout.tsx index 54f02712e..1a6a3db02 100644 --- a/src/components/payment/Checkout.tsx +++ b/src/components/payment/Checkout.tsx @@ -9,7 +9,6 @@ import type { ApiWebDocument, } from '../../api/types'; import type { FormEditDispatch } from '../../hooks/reducers/usePaymentReducer'; -import type { LangCode } from '../../types'; import type { IconName } from '../../types/icons'; import { PaymentStep } from '../../types'; @@ -238,7 +237,7 @@ const Checkout: FC = ({ export default memo(Checkout); function renderPaymentItem( - langCode: LangCode | undefined, title: string, value: number, currency: string, main = false, + langCode: string | undefined, title: string, value: number, currency: string, main = false, ) { return (
diff --git a/src/components/right/statistics/StatisticsRecentMessage.tsx b/src/components/right/statistics/StatisticsRecentMessage.tsx index ecd842f5f..1e3f74af7 100644 --- a/src/components/right/statistics/StatisticsRecentMessage.tsx +++ b/src/components/right/statistics/StatisticsRecentMessage.tsx @@ -3,7 +3,7 @@ import React, { memo, useCallback } from '../../../lib/teact/teact'; import { getActions } from '../../../global'; import type { ApiMessage, StatisticsMessageInteractionCounter } from '../../../api/types'; -import type { LangFn } from '../../../hooks/useOldLang'; +import type { OldLangFn } from '../../../hooks/useOldLang'; import { getMessageMediaHash, @@ -67,7 +67,7 @@ const StatisticsRecentMessage: FC = ({ postStatistic, message }) => { ); }; -function renderSummary(lang: LangFn, message: ApiMessage, blobUrl?: string, isRoundVideo?: boolean) { +function renderSummary(lang: OldLangFn, message: ApiMessage, blobUrl?: string, isRoundVideo?: boolean) { if (!blobUrl) { return renderMessageSummary(lang, message); } diff --git a/src/components/right/statistics/StatisticsRecentStory.tsx b/src/components/right/statistics/StatisticsRecentStory.tsx index 12ff26fa9..5cf7cb323 100644 --- a/src/components/right/statistics/StatisticsRecentStory.tsx +++ b/src/components/right/statistics/StatisticsRecentStory.tsx @@ -6,7 +6,7 @@ import type { ApiTypeStory, StatisticsStoryInteractionCounter, } from '../../../api/types'; -import type { LangFn } from '../../../hooks/useOldLang'; +import type { OldLangFn } from '../../../hooks/useOldLang'; import { getStoryMediaHash } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; @@ -65,7 +65,7 @@ function StatisticsRecentStory({ chat, story, postStatistic }: OwnProps) { ); } -function renderSummary(lang: LangFn, chat: ApiChat, blobUrl?: string) { +function renderSummary(lang: OldLangFn, chat: ApiChat, blobUrl?: string) { return ( {blobUrl ? ( diff --git a/src/components/test/TestLocale.tsx b/src/components/test/TestLocale.tsx index 2a0f51cb4..4e6bd9465 100644 --- a/src/components/test/TestLocale.tsx +++ b/src/components/test/TestLocale.tsx @@ -9,6 +9,9 @@ const storedParameter: LangFnParameters = { variables: { count: 42, }, + options: { + pluralValue: 42, + }, }; const storedAdvancedParameter: LangFnParameters = { @@ -39,7 +42,7 @@ const TestLocale = () => { withMarkdown: true, })}

-

{lang('Participants', { count: 42 })}

+

{lang('Participants', { count: 42 }, { pluralValue: 42 })}

{lang('ChatServiceGroupUpdatedPinnedMessage1', { message: 'Some message', diff --git a/src/components/ui/Notification.tsx b/src/components/ui/Notification.tsx index edc16ba65..e9deeded1 100644 --- a/src/components/ui/Notification.tsx +++ b/src/components/ui/Notification.tsx @@ -1,19 +1,21 @@ -import type { FC, TeactNode } from '../../lib/teact/teact'; +import type { FC } from '../../lib/teact/teact'; import React, { - useCallback, useEffect, + useMemo, useRef, useState, } from '../../lib/teact/teact'; import { getActions } from '../../global'; -import type { CallbackAction } from '../../global/types'; -import type { IconName } from '../../types/icons'; +import type { ApiNotification } from '../../api/types'; +import { isLangFnParam } from '../../util/localization/types'; import { ANIMATION_END_DELAY } from '../../config'; import buildClassName from '../../util/buildClassName'; import captureEscKeyListener from '../../util/captureEscKeyListener'; +import renderText from '../common/helpers/renderText'; +import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated'; @@ -25,57 +27,55 @@ import RoundTimer from './RoundTimer'; import './Notification.scss'; type OwnProps = { - title?: TeactNode; - containerId?: string; - message: TeactNode; - duration?: number; - action?: CallbackAction | CallbackAction[]; - actionText?: string; - className?: string; - icon?: IconName; - shouldDisableClickDismiss?: boolean; - dismissAction?: CallbackAction; - shouldShowTimer?: boolean; - cacheBreaker?: string; - onDismiss: NoneToVoidFunction; + notification: ApiNotification; }; const DEFAULT_DURATION = 3000; const ANIMATION_DURATION = 150; const Notification: FC = ({ - title, - className, - message, - duration = DEFAULT_DURATION, - containerId, - icon, - action, - actionText, - shouldDisableClickDismiss, - dismissAction, - shouldShowTimer, - cacheBreaker, - onDismiss, + notification, }) => { const actions = getActions(); + const lang = useLang(); + + const { + localId, + message, + action, + actionText, + cacheBreaker, + className, + disableClickDismiss, + dismissAction, + duration = DEFAULT_DURATION, + icon, + shouldShowTimer, + title, + containerSelector, + } = notification; + const [isOpen, setIsOpen] = useState(true); // eslint-disable-next-line no-null/no-null const timerRef = useRef(null); const { transitionClassNames } = useShowTransitionDeprecated(isOpen); + const handleDismiss = useLastCallback(() => { + actions.dismissNotification({ localId }); + }); + const closeAndDismiss = useLastCallback((force?: boolean) => { - if (!force && shouldDisableClickDismiss) return; + if (!force && disableClickDismiss) return; setIsOpen(false); - setTimeout(onDismiss, ANIMATION_DURATION + ANIMATION_END_DELAY); + setTimeout(handleDismiss, ANIMATION_DURATION + ANIMATION_END_DELAY); if (dismissAction) { // @ts-ignore actions[dismissAction.action](dismissAction.payload); } }); - const handleClick = useCallback(() => { + const handleClick = useLastCallback(() => { if (action) { if (Array.isArray(action)) { // @ts-ignore @@ -86,7 +86,7 @@ const Notification: FC = ({ } } closeAndDismiss(); - }, [action, actions, closeAndDismiss]); + }); useEffect(() => (isOpen ? captureEscKeyListener(closeAndDismiss) : undefined), [isOpen, closeAndDismiss]); @@ -102,7 +102,7 @@ const Notification: FC = ({ }, [duration, cacheBreaker]); // Reset timer if `cacheBreaker` changes const handleMouseEnter = useLastCallback(() => { - if (shouldDisableClickDismiss) return; + if (disableClickDismiss) return; if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = undefined; @@ -110,15 +110,45 @@ const Notification: FC = ({ }); const handleMouseLeave = useLastCallback(() => { - if (shouldDisableClickDismiss) return; + if (disableClickDismiss) return; if (timerRef.current) { clearTimeout(timerRef.current); } timerRef.current = window.setTimeout(closeAndDismiss, duration); }); + const renderedTitle = useMemo(() => { + if (!title) return undefined; + if (isLangFnParam(title)) { + return lang.with(title); + } + + return renderText(title, ['simple_markdown', 'emoji', 'br', 'links']); + }, [lang, title]); + + const renderedMessage = useMemo(() => { + if (isLangFnParam(message)) { + return lang.with(message); + } + + if (typeof message === 'string') { + return renderText(message, ['simple_markdown', 'emoji', 'br', 'links']); + } + + return message; + }, [lang, message]); + + const renderedActionText = useMemo(() => { + if (!actionText) return undefined; + if (isLangFnParam(actionText)) { + return lang.with(actionText); + } + + return actionText; + }, [lang, actionText]); + return ( - +

= ({ >
- {title &&
{title}
} - {message} + {renderedTitle && ( +
{renderedTitle}
+ )} + {renderedMessage}
- {action && actionText && ( + {action && renderedActionText && ( )} {shouldShowTimer && ( diff --git a/src/components/ui/Portal.ts b/src/components/ui/Portal.ts index d41fc771a..8fdffcf41 100644 --- a/src/components/ui/Portal.ts +++ b/src/components/ui/Portal.ts @@ -3,19 +3,19 @@ import { useLayoutEffect, useRef } from '../../lib/teact/teact'; import TeactDOM from '../../lib/teact/teact-dom'; type OwnProps = { - containerId?: string; + containerSelector?: string; className?: string; children: VirtualElement; }; -const Portal: FC = ({ containerId, className, children }) => { +const Portal: FC = ({ containerSelector, className, children }) => { const elementRef = useRef(); if (!elementRef.current) { elementRef.current = document.createElement('div'); } useLayoutEffect(() => { - const container = document.querySelector(containerId || '#portals'); + const container = document.querySelector(containerSelector || '#portals'); if (!container) { return undefined; } @@ -31,7 +31,7 @@ const Portal: FC = ({ containerId, className, children }) => { TeactDOM.render(undefined, element); container.removeChild(element); }; - }, [className, containerId]); + }, [className, containerSelector]); return TeactDOM.render(children, elementRef.current); }; diff --git a/src/config.ts b/src/config.ts index a68261dea..1c6e96c1a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -294,6 +294,7 @@ export const PURCHASE_USERNAME = 'auction'; export const ACCEPTABLE_USERNAME_ERRORS = new Set([USERNAME_PURCHASE_ERROR, 'USERNAME_INVALID']); export const TME_WEB_DOMAINS = new Set(['t.me', 'web.t.me', 'a.t.me', 'k.t.me', 'z.t.me']); export const WEB_APP_PLATFORM = 'weba'; +export const LANG_PACK = 'weba'; // eslint-disable-next-line max-len export const COUNTRIES_WITH_12H_TIME_FORMAT = new Set(['AU', 'BD', 'CA', 'CO', 'EG', 'HN', 'IE', 'IN', 'JO', 'MX', 'MY', 'NI', 'NZ', 'PH', 'PK', 'SA', 'SV', 'US']); @@ -321,7 +322,7 @@ export const MAX_MEDIA_FILES_FOR_ALBUM = 10; export const MAX_ACTIVE_PINNED_CHATS = 5; export const SCHEDULED_WHEN_ONLINE = 0x7FFFFFFE; export const DEFAULT_LANG_CODE = 'en'; -export const DEFAULT_LANG_PACK = 'android'; +export const OLD_DEFAULT_LANG_PACK = 'android'; export const LANG_PACKS = ['android', 'ios', 'tdesktop', 'macos'] as const; export const FEEDBACK_URL = 'https://bugs.telegram.org/?tag_ids=41&sort=time'; export const FAQ_URL = 'https://telegram.org/faq'; diff --git a/src/global/actions/api/settings.ts b/src/global/actions/api/settings.ts index 6a31cf1e6..c31ca6ed6 100644 --- a/src/global/actions/api/settings.ts +++ b/src/global/actions/api/settings.ts @@ -373,7 +373,13 @@ addActionHandler('loadLanguages', async (global): Promise => { } global = getGlobal(); - global = replaceSettings(global, { languages: result }); + global = { + ...global, + settings: { + ...global.settings, + languages: result, + }, + }; setGlobal(global); }); diff --git a/src/global/actions/apiUpdaters/initial.ts b/src/global/actions/apiUpdaters/initial.ts index e8e62aa5c..701a3bee1 100644 --- a/src/global/actions/apiUpdaters/initial.ts +++ b/src/global/actions/apiUpdaters/initial.ts @@ -6,6 +6,7 @@ import type { ApiUpdateServerTimeOffset, ApiUpdateSession, } from '../../../api/types'; +import type { LangCode } from '../../../types'; import type { RequiredGlobalActions } from '../../index'; import type { ActionReturnType, GlobalState } from '../../types'; @@ -97,7 +98,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { }); function onUpdateApiReady(global: T) { - void oldSetLanguage(global.settings.byKey.language); + void oldSetLanguage(global.settings.byKey.language as LangCode); } function onUpdateAuthorizationState(global: T, update: ApiUpdateAuthorizationState) { diff --git a/src/global/actions/apiUpdaters/misc.ts b/src/global/actions/apiUpdaters/misc.ts index b73e48c0c..c394d95c5 100644 --- a/src/global/actions/apiUpdaters/misc.ts +++ b/src/global/actions/apiUpdaters/misc.ts @@ -1,6 +1,7 @@ import type { ActionReturnType } from '../../types'; import { PaymentStep } from '../../../types'; +import { applyLangPackDifference, requestLangPackDifference } from '../../../util/localization'; import { addActionHandler, setGlobal } from '../../index'; import { addBlockedUser, @@ -194,6 +195,15 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { setGlobal(global); break; } + + case 'updateLangPackTooLong': { + requestLangPackDifference(update.langCode); + break; + } + + case 'updateLangPack': { + applyLangPackDifference(update.version, update.strings, update.keysToRemove); + } } return undefined; diff --git a/src/global/actions/ui/calls.ts b/src/global/actions/ui/calls.ts index e47b13088..1067bdea4 100644 --- a/src/global/actions/ui/calls.ts +++ b/src/global/actions/ui/calls.ts @@ -199,7 +199,9 @@ addActionHandler('createGroupCallInviteLink', async (global, actions, payload): copyTextToClipboard(inviteLink); actions.showNotification({ - message: 'Link copied to clipboard', + message: { + key: 'LinkCopied', + }, tabId, }); }); diff --git a/src/global/actions/ui/initial.ts b/src/global/actions/ui/initial.ts index ba4f0fe64..c0819ea1a 100644 --- a/src/global/actions/ui/initial.ts +++ b/src/global/actions/ui/initial.ts @@ -1,5 +1,6 @@ import { addCallback } from '../../../lib/teact/teactn'; +import type { LangCode } from '../../../types'; import type { ActionReturnType, GlobalState } from '../../types'; import { requestMutation } from '../../../lib/fasterdom/fasterdom'; @@ -134,7 +135,7 @@ addCallback((global: GlobalState) => { const performanceType = selectPerformanceSettings(global); - void oldSetLanguage(language, undefined, true); + void oldSetLanguage(language as LangCode, undefined, true); requestMutation(() => { document.documentElement.style.setProperty( diff --git a/src/global/actions/ui/settings.ts b/src/global/actions/ui/settings.ts index 3954a8d5f..f1fc4c4d8 100644 --- a/src/global/actions/ui/settings.ts +++ b/src/global/actions/ui/settings.ts @@ -1,7 +1,7 @@ import { addCallback } from '../../../lib/teact/teactn'; import type { ActionReturnType, GlobalState } from '../../types'; -import { SettingsScreens } from '../../../types'; +import { type LangCode, SettingsScreens } from '../../../types'; import { requestMutation } from '../../../lib/fasterdom/fasterdom'; import { disableDebugConsole, initDebugConsole } from '../../../util/debugConsole'; @@ -53,7 +53,7 @@ addCallback((global: GlobalState) => { } if (settings.language !== prevSettings.language) { - oldSetLanguage(settings.language); + oldSetLanguage(settings.language as LangCode); } if (settings.timeFormat !== prevSettings.timeFormat) { diff --git a/src/global/helpers/chats.ts b/src/global/helpers/chats.ts index 8ccf2d6af..b8f68759e 100644 --- a/src/global/helpers/chats.ts +++ b/src/global/helpers/chats.ts @@ -9,7 +9,7 @@ import type { ApiTopic, ApiUser, } from '../../api/types'; -import type { LangFn } from '../../hooks/useOldLang'; +import type { OldLangFn } from '../../hooks/useOldLang'; import type { CustomPeer, NotifyException, NotifySettings, ThreadId, } from '../../types'; @@ -101,7 +101,7 @@ export function getPrivateChatUserId(chat: ApiChat) { return chat.id; } -export function getChatTitle(lang: LangFn, chat: ApiChat, isSelf = false) { +export function getChatTitle(lang: OldLangFn, chat: ApiChat, isSelf = false) { if (isSelf) { return lang('SavedMessages'); } @@ -247,7 +247,7 @@ export function getAllowedAttachmentOptions( } export function getMessageSendingRestrictionReason( - lang: LangFn, + lang: OldLangFn, currentUserBannedRights?: ApiChatBannedRights, defaultBannedRights?: ApiChatBannedRights, ) { @@ -272,7 +272,7 @@ export function getMessageSendingRestrictionReason( } export function getForumComposerPlaceholder( - lang: LangFn, + lang: OldLangFn, chat?: ApiChat, threadId: ThreadId = MAIN_THREAD_ID, topics?: Record, @@ -341,7 +341,7 @@ export function getCanDeleteChat(chat: ApiChat) { return isChatBasicGroup(chat) || ((isChatSuperGroup(chat) || isChatChannel(chat)) && chat.isCreator); } -export function getFolderDescriptionText(lang: LangFn, folder: ApiChatFolder, chatsCount?: number) { +export function getFolderDescriptionText(lang: OldLangFn, folder: ApiChatFolder, chatsCount?: number) { const { excludedChatIds, includedChatIds, bots, groups, contacts, nonContacts, channels, @@ -376,7 +376,7 @@ export function getFolderDescriptionText(lang: LangFn, folder: ApiChatFolder, ch } } -export function getMessageSenderName(lang: LangFn, chatId: string, sender?: ApiPeer) { +export function getMessageSenderName(lang: OldLangFn, chatId: string, sender?: ApiPeer) { if (!sender || isUserId(chatId)) { return undefined; } @@ -395,7 +395,7 @@ export function getMessageSenderName(lang: LangFn, chatId: string, sender?: ApiP } export function filterChatsByName( - lang: LangFn, + lang: OldLangFn, chatIds: string[], chatsById: Record, query?: string, @@ -475,7 +475,7 @@ export function getIsSavedDialog(chatId: string, threadId: ThreadId | undefined, return chatId === currentUserId && threadId !== MAIN_THREAD_ID; } -export function getGroupStatus(lang: LangFn, chat: ApiChat) { +export function getGroupStatus(lang: OldLangFn, chat: ApiChat) { const chatTypeString = lang(getChatTypeString(chat)); const { membersCount } = chat; diff --git a/src/global/helpers/messageSummary.ts b/src/global/helpers/messageSummary.ts index d08f52817..14aadddfb 100644 --- a/src/global/helpers/messageSummary.ts +++ b/src/global/helpers/messageSummary.ts @@ -3,7 +3,7 @@ import type { TeactNode } from '../../lib/teact/teact'; import type { ApiMediaExtendedPreview, ApiMessage, MediaContent, StatefulMediaContent, } from '../../api/types'; -import type { LangFn } from '../../hooks/useOldLang'; +import type { OldLangFn } from '../../hooks/useOldLang'; import { ApiMessageEntityTypes } from '../../api/types'; import { CONTENT_NOT_SUPPORTED } from '../../config'; @@ -17,7 +17,7 @@ const SPOILER_CHARS = ['â º', 'â µ', 'â ž', 'â Ÿ']; export const TRUNCATED_SUMMARY_LENGTH = 80; export function getMessageSummaryText( - lang: LangFn, + lang: OldLangFn, message: ApiMessage, statefulContent: StatefulMediaContent | undefined, noEmoji = false, @@ -106,12 +106,12 @@ export function getMessageSummaryEmoji(message: ApiMessage) { } export function getMediaContentTypeDescription( - lang: LangFn, content: MediaContent, statefulContent: StatefulMediaContent | undefined, + lang: OldLangFn, content: MediaContent, statefulContent: StatefulMediaContent | undefined, ) { return getSummaryDescription(lang, content, statefulContent); } export function getMessageSummaryDescription( - lang: LangFn, + lang: OldLangFn, message: ApiMessage, statefulContent: StatefulMediaContent | undefined, truncatedText?: string | TeactNode, @@ -120,7 +120,7 @@ export function getMessageSummaryDescription( return getSummaryDescription(lang, message.content, statefulContent, message, truncatedText, isExtended); } function getSummaryDescription( - lang: LangFn, + lang: OldLangFn, mediaContent: MediaContent, statefulContent: StatefulMediaContent | undefined, message?: ApiMessage, diff --git a/src/global/helpers/messages.ts b/src/global/helpers/messages.ts index 79d1e23fb..c1661f641 100644 --- a/src/global/helpers/messages.ts +++ b/src/global/helpers/messages.ts @@ -9,7 +9,7 @@ import type { import type { ApiPoll, MediaContainer, MediaContent, StatefulMediaContent, } from '../../api/types/messages'; -import type { LangFn } from '../../hooks/useOldLang'; +import type { OldLangFn } from '../../hooks/useOldLang'; import type { ThreadId } from '../../types'; import type { GlobalState } from '../types'; import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types'; @@ -208,7 +208,7 @@ export function isAnonymousOwnMessage(message: ApiMessage) { return Boolean(message.senderId) && !isUserId(message.senderId) && isOwnMessage(message); } -export function getSenderTitle(lang: LangFn, sender: ApiPeer) { +export function getSenderTitle(lang: OldLangFn, sender: ApiPeer) { return isPeerUser(sender) ? getUserFullName(sender) : getChatTitle(lang, sender); } @@ -344,10 +344,10 @@ export function extractMessageText(message: ApiMessage | ApiStory, inChatList = return { text, entities }; } -export function getExpiredMessageDescription(langFn: LangFn, message: ApiMessage): string | undefined { +export function getExpiredMessageDescription(langFn: OldLangFn, message: ApiMessage): string | undefined { return getExpiredMessageContentDescription(langFn, message.content); } -export function getExpiredMessageContentDescription(langFn: LangFn, mediaContent: MediaContent): string | undefined { +export function getExpiredMessageContentDescription(langFn: OldLangFn, mediaContent: MediaContent): string | undefined { const { isExpiredVoice, isExpiredRoundVideo } = mediaContent; if (isExpiredVoice) { return langFn('Message.VoiceMessageExpired'); diff --git a/src/global/helpers/renderMessageSummaryHtml.ts b/src/global/helpers/renderMessageSummaryHtml.ts index 394a2c77d..39ffb627e 100644 --- a/src/global/helpers/renderMessageSummaryHtml.ts +++ b/src/global/helpers/renderMessageSummaryHtml.ts @@ -1,5 +1,5 @@ import type { ApiMessage } from '../../api/types'; -import type { LangFn } from '../../hooks/useOldLang'; +import type { OldLangFn } from '../../hooks/useOldLang'; import { renderMessageText } from '../../components/common/helpers/renderMessageText'; import { getGlobal } from '..'; @@ -7,7 +7,7 @@ import { getMessageStatefulContent } from './messages'; import { getMessageSummaryDescription, getMessageSummaryEmoji } from './messageSummary'; export function renderMessageSummaryHtml( - lang: LangFn, + lang: OldLangFn, message: ApiMessage, ) { const global = getGlobal(); diff --git a/src/global/helpers/users.ts b/src/global/helpers/users.ts index 74c23bd05..0ab94b8b5 100644 --- a/src/global/helpers/users.ts +++ b/src/global/helpers/users.ts @@ -1,5 +1,5 @@ import type { ApiPeer, ApiUser, ApiUserStatus } from '../../api/types'; -import type { LangFn } from '../../hooks/useOldLang'; +import type { OldLangFn } from '../../hooks/useOldLang'; import { ANONYMOUS_USER_ID, SERVICE_NOTIFICATIONS_USER_ID } from '../../config'; import { formatFullDate, formatTime } from '../../util/dates/dateFormat'; @@ -66,7 +66,7 @@ export function getUserFullName(user?: ApiUser) { } export function getUserStatus( - lang: LangFn, user: ApiUser, userStatus: ApiUserStatus | undefined, + lang: OldLangFn, user: ApiUser, userStatus: ApiUserStatus | undefined, ) { if (user.id === SERVICE_NOTIFICATIONS_USER_ID) { return lang('ServiceNotifications'); diff --git a/src/global/types.ts b/src/global/types.ts index db8dca3da..da0024e95 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -36,6 +36,7 @@ import type { ApiInputInvoiceStarGift, ApiInputMessageReplyInfo, ApiKeyboardButton, + ApiLanguage, ApiMediaFormat, ApiMessage, ApiMessageEntity, @@ -115,7 +116,6 @@ import type { InlineBotSettings, ISettings, IThemeSettings, - LangCode, LoadMoreDirection, ManagementProgress, ManagementScreens, @@ -1199,7 +1199,7 @@ export type GlobalState = { defaultTopicIconsId?: string; defaultStatusIconsId?: string; premiumGifts?: ApiStickerSet; - emojiKeywords: Partial>; + emojiKeywords: Record; gifs: { saved: { @@ -1243,6 +1243,7 @@ export type GlobalState = { notifyExceptions?: Record; lastPremiumBandwithNotificationDate?: number; paidReactionPrivacy?: boolean; + languages?: ApiLanguage[]; }; push?: { @@ -1377,7 +1378,7 @@ export interface ActionPayloads { faveSticker: { sticker: ApiSticker } & WithTabId; unfaveSticker: { sticker: ApiSticker }; toggleStickerSet: { stickerSetId: string }; - loadEmojiKeywords: { language: LangCode }; + loadEmojiKeywords: { language: string }; // groups togglePreHistoryHidden: { @@ -1484,7 +1485,7 @@ export interface ActionPayloads { updateContentSettings: boolean; loadCountryList: { - langCode?: LangCode; + langCode?: string; }; ensureTimeFormat: WithTabId | undefined; diff --git a/src/hooks/useConnectionStatus.ts b/src/hooks/useConnectionStatus.ts index 402d568e7..3599c7358 100644 --- a/src/hooks/useConnectionStatus.ts +++ b/src/hooks/useConnectionStatus.ts @@ -1,5 +1,5 @@ import type { GlobalState } from '../global/types'; -import type { LangFn } from './useOldLang'; +import type { OldLangFn } from './useOldLang'; import useBrowserOnline from './window/useBrowserOnline'; @@ -16,7 +16,7 @@ type ConnectionStatusPosition = | 'none'; export default function useConnectionStatus( - lang: LangFn, + lang: OldLangFn, connectionState: GlobalState['connectionState'], isSyncing: boolean | undefined, hasMiddleHeader: boolean, diff --git a/src/hooks/useOldLang.ts b/src/hooks/useOldLang.ts index c90b4ff33..c391095a2 100644 --- a/src/hooks/useOldLang.ts +++ b/src/hooks/useOldLang.ts @@ -2,11 +2,11 @@ import * as langProvider from '../util/oldLangProvider'; import useEffectOnce from './useEffectOnce'; import useForceUpdate from './useForceUpdate'; -export type LangFn = langProvider.LangFn; +export type OldLangFn = langProvider.LangFn; /** * @deprecated */ -const useOldLang = (): LangFn => { +const useOldLang = (): OldLangFn => { const forceUpdate = useForceUpdate(); useEffectOnce(() => { diff --git a/src/lib/gramjs/client/TelegramClient.js b/src/lib/gramjs/client/TelegramClient.js index 12c1d8a34..cfd83e782 100644 --- a/src/lib/gramjs/client/TelegramClient.js +++ b/src/lib/gramjs/client/TelegramClient.js @@ -73,6 +73,7 @@ class TelegramClient { systemVersion: undefined, appVersion: undefined, langCode: 'en', + langPack: 'weba', systemLangCode: 'en', baseLogger: 'gramjs', useWSS: false, @@ -158,7 +159,7 @@ class TelegramClient { .toString() || '1.0', appVersion: args.appVersion || '1.0', langCode: args.langCode, - langPack: 'weba', + langPack: args.langPack, systemLangCode: args.systemLangCode, query: x, proxy: undefined, // no proxies yet. diff --git a/src/types/index.ts b/src/types/index.ts index a18bdb96c..80ca274bd 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -11,7 +11,6 @@ import type { ApiExportedInvite, ApiFakeType, ApiLabeledPrice, - ApiLanguage, ApiMessage, ApiPhoto, ApiReaction, @@ -113,8 +112,7 @@ export interface ISettings extends NotifySettings, Record { shouldSuggestCustomEmoji: boolean; shouldUpdateStickerSetOrder: boolean; hasPassword?: boolean; - languages?: ApiLanguage[]; - language: LangCode; + language: string; isSensitiveEnabled?: boolean; canChangeSensitive?: boolean; timeFormat: TimeFormat; @@ -481,8 +479,8 @@ export type NotifyException = { export type EmojiKeywords = { isLoading?: boolean; - version: number; - keywords: Record; + version?: number; + keywords?: Record; }; export type InlineBotSettings = { diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 31a0d347c..05c117f3c 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -17,17 +17,7 @@ export interface LangPair { 'DeleteChatUser': undefined; 'AccDescrGroup': undefined; 'AccDescrChannel': undefined; - 'ChatServiceGroupUpdatedPinnedMessage1': { - 'user': string | number; - 'message': string | number; - }; - 'MessagePinnedGenericMessage': { - 'user': string | number; - }; 'Nothing': undefined; - 'UserTyping': { - 'user': string | number; - }; 'SendActionRecordVideo': undefined; 'SendActionUploadVideo': undefined; 'SendActionRecordAudio': undefined; @@ -38,74 +28,12 @@ export interface LangPair { 'SendActionRecordRound': undefined; 'SendActionUploadRound': undefined; 'SendActionChooseSticker': undefined; - 'UserActionWatchingAnimations': { - 'emoji': string | number; - }; - 'SetUrlAvailable': { - 'url': string | number; - }; 'SetUrlInUse': undefined; - 'UsernameAvailable': { - 'username': string | number; - }; 'UsernameInUse': undefined; 'CreateGroupError': undefined; 'PasscodeControllerErrorCurrent': undefined; - 'LimitReachedChatInFolders': { - 'limit': string | number; - 'limit2': string | number; - }; - 'LimitReachedFileSize': { - 'limit': string | number; - 'limit2': string | number; - }; - 'LimitReachedFolders': { - 'limit': string | number; - 'limit2': string | number; - }; - 'LimitReachedPinDialogs': { - 'limit': string | number; - 'limit2': string | number; - }; - 'LimitReachedPublicLinks': { - 'limit2': string | number; - }; - 'LimitReachedCommunities': { - 'limit': string | number; - 'limit2': string | number; - }; - 'LimitReachedChatInFoldersLocked': { - 'limit': string | number; - }; - 'LimitReachedFileSizeLocked': { - 'limit': string | number; - }; - 'LimitReachedFoldersLocked': { - 'limit': string | number; - }; - 'LimitReachedPinDialogsLocked': { - 'limit': string | number; - }; 'LimitReachedPublicLinksLocked': undefined; - 'LimitReachedCommunitiesLocked': { - 'limit': string | number; - }; - 'LimitReachedChatInFoldersPremium': { - 'limit': string | number; - }; - 'LimitReachedFileSizePremium': { - 'limit': string | number; - }; - 'LimitReachedFoldersPremium': { - 'limit': string | number; - }; - 'LimitReachedPinDialogsPremium': { - 'limit': string | number; - }; 'LimitReachedPublicLinksPremium': undefined; - 'LimitReachedCommunitiesPremium': { - 'limit': string | number; - }; 'PremiumPreviewLimits': undefined; 'PremiumPreviewReactions': undefined; 'PremiumPreviewStickers': undefined; @@ -116,13 +44,6 @@ export interface LangPair { 'PremiumPreviewUploads': undefined; 'PremiumPreviewAdvancedChatManagement': undefined; 'PremiumPreviewAnimatedProfiles': undefined; - 'PremiumPreviewLimitsDescription': { - 'limit1': string | number; - 'limit2': string | number; - 'limit3': string | number; - 'limit4': string | number; - 'limit5': string | number; - }; 'PremiumPreviewReactionsDescription': undefined; 'PremiumPreviewStickersDescription': undefined; 'PremiumPreviewNoAdsDescription': undefined; @@ -251,9 +172,6 @@ export interface LangPair { 'ThisIsYou': undefined; 'VoipGroupMutedForMe': undefined; 'WantsToSpeak': undefined; - 'SpeakingWithVolume': { - 'volume': string | number; - }; 'Speaking': undefined; 'Listening': undefined; 'VoipGroupInviteMember': undefined; @@ -265,9 +183,6 @@ export interface LangPair { 'VoipGroupMuteForMe': undefined; 'VoipGroupUserRemove': undefined; 'Back': undefined; - 'Participants': { - 'count': string | number; - }; 'VoipChatJoin': undefined; 'CallStatusHanging': undefined; 'CallStatusRequesting': undefined; @@ -275,9 +190,6 @@ export interface LangPair { 'CallStatusIncoming': undefined; 'CallStatusWaiting': undefined; 'CallStatusExchanging': undefined; - 'CallEmojiKeyTooltip': { - 'user': string | number; - }; 'CallMuteAudio': undefined; 'CallUnmuteAudio': undefined; 'CallStopVideo': undefined; @@ -297,13 +209,6 @@ export interface LangPair { 'SponsoredMessageAlertLearnMoreUrl': undefined; 'SponsoredMessageInfoDescription4': undefined; 'Close': undefined; - 'ConversationScheduleMessageSendToday': { - 'time': string | number; - }; - 'ConversationScheduleMessageSendOn': { - 'date': string | number; - 'time': string | number; - }; 'Phone': undefined; 'Username': undefined; 'UserBio': undefined; @@ -314,27 +219,10 @@ export interface LangPair { 'BlockedListNotFound': undefined; 'TextCopied': undefined; 'Copy': undefined; - 'ChatListDeleteAndLeaveGroupConfirmation': { - 'chat': string | number; - }; - 'ChannelLeaveAlertWithName': { - 'chat': string | number; - }; - 'ChatListDeleteChatConfirmation': { - 'chat': string | number; - }; 'DeleteAndStop': undefined; - 'ChatListDeleteForEveryone': { - 'user': string | number; - }; 'DeleteForAll': undefined; 'DeleteSingleMessagesTitle': undefined; 'AreYouSureDeleteSingleMessage': undefined; - 'DeleteForMeChatHint': undefined; - 'DeleteForEveryoneHint': undefined; - 'ConversationDeleteMessagesFor': { - 'user': string | number; - }; 'ConversationDeleteMessagesForEveryone': undefined; 'ChatListDeleteForCurrentUser': undefined; 'Delete': undefined; @@ -344,24 +232,12 @@ export interface LangPair { 'SetReminder': undefined; 'ScheduleMessage': undefined; 'Updating': undefined; - 'OnlineCount': { - 'count': string | number; - }; - 'Subscribers': { - 'count': string | number; - }; - 'NMembers': { - 'count': string | number; - }; 'SelectChat': undefined; 'PinMessageAlertChannel': undefined; 'PinMessageAlert': undefined; 'PinMessageAlertChat': undefined; 'PinMessageAlertTitle': undefined; 'DialogPin': undefined; - 'ConversationPinMessagesFor': { - 'user': string | number; - }; 'ConversationPinMessageAlertPinAndNotifyMembers': undefined; 'SavedMessages': undefined; 'AccDescrPrevious': undefined; @@ -381,9 +257,6 @@ export interface LangPair { 'DeleteFromRecent': undefined; 'AccDescrStickerSet': undefined; 'ChatPanelUnpinAllMessages': undefined; - 'ChatUnpinAllMessagesConfirmation': { - 'count': string | number; - }; 'DialogUnpin': undefined; 'ArchivedChats': undefined; 'FilterAddTo': undefined; @@ -418,9 +291,6 @@ export interface LangPair { 'GroupName': undefined; 'DescriptionOptionalPlaceholder': undefined; 'DescriptionInfo': undefined; - 'GroupInfoParticipantCount': { - 'count': string | number; - }; 'ChannelIntroCreateChannel': undefined; 'NewMessageTitle': undefined; 'ChatListSearchNoResults': undefined; @@ -463,12 +333,6 @@ export interface LangPair { 'AuthSessionsViewAcceptTitle': undefined; 'SessionPreviewAcceptSecret': undefined; 'SessionPreviewAcceptCalls': undefined; - 'Weeks': { - 'count': string | number; - }; - 'Months': { - 'count': string | number; - }; 'AuthSessionsCurrentSession': undefined; 'TerminateAllSessions': undefined; 'OtherSessions': undefined; @@ -482,12 +346,6 @@ export interface LangPair { 'AuthSessionsLogOutApplications': undefined; 'ClearOtherWebSessionsHelp': undefined; 'AreYouSureWebSessions': undefined; - 'AutodownloadSizeLimitUpTo': { - 'limit': string | number; - }; - 'FileSizeMB': { - 'count': string | number; - }; 'AutoDownloadMaxFileSize': undefined; 'AutoDownloadSettingsContacts': undefined; 'AutoDownloadSettingsPrivateChats': undefined; @@ -570,9 +428,6 @@ export interface LangPair { 'P2PEverybody': undefined; 'P2PContacts': undefined; 'P2PNobody': undefined; - 'Users': { - 'count': string | number; - }; 'PrivacySettingsWebSessions': undefined; 'PasswordOn': undefined; 'PasswordOff': undefined; @@ -600,9 +455,6 @@ export interface LangPair { 'NeverAllow': undefined; 'AlwaysAllowPlaceholder': undefined; 'NeverAllowPlaceholder': undefined; - 'StickerPackStickerCount': { - 'count': string | number; - }; 'PleaseEnterPassword': undefined; 'PasswordHintPlaceholder': undefined; 'TwoStepVerificationPasswordSetInfo': undefined; @@ -617,16 +469,7 @@ export interface LangPair { 'YourEmailSkipWarningText': undefined; 'SetAdditionalPasswordInfo': undefined; 'EditAdminTransferSetPassword': undefined; - 'WebAppAddToAttachmentText': { - 'bot': string | number; - }; 'BotOpenPageTitle': undefined; - 'BotPermissionGameAlert': { - 'bot': string | number; - }; - 'BotOpenPageMessage': { - 'bot': string | number; - }; 'FilterDeleteAlert': undefined; 'RequestToJoinChannelSentDescription': undefined; 'RequestToJoinGroupSentDescription': undefined; @@ -646,43 +489,18 @@ export interface LangPair { 'PasscodeWrong': undefined; 'PasscodeEnterPasscodePlaceholder': undefined; 'MobileHidden': undefined; - 'NewContactPhoneHiddenText': { - 'user': string | number; - }; 'NewContactShare': undefined; - 'AddContactSharedContactExceptionInfo': { - 'user': string | number; - }; 'ContactPhone': undefined; 'NewContact': undefined; 'Done': undefined; - 'FileSizeGB': { - 'count': string | number; - }; 'LimitReached': undefined; 'IncreaseLimit': undefined; 'LimitFree': undefined; 'LimitPremium': undefined; - 'SubscribeToPremium': { - 'price': string | number; - }; - 'TelegramPremiumUserDialogTitle': { - 'user': string | number; - }; 'TelegramPremiumSubscribedTitle': undefined; 'AboutPremiumDescription': undefined; 'AboutPremiumDescription2': undefined; 'OpenUrlTitle': undefined; - 'OpenUrlAlert2': { - 'url': string | number; - }; - 'ConversationOpenBotLinkLogin': { - 'url': string | number; - 'user': string | number; - }; - 'ConversationOpenBotLinkAllowMessages': { - 'bot': string | number; - }; 'BotWebViewOpenBot': undefined; 'WebAppReloadPage': undefined; 'WebAppRemoveBot': undefined; @@ -699,38 +517,13 @@ export interface LangPair { 'NewContactAdd': undefined; 'NewContactBlock': undefined; 'ReportSpamAndLeave': undefined; - 'BlockUserTitle': { - 'user': string | number; - }; - 'UserInfoBlockConfirmationTitle': { - 'user': string | number; - }; 'ChatConfirmReportSpamChannel': undefined; 'Block': undefined; 'DeleteThisChat': undefined; - 'ReportChat': { - 'peer': string | number; - }; - 'PreviewSenderSendPhoto': { - 'count': string | number; - }; - 'PreviewSenderSendVideo': { - 'count': string | number; - }; - 'PreviewSenderSendAudio': { - 'count': string | number; - }; - 'PreviewSenderSendFile': { - 'count': string | number; - }; - 'PreviewDraggingAddItems': undefined; 'Caption': undefined; 'AttachmentMenuPhotoOrVideo': undefined; 'AttachDocument': undefined; 'Poll': undefined; - 'SlowModeHint': { - 'time': string | number; - }; 'SendMessageAsTitle': undefined; 'Message': undefined; 'RecentStickers': undefined; @@ -782,9 +575,6 @@ export interface LangPair { 'EventLogFilterPinnedMessages': undefined; 'UnpinMessageAlertTitle': undefined; 'PinnedMessage': undefined; - 'Comments': { - 'count': string | number; - }; 'LeaveAComment': undefined; 'PollsStopWarning': undefined; 'PollsStopSure': undefined; @@ -795,7 +585,6 @@ export interface LangPair { 'DiscussChannel': undefined; 'ForwardedMessage': undefined; 'ContextForwardMsg': undefined; - 'ShareLinkCopied': undefined; 'MessageScheduleSend': undefined; 'MessageScheduleEditTime': undefined; 'Reply': undefined; @@ -808,34 +597,15 @@ export interface LangPair { 'MediaDownload': undefined; 'CommonSelect': undefined; 'ContextReportMsg': undefined; - 'ChatContextReactionCount': { - 'count': string | number; - }; - 'ConversationContextMenuSeen': { - 'count': string | number; - }; 'ConversationContextMenuNoViews': undefined; 'HideAd': undefined; - 'EditedDate': { - 'date': string | number; - }; - 'ForwardedDate': { - 'date': string | number; - }; 'EditedMessage': undefined; 'CallAgain': undefined; 'CallBack': undefined; - 'CallMessageWithDuration': { - 'time': string | number; - 'duration': string | number; - }; 'PollSubmitVotes': undefined; 'PollViewResults': undefined; 'ChatQuizTotalVotesEmpty': undefined; 'ChatPollTotalVotesResultEmpty': undefined; - 'Answer': { - 'count': string | number; - }; 'Vote': undefined; 'MessageRecommendedLabel': undefined; 'SponsoredMessage': undefined; @@ -847,29 +617,11 @@ export interface LangPair { 'UnreadMessages': undefined; 'DiscussionStarted': undefined; 'MessageScheduledUntilOnline': undefined; - 'MessageScheduledOn': { - 'date': string | number; - }; - 'VoiceOverChatMessagesSelected': { - 'count': string | number; - }; 'ChatForwardActionHeader': undefined; 'ConversationReportMessages': undefined; 'ContextCopySelectedItems': undefined; 'EditAdminGroupDeleteMessages': undefined; - 'ChatPinnedUnpinAll': { - 'count': string | number; - }; - 'CommentsCount': { - 'count': string | number; - }; - 'PinnedMessagesCount': { - 'count': string | number; - }; 'Reminders': undefined; - 'Messages': { - 'count': string | number; - }; 'ScheduledMessagesEmptyPlaceholder': undefined; 'ConversationCloudStorageInfoTitle': undefined; 'ConversationClousStorageInfoDescription1': undefined; @@ -878,18 +630,12 @@ export interface LangPair { 'ConversationClousStorageInfoDescription4': undefined; 'EmptyGroupInfoTitle': undefined; 'EmptyGroupInfoSubtitle': undefined; - 'EmptyGroupInfoLine1': { - 'count': string | number; - }; 'EmptyGroupInfoLine2': undefined; 'EmptyGroupInfoLine3': undefined; 'EmptyGroupInfoLine4': undefined; 'Reactions': undefined; 'MarkAllAsRead': undefined; 'PaymentCardNumber': undefined; - 'PaymentCheckoutAcceptRecurrent': { - 'bot': string | number; - }; 'CheckoutTotalAmount': undefined; 'PaymentCheckoutMethod': undefined; 'PaymentCheckoutProvider': undefined; @@ -910,9 +656,6 @@ export interface LangPair { 'PaymentShippingMethod': undefined; 'PaymentCardInfo': undefined; 'PaymentCheckout': undefined; - 'CheckoutPayPrice': { - 'amount': string | number; - }; 'PaymentReceipt': undefined; 'PaymentShippingAddress1Placeholder': undefined; 'PaymentShippingAddress2Placeholder': undefined; @@ -926,9 +669,6 @@ export interface LangPair { 'PaymentShippingSaveInfo': undefined; 'ChannelAddUsers': undefined; 'GroupRemovedRemove': undefined; - 'PeerInfoConfirmRemovePeer': { - 'user': string | number; - }; 'BoxRemove': undefined; 'NoGIFsFound': undefined; 'ChannelAddToChannel': undefined; @@ -949,9 +689,6 @@ export interface LangPair { 'ChannelDeleteAlert': undefined; 'ChannelLeaveAlert': undefined; 'ChannelCreator': undefined; - 'EditAdminPromotedBy': { - 'user': string | number; - }; 'ChannelAdmin': undefined; 'EventLog': undefined; 'EventLogInfoDetailChannel': undefined; @@ -966,9 +703,6 @@ export interface LangPair { 'ChannelVisibilityForwardingGroupTitle': undefined; 'ChannelVisibilityForwardingChannelInfo': undefined; 'ChannelVisibilityForwardingGroupInfo': undefined; - 'UserRemovedBy': { - 'user': string | number; - }; 'Unblock': undefined; 'NoBlockedChannel2': undefined; 'NoBlockedGroup2': undefined; @@ -976,12 +710,6 @@ export interface LangPair { 'DiscussionUnlinkGroup': undefined; 'DiscussionUnlinkChannel': undefined; 'ChannelDiscussionGroupLinkGroup': undefined; - 'DiscussionUnlinkChannelAlert': { - 'chat': string | number; - }; - 'DiscussionUnlinkGroupAlert': { - 'channel': string | number; - }; 'DiscussionChannelHelp': undefined; 'DiscussionCreateGroup': undefined; 'DiscussionChannelHelp2': undefined; @@ -997,9 +725,6 @@ export interface LangPair { 'ChatHistory': undefined; 'DeleteMega': undefined; 'AreYouSureDeleteAndExit': undefined; - 'AreYouSureDeleteThisChatWithGroup': { - 'chat': string | number; - }; 'DeleteGroupForAll': undefined; 'EditAdminWhatCanDo': undefined; 'EditAdminChangeChannelInfo': undefined; @@ -1040,12 +765,6 @@ export interface LangPair { 'LinkNameHint': undefined; 'LinkNameHelp': undefined; 'LimitByPeriod': undefined; - 'Hours': { - 'count': string | number; - }; - 'Days': { - 'count': string | number; - }; 'NoLimit': undefined; 'GroupInviteExpireCustom': undefined; 'TimeLimitHelp': undefined; @@ -1055,31 +774,13 @@ export interface LangPair { 'SaveLink': undefined; 'CreateLink': undefined; 'LinkCopied': undefined; - 'PeopleJoined': { - 'count': string | number; - }; 'NoOneJoined': undefined; - 'PeopleCanJoinViaLinkCount': { - 'count': string | number; - }; 'NoOneJoinedYet': undefined; 'CopyLink': undefined; 'ExpiredLink': undefined; - 'LinkExpiresIn': { - 'time': string | number; - }; 'LinkCreatedeBy': undefined; - 'CanJoin': { - 'count': string | number; - }; 'Revoked': undefined; - 'JoinRequests': { - 'count': string | number; - }; 'LinkLimitReached': undefined; - 'InviteLinkExpiresIn': { - 'time': string | number; - }; 'InviteLinkExpired': undefined; 'Permanent': undefined; 'DeleteLink': undefined; @@ -1135,40 +836,16 @@ export interface LangPair { 'GraphZoomOut': undefined; 'ChannelStatsRecentHeader': undefined; 'StatisticOverview': undefined; - 'ChannelStatsViewsCount': { - 'count': string | number; - }; - 'ChannelStatsSharesCount': { - 'count': string | number; - }; 'Stickers': undefined; 'StickersInstalled': undefined; 'StickersInstall': undefined; 'CropImage': undefined; 'ReportPeerAlertSuccess': undefined; - 'UsernameByPhoneNotFound': { - 'phone': string | number; - }; 'WebAppAddToAttachmentUnavailableError': undefined; - 'LimitReachedFavoriteGifs': { - 'count': string | number; - }; 'LimitReachedFavoriteGifsSubtitlePremium': undefined; - 'LimitReachedFavoriteGifsSubtitle': { - 'count': string | number; - }; - 'LimitReachedFavoriteStickers': { - 'count': string | number; - }; 'LimitReachedFavoriteStickersSubtitlePremium': undefined; - 'LimitReachedFavoriteStickersSubtitle': { - 'count': string | number; - }; 'StickerPackErrorNotFound': undefined; 'ContactsPhoneNumberNotRegistred': undefined; - 'VoipPeerIncompatible': { - 'user': string | number; - }; 'NoUsernameFound': undefined; 'HiddenName': undefined; 'ChannelPersmissionDeniedSendMessagesForever': undefined; @@ -1194,21 +871,6 @@ export interface LangPair { 'WithinAWeek': undefined; 'LastSeenOffline': undefined; 'LastSeenJustNow': undefined; - 'LastSeenMinutesAgo': { - 'count': string | number; - }; - 'LastSeenHoursAgo': { - 'count': string | number; - }; - 'LastSeenTodayAt': { - 'time': string | number; - }; - 'LastSeenYesterdayAt': { - 'time': string | number; - }; - 'LastSeenAtDate': { - 'date': string | number; - }; 'Online': undefined; 'Lately': undefined; 'VoipMutedTapedForSpeak': undefined; @@ -1233,19 +895,6 @@ export interface LangPair { 'WeekdayYesterday': undefined; 'User': undefined; 'SecretChat': undefined; - 'ChannelPermissionDeniedSendMessagesUntil': { - 'date': string | number; - }; - 'FormatDateAtTime': { - 'date': string | number; - 'time': string | number; - }; - 'StickerPackRemoveStickerCount': { - 'count': string | number; - }; - 'StickerPackAddStickerCount': { - 'count': string | number; - }; 'ChatListFilterErrorEmpty': undefined; 'ChatListFilterErrorTitleEmpty': undefined; 'FilterMuted': undefined; @@ -1260,29 +909,8 @@ export interface LangPair { 'CaptionsLimitTitle': undefined; 'FoldersLimitTitle': undefined; 'ChatPerFolderLimitTitle': undefined; - 'GroupsAndChannelsLimitSubtitle': { - 'count': string | number; - }; - 'PinChatsLimitSubtitle': { - 'count': string | number; - }; - 'PublicLinksLimitSubtitle': { - 'count': string | number; - }; - 'SavedGifsLimitSubtitle': { - 'count': string | number; - }; - 'FavoriteStickersLimitSubtitle': { - 'count': string | number; - }; 'BioLimitSubtitle': undefined; 'CaptionsLimitSubtitle': undefined; - 'FoldersLimitSubtitle': { - 'count': string | number; - }; - 'ChatPerFolderLimitSubtitle': { - 'count': string | number; - }; 'TelegramPremiumUserDialogSubtitle': undefined; 'TelegramPremiumSubscribedSubtitle': undefined; 'TelegramPremiumSubtitle': undefined; @@ -1297,10 +925,6 @@ export interface LangPair { 'Emoji8': undefined; 'GroupInfoDeleteAndExit': undefined; 'HidAccount': undefined; - 'ChatOutgoingContextMixedReactionCount': { - 'count': string | number; - 'total': string | number; - }; 'ConversationViewBot': undefined; 'ConversationViewPost': undefined; 'ConversationViewChannel': undefined; @@ -1315,34 +939,7 @@ export interface LangPair { 'WaitingForNetwork': undefined; 'ScheduleSendWhenOnline': undefined; 'VoipIncoming': undefined; - 'FileSizeB': { - 'count': string | number; - }; - 'FileSizeKB': { - 'count': string | number; - }; - 'Years': { - 'count': string | number; - }; - 'MessageTimerShortHours': { - 'count': string | number; - }; - 'MessageTimerShortDays': { - 'count': string | number; - }; 'LiveLocationUpdatedJustNow': undefined; - 'LiveLocationUpdatedMinutesAgo': { - 'count': string | number; - }; - 'LiveLocationUpdatedTodayAt': { - 'time': string | number; - }; - 'Seconds': { - 'count': string | number; - }; - 'Minutes': { - 'count': string | number; - }; 'AudioPause': undefined; 'AudioPlay': undefined; 'ToggleUserNotifications': undefined; @@ -1407,9 +1004,6 @@ export interface LangPair { 'TooManyTabsReload': undefined; 'SlowmodeEnabled': undefined; 'SomethingWentWrong': undefined; - 'MediaViewDownloading': { - 'count': string | number; - }; 'VideoPlayerBuffering': undefined; 'VideoPlayerFullscreen': undefined; 'PlayerVolume': undefined; @@ -1433,9 +1027,6 @@ export interface LangPair { 'AriaOpenSymbolMenu': undefined; 'AriaComposerOpenScheduled': undefined; 'AriaComposerCancelVoice': undefined; - 'PreviewForwardedMessage': { - 'count': string | number; - }; 'PreviewEditMessage': undefined; 'FileDropZoneTitle': undefined; 'FileDropZoneQuick': undefined; @@ -1460,15 +1051,8 @@ export interface LangPair { 'ChatMemberListNoAccess': undefined; 'NoMembersFound': undefined; 'Profile': undefined; - 'SearchMessagesFound': { - 'count': string | number; - }; 'ChannelManagementAddAdminDescription': undefined; 'GroupManagementAddAdminDescription': undefined; - 'ChannelManagementLinkDiscussion': { - 'group': string | number; - 'channel': string | number; - }; 'ChannelManagementLinkPrivate': undefined; 'NoDiscussionsLinked': undefined; 'ManagementRemoveAdminConfirm': undefined; @@ -1488,12 +1072,6 @@ export interface LangPair { 'FoldersAllChatsDesc': undefined; 'RemoveSymbol': undefined; 'FocusMessage': undefined; - 'ShowMoreChats': { - 'count': string | number; - }; - 'ShowMoreVoters': { - 'count': string | number; - }; 'HiddenSendersNameDescription': undefined; 'ShowSenderNames': undefined; 'ShowSendersName': undefined; @@ -1512,190 +1090,623 @@ export interface LangPair { 'MenuInstallApp': undefined; 'RemoveEffect': undefined; 'ReplyInPrivateMessage': undefined; - 'ProfileOpenAppAbout': { - 'terms': string | number; - }; 'ProfileOpenAppTerms': undefined; 'ProfileBotOpenAppInfoLink': undefined; 'MonetizationInfoTONTitle': undefined; - 'ChannelEarnLearnCoinAbout': { - 'link': string | number; - }; - 'MonetizationBalanceZeroInfo': { - 'link': string | number; - }; - 'ChannelEarnAbout': { - 'link': string | number; - }; 'AriaSearchOlderResult': undefined; 'AriaSearchNewerResult': undefined; - 'CreditsBoxHistoryEntryGiftOutAbout': { - 'user': string | number; - 'link': string | number; - }; - 'StarsTransactionTOS': { - 'link': string | number; - }; 'StarsTransactionTOSLinkText': undefined; 'StarsTransactionTOSLink': undefined; - 'GiftStarsOutgoing': { - 'user': string | number; - }; 'GiftPremiumHeader': undefined; - 'GiftPremiumDescription': { - 'user': string | number; - 'link': string | number; - }; 'GiftPremiumDescriptionLinkCaption': undefined; 'GiftPremiumDescriptionLink': undefined; 'StarsGiftHeader': undefined; - 'StarGiftDescription': { - 'user': string | number; - }; 'GiftLimited': undefined; - 'GiftDiscount': { - 'percent': string | number; - }; - 'GiftSoldCount': { - 'count': string | number; - }; - 'GiftLeftCount': { - 'count': string | number; - }; 'GiftSoldOut': undefined; 'GiftMessagePlaceholder': undefined; 'GiftHideMyName': undefined; - 'GiftHideNameDescription': { - 'profile': string | number; - 'receiver': string | number; - }; - 'GiftSend': { - 'amount': string | number; - }; 'GiftInfoSent': undefined; 'GiftInfoReceived': undefined; 'GiftInfoTitle': undefined; - 'GiftInfoDescription': { - 'amount': string | number; - }; - 'GiftInfoDescriptionOut': { - 'user': string | number; - 'amount': string | number; - }; - 'GiftInfoDescriptionConverted': { - 'amount': string | number; - }; - 'GiftInfoDescriptionOutConverted': { - 'user': string | number; - 'amount': string | number; - }; 'GiftInfoFrom': undefined; 'GiftInfoDate': undefined; 'GiftInfoValue': undefined; 'GiftInfoMakeVisible': undefined; 'GiftInfoMakeInvisible': undefined; - 'GiftInfoConvert': { - 'amount': string | number; - }; 'GiftInfoConvertTitle': undefined; - 'GiftInfoConvertDescription1': { - 'user': string | number; - 'amount': string | number; - }; 'GiftInfoConvertDescription2': undefined; - 'GiftInfoConvertDescriptionPeriod': { - 'count': string | number; - }; - 'GiftInfoSaved': { - 'link': string | number; - }; 'GiftInfoSavedView': undefined; 'GiftInfoHidden': undefined; 'GiftInfoAvailability': undefined; - 'GiftInfoAvailabilityValue': { - 'count': string | number; - 'total': string | number; - }; 'GiftInfoFirstSale': undefined; 'GiftInfoLastSale': undefined; 'GiftInfoSoldOutTitle': undefined; 'GiftInfoSoldOutDescription': undefined; 'GiftInfoSenderHidden': undefined; - 'StarsAmount': { - 'amount': string | number; - }; - 'StarsAmountText': { - 'amount': string | number; - }; 'AllGiftsCategory': undefined; 'LimitedGiftsCategory': undefined; 'PremiumGiftDescription': undefined; - 'SendPaidReaction': { - 'amount': string | number; - }; - 'StarsReactionTerms': { - 'link': string | number; - }; 'StarsReactionLinkText': undefined; 'StarsReactionLink': undefined; - 'MiniAppsMoreTabs': { - 'botName': string | number; - 'count': string | number; - }; - 'PrizeCredits': { - 'count': string | number; - }; - 'ActionStarGiftTitle': { - 'user': string | number; - 'count': string | number; - }; - 'ActionStarGiftOutTitle': { - 'count': string | number; - }; - 'ActionStarGiftOutDescription': { - 'user': string | number; - 'count': string | number; - }; - 'ActionStarGiftDescription': { - 'count': string | number; - }; 'ActionStarGiftDisplaying': undefined; 'GiftTo': undefined; 'GiftFrom': undefined; 'ReceivedGift': undefined; 'SentGift': undefined; - 'StarGiftInfoDescriptionInbound': { - 'count': string | number; - 'link': string | number; - }; - 'StarGiftInfoDescriptionOutgoing': { - 'user': string | number; - 'count': string | number; - 'link': string | number; - }; 'StarGiftInfoLinkCaption': undefined; 'StarGiftDisplayOnMyPage': undefined; 'StarGiftConvertTo': undefined; 'StarGiftHideFromMyPage': undefined; 'StarGiftSenderPrivacyNote': undefined; 'StarGiftAvailability': undefined; - 'StarGiftAvailabilityValue': { - 'number': string | number; - 'total': string | number; - }; - 'StarsSubscribeText': { - 'chat': string | number; - 'amount': string | number; - }; - 'StarsSubscribeInfo': { - 'link': string | number; - }; 'StarsSubscribeInfoLinkText': undefined; 'StarsSubscribeInfoLink': undefined; - 'StarsPerMonth': { - 'amount': string | number; - }; - } -export type LangKey = keyof LangPair; +export interface LangPairWithVariables { + 'ChatServiceGroupUpdatedPinnedMessage1': { + 'user': V; + 'message': V; + }; + 'MessagePinnedGenericMessage': { + 'user': V; + }; + 'UserTyping': { + 'user': V; + }; + 'UserActionWatchingAnimations': { + 'emoji': V; + }; + 'SetUrlAvailable': { + 'url': V; + }; + 'UsernameAvailable': { + 'username': V; + }; + 'LimitReachedChatInFolders': { + 'limit': V; + 'limit2': V; + }; + 'LimitReachedFileSize': { + 'limit': V; + 'limit2': V; + }; + 'LimitReachedFolders': { + 'limit': V; + 'limit2': V; + }; + 'LimitReachedPinDialogs': { + 'limit': V; + 'limit2': V; + }; + 'LimitReachedPublicLinks': { + 'limit2': V; + }; + 'LimitReachedCommunities': { + 'limit': V; + 'limit2': V; + }; + 'LimitReachedChatInFoldersLocked': { + 'limit': V; + }; + 'LimitReachedFileSizeLocked': { + 'limit': V; + }; + 'LimitReachedFoldersLocked': { + 'limit': V; + }; + 'LimitReachedPinDialogsLocked': { + 'limit': V; + }; + 'LimitReachedCommunitiesLocked': { + 'limit': V; + }; + 'LimitReachedChatInFoldersPremium': { + 'limit': V; + }; + 'LimitReachedFileSizePremium': { + 'limit': V; + }; + 'LimitReachedFoldersPremium': { + 'limit': V; + }; + 'LimitReachedPinDialogsPremium': { + 'limit': V; + }; + 'LimitReachedCommunitiesPremium': { + 'limit': V; + }; + 'PremiumPreviewLimitsDescription': { + 'limit1': V; + 'limit2': V; + 'limit3': V; + 'limit4': V; + 'limit5': V; + }; + 'SpeakingWithVolume': { + 'volume': V; + }; + 'CallEmojiKeyTooltip': { + 'user': V; + }; + 'ConversationScheduleMessageSendToday': { + 'time': V; + }; + 'ConversationScheduleMessageSendOn': { + 'date': V; + 'time': V; + }; + 'ChatListDeleteAndLeaveGroupConfirmation': { + 'chat': V; + }; + 'ChannelLeaveAlertWithName': { + 'chat': V; + }; + 'ChatListDeleteChatConfirmation': { + 'chat': V; + }; + 'ChatListDeleteForEveryone': { + 'user': V; + }; + 'ConversationDeleteMessagesFor': { + 'user': V; + }; + 'ConversationPinMessagesFor': { + 'user': V; + }; + 'AutodownloadSizeLimitUpTo': { + 'limit': V; + }; + 'FileSizeMB': { + 'count': V; + }; + 'WebAppAddToAttachmentText': { + 'bot': V; + }; + 'BotPermissionGameAlert': { + 'bot': V; + }; + 'BotOpenPageMessage': { + 'bot': V; + }; + 'NewContactPhoneHiddenText': { + 'user': V; + }; + 'AddContactSharedContactExceptionInfo': { + 'user': V; + }; + 'FileSizeGB': { + 'count': V; + }; + 'SubscribeToPremium': { + 'price': V; + }; + 'TelegramPremiumUserDialogTitle': { + 'user': V; + }; + 'OpenUrlAlert2': { + 'url': V; + }; + 'ConversationOpenBotLinkLogin': { + 'url': V; + 'user': V; + }; + 'ConversationOpenBotLinkAllowMessages': { + 'bot': V; + }; + 'BlockUserTitle': { + 'user': V; + }; + 'UserInfoBlockConfirmationTitle': { + 'user': V; + }; + 'ReportChat': { + 'peer': V; + }; + 'SlowModeHint': { + 'time': V; + }; + 'EditedDate': { + 'date': V; + }; + 'ForwardedDate': { + 'date': V; + }; + 'CallMessageWithDuration': { + 'time': V; + 'duration': V; + }; + 'MessageScheduledOn': { + 'date': V; + }; + 'EmptyGroupInfoLine1': { + 'count': V; + }; + 'PaymentCheckoutAcceptRecurrent': { + 'bot': V; + }; + 'CheckoutPayPrice': { + 'amount': V; + }; + 'PeerInfoConfirmRemovePeer': { + 'user': V; + }; + 'EditAdminPromotedBy': { + 'user': V; + }; + 'UserRemovedBy': { + 'user': V; + }; + 'DiscussionUnlinkChannelAlert': { + 'chat': V; + }; + 'DiscussionUnlinkGroupAlert': { + 'channel': V; + }; + 'AreYouSureDeleteThisChatWithGroup': { + 'chat': V; + }; + 'LinkExpiresIn': { + 'time': V; + }; + 'InviteLinkExpiresIn': { + 'time': V; + }; + 'UsernameByPhoneNotFound': { + 'phone': V; + }; + 'LimitReachedFavoriteGifs': { + 'count': V; + }; + 'LimitReachedFavoriteGifsSubtitle': { + 'count': V; + }; + 'LimitReachedFavoriteStickers': { + 'count': V; + }; + 'LimitReachedFavoriteStickersSubtitle': { + 'count': V; + }; + 'VoipPeerIncompatible': { + 'user': V; + }; + 'LastSeenTodayAt': { + 'time': V; + }; + 'LastSeenYesterdayAt': { + 'time': V; + }; + 'LastSeenAtDate': { + 'date': V; + }; + 'ChannelPermissionDeniedSendMessagesUntil': { + 'date': V; + }; + 'FormatDateAtTime': { + 'date': V; + 'time': V; + }; + 'GroupsAndChannelsLimitSubtitle': { + 'count': V; + }; + 'PinChatsLimitSubtitle': { + 'count': V; + }; + 'PublicLinksLimitSubtitle': { + 'count': V; + }; + 'SavedGifsLimitSubtitle': { + 'count': V; + }; + 'FavoriteStickersLimitSubtitle': { + 'count': V; + }; + 'FoldersLimitSubtitle': { + 'count': V; + }; + 'ChatPerFolderLimitSubtitle': { + 'count': V; + }; + 'ChatOutgoingContextMixedReactionCount': { + 'count': V; + 'total': V; + }; + 'FileSizeB': { + 'count': V; + }; + 'FileSizeKB': { + 'count': V; + }; + 'MessageTimerShortHours': { + 'count': V; + }; + 'MessageTimerShortDays': { + 'count': V; + }; + 'LiveLocationUpdatedTodayAt': { + 'time': V; + }; + 'MediaViewDownloading': { + 'count': V; + }; + 'ChannelManagementLinkDiscussion': { + 'group': V; + 'channel': V; + }; + 'ProfileOpenAppAbout': { + 'terms': V; + }; + 'ChannelEarnLearnCoinAbout': { + 'link': V; + }; + 'MonetizationBalanceZeroInfo': { + 'link': V; + }; + 'ChannelEarnAbout': { + 'link': V; + }; + 'CreditsBoxHistoryEntryGiftOutAbout': { + 'user': V; + 'link': V; + }; + 'StarsTransactionTOS': { + 'link': V; + }; + 'GiftStarsOutgoing': { + 'user': V; + }; + 'GiftPremiumDescription': { + 'user': V; + 'link': V; + }; + 'StarGiftDescription': { + 'user': V; + }; + 'GiftDiscount': { + 'percent': V; + }; + 'GiftSoldCount': { + 'count': V; + }; + 'GiftLeftCount': { + 'count': V; + }; + 'GiftHideNameDescription': { + 'profile': V; + 'receiver': V; + }; + 'GiftSend': { + 'amount': V; + }; + 'GiftInfoConvertDescription1': { + 'user': V; + 'amount': V; + }; + 'GiftInfoSaved': { + 'link': V; + }; + 'StarsAmount': { + 'amount': V; + }; + 'SendPaidReaction': { + 'amount': V; + }; + 'StarsReactionTerms': { + 'link': V; + }; + 'PrizeCredits': { + 'count': V; + }; + 'ActionStarGiftTitle': { + 'user': V; + 'count': V; + }; + 'ActionStarGiftOutTitle': { + 'count': V; + }; + 'ActionStarGiftOutDescription': { + 'user': V; + 'count': V; + }; + 'ActionStarGiftDescription': { + 'count': V; + }; + 'StarGiftInfoDescriptionInbound': { + 'count': V; + 'link': V; + }; + 'StarGiftInfoDescriptionOutgoing': { + 'user': V; + 'count': V; + 'link': V; + }; + 'StarGiftAvailabilityValue': { + 'number': V; + 'total': V; + }; + 'StarsSubscribeInfo': { + 'link': V; + }; + 'StarsPerMonth': { + 'amount': V; + }; +} + +export interface LangPairPlural { + 'DeleteForMeChatHint': undefined; + 'DeleteForEveryoneHint': undefined; + 'PreviewDraggingAddItems': undefined; +} + +export interface LangPairPluralWithVariables { + 'Participants': { + 'count': V; + }; + 'OnlineCount': { + 'count': V; + }; + 'Subscribers': { + 'count': V; + }; + 'NMembers': { + 'count': V; + }; + 'ChatUnpinAllMessagesConfirmation': { + 'count': V; + }; + 'GroupInfoParticipantCount': { + 'count': V; + }; + 'Weeks': { + 'count': V; + }; + 'Months': { + 'count': V; + }; + 'Users': { + 'count': V; + }; + 'StickerPackStickerCount': { + 'count': V; + }; + 'PreviewSenderSendPhoto': { + 'count': V; + }; + 'PreviewSenderSendVideo': { + 'count': V; + }; + 'PreviewSenderSendAudio': { + 'count': V; + }; + 'PreviewSenderSendFile': { + 'count': V; + }; + 'Comments': { + 'count': V; + }; + 'ChatContextReactionCount': { + 'count': V; + }; + 'ConversationContextMenuSeen': { + 'count': V; + }; + 'Answer': { + 'count': V; + }; + 'VoiceOverChatMessagesSelected': { + 'count': V; + }; + 'ChatPinnedUnpinAll': { + 'count': V; + }; + 'CommentsCount': { + 'count': V; + }; + 'PinnedMessagesCount': { + 'count': V; + }; + 'Messages': { + 'count': V; + }; + 'Hours': { + 'count': V; + }; + 'Days': { + 'count': V; + }; + 'PeopleJoined': { + 'count': V; + }; + 'PeopleCanJoinViaLinkCount': { + 'count': V; + }; + 'CanJoin': { + 'count': V; + }; + 'JoinRequests': { + 'count': V; + }; + 'ChannelStatsViewsCount': { + 'count': V; + }; + 'ChannelStatsSharesCount': { + 'count': V; + }; + 'LastSeenMinutesAgo': { + 'count': V; + }; + 'LastSeenHoursAgo': { + 'count': V; + }; + 'StickerPackRemoveStickerCount': { + 'count': V; + }; + 'StickerPackAddStickerCount': { + 'count': V; + }; + 'Years': { + 'count': V; + }; + 'LiveLocationUpdatedMinutesAgo': { + 'count': V; + }; + 'Seconds': { + 'count': V; + }; + 'Minutes': { + 'count': V; + }; + 'PreviewForwardedMessage': { + 'count': V; + }; + 'SearchMessagesFound': { + 'count': V; + }; + 'ShowMoreChats': { + 'count': V; + }; + 'ShowMoreVoters': { + 'count': V; + }; + 'GiftInfoDescription': { + 'amount': V; + }; + 'GiftInfoDescriptionOut': { + 'user': V; + 'amount': V; + }; + 'GiftInfoDescriptionConverted': { + 'amount': V; + }; + 'GiftInfoDescriptionOutConverted': { + 'user': V; + 'amount': V; + }; + 'GiftInfoConvert': { + 'amount': V; + }; + 'GiftInfoConvertDescriptionPeriod': { + 'count': V; + }; + 'GiftInfoAvailabilityValue': { + 'count': V; + 'total': V; + }; + 'StarsAmountText': { + 'amount': V; + }; + 'MiniAppsMoreTabs': { + 'botName': V; + 'count': V; + }; + 'StarsSubscribeText': { + 'chat': V; + 'amount': V; + }; +} +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; diff --git a/src/util/dates/dateFormat.ts b/src/util/dates/dateFormat.ts index d6262ca6e..9aa981808 100644 --- a/src/util/dates/dateFormat.ts +++ b/src/util/dates/dateFormat.ts @@ -1,4 +1,4 @@ -import type { LangFn } from '../../hooks/useOldLang'; +import type { OldLangFn } from '../../hooks/useOldLang'; import type { TimeFormat } from '../../types'; import withCache from '../withCache'; @@ -39,7 +39,7 @@ function toIsoString(date: Date) { } // @optimization `toLocaleTimeString` is avoided because of bad performance -export function formatTime(lang: LangFn, datetime: number | Date) { +export function formatTime(lang: OldLangFn, datetime: number | Date) { const date = typeof datetime === 'number' ? new Date(datetime) : datetime; const timeFormat = lang.timeFormat || '24h'; @@ -53,7 +53,7 @@ export function formatTime(lang: LangFn, datetime: number | Date) { return `${String(hours).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}${marker}`; } -export function formatPastTimeShort(lang: LangFn, datetime: number | Date, alwaysShowTime = false) { +export function formatPastTimeShort(lang: OldLangFn, datetime: number | Date, alwaysShowTime = false) { const date = typeof datetime === 'number' ? new Date(datetime) : datetime; const time = formatTime(lang, date); @@ -76,16 +76,16 @@ export function formatPastTimeShort(lang: LangFn, datetime: number | Date, alway return alwaysShowTime ? lang('FullDateTimeFormat', [formattedDate, time]) : formattedDate; } -export function formatFullDate(lang: LangFn, datetime: number | Date) { +export function formatFullDate(lang: OldLangFn, datetime: number | Date) { return formatDateToString(datetime, lang.code, false, 'numeric'); } -export function formatMonthAndYear(lang: LangFn, date: Date, isShort = false) { +export function formatMonthAndYear(lang: OldLangFn, date: Date, isShort = false) { return formatDateToString(date, lang.code, false, isShort ? 'short' : 'long', true); } export function formatCountdown( - lang: LangFn, + lang: OldLangFn, msLeft: number, ) { const days = Math.floor(msLeft / MILLISECONDS_IN_DAY); @@ -104,7 +104,7 @@ export function formatCountdown( } } -export function formatCountdownShort(lang: LangFn, msLeft: number): string { +export function formatCountdownShort(lang: OldLangFn, msLeft: number): string { if (msLeft < 60 * 1000) { return Math.ceil(msLeft / 1000).toString(); } else if (msLeft < 60 * 60 * 1000) { @@ -116,7 +116,7 @@ export function formatCountdownShort(lang: LangFn, msLeft: number): string { } } -export function formatLastUpdated(lang: LangFn, currentTime: number, lastUpdated = currentTime) { +export function formatLastUpdated(lang: OldLangFn, currentTime: number, lastUpdated = currentTime) { const seconds = currentTime - lastUpdated; if (seconds < 60) { return lang('LiveLocationUpdated.JustNow'); @@ -127,7 +127,7 @@ export function formatLastUpdated(lang: LangFn, currentTime: number, lastUpdated } } -export function formatRelativeTime(lang: LangFn, currentTime: number, lastUpdated = currentTime) { +export function formatRelativeTime(lang: OldLangFn, currentTime: number, lastUpdated = currentTime) { const seconds = currentTime - lastUpdated; if (seconds < 60) { @@ -156,7 +156,7 @@ export function formatRelativeTime(lang: LangFn, currentTime: number, lastUpdate type DurationType = 'Seconds' | 'Minutes' | 'Hours' | 'Days' | 'Weeks'; -export function formatTimeDuration(lang: LangFn, duration: number, showLast = 2) { +export function formatTimeDuration(lang: OldLangFn, duration: number, showLast = 2) { if (!duration) { return undefined; } @@ -196,7 +196,7 @@ export function formatTimeDuration(lang: LangFn, duration: number, showLast = 2) } export function formatHumanDate( - lang: LangFn, + lang: OldLangFn, datetime: number | Date, isShort = false, noWeekdays = false, @@ -237,13 +237,13 @@ export function formatHumanDate( * Returns weekday name * @param day 0 - Sunday, 1 - Monday, ... */ -export function formatWeekday(lang: LangFn, day: number, isShort = false) { +export function formatWeekday(lang: OldLangFn, day: number, isShort = false) { const weekDay = WEEKDAYS_FULL[day]; return isShort ? lang(`Weekday.Short${weekDay}`) : lang(`Weekday.${weekDay}`); } export function formatMediaDateTime( - lang: LangFn, + lang: OldLangFn, datetime: number | Date, isUpperFirst?: boolean, ) { @@ -350,7 +350,7 @@ export function formatDateTimeToString( } export function formatDateAtTime( - lang: LangFn, + lang: OldLangFn, datetime: number | Date, ) { const date = typeof datetime === 'number' ? new Date(datetime) : datetime; @@ -375,7 +375,7 @@ export function formatDateAtTime( } export function formatDateInFuture( - lang: LangFn, + lang: OldLangFn, currentTime: number, datetime: number, ) { diff --git a/src/util/formatCurrency.tsx b/src/util/formatCurrency.tsx index d4edcb4c7..5151c35d9 100644 --- a/src/util/formatCurrency.tsx +++ b/src/util/formatCurrency.tsx @@ -1,7 +1,5 @@ import React, { type TeactNode } from '../lib/teact/teact'; -import type { LangCode } from '../types'; - import { STARS_CURRENCY_CODE } from '../config'; import StarIcon from '../components/common/icons/StarIcon'; @@ -9,7 +7,7 @@ import StarIcon from '../components/common/icons/StarIcon'; export function formatCurrency( totalPrice: number, currency: string, - locale: LangCode = 'en', + locale: string = 'en', options?: { shouldOmitFractions?: boolean; iconClassName?: string; @@ -27,7 +25,7 @@ export function formatCurrency( export function formatCurrencyAsString( totalPrice: number, currency: string, - locale: LangCode = 'en', + locale: string = 'en', options?: { shouldOmitFractions?: boolean; }, diff --git a/src/util/localization/format.tsx b/src/util/localization/format.tsx new file mode 100644 index 000000000..7b25ff859 --- /dev/null +++ b/src/util/localization/format.tsx @@ -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]: , + }, + }); +} diff --git a/src/util/localization/index.ts b/src/util/localization/index.ts index 3e868c077..b4edab1fb 100644 --- a/src/util/localization/index.ts +++ b/src/util/localization/index.ts @@ -4,21 +4,23 @@ import type { ApiLanguage, CachedLangData, LangPack, + LangPackStringValue, } from '../../api/types'; -import type { LangKey } from '../../types/language'; +import type { LangKey, LangVariable } from '../../types/language'; import { type AdvancedLangFnOptions, + type AdvancedLangFnOptionsWithPlural, areAdvancedLangFnOptions, isDeletedLangString, isPluralLangString, type LangFn, type LangFnOptions, + type LangFnOptionsWithPlural, type LangFnParameters, - type LangFnWithFunction, type LangFormatters, } from './types'; -import { DEBUG } from '../../config'; +import { DEBUG, LANG_PACK } from '../../config'; import { callApi } from '../../api/gramjs'; import renderText, { type TextFilter } from '../../components/common/helpers/renderText'; import { MAIN_IDB_STORE } from '../browser/idb'; @@ -37,7 +39,6 @@ import LimitedMap from '../primitives/LimitedMap'; import initialStrings from '../../assets/localization/initialStrings'; -const LANG_PACK = 'weba'; const LANGPACK_STORE_PREFIX = 'langpack-'; const FORMATTERS_FALLBACK_LANG = 'en'; @@ -115,14 +116,22 @@ async function fetchDifference() { langCode: langPack.langCode, fromVersion: langPack.version, }); - if (!result || result.version === langPack.version) return; + if (!result) return; + + applyLangPackDifference(result.version, result.strings, result.keysToRemove); +} + +export function applyLangPackDifference( + version: number, strings: Record, keysToRemove: string[], +) { + if (!langPack || !language || version === langPack.version) return; const newLangPack = { ...langPack, - version: result.version, + version, strings: { - ...omit(langPack.strings, result.keysToRemove), - ...result.strings, + ...omit(langPack.strings, keysToRemove), + ...strings, }, }; updateLangPack(newLangPack); @@ -242,6 +251,11 @@ export async function loadAndChangeLanguage(langCode: string, shouldCheckCache?: return changeLanguage(remoteLanguage); } +export function requestLangPackDifference(langCode: string) { + if (language?.langCode !== langCode) return; + fetchDifference(); +} + export async function changeLanguage(newLanguage: ApiLanguage) { if (langPack && language?.langCode === newLanguage.langCode) return; @@ -300,10 +314,10 @@ function createTranslationFn(): LangFn { fn.pluralCode = language?.pluralCode || FORMATTERS_FALLBACK_LANG; fn.with = (({ key, variables, options }: LangFnParameters) => { if (options && areAdvancedLangFnOptions(options)) { - return processTranslationAdvanced(key, variables as Record, options); + return processTranslationAdvanced(key, variables as Record, options); } - return processTranslation(key, variables as Record, options); - }) as LangFnWithFunction; + return processTranslation(key, variables as Record, options); + }); fn.region = (code: string) => formatters?.region.of(code); fn.conjunction = (list: string[]) => formatters?.conjunction.format(list) || list.join(', '); fn.disjunction = (list: string[]) => formatters?.disjunction.format(list) || list.join(', '); @@ -315,7 +329,7 @@ export function getTranslationFn(): LangFn { return translationFn; } -function getString(langKey: LangKey, count: number, options?: Pick) { +function getString(langKey: LangKey, count: number) { let langPackStringValue = langPack?.strings[langKey]; if (!langPackStringValue && !fallbackLangPack) { @@ -327,7 +341,7 @@ function getString(langKey: LangKey, count: number, options?: Pick, options?: LangFnOptions, + langKey: LangKey, + variables?: Record, + options?: LangFnOptions | LangFnOptionsWithPlural, ): string { const cacheKey = `${langKey}-${JSON.stringify(variables)}-${JSON.stringify(options)}`; if (TRANSLATION_CACHE.has(cacheKey)) { return TRANSLATION_CACHE.get(cacheKey)!; } - const string = getString(langKey, options?.pluralValue || Number(variables?.count) || 0, options); + const pluralValue = options && 'pluralValue' in options ? Number(options.pluralValue) : 0; + const string = getString(langKey, pluralValue); if (!string) return langKey; const variableEntries = variables ? Object.entries(variables) : []; const finalString = variableEntries.reduce((result, [key, value]) => { - return result.replace(`{${key}}`, String(value)); + if (!value) return result; + + const valueAsString = Number.isInteger(value) ? formatters!.number.format(value as number) : String(value); + return result.replace(`{${key}}`, valueAsString); }, string); TRANSLATION_CACHE.set(cacheKey, finalString); @@ -359,9 +379,12 @@ function processTranslation( } function processTranslationAdvanced( - langKey: LangKey, variables?: Record, options?: AdvancedLangFnOptions, + langKey: LangKey, + variables?: Record, + options?: AdvancedLangFnOptions | AdvancedLangFnOptionsWithPlural, ): TeactNode { - const string = getString(langKey, options?.pluralValue || Number(variables?.count) || 0, options); + const pluralValue = options && 'pluralValue' in options ? Number(options.pluralValue) : 0; + const string = getString(langKey, pluralValue); if (!string) return langKey; const variableEntries = variables ? Object.entries(variables) : []; @@ -389,7 +412,10 @@ function processTranslationAdvanced( return renderText(curr, filters, { markdownPostProcessor: (part: string) => { return variableEntries.reduce((result, [key, value]): TeactNode[] => { - return replaceInStringsWithTeact(result, `{${key}}`, value); + if (!value) return result; + + const preparedValue = Number.isInteger(value) ? formatters!.number.format(value as number) : value; + return replaceInStringsWithTeact(result, `{${key}}`, preparedValue); }, [part] as TeactNode[]); }, }); @@ -397,7 +423,10 @@ function processTranslationAdvanced( } return variableEntries.reduce((result, [key, value]): TeactNode[] => { - return replaceInStringsWithTeact(result, `{${key}}`, value); + if (!value) return result; + + const preparedValue = Number.isInteger(value) ? formatters!.number.format(value as number) : value; + return replaceInStringsWithTeact(result, `{${key}}`, preparedValue); }, tempResult); } diff --git a/src/util/localization/types.ts b/src/util/localization/types.ts index 227428bc8..b38c00da8 100644 --- a/src/util/localization/types.ts +++ b/src/util/localization/types.ts @@ -7,30 +7,51 @@ import type { LangPackStringValueRegular, } from '../../api/types'; import type { TextFilter } from '../../components/common/helpers/renderText'; -import type { LangKey, LangPair } from '../../types/language'; +import type { + LangPairPluralWithVariables, + LangPairWithVariables, + PluralLangKey, + PluralLangKeyWithVariables, + RegularLangKey, + RegularLangKeyWithVariables, +} from '../../types/language'; -type ReplaceTypeValues = { - [K in keyof T]: R; -}; - -export interface LangFnOptions { - pluralValue?: number; +export interface LangFnOptionsRegular { withNodes?: never; + withMarkdown?: never; + pluralValue?: never; } -export interface AdvancedLangFnOptions { - pluralValue?: number; +export type LangFnOptionsWithPlural = Omit & { + pluralValue: number; +}; + +export type LangFnOptions = LangFnOptionsRegular | LangFnOptionsWithPlural; + +export interface AdvancedLangFnOptionsRegular { withNodes: true; withMarkdown?: boolean; + pluralValue?: never; renderTextFilters?: TextFilter[]; specialReplacement?: Record; } -type LangPairWithNodes = { - [K in keyof LangPair]: LangPair[K] extends object ? ReplaceTypeValues : LangPair[K]; +export type AdvancedLangFnOptionsWithPlural = Omit & { + pluralValue: number; }; -type RegularLangFnParameters = { +export type AdvancedLangFnOptions = AdvancedLangFnOptionsRegular | AdvancedLangFnOptionsWithPlural; + +type LangPairWithNodes = LangPairWithVariables; +type LangPairPluralWithNodes = LangPairPluralWithVariables; + +type RegularLangFnParametersWithoutVariables = { + key: RegularLangKey; + variables?: undefined; + options?: LangFnOptions; +}; + +type RegularLangFnParametersWithVariables = { [K in keyof T]: { key: K; variables: T[K]; @@ -38,7 +59,33 @@ type RegularLangFnParameters = { } }[keyof T]; -type AdvancedLangFnParameters = { +type RegularLangFnPluralParameters = { + key: PluralLangKey; + variables?: undefined; + options: LangFnOptionsWithPlural; +}; + +type RegularLangFnPluralParametersWithVariables = { + [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 = { [K in keyof T]: { key: K; variables: T[K]; @@ -46,21 +93,56 @@ type AdvancedLangFnParameters = { } }[keyof T]; -export type LangFnParameters = RegularLangFnParameters | AdvancedLangFnParameters; - -export type LangFnWithFunction = { - (params: RegularLangFnParameters): string; - (params: AdvancedLangFnParameters): TeactNode; +type AdvancedLangFnPluralParameters = { + key: PluralLangKey; + variables?: undefined; + options: AdvancedLangFnOptionsWithPlural; }; +type AdvancedLangFnPluralParametersWithVariables = { + [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 = { - ( - key: K, variables?: V, options?: LangFnOptions, + ( + key: K, variables?: undefined, options?: LangFnOptions, ): string; - ( + ( + key: K, variables: undefined, options: LangFnOptionsWithPlural, + ): string; + ( + key: K, variables: V, options?: LangFnOptions, + ): string; + ( + key: K, variables: V, options: LangFnOptionsWithPlural, + ): string; + + ( + key: K, variables?: undefined, options?: AdvancedLangFnOptions, + ): TeactNode; + ( + key: K, variables: undefined, options: AdvancedLangFnOptionsWithPlural, + ): TeactNode; + ( key: K, variables: V, options: AdvancedLangFnOptions, ): TeactNode; - with: LangFnWithFunction; + ( + key: K, variables: V, options: AdvancedLangFnOptionsWithPlural, + ): TeactNode; + + with: (params: LangFnParameters) => TeactNode; region: (code: string) => string | undefined; conjunction: (list: string[]) => string; disjunction: (list: string[]) => string; @@ -101,5 +183,5 @@ export function isLangFnParam(object: unknown): object is LangFnParameters { export function areAdvancedLangFnOptions( params: LangFnOptions | AdvancedLangFnOptions, ): params is AdvancedLangFnOptions { - return 'withNodes' in params; + return 'withNodes' in params && Boolean(params.withNodes); } diff --git a/src/util/oldLangProvider.ts b/src/util/oldLangProvider.ts index 340993bce..01b498161 100644 --- a/src/util/oldLangProvider.ts +++ b/src/util/oldLangProvider.ts @@ -4,7 +4,7 @@ import type { ApiOldLangPack, ApiOldLangString } from '../api/types'; import type { LangCode, TimeFormat } from '../types'; import { - DEFAULT_LANG_CODE, DEFAULT_LANG_PACK, LANG_CACHE_NAME, LANG_PACKS, + DEFAULT_LANG_CODE, LANG_CACHE_NAME, LANG_PACKS, OLD_DEFAULT_LANG_PACK, } from '../config'; import { callApi } from '../api/gramjs'; import * as cacheApi from './cacheApi'; @@ -155,14 +155,14 @@ export async function getTranslationForLangString(langCode: string, key: string) let translateString: ApiOldLangString | undefined; const cachedValue = await cacheApi.fetch( LANG_CACHE_NAME, - `${DEFAULT_LANG_PACK}_${langCode}_${key}`, + `${OLD_DEFAULT_LANG_PACK}_${langCode}_${key}`, cacheApi.Type.Json, ); if (cachedValue) { translateString = cachedValue.value; } else { - translateString = await fetchRemoteString(DEFAULT_LANG_PACK, langCode, key); + translateString = await fetchRemoteString(OLD_DEFAULT_LANG_PACK, langCode, key); } return processTranslation(translateString, key); @@ -199,7 +199,9 @@ export async function oldSetLanguage(langCode: LangCode, callback?: NoneToVoidFu langPack = newLangPack; document.documentElement.lang = langCode; - const { languages, timeFormat } = getGlobal().settings.byKey; + const global = getGlobal(); + const { languages, byKey } = global.settings; + const timeFormat = byKey?.timeFormat; const langInfo = languages?.find((lang) => lang.langCode === langCode); translationFn = createLangFn(); translationFn.isRtl = Boolean(langInfo?.isRtl); diff --git a/src/util/textFormat.ts b/src/util/textFormat.ts index 146606a64..abae4d727 100644 --- a/src/util/textFormat.ts +++ b/src/util/textFormat.ts @@ -1,4 +1,4 @@ -import type { LangFn } from '../hooks/useOldLang'; +import type { OldLangFn } from '../hooks/useOldLang'; import EMOJI_REGEX from '../lib/twemojiRegex'; import fixNonStandardEmoji from './emoji/fixNonStandardEmoji'; @@ -48,7 +48,7 @@ export const getFirstLetters = withCache((phrase: string, count = 2) => { }); const FILE_SIZE_UNITS = ['B', 'KB', 'MB', 'GB']; -export function formatFileSize(lang: LangFn, bytes: number, decimals = 1): string { +export function formatFileSize(lang: OldLangFn, bytes: number, decimals = 1): string { if (bytes === 0) { return lang('FileSize.B', 0); }