Files
Starlight_Launcher/apps/app-frontend/src/helpers/content-search.ts
T

172 lines
5.5 KiB
TypeScript

import type { ContentItem } from '@modrinth/ui'
import { invoke } from '@tauri-apps/api/core'
export interface ChineseSearchTranslation {
chineseName: string
curseforgeSlug?: string
modrinthSlug?: string
matchScore: number
exact: boolean
}
export interface ChineseSearchResolution {
isChinese: boolean
normalizedQuery: string
curseforgeQuery?: string
modrinthQuery?: string
modrinthSlugs: string[]
translations: ChineseSearchTranslation[]
}
export interface ContentSearchExpansion {
suggestedSplit?: string | null
}
export interface ChineseNameLookup {
modrinth: Record<string, string>
curseforge: Record<string, string>
}
export interface WikiIdLookup {
modrinth: Record<string, number>
curseforge: Record<string, number>
}
export function containsChineseSearchText(query: string): boolean {
return /[\u3400-\u4dbf\u4e00-\u9fff]/u.test(query)
}
export function resolveChineseContentSearch(query: string) {
return invoke<ChineseSearchResolution>('plugin:content-search|resolve_chinese_content_search', {
query,
})
}
/**
* Resolves the dictionary-based segmentation of a compact (separator-free)
* search query, e.g. `sodiumextra` → `sodium extra`. Returns `null` when the
* bundled dictionary cannot safely split the query.
*/
export function expandContentSearchQuery(query: string) {
return invoke<ContentSearchExpansion>('plugin:content-search|expand_content_search_query', {
query,
})
}
export function lookupChineseContentNames(modrinthSlugs: string[], curseforgeSlugs: string[]) {
return invoke<ChineseNameLookup>('plugin:content-search|lookup_chinese_content_names', {
modrinthSlugs,
curseforgeSlugs,
})
}
export function lookupContentWikiIds(modrinthSlugs: string[], curseforgeSlugs: string[]) {
return invoke<WikiIdLookup>('plugin:content-search|lookup_content_wiki_ids', {
modrinthSlugs,
curseforgeSlugs,
})
}
/**
* Resolves the MC 百科 (mcmod.cn) page URL for a project slug, or `null`
* when the bundled wiki dictionary has no entry for it.
*/
export async function resolveMcmodUrl(
slug: string | null | undefined,
provider: 'modrinth' | 'curseforge',
): Promise<string | null> {
if (!slug) return null
const lookup = await lookupContentWikiIds(
provider === 'modrinth' ? [slug] : [],
provider === 'curseforge' ? [slug] : [],
).catch(() => null)
const wikiId = provider === 'curseforge' ? lookup?.curseforge[slug] : lookup?.modrinth[slug]
return wikiId ? `https://www.mcmod.cn/class/${wikiId}.html` : null
}
export function bilingualTitle(chineseName: string, originalTitle: string) {
const chineseTitle = chineseName.replace(/\s+\([^()]*[A-Za-z][^()]*\)$/u, '').trim()
if (
!chineseTitle ||
chineseTitle.toLocaleLowerCase() === originalTitle.toLocaleLowerCase() ||
originalTitle.startsWith(`${chineseTitle} (`)
) {
return originalTitle
}
return `${chineseTitle} (${originalTitle})`
}
/**
* Rewrites search hit titles to the bilingual `中文名 (English)` format,
* resolving names from the bundled wiki dictionary by project slug. Hits are
* returned unchanged unless the locale is zh-CN; hits already carrying a
* bilingual title (e.g. from the Chinese search flow) are left untouched.
*/
export async function translateSearchHitTitles<
T extends { slug?: string | null; title: string; provider: 'modrinth' | 'curseforge' },
>(hits: T[], locale: string): Promise<T[]> {
if (locale !== 'zh-CN' || hits.length === 0) return hits
const modrinthSlugs: string[] = []
const curseforgeSlugs: string[] = []
for (const hit of hits) {
if (!hit.slug) continue
if (hit.provider === 'curseforge') curseforgeSlugs.push(hit.slug)
else if (hit.provider === 'modrinth') modrinthSlugs.push(hit.slug)
}
if (modrinthSlugs.length === 0 && curseforgeSlugs.length === 0) return hits
const lookup = await lookupChineseContentNames(modrinthSlugs, curseforgeSlugs).catch(() => null)
if (!lookup) return hits
return hits.map((hit) => {
if (!hit.slug) return hit
const chineseName =
hit.provider === 'curseforge'
? lookup.curseforge[hit.slug]
: hit.provider === 'modrinth'
? lookup.modrinth[hit.slug]
: undefined
if (!chineseName) return hit
return { ...hit, title: bilingualTitle(chineseName, hit.title) }
})
}
/**
* Rewrites content item titles to the bilingual `中文名 (English)` format used
* by the Browse page, resolving names from the bundled wiki dictionary by
* project slug. Items are returned unchanged unless the locale is zh-CN.
*/
export async function translateContentItemTitles<T extends ContentItem>(
items: T[],
locale: string,
): Promise<T[]> {
if (locale !== 'zh-CN' || items.length === 0) return items
const modrinthSlugs: string[] = []
const curseforgeSlugs: string[] = []
for (const item of items) {
const slug = item.project?.slug
if (!slug) continue
if (item.origin_provider === 'curseforge') curseforgeSlugs.push(slug)
else if (item.origin_provider === 'modrinth') modrinthSlugs.push(slug)
}
if (modrinthSlugs.length === 0 && curseforgeSlugs.length === 0) return items
const lookup = await lookupChineseContentNames(modrinthSlugs, curseforgeSlugs).catch(() => null)
if (!lookup) return items
return items.map((item) => {
const project = item.project
if (!project?.slug) return item
const chineseName =
item.origin_provider === 'curseforge'
? lookup.curseforge[project.slug]
: item.origin_provider === 'modrinth'
? lookup.modrinth[project.slug]
: undefined
if (!chineseName) return item
return { ...item, project: { ...project, title: bilingualTitle(chineseName, project.title) } }
})
}