| 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 | }) |