feat(i18n): localize the emoji and sticker picker into English and Russian

This commit is contained in:
heaven 2026-06-03 16:32:00 +03:00
parent cd050c309b
commit 8989c0d7f7
8 changed files with 105 additions and 33 deletions

View file

@ -1,4 +1,28 @@
{
"EmojiBoard": {
"sticker": "Sticker",
"emoji": "Emoji",
"search": "Search",
"search_or_react": "Search or Text Reaction",
"react": "React",
"no_sticker_packs": "No Sticker Packs!",
"add_stickers_hint": "Add stickers from user, room or space settings.",
"recent": "Recent",
"personal_pack": "Personal Pack",
"unknown": "Unknown",
"unknown_pack": "Unknown Pack",
"search_results": "Search Results",
"no_results": "No Results found",
"aria_emoji": "{{name}} emoji",
"cat_people": "Smileys & People",
"cat_nature": "Animals & Nature",
"cat_food": "Food & Drinks",
"cat_activity": "Activity",
"cat_travel": "Travel & Places",
"cat_object": "Objects",
"cat_symbol": "Symbols",
"cat_flag": "Flags"
},
"Organisms": {
"RoomCommon": {
"changed_room_name": " changed room name"

View file

@ -1,4 +1,28 @@
{
"EmojiBoard": {
"sticker": "Стикеры",
"emoji": "Эмодзи",
"search": "Поиск",
"search_or_react": "Поиск или текст-реакция",
"react": "Реакция",
"no_sticker_packs": "Нет наборов стикеров",
"add_stickers_hint": "Добавьте стикеры в настройках профиля, комнаты или пространства.",
"recent": "Недавние",
"personal_pack": "Личный набор",
"unknown": "Без названия",
"unknown_pack": "Набор без названия",
"search_results": "Результаты поиска",
"no_results": "Ничего не найдено",
"aria_emoji": "{{name}}, эмодзи",
"cat_people": "Смайлы и люди",
"cat_nature": "Животные и природа",
"cat_food": "Еда и напитки",
"cat_activity": "Активности",
"cat_travel": "Путешествия и места",
"cat_object": "Предметы",
"cat_symbol": "Символы",
"cat_flag": "Флаги"
},
"Organisms": {
"RoomCommon": {
"changed_room_name": " изменил(а) название комнаты"

View file

@ -15,6 +15,7 @@ import { isKeyHotkey } from 'is-hotkey';
import { Room } from 'matrix-js-sdk';
import { atom, PrimitiveAtom, useAtom, useSetAtom } from 'jotai';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useTranslation } from 'react-i18next';
import { IEmoji } from '../../plugins/emoji';
import { emojiGroups, emojis } from '../../plugins/emoji-data';
import { useEmojiGroupLabels } from './useEmojiGroupLabels';
@ -76,6 +77,7 @@ const useGroups = (
const recentEmojis = useRecentEmoji(mx, 21);
const labels = useEmojiGroupLabels();
const { t } = useTranslation();
const emojiGroupItems = useMemo(() => {
const g: EmojiGroupItem[] = [];
@ -83,17 +85,18 @@ const useGroups = (
g.push({
id: RECENT_GROUP_ID,
name: 'Recent',
name: t('EmojiBoard.recent'),
items: recentEmojis,
});
imagePacks.forEach((pack) => {
let label = pack.meta.name;
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
if (!label)
label = isUserId(pack.id) ? t('EmojiBoard.personal_pack') : mx.getRoom(pack.id)?.name;
g.push({
id: pack.id,
name: label ?? 'Unknown',
name: label ?? t('EmojiBoard.unknown'),
items: pack
.getImages(ImageUsage.Emoticon)
.sort((a, b) => a.shortcode.localeCompare(b.shortcode)),
@ -109,7 +112,7 @@ const useGroups = (
});
return g;
}, [mx, recentEmojis, labels, imagePacks, tab]);
}, [mx, recentEmojis, labels, imagePacks, tab, t]);
const stickerGroupItems = useMemo(() => {
const g: StickerGroupItem[] = [];
@ -117,11 +120,12 @@ const useGroups = (
imagePacks.forEach((pack) => {
let label = pack.meta.name;
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
if (!label)
label = isUserId(pack.id) ? t('EmojiBoard.personal_pack') : mx.getRoom(pack.id)?.name;
g.push({
id: pack.id,
name: label ?? 'Unknown',
name: label ?? t('EmojiBoard.unknown'),
items: pack
.getImages(ImageUsage.Sticker)
.sort((a, b) => a.shortcode.localeCompare(b.shortcode)),
@ -129,7 +133,7 @@ const useGroups = (
});
return g;
}, [mx, imagePacks, tab]);
}, [mx, imagePacks, tab, t]);
return [emojiGroupItems, stickerGroupItems];
};
@ -178,6 +182,7 @@ function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarP
const usage = ImageUsage.Emoticon;
const labels = useEmojiGroupLabels();
const icons = useEmojiGroupIcons();
const { t } = useTranslation();
const handleScrollToGroup = (groupId: string) => {
setActiveGroupId(groupId);
@ -190,7 +195,7 @@ function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarP
<GroupIcon
active={activeGroupId === RECENT_GROUP_ID}
id={RECENT_GROUP_ID}
label="Recent"
label={t('EmojiBoard.recent')}
icon={Icons.RecentClock}
onClick={handleScrollToGroup}
/>
@ -200,7 +205,8 @@ function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarP
<SidebarDivider />
{packs.map((pack) => {
let label = pack.meta.name;
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
if (!label)
label = isUserId(pack.id) ? t('EmojiBoard.personal_pack') : mx.getRoom(pack.id)?.name;
const url =
mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ?? undefined;
@ -210,7 +216,7 @@ function EmojiSidebar({ activeGroupAtom, packs, onScrollToGroup }: EmojiSidebarP
key={pack.id}
active={activeGroupId === pack.id}
id={pack.id}
label={label ?? 'Unknown Pack'}
label={label ?? t('EmojiBoard.unknown_pack')}
url={url}
onClick={handleScrollToGroup}
/>
@ -252,6 +258,7 @@ function StickerSidebar({ activeGroupAtom, packs, onScrollToGroup }: StickerSide
const [activeGroupId, setActiveGroupId] = useAtom(activeGroupAtom);
const usage = ImageUsage.Sticker;
const { t } = useTranslation();
const handleScrollToGroup = (groupId: string) => {
setActiveGroupId(groupId);
@ -263,7 +270,8 @@ function StickerSidebar({ activeGroupAtom, packs, onScrollToGroup }: StickerSide
<SidebarStack>
{packs.map((pack) => {
let label = pack.meta.name;
if (!label) label = isUserId(pack.id) ? 'Personal Pack' : mx.getRoom(pack.id)?.name;
if (!label)
label = isUserId(pack.id) ? t('EmojiBoard.personal_pack') : mx.getRoom(pack.id)?.name;
const url =
mxcUrlToHttp(mx, pack.getAvatarUrl(usage) ?? '', useAuthentication) ?? undefined;
@ -273,7 +281,7 @@ function StickerSidebar({ activeGroupAtom, packs, onScrollToGroup }: StickerSide
key={pack.id}
active={activeGroupId === pack.id}
id={pack.id}
label={label ?? 'Unknown Pack'}
label={label ?? t('EmojiBoard.unknown_pack')}
url={url}
onClick={handleScrollToGroup}
/>
@ -378,6 +386,7 @@ export function EmojiBoard({
addToRecentEmoji = true,
}: EmojiBoardProps) {
const mx = useMatrixClient();
const { t } = useTranslation();
const emojiTab = tab === EmojiBoardTab.Emoji;
const usage = emojiTab ? ImageUsage.Emoticon : ImageUsage.Sticker;
@ -542,7 +551,9 @@ export function EmojiBoard({
{searchedItems && (
<EmojiGroup
id={SEARCH_GROUP_ID}
label={searchedItems.length ? 'Search Results' : 'No Results found'}
label={
searchedItems.length ? t('EmojiBoard.search_results') : t('EmojiBoard.no_results')
}
>
{searchedItems.map(renderItem)}
</EmojiGroup>

View file

@ -1,5 +1,6 @@
import React from 'react';
import { Box } from 'folds';
import { useTranslation } from 'react-i18next';
import { MatrixClient } from 'matrix-js-sdk';
import { EmojiItemInfo, EmojiType } from '../types';
import * as css from './styles.css';
@ -27,6 +28,7 @@ type EmojiItemProps = {
emoji: IEmoji;
};
export function EmojiItem({ emoji }: EmojiItemProps) {
const { t } = useTranslation();
return (
<Box
as="button"
@ -35,7 +37,7 @@ export function EmojiItem({ emoji }: EmojiItemProps) {
justifyContent="Center"
className={css.EmojiItem}
title={emoji.label}
aria-label={`${emoji.label} emoji`}
aria-label={t('EmojiBoard.aria_emoji', { name: emoji.label })}
data-emoji-type={EmojiType.Emoji}
data-emoji-data={emoji.unicode}
data-emoji-shortcode={emoji.shortcode}
@ -51,6 +53,7 @@ type CustomEmojiItemProps = {
image: PackImageReader;
};
export function CustomEmojiItem({ mx, useAuthentication, image }: CustomEmojiItemProps) {
const { t } = useTranslation();
return (
<Box
as="button"
@ -59,7 +62,7 @@ export function CustomEmojiItem({ mx, useAuthentication, image }: CustomEmojiIte
justifyContent="Center"
className={css.EmojiItem}
title={image.body || image.shortcode}
aria-label={`${image.body || image.shortcode} emoji`}
aria-label={t('EmojiBoard.aria_emoji', { name: image.body || image.shortcode })}
data-emoji-type={EmojiType.CustomEmoji}
data-emoji-data={image.url}
data-emoji-shortcode={image.shortcode}
@ -81,6 +84,7 @@ type StickerItemProps = {
};
export function StickerItem({ mx, useAuthentication, image }: StickerItemProps) {
const { t } = useTranslation();
return (
<Box
as="button"
@ -89,7 +93,7 @@ export function StickerItem({ mx, useAuthentication, image }: StickerItemProps)
justifyContent="Center"
className={css.StickerItem}
title={image.body || image.shortcode}
aria-label={`${image.body || image.shortcode} emoji`}
aria-label={t('EmojiBoard.aria_emoji', { name: image.body || image.shortcode })}
data-emoji-type={EmojiType.Sticker}
data-emoji-data={image.url}
data-emoji-shortcode={image.shortcode}

View file

@ -1,7 +1,9 @@
import React from 'react';
import { Box, toRem, config, Icons, Icon, Text } from 'folds';
import { useTranslation } from 'react-i18next';
export function NoStickerPacks() {
const { t } = useTranslation();
return (
<Box
style={{ padding: `${toRem(60)} ${config.space.S500}` }}
@ -12,9 +14,9 @@ export function NoStickerPacks() {
>
<Icon size="600" src={Icons.Sticker} />
<Box direction="Inherit">
<Text align="Center">No Sticker Packs!</Text>
<Text align="Center">{t('EmojiBoard.no_sticker_packs')}</Text>
<Text priority="300" align="Center" size="T200">
Add stickers from user, room or space settings.
{t('EmojiBoard.add_stickers_hint')}
</Text>
</Box>
</Box>

View file

@ -1,5 +1,6 @@
import React, { ChangeEventHandler, useRef } from 'react';
import { Input, Chip, Icon, Icons, Text } from 'folds';
import { useTranslation } from 'react-i18next';
import { mobileOrTablet } from '../../../utils/user-agent';
type SearchInputProps = {
@ -15,6 +16,7 @@ export function SearchInput({
onTextCustomEmojiSelect,
}: SearchInputProps) {
const inputRef = useRef<HTMLInputElement>(null);
const { t } = useTranslation();
const handleReact = () => {
const textEmoji = inputRef.current?.value.trim();
@ -27,7 +29,7 @@ export function SearchInput({
ref={inputRef}
variant="SurfaceVariant"
size="400"
placeholder={allowTextCustomEmoji ? 'Search or Text Reaction ' : 'Search'}
placeholder={allowTextCustomEmoji ? t('EmojiBoard.search_or_react') : t('EmojiBoard.search')}
maxLength={50}
after={
allowTextCustomEmoji && query ? (
@ -38,7 +40,7 @@ export function SearchInput({
outlined
onClick={handleReact}
>
<Text size="L400">React</Text>
<Text size="L400">{t('EmojiBoard.react')}</Text>
</Chip>
) : (
<Icon src={Icons.Search} size="50" />

View file

@ -1,5 +1,6 @@
import React, { CSSProperties } from 'react';
import { Badge, Box, Text } from 'folds';
import { useTranslation } from 'react-i18next';
import { EmojiBoardTab } from '../types';
const styles: CSSProperties = {
@ -13,6 +14,7 @@ export function EmojiBoardTabs({
tab: EmojiBoardTab;
onTabChange: (tab: EmojiBoardTab) => void;
}) {
const { t } = useTranslation();
return (
<Box gap="100">
<Badge
@ -24,7 +26,7 @@ export function EmojiBoardTabs({
onClick={() => onTabChange(EmojiBoardTab.Sticker)}
>
<Text as="span" size="L400">
Sticker
{t('EmojiBoard.sticker')}
</Text>
</Badge>
<Badge
@ -36,7 +38,7 @@ export function EmojiBoardTabs({
onClick={() => onTabChange(EmojiBoardTab.Emoji)}
>
<Text as="span" size="L400">
Emoji
{t('EmojiBoard.emoji')}
</Text>
</Badge>
</Box>

View file

@ -1,19 +1,22 @@
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { EmojiGroupId } from '../../plugins/emoji';
export type IEmojiGroupLabels = Record<EmojiGroupId, string>;
export const useEmojiGroupLabels = (): IEmojiGroupLabels =>
useMemo(
export const useEmojiGroupLabels = (): IEmojiGroupLabels => {
const { t } = useTranslation();
return useMemo(
() => ({
[EmojiGroupId.People]: 'Smileys & People',
[EmojiGroupId.Nature]: 'Animals & Nature',
[EmojiGroupId.Food]: 'Food & Drinks',
[EmojiGroupId.Activity]: 'Activity',
[EmojiGroupId.Travel]: 'Travel & Places',
[EmojiGroupId.Object]: 'Objects',
[EmojiGroupId.Symbol]: 'Symbols',
[EmojiGroupId.Flag]: 'Flags',
[EmojiGroupId.People]: t('EmojiBoard.cat_people'),
[EmojiGroupId.Nature]: t('EmojiBoard.cat_nature'),
[EmojiGroupId.Food]: t('EmojiBoard.cat_food'),
[EmojiGroupId.Activity]: t('EmojiBoard.cat_activity'),
[EmojiGroupId.Travel]: t('EmojiBoard.cat_travel'),
[EmojiGroupId.Object]: t('EmojiBoard.cat_object'),
[EmojiGroupId.Symbol]: t('EmojiBoard.cat_symbol'),
[EmojiGroupId.Flag]: t('EmojiBoard.cat_flag'),
}),
[]
[t]
);
};