fix(i18n): review translations and fix i18n library misuse (#2750)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-13 22:58:09 +02:00
1 parent 78f783f0c4
commit 8cb097f954
575 files changed
+192671 -160540

No files matched your search

@@ -1,7 +1,9 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import styles from '@app/features/guild/components/UploadSlotInfo.module.css';
import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
import {Button} from '@app/features/ui/button/Button';
import {useLingui} from '@lingui/react';
import {UploadIcon} from '@phosphor-icons/react';
import {observer} from 'mobx-react-lite';
import type React from 'react';
@@ -18,7 +20,9 @@ interface UploadSlotInfoProps {
export const UploadSlotInfo: React.FC<UploadSlotInfoProps> = observer(
({title, currentCount, maxCount, description, uploadButtonText, onUploadClick, additionalSlots}) => {
const maxSlotCount = maxCount === Number.POSITIVE_INFINITY ? '∞' : maxCount;
const {i18n} = useLingui();
const numberFormat = getCachedNumberFormat(i18n.locale);
const maxSlotCount = maxCount === Number.POSITIVE_INFINITY ? '∞' : numberFormat.format(maxCount);
return (
<div className={styles.container} data-flx="guild.upload-slot-info.container">
<div className={styles.header} data-flx="guild.upload-slot-info.header">
@@ -29,7 +33,7 @@ export const UploadSlotInfo: React.FC<UploadSlotInfoProps> = observer(
<div className={styles.stats} data-flx="guild.upload-slot-info.stats">
{additionalSlots || (
<span data-flx="guild.upload-slot-info.span">
{currentCount} / {maxSlotCount}
{numberFormat.format(currentCount)} / {maxSlotCount}
</span>
)}
</div>
@@ -10,6 +10,7 @@ import {
MUTE_COMMUNITY_DESCRIPTOR,
UNMUTE_COMMUNITY_DESCRIPTOR,
} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
import {useMuteSheet} from '@app/features/notification/hooks/useMuteSheet';
import {useGuildMenuData} from '@app/features/ui/action_menu/items/GuildMenuData';
import {
@@ -80,6 +81,7 @@ export const GuildHeaderBottomSheet: React.FC<GuildHeaderBottomSheetProps> = obs
const guildCounts = GuildCount.getCounts(guild.id);
const presenceCount = guildCounts?.onlineCount ?? 0;
const memberCount = guildCounts?.memberCount ?? 0;
const formattedPresenceCount = getCachedNumberFormat(i18n.locale).format(presenceCount);
const headerContent = (
<div className={headerStyles.header} data-flx="guild.guild-header-bottom-sheet.div">
<div className={headerStyles.avatarWrapper} data-flx="guild.guild-header-bottom-sheet.div--2">
@@ -104,7 +106,7 @@ export const GuildHeaderBottomSheet: React.FC<GuildHeaderBottomSheetProps> = obs
data-flx="guild.guild-header-bottom-sheet.div--6"
/>
<span className={headerStyles.statText} data-flx="guild.guild-header-bottom-sheet.span--2">
{i18n._(ONLINE_DESCRIPTOR, {presenceCount})}
{i18n._(ONLINE_DESCRIPTOR, {presenceCount: formattedPresenceCount})}
</span>
</div>
<div className={headerStyles.stat} data-flx="guild.guild-header-bottom-sheet.div--7">
@@ -10,7 +10,7 @@ import {focusChannelTextareaAfterNavigation} from '@app/features/messaging/utils
import * as NavigationCommands from '@app/features/navigation/commands/NavigationCommands';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import FocusRing from '@app/features/ui/focus_ring/FocusRing';
import {formatShortRelativeTime} from '@fluxer/date_utils/src/DateDuration';
import {formatShortRelativeTime} from '@app/features/ui/utils/ShortRelativeTimeLabels';
import * as SnowflakeUtils from '@fluxer/snowflake/src/SnowflakeUtils';
import {msg} from '@lingui/core/macro';
import {Trans, useLingui} from '@lingui/react/macro';
@@ -54,7 +54,10 @@ export const DuplicateGroupConfirmModal = observer(({channels, onConfirm}: Dupli
<div className={styles.channelList} data-flx="guild.duplicate-group-confirm-modal.description.channel-list">
{channels.map((channel) => {
const lastActivitySnowflake = channel.lastMessageId ?? channel.id;
const lastActiveText = formatShortRelativeTime(SnowflakeUtils.extractTimestamp(lastActivitySnowflake));
const lastActiveText = formatShortRelativeTime(
i18n,
SnowflakeUtils.extractTimestamp(lastActivitySnowflake),
);
const lastActiveLabel = lastActiveText || i18n._(NO_ACTIVITY_YET_DESCRIPTOR);
return (
<FocusRing
@@ -32,6 +32,7 @@ import {
ShieldIcon,
StarIcon,
} from '@phosphor-icons/react';
import {formatListWithConfig} from '@pkgs/list_utils/src/ListFormatting';
import {observer} from 'mobx-react-lite';
import type {ReactNode} from 'react';
import {useCallback, useMemo, useState} from 'react';
@@ -138,8 +139,8 @@ export const GuildFolderSettingsModal = observer(({folderId}: GuildFolderSetting
.slice(0, 3)
.map((guildId) => Guilds.getGuild(guildId)?.name)
.filter((name): name is string => name != null);
return guildNames.join(', ');
}, [folder]);
return formatListWithConfig(guildNames, {locale: i18n.locale, style: 'narrow', type: 'conjunction'});
}, [folder, i18n.locale]);
const [name, setName] = useState(folder?.name ?? '');
const nameRef = useCursorAtEnd<HTMLInputElement>();
const [color, setColor] = useState(folder?.color ?? 0);
@@ -80,7 +80,7 @@ const UNREAD_BADGES_SECTION_DESCRIPTION_DESCRIPTOR = msg({
'Helper text under the unread badges section in the community notification settings modal. The quoted "Default" should match the matching option label.',
});
const COMMUNITY_UNREAD_BADGES_LEVEL_ARIA_DESCRIPTOR = msg({
message: 'Community unread badges level',
message: 'Community unread badge level',
comment: 'Accessible label for the unread badges level radio group in the community notification settings modal.',
});
const SUPPRESS_EVERYONE_AND_HERE_SWITCH_DESCRIPTOR = msg({
@@ -100,7 +100,7 @@ const REPLY_MENTION_PREFERENCE_SECTION_DESCRIPTOR = msg({
});
const REPLY_MENTION_PREFERENCE_SECTION_DESCRIPTION_DESCRIPTOR = msg({
message:
'Override how replies to your messages handle the @ mention in this community. "Inherit from account" uses your account-wide setting.',
'Override how replies to your messages handle the @mention in this community. "Inherit from account" uses your account-wide setting.',
comment:
'Helper text under the reply mention preference section in the community notification settings modal. The quoted "Inherit from account" should match the matching option label.',
});
@@ -5,7 +5,7 @@ import * as Modal from '@app/features/app/components/dialogs/Modal';
import {ExternalLink} from '@app/features/app/components/shared/ExternalLink';
import {
AVATAR_RECOMMENDED_SIZE_LABEL,
IMAGE_MAX_SIZE_LABEL,
IMAGE_MAX_SIZE_BYTES,
PRODUCT_NAME,
STATIC_IMAGE_FORMATS,
} from '@app/features/app/config/I18nDisplayConstants';
@@ -37,6 +37,7 @@ import {
VERIFY_EMAIL_DESCRIPTOR,
} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {openFilePicker} from '@app/features/messaging/utils/FilePickerUtils';
import {formatFileSize} from '@app/features/messaging/utils/FileUtils';
import * as NavigationCommands from '@app/features/navigation/commands/NavigationCommands';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
@@ -98,7 +99,7 @@ export const GuildCreateForm = observer(() => {
if (file.size > 10 * 1024 * 1024) {
showIconUploadErrorModal(
i18n._(ICON_FILE_IS_TOO_LARGE_PLEASE_CHOOSE_A_DESCRIPTOR, {
imageMaxSizeLabel: IMAGE_MAX_SIZE_LABEL,
imageMaxSizeLabel: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
}),
);
return;
@@ -184,7 +185,7 @@ export const GuildCreateForm = observer(() => {
title: i18n._(CHANGE_ICON_DESCRIPTOR),
uploadHint: formatImageUploadRecommendedHint(i18n, {
formats: STATIC_IMAGE_FORMATS,
maxSize: IMAGE_MAX_SIZE_LABEL,
maxSize: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
recommendedSize: AVATAR_RECOMMENDED_SIZE_LABEL,
}),
onPickUpload: handleIconUpload,
@@ -5,7 +5,7 @@ import * as Modal from '@app/features/app/components/dialogs/Modal';
import {ExternalLink} from '@app/features/app/components/shared/ExternalLink';
import {
AVATAR_RECOMMENDED_SIZE_LABEL,
IMAGE_MAX_SIZE_LABEL,
IMAGE_MAX_SIZE_BYTES,
PRODUCT_NAME,
STATIC_IMAGE_FORMATS,
THE_OTHER_PLATFORM_TEMPLATE_EXAMPLE_URL,
@@ -53,6 +53,7 @@ import {
} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {openExternalUrlWithWarning} from '@app/features/messaging/utils/ExternalLinkUtils';
import {openFilePicker} from '@app/features/messaging/utils/FilePickerUtils';
import {formatFileSize} from '@app/features/messaging/utils/FileUtils';
import * as NavigationCommands from '@app/features/navigation/commands/NavigationCommands';
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
import {Button} from '@app/features/ui/button/Button';
@@ -146,7 +147,8 @@ type TemplateImportStep = 'url' | 'json' | 'create';
const TEMPLATE_IMPORT_STEP_ORDER: ReadonlyArray<TemplateImportStep> = ['url', 'json', 'create'];
const TEMPLATE_STATS_DESCRIPTOR = msg({
message: '{textChannelCount} text, {voiceChannelCount} voice, {categoryCount} categories, {roleCount} roles',
message:
'{textChannelCount, plural, one {# text channel} other {# text channels}}, {voiceChannelCount, plural, one {# voice channel} other {# voice channels}}, {categoryCount, plural, one {# category} other {# categories}}, {roleCount, plural, one {# role} other {# roles}}',
comment:
'Template preview summary listing how many text channels, voice channels, categories, and roles will be imported.',
});
@@ -278,7 +280,7 @@ export const TemplateImportForm = observer(() => {
if (file.size > 10 * 1024 * 1024) {
showIconUploadErrorModal(
i18n._(ICON_FILE_IS_TOO_LARGE_PLEASE_CHOOSE_A_DESCRIPTOR, {
imageMaxSizeLabel: IMAGE_MAX_SIZE_LABEL,
imageMaxSizeLabel: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
}),
);
return;
@@ -329,7 +331,7 @@ export const TemplateImportForm = observer(() => {
title: i18n._(CHANGE_ICON_DESCRIPTOR),
uploadHint: formatImageUploadRecommendedHint(i18n, {
formats: STATIC_IMAGE_FORMATS,
maxSize: IMAGE_MAX_SIZE_LABEL,
maxSize: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
recommendedSize: AVATAR_RECOMMENDED_SIZE_LABEL,
}),
onPickUpload: handleIconUpload,
@@ -37,6 +37,7 @@ import type {GuildAuditLogEntryResponse} from '@fluxer/schema/src/domains/guild/
import type {I18n} from '@lingui/core';
import {msg} from '@lingui/core/macro';
import {Plural, Trans} from '@lingui/react/macro';
import {formatListWithConfig} from '@pkgs/list_utils/src/ListFormatting';
import type {ReactNode} from 'react';
const SAFE_DESCRIPTOR = msg({
@@ -67,7 +68,8 @@ export type ChangeRenderer = (
const renderInline = (value: unknown, i18nInstance: I18n, guildId?: string): ReactNode =>
renderValueInline(value, guildId, i18nInstance);
const joinLabels = (labels: Array<string>): string => labels.join(', ');
const joinLabels = (labels: Array<string>, i18nInstance: I18n = i18n): string =>
formatListWithConfig(labels, {locale: i18nInstance.locale, style: 'long', type: 'conjunction'});
const normalizeStringArray = (value: unknown): Array<string> =>
Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
const getRoleNameResolver = (guildId: string): ((roleId: string) => string) => {
@@ -88,10 +90,12 @@ const getRoleDiff = (oldValue: unknown, newValue: unknown): {added: Array<string
const mapFeatureLabels = (features: Array<string>, i18nInstance: I18n): Array<string> =>
features.map((feature) => getFeatureLabel(feature, i18nInstance) ?? feature);
const formatPermissionList = (flags: Array<bigint>, i18nInstance: I18n = i18n): string =>
flags
.map((flag) => formatPermissionLabel(i18nInstance, flag, false))
.filter((label): label is string => label !== null)
.join(', ');
joinLabels(
flags
.map((flag) => formatPermissionLabel(i18nInstance, flag, false))
.filter((label): label is string => label !== null),
i18nInstance,
);
const whenOldValueMissing =
(hasNoOld: ChangeRenderer, hasOld: ChangeRenderer): ChangeRenderer =>
(change, ctx) =>
@@ -233,10 +237,10 @@ const GUILD_CHANGE_RENDERERS: Record<string, ChangeRenderer> = {
<Trans>Set the AFK channel to {renderInline(change.newValue, i18n, guildId)}.</Trans>
),
afk_timeout: (change, {i18n}) => {
const raw = safeScalarString(change.newValue, i18n);
const minutes = raw != null ? Number(raw) : Number.NaN;
if (Number.isNaN(minutes))
return <Trans>Set the AFK timeout to {renderInline(change.newValue, i18n)} minutes.</Trans>;
const seconds = change.newValue;
if (typeof seconds !== 'number' || !Number.isFinite(seconds))
return <Trans>Set the AFK timeout to {renderInline(change.newValue, i18n)} seconds.</Trans>;
const minutes = seconds / 60;
return (
<Plural
value={minutes}
@@ -658,26 +662,34 @@ const ROLE_CHANGE_RENDERERS: Record<string, ChangeRenderer> = {
},
hoist: (change) =>
change.newValue === true ? (
<Trans>Display role members separately.</Trans>
<Trans>Enabled separate display of members with this role.</Trans>
) : (
<Trans>Don't display separately.</Trans>
<Trans>Disabled separate display of members with this role.</Trans>
),
mentionable: (change) =>
change.newValue === true ? <Trans>Allow @mention.</Trans> : <Trans>Disallow @mention.</Trans>,
change.newValue === true ? (
<Trans>Allowed everyone to mention this role.</Trans>
) : (
<Trans>Restricted mentions of this role to members with permission.</Trans>
),
icon_hash: () => <Trans>Updated the role icon.</Trans>,
unicode_emoji: whenNewValueMissing(
() => <Trans>Removed the unicode emoji.</Trans>,
(change, {i18n}) => <Trans>Set the unicode emoji to {renderInline(change.newValue, i18n)}.</Trans>,
() => <Trans>Removed the Unicode emoji.</Trans>,
(change, {i18n}) => <Trans>Set the Unicode emoji to {renderInline(change.newValue, i18n)}.</Trans>,
),
};
const INVITE_CHANGE_RENDERERS: Record<string, ChangeRenderer> = {
code: (change, {i18n}) => <Trans>Invite code is {renderInline(change.newValue, i18n)}.</Trans>,
max_uses: (change, {i18n}) =>
change.newValue === 0 ? (
<Trans>This invite has unlimited uses.</Trans>
) : (
<Trans>This invite expires after {renderInline(change.newValue, i18n)} uses.</Trans>
),
max_uses: (change) => {
if (change.newValue === 0) return <Trans>This invite has unlimited uses.</Trans>;
if (typeof change.newValue !== 'number') return null;
const count = change.newValue;
return (
<Trans>
This invite expires after <Plural value={count} one="# use" other="# uses" />.
</Trans>
);
},
max_age: (change) => {
if (change.newValue === 0) return <Trans>This invite never expires.</Trans>;
if (typeof change.newValue !== 'number') return null;
@@ -17,6 +17,7 @@ import {
shouldHideChangeKey,
toChangeShape,
} from '@app/features/guild/utils/guild_tabs/GuildAuditLogTabUtils';
import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
import type {ComboboxOption} from '@app/features/ui/components/form/FormCombobox';
import type {User} from '@app/features/user/models/User';
import {getFormattedDateTime} from '@app/features/user/utils/DateFormatting';
@@ -144,7 +145,7 @@ export const renderValueInline = (value: unknown, guildId: string | undefined, i
if (isEmptyString(value)) return renderBoldValue(i18n._(NOTHING_DESCRIPTOR));
const scalar = safeScalarString(value, i18n);
if (scalar !== null) {
if (typeof value === 'number') return renderBoldValue(scalar);
if (typeof value === 'number') return renderBoldValue(getCachedNumberFormat(i18n.locale).format(value));
if (typeof value === 'boolean') return renderBoldValue(scalar);
if (typeof value === 'string' && looksLikeSnowflake(value)) {
if (guildId) {
@@ -30,7 +30,7 @@ import type {
DiscoveryStatusResponse,
} from '@fluxer/schema/src/domains/guild/GuildDiscoverySchemas';
import {msg} from '@lingui/core/macro';
import {Trans, useLingui} from '@lingui/react/macro';
import {Plural, Trans, useLingui} from '@lingui/react/macro';
import {InfoIcon, WarningIcon} from '@phosphor-icons/react';
import {clsx} from 'clsx';
import type React from 'react';
@@ -346,8 +346,15 @@ const GuildDiscoveryTab: React.FC<{guildId: string}> = ({guildId}) => {
<Trans>Not enough members</Trans>
</p>
<p className={styles.warningText} data-flx="guild.guild-tabs.guild-discovery-tab.warning-text">
<Trans>
Your community needs at least {minMemberCount} members before it can be listed in Discovery.
<Trans comment="Warning in the community Discovery settings tab when the community is below the minimum size. The plural count is the minimum number of members required to be listed.">
Your community needs at least{' '}
<Plural
value={minMemberCount}
one="# member"
other="# members"
data-flx="guild.guild-tabs.guild-discovery-tab.min-member-count.plural"
/>{' '}
before it can be listed in Discovery.
</Trans>
</p>
</div>
@@ -621,7 +628,7 @@ const GuildDiscoveryTab: React.FC<{guildId: string}> = ({guildId}) => {
disabled={!eligible && canApply}
data-flx="guild.guild-tabs.guild-discovery-tab.button.submit"
>
{hasActiveApplication ? <Trans>Save</Trans> : <Trans>Apply</Trans>}
{hasActiveApplication ? <Trans>Save</Trans> : <Trans>Submit application</Trans>}
</Button>
</div>
</div>
@@ -179,7 +179,7 @@ const GuildEmojiTab: React.FC<{guildId: string}> = observer(function GuildEmojiT
}, [filteredEmojis]);
const maxEmojis = guild?.maxEmojis ?? MAX_GUILD_EMOJIS;
const currentEmojiCount = emojis.length;
const emojiMaxSizeLabel = formatFileSize(GlobalLimits.getEmojiMaxSize());
const emojiMaxSizeLabel = formatFileSize(i18n.locale, GlobalLimits.getEmojiMaxSize());
const canModifyEmoji = useCallback(
(emoji: GuildEmojiWithUser): boolean => {
if (canManageExpressions) return true;
@@ -292,7 +292,7 @@ const GuildEmojiTab: React.FC<{guildId: string}> = observer(function GuildEmojiT
const preparedEmojis: Array<{name: string; image: string; file: File}> = [];
const preparationFailures: Array<EmojiUploadFailure> = [];
const maxEmojiSize = GlobalLimits.getEmojiMaxSize();
const maxEmojiSizeLabel = formatFileSize(maxEmojiSize);
const maxEmojiSizeLabel = formatFileSize(i18n.locale, maxEmojiSize);
for (const file of filesWithinSlots) {
try {
const base64Image = await ImageCropUtils.optimizeEmojiImage(file, maxEmojiSize, 128);
@@ -307,7 +307,7 @@ const GuildEmojiTab: React.FC<{guildId: string}> = observer(function GuildEmojiT
? EMOJI_PROCESSED_FILE_TOO_LARGE_DESCRIPTOR
: EMOJI_SOURCE_FILE_TOO_LARGE_DESCRIPTOR,
{
fileSize: formatFileSize(error.actualSizeBytes),
fileSize: formatFileSize(i18n.locale, error.actualSizeBytes),
maxSize: maxEmojiSizeLabel,
},
)
@@ -87,6 +87,7 @@ import {useState} from 'react';
const MESSAGE_DESCRIPTOR = msg({
message: 'Message',
context: 'message-action',
comment:
'Member actions sheet menu item. Sends a direct message to the target user. Standalone verb used as a menu label.',
});
@@ -68,7 +68,7 @@ const VERIFICATION_LEVEL_MEDIUM_NAME_DESCRIPTOR = msg({
'Member verification level option in the community moderation settings tab. Short standalone severity label.',
});
const VERIFICATION_LEVEL_MEDIUM_DESCRIPTION_DESCRIPTOR = msg({
message: "Requires a verified email address, and an account that's at least 5 minutes old.",
message: "Requires a verified email address and an account that's at least 5 minutes old.",
comment: 'Helper text for the "Medium" member verification level option in the community moderation settings tab.',
});
const VERIFICATION_LEVEL_HIGH_NAME_DESCRIPTOR = msg({
@@ -313,7 +313,7 @@ const GuildModerationTab: React.FC<{guildId: string}> = observer(({guildId}) =>
{isDiscoverable && (
<p data-flx="guild.guild-tabs.guild-moderation-tab.p--3">
<Trans>
Communities listed in Discovery require at least verified email. None cannot be selected while
Communities listed in Discovery require at least email verification. None cannot be selected while
Discovery is enabled.
</Trans>
</p>
@@ -354,8 +354,8 @@ const GuildModerationTab: React.FC<{guildId: string}> = observer(({guildId}) =>
data-flx="guild.guild-tabs.guild-moderation-tab.section-description--2"
>
<Trans>
Communities listed in Discovery are required to scan all members. This setting cannot be changed while
Discovery is enabled.
Communities listed in Discovery are required to scan messages from all members. This setting cannot be
changed while Discovery is enabled.
</Trans>
</p>
)}
@@ -464,7 +464,7 @@ const GuildModerationTab: React.FC<{guildId: string}> = observer(({guildId}) =>
data-flx="guild.guild-tabs.guild-moderation-tab.section-description--4"
>
<Trans>
Require two-factor authentication for moderators before they can ban, kick, timeout, or remove
Require two-factor authentication for moderators before they can ban, kick, time out, or remove
messages.
</Trans>
</p>
@@ -78,7 +78,7 @@ const YOU_CANNOT_GRANT_A_PERMISSION_YOU_DON_T_DESCRIPTOR = msg({
'Tooltip on a permission toggle in the role editor that the current user cannot grant because they do not hold that permission themselves.',
});
const YOU_CANNOT_REMOVE_THIS_PERMISSION_BECAUSE_IT_WOULD_DESCRIPTOR = msg({
message: 'You cannot remove this permission because it would remove it from yourself',
message: 'You cannot remove this permission because it would remove it from you',
comment:
'Tooltip on a permission toggle in the role editor when revoking that permission would leave the current user without it.',
});
@@ -227,7 +227,7 @@ const GuildStickersTab: React.FC<{guildId: string}> = observer(function GuildSti
},
[canManageExpressions, canCreateExpressions, currentUserId],
);
const stickerMaxSizeLabel = formatFileSize(GlobalLimits.getStickerMaxSize());
const stickerMaxSizeLabel = formatFileSize(i18n.locale, GlobalLimits.getStickerMaxSize());
return (
<div className={styles.container} data-flx="guild.guild-tabs.guild-stickers-tab.container">
<CloneAllowedToggle
@@ -4,7 +4,7 @@ import {ImagePreviewField} from '@app/features/app/components/shared/ImagePrevie
import {
ANIMATED_BANNER_FEATURE,
ANIMATED_IMAGE_FORMATS,
IMAGE_MAX_SIZE_LABEL,
IMAGE_MAX_SIZE_BYTES,
STATIC_IMAGE_FORMATS,
WIDE_IMAGE_ASPECT_RATIO_LABEL,
} from '@app/features/app/config/I18nDisplayConstants';
@@ -16,6 +16,7 @@ import styles from '@app/features/guild/components/modals/guild_tabs/guild_overv
import type {GuildLike} from '@app/features/guild/components/modals/guild_tabs/guild_overview_tab/GuildOverviewTypes';
import {useGuildImageAssetField} from '@app/features/guild/components/modals/guild_tabs/guild_overview_tab/hooks/useGuildImageAssetField';
import type {FormInputs} from '@app/features/guild/utils/guild_tabs/GuildOverviewTabUtils';
import {formatFileSize} from '@app/features/messaging/utils/FileUtils';
import {Button} from '@app/features/ui/button/Button';
import * as AvatarUtils from '@app/features/user/utils/AvatarUtils';
import {GuildFeatures} from '@fluxer/constants/src/GuildConstants';
@@ -96,7 +97,7 @@ export const GuildBannerUploadField: React.FC<{
sourceModalTitle: i18n._(CHANGE_BANNER_DESCRIPTOR),
uploadHint: formatImageUploadMinimumHint(i18n, {
formats: bannerFormats,
maxSize: IMAGE_MAX_SIZE_LABEL,
maxSize: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
minimumSize: bannerMinimumSize,
aspectRatio: WIDE_IMAGE_ASPECT_RATIO_LABEL,
}),
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {
IMAGE_MAX_SIZE_LABEL,
IMAGE_MAX_SIZE_BYTES,
STATIC_IMAGE_WITH_AVIF_FORMATS,
WIDE_IMAGE_ASPECT_RATIO_LABEL,
} from '@app/features/app/config/I18nDisplayConstants';
@@ -13,6 +13,7 @@ import styles from '@app/features/guild/components/modals/guild_tabs/guild_overv
import type {GuildLike} from '@app/features/guild/components/modals/guild_tabs/guild_overview_tab/GuildOverviewTypes';
import {useGuildImageAssetField} from '@app/features/guild/components/modals/guild_tabs/guild_overview_tab/hooks/useGuildImageAssetField';
import type {FormInputs} from '@app/features/guild/utils/guild_tabs/GuildOverviewTabUtils';
import {formatFileSize} from '@app/features/messaging/utils/FileUtils';
import {Button} from '@app/features/ui/button/Button';
import {msg} from '@lingui/core/macro';
import {Trans, useLingui} from '@lingui/react/macro';
@@ -75,7 +76,7 @@ export const GuildEmbedSplashUploadField: React.FC<{
sourceModalTitle: i18n._(CHANGE_CHAT_EMBED_BACKGROUND_DESCRIPTOR),
uploadHint: formatImageUploadMinimumHintWithNote(i18n, {
formats: STATIC_IMAGE_WITH_AVIF_FORMATS,
maxSize: IMAGE_MAX_SIZE_LABEL,
maxSize: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
minimumSize: embedSplashMinimumSize,
aspectRatio: WIDE_IMAGE_ASPECT_RATIO_LABEL,
note: i18n._(SHOWN_IN_INVITE_EMBEDS_IN_CHAT_DESCRIPTOR),
@@ -4,7 +4,7 @@ import {
ANIMATED_ICON_FEATURE,
ANIMATED_IMAGE_FORMATS,
AVATAR_RECOMMENDED_SIZE_LABEL,
IMAGE_MAX_SIZE_LABEL,
IMAGE_MAX_SIZE_BYTES,
STATIC_IMAGE_FORMATS,
} from '@app/features/app/config/I18nDisplayConstants';
import {AssetType} from '@app/features/expressions/components/modals/AssetCropModal';
@@ -15,6 +15,7 @@ import type {GuildLike} from '@app/features/guild/components/modals/guild_tabs/g
import {useGuildImageAssetField} from '@app/features/guild/components/modals/guild_tabs/guild_overview_tab/hooks/useGuildImageAssetField';
import {GuildIcon} from '@app/features/guild/components/popouts/GuildIcon';
import type {FormInputs} from '@app/features/guild/utils/guild_tabs/GuildOverviewTabUtils';
import {formatFileSize} from '@app/features/messaging/utils/FileUtils';
import {Button} from '@app/features/ui/button/Button';
import {GuildFeatures} from '@fluxer/constants/src/GuildConstants';
import {msg} from '@lingui/core/macro';
@@ -74,7 +75,7 @@ export const GuildIconUploadField: React.FC<{
sourceModalTitle: i18n._(CHANGE_ICON_DESCRIPTOR),
uploadHint: formatImageUploadRecommendedHint(i18n, {
formats: iconFormats,
maxSize: IMAGE_MAX_SIZE_LABEL,
maxSize: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
recommendedSize: AVATAR_RECOMMENDED_SIZE_LABEL,
}),
});
@@ -2,7 +2,7 @@
import {ImagePreviewField} from '@app/features/app/components/shared/ImagePreviewField';
import {
IMAGE_MAX_SIZE_LABEL,
IMAGE_MAX_SIZE_BYTES,
STATIC_IMAGE_WITH_AVIF_FORMATS,
WIDE_IMAGE_ASPECT_RATIO_LABEL,
} from '@app/features/app/config/I18nDisplayConstants';
@@ -14,6 +14,7 @@ import styles from '@app/features/guild/components/modals/guild_tabs/guild_overv
import type {GuildLike} from '@app/features/guild/components/modals/guild_tabs/guild_overview_tab/GuildOverviewTypes';
import {useGuildImageAssetField} from '@app/features/guild/components/modals/guild_tabs/guild_overview_tab/hooks/useGuildImageAssetField';
import type {FormInputs} from '@app/features/guild/utils/guild_tabs/GuildOverviewTabUtils';
import {formatFileSize} from '@app/features/messaging/utils/FileUtils';
import {Button} from '@app/features/ui/button/Button';
import * as AvatarUtils from '@app/features/user/utils/AvatarUtils';
import {msg} from '@lingui/core/macro';
@@ -81,7 +82,7 @@ export const GuildInviteSplashUploadField: React.FC<{
sourceModalTitle: i18n._(CHANGE_INVITE_BACKGROUND_DESCRIPTOR),
uploadHint: formatImageUploadMinimumHint(i18n, {
formats: STATIC_IMAGE_WITH_AVIF_FORMATS,
maxSize: IMAGE_MAX_SIZE_LABEL,
maxSize: formatFileSize(i18n.locale, IMAGE_MAX_SIZE_BYTES),
minimumSize: splashMinimumSize,
aspectRatio: WIDE_IMAGE_ASPECT_RATIO_LABEL,
}),
@@ -30,6 +30,7 @@ import {
INVALID_IMAGE_TRY_ANOTHER_DESCRIPTOR,
} from '@app/features/i18n/utils/CommonMessageDescriptors';
import {openFilePicker} from '@app/features/messaging/utils/FilePickerUtils';
import {formatFileSize} from '@app/features/messaging/utils/FileUtils';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
import * as ToastCommands from '@app/features/ui/commands/ToastCommands';
@@ -218,8 +219,7 @@ export function useGuildImageAssetField({
if (!canManage) return;
const requestId = ++requestIdRef.current;
if (file.size > maxBytes) {
const maxMB = Math.round(maxBytes / (1024 * 1024));
const maxSizeLabel = `${maxMB}MB`;
const maxSizeLabel = formatFileSize(i18n.locale, maxBytes);
showErrorModal(i18n._(FILE_IS_TOO_LARGE_PLEASE_CHOOSE_A_FILE_DESCRIPTOR, {label, maxSizeLabel}));
return;
}
@@ -300,8 +300,7 @@ export function useGuildImageAssetField({
setIsProcessing(true);
try {
if (croppedBlob.size > maxBytes) {
const maxMB = Math.round(maxBytes / (1024 * 1024));
const maxSizeLabel = `${maxMB}MB`;
const maxSizeLabel = formatFileSize(i18n.locale, maxBytes);
showErrorModal(i18n._(CROPPED_IMAGE_IS_TOO_LARGE_PLEASE_CHOOSE_A_DESCRIPTOR, {maxSizeLabel}));
return;
}
@@ -34,7 +34,7 @@ const AFK_TIMEOUT_DESCRIPTOR = msg({
comment: 'Short label in the idle settings section. Keep it concise.',
});
const SECONDS_DESCRIPTOR = msg({
message: '{seconds} seconds',
message: '{seconds, plural, one {# second} other {# seconds}}',
comment: 'AFK timeout label for a stored custom duration shown in the idle settings section.',
});
export const IdleSettingsSection: React.FC<{
@@ -35,7 +35,7 @@ const ENABLE_MESSAGE_HISTORY_THRESHOLD_DESCRIPTOR = msg({
});
const ALLOW_MEMBERS_WITHOUT_THE_PERMISSION_TO_SEE_MESSAGES_DESCRIPTOR = msg({
message:
'Allow members without the {readMessageHistoryPermissionLabel} permission to see messages after a specific date.',
'Allow members without the {readMessageHistoryPermissionLabel} permission to see messages sent after a specific date.',
comment: 'Label in the message history threshold content. Keep the tone plain and specific.',
});
const THRESHOLD_DATE_DESCRIPTOR = msg({
@@ -74,7 +74,7 @@ export const MessageHistoryThresholdDescription: React.FC = () => {
</p>
<p data-flx="guild.guild-tabs.guild-overview-tab.message-history-threshold-content.message-history-threshold-description.p--2">
<Trans>
If you'd like these members to access historical messages after a specific date, enable this feature and
If you'd like these members to access historical messages sent after a specific date, enable this feature and
choose a threshold below.
</Trans>
</p>
@@ -32,7 +32,7 @@ import {useDrag, useDrop} from 'react-dnd';
import {getEmptyImage} from 'react-dnd-html5-backend';
const YOU_CANNOT_EDIT_THIS_ROLE_BECAUSE_IT_IS_DESCRIPTOR = msg({
message: 'You cannot edit this role because it is your highest role or above you',
message: 'You cannot edit this role because it is at or above your highest role',
comment:
'Tooltip on a disabled role row in the community roles settings tab. Explains the role hierarchy rule: you cannot edit roles at or above your own highest role.',
});