Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Handle inconsistency in Next.js when using usePathname with custom prefixes, localePrefix: 'as-needed' and static rendering #1573

Merged
merged 1 commit into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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