Skip to content

Commit

Permalink
fix: Handle inconsistency in Next.js when using usePathname with cu…
Browse files Browse the repository at this point in the history
…stom prefixes, `localePrefix: 'as-needed'` and static rendering (#1573)

Fixes #1568
  • Loading branch information
amannn authored Nov 25, 2024
1 parent 6cc710a commit 20fd0f0
Show file tree
Hide file tree
Showing 8 changed files with 66 additions and 19 deletions.
8 changes: 4 additions & 4 deletions packages/next-intl/.size-limit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,25 @@ const config: SizeLimitConfig = [
name: "import {createSharedPathnamesNavigation} from 'next-intl/navigation' (react-client)",
path: 'dist/production/navigation.react-client.js',
import: '{createSharedPathnamesNavigation}',
limit: '4.045 KB'
limit: '4.125 KB'
},
{
name: "import {createLocalizedPathnamesNavigation} from 'next-intl/navigation' (react-client)",
path: 'dist/production/navigation.react-client.js',
import: '{createLocalizedPathnamesNavigation}',
limit: '4.045 KB'
limit: '4.115 KB'
},
{
name: "import {createNavigation} from 'next-intl/navigation' (react-client)",
path: 'dist/production/navigation.react-client.js',
import: '{createNavigation}',
limit: '4.055 KB'
limit: '4.115 KB'
},
{
name: "import {createSharedPathnamesNavigation} from 'next-intl/navigation' (react-server)",
path: 'dist/production/navigation.react-server.js',
import: '{createSharedPathnamesNavigation}',
limit: '16.795 KB'
limit: '16.805 KB'
},
{
name: "import {createLocalizedPathnamesNavigation} from 'next-intl/navigation' (react-server)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export default function createLocalizedPathnamesNavigation<
}

function usePathname(): keyof AppPathnames {
const pathname = useBasePathname(config.localePrefix);
const pathname = useBasePathname(config);
const locale = useTypedLocale();

// @ts-expect-error -- Mirror the behavior from Next.js, where `null` is returned when `usePathname` is used outside of Next, but the types indicate that a string is always returned.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,28 @@ describe("localePrefix: 'as-needed'", () => {
});
});

describe("localePrefix: 'as-needed', custom `prefixes`", () => {
const {usePathname} = createNavigation({
defaultLocale,
locales,
localePrefix: {
mode: 'as-needed',
prefixes: {
en: '/uk'
}
}
});
const renderPathname = getRenderPathname(usePathname);

// https://github.com/vercel/next.js/issues/73085
it('is tolerant when a locale is used in the pathname for the default locale', () => {
mockCurrentLocale('en');
mockLocation({pathname: '/en/about'});
renderPathname();
screen.getByText('/about');
});
});

describe("localePrefix: 'as-needed', with `basePath` and `domains`", () => {
const {useRouter} = createNavigation({
locales,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default function createNavigation<
function usePathname(): [AppPathnames] extends [never]
? string
: keyof AppPathnames {
const pathname = useBasePathname(config.localePrefix);
const pathname = useBasePathname(config);
const locale = useTypedLocale();

// @ts-expect-error -- Mirror the behavior from Next.js, where `null` is returned when `usePathname` is used outside of Next, but the types indicate that a string is always returned.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ export default function createSharedPathnamesNavigation<
}

function usePathname(): string {
const result = useBasePathname(localePrefix);
const result = useBasePathname({
localePrefix,
defaultLocale: routing?.defaultLocale
});
// @ts-expect-error -- Mirror the behavior from Next.js, where `null` is returned when `usePathname` is used outside of Next, but the types indicate that a string is always returned.
return result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ function mockPathname(pathname: string) {
}

function Component() {
const pathname = useBasePathname({
// The mode is not used, only the absence of
// `prefixes` is relevant for this test suite
mode: 'as-needed'
return useBasePathname({
localePrefix: {
// The mode is not used, only the absence of
// `prefixes` is relevant for this test suite
mode: 'as-needed'
}
});
return <>{pathname}</>;
}

describe('unprefixed routing', () => {
Expand Down
29 changes: 23 additions & 6 deletions packages/next-intl/src/navigation/react-client/useBasePathname.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Locales
} from '../../routing/types';
import {
getLocaleAsPrefix,
getLocalePrefix,
hasPathnamePrefixed,
unprefixPathname
Expand All @@ -15,7 +16,10 @@ import {
export default function useBasePathname<
AppLocales extends Locales,
AppLocalePrefixMode extends LocalePrefixMode
>(localePrefix: LocalePrefixConfigVerbose<AppLocales, AppLocalePrefixMode>) {
>(config: {
localePrefix: LocalePrefixConfigVerbose<AppLocales, AppLocalePrefixMode>;
defaultLocale?: AppLocales[number];
}) {
// The types aren't entirely correct here. Outside of Next.js
// `useParams` can be called, but the return type is `null`.

Expand All @@ -34,12 +38,25 @@ export default function useBasePathname<
return useMemo(() => {
if (!pathname) return pathname;

const prefix = getLocalePrefix(locale, localePrefix);
let unlocalizedPathname = pathname;

const prefix = getLocalePrefix(locale, config.localePrefix);
const isPathnamePrefixed = hasPathnamePrefixed(prefix, pathname);
const unlocalizedPathname = isPathnamePrefixed
? unprefixPathname(pathname, prefix)
: pathname;

if (isPathnamePrefixed) {
unlocalizedPathname = unprefixPathname(pathname, prefix);
} else if (
config.localePrefix.mode === 'as-needed' &&
config.defaultLocale === locale &&
config.localePrefix.prefixes
) {
// Workaround for https://github.com/vercel/next.js/issues/73085
const localeAsPrefix = getLocaleAsPrefix(locale);
if (hasPathnamePrefixed(localeAsPrefix, pathname)) {
unlocalizedPathname = unprefixPathname(pathname, localeAsPrefix);
}
}

return unlocalizedPathname;
}, [locale, localePrefix, pathname]);
}, [config.defaultLocale, config.localePrefix, locale, pathname]);
}
6 changes: 5 additions & 1 deletion packages/next-intl/src/shared/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,14 @@ export function getLocalePrefix<
(localePrefix.mode !== 'never' && localePrefix.prefixes?.[locale]) ||
// We return a prefix even if `mode: 'never'`. It's up to the consumer
// to decide to use it or not.
'/' + locale
getLocaleAsPrefix(locale)
);
}

export function getLocaleAsPrefix(locale: string) {
return '/' + locale;
}

export function templateToRegex(template: string): RegExp {
const regexPattern = template
// Replace optional catchall ('[[...slug]]')
Expand Down

0 comments on commit 20fd0f0

Please sign in to comment.