Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions POSITIONING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ REDACTED — internal only, never on any public surface.)
note the earlier "Europe-led / Korean ~0 / US minor" read was **wrong**: the US
is a **top-2 market** and Korea is mid-pack (~#6), not negligible. Default
framing is global / English-default — a single English CWS listing (ko/ja store
listings were dropped; the in-product UI stays localized in 11 languages). If a
listings were dropped; the in-product UI stays localized in 12 premium languages). If a
localized listing is ever revisited, Japanese and Korean (each ~#6 by
language/region) are the first candidates — not a priority now.
- **Why Italy is #1 — a verified organic-referral mechanism** (web-verified
Expand Down Expand Up @@ -328,7 +328,7 @@ Once #1–#2 are cleared:
- If a PR meaningfully improves the AI tutor's contextual awareness of
the current lesson, default ship.
- If a PR ships a new feature without updating the relevant `_LABELS`
dict to cover all 11 premium languages, default reject (we don't ship
dict to cover all 12 premium languages, default reject (we don't ship
English-only UI in a "translate this for non-English speakers"
product).
- If two PRs conflict and one has measurable Korean-user impact, the
Expand Down
5 changes: 3 additions & 2 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,9 @@ narrowed.
- ~~**CWS listing — multilingual translations.**~~ Dropped (v3.5.39): the CWS
listing is English-only. Every locale falls back to EN, and hand-maintaining
parallel localized listings caused drift (#158) for marginal per-market gain.
The extension UI stays localized in 11 languages; only the store metadata is
EN-only.
The in-product UI stays localized in 12 premium languages (and the browser-facing
extension name/description in the 33 `_locales/` Chrome-metadata locales); only the
Chrome Web Store *listing copy* (screenshots / long description) is EN-only.
- [ ] **YouTube `_BG_YT_CLIENT_VERSION` auto-bump GH Action.** Currently
manual every few weeks (see comment in `src/background/background.js`).
Cron workflow that pings InnerTube and opens a PR when stale. Same
Expand Down
2 changes: 1 addition & 1 deletion docs/TELEMETRY_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ Approximate scope when this is approved:
2. `src/background/background.js`: wire `chrome.runtime.onInstalled` + `chrome.runtime.setUninstallURL`, hook the global `self.addEventListener('error', ...)` for service worker errors.
3. `src/content/content.js`: hook `window.addEventListener('error', ...)` and `window.addEventListener('unhandledrejection', ...)` — gated behind opt-in flag.
4. `src/popup/popup.html` + `popup.js`: two new toggles, "Delete my data" button, "What we collect" link to privacy policy.
5. `_locales/*/messages.json`: 11 premium-language strings for the new popup copy. **POSITIONING operating-principle gate**: "default reject" for new UI without all-11-language coverage.
5. `_locales/*/messages.json`: new popup-copy strings (browser-metadata layer, all 33 `_locales/`). **POSITIONING operating-principle gate**: "default reject" for new in-product UI without all-12-premium-language coverage in the `src/lib/constants.js` `_LABELS` dicts.
6. `docs/privacy.html`: the rewrite above.
7. `tests/lib/telemetry.test.js`: ~10 unit tests covering payload sanitization, regex stripping, slug-hash determinism, opt-out clears `client_id`.
8. `tests/e2e/telemetry.spec.js`: 1 scenario — toggle on, trigger an error, intercept the POST, assert payload shape and excluded-fields.
Expand Down
17 changes: 16 additions & 1 deletion scripts/check-i18n-keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ function matchExports(src) {
let dicts;
try {
const names = matchExports(constantsSrc);
// matchExports targets the *_LABELS/UI/QUESTIONS/... dicts, but we also need
// PREMIUM_LANGUAGE_CODES to derive the expected-language set (below) from the
// source of truth instead of a hand-maintained list that goes stale.
if (/\bconst PREMIUM_LANGUAGE_CODES\b/.test(constantsSrc) && !names.includes('PREMIUM_LANGUAGE_CODES')) {
names.push('PREMIUM_LANGUAGE_CODES');
}
// Both source files reference `window` at the top; provide a stub so the
// top-level `if (typeof window !== 'undefined')` guards don't trip.
const runner = new Function('window', `${selectorsSrc}\n${constantsSrc}\nreturn { ${names.join(', ')} };`);
Expand All @@ -109,7 +115,16 @@ try {
process.exit(errors > 0 ? 1 : 0);
}

const expectedLangs = new Set(['en', 'ko', 'ja', 'zh-CN', 'zh-TW', 'es', 'fr', 'de', 'pt-BR', 'ru', 'vi']);
// English + every premium-dictionary language. Derived from PREMIUM_LANGUAGE_CODES
// so it self-updates when a premium locale is added — the previous hand-coded list
// was en+10 and silently missed the it/id UI labels. Falls back to the full 12 if
// the constant can't be loaded.
const PREMIUM_FALLBACK = ['ko', 'ja', 'zh-CN', 'zh-TW', 'es', 'fr', 'it', 'de', 'pt-BR', 'ru', 'vi', 'id'];
const premiumCodes =
Array.isArray(dicts.PREMIUM_LANGUAGE_CODES) && dicts.PREMIUM_LANGUAGE_CODES.length
? dicts.PREMIUM_LANGUAGE_CODES
: PREMIUM_FALLBACK;
const expectedLangs = new Set(['en', ...premiumCodes]);

/**
* Validate a flat lang map: { en: 'x', ko: 'x', ... }.
Expand Down
8 changes: 8 additions & 0 deletions src/lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -364,10 +364,12 @@ const BANNER_UI = {
'zh-TW': { prompt: '將此頁面翻譯為', confirm: '翻譯', dismiss: '關閉' },
es: { prompt: '¿Traducir esta página a', confirm: 'Traducir', dismiss: 'Cerrar' },
fr: { prompt: 'Traduire cette page en', confirm: 'Traduire', dismiss: 'Fermer' },
it: { prompt: 'Traduci questa pagina in', confirm: 'Traduci', dismiss: 'Chiudi' },
de: { prompt: 'Diese Seite übersetzen auf', confirm: 'Übersetzen', dismiss: 'Schließen' },
'pt-BR': { prompt: 'Traduzir esta página para', confirm: 'Traduzir', dismiss: 'Fechar' },
ru: { prompt: 'Перевести эту страницу на', confirm: 'Перевести', dismiss: 'Закрыть' },
vi: { prompt: 'Dịch trang này sang', confirm: 'Dịch', dismiss: 'Đóng' },
id: { prompt: 'Terjemahkan halaman ini ke', confirm: 'Terjemahkan', dismiss: 'Tutup' },
};

// Onboarding banner for ALL first-time visitors (including English speakers)
Expand Down Expand Up @@ -500,6 +502,12 @@ const EXAMPLE_QUESTIONS = {
'Fammi un quiz su questa lezione',
'Riassumi questa lezione',
],
id: [
'Jelaskan konsep ini secara sederhana',
'Apa poin-poin pentingnya?',
'Beri saya kuis tentang pelajaran ini',
'Ringkas pelajaran ini',
],
};

const DASHBOARD_LABELS = {
Expand Down
Loading