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:
+61
-46
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user