refactor: add typeScript support to enforce valid translation keys (#6776)

This commit is contained in:
Ajay Kumbhare 2023-07-20 21:45:32 +05:30 committed by GitHub
parent 5e3550fc14
commit f7c3644342
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 38 additions and 19 deletions

View File

@ -65,7 +65,7 @@ export const actionChangeExportScale = register({
); );
const scaleButtonTitle = `${t( const scaleButtonTitle = `${t(
"buttons.scale", "imageExportDialog.label.scale",
)} ${s}x (${width}x${height})`; )} ${s}x (${width}x${height})`;
return ( return (
@ -102,7 +102,7 @@ export const actionChangeExportBackground = register({
checked={appState.exportBackground} checked={appState.exportBackground}
onChange={(checked) => updateData(checked)} onChange={(checked) => updateData(checked)}
> >
{t("labels.withBackground")} {t("imageExportDialog.label.withBackground")}
</CheckboxItem> </CheckboxItem>
), ),
}); });
@ -121,8 +121,8 @@ export const actionChangeExportEmbedScene = register({
checked={appState.exportEmbedScene} checked={appState.exportEmbedScene}
onChange={(checked) => updateData(checked)} onChange={(checked) => updateData(checked)}
> >
{t("labels.exportEmbedScene")} {t("imageExportDialog.label.embedScene")}
<Tooltip label={t("labels.exportEmbedScene_details")} long={true}> <Tooltip label={t("imageExportDialog.tooltip.embedScene")} long={true}>
<div className="excalidraw-tooltip-icon">{questionCircle}</div> <div className="excalidraw-tooltip-icon">{questionCircle}</div>
</Tooltip> </Tooltip>
</CheckboxItem> </CheckboxItem>
@ -277,7 +277,7 @@ export const actionExportWithDarkMode = register({
onChange={(theme: Theme) => { onChange={(theme: Theme) => {
updateData(theme === THEME.DARK); updateData(theme === THEME.DARK);
}} }}
title={t("labels.toggleExportColorScheme")} title={t("imageExportDialog.label.darkMode")}
/> />
</div> </div>
), ),

View File

@ -8,7 +8,7 @@ import {
} from "./colorPickerUtils"; } from "./colorPickerUtils";
import HotkeyLabel from "./HotkeyLabel"; import HotkeyLabel from "./HotkeyLabel";
import { ColorPaletteCustom } from "../../colors"; import { ColorPaletteCustom } from "../../colors";
import { t } from "../../i18n"; import { TranslationKeys, t } from "../../i18n";
interface PickerColorListProps { interface PickerColorListProps {
palette: ColorPaletteCustom; palette: ColorPaletteCustom;
@ -48,7 +48,11 @@ const PickerColorList = ({
(Array.isArray(value) ? value[activeShade] : value) || "transparent"; (Array.isArray(value) ? value[activeShade] : value) || "transparent";
const keybinding = colorPickerHotkeyBindings[index]; const keybinding = colorPickerHotkeyBindings[index];
const label = t(`colors.${key.replace(/\d+/, "")}`, null, ""); const label = t(
`colors.${key.replace(/\d+/, "")}` as unknown as TranslationKeys,
null,
"",
);
return ( return (
<button <button

View File

@ -1,6 +1,6 @@
import clsx from "clsx"; import clsx from "clsx";
import { Popover } from "./Popover"; import { Popover } from "./Popover";
import { t } from "../i18n"; import { t, TranslationKeys } from "../i18n";
import "./ContextMenu.scss"; import "./ContextMenu.scss";
import { import {
@ -83,10 +83,14 @@ export const ContextMenu = React.memo(
if (item.contextItemLabel) { if (item.contextItemLabel) {
if (typeof item.contextItemLabel === "function") { if (typeof item.contextItemLabel === "function") {
label = t( label = t(
item.contextItemLabel(elements, appState, actionManager.app), item.contextItemLabel(
elements,
appState,
actionManager.app,
) as unknown as TranslationKeys,
); );
} else { } else {
label = t(item.contextItemLabel); label = t(item.contextItemLabel as unknown as TranslationKeys);
} }
} }

View File

@ -3,7 +3,7 @@ import { t } from "../i18n";
import { useExcalidrawContainer } from "./App"; import { useExcalidrawContainer } from "./App";
export const Section: React.FC<{ export const Section: React.FC<{
heading: string; heading: "canvasActions" | "selectedShapeActions" | "shapes";
children?: React.ReactNode | ((heading: React.ReactNode) => React.ReactNode); children?: React.ReactNode | ((heading: React.ReactNode) => React.ReactNode);
className?: string; className?: string;
}> = ({ heading, children, ...props }) => { }> = ({ heading, children, ...props }) => {

View File

@ -3,6 +3,7 @@ import { render } from "@testing-library/react";
import fallbackLangData from "../locales/en.json"; import fallbackLangData from "../locales/en.json";
import Trans from "./Trans"; import Trans from "./Trans";
import { TranslationKeys } from "../i18n";
describe("Test <Trans/>", () => { describe("Test <Trans/>", () => {
it("should translate the the strings correctly", () => { it("should translate the the strings correctly", () => {
@ -18,24 +19,27 @@ describe("Test <Trans/>", () => {
const { getByTestId } = render( const { getByTestId } = render(
<> <>
<div data-testid="test1"> <div data-testid="test1">
<Trans i18nKey="transTest.key1" audience="world" /> <Trans
i18nKey={"transTest.key1" as unknown as TranslationKeys}
audience="world"
/>
</div> </div>
<div data-testid="test2"> <div data-testid="test2">
<Trans <Trans
i18nKey="transTest.key2" i18nKey={"transTest.key2" as unknown as TranslationKeys}
link={(el) => <a href="https://example.com">{el}</a>} link={(el) => <a href="https://example.com">{el}</a>}
/> />
</div> </div>
<div data-testid="test3"> <div data-testid="test3">
<Trans <Trans
i18nKey="transTest.key3" i18nKey={"transTest.key3" as unknown as TranslationKeys}
link={(el) => <a href="https://example.com">{el}</a>} link={(el) => <a href="https://example.com">{el}</a>}
location="the button" location="the button"
/> />
</div> </div>
<div data-testid="test4"> <div data-testid="test4">
<Trans <Trans
i18nKey="transTest.key4" i18nKey={"transTest.key4" as unknown as TranslationKeys}
link={(el) => <a href="https://example.com">{el}</a>} link={(el) => <a href="https://example.com">{el}</a>}
location="the button" location="the button"
bold={(el) => <strong>{el}</strong>} bold={(el) => <strong>{el}</strong>}
@ -43,7 +47,7 @@ describe("Test <Trans/>", () => {
</div> </div>
<div data-testid="test5"> <div data-testid="test5">
<Trans <Trans
i18nKey="transTest.key5" i18nKey={"transTest.key5" as unknown as TranslationKeys}
connect-link={(el) => <a href="https://example.com">{el}</a>} connect-link={(el) => <a href="https://example.com">{el}</a>}
/> />
</div> </div>

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import { useI18n } from "../i18n"; import { TranslationKeys, useI18n } from "../i18n";
// Used for splitting i18nKey into tokens in Trans component // Used for splitting i18nKey into tokens in Trans component
// Example: // Example:
@ -153,7 +153,7 @@ const Trans = ({
children, children,
...props ...props
}: { }: {
i18nKey: string; i18nKey: TranslationKeys;
[key: string]: React.ReactNode | ((el: React.ReactNode) => React.ReactNode); [key: string]: React.ReactNode | ((el: React.ReactNode) => React.ReactNode);
}) => { }) => {
const { t } = useI18n(); const { t } = useI18n();

View File

@ -3,6 +3,7 @@ import percentages from "./locales/percentages.json";
import { ENV } from "./constants"; import { ENV } from "./constants";
import { jotaiScope, jotaiStore } from "./jotai"; import { jotaiScope, jotaiStore } from "./jotai";
import { atom, useAtomValue } from "jotai"; import { atom, useAtomValue } from "jotai";
import { NestedKeyOf } from "./utility-types";
const COMPLETION_THRESHOLD = 85; const COMPLETION_THRESHOLD = 85;
@ -12,6 +13,8 @@ export interface Language {
rtl?: boolean; rtl?: boolean;
} }
export type TranslationKeys = NestedKeyOf<typeof fallbackLangData>;
export const defaultLang = { code: "en", label: "English" }; export const defaultLang = { code: "en", label: "English" };
export const languages: Language[] = [ export const languages: Language[] = [
@ -123,7 +126,7 @@ const findPartsForData = (data: any, parts: string[]) => {
}; };
export const t = ( export const t = (
path: string, path: NestedKeyOf<typeof fallbackLangData>,
replacement?: { [key: string]: string | number } | null, replacement?: { [key: string]: string | number } | null,
fallback?: string, fallback?: string,
) => { ) => {

View File

@ -50,3 +50,7 @@ export type ExtractSetType<T extends Set<any>> = T extends Set<infer U>
export type SameType<T, U> = T extends U ? (U extends T ? true : false) : false; export type SameType<T, U> = T extends U ? (U extends T ? true : false) : false;
export type Assert<T extends true> = T; export type Assert<T extends true> = T;
export type NestedKeyOf<T, K = keyof T> = K extends keyof T & (string | number)
? `${K}` | (T[K] extends object ? `${K}.${NestedKeyOf<T[K]>}` : never)
: never;