Skip to content

Commit

Permalink
poc: multi-language search in EntitySelector
Browse files Browse the repository at this point in the history
  • Loading branch information
marcelgerber committed Dec 22, 2024
1 parent c3a1fb3 commit 003a4eb
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ import {
Tippy,
excludeUndefined,
intersection,
getUserNavigatorLanguagesNonEnglish,
getRegionAlternativeNames,
uniqBy,
} from "@ourworldindata/utils"
import {
Checkbox,
Expand Down Expand Up @@ -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<ColumnSlug, CoreValueType | undefined>

// TODO test that this still works?
sortColumnValues: Record<ColumnSlug, CoreValueType | undefined>
}

// TODO get rid of this
type SearchableEntityForFuzzy = SearchableEntity & {
nameForFuzzy: string
}

interface PartitionedEntities {
selected: SearchableEntity[]
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -542,13 +559,38 @@ export class EntitySelector extends React.Component<{
return !this.manager.canChangeEntity
}

@computed get fuzzy(): FuzzySearch<SearchableEntity> {
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<SearchableEntityForFuzzy> {
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 {
Expand Down Expand Up @@ -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" }

Expand Down
8 changes: 8 additions & 0 deletions packages/@ourworldindata/utils/src/Util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2058,3 +2058,11 @@ export function isArrayDifferentFromReference<T>(
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"))
}
3 changes: 3 additions & 0 deletions packages/@ourworldindata/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ export {
lazy,
getParentVariableIdFromChartConfig,
isArrayDifferentFromReference,
getUserNavigatorLanguages,
getUserNavigatorLanguagesNonEnglish,
} from "./Util.js"

export {
Expand Down Expand Up @@ -259,6 +261,7 @@ export {
type Aggregate,
getOthers,
countriesByName,
getRegionAlternativeNames,
} from "./regions.js"

export { getStylesForTargetHeight } from "./react-select.js"
Expand Down
55 changes: 55 additions & 0 deletions packages/@ourworldindata/utils/src/regions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,58 @@ export const getRegionByNameOrVariantName = (
nameOrVariantName: string
): Region | undefined =>
regionsByNameOrVariantNameLowercase().get(nameOrVariantName.toLowerCase())

const _IntlDisplayNamesInstances = new Map<string, Intl.DisplayNames>()
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<string, string[] | undefined>()
export const getRegionAlternativeNames = (
regionName: string,
languages: readonly string[]
): string[] | undefined => {
if (!_regionAlternativeNames.has(regionName)) {
const region = getRegionByNameOrVariantName(regionName)
if (region) {
const names = new Set<string>()
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)!
}

0 comments on commit 003a4eb

Please sign in to comment.