mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +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
@@ -24,7 +24,7 @@ import type {I18n} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
|
||||
const YOU_CAN_T_ADD_NEW_REACTIONS_WHILE_YOU_DESCRIPTOR = msg({
|
||||
message: "You can't add new reactions while you're on timeout.",
|
||||
message: "You can't add new reactions while you're timed out.",
|
||||
comment: 'Error message in the messaging commands.',
|
||||
});
|
||||
const logger = new Logger('MessageReactions');
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ const ATTACHMENT_UPLOAD_UNAVAILABLE_DESCRIPTOR = msg({
|
||||
});
|
||||
export const AttachmentUploadConnectivityModal = observer(() => {
|
||||
const {i18n} = useLingui();
|
||||
const fallbackSizeFormatted = formatFileSize(MULTIPART_ATTACHMENT_FALLBACK_MAX_REQUEST_SIZE);
|
||||
const fallbackSizeFormatted = formatFileSize(i18n.locale, MULTIPART_ATTACHMENT_FALLBACK_MAX_REQUEST_SIZE);
|
||||
return (
|
||||
<GenericErrorModal
|
||||
title={i18n._(ATTACHMENT_UPLOAD_UNAVAILABLE_DESCRIPTOR)}
|
||||
|
||||
@@ -42,7 +42,7 @@ const INSTANCE_ADMIN_LIMIT_DESCRIPTOR = msg({
|
||||
});
|
||||
const PREMIUM_FILE_SIZE_UPSELL_DESCRIPTOR = msg({
|
||||
message:
|
||||
'With {premiumProductName}, your per-file upload limit increases to {fileSizeLimit}, plus animated avatars, longer messages, and many other {premiumProductName} perks.',
|
||||
'With {premiumProductName}, your per-file upload limit increases to {fileSizeLimit}. You also get animated avatars, longer messages, and many other {premiumProductName} perks.',
|
||||
comment:
|
||||
'Premium upsell sentence in the file-too-large alert. premiumProductName is the paid product name; fileSizeLimit is a localized file size.',
|
||||
});
|
||||
@@ -58,7 +58,7 @@ export const FileSizeTooLargeModal = observer(({oversizedFileCount}: FileSizeToo
|
||||
const maxAttachmentFileSize = user?.maxAttachmentFileSize ?? 25 * 1024 * 1024;
|
||||
const premiumMaxAttachmentFileSize = Limits.getPremiumValue('max_attachment_file_size', ATTACHMENT_MAX_SIZE_PREMIUM);
|
||||
const canUpgradeAttachmentLimit = maxAttachmentFileSize < premiumMaxAttachmentFileSize;
|
||||
const maxSizeFormatted = formatFileSize(maxAttachmentFileSize);
|
||||
const maxSizeFormatted = formatFileSize(i18n.locale, maxAttachmentFileSize);
|
||||
const hasKnownOversizedFileCount = oversizedFileCount != null;
|
||||
const hasMultipleOversizedFiles = (oversizedFileCount ?? 0) > 1;
|
||||
const handleGetPlutoniumClick = useCallback(() => {
|
||||
@@ -85,7 +85,7 @@ export const FileSizeTooLargeModal = observer(({oversizedFileCount}: FileSizeToo
|
||||
title={i18n._(FILE_SIZE_LIMIT_EXCEEDED_DESCRIPTOR)}
|
||||
description={`${baseDescription} ${i18n._(PREMIUM_FILE_SIZE_UPSELL_DESCRIPTOR, {
|
||||
premiumProductName: PREMIUM_PRODUCT_NAME,
|
||||
fileSizeLimit: formatFileSize(premiumMaxAttachmentFileSize),
|
||||
fileSizeLimit: formatFileSize(i18n.locale, premiumMaxAttachmentFileSize),
|
||||
})}`}
|
||||
primaryText={i18n._(GET_PREMIUM_DESCRIPTOR, {premiumProductName: PREMIUM_PRODUCT_NAME})}
|
||||
primaryVariant="primary"
|
||||
|
||||
@@ -6,7 +6,7 @@ import {useLingui} from '@lingui/react/macro';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
|
||||
const THAT_MESSAGE_DIDN_T_DELETE_DESCRIPTOR = msg({
|
||||
message: "That message didn't delete",
|
||||
message: "That message couldn't be deleted",
|
||||
comment: 'Label in the message delete failed modal. Keep the tone plain and specific.',
|
||||
});
|
||||
const TRY_DELETING_THAT_MESSAGE_AGAIN_DESCRIPTOR = msg({
|
||||
|
||||
@@ -26,8 +26,8 @@ import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import {useCallback, useMemo, useState} from 'react';
|
||||
|
||||
const FAILED_TO_LOAD_DESCRIPTOR = msg({
|
||||
message: '(failed to load)',
|
||||
const EMOJI_FAILED_TO_LOAD_DESCRIPTOR = msg({
|
||||
message: '{emojiName} (failed to load)',
|
||||
comment: 'Error message in the messaging emoji renderer.',
|
||||
});
|
||||
|
||||
@@ -216,7 +216,7 @@ const EmojiRendererInner = observer(function EmojiRendererInner({
|
||||
<img
|
||||
draggable={false}
|
||||
className={className}
|
||||
alt={hasFailed ? `${emojiData.name} ${i18n._(FAILED_TO_LOAD_DESCRIPTOR)}` : emojiData.name}
|
||||
alt={hasFailed ? i18n._(EMOJI_FAILED_TO_LOAD_DESCRIPTOR, {emojiName: emojiData.name}) : emojiData.name}
|
||||
src={emojiUrl}
|
||||
style={hasFailed ? FAILED_EMOJI_STYLE : undefined}
|
||||
data-message-id={messageId}
|
||||
|
||||
@@ -40,6 +40,7 @@ const CHANNEL_LINK_DESCRIPTOR = msg({
|
||||
message: 'channel link',
|
||||
comment: 'Short label in the messaging mention renderer. Keep it concise.',
|
||||
});
|
||||
const UNKNOWN_MENTION_DESCRIPTOR = msg({message: 'unknown-mention'});
|
||||
|
||||
interface InteractiveChannelMentionProps {
|
||||
channel: Channel;
|
||||
@@ -366,7 +367,7 @@ export const MentionRenderer = observer(function MentionRenderer({
|
||||
default:
|
||||
return (
|
||||
<span key={id} data-flx="messaging.markdown.renderers.mention-renderer.span--13">
|
||||
{'<unknown-mention>'}
|
||||
{`<${i18n._(UNKNOWN_MENTION_DESCRIPTOR)}>`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ export const TimestampRenderer = observer(function TimestampRenderer({
|
||||
const relativeDisplayTime = useMemo(() => {
|
||||
return isValidTimestamp ? formatTimestamp(timestamp, style, i18n) : '';
|
||||
}, [tick, isValidTimestamp, timestamp, style, i18n.locale]);
|
||||
const relativeTime = date !== null ? DateTime.fromJSDate(date).toRelative() : null;
|
||||
const relativeTime = date !== null ? DateTime.fromJSDate(date).setLocale(locale).toRelative() : null;
|
||||
if (date === null || fullDateTime === null) {
|
||||
return React.createElement('span', {className: markupStyles.timestamp}, String(timestamp));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as Modal from '@app/features/app/components/dialogs/Modal';
|
||||
import {cropAnimatedImageWithWorkerPool} from '@app/features/expressions/workers/AnimatedImageCropWorkerManager';
|
||||
import {showMessagingErrorModal} from '@app/features/messaging/components/alerts/MessagingErrorModalUtils';
|
||||
import styles from '@app/features/messaging/components/modals/ImageCropModal.module.css';
|
||||
import {formatFileSize} from '@app/features/messaging/utils/FileUtils';
|
||||
import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import {Button} from '@app/features/ui/button/Button';
|
||||
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
@@ -28,6 +29,11 @@ const COULDN_T_CROP_IMAGE_DESCRIPTOR = msg({
|
||||
message: "Couldn't crop image",
|
||||
comment: 'Title of the error modal shown when image cropping fails.',
|
||||
});
|
||||
const CROPPED_IMAGE_IS_TOO_LARGE_PLEASE_CHOOSE_A_DESCRIPTOR = msg({
|
||||
message: 'Cropped image is too large. Choose a smaller area or a smaller file (max {maxSizeLabel}).',
|
||||
comment:
|
||||
'Error modal body shown when a cropped image still exceeds the size limit. {maxSizeLabel} is a formatted file size.',
|
||||
});
|
||||
const logger = new Logger('ImageCropModal');
|
||||
|
||||
interface Point {
|
||||
@@ -47,6 +53,16 @@ interface DragBoundaries {
|
||||
right: number;
|
||||
}
|
||||
|
||||
class CropSizeLimitError extends Error {
|
||||
readonly maxBytes: number;
|
||||
|
||||
constructor(actualBytes: number, maxBytes: number) {
|
||||
super(`Cropped image is ${actualBytes} bytes and exceeds the limit of ${maxBytes} bytes`);
|
||||
this.name = 'CropSizeLimitError';
|
||||
this.maxBytes = maxBytes;
|
||||
}
|
||||
}
|
||||
|
||||
interface AnimatedImageCropOptions {
|
||||
x: number;
|
||||
y: number;
|
||||
@@ -285,7 +301,7 @@ async function exportStaticImage(
|
||||
}, 'image/png');
|
||||
});
|
||||
if (blob.size > maxBytes) {
|
||||
throw new Error(`Image size ${(blob.size / 1024).toFixed(1)} KB exceeds max ${(maxBytes / 1024).toFixed(0)} KB`);
|
||||
throw new CropSizeLimitError(blob.size, maxBytes);
|
||||
}
|
||||
return blob;
|
||||
}
|
||||
@@ -343,9 +359,7 @@ async function exportAnimatedImage(
|
||||
throw new Error('Empty animated image blob returned');
|
||||
}
|
||||
if (resultBlob.size > maxBytes) {
|
||||
throw new Error(
|
||||
`Animated image size ${(resultBlob.size / 1024).toFixed(1)} KB exceeds max ${(maxBytes / 1024).toFixed(0)} KB`,
|
||||
);
|
||||
throw new CropSizeLimitError(resultBlob.size, maxBytes);
|
||||
}
|
||||
return resultBlob;
|
||||
}
|
||||
@@ -663,7 +677,12 @@ export const ImageCropModal: React.FC<ImageCropModalProps> = observer(
|
||||
ModalCommands.pop();
|
||||
} catch (error) {
|
||||
logger.error('Error cropping image:', error);
|
||||
const message = error instanceof Error && error.message ? error.message : errorMessage;
|
||||
const message =
|
||||
error instanceof CropSizeLimitError
|
||||
? i18n._(CROPPED_IMAGE_IS_TOO_LARGE_PLEASE_CHOOSE_A_DESCRIPTOR, {
|
||||
maxSizeLabel: formatFileSize(i18n.locale, error.maxBytes),
|
||||
})
|
||||
: errorMessage;
|
||||
showMessagingErrorModal({
|
||||
title: i18n._(COULDN_T_CROP_IMAGE_DESCRIPTOR),
|
||||
message,
|
||||
|
||||
@@ -42,6 +42,7 @@ import LayerManager from '@app/features/ui/state/LayerManager';
|
||||
import MobileLayout from '@app/features/ui/state/MobileLayout';
|
||||
import OverlayStack from '@app/features/ui/state/OverlayStack';
|
||||
import {Tooltip} from '@app/features/ui/tooltip/Tooltip';
|
||||
import {formatRoundedPercentage} from '@app/features/ui/utils/PercentageFormatting';
|
||||
import {MobileVideoViewer} from '@app/features/voice/components/modals/MobileVideoViewer';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {CaretLeftIcon, CaretRightIcon} from '@phosphor-icons/react';
|
||||
@@ -663,7 +664,7 @@ export const MediaModal: FC<MediaModalProps> = observer(
|
||||
onPointerLeave={handleHudPointerLeave}
|
||||
data-flx="messaging.media-modal.bottom-info-bar"
|
||||
>
|
||||
{[fileName, dimensions, fileSize, `${panZoomInfo.zoomPercent}%`]
|
||||
{[fileName, dimensions, fileSize, formatRoundedPercentage(i18n.locale, panZoomInfo.zoomPercent)]
|
||||
.filter(Boolean)
|
||||
.map((part, index) => (
|
||||
<span
|
||||
|
||||
@@ -523,7 +523,7 @@ const MediaViewerModalComponent: FC = observer(() => {
|
||||
? `${currentItem.naturalWidth}×${currentItem.naturalHeight}`
|
||||
: undefined;
|
||||
const fileName = currentItem.filename || currentItem.originalSrc.split('/').pop()?.split('?')[0] || 'media';
|
||||
const fileSize = currentItem.fileSize != null ? formatFileSize(currentItem.fileSize) : undefined;
|
||||
const fileSize = currentItem.fileSize != null ? formatFileSize(i18n.locale, currentItem.fileSize) : undefined;
|
||||
const expiryInfo =
|
||||
currentItem.expiresAt && currentItem.expiresAt.length > 0
|
||||
? {
|
||||
|
||||
@@ -26,7 +26,7 @@ const YOU_CAN_ADD_COMMENTS_BEFORE_UPLOADING_DIRECT_UPLOAD_DESCRIPTOR = msg({
|
||||
comment: 'Helper text in the upload drop modal during slowmode, when direct upload is disabled.',
|
||||
});
|
||||
const YOU_CAN_ADD_COMMENTS_BEFORE_UPLOADING_HOLD_SHIFT_DESCRIPTOR = msg({
|
||||
message: 'You can add comments before uploading. Hold shift to upload directly.',
|
||||
message: 'You can add comments before uploading. Hold Shift to upload directly.',
|
||||
comment: 'Helper text in the upload drop modal explaining the shift-to-direct-upload behavior.',
|
||||
});
|
||||
const DIRECT_UPLOAD_ACTIVE_DESCRIPTOR = msg({
|
||||
|
||||
@@ -95,7 +95,7 @@ export const ROTATE_CLOCKWISE_DESCRIPTOR = msg({
|
||||
comment: 'Tooltip on the media viewer button that rotates the current media item 90 degrees clockwise.',
|
||||
});
|
||||
export const ROTATE_ANTICLOCKWISE_DESCRIPTOR = msg({
|
||||
message: 'Rotate anticlockwise',
|
||||
message: 'Rotate counterclockwise',
|
||||
comment: 'Tooltip on the media viewer button that rotates the current media item 90 degrees anticlockwise.',
|
||||
});
|
||||
export const FORWARD_MEDIA_DESCRIPTOR = msg({
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {MessageDescriptor} from '@lingui/core';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
|
||||
const AUTO_DETECT_LANGUAGE_DESCRIPTOR = msg({
|
||||
message: 'Auto detect language',
|
||||
message: 'Auto-detect language',
|
||||
comment: 'Short label in the attachment preview utils helper. Keep it concise.',
|
||||
});
|
||||
const PLAIN_TEXT_DESCRIPTOR = msg({
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
export function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${Math.round((bytes / k ** i) * 10) / 10} ${sizes[i]}`;
|
||||
import {getCachedNumberFormat} from '@app/features/i18n/utils/IntlCache';
|
||||
|
||||
const FILE_SIZE_UNIT_BYTES = 1024;
|
||||
const FILE_SIZE_UNITS = ['byte', 'kilobyte', 'megabyte', 'gigabyte'] as const;
|
||||
|
||||
export function formatFileSize(locale: string, bytes: number): string {
|
||||
if (!Number.isSafeInteger(bytes) || bytes < 0) {
|
||||
throw new RangeError('File size must be a non-negative safe integer');
|
||||
}
|
||||
const unitIndex =
|
||||
bytes === 0
|
||||
? 0
|
||||
: Math.min(Math.floor(Math.log(bytes) / Math.log(FILE_SIZE_UNIT_BYTES)), FILE_SIZE_UNITS.length - 1);
|
||||
return getCachedNumberFormat(locale, {
|
||||
style: 'unit',
|
||||
unit: FILE_SIZE_UNITS[unitIndex],
|
||||
unitDisplay: 'narrow',
|
||||
maximumFractionDigits: 1,
|
||||
}).format(bytes / FILE_SIZE_UNIT_BYTES ** unitIndex);
|
||||
}
|
||||
@@ -67,7 +67,7 @@ describe('reaction tooltip catalog', () => {
|
||||
expect(render('nl', 'test', 1)).toBe('test reageerde met :100:');
|
||||
expect(render('nl', 'test en alex', 2)).toBe('test en alex reageerden met :100:');
|
||||
expect(render('pt-BR', 'test', 1)).toBe('test reagiu com :100:');
|
||||
expect(render('de', 'test', 1)).toBe(':100: Reaktion von test');
|
||||
expect(render('de', 'test', 1)).toBe('test hat mit :100: reagiert');
|
||||
});
|
||||
|
||||
it('leaves English as the emoji-first phrasing the design intends', () => {
|
||||
|
||||
Reference in new issue
Block a user