Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"./internal": "./internal/index.js",
"./internal/metrics": "./internal/metrics.js",
"./internal/testing": "./internal/testing.js",
"./internal/locale": "./internal/locale.js",
"./internal/analytics-metadata": "./internal/analytics-metadata/index.js",
"./internal/analytics-metadata/utils": "./internal/analytics-metadata/utils.js",
"./package.json": "./package.json"
Expand All @@ -40,7 +41,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
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
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';
16 changes: 16 additions & 0 deletions src/internal/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/internal/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');
});
});
});
46 changes: 46 additions & 0 deletions src/internal/locale/__tests__/normalize-start-of-week.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { normalizeStartOfWeek } from '../normalize-start-of-week';

// Mock the weekstart module
jest.mock('weekstart', () => ({
getWeekStartByLocale: jest.fn(),
}));

import { getWeekStartByLocale } from 'weekstart';

const mockGetWeekStartByLocale = getWeekStartByLocale as jest.MockedFunction<typeof getWeekStartByLocale>;

describe('normalizeStartOfWeek', () => {
beforeEach(() => {
mockGetWeekStartByLocale.mockReset();
});

describe('when startOfWeek is undefined', () => {
test('should call getWeekStartByLocale with the provided locale', () => {
mockGetWeekStartByLocale.mockReturnValue(0);
const result = normalizeStartOfWeek(undefined, 'en-US');

expect(mockGetWeekStartByLocale).toHaveBeenCalledWith('en-US');
expect(mockGetWeekStartByLocale).toHaveBeenCalledTimes(1);
expect(result).toBe(0);
});

test('should return Sunday (0) for US locale', () => {
mockGetWeekStartByLocale.mockReturnValue(0);
expect(normalizeStartOfWeek(undefined, 'en-US')).toBe(0);
});

test('should return Monday (1) for French locale', () => {
mockGetWeekStartByLocale.mockReturnValue(1);
expect(normalizeStartOfWeek(undefined, 'fr-FR')).toBe(1);
});
});

test('should prioritize provided number over locale', () => {
mockGetWeekStartByLocale.mockReturnValue(0);
expect(normalizeStartOfWeek(1, 'en-US')).toBe(1);
expect(mockGetWeekStartByLocale).not.toHaveBeenCalled();
});
});
10 changes: 10 additions & 0 deletions src/internal/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 });
}
6 changes: 6 additions & 0 deletions src/internal/locale/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

export { mergeLocales } from './merge-locales';
export { normalizeLocale } from './normalize-locale';
export { DayIndex, normalizeStartOfWeek } from './normalize-start-of-week';
10 changes: 10 additions & 0 deletions src/internal/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/internal/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 '../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/internal/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?'

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

View workflow job for this annotation

GitHub Actions / build / build

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/internal/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) {
Copy link
Member

Choose a reason for hiding this comment

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

Actually I see that this function does not have its own coverage (also not in components) and since we are now exporting as part of this package, I think it is more necessary. Could you add some coverage? A couple cases where the start of the week is different would suffice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added 👍

return (typeof startOfWeek === 'number' ? startOfWeek % 7 : getWeekStartByLocale(locale)) as DayIndex;
}
Loading