Skip to content

Commit

Permalink
fix: Move locale validation to defineRouting (#1560)
Browse files Browse the repository at this point in the history
  • Loading branch information
amannn authored Nov 19, 2024
1 parent c8eb4ad commit 5a7f7be
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 88 deletions.
4 changes: 4 additions & 0 deletions packages/next-intl/src/routing/defineRouting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
Locales,
Pathnames
} from './types.tsx';
import validateLocales from './validateLocales.tsx';

export default function defineRouting<
const AppLocales extends Locales,
Expand All @@ -19,5 +20,8 @@ export default function defineRouting<
AppDomains
>
) {
if (process.env.NODE_ENV !== 'production') {
validateLocales(config.locales);
}
return config;
}
71 changes: 71 additions & 0 deletions packages/next-intl/src/routing/validateLocales.test.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof vi.spyOn>;

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<typeof vi.spyOn>;

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();
});
});
16 changes: 16 additions & 0 deletions packages/next-intl/src/routing/validateLocales.tsx
Original file line number Diff line number Diff line change
@@ -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").`
);
}
}
}
71 changes: 1 addition & 70 deletions packages/use-intl/src/core/hasLocale.test.tsx
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -24,72 +24,3 @@ it('can be called with a non-matching narrow candidate', () => {
candidate satisfies never;
}
});

describe('accepts valid formats', () => {
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;

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<typeof vi.spyOn>;

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();
});
});
18 changes: 0 additions & 18 deletions packages/use-intl/src/core/hasLocale.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<LocaleType extends Locale>(
locales: ReadonlyArray<LocaleType>,
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);
}

0 comments on commit 5a7f7be

Please sign in to comment.