chore: upgrade next-intl to version 4.0.0 and update type definitions

- Update `next-intl` dependency from version 3.26.5 to 4.0.0
- Modify global type definitions to include new `Locale` and `Messages` types
- Refactor various components and pages to use `Locale` type for params
- Enhance internationalization handling by integrating `hasLocale` checks
- Clean up imports and ensure consistent usage of `next-intl` features
This commit is contained in:
javayhu 2025-03-13 00:23:37 +08:00
parent d7389eef47
commit df1c75f56a
28 changed files with 86 additions and 58 deletions

18
global.d.ts vendored
View File

@ -1,8 +1,14 @@
import en from './messages/en.json';
import { routing } from '@/i18n/routing';
import messages from './messages/en.json';
type Messages = typeof en;
declare global {
// Use type safe message keys with `next-intl`
interface IntlMessages extends Messages {}
/**
* next-intl 4.0.0
*
* https://github.com/amannn/next-intl/blob/main/examples/example-app-router/global.d.ts
*/
declare module 'next-intl' {
interface AppConfig {
Locale: (typeof routing.locales)[number];
Messages: typeof messages;
}
}

View File

@ -51,7 +51,7 @@
"mdast-util-toc": "^7.1.0",
"motion": "^12.4.3",
"next": "15.2.1",
"next-intl": "^3.26.5",
"next-intl": "^4.0.0",
"next-plausible": "^3.12.4",
"next-safe-action": "^7.10.4",
"next-themes": "^0.4.4",

37
pnpm-lock.yaml generated
View File

@ -132,8 +132,8 @@ importers:
specifier: 15.2.1
version: 15.2.1(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
next-intl:
specifier: ^3.26.5
version: 3.26.5(next@15.2.1(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)
specifier: ^4.0.0
version: 4.0.0(next@15.2.1(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(typescript@5.7.3)
next-plausible:
specifier: ^3.12.4
version: 3.12.4(next@15.2.1(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@ -2231,6 +2231,9 @@ packages:
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
'@schummar/icu-type-parser@1.21.5':
resolution: {integrity: sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==}
'@selderee/plugin-htmlparser2@0.11.0':
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
@ -3641,11 +3644,15 @@ packages:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'}
next-intl@3.26.5:
resolution: {integrity: sha512-EQlCIfY0jOhRldiFxwSXG+ImwkQtDEfQeSOEQp6ieAGSLWGlgjdb/Ck/O7wMfC430ZHGeUKVKax8KGusTPKCgg==}
next-intl@4.0.0:
resolution: {integrity: sha512-l+I1PLAFrjzYzrc340n1vssDJ7pP1gtYT1jOWlRWIHkyrPdyosEIHPC+LiqJP4vWvWtCZzzqTn9AaBF+x5Ja8g==}
peerDependencies:
next: ^10.0.0 || ^11.0.0 || ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0
next: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
typescript: ^5.0.0
peerDependenciesMeta:
typescript:
optional: true
next-plausible@3.12.4:
resolution: {integrity: sha512-cD3+ixJxf8yBYvsideTxqli3fvrB7R4BXcvsNJz8Sm2X1QN039WfiXjCyNWkub4h5++rRs6fHhchUMnOuJokcg==}
@ -4518,6 +4525,11 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
use-intl@4.0.0:
resolution: {integrity: sha512-/fmC7haEMVNa0isXGRGUir56fD4I9LRnOgbeBmji+bow6U8pE7WD+2X2sjqh+0h3yJ0T36PA6JXZ6PlVeRyt8w==}
peerDependencies:
react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0
use-sidecar@1.1.3:
resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
engines: {node: '>=10'}
@ -6293,6 +6305,8 @@ snapshots:
dependencies:
react: 19.0.0
'@schummar/icu-type-parser@1.21.5': {}
'@selderee/plugin-htmlparser2@0.11.0':
dependencies:
domhandler: 5.0.3
@ -8054,13 +8068,15 @@ snapshots:
negotiator@1.0.0: {}
next-intl@3.26.5(next@15.2.1(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0):
next-intl@4.0.0(next@15.2.1(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(typescript@5.7.3):
dependencies:
'@formatjs/intl-localematcher': 0.5.10
negotiator: 1.0.0
next: 15.2.1(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
react: 19.0.0
use-intl: 3.26.5(react@19.0.0)
use-intl: 4.0.0(react@19.0.0)
optionalDependencies:
typescript: 5.7.3
next-plausible@3.12.4(next@15.2.1(@babel/core@7.24.5)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
@ -9109,6 +9125,13 @@ snapshots:
intl-messageformat: 10.7.15
react: 19.0.0
use-intl@4.0.0(react@19.0.0):
dependencies:
'@formatjs/fast-memoize': 2.2.6
'@schummar/icu-type-parser': 1.21.5
intl-messageformat: 10.7.15
react: 19.0.0
use-sidecar@1.1.3(@types/react@19.0.9)(react@19.0.0):
dependencies:
detect-node-es: 1.1.0

View File

@ -14,7 +14,7 @@ import Testimonials from '@/components/blocks/testimonials/testimonials';
import { getTranslations } from 'next-intl/server';
interface HomePageProps {
params: Promise<{ locale: string }>;
params: Promise<{ locale: Locale }>;
}
export default async function HomePage(props: HomePageProps) {

View File

@ -3,7 +3,7 @@ import CallToAction2 from '@/components/blocks/call-to-action/call-to-action-2';
import CallToAction3 from '@/components/blocks/call-to-action/call-to-action-3';
interface CallToActionPageProps {
params: Promise<{ locale: string }>;
params: Promise<{ locale: Locale }>;
}
/**

View File

@ -6,7 +6,7 @@ import Content5 from '@/components/blocks/content/content-5';
import Content6 from '@/components/blocks/content/content-6';
interface ContentPageProps {
params: Promise<{ locale: string }>;
params: Promise<{ locale: Locale }>;
}
export default async function ContentPage(props: ContentPageProps) {

View File

@ -1,7 +1,7 @@
import FAQs from '@/components/blocks/faq/faqs';
interface FAQPageProps {
params: Promise<{ locale: string }>;
params: Promise<{ locale: Locale }>;
}
/**

View File

@ -8,7 +8,7 @@ import Features8 from '@/components/blocks/features/features-8';
import Features9 from '@/components/blocks/features/features-9';
interface FeaturesPageProps {
params: Promise<{ locale: string }>;
params: Promise<{ locale: Locale }>;
}
export default async function FeaturesPage(props: FeaturesPageProps) {

View File

@ -4,7 +4,7 @@ import HeroSection3 from '@/components/blocks/hero/hero-section-3';
import HeroSection4 from '@/components/blocks/hero/hero-section-4';
interface HeroPageProps {
params: Promise<{ locale: string }>;
params: Promise<{ locale: Locale }>;
}
/**

View File

@ -5,7 +5,7 @@ import PricingComparator from '@/components/pricing-comparator';
import { getTranslations } from 'next-intl/server';
interface PricingPageProps {
params: Promise<{ locale: string }>;
params: Promise<{ locale: Locale }>;
}
/**

View File

@ -4,7 +4,7 @@ import Stats3 from '@/components/blocks/stats/stats-3';
import Stats4 from '@/components/blocks/stats/stats-4';
interface StatsPageProps {
params: Promise<{ locale: string }>;
params: Promise<{ locale: Locale }>;
}
/**

View File

@ -5,7 +5,7 @@ import Testimonials5 from '@/components/blocks/testimonials/testimonials-5';
import Testimonials6 from '@/components/blocks/testimonials/testimonials-6';
interface TestimonialsPageProps {
params: Promise<{ locale: string }>;
params: Promise<{ locale: Locale }>;
}
/**

View File

@ -13,7 +13,7 @@ import { getBaseUrl } from '@/lib/urls/get-base-url';
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string; locale: string }>;
params: Promise<{ slug: string; locale: Locale }>;
}): Promise<Metadata | undefined> {
const resolvedParams = await params;
const { slug, locale } = resolvedParams;

View File

@ -5,7 +5,7 @@ import PricingComparator from '@/components/pricing-comparator';
import { getTranslations } from 'next-intl/server';
interface PricingPageProps {
params: Promise<{ locale: string }>;
params: Promise<{ locale: Locale }>;
}
export default async function PricingPage(props: PricingPageProps) {

View File

@ -4,8 +4,7 @@ import { routing } from '@/i18n/routing';
import { cn } from '@/lib/utils';
import { GeistMono } from 'geist/font/mono';
import { GeistSans } from 'geist/font/sans';
import { NextIntlClientProvider } from 'next-intl';
import { getMessages } from 'next-intl/server';
import { Locale, hasLocale, NextIntlClientProvider } from 'next-intl';
import { notFound } from 'next/navigation';
import { ReactNode } from 'react';
import { Toaster } from 'sonner';
@ -15,7 +14,7 @@ import '@/styles/globals.css';
interface LocaleLayoutProps {
children: ReactNode;
params: Promise<{ locale: string }>;
params: Promise<{ locale: Locale }>;
}
/**
@ -32,13 +31,10 @@ export default async function LocaleLayout({
const { locale } = await params;
// Ensure that the incoming `locale` is valid
if (!routing.locales.includes(locale as any)) {
if (!hasLocale(routing.locales, locale)) {
notFound();
}
// Providing all messages to the client side
const messages = await getMessages();
return (
<html lang={locale} suppressHydrationWarning>
<body
@ -53,7 +49,7 @@ export default async function LocaleLayout({
GeistMono.variable
)}
>
<NextIntlClientProvider messages={messages}>
<NextIntlClientProvider>
<Providers>
{children}

View File

@ -8,9 +8,9 @@ import {
SelectValue,
} from '@/components/ui/select';
import { useLocalePathname, useLocaleRouter } from '@/i18n/navigation';
import { DEFAULT_LOCALE, Locale, LOCALE_LIST, routing } from '@/i18n/routing';
import { DEFAULT_LOCALE, LOCALE_LIST, routing } from '@/i18n/routing';
import { useLocaleStore } from '@/stores/locale-store';
import { useLocale } from 'next-intl';
import { Locale, useLocale } from 'next-intl';
import { useParams } from 'next/navigation';
import { useEffect, useTransition } from 'react';

View File

@ -8,10 +8,10 @@ import {
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useLocalePathname, useLocaleRouter } from '@/i18n/navigation';
import { Locale, LOCALE_LIST, routing } from '@/i18n/routing';
import { LOCALE_LIST, routing } from '@/i18n/routing';
import { useLocaleStore } from '@/stores/locale-store';
import { Languages } from 'lucide-react';
import { useLocale, useTranslations } from 'next-intl';
import { Locale, useLocale, useTranslations } from 'next-intl';
import { useParams } from 'next/navigation';
import { useEffect, useTransition } from 'react';

View File

@ -1,11 +1,8 @@
import deepmerge from 'deepmerge';
import { routing } from './routing';
import { Locale, Messages } from 'next-intl';
import type messages from '../../messages/en.json';
export type Messages = typeof messages;
export const importLocale = async (locale: string): Promise<Messages> => {
const importLocale = async (locale: Locale): Promise<Messages> => {
return (await import(`../../messages/${locale}.json`)).default as Messages;
};
@ -16,7 +13,7 @@ export const importLocale = async (locale: string): Promise<Messages> => {
* https://next-intl.dev/docs/usage/configuration#messages
*/
export const getMessagesForLocale = async (
locale: string
locale: Locale
): Promise<Messages> => {
const localeMessages = await importLocale(locale);
if (locale === routing.defaultLocale) {

View File

@ -5,6 +5,7 @@ import { routing } from './routing';
* Navigation APIs
*
* https://next-intl.dev/docs/routing/navigation
* https://github.com/amannn/next-intl/blob/main/examples/example-app-router/src/i18n/navigation.ts
*/
export const {
Link: LocaleLink,

View File

@ -1,3 +1,4 @@
import { hasLocale } from 'next-intl';
import { getRequestConfig } from 'next-intl/server';
import { getMessagesForLocale } from './messages';
import { routing } from './routing';
@ -11,15 +12,17 @@ import { routing } from './routing';
* The first component to use internationalization will call the function defined with getRequestConfig.
*
* https://next-intl.dev/docs/usage/configuration
* https://github.com/amannn/next-intl/blob/main/examples/example-app-router/src/i18n/request.ts
*/
export default getRequestConfig(async ({ requestLocale }) => {
// This typically corresponds to the `[locale]` segment
let locale = await requestLocale;
let requested = await requestLocale;
// Ensure that the incoming `locale` is valid
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
// https://next-intl.dev/blog/next-intl-4-0?s#strictly-typed-locale
const locale = hasLocale(routing.locales, requested)
? requested
: routing.defaultLocale;
// https://next-intl.dev/docs/usage/configuration#messages
// If you have incomplete messages for a given locale and would like to use messages

View File

@ -14,6 +14,7 @@ export const LOCALE_COOKIE_NAME = 'NEXT_LOCALE';
* Next.js internationalized routing
*
* https://next-intl.dev/docs/routing
* https://github.com/amannn/next-intl/blob/main/examples/example-app-router/src/i18n/routing.ts
*/
export const routing = defineRouting({
// A list of all locales that are supported
@ -46,4 +47,4 @@ export const routing = defineRouting({
});
// export type Pathnames = keyof typeof routing.pathnames;
export type Locale = (typeof routing.locales)[number];
// export type Locale = (typeof routing.locales)[number];

View File

@ -6,7 +6,7 @@ import { allPages } from 'content-collections';
* @param locale The locale to get the page for
* @returns The custom page or undefined if not found
*/
export async function getCustomPage(type: string, locale: string) {
export async function getCustomPage(type: string, locale: Locale) {
// Find page with matching slug and locale
const page = allPages.find(
(page) => page.slugAsParams === `${type}` && page.locale === locale

View File

@ -5,7 +5,7 @@ import { allReleases } from 'content-collections';
* @param locale The locale to get releases for
* @returns An array of releases sorted by date (newest first)
*/
export async function getReleases(locale: string) {
export async function getReleases(locale: Locale) {
// Find all published releases with matching locale
const releases = allReleases.filter(
(release) => release.published && release.locale === locale

View File

@ -1,8 +1,9 @@
import { getWebsiteInfo } from '@/config';
import { Locale, LOCALE_COOKIE_NAME, routing } from '@/i18n/routing';
import { LOCALE_COOKIE_NAME, routing } from '@/i18n/routing';
import { createTranslator } from '@/i18n/translator';
import { type ClassValue, clsx } from 'clsx';
import { parse as parseCookies } from 'cookie';
import { Locale } from 'next-intl';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
@ -19,7 +20,7 @@ export function cn(...inputs: ClassValue[]) {
export function createTitle(
title: string,
addSuffix: boolean = true,
locale: string = 'en'
locale: Locale = routing.defaultLocale
): string {
// Create a simple translator function for default values
const t = createTranslator((key: string) => key);

View File

@ -1,9 +1,9 @@
import type { Messages } from '@/i18n/messages';
import { getMessagesForLocale } from '@/i18n/messages';
import { Locale, routing } from '@/i18n/routing';
import { routing } from '@/i18n/routing';
import { mailTemplates } from '@/mail/emails';
import { sendEmail } from '@/mail/provider/resend';
import { render } from '@react-email/render';
import { Locale, Messages } from 'next-intl';
type Template = keyof typeof mailTemplates;

View File

@ -1,5 +1,4 @@
import type { Locale } from '@/i18n/routing';
import type { Messages } from '@/i18n/messages';
import { Locale, Messages } from 'next-intl';
export interface EmailParams {
to: string;

View File

@ -1,7 +1,8 @@
import { MetadataRoute } from 'next';
import { routing, Locale } from '@/i18n/routing';
import { routing } from '@/i18n/routing';
import { getLocalePathname } from '@/i18n/navigation';
import { getBaseUrl } from './lib/urls/get-base-url';
import { Locale } from 'next-intl';
/**
* https://github.com/javayhu/cnblocks/blob/main/app/sitemap.ts

View File

@ -1,4 +1,4 @@
import { Locale } from '@/i18n/routing';
import { Locale } from 'next-intl';
import { create } from 'zustand';
interface LocaleState {