feat: localisation and multi-lang support (#95)

* feat: setup localisation with lingui

* Fix hydration and locale issues

* Add missing translations and tweak selector styling

* Extend language support

* Update lang support

* Extend lang support

* Fix build and add dyanmic imports
This commit is contained in:
Henry
2025-06-25 21:30:24 +01:00
committed by GitHub
parent 014fdbb7d8
commit dd061c6d00
79 changed files with 11822 additions and 883 deletions

View File

@@ -1,4 +1,7 @@
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { formatDistanceToNow } from "date-fns";
import { de, enGB, es, fr, it, nl } from "date-fns/locale";
import {
HiOutlineArrowLeft,
HiOutlineArrowRight,
@@ -8,24 +11,24 @@ import {
HiOutlineUserMinus,
HiOutlineUserPlus,
} from "react-icons/hi2";
import type { GetCardByIdOutput } from "@kan/api/types";
import { authClient } from "@kan/auth/client";
import Avatar from "~/components/Avatar";
import { useLocalisation } from "~/hooks/useLocalisation";
import Comment from "./Comment";
import { authClient } from "@kan/auth/client";
type ActivityType =
NonNullable<GetCardByIdOutput>["activities"][number]["type"];
const ACTIVITY_TYPE_MAP = {
"card.created": "created the card",
"card.updated.title": "updated the title",
"card.updated.description": "updated the description",
"card.updated.list": "moved the card to another list",
"card.updated.label.added": "added a label to the card",
"card.updated.label.removed": "removed a label from the card",
"card.updated.member.added": "added a member to the card",
"card.updated.member.removed": "removed a member from the card",
const dateLocaleMap = {
en: enGB,
fr: fr,
de: de,
es: es,
it: it,
nl: nl,
} as const;
const getActivityText = ({
@@ -45,6 +48,17 @@ const getActivityText = ({
isSelf: boolean;
label: string | null;
}) => {
const ACTIVITY_TYPE_MAP = {
"card.created": t`created the card`,
"card.updated.title": t`updated the title`,
"card.updated.description": t`updated the description`,
"card.updated.list": t`moved the card to another list`,
"card.updated.label.added": t`added a label to the card`,
"card.updated.label.removed": t`removed a label from the card`,
"card.updated.member.added": t`added a member to the card`,
"card.updated.member.removed": t`removed a member from the card`,
} as const;
if (!(type in ACTIVITY_TYPE_MAP)) return null;
const baseText = ACTIVITY_TYPE_MAP[type as keyof typeof ACTIVITY_TYPE_MAP];
@@ -56,54 +70,54 @@ const getActivityText = ({
if (type === "card.updated.title" && toTitle) {
return (
<>
<Trans>
updated the title to <TextHighlight>{toTitle}</TextHighlight>
</>
</Trans>
);
}
if (type === "card.updated.list" && fromList && toList) {
return (
<>
<Trans>
moved the card from <TextHighlight>{fromList}</TextHighlight> to
<TextHighlight>{toList}</TextHighlight>
</>
</Trans>
);
}
if (type === "card.updated.member.added" && memberName) {
if (isSelf) return <>self-assigned the card</>;
if (isSelf) return <Trans>self-assigned the card</Trans>;
return (
<>
<Trans>
assigned <TextHighlight>{memberName}</TextHighlight> to the card
</>
</Trans>
);
}
if (type === "card.updated.member.removed" && memberName) {
if (isSelf) return <>unassigned themselves from the card</>;
if (isSelf) return <Trans>unassigned themselves from the card</Trans>;
return (
<>
<Trans>
unassigned <TextHighlight>{memberName}</TextHighlight> from the card
</>
</Trans>
);
}
if (type === "card.updated.label.added" && label) {
return (
<>
<Trans>
added label <TextHighlight>{label}</TextHighlight>
</>
</Trans>
);
}
if (type === "card.updated.label.removed" && label) {
return (
<>
<Trans>
removed label <TextHighlight>{label}</TextHighlight>
</>
</Trans>
);
}
@@ -148,6 +162,9 @@ const ActivityList = ({
isAdmin?: boolean;
}) => {
const { data } = authClient.useSession();
const { locale } = useLocalisation();
const currentDateLocale = dateLocaleMap[locale] || enGB;
return (
<div className="flex flex-col space-y-4 pt-4">
@@ -158,7 +175,7 @@ const ActivityList = ({
fromList: activity.fromList?.name ?? null,
toList: activity.toList?.name ?? null,
memberName: activity.member?.user?.name ?? null,
isSelf: activity.member?.user?.id === data?.user?.id,
isSelf: activity.member?.user?.id === data?.user.id,
label: activity.label?.name ?? null,
});
@@ -171,10 +188,10 @@ const ActivityList = ({
name={activity.user?.name ?? ""}
email={activity.user?.email ?? ""}
isLoading={isLoading}
createdAt={activity.createdAt}
createdAt={activity.createdAt.toISOString()}
comment={activity.comment?.comment}
isEdited={!!activity.comment?.updatedAt}
isAuthor={activity.comment?.createdBy === data?.user?.id}
isAuthor={activity.comment?.createdBy === data?.user.id}
isAdmin={isAdmin ?? false}
/>
);
@@ -213,6 +230,7 @@ const ActivityList = ({
<span className="space-x-1 text-light-900 dark:text-dark-800">
{formatDistanceToNow(new Date(activity.createdAt), {
addSuffix: true,
locale: currentDateLocale,
})}
</span>
</p>