main
ts 84 lines 3 KB
Raw
1 import Koa from 'koa'
2 import { CFG, Dict, hasProp, onlyTruthy, tryJson } from './misc'
3 import { expiringCache } from './expiringCache'
4 import { readFile } from 'fs/promises'
5 import { defineConfig } from './config'
6 import { watchLoad } from './watchLoad'
7 import EMBEDDED_TRANSLATIONS from './langs/embedded'
8 import { EMBEDDED_LANGUAGE } from './const'
9 import { mapPlugins } from './plugins'
10 import { join } from 'path'
11 import _ from 'lodash'
12
13 const PREFIX = 'hfs-lang-'
14 const SUFFIX = '.json'
15
16 export function normalizeLangCode(code: string) {
17 code = code.toLowerCase().split(';', 1)[0]!.trim()
18 return /^[a-z]{2,3}(-[a-z0-9]{2,8})*$/.test(code) ? code : ''
19 }
20
21 export function code2file(code: string) {
22 return PREFIX + code.toLowerCase() + SUFFIX
23 }
24
25 export function file2code(fn: string) {
26 return fn.replace(PREFIX, '').replace(SUFFIX, '')
27 }
28
29 const cache = expiringCache<Dict>(3_000) // 3 seconds for both a good dx and acceptable performance
30 export async function getLangData(ctxOrLangCsv: Koa.Context | string) {
31 if (typeof ctxOrLangCsv !== 'string') {
32 const ctx = ctxOrLangCsv
33 const param = String(ctx.query.lang || '')
34 if (!param && forceLangData)
35 return forceLangData
36 ctxOrLangCsv = param || ctx.get('Accept-Language') || ''
37 }
38 const csv = ctxOrLangCsv.toLowerCase()
39 return cache.try(csv, async () => {
40 const langs = csv.split(',').map(normalizeLangCode).filter(Boolean)
41 const ret: Dict = {}
42 let i = 0
43 while (i < langs.length) {
44 let k = langs[i] || '' // shut up ts
45 if (!k || k === EMBEDDED_LANGUAGE) break
46 const fn = code2file(k)
47 try { ret[k] = JSON.parse(await readFile(fn, 'utf8')) } // allow external files to override embedded translations
48 catch {
49 if (hasProp(EMBEDDED_TRANSLATIONS, k))
50 ret[k] = _.cloneDeep(EMBEDDED_TRANSLATIONS[k])
51 else {
52 do { k = k.substring(0, k.lastIndexOf('-'))
53 } while (k && langs.includes(k))
54 if (k) {
55 langs[i] = k // overwrite and retry
56 continue
57 }
58 }
59 }
60 const fromPlugins = onlyTruthy(await Promise.all(mapPlugins(async pl =>
61 tryJson(await readFile(join(pl.folder, fn), 'utf8').catch(() => ''))?.translate )))
62 if (fromPlugins.length)
63 _.defaults((ret[k] ||= {}).translate ||= {}, ...fromPlugins) // be sure we have an entry for k
64
65 i++
66 }
67 return ret
68 })
69 }
70
71 let forceLangData: any
72 let undo: any
73 defineConfig(CFG.force_lang, '', v => {
74 undo?.()
75 if (!v)
76 return forceLangData = undefined
77 const translation = (EMBEDDED_TRANSLATIONS as any)[v]
78 forceLangData = { [v]: translation }
79 if (v === EMBEDDED_LANGUAGE) return
80 const res = watchLoad(code2file(v), data => {
81 forceLangData = { [v]: tryJson(data) || translation }
82 })
83 undo = res.unwatch
84 })