mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-19 05:55:34 +09:00
fix(i18n): review translations and fix i18n library misuse (#2750)
This commit is contained in:
1 parent
78f783f0c4
commit
8cb097f954
575 files changed
+192671
-160540
No files matched your search
@@ -48,11 +48,11 @@ import type React from 'react';
|
||||
|
||||
const OPEN_IN_COMMUNITY_DESCRIPTOR = msg({
|
||||
message: 'Open in community',
|
||||
comment: 'Action that opens the selected user profile inside the current community.',
|
||||
comment: 'Favorites menu action that opens the favorited channel in the community it belongs to.',
|
||||
});
|
||||
const MOVE_TO_DESCRIPTOR = msg({
|
||||
message: 'Move to',
|
||||
comment: 'Submenu label that moves the selected message to another channel.',
|
||||
comment: 'Favorites menu submenu label that moves the favorited channel into another favorites category.',
|
||||
});
|
||||
|
||||
interface FavoritesChannelContextMenuProps {
|
||||
|
||||
@@ -4,6 +4,7 @@ import styles from '@app/features/ui/action_menu/MenuItem.module.css';
|
||||
import {Slider} from '@app/features/ui/components/Slider';
|
||||
import {formatRoundedPercentage} from '@app/features/ui/utils/PercentageFormatting';
|
||||
import {ContextMenu as BaseContextMenu} from '@base-ui/react/context-menu';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {clsx} from 'clsx';
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
|
||||
@@ -20,11 +21,12 @@ interface MenuItemSliderProps {
|
||||
|
||||
export const MenuItemSlider = React.forwardRef<HTMLDivElement, MenuItemSliderProps>(
|
||||
({label, value, minValue = 0, maxValue = 100, disabled = false, onChange, onFormat, step = 1}, forwardedRef) => {
|
||||
const {i18n} = useLingui();
|
||||
const [localValue, setLocalValue] = useState(value);
|
||||
useEffect(() => {
|
||||
setLocalValue(value);
|
||||
}, [value]);
|
||||
const formattedValue = onFormat ? onFormat(localValue) : formatRoundedPercentage(localValue);
|
||||
const formattedValue = onFormat ? onFormat(localValue) : formatRoundedPercentage(i18n.locale, localValue);
|
||||
const handleValueChange = useCallback(
|
||||
(newValue: number) => {
|
||||
setLocalValue(newValue);
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
COPY_TEXT_DESCRIPTOR,
|
||||
OPEN_LINK_DESCRIPTOR,
|
||||
} from '@app/features/i18n/utils/CommonMessageDescriptors';
|
||||
import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
|
||||
import * as ReactionCommands from '@app/features/messaging/commands/ReactionCommands';
|
||||
import {MessageReactionsModal} from '@app/features/messaging/components/modals/MessageReactionsModal';
|
||||
import {ReactionImage} from '@app/features/messaging/components/ReactionImage';
|
||||
@@ -41,6 +42,7 @@ import {MenuItemSubmenu} from '@app/features/ui/action_menu/MenuItemSubmenu';
|
||||
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
import {modal} from '@app/features/ui/commands/ModalCommands';
|
||||
import * as TextCopyCommands from '@app/features/ui/commands/TextCopyCommands';
|
||||
import {CTRL_DESCRIPTOR} from '@app/features/ui/keybind_hint/KeybindHint';
|
||||
import type {MenuGroupType, MenuItemType} from '@app/features/ui/menu_bottom_sheet/MenuBottomSheet';
|
||||
import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
|
||||
import {openExternalUrl} from '@app/features/ui/utils/NativeUtils';
|
||||
@@ -158,7 +160,7 @@ const RemoveReactionsSubmenuItem = observer(
|
||||
icon={renderEmojiPreview()}
|
||||
onClick={handleSelect}
|
||||
closeOnSelect={false}
|
||||
hint={`${reaction.count}`}
|
||||
hint={getCachedNumberFormat(i18n.locale).format(reaction.count)}
|
||||
data-flx="ui.action-menu.message-context-menu.remove-reactions-submenu-item.menu-item.select"
|
||||
>
|
||||
{label}
|
||||
@@ -380,8 +382,8 @@ export const MessageContextMenu: React.FC<MessageContextMenuProps> = observer(
|
||||
return () => document.removeEventListener('selectionchange', handleSelectionChange);
|
||||
}, [restoreSelection, normalizeSelectionText]);
|
||||
const copyShortcut = useMemo(() => {
|
||||
return /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? '⌘C' : 'Ctrl+C';
|
||||
}, []);
|
||||
return /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? '⌘C' : `${i18n._(CTRL_DESCRIPTOR)}+C`;
|
||||
}, [i18n.locale]);
|
||||
const handleCopySelection = useCallback(async () => {
|
||||
if (!selectionText) return;
|
||||
await TextCopyCommands.copy(i18n, selectionText, true);
|
||||
|
||||
@@ -40,6 +40,7 @@ const ACTIONS_DESCRIPTOR = msg({
|
||||
});
|
||||
const MESSAGE_DESCRIPTOR = msg({
|
||||
message: 'Message',
|
||||
context: 'message-action',
|
||||
comment: 'Action that opens a DM conversation with the selected user.',
|
||||
});
|
||||
const logger = new Logger('ActionsSubmenu');
|
||||
|
||||
@@ -103,7 +103,7 @@ import {
|
||||
FAILED_TO_PIN_GROUP_DM_DESCRIPTOR,
|
||||
FAILED_TO_UNPIN_DM_DESCRIPTOR,
|
||||
FAILED_TO_UNPIN_GROUP_DM_DESCRIPTOR,
|
||||
INVITE_SENT_TO_DESCRIPTOR,
|
||||
INVITE_SENT_FOR_DESCRIPTOR,
|
||||
PINNED_GROUP_DM_DESCRIPTOR,
|
||||
UNPINNED_GROUP_DM_DESCRIPTOR,
|
||||
USER_ID_COPIED_DESCRIPTOR,
|
||||
@@ -328,7 +328,7 @@ export function useDMMenuData(
|
||||
if (result) {
|
||||
ToastCommands.createToast({
|
||||
type: 'success',
|
||||
children: i18n._(INVITE_SENT_TO_DESCRIPTOR, {guildName}),
|
||||
children: i18n._(INVITE_SENT_FOR_DESCRIPTOR, {guildName}),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import GuildMembers from '@app/features/member/state/GuildMembers';
|
||||
import * as MessageCommands from '@app/features/messaging/commands/MessageCommands';
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import {SendInviteToCommunityIcon} from '@app/features/ui/action_menu/ContextMenuIcons';
|
||||
import {INVITE_SENT_FOR_DESCRIPTOR} from '@app/features/ui/action_menu/items/dm_menu_data/shared';
|
||||
import {
|
||||
beginInviteToCommunityGuard,
|
||||
getInviteToCommunityGuardKey,
|
||||
@@ -24,7 +25,7 @@ import * as ToastCommands from '@app/features/ui/commands/ToastCommands';
|
||||
import type {User} from '@app/features/user/models/User';
|
||||
import type {Invite} from '@fluxer/schema/src/domains/invite/InviteSchemas';
|
||||
import {fromTimestamp} from '@fluxer/snowflake/src/SnowflakeUtils';
|
||||
import {Trans, useLingui} from '@lingui/react/macro';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import {useCallback, useMemo, useRef, useState} from 'react';
|
||||
@@ -89,7 +90,7 @@ export const InviteToCommunityMenuItem: React.FC<InviteToCommunityMenuItemProps>
|
||||
if (result) {
|
||||
ToastCommands.createToast({
|
||||
type: 'success',
|
||||
children: <Trans>Invite sent to {candidate.guild.name}</Trans>,
|
||||
children: i18n._(INVITE_SENT_FOR_DESCRIPTOR, {guildName: candidate.guild.name}),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -104,7 +105,7 @@ export const InviteToCommunityMenuItem: React.FC<InviteToCommunityMenuItemProps>
|
||||
scheduleInviteToCommunityGuardRelease(guardKey);
|
||||
}
|
||||
},
|
||||
[onClose, user.id],
|
||||
[onClose, user.id, i18n],
|
||||
);
|
||||
if (user.bot || candidates.length === 0) {
|
||||
return null;
|
||||
|
||||
@@ -131,7 +131,7 @@ const DOWNLOAD_MEDIA_DESCRIPTOR = msg({
|
||||
comment: 'Generic media context menu download action when the specific type is unknown.',
|
||||
});
|
||||
const ATTACHMENT_IS_EXPIRED_OR_UNAVAILABLE_DESCRIPTOR = msg({
|
||||
message: 'Attachment is expired or unavailable',
|
||||
message: 'Attachment has expired or is unavailable',
|
||||
comment: 'Toast shown when an attachment URL is no longer accessible.',
|
||||
});
|
||||
const URL_COPIED_TO_CLIPBOARD_DESCRIPTOR = msg({
|
||||
|
||||
@@ -25,6 +25,7 @@ import {useCallback} from 'react';
|
||||
|
||||
const MESSAGE_DESCRIPTOR = msg({
|
||||
message: 'Message',
|
||||
context: 'message-action',
|
||||
comment: 'Action that opens a DM conversation with the selected user.',
|
||||
});
|
||||
const logger = new Logger('MessageUserMenuItem');
|
||||
|
||||
@@ -14,7 +14,7 @@ import type React from 'react';
|
||||
import {useCallback} from 'react';
|
||||
|
||||
const STAFF_DM_ACCESS_FAILED_TITLE_DESCRIPTOR = msg({
|
||||
message: "Couldn't update staff DM access",
|
||||
message: "Couldn't update direct message permission",
|
||||
comment: 'Title of the error modal shown when toggling staff DM access fails.',
|
||||
});
|
||||
const STAFF_DM_ACCESS_FAILED_MESSAGE_DESCRIPTOR = msg({
|
||||
@@ -33,9 +33,9 @@ const MENTION_BYPASS_FAILED_MESSAGE_DESCRIPTOR = msg({
|
||||
function showStaffControlErrorModal(title: string, message: string, flxKey: string): void {
|
||||
ModalCommands.push(modal(() => <GenericErrorModal title={title} message={message} data-flx={flxKey} />));
|
||||
}
|
||||
const ALLOW_STAFF_DM_ACCESS_DESCRIPTOR = msg({
|
||||
message: 'Allow staff DM access',
|
||||
comment: 'Staff setting that allows the Fluxer staff to message the selected user.',
|
||||
const ALLOW_DIRECT_MESSAGES_WITH_THIS_USER_DESCRIPTOR = msg({
|
||||
message: 'Allow direct messages with this user',
|
||||
comment: 'Staff-only setting that allows the current staff member and selected user to exchange direct messages.',
|
||||
});
|
||||
const BYPASS_MENTION_SUPPRESSION_DESCRIPTOR = msg({
|
||||
message: 'Bypass mention suppression',
|
||||
@@ -106,7 +106,7 @@ export const StaffUserControlsMenuItems: React.FC<StaffUserControlsMenuItemsProp
|
||||
onCheckedChange={handleStaffDmAccessChange}
|
||||
data-flx="ui.action-menu.items.staff-user-controls-menu-items.checkbox-item"
|
||||
>
|
||||
{i18n._(ALLOW_STAFF_DM_ACCESS_DESCRIPTOR)}
|
||||
{i18n._(ALLOW_DIRECT_MESSAGES_WITH_THIS_USER_DESCRIPTOR)}
|
||||
</CheckboxItem>
|
||||
<CheckboxItem
|
||||
checked={mentionBypassUserIds.includes(user.id)}
|
||||
|
||||
@@ -105,8 +105,9 @@ import {
|
||||
FOCUS_THIS_PERSON_DESCRIPTOR,
|
||||
MENTION_DESCRIPTOR,
|
||||
MESSAGE_DESCRIPTOR,
|
||||
MUTE_DESCRIPTOR,
|
||||
MUTE_DEVICE_DESCRIPTOR,
|
||||
MUTE_MICROPHONE_DESCRIPTOR,
|
||||
MUTE_PLAYBACK_DESCRIPTOR,
|
||||
POP_OUT_CAMERA_DESCRIPTOR,
|
||||
POP_OUT_USER_DESCRIPTOR,
|
||||
PREVIEW_CAMERA_DESCRIPTOR,
|
||||
@@ -128,6 +129,7 @@ import type {
|
||||
MenuSliderType,
|
||||
MenuSubmenuItemType,
|
||||
} from '@app/features/ui/menu_bottom_sheet/MenuBottomSheet';
|
||||
import {formatRoundedPercentage} from '@app/features/ui/utils/PercentageFormatting';
|
||||
import * as UserProfileCommands from '@app/features/user/commands/UserProfileCommands';
|
||||
import {ChangeNicknameModal} from '@app/features/user/components/modals/ChangeNicknameModal';
|
||||
import {UserSettingsModal} from '@app/features/user/components/modals/UserSettingsModal';
|
||||
@@ -411,7 +413,7 @@ export function useVoiceParticipantMenuData(options: VoiceParticipantMenuDataOpt
|
||||
MediaEngine.applyLocalAudioPreferencesForUser(user.id);
|
||||
}
|
||||
},
|
||||
onFormat: (value: number) => `${Math.round(value)}%`,
|
||||
onFormat: (value: number) => formatRoundedPercentage(i18n.locale, value),
|
||||
factoryDefaultValue: 100,
|
||||
});
|
||||
const buildConnectionVolumeSlider = (targetConnectionId: string): MenuSliderType => ({
|
||||
@@ -425,7 +427,7 @@ export function useVoiceParticipantMenuData(options: VoiceParticipantMenuDataOpt
|
||||
MediaEngine.applyLocalAudioPreferencesForUser(user.id);
|
||||
}
|
||||
},
|
||||
onFormat: (value: number) => `${Math.round(value)}%`,
|
||||
onFormat: (value: number) => formatRoundedPercentage(i18n.locale, value),
|
||||
factoryDefaultValue: 100,
|
||||
});
|
||||
const buildVolumeControls = (targetConnectionId?: string): Array<MenuSliderType> => {
|
||||
@@ -449,7 +451,7 @@ export function useVoiceParticipantMenuData(options: VoiceParticipantMenuDataOpt
|
||||
minValue: 0,
|
||||
maxValue: 200,
|
||||
onChange: (value: number) => EntranceSoundListenerPrefs.setVolume(user.id, value),
|
||||
onFormat: (value: number) => `${Math.round(value)}%`,
|
||||
onFormat: (value: number) => formatRoundedPercentage(i18n.locale, value),
|
||||
factoryDefaultValue: 100,
|
||||
},
|
||||
],
|
||||
@@ -771,7 +773,7 @@ export function useVoiceParticipantMenuData(options: VoiceParticipantMenuDataOpt
|
||||
data-flx="ui.action-menu.items.voice-participant-menu-data.groups.self-mute-icon--2"
|
||||
/>
|
||||
),
|
||||
label: i18n._(MUTE_DESCRIPTOR),
|
||||
label: i18n._(MUTE_MICROPHONE_DESCRIPTOR),
|
||||
checked: isSelfMuted,
|
||||
onChange: () => {
|
||||
VoiceStateCommands.toggleSelfMute(null);
|
||||
@@ -889,7 +891,7 @@ export function useVoiceParticipantMenuData(options: VoiceParticipantMenuDataOpt
|
||||
data-flx="ui.action-menu.items.voice-participant-menu-data.menu-assembly.local-mute-icon"
|
||||
/>
|
||||
),
|
||||
label: i18n._(MUTE_DESCRIPTOR),
|
||||
label: i18n._(MUTE_PLAYBACK_DESCRIPTOR),
|
||||
checked: isParticipantLocallyMuted,
|
||||
onChange: (checked: boolean) => {
|
||||
ParticipantVolume.setLocalMute(user.id, checked);
|
||||
|
||||
@@ -64,6 +64,7 @@ import {useCallback, useMemo} from 'react';
|
||||
|
||||
const MUTE_DESCRIPTOR = msg({
|
||||
message: 'Mute',
|
||||
context: 'voice-control-action',
|
||||
comment: "Voice menu checkbox label. Mutes the current user's microphone, not what they hear.",
|
||||
});
|
||||
const TURN_OFF_STREAM_DESCRIPTOR = msg({
|
||||
@@ -72,6 +73,7 @@ const TURN_OFF_STREAM_DESCRIPTOR = msg({
|
||||
});
|
||||
const MUTE_2_DESCRIPTOR = msg({
|
||||
message: 'Mute',
|
||||
context: 'playback-control-action',
|
||||
comment: 'Voice menu checkbox label. Locally mutes another participant only for the current user.',
|
||||
});
|
||||
const DISABLE_VIDEO_LOCALLY_DESCRIPTOR = msg({
|
||||
@@ -300,7 +302,6 @@ export const ParticipantVolumeSlider: React.FC<ParticipantVolumeSliderProps> = o
|
||||
minValue={0}
|
||||
maxValue={200}
|
||||
onChange={handleChange}
|
||||
onFormat={(value) => `${Math.round(value)}%`}
|
||||
data-flx="ui.action-menu.items.voice-participant-menu-items.participant-volume-slider.menu-item-slider.change"
|
||||
/>
|
||||
);
|
||||
@@ -358,7 +359,6 @@ export const EntranceSoundListenerSubmenu: React.FC<EntranceSoundListenerSubmenu
|
||||
minValue={0}
|
||||
maxValue={200}
|
||||
onChange={handleVolumeChange}
|
||||
onFormat={(value) => `${Math.round(value)}%`}
|
||||
data-flx="ui.action-menu.items.voice-participant-menu-items.entrance-sound-volume-slider"
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import type {VoiceParticipantMenuScreenShareSource} from '@app/features/ui/action_menu/items/VoiceParticipantMenuTypes';
|
||||
import {
|
||||
CHANGE_STREAM_DESCRIPTOR,
|
||||
MUTE_DESCRIPTOR,
|
||||
MUTE_PLAYBACK_DESCRIPTOR,
|
||||
PAUSE_OWN_STREAM_PREVIEW_DESCRIPTOR,
|
||||
POP_OUT_STREAM_DESCRIPTOR,
|
||||
SCREEN_SHARE_PRIVACY_DESCRIPTOR,
|
||||
@@ -30,6 +30,7 @@ import type {
|
||||
MenuSliderType,
|
||||
MenuSubmenuItemType,
|
||||
} from '@app/features/ui/menu_bottom_sheet/MenuBottomSheet';
|
||||
import {formatRoundedPercentage} from '@app/features/ui/utils/PercentageFormatting';
|
||||
import {copyVoiceDiagnostics} from '@app/features/voice/commands/VoiceDiagnosticsCommands';
|
||||
import * as VoiceSettingsCommands from '@app/features/voice/commands/VoiceSettingsCommands';
|
||||
import {changeActiveScreenShare, stopActiveScreenShare} from '@app/features/voice/components/ActiveScreenShareMenu';
|
||||
@@ -150,7 +151,7 @@ function buildStreamAudioActions(
|
||||
data-flx="ui.action-menu.items.voice-participant-stream-menu-builder.build-stream-audio-actions.local-mute-icon"
|
||||
/>
|
||||
),
|
||||
label: i18n._(MUTE_DESCRIPTOR),
|
||||
label: i18n._(MUTE_PLAYBACK_DESCRIPTOR),
|
||||
checked: isStreamMuted,
|
||||
onChange: (checked: boolean) => {
|
||||
StreamAudioPrefs.setMuted(source.streamKey, checked);
|
||||
@@ -166,7 +167,7 @@ function buildStreamAudioActions(
|
||||
StreamAudioPrefs.setVolume(source.streamKey, value);
|
||||
MediaEngine.applyLocalAudioPreferencesForUser(userId);
|
||||
},
|
||||
onFormat: (value: number) => `${Math.round(value)}%`,
|
||||
onFormat: (value: number) => formatRoundedPercentage(i18n.locale, value),
|
||||
factoryDefaultValue: 100,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import {msg} from '@lingui/core/macro';
|
||||
|
||||
export const INVITE_SENT_TO_DESCRIPTOR = msg({
|
||||
message: 'Invite sent to {guildName}',
|
||||
comment: 'Toast confirming an invite was sent to the named community.',
|
||||
export const INVITE_SENT_FOR_DESCRIPTOR = msg({
|
||||
message: 'Invite sent for {guildName}',
|
||||
comment: 'Toast confirming a community invite was sent to a DM recipient. guildName is the community name.',
|
||||
});
|
||||
export const ARE_YOU_SURE_YOU_WANT_TO_CLOSE_YOUR_DESCRIPTOR = msg({
|
||||
message: 'Close your DM with {recipientUsername}? You can reopen it anytime.',
|
||||
|
||||
@@ -33,11 +33,16 @@ export const SCREEN_SHARE_PRIVACY_DESCRIPTOR = msg({
|
||||
message: 'Screen share privacy',
|
||||
comment: 'Voice screen share menu action that opens the screen share preview privacy controls.',
|
||||
});
|
||||
export const MUTE_DESCRIPTOR = msg({
|
||||
export const MUTE_MICROPHONE_DESCRIPTOR = msg({
|
||||
message: 'Mute',
|
||||
context: 'voice-control-action',
|
||||
comment:
|
||||
'Voice menu toggle label. Mutes the current user microphone or, for a stream, mutes that stream audio locally.',
|
||||
});
|
||||
export const MUTE_PLAYBACK_DESCRIPTOR = msg({
|
||||
message: 'Mute',
|
||||
context: 'playback-control-action',
|
||||
});
|
||||
export const UNFOCUS_DESCRIPTOR = msg({
|
||||
message: 'Unfocus',
|
||||
comment: 'Voice call layout action that removes focus from the currently pinned participant.',
|
||||
@@ -72,6 +77,7 @@ export const MENTION_DESCRIPTOR = msg({
|
||||
});
|
||||
export const MESSAGE_DESCRIPTOR = msg({
|
||||
message: 'Message',
|
||||
context: 'message-action',
|
||||
comment: 'Voice participant menu action that opens a DM conversation with the selected user.',
|
||||
});
|
||||
export const MUTE_DEVICE_DESCRIPTOR = msg({
|
||||
|
||||
@@ -19,6 +19,7 @@ import {useEffect, useState} from 'react';
|
||||
|
||||
const MUTED_VOICE_BADGE_LABEL = msg({
|
||||
message: 'Muted',
|
||||
context: 'microphone-state',
|
||||
comment: 'Accessible label for the muted voice badge shown on an avatar.',
|
||||
});
|
||||
const DEAFENED_VOICE_BADGE_LABEL = msg({
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
import {msg} from '@lingui/core/macro';
|
||||
|
||||
export const CHARACTERS_LEFT_DESCRIPTOR = msg({
|
||||
message: '{remaining} characters left',
|
||||
message: '{remaining, plural, one {# character left} other {# characters left}}',
|
||||
comment: 'Character counter status showing the number of characters remaining.',
|
||||
});
|
||||
|
||||
export const CHARACTERS_LEFT_GET_TO_WRITE_UP_TO_CHARACTERS_DESCRIPTOR = msg({
|
||||
message: '{remaining} characters left. Get {premiumProductName} to write up to {premiumMaxLength} characters.',
|
||||
message:
|
||||
'{remaining, plural, one {# character left} other {# characters left}}. Get {premiumProductName} to write up to {premiumMaxLength, plural, one {# character} other {# characters}}.',
|
||||
comment: 'Character counter status with an upsell to the premium tier.',
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {PREMIUM_PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants';
|
||||
import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
|
||||
import {shouldShowPremiumFeatures} from '@app/features/premium/utils/PremiumUtils';
|
||||
import {CharacterCountAnnouncer} from '@app/features/ui/character_counter/CharacterCountAnnouncer';
|
||||
import styles from '@app/features/ui/character_counter/CharacterCounter.module.css';
|
||||
@@ -28,6 +29,7 @@ export const CharacterCounter = observer(
|
||||
({currentLength, maxLength, canUpgrade, premiumMaxLength, onUpgradeClick, className}: CharacterCounterProps) => {
|
||||
const {i18n} = useLingui();
|
||||
const remaining = maxLength - currentLength;
|
||||
const formattedRemaining = getCachedNumberFormat(i18n.locale).format(remaining);
|
||||
const isOverLimit = remaining < 0;
|
||||
const isNearingLimit = remaining < 50;
|
||||
const showPremiumFeatures = shouldShowPremiumFeatures();
|
||||
@@ -53,7 +55,7 @@ export const CharacterCounter = observer(
|
||||
className={clsx(styles.counterButton, colorClass, className)}
|
||||
>
|
||||
<span aria-hidden="true" data-flx="ui.character-counter.character-counter.span">
|
||||
{remaining}
|
||||
{formattedRemaining}
|
||||
</span>
|
||||
</button>
|
||||
</FocusRing>
|
||||
@@ -65,7 +67,7 @@ export const CharacterCounter = observer(
|
||||
data-flx="ui.character-counter.character-counter.counter-span"
|
||||
className={clsx(styles.counterSpan, colorClass, className)}
|
||||
>
|
||||
{remaining}
|
||||
{formattedRemaining}
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -48,7 +48,7 @@ function resolveMentionBadgeMotion(useReducedMotion: boolean): MentionBadgeMotio
|
||||
const formatMentionCount = (mentionCount: number) => {
|
||||
const locale = getCurrentLocale();
|
||||
if (mentionCount > 99 && mentionCount < 1000) {
|
||||
return '99+';
|
||||
return `${formatNumber(99, locale)}+`;
|
||||
}
|
||||
if (mentionCount >= 1000) {
|
||||
return formatCompactNumber(mentionCount, locale, 0).replace(/\s/g, '');
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {getStatusTypeLabel} from '@app/features/app/constants/AppConstants';
|
||||
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
||||
import styles from '@app/features/ui/components/StatusIndicator.module.css';
|
||||
import {StatusTypes} from '@fluxer/constants/src/StatusConstants';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import clsx from 'clsx';
|
||||
import {memo} from 'react';
|
||||
|
||||
const STATUS_DESCRIPTOR = msg({message: '{effectiveStatusLabel} status'});
|
||||
|
||||
interface StatusIndicatorProps {
|
||||
status: string;
|
||||
size?: number;
|
||||
@@ -17,7 +22,9 @@ interface StatusIndicatorProps {
|
||||
const normalizeStatus = (status: string) => (status === StatusTypes.INVISIBLE ? StatusTypes.OFFLINE : status);
|
||||
export const StatusIndicator = memo(
|
||||
({status, size = 12, className, appearance = 'default', monochromeColor}: StatusIndicatorProps) => {
|
||||
const {i18n} = useLingui();
|
||||
const normalizedStatus = normalizeStatus(status);
|
||||
const effectiveStatusLabel = getStatusTypeLabel(i18n, normalizedStatus);
|
||||
const maskId = `flx-mask-presence-${normalizedStatus}`;
|
||||
const fill =
|
||||
appearance === 'monochrome' ? (monochromeColor ?? 'currentColor') : `var(--status-${normalizedStatus})`;
|
||||
@@ -29,7 +36,7 @@ export const StatusIndicator = memo(
|
||||
viewBox="0 0 1 1"
|
||||
preserveAspectRatio="none"
|
||||
aria-hidden={false}
|
||||
aria-label={`status-${normalizedStatus}`}
|
||||
aria-label={i18n._(STATUS_DESCRIPTOR, {effectiveStatusLabel})}
|
||||
role="img"
|
||||
data-flx="ui.status-indicator.display-block"
|
||||
>
|
||||
|
||||
@@ -293,7 +293,7 @@ export const ColorPickerField: React.FC<ColorPickerFieldProps> = observer((props
|
||||
className={clsx(styles.description, descriptionClassName)}
|
||||
data-flx="ui.form.color-picker-field.description"
|
||||
>
|
||||
{description ?? <Trans>Type a color (hex, rgb(), hsl, or name), or use the picker.</Trans>}
|
||||
{description ?? <Trans>Type a color (hex, rgb(), hsl(), or name) or use the picker.</Trans>}
|
||||
</p>
|
||||
)}
|
||||
{showError && (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {getCachedDateTimeFormat} from '@app/features/i18n/utils/IntlCache';
|
||||
import {PASSWORD_MANAGER_IGNORE_ATTRIBUTES} from '@app/features/platform/utils/PasswordManagerAutocomplete';
|
||||
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
||||
import styles from '@app/features/ui/components/form/DateTimePickerField.module.css';
|
||||
@@ -9,7 +10,6 @@ import {msg} from '@lingui/core/macro';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {CalendarBlankIcon} from '@phosphor-icons/react';
|
||||
import {clsx} from 'clsx';
|
||||
import {DateTime} from 'luxon';
|
||||
import type React from 'react';
|
||||
import {useCallback, useMemo, useState} from 'react';
|
||||
import {Button, Dialog, DialogTrigger, Popover} from 'react-aria-components';
|
||||
@@ -48,12 +48,11 @@ interface DateTimePickerFieldProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function formatDisplayDate(date: Date | null): string {
|
||||
function formatDisplayDate(locale: string, date: Date | null): string {
|
||||
if (!date) {
|
||||
return '';
|
||||
}
|
||||
const dt = DateTime.fromJSDate(date);
|
||||
return dt.toFormat('d LLL yyyy, HH:mm');
|
||||
return getCachedDateTimeFormat(locale, {dateStyle: 'medium', timeStyle: 'short'}).format(date);
|
||||
}
|
||||
|
||||
function toTimeString(date: Date): string {
|
||||
@@ -64,7 +63,7 @@ export const DateTimePickerField: React.FC<DateTimePickerFieldProps> = (props) =
|
||||
const {i18n} = useLingui();
|
||||
const {label, description, value, onChange, minDate, maxDate, disabled, error, className} = props;
|
||||
const [popoutOpen, setPopoutOpen] = useState(false);
|
||||
const displayValue = useMemo(() => formatDisplayDate(value), [value]);
|
||||
const displayValue = useMemo(() => formatDisplayDate(i18n.locale, value), [i18n.locale, value]);
|
||||
const timeValue = useMemo(() => (value ? toTimeString(value) : '00:00'), [value]);
|
||||
const handleDaySelect = useCallback(
|
||||
(selected: Date | undefined) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
|
||||
import {shouldDisableAutofocusOnMobile} from '@app/features/platform/utils/AutofocusUtils';
|
||||
import type {TextareaAutosizeProps} from '@app/features/platform/utils/AutoResizingTextarea';
|
||||
import {TextareaAutosize} from '@app/features/platform/utils/AutoResizingTextarea';
|
||||
@@ -372,7 +373,9 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
},
|
||||
forwardedRef,
|
||||
) => {
|
||||
const {i18n} = useLingui();
|
||||
const disableAutofocus = shouldDisableAutofocusOnMobile();
|
||||
const numberFormat = getCachedNumberFormat(i18n.locale);
|
||||
const currentValue = useMemo(() => value || '', [value]);
|
||||
const currentLength = useMemo(() => String(currentValue).length, [currentValue]);
|
||||
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
@@ -437,7 +440,7 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
aria-hidden="true"
|
||||
data-flx="ui.form.input.character-counter.character-count"
|
||||
>
|
||||
{currentLength}/{maxLength}
|
||||
{numberFormat.format(currentLength)}/{numberFormat.format(maxLength)}
|
||||
</span>
|
||||
<CharacterCountAnnouncer
|
||||
currentLength={currentLength}
|
||||
@@ -446,7 +449,7 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
/>
|
||||
</>
|
||||
),
|
||||
[showCharacterCount, maxLength, currentLength],
|
||||
[showCharacterCount, maxLength, currentLength, numberFormat],
|
||||
);
|
||||
const labelRight = useMemo(
|
||||
() => (
|
||||
@@ -487,7 +490,7 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
aria-hidden="true"
|
||||
data-flx="ui.form.input.textarea-with-actions.character-count"
|
||||
>
|
||||
{currentLength}/{maxLength}
|
||||
{numberFormat.format(currentLength)}/{numberFormat.format(maxLength)}
|
||||
</span>
|
||||
<CharacterCountAnnouncer
|
||||
currentLength={currentLength}
|
||||
@@ -511,6 +514,7 @@ export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
maxLength,
|
||||
characterCountTooltip,
|
||||
currentLength,
|
||||
numberFormat,
|
||||
],
|
||||
);
|
||||
const simpleTextarea = useMemo(
|
||||
|
||||
@@ -4,17 +4,18 @@ import Accessibility from '@app/features/accessibility/state/Accessibility';
|
||||
import type {KeybindCommand, KeyCombo} from '@app/features/input/state/InputKeybind';
|
||||
import Keybind from '@app/features/input/state/InputKeybind';
|
||||
import {
|
||||
formatPrimaryKeySymbol,
|
||||
getPrimaryKeyForComboDisplay,
|
||||
isPrimaryKeyAlreadyRepresentedByModifier,
|
||||
} from '@app/features/input/utils/KeybindUtils';
|
||||
import {SHIFT_KEY_LABEL} from '@app/features/input/utils/KeyboardUtils';
|
||||
import {SHIFT_KEY_DESCRIPTOR} from '@app/features/input/utils/KeyboardUtils';
|
||||
import styles from '@app/features/ui/keybind_hint/KeybindHint.module.css';
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
|
||||
const CTRL_DESCRIPTOR = msg({
|
||||
export const CTRL_DESCRIPTOR = msg({
|
||||
message: 'Ctrl',
|
||||
comment: 'Keyboard shortcut keycap label for Control on non-Mac platforms. Keep it short.',
|
||||
});
|
||||
@@ -75,7 +76,7 @@ const formatKeyParts = (i18n: I18n, combo: KeyCombo): Array<KeyPart> => {
|
||||
parts.push(mac ? {label: '⌘', isSymbol: true} : {label: i18n._(WIN_DESCRIPTOR)});
|
||||
}
|
||||
if (combo.shift) {
|
||||
parts.push({label: SHIFT_KEY_LABEL});
|
||||
parts.push({label: i18n._(SHIFT_KEY_DESCRIPTOR)});
|
||||
}
|
||||
if (combo.alt) {
|
||||
parts.push(mac ? {label: '⌥', isSymbol: true} : {label: i18n._(ALT_DESCRIPTOR)});
|
||||
@@ -109,17 +110,17 @@ const formatKeyParts = (i18n: I18n, combo: KeyCombo): Array<KeyPart> => {
|
||||
} else if (key === 'PageDown') {
|
||||
parts.push({label: i18n._(PGDN_DESCRIPTOR)});
|
||||
} else if (isShiftKeyName(key)) {
|
||||
parts.push({label: SHIFT_KEY_LABEL});
|
||||
parts.push({label: i18n._(SHIFT_KEY_DESCRIPTOR)});
|
||||
} else if (/^Key[A-Z]$/.test(key)) {
|
||||
parts.push({label: key.slice(3)});
|
||||
} else if (/^Digit[0-9]$/.test(key)) {
|
||||
parts.push({label: key.slice(5)});
|
||||
} else if (/^Numpad[0-9]$/.test(key)) {
|
||||
parts.push({label: `Numpad ${key.slice(6)}`});
|
||||
parts.push({label: formatPrimaryKeySymbol(i18n, key)});
|
||||
} else if (key.length === 1) {
|
||||
parts.push({label: key.toUpperCase()});
|
||||
} else if (key) {
|
||||
parts.push({label: key});
|
||||
parts.push({label: formatPrimaryKeySymbol(i18n, key)});
|
||||
}
|
||||
return parts;
|
||||
};
|
||||
|
||||
@@ -54,6 +54,10 @@ const IS_NOW_YOUR_FRIEND_DESCRIPTOR = msg({
|
||||
message: '{displayName} is now your friend!',
|
||||
comment: 'Toast title announcing a newly accepted friend request.',
|
||||
});
|
||||
const GROUP_DM_DESCRIPTOR = msg({
|
||||
message: 'Group DM',
|
||||
comment: 'Fallback name shown in a desktop notification for a group DM that has no custom name.',
|
||||
});
|
||||
const logger = new Logger('Notification');
|
||||
const shouldManagePushSubscriptions = (): boolean => isInstalledPwa();
|
||||
|
||||
@@ -335,13 +339,15 @@ class NotificationState {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ChannelTypes.GROUP_DM:
|
||||
case ChannelTypes.GROUP_DM: {
|
||||
const groupDmName = channel.name || i18n._(GROUP_DM_DESCRIPTOR);
|
||||
if (useMacOSNotificationPresentation) {
|
||||
subtitle = channel.name || 'Group DM';
|
||||
subtitle = groupDmName;
|
||||
} else {
|
||||
title = `${title} (${channel.name || 'Group DM'})`;
|
||||
title = `${title} (${groupDmName})`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
const body = buildMessageNotificationBody(data.messageRecord, i18n);
|
||||
const notificationUrl =
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
|
||||
|
||||
export function roundPercentage(value: number): number {
|
||||
return Number.isFinite(value) ? Math.round(value) : 0;
|
||||
}
|
||||
|
||||
export function formatRoundedPercentage(value: number): string {
|
||||
return `${roundPercentage(value)}%`;
|
||||
export function formatRoundedPercentage(locale: string, value: number): string {
|
||||
return getCachedNumberFormat(locale, {style: 'percent', maximumFractionDigits: 0}).format(
|
||||
roundPercentage(value) / 100,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {getShortRelativeTimeParts} from '@fluxer/date_utils/src/DateDuration';
|
||||
import type {DateInput} from '@fluxer/date_utils/src/DateTypes';
|
||||
import type {I18n} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
|
||||
const NOW_DESCRIPTOR = msg({
|
||||
message: 'now',
|
||||
context: 'short-relative-time',
|
||||
comment:
|
||||
'Compact age label shown beside a list row (DM list, invite list) when the event happened less than a minute ago. Keep it as short as possible; it sits in a narrow column.',
|
||||
});
|
||||
const MINUTES_DESCRIPTOR = msg({
|
||||
message: '{count, plural, one {#m} other {#m}}',
|
||||
context: 'short-relative-time',
|
||||
comment:
|
||||
'Compact age label shown beside a list row (DM list, invite list). count is a number of minutes; the "m" is an abbreviation of "minutes". Keep it as short as possible; it sits in a narrow column.',
|
||||
});
|
||||
const HOURS_DESCRIPTOR = msg({
|
||||
message: '{count, plural, one {#h} other {#h}}',
|
||||
context: 'short-relative-time',
|
||||
comment:
|
||||
'Compact age label shown beside a list row (DM list, invite list). count is a number of hours; the "h" is an abbreviation of "hours". Keep it as short as possible; it sits in a narrow column.',
|
||||
});
|
||||
const DAYS_DESCRIPTOR = msg({
|
||||
message: '{count, plural, one {#d} other {#d}}',
|
||||
context: 'short-relative-time',
|
||||
comment:
|
||||
'Compact age label shown beside a list row (DM list, invite list). count is a number of days; the "d" is an abbreviation of "days". Keep it as short as possible; it sits in a narrow column.',
|
||||
});
|
||||
const WEEKS_DESCRIPTOR = msg({
|
||||
message: '{count, plural, one {#w} other {#w}}',
|
||||
context: 'short-relative-time',
|
||||
comment:
|
||||
'Compact age label shown beside a list row (DM list, invite list). count is a number of weeks; the "w" is an abbreviation of "weeks". Keep it as short as possible; it sits in a narrow column.',
|
||||
});
|
||||
const MONTHS_DESCRIPTOR = msg({
|
||||
message: '{count, plural, one {#mo} other {#mo}}',
|
||||
context: 'short-relative-time',
|
||||
comment:
|
||||
'Compact age label shown beside a list row (DM list, invite list). count is a number of months; the "mo" is an abbreviation of "months". Keep it as short as possible; it sits in a narrow column.',
|
||||
});
|
||||
const YEARS_DESCRIPTOR = msg({
|
||||
message: '{count, plural, one {#y} other {#y}}',
|
||||
context: 'short-relative-time',
|
||||
comment:
|
||||
'Compact age label shown beside a list row (DM list, invite list). count is a number of years; the "y" is an abbreviation of "years". Keep it as short as possible; it sits in a narrow column.',
|
||||
});
|
||||
|
||||
export function formatShortRelativeTime(i18n: I18n, timestamp: DateInput, minUnit: '1m' | 'now' = 'now'): string {
|
||||
const {unit, value} = getShortRelativeTimeParts(timestamp, minUnit);
|
||||
switch (unit) {
|
||||
case 'now':
|
||||
return i18n._(NOW_DESCRIPTOR);
|
||||
case 'minute':
|
||||
return i18n._(MINUTES_DESCRIPTOR, {count: value});
|
||||
case 'hour':
|
||||
return i18n._(HOURS_DESCRIPTOR, {count: value});
|
||||
case 'day':
|
||||
return i18n._(DAYS_DESCRIPTOR, {count: value});
|
||||
case 'week':
|
||||
return i18n._(WEEKS_DESCRIPTOR, {count: value});
|
||||
case 'month':
|
||||
return i18n._(MONTHS_DESCRIPTOR, {count: value});
|
||||
default:
|
||||
return i18n._(YEARS_DESCRIPTOR, {count: value});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user