diff --git a/examples/app/.gitignore b/examples/app/.gitignore
new file mode 100644
index 0000000..e985853
--- /dev/null
+++ b/examples/app/.gitignore
@@ -0,0 +1 @@
+.vercel
diff --git a/examples/app/README.md b/examples/app/README.md
new file mode 100644
index 0000000..c403366
--- /dev/null
+++ b/examples/app/README.md
@@ -0,0 +1,36 @@
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+# or
+bun dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/examples/app/app/[locale]/[...notFound]/page.tsx b/examples/app/app/[locale]/[...notFound]/page.tsx
new file mode 100644
index 0000000..2c4bcd6
--- /dev/null
+++ b/examples/app/app/[locale]/[...notFound]/page.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+
+export default function NotFound() {
+ return (
+
+
Custom Not Found page
+
Could not find requested resource
+
+ );
+}
diff --git a/examples/app/app/[locale]/client-component.tsx b/examples/app/app/[locale]/client-component.tsx
new file mode 100644
index 0000000..490f656
--- /dev/null
+++ b/examples/app/app/[locale]/client-component.tsx
@@ -0,0 +1,46 @@
+'use client';
+
+// @ts-expect-error - missing import
+import React, { use } from 'react';
+import { useChangeLocale, getI18n, getLocale, getScopedI18n } from '@/locales';
+
+export function ClientComponent() {
+ const t = use(getI18n());
+ const scopedT = use(getScopedI18n('hello'));
+ const locale = getLocale();
+ const changeLocale = useChangeLocale();
+
+ return (
+
+
{t('hello.world')}
+
{scopedT('world')}
+
+ {t('hello.param', {
+ name: 'John',
+ })}
+
+
+ {scopedT('param', {
+ name: 'John',
+ })}
+
+
Current locale: {locale}
+
{
+ changeLocale('en');
+ }}
+ >
+ EN
+
+
{
+ changeLocale('fr');
+ }}
+ >
+ FR
+
+
+ );
+}
diff --git a/examples/app/app/[locale]/layout.tsx b/examples/app/app/[locale]/layout.tsx
new file mode 100644
index 0000000..8a7456d
--- /dev/null
+++ b/examples/app/app/[locale]/layout.tsx
@@ -0,0 +1,21 @@
+import { generateI18nStaticParams } from '@/locales';
+import React from 'react';
+
+export const dynamicParams = false;
+export function generateStaticParams() {
+ return generateI18nStaticParams();
+}
+
+export default function RootLayout({
+ children,
+ params: { locale },
+}: Readonly<{
+ children: React.ReactNode;
+ params: { locale: string };
+}>) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/examples/app/app/[locale]/page.tsx b/examples/app/app/[locale]/page.tsx
new file mode 100644
index 0000000..55eeeb3
--- /dev/null
+++ b/examples/app/app/[locale]/page.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { getI18n, getLocale, getScopedI18n } from '@/locales';
+import { ClientComponent } from './client-component';
+import { Suspense } from 'react';
+
+export default async function Home() {
+ const t = await getI18n();
+ const scopedT = await getScopedI18n('hello');
+ const locale = getLocale();
+
+ return (
+
+
{t('hello.world')}
+
{scopedT('world')}
+
+ {t('hello.param', {
+ name: 'John',
+ })}
+
+
+ {scopedT('param', {
+ name: 'John',
+ })}
+
+
Current locale: {locale}
+
+
+
+
+
+ );
+}
diff --git a/examples/app/locales/en.ts b/examples/app/locales/en.ts
new file mode 100644
index 0000000..4e48224
--- /dev/null
+++ b/examples/app/locales/en.ts
@@ -0,0 +1,4 @@
+export default {
+ 'hello.world': 'Hello, World!',
+ 'hello.param': 'Hello, {name}!',
+} as const;
diff --git a/examples/app/locales/fr.ts b/examples/app/locales/fr.ts
new file mode 100644
index 0000000..5728a7e
--- /dev/null
+++ b/examples/app/locales/fr.ts
@@ -0,0 +1,4 @@
+export default {
+ 'hello.world': 'Bonjour, le monde !',
+ 'hello.param': 'Bonjour, {name} !',
+} as const;
diff --git a/examples/app/locales/index.ts b/examples/app/locales/index.ts
new file mode 100644
index 0000000..d50d6cb
--- /dev/null
+++ b/examples/app/locales/index.ts
@@ -0,0 +1,14 @@
+import { createI18n } from 'next-international/app';
+// import en from './en';
+
+export const { getI18n, getScopedI18n, generateI18nStaticParams, getLocale, useChangeLocale } = createI18n(
+ {
+ en: () => import('./en'),
+ fr: () => import('./fr'),
+ },
+ {
+ // segmentName: 'locale',
+ // basePath: '/base',
+ // fallbackLocale: en,
+ },
+);
diff --git a/examples/app/middleware.ts b/examples/app/middleware.ts
new file mode 100644
index 0000000..4be91f1
--- /dev/null
+++ b/examples/app/middleware.ts
@@ -0,0 +1,18 @@
+import { NextResponse } from 'next/server';
+import { createI18nMiddleware } from 'next-international/middleware';
+
+export const middleware = createI18nMiddleware(
+ request => {
+ console.log('User middleware:', request.url);
+ return NextResponse.next();
+ },
+ {
+ locales: ['en', 'fr'],
+ defaultLocale: 'en',
+ // urlMappingStrategy: 'rewrite',
+ },
+);
+
+export const config = {
+ matcher: ['/((?!api|static|.*\\..*|_next|favicon.ico|robots.txt).*)'],
+};
diff --git a/examples/app/next-env.d.ts b/examples/app/next-env.d.ts
new file mode 100644
index 0000000..4f11a03
--- /dev/null
+++ b/examples/app/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/examples/app/next.config.mjs b/examples/app/next.config.mjs
new file mode 100644
index 0000000..4678774
--- /dev/null
+++ b/examples/app/next.config.mjs
@@ -0,0 +1,4 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {};
+
+export default nextConfig;
diff --git a/examples/app/package.json b/examples/app/package.json
new file mode 100644
index 0000000..4ec9f72
--- /dev/null
+++ b/examples/app/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "app",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start",
+ "lint": "next lint"
+ },
+ "dependencies": {
+ "next": "14.1.1",
+ "next-international": "workspace:*",
+ "react": "^18",
+ "react-dom": "^18"
+ },
+ "devDependencies": {
+ "@types/node": "^20",
+ "@types/react": "^18",
+ "@types/react-dom": "^18",
+ "typescript": "^5"
+ }
+}
diff --git a/examples/app/tsconfig.json b/examples/app/tsconfig.json
new file mode 100644
index 0000000..e7ff90f
--- /dev/null
+++ b/examples/app/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/packages/international-types/index.ts b/packages/international-types/index.ts
index 8fba9a1..e396bff 100644
--- a/packages/international-types/index.ts
+++ b/packages/international-types/index.ts
@@ -1,42 +1,8 @@
-export type LocaleValue = string | number | boolean | null | undefined | Date;
-export type BaseLocale = Record;
-export type ImportedLocales = Record Promise>;
-export type ExplicitLocales = Record;
-
-export type LocaleKeys<
- Locale extends BaseLocale,
- Scope extends Scopes | undefined,
- Key extends string = Extract,
-> = Scope extends undefined ? RemovePlural : Key extends `${Scope}.${infer Test}` ? RemovePlural : never;
-
-type Delimiter = `=${number}` | 'other';
-
-type ExtractParams = Value extends ''
- ? []
- : Value extends `${string}{${infer Param}}${infer Tail}`
- ? [Param, ...ExtractParams]
- : [];
-
-export type Params = Value extends ''
- ? []
- : // Plural with 3 cases
- Value extends `{${infer Param}, plural, ${Delimiter} {${infer Content}} ${Delimiter} {${infer Content2}} ${Delimiter} {${infer Content3}}}`
- ? [Param, ...ExtractParams, ...ExtractParams, ...ExtractParams]
- : // Plural with 2 cases
- Value extends `{${infer Param}, plural, ${Delimiter} {${infer Content}} ${Delimiter} {${infer Content2}}}`
- ? [Param, ...ExtractParams, ...ExtractParams]
- : // Simple cases (e.g `This is a {param}`)
- Value extends `${string}{${infer Param}}${infer Tail}`
- ? [Param, ...Params]
- : [];
-
-export type GetParams = Value extends ''
- ? []
- : Value extends `${string}{${infer Param}}${infer Tail}`
- ? [Param, ...Params]
- : [];
-
-export type ParamsObject = Record[number], LocaleValue>;
+export type Param = string | number;
+export type LocaleType = Record;
+export type LocalesObject = {
+ [locale: string]: () => Promise<{ default: LocaleType }>;
+};
type ExtractScopes<
Value extends string,
@@ -48,91 +14,34 @@ type ExtractScopes<
]
: [];
-export type Scopes = ExtractScopes>[number];
+export type Scopes<
+ Locale extends LocaleType,
+ TheKeys extends string = Extract,
+> = ExtractScopes[number];
-export type ScopedValue<
- Locale extends BaseLocale,
+export type Keys<
+ Locale extends LocaleType,
Scope extends Scopes | undefined,
- Key extends LocaleKeys,
+ TheKeys extends string = Extract,
> = Scope extends undefined
- ? IsPlural extends true
- ? Locale[`${Key}#${PluralSuffix}`]
- : Locale[Key]
- : IsPlural extends true
- ? Locale[`${Scope}.${Key}#${PluralSuffix}`]
- : Locale[`${Scope}.${Key}`];
-
-// From https://github.com/microsoft/TypeScript/issues/13298#issuecomment-885980381
-type UnionToIntersection = (U extends never ? never : (arg: U) => never) extends (arg: infer I) => void ? I : never;
-
-type UnionToTuple = UnionToIntersection T> extends (_: never) => infer W
- ? [...UnionToTuple>, W]
- : [];
-
-// Given a object type with string keys, return the "first" key.
-// Because the key ordering is not guaranteed, this type should be used
-// only when the key order is not important.
-type SomeKey> = UnionToTuple[0] extends string
- ? UnionToTuple[0]
- : never;
-
-// Gets a single locale type from an object of the shape of BaseLocales.
-export type GetLocaleType = Locales extends ImportedLocales
- ? Awaited]>>['default']
- : Locales[SomeKey];
-
-type Join = K extends string | number
- ? P extends string | number
- ? `${K}${'' extends P ? '' : '.'}${P}`
- : never
- : never;
-
-type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]];
-
-type Leaves = [D] extends [never]
- ? never
- : T extends object
- ? { [K in keyof T]-?: Join> }[keyof T]
- : '';
-
-type FollowPath = P extends `${infer U}.${infer R}`
- ? U extends keyof T
- ? FollowPath
- : P extends keyof T
- ? T[P]
- : never
- : P extends keyof T
- ? T[P]
+ ? RemovePlural
+ : TheKeys extends `${Scope}.${infer Tail}`
+ ? RemovePlural
: never;
-export type FlattenLocale> = {
- [K in Leaves]: FollowPath;
-};
-
-type PluralSuffix = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
+export type ExtractParams = Value extends ''
+ ? []
+ : Value extends `${string}{${infer Param}}${infer Tail}`
+ ? [Param, ...ExtractParams]
+ : [];
-type RemovePlural = Key extends `${infer Head}#${PluralSuffix}` ? Head : Key;
+export type PluralSuffix = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
+export type RemovePlural = Key extends `${infer Head}#${PluralSuffix}` ? Head : Key;
-type GetPlural<
- Key extends string,
+export type IsPlural<
+ Locale extends LocaleType,
Scope extends Scopes | undefined,
- Locale extends BaseLocale,
-> = Scope extends undefined
- ? `${Key}#${PluralSuffix}` & keyof Locale extends infer PluralKey
- ? PluralKey extends `${string}#${infer Plural extends PluralSuffix}`
- ? Plural
- : never
- : never
- : `${Scope}.${Key}#${PluralSuffix}` & keyof Locale extends infer PluralKey
- ? PluralKey extends `${string}#${infer Plural extends PluralSuffix}`
- ? Plural
- : never
- : never;
-
-type IsPlural<
Key extends string,
- Scope extends Scopes | undefined,
- Locale extends BaseLocale,
> = Scope extends undefined
? `${Key}#${PluralSuffix}` & keyof Locale extends never
? false
@@ -141,65 +50,45 @@ type IsPlural<
? false
: true;
-type GetCountUnion<
- Key extends string,
- Scope extends Scopes | undefined,
- Locale extends BaseLocale,
- Plural extends PluralSuffix = GetPlural,
-> = Plural extends 'zero'
- ? 0
- : Plural extends 'one'
- ? // eslint-disable-next-line @typescript-eslint/ban-types
- 1 | 21 | 31 | 41 | 51 | 61 | 71 | 81 | 91 | 101 | (number & {})
- : Plural extends 'two'
- ? // eslint-disable-next-line @typescript-eslint/ban-types
- 2 | 22 | 32 | 42 | 52 | 62 | 72 | 82 | 92 | 102 | (number & {})
- : number;
+export type GetPluralCount = number;
-type AddCount | undefined, Locale extends BaseLocale> = T extends []
- ? [
+export type Params<
+ Locale extends LocaleType,
+ Scope extends Scopes | undefined,
+ Key extends Keys,
+ Plural extends boolean = IsPlural,
+ Value extends string = Scope extends undefined
+ ? Plural extends true
+ ? Locale[`${Key}#${PluralSuffix}`]
+ : Locale[Key]
+ : Plural extends true
+ ? Locale[`${Scope}.${Key}#${PluralSuffix}`]
+ : Locale[`${Scope}.${Key}`],
+ TheParams extends string[] = ExtractParams,
+> = Plural extends true
+ ? TheParams['length'] extends 0
+ ? [{ count: GetPluralCount }]
+ : [
+ { count: GetPluralCount } & {
+ [K in TheParams[number]]: Param;
+ },
+ ]
+ : TheParams['length'] extends 0
+ ? []
+ : [
{
- /**
- * The `count` depends on the plural tags defined in your locale,
- * and the current locale rules.
- *
- * - `zero` allows 0
- * - `one` autocompletes 1, 21, 31, 41... but allows any number
- * - `two` autocompletes 2, 22, 32, 42... but allows any number
- * - `few`, `many` and `other` allow any number
- *
- * @see https://www.unicode.org/cldr/charts/43/supplemental/language_plural_rules.html
- */
- count: GetCountUnion;
+ [K in TheParams[number]]: Param;
},
- ]
- : T extends [infer R]
- ? [
- {
- /**
- * The `count` depends on the plural tags defined in your locale,
- * and the current locale rules.
- *
- * - `zero` allows 0
- * - `one` autocompletes 1, 21, 31, 41... but allows any number
- * - `two` autocompletes 2, 22, 32, 42... but allows any number
- * - `few`, `many` and `other` allow any number
- *
- * @see https://www.unicode.org/cldr/charts/43/supplemental/language_plural_rules.html
- */
- count: GetCountUnion;
- } & R,
- ]
+ ];
+
+type UnionToIntersection = (U extends never ? never : (arg: U) => never) extends (arg: infer I) => void ? I : never;
+
+type UnionToTuple = UnionToIntersection T> extends (_: never) => infer W
+ ? [...UnionToTuple>, W]
+ : [];
+
+type SomeKey> = UnionToTuple[0] extends string
+ ? UnionToTuple[0]
: never;
-export type CreateParams<
- T,
- Locale extends BaseLocale,
- Scope extends Scopes | undefined,
- Key extends LocaleKeys,
- Value extends LocaleValue = ScopedValue,
-> = IsPlural extends true
- ? AddCount['length'] extends 0 ? [] : [T], Key, Scope, Locale>
- : GetParams['length'] extends 0
- ? []
- : [T];
+export type GetLocale = Awaited]>>['default'];
diff --git a/packages/international-types/package.json b/packages/international-types/package.json
index dcdae36..21a1e0b 100644
--- a/packages/international-types/package.json
+++ b/packages/international-types/package.json
@@ -23,6 +23,7 @@
"homepage": "https://next-international.vercel.app",
"license": "MIT",
"scripts": {
- "build": "tsc --declaration --emitDeclarationOnly --outDir dist index.ts"
+ "build": "tsc --declaration --emitDeclarationOnly --outDir dist index.ts",
+ "watch": "pnpm build --watch"
}
}
diff --git a/packages/next-international/app.d.ts b/packages/next-international/app.d.ts
new file mode 100644
index 0000000..0bcbf3f
--- /dev/null
+++ b/packages/next-international/app.d.ts
@@ -0,0 +1 @@
+export * from './dist/app/app/app/client';
diff --git a/packages/next-international/package.json b/packages/next-international/package.json
index 7949d9a..1bcd6ab 100644
--- a/packages/next-international/package.json
+++ b/packages/next-international/package.json
@@ -19,8 +19,13 @@
"default": "./dist/app/server/index.js"
},
"./middleware": {
- "types": "./dist/app/middleware/index.d.ts",
- "default": "./dist/app/middleware/index.js"
+ "types": "./dist/app/app/middleware/index.d.ts",
+ "default": "./dist/app/app/middleware/index.js"
+ },
+ "./app": {
+ "types": "./dist/app/app/app/client.d.ts",
+ "react-server": "./dist/app/app/app/server.js",
+ "default": "./dist/app/app/app/client.js"
}
},
"keywords": [
@@ -33,7 +38,8 @@
"dist",
"client.d.ts",
"server.d.ts",
- "middleware.d.ts"
+ "middleware.d.ts",
+ "app.d.ts"
],
"repository": {
"type": "git",
@@ -45,12 +51,12 @@
"homepage": "https://next-international.vercel.app",
"license": "MIT",
"scripts": {
- "build": "tsup src/index.ts src/app/client/index.ts src/app/server/index.ts src/app/middleware/index.ts --external next --external react --dts",
+ "build": "tsup src/app/app/client.ts src/app/app/server.ts src/app/middleware/index.ts --out-dir dist/app/app --external next --external react --dts",
"watch": "pnpm build --watch"
},
"devDependencies": {
"@types/react": "^18.2.45",
- "next": "^14.0.4",
+ "next": "^14.1.1",
"react": "^18.2.0",
"tsup": "^8.0.1"
},
diff --git a/packages/next-international/src/app/app/client.ts b/packages/next-international/src/app/app/client.ts
new file mode 100644
index 0000000..aa05eec
--- /dev/null
+++ b/packages/next-international/src/app/app/client.ts
@@ -0,0 +1,98 @@
+import type { GetLocale, LocaleType, LocalesObject } from 'international-types';
+import type {
+ CreateI18n,
+ GenerateI18nStaticParams,
+ I18nConfig,
+ UseChangeLocale,
+ GetI18n,
+ UseLocale,
+ GetScopedI18n,
+} from './types';
+import { SEGMENT_NAME } from './constants';
+import { useParams, useRouter, usePathname, useSearchParams } from 'next/navigation';
+import { createT } from './utils';
+
+function getLocaleCache(config: I18nConfig) {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const params = useParams();
+ const locale = params[config.segmentName ?? SEGMENT_NAME];
+
+ if (typeof locale !== 'string') {
+ throw new Error(`Invariant: locale not found in params: ${JSON.stringify(params, null, 2)}`);
+ }
+
+ return locale;
+}
+
+export function createI18n>(
+ locales: Locales,
+ config: I18nConfig = {},
+): CreateI18n {
+ const localesCache = new Map>();
+
+ const getI18n: GetI18n = async () => {
+ const locale = getLocaleCache(config);
+ const data = localesCache.get(locale) ?? (await locales[locale]()).default;
+
+ // @ts-expect-error - no types
+ return (key, ...params) => createT(locale, data, undefined, key, ...params);
+ };
+
+ const getScopedI18n: GetScopedI18n = async scope => {
+ const locale = getLocaleCache(config);
+ const data = localesCache.get(locale) ?? (await locales[locale]()).default;
+
+ // @ts-expect-error - no types
+ return (key, ...params) => createT(locale, data, scope, key, ...params);
+ };
+
+ const generateI18nStaticParams: GenerateI18nStaticParams = () => {
+ return Object.keys(locales).map(locale => ({ [config.segmentName ?? SEGMENT_NAME]: locale }));
+ };
+
+ const getLocale: UseLocale = () => {
+ return getLocaleCache(config);
+ };
+
+ const useChangeLocale: UseChangeLocale = changeLocaleConfig => {
+ const router = useRouter();
+ const currentLocale = getLocaleCache(config);
+ const path = usePathname();
+ // We call the hook conditionally to avoid always opting out of Static Rendering.
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const searchParams = changeLocaleConfig?.preserveSearchParams ? useSearchParams().toString() : undefined;
+ const finalSearchParams = searchParams ? `?${searchParams}` : '';
+
+ let pathWithoutLocale = path;
+
+ if (config.basePath) {
+ pathWithoutLocale = pathWithoutLocale.replace(config.basePath, '');
+ }
+
+ if (pathWithoutLocale.startsWith(`/${currentLocale}/`)) {
+ pathWithoutLocale = pathWithoutLocale.replace(`/${currentLocale}/`, '/');
+ } else if (pathWithoutLocale === `/${currentLocale}`) {
+ pathWithoutLocale = '/';
+ }
+
+ return newLocale => {
+ if (newLocale === currentLocale) return;
+
+ locales[newLocale]().then(module => {
+ const finalLocale = newLocale as string;
+
+ localesCache.set(finalLocale, module.default);
+ document.cookie = `locale=${finalLocale};`;
+ router.push(`/${newLocale as string}${pathWithoutLocale}${finalSearchParams}`);
+ });
+ };
+ };
+
+ return {
+ getI18n,
+ getScopedI18n,
+ generateI18nStaticParams,
+ getLocale,
+ useChangeLocale,
+ };
+}
diff --git a/packages/next-international/src/app/app/constants.ts b/packages/next-international/src/app/app/constants.ts
new file mode 100644
index 0000000..b6bfe90
--- /dev/null
+++ b/packages/next-international/src/app/app/constants.ts
@@ -0,0 +1 @@
+export const SEGMENT_NAME = 'locale';
diff --git a/packages/next-international/src/app/app/server.ts b/packages/next-international/src/app/app/server.ts
new file mode 100644
index 0000000..0264853
--- /dev/null
+++ b/packages/next-international/src/app/app/server.ts
@@ -0,0 +1,83 @@
+import type { GetLocale, LocaleType, LocalesObject } from 'international-types';
+import type {
+ CreateI18n,
+ GenerateI18nStaticParams,
+ I18nConfig,
+ UseChangeLocale,
+ GetI18n,
+ UseLocale,
+ GetScopedI18n,
+} from './types';
+import { SEGMENT_NAME } from './constants';
+// @ts-expect-error - no types
+import { cache } from 'react';
+import { staticGenerationAsyncStorage } from 'next/dist/client/components/static-generation-async-storage.external';
+import { createT } from './utils';
+
+const getLocaleCache = cache(() => {
+ const store = staticGenerationAsyncStorage.getStore();
+ const url = store?.urlPathname;
+
+ if (typeof url !== 'string') {
+ throw new Error('Invariant: urlPathname is not a string: ' + JSON.stringify(store, null, 2));
+ }
+
+ let locale = url.split('/')[1].split('?')[0];
+
+ if (locale === '') {
+ const cookie = (store?.incrementalCache?.requestHeaders?.['cookie'] as string)
+ ?.split(';')
+ .find(c => c.trim().startsWith('locale='))
+ ?.split('=')[1];
+
+ if (!cookie) {
+ throw new Error('Invariant: locale cookie not found');
+ }
+
+ locale = cookie;
+ }
+
+ return locale;
+});
+
+export function createI18n>(
+ locales: Locales,
+ config: I18nConfig = {},
+): CreateI18n {
+ const getI18n: GetI18n = async () => {
+ const locale = getLocaleCache();
+ const data = (await locales[locale]()).default;
+
+ return (key, ...params) => createT(locale, data, undefined, key, ...params);
+ };
+
+ const getScopedI18n: GetScopedI18n = async scope => {
+ const locale = getLocaleCache();
+ const data = (await locales[locale]()).default;
+
+ // @ts-expect-error - no types
+ return (key, ...params) => createT(locale, data, scope, key, ...params);
+ };
+
+ const generateI18nStaticParams: GenerateI18nStaticParams = () => {
+ return Object.keys(locales).map(locale => ({ [config.segmentName ?? SEGMENT_NAME]: locale }));
+ };
+
+ const getLocale: UseLocale = () => {
+ return getLocaleCache();
+ };
+
+ const useChangeLocale: UseChangeLocale = () => {
+ return () => {
+ throw new Error('Invariant: useChangeLocale only works in Client Components');
+ };
+ };
+
+ return {
+ getI18n,
+ getScopedI18n,
+ generateI18nStaticParams,
+ getLocale,
+ useChangeLocale,
+ };
+}
diff --git a/packages/next-international/src/app/app/types.ts b/packages/next-international/src/app/app/types.ts
new file mode 100644
index 0000000..3985ed7
--- /dev/null
+++ b/packages/next-international/src/app/app/types.ts
@@ -0,0 +1,60 @@
+import type { Keys, LocaleType, LocalesObject, Params, Scopes } from 'international-types';
+import type { ReactNode } from 'react';
+
+export type GetI18n = () => Promise<
+ >(key: Key, ...params: Params) => string | ReactNode[]
+>;
+
+export type GetScopedI18n = >(
+ scope: Scope,
+) => Promise<
+ >(key: Key, ...params: Params) => string | ReactNode[]
+>;
+
+export type GenerateI18nStaticParams = () => Array>;
+
+export type UseLocale = () => keyof Locales;
+
+type UseChangeLocaleConfig = {
+ /**
+ * If `true`, the search params will be preserved when changing the locale.
+ * Don't forget to **wrap the component in a `Suspense` boundary to avoid opting out the page from Static Rendering**.
+ *
+ * @see https://nextjs.org/docs/app/api-reference/functions/use-search-params#static-rendering
+ * @default false
+ */
+ preserveSearchParams?: boolean;
+};
+
+export type UseChangeLocale = (
+ config?: UseChangeLocaleConfig,
+) => (locale: keyof Locales) => void;
+
+export type CreateI18n = {
+ getI18n: GetI18n;
+ getScopedI18n: GetScopedI18n;
+ generateI18nStaticParams: GenerateI18nStaticParams;
+ getLocale: UseLocale;
+ useChangeLocale: UseChangeLocale;
+};
+
+export type I18nConfig = {
+ /**
+ * The name of the Next.js layout segment param that will be used to determine the locale in a client component.
+ *
+ * An app directory folder hierarchy that looks like `app/[locale]/products/[category]/[subCategory]/page.tsx` would be `locale`.
+ *
+ * @default locale
+ */
+ segmentName?: string;
+ /**
+ * If you are using a custom basePath inside `next.config.js`, you must also specify it here.
+ *
+ * @see https://nextjs.org/docs/app/api-reference/next-config-js/basePath
+ */
+ basePath?: string;
+ /**
+ * A locale to use if some keys aren't translated, to fallback to this locale instead of showing the translation key.
+ */
+ fallbackLocale?: Record;
+};
diff --git a/packages/next-international/src/app/app/utils.ts b/packages/next-international/src/app/app/utils.ts
new file mode 100644
index 0000000..f739da5
--- /dev/null
+++ b/packages/next-international/src/app/app/utils.ts
@@ -0,0 +1,106 @@
+import type { Keys, LocaleType, Params, Scopes } from 'international-types';
+import type { ReactNode } from 'react';
+import { cloneElement, isValidElement } from 'react';
+
+function flattenLocale(locale: Record, prefix = ''): Locale {
+ return Object.entries(locale).reduce(
+ (prev, [name, value]) => ({
+ ...prev,
+ ...(typeof value === 'string'
+ ? { [prefix + name]: value }
+ : flattenLocale(value as unknown as Locale, `${prefix}${name}.`)),
+ }),
+ {} as Locale,
+ );
+}
+
+type Cache = {
+ content: LocaleType;
+ pluralKeys: Set;
+};
+
+const LOCALE_CACHE = new Map();
+
+export function createT<
+ Locale extends LocaleType,
+ Scope extends Scopes | undefined,
+ Key extends Keys,
+ Param extends Params,
+>(locale: string, data: Locale, scope: Scope, key: Key, ...params: Param) {
+ let cache = LOCALE_CACHE.get(locale);
+
+ if (!cache) {
+ const content = flattenLocale(data);
+ const pluralKeys = new Set(
+ Object.keys(content)
+ .filter(key => key.includes('#'))
+ .map(key => key.split('#', 1)[0]),
+ );
+
+ const newCache = {
+ content,
+ pluralKeys,
+ };
+
+ cache = newCache;
+ LOCALE_CACHE.set(locale, newCache);
+ }
+
+ const { content, pluralKeys } = cache;
+ const pluralRules = new Intl.PluralRules(locale);
+
+ function getPluralKey(count: number) {
+ if (count === 0) return 'zero';
+ return pluralRules.select(count);
+ }
+
+ const paramObject = params[0];
+ let isPlural = false;
+
+ if (paramObject && 'count' in paramObject) {
+ const isPluralKey = scope ? pluralKeys.has(`${scope}.${key}`) : pluralKeys.has(key);
+
+ if (isPluralKey) {
+ // @ts-expect-error - no types
+ key = `${key}#${getPluralKey(paramObject.count)}` as Key;
+ isPlural = true;
+ }
+ }
+
+ let value = scope ? content[`${scope}.${key}`] : content[key];
+
+ if (!value && isPlural) {
+ const baseKey = key.split('#', 1)[0] as Key;
+ value = (content[`${baseKey}#other`] || key)?.toString();
+ } else {
+ value = (value || key)?.toString();
+ }
+
+ if (!paramObject) {
+ return value;
+ }
+
+ let isString = true;
+
+ const result = value?.split(/({[^}]*})/).map((part, index) => {
+ const match = part.match(/{(.*)}/);
+
+ if (match) {
+ const param = match[1] as keyof Locale;
+ // @ts-expect-error - no types
+ const paramValue = paramObject[param];
+
+ if (isValidElement(paramValue)) {
+ isString = false;
+ return cloneElement(paramValue, { key: `${String(param)}-${index}` });
+ }
+
+ return paramValue as ReactNode;
+ }
+
+ // if there's no match - it's not a variable and just a normal string
+ return part;
+ });
+
+ return isString ? result?.join('') : result;
+}
diff --git a/packages/next-international/src/app/middleware/index.ts b/packages/next-international/src/app/middleware/index.ts
index 427bad9..7d27853 100644
--- a/packages/next-international/src/app/middleware/index.ts
+++ b/packages/next-international/src/app/middleware/index.ts
@@ -1,114 +1,104 @@
-import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
+import type { NextMiddleware, NextRequest } from 'next/server';
+
+type I18nMiddlewareConfig = {
+ locales: Locales;
+ defaultLocale: Locales[number];
+ /**
+ * When a url is not prefixed with a locale, this setting determines whether the middleware should perform a *redirect* or *rewrite* to the default locale.
+ *
+ * **redirect**: `https://example.com/products` -> *redirect* to `https://example.com/en/products` -> client sees the locale in the url
+ *
+ * **rewrite**: `https://example.com/products` -> *rewrite* to `https://example.com/en/products` -> client doesn't see the locale in the url
+ *
+ * **rewriteDefault**: `https://example.com/products` -> use *rewrite* for the default locale, *redirect* for all other locales
+ *
+ * @default redirect
+ */
+ urlMappingStrategy?: 'redirect' | 'rewrite' | 'rewriteDefault';
+ /**
+ * Override the resolution of a locale from a `Request`, which by default will try to extract it from the `Accept-Language` header. This can be useful to force the use of a specific locale regardless of the `Accept-Language` header.
+ *
+ * @description This function will only be called if the user doesn't already have a `Next-Locale` cookie.
+ */
+ resolveLocaleFromRequest?: (request: NextRequest) => string | null;
+};
-import { LOCALE_COOKIE, LOCALE_HEADER } from '../../common/constants';
-import { warn } from '../../helpers/log';
-import type { I18nMiddlewareConfig } from '../../types';
-
-const DEFAULT_STRATEGY: NonNullable['urlMappingStrategy']> = 'redirect';
-
-export function createI18nMiddleware(config: I18nMiddlewareConfig) {
- return function I18nMiddleware(request: NextRequest) {
- const locale = localeFromRequest(config.locales, request, config.resolveLocaleFromRequest) ?? config.defaultLocale;
- const nextUrl = request.nextUrl;
-
- // If the locale from the request is not an handled locale, then redirect to the same URL with the default locale
- if (noLocalePrefix(config.locales, nextUrl.pathname)) {
- nextUrl.pathname = `/${locale}${nextUrl.pathname}`;
-
- const strategy = config.urlMappingStrategy ?? DEFAULT_STRATEGY;
- if (strategy === 'rewrite' || (strategy === 'rewriteDefault' && locale === config.defaultLocale)) {
- const response = NextResponse.rewrite(nextUrl);
- return addLocaleToResponse(request, response, locale);
- } else {
- if (!['redirect', 'rewriteDefault'].includes(strategy)) {
- warn(`Invalid urlMappingStrategy: ${strategy}. Defaulting to redirect.`);
- }
-
- const response = NextResponse.redirect(nextUrl);
- return addLocaleToResponse(request, response, locale);
- }
- }
-
- let response = NextResponse.next();
- const pathnameLocale = nextUrl.pathname.split('/', 2)?.[1];
-
- if (!pathnameLocale || config.locales.includes(pathnameLocale)) {
- // If the URL mapping strategy is set to 'rewrite' and the locale from the request doesn't match the locale in the pathname,
- // or if the URL mapping strategy is set to 'rewriteDefault' and the locale from the request doesn't match the locale in the pathname
- // or is the same as the default locale, then proceed with the following logic
- if (
- (config.urlMappingStrategy === 'rewrite' && pathnameLocale !== locale) ||
- (config.urlMappingStrategy === 'rewriteDefault' &&
- (pathnameLocale !== locale || pathnameLocale === config.defaultLocale))
- ) {
- // Remove the locale from the pathname
- const pathnameWithoutLocale = nextUrl.pathname.slice(pathnameLocale.length + 1);
-
- // Create a new URL without the locale in the pathname
- const newUrl = new URL(pathnameWithoutLocale || '/', request.url);
-
- // Preserve the original search parameters
- newUrl.search = nextUrl.search;
- response = NextResponse.redirect(newUrl);
- }
-
- return addLocaleToResponse(request, response, pathnameLocale ?? config.defaultLocale);
- }
-
- return response;
- };
-}
-
-/**
- * Retrieve `Next-Locale` header from request
- * and check if it is an handled locale.
- */
-function localeFromRequest(
- locales: Locales,
+function getLocaleFromRequest(
request: NextRequest,
- resolveLocaleFromRequest: NonNullable<
- I18nMiddlewareConfig['resolveLocaleFromRequest']
- > = defaultResolveLocaleFromRequest,
+ config: I18nMiddlewareConfig,
) {
- const locale = request.cookies.get(LOCALE_COOKIE)?.value ?? resolveLocaleFromRequest(request);
+ if (config.resolveLocaleFromRequest) {
+ return config.resolveLocaleFromRequest(request);
+ }
+
+ const locale = request.cookies.get('locale')?.value;
- if (!locale || !locales.includes(locale)) {
+ if (!locale || !config.locales.includes(locale)) {
return null;
}
return locale;
}
-/**
- * Default implementation of the `resolveLocaleFromRequest` function for the I18nMiddlewareConfig.
- * This function extracts the locale from the 'Accept-Language' header of the request.
- */
-const defaultResolveLocaleFromRequest: NonNullable['resolveLocaleFromRequest']> = request => {
- const header = request.headers.get('Accept-Language');
- const locale = header?.split(',', 1)?.[0]?.split('-', 1)?.[0];
- return locale ?? null;
-};
-
-/**
- * Returns `true` if the pathname does not start with an handled locale
- */
function noLocalePrefix(locales: readonly string[], pathname: string) {
return locales.every(locale => {
return !(pathname === `/${locale}` || pathname.startsWith(`/${locale}/`));
});
}
-/**
- * Add `X-Next-Locale` header and `Next-Locale` cookie to response
- *
- * **NOTE:** The cookie is only set if the locale is different from the one in the cookie
- */
-function addLocaleToResponse(request: NextRequest, response: NextResponse, locale: string) {
- response.headers.set(LOCALE_HEADER, locale);
+export function createI18nMiddleware(
+ middleware: NextMiddleware,
+ config: I18nMiddlewareConfig,
+): NextMiddleware {
+ return async (request, event) => {
+ let currentLocale = getLocaleFromRequest(request, config);
+
+ if (currentLocale === null) {
+ currentLocale = config.defaultLocale;
+ const response = await middleware(request, event);
+
+ if (response instanceof NextResponse) {
+ response.cookies.set('locale', currentLocale, { sameSite: 'strict' });
+ } else if (response instanceof Response) {
+ const cookies = response.headers.get('set-cookie') ?? '';
+ response.headers.set('set-cookie', `${cookies}; locale=${currentLocale}; SameSite=Strict`);
+ }
- if (request.cookies.get(LOCALE_COOKIE)?.value !== locale) {
- response.cookies.set(LOCALE_COOKIE, locale, { sameSite: 'strict' });
- }
- return response;
+ return response;
+ }
+
+ if (!config.urlMappingStrategy || config.urlMappingStrategy === 'redirect') {
+ const nextUrl = request.nextUrl;
+ const pathname = new URL(request.url).pathname;
+
+ if (noLocalePrefix(config.locales, pathname)) {
+ nextUrl.pathname = `/${currentLocale}${pathname}`;
+ const response = NextResponse.redirect(nextUrl);
+ return response;
+ }
+ }
+
+ if (
+ (config.urlMappingStrategy === 'rewriteDefault' && currentLocale === config.defaultLocale) ||
+ config.urlMappingStrategy === 'rewrite'
+ ) {
+ const nextUrl = request.nextUrl;
+
+ if (noLocalePrefix(config.locales, nextUrl.pathname)) {
+ nextUrl.pathname = `/${currentLocale}${nextUrl.pathname}`;
+ const response = NextResponse.rewrite(nextUrl);
+ return response;
+ }
+
+ const urlWithoutLocale = nextUrl.pathname.slice(currentLocale.length + 1);
+ const newUrl = new URL(urlWithoutLocale || '/', request.url);
+ newUrl.search = nextUrl.search;
+
+ const response = NextResponse.redirect(newUrl);
+ return response;
+ }
+
+ return middleware(request, event);
+ };
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 13d7d81..0fd7c42 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -127,6 +127,34 @@ importers:
specifier: ^5.2.2
version: 5.2.2
+ examples/app:
+ dependencies:
+ next:
+ specifier: 14.1.1
+ version: 14.1.1(@babel/core@7.23.2)(react-dom@18.2.0)(react@18.2.0)
+ next-international:
+ specifier: workspace:*
+ version: link:../../packages/next-international
+ react:
+ specifier: ^18
+ version: 18.2.0
+ react-dom:
+ specifier: ^18
+ version: 18.2.0(react@18.2.0)
+ devDependencies:
+ '@types/node':
+ specifier: ^20
+ version: 20.8.7
+ '@types/react':
+ specifier: ^18
+ version: 18.2.45
+ '@types/react-dom':
+ specifier: ^18
+ version: 18.0.6
+ typescript:
+ specifier: ^5
+ version: 5.2.2
+
examples/next-app:
dependencies:
next:
@@ -207,8 +235,8 @@ importers:
specifier: ^18.2.45
version: 18.2.45
next:
- specifier: ^14.0.4
- version: 14.0.4(@babel/core@7.23.2)(react-dom@18.2.0)(react@18.2.0)
+ specifier: ^14.1.1
+ version: 14.1.1(@babel/core@7.23.2)(react-dom@18.2.0)(react@18.2.0)
react:
specifier: ^18.2.0
version: 18.2.0
@@ -2373,6 +2401,10 @@ packages:
/@next/env@14.0.4:
resolution: {integrity: sha512-irQnbMLbUNQpP1wcE5NstJtbuA/69kRfzBrpAD7Gsn8zm/CY6YQYc3HQBz8QPxwISG26tIm5afvvVbu508oBeQ==}
+ dev: false
+
+ /@next/env@14.1.1:
+ resolution: {integrity: sha512-7CnQyD5G8shHxQIIg3c7/pSeYFeMhsNbpU/bmvH7ZnDql7mNRgg8O2JZrhrc/soFnfBnKP4/xXNiiSIPn2w8gA==}
/@next/eslint-plugin-next@13.5.6:
resolution: {integrity: sha512-ng7pU/DDsxPgT6ZPvuprxrkeew3XaRf4LAT4FabaEO/hAbvVx4P7wqnqdbTdDn1kgTvsI4tpIgT4Awn/m0bGbg==}
@@ -2386,6 +2418,15 @@ packages:
cpu: [arm64]
os: [darwin]
requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-darwin-arm64@14.1.1:
+ resolution: {integrity: sha512-yDjSFKQKTIjyT7cFv+DqQfW5jsD+tVxXTckSe1KIouKk75t1qZmj/mV3wzdmFb0XHVGtyRjDMulfVG8uCKemOQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
optional: true
/@next/swc-darwin-x64@14.0.4:
@@ -2394,6 +2435,15 @@ packages:
cpu: [x64]
os: [darwin]
requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-darwin-x64@14.1.1:
+ resolution: {integrity: sha512-KCQmBL0CmFmN8D64FHIZVD9I4ugQsDBBEJKiblXGgwn7wBCSe8N4Dx47sdzl4JAg39IkSN5NNrr8AniXLMb3aw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
optional: true
/@next/swc-linux-arm64-gnu@14.0.4:
@@ -2402,6 +2452,15 @@ packages:
cpu: [arm64]
os: [linux]
requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-linux-arm64-gnu@14.1.1:
+ resolution: {integrity: sha512-YDQfbWyW0JMKhJf/T4eyFr4b3tceTorQ5w2n7I0mNVTFOvu6CGEzfwT3RSAQGTi/FFMTFcuspPec/7dFHuP7Eg==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
optional: true
/@next/swc-linux-arm64-musl@14.0.4:
@@ -2410,6 +2469,15 @@ packages:
cpu: [arm64]
os: [linux]
requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-linux-arm64-musl@14.1.1:
+ resolution: {integrity: sha512-fiuN/OG6sNGRN/bRFxRvV5LyzLB8gaL8cbDH5o3mEiVwfcMzyE5T//ilMmaTrnA8HLMS6hoz4cHOu6Qcp9vxgQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
optional: true
/@next/swc-linux-x64-gnu@14.0.4:
@@ -2418,6 +2486,15 @@ packages:
cpu: [x64]
os: [linux]
requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-linux-x64-gnu@14.1.1:
+ resolution: {integrity: sha512-rv6AAdEXoezjbdfp3ouMuVqeLjE1Bin0AuE6qxE6V9g3Giz5/R3xpocHoAi7CufRR+lnkuUjRBn05SYJ83oKNQ==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
optional: true
/@next/swc-linux-x64-musl@14.0.4:
@@ -2426,6 +2503,15 @@ packages:
cpu: [x64]
os: [linux]
requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-linux-x64-musl@14.1.1:
+ resolution: {integrity: sha512-YAZLGsaNeChSrpz/G7MxO3TIBLaMN8QWMr3X8bt6rCvKovwU7GqQlDu99WdvF33kI8ZahvcdbFsy4jAFzFX7og==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
optional: true
/@next/swc-win32-arm64-msvc@14.0.4:
@@ -2434,6 +2520,15 @@ packages:
cpu: [arm64]
os: [win32]
requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-win32-arm64-msvc@14.1.1:
+ resolution: {integrity: sha512-1L4mUYPBMvVDMZg1inUYyPvFSduot0g73hgfD9CODgbr4xiTYe0VOMTZzaRqYJYBA9mana0x4eaAaypmWo1r5A==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+ requiresBuild: true
optional: true
/@next/swc-win32-ia32-msvc@14.0.4:
@@ -2442,6 +2537,15 @@ packages:
cpu: [ia32]
os: [win32]
requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-win32-ia32-msvc@14.1.1:
+ resolution: {integrity: sha512-jvIE9tsuj9vpbbXlR5YxrghRfMuG0Qm/nZ/1KDHc+y6FpnZ/apsgh+G6t15vefU0zp3WSpTMIdXRUsNl/7RSuw==}
+ engines: {node: '>= 10'}
+ cpu: [ia32]
+ os: [win32]
+ requiresBuild: true
optional: true
/@next/swc-win32-x64-msvc@14.0.4:
@@ -2450,6 +2554,15 @@ packages:
cpu: [x64]
os: [win32]
requiresBuild: true
+ dev: false
+ optional: true
+
+ /@next/swc-win32-x64-msvc@14.1.1:
+ resolution: {integrity: sha512-S6K6EHDU5+1KrBDLko7/c1MNy/Ya73pIAmvKeFwsF4RmBFJSO7/7YeD4FnZ4iBdzE69PpQ4sOMU9ORKeNuxe8A==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
optional: true
/@nodelib/fs.scandir@2.1.5:
@@ -3568,7 +3681,7 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
dependencies:
- caniuse-lite: 1.0.30001509
+ caniuse-lite: 1.0.30001587
electron-to-chromium: 1.4.447
node-releases: 2.0.12
update-browserslist-db: 1.0.11(browserslist@4.21.9)
@@ -3620,6 +3733,10 @@ packages:
/caniuse-lite@1.0.30001509:
resolution: {integrity: sha512-2uDDk+TRiTX5hMcUYT/7CSyzMZxjfGu0vAUjS2g0LSD8UoXOv0LtpH4LxGMemsiPq6LCVIUjNwVM0erkOkGCDA==}
+ dev: false
+
+ /caniuse-lite@1.0.30001587:
+ resolution: {integrity: sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA==}
/ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
@@ -5359,6 +5476,7 @@ packages:
/glob-to-regexp@0.4.1:
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
+ dev: false
/glob@7.1.6:
resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==}
@@ -7253,6 +7371,45 @@ packages:
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
+ dev: false
+
+ /next@14.1.1(@babel/core@7.23.2)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-McrGJqlGSHeaz2yTRPkEucxQKe5Zq7uPwyeHNmJaZNY4wx9E9QdxmTp310agFRoMuIYgQrCrT3petg13fSVOww==}
+ engines: {node: '>=18.17.0'}
+ hasBin: true
+ peerDependencies:
+ '@opentelemetry/api': ^1.1.0
+ react: ^18.2.0
+ react-dom: ^18.2.0
+ sass: ^1.3.0
+ peerDependenciesMeta:
+ '@opentelemetry/api':
+ optional: true
+ sass:
+ optional: true
+ dependencies:
+ '@next/env': 14.1.1
+ '@swc/helpers': 0.5.2
+ busboy: 1.6.0
+ caniuse-lite: 1.0.30001587
+ graceful-fs: 4.2.11
+ postcss: 8.4.31
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ styled-jsx: 5.1.1(@babel/core@7.23.2)(react@18.2.0)
+ optionalDependencies:
+ '@next/swc-darwin-arm64': 14.1.1
+ '@next/swc-darwin-x64': 14.1.1
+ '@next/swc-linux-arm64-gnu': 14.1.1
+ '@next/swc-linux-arm64-musl': 14.1.1
+ '@next/swc-linux-x64-gnu': 14.1.1
+ '@next/swc-linux-x64-musl': 14.1.1
+ '@next/swc-win32-arm64-msvc': 14.1.1
+ '@next/swc-win32-ia32-msvc': 14.1.1
+ '@next/swc-win32-x64-msvc': 14.1.1
+ transitivePeerDependencies:
+ - '@babel/core'
+ - babel-plugin-macros
/nextra-theme-docs@2.13.2(next@14.0.4)(nextra@2.13.2)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-yE4umXaImp1/kf/sFciPj2+EFrNSwd9Db26hi98sIIiujzGf3+9eUgAz45vF9CwBw50FSXxm1QGRcY+slQ4xQQ==}
@@ -9222,6 +9379,7 @@ packages:
dependencies:
glob-to-regexp: 0.4.1
graceful-fs: 4.2.11
+ dev: false
/web-namespaces@2.0.1:
resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==}