diff --git a/packages/next-intl/src/routing/defineRouting.tsx b/packages/next-intl/src/routing/defineRouting.tsx index b975a519f..1b34187ce 100644 --- a/packages/next-intl/src/routing/defineRouting.tsx +++ b/packages/next-intl/src/routing/defineRouting.tsx @@ -5,6 +5,7 @@ import type { Locales, Pathnames } from './types.tsx'; +import validateLocales from './validateLocales.tsx'; export default function defineRouting< const AppLocales extends Locales, @@ -19,5 +20,8 @@ export default function defineRouting< AppDomains > ) { + if (process.env.NODE_ENV !== 'production') { + validateLocales(config.locales); + } return config; } diff --git a/packages/next-intl/src/routing/validateLocales.test.tsx b/packages/next-intl/src/routing/validateLocales.test.tsx new file mode 100644 index 000000000..5e6d0f851 --- /dev/null +++ b/packages/next-intl/src/routing/validateLocales.test.tsx @@ -0,0 +1,71 @@ +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; +import validateLocales from './validateLocales.tsx'; + +describe('accepts valid formats', () => { + let consoleErrorSpy: ReturnType; + + beforeEach(() => { + consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); + }); + + it.each([ + 'en', + 'en-US', + 'EN-US', + 'en-us', + 'en-GB', + 'zh-Hans-CN', + 'es-419', + 'en-Latn', + 'zh-Hans', + 'en-US-u-ca-buddhist', + 'en-x-private1', + 'en-US-u-nu-thai', + 'ar-u-nu-arab', + 'en-t-m0-true', + 'zh-Hans-CN-x-private1-private2', + 'en-US-u-ca-gregory-nu-latn', + 'en-US-x-usd', + + // Somehow tolerated by Intl.Locale + 'english' + ])('accepts: %s', (locale) => { + validateLocales([locale]); + expect(consoleErrorSpy).not.toHaveBeenCalled(); + }); +}); + +describe('warns for invalid formats', () => { + let consoleErrorSpy: ReturnType; + + beforeEach(() => { + consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleErrorSpy.mockRestore(); + }); + + it.each([ + 'en_US', + 'en-', + 'e-US', + 'en-USA', + 'und', + '123', + '-en', + 'en--US', + 'toolongstring', + 'en-US-', + '@#$', + 'en US', + 'en.US' + ])('rejects: %s', (locale) => { + validateLocales([locale]); + expect(consoleErrorSpy).toHaveBeenCalled(); + }); +}); diff --git a/packages/next-intl/src/routing/validateLocales.tsx b/packages/next-intl/src/routing/validateLocales.tsx new file mode 100644 index 000000000..ba09f20b7 --- /dev/null +++ b/packages/next-intl/src/routing/validateLocales.tsx @@ -0,0 +1,16 @@ +import type {Locales} from './types.tsx'; + +export default function validateLocales(locales: Locales) { + for (const locale of locales) { + try { + const constructed = new Intl.Locale(locale); + if (!constructed.language) { + throw new Error('Language is required'); + } + } catch { + console.error( + `Found invalid locale within provided \`locales\`: "${locale}"\nPlease ensure you're using a valid Unicode locale identifier (e.g. "en-US").` + ); + } + } +} diff --git a/packages/use-intl/src/core/hasLocale.test.tsx b/packages/use-intl/src/core/hasLocale.test.tsx index 19a6dfdcb..713260179 100644 --- a/packages/use-intl/src/core/hasLocale.test.tsx +++ b/packages/use-intl/src/core/hasLocale.test.tsx @@ -1,4 +1,4 @@ -import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; +import {it} from 'vitest'; import hasLocale from './hasLocale.tsx'; it('narrows down the type', () => { @@ -24,72 +24,3 @@ it('can be called with a non-matching narrow candidate', () => { candidate satisfies never; } }); - -describe('accepts valid formats', () => { - let consoleErrorSpy: ReturnType; - - beforeEach(() => { - consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - }); - - afterEach(() => { - consoleErrorSpy.mockRestore(); - }); - - it.each([ - 'en', - 'en-US', - 'EN-US', - 'en-us', - 'en-GB', - 'zh-Hans-CN', - 'es-419', - 'en-Latn', - 'zh-Hans', - 'en-US-u-ca-buddhist', - 'en-x-private1', - 'en-US-u-nu-thai', - 'ar-u-nu-arab', - 'en-t-m0-true', - 'zh-Hans-CN-x-private1-private2', - 'en-US-u-ca-gregory-nu-latn', - 'en-US-x-usd', - - // Somehow tolerated by Intl.Locale - 'english' - ])('accepts: %s', (locale) => { - expect(hasLocale([locale] as const, locale)).toBe(true); - expect(consoleErrorSpy).not.toHaveBeenCalled(); - }); -}); - -describe('warns for invalid formats', () => { - let consoleErrorSpy: ReturnType; - - beforeEach(() => { - consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - }); - - afterEach(() => { - consoleErrorSpy.mockRestore(); - }); - - it.each([ - 'en_US', - 'en-', - 'e-US', - 'en-USA', - 'und', - '123', - '-en', - 'en--US', - 'toolongstring', - 'en-US-', - '@#$', - 'en US', - 'en.US' - ])('rejects: %s', (locale) => { - hasLocale([locale] as const, locale); - expect(consoleErrorSpy).toHaveBeenCalled(); - }); -}); diff --git a/packages/use-intl/src/core/hasLocale.tsx b/packages/use-intl/src/core/hasLocale.tsx index 6094cfd01..5bb68714c 100644 --- a/packages/use-intl/src/core/hasLocale.tsx +++ b/packages/use-intl/src/core/hasLocale.tsx @@ -3,29 +3,11 @@ import type {Locale} from './AppConfig.tsx'; /** * Checks if a locale exists in a list of locales. * - * Additionally, in development, the provided locales are validated to - * ensure they follow the Unicode language identifier standard. - * * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale */ export default function hasLocale( locales: ReadonlyArray, candidate?: string | null ): candidate is LocaleType { - if (process.env.NODE_ENV !== 'production') { - for (const locale of locales) { - try { - const constructed = new Intl.Locale(locale); - if (!constructed.language) { - throw new Error('Language is required'); - } - } catch { - console.error( - `Found invalid locale within provided \`locales\`: "${locale}"\nPlease ensure you're using a valid Unicode locale identifier (e.g. "en-US").` - ); - } - } - } - return locales.includes(candidate as LocaleType); }