diff --git a/apps/main/src/components/ChatMessages.tsx b/apps/main/src/components/ChatMessages.tsx index 173dff7..b97d4e6 100644 --- a/apps/main/src/components/ChatMessages.tsx +++ b/apps/main/src/components/ChatMessages.tsx @@ -1,10 +1,11 @@ import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { ChatProvider, ChatMessages as ChatMessageList, ChatComposer, } from "@xtablo/chat-ui"; -import type { ChatMessageData, ChatUser, TypingUser } from "@xtablo/chat-ui"; +import type { ChatLabels, ChatMessageData, ChatUser, TypingUser } from "@xtablo/chat-ui"; interface ChatMessage { id: string; @@ -45,8 +46,9 @@ export function ChatMessages({ onLoadMore: _onLoadMore, onSend, onTyping, - placeholder = "Envoyer un message...", + placeholder, }: ChatMessagesProps) { + const { t } = useTranslation("chat"); const membersById = useMemo(() => { const map = new Map(); for (const m of members) { @@ -58,10 +60,10 @@ export function ChatMessages({ const currentUser = useMemo( () => ({ id: currentUserId, - name: membersById.get(currentUserId)?.name ?? "Moi", + name: membersById.get(currentUserId)?.name ?? t("currentUserName"), avatar: membersById.get(currentUserId)?.avatar_url ?? undefined, }), - [currentUserId, membersById], + [currentUserId, membersById, t], ); const chatMessages = useMemo( @@ -73,29 +75,41 @@ export function ChatMessages({ return { id: msg.id, senderId: userId, - senderName: member?.name ?? "Utilisateur", + senderName: member?.name ?? t("defaultUserName"), senderAvatar: member?.avatar_url ?? undefined, text: msg.text, timestamp: new Date(createdAt), status: msg.optimistic ? "sending" : undefined, }; }), - [messages, membersById], + [messages, membersById, t], ); const chatTypingUsers = useMemo( () => typingUsers.map((userId) => ({ id: userId, - name: membersById.get(userId)?.name ?? "Utilisateur", + name: membersById.get(userId)?.name ?? t("defaultUserName"), avatar: membersById.get(userId)?.avatar_url ?? undefined, })), - [typingUsers, membersById], + [typingUsers, membersById, t], + ); + + const chatLabels = useMemo( + () => ({ + composerPlaceholder: t("placeholder"), + typingOne: t("typingOne"), + typingTwo: t("typingTwo"), + typingMany: t("typingMany"), + scrollToBottom: t("scrollToBottom"), + }), + [t], ); return ( { if (_isTyping) onTyping(); }} - placeholder={placeholder} + placeholder={placeholder ?? t("placeholder")} /> ); diff --git a/apps/main/src/components/TabloDiscussionSection.tsx b/apps/main/src/components/TabloDiscussionSection.tsx index c191485..e223e99 100644 --- a/apps/main/src/components/TabloDiscussionSection.tsx +++ b/apps/main/src/components/TabloDiscussionSection.tsx @@ -1,5 +1,6 @@ import { UserTablo } from "@xtablo/shared/types/tablos.types"; import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { useChat } from "../hooks/useChat"; import { useTabloMembers } from "../hooks/tablos"; import { useUser } from "../providers/UserStoreProvider"; @@ -12,6 +13,7 @@ interface TabloDiscussionSectionProps { } export const TabloDiscussionSection = ({ tablo, isAdmin }: TabloDiscussionSectionProps) => { + const { t } = useTranslation("chat"); const user = useUser(); const { messages, @@ -36,8 +38,8 @@ export const TabloDiscussionSection = ({ tablo, isAdmin }: TabloDiscussionSectio
-

Discussion

-

Conversations liées à ce tablo

+

{t("discussionTitle")}

+

{t("discussionSubtitle")}

diff --git a/apps/main/src/hooks/useChat.ts b/apps/main/src/hooks/useChat.ts index 96b223c..aafa5dd 100644 --- a/apps/main/src/hooks/useChat.ts +++ b/apps/main/src/hooks/useChat.ts @@ -107,6 +107,7 @@ export function useChat(channelId: string | undefined) { break; case "typing": + if (msg.userId === session?.user?.id) break; setTypingUsers((prev) => msg.isTyping ? prev.includes(msg.userId) ? prev : [...prev, msg.userId] diff --git a/apps/main/src/i18n.ts b/apps/main/src/i18n.ts index 7441806..a8edd69 100644 --- a/apps/main/src/i18n.ts +++ b/apps/main/src/i18n.ts @@ -3,6 +3,7 @@ import LanguageDetector from "i18next-browser-languagedetector"; import { initReactI18next } from "react-i18next"; import authEn from "./locales/en/auth.json"; import availabilitiesEn from "./locales/en/availabilities.json"; +import chatEn from "./locales/en/chat.json"; import commonEn from "./locales/en/common.json"; import componentsEn from "./locales/en/components.json"; import modalsEn from "./locales/en/modals.json"; @@ -15,6 +16,7 @@ import settingsEn from "./locales/en/settings.json"; import tabloEn from "./locales/en/tablo.json"; import authFr from "./locales/fr/auth.json"; import availabilitiesFr from "./locales/fr/availabilities.json"; +import chatFr from "./locales/fr/chat.json"; // Import translation files import commonFr from "./locales/fr/common.json"; import componentsFr from "./locales/fr/components.json"; @@ -45,6 +47,7 @@ i18n notes: notesFr, tablo: tabloFr, onboarding: onboardingFr, + chat: chatFr, }, en: { common: commonEn, @@ -59,6 +62,7 @@ i18n notes: notesEn, tablo: tabloEn, onboarding: onboardingEn, + chat: chatEn, }, }, lng: "fr", diff --git a/apps/main/src/locales/en/chat.json b/apps/main/src/locales/en/chat.json new file mode 100644 index 0000000..c1a48f8 --- /dev/null +++ b/apps/main/src/locales/en/chat.json @@ -0,0 +1,13 @@ +{ + "pageTitle": "Discussions", + "placeholder": "Type a message...", + "selectConversation": "Select a conversation to start chatting", + "defaultUserName": "User", + "currentUserName": "Me", + "discussionTitle": "Discussion", + "discussionSubtitle": "Conversations related to this tablo", + "typingOne": "{{name}} is typing", + "typingTwo": "{{name1}} and {{name2}} are typing", + "typingMany": "Several people are typing", + "scrollToBottom": "Scroll to bottom" +} diff --git a/apps/main/src/locales/fr/chat.json b/apps/main/src/locales/fr/chat.json new file mode 100644 index 0000000..3c730aa --- /dev/null +++ b/apps/main/src/locales/fr/chat.json @@ -0,0 +1,13 @@ +{ + "pageTitle": "Discussions", + "placeholder": "Envoyer un message...", + "selectConversation": "Sélectionnez une conversation pour commencer", + "defaultUserName": "Utilisateur", + "currentUserName": "Moi", + "discussionTitle": "Discussion", + "discussionSubtitle": "Conversations liées à ce tablo", + "typingOne": "{{name}} est en train d'écrire", + "typingTwo": "{{name1}} et {{name2}} sont en train d'écrire", + "typingMany": "Plusieurs personnes sont en train d'écrire", + "scrollToBottom": "Aller en bas" +} diff --git a/apps/main/src/pages/chat.tsx b/apps/main/src/pages/chat.tsx index ea61187..81d74ff 100644 --- a/apps/main/src/pages/chat.tsx +++ b/apps/main/src/pages/chat.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useNavigate, useParams } from "react-router-dom"; import { ChatChannelPreview } from "../components/ChatChannelPreview"; import { ChatHeader } from "../components/ChatHeader"; @@ -9,6 +10,7 @@ import { useTablosList, useTabloMembers } from "../hooks/tablos"; import { useUser } from "../providers/UserStoreProvider"; export function ChatPage() { + const { t } = useTranslation("chat"); const user = useUser(); const { channelId } = useParams(); const navigate = useNavigate(); @@ -44,7 +46,7 @@ export function ChatPage() { return (
-

Discussions

+

{t("pageTitle")}

{/* Channel list sidebar */} @@ -87,13 +89,13 @@ export function ChatPage() { onLoadMore={loadMoreMessages} onSend={sendMessage} onTyping={sendTyping} - placeholder="Type a message..." + placeholder={t("placeholder")} />
) : (
- Select a conversation to start chatting + {t("selectConversation")}
)}
diff --git a/packages/chat-ui/src/components/chat.tsx b/packages/chat-ui/src/components/chat.tsx index ef96759..6cd9279 100644 --- a/packages/chat-ui/src/components/chat.tsx +++ b/packages/chat-ui/src/components/chat.tsx @@ -27,6 +27,7 @@ import { createPortal } from "react-dom" import type { ChatUser, ChatConfig, + ChatLabels, ChatMessageData, MessageGroup, TypingUser, @@ -56,6 +57,7 @@ interface ChatProviderProps { currentUser: ChatUser dateFormat?: "relative" | "absolute" | "time-only" messageGroupingInterval?: number + labels?: ChatLabels onReactionAdd?: (messageId: string, emoji: string) => void onReactionRemove?: (messageId: string, emoji: string) => void onReply?: (message: ChatMessageData) => void @@ -71,6 +73,7 @@ function ChatProvider({ currentUser, dateFormat = "relative", messageGroupingInterval = 120, + labels, onReactionAdd, onReactionRemove, onReply, @@ -86,6 +89,7 @@ function ChatProvider({ currentUser, dateFormat, messageGroupingInterval, + labels, onReactionAdd, onReactionRemove, onReply, @@ -93,7 +97,7 @@ function ChatProvider({ onDelete, onPin, }), - [currentUser, dateFormat, messageGroupingInterval, onReactionAdd, onReactionRemove, onReply, onEdit, onDelete, onPin] + [currentUser, dateFormat, messageGroupingInterval, labels, onReactionAdd, onReactionRemove, onReply, onEdit, onDelete, onPin] ) return ( @@ -879,14 +883,15 @@ interface ChatTypingIndicatorProps { } function ChatTypingIndicator({ users, className }: ChatTypingIndicatorProps) { + const { labels } = useChatContext() if (users.length === 0) return null const label = users.length === 1 - ? `${users[0]!.name} is typing` + ? (labels?.typingOne?.replace("{{name}}", users[0]!.name) ?? `${users[0]!.name} is typing`) : users.length === 2 - ? `${users[0]!.name} and ${users[1]!.name} are typing` - : "Several people are typing" + ? (labels?.typingTwo?.replace("{{name1}}", users[0]!.name).replace("{{name2}}", users[1]!.name) ?? `${users[0]!.name} and ${users[1]!.name} are typing`) + : (labels?.typingMany ?? "Several people are typing") return (
0 - ? `${unseenCount} new messages, scroll to bottom` - : "Scroll to bottom" + aria-label={labels?.scrollToBottom ?? "Scroll to bottom" } > diff --git a/packages/chat-ui/src/index.ts b/packages/chat-ui/src/index.ts index 4d5c740..2ea8fee 100644 --- a/packages/chat-ui/src/index.ts +++ b/packages/chat-ui/src/index.ts @@ -95,6 +95,7 @@ export type { FileValidationResult } from "./security" // Types export type { ChatUser, + ChatLabels, ChatMessageData, ChatConfig, MessageGroup, diff --git a/packages/chat-ui/src/types.ts b/packages/chat-ui/src/types.ts index f933a99..1a7da7e 100644 --- a/packages/chat-ui/src/types.ts +++ b/packages/chat-ui/src/types.ts @@ -38,10 +38,19 @@ export interface ChatMessageData { readBy?: { userId: string; name: string; avatar?: string }[] } +export interface ChatLabels { + composerPlaceholder?: string + typingOne?: string // e.g. "{{name}} is typing" + typingTwo?: string // e.g. "{{name1}} and {{name2}} are typing" + typingMany?: string // e.g. "Several people are typing" + scrollToBottom?: string +} + export interface ChatConfig { currentUser: ChatUser dateFormat?: "relative" | "absolute" | "time-only" messageGroupingInterval?: number // seconds, default 120 + labels?: ChatLabels // Callbacks onReactionAdd?: (messageId: string, emoji: string) => void