test-article-reading-i18n.mjs 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. import assert from 'node:assert/strict'
  2. import { readFile, readdir } from 'node:fs/promises'
  3. import path from 'node:path'
  4. const root = new URL('../', import.meta.url)
  5. const read = p => readFile(new URL(p, root), 'utf8')
  6. const locales = {}
  7. for (const loc of ['cn', 'en', 'hk']) {
  8. locales[loc] = JSON.parse(await read(`src/views/lang/${loc}.json`))
  9. }
  10. // 扫描源码中真正引用到的 ssArticle* 键,而不是维护一张会过期的手写清单
  11. const scanRoots = [
  12. 'src/views/Editor/EnglishSpeaking',
  13. 'src/views/Student/components/ArticleReadingClassPanel',
  14. 'src/views/components/element/FrameElement',
  15. 'src/views/Editor/CanvasTool',
  16. 'src/components/CollapsibleToolbar',
  17. 'src/views/Student',
  18. ]
  19. async function collectFiles(dir) {
  20. const out = []
  21. let entries
  22. try {
  23. entries = await readdir(new URL(dir + '/', root), { withFileTypes: true })
  24. } catch {
  25. return out
  26. }
  27. for (const entry of entries) {
  28. const child = path.posix.join(dir, entry.name)
  29. if (entry.isDirectory()) out.push(...await collectFiles(child))
  30. else if (/\.(vue|ts)$/.test(entry.name)) out.push(child)
  31. }
  32. return out
  33. }
  34. const files = (await Promise.all(scanRoots.map(collectFiles))).flat()
  35. const used = new Set()
  36. for (const file of files) {
  37. const source = await read(file)
  38. for (const match of source.matchAll(/\blang\.(ssArticle[A-Za-z0-9]*)/g)) used.add(match[1])
  39. }
  40. assert.ok(used.size > 20, `expected many article keys in use, found ${used.size}`)
  41. // 每个被引用的键在三个语言包里都必须存在、非空、无首尾空白
  42. const missing = []
  43. for (const key of [...used].sort()) {
  44. for (const loc of ['cn', 'en', 'hk']) {
  45. const value = locales[loc][key]
  46. if (typeof value !== 'string' || value.length === 0) {
  47. missing.push(`${loc}.json missing ${key}`)
  48. continue
  49. }
  50. assert.equal(value, value.trim(), `${loc}.json ${key} has surrounding whitespace`)
  51. }
  52. }
  53. assert.deepEqual(missing, [], `unresolved article i18n keys:\n${missing.join('\n')}`)
  54. // 三个语言包的 ssArticle* 键集合必须完全一致,防止某个语言漏翻
  55. const keySets = Object.fromEntries(
  56. Object.entries(locales).map(([loc, pack]) => [
  57. loc,
  58. Object.keys(pack).filter(key => key.startsWith('ssArticle')).sort(),
  59. ]),
  60. )
  61. assert.deepEqual(keySets.en, keySets.cn, 'en.json article keys differ from cn.json')
  62. assert.deepEqual(keySets.hk, keySets.cn, 'hk.json article keys differ from cn.json')
  63. // en 与 cn 的文案不应逐字相同(除少数本就一致的符号型文案)
  64. const identical = keySets.cn.filter(key => locales.en[key] === locales.cn[key])
  65. assert.ok(
  66. identical.length <= 2,
  67. `en.json looks untranslated for: ${identical.join(', ')}`,
  68. )
  69. console.log(`article reading i18n: ${used.size} keys used, ${keySets.cn.length} defined, all three locales consistent`)