From 6c5047ba3eef4fb424fbadda206f0cea6664ca75 Mon Sep 17 00:00:00 2001 From: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com> Date: Wed, 26 Mar 2025 13:23:53 -0700 Subject: [PATCH] Highlight components in ICU Book --- .github/workflows/icu-book.yml | 11 +++++- .storybook/icu-lookup.html | 3 +- .storybook/test-runner.ts | 43 +++++++++++++++++++---- ts/scripts/compile-stories-icu-lookup.ts | 44 ++++++++++-------------- ts/types/Util.ts | 2 +- ts/util/setupI18nMain.ts | 10 +++--- 6 files changed, 73 insertions(+), 40 deletions(-) diff --git a/.github/workflows/icu-book.yml b/.github/workflows/icu-book.yml index 98e245b7dc..3248804f48 100644 --- a/.github/workflows/icu-book.yml +++ b/.github/workflows/icu-book.yml @@ -52,7 +52,16 @@ jobs: ARTIFACTS_DIR: stories/data - run: pnpm run build:esbuild - run: node ts/scripts/compile-stories-icu-lookup.js stories - - name: Upload artifacts + + - name: Upload test artifacts + if: github.event_name == 'workflow_dispatch' + uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4 + with: + name: desktop-test-icu + path: stories + + - name: Upload release artifacts + if: github.event_name != 'workflow_dispatch' uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4 with: name: desktop-${{ github.ref_name }}-icu diff --git a/.storybook/icu-lookup.html b/.storybook/icu-lookup.html index 89009f33f0..ce87ac7ead 100644 --- a/.storybook/icu-lookup.html +++ b/.storybook/icu-lookup.html @@ -56,7 +56,8 @@ story.appendChild(title); const img = document.createElement('img'); - img.src = `data/${encodeURIComponent(storyId)}/screenshot.png`; + img.src = `data/${encodeURIComponent(storyId)}/` + + `${encodeURIComponent(key.replace(/^icu:/, ''))}.jpg`; img.loading = 'lazy'; story.appendChild(img); } diff --git a/.storybook/test-runner.ts b/.storybook/test-runner.ts index dc8d34760d..04e1a0701e 100644 --- a/.storybook/test-runner.ts +++ b/.storybook/test-runner.ts @@ -8,6 +8,8 @@ import { waitForPageReady, } from '@storybook/test-runner'; +const SECOND = 1000; + const { ARTIFACTS_DIR } = process.env; const config: TestRunnerConfig = { @@ -38,15 +40,44 @@ const config: TestRunnerConfig = { return; } - const image = await page.screenshot({ fullPage: true }); - const dir = join(ARTIFACTS_DIR, context.id); await mkdir(dir, { recursive: true }); - await Promise.all([ - writeFile(join(dir, 'screenshot.png'), image), - writeFile(join(dir, 'strings.json'), JSON.stringify(result)), - ]); + for (const [key, value] of result) { + const locator = page + .getByText(value) + .or(page.getByTitle(value)) + .or(page.getByLabel(value)); + + if (await locator.count()) { + const first = locator.first(); + + try { + await first.focus({ timeout: SECOND }); + } catch { + // Opportunistic + } + try { + if (await first.isVisible()) { + await first.scrollIntoViewIfNeeded({ timeout: SECOND }); + } + } catch { + // Opportunistic + } + } + + const image = await page.screenshot({ + animations: 'disabled', + fullPage: true, + mask: [locator], + // Semi-transparent ultramarine + maskColor: 'rgba(44, 107, 273, 0.3)', + type: 'jpeg', + quality: 95, + }); + + await writeFile(join(dir, `${key.replace(/^icu:/, '')}.jpg`), image); + } }, }; export default config; diff --git a/ts/scripts/compile-stories-icu-lookup.ts b/ts/scripts/compile-stories-icu-lookup.ts index 8fc6430cf5..0846d64bfe 100644 --- a/ts/scripts/compile-stories-icu-lookup.ts +++ b/ts/scripts/compile-stories-icu-lookup.ts @@ -1,22 +1,23 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import { readdir, readFile, writeFile } from 'node:fs/promises'; -import { join } from 'node:path'; +import { readFile, writeFile } from 'node:fs/promises'; +import { join, basename } from 'node:path'; import pMap from 'p-map'; -import z from 'zod'; +import fastGlob from 'fast-glob'; import { drop } from '../util/drop'; -const jsonSchema = z.string().array(); - async function main(): Promise { const source = process.argv[2]; if (!source) { throw new Error('Missing required source directory argument'); } - const ids = await readdir(join(source, 'data'), { withFileTypes: true }); + const dirEntries = await fastGlob('*/*.jpg', { + cwd: join(source, 'data'), + onlyFiles: true, + }); const enMessages = JSON.parse( await readFile( @@ -28,27 +29,18 @@ async function main(): Promise { const icuToStory: Record> = Object.create(null); await pMap( - ids, - async entity => { - if (!entity.isDirectory()) { - return; - } - - const storyId = entity.name; - const dir = join(source, 'data', storyId); - - const strings = jsonSchema.parse( - JSON.parse(await readFile(join(dir, 'strings.json'), 'utf8')) - ); - - for (const icuId of strings) { - let list = icuToStory[icuId]; - if (list == null) { - list = []; - icuToStory[icuId] = list; - } - list.push(storyId); + dirEntries, + async entry => { + const [storyId, imageFile] = entry.split('/', 2); + + const icuId = `icu:${basename(imageFile, '.jpg')}`; + + let list = icuToStory[icuId]; + if (list == null) { + list = []; + icuToStory[icuId] = list; } + list.push(storyId); }, { concurrency: 20 } ); diff --git a/ts/types/Util.ts b/ts/types/Util.ts index cbc8ed6591..1cc7034d9c 100644 --- a/ts/types/Util.ts +++ b/ts/types/Util.ts @@ -51,7 +51,7 @@ export type LocalizerType = { // Storybook trackUsage(): void; - stopTrackingUsage(): Array; + stopTrackingUsage(): Array<[string, string]>; }; export enum SentMediaQualityType { diff --git a/ts/util/setupI18nMain.ts b/ts/util/setupI18nMain.ts index 4f1259a5c2..7967b58634 100644 --- a/ts/util/setupI18nMain.ts +++ b/ts/util/setupI18nMain.ts @@ -121,7 +121,7 @@ export function setupI18n( renderEmojify, }); - let usedStrings: Set | undefined; + let usedStrings: Map | undefined; const localizer: LocalizerType = (< Key extends keyof ICUStringMessageParamsByKeyType, @@ -130,13 +130,13 @@ export function setupI18n( substitutions: ICUStringMessageParamsByKeyType[Key], options?: LocalizerOptions ) => { - usedStrings?.add(key); - const result = intl.formatMessage( { id: key }, normalizeSubstitutions(substitutions, options) ); + usedStrings?.set(key, result); + strictAssert(result !== key, `i18n: missing translation for "${key}"`); return result; @@ -159,13 +159,13 @@ export function setupI18n( if (usedStrings !== undefined) { throw new Error('Already tracking usage'); } - usedStrings = new Set(); + usedStrings = new Map(); }; localizer.stopTrackingUsage = () => { if (usedStrings === undefined) { throw new Error('Not tracking usage'); } - const result = Array.from(usedStrings); + const result = Array.from(usedStrings.entries()); usedStrings = undefined; return result; };