diff --git a/packages/autocomplete-js/src/__tests__/api.test.ts b/packages/autocomplete-js/src/__tests__/api.test.ts index 21313204a..9bf013ba5 100644 --- a/packages/autocomplete-js/src/__tests__/api.test.ts +++ b/packages/autocomplete-js/src/__tests__/api.test.ts @@ -5,6 +5,10 @@ import { createCollection } from '../../../../test/utils'; import { autocomplete } from '../autocomplete'; describe('api', () => { + afterEach(() => { + document.body.innerHTML = ''; + }); + describe('setActiveItemId', () => { test('sets `activeItemId` value in the state', () => { const onStateChange = jest.fn(); @@ -271,6 +275,29 @@ describe('api', () => { ).toBeInTheDocument(); }); }); + + test('overrides the default translations', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + + const { update } = autocomplete<{ label: string }>({ + container, + }); + + expect( + document.querySelector('.aa-SubmitButton') + ).toHaveAttribute('title', 'Submit'); + + update({ + translations: { + submitButtonTitle: 'Envoyer', + }, + }); + + expect( + document.querySelector('.aa-SubmitButton') + ).toHaveAttribute('title', 'Envoyer'); + }); }); describe('destroy', () => { diff --git a/packages/autocomplete-js/src/__tests__/translations.test.ts b/packages/autocomplete-js/src/__tests__/translations.test.ts new file mode 100644 index 000000000..b6cb2e884 --- /dev/null +++ b/packages/autocomplete-js/src/__tests__/translations.test.ts @@ -0,0 +1,147 @@ +import { waitFor } from '@testing-library/dom'; + +import { autocomplete } from '../autocomplete'; + +describe('translations', () => { + afterEach(() => { + document.body.innerHTML = ''; + }); + + describe('regular DOM', () => { + test('provides default translations', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + + autocomplete<{ label: string }>({ + container, + }); + + expect( + document.querySelector('.aa-ClearButton') + ).toHaveAttribute('title', 'Clear'); + expect( + document.querySelector('.aa-SubmitButton') + ).toHaveAttribute('title', 'Submit'); + }); + + test('allows custom translations', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + + autocomplete<{ label: string }>({ + container, + translations: { + clearButtonTitle: 'Effacer', + submitButtonTitle: 'Envoyer', + }, + }); + + expect( + document.querySelector('.aa-ClearButton') + ).toHaveAttribute('title', 'Effacer'); + expect( + document.querySelector('.aa-SubmitButton') + ).toHaveAttribute('title', 'Envoyer'); + }); + }); + + describe('detached DOM', () => { + const originalMatchMedia = window.matchMedia; + + beforeAll(() => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn((query) => ({ + matches: true, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); + }); + + afterAll(() => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: originalMatchMedia, + }); + }); + + test('provides default translations', async () => { + const container = document.createElement('div'); + document.body.appendChild(container); + + const { destroy } = autocomplete<{ label: string }>({ + container, + detachedMediaQuery: '', + }); + + const searchButton = container.querySelector( + '.aa-DetachedSearchButton' + ); + + searchButton.click(); + + await waitFor(() => { + expect( + document.querySelector('.aa-DetachedOverlay') + ).toBeInTheDocument(); + }); + + expect( + document.querySelector('.aa-ClearButton') + ).toHaveAttribute('title', 'Clear'); + expect( + document.querySelector('.aa-SubmitButton') + ).toHaveAttribute('title', 'Submit'); + expect( + document.querySelector('.aa-DetachedCancelButton') + ).toHaveTextContent('Cancel'); + + destroy(); + }); + + test('allows custom translations', async () => { + const container = document.createElement('div'); + document.body.appendChild(container); + + const { destroy } = autocomplete({ + container, + detachedMediaQuery: '', + translations: { + clearButtonTitle: 'Effacer', + detachedCancelButtonText: 'Annuler', + submitButtonTitle: 'Envoyer', + }, + }); + + const searchButton = container.querySelector( + '.aa-DetachedSearchButton' + ); + + searchButton.click(); + + await waitFor(() => { + expect( + document.querySelector('.aa-DetachedOverlay') + ).toBeInTheDocument(); + }); + + expect( + document.querySelector('.aa-ClearButton') + ).toHaveAttribute('title', 'Effacer'); + expect( + document.querySelector('.aa-SubmitButton') + ).toHaveAttribute('title', 'Envoyer'); + expect( + document.querySelector('.aa-DetachedCancelButton') + ).toHaveTextContent('Annuler'); + + destroy(); + }); + }); +}); diff --git a/packages/autocomplete-js/src/autocomplete.ts b/packages/autocomplete-js/src/autocomplete.ts index 459f01e38..77b36da1c 100644 --- a/packages/autocomplete-js/src/autocomplete.ts +++ b/packages/autocomplete-js/src/autocomplete.ts @@ -118,6 +118,7 @@ export function autocomplete( propGetters, setIsModalOpen, state: lastStateRef.current, + translations: props.value.renderer.translations, }) ); diff --git a/packages/autocomplete-js/src/createAutocompleteDom.ts b/packages/autocomplete-js/src/createAutocompleteDom.ts index 8bb66c64c..0d7a04139 100644 --- a/packages/autocomplete-js/src/createAutocompleteDom.ts +++ b/packages/autocomplete-js/src/createAutocompleteDom.ts @@ -12,6 +12,7 @@ import { AutocompleteDom, AutocompletePropGetters, AutocompleteState, + AutocompleteTranslations, } from './types'; import { setProperties } from './utils'; @@ -25,6 +26,7 @@ type CreateDomProps = { propGetters: AutocompletePropGetters; setIsModalOpen(value: boolean): void; state: AutocompleteState; + translations: AutocompleteTranslations; }; export function createAutocompleteDom({ @@ -37,6 +39,7 @@ export function createAutocompleteDom({ propGetters, setIsModalOpen, state, + translations, }: CreateDomProps): AutocompleteDom { const createDomElement = getCreateDomElement(environment); @@ -72,7 +75,7 @@ export function createAutocompleteDom({ const submitButton = createDomElement('button', { class: classNames.submitButton, type: 'submit', - title: 'Submit', + title: translations.submitButtonTitle, children: [SearchIcon({ environment })], }); const label = createDomElement('label', { @@ -83,7 +86,7 @@ export function createAutocompleteDom({ const clearButton = createDomElement('button', { class: classNames.clearButton, type: 'reset', - title: 'Clear', + title: translations.clearButtonTitle, children: [ClearIcon({ environment })], }); const loadingIndicator = createDomElement('div', { @@ -164,7 +167,7 @@ export function createAutocompleteDom({ }); const detachedCancelButton = createDomElement('button', { class: classNames.detachedCancelButton, - textContent: 'Cancel', + textContent: translations.detachedCancelButtonText, onClick() { autocomplete.setIsOpen(false); setIsModalOpen(false); diff --git a/packages/autocomplete-js/src/getDefaultOptions.ts b/packages/autocomplete-js/src/getDefaultOptions.ts index a27a2ce98..b9463498c 100644 --- a/packages/autocomplete-js/src/getDefaultOptions.ts +++ b/packages/autocomplete-js/src/getDefaultOptions.ts @@ -21,6 +21,7 @@ import { AutocompleteOptions, AutocompleteRender, AutocompleteRenderer, + AutocompleteTranslations, } from './types'; import { getHTMLElement, mergeClassNames } from './utils'; @@ -82,6 +83,7 @@ export function getDefaultOptions( renderer, detachedMediaQuery, components, + translations, ...core } = options; @@ -104,6 +106,11 @@ export function getDefaultOptions( ReverseSnippet: createReverseSnippetComponent(defaultedRenderer), Snippet: createSnippetComponent(defaultedRenderer), }; + const defaultTranslations: AutocompleteTranslations = { + clearButtonTitle: 'Clear', + detachedCancelButtonText: 'Cancel', + submitButtonTitle: 'Submit', + }; return { renderer: { @@ -136,6 +143,10 @@ export function getDefaultOptions( ...defaultComponents, ...components, }, + translations: { + ...defaultTranslations, + ...translations, + }, }, core: { ...core, diff --git a/packages/autocomplete-js/src/types/AutocompleteOptions.ts b/packages/autocomplete-js/src/types/AutocompleteOptions.ts index a5c0d14fd..cfdc9bbb0 100644 --- a/packages/autocomplete-js/src/types/AutocompleteOptions.ts +++ b/packages/autocomplete-js/src/types/AutocompleteOptions.ts @@ -14,6 +14,7 @@ import { AutocompleteRender } from './AutocompleteRender'; import { AutocompleteRenderer } from './AutocompleteRenderer'; import { AutocompleteSource } from './AutocompleteSource'; import { AutocompleteState } from './AutocompleteState'; +import { AutocompleteTranslations } from './AutocompleteTranslations'; export interface OnStateChangeProps extends AutocompleteScopeApi { @@ -108,4 +109,12 @@ export interface AutocompleteOptions * @link https://www.algolia.com/doc/ui-libraries/autocomplete/api-reference/autocomplete-js/autocomplete/#param-components */ components?: PublicAutocompleteComponents; + /** + * A mapping of translation strings. + * + * Defaults to English values. + * + * @link https://www.algolia.com/doc/ui-libraries/autocomplete/api-reference/autocomplete-js/autocomplete/#param-translations + */ + translations?: Partial; } diff --git a/packages/autocomplete-js/src/types/AutocompleteTranslations.ts b/packages/autocomplete-js/src/types/AutocompleteTranslations.ts new file mode 100644 index 000000000..f42c53645 --- /dev/null +++ b/packages/autocomplete-js/src/types/AutocompleteTranslations.ts @@ -0,0 +1,5 @@ +export type AutocompleteTranslations = { + detachedCancelButtonText: string; + clearButtonTitle: string; + submitButtonTitle: string; +}; diff --git a/packages/autocomplete-js/src/types/index.ts b/packages/autocomplete-js/src/types/index.ts index 708ad8837..0c6357653 100644 --- a/packages/autocomplete-js/src/types/index.ts +++ b/packages/autocomplete-js/src/types/index.ts @@ -10,4 +10,5 @@ export * from './AutocompleteRender'; export * from './AutocompleteRenderer'; export * from './AutocompleteSource'; export * from './AutocompleteState'; +export * from './AutocompleteTranslations'; export * from './HighlightHitParams';