Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
14 changes: 13 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"prepare": "husky"
},
"dependencies": {
"tslib": "^2.3.1"
"tslib": "^2.3.1",
"weekstart": "^2.0.0"
},
"devDependencies": {
"@cloudscape-design/browser-test-tools": "^3.0.0",
Expand Down
7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,10 @@
export { default as useContainerQuery } from './container-queries/use-container-query';
export { default as useControllableState } from './use-controllable-state/use-controllable-state';

export type { PropertyDescriptions } from './use-controllable-state/interfaces';
export type { ContainerQueryEntry } from './container-queries/interfaces';
export type { PropertyDescriptions } from './use-controllable-state/interfaces';

// Locale utils
export { mergeLocales } from './locale/merge-locales';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For cases like this in which we are the only known consumers, we add new exports in ./internal instead.

I would propose to add one entry-point index file inside ./internal/locale and an exports entry in the package.json file for ./internal/locale/index.js

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, thanks for the context. Updated in the next rev

export { normalizeLocale } from './locale/normalize-locale';
export { normalizeStartOfWeek, DayIndex } from './locale/normalize-start-of-week';
16 changes: 16 additions & 0 deletions src/locale/__tests__/merge-locales.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { mergeLocales } from '../merge-locales';

test('should return the first locale if it is fully specified', () => {
expect(mergeLocales('en-US', 'fr-CA')).toEqual('en-US');
});

test('should return the second locale if it extends the first', () => {
expect(mergeLocales('en', 'en-US')).toEqual('en-US');
});

test('should return the first locale if the second is different', () => {
expect(mergeLocales('en', 'fr-CA')).toEqual('en');
});
75 changes: 75 additions & 0 deletions src/locale/__tests__/normalize-locale.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { normalizeLocale } from '../normalize-locale';
import setOptions from './utils/intl-polyfill';

function withDocumentLang(lang: string, callback: () => void) {
const htmlElement = document.querySelector('html');
htmlElement!.setAttribute('lang', lang);
callback();
htmlElement!.removeAttribute('lang');
}

describe('normalizeLocale', () => {
let consoleSpy: jest.SpyInstance;
beforeEach(() => {
consoleSpy = jest.spyOn(console, 'warn');
setOptions({ locale: 'en-US' });
});

afterEach(() => {
expect(consoleSpy).not.toHaveBeenCalled();
});

test('should return the provided value', () => {
expect(normalizeLocale('DatePickerTest', 'en-US')).toBe('en-US');
});

test('should extend provided value if it is short form', () => {
expect(normalizeLocale('DatePickerTest', 'en')).toBe('en-US');
setOptions({ locale: 'en-GB' });
expect(normalizeLocale('DatePickerTest', 'en')).toBe('en-GB');
});

test('should not extend the provided value if it starts from different language', () => {
expect(normalizeLocale('DatePickerTest', 'fr')).toBe('fr');
});

test('should replace underscores with dashes', () => {
expect(normalizeLocale('DatePickerTest', 'zh_CN')).toBe('zh-CN');
});

test('should warn if the provided value is in invalid format', () => {
expect(normalizeLocale('DatePickerTest', 'not-locale')).toBe('en-US');
expect(consoleSpy).toHaveBeenCalledWith(
'[AwsUi] [DatePickerTest] Invalid locale provided: not-locale. Falling back to default'
);
consoleSpy.mockReset();
});

test('should return document language by default', () => {
withDocumentLang('en', () => {
expect(normalizeLocale('DatePickerTest', null)).toBe('en-US');
});
});

test('should not extend document language with locale if they do not match', () => {
withDocumentLang('fr', () => {
expect(normalizeLocale('DatePickerTest', null)).toBe('fr');
});
});

test('should combine values from document lang and browser locale', () => {
setOptions({ locale: 'fr-CA' });
withDocumentLang('fr', () => {
expect(normalizeLocale('DatePickerTest', null)).toBe('fr-CA');
});
});

test('should replace underscores with dashes in document lang', () => {
withDocumentLang('zh_CN', () => {
expect(normalizeLocale('DatePickerTest', null)).toBe('zh-CN');
});
});
});
10 changes: 10 additions & 0 deletions src/locale/__tests__/utils/intl-polyfill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

// We use resolvedOptions to detect browser locale. This method allows us to override the value for testing.
export default function setResolvedOptions(newValue: { locale: string }): void {
const dateTimeFormat = new Intl.DateTimeFormat(newValue.locale);
const resolvedOptions = dateTimeFormat.resolvedOptions();

window.Intl.DateTimeFormat.prototype.resolvedOptions = () => ({ ...resolvedOptions, ...newValue });
}
10 changes: 10 additions & 0 deletions src/locale/merge-locales.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

export function mergeLocales(locale: string, fullLocale: string) {
const isShort = locale.length === 2;
if (isShort && fullLocale.indexOf(locale) === 0) {
return fullLocale;
}
return locale;
}
43 changes: 43 additions & 0 deletions src/locale/normalize-locale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { warnOnce } from '../internal/logging';

import { mergeLocales } from './merge-locales';

export function normalizeLocale(component: string, locale: string | null): string {
locale = checkLocale(component, locale);
const browserLocale = getBrowserLocale();
if (locale) {
return mergeLocales(locale, browserLocale);
}
const htmlLocale = checkLocale(component, getHtmlElement()?.getAttribute('lang'));
if (htmlLocale) {
return mergeLocales(htmlLocale, browserLocale);
}
return browserLocale;
}

function checkLocale(component: string, locale: string | null | undefined): string {
if (!locale || locale === '') {
return '';
}

// Support underscore-delimited locales
locale = locale && locale.replace(/^([a-z]{2})_/, '$1-');
// Check that the value matches aa-BB pattern
// TODO: support full BCP 47 spec?

Check warning on line 29 in src/locale/normalize-locale.ts

View workflow job for this annotation

GitHub Actions / build / build

Unexpected 'todo' comment: 'TODO: support full BCP 47 spec?'

Check warning on line 29 in src/locale/normalize-locale.ts

View workflow job for this annotation

GitHub Actions / dry-run / Build component-toolkit

Unexpected 'todo' comment: 'TODO: support full BCP 47 spec?'
if (locale && !locale.match(/^[a-z]{2}(-[A-Z]{2})?$/)) {
warnOnce(component, `Invalid locale provided: ${locale}. Falling back to default`);
locale = '';
}
return locale;
}

function getHtmlElement() {
return typeof document !== 'undefined' ? document.querySelector('html') : null;
}

function getBrowserLocale() {
return new Intl.DateTimeFormat().resolvedOptions().locale;
}
10 changes: 10 additions & 0 deletions src/locale/normalize-start-of-week.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { getWeekStartByLocale } from 'weekstart';

export type DayIndex = 0 | 1 | 2 | 3 | 4 | 5 | 6;

export function normalizeStartOfWeek(startOfWeek: number | undefined, locale: string) {
return (typeof startOfWeek === 'number' ? startOfWeek % 7 : getWeekStartByLocale(locale)) as DayIndex;
}
Loading