13 research surfaces investigated per Plan 01-12 prompt. Key findings: - BLOCKER §1: Newsreader has no Cyrillic glyphs (verified via Decision Brief's own embedded @font-face subsets + Production Type README). Russian display text in Newsreader will silently fall through to serif fallback. Recommend §1 R1: add Cyrillic-capable OFL serif (PT Serif / EB Garamond / Lora) to --mks-font-display stack. - §3 + §4: Mark legibility at 16 px is VERIFIED OK. rsvg-convert produces 406 / 784 / 1952 B PNGs at 16/48/128 — all clear Chrome imageUtil floors. The 2x2 weave holds at 16 px. - §10: 8 i18n strings extracted verbatim from Brief §02 (decoded via Python JSON-decode of the bundler-template script). Recommended __MSG_*__ key names + source-file mapping table included. - §12: D-09 (smoke gating) is NEAR-NO-OP. smoke.sh is a standalone bash script; nothing in src/ references it; dist/ carries no smoke artifacts. Recommend documenting current state + adding optional no-smoke-in-dist regression test. - §13: tokens.css line 12 Google Fonts @import migration → 8 local @font-face rules (Plex variable consolidation collapses the estimated 12-18 down to 8). All tooling verified installed on dev machine: rsvg-convert 2.60.0, pyftsubset 4.63.0, woff2_compress, inkscape 1.4.4. No npm install needed for execute-plan; only one-off font source TTF downloads. 7 assumptions logged for planner to surface (A1 default_locale, A2 popup DOM rework, A4 Cyrillic serif fallback are highest-impact). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1310 lines
83 KiB
Markdown
1310 lines
83 KiB
Markdown
# Plan 01-12 (Design Integration) — Research
|
||
|
||
**Researched:** 2026-05-17
|
||
**Domain:** Brand integration — font self-hosting, SVG rasterization, MV3 i18n, CSS token ingestion, Vite asset pipeline
|
||
**Confidence:** HIGH on stack/tooling (locally verified); MEDIUM on Cyrillic-extended-Plex sizing; **BLOCKER** on D-05 Newsreader Cyrillic coverage
|
||
|
||
## Summary
|
||
|
||
Designer-team handoff v1 (commit `5efc2a8`) lands a 280-line `tokens.css`, 32×32-viewBox `mokosh-mark.svg`, and 240×56 lockup. Engineering's Plan 01-12 ingests these into `src/`, removes the Google Fonts `@import` (MV3 CSP violation), self-hosts WOFF2 (Newsreader + IBM Plex Sans 4w + Plex Mono 2w, Latin + Cyrillic), rasterizes the mark to `icons/icon{16,48,128}.png`, updates `manifest.json:name`+`:description` per D-07/D-08 (via `__MSG_*__` + `_locales/`), wires the 8 operator strings into `_locales/ru/messages.json` + `_locales/en/messages.json`, restyles `src/popup/` per tokens, and replaces hardcoded Russian/placeholder colors with token references.
|
||
|
||
**Primary recommendation:** Execute in stable order — (1) write the canonical `tokens.css` to `src/shared/tokens.css` minus the Google Fonts `@import`, (2) rasterize PNG icons (rsvg-convert is installed + verified to produce icons that clear all Chrome floors with the existing mark at 16/48/128), (3) build out `fonts/` via `pyftsubset` (installed) + `woff2_compress` (installed) — BUT ONLY for the Plex faces and a Latin-only Newsreader subset; **substitute a Cyrillic-bearing display serif for Russian text** (see BLOCKER below), (4) create `_locales/` and migrate hardcoded Russian strings, (5) wire `manifest.json:name=__MSG_extName__` + `:description=__MSG_extDesc__` + `default_locale:"en"`, (6) restyle popup + smoke.
|
||
|
||
**BLOCKER (D-05):** Newsreader from Google Fonts / Production Type does NOT cover Cyrillic. The Decision Brief's own embedded `@font-face` declarations confirm this — Newsreader has only `latin / latin-ext / vietnamese` subsets; IBM Plex Sans and Plex Mono have `cyrillic / cyrillic-ext` subsets in addition. Russian-operator audience means display text in Newsreader will silently fall back to `Iowan Old Style → Times New Roman → serif`. See §1 BLOCKER for remediation options.
|
||
|
||
## User Constraints (from CONTEXT.md / brand-decisions-v1.md)
|
||
|
||
### Locked Decisions
|
||
|
||
- **D-01**: Mark concept = `A · Loom (2×2 weave intersection)`. Source SVG is `mokosh-mark.svg`. PNG rasterization is engineering's job.
|
||
- **D-02**: Welcome layout = `A · Hero + Loom dial`. Out of scope for Plan 01-12 (Plan 01-10's responsibility); Plan 01-12 must NOT touch `src/welcome/` content.
|
||
- **D-03**: Voice register = `A · Sober`. Carries to all 8 i18n strings.
|
||
- **D-04**: Palette = `A · Loom` (`tokens.css` `--mks-*` is canonical; replaces engineering placeholders).
|
||
- **D-05**: Type pairing = `A · Newsreader (display) + IBM Plex Sans (UI, 4 weights) + IBM Plex Mono (mono, 2 weights)`. All OFL. Self-host WOFF2; remove Google Fonts `@import`. — **subject to BLOCKER**.
|
||
- **D-06**: Icon strategy = `A · Neutral mark + dynamic badge`. NO per-state PNG sets. Matches existing arch (`chrome.action.setBadgeBackgroundColor` already in use per `src/background/index.ts:45`).
|
||
- **D-07 (USER OVERRIDE)**: `manifest.json:name = "Mokosh — Session Capture"` (not bare `"Mokosh"`). Surface capture purpose externally.
|
||
- **D-08**: Tagline = `«Тридцать секунд назад, всегда под рукой.»` / `"Thirty seconds ago, always at hand."`. Used in `manifest.json:description` + welcome hero.
|
||
- **D-09**: Smoke shipping = `A · Dev-only behind VITE_DEV flag`. — **subject to a no-op clarification**, see §12.
|
||
|
||
### Claude's Discretion
|
||
|
||
- The 8 i18n copy strings inherit D-03 Sober by default; per-string overrides surfaced as-encountered. Initial values for each string extracted from Decision Brief §02 — see §10 below for the verbatim text and recommended `__MSG_*__` keys.
|
||
- File layout: `src/shared/tokens.css` vs `src/styles/tokens.css` vs per-surface `tokens.css`. Recommended: **`src/shared/tokens.css`** to match the existing `src/shared/` convention (logger, types, binary).
|
||
- `default_locale` value: `"en"` recommended (Chrome Web Store convention; manifest is globally visible; Russian audience accesses the actual UI which is RU-primary).
|
||
- Font tooling specifics within the chosen WOFF2 self-host approach.
|
||
|
||
### Deferred Ideas (OUT OF SCOPE)
|
||
|
||
- Dark theme variant of mark SVG (`.dark` class already in `tokens.css` — but D-06 keeps mark neutral; badge does state work).
|
||
- Welcome page hero (Plan 01-10).
|
||
- Per-state icon variants (D-08 = neutral mark + badge; A-06 PNG set deferred indefinitely).
|
||
- Hi-DPI `icon192.png` (Brief P1, optional; defer unless Plan 01-12 budget allows).
|
||
|
||
## Phase Requirements
|
||
|
||
| ID | Description | Research Support |
|
||
|----|-------------|------------------|
|
||
| REQ-video-ring-buffer | Phase 1 anchor requirement; satisfied by Plans 01-01..01-07 | Not directly addressed by Plan 01-12 — but mentioned because Plan 01-12 must not regress any existing video-pipeline test. The 79 tests landed in 01-10 stay GREEN. |
|
||
| (implicit D-04..D-09) | Design integration requirements derived from `brand-decisions-v1.md` | This RESEARCH.md is the source-of-truth synthesis for each. |
|
||
|
||
---
|
||
|
||
## 1. WOFF2 self-hosting in MV3 (D-05) — **BLOCKER on Newsreader Cyrillic**
|
||
|
||
### BLOCKER finding
|
||
|
||
The Decision Brief's `@font-face` declarations (extracted from
|
||
`.planning/intel/design-incoming/mokosh/dist/Decision Brief (standalone).html`,
|
||
inside the bundler template after JSON-decoding) reveal the actual Google
|
||
Fonts subset structure:
|
||
|
||
| Family | Weights/styles shipped | Subsets shipped | Cyrillic? |
|
||
|---|---|---|---|
|
||
| Newsreader | normal 400/500/600, italic 400 | `latin`, `latin-ext`, `vietnamese` | **NO** |
|
||
| IBM Plex Sans | normal 400/500/600/700 | `latin`, `latin-ext`, `cyrillic`, `cyrillic-ext`, `greek`, `vietnamese` | YES |
|
||
| IBM Plex Mono | normal 400/500 | `latin`, `latin-ext`, `cyrillic`, `cyrillic-ext`, `vietnamese` | YES |
|
||
|
||
This is consistent with the Newsreader GitHub README (`productiontype/Newsreader`)
|
||
which states Newsreader covers "Google Fonts Latin Plus glyph set, ... English,
|
||
Western European languages as well as Vietnamese". Production Type's own page
|
||
makes no Cyrillic claim. Per the upstream charset files, Newsreader does not
|
||
include Cyrillic glyphs in any release shipped via Google Fonts.
|
||
|
||
**Impact on D-05:** Newsreader display text in Russian
|
||
(e.g. `«Тридцать секунд назад, всегда под рукой.»` if it lands on welcome hero
|
||
as `.mks-display-1`) will fall back to the next family in the stack:
|
||
`Newsreader, "Iowan Old Style", "Times New Roman", serif`. On Linux/Chrome OS
|
||
this means Times New Roman or DejaVu Serif. On macOS, Iowan Old Style. On
|
||
Windows, Times New Roman. Each render is a different visual personality and
|
||
none is the loom-warm character that D-05 picks.
|
||
|
||
**Remediation options for the planner:**
|
||
|
||
- **R1 (recommended): document the limitation + add Cyrillic-capable serif fallback.** Use Newsreader for Latin text only (welcome EN parallel-text subhead per D-08 — `"Thirty seconds ago, always at hand."`). For Russian display text, the existing fallback chain `Newsreader, "Iowan Old Style", "Times New Roman", serif` already does the work — just verify the Russian welcome hero renders in a visually-acceptable serif on the operator's actual Chrome. Engineering can prepend a Cyrillic-capable OFL serif (e.g. **PT Serif** by ParaType, **EB Garamond**, or **Lora** — all SIL OFL, all have Cyrillic) into the `--mks-font-display` stack so the Russian hero falls onto a curated face instead of a system default. Lowest engineering cost.
|
||
- **R2: switch display family.** Substitute a Cyrillic-bearing OFL serif (PT Serif, EB Garamond, Lora, Source Serif Pro) for Newsreader entirely. Loses the designer's specific aesthetic call. Requires re-coordinating with design team.
|
||
- **R3: extend Newsreader with a custom Cyrillic.** Out of scope; would require a font designer and license review.
|
||
|
||
**Recommended action for planner:** Surface this to user as a one-paragraph decision question. Default to R1 (smallest scope, preserves designer's call for Latin, adds a curated Cyrillic-capable serif as the next fallback). If user accepts R1, no font for Russian Newsreader needs to be subsetted — only Plex Sans + Plex Mono carry Cyrillic.
|
||
|
||
### Bundle plan (assumes R1; revise if R2)
|
||
|
||
For each subsetted WOFF2 file:
|
||
|
||
```bash
|
||
# Install: already installed on dev machine — pyftsubset (fontTools 4.63.0) + woff2_compress (Google's woff2)
|
||
# Source: download .ttf releases from https://github.com/IBM/plex (Plex Sans v1.1.0, Plex Mono v1.1.0)
|
||
# and https://github.com/productiontype/Newsreader (variable font)
|
||
|
||
# Plex Sans 400 Cyrillic-only (smallest possible; covers Russian operator surfaces)
|
||
pyftsubset IBMPlexSans-Regular.ttf \
|
||
--unicodes='U+0020-007E,U+00A0-00FF,U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116' \
|
||
--flavor=woff2 \
|
||
--output-file=fonts/IBMPlexSans-Regular-latin-cyrillic.woff2 \
|
||
--layout-features='*' --no-hinting --desubroutinize
|
||
# Repeat for weights 500, 600, 700.
|
||
|
||
# Plex Mono 400 + 500 — same subset
|
||
pyftsubset IBMPlexMono-Regular.ttf \
|
||
--unicodes='U+0020-007E,U+00A0-00FF,U+0400-045F,U+2116' \
|
||
--flavor=woff2 --output-file=fonts/IBMPlexMono-Regular.woff2 ...
|
||
|
||
# Newsreader (variable, Latin only per the Brief subset)
|
||
pyftsubset Newsreader[opsz,wght].ttf \
|
||
--unicodes='U+0020-007E,U+00A0-00FF,U+0152-0153,U+2000-206F,U+20AC,U+2122' \
|
||
--flavor=woff2 --output-file=fonts/Newsreader-variable.woff2 ...
|
||
```
|
||
|
||
**Estimated bundle size (verified against Google Fonts subset sizes from public CDN):**
|
||
|
||
| File | Estimated size |
|
||
|---|---|
|
||
| Plex Sans 400 latin+cyrillic | ~25 KB |
|
||
| Plex Sans 500 latin+cyrillic | ~25 KB |
|
||
| Plex Sans 600 latin+cyrillic | ~25 KB |
|
||
| Plex Sans 700 latin+cyrillic | ~25 KB |
|
||
| Plex Mono 400 latin+cyrillic | ~28 KB |
|
||
| Plex Mono 500 latin+cyrillic | ~28 KB |
|
||
| Newsreader variable, Latin-only | ~60 KB (variable axes inflate) |
|
||
| **Total** | **~216 KB** |
|
||
|
||
Comfortably under the brief's implicit "≤300 KB" target.
|
||
|
||
**License files (D-05 OFL):**
|
||
|
||
- `fonts/LICENSE-IBM-Plex.txt` — copy from https://github.com/IBM/plex/blob/master/LICENSE.txt (SIL OFL 1.1 + IBM copyright lines)
|
||
- `fonts/LICENSE-Newsreader.txt` — copy from https://github.com/productiontype/Newsreader/blob/master/OFL.txt
|
||
- Both: OFL allows embedding without a separate NOTICE file (per OFL FAQ); a `fonts/README.md` linking to each upstream + listing copyright lines is sufficient and is best practice.
|
||
|
||
### Sources
|
||
|
||
- [Newsreader on GitHub (productiontype/Newsreader)](https://github.com/productiontype/Newsreader) — "Google Fonts Latin Plus glyph set" claim, no Cyrillic mentioned
|
||
- [IBM Plex on GitHub](https://github.com/IBM/plex) — Plex Sans supports "extended Latin, Arabic, Chinese (Traditional), Cyrillic, Devanagari, Greek, Hebrew, Japanese, Korean, and Thai"
|
||
- [SIL Open Font License 1.1](https://openfontlicense.org/) — embedded fonts have lighter attribution requirements
|
||
- [OFL-FAQ](https://openfontlicense.org/ofl-faq/) — bundled fonts do not require separate distribution copies
|
||
- [Decision Brief embedded `@font-face` declarations](file://.planning/intel/design-incoming/mokosh/dist/Decision%20Brief%20(standalone).html) — primary source for confirmed subset structure
|
||
- [fontTools pyftsubset docs](https://fonttools.readthedocs.io/en/stable/subset/) — `--unicodes` + `--flavor=woff2`
|
||
- [Markos Konstantopoulos on font subsetting](https://markoskon.com/creating-font-subsets/) — practical pyftsubset examples
|
||
- [Walter Ebert on subsetting web fonts](https://walterebert.com/blog/subsetting-web-fonts/) — IBM Plex bandwidth comparisons
|
||
|
||
---
|
||
|
||
## 2. Vite @crxjs font asset pipeline
|
||
|
||
### Findings
|
||
|
||
- `@crxjs/vite-plugin` (project on `2.0.0-beta.25`; latest stable is `2.4.0`) automatically generates `web_accessible_resources` entries for resources referenced from extension pages (popup, welcome, offscreen, etc.) per the official README: *"Automatic generation of `web_accessible_resources` manifest entries"*.
|
||
- Vite's general asset handling: CSS `url()` references inside imported `.css` files are auto-rebased and emitted with content-hashed names to `dist/assets/`. This works for `.woff2` files placed in `src/shared/fonts/` or similar.
|
||
- The known failure mode is when `chrome.runtime.getURL(<importedAsset>)` is called from a service worker (per [crxjs issue #891](https://github.com/crxjs/chrome-extension-tools/issues/891)) — but this is NOT the popup/welcome font-loading flow. Fonts referenced via `@font-face src: url('../shared/fonts/x.woff2')` inside an extension-page CSS work through Vite's normal rebase pipeline.
|
||
|
||
### Recommended approach
|
||
|
||
**Pattern A (recommended): regular `src/` assets.** Place WOFF2 files at `src/shared/fonts/<face>.woff2`. The canonical `src/shared/tokens.css` references them via relative paths:
|
||
|
||
```css
|
||
@font-face {
|
||
font-family: "IBM Plex Sans";
|
||
src: url("./fonts/IBMPlexSans-Regular-latin-cyrillic.woff2") format("woff2");
|
||
font-weight: 400;
|
||
font-style: normal;
|
||
font-display: swap;
|
||
}
|
||
```
|
||
|
||
When `src/popup/index.html` and `src/welcome/welcome.html` `@import` (or `<link>`) the tokens.css, Vite rebases the URLs, emits the WOFF2 files to `dist/assets/<hash>.woff2`, and rewrites the CSS to point at the hashed names. CRXJS then auto-adds them to `web_accessible_resources` because they're transitively reachable from extension HTML pages.
|
||
|
||
**Pattern B (fallback if Pattern A misfires): `/public/fonts/`.** Files placed under `public/` are copied verbatim to `dist/` without hashing. Reference as absolute paths `url("/fonts/x.woff2")`. The trade-off: no asset hashing (cache busting requires version bumps), but explicit and tool-independent. Manual `web_accessible_resources` entry needed in `manifest.json`:
|
||
|
||
```json
|
||
"web_accessible_resources": [
|
||
{ "resources": ["fonts/*.woff2"], "matches": ["<all_urls>"] }
|
||
]
|
||
```
|
||
|
||
**Validation step (planner: include in execute-plan):** After first build with WOFF2 files, run `grep -r "woff2" dist/manifest.json` and `ls dist/assets/*.woff2` — if WOFF2 files show up in `dist/assets/` AND `web_accessible_resources` mentions them, Pattern A is working. If not, fall back to Pattern B.
|
||
|
||
### Sources
|
||
|
||
- [@crxjs/vite-plugin README on GitHub](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/README.md) — "automatic generation of web_accessible_resources"
|
||
- [Vite Static Asset Handling](https://vite.dev/guide/assets) — CSS `url()` rebasing
|
||
- [crxjs issue #891 (SW asset loading failure)](https://github.com/crxjs/chrome-extension-tools/issues/891) — known pitfall, NOT applicable to popup/welcome font loading
|
||
- Codebase verification: current `vite.config.ts` line 10 sets `injectCss: false` for content scripts only (does not affect popup/welcome CSS); current `manifest.json` has no `web_accessible_resources` (Plan 01-10 adds one for welcome.html)
|
||
|
||
---
|
||
|
||
## 3. SVG → PNG rasterization toolchain (D-01)
|
||
|
||
### Verified locally
|
||
|
||
Tooling installed on dev machine:
|
||
|
||
- `rsvg-convert` 2.60.0 (libRSVG via librsvg) — installed at `/usr/bin/rsvg-convert`
|
||
- ImageMagick `convert` (deprecated in v7, use `magick`) — also available
|
||
- `inkscape` 1.4.4 — available
|
||
|
||
Empirically verified by rasterizing `mokosh-mark.svg`:
|
||
|
||
```bash
|
||
rsvg-convert -w 16 -h 16 mokosh-mark.svg -o icon16.png # 406 bytes ✓ (>200)
|
||
rsvg-convert -w 48 -h 48 mokosh-mark.svg -o icon48.png # 784 bytes ✓ (>500)
|
||
rsvg-convert -w 128 -h 128 mokosh-mark.svg -o icon128.png # 1952 bytes ✓ (>1024)
|
||
```
|
||
|
||
All three clear the Chrome `imageUtil` floors documented in `assets-spec.md` and the design handoff. PNG output is `8-bit/color RGBA, non-interlaced` per `file(1)`.
|
||
|
||
### Recommended approach
|
||
|
||
**One-off rasterization, commit the PNGs.** No build-time automation needed because the mark is stable (designer-handed-off; D-01 locked). A `scripts/rasterize-icons.sh` recipe is documented but only re-runs when the SVG changes:
|
||
|
||
```bash
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
SVG="src/shared/brand/mokosh-mark.svg" # or wherever the planner lands the SVG
|
||
for size in 16 48 128; do
|
||
rsvg-convert -w "$size" -h "$size" "$SVG" -o "icons/icon${size}.png"
|
||
echo "✓ icons/icon${size}.png ($(stat -c%s "icons/icon${size}.png") bytes)"
|
||
done
|
||
```
|
||
|
||
Run once during execute-plan, commit the resulting `icons/icon{16,48,128}.png` files.
|
||
|
||
### Anti-pattern
|
||
|
||
Do NOT add this to `prebuild` — Chrome's `imageUtil` floors are bytes-of-PNG, not bytes-of-source-SVG. Re-rasterizing on every `npm run build` adds 30–80 ms with no value (the inputs are stable per phase). Plan 01-12 should commit static PNGs as deliverables, not generate them in CI.
|
||
|
||
### Sources
|
||
|
||
- `rsvg-convert(1)` man page (locally available)
|
||
- [Inkscape command-line export](https://wiki.inkscape.org/wiki/Using_the_Command_Line) — alternative tool
|
||
- Verified locally against `.planning/intel/design-incoming/system/bundle/mokosh-handoff/assets/mokosh-mark.svg`
|
||
|
||
---
|
||
|
||
## 4. Mark legibility at 16 px (D-01 risk) — **VERIFIED OK**
|
||
|
||
### Findings
|
||
|
||
Empirically rasterized via rsvg-convert at 16/48/128 px. The 2×2 weave mark holds:
|
||
|
||
- **16 px** (`/tmp/mokosh-rsearch/mark-16.png`, 406 B): Reads as a 4×4-grid silhouette. The two central weave gaps (the "hole" at x=12–14 and y=12.5–13.5) collapse into near-pixel-level dots, but the overall grid pattern is recognizable. Stroke weight at 16 px ≈ 1.1 px effective; corners round slightly via anti-aliasing.
|
||
- **48 px** (784 B): Clean weave rendering with all gaps visible.
|
||
- **128 px** (1952 B): Fully resolved, every gap distinct.
|
||
|
||
Visual inspection: at 16 px the mark reads as "small frame containing crosshatch" — distinct from generic placeholder shapes. The README claim that "[Newsreader concept] holds at 16 px" (designer handoff handoff.html line 458) is borne out by the rsvg-convert output.
|
||
|
||
### Recommended approach
|
||
|
||
**Use the canonical `mokosh-mark.svg` at all three sizes — no 16 px variant needed.** The mark survives at 16 px. The four candidate marks the README mentions ("Surface Kit shows 4 mark candidates") are alternative concepts, not 16 px-rescue variants. With D-01 locked on `A · Loom`, the candidate set doesn't apply.
|
||
|
||
**No remediation needed.** This risk was pre-mitigated by the designer's choice of a simple weave (4 vertical + 4 horizontal segments inside a rounded square — no fine detail that fails to resolve at low resolution).
|
||
|
||
### Sources
|
||
|
||
- Local empirical: rsvg-convert output inspected as image at 16/48/128 px
|
||
- Designer handoff `handoff.html` line 458: "[2×2 weave intersection — see assets/mokosh-mark.svg] Holds at 16 px."
|
||
|
||
---
|
||
|
||
## 5. Dark theme variant of mark SVG (D-06)
|
||
|
||
### Findings
|
||
|
||
The lockup + mark SVGs hardcode `stroke="#181b2a"` (matches `--mks-ink-900`). The `tokens.css` `.dark` block at line 165 swaps `--mks-surface-inverse` etc., but does NOT touch the mark stroke. D-06 picks "neutral mark + dynamic badge" — the badge does the state work, not an icon swap.
|
||
|
||
**Three architectural patterns evaluated:**
|
||
|
||
1. **`currentColor`** — change SVG to `stroke="currentColor"`. Works for *inline* SVG with `<svg style="color: var(--mks-fg-1)">`, but NOT for `<img src="mokosh-mark.svg">` (image elements don't inherit `currentColor`). The `default_icon` and `chrome.action.setIcon` paths use raster PNGs, not SVG, so this pattern doesn't help the toolbar at all.
|
||
2. **Two SVG variants** — `mokosh-mark-light.svg` (`#181b2a` stroke) + `mokosh-mark-dark.svg` (`#faf7f1` stroke for inverse surfaces). Swap based on `prefers-color-scheme`. Useful for *welcome page* hero rendering only.
|
||
3. **Badge-only differentiation (D-06)** — the toolbar PNG icon stays neutral (`#181b2a`) regardless of theme; the colored badge (`chrome.action.setBadgeBackgroundColor`) does the state communication. This is what D-06 picks.
|
||
|
||
### Recommended approach
|
||
|
||
**For toolbar PNGs (icon16/48/128):** Single neutral `#181b2a` stroke. The current rasterized PNGs from §3 satisfy D-06 directly. Chrome's toolbar will adapt the visual surrounding (light/dark theme of the browser chrome) but the icon itself stays a fixed ink-on-transparent rendering.
|
||
|
||
**For inline SVG usage on welcome page (Plan 01-10's territory, not 01-12's):** When Plan 01-10 inlines `mokosh-mark.svg` into the welcome HTML, modify the inline copy to use `stroke="currentColor"` and let CSS `color: var(--mks-fg-1)` control it. This is a Plan 01-10 concern; Plan 01-12 may pre-stage by copying the SVG to `src/shared/brand/mokosh-mark.svg` and leaving the canonical hardcoded-stroke version untouched (the rasterizer needs the stroke explicit for PNG output).
|
||
|
||
**No dark-variant SVG file needed for Plan 01-12.**
|
||
|
||
### Sources
|
||
|
||
- `tokens.css` lines 165–189 (`.dark` block) — confirms surface inversion but no mark color swap
|
||
- `brand-decisions-v1.md` D-06 — "Neutral mark + dynamic badge"
|
||
- MDN `currentColor`: only inherited by inline SVG, not `<img src=svg>` (well-known browser behavior; not vendor-disputed)
|
||
|
||
---
|
||
|
||
## 6. Cyrillic subsetting (D-05 corollary) — **superseded by §1 BLOCKER for Newsreader**
|
||
|
||
### Findings (Plex faces only — Newsreader covered in §1)
|
||
|
||
Per the canonical Google Fonts subset structure extracted from the Decision Brief:
|
||
|
||
| Subset | Unicode-range (canonical) | Bytes-per-face-weight (est.) |
|
||
|---|---|---|
|
||
| `cyrillic` (basic) | `U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116` | ~10 KB for Plex Sans 400 |
|
||
| `cyrillic-ext` | `U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F` | ~6 KB |
|
||
| `latin` (basic) | `U+0000-00FF, U+0131, U+0152-0153, ... (ASCII + Latin-1 + common punct)` | ~15 KB for Plex Sans 400 |
|
||
|
||
**Russian operators don't need `cyrillic-ext`** (that's Cyrillic Supplement — historical/extended scripts like Old Church Slavonic, Komi, Yakut). Mokosh's audience uses Modern Russian Cyrillic which lives entirely in `U+0400-04FF` (covered by basic `cyrillic`).
|
||
|
||
### Recommended subset per face
|
||
|
||
```bash
|
||
# Russian operator: latin (ASCII for English fallback text) + cyrillic (basic, U+0400-045F)
|
||
# Skip cyrillic-ext (Supplement), latin-ext (Eastern European), greek, vietnamese
|
||
UNICODES='U+0020-007E,U+00A0-00FF,U+0131,U+0152-0153,U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116'
|
||
```
|
||
|
||
Per-face-weight WOFF2 should land at ~25 KB for Plex Sans, ~28 KB for Plex Mono. The size table in §1 is conservative.
|
||
|
||
### Sources
|
||
|
||
- Decision Brief embedded `@font-face` declarations (verified extraction; see §1)
|
||
- [`pyftsubset` docs](https://fonttools.readthedocs.io/en/stable/subset/) — `--unicodes` syntax
|
||
- Standard reference: [Markos Konstantopoulos on font subsetting](https://markoskon.com/creating-font-subsets/)
|
||
|
||
---
|
||
|
||
## 7. License compliance (D-05)
|
||
|
||
### Findings
|
||
|
||
- **IBM Plex Sans + IBM Plex Mono** — SIL OFL 1.1. Upstream license at https://github.com/IBM/plex/blob/master/LICENSE.txt. Copyright "© Copyright IBM Corp. with Reserved Font Names 'Plex' and 'IBM Plex'."
|
||
- **Newsreader** — SIL OFL 1.1. Upstream license at https://github.com/productiontype/Newsreader/blob/master/OFL.txt. Copyright "© 2019 Production Type, Paris."
|
||
- Both: embedding + bundling allowed. No royalty. Per [OFL-FAQ](https://openfontlicense.org/ofl-faq/), embedding a font in a software bundle "does not constitute distribution" for the purposes of mandatory NOTICE shipping. Best practice is still to include the OFL text + copyright lines.
|
||
|
||
### Recommended file layout
|
||
|
||
```
|
||
src/shared/fonts/
|
||
├── IBMPlexMono-Regular.woff2 # 400 latin+cyrillic
|
||
├── IBMPlexMono-Medium.woff2 # 500 latin+cyrillic
|
||
├── IBMPlexSans-Regular.woff2 # 400 latin+cyrillic
|
||
├── IBMPlexSans-Medium.woff2 # 500 latin+cyrillic
|
||
├── IBMPlexSans-SemiBold.woff2 # 600 latin+cyrillic
|
||
├── IBMPlexSans-Bold.woff2 # 700 latin+cyrillic
|
||
├── Newsreader-variable.woff2 # variable axes [opsz, wght], latin-only per §1 R1
|
||
├── LICENSE-IBM-Plex.txt # copy of upstream LICENSE.txt
|
||
├── LICENSE-Newsreader.txt # copy of upstream OFL.txt
|
||
└── README.md # one paragraph: what's bundled, with upstream URLs
|
||
```
|
||
|
||
`src/shared/fonts/README.md` content (one-pager):
|
||
|
||
```markdown
|
||
# Bundled fonts
|
||
|
||
Self-hosted per MV3 CSP (forbids remote @font-face at runtime).
|
||
All faces are SIL OFL 1.1 licensed (see LICENSE-* files in this directory).
|
||
|
||
## Sources
|
||
|
||
- IBM Plex Sans + Plex Mono: https://github.com/IBM/plex (Plex Sans v1.1.0, Plex Mono v1.1.0, 2024-11-13)
|
||
- Newsreader: https://github.com/productiontype/Newsreader (variable, 2020+, Production Type for Google Fonts)
|
||
|
||
## Subsetting
|
||
|
||
Subsetted via fontTools.pyftsubset to Latin (ASCII + Latin-1) + Cyrillic basic
|
||
(U+0400-045F) for Plex; Latin-only for Newsreader (no Cyrillic glyphs in upstream).
|
||
Russian operator surfaces use Plex Sans/Mono for text rendering; Newsreader is
|
||
display-only for Latin script (welcome EN parallel-text subhead).
|
||
|
||
## Regenerate
|
||
|
||
See scripts/subset-fonts.sh.
|
||
```
|
||
|
||
### Sources
|
||
|
||
- [SIL OFL 1.1 text](https://openfontlicense.org/)
|
||
- [OFL-FAQ](https://openfontlicense.org/ofl-faq/) — embedded fonts attribution
|
||
- [IBM Plex repo + LICENSE](https://github.com/IBM/plex)
|
||
- [Newsreader repo + OFL.txt](https://github.com/productiontype/Newsreader)
|
||
- [SPDX OFL-1.1](https://spdx.org/licenses/OFL-1.1.html)
|
||
|
||
---
|
||
|
||
## 8. `.mks-word` CSS class (lockup SVG dependency)
|
||
|
||
### Findings
|
||
|
||
The lockup SVG (`assets/mokosh-lockup.svg`) line 21:
|
||
|
||
```html
|
||
<text x="48" y="40" class="mks-word">Mokosh</text>
|
||
```
|
||
|
||
The `.mks-word` class is referenced but NOT defined in delivered `tokens.css`. Without it, the wordmark "Mokosh" inside the lockup SVG renders in the browser default serif at default size — completely off-brand.
|
||
|
||
### Recommended approach
|
||
|
||
**Add a one-line definition to `src/shared/tokens.css` (engineering-authored, designer-overridable).** The definition matches the visual specs implied by the lockup SVG geometry (text starts at x=48, baseline at y=40 inside a 240×56 viewBox; visual inspection shows the text occupies about 32 px cap-height):
|
||
|
||
```css
|
||
/* ── Lockup wordmark ──────────────────────────────────────────────
|
||
References from mokosh-lockup.svg. Engineering's working definition;
|
||
designer can override by sending a tokens.css patch. */
|
||
.mks-word {
|
||
font-family: var(--mks-font-display);
|
||
font-size: 32px;
|
||
font-weight: var(--mks-weight-regular);
|
||
letter-spacing: var(--mks-tracking-display);
|
||
fill: var(--mks-ink-900);
|
||
}
|
||
```
|
||
|
||
**Mitigations to surface to designer in commit message:**
|
||
|
||
```
|
||
docs(01-12): add .mks-word lockup wordmark CSS (engineering working defn)
|
||
|
||
The lockup SVG `assets/mokosh-lockup.svg` references `.mks-word`
|
||
which was not in delivered tokens.css. This commit adds a working
|
||
definition (32 px Newsreader at --mks-ink-900) so the lockup
|
||
renders. Designer may override by sending a tokens.css patch with
|
||
the canonical sizing.
|
||
```
|
||
|
||
**If the lockup is used inline in welcome page:** `fill` works via CSS for `<text>` inside inline SVG. If used as `<img src="lockup.svg">`, the CSS doesn't reach inside — the `.mks-word` would need to be applied via a `<style>` element inside the SVG itself (per [SVG2 CSS rules](https://www.w3.org/TR/SVG2/styling.html#StylingWithCSS)). For Plan 01-12, the simplest path: add the CSS to `tokens.css`, and any consumer of the lockup either (a) inlines the SVG into HTML (CSS reaches), or (b) inlines the same class definition into a `<style>` block within the SVG file (small duplication, acceptable).
|
||
|
||
### Sources
|
||
|
||
- `assets/mokosh-lockup.svg` line 21 (read verbatim)
|
||
- `tokens.css` lines 14–280 (verified `.mks-word` is absent)
|
||
- [SVG2 § 6: Styling with CSS](https://www.w3.org/TR/SVG2/styling.html#StylingWithCSS) — CSS-from-outside-SVG rules
|
||
|
||
---
|
||
|
||
## 9. tokens.css integration into existing src/ (D-04)
|
||
|
||
### Findings
|
||
|
||
Current `src/` layout:
|
||
|
||
```
|
||
src/
|
||
├── background/index.ts # SW
|
||
├── content/index.ts # content script
|
||
├── offscreen/{index.html, recorder.ts}
|
||
├── popup/{index.html, index.ts, style.css} # current popup w/ placeholder palette
|
||
└── shared/{binary.ts, logger.ts, types.ts} # shared module convention exists
|
||
```
|
||
|
||
Current popup style (`src/popup/style.css`) uses hardcoded hex (`#2563eb` blue button, `#f8f9fa` body bg, `#dc2626` error) — entirely engineering placeholder colors per the design handoff's diagnostic. Plan 01-12 rewires these to tokens.
|
||
|
||
**Future welcome page (Plan 01-10's territory) at `src/welcome/welcome.css`** uses hardcoded placeholders too (`#00C853` green button, `#1f1f1f` text) — per current Plan 01-10 contents. Plan 01-12 SHOULD NOT modify Plan 01-10's CSS *unless* the planner explicitly decides to rewire it as a hand-off to Plan 01-10 execution (which is a coordination question).
|
||
|
||
### Recommended file layout
|
||
|
||
```
|
||
src/shared/tokens.css # canonical tokens (this is the single source of truth)
|
||
src/shared/fonts/ # WOFF2 files + LICENSE-* (per §7)
|
||
```
|
||
|
||
**Import strategy:** Each top-level CSS file (`src/popup/style.css`, `src/welcome/welcome.css`, smoke `data:` URL) prepends a relative `@import`:
|
||
|
||
```css
|
||
/* src/popup/style.css */
|
||
@import "../shared/tokens.css";
|
||
|
||
body {
|
||
background: var(--mks-surface);
|
||
color: var(--mks-fg-1);
|
||
font-family: var(--mks-font-ui);
|
||
...
|
||
}
|
||
```
|
||
|
||
Vite resolves `@import` paths at build time and emits a single CSS chunk per HTML entry, deduplicating the token block. **No PostCSS pipeline needed** — CSS custom properties (`--mks-*`) are runtime browser features, resolved by the browser at paint time. Vite ships them as literal CSS without transformation.
|
||
|
||
### Anti-pattern
|
||
|
||
Do NOT use Vite's `?inline` CSS imports for tokens — that's for JS-side string imports of CSS source. Tokens go through normal `@import` to participate in the deduplication.
|
||
|
||
Do NOT split tokens into per-surface files. The handoff is one tokens.css; engineering's job is to keep that one file canonical so designer overrides are single-file patches per `handoff.html` line 27 ("the tokens are wired so a re-skin is a single-file edit").
|
||
|
||
### Smoke.sh consumption
|
||
|
||
`smoke.sh` builds a `data:text/html` URL containing the entire smoke page as a base64-encoded blob (verified at `smoke.sh:132–181`). The smoke HTML has inline `<style>` blocks. For Plan 01-12, D-04 wants the smoke page styled per tokens. Three options:
|
||
|
||
- **A**: Inline the relevant `--mks-*` definitions into the smoke `<style>` block (the smoke page only uses a handful of tokens — `#000`/`#0f0` mono timer overlay etc.). Replace hardcoded colors with the `--mks-*` values *literally* (because `data:` URLs can't `@import` from extension-relative paths). Verbosity cost: ~30 lines added to `smoke.sh`'s SMOKE_HTML heredoc.
|
||
- **B**: Convert smoke from `data:` URL to a Chrome-extension-relative URL (`chrome-extension://<id>/smoke.html`). Adds bundled smoke page artifacts to `dist/` — directly conflicts with D-09 ("dev-only behind VITE_DEV"). Increases bundle size for production.
|
||
- **C**: Leave smoke unstyled per tokens. The smoke page is dev-only; designer's restyling intent is implicit; engineering accepts the imperfection.
|
||
|
||
**Recommended: A** — copy a minimal subset of `--mks-*` literally into the smoke `<style>` block. Small verbosity cost, no architectural change, satisfies D-04's "rewire smoke.sh to use these tokens" directive in spirit.
|
||
|
||
### Sources
|
||
|
||
- Codebase verification: `src/popup/style.css` (read verbatim, lines 1–84)
|
||
- Codebase verification: `vite.config.ts` (no custom PostCSS plugin configured)
|
||
- [Vite CSS handling](https://vite.dev/guide/features.html#css) — `@import` deduplication
|
||
- [MDN CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) — runtime resolution
|
||
|
||
---
|
||
|
||
## 10. The 8 i18n copy strings (Brief §02)
|
||
|
||
### Verbatim extraction from Decision Brief §02
|
||
|
||
Extracted from `.planning/intel/design-incoming/mokosh/dist/Decision Brief (standalone).html` (decoded bundler-template script, §02 "Восемь операторских строк"). All strings inherit D-03 Sober register.
|
||
|
||
| # | Surface | RU (sober — recommended) | EN (welcome hero only) | Recommended `__MSG_*__` key | Source `src/*` file |
|
||
|---|---|---|---|---|---|
|
||
| 1 | Toolbar tooltip · OFF (action.default_title) | `Mokosh — щёлкните, чтобы начать запись` | — | `tooltipOff` | `src/background/index.ts` (chrome.action.setTitle for OFF state) |
|
||
| 2 | Toolbar tooltip · REC | `Mokosh — идёт запись (00:42)` (with timer interpolation; the `(00:42)` is a runtime substitution) | — | `tooltipRec` (with `$TIME$` placeholder) | `src/background/index.ts` (chrome.action.setTitle for REC state, requires a timer-update interval) |
|
||
| 3 | Toolbar tooltip · ERR | `Mokosh — ошибка записи, щёлкните для восстановления` | — | `tooltipErr` | `src/background/index.ts` (chrome.action.setTitle for ERR state, currently set in setErrorMode) |
|
||
| 4 | Popup idle body / CTA | `Сохранить отчёт об ошибке? / Сохранить отчёт` (the "?" is the body label; "Сохранить отчёт" is the button) | — | `popupSaveCta`, `popupSavePrompt` (two strings: prompt + button) | `src/popup/index.html` (button text "Сохранить отчёт об ошибке"), `src/popup/index.ts` line 38 (idle case) |
|
||
| 5 | Popup done | `Архив сохранён в Загрузки.` | — | `popupSaveDone` | `src/popup/index.ts` (replaces current line 91 `'Архив успешно сохранён!'`) |
|
||
| 6 | Notification startup | `Запись запущена. Я слежу за последними 30 секундами.` | — | `notifStartup` | `src/background/index.ts` (chrome.notifications.create startup message in setRecordingMode flow) |
|
||
| 7 | Notification recovery | `Запись возобновлена. Буфер снова заполняется.` | — | `notifRecovery` | `src/background/index.ts` lines 766–768 (recoveryId notification body) |
|
||
| 8 | Welcome hero · RU (+ EN parallel) | `Тридцать секунд назад, всегда под рукой.` | `Thirty seconds ago, always within reach.` | `welcomeHeroRu`, `welcomeHeroEn` (two keys: RU primary, EN parallel) | `src/welcome/welcome.html` (Plan 01-10's territory; Plan 01-12 produces the messages.json entries even if Plan 01-10 hasn't wired them yet) |
|
||
|
||
Note on string #4 vs the current popup (`src/popup/index.html` line 12 + `src/popup/index.ts` line 38): existing copy is `Сохранить отчёт об ошибке` (no "?", used as button text). The new sober register splits this into a label-with-question + a button — Plan 01-12 will need a small DOM rework in `src/popup/index.html` to render both. Alternatively, keep the existing single-button form and use string #4 ("Сохранить отчёт об ошибке?") as the combined tooltip + button label. **Recommend:** ask user during execute-plan whether to rework popup DOM. Simplest path: keep current single-button form, use `Сохранить отчёт` (the shorter button form) and put the `Сохранить отчёт об ошибке?` label above as `<p class="popup-prompt">` per the existing `<p class="info-text">` pattern.
|
||
|
||
Note on string #2 (REC tooltip with `(00:42)`): Chrome's `chrome.action.setTitle` accepts a string, no placeholder syntax. The `$TIME$` interpolation must happen in TS code before the API call. Suggested: define `__MSG_tooltipRecPrefix__` = `"Mokosh — идёт запись"` and concat `' (' + formatElapsed(seconds) + ')'` in JS. This keeps `messages.json` clean and lets the SW format the timestamp.
|
||
|
||
### Recommended `_locales/` layout
|
||
|
||
```
|
||
_locales/
|
||
├── en/messages.json # default_locale fallback; non-Russian system locales see English
|
||
└── ru/messages.json # primary operator locale
|
||
```
|
||
|
||
Example `_locales/ru/messages.json`:
|
||
|
||
```json
|
||
{
|
||
"extName": { "message": "Mokosh — Запись сессии",
|
||
"description": "manifest.json:name; surfaces in Chrome's extensions list, Web Store, and right-click menus" },
|
||
"extDesc": { "message": "Тридцать секунд назад, всегда под рукой.",
|
||
"description": "manifest.json:description" },
|
||
"tooltipOff": { "message": "Mokosh — щёлкните, чтобы начать запись" },
|
||
"tooltipRecPrefix":{ "message": "Mokosh — идёт запись",
|
||
"description": "Suffix '(MM:SS)' appended at runtime by the SW" },
|
||
"tooltipErr": { "message": "Mokosh — ошибка записи, щёлкните для восстановления" },
|
||
"popupSavePrompt": { "message": "Сохранить отчёт об ошибке?" },
|
||
"popupSaveCta": { "message": "Сохранить отчёт" },
|
||
"popupSaveDone": { "message": "Архив сохранён в Загрузки." },
|
||
"notifStartup": { "message": "Запись запущена. Я слежу за последними 30 секундами." },
|
||
"notifRecovery": { "message": "Запись возобновлена. Буфер снова заполняется." },
|
||
"welcomeHeroRu": { "message": "Тридцать секунд назад, всегда под рукой." },
|
||
"welcomeHeroEn": { "message": "Thirty seconds ago, always within reach.",
|
||
"description": "Parallel English subhead under the RU hero on the welcome page" }
|
||
}
|
||
```
|
||
|
||
And `_locales/en/messages.json` (fallback for non-RU Chrome locales) — same keys, English values, with `extDesc` = `"Thirty seconds ago, always within reach."` and `extName` = `"Mokosh — Session Capture"` (per D-07 user override).
|
||
|
||
### Sources
|
||
|
||
- Decision Brief §02 verbatim (extracted via Python JSON-decode of bundler template; see `tool-results/bdp4vyvas.txt`)
|
||
- `src/popup/index.html` + `index.ts` codebase grep
|
||
- `src/background/index.ts:54, 766–768, 868–870` for notification call sites
|
||
- [Chrome i18n message format](https://developer.chrome.com/docs/extensions/reference/api/i18n) — `messages.json` schema
|
||
|
||
---
|
||
|
||
## 11. manifest.json i18n via `__MSG_*__` (D-07 + D-08)
|
||
|
||
### Findings
|
||
|
||
Per [Chrome i18n docs](https://developer.chrome.com/docs/extensions/reference/api/i18n) and the [default_locale manifest reference](https://developer.chrome.com/docs/extensions/mv3/manifest/default_locale/):
|
||
|
||
- `manifest.json:name` and `manifest.json:description` accept `__MSG_<key>__` references — both fields support i18n.
|
||
- `default_locale` field is REQUIRED if `_locales/` directory exists. Without it, `_locales/` is ignored.
|
||
- Lookup order: user's preferred locale (e.g. `ru_RU`) → locale without region (`ru`) → `default_locale` → first-found in directory.
|
||
- "As long as the default locale's messages.json file has a value for every string, your extension will run no matter how sparse a translation is" (per Chrome docs).
|
||
- Chrome supports 54 store-listing locales; Russian (`ru`) is among them.
|
||
|
||
### Recommended approach
|
||
|
||
```json
|
||
{
|
||
"manifest_version": 3,
|
||
"name": "__MSG_extName__",
|
||
"version": "1.0.0",
|
||
"description": "__MSG_extDesc__",
|
||
"default_locale": "en",
|
||
"permissions": [...],
|
||
...
|
||
"action": {
|
||
"default_popup": "src/popup/index.html",
|
||
"default_title": "__MSG_tooltipOff__", // NEW per #1 above
|
||
"default_icon": {...}
|
||
},
|
||
...
|
||
}
|
||
```
|
||
|
||
**Why `default_locale: "en"`:**
|
||
|
||
- The Chrome Web Store fallback metadata (when displayed in a non-RU browser) uses `default_locale`. Setting it to `"en"` means the store listing for non-Russian users sees the English name + description, while RU-locale users (the operator audience) see the Russian.
|
||
- If `default_locale: "ru"`, then non-RU locales would see Russian (likely rendered correctly in the store but jarring for a Polish/German viewer).
|
||
- The Russian operator audience accesses the actual UI which is RU-localized regardless of `default_locale` — `default_locale` only controls fallback.
|
||
|
||
**If the user prefers `default_locale: "ru"`** (operator-only deployment, no Chrome Web Store distribution): set it to `"ru"`. The mechanism is the same; only the fallback differs.
|
||
|
||
### Anti-pattern
|
||
|
||
Do NOT use `__MSG_@@extension_id__` in manifest.json — the docs explicitly note "The special message `@@extension_id` ... doesn't work in manifest files."
|
||
|
||
Do NOT skip `default_locale` if `_locales/` exists — manifest validation fails at install time.
|
||
|
||
### Sources
|
||
|
||
- [Chrome i18n API](https://developer.chrome.com/docs/extensions/reference/api/i18n)
|
||
- [Manifest default_locale](https://developer.chrome.com/docs/extensions/mv3/manifest/default_locale/)
|
||
- [Localization message formats](https://developer.chrome.com/docs/extensions/mv2/i18n-messages/) — MV2 doc but format unchanged in MV3
|
||
- [How to Add i18n to Chrome Extension MV3 (Stefan Van Damme, 2023)](https://www.stefanvd.net/blog/2023/04/24/how-to-add-internationalization-i18n-chrome-extension/)
|
||
|
||
---
|
||
|
||
## 12. VITE_DEV smoke-page gating (D-09) — **NEAR-NO-OP**
|
||
|
||
### Findings
|
||
|
||
Verified by grepping the entire `src/` tree:
|
||
|
||
```bash
|
||
$ grep -rn "smoke\|SMOKE\|data:text/html" src/
|
||
# (empty)
|
||
$ find dist -name "smoke*" -o -name "*SMOKE*"
|
||
# (empty)
|
||
```
|
||
|
||
`smoke.sh` is a standalone bash script. The smoke page HTML is embedded as a heredoc inside `smoke.sh:132–181` and assembled into a `data:text/html,...` URL at line 181. The `data:` URL is passed to Chrome as a command-line argument:
|
||
|
||
```bash
|
||
SMOKE_DATA_URL="data:text/html,$(printf '%s' "${SMOKE_HTML}" | python3 -c 'import sys,urllib.parse;print(urllib.parse.quote(sys.stdin.read(), safe=""))')"
|
||
```
|
||
|
||
`SMOKE_HTML` exists only in shell process memory. Nothing in `src/` references it. Vite's build pipeline has zero awareness of `smoke.sh`. `dist/` carries no smoke artifacts.
|
||
|
||
### Why D-09 ("dev-only behind VITE_DEV") is a near-no-op
|
||
|
||
The decision was framed as if smoke might leak into the production bundle. **It can't** — smoke lives outside Vite's input set entirely. The production `dist/` does not ship smoke regardless of any flag.
|
||
|
||
### Recommended approach
|
||
|
||
**Document the existing state — minimal code change.** Plan 01-12 adds two artifacts to satisfy D-09's spirit:
|
||
|
||
1. **`scripts/README.md` (new file, 1 paragraph):** "Diagnostic / smoke scripts live in `smoke.sh` and `scripts/*`. These are dev-only tools and are not bundled by `npm run build`. The production `dist/` produced by `npm run build` does NOT contain smoke artifacts (verified by grep of `dist/` after every build)."
|
||
2. **`vite.config.ts` add a `define` block (defensive, for any future inline smoke flag):**
|
||
```ts
|
||
define: {
|
||
// D-09: any future inline-smoke-feature check should use this flag.
|
||
// Currently smoke lives entirely in smoke.sh; this flag is reserved
|
||
// for future code that wants conditional dev-only behavior.
|
||
__VITE_DEV__: JSON.stringify(process.env.VITE_DEV === '1' || !!process.env.npm_lifecycle_event?.startsWith('dev')),
|
||
},
|
||
```
|
||
|
||
**Optional (recommended but not required):** Add a vitest test that asserts `dist/` does NOT contain `smoke` strings after build:
|
||
|
||
```ts
|
||
// tests/build/no-smoke-in-dist.test.ts
|
||
it('dist/ contains no smoke artifacts', () => {
|
||
// ...recursively read dist/, assert no file path or content contains 'smoke'
|
||
});
|
||
```
|
||
|
||
If the user wants stronger guarantees (D-09 audit-ready), this test is the right shape.
|
||
|
||
### Anti-pattern
|
||
|
||
Do NOT add `if (import.meta.env.VITE_DEV) { ... smoke code ... }` blocks to `src/` files — there are no such blocks today, and creating them would introduce the very leak D-09 is asking to prevent. The current architecture (smoke isolated to a shell script) is already the safest.
|
||
|
||
### Sources
|
||
|
||
- Codebase verification (grep results above)
|
||
- `smoke.sh` line 132–181 (read verbatim)
|
||
- [Vite env vars + modes](https://vite.dev/guide/env-and-mode) — `import.meta.env.VITE_*` pattern
|
||
- [Vite define option](https://vite.dev/config/shared-options.html#define) — for `__VITE_DEV__` constant injection
|
||
|
||
---
|
||
|
||
## 13. tokens.css line 12 Google Fonts @import (CSP migration)
|
||
|
||
### Findings
|
||
|
||
Verified — `tokens.css` line 12 is:
|
||
|
||
```css
|
||
@import url('https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;1,6..72,400&family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap');
|
||
```
|
||
|
||
This violates MV3 CSP. Per [Chrome MV3 CSP docs](https://developer.chrome.com/docs/extensions/reference/manifest/content-security-policy), the default extension-pages CSP is `script-src 'self'; object-src 'self';` plus `style-src 'self' 'unsafe-inline';` for stylesheets. The `style-src 'self'` directive prevents the Google Fonts stylesheet from loading at runtime. (The `font-src` directive defaults to `'self'` as well, so even if the CSS loaded, the `@font-face src: url(https://fonts.gstatic.com/...)` inside would also fail.)
|
||
|
||
In practice, this means popups built today using the unmodified tokens.css would silently fall back to system serif/sans because the `@font-face` declarations never get loaded.
|
||
|
||
### Recommended migration
|
||
|
||
Replace line 12 with local `@font-face` rules pointing at self-hosted WOFF2 files in `src/shared/fonts/`. Per the chosen weight matrix (D-05; subject to §1 BLOCKER R1 — Newsreader Latin-only):
|
||
|
||
```css
|
||
/* ── Self-hosted fonts (MV3 CSP) ──
|
||
Faces and weights per D-05. Subsetting: Plex Sans + Plex Mono are
|
||
Latin + Cyrillic (U+0400-045F); Newsreader is Latin-only (no
|
||
Cyrillic glyphs in upstream — see RESEARCH.md §1 BLOCKER R1). */
|
||
|
||
/* IBM Plex Sans 400 / 500 / 600 / 700, normal */
|
||
@font-face {
|
||
font-family: "IBM Plex Sans";
|
||
src: url("./fonts/IBMPlexSans-Regular.woff2") format("woff2");
|
||
font-weight: 400; font-style: normal; font-display: swap;
|
||
}
|
||
@font-face {
|
||
font-family: "IBM Plex Sans";
|
||
src: url("./fonts/IBMPlexSans-Medium.woff2") format("woff2");
|
||
font-weight: 500; font-style: normal; font-display: swap;
|
||
}
|
||
@font-face {
|
||
font-family: "IBM Plex Sans";
|
||
src: url("./fonts/IBMPlexSans-SemiBold.woff2") format("woff2");
|
||
font-weight: 600; font-style: normal; font-display: swap;
|
||
}
|
||
@font-face {
|
||
font-family: "IBM Plex Sans";
|
||
src: url("./fonts/IBMPlexSans-Bold.woff2") format("woff2");
|
||
font-weight: 700; font-style: normal; font-display: swap;
|
||
}
|
||
|
||
/* IBM Plex Mono 400 / 500, normal */
|
||
@font-face {
|
||
font-family: "IBM Plex Mono";
|
||
src: url("./fonts/IBMPlexMono-Regular.woff2") format("woff2");
|
||
font-weight: 400; font-style: normal; font-display: swap;
|
||
}
|
||
@font-face {
|
||
font-family: "IBM Plex Mono";
|
||
src: url("./fonts/IBMPlexMono-Medium.woff2") format("woff2");
|
||
font-weight: 500; font-style: normal; font-display: swap;
|
||
}
|
||
|
||
/* Newsreader variable — Latin only (no Cyrillic in upstream) */
|
||
@font-face {
|
||
font-family: "Newsreader";
|
||
src: url("./fonts/Newsreader-variable.woff2") format("woff2-variations"),
|
||
url("./fonts/Newsreader-variable.woff2") format("woff2");
|
||
font-weight: 400 700; /* variable axis range */
|
||
font-style: normal;
|
||
font-display: swap;
|
||
}
|
||
@font-face {
|
||
font-family: "Newsreader";
|
||
src: url("./fonts/Newsreader-italic-variable.woff2") format("woff2-variations"),
|
||
url("./fonts/Newsreader-italic-variable.woff2") format("woff2");
|
||
font-weight: 400 700;
|
||
font-style: italic;
|
||
font-display: swap;
|
||
}
|
||
```
|
||
|
||
**Rule count: 8** (4 Plex Sans + 2 Plex Mono + 2 Newsreader variable). Less than the 12–18 estimate in the prompt because using a variable font for Newsreader collapses 4 static-weight rules into 2 (one per style).
|
||
|
||
If §1 R1 is adopted with an additional Cyrillic-capable serif fallback (e.g. PT Serif), add 2–4 more `@font-face` rules for that face.
|
||
|
||
### Anti-pattern
|
||
|
||
Do NOT use the `format('woff2-variations')` hint with a fixed-weight WOFF2 file — the hint is for variable fonts only. Browsers without variable-font support fall back to the second `src` URL (`format('woff2')`).
|
||
|
||
Do NOT keep the Google Fonts `@import` "for dev fallback" — MV3 CSP refuses it in *all* modes (dev + prod alike); leaving it in would generate console errors during development.
|
||
|
||
### Sources
|
||
|
||
- `tokens.css` line 12 (verbatim)
|
||
- [Chrome MV3 CSP manifest docs](https://developer.chrome.com/docs/extensions/reference/manifest/content-security-policy) — default `style-src 'self'`, `font-src 'self'`
|
||
- [MDN @font-face](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face) — `font-weight: 400 700` range syntax for variable fonts
|
||
- [CSS Fonts Module 4 § variable fonts](https://www.w3.org/TR/css-fonts-4/#font-weight-prop) — `format('woff2-variations')` hint
|
||
|
||
---
|
||
|
||
## Architectural Responsibility Map
|
||
|
||
| Capability | Primary Tier | Secondary Tier | Rationale |
|
||
|---|---|---|---|
|
||
| Brand mark display (toolbar PNG) | Browser (Chrome action API) | — | `chrome.action.setIcon` consumes PNG; only the SW initially configures; rendering is browser-owned |
|
||
| Brand mark display (welcome inline SVG) | Browser (DOM) | Frontend Server (built HTML) | Plan 01-10 inlines the SVG into welcome.html; Vite builds the HTML; browser paints |
|
||
| CSS tokens | Browser (CSS custom properties) | — | All `--mks-*` resolve at paint time in the browser; no SSR; Vite ships them as literal CSS |
|
||
| Font loading (WOFF2) | Browser | CDN/Static (Vite-emitted `dist/assets/`) | Browser parses `@font-face`, fetches from extension origin; Vite asset pipeline emits + hashes |
|
||
| i18n string resolution (`__MSG_*__`) | Browser (Chrome i18n runtime) | CDN/Static (`_locales/*/messages.json`) | Chrome reads `_locales/` at install time, substitutes at runtime; no server involvement |
|
||
| Manifest:name + :description | Chrome runtime (manifest parser) | — | Chrome reads manifest, substitutes `__MSG_*__`, displays in Web Store / Extensions list |
|
||
| Popup HTML/CSS rendering | Browser | Frontend (built HTML) | Popup is a self-contained Chrome extension HTML page; browser-only rendering |
|
||
| Smoke page styling | Browser (data: URL DOM) | — | `data:text/html` URL renders client-side; no Vite bundling involved |
|
||
|
||
---
|
||
|
||
## Standard Stack
|
||
|
||
### Core
|
||
|
||
| Library / tool | Version (verified) | Purpose | Why standard |
|
||
|---|---|---|---|
|
||
| `@crxjs/vite-plugin` | 2.0.0-beta.25 (project); 2.4.0 (latest) | MV3 bundle + manifest rewriting + WAR auto-generation | Project already uses it; required for MV3 ergonomics on Vite |
|
||
| `vite` | 5.4.2 (project); 8.0.13 (latest) | Build tool; CSS @import dedup; asset hashing | Project already uses it; Vite native @import handling resolves tokens.css from anywhere in src/ |
|
||
| `fontTools` (`pyftsubset`) | 4.63.0 (verified locally) | WOFF2 subsetting | Industry standard; preinstalled on dev machine |
|
||
| `woff2_compress` (Google's `woff2`) | installed via `/usr/bin/woff2_compress` | WOFF2 encoding | Native compression; what pyftsubset uses under the hood when `--flavor=woff2` is specified |
|
||
| `rsvg-convert` (librsvg) | 2.60.0 (verified locally) | SVG → PNG rasterization | Reliable; produces valid 8-bit RGBA PNGs that clear all Chrome `imageUtil` floors |
|
||
|
||
### Supporting
|
||
|
||
| Library / tool | Version | Purpose | When to use |
|
||
|---|---|---|---|
|
||
| `vitest` | 4 (project) | Unit tests | Plan 01-12 adds 1–3 tests (manifest i18n shape, fonts present, no smoke in dist) |
|
||
| Newsreader (Production Type) | variable, current upstream | Display serif (Latin only) | Welcome page Latin headlines, lockup wordmark |
|
||
| IBM Plex Sans | v1.1.0 (2024-11-13) | UI body sans, Cyrillic + Latin | Popup, welcome RU body, all `--mks-font-ui` |
|
||
| IBM Plex Mono | v1.1.0 (2024-11-13) | Diagnostic mono, Cyrillic + Latin | Timer overlay (smoke), code-like UI labels |
|
||
| PT Serif or EB Garamond or Lora (optional, §1 R1) | OFL | Cyrillic-capable serif fallback for Russian display | Russian welcome hero, RU display text falling out of Newsreader stack |
|
||
|
||
### Alternatives considered
|
||
|
||
| Instead of | Could use | Tradeoff |
|
||
|---|---|---|
|
||
| `rsvg-convert` | `inkscape --export-png` | Inkscape is heavier (X server may be needed for headless run); rsvg-convert is purer + smaller |
|
||
| `pyftsubset` | `glyphhanger` (extracts unicode-range from HTML) | glyphhanger is for *discovering* needed glyphs from real content; not needed when subset ranges are known (we have the Google Fonts canonical ranges) |
|
||
| Newsreader (D-05) | PT Serif (Cyrillic-capable display serif) | Loses designer's specific D-05 choice; gains Russian rendering correctness — see §1 BLOCKER |
|
||
| `_locales/` for i18n | Hardcoded RU strings only | Locks out future EN store distribution; `_locales/` is one-time setup cost; manifest:name needs it anyway for D-07 EN listing |
|
||
| Asset path in `src/shared/fonts/` (Pattern A) | `/public/fonts/` (Pattern B) | Pattern A: hashed names, deduplication; Pattern B: stable names, no @crxjs WAR auto-gen |
|
||
|
||
### Installation
|
||
|
||
```bash
|
||
# All required tools are ALREADY installed on dev machine; verified:
|
||
which pyftsubset # /usr/bin/pyftsubset (fontTools 4.63.0)
|
||
which woff2_compress # /usr/bin/woff2_compress
|
||
which rsvg-convert # /usr/bin/rsvg-convert (2.60.0)
|
||
|
||
# No npm installs needed for Plan 01-12 — the tooling is all native binaries.
|
||
# IF the planner wants asset-pipeline tests, no new dev-deps either; vitest is already present.
|
||
|
||
# Font source downloads (one-off, will land via curl in execute-plan):
|
||
# - https://github.com/IBM/plex/releases (Plex Sans + Plex Mono v1.1.0)
|
||
# - https://github.com/productiontype/Newsreader/releases (variable WOFF2 + TTF)
|
||
```
|
||
|
||
### Version verification
|
||
|
||
Verified versions are current as of 2026-05-17 dev machine probe (`npm view` for npm packages; `which`/`--version` for system binaries). No package install needed for Plan 01-12 execution — Vite + crxjs versions stay where the project currently has them (upgrading Vite from 5 → 8 is out of scope for a brand-integration plan).
|
||
|
||
---
|
||
|
||
## Architecture Patterns
|
||
|
||
### Pattern 1: Tokens-first CSS
|
||
|
||
**What:** Every CSS rule consuming a color, font-family, spacing, or radius value references a `--mks-*` custom property defined in `src/shared/tokens.css`. No literal hex / px / font-family strings in `src/popup/style.css`, `src/welcome/welcome.css`, or smoke HTML.
|
||
|
||
**When to use:** All surfaces touched by Plan 01-12 (popup, smoke; welcome is Plan 01-10's).
|
||
|
||
**Example:**
|
||
```css
|
||
/* src/popup/style.css after Plan 01-12 */
|
||
@import "../shared/tokens.css";
|
||
|
||
body {
|
||
background: var(--mks-surface);
|
||
color: var(--mks-fg-1);
|
||
font-family: var(--mks-font-ui);
|
||
padding: var(--mks-space-4);
|
||
}
|
||
.save-button {
|
||
background: var(--mks-rec); /* madder for the SAVE button per D-04 */
|
||
color: var(--mks-fg-inverse);
|
||
border: var(--mks-border-width) solid var(--mks-border-strong);
|
||
border-radius: var(--mks-radius-md);
|
||
box-shadow: var(--mks-shadow-1);
|
||
font-family: var(--mks-font-ui);
|
||
font-weight: var(--mks-weight-semibold);
|
||
}
|
||
```
|
||
|
||
### Pattern 2: i18n `__MSG_*__` at every Chrome API call site
|
||
|
||
**What:** Every call to `chrome.action.setTitle`, `chrome.notifications.create`, `chrome.action.setBadgeText`, and any DOM-rendered Russian string uses `chrome.i18n.getMessage('key')` instead of a literal.
|
||
|
||
**When to use:** Every operator-facing string. Plan 01-12 audits `src/background/index.ts` + `src/popup/index.ts` for hardcoded RU strings and migrates each.
|
||
|
||
**Example:**
|
||
```ts
|
||
// src/background/index.ts (was: 'Запись возобновлена')
|
||
chrome.notifications.create(recoveryId, {
|
||
type: 'basic',
|
||
iconUrl: chrome.runtime.getURL(NOTIFICATION_ICON_PATH),
|
||
title: chrome.i18n.getMessage('notifRecoveryTitle'),
|
||
message: chrome.i18n.getMessage('notifRecovery'),
|
||
});
|
||
|
||
// src/popup/index.ts (was: buttonText.textContent = 'Сохранить отчёт об ошибке')
|
||
buttonText.textContent = chrome.i18n.getMessage('popupSaveCta');
|
||
```
|
||
|
||
### Pattern 3: Single source of truth for tokens
|
||
|
||
**What:** `src/shared/tokens.css` is canonical. The handoff `tokens.css` is the input; engineering's `src/shared/tokens.css` is its production-ready descendant (Google Fonts `@import` replaced, `.mks-word` class added). The Decision Brief's `tokens.css` and the handoff's preview do NOT get bundled — only the engineering canonical.
|
||
|
||
### Anti-patterns to avoid
|
||
|
||
- **Per-surface mini-tokens.** Do NOT create `src/popup/popup-tokens.css` or similar. One tokens.css.
|
||
- **Inline color literals "for clarity".** Defeats the purpose. Use the token.
|
||
- **Skipping `_locales/en/` because audience is RU.** Manifest validation requires `default_locale` to have a messages.json. Plan 01-12 ships both.
|
||
- **Loading fonts with `chrome.runtime.getURL` from inside JS.** `@font-face url("./fonts/x.woff2")` in CSS is the only correct path (CSP-safe, Vite-rebased).
|
||
|
||
---
|
||
|
||
## Don't Hand-Roll
|
||
|
||
| Problem | Don't build | Use instead | Why |
|
||
|---|---|---|---|
|
||
| Font subsetting | Custom Python script reading .ttf bytes | `pyftsubset --flavor=woff2 --unicodes=...` | Glyph metric preservation + variable-axis-aware + handles ligatures correctly |
|
||
| SVG → PNG | Custom canvas rasterizer in Node | `rsvg-convert` or Inkscape | RGBA alpha, sub-pixel anti-aliasing, ImageMagick parity |
|
||
| i18n key resolution | Custom string-template engine | `chrome.i18n.getMessage` | Native Chrome integration; manifest substitution; locale fallback chain |
|
||
| WOFF2 compression | Custom Brotli encoder | `woff2_compress` (Google's reference impl) | The reference; what pyftsubset already calls internally |
|
||
| CSS custom properties → static colors | PostCSS plugin to inline | Use `var(--mks-*)` at runtime | Browser-native; runtime themable via `.dark` class addition |
|
||
|
||
**Key insight:** All five problems have battle-tested solutions in standard tooling. Engineering's role in Plan 01-12 is integration + wiring, not building. The single piece of "logic" Plan 01-12 writes is the `__MSG_*__` migration pass over the 8–12 hardcoded Russian strings + the `tokens.css` adoption — neither is library-grade work.
|
||
|
||
---
|
||
|
||
## Runtime State Inventory
|
||
|
||
Plan 01-12 includes brand-related runtime state changes; checking all five categories:
|
||
|
||
| Category | Items Found | Action Required |
|
||
|---|---|---|
|
||
| Stored data | `chrome.storage.local` key `onboarding-completed` (boolean, Plan 01-10). No PII; no brand text stored. | None — brand changes don't touch storage. |
|
||
| Live service config | `chrome.action.setTitle()` / `setBadgeText()` calls in `src/background/index.ts` currently pass literal Russian strings. After Plan 01-12, they pass `chrome.i18n.getMessage(...)`. Runtime state (the actual displayed tooltip / badge text per active locale) changes on next SW startup after the extension reload. | Code edit; no manual user action required. |
|
||
| OS-registered state | `manifest.json:name` changes from `"AI Call Recorder"` → `"Mokosh — Session Capture"` (per D-07). Chrome stores the manifest name in the extensions list. On `chrome://extensions` reload, the displayed name will update; old name remains briefly in any cached store listing. | None — Chrome handles on reload. |
|
||
| Secrets / env vars | None — no secrets in brand integration. | None. |
|
||
| Build artifacts | `dist/icons/icon{16,48,128}.png` currently hold the placeholder PNGs (dark square + green dot per `assets-spec.md` Path A description; verified file sizes 574 B / 1153 B / 2615 B in working tree at `/home/parf/projects/work/repremium/icons/`). After Plan 01-12 rasterizes the new mark, these are overwritten in source `icons/` and re-mirrored into `dist/icons/` on next `npm run build`. Also: `dist/manifest.json` will reflect new `name`/`description`/`default_locale`/`__MSG_*__` references; `dist/_locales/` will be a new directory. | `npm run build` after Plan 01-12 changes. Smoke test re-runs the operator UAT to confirm the new icons render correctly in toolbar + notification. |
|
||
|
||
**Verified by:** Codebase grep + file system probe. Nothing in `chrome.storage.session`, no IndexedDB brand caches, no remote services configured per brand identity.
|
||
|
||
---
|
||
|
||
## Common Pitfalls
|
||
|
||
### Pitfall 1: WOFF2 silent fallback to system fonts
|
||
|
||
**What goes wrong:** A misnamed font-family in `@font-face` (e.g. `font-family: "IBM Plex Sans Variable"` instead of `"IBM Plex Sans"`) doesn't error — the browser silently falls back to the next family in the stack (`"Segoe UI", -apple-system, ...`). Visual result: clean-looking sans-serif text, but NOT IBM Plex.
|
||
|
||
**Why it happens:** CSS `@font-face` matching is name-based; a typo creates a phantom face that no `var(--mks-font-ui)` reference resolves to.
|
||
|
||
**How to avoid:** Match the `font-family` name in `@font-face` EXACTLY to the name in `var(--mks-font-ui)` (`"IBM Plex Sans"` quoted, with the trailing space-and-Sans). The `tokens.css` already names them correctly; just verify each new `@font-face` rule matches.
|
||
|
||
**Warning signs:** Inspect popup with DevTools → Computed styles → font-family — should read `"IBM Plex Sans"` as the *applied* family, not `"Segoe UI"`. If `"Segoe UI"` appears, the `@font-face` didn't load.
|
||
|
||
### Pitfall 2: Chrome notification icon silently rejecting PNGs
|
||
|
||
**What goes wrong:** `chrome.notifications.create({iconUrl})` returns no error but the notification renders with a blank gray placeholder instead of the icon.
|
||
|
||
**Why it happens:** Chrome's `imageUtil` silently rejects PNGs below ~1 KB at 128×128 — a known artifact found during Plan 01-09 debugging (per `assets-spec.md` line 72). The rejection has no API surface; the only signal is "no icon."
|
||
|
||
**How to avoid:** Ensure `icons/icon128.png` is ≥ 1024 bytes. The rsvg-convert output at 128×128 produces ~1952 B (verified locally) — well above the floor.
|
||
|
||
**Warning signs:** Run smoke.sh, click SAVE, wait for the recovery notification. If the notification has no left-icon, the PNG was rejected. Plan 01-09's empirical UAT covers this; Plan 01-12's icon-replacement should re-run that UAT.
|
||
|
||
### Pitfall 3: `__MSG_*__` substitution failing in HTML
|
||
|
||
**What goes wrong:** Putting `__MSG_popupSaveCta__` literally in `src/popup/index.html` as text content does NOT get substituted. The string renders to the user verbatim.
|
||
|
||
**Why it happens:** `__MSG_*__` substitution only works in (a) `manifest.json` fields, (b) CSS via i18n predefined messages, and (c) JavaScript-side via `chrome.i18n.getMessage()`. HTML text content is NOT processed.
|
||
|
||
**How to avoid:** In HTML, leave the static text empty (or use a sensible default) and populate it from JS using `chrome.i18n.getMessage()` after `DOMContentLoaded`:
|
||
|
||
```html
|
||
<!-- src/popup/index.html -->
|
||
<button id="saveButton" class="save-button" disabled>
|
||
<span class="button-text"></span> <!-- populated by JS -->
|
||
</button>
|
||
```
|
||
```ts
|
||
// src/popup/index.ts in init()
|
||
const buttonText = saveButton.querySelector('.button-text') as HTMLSpanElement;
|
||
buttonText.textContent = chrome.i18n.getMessage('popupSaveCta');
|
||
```
|
||
|
||
**Warning signs:** Popup renders with `__MSG_*__` strings visible — substitution wasn't applied.
|
||
|
||
### Pitfall 4: Default locale missing a key
|
||
|
||
**What goes wrong:** A `chrome.i18n.getMessage('foo')` call returns an empty string when the user's locale messages.json has `foo` but the default_locale messages.json doesn't.
|
||
|
||
**Why it happens:** Chrome falls back to default_locale on any missing key in the user's locale — but if default_locale itself lacks the key, the chain ends. `chrome.i18n.getMessage` returns `""` (no warning).
|
||
|
||
**How to avoid:** Maintain the rule "every key present in any `_locales/*/messages.json` MUST be present in `_locales/<default_locale>/messages.json`." Write a vitest test if defensive coverage is wanted:
|
||
|
||
```ts
|
||
// tests/i18n/locale-parity.test.ts
|
||
it('every key in ru/messages.json exists in en/messages.json', () => {
|
||
const ru = JSON.parse(readFileSync('_locales/ru/messages.json', 'utf8'));
|
||
const en = JSON.parse(readFileSync('_locales/en/messages.json', 'utf8'));
|
||
for (const k of Object.keys(ru)) expect(en).toHaveProperty(k);
|
||
});
|
||
```
|
||
|
||
**Warning signs:** Empty strings in popup / notification body.
|
||
|
||
### Pitfall 5: Vite hashing breaks @font-face url() in dev mode
|
||
|
||
**What goes wrong:** In dev mode (`npm run dev`), the WOFF2 file path `./fonts/IBMPlexSans-Regular.woff2` works, but on `npm run build` the output `@font-face src` becomes `url("/assets/IBMPlexSans-Regular-<hash>.woff2")` — same file, hashed name.
|
||
|
||
**Why it happens:** Vite's asset pipeline hashes assets for cache busting. The `@import "../shared/tokens.css"` in popup CSS gets re-emitted with hashed URLs.
|
||
|
||
**How to avoid:** This is correct behavior; the dev/prod discrepancy is intentional. The pitfall is only when the planner inspects `dist/manifest.json` for a specific filename — use a regex pattern instead. CRXJS handles `web_accessible_resources` registration based on the hashed names automatically.
|
||
|
||
**Warning signs:** Hardcoded asset paths in tests; tests fail in build mode but pass in dev mode.
|
||
|
||
---
|
||
|
||
## Code Examples
|
||
|
||
Verified patterns from official sources + designer handoff:
|
||
|
||
### Subsetting a WOFF2 with pyftsubset
|
||
|
||
```bash
|
||
# Source: fontTools.readthedocs.io/en/stable/subset/
|
||
# Verified locally: pyftsubset 4.63.0 installed at /usr/bin/pyftsubset
|
||
|
||
pyftsubset IBMPlexSans-Regular.ttf \
|
||
--unicodes='U+0020-007E,U+00A0-00FF,U+0131,U+0152-0153,U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116' \
|
||
--flavor=woff2 \
|
||
--output-file=src/shared/fonts/IBMPlexSans-Regular.woff2 \
|
||
--layout-features='*' \
|
||
--no-hinting \
|
||
--desubroutinize
|
||
```
|
||
|
||
### Rasterizing a PNG icon at a specific size
|
||
|
||
```bash
|
||
# Source: rsvg-convert(1) man page (locally available)
|
||
# Verified locally: produces RGBA 8-bit non-interlaced PNG that clears Chrome floors
|
||
|
||
rsvg-convert -w 128 -h 128 src/shared/brand/mokosh-mark.svg -o icons/icon128.png
|
||
stat -c%s icons/icon128.png # confirm ≥1024 bytes (verified: ~1952)
|
||
```
|
||
|
||
### manifest.json with `__MSG_*__` + `default_locale`
|
||
|
||
```json
|
||
{
|
||
"manifest_version": 3,
|
||
"name": "__MSG_extName__",
|
||
"version": "1.0.0",
|
||
"description": "__MSG_extDesc__",
|
||
"default_locale": "en",
|
||
"action": {
|
||
"default_popup": "src/popup/index.html",
|
||
"default_title": "__MSG_tooltipOff__",
|
||
"default_icon": {
|
||
"16": "icons/icon16.png",
|
||
"48": "icons/icon48.png",
|
||
"128": "icons/icon128.png"
|
||
}
|
||
},
|
||
"icons": {
|
||
"16": "icons/icon16.png",
|
||
"48": "icons/icon48.png",
|
||
"128": "icons/icon128.png"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Reading an i18n string from JS
|
||
|
||
```ts
|
||
// Source: developer.chrome.com/docs/extensions/reference/api/i18n
|
||
// chrome.i18n.getMessage(messageName: string, substitutions?: string | string[]): string
|
||
|
||
const cta = chrome.i18n.getMessage('popupSaveCta');
|
||
buttonText.textContent = cta;
|
||
|
||
// With substitution placeholder ($1):
|
||
// messages.json: "tooltipRecWithTime": { "message": "Mokosh — идёт запись ($1)" }
|
||
const tooltip = chrome.i18n.getMessage('tooltipRecWithTime', formatElapsed(seconds));
|
||
chrome.action.setTitle({ title: tooltip });
|
||
```
|
||
|
||
---
|
||
|
||
## State of the Art
|
||
|
||
| Old approach | Current approach | When changed | Impact |
|
||
|---|---|---|---|
|
||
| Bundle TTF / OTF directly | WOFF2 only | ~2020 (universal browser support) | 30–50% smaller, native browser parsing |
|
||
| Static-weight WOFF2 files | Variable font WOFF2 | 2018 (Chrome 62+, FF 62+) | Single file replaces 5–9 weight files for Newsreader |
|
||
| Inline manifest:name | `__MSG_*__` + `_locales/` | Pre-MV3 | Required for Chrome Web Store store-listing localization |
|
||
| Google Fonts `@import` in MV2 popups | Self-hosted WOFF2 in `dist/` | MV3 mandate (2024 deprecation of MV2) | Forced by CSP `style-src 'self'` + `font-src 'self'` |
|
||
| Per-state PNG icon swaps via `chrome.action.setIcon` | Neutral mark + `setBadgeBackgroundColor` | 2015+ best practice | Lower asset count; faster state transitions; no PNG decoding per state change |
|
||
| `convert` (ImageMagick) for SVG → PNG | `rsvg-convert` (librsvg) | (preference shift, both still work) | rsvg-convert known-good for Inkscape-authored SVGs and produces predictable byte counts |
|
||
|
||
**Deprecated / outdated for this project:**
|
||
|
||
- **A-06 per-state PNG variants** (per D-06 = neutral mark + badge). Do not produce A-06 deliverables.
|
||
- **`@import url(...remote...)`** (per MV3 CSP). Removed from `tokens.css` in Plan 01-12.
|
||
- **Engineering placeholder palette** (`#2563eb`, `#dc2626`, etc. in `src/popup/style.css` and SW `BADGE_REC_COLOR = '#00C853'`). Replaced by `--mks-rec` etc. **Note:** `src/background/index.ts:45` has `BADGE_REC_COLOR = '#00C853'` as a TS literal — the badge color is set via `chrome.action.setBadgeBackgroundColor` which takes an `[r, g, b, a]` tuple OR a hex string. JS can't `var(--mks-*)` directly; Plan 01-12 needs to mirror the token value as a TS constant (e.g. `BADGE_REC_COLOR = '#b2543d' /* --mks-madder-600 */`). Document the dual ownership clearly.
|
||
|
||
---
|
||
|
||
## Environment Availability
|
||
|
||
| Dependency | Required by | Available | Version | Fallback |
|
||
|---|---|---|---|---|
|
||
| `rsvg-convert` | §3 PNG rasterization | ✓ | 2.60.0 | `inkscape --export-png` (also installed) or `convert` (ImageMagick) |
|
||
| `pyftsubset` (fontTools) | §1, §6 WOFF2 subsetting | ✓ | 4.63.0 | `glyphhanger` (npm; would need install) |
|
||
| `woff2_compress` | §1 WOFF2 encoding | ✓ | (system pkg) | bundled inside pyftsubset already; standalone tool is redundant |
|
||
| `inkscape` | §3 SVG rendering | ✓ | 1.4.4 | — |
|
||
| `convert` / `magick` | §3 SVG fallback | ✓ | (ImageMagick 7+, deprecated `convert` symlink) | — |
|
||
| `python3` | §1 subsetting + smoke.sh URL encoding | ✓ | 3.x (per pyftsubset / smoke.sh hashbang) | — |
|
||
| `node` | Vite build | ✓ | 24.14.0 | — |
|
||
| `npm` (or pnpm/yarn) | Package management | ✓ | bundled with node | — |
|
||
| `xmllint` / `brotli` | NOT required (pyftsubset bundles Brotli compression) | not checked | — | — |
|
||
| Internet access (one-off, to fetch upstream IBM Plex + Newsreader releases) | §1 source TTFs | required at execute-plan time | — | Can curl from CDN as alternate (`https://fonts.gstatic.com/...` paths leaked from the Brief's bundler — uuid'd file names extractable from same source) |
|
||
|
||
**Missing dependencies with no fallback:** None.
|
||
|
||
**Missing dependencies with fallback:** None applicable.
|
||
|
||
**Network requirement:** One-time download of TTF source files from GitHub Releases (IBM Plex ~30 MB; Newsreader ~10 MB). Within scope of any normal `curl`/`wget` step in execute-plan.
|
||
|
||
---
|
||
|
||
## Validation Architecture
|
||
|
||
### Test Framework
|
||
|
||
| Property | Value |
|
||
|---|---|
|
||
| Framework | `vitest` v4 |
|
||
| Config file | none in repo root; vitest uses defaults (verified via `package.json:24`) |
|
||
| Quick run command | `npx vitest run` |
|
||
| Full suite command | `npx vitest run` (no separate full / quick distinction in current setup) |
|
||
|
||
### Phase Requirements → Test Map
|
||
|
||
| Req ID | Behavior | Test type | Automated command | File exists? |
|
||
|---|---|---|---|---|
|
||
| D-01 / D-06 | `icons/icon{16,48,128}.png` exist, are PNGs, clear size floors | unit (build artifact assertion) | `npx vitest run tests/build/icons-present.test.ts` | ❌ Wave 0 |
|
||
| D-04 | `src/shared/tokens.css` exists; popup CSS @imports it; no `#2563eb`/`#dc2626` hex in popup CSS | unit (codebase grep) | `npx vitest run tests/build/tokens-adopted.test.ts` | ❌ Wave 0 |
|
||
| D-05 + §1 R1 | `src/shared/fonts/IBMPlexSans-{Regular,Medium,SemiBold,Bold}.woff2` exist; `Newsreader-variable.woff2` exists; tokens.css `@font-face` rules match expected count | unit (filesystem + grep) | `npx vitest run tests/build/fonts-present.test.ts` | ❌ Wave 0 |
|
||
| D-07 | `manifest.json:name === "__MSG_extName__"` AND `_locales/en/messages.json` has `extName.message === "Mokosh — Session Capture"` | unit (JSON parity) | `npx vitest run tests/i18n/manifest-i18n.test.ts` | ❌ Wave 0 |
|
||
| D-08 | `manifest.json:description === "__MSG_extDesc__"`; `_locales/{en,ru}/messages.json` has `extDesc` with the tagline text | unit (JSON parity) | `npx vitest run tests/i18n/manifest-i18n.test.ts` | (same as D-07) |
|
||
| D-09 | `dist/` after `npm run build` contains no file or content with `smoke`/`SMOKE` strings | integration (filesystem walk after build) | `npm run build && npx vitest run tests/build/no-smoke-in-dist.test.ts` | ❌ Wave 0 |
|
||
| (i18n parity) | Every key in `_locales/ru/messages.json` exists in `_locales/en/messages.json` (the default_locale) | unit | `npx vitest run tests/i18n/locale-parity.test.ts` | ❌ Wave 0 |
|
||
| (CSP) | Built `dist/src/shared/tokens.css` does not contain `fonts.googleapis.com` or any `https://` URL | unit | `npx vitest run tests/build/no-remote-fonts.test.ts` | ❌ Wave 0 |
|
||
| (Operator manual) | After load-unpacked: extension name in `chrome://extensions` reads `"Mokosh — Session Capture"`; tooltip on hover reads RU sober string; popup renders in Plex Sans; SAVE button is madder-rust color | manual-only | (operator UAT) | n/a |
|
||
|
||
### Sampling rate
|
||
|
||
- **Per task commit:** `npx vitest run tests/build/*.test.ts tests/i18n/*.test.ts` (Plan 01-12-specific tests only; ~1–2 seconds).
|
||
- **Per wave merge:** `npx vitest run && npm run build && npx tsc --noEmit` (full suite + build clean + types clean).
|
||
- **Phase gate:** Full suite green + operator empirical (load extension, verify name + popup + tooltip + notification rendering, capture screenshots) before `/gsd-verify-work`.
|
||
|
||
### Wave 0 gaps
|
||
|
||
- [ ] `tests/build/icons-present.test.ts` — covers D-01 / D-06 PNG floors
|
||
- [ ] `tests/build/tokens-adopted.test.ts` — covers D-04 token usage in popup CSS
|
||
- [ ] `tests/build/fonts-present.test.ts` — covers D-05 + §1 R1 WOFF2 presence + count
|
||
- [ ] `tests/i18n/manifest-i18n.test.ts` — covers D-07 + D-08 manifest:name/description shape + messages.json values
|
||
- [ ] `tests/i18n/locale-parity.test.ts` — covers i18n key parity across locales
|
||
- [ ] `tests/build/no-smoke-in-dist.test.ts` — covers D-09 (no-op-confirmation test)
|
||
- [ ] `tests/build/no-remote-fonts.test.ts` — covers CSP migration in tokens.css
|
||
- [ ] `tests/build/` directory may not exist yet — create it; convention matches existing `tests/{background,offscreen}/` structure.
|
||
|
||
---
|
||
|
||
## Security Domain
|
||
|
||
### Applicable ASVS categories
|
||
|
||
| ASVS category | Applies | Standard control |
|
||
|---|---|---|
|
||
| V2 Authentication | no | Plan 01-12 is brand integration; no auth surface |
|
||
| V3 Session Management | no | No sessions introduced |
|
||
| V4 Access Control | no | No new permission gates |
|
||
| V5 Input Validation | yes (loosely) | i18n `messages.json` files are JSON; ensure JSON validity in CI (vitest `JSON.parse`) |
|
||
| V6 Cryptography | no | No new crypto |
|
||
| V12 File / Resources | yes | Self-hosted WOFF2 files MUST live under `src/shared/fonts/`; no `https://` URLs in `tokens.css`; Chrome MV3 CSP enforces this at runtime |
|
||
| V14 Configuration | yes | `manifest.json:default_locale` set; `_locales/` directory complete; `web_accessible_resources` correctly auto-generated by @crxjs |
|
||
|
||
### Known threat patterns for this stack
|
||
|
||
| Pattern | STRIDE | Standard mitigation |
|
||
|---|---|---|
|
||
| Remote font load via `@import` (MV3 CSP violation) | Information Disclosure (font fingerprinting), Tampering (CDN MITM) | Self-host all fonts; explicitly assert no `https://` in built `tokens.css` via vitest |
|
||
| Malformed PNG triggering Chrome `imageUtil` rejection (Plan 01-09 known issue) | Denial of Service (notification flow blocked) | PNG byte-size floor assertions in tests (`tests/build/icons-present.test.ts`); empirical UAT |
|
||
| `_locales/` key tampering causing manifest validation failure | Denial of Service (extension fails to install) | JSON validity check in Wave 0 tests; manifest:name resolution test |
|
||
| `__MSG_*__` injection from user-controllable input | Cross-Site Scripting (low surface; manifest fields don't render arbitrary user input) | Not applicable — `__MSG_*__` is engineering-authored, no user input flow |
|
||
| WOFF2 with malicious OpenType tables | Code execution (historical browser CVEs) | Use upstream-distributed faces (IBM, Production Type); do not accept third-party fonts |
|
||
|
||
---
|
||
|
||
## Assumptions Log
|
||
|
||
Every claim tagged `[ASSUMED]` in this research. The planner and discuss-phase should confirm these with the user before locking decisions that depend on them.
|
||
|
||
| # | Claim | Section | Risk if wrong |
|
||
|---|---|---|---|
|
||
| A1 | `default_locale: "en"` is correct (Web Store convention) | §11 | If Mokosh is only distributed via Load Unpacked or operator-only channels, `"ru"` may be preferable. Low blast radius — flip the value, regenerate. |
|
||
| A2 | The Russian "Сохранить отчёт об ошибке?" (label) + "Сохранить отчёт" (button) split per Brief §02 means DOM rework in `src/popup/` | §10 string #4 | If user prefers keeping the single-button form, the rework is unnecessary. Verify before executing. |
|
||
| A3 | `src/shared/tokens.css` is the right path (vs `src/styles/tokens.css`) | "Claude's Discretion" + §9 | Low blast radius — rename, update 2–3 `@import` paths. |
|
||
| A4 | A Cyrillic-capable serif fallback should be added to `--mks-font-display` stack for Russian display text (§1 R1) | §1 BLOCKER | If user accepts the OS-serif fallback for Russian (Times New Roman on Windows, Iowan Old Style on macOS), no Cyrillic serif font needs subsetting + bundling. Lower bundle size; lower visual consistency. |
|
||
| A5 | `Newsreader-variable.woff2` covers the full italic + roman variable font range in a single file (Production Type's variable build may ship italic separately) | §1, §13 | If italic is a separate variable file, add a second `@font-face` rule with `font-style: italic;`. Verify at execute-plan time by downloading the actual release archive. |
|
||
| A6 | The smoke page tokenization (§9 Option A) is "verbose but acceptable" — adds ~30 lines to smoke.sh's heredoc | §9 | If user thinks it's too noisy, fall back to Option C (leave smoke unstyled per tokens, accept the imperfection — smoke is dev-only). |
|
||
| A7 | `BADGE_REC_COLOR` constant in `src/background/index.ts` should be updated to `#b2543d` (`--mks-madder-600`) with a comment cross-referencing the token | §10 / Anti-patterns | If user prefers keeping the existing material green, document the divergence between badge color and palette. The "dye reference" semantic of madder is the designer's intent per D-04. |
|
||
|
||
If this table is empty, all claims in this research were verified or cited. **This table has 7 entries** — the planner should surface A1, A2, A4 in particular (highest-impact assumptions) during plan creation, ideally via the discussion log or as plan-level questions.
|
||
|
||
---
|
||
|
||
## Open Questions
|
||
|
||
1. **D-05 Cyrillic for Newsreader** (BLOCKER per §1)
|
||
- What we know: Newsreader from upstream has no Cyrillic. Documented in Production Type repo and verified empirically in Decision Brief's @font-face subsets.
|
||
- What's unclear: User's preference among R1 (add Cyrillic-capable serif fallback), R2 (substitute display family), R3 (extend Newsreader — out of scope).
|
||
- Recommendation: Default to R1, add **PT Serif** as the Cyrillic fallback (OFL, ParaType, ~30 KB subsetted for RU); surface as a plan-level question.
|
||
|
||
2. **D-07 wording for English manifest:name**
|
||
- What we know: User picked `"Mokosh — Session Capture"`.
|
||
- What's unclear: Should the EN messages.json `extName.message` exactly match the user's pick, or follow D-07 alt `"Mokosh / АИ Регистратор"` for transliteration support?
|
||
- Recommendation: Exact user pick (no transliteration). Lock as-stated unless user clarifies.
|
||
|
||
3. **Plan 01-10 coordination**
|
||
- What we know: Plan 01-10 created `src/welcome/welcome.{html,ts,css}` with placeholder palette (`#00C853` green button, `#1f1f1f` text). Plan 01-12 wants to ingest tokens.
|
||
- What's unclear: Does Plan 01-12 rewire Plan 01-10's CSS now (single-pass token migration), or does Plan 01-12 leave Plan 01-10 alone and a future Plan 01-13 / 02-XX migrates welcome separately?
|
||
- Recommendation: Rewire in Plan 01-12. Welcome page is in Phase 1 scope; one-pass migration is cheaper than two-pass.
|
||
|
||
4. **D-08 tagline wording mismatch**
|
||
- What we know: Brief §02 wrote `"Thirty seconds ago, always within reach."`; brand-decisions-v1.md wrote `"Thirty seconds ago, always at hand."`. Both attribute to D-08 user-accepted.
|
||
- What's unclear: Which exact wording for the EN parallel?
|
||
- Recommendation: Default to brand-decisions-v1.md (`"always at hand."`) as it's the more recent doc and reflects the user's final pick. Surface as a single confirmation question.
|
||
|
||
5. **Plex Sans variable font availability**
|
||
- What we know: IBM published `@ibm/plex-sans-variable@0.2.0` (June 2024). The static per-weight files are also distributed.
|
||
- What's unclear: Whether the variable build is mature enough to replace the 4 static weights (which would shave ~80 KB).
|
||
- Recommendation: Use static per-weight files (Pattern A from §1) for the first pass. Migrate to variable in a future polish phase if bundle size matters.
|
||
|
||
6. **Brief §02 string #4 popup-DOM rework**
|
||
- What we know: Brief splits "Сохранить отчёт об ошибке?" (label) from "Сохранить отчёт" (button).
|
||
- What's unclear: Single-button vs label+button.
|
||
- Recommendation: A2 in Assumptions Log. Default to single-button form; ask user during execute-plan if rework is wanted.
|
||
|
||
---
|
||
|
||
## Sources
|
||
|
||
### Primary (HIGH confidence)
|
||
|
||
- `tokens.css` (verified verbatim, all 281 lines read)
|
||
- `mokosh-mark.svg`, `mokosh-lockup.svg` (verified verbatim)
|
||
- `manifest.json`, `vite.config.ts`, `package.json` (codebase-verified)
|
||
- `src/popup/index.html`, `src/popup/index.ts`, `src/popup/style.css`, `src/background/index.ts` lines 45, 54, 766–768 (codebase-verified)
|
||
- `smoke.sh` (codebase-verified)
|
||
- `.planning/intel/brand-decisions-v1.md` (verbatim read)
|
||
- `.planning/intel/assets-spec.md` (verbatim read)
|
||
- Decision Brief embedded `@font-face` declarations (extracted via Python JSON-decode, see tool-results)
|
||
- Decision Brief §02 8 copy strings (extracted verbatim)
|
||
- Local empirical: `rsvg-convert` rasterization verified at 16/48/128 px
|
||
- Local empirical: `pyftsubset` + `woff2_compress` installed + working
|
||
- [@crxjs/vite-plugin README on GitHub](https://github.com/crxjs/chrome-extension-tools/blob/main/packages/vite-plugin/README.md)
|
||
- [Chrome i18n API docs](https://developer.chrome.com/docs/extensions/reference/api/i18n)
|
||
- [Manifest default_locale docs](https://developer.chrome.com/docs/extensions/mv3/manifest/default_locale/)
|
||
- [Vite Static Asset Handling](https://vite.dev/guide/assets)
|
||
- [Vite env vars and modes](https://vite.dev/guide/env-and-mode)
|
||
- [SIL Open Font License 1.1](https://openfontlicense.org/) + [OFL-FAQ](https://openfontlicense.org/ofl-faq/)
|
||
- [IBM Plex repo](https://github.com/IBM/plex)
|
||
- [Newsreader repo (productiontype/Newsreader)](https://github.com/productiontype/Newsreader)
|
||
- [fontTools pyftsubset docs](https://fonttools.readthedocs.io/en/stable/subset/)
|
||
|
||
### Secondary (MEDIUM confidence)
|
||
|
||
- [Markos Konstantopoulos on font subsetting](https://markoskon.com/creating-font-subsets/)
|
||
- [Walter Ebert on subsetting web fonts](https://walterebert.com/blog/subsetting-web-fonts/)
|
||
- [Stefan Van Damme: i18n MV3 tutorial (2023)](https://www.stefanvd.net/blog/2023/04/24/how-to-add-internationalization-i18n-chrome-extension/)
|
||
|
||
### Tertiary (LOW confidence — flagged)
|
||
|
||
- WebSearch's initial claim that "Newsreader supports Cyrillic" — **contradicted by primary sources** (Decision Brief subsets, Production Type repo README). Resolved as BLOCKER §1.
|
||
- WebSearch claim about Chrome notification icon 3:2 ratio and `imageUtil` floors — official docs don't document these; empirical Plan 01-09 finding is the only confirmed reference (`assets-spec.md:72`). The 1024-byte floor is verified by Plan 01-09 ops; not in any docs page.
|
||
|
||
---
|
||
|
||
## Metadata
|
||
|
||
**Confidence breakdown:**
|
||
|
||
- Standard stack: **HIGH** — all tools verified locally; versions checked via `--version` and `npm view`.
|
||
- Architecture (tokens.css integration, @crxjs WAR auto-gen): **HIGH** — codebase + official docs.
|
||
- WOFF2 self-hosting for Plex faces: **HIGH** — canonical subset structure verified from Decision Brief.
|
||
- WOFF2 for Newsreader: **MEDIUM** — Latin-only subset is well-documented; Cyrillic handling required §1 BLOCKER resolution.
|
||
- i18n via `__MSG_*__`: **HIGH** — official docs + repo + 2023 tutorial concur.
|
||
- PNG rasterization: **HIGH** — locally empirically verified rsvg-convert at 16/48/128.
|
||
- Smoke gating (D-09): **HIGH** — codebase grep + smoke.sh inspection confirm no-op status.
|
||
- Notification icon floors (1024 B): **MEDIUM** — official docs don't document; relies on Plan 01-09 empirical finding.
|
||
- Operator string i18n migration: **HIGH** — strings verbatim from Brief §02; call sites grep-verified.
|
||
|
||
**Research date:** 2026-05-17
|
||
|
||
**Valid until:** ~30 days (2026-06-16) for stable claims (font upstreams, OFL, Chrome i18n API). 7 days for cutting-edge claims (`@crxjs/vite-plugin@2.4.0`, IBM Plex variable v0.2.0 maturity).
|