ui-next: inject i18n, code-langs & version (#1193)

This commit is contained in:
Baoshuo authored and GitHub committed 2026-07-30 20:17:56 +08:00
1 parent b9118113cb
commit a8ec204a04
2 files changed
+95 -9

No files matched your search

+94 -9
View File
@@ -3,10 +3,10 @@ import path from 'path';
import esbuild from 'esbuild';
import c2k from 'koa2-connect/ts';
import { createServer, type Plugin } from 'vite';
import { serializer } from '@hydrooj/framework';
import { HandlerCommon, serializer } from '@hydrooj/framework';
import {
Context, Handler, Logger,
NotFoundError, param, sha1, size, Types,
NotFoundError, param, SettingModel, sha1, size, Types,
} from 'hydrooj';
const logger = new Logger('ui-next');
@@ -112,6 +112,50 @@ function addFile(name: string, content: string) {
hashes[name] = sha1(content).substring(0, 8);
}
async function buildI18n() {
const localeList: Record<string, { name: string, flag: string }> = {};
for (const lang in global.Hydro.locales) {
if (!/^[a-zA-Z_]+$/.test(lang)) continue;
if (!global.Hydro.locales[lang].__interface) continue;
addFile(`lang-${lang}.js`, `window.HydroLocale=${JSON.stringify(global.Hydro.locales[lang][Symbol.for('iterate')])};`);
const id = global.Hydro.locales[lang].__id;
if (id) localeList[id] = { name: global.Hydro.locales[lang].__langname, flag: global.Hydro.locales[lang].__flag };
}
addFile('locale-list.js', `window.HydroLocaleList=${JSON.stringify(localeList)};`);
}
async function buildCodeLangs() {
addFile('code-langs.js', `window.HydroCodeLangs=${JSON.stringify(SettingModel.langs)};`);
}
async function buildVersions() {
const versions: Record<string, string> = { ...global.Hydro.version };
try {
const { simpleGit } = require('simple-git') as typeof import('simple-git');
const fetchAddonVersion = async (name: string, addonPath: string) => {
try {
const git = simpleGit(addonPath);
const [log, status] = await Promise.all([git.log(), git.status()]);
if (log.all.length > 0) {
let hash = log.all[0].hash.substring(0, 7);
if (!status.isClean()) hash += '-dirty';
versions[name] = versions[name] ? `${versions[name]}-${hash}` : hash;
}
} catch (e) {
logger.debug('Could not get git hash for addon %s: %o', name, e);
}
};
await Promise.all(
Object.entries(global.addons)
.filter(([name]) => name !== 'hydrooj') // already handled in loader.ts
.map(([name, addonPath]) => fetchAddonVersion(name, addonPath as string)),
);
} catch (e) {
logger.debug('simple-git not available: %o', e);
}
addFile('versions.js', `window.HydroVersions=${JSON.stringify(versions)};`);
}
class UiNextConstantHandler extends Handler {
noCheckPermView = true;
@@ -219,10 +263,32 @@ export async function buildPlugins() {
}
}
const HASH_FALLBACK = '00000000';
const getViewLang = (handler: HandlerCommon) => handler.user?.viewLang || handler.session?.viewLang || 'zh';
const injectedScripts = (resolve: (name: string) => string, viewLang: string) => [
'code-langs.js',
'locale-list.js',
`lang-${viewLang}.js`,
'versions.js',
].map((name) => `<script src="${resolve(name)}"></script>`);
export async function apply(ctx: Context) {
if (process.env.HYDRO_CLI) return;
ctx.Route('ui_next_constants', '/plugins/:version/:name', UiNextConstantHandler);
if (process.env.DEV) {
ctx.on('app/started', async () => {
await buildI18n();
await buildCodeLangs();
await buildVersions();
});
ctx.on('app/i18n/update', buildI18n);
ctx.on('system/setting-loaded', buildCodeLangs);
ctx.on('system/setting', buildCodeLangs);
const vite = await createServer({
root: __dirname,
clearScreen: false,
@@ -265,7 +331,13 @@ export async function apply(ctx: Context) {
route_map: ctx.server.routeMap,
endpoint: ctx.setting.get('server.url') || undefined,
}, serializer(false, context.handler));
const htmlToRender = html.replace(INJECT_MARKER, buildInject(serialized));
const ts = Date.now();
const devAssetUrl = (name: string) => `/plugins/0/${name}?_=${ts}`;
const injectHtml = [
buildInject(serialized),
...injectedScripts(devAssetUrl, getViewLang(context.handler)),
].join('\n');
const htmlToRender = html.replace(INJECT_MARKER, injectHtml);
return await vite.transformIndexHtml(context.handler.context.req.url!, htmlToRender);
},
});
@@ -275,7 +347,14 @@ export async function apply(ctx: Context) {
await vite.close().catch((e) => console.error(e));
};
} else {
ctx.Route('ui_next_constants', '/plugins/:version/:name', UiNextConstantHandler);
const build = async () => {
await buildPlugins();
await buildI18n();
await buildCodeLangs();
await buildVersions();
};
ctx.on('app/started', build);
ctx.server.registerRenderer('next', {
name: 'next',
accept: [],
@@ -298,19 +377,25 @@ export async function apply(ctx: Context) {
url: context.handler.context.req.url!,
route_map: ctx.server.routeMap,
endpoint: ctx.setting.get('server.url') || undefined,
plugins_url: `/plugins/${hashes['plugins.js'] || '00000000'}/plugins.js`,
plugins_url: `/plugins/${hashes['plugins.js'] || HASH_FALLBACK}/plugins.js`,
}, serializer(false, context.handler));
return html.replace(INJECT_MARKER, buildInject(serialized));
const prodAssetUrl = (name: string) => `/plugins/${hashes[name] || HASH_FALLBACK}/${name}`;
const injectHtml = [
buildInject(serialized),
...injectedScripts(prodAssetUrl, getViewLang(context.handler)),
].join('\n');
return html.replace(INJECT_MARKER, injectHtml);
},
});
ctx.on('app/started', buildPlugins);
const debouncedBuild = ctx.debounce(buildPlugins, 2000);
const debouncedBuild = ctx.debounce(build, 2000);
const triggerHotUpdate = (filePath?: string) => {
if (filePath && !filePath.includes('/ui/')) return;
debouncedBuild();
};
ctx.on('app/watch/change', triggerHotUpdate);
ctx.on('app/watch/unlink', triggerHotUpdate);
ctx.on('system/setting', () => debouncedBuild());
ctx.on('system/setting-loaded', buildCodeLangs);
ctx.on('system/setting', debouncedBuild);
ctx.on('app/i18n/update', debouncedBuild);
}
}
+1
View File
@@ -29,6 +29,7 @@
"esbuild": "0.25.2",
"is-relative-url": "^4.1.0",
"path-to-regexp": "^8.4.2",
"simple-git": "^3.36.0",
"react": "^19.2.8",
"react-dom": "^19.2.8"
},