i18n: translate the chat composer and messages
OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s
Catching up on the fork's rule that every update also translates one existing system — three updates went by without one. The composer's placeholder takes the channel as a parameter rather than being concatenated, since Portuguese puts the channel elsewhere in the sentence than English does. The translation hook is bound to `tr` in these two files, not `t`: both already use `t` for transfer records in inner scopes, which shadowed the function and made it uncallable exactly where it was needed.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { useT } from '../../i18n';
|
||||
import { createPortal } from 'react-dom';
|
||||
import type { MessageWithUser, Embed, User } from '@backspace/shared';
|
||||
import { MarkdownRenderer } from './MarkdownRenderer';
|
||||
@@ -131,6 +132,7 @@ export function Message({ message, isCompact, isFirstInGroup, previousMessageId
|
||||
const editMessage = useChatStore((s) => s.editMessage);
|
||||
const deleteMessage = useChatStore((s) => s.deleteMessage);
|
||||
const members = useSpaceStore((s) => s.members);
|
||||
const tr = useT();
|
||||
const openUserProfile = useUIStore((s) => s.openUserProfile);
|
||||
|
||||
const pending = isPendingMessage(message) ? message.__pending : null;
|
||||
@@ -676,7 +678,7 @@ export function Message({ message, isCompact, isFirstInGroup, previousMessageId
|
||||
className={`p-1 hover:bg-interactive-hover rounded transition-colors text-[14px] leading-none ${
|
||||
showReactionPicker ? 'text-accent-primary' : 'text-txt-tertiary hover:text-txt-secondary'
|
||||
}`}
|
||||
title="Add reaction"
|
||||
title={tr('chat.message.addReaction')}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm1-13h-2v4H7v2h4v4h2v-4h4v-2h-4V7z" />
|
||||
@@ -687,7 +689,7 @@ export function Message({ message, isCompact, isFirstInGroup, previousMessageId
|
||||
<button
|
||||
onClick={() => setReplyTo(message)}
|
||||
className="px-2 h-full text-txt-tertiary hover:text-txt-primary hover:bg-interactive-hover transition-all flex items-center justify-center"
|
||||
title="Reply"
|
||||
title={tr('chat.message.reply')}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M10 9V5L3 12L10 19V14.9C15 14.9 18.5 16.5 21 20C20 15 17 10 10 9Z" />
|
||||
@@ -700,7 +702,7 @@ export function Message({ message, isCompact, isFirstInGroup, previousMessageId
|
||||
setIsEditing(true);
|
||||
}}
|
||||
className="px-2 h-full text-txt-tertiary hover:text-txt-primary hover:bg-interactive-hover transition-all flex items-center justify-center"
|
||||
title="Edit"
|
||||
title={tr('chat.message.edit')}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z" />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState, useRef, useCallback, useMemo, useEffect } from 'react';
|
||||
import { useT } from '../../i18n';
|
||||
import { useChatStore } from '../../stores/chatStore';
|
||||
import { isDmChannel, getChannelOrigin, useSpaceStore } from '../../stores/spaceStore';
|
||||
import { wsSend } from '../../hooks/useWebSocket';
|
||||
@@ -94,6 +95,7 @@ export function MessageInput({ channelId, channelName, placeholder }: MessageInp
|
||||
const typingTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
// Feature flags
|
||||
const tr = useT();
|
||||
const gifEnabled = useSettingsStore((s) => s.gifEnabled);
|
||||
|
||||
// Permission gating: DM channels always allow sending; space channels check SEND_MESSAGES
|
||||
@@ -775,7 +777,7 @@ export function MessageInput({ channelId, channelName, placeholder }: MessageInp
|
||||
{chatReplyTo && (
|
||||
<div className="bg-interactive-hover rounded-t-lg px-4 py-2 flex items-center justify-between border-b border-white/[0.06]">
|
||||
<div className="flex items-center gap-1 text-[14px] text-txt-message truncate">
|
||||
<span className="opacity-60">Replying to</span>
|
||||
<span className="opacity-60">{tr('chat.composer.replyingTo')}</span>
|
||||
<span className="font-bold">
|
||||
{chatReplyTo.user.displayName ?? chatReplyTo.user.username}
|
||||
</span>
|
||||
@@ -783,7 +785,7 @@ export function MessageInput({ channelId, channelName, placeholder }: MessageInp
|
||||
<button
|
||||
onClick={() => chatSetReplyTo(null)}
|
||||
className="text-txt-tertiary hover:text-txt-primary transition-colors"
|
||||
aria-label="Cancel reply"
|
||||
aria-label={tr('chat.composer.cancelReply')}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M18.4 4L12 10.4L5.6 4L4 5.6L10.4 12L4 18.4L5.6 20L12 13.6L18.4 20L20 18.4L13.6 12L20 5.6L18.4 4Z" />
|
||||
@@ -867,7 +869,7 @@ export function MessageInput({ channelId, channelName, placeholder }: MessageInp
|
||||
<button
|
||||
onClick={() => removeStagedTransfer(t.id)}
|
||||
className="absolute -top-2 -right-2 w-7 h-7 bg-accent-rose hover:bg-accent-rose/80 shadow-elevation-high rounded-lg flex items-center justify-center text-white transition-colors z-10"
|
||||
aria-label="Remove attachment"
|
||||
aria-label={tr('chat.composer.removeAttachment')}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M5 2a1 1 0 011-1h4a1 1 0 011 1v1h3a1 1 0 110 2h-.08L13 14a2 2 0 01-2 2H5a2 2 0 01-2-2L2.08 5H2a1 1 0 110-2h3V2zm2 0v1h2V2H7z" />
|
||||
@@ -886,8 +888,8 @@ export function MessageInput({ channelId, channelName, placeholder }: MessageInp
|
||||
<button
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="w-10 h-10 md:w-[34px] md:h-[34px] flex items-center justify-center rounded-[6px] text-txt-tertiary hover:text-txt-secondary transition-colors flex-shrink-0"
|
||||
title="Attach file"
|
||||
aria-label="Attach file"
|
||||
title={tr('chat.composer.attach')}
|
||||
aria-label={tr('chat.composer.attach')}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z" />
|
||||
@@ -915,14 +917,16 @@ export function MessageInput({ channelId, channelName, placeholder }: MessageInp
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={canAttachFiles ? handlePaste : undefined}
|
||||
placeholder={placeholder ?? `Message ${channelName.startsWith('@') ? channelName : `#${channelName}`}`}
|
||||
placeholder={placeholder ?? tr('chat.composer.placeholder', {
|
||||
channel: channelName.startsWith('@') ? channelName : `#${channelName}`,
|
||||
})}
|
||||
className="input-embedded flex-1 py-[10px] px-1 resize-none text-[15px] leading-[1.375rem] max-h-[50vh] scrollbar-thin"
|
||||
rows={1}
|
||||
/>
|
||||
|
||||
{/* Active-upload indicator */}
|
||||
{anyActiveOrQueued && (
|
||||
<div className="p-3 text-txt-tertiary" title="Uploading…" aria-label="Uploading">
|
||||
<div className="p-3 text-txt-tertiary" title={tr('chat.composer.uploadingEllipsis')} aria-label={tr('chat.composer.uploading')}>
|
||||
<svg className="w-5 h-5 animate-spin" viewBox="0 0 24 24" fill="none">
|
||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
||||
@@ -934,7 +938,7 @@ export function MessageInput({ channelId, channelName, placeholder }: MessageInp
|
||||
{failedCount > 0 && (
|
||||
<span
|
||||
className="text-[12px] font-medium text-accent-rose px-1 flex-shrink-0"
|
||||
title="Remove or retry the failed attachment to send"
|
||||
title={tr('chat.composer.failedAttachment')}
|
||||
>
|
||||
{failedCount} failed
|
||||
</span>
|
||||
@@ -956,8 +960,8 @@ export function MessageInput({ channelId, channelName, placeholder }: MessageInp
|
||||
className={`w-10 h-10 md:w-[34px] md:h-[34px] flex items-center justify-center rounded-[6px] transition-colors flex-shrink-0 ${
|
||||
activePopover === 'gif' ? 'text-accent-primary' : 'text-txt-tertiary hover:text-txt-secondary'
|
||||
}`}
|
||||
title="GIF"
|
||||
aria-label="GIF picker"
|
||||
title={tr('chat.composer.gif')}
|
||||
aria-label={tr('chat.composer.gifPicker')}
|
||||
>
|
||||
{/* Outlined badge, not a filled block: the solid rectangle read as
|
||||
a plain square rather than a GIF picker. Letters reuse the
|
||||
@@ -977,8 +981,8 @@ export function MessageInput({ channelId, channelName, placeholder }: MessageInp
|
||||
className={`w-10 h-10 md:w-[34px] md:h-[34px] flex items-center justify-center rounded-[6px] transition-colors flex-shrink-0 ${
|
||||
activePopover === 'emoji' ? 'text-accent-primary' : 'text-txt-tertiary hover:text-txt-secondary'
|
||||
}`}
|
||||
title="Emoji"
|
||||
aria-label="Emoji picker"
|
||||
title={tr('chat.composer.emoji')}
|
||||
aria-label={tr('chat.composer.emojiPicker')}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5s.67 1.5 1.5 1.5zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5s.67 1.5 1.5 1.5zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z" />
|
||||
@@ -991,8 +995,8 @@ export function MessageInput({ channelId, channelName, placeholder }: MessageInp
|
||||
onClick={() => void handleSubmit()}
|
||||
disabled={anyUnshippable}
|
||||
className="w-10 h-10 md:w-[34px] md:h-[34px] flex items-center justify-center rounded-[6px] bg-accent-primary hover:bg-accent-primary-hover text-white transition-all duration-150 flex-shrink-0 disabled:opacity-50"
|
||||
aria-label="Send message"
|
||||
title="Send"
|
||||
aria-label={tr('chat.composer.sendMessage')}
|
||||
title={tr('chat.composer.send')}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M3.4 20.4l17.45-7.48a1 1 0 000-1.84L3.4 3.6a.993.993 0 00-1.39.91L2 9.12c0 .5.37.93.87.99L17 12 2.87 13.88c-.5.07-.87.5-.87 1l.01 4.61c0 .71.73 1.2 1.39.91z" />
|
||||
|
||||
Reference in New Issue
Block a user