Compare commits

...
Author SHA1 Message Date
Aaron Reisman 28ed897902 feat(i18n): add type-safe translations with AST-based key validation
- Rewrite @plane/i18n package with type-safe t() function
- Generate TypeScript types from English translation JSON
- Fix ICU plural format parsing in type generator
- Add 'as const' assertions to i18n fields in constants
- Create AST-based check:i18n script for each app
- Add turbo task for parallel i18n validation
- Convert translation files from TS to JSON format
- Rename locale directories (ua→uk, vi-VN→vi, tr-TR→tr)
- Add missing translation keys and fix key mismatches
- Export KeysWithoutParams type for dynamic key handling

Scripts:
- pnpm check:i18n (per app) - AST-based missing key detection
- pnpm i18n:validate - Cross-locale validation
- pnpm i18n:generate-types - Generate TS types from JSON

All apps pass type checking with 0 errors.
2025-12-22 22:27:44 +07:00
190 changed files with 57856 additions and 1440 deletions
+1
View File
@@ -11,6 +11,7 @@
"preview": "react-router build && serve -s build/client -l 3001",
"start": "serve -s build/client -l 3001",
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist && rm -rf build",
"check:i18n": "tsx ../../packages/i18n/scripts/check-app-keys.ts",
"check:lint": "eslint . --cache --cache-location node_modules/.cache/eslint/ --max-warnings=485",
"check:types": "react-router typegen && tsc --noEmit",
"check:format": "prettier . --cache --check",
@@ -2,7 +2,8 @@ import React, { useState } from "react";
import { observer } from "mobx-react";
// plane imports
import { ISSUE_PRIORITY_FILTERS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { PriorityIcon } from "@plane/propel/icons";
// local imports
import { FilterHeader } from "./helpers/filter-header";
@@ -42,7 +43,7 @@ export const FilterPriority = observer(function FilterPriority(props: Props) {
isChecked={appliedFilters?.includes(priority.key) ? true : false}
onClick={() => handleUpdate(priority.key)}
icon={<PriorityIcon priority={priority.key} className="h-3.5 w-3.5" />}
title={t(priority.titleTranslationKey)}
title={t(priority.titleTranslationKey as KeysWithoutParams<"translation">)}
/>
))
) : (
@@ -1,5 +1,6 @@
import { SignalHigh } from "lucide-react";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
// types
import { PriorityIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
@@ -29,7 +30,10 @@ export function IssueBlockPriority({
if (priority_detail === null) return <></>;
return (
<Tooltip tooltipHeading="Priority" tooltipContent={t(priority_detail?.titleTranslationKey || "")}>
<Tooltip
tooltipHeading="Priority"
tooltipContent={t((priority_detail?.titleTranslationKey || "") as KeysWithoutParams<"translation">)}
>
<div
className={cn(
"h-full flex items-center gap-1.5 border-[0.5px] rounded-sm text-11 px-2 py-0.5",
@@ -59,7 +63,11 @@ export function IssueBlockPriority({
) : (
<SignalHigh className="size-3" />
)}
{shouldShowName && <span className="pl-2 text-13">{t(priority_detail?.titleTranslationKey || "")}</span>}
{shouldShowName && (
<span className="pl-2 text-13">
{t((priority_detail?.titleTranslationKey || "") as KeysWithoutParams<"translation">)}
</span>
)}
</div>
</Tooltip>
);
@@ -3,7 +3,8 @@ import { useRouter, useSearchParams } from "next/navigation";
// ui
import { SITES_ISSUE_LAYOUTS } from "@plane/constants";
// plane i18n
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { Tooltip } from "@plane/propel/tooltip";
// helpers
import { queryParamGenerator } from "@/helpers/query-param-generator";
@@ -47,7 +48,7 @@ export const IssuesLayoutSelection = observer(function IssuesLayoutSelection(pro
if (!layoutOptions[layout.key]) return;
return (
<Tooltip key={layout.key} tooltipContent={t(layout.titleTranslationKey)}>
<Tooltip key={layout.key} tooltipContent={t(layout.titleTranslationKey as KeysWithoutParams<"translation">)}>
<button
type="button"
className={`group grid h-[22px] w-7 place-items-center overflow-hidden rounded-sm transition-all bg-layer-transparent hover:bg-layer-transparent-hover ${
@@ -2,7 +2,8 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { LinkIcon } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import {
StatePropertyIcon,
StateGroupIcon,
@@ -100,7 +101,7 @@ export const PeekOverviewIssueProperties = observer(function PeekOverviewIssuePr
}`}
>
{priority && <PriorityIcon priority={priority?.key} size={12} className="flex-shrink-0" />}
<span>{t(priority?.titleTranslationKey || "common.none")}</span>
<span>{t((priority?.titleTranslationKey || "common.none") as KeysWithoutParams<"translation">)}</span>
</div>
</div>
</div>
+1
View File
@@ -10,6 +10,7 @@
"preview": "react-router build && PORT=3002 react-router-serve ./build/server/index.js",
"start": "PORT=3002 react-router-serve ./build/server/index.js",
"clean": "rm -rf .turbo && rm -rf .next && rm -rf .react-router && rm -rf node_modules && rm -rf dist && rm -rf build",
"check:i18n": "tsx ../../packages/i18n/scripts/check-app-keys.ts",
"check:lint": "eslint . --cache --cache-location node_modules/.cache/eslint/ --max-warnings=932",
"check:types": "react-router typegen && tsc --noEmit",
"check:format": "prettier . --cache --check",
@@ -4,7 +4,8 @@ import { useParams } from "next/navigation";
// plane constants
import { EIssueFilterType, ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
// plane i18n
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
// icons
import { ChevronDownIcon } from "@plane/propel/icons";
// types
@@ -103,7 +104,7 @@ export const ProfileIssuesMobileHeader = observer(function ProfileIssuesMobileHe
className="flex items-center gap-2"
>
<IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="h-3 w-3" />
<div className="text-tertiary">{t(layout.i18n_title)}</div>
<div className="text-tertiary">{t(layout.i18n_title as KeysWithoutParams<"translation">)}</div>
</CustomMenu.MenuItem>
);
})}
@@ -3,7 +3,8 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { EIssueFilterType, ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { CalendarLayoutIcon, BoardLayoutIcon, ListLayoutIcon, ChevronDownIcon } from "@plane/propel/icons";
import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, EIssueLayoutTypes } from "@plane/types";
import { EIssuesStoreType } from "@plane/types";
@@ -110,7 +111,7 @@ export const CycleIssuesMobileHeader = observer(function CycleIssuesMobileHeader
className="flex items-center gap-2"
>
<IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="w-3 h-3" />
<div className="text-tertiary">{t(layout.titleTranslationKey)}</div>
<div className="text-tertiary">{t(layout.titleTranslationKey as KeysWithoutParams<"translation">)}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
@@ -48,7 +48,7 @@ function ProjectCyclesPage({ params }: Route.ComponentProps) {
const resolvedEmptyState = resolvedTheme === "light" ? lightEmptyState : darkEmptyState;
const totalCycles = currentProjectCycleIds?.length ?? 0;
const project = getProjectById(projectId);
const pageTitle = project?.name ? `${project?.name} - ${t("common.cycles", { count: 2 })}` : undefined;
const pageTitle = project?.name ? `${project?.name} - ${t("common.cycles")}` : undefined;
const hasAdminLevelPermission = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT);
const hasMemberLevelPermission = allowPermissions(
[EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER],
@@ -3,7 +3,8 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { EIssueFilterType, ISSUE_LAYOUTS, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { CalendarLayoutIcon, BoardLayoutIcon, ListLayoutIcon, ChevronDownIcon } from "@plane/propel/icons";
import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, EIssueLayoutTypes } from "@plane/types";
import { EIssuesStoreType } from "@plane/types";
@@ -90,7 +91,7 @@ export const ModuleIssuesMobileHeader = observer(function ModuleIssuesMobileHead
className="flex items-center gap-2"
>
<IssueLayoutIcon layout={ISSUE_LAYOUTS[index].key} className="h-3 w-3" />
<div className="text-tertiary">{t(layout.i18n_title)}</div>
<div className="text-tertiary">{t(layout.i18n_title as KeysWithoutParams<"translation">)}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
@@ -130,7 +130,7 @@ export const GlobalIssuesHeader = observer(function GlobalIssuesHeader() {
onChange={(value: string) => {
router.push(`/${workspaceSlug}/workspace-views/${value}`);
}}
title={viewDetails?.name ?? t(defaultViewDetails?.i18n_label ?? "")}
title={viewDetails?.name ?? (defaultViewDetails?.i18n_label ? t(defaultViewDetails.i18n_label) : "")}
icon={
<Breadcrumbs.Icon>
<ViewsIcon className="size-4 flex-shrink-0 text-tertiary" />
@@ -1,7 +1,8 @@
import { observer } from "mobx-react";
// plane imports
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
// components
import { NotAuthorizedView } from "@/components/auth-screens/not-authorized-view";
import { PageHead } from "@/components/core/page-title";
@@ -41,7 +42,9 @@ function MembersSettingsPage({ params }: Route.ComponentProps) {
return (
<SettingsContentWrapper size="lg">
<PageHead title={pageTitle} />
<SettingsHeading title={t(getProjectSettingsPageLabelI18nKey("members", "common.members"))} />
<SettingsHeading
title={t(getProjectSettingsPageLabelI18nKey("members", "common.members") as KeysWithoutParams<"translation">)}
/>
<ProjectSettingsMemberDefaults projectId={projectId} workspaceSlug={workspaceSlug} />
<ProjectTeamspaceList projectId={projectId} workspaceSlug={workspaceSlug} />
<ProjectMemberList projectId={projectId} workspaceSlug={workspaceSlug} />
+7 -7
View File
@@ -24,16 +24,16 @@ const WORKSPACE_ACTION_LINKS = [
{
key: "create_workspace",
Icon: CirclePlus,
i18n_label: "create_workspace",
i18n_label: "create_workspace" as const,
href: "/create-workspace",
},
{
key: "invitations",
Icon: Mails,
i18n_label: "workspace_invites",
i18n_label: "workspace_invites" as const,
href: "/invitations",
},
];
] as const;
function ProjectActionIcons({ type, size, className = "" }: { type: string; size?: number; className?: string }) {
const icons = {
@@ -107,8 +107,8 @@ export const ProfileLayoutSidebar = observer(function ProfileLayoutSidebar() {
.catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: t("sign_out.toast.error.title"),
message: t("sign_out.toast.error.message"),
title: t("auth.sign_out.toast.error.title"),
message: t("auth.sign_out.toast.error.message"),
})
)
.finally(() => setIsSigningOut(false));
@@ -148,7 +148,7 @@ export const ProfileLayoutSidebar = observer(function ProfileLayoutSidebar() {
return (
<Link key={link.key} href={link.href} className="block w-full" onClick={handleItemClick}>
<Tooltip
tooltipContent={t(link.key)}
tooltipContent={t(link.i18n_label)}
position="right"
className="ml-2"
disabled={!sidebarCollapsed}
@@ -221,7 +221,7 @@ export const ProfileLayoutSidebar = observer(function ProfileLayoutSidebar() {
{WORKSPACE_ACTION_LINKS.map((link) => (
<Link className="block w-full" key={link.key} href={link.href} onClick={handleItemClick}>
<Tooltip
tooltipContent={t(link.key)}
tooltipContent={t(link.i18n_label)}
position="right"
className="ml-2"
disabled={!sidebarCollapsed}
@@ -2,7 +2,10 @@ import { observer } from "mobx-react";
import { AlertOctagon, BarChart4, CircleDashed, Folder, Microscope, Search } from "lucide-react";
// plane imports
import { MARKETING_PRICING_PAGE_LINK } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams, PrefixedKeyWithoutParams} from "@plane/i18n";
type I18nKey = KeysWithoutParams<"translation"> | PrefixedKeyWithoutParams;
import { getButtonStyling } from "@plane/propel/button";
import { ContentWrapper } from "@plane/ui";
import { cn } from "@plane/utils";
@@ -20,46 +23,46 @@ import { useUser } from "@/hooks/store/user";
export const WORKSPACE_ACTIVE_CYCLES_DETAILS = [
{
key: "10000_feet_view",
key: "10000_feet_view" as const,
title: "10,000-feet view of all active cycles.",
description:
"Zoom out to see running cycles across all your projects at once instead of going from Cycle to Cycle in each project.",
icon: Folder,
},
{
key: "get_snapshot_of_each_active_cycle",
key: "get_snapshot_of_each_active_cycle" as const,
title: "Get a snapshot of each active cycle.",
description:
"Track high-level metrics for all active cycles, see their state of progress, and get a sense of scope against deadlines.",
icon: CircleDashed,
},
{
key: "compare_burndowns",
key: "compare_burndowns" as const,
title: "Compare burndowns.",
description: "Monitor how each of your teams are performing with a peek into each cycles burndown report.",
description: "Monitor how each of your teams are performing with a peek into each cycle's burndown report.",
icon: BarChart4,
},
{
key: "quickly_see_make_or_break_issues",
key: "quickly_see_make_or_break_issues" as const,
title: "Quickly see make-or-break work items. ",
description:
"Preview high-priority work items for each cycle against due dates. See all of them per cycle in one click.",
icon: AlertOctagon,
},
{
key: "zoom_into_cycles_that_need_attention",
key: "zoom_into_cycles_that_need_attention" as const,
title: "Zoom into cycles that need attention. ",
description: "Investigate the state of any cycle that doesnt conform to expectations in one click.",
description: "Investigate the state of any cycle that doesn't conform to expectations in one click.",
icon: Search,
},
{
key: "stay_ahead_of_blockers",
key: "stay_ahead_of_blockers" as const,
title: "Stay ahead of blockers.",
description:
"Spot challenges from one project to another and see inter-cycle dependencies that arent obvious from any other view.",
"Spot challenges from one project to another and see inter-cycle dependencies that aren't obvious from any other view.",
icon: Microscope,
},
];
] as const;
export const WorkspaceActiveCyclesUpgrade = observer(function WorkspaceActiveCyclesUpgrade() {
const { t } = useTranslation();
@@ -118,7 +121,7 @@ export const WorkspaceActiveCyclesUpgrade = observer(function WorkspaceActiveCyc
<h3 className="font-medium">{t(item.key)}</h3>
<item.icon className="mt-1 h-4 w-4 text-blue-500" />
</div>
<span className="text-13 text-tertiary">{t(`${item.key}_description`)}</span>
<span className="text-13 text-tertiary">{t(`${item.key}_description` as I18nKey)}</span>
</div>
))}
</div>
@@ -4,7 +4,8 @@ import { useTheme } from "next-themes";
// plane imports
import type { I_THEME_OPTION } from "@plane/constants";
import { THEME_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams, PrefixedKeyWithoutParams} from "@plane/i18n";
import { setPromiseToast } from "@plane/propel/toast";
// components
import { CustomThemeSelector } from "@/components/core/theme/custom-theme-selector";
@@ -14,11 +15,13 @@ import { PreferencesSection } from "@/components/preferences/section";
// hooks
import { useUserProfile } from "@/hooks/store/user";
type I18nKey = KeysWithoutParams<"translation"> | PrefixedKeyWithoutParams;
export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
option: {
id: string;
title: string;
description: string;
title: I18nKey;
description: I18nKey;
};
}) {
// store hooks
@@ -2,7 +2,8 @@ import type { FC } from "react";
import { Controller, useFormContext } from "react-hook-form";
// plane imports
import { NETWORK_CHOICES, ETabIndices } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams, PrefixedKeyWithoutParams} from "@plane/i18n";
import type { IProject } from "@plane/types";
import { CustomSelect } from "@plane/ui";
import { getTabIndex } from "@plane/utils";
@@ -10,6 +11,8 @@ import { getTabIndex } from "@plane/utils";
import { MemberDropdown } from "@/components/dropdowns/member/dropdown";
import { ProjectNetworkIcon } from "@/components/project/project-network-icon";
type I18nKey = KeysWithoutParams<"translation"> | PrefixedKeyWithoutParams;
type Props = {
isMobile?: boolean;
};
@@ -37,7 +40,7 @@ function ProjectAttributes(props: Props) {
{currentNetwork ? (
<>
<ProjectNetworkIcon iconKey={currentNetwork.iconKey} />
{t(currentNetwork.i18n_label)}
{t(currentNetwork.i18n_label as I18nKey)}
</>
) : (
<span className="text-placeholder">{t("select_network")}</span>
@@ -55,8 +58,8 @@ function ProjectAttributes(props: Props) {
<div className="flex items-start gap-2">
<ProjectNetworkIcon iconKey={network.iconKey} className="h-3.5 w-3.5" />
<div className="-mt-1">
<p>{t(network.i18n_label)}</p>
<p className="text-11 text-placeholder">{t(network.description)}</p>
<p>{t(network.i18n_label as I18nKey)}</p>
<p className="text-11 text-placeholder">{t(network.description as I18nKey)}</p>
</div>
</div>
</CustomSelect.Option>
@@ -9,7 +9,8 @@ import { Pin, PinOff } from "lucide-react";
// plane imports
import type { IWorkspaceSidebarNavigationItem } from "@plane/constants";
import { EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { Tooltip } from "@plane/propel/tooltip";
import { DragHandle, DropIndicator } from "@plane/ui";
import { cn } from "@plane/utils";
@@ -189,7 +190,9 @@ export const ExtendedSidebarItem = observer(function ExtendedSidebarItem(props:
<Link href={itemHref} onClick={() => handleLinkClick()} className="group flex-grow">
<div className="flex items-center gap-1.5 py-[1px]">
{icon}
<p className="text-13 leading-5 font-medium">{t(item.labelTranslationKey)}</p>
<p className="text-13 leading-5 font-medium">
{t(item.labelTranslationKey as KeysWithoutParams<"translation">)}
</p>
</div>
</Link>
<div className="flex items-center gap-2">
@@ -103,7 +103,7 @@ export const AuthHeader = observer(function AuthHeader(props: TAuthHeader) {
return (
<div className="flex flex-col gap-1">
<span className="text-h4-semibold text-primary">{typeof header === "string" ? t(header) : header}</span>
<span className="text-h4-semibold text-primary">{header}</span>
<span className="text-h4-semibold text-placeholder">{subHeader}</span>
</div>
);
@@ -15,6 +15,8 @@ type TAuthEmailForm = {
onSubmit: (data: IEmailCheckData) => Promise<void>;
};
const EMAIL_ERROR_KEY = "auth.common.email.errors.invalid" as const;
export const AuthEmailForm = observer(function AuthEmailForm(props: TAuthEmailForm) {
const { onSubmit, defaultEmail } = props;
// states
@@ -23,7 +25,7 @@ export const AuthEmailForm = observer(function AuthEmailForm(props: TAuthEmailFo
// plane hooks
const { t } = useTranslation();
const emailError = useMemo(
() => (email && !checkEmailValidity(email) ? { email: "auth.common.email.errors.invalid" } : undefined),
() => (email && !checkEmailValidity(email) ? { email: EMAIL_ERROR_KEY } : undefined),
[email]
);
@@ -1,10 +1,11 @@
import React from "react";
// plane package imports
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams, PrefixedKeyWithoutParams} from "@plane/i18n";
import { cn } from "@plane/utils";
type Props = {
i18nTitle: string;
i18nTitle: KeysWithoutParams<"translation"> | PrefixedKeyWithoutParams;
children: React.ReactNode;
className?: string;
};
@@ -10,18 +10,18 @@ import { useInstance } from "@/hooks/store/use-instance";
const authContentMap = {
[EAuthModes.SIGN_IN]: {
pageTitle: "Sign up",
text: "auth.common.new_to_plane",
linkText: "Sign up",
pageTitle: "auth.sign_up.header.step.email.header" as const,
text: "auth.common.new_to_plane" as const,
linkText: "auth.sign_up.header.step.email.header" as const,
linkHref: "/sign-up",
},
[EAuthModes.SIGN_UP]: {
pageTitle: "Sign in",
text: "auth.common.already_have_an_account",
linkText: "Sign in",
pageTitle: "auth.sign_in.header.step.email.header" as const,
text: "auth.common.already_have_an_account" as const,
linkText: "auth.common.login" as const,
linkHref: "/sign-in",
},
};
} as const;
type AuthHeaderProps = {
type: EAuthModes;
@@ -1,4 +1,5 @@
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { Tooltip } from "@plane/propel/tooltip";
import type { TBaseLayoutType } from "@plane/types";
import { cn } from "@plane/utils";
@@ -27,7 +28,11 @@ export function LayoutSwitcher(props: Props) {
{BASE_LAYOUTS.filter((l) => (layouts ? layouts.includes(l.key) : true)).map((layout) => {
const Icon = layout.icon;
return (
<Tooltip key={layout.key} tooltipContent={t(layout.label)} isMobile={isMobile}>
<Tooltip
key={layout.key}
tooltipContent={t(layout.label as KeysWithoutParams<"translation">)}
isMobile={isMobile}
>
<button
type="button"
className={cn(
@@ -1,16 +1,19 @@
import type { LucideIcon } from "lucide-react";
// plane ui
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams, PrefixedKeyWithoutParams} from "@plane/i18n";
import { Tooltip } from "@plane/propel/tooltip";
// plane utils
import { cn } from "@plane/utils";
type I18nKey = KeysWithoutParams<"translation"> | PrefixedKeyWithoutParams;
type Props = {
onChange: (value: number) => void;
value: number;
accessSpecifiers: {
key: number;
i18n_label?: string;
i18n_label?: I18nKey;
label?: string;
icon: LucideIcon;
}[];
@@ -3,7 +3,8 @@ import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
import { Transition, Dialog } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams, PrefixedKeyWithoutParams} from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Input } from "@plane/ui";
@@ -27,6 +28,29 @@ const defaultValues: TUniqueCodeValuesForm = { email: "", code: "" };
// service initialization
const authService = new AuthService();
type I18nKey = KeysWithoutParams<"translation"> | PrefixedKeyWithoutParams;
type ChangeEmailTranslationPath =
| "title"
| "description"
| "toasts.success_title"
| "toasts.success_message"
| "form.email.label"
| "form.email.placeholder"
| "form.email.errors.required"
| "form.email.errors.invalid"
| "form.email.errors.exists"
| "form.email.errors.validation_failed"
| "form.code.label"
| "form.code.placeholder"
| "form.code.errors.required"
| "form.code.errors.invalid"
| "form.code.helper_text"
| "actions.cancel"
| "actions.confirm"
| "actions.continue"
| "states.sending";
export const ChangeEmailModal = observer(function ChangeEmailModal(props: Props) {
const { isOpen, onClose } = props;
// states
@@ -34,7 +58,8 @@ export const ChangeEmailModal = observer(function ChangeEmailModal(props: Props)
// store hooks
const { signOut } = useUser();
const { t } = useTranslation();
const changeEmailT = (path: string) => t(`account_settings.profile.change_email_modal.${path}`);
const changeEmailT = (path: ChangeEmailTranslationPath) =>
t(`account_settings.profile.change_email_modal.${path}` as I18nKey);
// form info
const {
handleSubmit,
@@ -56,8 +81,8 @@ export const ChangeEmailModal = observer(function ChangeEmailModal(props: Props)
await signOut().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: t("sign_out.toast.error.title"),
message: t("sign_out.toast.error.message"),
title: t("auth.sign_out.toast.error.title"),
message: t("auth.sign_out.toast.error.message"),
})
);
};
@@ -1,7 +1,8 @@
import { observer } from "mobx-react";
import { Tab } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import type { TWorkItemFilterCondition } from "@plane/shared-state";
import type { TCycleDistribution, TCycleEstimateDistribution, TCyclePlotType } from "@plane/types";
import { cn, toFilterArray } from "@plane/utils";
@@ -137,7 +138,7 @@ export const CycleProgressStats = observer(function CycleProgressStats(props: TC
key={stat.key}
onClick={() => setCycleTab(stat.key)}
>
{t(stat.i18n_title)}
{t(stat.i18n_title as KeysWithoutParams<"translation">)}
</Tab>
))}
</Tab.List>
@@ -5,7 +5,8 @@ import { EIssueCommentAccessSpecifier } from "@plane/constants";
// editor
import type { EditorRefApi } from "@plane/editor";
// i18n
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
// ui
import { Button } from "@plane/propel/button";
import { Tooltip } from "@plane/propel/tooltip";
@@ -177,7 +178,7 @@ export function IssueCommentToolbar(props: Props) {
disabled={isSubmitButtonDisabled}
loading={isSubmitting}
>
{t(submitButtonText)}
{t(submitButtonText as KeysWithoutParams<"translation">)}
</Button>
</div>
)}
@@ -4,7 +4,8 @@ import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
// plane imports
import type { EditorRefApi, TExtensions } from "@plane/editor";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import type { EFileAssetType, TNameDescriptionLoader } from "@plane/types";
import { getDescriptionPlaceholderI18n } from "@plane/utils";
// components
@@ -218,7 +219,11 @@ export const DescriptionInput = observer(function DescriptionInput(props: Props)
hasUnsavedChanges.current = true;
debouncedFormSave();
}}
placeholder={placeholder ?? ((isFocused, value) => t(getDescriptionPlaceholderI18n(isFocused, value)))}
placeholder={
placeholder ??
((isFocused, value) =>
t(getDescriptionPlaceholderI18n(isFocused, value) as KeysWithoutParams<"translation">))
}
searchMentionCallback={async (payload) =>
await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", {
...payload,
@@ -1,9 +1,12 @@
import { Info } from "lucide-react";
// plane imports
import { EEstimateSystem, ESTIMATE_SYSTEMS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams, PrefixedKeyWithoutParams} from "@plane/i18n";
import { Tooltip } from "@plane/propel/tooltip";
import type { TEstimateSystemKeys } from "@plane/types";
type I18nKey = KeysWithoutParams<"translation"> | PrefixedKeyWithoutParams;
// components
import { convertMinutesToHoursMinutesString } from "@plane/utils";
// plane web imports
@@ -36,21 +39,22 @@ export function EstimateCreateStageOne(props: TEstimateCreateStageOne) {
const currentSystem = system as TEstimateSystemKeys;
const isEnabled = isEstimateSystemEnabled(currentSystem);
if (!isEnabled) return null;
const i18nName = ESTIMATE_SYSTEMS[currentSystem]?.i18n_name;
return {
label: !ESTIMATE_SYSTEMS[currentSystem]?.is_available ? (
<div className="relative flex items-center gap-2 cursor-no-drop text-tertiary">
{t(ESTIMATE_SYSTEMS[currentSystem]?.i18n_name)}
{i18nName ? t(i18nName as I18nKey) : null}
<Tooltip tooltipContent={t("common.coming_soon")}>
<Info size={12} />
</Tooltip>
</div>
) : !isEnabled ? (
<div className="relative flex items-center gap-2 cursor-no-drop text-tertiary">
{t(ESTIMATE_SYSTEMS[currentSystem]?.i18n_name)}
{i18nName ? t(i18nName as I18nKey) : null}
<UpgradeBadge />
</div>
) : (
<div>{t(ESTIMATE_SYSTEMS[currentSystem]?.i18n_name)}</div>
<div>{i18nName ? t(i18nName as I18nKey) : null}</div>
),
value: system,
disabled: !isEnabled,
@@ -7,7 +7,8 @@ import { Check, Hotel } from "lucide-react";
// plane ui
import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants";
import { useLocalStorage } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { MembersPropertyIcon, ProjectIcon, CloseIcon } from "@plane/propel/icons";
import { cn, getFileURL } from "@plane/utils";
// helpers
@@ -158,8 +159,10 @@ export const NoProjectsEmptyState = observer(function NoProjectsEmptyState() {
>
<span className="text-24 my-auto">{item.icon}</span>
</div>
<h3 className="text-13 font-medium text-primary mb-2">{t(item.title)}</h3>
<p className="text-11 text-tertiary mb-2">{t(item.description)}</p>
<h3 className="text-13 font-medium text-primary mb-2">
{t(item.title as KeysWithoutParams<"translation">)}
</h3>
<p className="text-11 text-tertiary mb-2">{t(item.description as KeysWithoutParams<"translation">)}</p>
{isStateComplete ? (
<div className="flex items-center gap-2 bg-[#17a34a] rounded-full p-1 w-fit">
<Check className="size-3 text-accent-primary text-on-color" />
@@ -182,7 +185,7 @@ export const NoProjectsEmptyState = observer(function NoProjectsEmptyState() {
}}
className={cn("text-accent-primary hover:text-accent-secondary text-13 font-medium", {})}
>
{t(item.cta.text)}
{t(item.cta.text as KeysWithoutParams<"translation">)}
</Link>
) : (
<button
@@ -190,7 +193,7 @@ export const NoProjectsEmptyState = observer(function NoProjectsEmptyState() {
className="text-accent-primary hover:text-accent-secondary text-13 font-medium text-left"
onClick={item.cta.onClick}
>
{t(item.cta.text)}
{t(item.cta.text as KeysWithoutParams<"translation">)}
</button>
))
)}
@@ -1,4 +1,5 @@
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { EmptyStateCompact } from "@plane/propel/empty-state";
import type { CompactAssetType } from "@plane/propel/empty-state";
@@ -34,7 +35,11 @@ export function RecentsEmptyState({ type }: { type: string }) {
return (
<div className="flex items-center justify-center py-10 bg-layer-1 w-full rounded-lg">
<EmptyStateCompact assetKey={assetKey} assetClassName="size-20" title={t(text)} />
<EmptyStateCompact
assetKey={assetKey}
assetClassName="size-20"
title={t(text as KeysWithoutParams<"translation">)}
/>
</div>
);
}
@@ -14,7 +14,8 @@ import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { createRoot } from "react-dom/client";
// plane types
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import type { InstructionType, TWidgetEntityData } from "@plane/types";
// plane ui
import { DropIndicator, ToggleSwitch } from "@plane/ui";
@@ -129,7 +130,7 @@ export const WidgetItem = observer(function WidgetItem(props: Props) {
>
<div className="flex items-center">
<WidgetItemDragHandle sort_order={widget.sort_order} isDragging={isDragging} />
<div>{t(widgetTitle, { count: 1 })}</div>
<div>{t(widgetTitle as KeysWithoutParams<"translation">)}</div>
</div>
<ToggleSwitch
value={widget.is_enabled}
@@ -1,6 +1,7 @@
import type { FC } from "react";
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { ChevronDownIcon } from "@plane/propel/icons";
import type { TRecentActivityFilterKeys } from "@plane/types";
import { CustomMenu } from "@plane/ui";
@@ -26,7 +27,9 @@ export const FiltersDropdown = observer(function FiltersDropdown(props: TFilters
setActiveFilter(filter.name);
}}
>
<div className="truncate font-medium text-11 capitalize">{t(filter.i18n_key)}</div>
<div className="truncate font-medium text-11 capitalize">
{t(filter.i18n_key as KeysWithoutParams<"translation">)}
</div>
</CustomMenu.MenuItem>
));
}
@@ -39,7 +42,7 @@ export const FiltersDropdown = observer(function FiltersDropdown(props: TFilters
placement="bottom-start"
customButton={
<button className="flex hover:bg-layer-transparent-hover px-2 py-1 rounded-sm gap-1 capitalize border border-subtle">
<span className="font-medium text-13 my-auto">{t(title || "")}</span>
<span className="font-medium text-13 my-auto">{t((title || "") as KeysWithoutParams<"translation">)}</span>
<ChevronDownIcon className={cn("size-3 my-auto text-tertiary hover:text-secondary duration-300")} />
</button>
}
@@ -3,7 +3,8 @@ import { observer } from "mobx-react";
// constants
// helpers
import { INBOX_STATUS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { cn, findHowManyDaysLeft } from "@plane/utils";
// store
import type { IInboxIssueStore } from "@/store/inbox/inbox-issue.store";
@@ -25,9 +26,12 @@ export const InboxIssueStatus = observer(function InboxIssueStatus(props: Props)
const isSnoozedDatePassed = inboxIssue.status === 0 && new Date(inboxIssue.snoozed_till ?? "") < new Date();
if (!inboxIssueStatusDetail || isSnoozedDatePassed) return <></>;
const description = t(inboxIssueStatusDetail.i18n_description(), {
days: findHowManyDaysLeft(new Date(inboxIssue.snoozed_till ?? "")),
});
const description =
inboxIssue.status === 0
? t("inbox_issue.status.snoozed.description", {
days: findHowManyDaysLeft(new Date(inboxIssue.snoozed_till ?? "")) ?? 0,
})
: t(inboxIssueStatusDetail.i18n_description() as KeysWithoutParams<"translation">);
const statusIcon = ICON_PROPERTIES[inboxIssue?.status];
return (
@@ -3,7 +3,8 @@ import { observer } from "mobx-react";
// plane imports
import { ETabIndices } from "@plane/constants";
import type { EditorRefApi } from "@plane/editor";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import type { TIssue } from "@plane/types";
import { EFileAssetType } from "@plane/types";
import { Loader } from "@plane/ui";
@@ -71,7 +72,9 @@ export const InboxIssueDescription = observer(function InboxIssueDescription(pro
projectId={projectId}
dragDropEnabled={false}
onChange={(_description: object, description_html: string) => handleData("description_html", description_html)}
placeholder={(isFocused, description) => t(`${getDescriptionPlaceholderI18n(isFocused, description)}`)}
placeholder={(isFocused, description) =>
t(getDescriptionPlaceholderI18n(isFocused, description) as KeysWithoutParams<"translation">)
}
searchMentionCallback={async (payload) =>
await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", {
...payload,
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
import type { TInboxIssueCurrentTab } from "@plane/types";
import { EInboxIssueCurrentTab } from "@plane/types";
@@ -94,7 +95,7 @@ export const InboxSidebar = observer(function InboxSidebar(props: IInboxSidebarP
}
}}
>
<div>{t(option?.i18n_label)}</div>
<div>{t(option?.i18n_label as KeysWithoutParams<"translation">)}</div>
{option?.key === "open" && currentTab === option?.key && (
<div className="rounded-full p-1.5 py-0.5 bg-accent-primary/20 text-accent-primary text-11 font-semibold">
{inboxIssuePaginationInfo?.total_results || 0}
@@ -2,7 +2,8 @@ import type { FC } from "react";
import { useState } from "react";
import { observer } from "mobx-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import type { TIssue, TIssueServiceType } from "@plane/types";
import { EIssueServiceType } from "@plane/types";
import { Collapsible } from "@plane/ui";
@@ -112,7 +113,9 @@ export const RelationsCollapsibleContent = observer(function RelationsCollapsibl
relationKey: relationKey,
issueIds: issueIds,
icon: issueRelationOption?.icon,
label: issueRelationOption?.i18n_label ? t(issueRelationOption?.i18n_label) : "",
label: issueRelationOption?.i18n_label
? t(issueRelationOption?.i18n_label as KeysWithoutParams<"translation">)
: "",
className: issueRelationOption?.className,
};
});
@@ -3,7 +3,8 @@ import React from "react";
import { observer } from "mobx-react";
import { Plus } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import type { TIssueServiceType } from "@plane/types";
import { CustomMenu } from "@plane/ui";
// hooks
@@ -56,7 +57,7 @@ export const RelationActionButton = observer(function RelationActionButton(props
>
<div className="flex items-center gap-2">
{item.icon(12)}
<span>{t(item.i18n_label)}</span>
<span>{t(item.i18n_label as KeysWithoutParams<"translation">)}</span>
</div>
</CustomMenu.MenuItem>
);
@@ -47,7 +47,7 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
message: t("entity.link_copied_to_clipboard", {
entity:
issueServiceType === EIssueServiceType.ISSUES
? t("common.sub_work_items", { count: 1 })
? t("common.sub_work_item")
: t("issue.label", { count: 1 }),
}),
});
@@ -63,7 +63,7 @@ export const useSubIssueOperations = (issueServiceType: TIssueServiceType): TSub
message: t("entity.fetch.failed", {
entity:
issueServiceType === EIssueServiceType.ISSUES
? t("common.sub_work_items", { count: 2 })
? t("common.sub_work_items")
: t("issue.label", { count: 2 }),
}),
});
@@ -4,7 +4,8 @@ import { observer } from "mobx-react";
import { Plus } from "lucide-react";
// plane imports
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { WorkItemsIcon } from "@plane/propel/icons";
import type { TIssue, TIssueServiceType } from "@plane/types";
import { CustomMenu } from "@plane/ui";
@@ -93,7 +94,7 @@ export const SubIssuesActionButton = observer(function SubIssuesActionButton(pro
>
<div className="flex items-center gap-2">
{item.icon}
<span>{t(item.i18n_label)}</span>
<span>{t(item.i18n_label as KeysWithoutParams<"translation">)}</span>
</div>
</CustomMenu.MenuItem>
))}
@@ -2,7 +2,8 @@ import { observer } from "mobx-react";
import { Check, ListFilter } from "lucide-react";
// plane imports
import type { TActivityFilters, TActivityFilterOption } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { IconButton } from "@plane/propel/icon-button";
import { PopoverMenu } from "@plane/ui";
// helper
@@ -53,7 +54,7 @@ export const ActivityFilter = observer(function ActivityFilter(props: TActivityF
{item.isSelected && <Check className="h-2.5 w-2.5" />}
</div>
<div className={cn("whitespace-nowrap", item.isSelected ? "text-primary" : "text-secondary")}>
{t(item.labelTranslationKey)}
{t(item.labelTranslationKey as KeysWithoutParams<"translation">)}
</div>
</div>
)}
@@ -96,7 +96,7 @@ export const IssueDetailQuickActions = observer(function IssueDetailQuickActions
});
} catch (error) {
setToast({
title: t("toast.error "),
title: t("toast.error"),
type: TOAST_TYPE.ERROR,
message: t("entity.delete.failed", { entity: t("issue.label", { count: 1 }) }),
});
@@ -104,7 +104,7 @@ export const IssueDetailRoot = observer(function IssueDetailRoot(props: TIssueDe
setToast({
title: t("common.error.label"),
type: TOAST_TYPE.ERROR,
message: t("entity.update.failed", { entity: t("issue.label") }),
message: t("entity.update.failed", { entity: t("issue.label", { count: 1 }) }),
});
}
},
@@ -115,7 +115,7 @@ export const IssueDetailRoot = observer(function IssueDetailRoot(props: TIssueDe
setToast({
title: t("common.success"),
type: TOAST_TYPE.SUCCESS,
message: t("entity.delete.success", { entity: t("issue.label") }),
message: t("entity.delete.success", { entity: t("issue.label", { count: 1 }) }),
});
captureSuccess({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
@@ -126,7 +126,7 @@ export const IssueDetailRoot = observer(function IssueDetailRoot(props: TIssueDe
setToast({
title: t("common.error.label"),
type: TOAST_TYPE.ERROR,
message: t("entity.delete.failed", { entity: t("issue.label") }),
message: t("entity.delete.failed", { entity: t("issue.label", { count: 1 }) }),
});
captureError({
eventName: WORK_ITEM_TRACKER_EVENTS.delete,
@@ -52,7 +52,7 @@ export const CalendarQuickAddIssueActions = observer(function CalendarQuickAddIs
).then(() => addIssuesToView?.(issueIds));
setPromiseToast(addExistingIssuesPromise, {
loading: t("issue.adding", { count: issueIds.length }),
loading: t("issue.adding"),
success: {
title: t("toast.success"),
message: () => t("entity.add.success", { entity: t("issue.label", { count: 2 }) }),
@@ -4,6 +4,8 @@ import { useParams } from "next/navigation";
import { useTranslation } from "@plane/i18n";
import { EmptyStateDetailed } from "@plane/propel/empty-state";
type TProfileViewId = "activity" | "assigned" | "created" | "subscribed";
// TODO: If projectViewId changes, everything breaks. Figure out a better way to handle this.
export const ProfileViewEmptyState = observer(function ProfileViewEmptyState() {
// plane hooks
@@ -13,11 +15,13 @@ export const ProfileViewEmptyState = observer(function ProfileViewEmptyState() {
if (!profileViewId) return null;
const viewId = profileViewId.toString() as TProfileViewId;
return (
<EmptyStateDetailed
assetKey="work-item"
title={t(`profile.empty_state.${profileViewId.toString()}.title`)}
description={t(`profile.empty_state.${profileViewId.toString()}.description`)}
title={t(`profile.empty_state.${viewId}.title`)}
description={t(`profile.empty_state.${viewId}.description`)}
/>
);
});
@@ -4,7 +4,8 @@ import { useParams } from "next/navigation";
// plane constants
import { ISSUE_DISPLAY_PROPERTIES } from "@plane/constants";
// plane i18n
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
// types
import type { IIssueDisplayProperties } from "@plane/types";
// plane web helpers
@@ -83,7 +84,7 @@ export const FilterDisplayProperties = observer(function FilterDisplayProperties
})
}
>
{t(displayProperty.titleTranslationKey)}
{t(displayProperty.titleTranslationKey as KeysWithoutParams<"translation">)}
</button>
</>
))}
@@ -1,6 +1,7 @@
import React from "react";
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import type { IIssueDisplayFilterOptions, TIssueExtraOptions } from "@plane/types";
// components
import { FilterOption } from "@/components/issues/issue-layouts/filters";
@@ -45,7 +46,7 @@ export const FilterExtraOptions = observer(function FilterExtraOptions(props: Pr
key={option.key}
isChecked={selectedExtraOptions?.[option.key] ? true : false}
onClick={() => handleUpdate(option.key, !selectedExtraOptions?.[option.key])}
title={t(option.titleTranslationKey)}
title={t(option.titleTranslationKey as KeysWithoutParams<"translation">)}
/>
);
})}
@@ -1,6 +1,7 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import type { IIssueDisplayFilterOptions, TIssueGroupByOptions } from "@plane/types";
// components
import { FilterHeader, FilterOption } from "@/components/issues/issue-layouts/filters";
@@ -47,7 +48,7 @@ export const FilterGroupBy = observer(function FilterGroupBy(props: Props) {
key={groupBy?.key}
isChecked={selectedGroupBy === groupBy?.key ? true : false}
onClick={() => handleUpdate(groupBy.key)}
title={t(groupBy.titleTranslationKey)}
title={t(groupBy.titleTranslationKey as KeysWithoutParams<"translation">)}
multiple={false}
/>
);
@@ -1,7 +1,8 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import { ISSUE_ORDER_BY_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import type { TIssueOrderByOptions } from "@plane/types";
// components
@@ -36,7 +37,7 @@ export const FilterOrderBy = observer(function FilterOrderBy(props: Props) {
key={orderBy?.key}
isChecked={activeOrderBy === orderBy?.key ? true : false}
onClick={() => handleUpdate(orderBy.key)}
title={t(orderBy.titleTranslationKey)}
title={t(orderBy.titleTranslationKey as KeysWithoutParams<"translation">)}
multiple={false}
/>
))}
@@ -1,7 +1,8 @@
import React, { useState } from "react";
import { observer } from "mobx-react";
import { ISSUE_GROUP_BY_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import type { IIssueDisplayFilterOptions, TIssueGroupByOptions } from "@plane/types";
// components
import { FilterHeader, FilterOption } from "@/components/issues/issue-layouts/filters";
@@ -43,7 +44,7 @@ export const FilterSubGroupBy = observer(function FilterSubGroupBy(props: Props)
key={subGroupBy?.key}
isChecked={selectedSubGroupBy === subGroupBy?.key ? true : false}
onClick={() => handleUpdate(subGroupBy.key)}
title={t(subGroupBy.titleTranslationKey)}
title={t(subGroupBy.titleTranslationKey as KeysWithoutParams<"translation">)}
multiple={false}
/>
);
@@ -1,6 +1,7 @@
// plane imports
import { ISSUE_LAYOUTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { Tooltip } from "@plane/propel/tooltip";
import type { EIssueLayoutTypes } from "@plane/types";
import { cn } from "@plane/utils";
@@ -28,7 +29,11 @@ export function LayoutSelection(props: Props) {
return (
<div className="flex items-center gap-1 rounded-md bg-layer-3 p-1">
{ISSUE_LAYOUTS.filter((l) => layouts.includes(l.key)).map((layout) => (
<Tooltip key={layout.key} tooltipContent={t(layout.i18n_title)} isMobile={isMobile}>
<Tooltip
key={layout.key}
tooltipContent={t(layout.i18n_title as KeysWithoutParams<"translation">)}
isMobile={isMobile}
>
<button
type="button"
className={cn(
@@ -1,5 +1,6 @@
import { ISSUE_LAYOUTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { ChevronDownIcon } from "@plane/propel/icons";
import type { EIssueLayoutTypes } from "@plane/types";
@@ -42,7 +43,7 @@ export function MobileLayoutSelection({
className="flex items-center gap-2"
>
<IssueLayoutIcon layout={layout.key} className="h-3 w-3" />
<div className="text-tertiary">{t(layout.i18n_label)}</div>
<div className="text-tertiary">{t(layout.i18n_label as KeysWithoutParams<"translation">)}</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
@@ -2,7 +2,8 @@ import { useRef } from "react";
import { AlertCircle } from "lucide-react";
// plane imports
import { ISSUE_ORDER_BY_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import type { TIssueOrderByOptions } from "@plane/types";
// helpers
import { cn } from "@plane/utils";
@@ -37,9 +38,12 @@ export function GroupDragOverlay(props: Props) {
const messageContainerRef = useRef<HTMLDivElement>(null);
const shouldOverlayBeVisible = isDraggingOverColumn && canOverlayBeVisible;
const readableOrderBy = t(
ISSUE_ORDER_BY_OPTIONS.find((orderByObj) => orderByObj.key === orderBy)?.titleTranslationKey || ""
);
const readableOrderBy = ISSUE_ORDER_BY_OPTIONS.find((orderByObj) => orderByObj.key === orderBy)?.titleTranslationKey
? t(
ISSUE_ORDER_BY_OPTIONS.find((orderByObj) => orderByObj.key === orderBy)!
.titleTranslationKey as KeysWithoutParams<"translation">
)
: "";
return (
<div
@@ -75,7 +79,7 @@ export function GroupDragOverlay(props: Props) {
<>
{readableOrderBy && (
<span>
{t("issue.layouts.ordered_by_label")} <span className="font-semibold">{t(readableOrderBy)}</span>.
{t("issue.layouts.ordered_by_label")} <span className="font-semibold">{readableOrderBy}</span>.
</span>
)}
<span>{t("entity.drop_here_to_move", { entity: isEpic ? "epic" : "work item" })}</span>
@@ -3,7 +3,8 @@ import { ArrowDownWideNarrow, ArrowUpNarrowWide, CheckIcon, ChevronDownIcon, Era
// constants
import { SPREADSHEET_PROPERTY_DETAILS } from "@plane/constants";
// i18n
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
// types
import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueOrderByOptions } from "@plane/types";
import { CustomMenu, Row } from "@plane/ui";
@@ -50,7 +51,9 @@ export function HeaderColumn(props: Props) {
<Row className="flex w-full cursor-pointer items-center justify-between gap-1.5 py-2 text-13 text-secondary hover:text-primary">
<div className="flex items-center gap-1.5">
{<SpreadSheetPropertyIcon iconKey={propertyDetails.icon} className="h-4 w-4 text-placeholder" />}
{property === "sub_issue_count" && isEpic ? t("issue.label", { count: 2 }) : t(propertyDetails.i18n_title)}
{property === "sub_issue_count" && isEpic
? t("issue.label", { count: 2 })
: t(propertyDetails.i18n_title as KeysWithoutParams<"translation">)}
</div>
<div className="ml-3 flex">
{activeSortingProperty === property && (
@@ -6,7 +6,8 @@ import { Sparkle } from "lucide-react";
// plane imports
import { ETabIndices } from "@plane/constants";
import type { EditorRefApi } from "@plane/editor";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { TIssue } from "@plane/types";
import { EFileAssetType } from "@plane/types";
@@ -190,7 +191,9 @@ export const IssueDescriptionEditor = observer(function IssueDescriptionEditor(p
onEnterKeyPress={() => submitBtnRef?.current?.click()}
ref={editorRef}
tabIndex={getIndex("description_html")}
placeholder={(isFocused, description) => t(getDescriptionPlaceholderI18n(isFocused, description))}
placeholder={(isFocused, description) =>
t(getDescriptionPlaceholderI18n(isFocused, description) as KeysWithoutParams<"translation">)
}
searchMentionCallback={async (payload) =>
await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", {
...payload,
@@ -5,7 +5,8 @@ import Link from "next/link";
import { MoveDiagonal, MoveRight } from "lucide-react";
// plane imports
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { CenterPanelIcon, CopyLinkIcon, FullScreenPanelIcon, SidePanelIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { Tooltip } from "@plane/propel/tooltip";
@@ -210,7 +211,7 @@ export const IssuePeekOverviewHeader = observer(function IssuePeekOverviewHeader
}`}
>
<mode.icon className="-my-1 h-4 w-4 flex-shrink-0" />
{t(mode.i18n_title)}
{t(mode.i18n_title as KeysWithoutParams<"translation">)}
</div>
</CustomSelect.Option>
))}
@@ -1,5 +1,6 @@
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
// ui
import type { TContextMenuItem } from "@plane/ui";
import { ContextMenu, CustomMenu } from "@plane/ui";
@@ -44,7 +45,7 @@ export const WorkspaceDraftIssueQuickActions = observer(function WorkspaceDraftI
>
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
<div>
<h5>{t(item.title || "")}</h5>
<h5>{t((item.title || "") as KeysWithoutParams<"translation">)}</h5>
{item.description && (
<p
className={cn("text-tertiary whitespace-pre-line", {
@@ -5,7 +5,8 @@ import { useSearchParams } from "next/navigation";
import { AlertCircle } from "lucide-react";
import { Disclosure, Transition } from "@headlessui/react";
import { EEstimateSystem } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { ChevronUpIcon, ChevronDownIcon } from "@plane/propel/icons";
import type { TModulePlotType } from "@plane/types";
import { EIssuesStoreType } from "@plane/types";
@@ -134,7 +135,10 @@ export const ModuleAnalyticsProgress = observer(function ModuleAnalyticsProgress
value={plotType}
label={
<span>
{t(moduleBurnDownChartOptions.find((v) => v.value === plotType)?.i18n_label || "none")}
{t(
(moduleBurnDownChartOptions.find((v) => v.value === plotType)?.i18n_label ||
"none") as KeysWithoutParams<"translation">
)}
</span>
}
onChange={onChange}
@@ -142,7 +146,7 @@ export const ModuleAnalyticsProgress = observer(function ModuleAnalyticsProgress
>
{moduleBurnDownChartOptions.map((item) => (
<CustomSelect.Option key={item.value} value={item.value}>
{t(item.i18n_label)}
{t(item.i18n_label as KeysWithoutParams<"translation">)}
</CustomSelect.Option>
))}
</CustomSelect>
@@ -1,6 +1,7 @@
import { observer } from "mobx-react";
import { Tab } from "@headlessui/react";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import type { TWorkItemFilterCondition } from "@plane/shared-state";
import type { TModuleDistribution, TModuleEstimateDistribution, TModulePlotType } from "@plane/types";
import { cn, toFilterArray } from "@plane/utils";
@@ -135,7 +136,7 @@ export const ModuleProgressStats = observer(function ModuleProgressStats(props:
key={stat.key}
onClick={() => setModuleTab(stat.key)}
>
{t(stat.i18n_title)}
{t(stat.i18n_title as KeysWithoutParams<"translation">)}
</Tab>
))}
</Tab.List>
@@ -5,7 +5,8 @@ import { useParams } from "next/navigation";
import { GripVertical, X } from "lucide-react";
// plane imports
import { WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { Checkbox, EModalPosition, EModalWidth, ModalCore, Sortable } from "@plane/ui";
import { cn } from "@plane/utils";
// hooks
@@ -214,7 +215,7 @@ export const CustomizeNavigationDialog = observer(function CustomizeNavigationDi
<div className="flex items-center gap-2 flex-1">
{getSidebarNavigationItemIcon(item.key)}
<label className="text-13 text-primary flex-1 cursor-pointer">
{t(item.labelTranslationKey)}
{t(item.labelTranslationKey as KeysWithoutParams<"translation">)}
</label>
</div>
</div>
@@ -244,7 +245,9 @@ export const CustomizeNavigationDialog = observer(function CustomizeNavigationDi
/>
<div className="flex items-center gap-2 flex-1">
{icon}
<span className="text-13 text-primary">{t(item.labelTranslationKey)}</span>
<span className="text-13 text-primary">
{t(item.labelTranslationKey as KeysWithoutParams<"translation">)}
</span>
</div>
</div>
);
@@ -2,7 +2,8 @@ import React from "react";
import { Link } from "react-router";
import { MoreHorizontal, Pin } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { SetAsDefaultIcon } from "@plane/propel/icons";
import { Menu } from "@plane/propel/menu";
import { Tooltip } from "@plane/propel/tooltip";
@@ -48,7 +49,7 @@ export function TabNavigationOverflowMenu({ overflowItems, isActive, tabPreferen
<Menu.MenuItem key={`${item.key}-overflow-${itemIsActive ? "active" : "inactive"}`} className="p-0 w-full">
<div className="flex items-center justify-between w-full group/menu-item">
<Link to={item.href} className="flex-1 min-w-0 w-full p-1">
<span className="text-11">{t(item.i18n_key)}</span>
<span className="text-11">{t(item.i18n_key as KeysWithoutParams<"translation">)}</span>
</Link>
<div className="flex items-center">
{/* Show Eye icon ONLY for user-hidden items */}
@@ -3,7 +3,8 @@ import React, { useEffect } from "react";
import { observer } from "mobx-react";
import { useParams, useLocation, Link, useNavigate } from "react-router";
import { EUserPermissionsLevel, EUserPermissions } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { TabNavigationList, TabNavigationItem } from "@plane/propel/tab-navigation";
import type { EUserProjectRoles } from "@plane/types";
// hooks
@@ -232,7 +233,7 @@ export const TabNavigationRoot = observer(function TabNavigationRoot(props: TTab
>
<Link to={item.href}>
<TabNavigationItem isActive={itemIsActive}>
<span>{t(item.i18n_key)}</span>
<span>{t(item.i18n_key as KeysWithoutParams<"translation">)}</span>
</TabNavigationItem>
</Link>
</div>
@@ -1,7 +1,8 @@
import { Link } from "react-router";
import { PinOff } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { ContextMenu } from "@plane/propel/context-menu";
import { SetAsDefaultIcon } from "@plane/propel/icons";
import { TabNavigationItem } from "@plane/propel/tab-navigation";
@@ -43,7 +44,7 @@ export function TabNavigationVisibleItem({
<ContextMenu.Trigger>
<Link key={`${item.key}-${isActive ? "active" : "inactive"}`} to={item.href}>
<TabNavigationItem isActive={isActive}>
<span>{t(item.i18n_key)}</span>
<span>{t(item.i18n_key as KeysWithoutParams<"translation">)}</span>
</TabNavigationItem>
</Link>
</ContextMenu.Trigger>
@@ -4,7 +4,8 @@ import type { LucideIcon } from "lucide-react";
import { Globe2, Lock } from "lucide-react";
// plane imports
import { ETabIndices, EPageAccess } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams, PrefixedKeyWithoutParams} from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { EmojiPicker, EmojiIconPickerTypes, Logo } from "@plane/propel/emoji-icon-picker";
import { PageIcon } from "@plane/propel/icons";
@@ -23,9 +24,11 @@ type Props = {
handleFormSubmit: () => Promise<void>;
};
type I18nKey = KeysWithoutParams<"translation"> | PrefixedKeyWithoutParams;
const PAGE_ACCESS_SPECIFIERS: {
key: EPageAccess;
i18n_label: string;
i18n_label: I18nKey;
icon: LucideIcon;
}[] = [
{ key: EPageAccess.PUBLIC, i18n_label: "common.access.public", icon: Globe2 },
@@ -132,7 +135,7 @@ export function PageForm(props: Props) {
accessSpecifiers={PAGE_ACCESS_SPECIFIERS}
isMobile={isMobile}
/>
<h6 className="text-11 font-medium">{t(i18n_access_label || "")}</h6>
<h6 className="text-11 font-medium">{t((i18n_access_label || "") as KeysWithoutParams<"translation">)}</h6>
</div>
<div className="flex items-center justify-end gap-2">
<Button variant="secondary" size="lg" onClick={handleModalClose} tabIndex={getIndex("cancel")}>
@@ -1,6 +1,7 @@
import { Tab } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
// plane web components
import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane";
@@ -18,7 +19,7 @@ export function PageNavigationPaneTabsList() {
type="button"
className="relative z-[1] flex-1 py-1.5 text-13 font-semibold outline-none"
>
{t(tab.i18n_label)}
{t(tab.i18n_label as KeysWithoutParams<"translation">)}
</Tab>
))}
{/* active tab indicator */}
@@ -24,8 +24,8 @@ export const usePowerKAccountCommands = (): TPowerKCommandConfig[] => {
signOut().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: t("sign_out.toast.error.title"),
message: t("sign_out.toast.error.message"),
title: t("auth.sign_out.toast.error.title"),
message: t("auth.sign_out.toast.error.message"),
})
);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -1,6 +1,7 @@
import { X } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
// local imports
import type { TPowerKContextType } from "../../core/types";
import { useContextIndicator } from "../../hooks/use-context-indicator";
@@ -26,7 +27,9 @@ export function PowerKModalContextIndicator(props: Props) {
<div className="w-full px-4 pt-3 pb-2">
<div className="max-w-full bg-layer-1 pl-2 pr-1 py-0.5 rounded-sm inline-flex items-center gap-1 truncate">
<div className="flex items-center gap-1.5 text-11 font-medium truncate">
<span className="shrink-0 text-secondary">{t(contextEntity.i18n_indicator)}</span>
<span className="shrink-0 text-secondary">
{t(contextEntity.i18n_indicator as KeysWithoutParams<"translation">)}
</span>
<span className="shrink-0 bg-layer-1 size-1 rounded-full" />
<p className="truncate">{contextIndicator}</p>
</div>
@@ -2,7 +2,8 @@ import React from "react";
import { Command } from "cmdk";
import { X, Search } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
// local imports
import type { TPowerKContext, TPowerKPageType } from "../../core/types";
import { POWER_K_MODAL_PAGE_DETAILS } from "./constants";
@@ -21,7 +22,7 @@ export function PowerKModalHeader(props: Props) {
const { t } = useTranslation();
// derived values
const placeholder = activePage
? t(POWER_K_MODAL_PAGE_DETAILS[activePage].i18n_placeholder)
? t(POWER_K_MODAL_PAGE_DETAILS[activePage].i18n_placeholder as KeysWithoutParams<"translation">)
: t("power_k.page_placeholders.default");
return (
@@ -2,7 +2,8 @@ import { observer } from "mobx-react";
// plane types
import { EUserPermissionsLevel } from "@plane/constants";
// components
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import type { TPowerKContext } from "@/components/power-k/core/types";
import { PowerKSettingsMenu } from "@/components/power-k/menus/settings";
// hooks
@@ -34,7 +35,7 @@ export const PowerKOpenProjectSettingsMenu = observer(function PowerKOpenProject
);
const settingsListWithIcons = settingsList.map((setting) => ({
...setting,
label: t(setting.i18n_label),
label: t(setting.i18n_label as KeysWithoutParams<"translation">),
icon: setting.Icon,
}));
@@ -1,7 +1,8 @@
import React from "react";
import { Command } from "cmdk";
// plane imports
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
// local imports
import type { TPowerKCommandConfig, TPowerKCommandGroup, TPowerKContext } from "../../core/types";
import { PowerKModalCommandItem } from "../modal/command-item";
@@ -45,8 +46,8 @@ export function CommandRenderer(props: Props) {
const title =
groupKey === "contextual" && activeContext
? t(CONTEXT_ENTITY_MAP[activeContext].i18n_title)
: t(POWER_K_GROUP_I18N_TITLES[groupKey]);
? t(CONTEXT_ENTITY_MAP[activeContext].i18n_title as KeysWithoutParams<"translation">)
: t(POWER_K_GROUP_I18N_TITLES[groupKey] as KeysWithoutParams<"translation">);
return (
<Command.Group key={groupKey} heading={title}>
@@ -55,7 +56,7 @@ export function CommandRenderer(props: Props) {
key={command.id}
icon={command.icon}
iconNode={command.iconNode}
label={t(command.i18n_title)}
label={t(command.i18n_title as KeysWithoutParams<"translation">)}
keySequence={command.keySequence}
shortcut={command.shortcut || command.modifierShortcut}
onSelect={() => onCommandSelect(command)}
@@ -1,5 +1,6 @@
// plane imports
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { substringMatch } from "@plane/utils";
// components
import type { TPowerKCommandConfig, TPowerKCommandGroup } from "@/components/power-k/core/types";
@@ -20,7 +21,9 @@ export function ShortcutRenderer(props: Props) {
const { t } = useTranslation();
// Apply search filter
const filteredCommands = commands.filter((command) => substringMatch(t(command.i18n_title), searchQuery));
const filteredCommands = commands.filter((command) =>
substringMatch(t(command.i18n_title as KeysWithoutParams<"translation">), searchQuery)
);
// Group commands - separate contextual by context type, others by group
type GroupedCommands = {
@@ -41,7 +44,7 @@ export function ShortcutRenderer(props: Props) {
if (!group) {
group = {
key: contextKey,
title: t(CONTEXT_ENTITY_MAP[command.contextType].i18n_title),
title: t(CONTEXT_ENTITY_MAP[command.contextType].i18n_title as KeysWithoutParams<"translation">),
priority: POWER_K_GROUP_PRIORITY.contextual,
commands: [],
};
@@ -56,7 +59,7 @@ export function ShortcutRenderer(props: Props) {
if (!group) {
group = {
key: groupKey,
title: t(POWER_K_GROUP_I18N_TITLES[groupKey as TPowerKCommandGroup]),
title: t(POWER_K_GROUP_I18N_TITLES[groupKey as TPowerKCommandGroup] as KeysWithoutParams<"translation">),
priority: POWER_K_GROUP_PRIORITY[groupKey as TPowerKCommandGroup],
commands: [],
};
@@ -81,7 +84,9 @@ export function ShortcutRenderer(props: Props) {
{group.commands.map((command) => (
<div key={command.id} className="mt-1">
<div className="flex items-center justify-between">
<h4 className="text-11 text-secondary text-left">{t(command.i18n_title)}</h4>
<h4 className="text-11 text-secondary text-left">
{t(command.i18n_title as KeysWithoutParams<"translation">)}
</h4>
<div className="flex items-center gap-x-1.5">
{command.keySequence && <KeySequenceBadge sequence={command.keySequence} />}
{(command.shortcut || command.modifierShortcut) && (
@@ -1,12 +1,17 @@
import { PREFERENCE_OPTIONS } from "@plane/constants";
import { PREFERENCE_COMPONENTS } from "@/plane-web/components/preferences/config";
type PreferenceComponentKey = keyof typeof PREFERENCE_COMPONENTS;
export function PreferencesList() {
return (
<div className="py-6 space-y-6">
{PREFERENCE_OPTIONS.map((option) => {
const Component = PREFERENCE_COMPONENTS[option.id as keyof typeof PREFERENCE_COMPONENTS];
return <Component key={option.id} option={option} />;
const key = option.id as PreferenceComponentKey;
const Component = PREFERENCE_COMPONENTS[key];
if (!Component) return null;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return <Component key={option.id} option={option as any} />;
})}
</div>
);
@@ -2,7 +2,8 @@ import Link from "next/link";
import { useParams } from "next/navigation";
// ui
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { UserCirclePropertyIcon, CreateIcon, LayerStackIcon } from "@plane/propel/icons";
import type { IUserProfileData } from "@plane/types";
import { Loader, Card, ECardSpacing, ECardDirection } from "@plane/ui";
@@ -50,7 +51,7 @@ export function ProfileStats({ userProfile }: Props) {
<card.icon className="h-5 w-5" />
</div>
<div className="space-y-1">
<p className="text-13 text-placeholder">{t(card.i18n_title)}</p>
<p className="text-13 text-placeholder">{t(card.i18n_title as KeysWithoutParams<"translation">)}</p>
<p className="text-18 font-semibold">{card.value}</p>
</div>
</Card>
+5 -2
View File
@@ -9,7 +9,8 @@ import { Disclosure, Transition } from "@headlessui/react";
// plane helpers
import { useOutsideClickDetector } from "@plane/hooks";
// types
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { Logo } from "@plane/propel/emoji-icon-picker";
import { ChevronDownIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
@@ -133,7 +134,9 @@ export const ProfileSidebar = observer(function ProfileSidebar(props: TProfileSi
<div className="mt-6 space-y-5">
{userDetails.map((detail) => (
<div key={detail.i18n_label} className="flex items-center gap-4 text-13">
<div className="w-2/5 flex-shrink-0 text-secondary">{t(detail.i18n_label)}</div>
<div className="w-2/5 flex-shrink-0 text-secondary">
{t(detail.i18n_label as KeysWithoutParams<"translation">)}
</div>
<div className="w-3/5 break-words font-medium">{detail.value}</div>
</div>
))}
@@ -1,7 +1,8 @@
import { observer } from "mobx-react";
// constants
import { NETWORK_CHOICES } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { CloseIcon } from "@plane/propel/icons";
type Props = {
@@ -20,7 +21,7 @@ export const AppliedAccessFilters = observer(function AppliedAccessFilters(props
const accessDetails = NETWORK_CHOICES.find((s) => `${s.key}` === status);
return (
<div key={status} className="flex items-center gap-1 rounded-sm px-1.5 py-1 text-11 bg-layer-1">
{accessDetails && t(accessDetails?.i18n_label)}
{accessDetails && t(accessDetails?.i18n_label as KeysWithoutParams<"translation">)}
{editable && (
<button
type="button"
@@ -92,7 +92,7 @@ export function ProjectAppliedFiltersList(props: Props) {
{/* Applied display filters */}
{appliedDisplayFilters.length > 0 && (
<Tag key="project_display_filters">
<span className="text-11 text-tertiary">{t("projects.label", { count: 2 })}</span>
<span className="text-11 text-tertiary">{t("workspace_projects.label", { count: 2 })}</span>
<AppliedProjectDisplayFilters
editable={isEditingAllowed}
values={appliedDisplayFilters}
@@ -2,7 +2,8 @@ import React, { useState } from "react";
import { observer } from "mobx-react";
// plane imports
import { NETWORK_CHOICES } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
// components
import { FilterHeader, FilterOption } from "@/components/issues/issue-layouts/filters";
// local imports
@@ -39,7 +40,7 @@ export const FilterAccess = observer(function FilterAccess(props: Props) {
isChecked={appliedFilters?.includes(`${access.key}`) ? true : false}
onClick={() => handleUpdate(`${access.key}`)}
icon={<ProjectNetworkIcon iconKey={access.iconKey} />}
title={t(access.i18n_label)}
title={t(access.i18n_label as KeysWithoutParams<"translation">)}
/>
))
) : (
+8 -5
View File
@@ -2,7 +2,8 @@ import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { Info, Lock } from "lucide-react";
import { NETWORK_CHOICES, PROJECT_TRACKER_ELEMENTS, PROJECT_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
// plane imports
import { Button } from "@plane/propel/button";
import { EmojiPicker, EmojiIconPickerTypes, Logo } from "@plane/propel/emoji-icon-picker";
@@ -249,7 +250,7 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
<span>{watch("identifier")} .</span>
<span className="flex items-center gap-1.5">
{project.network === 0 && <Lock className="h-2.5 w-2.5 text-on-color " />}
{currentNetwork && t(currentNetwork?.i18n_label)}
{currentNetwork && t(currentNetwork?.i18n_label as KeysWithoutParams<"translation">)}
</span>
</span>
</div>
@@ -386,7 +387,7 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
{selectedNetwork ? (
<>
<ProjectNetworkIcon iconKey={selectedNetwork.iconKey} className="h-3.5 w-3.5" />
{t(selectedNetwork.i18n_label)}
{t(selectedNetwork.i18n_label as KeysWithoutParams<"translation">)}
</>
) : (
<span className="text-placeholder">{t("select_network")}</span>
@@ -403,8 +404,10 @@ export function ProjectDetailsForm(props: IProjectDetailsForm) {
<div className="flex items-start gap-2">
<ProjectNetworkIcon iconKey={network.iconKey} className="h-3.5 w-3.5" />
<div className="-mt-1">
<p>{t(network.i18n_label)}</p>
<p className="text-11 text-placeholder">{t(network.description)}</p>
<p>{t(network.i18n_label as KeysWithoutParams<"translation">)}</p>
<p className="text-11 text-placeholder">
{t(network.description as KeysWithoutParams<"translation">)}
</p>
</div>
</div>
</CustomSelect.Option>
@@ -5,7 +5,8 @@ import { ArrowDownWideNarrow, ArrowUpNarrowWide, CheckIcon, ChevronDownIcon, Era
import type { IProjectMemberDisplayProperties, TMemberOrderByOptions } from "@plane/constants";
import { MEMBER_PROPERTY_DETAILS } from "@plane/constants";
// i18n
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
// types
import { CustomMenu } from "@plane/ui";
import type { IMemberFilters } from "@/store/member/utils";
@@ -42,7 +43,7 @@ export const MemberHeaderColumn = observer(function MemberHeaderColumn(props: Pr
className="!w-full"
customButton={
<div className="flex w-full cursor-pointer items-center justify-between gap-1.5 py-2 text-13 text-secondary hover:text-primary">
<span>{t(propertyDetails.i18n_title)}</span>
<span>{t(propertyDetails.i18n_title as KeysWithoutParams<"translation">)}</span>
<div className="ml-3 flex">
{(activeSortingProperty === propertyDetails.ascendingOrderKey ||
activeSortingProperty === propertyDetails.descendingOrderKey) && (
@@ -1,7 +1,8 @@
import { observer } from "mobx-react";
// plane imports
import { PROJECT_TRACKER_EVENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { setPromiseToast } from "@plane/propel/toast";
import { Tooltip } from "@plane/propel/tooltip";
import type { IProject } from "@plane/types";
@@ -69,7 +70,10 @@ export const ProjectFeaturesList = observer(function ProjectFeaturesList(props:
<div className="space-y-6">
{Object.entries(PROJECT_FEATURES_LIST).map(([featureSectionKey, feature]) => (
<div key={featureSectionKey} className="">
<SettingsHeading title={t(feature.key)} description={t(`${feature.key}_description`)} />
<SettingsHeading
title={t(feature.key as KeysWithoutParams<"translation">)}
description={t(`${feature.key}_description` as KeysWithoutParams<"translation">)}
/>
{Object.entries(feature.featureList).map(([featureItemKey, featureItem]) => (
<div key={featureItemKey} className="gap-x-8 gap-y-2 border-b border-subtle bg-surface-1 py-4">
<div key={featureItemKey} className="flex items-center justify-between">
@@ -77,7 +81,9 @@ export const ProjectFeaturesList = observer(function ProjectFeaturesList(props:
<div className="flex items-center justify-center rounded-sm bg-surface-2 p-3">{featureItem.icon}</div>
<div>
<div className="flex items-center gap-2">
<h4 className="text-13 font-medium leading-5">{t(featureItem.key)}</h4>
<h4 className="text-13 font-medium leading-5">
{t(featureItem.key as KeysWithoutParams<"translation">)}
</h4>
{featureItem.isPro && (
<Tooltip tooltipContent="Pro feature" position="top">
<UpgradeBadge className="rounded-sm" />
@@ -85,7 +91,7 @@ export const ProjectFeaturesList = observer(function ProjectFeaturesList(props:
)}
</div>
<p className="text-13 leading-5 tracking-tight text-tertiary">
{t(`${featureItem.key}_description`)}
{t(`${featureItem.key}_description` as KeysWithoutParams<"translation">)}
</p>
</div>
</div>
@@ -2,7 +2,8 @@ import { useRef } from "react";
import { observer } from "mobx-react";
import { Menu } from "lucide-react";
import { useOutsideClickDetector } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { ChevronRightIcon } from "@plane/propel/icons";
import { useUserSettings } from "@/hooks/store/user";
@@ -39,7 +40,9 @@ export const SettingsMobileNav = observer(function SettingsMobileNav(props: Prop
{/* path */}
<div className="flex items-center gap-2">
<ChevronRightIcon className="size-4 text-tertiary" />
<span className="text-13 font-medium text-secondary">{t(activePath)}</span>
<span className="text-13 font-medium text-secondary">
{t(activePath as KeysWithoutParams<"translation">)}
</span>
</div>
</div>
</div>
@@ -3,7 +3,8 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { usePathname, useParams } from "next/navigation";
import { EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { Loader } from "@plane/ui";
import { cn } from "@plane/utils";
import { useProject } from "@/hooks/store/use-project";
@@ -66,7 +67,9 @@ export const NavItemChildren = observer(function NavItemChildren(props: { projec
"text-11 font-medium"
)}
>
{t(getProjectSettingsPageLabelI18nKey(link.key, link.i18n_label))}
{t(
getProjectSettingsPageLabelI18nKey(link.key, link.i18n_label) as KeysWithoutParams<"translation">
)}
</div>
</Link>
)
@@ -4,7 +4,8 @@ import Link from "next/link";
import { useParams } from "next/navigation";
import { Disclosure } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import type { EUserWorkspaceRoles } from "@plane/types";
import { cn, joinUrlPath } from "@plane/utils";
// hooks
@@ -49,7 +50,7 @@ const SettingsSidebarNavItem = observer(function SettingsSidebarNavItem(props: T
{setting.icon
? setting.icon
: actionIcons && actionIcons({ type: setting.key, size: 16, className: "w-4 h-4" })}
<div className="text-13 font-medium truncate">{t(setting.i18n_label)}</div>
<div className="text-13 font-medium truncate">{t(setting.i18n_label as KeysWithoutParams<"translation">)}</div>
</div>
{appendItemsToTitle?.(setting.key)}
</>
@@ -1,5 +1,6 @@
import { observer } from "mobx-react";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { cn } from "@plane/utils";
import { SettingsSidebarHeader } from "./header";
import type { TSettingItem } from "./nav-item";
@@ -51,7 +52,9 @@ export const SettingsSidebar = observer(function SettingsSidebar(props: Settings
if (groupedSettings[category].length === 0) return null;
return (
<div key={category} className="py-3">
<span className="text-13 font-semibold text-tertiary capitalize mb-2 px-2">{t(category)}</span>
<span className="text-13 font-semibold text-tertiary capitalize mb-2 px-2">
{t(category as KeysWithoutParams<"translation">)}
</span>
<div className="relative flex flex-col gap-0.5 h-full mt-2">
{groupedSettings[category].map(
(setting) =>
@@ -1,6 +1,7 @@
import { observer } from "mobx-react";
// icons
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { CloseIcon } from "@plane/propel/icons";
// constants
// helpers
@@ -31,7 +32,7 @@ export const AppliedAccessFilters = observer(function AppliedAccessFilters(props
return (
<div key={access} className="flex items-center gap-1 rounded-sm bg-layer-1 py-1 px-1.5 text-11">
<span className="normal-case">{t(label)}</span>
<span className="normal-case">{t(label as KeysWithoutParams<"translation">)}</span>
{editable && (
<button
type="button"
@@ -1,5 +1,6 @@
// types
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import type { TWebhookEventTypes } from "@plane/types";
type Props = {
@@ -36,7 +37,7 @@ export function WebhookOptions(props: Props) {
onChange={() => onChange(option.key)}
/>
<label className="text-13" htmlFor={option.key}>
{t(option.i18n_label)}
{t(option.i18n_label as KeysWithoutParams<"translation">)}
</label>
</div>
))}
@@ -52,7 +52,7 @@ export const NotificationItemSnoozeOption = observer(function NotificationItemSn
await unSnoozeNotification(workspaceSlug);
setToast({
title: `${t("common.success")}!`,
message: t("notification.toasts.un_snoozed"),
message: t("notification.toasts.unsnoozed"),
type: TOAST_TYPE.SUCCESS,
});
} catch (e) {
@@ -8,7 +8,8 @@ import {
WORKSPACE_TRACKER_ELEMENTS,
WORKSPACE_TRACKER_EVENTS,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IWorkspace } from "@plane/types";
@@ -256,7 +257,9 @@ export const CreateWorkspaceForm = observer(function CreateWorkspaceForm(props:
disabled={!isValid}
loading={isSubmitting}
>
{isSubmitting ? t(primaryButtonText.loading) : t(primaryButtonText.default)}
{isSubmitting
? t(primaryButtonText.loading as KeysWithoutParams<"translation">)
: t(primaryButtonText.default as KeysWithoutParams<"translation">)}
</Button>
{!secondaryButton && (
<Button variant="secondary" type="button" size="xl" onClick={() => router.back()}>
@@ -3,7 +3,8 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
import { EUserPermissionsLevel, EUserPermissions } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { CycleIcon, IntakeIcon, ModuleIcon, PageIcon, ViewsIcon, WorkItemsIcon } from "@plane/propel/icons";
import type { EUserProjectRoles } from "@plane/types";
// plane ui
@@ -176,7 +177,7 @@ export const ProjectNavigation = observer(function ProjectNavigation(props: TPro
<SidebarNavItem isActive={!!isActive(item)}>
<div className="flex items-center gap-1.5 py-[1px]">
<item.icon className={`flex-shrink-0 size-4 ${item.name === "Intake" ? "stroke-1" : "stroke-[1.5]"}`} />
<span className="text-11 font-medium">{t(item.i18n_key)}</span>
<span className="text-11 font-medium">{t(item.i18n_key as KeysWithoutParams<"translation">)}</span>
</div>
</SidebarNavItem>
</Link>
@@ -5,7 +5,8 @@ import { useParams, usePathname } from "next/navigation";
// plane imports
import type { IWorkspaceSidebarNavigationItem } from "@plane/constants";
import { EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { joinUrlPath } from "@plane/utils";
// components
import { SidebarNavItem } from "@/components/sidebar/sidebar-navigation";
@@ -66,7 +67,9 @@ export const SidebarItemBase = observer(function SidebarItemBase({
<SidebarNavItem isActive={item.highlight(pathname, itemHref)}>
<div className="flex items-center gap-1.5 py-[1px]">
{icon}
<p className="text-13 leading-5 font-medium">{t(item.labelTranslationKey)}</p>
<p className="text-13 leading-5 font-medium">
{t(item.labelTranslationKey as KeysWithoutParams<"translation">)}
</p>
</div>
{additionalRender?.(item.key, slug)}
</SidebarNavItem>
@@ -4,7 +4,8 @@ import { useParams, usePathname } from "next/navigation";
// plane imports
import { EUserPermissionsLevel, SIDEBAR_TRACKER_ELEMENTS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import type { EUserWorkspaceRoles } from "@plane/types";
// components
import { SidebarNavItem } from "@/components/sidebar/sidebar-navigation";
@@ -61,7 +62,9 @@ export const SidebarUserMenuItem = observer(function SidebarUserMenuItem(props:
<SidebarNavItem isActive={isActive}>
<div className="flex items-center gap-1.5 py-[1px]">
<item.Icon className="size-4 flex-shrink-0" />
<p className="text-13 leading-5 font-medium">{t(item.labelTranslationKey)}</p>
<p className="text-13 leading-5 font-medium">
{t(item.labelTranslationKey as KeysWithoutParams<"translation">)}
</p>
</div>
{item.key === "notifications" && <NotificationAppSidebarOption workspaceSlug={workspaceSlug.toString()} />}
</SidebarNavItem>
@@ -38,8 +38,8 @@ export const UserMenuRoot = observer(function UserMenuRoot(props: Props) {
await signOut().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: t("sign_out.toast.error.title"),
message: t("sign_out.toast.error.message"),
title: t("auth.sign_out.toast.error.title"),
message: t("auth.sign_out.toast.error.message"),
})
);
};
@@ -3,7 +3,8 @@ import Link from "next/link";
import { useParams, usePathname } from "next/navigation";
// plane imports
import { EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import type { EUserWorkspaceRoles } from "@plane/types";
import { cn } from "@plane/utils";
// components
@@ -58,7 +59,9 @@ export const SidebarWorkspaceMenuItem = observer(function SidebarWorkspaceMenuIt
"rotate-180": item.key === "active_cycles",
})}
/>
<p className="text-13 leading-5 font-medium">{t(item.labelTranslationKey)}</p>
<p className="text-13 leading-5 font-medium">
{t(item.labelTranslationKey as KeysWithoutParams<"translation">)}
</p>
</div>
<div className="flex-shrink-0">
<UpgradeBadge />
@@ -49,8 +49,8 @@ export const WorkspaceMenuRoot = observer(function WorkspaceMenuRoot(props: Work
await signOut().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: t("sign_out.toast.error.title"),
message: t("sign_out.toast.error.message"),
title: t("auth.sign_out.toast.error.title"),
message: t("auth.sign_out.toast.error.message"),
})
);
};
@@ -1,7 +1,8 @@
import { observer } from "mobx-react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
// helpers
import { truncateText } from "@plane/utils";
@@ -20,7 +21,9 @@ export const GlobalDefaultViewListItem = observer(function GlobalDefaultViewList
<div className="flex w-full items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex flex-col">
<p className="truncate text-13 font-medium leading-4">{truncateText(t(view.i18n_label), 75)}</p>
<p className="truncate text-13 font-medium leading-4">
{truncateText(t(view.i18n_label as KeysWithoutParams<"translation">), 75)}
</p>
</div>
</div>
</div>
@@ -1,7 +1,8 @@
import { observer } from "mobx-react";
import { ExternalLink, LinkIcon } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { useTranslation } from "@plane/i18n";
import type {KeysWithoutParams} from "@plane/i18n";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
// ui
import type { TStaticViewTypes } from "@plane/types";
@@ -75,7 +76,7 @@ export const DefaultWorkspaceViewQuickActions = observer(function DefaultWorkspa
>
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
<div>
<h5>{t(item.title || "")}</h5>
<h5>{t((item.title || "") as KeysWithoutParams<"translation">)}</h5>
{item.description && (
<p
className={cn("text-tertiary whitespace-pre-line", {
+1
View File
@@ -10,6 +10,7 @@
"preview": "react-router build && serve -s build/client -l 3000",
"start": "serve -s build/client -l 3000",
"clean": "rm -rf .turbo && rm -rf .next && rm -rf .react-router && rm -rf node_modules && rm -rf dist && rm -rf build",
"check:i18n": "tsx ../../packages/i18n/scripts/check-app-keys.ts",
"check:lint": "eslint . --cache --cache-location node_modules/.cache/eslint/ --max-warnings=14367",
"check:types": "react-router typegen && tsc --noEmit",
"check:format": "prettier . --cache --check",
+4 -4
View File
@@ -24,11 +24,11 @@
"tsdown": "catalog:",
"typescript": "catalog:"
},
"main": "./dist/index.mjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index.mjs",
".": "./dist/index.js",
"./package.json": "./package.json"
}
}
+4 -9
View File
@@ -32,15 +32,10 @@ export enum EChartModels {
PROGRESS = "PROGRESS",
}
export const CHART_COLOR_PALETTES: {
key: TChartColorScheme;
i18n_label: string;
light: string[];
dark: string[];
}[] = [
export const CHART_COLOR_PALETTES = [
{
key: "modern",
i18n_label: "dashboards.widget.color_palettes.modern",
i18n_label: "dashboards.widget.color_palettes.modern" as const,
light: [
"#6172E8",
"#8B6EDB",
@@ -68,7 +63,7 @@ export const CHART_COLOR_PALETTES: {
},
{
key: "horizon",
i18n_label: "dashboards.widget.color_palettes.horizon",
i18n_label: "dashboards.widget.color_palettes.horizon" as const,
light: [
"#E76E50",
"#289D90",
@@ -96,7 +91,7 @@ export const CHART_COLOR_PALETTES: {
},
{
key: "earthen",
i18n_label: "dashboards.widget.color_palettes.earthen",
i18n_label: "dashboards.widget.color_palettes.earthen" as const,
light: [
"#386641",
"#6A994E",
+13 -20
View File
@@ -1,40 +1,33 @@
// types
export const CYCLE_STATUS: {
i18n_label: string;
value: "current" | "upcoming" | "completed" | "draft";
i18n_title: string;
color: string;
textColor: string;
bgColor: string;
}[] = [
export const CYCLE_STATUS = [
{
i18n_label: "project_cycles.status.days_left",
value: "current",
i18n_title: "project_cycles.status.in_progress",
i18n_label: "project_cycles.status.days_left" as const,
value: "current" as const,
i18n_title: "project_cycles.status.in_progress" as const,
color: "#F59E0B",
textColor: "text-amber-500",
bgColor: "bg-amber-50",
},
{
i18n_label: "project_cycles.status.yet_to_start",
value: "upcoming",
i18n_title: "project_cycles.status.yet_to_start",
i18n_label: "project_cycles.status.yet_to_start" as const,
value: "upcoming" as const,
i18n_title: "project_cycles.status.yet_to_start" as const,
color: "#3F76FF",
textColor: "text-blue-500",
bgColor: "bg-indigo-50",
},
{
i18n_label: "project_cycles.status.completed",
value: "completed",
i18n_title: "project_cycles.status.completed",
i18n_label: "project_cycles.status.completed" as const,
value: "completed" as const,
i18n_title: "project_cycles.status.completed" as const,
color: "#16A34A",
textColor: "text-green-600",
bgColor: "bg-green-50",
},
{
i18n_label: "project_cycles.status.draft",
value: "draft",
i18n_title: "project_cycles.status.draft",
i18n_label: "project_cycles.status.draft" as const,
value: "draft" as const,
i18n_title: "project_cycles.status.draft" as const,
color: "#525252",
textColor: "text-tertiary",
bgColor: "bg-surface-2",
+11 -11
View File
@@ -23,11 +23,11 @@ export const estimateCount = {
export const ESTIMATE_SYSTEMS: TEstimateSystems = {
points: {
name: "Points",
i18n_name: "project_settings.estimates.systems.points.label",
i18n_name: "project_settings.estimates.systems.points.label" as const,
templates: {
fibonacci: {
title: "Fibonacci",
i18n_title: "project_settings.estimates.systems.points.fibonacci",
i18n_title: "project_settings.estimates.systems.points.fibonacci" as const,
values: [
{ id: undefined, key: 1, value: "1" },
{ id: undefined, key: 2, value: "2" },
@@ -39,7 +39,7 @@ export const ESTIMATE_SYSTEMS: TEstimateSystems = {
},
linear: {
title: "Linear",
i18n_title: "project_settings.estimates.systems.points.linear",
i18n_title: "project_settings.estimates.systems.points.linear" as const,
values: [
{ id: undefined, key: 1, value: "1" },
{ id: undefined, key: 2, value: "2" },
@@ -51,7 +51,7 @@ export const ESTIMATE_SYSTEMS: TEstimateSystems = {
},
squares: {
title: "Squares",
i18n_title: "project_settings.estimates.systems.points.squares",
i18n_title: "project_settings.estimates.systems.points.squares" as const,
values: [
{ id: undefined, key: 1, value: "1" },
{ id: undefined, key: 2, value: "4" },
@@ -63,7 +63,7 @@ export const ESTIMATE_SYSTEMS: TEstimateSystems = {
},
custom: {
title: "Custom",
i18n_title: "project_settings.estimates.systems.points.custom",
i18n_title: "project_settings.estimates.systems.points.custom" as const,
values: [
{ id: undefined, key: 1, value: "1" },
{ id: undefined, key: 2, value: "2" },
@@ -76,11 +76,11 @@ export const ESTIMATE_SYSTEMS: TEstimateSystems = {
},
categories: {
name: "Categories",
i18n_name: "project_settings.estimates.systems.categories.label",
i18n_name: "project_settings.estimates.systems.categories.label" as const,
templates: {
t_shirt_sizes: {
title: "T-Shirt Sizes",
i18n_title: "project_settings.estimates.systems.categories.t_shirt_sizes",
i18n_title: "project_settings.estimates.systems.categories.t_shirt_sizes" as const,
values: [
{ id: undefined, key: 1, value: "XS" },
{ id: undefined, key: 2, value: "S" },
@@ -92,7 +92,7 @@ export const ESTIMATE_SYSTEMS: TEstimateSystems = {
},
easy_to_hard: {
title: "Easy to hard",
i18n_title: "project_settings.estimates.systems.categories.easy_to_hard",
i18n_title: "project_settings.estimates.systems.categories.easy_to_hard" as const,
values: [
{ id: undefined, key: 1, value: "Easy" },
{ id: undefined, key: 2, value: "Medium" },
@@ -102,7 +102,7 @@ export const ESTIMATE_SYSTEMS: TEstimateSystems = {
},
custom: {
title: "Custom",
i18n_title: "project_settings.estimates.systems.categories.custom",
i18n_title: "project_settings.estimates.systems.categories.custom" as const,
values: [
{ id: undefined, key: 1, value: "Easy" },
{ id: undefined, key: 2, value: "Hard" },
@@ -115,11 +115,11 @@ export const ESTIMATE_SYSTEMS: TEstimateSystems = {
},
time: {
name: "Time",
i18n_name: "project_settings.estimates.systems.time.label",
i18n_name: "project_settings.estimates.systems.time.label" as const,
templates: {
hours: {
title: "Hours",
i18n_title: "project_settings.estimates.systems.time.hours",
i18n_title: "project_settings.estimates.systems.time.hours" as const,
values: [
{ id: undefined, key: 1, value: "1" },
{ id: undefined, key: 2, value: "2" },

Some files were not shown because too many files have changed in this diff Show More