Internationalization (i18n) (#627)

* `feat: added internationalization lib, added english labels`
* `fixes from code review`
* `feat: add crowdin github actions`
* `fix: added support for i18n in testing. Fixed broken tests`
* `fix: missing translations`
* `removed unneded import`
* `more components updated with missing translations`
* `fixed lint issue`
* `Refactor: updated how translations are scoped, updated all references`
* `fixing broken tests`
* `feat: added language switcher, updated some translations`
* `fixed linting issues`
* `reverting vite config`
* `add english group id's to command palette`
* `updated PR template for i18n`
This commit is contained in:
Dan Ditomaso
2025-05-27 08:33:43 -04:00
committed by GitHub
parent 9d74fe2d6e
commit eb2a2717b1
95 changed files with 4651 additions and 1497 deletions
@@ -17,6 +17,7 @@ import {
} from "@core/stores/messageStore/index.ts";
import { Protobuf, Types } from "@meshtastic/js";
import { Message } from "@core/stores/messageStore/types.ts";
import { useTranslation } from "react-i18next";
// import { MessageActionsMenu } from "@components/PageComponents/Messages/MessageActionsMenu.tsx"; // Uncomment if needed later
interface MessageStatusInfo {
@@ -26,37 +27,6 @@ interface MessageStatusInfo {
iconClassName?: string;
}
const MESSAGE_STATUS_MAP: Record<MessageState, MessageStatusInfo> = {
[MessageState.Ack]: {
displayText: "Message delivered",
icon: CheckCircle2,
ariaLabel: "Message delivered",
iconClassName: "text-green-500",
},
[MessageState.Waiting]: {
displayText: "Waiting for delivery",
icon: CircleEllipsis,
ariaLabel: "Sending message",
iconClassName: "text-slate-400",
},
[MessageState.Failed]: {
displayText: "Delivery failed",
icon: AlertCircle,
ariaLabel: "Message delivery failed",
iconClassName: "text-red-500 dark:text-red-400",
},
};
const UNKNOWN_STATUS: MessageStatusInfo = {
displayText: "Unknown state",
icon: AlertCircle,
ariaLabel: "Message status unknown",
iconClassName: "text-red-500 dark:text-red-400",
};
const getMessageStatusInfo = (state: MessageState): MessageStatusInfo =>
MESSAGE_STATUS_MAP[state] ?? UNKNOWN_STATUS;
const StatusTooltip = (
{ statusInfo, children }: {
statusInfo: MessageStatusInfo;
@@ -81,16 +51,55 @@ interface MessageItemProps {
export const MessageItem = ({ message }: MessageItemProps) => {
const { getNode } = useDevice();
const { getMyNodeNum } = useMessageStore();
const { t, i18n } = useTranslation();
const MESSAGE_STATUS_MAP = useMemo(
(): Record<MessageState, MessageStatusInfo> => ({
[MessageState.Ack]: {
displayText: t("message_item_status_delivered_displayText"),
icon: CheckCircle2,
ariaLabel: t("message_item_status_delivered_ariaLabel"),
iconClassName: "text-green-500",
},
[MessageState.Waiting]: {
displayText: t("message_item_status_waiting_displayText"),
icon: CircleEllipsis,
ariaLabel: t("message_item_status_waiting_ariaLabel"),
iconClassName: "text-slate-400",
},
[MessageState.Failed]: {
displayText: t("message_item_status_failed_displayText"),
icon: AlertCircle,
ariaLabel: t("message_item_status_failed_ariaLabel"),
iconClassName: "text-red-500 dark:text-red-400",
},
}),
[t],
);
const UNKNOWN_STATUS = useMemo((): MessageStatusInfo => ({
displayText: t("message_item_status_unknown_displayText"),
icon: AlertCircle,
ariaLabel: t("message_item_status_unknown_ariaLabel"),
iconClassName: "text-red-500 dark:text-red-400",
}), [t]);
const getMessageStatusInfo = useMemo(
() => (state: MessageState): MessageStatusInfo =>
MESSAGE_STATUS_MAP[state] ?? UNKNOWN_STATUS,
[MESSAGE_STATUS_MAP, UNKNOWN_STATUS],
);
const messageUser: Protobuf.Mesh.NodeInfo | null | undefined = useMemo(() => {
return message.from != null ? getNode(message.from) : null;
}, [getNode, message.from]);
const myNodeNum = useMemo(() => getMyNodeNum(), [getMyNodeNum]);
const { displayName, shortName, isFavorite } = useMemo(() => {
const userIdHex = message.from.toString(16).toUpperCase().padStart(2, "0");
const last4 = userIdHex.slice(-4);
const fallbackName = `Meshtastic ${last4}`;
const fallbackName = t("message_item_fallbackName_withLastFour", { last4 });
const longName = messageUser?.user?.longName;
const derivedShortName = messageUser?.user?.shortName || fallbackName;
const derivedDisplayName = longName || derivedShortName;
@@ -101,7 +110,7 @@ export const MessageItem = ({ message }: MessageItemProps) => {
shortName: derivedShortName,
isFavorite: isFavorite,
};
}, [messageUser, message.from]);
}, [messageUser, message.from, t, myNodeNum]);
const messageStatusInfo = getMessageStatusInfo(message.state);
const StatusIconComponent = messageStatusInfo.icon;
@@ -110,7 +119,7 @@ export const MessageItem = ({ message }: MessageItemProps) => {
() => message.date ? new Date(message.date) : null,
[message.date],
);
const locale = "en-US"; // TODO: Make dynamic via props or context
const locale = i18n.language;
const formattedTime = useMemo(
() =>