Expand i18n locales and improve reviews UI

Add many new language packs with RTL support, refresh brand assets, and harden review rendering with tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
michilis
2026-08-21 05:06:58 +02:00
co-authored by Cursor
parent 1c5df18e81
commit 47e6537dde
51 changed files with 15291 additions and 246 deletions
+61 -46
View File
@@ -6,6 +6,7 @@
* files change roughly never.
*
* node scripts/make-assets.mjs
* node scripts/make-assets.mjs --no-card # icon set only, needs no browser
*
* Playwright and sharp are not dependencies of this workspace. Both arrive transitively
* under `astro` (which ships sharp for its image pipeline) and are resolved through it,
@@ -14,10 +15,12 @@
*
* The card is drawn in a real browser rather than assembled by hand because it uses the
* site's own fonts and tokens: a social card that does not look like the site is worth
* less than no card. The moai is the same rect geometry as `components/Moai.astro`.
* less than no card. The moai on the card is the same rect geometry as
* `components/Moai.astro`. The icon set is a different mark: the pixel cashu nut carried
* over from the old site (`src/assets/cashu-logo.png`), composed with sharp alone.
*/
import { createRequire } from 'node:module';
import { mkdir, writeFile } from 'node:fs/promises';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -100,14 +103,8 @@ p { margin-top: 28px; font-size: 27px; line-height: 1.45; color: #9A927E; max-wi
<div class="host">cashumints.space</div>
</body></html>`;
/* The icon master: the moai on the site's background, generous padding. */
const iconHtml = `<!doctype html><html><head><meta charset="utf-8"><style>
* { margin: 0; padding: 0; }
body {
width: 512px; height: 512px; background: #0E0D0B;
display: flex; align-items: center; justify-content: center;
}
</style></head><body>${moaiSvg(320)}</body></html>`;
/* The icon source: the old site's pixel cashu nut, 1000x1000 with transparent margins. */
const logoPath = path.join(root, 'src', 'assets', 'cashu-logo.png');
/**
* A 32x32 ICO wrapping a PNG payload.
@@ -138,46 +135,64 @@ function icoFromPng(png) {
await mkdir(publicDir, { recursive: true });
const browser = await chromium.launch();
try {
const page = await browser.newPage({
viewport: { width: 1200, height: 630 },
deviceScaleFactor: 1,
});
/*
* The icon master: the nut trimmed and centred on the brand ground, at the footprint the
* moai used to occupy (glyph ≈ 380px tall in the 512 tile). Content this size also stays
* inside the circle a maskable icon is cropped to.
*/
const glyph = await sharp(logoPath).trim().resize(384, 384, { fit: 'inside' }).png().toBuffer();
const master = await sharp({
create: { width: 512, height: 512, channels: 4, background: '#0E0D0B' },
})
.composite([{ input: glyph, gravity: 'centre' }])
.png()
.toBuffer();
await page.setContent(cardHtml, { waitUntil: 'load' });
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: path.join(publicDir, 'og.png') });
console.log('og.png');
for (const [name, size] of [
['icon-512.png', 512],
['icon-192.png', 192],
['favicon-32.png', 32],
]) {
await sharp(master).resize(size, size).png({ compressionLevel: 9 }).toFile(path.join(publicDir, name));
console.log(name);
}
await page.setViewportSize({ width: 512, height: 512 });
await page.setContent(iconHtml, { waitUntil: 'load' });
const master = await page.screenshot({ type: 'png' });
// iOS ignores transparency and composites on white; the master's ground is already opaque.
await sharp(master)
.resize(180, 180)
.png({ compressionLevel: 9 })
.toFile(path.join(publicDir, 'apple-touch-icon.png'));
console.log('apple-touch-icon.png');
for (const [name, size] of [
['icon-512.png', 512],
['icon-192.png', 192],
['favicon-32.png', 32],
]) {
await sharp(master).resize(size, size).png({ compressionLevel: 9 }).toFile(path.join(publicDir, name));
console.log(name);
}
const png32 = await sharp(master).resize(32, 32).png({ compressionLevel: 9 }).toBuffer();
await writeFile(path.join(publicDir, 'favicon.ico'), icoFromPng(png32));
console.log('favicon.ico');
// iOS ignores transparency and composites on white, so flatten onto the brand ground.
await sharp(master)
.resize(180, 180)
.flatten({ background: '#0E0D0B' })
.png({ compressionLevel: 9 })
.toFile(path.join(publicDir, 'apple-touch-icon.png'));
console.log('apple-touch-icon.png');
// The standalone SVG favicon: the nut itself, transparent like the old site served it.
// The PNG rides inside the SVG so one source file covers every size; `pixelated` keeps
// the pixel art crisp if anything scales it up.
const logoB64 = (await readFile(logoPath)).toString('base64');
await writeFile(
path.join(publicDir, 'favicon.svg'),
`<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="1000" viewBox="0 0 1000 1000">` +
`<image width="1000" height="1000" style="image-rendering:pixelated" ` +
`href="data:image/png;base64,${logoB64}"/></svg>`,
);
console.log('favicon.svg');
const png32 = await sharp(master).resize(32, 32).png({ compressionLevel: 9 }).toBuffer();
await writeFile(path.join(publicDir, 'favicon.ico'), icoFromPng(png32));
console.log('favicon.ico');
if (!process.argv.includes('--no-card')) {
const browser = await chromium.launch();
try {
const page = await browser.newPage({
viewport: { width: 1200, height: 630 },
deviceScaleFactor: 1,
});
// The standalone SVG favicon: the same mark, at any size, for browsers that take it.
await writeFile(path.join(publicDir, 'favicon.svg'), moaiSvg(11));
console.log('favicon.svg');
} finally {
await browser.close();
await page.setContent(cardHtml, { waitUntil: 'load' });
await page.evaluate(() => document.fonts.ready);
await page.screenshot({ path: path.join(publicDir, 'og.png') });
console.log('og.png');
} finally {
await browser.close();
}
}