feat(i18n): add i18n support

This commit is contained in:
JiPai
2024-11-08 01:32:46 +08:00
parent 237106428b
commit 0ca8ee17be
9 changed files with 145 additions and 2 deletions

View File

@@ -18,6 +18,7 @@ import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useTranslation } from "next-i18next";
import { useEffect } from "react"; import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -51,6 +52,7 @@ const randomImages = [
export const ProfileForm = () => { export const ProfileForm = () => {
const { data, refetch } = api.auth.get.useQuery(); const { data, refetch } = api.auth.get.useQuery();
const { mutateAsync, isLoading } = api.auth.update.useMutation(); const { mutateAsync, isLoading } = api.auth.update.useMutation();
const { t } = useTranslation("common");
const form = useForm<Profile>({ const form = useForm<Profile>({
defaultValues: { defaultValues: {
@@ -91,7 +93,9 @@ export const ProfileForm = () => {
<Card className="bg-transparent"> <Card className="bg-transparent">
<CardHeader className="flex flex-row gap-2 flex-wrap justify-between items-center"> <CardHeader className="flex flex-row gap-2 flex-wrap justify-between items-center">
<div> <div>
<CardTitle className="text-xl">Account</CardTitle> <CardTitle className="text-xl">
{t("dashboard.settings.profile.title")}
</CardTitle>
<CardDescription> <CardDescription>
Change the details of your profile here. Change the details of your profile here.
</CardDescription> </CardDescription>

View File

@@ -0,0 +1,9 @@
/** @type {import('next-i18next').UserConfig} */
module.exports = {
i18n: {
defaultLocale: "en",
locales: ["en", "zh-Hans"],
localeDetection: false,
},
fallbackLng: "en",
};

View File

@@ -84,6 +84,7 @@
"dotenv": "16.4.5", "dotenv": "16.4.5",
"drizzle-orm": "^0.30.8", "drizzle-orm": "^0.30.8",
"drizzle-zod": "0.5.1", "drizzle-zod": "0.5.1",
"i18next": "^23.16.4",
"input-otp": "^1.2.4", "input-otp": "^1.2.4",
"js-yaml": "4.1.0", "js-yaml": "4.1.0",
"lodash": "4.17.21", "lodash": "4.17.21",
@@ -91,6 +92,7 @@
"lucide-react": "^0.312.0", "lucide-react": "^0.312.0",
"nanoid": "3", "nanoid": "3",
"next": "^15.0.1", "next": "^15.0.1",
"next-i18next": "^15.3.1",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"node-pty": "1.0.0", "node-pty": "1.0.0",
"node-schedule": "2.1.1", "node-schedule": "2.1.1",
@@ -100,6 +102,7 @@
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "^7.49.3", "react-hook-form": "^7.49.3",
"react-i18next": "^15.1.0",
"recharts": "^2.12.7", "recharts": "^2.12.7",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"sonner": "^1.4.0", "sonner": "^1.4.0",

View File

@@ -3,6 +3,7 @@ import "@/styles/globals.css";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import type { NextPage } from "next"; import type { NextPage } from "next";
import { appWithTranslation } from "next-i18next";
import { ThemeProvider } from "next-themes"; import { ThemeProvider } from "next-themes";
import type { AppProps } from "next/app"; import type { AppProps } from "next/app";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
@@ -27,6 +28,7 @@ const MyApp = ({
pageProps: { ...pageProps }, pageProps: { ...pageProps },
}: AppPropsWithLayout) => { }: AppPropsWithLayout) => {
const getLayout = Component.getLayout ?? ((page) => page); const getLayout = Component.getLayout ?? ((page) => page);
return ( return (
<> <>
<style jsx global>{` <style jsx global>{`
@@ -59,4 +61,4 @@ const MyApp = ({
); );
}; };
export default api.withTRPC(MyApp); export default api.withTRPC(appWithTranslation(MyApp));

View File

@@ -4,9 +4,11 @@ import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout"; import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root"; import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
import { getLocale } from "@/utils/i18n";
import { validateRequest } from "@dokploy/server"; import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server"; import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next"; import type { GetServerSidePropsContext } from "next";
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import React, { type ReactElement } from "react"; import React, { type ReactElement } from "react";
import superjson from "superjson"; import superjson from "superjson";
@@ -41,6 +43,7 @@ export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>, ctx: GetServerSidePropsContext<{ serviceId: string }>,
) { ) {
const { req, res } = ctx; const { req, res } = ctx;
const locale = getLocale(req.cookies);
const { user, session } = await validateRequest(req, res); const { user, session } = await validateRequest(req, res);
const helpers = createServerSideHelpers({ const helpers = createServerSideHelpers({
@@ -75,6 +78,7 @@ export async function getServerSideProps(
return { return {
props: { props: {
trpcState: helpers.dehydrate(), trpcState: helpers.dehydrate(),
...(await serverSideTranslations(locale, ["common"])),
}, },
}; };
} }

View File

@@ -0,0 +1,9 @@
{
"dashboard": {
"settings": {
"profile": {
"title": "Account"
}
}
}
}

View File

@@ -0,0 +1,9 @@
{
"dashboard": {
"settings": {
"profile": {
"title": "账户偏好"
}
}
}
}

View File

@@ -0,0 +1,6 @@
import type { NextApiRequestCookies } from "next/dist/server/api-utils";
export function getLocale(cookies: NextApiRequestCookies) {
const locale = cookies.DOKPLOY_LOCALE ?? "en";
return locale;
}

97
pnpm-lock.yaml generated
View File

@@ -247,6 +247,9 @@ importers:
drizzle-zod: drizzle-zod:
specifier: 0.5.1 specifier: 0.5.1
version: 0.5.1(drizzle-orm@0.30.10(@types/react@18.3.5)(postgres@3.4.4)(react@18.2.0))(zod@3.23.8) version: 0.5.1(drizzle-orm@0.30.10(@types/react@18.3.5)(postgres@3.4.4)(react@18.2.0))(zod@3.23.8)
i18next:
specifier: ^23.16.4
version: 23.16.4
input-otp: input-otp:
specifier: ^1.2.4 specifier: ^1.2.4
version: 1.2.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0) version: 1.2.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -268,6 +271,9 @@ importers:
next: next:
specifier: ^15.0.1 specifier: ^15.0.1
version: 15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) version: 15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next-i18next:
specifier: ^15.3.1
version: 15.3.1(i18next@23.16.4)(next@15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-i18next@15.1.0(i18next@23.16.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0)
next-themes: next-themes:
specifier: ^0.2.1 specifier: ^0.2.1
version: 0.2.1(next@15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0) version: 0.2.1(next@15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -295,6 +301,9 @@ importers:
react-hook-form: react-hook-form:
specifier: ^7.49.3 specifier: ^7.49.3
version: 7.52.1(react@18.2.0) version: 7.52.1(react@18.2.0)
react-i18next:
specifier: ^15.1.0
version: 15.1.0(i18next@23.16.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
recharts: recharts:
specifier: ^2.12.7 specifier: ^2.12.7
version: 2.12.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0) version: 2.12.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -3206,6 +3215,9 @@ packages:
'@types/hast@2.3.10': '@types/hast@2.3.10':
resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==} resolution: {integrity: sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==}
'@types/hoist-non-react-statics@3.3.5':
resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==}
'@types/http-cache-semantics@4.0.4': '@types/http-cache-semantics@4.0.4':
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
@@ -3893,6 +3905,9 @@ packages:
core-js-pure@3.38.1: core-js-pure@3.38.1:
resolution: {integrity: sha512-BY8Etc1FZqdw1glX0XNOq2FDwfrg/VGqoZOZCdaL+UmdaqDwQwYXkMJT4t6In+zfEfOJDcM9T0KdbBeJg8KKCQ==} resolution: {integrity: sha512-BY8Etc1FZqdw1glX0XNOq2FDwfrg/VGqoZOZCdaL+UmdaqDwQwYXkMJT4t6In+zfEfOJDcM9T0KdbBeJg8KKCQ==}
core-js@3.39.0:
resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==}
cosmiconfig-typescript-loader@5.0.0: cosmiconfig-typescript-loader@5.0.0:
resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==} resolution: {integrity: sha512-+8cK7jRAReYkMwMiG+bxhcNKiHJDM6bR9FD/nGBXOWdMLuYawjF5cGrtLilJ+LGd3ZjCXnJjR5DkfWPoIVlqJA==}
engines: {node: '>=v16'} engines: {node: '>=v16'}
@@ -4662,10 +4677,16 @@ packages:
highlight.js@10.7.3: highlight.js@10.7.3:
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
hoist-non-react-statics@3.3.2:
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
hono@4.5.8: hono@4.5.8:
resolution: {integrity: sha512-pqpSlcdqGkpTTRpLYU1PnCz52gVr0zVR9H5GzMyJWuKQLLEBQxh96q45QizJ2PPX8NATtz2mu31/PKW/Jt+90Q==} resolution: {integrity: sha512-pqpSlcdqGkpTTRpLYU1PnCz52gVr0zVR9H5GzMyJWuKQLLEBQxh96q45QizJ2PPX8NATtz2mu31/PKW/Jt+90Q==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
html-parse-stringify@3.0.1:
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
html-to-text@9.0.5: html-to-text@9.0.5:
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -4701,6 +4722,12 @@ packages:
resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==}
engines: {node: '>=10.18'} engines: {node: '>=10.18'}
i18next-fs-backend@2.3.2:
resolution: {integrity: sha512-LIwUlkqDZnUI8lnUxBnEj8K/FrHQTT/Sc+1rvDm9E8YvvY5YxzoEAASNx+W5M9DfD5s77lI5vSAFWeTp26B/3Q==}
i18next@23.16.4:
resolution: {integrity: sha512-9NIYBVy9cs4wIqzurf7nLXPyf3R78xYbxExVqHLK9od3038rjpyOEzW+XB130kZ1N4PZ9inTtJ471CRJ4Ituyg==}
iconv-lite@0.4.24: iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@@ -5247,6 +5274,15 @@ packages:
resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==} resolution: {integrity: sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
next-i18next@15.3.1:
resolution: {integrity: sha512-+pa2pZJb7B6k5PKW3TLVMmAodqkNaOBWVYlpWX56mgcEJz0UMW+MKSdKM9Z72CHp6Bp48g7OWwDnLqxXNp/84w==}
engines: {node: '>=14'}
peerDependencies:
i18next: '>= 23.7.13'
next: '>= 12.0.0'
react: '>= 17.0.2'
react-i18next: '>= 13.5.0'
next-themes@0.2.1: next-themes@0.2.1:
resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
peerDependencies: peerDependencies:
@@ -5726,6 +5762,19 @@ packages:
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19 react: ^16.8.0 || ^17 || ^18 || ^19
react-i18next@15.1.0:
resolution: {integrity: sha512-zj3nJynMnZsy2gPZiOTC7XctCY5eQGqT3tcKMmfJWC9FMvgd+960w/adq61j8iPzpwmsXejqID9qC3Mqu1Xu2Q==}
peerDependencies:
i18next: '>= 23.2.3'
react: '>= 16.8.0'
react-dom: '*'
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
react-immutable-proptypes@2.2.0: react-immutable-proptypes@2.2.0:
resolution: {integrity: sha512-Vf4gBsePlwdGvSZoLSBfd4HAP93HDauMY4fDjXhreg/vg6F3Fj/MXDNyTbltPC/xZKmZc+cjLu3598DdYK6sgQ==} resolution: {integrity: sha512-Vf4gBsePlwdGvSZoLSBfd4HAP93HDauMY4fDjXhreg/vg6F3Fj/MXDNyTbltPC/xZKmZc+cjLu3598DdYK6sgQ==}
peerDependencies: peerDependencies:
@@ -6570,6 +6619,10 @@ packages:
jsdom: jsdom:
optional: true optional: true
void-elements@3.1.0:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
w3c-keyname@2.2.8: w3c-keyname@2.2.8:
resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
@@ -9282,6 +9335,11 @@ snapshots:
dependencies: dependencies:
'@types/unist': 2.0.10 '@types/unist': 2.0.10
'@types/hoist-non-react-statics@3.3.5':
dependencies:
'@types/react': 18.3.5
hoist-non-react-statics: 3.3.2
'@types/http-cache-semantics@4.0.4': {} '@types/http-cache-semantics@4.0.4': {}
'@types/http-errors@2.0.4': {} '@types/http-errors@2.0.4': {}
@@ -10056,6 +10114,8 @@ snapshots:
core-js-pure@3.38.1: {} core-js-pure@3.38.1: {}
core-js@3.39.0: {}
cosmiconfig-typescript-loader@5.0.0(@types/node@18.19.42)(cosmiconfig@9.0.0(typescript@5.5.3))(typescript@5.5.3): cosmiconfig-typescript-loader@5.0.0(@types/node@18.19.42)(cosmiconfig@9.0.0(typescript@5.5.3))(typescript@5.5.3):
dependencies: dependencies:
'@types/node': 18.19.42 '@types/node': 18.19.42
@@ -10820,8 +10880,16 @@ snapshots:
highlight.js@10.7.3: {} highlight.js@10.7.3: {}
hoist-non-react-statics@3.3.2:
dependencies:
react-is: 16.13.1
hono@4.5.8: {} hono@4.5.8: {}
html-parse-stringify@3.0.1:
dependencies:
void-elements: 3.1.0
html-to-text@9.0.5: html-to-text@9.0.5:
dependencies: dependencies:
'@selderee/plugin-htmlparser2': 0.11.0 '@selderee/plugin-htmlparser2': 0.11.0
@@ -10865,6 +10933,12 @@ snapshots:
hyperdyperid@1.2.0: {} hyperdyperid@1.2.0: {}
i18next-fs-backend@2.3.2: {}
i18next@23.16.4:
dependencies:
'@babel/runtime': 7.25.0
iconv-lite@0.4.24: iconv-lite@0.4.24:
dependencies: dependencies:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
@@ -11365,6 +11439,18 @@ snapshots:
neotraverse@0.6.18: {} neotraverse@0.6.18: {}
next-i18next@15.3.1(i18next@23.16.4)(next@15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-i18next@15.1.0(i18next@23.16.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.25.0
'@types/hoist-non-react-statics': 3.3.5
core-js: 3.39.0
hoist-non-react-statics: 3.3.2
i18next: 23.16.4
i18next-fs-backend: 2.3.2
next: 15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-i18next: 15.1.0(i18next@23.16.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next-themes@0.2.1(next@15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0): next-themes@0.2.1(next@15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies: dependencies:
next: 15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) next: 15.0.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
@@ -11854,6 +11940,15 @@ snapshots:
dependencies: dependencies:
react: 18.2.0 react: 18.2.0
react-i18next@15.1.0(i18next@23.16.4)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.25.0
html-parse-stringify: 3.0.1
i18next: 23.16.4
react: 18.2.0
optionalDependencies:
react-dom: 18.2.0(react@18.2.0)
react-immutable-proptypes@2.2.0(immutable@3.8.2): react-immutable-proptypes@2.2.0(immutable@3.8.2):
dependencies: dependencies:
immutable: 3.8.2 immutable: 3.8.2
@@ -12791,6 +12886,8 @@ snapshots:
- supports-color - supports-color
- terser - terser
void-elements@3.1.0: {}
w3c-keyname@2.2.8: {} w3c-keyname@2.2.8: {}
watchpack@2.4.1: watchpack@2.4.1: