Wrap intl in span identifying translation

This commit is contained in:
Jamie Curnow
2025-10-02 23:06:51 +10:00
parent fcb08d3003
commit 227e818040
68 changed files with 1076 additions and 510 deletions

View File

@@ -1,6 +1,6 @@
import { useNavigate } from "react-router-dom";
import { Button } from "src/components";
import { intl } from "src/locale";
import { T } from "src/locale";
export function ErrorNotFound() {
const navigate = useNavigate();
@@ -8,11 +8,15 @@ export function ErrorNotFound() {
return (
<div className="container-tight py-4">
<div className="empty">
<p className="empty-title">{intl.formatMessage({ id: "notfound.title" })}</p>
<p className="empty-subtitle text-secondary">{intl.formatMessage({ id: "notfound.text" })}</p>
<p className="empty-title">
<T id="notfound.title" />
</p>
<p className="empty-subtitle text-secondary">
<T id="notfound.text" />
</p>
<div className="empty-action">
<Button type="button" size="md" onClick={() => navigate("/")}>
{intl.formatMessage({ id: "notfound.action" })}
<T id="notfound.action" />
</Button>
</div>
</div>

View File

@@ -0,0 +1,99 @@
import { IconLock, IconLockOpen2 } from "@tabler/icons-react";
import { Field, useFormikContext } from "formik";
import type { ReactNode } from "react";
import Select, { type ActionMeta, components, type OptionProps } from "react-select";
import type { AccessList } from "src/api/backend";
import { useAccessLists } from "src/hooks";
import { DateTimeFormat, intl, T } from "src/locale";
interface AccessOption {
readonly value: number;
readonly label: string;
readonly subLabel: string;
readonly icon: ReactNode;
}
const Option = (props: OptionProps<AccessOption>) => {
return (
<components.Option {...props}>
<div className="flex-fill">
<div className="font-weight-medium">
{props.data.icon} <strong>{props.data.label}</strong>
</div>
<div className="text-secondary mt-1 ps-3">{props.data.subLabel}</div>
</div>
</components.Option>
);
};
interface Props {
id?: string;
name?: string;
label?: string;
}
export function AccessField({ name = "accessListId", label = "access.title", id = "accessListId" }: Props) {
const { isLoading, isError, error, data } = useAccessLists();
const { setFieldValue } = useFormikContext();
const handleChange = (newValue: any, _actionMeta: ActionMeta<AccessOption>) => {
setFieldValue(name, newValue?.value);
};
const options: AccessOption[] =
data?.map((item: AccessList) => ({
value: item.id || 0,
label: item.name,
subLabel: intl.formatMessage(
{ id: "access.subtitle" },
{
users: item?.items?.length,
rules: item?.clients?.length,
date: item?.createdOn ? DateTimeFormat(item?.createdOn) : "N/A",
},
),
icon: <IconLock size={14} className="text-lime" />,
})) || [];
// Public option
options?.unshift({
value: 0,
label: intl.formatMessage({ id: "access.public" }),
subLabel: "No basic auth required",
icon: <IconLockOpen2 size={14} className="text-red" />,
});
return (
<Field name={name}>
{({ field, form }: any) => (
<div className="mb-3">
<label className="form-label" htmlFor={id}>
<T id={label} />
</label>
{isLoading ? <div className="placeholder placeholder-lg col-12 my-3 placeholder-glow" /> : null}
{isError ? <div className="invalid-feedback">{`${error}`}</div> : null}
{!isLoading && !isError ? (
<Select
className="react-select-container"
classNamePrefix="react-select"
defaultValue={options.find((o) => o.value === field.value) || options[0]}
options={options}
components={{ Option }}
styles={{
option: (base) => ({
...base,
height: "100%",
}),
}}
onChange={handleChange}
/>
) : null}
{form.errors[field.name] ? (
<div className="invalid-feedback">
{form.errors[field.name] && form.touched[field.name] ? form.errors[field.name] : null}
</div>
) : null}
</div>
)}
</Field>
);
}

View File

@@ -0,0 +1,36 @@
import { useFormikContext } from "formik";
import { T } from "src/locale";
interface Props {
id?: string;
name?: string;
}
export function BasicAuthField({ name = "items", id = "items" }: Props) {
const { setFieldValue } = useFormikContext();
return (
<>
<div className="row">
<div className="col-6">
<label className="form-label" htmlFor="...">
<T id="username" />
</label>
</div>
<div className="col-6">
<label className="form-label" htmlFor="...">
<T id="password" />
</label>
</div>
</div>
<div className="row mb-3">
<div className="col-6">
<input id="name" type="text" required autoComplete="off" className="form-control" />
</div>
<div className="col-6">
<input id="pw" type="password" required autoComplete="off" className="form-control" />
</div>
</div>
<button className="btn">+</button>
</>
);
}

View File

@@ -10,7 +10,6 @@ interface DNSProviderOption {
readonly label: string;
readonly credentials: string;
}
export function DNSProviderFields() {
const { values, setFieldValue } = useFormikContext();
const { data: dnsProviders, isLoading } = useDnsProviders();
@@ -100,6 +99,7 @@ export function DNSProviderFields() {
<input
id="propagationSeconds"
type="number"
x
className="form-control"
min={0}
max={600}

View File

@@ -1,10 +1,11 @@
import { Field, useFormikContext } from "formik";
import type { ReactNode } from "react";
import type { ActionMeta, MultiValue } from "react-select";
import CreatableSelect from "react-select/creatable";
import { intl } from "src/locale";
import { intl, T } from "src/locale";
import { validateDomain, validateDomains } from "src/modules/Validations";
export type SelectOption = {
type SelectOption = {
label: string;
value: string;
color?: string;
@@ -35,14 +36,14 @@ export function DomainNamesField({
setFieldValue(name, doms);
};
const helperTexts: string[] = [];
const helperTexts: ReactNode[] = [];
if (maxDomains) {
helperTexts.push(intl.formatMessage({ id: "domain-names.max" }, { count: maxDomains }));
helperTexts.push(<T id="domain-names.max" data={{ count: maxDomains }} />);
}
if (!isWildcardPermitted) {
helperTexts.push(intl.formatMessage({ id: "domain-names.wildcards-not-permitted" }));
helperTexts.push(<T id="domain-names.wildcards-not-permitted" />);
} else if (!dnsProviderWildcardSupported) {
helperTexts.push(intl.formatMessage({ id: "domain-names.wildcards-not-supported" }));
helperTexts.push(<T id="domain-names.wildcards-not-supported" />);
}
return (
@@ -50,7 +51,7 @@ export function DomainNamesField({
{({ field, form }: any) => (
<div className="mb-3">
<label className="form-label" htmlFor={id}>
{intl.formatMessage({ id: label })}
<T id={label} />
</label>
<CreatableSelect
className="react-select-container"
@@ -68,8 +69,8 @@ export function DomainNamesField({
{form.errors[field.name] && form.touched[field.name] ? (
<small className="text-danger">{form.errors[field.name]}</small>
) : helperTexts.length ? (
helperTexts.map((i) => (
<small key={i} className="text-info">
helperTexts.map((i, idx) => (
<small key={idx} className="text-info">
{i}
</small>
))

View File

@@ -1,6 +1,6 @@
import CodeEditor from "@uiw/react-textarea-code-editor";
import { Field } from "formik";
import { intl } from "src/locale";
import { intl, T } from "src/locale";
interface Props {
id?: string;
@@ -17,7 +17,7 @@ export function NginxConfigField({
{({ field }: any) => (
<div className="mt-3">
<label htmlFor={id} className="form-label">
{intl.formatMessage({ id: label })}
<T id={label} />
</label>
<CodeEditor
language="nginx"

View File

@@ -3,7 +3,7 @@ import { Field, useFormikContext } from "formik";
import Select, { type ActionMeta, components, type OptionProps } from "react-select";
import type { Certificate } from "src/api/backend";
import { useCertificates } from "src/hooks";
import { DateTimeFormat, intl } from "src/locale";
import { DateTimeFormat, T } from "src/locale";
interface CertOption {
readonly value: number | "new";
@@ -106,7 +106,7 @@ export function SSLCertificateField({
{({ field, form }: any) => (
<div className="mb-3">
<label className="form-label" htmlFor={id}>
{intl.formatMessage({ id: label })}
<T id={label} />
</label>
{isLoading ? <div className="placeholder placeholder-lg col-12 my-3 placeholder-glow" /> : null}
{isError ? <div className="invalid-feedback">{`${error}`}</div> : null}

View File

@@ -1,7 +1,7 @@
import cn from "classnames";
import { Field, useFormikContext } from "formik";
import { DNSProviderFields, DomainNamesField } from "src/components";
import { intl } from "src/locale";
import { T } from "src/locale";
interface Props {
forHttp?: boolean; // the sslForced, http2Support, hstsEnabled, hstsSubdomains fields
@@ -49,7 +49,7 @@ export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomain
disabled={!hasCertificate}
/>
<span className="form-check-label">
{intl.formatMessage({ id: "domains.force-ssl" })}
<T id="domains.force-ssl" />
</span>
</label>
)}
@@ -67,7 +67,7 @@ export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomain
disabled={!hasCertificate}
/>
<span className="form-check-label">
{intl.formatMessage({ id: "domains.http2-support" })}
<T id="domains.http2-support" />
</span>
</label>
)}
@@ -87,7 +87,7 @@ export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomain
disabled={!hasCertificate || !sslForced}
/>
<span className="form-check-label">
{intl.formatMessage({ id: "domains.hsts-enabled" })}
<T id="domains.hsts-enabled" />
</span>
</label>
)}
@@ -105,7 +105,7 @@ export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomain
disabled={!hasCertificate || !hstsEnabled}
/>
<span className="form-check-label">
{intl.formatMessage({ id: "domains.hsts-subdomains" })}
<T id="domains.hsts-subdomains" />
</span>
</label>
)}
@@ -131,7 +131,7 @@ export function SSLOptionsFields({ forHttp = true, forceDNSForNew, requireDomain
onChange={(e) => handleToggleChange(e, field.name)}
/>
<span className="form-check-label">
{intl.formatMessage({ id: "domains.use-dns" })}
<T id="domains.use-dns" />
</span>
</label>
)}

View File

@@ -1,3 +1,5 @@
export * from "./AccessField";
export * from "./BasicAuthField";
export * from "./DNSProviderFields";
export * from "./DomainNamesField";
export * from "./NginxConfigField";

View File

@@ -2,7 +2,7 @@ import type { ReactNode } from "react";
import Alert from "react-bootstrap/Alert";
import { Loading, LoadingPage } from "src/components";
import { useUser } from "src/hooks";
import { intl } from "src/locale";
import { T } from "src/locale";
interface Props {
permission: string;
@@ -64,7 +64,11 @@ function HasPermission({
return <>{children}</>;
}
return !hideError ? <Alert variant="danger">{intl.formatMessage({ id: "no-permission-error" })}</Alert> : null;
return !hideError ? (
<Alert variant="danger">
<T id="no-permission-error" />
</Alert>
) : null;
}
export { HasPermission };

View File

@@ -1,8 +1,9 @@
import { intl } from "src/locale";
import type { ReactNode } from "react";
import { T } from "src/locale";
import styles from "./Loading.module.css";
interface Props {
label?: string;
label?: string | ReactNode;
noLogo?: boolean;
}
export function Loading({ label, noLogo }: Props) {
@@ -13,7 +14,7 @@ export function Loading({ label, noLogo }: Props) {
<img className={styles.logo} src="/images/logo-no-text.svg" alt="" />
</div>
)}
<div className="text-secondary mb-3">{label || intl.formatMessage({ id: "loading" })}</div>
<div className="text-secondary mb-3">{label || <T id="loading" />}</div>
<div className="progress progress-sm">
<div className="progress-bar progress-bar-indeterminate" />
</div>

View File

@@ -2,7 +2,7 @@ import cn from "classnames";
import { Flag } from "src/components";
import { useLocaleState } from "src/context";
import { useTheme } from "src/hooks";
import { changeLocale, getFlagCodeForLocale, intl, localeOptions } from "src/locale";
import { changeLocale, getFlagCodeForLocale, localeOptions, T } from "src/locale";
import styles from "./LocalePicker.module.css";
function LocalePicker() {
@@ -35,34 +35,13 @@ function LocalePicker() {
changeTo(item[0]);
}}
>
<Flag countryCode={getFlagCodeForLocale(item[0])} />{" "}
{intl.formatMessage({ id: `locale-${item[1]}` })}
<Flag countryCode={getFlagCodeForLocale(item[0])} /> <T id={`locale-${item[1]}`} />
</a>
);
})}
</div>
</div>
);
// <div className={className}>
// <Menu>
// <MenuButton as={Button} {...additionalProps}>
// <Flag countryCode={getFlagCodeForLocale(locale)} />
// </MenuButton>
// <MenuList>
// {localeOptions.map((item) => {
// return (
// <MenuItem
// icon={<Flag countryCode={getFlagCodeForLocale(item[0])} />}
// onClick={() => changeTo(item[0])}
// key={`locale-${item[0]}`}>
// <span>{intl.formatMessage({ id: `locale-${item[1]}` })}</span>
// </MenuItem>
// );
// })}
// </MenuList>
// </Menu>
// </Box>
}
export { LocalePicker };

View File

@@ -1,5 +1,5 @@
import { useHealth } from "src/hooks";
import { intl } from "src/locale";
import { T } from "src/locale";
export function SiteFooter() {
const health = useHealth();
@@ -25,7 +25,7 @@ export function SiteFooter() {
className="link-secondary"
rel="noopener"
>
{intl.formatMessage({ id: "footer.github-fork" })}
<T id="footer.github-fork" />
</a>
</li>
</ul>

View File

@@ -3,7 +3,7 @@ import { useState } from "react";
import { LocalePicker, ThemeSwitcher } from "src/components";
import { useAuthState } from "src/context";
import { useUser } from "src/hooks";
import { intl } from "src/locale";
import { T } from "src/locale";
import { ChangePasswordModal, UserModal } from "src/modals";
import styles from "./SiteHeader.module.css";
@@ -66,9 +66,7 @@ export function SiteHeader() {
<div className="d-none d-xl-block ps-2">
<div>{currentUser?.nickname}</div>
<div className="mt-1 small text-secondary">
{intl.formatMessage({
id: isAdmin ? "role.admin" : "role.standard-user",
})}
<T id={isAdmin ? "role.admin" : "role.standard-user"} />
</div>
</div>
</a>
@@ -82,7 +80,7 @@ export function SiteHeader() {
}}
>
<IconUser width={18} />
{intl.formatMessage({ id: "user.edit-profile" })}
<T id="user.edit-profile" />
</a>
<a
href="?"
@@ -93,7 +91,7 @@ export function SiteHeader() {
}}
>
<IconLock width={18} />
{intl.formatMessage({ id: "user.change-password" })}
<T id="user.change-password" />
</a>
<div className="dropdown-divider" />
<a
@@ -105,7 +103,7 @@ export function SiteHeader() {
}}
>
<IconLogout width={18} />
{intl.formatMessage({ id: "user.logout" })}
<T id="user.logout" />
</a>
</div>
</div>

View File

@@ -10,7 +10,7 @@ import {
import cn from "classnames";
import React from "react";
import { HasPermission, NavLink } from "src/components";
import { intl } from "src/locale";
import { T } from "src/locale";
interface MenuItem {
label: string;
@@ -108,7 +108,9 @@ const getMenuItem = (item: MenuItem, onClick?: () => void) => {
<span className="nav-link-icon d-md-none d-lg-inline-block">
{item.icon && React.createElement(item.icon, { height: 24, width: 24 })}
</span>
<span className="nav-link-title">{intl.formatMessage({ id: item.label })}</span>
<span className="nav-link-title">
<T id={item.label} />
</span>
</NavLink>
</li>
</HasPermission>
@@ -136,7 +138,9 @@ const getMenuDropown = (item: MenuItem, onClick?: () => void) => {
<span className="nav-link-icon d-md-none d-lg-inline-block">
<IconDeviceDesktop height={24} width={24} />
</span>
<span className="nav-link-title">{intl.formatMessage({ id: item.label })}</span>
<span className="nav-link-title">
<T id={item.label} />
</span>
</a>
<div className="dropdown-menu">
{item.items?.map((subitem, idx) => {
@@ -148,7 +152,7 @@ const getMenuDropown = (item: MenuItem, onClick?: () => void) => {
hideError
>
<NavLink to={subitem.to} isDropdownItem onClick={onClick}>
{intl.formatMessage({ id: subitem.label })}
<T id={subitem.label} />
</NavLink>
</HasPermission>
);

View File

@@ -1,13 +1,9 @@
import type { Certificate } from "src/api/backend";
import { intl } from "src/locale";
import { T } from "src/locale";
interface Props {
certificate?: Certificate;
}
export function CertificateFormatter({ certificate }: Props) {
if (certificate) {
return intl.formatMessage({ id: "lets-encrypt" });
}
return intl.formatMessage({ id: "http-only" });
return <T id={certificate ? "lets-encrypt" : "http-only"} />;
}

View File

@@ -1,4 +1,4 @@
import { DateTimeFormat, intl } from "src/locale";
import { DateTimeFormat, T } from "src/locale";
interface Props {
domains: string[];
@@ -34,7 +34,7 @@ export function DomainsFormatter({ domains, createdOn }: Props) {
</div>
{createdOn ? (
<div className="text-secondary mt-1">
{intl.formatMessage({ id: "created-on" }, { date: DateTimeFormat(createdOn) })}
<T id="created-on" data={{ date: DateTimeFormat(createdOn) }} />
</div>
) : null}
</div>

View File

@@ -1,11 +1,13 @@
import { intl } from "src/locale";
import cn from "classnames";
import { T } from "src/locale";
interface Props {
enabled: boolean;
}
export function EnabledFormatter({ enabled }: Props) {
if (enabled) {
return <span className="badge bg-lime-lt">{intl.formatMessage({ id: "enabled" })}</span>;
}
return <span className="badge bg-red-lt">{intl.formatMessage({ id: "disabled" })}</span>;
return (
<span className={cn("badge", enabled ? "bg-lime-lt" : "bg-red-lt")}>
<T id={enabled ? "enabled" : "disabled"} />
</span>
);
}

View File

@@ -1,10 +1,6 @@
import { IconArrowsCross, IconBolt, IconBoltOff, IconDisc, IconUser } from "@tabler/icons-react";
import type { AuditLog } from "src/api/backend";
import { DateTimeFormat, intl } from "src/locale";
const getEventTitle = (event: AuditLog) => (
<span>{intl.formatMessage({ id: `event.${event.action}-${event.objectType}` })}</span>
);
import { DateTimeFormat, T } from "src/locale";
const getEventValue = (event: AuditLog) => {
switch (event.objectType) {
@@ -63,7 +59,9 @@ export function EventFormatter({ row }: Props) {
return (
<div className="flex-fill">
<div className="font-weight-medium">
{getIcon(row)} {getEventTitle(row)} &mdash; <span className="badge">{getEventValue(row)}</span>
{getIcon(row)}
<T id={`event.${row.action}-${row.objectType}`} />
&mdash; <span className="badge">{getEventValue(row)}</span>
</div>
<div className="text-secondary mt-1">{DateTimeFormat(row.createdOn)}</div>
</div>

View File

@@ -1,4 +1,4 @@
import { intl } from "src/locale";
import { T } from "src/locale";
interface Props {
roles: string[];
@@ -12,7 +12,7 @@ export function RolesFormatter({ roles }: Props) {
<>
{r.map((role: string) => (
<span key={role} className="badge bg-yellow-lt me-1">
{intl.formatMessage({ id: `role.${role}` })}
<T id={`role.${role}`} />
</span>
))}
</>

View File

@@ -1,11 +1,13 @@
import { intl } from "src/locale";
import cn from "classnames";
import { T } from "src/locale";
interface Props {
enabled: boolean;
}
export function StatusFormatter({ enabled }: Props) {
if (enabled) {
return <span className="badge bg-lime-lt">{intl.formatMessage({ id: "online" })}</span>;
}
return <span className="badge bg-red-lt">{intl.formatMessage({ id: "offline" })}</span>;
return (
<span className={cn("badge", enabled ? "bg-lime-lt" : "bg-red-lt")}>
<T id={enabled ? "online" : "offline"} />
</span>
);
}

View File

@@ -1,4 +1,4 @@
import { DateTimeFormat, intl } from "src/locale";
import { DateTimeFormat, T } from "src/locale";
interface Props {
value: string;
@@ -13,9 +13,7 @@ export function ValueWithDateFormatter({ value, createdOn, disabled }: Props) {
</div>
{createdOn ? (
<div className={`text-secondary mt-1 ${disabled ? "text-red" : ""}`}>
{disabled
? intl.formatMessage({ id: "disabled" })
: intl.formatMessage({ id: "created-on" }, { date: DateTimeFormat(createdOn) })}
<T id={disabled ? "disabled" : "created-on"} data={{ date: DateTimeFormat(createdOn) }} />
</div>
) : null}
</div>