fix: localize skin site UI and renew JWT on startup
Catalog missing skin-site messages and check source IDs during i18n validation. Renew the saved JWT before loading session data, guard overlapping login/logout requests, and reload announcements after authentication with a retry action.
This commit is contained in:
1 parent
2d7683b0bb
commit
7f3a1cdf5c
11 files changed
+4744
-2341
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { parse, TYPE } from '@formatjs/icu-messageformat-parser'
|
||||
import { collectSkinSiteMessages } from './skin-site-messages.mjs'
|
||||
|
||||
const localePairs = [
|
||||
[
|
||||
@@ -10,6 +11,22 @@ const localePairs = [
|
||||
]
|
||||
|
||||
const failures = []
|
||||
const skinSiteCatalogs = Object.fromEntries(
|
||||
await Promise.all(
|
||||
['en-US', 'zh-CN'].map(async (locale) => [
|
||||
locale,
|
||||
JSON.parse(await readFile(`apps/app-frontend/src/locales/${locale}/index.json`, 'utf8')),
|
||||
]),
|
||||
),
|
||||
)
|
||||
for (const { id, file } of collectSkinSiteMessages()) {
|
||||
for (const locale of ['en-US', 'zh-CN']) {
|
||||
const catalogPath = `apps/app-frontend/src/locales/${locale}/index.json`
|
||||
const catalog = skinSiteCatalogs[locale]
|
||||
if (!catalog[id]?.message)
|
||||
failures.push(`${catalogPath}: missing source message ${id} (${file})`)
|
||||
}
|
||||
}
|
||||
const allowedUntranslatedMessages = new Set([
|
||||
'Chaos Cubed',
|
||||
'MINECON Earth 2017',
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import fs from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
import { collectSkinSiteMessages } from './skin-site-messages.mjs'
|
||||
|
||||
test('all skin-site messages used in source exist in both release language catalogs', () => {
|
||||
const messages = collectSkinSiteMessages()
|
||||
assert.ok(messages.length > 100, 'The check must scan the actual UI')
|
||||
for (const locale of ['en-US', 'zh-CN']) {
|
||||
const catalog = JSON.parse(
|
||||
fs.readFileSync(`apps/app-frontend/src/locales/${locale}/index.json`, 'utf8'),
|
||||
)
|
||||
const missing = messages.filter(({ id }) => !catalog[id]?.message)
|
||||
assert.deepEqual(
|
||||
missing.map(({ id }) => id),
|
||||
[],
|
||||
`${locale} has uncatalogued UI messages`,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
test('Chinese login and announcement labels do not fall back to English', () => {
|
||||
const catalog = JSON.parse(
|
||||
fs.readFileSync('apps/app-frontend/src/locales/zh-CN/index.json', 'utf8'),
|
||||
)
|
||||
for (const id of [
|
||||
'app.skin-site.login.title',
|
||||
'app.skin-site.login.username',
|
||||
'app.skin-site.login.password-hint',
|
||||
'app.skin-site.home.announcements.title',
|
||||
'app.skin-site.home.announcements.failed',
|
||||
]) {
|
||||
assert.match(catalog[id]?.message ?? '', /\p{Script=Han}/u, id)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,59 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { createRequire } from 'node:module'
|
||||
|
||||
const require = createRequire(new URL('../../apps/app-frontend/package.json', import.meta.url))
|
||||
const ts = require('typescript')
|
||||
const { parse } = require('vue/compiler-sfc')
|
||||
|
||||
/** Read the message IDs actually used by skin-site UI, including uncatalogued messages. */
|
||||
export function collectSkinSiteMessages(root = 'apps/app-frontend/src') {
|
||||
const messages = new Map()
|
||||
function walk(directory) {
|
||||
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
||||
const file = path.join(directory, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name !== 'locales') walk(file)
|
||||
continue
|
||||
}
|
||||
if (!/\.(vue|ts)$/.test(file) || file.endsWith('.test.ts')) continue
|
||||
let source = fs.readFileSync(file, 'utf8')
|
||||
if (file.endsWith('.vue')) {
|
||||
const { descriptor } = parse(source)
|
||||
source = [descriptor.script?.content, descriptor.scriptSetup?.content]
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
}
|
||||
const ast = ts.createSourceFile(`${file}.ts`, source, ts.ScriptTarget.Latest, true)
|
||||
function visit(node) {
|
||||
if (ts.isObjectLiteralExpression(node)) {
|
||||
const properties = Object.fromEntries(
|
||||
node.properties
|
||||
.filter(ts.isPropertyAssignment)
|
||||
.map((property) => [
|
||||
property.name.getText(ast).replace(/^['"]|['"]$/g, ''),
|
||||
property.initializer,
|
||||
]),
|
||||
)
|
||||
if (
|
||||
properties.id &&
|
||||
properties.defaultMessage &&
|
||||
ts.isStringLiteralLike(properties.id) &&
|
||||
ts.isStringLiteralLike(properties.defaultMessage) &&
|
||||
properties.id.text.startsWith('app.skin-site.')
|
||||
) {
|
||||
messages.set(properties.id.text, {
|
||||
id: properties.id.text,
|
||||
defaultMessage: properties.defaultMessage.text,
|
||||
file,
|
||||
})
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit)
|
||||
}
|
||||
visit(ast)
|
||||
}
|
||||
}
|
||||
walk(root)
|
||||
return [...messages.values()]
|
||||
}
|
||||
Reference in new issue
Block a user