Skip to content

Commit 7ffad52

Browse files
feat: support i18n
1 parent 0dc80bd commit 7ffad52

27 files changed

+416
-199
lines changed

i18next-parser.config.mjs

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default {
2+
output: "src/i18n/locales/$LOCALE/$NAMESPACE.json",
3+
locales: ["en"]
4+
};

package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"private": false,
1010
"types": "dist/index.d.ts",
1111
"scripts": {
12-
"build": "vite build",
12+
"i18n-extract": "i18next 'src/**/*.tsx' --config i18next-parser.config.mjs",
13+
"build": "pnpm run i18n-extract && vite build",
1314
"storybook": "storybook dev -p 6006",
1415
"build-storybook": "storybook build",
1516
"prepublishOnly": "pnpm build",
@@ -34,6 +35,7 @@
3435
"discord-api-types": "^0.37.52",
3536
"filesize": "^10.0.6",
3637
"highlight.js": "^11.7.0",
38+
"i18next": "^23.4.4",
3739
"lodash": "^4.17.21",
3840
"lottie-web": "^5.10.1",
3941
"memoizee": "^0.4.15",
@@ -43,6 +45,7 @@
4345
"react": "^18.2.0",
4446
"react-dom": "^18.2.0",
4547
"react-easy-emoji": "^1.8.0",
48+
"react-i18next": "^13.1.2",
4649
"react-lazy-load-image-component": "^1.6.0",
4750
"simple-markdown": "^0.7.3"
4851
},
@@ -78,6 +81,7 @@
7881
"eslint-plugin-react": "^7.32.2",
7982
"eslint-plugin-storybook": "^0.6.13",
8083
"gh-pages": "^5.0.0",
84+
"i18next-parser": "^8.5.0",
8185
"prettier": "^2.8.1",
8286
"react-docgen-typescript-loader": "^3.7.2",
8387
"storybook": "^7.2.3",

src/ChatTag/index.tsx

+14-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React from "react";
33
import * as Styles from "./style";
44
import type { APIUser } from "discord-api-types/v10";
55
import { useConfig } from "../core/ConfigContext";
6+
import { useTranslation } from "react-i18next";
67

78
// todo: support custom
89
const verified = (
@@ -35,6 +36,8 @@ interface TagProps {
3536
}
3637

3738
function ChatTag({ author, crossPost, referenceGuild }: TagProps) {
39+
const { t } = useTranslation();
40+
3841
const { chatBadge: ChatBadge } = useConfig();
3942

4043
if (ChatBadge !== undefined) {
@@ -46,15 +49,22 @@ function ChatTag({ author, crossPost, referenceGuild }: TagProps) {
4649

4750
if (author.system || referenceGuild === "667560445975986187")
4851
return (
49-
<Styles.Tag className="verified system">{verified} SYSTEM</Styles.Tag>
52+
<Styles.Tag className="verified system">
53+
{verified} {t("chatTag.system")}
54+
</Styles.Tag>
5055
);
5156

52-
if (crossPost) return <Styles.Tag className="server">SERVER</Styles.Tag>;
57+
if (crossPost)
58+
return <Styles.Tag className="server">{t("chatTag.server")}</Styles.Tag>;
5359

5460
if (isVerifiedBot(author.flags))
55-
return <Styles.Tag className="verified bot">{verified} BOT</Styles.Tag>;
61+
return (
62+
<Styles.Tag className="verified bot">
63+
{verified} {t("chatTag.bot")}
64+
</Styles.Tag>
65+
);
5666

57-
return <Styles.Tag className="bot">BOT</Styles.Tag>;
67+
return <Styles.Tag className="bot">{t("chatTag.bot")}</Styles.Tag>;
5868
}
5969

6070
export default ChatTag;

src/Content/Attachment/ImageAttachment.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import useSize from "./useSize";
22
import React from "react";
33
import * as Styles from "./style";
44
import type { APIAttachment } from "discord-api-types/v10";
5+
import { t } from "i18next";
56

67
interface ImageAttachmentProps {
78
attachment: APIAttachment;
@@ -29,7 +30,7 @@ function ImageAttachment(props: ImageAttachmentProps) {
2930
height={height}
3031
placeholder={
3132
<Styles.LazyImagePlaceholder style={{ width, height }}>
32-
Loading...
33+
{t("loading")}
3334
</Styles.LazyImagePlaceholder>
3435
}
3536
/>

src/Content/Attachment/VideoAttachment.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ function VideoAttachment(props: VideoAttachmentProps) {
7575

7676
if (video === null) return;
7777

78-
if (video.paused) video.play();
78+
if (video.paused) void video.play();
7979
else video.pause();
8080
}
8181

src/Content/Thread/ThreadButton.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React from "react";
44
import type { APIChannel } from "discord-api-types/v10";
55
import { MessageType } from "discord-api-types/v10";
66
import { useConfig } from "../../core/ConfigContext";
7+
import { useTranslation } from "react-i18next";
78

89
interface ThreadButtonProps {
910
messageType: MessageType;
@@ -14,6 +15,8 @@ interface ThreadButtonProps {
1415
}
1516

1617
function ThreadButton(props: ThreadButtonProps) {
18+
const { t } = useTranslation();
19+
1720
const { seeThreadOnClick } = useConfig();
1821

1922
return (
@@ -29,7 +32,7 @@ function ThreadButton(props: ThreadButtonProps) {
2932
onClick={() => seeThreadOnClick?.(props.messageId, props.thread)}
3033
role="button"
3134
>
32-
See Thread
35+
{t("ThreadButton.seeThread")}
3336
</Styles.SeeThreadButton>
3437
</Styles.ThreadButtonTopLine>
3538
</Styles.ThreadButton>

src/Content/index.tsx

+10-4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Reactions from "../Message/Reactions";
1515
import ThreadButton from "./Thread/ThreadButton";
1616
import Components from "../Message/Components";
1717
import getDisplayName from "../utils/getDisplayName";
18+
import { useTranslation } from "react-i18next";
1819

1920
interface EditedProps {
2021
editedAt: string;
@@ -69,6 +70,8 @@ interface ContentCoreProps {
6970
}
7071

7172
function ContentCore(props: ContentCoreProps) {
73+
const { t } = useTranslation();
74+
7275
if (!props.showTooltip) return <>{props.children}</>;
7376

7477
return (
@@ -78,7 +81,7 @@ function ContentCore(props: ContentCoreProps) {
7881
{props.referencedMessage ? (
7982
<Message message={props.referencedMessage} isFirstMessage={true} />
8083
) : (
81-
"This message has been deleted or is unavailable"
84+
t("messageGeneric.deleted_or_unavailable")
8285
)}
8386
</Styles.ContentMessageTooltip>
8487
}
@@ -97,16 +100,19 @@ interface ContentProps {
97100
}
98101

99102
function Content(props: ContentProps) {
103+
const { t } = useTranslation();
104+
100105
const dominantAccessoryText = useMemo(() => {
101106
if (!props.isReplyContent) return null;
102107

103-
if (props.message.interaction) return "Click to see command";
108+
if (props.message.interaction)
109+
return t("messageGeneric.slash_command_click");
104110

105111
if (props.message.sticker_items && props.message.sticker_items?.length > 0)
106-
return "Click to see sticker";
112+
return t("messageGeneric.sticker_click");
107113

108114
if (props.message.attachments.length > 0 || props.message.embeds.length > 0)
109-
return "Click to see attachment";
115+
return t("messageGeneric.attachment_click");
110116

111117
return null;
112118
}, [props.message.attachments, props.message.stickers, props.isReplyContent]);

src/Message/MessageAuthor.tsx

+6-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from "react";
2-
import { ComponentProps, useMemo } from "react";
2+
import type { ComponentProps } from "react";
3+
import { useMemo } from "react";
34
import ChatTag from "../ChatTag";
45
import RoleIcon from "./RoleIcon";
56
import getAvatar from "../utils/getAvatar";
@@ -8,7 +9,8 @@ import type { APIRole, APIUser, Snowflake } from "discord-api-types/v10";
89
import { useConfig } from "../core/ConfigContext";
910
import getDisplayName from "../utils/getDisplayName";
1011

11-
interface MessageAuthorProps extends ComponentProps<typeof Styles.MessageAuthor> {
12+
interface MessageAuthorProps
13+
extends ComponentProps<typeof Styles.MessageAuthor> {
1214
author: APIUser;
1315
avatarAnimated?: boolean;
1416
onlyShowUsername?: boolean;
@@ -74,7 +76,7 @@ function MessageAuthor({
7476
<Styles.MessageAuthor
7577
clickable={userMentionOnClick !== undefined}
7678
{...props}
77-
onClick={() => userMentionOnClick?.(user)}
79+
onClick={() => userMentionOnClick?.(author)}
7880
>
7981
<Styles.Username style={{ color: dominantRoleColor }}>
8082
{displayName}
@@ -87,7 +89,7 @@ function MessageAuthor({
8789
<Styles.MessageAuthor
8890
clickable={userMentionOnClick !== undefined}
8991
{...props}
90-
onClick={() => userMentionOnClick?.(user)}
92+
onClick={() => userMentionOnClick?.(author)}
9193
>
9294
<Styles.Avatar
9395
src={getAvatar(author, {

src/Message/Reactions/Reaction.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ import React, { useCallback, useMemo } from "react";
33
import Tooltip from "../../Tooltip";
44
import { findDefaultEmojiByUnicode } from "../../emojiData";
55
import type { APIReaction } from "discord-api-types/v10";
6+
import { useTranslation } from "react-i18next";
67

78
interface ReactionProps {
89
reaction: APIReaction;
910
}
1011

1112
function Reaction(props: ReactionProps) {
13+
const { t } = useTranslation();
14+
1215
const emojiUrl = useMemo(() => {
1316
if (props.reaction.emoji.id === null) return null;
1417

@@ -40,7 +43,7 @@ function Reaction(props: ReactionProps) {
4043
{props.reaction.emoji.id !== null
4144
? emojiName
4245
: findDefaultEmojiByUnicode(props.reaction.emoji.name ?? "")
43-
?.keywords?.[0] ?? "unknown emoji"}
46+
?.keywords?.[0] ?? t("unknownEntities.emoji")}
4447
:
4548
</Styles.ReactionTooltip>
4649
);

src/Message/RoleIcon.tsx

+32-35
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,52 @@
1-
import { PureComponent } from "react";
21
import * as Styles from "./style/message";
32
import Tooltip from "../Tooltip";
43
import webpCheck from "../utils/webpCheck";
5-
import { memoize } from "lodash";
64
import * as React from "react";
75
import { Twemoji } from "../Emoji";
86
import type { APIRole } from "discord-api-types/v10";
7+
import { useTranslation } from "react-i18next";
98

109
interface RoleIconProps {
1110
role: APIRole;
1211
}
1312

14-
class RoleIcon extends PureComponent<RoleIconProps> {
15-
private getRoleIcon = memoize((icon: string, roleId: string): string =>
16-
webpCheck(`https://cdn.discordapp.com/role-icons/${roleId}/${icon}.webp`)
17-
);
18-
19-
render() {
20-
if (
21-
this.props.role.unicode_emoji !== null &&
22-
this.props.role.unicode_emoji !== undefined
23-
)
24-
return (
25-
<Tooltip overlay={this.props.role.name} placement="top">
26-
<span>
27-
<Styles.RoleIcon
28-
as={Twemoji}
29-
disableTooltip={true}
30-
emojiName={this.props.role.unicode_emoji ?? "unknown emoji"}
31-
>
32-
{this.props.role.unicode_emoji}
33-
</Styles.RoleIcon>
34-
</span>
35-
</Tooltip>
36-
);
13+
function RoleIcon(props: RoleIconProps) {
14+
const { t } = useTranslation();
3715

38-
if (this.props.role.icon === null || this.props.role.icon === undefined) {
39-
console.error(
40-
"Role icon AND unicode_emoji is null or undefined but RoleIcon was rendered."
41-
);
42-
return null;
43-
}
44-
45-
const iconUrl = this.getRoleIcon(this.props.role.icon, this.props.role.id);
16+
const roleIconUrl = webpCheck(
17+
`https://cdn.discordapp.com/role-icons/${props.role.id}/${props.role.icon}.webp`
18+
);
4619

20+
if (
21+
props.role.unicode_emoji !== null &&
22+
props.role.unicode_emoji !== undefined
23+
)
4724
return (
48-
<Tooltip overlay={this.props.role.name} placement="top">
49-
<Styles.RoleIcon src={iconUrl} />
25+
<Tooltip overlay={props.role.name} placement="top">
26+
<span>
27+
<Styles.RoleIcon
28+
as={Twemoji}
29+
disableTooltip={true}
30+
emojiName={props.role.unicode_emoji ?? t("unknownEntities.emoji")}
31+
>
32+
{props.role.unicode_emoji}
33+
</Styles.RoleIcon>
34+
</span>
5035
</Tooltip>
5136
);
37+
38+
if (props.role.icon === null || props.role.icon === undefined) {
39+
console.error(
40+
"Role icon AND unicode_emoji is null or undefined but RoleIcon was rendered."
41+
);
42+
return null;
5243
}
44+
45+
return (
46+
<Tooltip overlay={props.role.name} placement="top">
47+
<Styles.RoleIcon src={roleIconUrl} />
48+
</Tooltip>
49+
);
5350
}
5451

5552
export default RoleIcon;

src/Message/variants/BoostTierUpgrade.tsx

+24-6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { SystemMessageIconSize } from "../style/message";
66
import type { APIMessage, Snowflake } from "discord-api-types/v10";
77
import { MessageType } from "discord-api-types/v10";
88
import { useConfig } from "../../core/ConfigContext";
9+
import { Trans, useTranslation } from "react-i18next";
910

1011
interface BoostTierUpgradeProps {
1112
createdAt: APIMessage["timestamp"];
@@ -22,6 +23,8 @@ function BoostTierUpgrade({
2223
type,
2324
author,
2425
}: BoostTierUpgradeProps) {
26+
const { t } = useTranslation();
27+
2528
const { resolveChannel, resolveGuild } = useConfig();
2629
const channel = resolveChannel(channelId);
2730
const guild =
@@ -30,7 +33,9 @@ function BoostTierUpgrade({
3033
: null;
3134

3235
const guildName =
33-
guild !== null ? guild.name ?? "Unknown Guild" : "Unknown Guild";
36+
guild !== null
37+
? guild.name ?? t("unknownEntities.guild")
38+
: t("unknownEntities.guild");
3439

3540
const newLevel = useMemo(() => {
3641
switch (type) {
@@ -45,7 +50,6 @@ function BoostTierUpgrade({
4550
}
4651
}, [type]);
4752

48-
// todo: guildNameHere
4953
return (
5054
<Styles.SystemMessage>
5155
<Styles.SystemMessageIcon
@@ -54,10 +58,24 @@ function BoostTierUpgrade({
5458
svg="IconBoost"
5559
/>
5660
<Styles.SystemMessageContent>
57-
<MessageAuthor author={author} guildId={guild?.id} onlyShowUsername />{" "}
58-
just boosted the server <strong>{content}</strong> time
59-
{content === "1" ? "" : "s"}! {guildName} has achieved{" "}
60-
<strong>Level {newLevel}!</strong>
61+
<Trans
62+
i18nKey="BoostTierUpgrade.content"
63+
count={content === "" ? 1 : parseInt(content)}
64+
values={{
65+
guildName,
66+
newLevel,
67+
}}
68+
components={{
69+
Author: (
70+
<MessageAuthor
71+
author={author}
72+
guildId={guild?.id}
73+
onlyShowUsername
74+
/>
75+
),
76+
}}
77+
t={t}
78+
/>
6179
</Styles.SystemMessageContent>
6280
<LargeTimestamp timestamp={createdAt} />
6381
</Styles.SystemMessage>

0 commit comments

Comments
 (0)