Update Owliver language and suggestions
This commit is contained in:
126
scripts/i18n-coverage.mjs
Normal file
126
scripts/i18n-coverage.mjs
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* How much of each page is actually translated.
|
||||
*
|
||||
* Renders every migrated page twice — once in English, once in Spanish — and
|
||||
* reports the text that did NOT change. That residue is the untranslated part,
|
||||
* and it is the only honest measure of coverage: a catalogue can be full of
|
||||
* Spanish that no component ever calls, and counting keys would score that as
|
||||
* finished.
|
||||
*
|
||||
* Reports rather than asserts. The point is a number per page while the work is
|
||||
* half done; `skill-check.mjs` is where a finished page gets a check that keeps
|
||||
* it finished.
|
||||
*
|
||||
* node scripts/i18n-coverage.mjs
|
||||
*/
|
||||
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { createServer } from 'vite';
|
||||
import React from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { withSourceResolution } from './ssr-resolve.mjs';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
/* The pages reach for `window` on the way up — a media query, a resize
|
||||
listener. The same minimal stub the skill-check uses, installed before any
|
||||
page module is loaded. */
|
||||
if (!('window' in globalThis)) {
|
||||
globalThis.window = {
|
||||
matchMedia: () => ({ matches: false, addEventListener() {}, removeEventListener() {} }),
|
||||
addEventListener() {}, removeEventListener() {},
|
||||
localStorage: { getItem: () => null, setItem() {}, removeItem() {} },
|
||||
};
|
||||
}
|
||||
|
||||
const server = await createServer({
|
||||
root: ROOT,
|
||||
server: { middlewareMode: true },
|
||||
appType: 'custom',
|
||||
logLevel: 'error',
|
||||
resolve: { alias: { 'react-hot-toast': join(ROOT, 'scripts/stubs/react-hot-toast.js') } },
|
||||
});
|
||||
withSourceResolution(server);
|
||||
|
||||
const { QueryClient, QueryClientProvider } = await import('@tanstack/react-query');
|
||||
const { MemoryRouter } = await import('react-router-dom');
|
||||
const { UiEditingProvider } = await server.ssrLoadModule('/src/components/ui-tree/UiEditingProvider.jsx');
|
||||
const i18n = (await server.ssrLoadModule('/src/lib/i18n/index.ts')).default;
|
||||
|
||||
const PAGES = [
|
||||
['Control Center', '/src/pages/admin/ControlCenter.jsx', '/admin', 'control-center'],
|
||||
['Positions', '/src/pages/admin/Positions.jsx', '/admin/positions', 'positions'],
|
||||
['Candidates', '/src/pages/admin/Candidates.jsx', '/admin/candidates', 'candidates'],
|
||||
['Talent Pool', '/src/pages/admin/TalentPool.jsx', '/admin/talent-pool', 'talent-pool'],
|
||||
['Analytics', '/src/pages/admin/Analytics.jsx', '/admin/analytics', 'analytics'],
|
||||
['Hired History', '/src/pages/admin/HiredHistory.jsx', '/admin/hired', 'hired-history'],
|
||||
['Activity', '/src/pages/admin/Activity.jsx', '/admin/activity', 'activity'],
|
||||
];
|
||||
|
||||
/* Text only, split into phrases on the punctuation the interface uses to
|
||||
separate one label from the next, so "17 total" counts separately from the
|
||||
heading above it. Numbers and symbols alone are not evidence either way —
|
||||
"27" is the same in both languages — so anything without a letter is
|
||||
dropped rather than scored as translated. */
|
||||
const phrases = (html) =>
|
||||
html
|
||||
.replace(/<[^>]*>/g, '\n')
|
||||
.split(/[\n·|•]+/)
|
||||
.map((p) => p.replace(/\s+/g, ' ').trim())
|
||||
.filter((p) => p.length > 2 && /[A-Za-z]{3}/.test(p));
|
||||
|
||||
async function render(path, page) {
|
||||
const Page = (await server.ssrLoadModule(path)).default;
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false, enabled: false } } });
|
||||
return renderToStaticMarkup(
|
||||
React.createElement(MemoryRouter, { initialEntries: [page.route] },
|
||||
React.createElement(QueryClientProvider, { client },
|
||||
React.createElement(UiEditingProvider, { page: page.key },
|
||||
React.createElement(Page))))
|
||||
);
|
||||
}
|
||||
|
||||
console.log('\n Translation coverage — text that changes when the language does\n');
|
||||
|
||||
let totalSame = 0;
|
||||
let totalAll = 0;
|
||||
|
||||
for (const [name, path, route, key] of PAGES) {
|
||||
let en;
|
||||
let es;
|
||||
try {
|
||||
await i18n.changeLanguage('en');
|
||||
en = phrases(await render(path, { route, key }));
|
||||
await i18n.changeLanguage('es');
|
||||
es = phrases(await render(path, { route, key }));
|
||||
await i18n.changeLanguage('en');
|
||||
} catch (err) {
|
||||
console.log(` ${name.padEnd(16)} could not render — ${String(err.message).slice(0, 60)}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* A phrase present in BOTH renders is one the switch did not touch. Proper
|
||||
nouns and figures land here too, which is why the sample below is printed:
|
||||
a human reads it and sees at once whether the residue is "KROW Forge" or a
|
||||
whole untranslated paragraph. */
|
||||
const esSet = new Set(es);
|
||||
const untouched = en.filter((p) => esSet.has(p));
|
||||
|
||||
totalSame += untouched.length;
|
||||
totalAll += en.length;
|
||||
|
||||
const pct = en.length ? Math.round(((en.length - untouched.length) / en.length) * 100) : 0;
|
||||
const bar = '█'.repeat(Math.round(pct / 5)).padEnd(20, '·');
|
||||
console.log(` ${name.padEnd(16)} ${bar} ${String(pct).padStart(3)}% ${en.length - untouched.length}/${en.length} phrases`);
|
||||
|
||||
if (untouched.length) {
|
||||
const sample = untouched.filter((p) => /[a-z]{4}/.test(p)).slice(0, 4);
|
||||
if (sample.length) console.log(` ${''.padEnd(16)} still English: ${sample.join(' / ').slice(0, 96)}`);
|
||||
}
|
||||
}
|
||||
|
||||
const overall = totalAll ? Math.round(((totalAll - totalSame) / totalAll) * 100) : 0;
|
||||
console.log(`\n Overall: ${overall}% of rendered phrases change with the language.\n`);
|
||||
|
||||
await server.close();
|
||||
Reference in New Issue
Block a user