From cf69c5c12ae19781af430a83593cd4da0f3b579f Mon Sep 17 00:00:00 2001 From: Ze Yu Date: Fri, 16 Dec 2022 22:18:07 +0800 Subject: [PATCH] Add translation configuration for all strings --- docs/src/language.md | 31 ++++++++++++++ docs/src/search_configuration.md | 4 -- packages/mdbook-infisearch/src/infisearch.js | 2 +- packages/search-ui/src/InputManager.ts | 3 +- packages/search-ui/src/Options.ts | 41 +++++++++++++++--- packages/search-ui/src/search.ts | 14 ++++--- packages/search-ui/src/search/options.ts | 10 ++--- .../search-ui/src/search/rootContainers.ts | 28 +++++++------ packages/search-ui/src/search/tips.ts | 42 ++++++++++--------- .../resultsRender/repeatedFooter.ts | 4 +- packages/search-ui/src/translations/en.ts | 37 ++++++++++++++++ packages/search-ui/src/utils/aria.ts | 4 +- packages/search-ui/src/utils/header.ts | 13 ++++-- packages/search-ui/src/utils/state.ts | 8 ++-- 14 files changed, 175 insertions(+), 66 deletions(-) create mode 100644 packages/search-ui/src/translations/en.ts diff --git a/docs/src/language.md b/docs/src/language.md index bf67ea02..a9da1fa4 100644 --- a/docs/src/language.md +++ b/docs/src/language.md @@ -94,3 +94,34 @@ Keeping them enables the following: - Processing phrase queries such as `"for tomorrow"` accurately; Stop words would be removed automatically from such queries. - Boolean queries of stop words (e.g. `if AND forecast AND sunny`) - More accurate ranking for free text queries, which uses stop words in term proximity ranking + +## UI Translations + +The UI's text can also be overwritten. +Refer to this [link](https://github.com/ang-zeyu/infisearch/tree/main/packages/search-ui/src/translations/en.ts) for the default set of texts. + +```ts +infisearch.init({ + uiOptions: { + translations: { ... } + } +}) +``` + +| Option | Default | Description | +| ----------- | ----------- | ----------- | +| `resultsLabel` | `'Site results'` | Accessibility label for the [`listbox`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/listbox_role) containing result previews. This is announced to screenreaders. +| `fsButtonLabel` | `'Search'` | Accessibility label for the original input element that functions as a button when the fullscreen UI is in use. +| `fsButtonPlaceholder` | `undefined`| Placeholder override for the provided `input` that functions as a button when the fullscreen UI is in use. +| `fsPlaceholder` | `'Search this site'` | Placeholder of the input element in the fullscreen UI. +| `fsCloseText` | `'Close'` | Text for the Close button. +| `filtersButton` | `'Filters'` | Text for the Filters button if any enum or numeric filters are configured. +| `numResultsFound` | `' results found'` | The text following the number of results found. +| `startSearching` | `'Start Searching Above!'`| Text shown when the input is empty. +| `startingUp` | `'... Starting Up ...'` | Text shown when InfiSearch is still not ready to perform any queries. The setup occurs extremely quickly, you will hopefully not be able to see this text most of the time. +| `navigation` | `'Navigation'` | Navigation controls text. +| `tipHeader` | `'🔎 Advanced search tips'` | Header of the tip popup. +| `tip` | `'Tip'` | First column header of the tip popup. +| `example` | `'Example'` | Second column header of the tip popup. +| `tipRows.xx` (refer [here](https://github.com/ang-zeyu/infisearch/tree/main/packages/search-ui/src/translations/en.ts)) | | Examples for usage of InfiSearch's advanced search syntax. +| `error` | `'Oops! Something went wrong... 🙁'` | Generic error text when something goes wrong diff --git a/docs/src/search_configuration.md b/docs/src/search_configuration.md index c0d45664..0caac75f 100644 --- a/docs/src/search_configuration.md +++ b/docs/src/search_configuration.md @@ -87,11 +87,7 @@ There are also several options specific to each mode. Note that `dropdown` and ` | Mode | Option | Default | Description | | ----------- | ----------- | ----------- | ----------- | | dropdown | `dropdownAlignment` | `'bottom-end'` | `'bottom'` or `'bottom-start'` or `'bottom-end'`.

This is the side of the input element to align the dropdown results container and dropdown seperator against.

The alignment will also be automatically flipped horizontally to ensure the most optimal placement. -| auto | `fsInputButtonText` | `undefined`| Placeholder override for the `input` if the fullscreen UI is in use.

This is added for keyboard [accessibility](./search_configuration_styling.md#styling-the-fullscreen-ui-input-button). -| fullscreen | `fsInputLabel` | `'Search'` | Accessibility label for the original input element, when the fullscreen UI is in use. | fullscreen | `fsContainer` | `` element | `id` of the element, or an element reference to attach the separate root container to. -| fullscreen | `fsPlaceholder` | `'Search this site'` | Placeholder of the input element in the fullscreen UI. -| fullscreen | `fsCloseText` | `'Close'` | Text for the Close button. | fullscreen | `fsScrollLock` | `true` | Whether to automatically scroll lock the body element when the fullscreen UI is opened. | all except target | `tip` | `true` | Whether to show the tip icon. When hovered over, this shows advanced usage information (e.g. how to perform phrase queries). | target | `target` | `undefined` | `id` of the element, or an element reference to attach results to.

Required if using `mode='target'`. diff --git a/packages/mdbook-infisearch/src/infisearch.js b/packages/mdbook-infisearch/src/infisearch.js index 55a03091..0b31984c 100644 --- a/packages/mdbook-infisearch/src/infisearch.js +++ b/packages/mdbook-infisearch/src/infisearch.js @@ -6,7 +6,7 @@ infisearch.init({ mode, dropdownAlignment: 'bottom-start', target: document.getElementById('infisearch-mdbook-target'), - fsInputButtonText: 'Search', + fsButtonPlaceholder: 'Search', sourceFilesUrl: base_url, resultsRenderOpts: { addSearchedTerms: 'search', diff --git a/packages/search-ui/src/InputManager.ts b/packages/search-ui/src/InputManager.ts index 66640593..e5a50f49 100644 --- a/packages/search-ui/src/InputManager.ts +++ b/packages/search-ui/src/InputManager.ts @@ -101,6 +101,7 @@ export class IManager { !that._mrlInputEl.value, isDone, isError, + that._mrlOptions.uiOptions.translations, ); that._mrlState.replaceWith(newIndicatorElement); @@ -108,7 +109,7 @@ export class IManager { } _mrlRefreshHeader(query?: Query) { - const el = headerRender(query, this._mrlGetOrSetFiltersShown); + const el = headerRender(query, this._mrlGetOrSetFiltersShown, this._mrlOptions.uiOptions.translations); this._mrlHeader.replaceWith(el); this._mrlHeader = el; } diff --git a/packages/search-ui/src/Options.ts b/packages/search-ui/src/Options.ts index 233eb6e5..8bd30a33 100644 --- a/packages/search-ui/src/Options.ts +++ b/packages/search-ui/src/Options.ts @@ -28,6 +28,40 @@ export interface NumericFilterBinding { ltePlaceholder?: string, } +export interface Translations { + resultsLabel: string, + fsButtonPlaceholder?: string, + fsButtonLabel: string, + fsPlaceholder: string, + fsCloseText: string, + filtersButton: string, + numResultsFound: string, + startSearching: string, + startingUp: string, + navigation: string, + tipHeader: string, + tip: string, + example: string, + tipRows: { + searchPhrases: string, + requireTerm: string, + excludeTerm: string, + flipResults: string, + groupTerms: string, + searchPrefixes: string, + searchSections: string, + exSearchPhrases: string, + exRequireTerm: string, + exExcludeTerm: string, + exFlipResults: string, + exGroupTerms: string, + exSearchPrefixes: string, + exSearchSections: string[], + } + + error: string, +} + export interface UiOptions { input: HTMLInputElement, inputDebounce?: number, @@ -35,13 +69,7 @@ export interface UiOptions { mode: UiMode, isMobileDevice: () => boolean, dropdownAlignment?: 'bottom-start' | 'bottom-end', - label: string, - resultsLabel: string, - fsInputButtonText: string, - fsInputLabel: string, fsContainer?: HTMLElement, - fsPlaceholder?: string, - fsCloseText?: string, fsScrollLock: boolean, target?: HTMLElement, tip: boolean, @@ -49,6 +77,7 @@ export interface UiOptions { sortFields: { [fieldName: string]: { asc: string, desc: string } }, multiSelectFilters: MultiSelectFilterBinding[], numericFilters: NumericFilterBinding[], + translations: Translations, // This is specific to the default resultsRender implementation, // pulling it up as its a common option sourceFilesUrl?: string, diff --git a/packages/search-ui/src/search.ts b/packages/search-ui/src/search.ts index 83ab37c7..2db6de4c 100644 --- a/packages/search-ui/src/search.ts +++ b/packages/search-ui/src/search.ts @@ -83,9 +83,13 @@ function init(options: Options): { const { input, mode, dropdownAlignment, - label, - fsInputButtonText, fsInputLabel, fsScrollLock, + fsScrollLock, target, + translations: { + fsButtonPlaceholder, + fsButtonLabel, + resultsLabel, + }, } = uiOptions; const searcher = new Searcher(options.searcherOptions); @@ -193,10 +197,10 @@ function init(options: Options): { // Otherwise, the input should be focused initState._mrlShowDropdown(); } - setDropdownInputAria(input, resultContainer, label, originalPlaceholder); + setDropdownInputAria(input, resultContainer, resultsLabel, originalPlaceholder); } else { initState._mrlHideDropdown(); - unsetDropdownInputAria(input, resultContainer, fsInputLabel, fsInputButtonText); + unsetDropdownInputAria(input, resultContainer, fsButtonLabel, fsButtonPlaceholder); } } toggleUiMode(); @@ -231,7 +235,7 @@ function init(options: Options): { addFsTriggerInputListeners(); } else if (input && mode === UiMode.Fullscreen) { // Fullscreen-only mode - setFsTriggerInput(input, fsInputButtonText, fsInputLabel); + setFsTriggerInput(input, fsButtonPlaceholder, fsButtonLabel); addFsTriggerInputListeners(); } else if (input && mode === UiMode.Target) { // Target diff --git a/packages/search-ui/src/search/options.ts b/packages/search-ui/src/search/options.ts index bab41995..c7813086 100644 --- a/packages/search-ui/src/search/options.ts +++ b/packages/search-ui/src/search/options.ts @@ -1,5 +1,6 @@ import { Options, UiMode } from '../Options'; import { listItemRender } from '../searchResultTransform/listItemRender'; +import { TRANSLATIONS } from '../translations/en'; export function prepareOptions(options: Options) { options.searcherOptions = options.searcherOptions || ({} as any); @@ -53,11 +54,10 @@ export function prepareOptions(options: Options) { uiOptions.resultsPerPage = uiOptions.resultsPerPage || 10; uiOptions.maxSubMatches = uiOptions.maxSubMatches || 2; - uiOptions.label = uiOptions.label || 'Search this site'; - uiOptions.resultsLabel = uiOptions.resultsLabel || 'Site results'; - uiOptions.fsInputLabel = uiOptions.fsInputLabel || 'Search'; - uiOptions.fsPlaceholder = uiOptions.fsPlaceholder || 'Search this site'; - uiOptions.fsCloseText = uiOptions.fsCloseText || 'Close'; + uiOptions.translations = { + ...TRANSLATIONS, + ...(uiOptions.translations || {}), + }; if (!('fsScrollLock' in uiOptions)) { uiOptions.fsScrollLock = true; } diff --git a/packages/search-ui/src/search/rootContainers.ts b/packages/search-ui/src/search/rootContainers.ts index 1824fe6c..3b194857 100644 --- a/packages/search-ui/src/search/rootContainers.ts +++ b/packages/search-ui/src/search/rootContainers.ts @@ -100,12 +100,12 @@ export function dropdownRootRender( return [root, scrollContainer]; } -export function setFsTriggerInput(input: HTMLElement, fsInputButtonText: string, fsInputLabel: string) { +export function setFsTriggerInput(input: HTMLElement, fsButtonPlaceholder: string, fsButtonLabel: string) { input.setAttribute('autocomplete', 'off'); input.setAttribute('readonly', ''); input.setAttribute('role', 'button'); - input.setAttribute('aria-label', fsInputLabel); - if (fsInputButtonText) input.setAttribute('placeholder', fsInputButtonText); + input.setAttribute('aria-label', fsButtonLabel); + if (fsButtonPlaceholder) input.setAttribute('placeholder', fsButtonPlaceholder); input.classList.add('infi-button-input'); } @@ -120,18 +120,18 @@ function unsetFsTriggerInput(input: HTMLElement, originalPlaceholder: string) { export function setDropdownInputAria( input: HTMLElement, resultContainer: HTMLElement, - label: string, + resultsLabel: string, originalPlaceholder: string, ) { unsetFsTriggerInput(input, originalPlaceholder); - setInputAria(input, resultContainer, label); + setInputAria(input, resultContainer, resultsLabel); } export function unsetDropdownInputAria( input: HTMLElement, resultContainer: HTMLElement, - fsInputLabel: string, - fsInputButtonText: string, + fsButtonLabel: string, + fsButtonPlaceholder: string, ) { resultContainer.removeAttribute('role'); resultContainer.removeAttribute('aria-label'); @@ -140,7 +140,7 @@ export function unsetDropdownInputAria( input.removeAttribute('aria-autocomplete'); input.removeAttribute('aria-controls'); unsetActiveDescendant(input); - setFsTriggerInput(input, fsInputButtonText, fsInputLabel); + setFsTriggerInput(input, fsButtonPlaceholder, fsButtonLabel); } // Incremental Id for pages with multiple UIs, for aria attributes. @@ -151,10 +151,12 @@ export function fsRootRender( onClose: (isKeyboardClose: boolean) => void, ): [HTMLElement, HTMLInputElement, () => void, (isKeyboardClose: boolean) => void] { const { - fsPlaceholder, - fsCloseText, + translations: { + fsPlaceholder, + fsCloseText, + resultsLabel, + }, fsContainer, - label, } = opts.uiOptions; const labelId = `infi-fs-label-${fsId}`; @@ -212,7 +214,7 @@ export function fsRootRender( innerRoot.onclick = (ev) => ev.stopPropagation(); innerRoot.onmousedown = (ev) => ev.stopPropagation(); - setInputAria(inputEl, resultContainer, label); + setInputAria(inputEl, resultContainer, resultsLabel); const rootBackdropEl = h('div', { class: 'infi-fs-backdrop' }, innerRoot); @@ -265,5 +267,5 @@ export function targetRender( resultContainer, ); - setInputAria(input, resultContainer, opts.uiOptions.label); + setInputAria(input, resultContainer, opts.uiOptions.translations.resultsLabel); } diff --git a/packages/search-ui/src/search/tips.ts b/packages/search-ui/src/search/tips.ts index 47232257..cb506e4d 100644 --- a/packages/search-ui/src/search/tips.ts +++ b/packages/search-ui/src/search/tips.ts @@ -7,7 +7,9 @@ export default function createTipButton( opts: UiOptions, cfg: InfiConfig, ): HTMLElement | string { - if (opts.tip === false) { + const { tip, translations } = opts; + + if (tip === false) { return ''; } @@ -21,40 +23,40 @@ export default function createTipButton( const tipListBody = h('tbody', {}); + const tipRows = translations.tipRows; + if (cfg.indexingConfig.withPositions) { tipListBody.append(createRow( - 'Search for phrases', - wrapInCode('"for tomorrow"'), + tipRows.searchPhrases, + wrapInCode(tipRows.exSearchPhrases), )); } tipListBody.append( createRow( - 'Require a term', - wrapInCode('+sunny weather'), + tipRows.requireTerm, + wrapInCode(tipRows.exRequireTerm), ), createRow( - 'Exclude a term', - wrapInCode('-cloudy sunny'), + tipRows.excludeTerm, + wrapInCode(tipRows.exExcludeTerm), ), createRow( - 'Flip search results', - wrapInCode('~rainy'), + tipRows.flipResults, + wrapInCode(tipRows.exFlipResults), ), createRow( - 'Group terms together', - wrapInCode('~(sunny warm cloudy)'), + tipRows.groupTerms, + wrapInCode(tipRows.exGroupTerms), ), createRow( - 'Search for prefixes', - wrapInCode('run*'), + tipRows.searchPrefixes, + wrapInCode(tipRows.exSearchPrefixes), ), createRow( - 'Search only specific sections', - h('ul', {}, - h('li', {}, wrapInCode('title:forecast')), - h('li', {}, wrapInCode('heading:sunny')), - h('li', {}, wrapInCode('body:(rainy gloomy)')), + tipRows.searchSections, + h('ul', {}, + ...tipRows.exSearchSections.map(t => h('li', {}, wrapInCode(t))), ), ), ); @@ -65,14 +67,14 @@ export default function createTipButton( h( 'thead', { class: 'infi-tip-table-header' }, - h('tr', {}, h('th', { scope: 'col' }, 'Tip'), h('th', {}, 'Example')), + h('tr', {}, h('th', { scope: 'col' }, translations.tip), h('th', {}, translations.example)), ), tipListBody, ); const tipPopup = h( 'div', { class: 'infi-tip-popup-root' }, h('div', { class: 'infi-tip-popup' }, - h('div', { class: 'infi-tip-popup-title' }, '🔎 Advanced search tips'), + h('div', { class: 'infi-tip-popup-title' }, translations.tipHeader), tipList, ), ); diff --git a/packages/search-ui/src/searchResultTransform/resultsRender/repeatedFooter.ts b/packages/search-ui/src/searchResultTransform/resultsRender/repeatedFooter.ts index fb6e86eb..31b0d084 100644 --- a/packages/search-ui/src/searchResultTransform/resultsRender/repeatedFooter.ts +++ b/packages/search-ui/src/searchResultTransform/resultsRender/repeatedFooter.ts @@ -10,7 +10,7 @@ export function resultSeparator( focusOption: (el: HTMLElement) => void, query: Query, ) { - const { resultsPerPage } = options.uiOptions; + const { resultsPerPage, translations } = options.uiOptions; const footer = h('div', { class: 'infi-footer', tabindex: '-1' }); if (!query.resultsTotal) { return footer; @@ -43,7 +43,7 @@ export function resultSeparator( const isDomFocused = document.activeElement === loadMoreButton; loadMoreButtonWrapped.remove(); - footer.append(stateRender(false, true, false, false, false)); + footer.append(stateRender(false, true, false, false, false, translations)); // Announce footer information if (isDomFocused) footer.focus({ preventScroll: true }); diff --git a/packages/search-ui/src/translations/en.ts b/packages/search-ui/src/translations/en.ts new file mode 100644 index 00000000..f3d5b233 --- /dev/null +++ b/packages/search-ui/src/translations/en.ts @@ -0,0 +1,37 @@ +export const TRANSLATIONS = { + resultsLabel: 'Site results', + fsButtonLabel: 'Search', + fsPlaceholder: 'Search this site', + fsCloseText: 'Close', + filtersButton: 'Filters', + numResultsFound: ' results found', + startSearching: 'Start Searching Above!', + startingUp: '... Starting Up ...', + error: 'Oops! Something went wrong... 🙁', + navigation: 'Navigation', + tipHeader: '🔎 Advanced search tips', + tip: 'Tip', + example: 'Example', + tipRows: { + searchPhrases: 'Search for phrases', + exSearchPhrases: '"for tomorrow"', + + requireTerm: 'Require a term', + exRequireTerm: '+sunny weather', + + excludeTerm: 'Exclude a term', + exExcludeTerm: '-cloudy sunny', + + flipResults: 'Flip search results', + exFlipResults: '~rainy', + + groupTerms: 'Group terms together', + exGroupTerms: '~(sunny warm cloudy)', + + searchPrefixes: 'Search for prefixes', + exSearchPrefixes: 'run*', + + searchSections: 'Search only specific sections', + exSearchSections: ['title:forecast', 'heading:sunny', 'body:(rainy gloomy)'], + }, +}; diff --git a/packages/search-ui/src/utils/aria.ts b/packages/search-ui/src/utils/aria.ts index 19a7b979..ab1237c8 100644 --- a/packages/search-ui/src/utils/aria.ts +++ b/packages/search-ui/src/utils/aria.ts @@ -16,7 +16,7 @@ export function unsetExpanded(combobox: HTMLElement) { combobox.setAttribute('aria-expanded', 'false'); } -export function setInputAria(input: HTMLElement, listbox: HTMLElement, label: string) { +export function setInputAria(input: HTMLElement, listbox: HTMLElement, listboxLabel: string) { input.setAttribute('role', 'combobox'); input.setAttribute('autocomplete', 'off'); input.setAttribute('aria-autocomplete', 'list'); @@ -24,5 +24,5 @@ export function setInputAria(input: HTMLElement, listbox: HTMLElement, label: st input.setAttribute('aria-controls', listId); unsetExpanded(input); listbox.setAttribute('role', 'listbox'); - listbox.setAttribute('aria-label', label); + listbox.setAttribute('aria-label', listboxLabel); } diff --git a/packages/search-ui/src/utils/header.ts b/packages/search-ui/src/utils/header.ts index 22398b3e..4e2b0580 100644 --- a/packages/search-ui/src/utils/header.ts +++ b/packages/search-ui/src/utils/header.ts @@ -1,5 +1,6 @@ import { Query } from '@infisearch/search-lib'; import h from '@infisearch/search-lib/lib/utils/dom'; +import { Translations } from '../Options'; function getArrow(invert: boolean) { // https://www.svgrepo.com/svg/49189/up-arrow (CC0 License) @@ -9,18 +10,22 @@ function getArrow(invert: boolean) { + '"x="0" y="0" viewBox="0 0 490 490" style="enable-background:new 0 0 490 490" xml:space="preserve">'; } -export function headerRender(query: Query, getOrSetFiltersShown: (setValue?: boolean) => boolean) { +export function headerRender( + query: Query, + getOrSetFiltersShown: (setValue?: boolean) => boolean, + translations: Translations, +) { const header = h('div', { class: 'infi-header' }); if (query) { header.append(h('div', { class: 'infi-results-found' }, - `${query.resultsTotal} results found`, + query.resultsTotal + translations.numResultsFound, )); } const instructions = h('div', { class: 'infi-instructions' }); - instructions.innerHTML = 'Navigation:' + instructions.innerHTML = translations.navigation + getArrow(false) + getArrow(true) // https://www.svgrepo.com/svg/355201/return (Apache license) @@ -35,7 +40,7 @@ export function headerRender(query: Query, getOrSetFiltersShown: (setValue?: boo class: 'infi-filters' + (getOrSetFiltersShown() ? ' active' : ''), type: 'button', }, - 'Filters', + translations.filtersButton, ); filters.onclick = (ev) => { ev.preventDefault(); diff --git a/packages/search-ui/src/utils/state.ts b/packages/search-ui/src/utils/state.ts index f4491b58..55e1d84f 100644 --- a/packages/search-ui/src/utils/state.ts +++ b/packages/search-ui/src/utils/state.ts @@ -1,4 +1,5 @@ import h from '@infisearch/search-lib/lib/utils/dom'; +import { Translations } from '../Options'; /** * Renders setup, loading, error, idle, blank (input is empty) states @@ -9,16 +10,17 @@ export function stateRender( blank: boolean, isDone: boolean, isError: boolean, + translations: Translations, ) { if (isError) { - return h('div', { class: 'infi-error' }, 'Oops! Something went wrong... 🙁'); + return h('div', { class: 'infi-error' }, translations.error); } else if (blank) { - return h('div', { class: 'infi-blank' }, 'Start Searching Above!'); + return h('div', { class: 'infi-blank' }, translations.startSearching); } const loadingSpinner = h('span', { class: 'infi-loading-indicator' }); if (isInitialising) { - const initialisingText = h('div', { class: 'infi-initialising-text' }, '... Starting Up ...'); + const initialisingText = h('div', { class: 'infi-initialising-text' }, translations.startingUp); return h('div', {}, loadingSpinner, initialisingText); } else if (isDone) { return h('div', {});