From 09e875150e25bb4fb69fc184977d5b87b16e89b6 Mon Sep 17 00:00:00 2001 From: Boris Serdiuk Date: Tue, 7 Jan 2025 17:15:35 +0100 Subject: [PATCH] feat: Print a warning when both I18nProvider and own i18n string are missing --- pages/alert/permutations.page.tsx | 2 +- pages/app-layout/utils/content-blocks.tsx | 1 + pages/key-value-pairs/permutations.page.tsx | 2 +- pages/steps/permutations-utils.tsx | 12 ++++++++ src/autosuggest/internal.tsx | 6 ++-- src/cards/index.tsx | 10 ++++--- src/form/internal.tsx | 2 +- src/i18n/__tests__/i18n.test.tsx | 21 +++++++++++++- src/i18n/__tests__/test-component.tsx | 6 ++++ src/i18n/context.ts | 13 ++++++++- .../components/dropdown-status/index.tsx | 28 ++++++------------- src/link/internal.tsx | 2 +- src/multiselect/embedded.tsx | 3 -- src/multiselect/internal.tsx | 10 +++---- src/multiselect/use-multiselect.tsx | 27 ++---------------- src/popover/internal.tsx | 2 +- .../property-filter-autosuggest.tsx | 2 ++ src/select/internal.tsx | 6 ++-- .../expandable-rows/expandable-rows-utils.ts | 8 ++++-- 19 files changed, 90 insertions(+), 73 deletions(-) diff --git a/pages/alert/permutations.page.tsx b/pages/alert/permutations.page.tsx index 821d8d5550..e19e2d52da 100644 --- a/pages/alert/permutations.page.tsx +++ b/pages/alert/permutations.page.tsx @@ -17,7 +17,7 @@ const longText = const longTextWithLink = ( <> Lorem ipsum dolor sit amet, consectetur adipisicing{' '} - + elit , sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud diff --git a/pages/app-layout/utils/content-blocks.tsx b/pages/app-layout/utils/content-blocks.tsx index ef1889b6b0..0385c3ab80 100644 --- a/pages/app-layout/utils/content-blocks.tsx +++ b/pages/app-layout/utils/content-blocks.tsx @@ -22,6 +22,7 @@ import styles from '../styles.scss'; export function Breadcrumbs() { return ( ([ { label: 'CNAMEs', value: ( - + abc.service23G24.xyz ), diff --git a/pages/steps/permutations-utils.tsx b/pages/steps/permutations-utils.tsx index be4bd33247..6fb25cebd6 100644 --- a/pages/steps/permutations-utils.tsx +++ b/pages/steps/permutations-utils.tsx @@ -316,6 +316,7 @@ export const loadingStepsInteractive: ReadonlyArray = [ Listed EC2 instances:{' '}
    @@ -357,6 +358,7 @@ export const loadingSteps2Interactive: ReadonlyArray = [ Listed EC2 instances:{' '}
      @@ -382,6 +384,7 @@ export const loadingSteps2Interactive: ReadonlyArray = [ Gathered Security Group IDs:{' '}
        @@ -414,6 +417,7 @@ export const loadingSteps3Interactive: ReadonlyArray = [ Listed EC2 instances:{' '}
          @@ -439,6 +443,7 @@ export const loadingSteps3Interactive: ReadonlyArray = [ Gathered Security Group IDs:{' '}
            @@ -476,6 +481,7 @@ export const successfulStepsInteractive: ReadonlyArray = [ Listed EC2 instances:{' '}
              @@ -501,6 +507,7 @@ export const successfulStepsInteractive: ReadonlyArray = [ Gathered Security Group IDs:{' '}
                @@ -538,6 +545,7 @@ export const blockedStepsInteractive: ReadonlyArray = [ Listed EC2 instances:{' '}
                  @@ -563,6 +571,7 @@ export const blockedStepsInteractive: ReadonlyArray = [ Gathered Security Group IDs:{' '}
                    @@ -601,6 +610,7 @@ export const failedStepsInteractive: ReadonlyArray = [ Listed EC2 instances:{' '}
                      @@ -634,6 +644,7 @@ export const failedStepsWithRetryTextInteractive: ReadonlyArray Listed EC2 instances:{' '}
                        @@ -672,6 +683,7 @@ export const failedStepsWithRetryButtonInteractive: ReadonlyArray
                          diff --git a/src/autosuggest/internal.tsx b/src/autosuggest/internal.tsx index c824b2446e..cef41f968a 100644 --- a/src/autosuggest/internal.tsx +++ b/src/autosuggest/internal.tsx @@ -77,9 +77,7 @@ const InternalAutosuggest = React.forwardRef((props: InternalAutosuggestProps, r ); const i18n = useInternalI18n('autosuggest'); - const errorIconAriaLabel = i18n('errorIconAriaLabel', restProps.errorIconAriaLabel); const selectedAriaLabel = i18n('selectedAriaLabel', restProps.selectedAriaLabel); - const recoveryText = i18n('recoveryText', restProps.recoveryText); if (restProps.recoveryText && !onLoadItems) { warnOnce('Autosuggest', '`onLoadItems` must be provided for `recoveryText` to be displayed.'); @@ -188,8 +186,8 @@ const InternalAutosuggest = React.forwardRef((props: InternalAutosuggestProps, r ...props, isEmpty, isFiltered, - recoveryText, - errorIconAriaLabel, + getRecoveryText: () => i18n('errorIconAriaLabel', restProps.errorIconAriaLabel), + getErrorIconAriaLabel: () => i18n('recoveryText', restProps.recoveryText), onRecoveryClick: handleRecoveryClick, filteringResultsText: filteredText, hasRecoveryCallback: !!onLoadItems, diff --git a/src/cards/index.tsx b/src/cards/index.tsx index 015b80fd91..f26821c717 100644 --- a/src/cards/index.tsx +++ b/src/cards/index.tsx @@ -98,10 +98,12 @@ const Cards = React.forwardRef(function ( selectionType, isItemDisabled, onSelectionChange, - ariaLabels: { - itemSelectionLabel: ariaLabels?.itemSelectionLabel, - selectionGroupLabel: i18n('ariaLabels.selectionGroupLabel', ariaLabels?.selectionGroupLabel), - }, + ariaLabels: selectionType + ? { + itemSelectionLabel: ariaLabels?.itemSelectionLabel, + selectionGroupLabel: i18n('ariaLabels.selectionGroupLabel', ariaLabels?.selectionGroupLabel), + } + : {}, }); const hasToolsHeader = header || filter || pagination || preferences; const hasFooterPagination = isMobile && variant === 'full-page' && !!pagination; diff --git a/src/form/internal.tsx b/src/form/internal.tsx index f9a66c6ef3..f86c4902fe 100644 --- a/src/form/internal.tsx +++ b/src/form/internal.tsx @@ -35,7 +35,7 @@ export default function InternalForm({ }: InternalFormProps) { const baseProps = getBaseProps(props); const i18n = useInternalI18n('form'); - const errorIconAriaLabel = i18n('errorIconAriaLabel', errorIconAriaLabelOverride); + const errorIconAriaLabel = errorText ? i18n('errorIconAriaLabel', errorIconAriaLabelOverride) : undefined; const analyticsComponentMetadata: GeneratedAnalyticsMetadataFormFragment = { component: { name: 'awsui.Form', diff --git a/src/i18n/__tests__/i18n.test.tsx b/src/i18n/__tests__/i18n.test.tsx index beb4a8cdda..a21748f44a 100644 --- a/src/i18n/__tests__/i18n.test.tsx +++ b/src/i18n/__tests__/i18n.test.tsx @@ -4,8 +4,20 @@ import React from 'react'; import { render } from '@testing-library/react'; +import { clearMessageCache } from '@cloudscape-design/component-toolkit/internal/testing'; + import { I18nProvider, I18nProviderProps } from '../../../lib/components/i18n'; -import { MESSAGES, TestComponent } from './test-component'; +import { MESSAGES, SimpleTestComponent, TestComponent } from './test-component'; + +beforeEach(() => { + clearMessageCache(); + jest.spyOn(console, 'warn').mockImplementation(() => {}); +}); + +afterEach(() => { + expect(console.warn).not.toHaveBeenCalled(); + jest.restoreAllMocks(); +}); describe('with custom "lang" on ', () => { afterEach(() => { @@ -145,3 +157,10 @@ it('allows nesting providers', () => { expect(container.querySelector('#top-level-string')).toHaveTextContent('My custom string'); expect(container.querySelector('#nested-string')).toHaveTextContent('nested string'); }); + +it('prints a warning when a string is not provided neither via prop nor I18nProvider', () => { + render(); + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith(expect.stringMatching(/topLevelString.*I18nProvider/)); + jest.mocked(console.warn).mockReset(); +}); diff --git a/src/i18n/__tests__/test-component.tsx b/src/i18n/__tests__/test-component.tsx index e4248ad6c2..535c996ce6 100644 --- a/src/i18n/__tests__/test-component.tsx +++ b/src/i18n/__tests__/test-component.tsx @@ -60,3 +60,9 @@ export function TestComponent(props: TestComponentProps) {
                        ); } + +export function SimpleTestComponent(props: TestComponentProps) { + const i18n = useInternalI18n('test-component'); + + return {i18n('topLevelString', props.topLevelString)}; +} diff --git a/src/i18n/context.ts b/src/i18n/context.ts index 508429b077..9a8c7cf11a 100644 --- a/src/i18n/context.ts +++ b/src/i18n/context.ts @@ -3,6 +3,9 @@ import React, { useContext } from 'react'; +import { warnOnce } from '@cloudscape-design/component-toolkit/internal'; + +import { isDevelopment } from '../internal/is-development'; import { I18nFormatArgTypes } from './messages-types'; export type CustomHandler = (formatFn: (args: FormatFnArgs) => string) => ReturnValue; @@ -20,7 +23,15 @@ interface InternalI18nContextProps { export const InternalI18nContext = React.createContext({ locale: null, - format: (_namespace: string, _component: string, _key: string, provided: T) => provided, + format: (_namespace: string, component: string, key: string, provided: T) => { + if (isDevelopment && !provided) { + warnOnce( + component, + `Localization is not provided for key ${key}. Provide the value as a prop or use I18nProvider` + ); + } + return provided; + }, }); export function useLocale(): string | null { diff --git a/src/internal/components/dropdown-status/index.tsx b/src/internal/components/dropdown-status/index.tsx index 3180cdb7ae..7b9002494b 100644 --- a/src/internal/components/dropdown-status/index.tsx +++ b/src/internal/components/dropdown-status/index.tsx @@ -12,7 +12,7 @@ import styles from './styles.css.js'; export { DropdownStatusProps }; -export interface DropdownStatusPropsExtended extends DropdownStatusProps { +export interface DropdownStatusPropsExtended extends Omit { isEmpty?: boolean; isNoMatch?: boolean; isFiltered?: boolean; @@ -30,27 +30,16 @@ export interface DropdownStatusPropsExtended extends DropdownStatusProps { * in case recoveryText was automatically provided by i18n. */ hasRecoveryCallback?: boolean; + + getErrorIconAriaLabel: () => string | undefined; + getRecoveryText: () => string | undefined; } function DropdownStatus({ children }: { children: React.ReactNode }) { return
                        {children}
                        ; } -type UseDropdownStatus = ({ - statusType, - empty, - loadingText, - finishedText, - filteringResultsText, - errorText, - recoveryText, - isEmpty, - isNoMatch, - isFiltered, - noMatch, - hasRecoveryCallback, - onRecoveryClick, -}: DropdownStatusPropsExtended) => DropdownStatusResult; +type UseDropdownStatus = (statusProps: DropdownStatusPropsExtended) => DropdownStatusResult; export interface DropdownStatusResult { isSticky: boolean; @@ -65,14 +54,14 @@ export const useDropdownStatus: UseDropdownStatus = ({ finishedText, filteringResultsText, errorText, - recoveryText, + getRecoveryText, isEmpty, isNoMatch, isFiltered, noMatch, onRecoveryClick, hasRecoveryCallback = false, - errorIconAriaLabel, + getErrorIconAriaLabel, }): DropdownStatusResult => { const previousStatusType = usePrevious(statusType); const statusResult: DropdownStatusResult = { isSticky: true, content: null, hasRecoveryButton: false }; @@ -80,6 +69,7 @@ export const useDropdownStatus: UseDropdownStatus = ({ if (statusType === 'loading') { statusResult.content = {loadingText}; } else if (statusType === 'error') { + const recoveryText = getRecoveryText(); statusResult.hasRecoveryButton = !!recoveryText && hasRecoveryCallback; statusResult.content = ( @@ -87,7 +77,7 @@ export const useDropdownStatus: UseDropdownStatus = ({ type="error" __display="inline" __animate={previousStatusType !== 'error'} - iconAriaLabel={errorIconAriaLabel} + iconAriaLabel={getErrorIconAriaLabel()} > {errorText} {' '} diff --git a/src/link/internal.tsx b/src/link/internal.tsx index cccffbab81..d659a523cc 100644 --- a/src/link/internal.tsx +++ b/src/link/internal.tsx @@ -180,7 +180,7 @@ const InternalLink = React.forwardRef( sharedProps['aria-labelledby'] = `${sharedProps.id} ${infoId} ${infoLinkLabelFromContext}`; } - const renderedExternalIconAriaLabel = i18n('externalIconAriaLabel', externalIconAriaLabel); + const renderedExternalIconAriaLabel = external ? i18n('externalIconAriaLabel', externalIconAriaLabel) : undefined; const content = ( <> {children} diff --git a/src/multiselect/embedded.tsx b/src/multiselect/embedded.tsx index e827c24d0b..7e01c4b936 100644 --- a/src/multiselect/embedded.tsx +++ b/src/multiselect/embedded.tsx @@ -44,7 +44,6 @@ const EmbeddedMultiselect = React.forwardRef( filteringType, ariaLabel, selectedOptions, - deselectAriaLabel, virtualScroll, filteringText = '', ...restProps @@ -58,8 +57,6 @@ const EmbeddedMultiselect = React.forwardRef( options, selectedOptions, filteringType, - disabled: false, - deselectAriaLabel, controlId: formFieldContext.controlId, ariaLabelId, footerId, diff --git a/src/multiselect/internal.tsx b/src/multiselect/internal.tsx index f2ed2cf44b..6025af7bd6 100644 --- a/src/multiselect/internal.tsx +++ b/src/multiselect/internal.tsx @@ -73,8 +73,6 @@ const InternalMultiselect = React.forwardRef( options, selectedOptions, filteringType, - disabled, - deselectAriaLabel, controlId, ariaLabelId, footerId, @@ -122,9 +120,11 @@ const InternalMultiselect = React.forwardRef( iconUrl: option.iconUrl, iconSvg: option.iconSvg, tags: option.tags, - dismissLabel: i18n('deselectAriaLabel', deselectAriaLabel?.(option), format => - format({ option__label: option.label ?? '' }) - ), + dismissLabel: hideTokens + ? undefined + : i18n('deselectAriaLabel', deselectAriaLabel?.(option), format => + format({ option__label: option.label ?? '' }) + ), })); const ListComponent = virtualScroll ? VirtualList : PlainList; diff --git a/src/multiselect/use-multiselect.tsx b/src/multiselect/use-multiselect.tsx index 307c9f8849..da323549f1 100644 --- a/src/multiselect/use-multiselect.tsx +++ b/src/multiselect/use-multiselect.tsx @@ -30,10 +30,8 @@ type UseMultiselectOptions = SomeRequired< | 'selectedOptions' | 'filteringType' | 'filteringResultsText' - | 'disabled' | 'noMatch' | 'renderHighlightedAriaLive' - | 'deselectAriaLabel' | 'keepOpen' | 'onBlur' | 'onFocus' @@ -56,7 +54,6 @@ export function useMultiselect({ options, filteringType, filteringResultsText, - disabled, statusType, empty, loadingText, @@ -65,7 +62,6 @@ export function useMultiselect({ noMatch, renderHighlightedAriaLive, selectedOptions, - deselectAriaLabel, keepOpen, onBlur, onFocus, @@ -82,10 +78,7 @@ export function useMultiselect({ }: UseMultiselectOptions) { checkOptionValueField('Multiselect', 'options', options); - const i18n = useInternalI18n('multiselect'); const i18nCommon = useInternalI18n('select'); - const recoveryText = i18nCommon('recoveryText', restProps.recoveryText); - const errorIconAriaLabel = i18nCommon('errorIconAriaLabel', restProps.errorIconAriaLabel); const selectedAriaLabel = i18nCommon('selectedAriaLabel', restProps.selectedAriaLabel); if (restProps.recoveryText && !onLoadItems) { @@ -190,14 +183,14 @@ export function useMultiselect({ loadingText, finishedText, errorText, - recoveryText, + getRecoveryText: () => i18nCommon('recoveryText', restProps.recoveryText), isEmpty, isNoMatch, noMatch, isFiltered, filteringResultsText: filteredText, onRecoveryClick: handleRecoveryClick, - errorIconAriaLabel: errorIconAriaLabel, + getErrorIconAriaLabel: () => i18nCommon('errorIconAriaLabel', restProps.errorIconAriaLabel), hasRecoveryCallback: !!onLoadItems, }); @@ -209,21 +202,6 @@ export function useMultiselect({ renderHighlightedAriaLive, }); - const tokens: TokenGroupProps['items'] = selectedOptions.map(option => ({ - label: option.label, - disabled: disabled || option.disabled, - labelTag: option.labelTag, - description: option.description, - iconAlt: option.iconAlt, - iconName: option.iconName, - iconUrl: option.iconUrl, - iconSvg: option.iconSvg, - tags: option.tags, - dismissLabel: i18n('deselectAriaLabel', deselectAriaLabel?.(option), format => - format({ option__label: option.label ?? '' }) - ), - })); - useEffect(() => { scrollToIndex.current?.(highlightedIndex); }, [highlightedIndex]); @@ -248,7 +226,6 @@ export function useMultiselect({ return { isOpen, - tokens, announcement, dropdownStatus, filteringValue, diff --git a/src/popover/internal.tsx b/src/popover/internal.tsx index a371d5dda1..9198bfa7bc 100644 --- a/src/popover/internal.tsx +++ b/src/popover/internal.tsx @@ -69,7 +69,7 @@ function InternalPopover( const clickFrameId = useRef(null); const i18n = useInternalI18n('popover'); - const dismissAriaLabel = i18n('dismissAriaLabel', restProps.dismissAriaLabel); + const dismissAriaLabel = dismissButton ? i18n('dismissAriaLabel', restProps.dismissAriaLabel) : undefined; const [visible, setVisible] = useState(false); diff --git a/src/property-filter/property-filter-autosuggest.tsx b/src/property-filter/property-filter-autosuggest.tsx index 46f76e925e..1a3a618f95 100644 --- a/src/property-filter/property-filter-autosuggest.tsx +++ b/src/property-filter/property-filter-autosuggest.tsx @@ -164,6 +164,8 @@ const PropertyFilterAutosuggest = React.forwardRef( isEmpty, onRecoveryClick: handleRecoveryClick, hasRecoveryCallback: !!onLoadItems, + getErrorIconAriaLabel: () => undefined, + getRecoveryText: () => props.recoveryText, }); let content = null; diff --git a/src/select/internal.tsx b/src/select/internal.tsx index 5657019db1..4ecc31c56d 100644 --- a/src/select/internal.tsx +++ b/src/select/internal.tsx @@ -78,9 +78,7 @@ const InternalSelect = React.forwardRef( const formFieldContext = useFormFieldContext(restProps); const i18n = useInternalI18n('select'); - const errorIconAriaLabel = i18n('errorIconAriaLabel', restProps.errorIconAriaLabel); const selectedAriaLabel = i18n('selectedAriaLabel', restProps.selectedAriaLabel); - const recoveryText = i18n('recoveryText', restProps.recoveryText); if (restProps.recoveryText && !onLoadItems) { warnOnce('Select', '`onLoadItems` must be provided for `recoveryText` to be displayed.'); @@ -193,13 +191,13 @@ const InternalSelect = React.forwardRef( loadingText, finishedText, errorText, - recoveryText, + getRecoveryText: () => i18n('recoveryText', restProps.recoveryText), isEmpty, isNoMatch, noMatch, isFiltered, filteringResultsText: filteredText, - errorIconAriaLabel, + getErrorIconAriaLabel: () => i18n('errorIconAriaLabel', restProps.errorIconAriaLabel), onRecoveryClick: handleRecoveryClick, hasRecoveryCallback: !!onLoadItems, }); diff --git a/src/table/expandable-rows/expandable-rows-utils.ts b/src/table/expandable-rows/expandable-rows-utils.ts index caead00e36..38e1072ca2 100644 --- a/src/table/expandable-rows/expandable-rows-utils.ts +++ b/src/table/expandable-rows/expandable-rows-utils.ts @@ -87,8 +87,12 @@ export function useExpandableTableProps({ isExpanded: expandedSet.has(item), onExpandableItemToggle: () => fireNonCancelableEvent(expandableRows?.onExpandableItemToggle, { item, expanded: !expandedSet.has(item) }), - expandButtonLabel: i18n('ariaLabels.expandButtonLabel', ariaLabels?.expandButtonLabel?.(item)), - collapseButtonLabel: i18n('ariaLabels.collapseButtonLabel', ariaLabels?.collapseButtonLabel?.(item)), + expandButtonLabel: isExpandable + ? i18n('ariaLabels.expandButtonLabel', ariaLabels?.expandButtonLabel?.(item)) + : undefined, + collapseButtonLabel: isExpandable + ? i18n('ariaLabels.collapseButtonLabel', ariaLabels?.collapseButtonLabel?.(item)) + : undefined, parent, children, };