Skip to content

Commit 93abc01

Browse files
committed
feat: Print a warning when both I18nProvider and own i18n string are missing
1 parent f7988a7 commit 93abc01

File tree

19 files changed

+90
-73
lines changed

19 files changed

+90
-73
lines changed

pages/alert/permutations.page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const longText =
1717
const longTextWithLink = (
1818
<>
1919
Lorem ipsum dolor sit amet, <Link href="#">consectetur</Link> adipisicing{' '}
20-
<Link external={true} href="#">
20+
<Link external={true} href="#" externalIconAriaLabel="(opens in new tab)">
2121
elit
2222
</Link>
2323
, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud

pages/app-layout/utils/content-blocks.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import styles from '../styles.scss';
2222
export function Breadcrumbs() {
2323
return (
2424
<BreadcrumbGroup
25+
expandAriaLabel="Show path"
2526
items={[
2627
{ text: 'Home', href: '#' },
2728
{ text: 'Service', href: '#' },

pages/key-value-pairs/permutations.page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const permutations = createPermutations<KeyValuePairsProps>([
5858
{
5959
label: 'CNAMEs',
6060
value: (
61-
<Link external={true} href="#">
61+
<Link external={true} href="#" externalIconAriaLabel="opens in a new tab">
6262
abc.service23G24.xyz
6363
</Link>
6464
),

pages/steps/permutations-utils.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ export const loadingStepsInteractive: ReadonlyArray<StepsProps.Step> = [
316316
Listed EC2 instances:{' '}
317317
<Popover
318318
header={'EC2 Instance IDs'}
319+
dismissAriaLabel="Dismiss"
319320
content={
320321
<Box fontSize="body-s">
321322
<ul>
@@ -357,6 +358,7 @@ export const loadingSteps2Interactive: ReadonlyArray<StepsProps.Step> = [
357358
Listed EC2 instances:{' '}
358359
<Popover
359360
header={'EC2 Instance IDs'}
361+
dismissAriaLabel="Dismiss"
360362
content={
361363
<Box fontSize="body-s">
362364
<ul>
@@ -382,6 +384,7 @@ export const loadingSteps2Interactive: ReadonlyArray<StepsProps.Step> = [
382384
Gathered Security Group IDs:{' '}
383385
<Popover
384386
header={'Security Group IDs'}
387+
dismissAriaLabel="Dismiss"
385388
content={
386389
<Box fontSize="body-s">
387390
<ul>
@@ -414,6 +417,7 @@ export const loadingSteps3Interactive: ReadonlyArray<StepsProps.Step> = [
414417
Listed EC2 instances:{' '}
415418
<Popover
416419
header={'EC2 Instance IDs'}
420+
dismissAriaLabel="Dismiss"
417421
content={
418422
<Box fontSize="body-s">
419423
<ul>
@@ -439,6 +443,7 @@ export const loadingSteps3Interactive: ReadonlyArray<StepsProps.Step> = [
439443
Gathered Security Group IDs:{' '}
440444
<Popover
441445
header={'Security Group IDs'}
446+
dismissAriaLabel="Dismiss"
442447
content={
443448
<Box fontSize="body-s">
444449
<ul>
@@ -476,6 +481,7 @@ export const successfulStepsInteractive: ReadonlyArray<StepsProps.Step> = [
476481
Listed EC2 instances:{' '}
477482
<Popover
478483
header={'EC2 Instance IDs'}
484+
dismissAriaLabel="Dismiss"
479485
content={
480486
<Box fontSize="body-s">
481487
<ul>
@@ -501,6 +507,7 @@ export const successfulStepsInteractive: ReadonlyArray<StepsProps.Step> = [
501507
Gathered Security Group IDs:{' '}
502508
<Popover
503509
header={'Security Group IDs'}
510+
dismissAriaLabel="Dismiss"
504511
content={
505512
<Box fontSize="body-s">
506513
<ul>
@@ -538,6 +545,7 @@ export const blockedStepsInteractive: ReadonlyArray<StepsProps.Step> = [
538545
Listed EC2 instances:{' '}
539546
<Popover
540547
header={'EC2 Instance IDs'}
548+
dismissAriaLabel="Dismiss"
541549
content={
542550
<Box fontSize="body-s">
543551
<ul>
@@ -563,6 +571,7 @@ export const blockedStepsInteractive: ReadonlyArray<StepsProps.Step> = [
563571
Gathered Security Group IDs:{' '}
564572
<Popover
565573
header={'Security Group IDs'}
574+
dismissAriaLabel="Dismiss"
566575
content={
567576
<Box fontSize="body-s">
568577
<ul>
@@ -601,6 +610,7 @@ export const failedStepsInteractive: ReadonlyArray<StepsProps.Step> = [
601610
Listed EC2 instances:{' '}
602611
<Popover
603612
header={'EC2 Instance IDs'}
613+
dismissAriaLabel="Dismiss"
604614
content={
605615
<Box fontSize="body-s">
606616
<ul>
@@ -634,6 +644,7 @@ export const failedStepsWithRetryTextInteractive: ReadonlyArray<StepsProps.Step>
634644
Listed EC2 instances:{' '}
635645
<Popover
636646
header={'EC2 Instance IDs'}
647+
dismissAriaLabel="Dismiss"
637648
content={
638649
<Box fontSize="body-s">
639650
<ul>
@@ -672,6 +683,7 @@ export const failedStepsWithRetryButtonInteractive: ReadonlyArray<StepsProps.Ste
672683
Listed EC2 instances:{' '}
673684
<Popover
674685
header={'EC2 Instance IDs'}
686+
dismissAriaLabel="Dismiss"
675687
content={
676688
<Box fontSize="body-s">
677689
<ul>

src/autosuggest/internal.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,7 @@ const InternalAutosuggest = React.forwardRef((props: InternalAutosuggestProps, r
7777
);
7878

7979
const i18n = useInternalI18n('autosuggest');
80-
const errorIconAriaLabel = i18n('errorIconAriaLabel', restProps.errorIconAriaLabel);
8180
const selectedAriaLabel = i18n('selectedAriaLabel', restProps.selectedAriaLabel);
82-
const recoveryText = i18n('recoveryText', restProps.recoveryText);
8381

8482
if (restProps.recoveryText && !onLoadItems) {
8583
warnOnce('Autosuggest', '`onLoadItems` must be provided for `recoveryText` to be displayed.');
@@ -188,8 +186,8 @@ const InternalAutosuggest = React.forwardRef((props: InternalAutosuggestProps, r
188186
...props,
189187
isEmpty,
190188
isFiltered,
191-
recoveryText,
192-
errorIconAriaLabel,
189+
getRecoveryText: () => i18n('errorIconAriaLabel', restProps.errorIconAriaLabel),
190+
getErrorIconAriaLabel: () => i18n('recoveryText', restProps.recoveryText),
193191
onRecoveryClick: handleRecoveryClick,
194192
filteringResultsText: filteredText,
195193
hasRecoveryCallback: !!onLoadItems,

src/cards/index.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,12 @@ const Cards = React.forwardRef(function <T = any>(
9898
selectionType,
9999
isItemDisabled,
100100
onSelectionChange,
101-
ariaLabels: {
102-
itemSelectionLabel: ariaLabels?.itemSelectionLabel,
103-
selectionGroupLabel: i18n('ariaLabels.selectionGroupLabel', ariaLabels?.selectionGroupLabel),
104-
},
101+
ariaLabels: selectionType
102+
? {
103+
itemSelectionLabel: ariaLabels?.itemSelectionLabel,
104+
selectionGroupLabel: i18n('ariaLabels.selectionGroupLabel', ariaLabels?.selectionGroupLabel),
105+
}
106+
: {},
105107
});
106108
const hasToolsHeader = header || filter || pagination || preferences;
107109
const hasFooterPagination = isMobile && variant === 'full-page' && !!pagination;

src/form/internal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export default function InternalForm({
3535
}: InternalFormProps) {
3636
const baseProps = getBaseProps(props);
3737
const i18n = useInternalI18n('form');
38-
const errorIconAriaLabel = i18n('errorIconAriaLabel', errorIconAriaLabelOverride);
38+
const errorIconAriaLabel = errorText ? i18n('errorIconAriaLabel', errorIconAriaLabelOverride) : undefined;
3939
const analyticsComponentMetadata: GeneratedAnalyticsMetadataFormFragment = {
4040
component: {
4141
name: 'awsui.Form',

src/i18n/__tests__/i18n.test.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,20 @@
44
import React from 'react';
55
import { render } from '@testing-library/react';
66

7+
import { clearMessageCache } from '@cloudscape-design/component-toolkit/internal/testing';
8+
79
import { I18nProvider, I18nProviderProps } from '../../../lib/components/i18n';
8-
import { MESSAGES, TestComponent } from './test-component';
10+
import { MESSAGES, SimpleTestComponent, TestComponent } from './test-component';
11+
12+
beforeEach(() => {
13+
clearMessageCache();
14+
jest.spyOn(console, 'warn').mockImplementation(() => {});
15+
});
16+
17+
afterEach(() => {
18+
expect(console.warn).not.toHaveBeenCalled();
19+
jest.restoreAllMocks();
20+
});
921

1022
describe('with custom "lang" on <html>', () => {
1123
afterEach(() => {
@@ -145,3 +157,10 @@ it('allows nesting providers', () => {
145157
expect(container.querySelector('#top-level-string')).toHaveTextContent('My custom string');
146158
expect(container.querySelector('#nested-string')).toHaveTextContent('nested string');
147159
});
160+
161+
it('prints a warning when a string is not provided neither via prop nor I18nProvider', () => {
162+
render(<SimpleTestComponent />);
163+
expect(console.warn).toHaveBeenCalledTimes(1);
164+
expect(console.warn).toHaveBeenCalledWith(expect.stringMatching(/topLevelString.*I18nProvider/));
165+
jest.mocked(console.warn).mockReset();
166+
});

src/i18n/__tests__/test-component.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,9 @@ export function TestComponent(props: TestComponentProps) {
6060
</ul>
6161
);
6262
}
63+
64+
export function SimpleTestComponent(props: TestComponentProps) {
65+
const i18n = useInternalI18n('test-component');
66+
67+
return <span id="top-level-string">{i18n('topLevelString', props.topLevelString)}</span>;
68+
}

src/i18n/context.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
import React, { useContext } from 'react';
55

6+
import { warnOnce } from '@cloudscape-design/component-toolkit/internal';
7+
8+
import { isDevelopment } from '../internal/is-development';
69
import { I18nFormatArgTypes } from './messages-types';
710

811
export type CustomHandler<ReturnValue, FormatFnArgs> = (formatFn: (args: FormatFnArgs) => string) => ReturnValue;
@@ -20,7 +23,15 @@ interface InternalI18nContextProps {
2023

2124
export const InternalI18nContext = React.createContext<InternalI18nContextProps>({
2225
locale: null,
23-
format: <T>(_namespace: string, _component: string, _key: string, provided: T) => provided,
26+
format: <T>(_namespace: string, component: string, key: string, provided: T) => {
27+
if (isDevelopment && !provided) {
28+
warnOnce(
29+
component,
30+
`Localization is not provided for key ${key}. Provide the value as a prop or use I18nProvider`
31+
);
32+
}
33+
return provided;
34+
},
2435
});
2536

2637
export function useLocale(): string | null {

src/internal/components/dropdown-status/index.tsx

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import styles from './styles.css.js';
1212

1313
export { DropdownStatusProps };
1414

15-
export interface DropdownStatusPropsExtended extends DropdownStatusProps {
15+
export interface DropdownStatusPropsExtended extends Omit<DropdownStatusProps, 'recoveryText' | 'errorIconAriaLabel'> {
1616
isEmpty?: boolean;
1717
isNoMatch?: boolean;
1818
isFiltered?: boolean;
@@ -30,27 +30,16 @@ export interface DropdownStatusPropsExtended extends DropdownStatusProps {
3030
* in case recoveryText was automatically provided by i18n.
3131
*/
3232
hasRecoveryCallback?: boolean;
33+
34+
getErrorIconAriaLabel: () => string | undefined;
35+
getRecoveryText: () => string | undefined;
3336
}
3437

3538
function DropdownStatus({ children }: { children: React.ReactNode }) {
3639
return <div className={styles.root}>{children}</div>;
3740
}
3841

39-
type UseDropdownStatus = ({
40-
statusType,
41-
empty,
42-
loadingText,
43-
finishedText,
44-
filteringResultsText,
45-
errorText,
46-
recoveryText,
47-
isEmpty,
48-
isNoMatch,
49-
isFiltered,
50-
noMatch,
51-
hasRecoveryCallback,
52-
onRecoveryClick,
53-
}: DropdownStatusPropsExtended) => DropdownStatusResult;
42+
type UseDropdownStatus = (statusProps: DropdownStatusPropsExtended) => DropdownStatusResult;
5443

5544
export interface DropdownStatusResult {
5645
isSticky: boolean;
@@ -65,29 +54,30 @@ export const useDropdownStatus: UseDropdownStatus = ({
6554
finishedText,
6655
filteringResultsText,
6756
errorText,
68-
recoveryText,
57+
getRecoveryText,
6958
isEmpty,
7059
isNoMatch,
7160
isFiltered,
7261
noMatch,
7362
onRecoveryClick,
7463
hasRecoveryCallback = false,
75-
errorIconAriaLabel,
64+
getErrorIconAriaLabel,
7665
}): DropdownStatusResult => {
7766
const previousStatusType = usePrevious(statusType);
7867
const statusResult: DropdownStatusResult = { isSticky: true, content: null, hasRecoveryButton: false };
7968

8069
if (statusType === 'loading') {
8170
statusResult.content = <InternalStatusIndicator type={'loading'}>{loadingText}</InternalStatusIndicator>;
8271
} else if (statusType === 'error') {
72+
const recoveryText = getRecoveryText();
8373
statusResult.hasRecoveryButton = !!recoveryText && hasRecoveryCallback;
8474
statusResult.content = (
8575
<span>
8676
<InternalStatusIndicator
8777
type="error"
8878
__display="inline"
8979
__animate={previousStatusType !== 'error'}
90-
iconAriaLabel={errorIconAriaLabel}
80+
iconAriaLabel={getErrorIconAriaLabel()}
9181
>
9282
{errorText}
9383
</InternalStatusIndicator>{' '}

src/link/internal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ const InternalLink = React.forwardRef(
180180
sharedProps['aria-labelledby'] = `${sharedProps.id} ${infoId} ${infoLinkLabelFromContext}`;
181181
}
182182

183-
const renderedExternalIconAriaLabel = i18n('externalIconAriaLabel', externalIconAriaLabel);
183+
const renderedExternalIconAriaLabel = external ? i18n('externalIconAriaLabel', externalIconAriaLabel) : undefined;
184184
const content = (
185185
<>
186186
{children}

src/multiselect/embedded.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ const EmbeddedMultiselect = React.forwardRef(
4444
filteringType,
4545
ariaLabel,
4646
selectedOptions,
47-
deselectAriaLabel,
4847
virtualScroll,
4948
filteringText = '',
5049
...restProps
@@ -58,8 +57,6 @@ const EmbeddedMultiselect = React.forwardRef(
5857
options,
5958
selectedOptions,
6059
filteringType,
61-
disabled: false,
62-
deselectAriaLabel,
6360
controlId: formFieldContext.controlId,
6461
ariaLabelId,
6562
footerId,

src/multiselect/internal.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,6 @@ const InternalMultiselect = React.forwardRef(
7373
options,
7474
selectedOptions,
7575
filteringType,
76-
disabled,
77-
deselectAriaLabel,
7876
controlId,
7977
ariaLabelId,
8078
footerId,
@@ -122,9 +120,11 @@ const InternalMultiselect = React.forwardRef(
122120
iconUrl: option.iconUrl,
123121
iconSvg: option.iconSvg,
124122
tags: option.tags,
125-
dismissLabel: i18n('deselectAriaLabel', deselectAriaLabel?.(option), format =>
126-
format({ option__label: option.label ?? '' })
127-
),
123+
dismissLabel: hideTokens
124+
? undefined
125+
: i18n('deselectAriaLabel', deselectAriaLabel?.(option), format =>
126+
format({ option__label: option.label ?? '' })
127+
),
128128
}));
129129

130130
const ListComponent = virtualScroll ? VirtualList : PlainList;

0 commit comments

Comments
 (0)