Skip to content

How to prevent exposing all translations to the client in App Router with next-intl #1847

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

Closed
3 tasks done
VSyak11 opened this issue Apr 17, 2025 · 4 comments
Closed
3 tasks done
Labels
bug Something isn't working Stale unconfirmed Needs triage.

Comments

@VSyak11
Copy link

VSyak11 commented Apr 17, 2025

Description

When using [email protected] with Next.js 15.2.5 and App Router, I noticed that all translations (including admin-only namespaces) are exposed in the client-rendered page, even if only one namespace is used.

This could be a security or privacy concern in cases where admin or restricted strings are included in translation files, but still visible to unauthorized users who inspect the client source.

Verifications

Mandatory reproduction URL

https://github.com/amannn/next-intl-bug-repro-app-router

Reproduction description

Steps to reproduce:

  1. Set up [email protected] with App Router in Next.js 15

  2. Place all translations into a single file (e.g. ru.json) like this:

{
"General": {
"hello": "Привет"
},
"Admin": {
"ban": "Забанить"
}
}

  1. Use only "General" namespace on the home page via const t = useTranslations("General")

  2. Visit the homepage and inspect the generated HTML or client-rendered JS

  3. 🔍 All translation data, including "Admin", is still visible in the serialized page data, even though it is never used.

Expected behaviour

Only the used namespace (e.g. "General") should be included in the client bundle or serialized page data. Other unused namespaces (e.g. "Admin") should not be exposed unless explicitly requested.

@VSyak11 VSyak11 added bug Something isn't working unconfirmed Needs triage. labels Apr 17, 2025
@ChristianIvicevic
Copy link

ChristianIvicevic commented Apr 17, 2025

By default next-intl just passes down the entire dictionary file into the context making it available throughout the client-side code as it doesn't possibly have any way of distinguishing which keys should be passed on to the client or skipped. If you're concerned about this behavior you are always able to manually pass a set of variables down to the client. It is something I do like this:

export const getClientMessages = cache(async () => (await import('@/i18n/en.client.json')).default)

// ...

export default async function Layout({ children }: { children: ReactNode }) {
	// ...
	const messages = await getClientMessages()

	return (
		<html lang={locale}>
			<body>
				<Providers messages={messages} locale={locale}>
					{children}
				</Providers>
			</body>
		</html>
	)
}

// ...

export function Providers({
	children,
	messages,
	locale,
}: {
	children: ReactNode
	messages: AbstractIntlMessages
	locale: string
}) {
	return (
		<NextIntlClientProvider messages={messages} locale={locale}>
			{children}
		</NextIntlClientProvider>
	)
}

My request config for next-intl loads all variables on the server so I have full access to everything server-side despite only passing a subset to the client:

export default getRequestConfig(async () => {
	const locale = 'en'

	return {
		locale,
		messages: {
			...(await import('@/i18n/en.client.json')).default,
			...(await import('@/i18n/en.server.json')).default,
		},
	}
})

@VSyak11
Copy link
Author

VSyak11 commented Apr 17, 2025

By default next-intl just passes down the entire dictionary file into the context making it available throughout the client-side code as it doesn't possibly have any way of distinguishing which keys should be passed on to the client or skipped. If you're concerned about this behavior you are always able to manually pass a set of variables down to the client. It is something I do like this:

export const getClientMessages = cache(async () => (await import('@/i18n/en.client.json')).default)

// ...

export default async function Layout({ children }: { children: ReactNode }) {
// ...
const messages = await getClientMessages()

return (



{children}



)
}

// ...

export function Providers({
children,
messages,
locale,
}: {
children: ReactNode
messages: AbstractIntlMessages
locale: string
}) {
return (

{children}

)
}
My request config for next-intl loads all variables on the server so I have full access to everything server-side despite only passing a subset to the client:

export default getRequestConfig(async () => {
const locale = 'en'

return {
locale,
messages: {
...(await import('@/i18n/en.client.json')).default,
...(await import('@/i18n/en.server.json')).default,
},
}
})

Maybe there is a way to remove the script from the source code containing the translation as in the example https://next-intl-example-app-router-without-i18n-routing.vercel.app/login if "View page source" then there will be the following script containing the translation data:

Image

@amannn
Copy link
Owner

amannn commented Apr 17, 2025

Yep, @ChristianIvicevic is right—you can currently achieve this by manually choosing which messages to pass to NextIntlClientProvider (see the Server & Client Components docs for details).

The eventual solution I'd like to arrive at here is #1.

Copy link

github-actions bot commented May 2, 2025

This issue has been automatically closed because there was no recent activity and it was marked as unconfirmed. Note that issues are regularly checked and if they remain in unconfirmed state, they might miss information required to be actionable or are potentially out-of-scope. If you'd like to discuss this topic further, feel free to open a discussion instead.

@github-actions github-actions bot added the Stale label May 2, 2025
@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale May 2, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working Stale unconfirmed Needs triage.
Projects
None yet
Development

No branches or pull requests

3 participants