Repository: tatethurston/nextjs-routes (ts)

Issue:
Typed locales if i18n is configured
The standard type for `locale`, for example in `const { locale } = useRouter()`is always `string|undefined`.

`undefined` is only applicable, if i18n is not configured.

What do you mean, is it in the scope of the package that `locale` and `locales` are typed properly related to the configuration in the next.config.js?

A config which looks like this...

```typescript
{
module.exports = {
  i18n: {
    localeDetection: true,
    defaultLocale: 'de-DE',
    locales: ['de-AT', 'de-DE', 'de-CH', 'en-FR', 'en-IT', 'en-GB', 'en-US']
  }
}

```

... would result into a type override which looks like this:


```typescript
declare module 'next/router' {
  import type { NextRouter as NextBaseRouter } from 'next/dist/client/router'
  export * from 'next/router'
  export { default } from 'next/router'

  type NextRouter = Omit<NextBaseRouter, 'locale'> & {
    locale: ['de-AT', 'de-DE', 'de-CH', 'en-FR', 'en-IT', 'en-GB', 'en-US']
  }

  export type { NextRouter }

  export function useRouter(): NextRouter
}
```

This is just a quick draft. We need to dig deeper into the package as, unfortunately, `locale` is typed on many code pieces in the routing types.

Looking forward for feedback about the idea. Maybe an v1 feature as well...

Pull request:
typed locales when i18n is configured
fixes #58

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fb5cfd8..fa0a810 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,20 @@
 # Changelog
 
+## 0.1.4
+
+- `nextjs-routes` now generates route types for [Nextjs i18n configuration](https://nextjs.org/docs/advanced-features/i18n-routing). Eg the following next config:
+
+```js
+module.exports = withRoutes({
+  i18n: {
+    defaultLocale: "de-DE",
+    locales: ["de-DE", "en-FR", "en-US"],
+  },
+});
+```
+
+Will make `locale` typed as `'de-DE' | 'en-FR' | 'en-US'` for `Link` and `useRouter`.
+
 ## 0.1.3
 
 - `nextjs-routes` [pageExtensions](https://nextjs.org/docs/api-reference/next.config.js/custom-page-extensions) has been updated to respect multiple extensions such as `.page.tsx`. In `0.1.2`, only single extensions `.tsx` were respected. This is now identical behavior to Next.js.
diff --git a/README.md b/README.md
index 8a36abe..5c47514 100644
--- a/README.md
+++ b/README.md
@@ -200,6 +200,23 @@ export const getServerSideProps: GetServerSideProps = async (context) => {
 };
 ```
 
+### Internationalization (i18n)
+
+`nextjs-routes` refines `Link` and `useRouter` based on your [Nextjs i18n configuration](https://nextjs.org/docs/advanced-features/i18n-routing).
+
+The following `next.config.js`:
+
+```js
+module.exports = withRoutes({
+  i18n: {
+    defaultLocale: "de-DE",
+    locales: ["de-DE", "en-FR", "en-US"],
+  },
+});
+```
+
+Will type `Link` and `useRouter`'s `locale` as `'de-DE' | 'en-FR' | 'en-US'`. All other i18n properties (`defaultLocale`, `domainLocales` and `locales`) are also typed.
+
 ## Configuration
 
 You can pass the following options to `withRoutes` in your `next.config.js`:
diff --git a/examples/src-pages/.eslintrc.json b/examples/intl/.eslintrc.json
similarity index 100%
rename from examples/src-pages/.eslintrc.json
rename to examples/intl/.eslintrc.json
diff --git a/examples/src-pages/.gitignore b/examples/intl/.gitignore
similarity index 100%
rename from examples/src-pages/.gitignore
rename to examples/intl/.gitignore
diff --git a/examples/src-pages/README.md b/examples/intl/README.md
similarity index 100%
rename from examples/src-pages/README.md
rename to examples/intl/README.md
diff --git a/examples/src-pages/next-env.d.ts b/examples/intl/next-env.d.ts
similarity index 100%
rename from examples/src-pages/next-env.d.ts
rename to examples/intl/next-env.d.ts
diff --git a/examples/src-pages/next.config.mjs b/examples/intl/next.config.mjs
similarity index 62%
rename from examples/src-pages/next.config.mjs
rename to examples/intl/next.config.mjs
index e78af1c..5646569 100644
--- a/examples/src-pages/next.config.mjs
+++ b/examples/intl/next.config.mjs
@@ -2,6 +2,10 @@ import withRoutes from "nextjs-routes/config";
 
 const nextConfig = {
   reactStrictMode: true,
+  i18n: {
+    locales: ["en-US", "fr", "nl-NL"],
+    defaultLocale: "en-US",
+  },
 };
 
 export default withRoutes()(nextConfig);
diff --git a/examples/src-pages/nextjs-routes.d.ts b/examples/intl/nextjs-routes.d.ts
similarity index 70%
rename from examples/src-pages/nextjs-routes.d.ts
rename to examples/intl/nextjs-routes.d.ts
index 32b161f..5a41861 100644
--- a/examples/src-pages/nextjs-routes.d.ts
+++ b/examples/intl/nextjs-routes.d.ts
@@ -5,10 +5,8 @@
 // prettier-ignore
 declare module "nextjs-routes" {
   export type Route =
-    | { pathname: "/api/hello"; query?: Query | undefined }
-    | { pathname: "/bars/[bar]"; query: Query<{ "bar": string }> }
-    | { pathname: "/foos/[foo]"; query: Query<{ "foo": string }> }
-    | { pathname: "/"; query?: Query | undefined };
+    | { pathname: "/"; query?: Query | undefined }
+    | { pathname: "/store"; query?: Query | undefined };
 
   type Query<Params = {}> = Params & { [key: string]: string | string[] | undefined };
 
@@ -35,8 +33,13 @@ declare module "next/link" {
 
   type RouteOrQuery = Route | { query?: { [key: string]: string | string[] | undefined } };
 
-  export interface LinkProps extends Omit<NextLinkProps, "href"> {
+  export interface LinkProps extends Omit<NextLinkProps, "href" | "locale"> {
     href: RouteOrQuery;
+    locale?: 
+      | "en-US"
+      | "fr"
+      | "nl-NL"
+      | false;
   }
 
   declare function Link(
@@ -61,25 +64,49 @@ declare module "next/router" {
   export * from "next/dist/client/router";
   export { default } from "next/dist/client/router";
 
-  type TransitionOptions = Parameters<Router["push"]>[2];
+  type NextTransitionOptions = NonNullable<Parameters<Router["push"]>[2]>;
 
-  type RouteOrQuery = Route | { query: { [key: string]: string | string[] | undefined } };
+  interface TransitionOptions extends Omit<NextTransitionOptions, 'locale'> {
+    locale?: 
+      | "en-US"
+      | "fr"
+      | "nl-NL"
+      | false;
+  };
+
+  type RouteOrQuery = 
+    | Route
+    | { query: { [key: string]: string | string[] | undefined } };
 
   export interface NextRouter<P extends Route["pathname"] = Route["pathname"]>
-    extends Omit<Router, "push" | "replace"> {
+    extends Omit<
+      Router,
+      "push" | "replace" | "locale" | "locales" | "defaultLocale" | "domainLocales"
+    > {
+    defaultLocale: "en-US";
+    domainLocales?: undefined;
+    locale: 
+      | "en-US"
+      | "fr"
+      | "nl-NL";
+    locales: [
+      "en-US",
+      "fr",
+      "nl-NL"
+    ];
     pathname: P;
-    route: P;
-    query: RoutedQuery<P>;
     push(
       url: RouteOrQuery,
       as?: string,
       options?: TransitionOptions
     ): Promise<boolean>;
+    query: RoutedQuery<P>;
     replace(
       url: RouteOrQuery,
       as?: string,
       options?: TransitionOptions
     ): Promise<boolean>;
+    route: P;
   }
 
   export function useRouter<P extends Route["pathname"]>(): NextRouter<P>;
diff --git a/examples/src-pages/package-lock.json b/examples/intl/package-lock.json
similarity index 100%
rename from examples/src-pages/package-lock.json
rename to examples/intl/package-lock.json
diff --git a/examples/src-pages/package.json b/examples/intl/package.json
similarity index 95%
rename from examples/src-pages/package.json
rename to examples/intl/package.json
index 495d24d..bf07c56 100644
--- a/examples/src-pages/package.json
+++ b/examples/intl/package.json
@@ -1,5 +1,5 @@
 {
-  "name": "src-pages",
+  "name": "intl",
   "version": "0.0.1",
   "private": true,
   "scripts": {
diff --git a/examples/src-pages/public/favicon.ico b/examples/intl/public/favicon.ico
similarity index 100%
rename from examples/src-pages/public/favicon.ico
rename to examples/intl/public/favicon.ico
diff --git a/examples/src-pages/public/vercel.svg b/examples/intl/public/vercel.svg
similarity index 100%
rename from examples/src-pages/public/vercel.svg
rename to examples/intl/public/vercel.svg
diff --git a/examples/src-pages/src/pages/_app.tsx b/examples/intl/src/pages/_app.tsx
similarity index 100%
rename from examples/src-pages/src/pages/_app.tsx
rename to examples/intl/src/pages/_app.tsx
diff --git a/examples/intl/src/pages/index.tsx b/examples/intl/src/pages/index.tsx
new file mode 100644
index 0000000..479f54d
--- /dev/null
+++ b/examples/intl/src/pages/index.tsx
@@ -0,0 +1,23 @@
+import type { NextPage } from "next";
+import Head from "next/head";
+import Link from "next/link";
+import { useRouter } from "next/router";
+
+const Home: NextPage = () => {
+  const { locale } = useRouter();
+  console.log({ locale });
+  return (
+    <>
+      <Head>
+        <title>Create Next App</title>
+        <meta name="description" content="Generated by create next app" />
+        <link rel="icon" href="/favicon.ico" />
+      </Head>
+      <main>
+        <Link href={{ pathname: "/store" }}>Store</Link>
+      </main>
+    </>
+  );
+};
+
+export default Home;
diff --git a/examples/src-pages/src/pages/index.tsx b/examples/intl/src/pages/store.tsx
similarity index 79%
rename from examples/src-pages/src/pages/index.tsx
rename to examples/intl/src/pages/store.tsx
index 9ba85f3..911cd7e 100644
--- a/examples/src-pages/src/pages/index.tsx
+++ b/examples/intl/src/pages/store.tsx
@@ -11,9 +11,7 @@ const Home: NextPage = () => {
         <link rel="icon" href="/favicon.ico" />
       </Head>
       <main>
-        <Link href={{ pathname: "/foos/[foo]", query: { foo: "Foo" } }}>
-          Foo
-        </Link>
+        <Link href={{ pathname: "/" }}>Home</Link>
       </main>
     </>
   );
diff --git a/examples/src-pages/tsconfig.json b/examples/intl/tsconfig.json
similarity index 100%
rename from examples/src-pages/tsconfig.json
rename to examples/intl/tsconfig.json
diff --git a/examples/src-pages/src/pages/api/hello.ts b/examples/src-pages/src/pages/api/hello.ts
deleted file mode 100644
index 74a3605..0000000
--- a/examples/src-pages/src/pages/api/hello.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
-import type { NextApiRequest, NextApiResponse } from "next";
-
-type Data = {
-  name: string;
-};
-
-export default function handler(
-  req: NextApiRequest,
-  res: NextApiResponse<Data>
-) {
-  res.status(200).json({ name: "John Doe" });
-}
diff --git a/examples/src-pages/src/pages/bars/[bar].tsx b/examples/src-pages/src/pages/bars/[bar].tsx
deleted file mode 100644
index 591c27c..0000000
--- a/examples/src-pages/src/pages/bars/[bar].tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { NextPage, GetStaticProps } from "next";
-import type { RoutedQuery } from "nextjs-routes";
-
-type BarPageProps = { bar: string };
-
-export const getStaticProps: GetStaticProps<
-  BarPageProps,
-  RoutedQuery<"/bars/[bar]">
-> = (context) => {
-  const { bar } = context.params!;
-
-  return {
-    props: { bar },
-  };
-};
-
-const Bar: NextPage<BarPageProps> = ({ bar }) => {
-  return <div>Bar: {bar}</div>;
-};
-
-export default Bar;
diff --git a/examples/src-pages/src/pages/foos/[foo].tsx b/examples/src-pages/src/pages/foos/[foo].tsx
deleted file mode 100644
index 945fb61..0000000
--- a/examples/src-pages/src/pages/foos/[foo].tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { NextPage } from "next";
-import { useRouter } from "next/router";
-
-const Foo: NextPage = () => {
-  const router = useRouter<"/foos/[foo]">();
-  const { foo } = router.query;
-  return <div>Foo: {foo}</div>;
-};
-
-export default Foo;
diff --git a/examples/typescript/types/nextjs-routes.d.ts b/examples/typescript/types/nextjs-routes.d.ts
index 32b161f..5b01784 100644
--- a/examples/typescript/types/nextjs-routes.d.ts
+++ b/examples/typescript/types/nextjs-routes.d.ts
@@ -35,8 +35,9 @@ declare module "next/link" {
 
   type RouteOrQuery = Route | { query?: { [key: string]: string | string[] | undefined } };
 
-  export interface LinkProps extends Omit<NextLinkProps, "href"> {
+  export interface LinkProps extends Omit<NextLinkProps, "href" | "locale"> {
     href: RouteOrQuery;
+    locale?: false;
   }
 
   declare function Link(
@@ -61,25 +62,38 @@ declare module "next/router" {
   export * from "next/dist/client/router";
   export { default } from "next/dist/client/router";
 
-  type TransitionOptions = Parameters<Router["push"]>[2];
+  type NextTransitionOptions = NonNullable<Parameters<Router["push"]>[2]>;
 
-  type RouteOrQuery = Route | { query: { [key: string]: string | string[] | undefined } };
+  interface TransitionOptions extends Omit<NextTransitionOptions, 'locale'> {
+    locale?: false;
+  };
+
+  type RouteOrQuery = 
+    | Route
+    | { query: { [key: string]: string | string[] | undefined } };
 
   export interface NextRouter<P extends Route["pathname"] = Route["pathname"]>
-    extends Omit<Router, "push" | "replace"> {
+    extends Omit<
+      Router,
+      "push" | "replace" | "locale" | "locales" | "defaultLocale" | "domainLocales"
+    > {
+    defaultLocale?: undefined;
+    domainLocales?: undefined;
+    locale?: undefined;
+    locales?: undefined;
     pathname: P;
-    route: P;
-    query: RoutedQuery<P>;
     push(
       url: RouteOrQuery,
       as?: string,
       options?: TransitionOptions
     ): Promise<boolean>;
+    query: RoutedQuery<P>;
     replace(
       url: RouteOrQuery,
       as?: string,
       options?: TransitionOptions
     ): Promise<boolean>;
+    route: P;
   }
 
   export function useRouter<P extends Route["pathname"]>(): NextRouter<P>;
diff --git a/src/core.ts b/src/core.ts
index d2584cf..a14023b 100644
--- a/src/core.ts
+++ b/src/core.ts
@@ -1,4 +1,5 @@
 import { existsSync, mkdirSync, writeFileSync } from "fs";
+import { I18NConfig } from "next/dist/server/config-shared.js";
 import { join } from "path";
 import { findFiles, getPagesDirectory } from "./utils.js";
 
@@ -89,7 +90,13 @@ function getQueryInterface(
   return [`{ ${keys} }`, requiredKeys];
 }
 
-function generate(routes: Route[]): string {
+function generate(routes: Route[], config: NextJSRoutesOptions): string {
+  const i18n = config.i18n ?? {
+    defaultLocale: "",
+    domains: [],
+    localeDetection: false,
+    locales: [],
+  };
   const unknownQueryParamsType =
     "{ [key: string]: string | string[] | undefined }";
   return `\
@@ -140,8 +147,15 @@ declare module "next/link" {
 
   type RouteOrQuery = Route | { query?: ${unknownQueryParamsType} };
 
-  export interface LinkProps extends Omit<NextLinkProps, "href"> {
+  export interface LinkProps extends Omit<NextLinkProps, "href" | "locale"> {
     href: RouteOrQuery;
+    locale?: ${
+      !i18n.locales.length
+        ? "false"
+        : `\n      | ${[...i18n.locales.map((x) => `"${x}"`), false].join(
+            "\n      | "
+          )}`
+    };
   }
 
   declare function Link(
@@ -166,25 +180,54 @@ declare module "next/router" {
   export * from "next/dist/client/router";
   export { default } from "next/dist/client/router";
 
-  type TransitionOptions = Parameters<Router["push"]>[2];
+  type NextTransitionOptions = NonNullable<Parameters<Router["push"]>[2]>;
+
+  interface TransitionOptions extends Omit<NextTransitionOptions, 'locale'> {
+    locale?: ${
+      !i18n.locales.length
+        ? "false"
+        : `\n      | ${[...i18n.locales.map((x) => `"${x}"`), false].join(
+            "\n      | "
+          )}`
+    };
+  };
 
-  type RouteOrQuery = Route | { query: ${unknownQueryParamsType} };
+  type RouteOrQuery = 
+    | Route
+    | { query: ${unknownQueryParamsType} };
 
   export interface NextRouter<P extends Route["pathname"] = Route["pathname"]>
-    extends Omit<Router, "push" | "replace"> {
+    extends Omit<
+      Router,
+      "push" | "replace" | "locale" | "locales" | "defaultLocale" | "domainLocales"
+    > {
+    defaultLocale${
+      i18n.defaultLocale ? `: "${i18n.defaultLocale}"` : "?: undefined"
+    };
+    domainLocales${
+      i18n.domains?.length ? `: ${print(i18n.domains, 4)}` : "?: undefined"
+    };
+    locale${
+      !i18n.locales.length
+        ? "?: undefined"
+        : `: \n      | ${i18n.locales.map((x) => `"${x}"`).join("\n      | ")}`
+    };
+    locales${
+      i18n.locales.length ? `: ${print(i18n.locales, 4)}` : "?: undefined"
+    };
     pathname: P;
-    route: P;
-    query: RoutedQuery<P>;
     push(
       url: RouteOrQuery,
       as?: string,
       options?: TransitionOptions
     ): Promise<boolean>;
+    query: RoutedQuery<P>;
     replace(
       url: RouteOrQuery,
       as?: string,
       options?: TransitionOptions
     ): Promise<boolean>;
+    route: P;
   }
 
   export function useRouter<P extends Route["pathname"]>(): NextRouter<P>;
@@ -192,6 +235,12 @@ declare module "next/router" {
 `;
 }
 
+function print(x: unknown, indent: number): string {
+  return JSON.stringify(x, undefined, 2)
+    .split("\n")
+    .join("\n" + " ".repeat(indent));
+}
+
 export const logger: Pick<Console, "error"> = {
   error: (str: string) => console.error("[nextjs-routes] " + str),
 };
@@ -211,6 +260,12 @@ interface NextJSRoutesOptions {
    * https://nextjs.org/docs/api-reference/next.config.js/custom-page-extensions
    */
   pageExtensions?: string[];
+  /**
+   * Internationalization configuration
+   *
+   * @see [Internationalization docs](https://nextjs.org/docs/advanced-features/i18n-routing)
+   */
+  i18n?: I18NConfig | null;
 }
 
 export function writeNextjsRoutes(options: NextJSRoutesOptions): void {
@@ -230,7 +285,7 @@ export function writeNextjsRoutes(options: NextJSRoutesOptions): void {
     return opts.pageExtensions.some((ext) => file.endsWith(ext));
   });
   const routes = nextRoutes(files, opts.pagesDirectory);
-  const generated = generate(routes);
+  const generated = generate(routes, opts);
   writeFileSync(outputFilepath, generated);
 }
 


Test patch:
diff --git a/e2e/nextjs-routes.d.ts b/e2e/nextjs-routes.d.ts
index 945b731..bfa3748 100644
--- a/e2e/nextjs-routes.d.ts
+++ b/e2e/nextjs-routes.d.ts
@@ -33,8 +33,9 @@ declare module "next/link" {
 
   type RouteOrQuery = Route | { query?: { [key: string]: string | string[] | undefined } };
 
-  export interface LinkProps extends Omit<NextLinkProps, "href"> {
+  export interface LinkProps extends Omit<NextLinkProps, "href" | "locale"> {
     href: RouteOrQuery;
+    locale?: false;
   }
 
   declare function Link(
@@ -59,25 +60,38 @@ declare module "next/router" {
   export * from "next/dist/client/router";
   export { default } from "next/dist/client/router";
 
-  type TransitionOptions = Parameters<Router["push"]>[2];
+  type NextTransitionOptions = NonNullable<Parameters<Router["push"]>[2]>;
 
-  type RouteOrQuery = Route | { query: { [key: string]: string | string[] | undefined } };
+  interface TransitionOptions extends Omit<NextTransitionOptions, 'locale'> {
+    locale?: false;
+  };
+
+  type RouteOrQuery = 
+    | Route
+    | { query: { [key: string]: string | string[] | undefined } };
 
   export interface NextRouter<P extends Route["pathname"] = Route["pathname"]>
-    extends Omit<Router, "push" | "replace"> {
+    extends Omit<
+      Router,
+      "push" | "replace" | "locale" | "locales" | "defaultLocale" | "domainLocales"
+    > {
+    defaultLocale?: undefined;
+    domainLocales?: undefined;
+    locale?: undefined;
+    locales?: undefined;
     pathname: P;
-    route: P;
-    query: RoutedQuery<P>;
     push(
       url: RouteOrQuery,
       as?: string,
       options?: TransitionOptions
     ): Promise<boolean>;
+    query: RoutedQuery<P>;
     replace(
       url: RouteOrQuery,
       as?: string,
       options?: TransitionOptions
     ): Promise<boolean>;
+    route: P;
   }
 
   export function useRouter<P extends Route["pathname"]>(): NextRouter<P>;
diff --git a/e2e/typetest.tsx b/e2e/typetest.tsx
index 82433b0..ac28bdd 100644
--- a/e2e/typetest.tsx
+++ b/e2e/typetest.tsx
@@ -42,7 +42,7 @@ function expectType<T>(_value: T) {}
   shallow
   passHref
   prefetch
-  locale="en"
+  locale={false}
   legacyBehavior={false}
   onClick={console.log}
   onMouseEnter={console.log}
@@ -107,7 +107,7 @@ router.push({ query: { foo: ["foo", "bar"] } });
 // Unaugmented options
 router.push({ query: {} }, undefined, {
   shallow: true,
-  locale: "en",
+  locale: false,
   scroll: true,
 });
 // @ts-expect-error shallow typo
@@ -142,7 +142,7 @@ router.replace({ query: { foo: ["bar", "baz"] } });
 // Unaugmented options
 router.replace({ query: {} }, undefined, {
   shallow: true,
-  locale: "en",
+  locale: false,
   scroll: true,
 });
 // @ts-expect-error shallow typo
diff --git a/src/__snapshots__/core.test.ts.snap b/src/__snapshots__/core.test.ts.snap
index d62b056..7538ce2 100644
--- a/src/__snapshots__/core.test.ts.snap
+++ b/src/__snapshots__/core.test.ts.snap
@@ -1,6 +1,6 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`route generation no routes 1`] = `
+exports[`route generation configuration i18n 1`] = `
 [
   [
     "nextjs-routes.d.ts",
@@ -11,7 +11,7 @@ exports[`route generation no routes 1`] = `
 // prettier-ignore
 declare module "nextjs-routes" {
   export type Route =
-    never;
+    | { pathname: "/"; query?: Query | undefined };
 
   type Query<Params = {}> = Params & { [key: string]: string | string[] | undefined };
 
@@ -38,8 +38,13 @@ declare module "next/link" {
 
   type RouteOrQuery = Route | { query?: { [key: string]: string | string[] | undefined } };
 
-  export interface LinkProps extends Omit<NextLinkProps, "href"> {
+  export interface LinkProps extends Omit<NextLinkProps, "href" | "locale"> {
     href: RouteOrQuery;
+    locale?: 
+      | "en-US"
+      | "fr"
+      | "nl-NL"
+      | false;
   }
 
   declare function Link(
@@ -64,25 +69,62 @@ declare module "next/router" {
   export * from "next/dist/client/router";
   export { default } from "next/dist/client/router";
 
-  type TransitionOptions = Parameters<Router["push"]>[2];
+  type NextTransitionOptions = NonNullable<Parameters<Router["push"]>[2]>;
+
+  interface TransitionOptions extends Omit<NextTransitionOptions, 'locale'> {
+    locale?: 
+      | "en-US"
+      | "fr"
+      | "nl-NL"
+      | false;
+  };
 
-  type RouteOrQuery = Route | { query: { [key: string]: string | string[] | undefined } };
+  type RouteOrQuery = 
+    | Route
+    | { query: { [key: string]: string | string[] | undefined } };
 
   export interface NextRouter<P extends Route["pathname"] = Route["pathname"]>
-    extends Omit<Router, "push" | "replace"> {
+    extends Omit<
+      Router,
+      "push" | "replace" | "locale" | "locales" | "defaultLocale" | "domainLocales"
+    > {
+    defaultLocale: "en-US";
+    domainLocales: [
+      {
+        "domain": "example.nl",
+        "defaultLocale": "nl-NL",
+        "locales": [
+          "nl-BE"
+        ]
+      },
+      {
+        "domain": "example.fr",
+        "defaultLocale": "fr",
+        "http": true
+      }
+    ];
+    locale: 
+      | "en-US"
+      | "fr"
+      | "nl-NL";
+    locales: [
+      "en-US",
+      "fr",
+      "nl-NL"
+    ];
     pathname: P;
-    route: P;
-    query: RoutedQuery<P>;
     push(
       url: RouteOrQuery,
       as?: string,
       options?: TransitionOptions
     ): Promise<boolean>;
+    query: RoutedQuery<P>;
     replace(
       url: RouteOrQuery,
       as?: string,
       options?: TransitionOptions
     ): Promise<boolean>;
+    route: P;
   }
 
   export function useRouter<P extends Route["pathname"]>(): NextRouter<P>;
@@ -92,7 +134,7 @@ declare module "next/router" {
 ]
 `;
 
-exports[`route generation outDir 1`] = `
+exports[`route generation configuration outDir 1`] = `
 [
   [
     "src/nextjs-routes.d.ts",
@@ -130,8 +172,9 @@ declare module "next/link" {
 
   type RouteOrQuery = Route | { query?: { [key: string]: string | string[] | undefined } };
 
-  export interface LinkProps extends Omit<NextLinkProps, "href"> {
+  export interface LinkProps extends Omit<NextLinkProps, "href" | "locale"> {
     href: RouteOrQuery;
+    locale?: false;
   }
 
   declare function Link(
@@ -156,25 +199,38 @@ declare module "next/router" {
   export * from "next/dist/client/router";
   export { default } from "next/dist/client/router";
 
-  type TransitionOptions = Parameters<Router["push"]>[2];
+  type NextTransitionOptions = NonNullable<Parameters<Router["push"]>[2]>;
 
-  type RouteOrQuery = Route | { query: { [key: string]: string | string[] | undefined } };
+  interface TransitionOptions extends Omit<NextTransitionOptions, 'locale'> {
+    locale?: false;
+  };
+
+  type RouteOrQuery = 
+    | Route
+    | { query: { [key: string]: string | string[] | undefined } };
 
   export interface NextRouter<P extends Route["pathname"] = Route["pathname"]>
-    extends Omit<Router, "push" | "replace"> {
+    extends Omit<
+      Router,
+      "push" | "replace" | "locale" | "locales" | "defaultLocale" | "domainLocales"
+    > {
+    defaultLocale?: undefined;
+    domainLocales?: undefined;
+    locale?: undefined;
+    locales?: undefined;
     pathname: P;
-    route: P;
-    query: RoutedQuery<P>;
     push(
       url: RouteOrQuery,
       as?: string,
       options?: TransitionOptions
     ): Promise<boolean>;
+    query: RoutedQuery<P>;
     replace(
       url: RouteOrQuery,
       as?: string,
       options?: TransitionOptions
     ): Promise<boolean>;
+    route: P;
   }
 
   export function useRouter<P extends Route["pathname"]>(): NextRouter<P>;
@@ -184,7 +240,7 @@ declare module "next/router" {
 ]
 `;
 
-exports[`route generation pageExtensions configured 1`] = `
+exports[`route generation configuration pageExtensions configured 1`] = `
 [
   [
     "nextjs-routes.d.ts",
@@ -224,8 +280,9 @@ declare module "next/link" {
 
   type RouteOrQuery = Route | { query?: { [key: string]: string | string[] | undefined } };
 
-  export interface LinkProps extends Omit<NextLinkProps, "href"> {
+  export interface LinkProps extends Omit<NextLinkProps, "href" | "locale"> {
     href: RouteOrQuery;
+    locale?: false;
   }
 
   declare function Link(
@@ -250,25 +307,38 @@ declare module "next/router" {
   export * from "next/dist/client/router";
   export { default } from "next/dist/client/router";
 
-  type TransitionOptions = Parameters<Router["push"]>[2];
+  type NextTransitionOptions = NonNullable<Parameters<Router["push"]>[2]>;
+
+  interface TransitionOptions extends Omit<NextTransitionOptions, 'locale'> {
+    locale?: false;
+  };
 
-  type RouteOrQuery = Route | { query: { [key: string]: string | string[] | undefined } };
+  type RouteOrQuery = 
+    | Route
+    | { query: { [key: string]: string | string[] | undefined } };
 
   export interface NextRouter<P extends Route["pathname"] = Route["pathname"]>
-    extends Omit<Router, "push" | "replace"> {
+    extends Omit<
+      Router,
+      "push" | "replace" | "locale" | "locales" | "defaultLocale" | "domainLocales"
+    > {
+    defaultLocale?: undefined;
+    domainLocales?: undefined;
+    locale?: undefined;
+    locales?: undefined;
     pathname: P;
-    route: P;
-    query: RoutedQuery<P>;
     push(
       url: RouteOrQuery,
       as?: string,
       options?: TransitionOptions
     ): Promise<boolean>;
+    query: RoutedQuery<P>;
     replace(
       url: RouteOrQuery,
       as?: string,
       options?: TransitionOptions
     ): Promise<boolean>;
+    route: P;
   }
 
   export function useRouter<P extends Route["pathname"]>(): NextRouter<P>;
@@ -278,7 +348,7 @@ declare module "next/router" {
 ]
 `;
 
-exports[`route generation pageExtensions default 1`] = `
+exports[`route generation configuration pageExtensions default 1`] = `
 [
   [
     "nextjs-routes.d.ts",
@@ -316,8 +386,9 @@ declare module "next/link" {
 
   type RouteOrQuery = Route | { query?: { [key: string]: string | string[] | undefined } };
 
-  export interface LinkProps extends Omit<NextLinkProps, "href"> {
+  export interface LinkProps extends Omit<NextLinkProps, "href" | "locale"> {
     href: RouteOrQuery;
+    locale?: false;
   }
 
   declare function Link(
@@ -342,25 +413,144 @@ declare module "next/router" {
   export * from "next/dist/client/router";
   export { default } from "next/dist/client/router";
 
-  type TransitionOptions = Parameters<Router["push"]>[2];
+  type NextTransitionOptions = NonNullable<Parameters<Router["push"]>[2]>;
+
+  interface TransitionOptions extends Omit<NextTransitionOptions, 'locale'> {
+    locale?: false;
+  };
 
-  type RouteOrQuery = Route | { query: { [key: string]: string | string[] | undefined } };
+  type RouteOrQuery = 
+    | Route
+    | { query: { [key: string]: string | string[] | undefined } };
 
   export interface NextRouter<P extends Route["pathname"] = Route["pathname"]>
-    extends Omit<Router, "push" | "replace"> {
+    extends Omit<
+      Router,
+      "push" | "replace" | "locale" | "locales" | "defaultLocale" | "domainLocales"
+    > {
+    defaultLocale?: undefined;
+    domainLocales?: undefined;
+    locale?: undefined;
+    locales?: undefined;
     pathname: P;
-    route: P;
+    push(
+      url: RouteOrQuery,
+      as?: string,
+      options?: TransitionOptions
+    ): Promise<boolean>;
     query: RoutedQuery<P>;
+    replace(
+      url: RouteOrQuery,
+      as?: string,
+      options?: TransitionOptions
+    ): Promise<boolean>;
+    route: P;
+  }
+
+  export function useRouter<P extends Route["pathname"]>(): NextRouter<P>;
+}
+",
+  ],
+]
+`;
+
+exports[`route generation no routes 1`] = `
+[
+  [
+    "nextjs-routes.d.ts",
+    "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+// This file will be automatically regenerated when your Next.js server is running.
+/* eslint-disable */
+
+// prettier-ignore
+declare module "nextjs-routes" {
+  export type Route =
+    never;
+
+  type Query<Params = {}> = Params & { [key: string]: string | string[] | undefined };
+
+  type QueryForPathname = {
+    [K in Route as K["pathname"]]: Exclude<K["query"], undefined>;
+  };
+
+  export type RoutedQuery<P extends Route["pathname"]> = QueryForPathname[P];
+
+  /**
+   * A typesafe utility function for generating paths in your application.
+   *
+   * route({ pathname: '/foos/[foo]', query: { foo: 'bar' }}) will produce '/foos/bar'.
+   */
+  export declare function route(r: Route): string;
+}
+
+// prettier-ignore
+declare module "next/link" {
+  import type { Route } from "nextjs-routes";
+  import type { LinkProps as NextLinkProps } from "next/dist/client/link";
+  import type { PropsWithChildren, MouseEventHandler } from "react";
+  export * from "next/dist/client/link";
+
+  type RouteOrQuery = Route | { query?: { [key: string]: string | string[] | undefined } };
+
+  export interface LinkProps extends Omit<NextLinkProps, "href" | "locale"> {
+    href: RouteOrQuery;
+    locale?: false;
+  }
+
+  declare function Link(
+    props: PropsWithChildren<LinkProps>
+  ): DetailedReactHTMLElement<
+    {
+      onMouseEnter?: MouseEventHandler<Element> | undefined;
+      onClick: MouseEventHandler;
+      href?: string | undefined;
+      ref?: any;
+    },
+    HTMLElement
+  >;
+
+  export default Link;
+}
+
+// prettier-ignore
+declare module "next/router" {
+  import type { Route, RoutedQuery } from "nextjs-routes";
+  import type { NextRouter as Router } from "next/dist/client/router";
+  export * from "next/dist/client/router";
+  export { default } from "next/dist/client/router";
+
+  type NextTransitionOptions = NonNullable<Parameters<Router["push"]>[2]>;
+
+  interface TransitionOptions extends Omit<NextTransitionOptions, 'locale'> {
+    locale?: false;
+  };
+
+  type RouteOrQuery = 
+    | Route
+    | { query: { [key: string]: string | string[] | undefined } };
+
+  export interface NextRouter<P extends Route["pathname"] = Route["pathname"]>
+    extends Omit<
+      Router,
+      "push" | "replace" | "locale" | "locales" | "defaultLocale" | "domainLocales"
+    > {
+    defaultLocale?: undefined;
+    domainLocales?: undefined;
+    locale?: undefined;
+    locales?: undefined;
+    pathname: P;
     push(
       url: RouteOrQuery,
       as?: string,
       options?: TransitionOptions
     ): Promise<boolean>;
+    query: RoutedQuery<P>;
     replace(
       url: RouteOrQuery,
       as?: string,
       options?: TransitionOptions
     ): Promise<boolean>;
+    route: P;
   }
 
   export function useRouter<P extends Route["pathname"]>(): NextRouter<P>;
@@ -425,8 +615,9 @@ declare module "next/link" {
 
   type RouteOrQuery = Route | { query?: { [key: string]: string | string[] | undefined } };
 
-  export interface LinkProps extends Omit<NextLinkProps, "href"> {
+  export interface LinkProps extends Omit<NextLinkProps, "href" | "locale"> {
     href: RouteOrQuery;
+    locale?: false;
   }
 
   declare function Link(
@@ -451,25 +642,38 @@ declare module "next/router" {
   export * from "next/dist/client/router";
   export { default } from "next/dist/client/router";
 
-  type TransitionOptions = Parameters<Router["push"]>[2];
+  type NextTransitionOptions = NonNullable<Parameters<Router["push"]>[2]>;
+
+  interface TransitionOptions extends Omit<NextTransitionOptions, 'locale'> {
+    locale?: false;
+  };
 
-  type RouteOrQuery = Route | { query: { [key: string]: string | string[] | undefined } };
+  type RouteOrQuery = 
+    | Route
+    | { query: { [key: string]: string | string[] | undefined } };
 
   export interface NextRouter<P extends Route["pathname"] = Route["pathname"]>
-    extends Omit<Router, "push" | "replace"> {
+    extends Omit<
+      Router,
+      "push" | "replace" | "locale" | "locales" | "defaultLocale" | "domainLocales"
+    > {
+    defaultLocale?: undefined;
+    domainLocales?: undefined;
+    locale?: undefined;
+    locales?: undefined;
     pathname: P;
-    route: P;
-    query: RoutedQuery<P>;
     push(
       url: RouteOrQuery,
       as?: string,
       options?: TransitionOptions
     ): Promise<boolean>;
+    query: RoutedQuery<P>;
     replace(
       url: RouteOrQuery,
       as?: string,
       options?: TransitionOptions
     ): Promise<boolean>;
+    route: P;
   }
 
   export function useRouter<P extends Route["pathname"]>(): NextRouter<P>;
diff --git a/src/core.test.ts b/src/core.test.ts
index 4ba7ddf..558dc86 100644
--- a/src/core.test.ts
+++ b/src/core.test.ts
@@ -75,31 +75,57 @@ describe("route generation", () => {
     expect(writeFileSyncMock.mock.calls).toMatchSnapshot();
   });
 
-  describe("pageExtensions", () => {
-    it("default", () => {
-      findFilesMock.mockReturnValueOnce(["pages/404.ts", "pages/404.md"]);
-      writeNextjsRoutes({ pagesDirectory: "pages" });
+  describe("configuration", () => {
+    describe("pageExtensions", () => {
+      it("default", () => {
+        findFilesMock.mockReturnValueOnce(["pages/404.ts", "pages/404.md"]);
+        writeNextjsRoutes({ pagesDirectory: "pages" });
+        expect(writeFileSyncMock.mock.calls).toMatchSnapshot();
+      });
+
+      it("configured", () => {
+        findFilesMock.mockReturnValueOnce([
+          "pages/404.ts",
+          "pages/index.md",
+          "pages/foo/index.page.tsx",
+          "pages/foo/index.test.tsx",
+        ]);
+        writeNextjsRoutes({
+          pagesDirectory: "pages",
+          pageExtensions: ["ts", "md", "page.tsx"],
+        });
+        expect(writeFileSyncMock.mock.calls).toMatchSnapshot();
+      });
+    });
+
+    it("outDir", () => {
+      findFilesMock.mockReturnValueOnce(["pages/404.ts"]);
+      writeNextjsRoutes({ pagesDirectory: "pages", outDir: "src" });
       expect(writeFileSyncMock.mock.calls).toMatchSnapshot();
     });
 
-    it("configured", () => {
-      findFilesMock.mockReturnValueOnce([
-        "pages/404.ts",
-        "pages/index.md",
-        "pages/foo/index.page.tsx",
-        "pages/foo/index.test.tsx",
-      ]);
+    it("i18n", () => {
+      findFilesMock.mockReturnValueOnce(["pages/index.ts"]);
       writeNextjsRoutes({
         pagesDirectory: "pages",
-        pageExtensions: ["ts", "md", "page.tsx"],
+        i18n: {
+          locales: ["en-US", "fr", "nl-NL"],
+          defaultLocale: "en-US",
+          domains: [
+            {
+              domain: "example.nl",
+              defaultLocale: "nl-NL",
+              locales: ["nl-BE"],
+            },
+            {
+              domain: "example.fr",
+              defaultLocale: "fr",
+              http: true,
+            },
+          ],
+        },
       });
       expect(writeFileSyncMock.mock.calls).toMatchSnapshot();
     });
   });
-
-  it("outDir", () => {
-    findFilesMock.mockReturnValueOnce(["pages/404.ts"]);
-    writeNextjsRoutes({ pagesDirectory: "pages", outDir: "src" });
-    expect(writeFileSyncMock.mock.calls).toMatchSnapshot();
-  });
 });