diff --git a/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.tsx b/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.tsx index 3d547020bbc..983ae18c9a5 100644 --- a/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.tsx +++ b/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.tsx @@ -19,6 +19,9 @@ import { Tippy, excludeUndefined, intersection, + getUserNavigatorLanguagesNonEnglish, + getRegionAlternativeNames, + uniqBy, } from "@ourworldindata/utils" import { Checkbox, @@ -81,9 +84,19 @@ interface SortConfig { type SearchableEntity = { name: string + // TODO: Add `regionIfApplicable: Region` property, should be useful + alternativeNames?: string[] | undefined local?: boolean isWorld?: boolean -} & Record + + // TODO test that this still works? + sortColumnValues: Record +} + +// TODO get rid of this +type SearchableEntityForFuzzy = SearchableEntity & { + nameForFuzzy: string +} interface PartitionedEntities { selected: SearchableEntity[] @@ -440,10 +453,13 @@ export class EntitySelector extends React.Component<{ } @computed private get availableEntities(): SearchableEntity[] { + const langs = getUserNavigatorLanguagesNonEnglish() return this.availableEntityNames.map((entityName) => { const searchableEntity: SearchableEntity = { name: entityName, + alternativeNames: getRegionAlternativeNames(entityName, langs), isWorld: entityName === "World", + sortColumnValues: {}, } if (this.localEntityNames) { @@ -453,7 +469,7 @@ export class EntitySelector extends React.Component<{ for (const column of this.sortColumns) { const rows = column.owidRowsByEntityName.get(entityName) ?? [] - searchableEntity[column.slug] = maxBy( + searchableEntity.sortColumnValues[column.slug] = maxBy( rows, (row) => row.originalTime )?.value @@ -518,11 +534,12 @@ export class EntitySelector extends React.Component<{ const [withValues, withoutValues] = partition( entities, (entity: SearchableEntity) => - isFiniteWithGuard(entity[sortConfig.slug]) + isFiniteWithGuard(entity.sortColumnValues[sortConfig.slug]) ) const sortedEntitiesWithValues = orderBy( withValues, - (entity: SearchableEntity) => entity[sortConfig.slug], + (entity: SearchableEntity) => + entity.sortColumnValues[sortConfig.slug], sortConfig.order ) const sortedEntitiesWithoutValues = orderBy( @@ -542,13 +559,38 @@ export class EntitySelector extends React.Component<{ return !this.manager.canChangeEntity } - @computed get fuzzy(): FuzzySearch { - return new FuzzySearch(this.sortedAvailableEntities, "name") + // TODO This is super duper hacky, but currently fuzzy only supports a single key for each entry :( + // TODO So, need to rework FuzzySearch to support multiple keys + @computed + get sortedAvailableEntitiesPreparedForFuzzy(): SearchableEntityForFuzzy[] { + const entities = this.sortedAvailableEntities.map((entity) => ({ + ...entity, + nameForFuzzy: entity.name, + })) + entities.forEach((entity) => { + entity.alternativeNames?.forEach((name) => { + entities.push({ + ...entity, + nameForFuzzy: name, + }) + }) + }) + return entities + } + + @computed get fuzzy(): FuzzySearch { + return new FuzzySearch( + this.sortedAvailableEntitiesPreparedForFuzzy, + "nameForFuzzy" + ) } @computed get searchResults(): SearchableEntity[] | undefined { if (!this.searchInput) return undefined - return this.fuzzy.search(this.searchInput) + return uniqBy( + this.fuzzy.search(this.searchInput), + (entity) => entity.name + ) } @computed get partitionedSearchResults(): PartitionedEntities | undefined { @@ -847,7 +889,7 @@ export class EntitySelector extends React.Component<{ if (!displayColumn) return undefined - const value = entity[displayColumn.slug] + const value = entity.sortColumnValues[displayColumn.slug] if (!isFiniteWithGuard(value)) return { formattedValue: "No data" } diff --git a/packages/@ourworldindata/utils/src/Util.ts b/packages/@ourworldindata/utils/src/Util.ts index 9908fd28753..7273675707d 100644 --- a/packages/@ourworldindata/utils/src/Util.ts +++ b/packages/@ourworldindata/utils/src/Util.ts @@ -2058,3 +2058,11 @@ export function isArrayDifferentFromReference( if (array.length !== referenceArray.length) return true return difference(array, referenceArray).length > 0 } + +export const getUserNavigatorLanguages = (): readonly string[] => { + return navigator.languages ?? [navigator.language] +} + +export const getUserNavigatorLanguagesNonEnglish = (): readonly string[] => { + return getUserNavigatorLanguages().filter((lang) => !lang.startsWith("en")) +} diff --git a/packages/@ourworldindata/utils/src/index.ts b/packages/@ourworldindata/utils/src/index.ts index 1a98410e6ce..c877d8ceeae 100644 --- a/packages/@ourworldindata/utils/src/index.ts +++ b/packages/@ourworldindata/utils/src/index.ts @@ -128,6 +128,8 @@ export { lazy, getParentVariableIdFromChartConfig, isArrayDifferentFromReference, + getUserNavigatorLanguages, + getUserNavigatorLanguagesNonEnglish, } from "./Util.js" export { @@ -259,6 +261,7 @@ export { type Aggregate, getOthers, countriesByName, + getRegionAlternativeNames, } from "./regions.js" export { getStylesForTargetHeight } from "./react-select.js" diff --git a/packages/@ourworldindata/utils/src/regions.ts b/packages/@ourworldindata/utils/src/regions.ts index 0d98a9589e6..13c07908510 100644 --- a/packages/@ourworldindata/utils/src/regions.ts +++ b/packages/@ourworldindata/utils/src/regions.ts @@ -115,3 +115,58 @@ export const getRegionByNameOrVariantName = ( nameOrVariantName: string ): Region | undefined => regionsByNameOrVariantNameLowercase().get(nameOrVariantName.toLowerCase()) + +const _IntlDisplayNamesInstances = new Map() +const getRegionTranslation = ( + regionCode: string, + languageCode: string +): string | undefined => { + try { + if (!_IntlDisplayNamesInstances.has(languageCode)) { + _IntlDisplayNamesInstances.set( + languageCode, + new Intl.DisplayNames([languageCode], { + type: "region", + fallback: "none", + }) + ) + } + return _IntlDisplayNamesInstances.get(languageCode)!.of(regionCode) + } catch { + return undefined + } +} + +const _regionAlternativeNames = new Map() +export const getRegionAlternativeNames = ( + regionName: string, + languages: readonly string[] +): string[] | undefined => { + if (!_regionAlternativeNames.has(regionName)) { + const region = getRegionByNameOrVariantName(regionName) + if (region) { + const names = new Set() + if ("variantNames" in region && region.variantNames) { + for (const variant of region.variantNames) { + names.add(variant) + } + } + + if ("shortCode" in region && region.shortCode) { + const regionCode = region.shortCode + + const translations = languages + .map((lang) => getRegionTranslation(regionCode, lang)) + .filter((name) => name !== undefined) + + for (const translation of translations) { + names.add(translation) + } + } + _regionAlternativeNames.set(regionName, Array.from(names)) + } else { + return undefined + } + } + return _regionAlternativeNames.get(regionName)! +}