diff --git a/OpenScreenShot-claude-code-brief.md b/OpenScreenShot-claude-code-brief.md new file mode 100644 index 0000000..e2b70c0 --- /dev/null +++ b/OpenScreenShot-claude-code-brief.md @@ -0,0 +1,409 @@ +# OpenScreenShot — Claude Code implementation brief + +Hand this file to Claude Code in the PGHQdev/OpenScreenShot repo. Do the work in this order. Do not skip P0. Do not invent a subscription, a cloud account, standing host permissions, or a rename of the GitHub/brand. + +- Repo: https://github.com/PGHQdev/OpenScreenShot +- Store: https://chromewebstore.google.com/detail/hdabbojjccojlapnfjpdppcpfcnhgmdp (hdabbojjccojlapnfjpdppcpfcnhgmdp) +- Site: https://openscreenshot.app/ (Astro app in site/) +- Date of brief: 2026-08-31 + +--- + +## 0. How to use this file + +You are implementing a positioning and packaging change, not a new product. + +1. Read sections 1-3 so you do not helpfully add features to the store title or request extra permissions. +2. Ship P0 as one PR (extension UX + locale strings + listing copy file + screenshot pipeline + review prompt + extra locales). +3. Ship P1 as a second PR (site compare page, README, homepage copy). +4. Leave P2 and Human / CWS console as a checklist at the end of the PR description. The founder uploads screenshots and pastes store copy in the Chrome Web Store dashboard. You cannot publish the store listing from git. + +If a path in this brief is slightly off, search the repo. The flags and files named below were confirmed on main at v1.5.0. + + +--- + +## 1. What we are trying to win + +Three scoreboards, in this order: + +1. Chrome Web Store installs + reviews, low churn. This is the emergency. 1,000 users and 4 ratings vs FullPage Capture at 100,000 users / 860 ratings / Featured, and a second clone at number 1 in CWS search. +2. GitHub stars, talk, backlinks, domain rating. Currently about 67 stars, a 14-point Show HN, absent from 2026 best-screenshot-extension roundups. +3. Later: a paid enterprise version. Modelled on GoFullPage (free capture forever, $12/yr consumer editor, custom enterprise for legal/gov/finance). Do not put a paywall on capture, annotate, blur, or PDF. Nimbus died that way. FullPage Capture's own reviews already say "don't add a subscription." + +### Position (memorize this) + +OpenScreenShot is the one-click full page screenshot you can actually audit. + +Same job as GoFullPage. The only listing still in the store that is really open source (MIT, public, linked) and really permissionless (host_permissions empty at install). FullPage Capture says the same privacy words while requesting standing access to every website and shipping a closed binary. + +CWS search is one query: full page screenshot. One-click is the product. Annotate is why they stay. Source plus zero host permissions is why they trust you over the clone. Recording, CLI, and MCP are GitHub and the website, not the store hero. + + +### What happened in the market (do not recode this, just do not fight it) + +- GoFullPage (11M users) was removed from CWS on 11 Aug 2026 for a copyright dispute, not malware. Chrome still showed "might be unsafe." Refugees grabbed whatever ranked for "full page screenshot." +- FullPage Capture (fullpagecapture.net) is a rename of an old listing (previously Screenshot Master), not a 13-day-old startup. Featured. 100k users. Privacy policy dated 18 Aug 2026. Standing access to every site. Not open source. Uninstall survey can send the last 5 page URLs. +- A second clone titled "Full Page Screenshot - Screen Capture and Editor" is number 1 in CWS search. OpenScreenShot is not on page 1. +- GoFullPage BETA is live at 100k users. Homepage still claims 11M. +- Winning tiles in search are saturated color blocks with "one click" in huge type. Ours are grey editor UI screenshots. Invisible at thumbnail size. +- GoFullPage privately forked in 2018 and still links the old MIT repo. We are the only one whose OSS claim is true. + +Search the repo for expressMode. It already exists and defaults to false. Popup is src/popup. Capture is src/background. Settings types are src/shared/types.ts. + +--- + +## 4. P0 — ship this week (one PR) + +### P0.1 Default toolbar click = full page capture + +Goal: first click after install behaves like GoFullPage and FullPage Capture. Click the icon, full page starts. No mode menu. + +1. Set DEFAULT_SETTINGS.expressMode to true for new installs. +2. Migrate existing installs to expressMode true once. Stored settings already persist false. If you only change the default, current users keep the picker. Bump a settings version or a one-shot migrated flag. On first run after this version, turn expressMode on unless you can prove the user toggled it themselves. Keep the settings toggle so they can go back. +3. When expressMode is true, a toolbar click captures full page (same pipeline as the popup Full Page item). Do not show the mode-picker popup. Reuse existing capture progress so the click does not feel dead. After capture, honor captureAction (editor / clipboard / download). Default stays editor. +4. When expressMode is false, current popup behavior, unchanged. +5. Other modes stay obvious: page right-click menu (full, visible, region, record, settings), existing keyboard shortcuts. Add a menu item that opens the picker/settings when the default popup is gone. +6. Settings label in plain language: "Clicking the toolbar icon captures the full page." Help text: "Turn off to choose Full page, Visible, or Region each time." +7. First capture after the migration: one dismissible note, once. "Toolbar click now captures the full page. Right-click for other modes." Never show again after dismiss. + +Tests: default true, migration, and toolbar-click branching. Manual: fresh profile captures on click; upgraded profile migrates; toggle off restores picker; shortcuts and context menu still work. + +Do not make region or visible the default. Full page is the CWS job. + + +### P0.2 Locale plus store name and short description + +Chrome listing name max 45 characters. Short description max 132 characters. + +Update the English messages (and every other locale): + +- extName: Full Page Screenshot - OpenScreenShot + (38 characters, fits) +- extDesc: Full page screenshot in one click. Open source, 100% private, no account, no uploads. + +Also put these exact strings in store/STORE_LISTING.md (create that file) so the founder can paste them into the Chrome Web Store dashboard, which does not always take extName as the visible title. + +Translate extName and extDesc in every locale. Keep the English keyword "Full Page Screenshot" in the English name. In other languages, use the natural equivalent of "full page screenshot" first, then OpenScreenShot. + +Leave the manifest name and description pointing at those message keys. The toolbar tooltip can stay OpenScreenShot. + + +### P0.3 Store long description (paste-ready) + +Create store/STORE_LISTING.md with the block below. This is the Chrome Web Store detailed description. First screen (before see-more) must sell one-click plus auditability. Features go below the fold. Use this English text unless a character limit forces a trim. Do not add an emoji wall. + +Title: Full Page Screenshot - OpenScreenShot + +Short description: Full page screenshot in one click. Open source, 100% private, no account, no uploads. + +Detailed description: + +The full page screenshot you can actually audit. + +Click the icon. Get the entire scrolling page. Annotate, export PNG or PDF, done. + +OpenScreenShot is MIT-licensed, 100 percent private, and asks for no standing access to the sites you visit. No account, no watermark, no uploads, no telemetry. It works with Wi-Fi off. Read the source: https://github.com/pghqdev/OpenScreenShot + +HOW IT WORKS +- Click the OpenScreenShot icon. The page scrolls and stitches itself into one image. +- Annotate with arrows, boxes, text, numbered steps, blur, and crop. All free. +- Export PNG, JPEG, WebP, or PDF, or copy to the clipboard. +- Right-click the page for visible-area or region capture, or to record the tab. + +WHY THIS ONE +- One click. No mode menu in the way of a full page screenshot. +- Open source (MIT). You can read every line. +- No standing site access at install. Access is only the tab you just captured. +- No account, no cloud, no watermark. Captures never leave your machine. +- Sticky headers appear once. Nested scrollers and long pages are handled. +- Annotate, blur, crop, and PDF are free. + +MORE (when you need it) +- Visible area and selected-region capture from the right-click menu or shortcuts. +- Tab recording with auto-zoom, trim, and WebM export. Optional, local, no account. +- Keyboard: Ctrl or Cmd Shift S full page. Other shortcuts stay as they are today. + +OpenScreenShot is free and open source. +Website: https://openscreenshot.app +Source: https://github.com/pghqdev/OpenScreenShot +Privacy: https://openscreenshot.app/privacy/ + +Do not put CLI, MCP, a who-it-is-for list, or sister-product cross-promos in the first screen. + + +### P0.4 Store gallery slides + +Five marketing slides, not raw UI captures. Extend the existing shots script. Put the PNGs in store/screenshots. + +1. Red: THE WHOLE PAGE. ONE CLICK. +2. Cyan: SCROLLS. STITCHES. DONE. +3. Black: ANNOTATE. NO PAYWALL. +4. Green: WI-FI OFF. STILL WORKS. +5. Orange: MIT. READ THE SOURCE. + +Huge type on a saturated field. Founder uploads them in that order. + + +### P0.5 Review prompt + +We have four ratings. That is why the listing looks empty next to hundreds of reviews. + +After a successful export or copy, not after every capture and not on first run, show one dismissible local prompt: + +If OpenScreenShot saved you a minute, a rating on the Chrome Web Store helps other people find a private tool. +Buttons: Rate on Chrome Web Store. Not now. + +Rules: +- Only after 3 successful exports or copies on that install. +- Once per install. If they click either button, never show again (a storage flag). +- Link the official listing reviews tab. +- No blocking the export, no fake stars, no network call, no uninstall survey. + +### P0.6 More languages + +Winners ship dozens of locales. We ship about 11. Add at least: Spanish, Latin American Spanish, Brazilian Portuguese, German, French, Italian, Japanese, Korean, Simplified Chinese, Traditional Chinese, Russian, Polish, Dutch, Turkish, Vietnamese, Indonesian, Arabic, Hindi. + +Minimum for each: extName, extDesc, popup mode labels, the express-mode settings string, context menu labels, review-prompt strings, and the errors a user sees on first capture. + +Do not machine-translate the privacy policy in this PR. + +### P0.7 Icon + +Lower priority than the gallery. Only if time: a mark that reads at 16 pixels. The search tile matters more. + + +--- + +## 5. P1 — this month (second PR) + +### P1.1 Homepage copy + +Files: the Astro homepage plus site i18n strings. + +Keep the H1 energy. Put "one click" in the subhead so it matches the store query. + +English: +- Eyebrow: Free · Open source · MIT licensed +- H1: Click. Captured. Done. +- Subhead: Full page screenshot in one click. So private it works with Wi-Fi off. No account, no watermark, no cloud. +- Primary CTA: Add to Chrome +- Secondary: View on GitHub + +Move CLI, MCP, and Record below the privacy/trust grid. Do not delete them. Demote them. + +Update the capture feature tile so the first sentence is: Click the icon. Get the whole page. + + +### P1.2 Comparison page + +Add a page at openscreenshot.app/compare/ + +Title: OpenScreenShot vs GoFullPage vs FullPage Capture + +Intro: +GoFullPage was removed from the Chrome Web Store in August 2026. OpenScreenShot is the open-source full page screenshot you can actually audit. FullPage Capture is a closed extension that says local while asking for access to every website. + +Table, facts only, no insults. + +- In the Chrome Web Store as of 31 Aug 2026: OpenScreenShot yes. FullPage Capture yes. GoFullPage main listing removed, BETA only. +- Open source: OpenScreenShot yes, MIT, public. FullPage Capture no. GoFullPage ancestor repo, product privately forked since 2018. +- Standing site access: OpenScreenShot none. FullPage Capture every site. GoFullPage none at install. +- One-click full page: all three yes. +- Annotate, blur, crop: OpenScreenShot free. FullPage Capture free. GoFullPage paid. +- PDF extras: OpenScreenShot free. FullPage Capture free. GoFullPage paid. +- Tab recording: OpenScreenShot free. The other two no. +- Readable source: github.com/pghqdev/OpenScreenShot. FullPage Capture none. GoFullPage old GitHub, not the shipping binary. + +CTA: Add to Chrome. Secondary: Star on GitHub. + +Legal: facts only. Private fork since 2018 is from GoFullPage's own FAQ. Do not say malware, stole, or scam. + +Link it from the homepage once. Add it to the sitemap and footer. + +### P1.3 README + +New lede: + +OpenScreenShot. One-click full page screenshots for Chrome. MIT licensed. Nothing leaves your machine. + +Most screenshot extensions want an account, every site you visit, or money to draw an arrow. This one captures the whole scrolling page when you click the icon, lets you annotate for free, and publishes every line. + +Keep badges, install, the permissions table, and dev docs. Add a short compare table and a link to the compare page. Mention toolbar click equals full page. Keep CLI and MCP below Features. + +### P1.4 Docs + +First recipe: click the icon to capture the full page. Document the settings toggle. + + +--- + +## 6. P2 — do not implement now + +Leave these in the PR description as not in this change: + +- No consumer paid plan. Capture, annotate, blur, crop, and PDF stay free forever. +- Enterprise later: seats, signed builds, security questionnaire, guaranteed capture, support. Sold to legal, government, and finance. The audit is the public source. Do not stub a paywall. +- Ko-fi stays. +- Product Hunt, Show HN, and pitching 2026 roundups are founder tasks. The compare page is the asset they need. + +--- + +## 7. Human checklist after the PR (founder) + +Claude Code cannot publish the store. + +1. Paste title, short description, and detailed description from store/STORE_LISTING.md. +2. Upload the five slides in order. Replace every current UI screenshot. +3. Confirm the privacy declarations still say the extension does not collect data. +4. Submit for review. +5. After it is live: install on a clean Chrome profile and time click-icon-to-full-page. If a picker appears, P0.1 failed. +6. Ask current users for a store review. Four ratings is the social-proof bug. + + +--- + +## 8. Acceptance criteria + +Extension: +- Fresh install: toolbar click starts a full-page capture. No mode menu. +- Upgraded install: same, unless the user turned the setting off. +- Setting off: popup picker restored. +- Right-click menu still has full, visible, region, record, settings. +- Shortcuts unchanged. +- No new install-time site access. No new network calls. +- Review prompt appears once after 3 exports, never again after dismiss. +- Unit tests cover expressMode default, migration, and toolbar-click branching. + +Listing assets: +- extName is Full Page Screenshot - OpenScreenShot (45 character max). +- extDesc is the one-click / open source / private line (132 character max). +- store/STORE_LISTING.md exists with paste-ready copy. +- Five billboard PNGs in store/screenshots. Headlines match P0.4. +- New locales exist with name, short desc, and UI chrome. + +Site / GitHub if P1 is included: +- Homepage subhead contains "one click" and still says Wi-Fi off / MIT. +- /compare/ renders the table and does not accuse anyone of malware. +- README lede is one-click plus MIT, not a feature dump. + +--- + +## 9. Suggested PRs + +PR 1: Make toolbar click capture the full page, and package the store for one-click search. + +PR 2: Add /compare, retarget homepage and README at one-click plus auditability. + +Do not mix a recorder rewrite, a new capture engine, or a rebrand into these PRs. + +--- + +## 10. Copy deck + +- CWS title: Full Page Screenshot - OpenScreenShot +- CWS short: Full page screenshot in one click. Open source, 100% private, no account, no uploads. +- Position: The one-click full page screenshot you can actually audit. +- Settings toggle: Clicking the toolbar icon captures the full page +- Site H1: Click. Captured. Done. +- Site subhead: Full page screenshot in one click. So private it works with Wi-Fi off. No account, no watermark, no cloud. + +That is the whole job. Default the icon. Billboards not UI shots. Tell the truth they cannot copy: MIT, no extra site access, Wi-Fi off. + + +--- + +## 11. Rating funnel (added 2026-08-31, replaces the old P0.5 "no uninstall" line) + +Intentional, not dark. FullPage Capture puts Rate in the capture chrome, opens a welcome page that makes you try a shot immediately, and catches uninstalls. Do that. Do not send the last pages they visited. Do not attach any captured URL to the uninstall form. Version and locale only. + +CWS review URL: +https://chromewebstore.google.com/detail/hdabbojjccojlapnfjpdppcpfcnhgmdp/reviews + +### Surface A — Welcome page on first install + +On chrome.runtime.onInstalled with reason install, open a bundled welcome page (same idea as their welcome/welcome.html). + +Headline: You're in. Click the icon. + +Body: OpenScreenShot captures the whole page in one click. No account. Nothing leaves this machine. Try it on this page right now. + +Primary line: Click the OpenScreenShot icon in the toolbar. + +Secondary: Or press Ctrl+Shift+S (Cmd+Shift+S on Mac). + +The welcome page itself should be a tall, colorful dummy article so a full-page capture looks good. After they capture, the editor/result chrome is the next surface. + +Do not open welcome on update, only on fresh install. + +### Surface B — Rate is always in the capture chrome + +On the result page and the editor header, add a star control labeled Rate, next to Save / PDF / Copy. Always visible. Not a modal. One click opens the CWS reviews tab. + +Tooltip: 30 seconds. It is how other people find a private screenshot tool. + +If they click it, set a local flag ratedOrDismissed so Surface C never nags. + +### Surface C — One post-success prompt + +After 3 successful exports or copies, if they have not used the Rate star, show a non-blocking banner or modal: + +Headline: Is this saving you time? + +Body: A short rating on the Chrome Web Store is the main reason a private tool gets found. Four reviews looks like nobody uses this. You can change that. + +Buttons: Rate on Chrome Web Store. Not now. + +Once per install. Never blocks save. No network call. + +### Surface D — Uninstall page (this is the one they asked for) + +Chrome cannot show an extension page after uninstall. Use chrome.runtime.setUninstallURL pointing at a page we host: + +https://openscreenshot.app/uninstall + +Set it on startup. Query allowed: extension version, locale. Query forbidden: any page URL, any capture, any title. + +Add site/src/pages/[...lang]/uninstall as an Astro page. No analytics. No cookies. Optional typed feedback only if they hit Send. + +Layout: + +Eyebrow: OpenScreenShot +Headline: Before you go. +Subhead: If it worked, a rating is the whole growth engine. If it failed, tell us. We read it. + +Two paths, not one dump form: + +Card 1 (happy / it was fine) +Headline: It worked, I just don't need it. +Body: Then a public rating is the highest-leverage thing you can do for an open-source tool. It takes 30 seconds. +Button: Rate OpenScreenShot (opens CWS reviews, new tab) + +Card 2 (broken / missing) +Headline: Something was wrong. +Body: What happened? What did you expect? +Textarea placeholder: Example: full page missed the footer on a dashboard. Or: I wanted region capture on first click. +Button: Send feedback +After send: Thanks. If you are willing, a rating still helps people who need a private option. [Rate anyway] + +Do not ask for email. Do not pre-fill the URL they were on. Do not list "last pages." If you need a backend for the textarea, use a simple form endpoint the founder already has, or GitHub issue create with a template. If neither exists, mailto:support@minimalistprojects.com with subject Uninstall feedback. + +Footer: Source is still public. github.com/pghqdev/OpenScreenShot + +Uninstall copy must not guilt, dark-pattern, or claim they will lose files (captures are already local). + +### What not to copy from FullPage Capture + +- Do not ship the last N page addresses on uninstall. +- Do not put Rate behind a "we read every report" issue modal only. The star lives in the header. +- Welcome page is for trying a capture, not for a feature tour. + +### Acceptance add-ons + +- Fresh install opens welcome. Update does not. +- Welcome page can be captured with the toolbar icon (one-click). +- Rate star is visible on result and editor. +- Uninstall of a loaded unpacked or store build opens openscreenshot.app/uninstall. +- The uninstall URL contains no page URLs. +- Privacy page mentions: uninstall opens a local-to-us feedback page. We do not receive your browsing history. + diff --git a/PRIVACY.md b/PRIVACY.md index 1dbe26a..4dd128a 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -45,6 +45,14 @@ The microphone and camera, when you enable them, are opened directly by your bro permission prompt — the extension never sees a stream until you grant it, and the stream never leaves your device. +## Uninstall + +If you uninstall the extension, your browser opens a feedback page at +. The page's address carries only the extension +version and your UI language. We do not receive your browsing history, any page you +visited, or any capture — the extension has none of that to send. The page itself sets no +cookies and runs no analytics; nothing is sent unless you type feedback and press Send. + ## Third-party services OpenScreenShot does not use any third-party analytics, advertising, or data-processing services. PDF export is handled by a small writer built into the extension (`src/editor/pdf-writer.ts`), so exporting a PDF makes no network request either. diff --git a/media/store/cws-1.jpg b/media/store/cws-1.jpg index c2ed406..4054eaa 100644 Binary files a/media/store/cws-1.jpg and b/media/store/cws-1.jpg differ diff --git a/media/store/cws-2.jpg b/media/store/cws-2.jpg index 18527c4..bc524bc 100644 Binary files a/media/store/cws-2.jpg and b/media/store/cws-2.jpg differ diff --git a/media/store/cws-3.jpg b/media/store/cws-3.jpg index 69ecc74..025d23c 100644 Binary files a/media/store/cws-3.jpg and b/media/store/cws-3.jpg differ diff --git a/media/store/cws-4.jpg b/media/store/cws-4.jpg index 33fe925..56f9a0c 100644 Binary files a/media/store/cws-4.jpg and b/media/store/cws-4.jpg differ diff --git a/media/store/marquee.jpg b/media/store/marquee.jpg index 1a9b85d..31f8509 100644 Binary files a/media/store/marquee.jpg and b/media/store/marquee.jpg differ diff --git a/public/_locales/ar/messages.json b/public/_locales/ar/messages.json new file mode 100644 index 0000000..c9b1406 --- /dev/null +++ b/public/_locales/ar/messages.json @@ -0,0 +1,116 @@ +{ + "extName": { + "message": "لقطة شاشة للصفحة الكاملة - OpenScreenShot", + "description": "Extension name shown in Chrome UI." + }, + "extDesc": { + "message": "لقطة شاشة للصفحة الكاملة بنقرة واحدة. مفتوح المصدر، خصوصية 100%، بلا حساب، بلا رفع.", + "description": "Extension description shown in the Chrome Web Store and extensions page." + }, + "modeFullPage": { + "message": "الصفحة الكاملة", + "description": "Capture mode: capture the entire scrolling page." + }, + "modeFullPageSub": { + "message": "التقاط الصفحة القابلة للتمرير بأكملها", + "description": "Subtitle for the Full Page capture mode." + }, + "modeVisible": { + "message": "المنطقة المرئية", + "description": "Capture mode: capture what is currently visible." + }, + "modeVisibleSub": { + "message": "ما تراه الآن", + "description": "Subtitle for the Visible Area capture mode." + }, + "modeRegion": { + "message": "منطقة محددة", + "description": "Capture mode: capture a user-dragged region." + }, + "modeRegionSub": { + "message": "انقر واسحب للتحديد", + "description": "Subtitle for the Selected Region capture mode." + }, + "repeatLastRegion": { + "message": "تكرار آخر منطقة", + "description": "Footer link and context menu item that re-captures the previous region selection." + }, + "settingsTitle": { + "message": "الإعدادات", + "description": "Settings panel heading." + }, + "expressLabel": { + "message": "النقر على أيقونة شريط الأدوات يلتقط الصفحة الكاملة", + "description": "Label for the capture-on-icon-click row in the popup, and the icon context-menu checkbox." + }, + "expressHint": { + "message": "أوقفه لاختيار الصفحة الكاملة أو المنطقة المرئية أو منطقة محددة في كل مرة.", + "description": "Hint shown under the express row when express mode is on." + }, + "expressMigratedNote": { + "message": "النقر على الأيقونة يلتقط الآن الصفحة الكاملة. انقر بزر الفأرة الأيمن للأوضاع الأخرى.", + "description": "One-time editor note after the update that made a toolbar click capture the full page." + }, + "editorRateLabel": { + "message": "قيّم", + "description": "Editor topbar: label on the always-visible Rate star." + }, + "editorRateTooltip": { + "message": "30 ثانية. هكذا يجد الآخرون أداة لقطات شاشة تحترم الخصوصية.", + "description": "Editor topbar: tooltip on the Rate star." + }, + "ratePromptHeadline": { + "message": "هل توفر لك هذه الأداة الوقت؟", + "description": "One-time post-export rate prompt: headline." + }, + "ratePromptBody": { + "message": "تقييم قصير في متجر Chrome الإلكتروني هو الطريقة الأساسية لاكتشاف أداة تحترم الخصوصية. يستغرق 30 ثانية.", + "description": "One-time post-export rate prompt: body." + }, + "ratePromptRate": { + "message": "قيّم في متجر Chrome الإلكتروني", + "description": "One-time post-export rate prompt: button that opens the reviews tab." + }, + "ratePromptLater": { + "message": "ليس الآن", + "description": "One-time post-export rate prompt: dismiss button." + }, + "errNoTab": { + "message": "لم يتم العثور على علامة تبويب نشطة.", + "description": "Capture error: no active tab." + }, + "errProtectedPage": { + "message": "يتعذر التقاط هذه الصفحة المحمية.", + "description": "Capture error: browser-protected page." + }, + "errBlankPage": { + "message": "لا تحتوي هذه الصفحة على محتوى قابل للتمرير.", + "description": "Capture error: nothing scrollable to capture." + }, + "errTooLarge": { + "message": "هذه الصفحة أطول من أن تلتقط في صورة واحدة ($PX$ بكسل). جرّب المنطقة المرئية أو منطقة محددة.", + "description": "Capture error: page taller than one canvas. $PX$ is the height in device pixels.", + "placeholders": { + "px": { + "content": "$1", + "example": "35000" + } + } + }, + "errNoRegion": { + "message": "المنطقة المحفوظة لا تناسب هذه الشاشة — حدد منطقة أولاً.", + "description": "Capture error: repeat-last-region with no region that fits." + }, + "errClipboard": { + "message": "تعذر نسخ لقطة الشاشة إلى الحافظة.", + "description": "Quick-action error: clipboard write failed." + }, + "errSave": { + "message": "تعذر حفظ لقطة الشاشة على القرص.", + "description": "Quick-action error: disk save failed." + }, + "errUnknown": { + "message": "فشل الالتقاط بشكل غير متوقع.", + "description": "Capture error: unexpected failure." + } +} diff --git a/public/_locales/de/messages.json b/public/_locales/de/messages.json index fb7a420..1369810 100644 --- a/public/_locales/de/messages.json +++ b/public/_locales/de/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Ganzseiten-Screenshot & Anmerkungen - OpenScreenShot", + "message": "Ganzseiten-Screenshot - OpenScreenShot", "description": "Extension name shown in Chrome UI." }, "extDesc": { - "message": "Screenshots: ganze Seite, Ausschnitt, Sichtbares. Mit Anmerkungs-Editor. PNG, JPEG, WebP, PDF. 100 % privat, offline, kein Upload.", + "message": "Ganzseiten-Screenshot mit einem Klick. Open Source, 100 % privat, kein Konto, kein Upload.", "description": "Extension description shown in the Chrome Web Store and extensions page." }, "modeFullPage": { @@ -222,13 +222,17 @@ "description": "Hint shown under the after-capture row when Download is selected." }, "expressLabel": { - "message": "Screenshot per Symbolklick", + "message": "Klick auf das Symbol nimmt die ganze Seite auf", "description": "Label for the capture-on-icon-click row in the popup, and the icon context-menu checkbox." }, "expressHint": { - "message": "Das Symbol nimmt jetzt die ganze Seite auf. Rechtsklick auf das Symbol schaltet das aus.", + "message": "Deaktivieren, um jedes Mal zwischen Ganze Seite, Sichtbarer Bereich oder Ausschnitt zu wählen.", "description": "Hint shown under the express row when express mode is on." }, + "expressMigratedNote": { + "message": "Ein Klick auf das Symbol nimmt jetzt die ganze Seite auf. Andere Modi per Rechtsklick.", + "description": "One-time editor note after the update that made a toolbar click capture the full page." + }, "recOverlayPause": { "message": "Pause", "description": "Recording control bar: button that pauses the active recording." @@ -2252,5 +2256,87 @@ "settingsSupport": { "message": "Unterstützen", "description": "Popup settings: label for the row holding the Ko-fi and cool-stuff links." + }, + "welcomeTitle": { + "message": "Willkommen", + "description": "Welcome page: browser tab title." + }, + "welcomeHeadline": { + "message": "Fertig. Klicke auf das Symbol.", + "description": "Welcome page: headline inviting the first icon click." + }, + "welcomeBody": { + "message": "OpenScreenShot nimmt die ganze Seite mit einem Klick auf. Kein Konto. Nichts verlässt diesen Rechner. Probiere es gleich auf dieser Seite.", + "description": "Welcome page: one-paragraph pitch under the headline." + }, + "welcomePrimary": { + "message": "Klicke auf das OpenScreenShot-Symbol in der Symbolleiste.", + "description": "Welcome page: the primary instruction line." + }, + "welcomeSecondary": { + "message": "Oder drücke Strg+Umschalt+S (Cmd+Umschalt+S auf dem Mac).", + "description": "Welcome page: the keyboard-shortcut alternative line." + }, + "editorRateLabel": { + "message": "Bewerten", + "description": "Editor topbar: label on the always-visible Rate star." + }, + "editorRateTooltip": { + "message": "30 Sekunden. So finden andere ein privates Screenshot-Tool.", + "description": "Editor topbar: tooltip on the Rate star." + }, + "ratePromptHeadline": { + "message": "Spart dir das Zeit?", + "description": "One-time post-export rate prompt: headline." + }, + "ratePromptBody": { + "message": "Eine kurze Bewertung im Chrome Web Store ist der wichtigste Weg, wie ein privates Tool gefunden wird. Sie dauert 30 Sekunden.", + "description": "One-time post-export rate prompt: body." + }, + "ratePromptRate": { + "message": "Im Chrome Web Store bewerten", + "description": "One-time post-export rate prompt: button that opens the reviews tab." + }, + "ratePromptLater": { + "message": "Nicht jetzt", + "description": "One-time post-export rate prompt: dismiss button." + }, + "errNoTab": { + "message": "Kein aktiver Tab gefunden.", + "description": "Capture error: no active tab." + }, + "errProtectedPage": { + "message": "Diese geschützte Seite kann nicht aufgenommen werden.", + "description": "Capture error: browser-protected page." + }, + "errBlankPage": { + "message": "Diese Seite hat keinen scrollbaren Inhalt.", + "description": "Capture error: nothing scrollable to capture." + }, + "errTooLarge": { + "message": "Diese Seite ist zu hoch für ein einzelnes Bild ($PX$ px). Versuche Sichtbarer Bereich oder Ausschnitt.", + "description": "Capture error: page taller than one canvas. $PX$ is the height in device pixels.", + "placeholders": { + "px": { + "content": "$1", + "example": "35000" + } + } + }, + "errNoRegion": { + "message": "Kein gespeicherter Ausschnitt passt auf diesen Bildschirm — wähle zuerst einen aus.", + "description": "Capture error: repeat-last-region with no region that fits." + }, + "errClipboard": { + "message": "Der Screenshot konnte nicht in die Zwischenablage kopiert werden.", + "description": "Quick-action error: clipboard write failed." + }, + "errSave": { + "message": "Der Screenshot konnte nicht gespeichert werden.", + "description": "Quick-action error: disk save failed." + }, + "errUnknown": { + "message": "Die Aufnahme ist unerwartet fehlgeschlagen.", + "description": "Capture error: unexpected failure." } } diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index 004aff9..a2fe76f 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Full Page Screenshot & Annotate - OpenScreenShot", + "message": "Full Page Screenshot - OpenScreenShot", "description": "Extension name shown in Chrome UI." }, "extDesc": { - "message": "Full page, region & visible screenshots with an annotation editor. Export PNG, JPEG, WebP, PDF. 100% private, offline, no uploads.", + "message": "Full page screenshot in one click. Open source, 100% private, no account, no uploads.", "description": "Extension description shown in the Chrome Web Store and extensions page." }, "modeFullPage": { @@ -222,13 +222,17 @@ "description": "Hint shown under the after-capture row when Download is selected." }, "expressLabel": { - "message": "Capture on icon click", + "message": "Icon click captures the full page", "description": "Label for the capture-on-icon-click row in the popup, and the icon context-menu checkbox." }, "expressHint": { - "message": "The icon now captures the full page. Right-click the icon to turn this off.", + "message": "Turn off to choose Full Page, Visible Area, or Selected Region each time.", "description": "Hint shown under the express row when express mode is on." }, + "expressMigratedNote": { + "message": "Toolbar click now captures the full page. Right-click for other modes.", + "description": "One-time editor note after the update that made a toolbar click capture the full page." + }, "recOverlayPause": { "message": "Pause", "description": "Recording control bar: button that pauses the active recording." @@ -302,7 +306,7 @@ "description": "Popup: toggle chip for the webcam track." }, "recWebcamNoPreview": { - "message": "Added to the export \u2014 no live preview while you record.", + "message": "Added to the export — no live preview while you record.", "description": "Popup: one-line disclosure shown under the Webcam toggle once it is on, before Record is pressed (task 40: the recorded page never shows a live self-view)." }, "recWebcamDenied": { @@ -318,47 +322,47 @@ "description": "Popup button shown after the user refuses the inline tabCapture prompt." }, "recFailStartUnreachable": { - "message": "Recording didn\u2019t start \u2014 the extension didn\u2019t answer. Try again.", + "message": "Recording didn’t start — the extension didn’t answer. Try again.", "description": "Popup: error toast when REC_START never reached the service worker." }, "recFailStartBlocked": { - "message": "Recording didn\u2019t start \u2014 this page can\u2019t be recorded. Switch to a normal tab and try again.", + "message": "Recording didn’t start — this page can’t be recorded. Switch to a normal tab and try again.", "description": "Popup: error toast when the start found no recordable tab (gone, or a protected URL)." }, "recFailStartBusy": { - "message": "A recording is already running \u2014 stop it before starting another.", + "message": "A recording is already running — stop it before starting another.", "description": "Popup: error toast when a start arrives while a recording is live." }, "recFailStartFailed": { - "message": "Recording didn\u2019t start \u2014 the tab couldn\u2019t be prepared. Reload the tab and try again.", + "message": "Recording didn’t start — the tab couldn’t be prepared. Reload the tab and try again.", "description": "Popup: error toast when the worker's start threw before the engine was asked to capture." }, "recFailCleanup": { - "message": "A failed recording is still stored \u2014 delete it on the Recorder page.", + "message": "A failed recording is still stored — delete it on the Recorder page.", "description": "Popup: error toast when a failed session could not be tidied up." }, "recFailEngine": { - "message": "Recording stopped before it began \u2014 the tab couldn\u2019t be captured. Try again.", + "message": "Recording stopped before it began — the tab couldn’t be captured. Try again.", "description": "Popup: error toast when the offscreen engine failed to open its streams." }, "recFailEngineUnreachable": { - "message": "Nothing is being recorded \u2014 the recorder never started. Stop and try again.", + "message": "Nothing is being recorded — the recorder never started. Stop and try again.", "description": "Popup: error toast when OFFSCREEN_START never reached the offscreen engine, so the badge and control bar claim a recording that is not happening." }, "recFailEngineStalled": { - "message": "Recording didn\u2019t start, and stopping it got no answer. Nothing was saved \u2014 try again.", + "message": "Recording didn’t start, and stopping it got no answer. Nothing was saved — try again.", "description": "Popup: the user pressed Stop on a recording that had not begun, and the offscreen recorder never answered. The run was torn down." }, "popupOpenFailed": { - "message": "That page couldn\u2019t be opened. Try again.", + "message": "That page couldn’t be opened. Try again.", "description": "Popup: error toast when chrome.tabs.create fails for the setup page, the editor or the recorder." }, "recFailChunkWrite": { - "message": "Part of this recording isn\u2019t being saved. Stop it and check what you have on the Recorder page.", + "message": "Part of this recording isn’t being saved. Stop it and check what you have on the Recorder page.", "description": "Popup: error toast when a chunk write to IndexedDB failed mid-recording, so the file is shorter than the clock says." }, "recFailEventsWrite": { - "message": "The cursor track isn\u2019t being saved. The video is fine; zoom and click effects will be missing.", + "message": "The cursor track isn’t being saved. The video is fine; zoom and click effects will be missing.", "description": "Popup: error toast when cursor-event writes to IndexedDB failed mid-recording, leaving the video intact." }, "recOverlayStarting": { @@ -374,35 +378,35 @@ "description": "Recording control bar: warning chip shown when a requested camera was declined or unavailable. Keep it short; it sits in a row of three-letter track chips." }, "recFailRecorderOpen": { - "message": "Your recording is saved \u2014 the Recorder page didn\u2019t open. Use Recover last recording below.", + "message": "Your recording is saved — the Recorder page didn’t open. Use Recover last recording below.", "description": "Popup: error toast when chrome.tabs.create for the recorder page failed after a recording finished." }, "recFailQuery": { - "message": "Recording status is unavailable \u2014 open the Recorder page to check.", + "message": "Recording status is unavailable — open the Recorder page to check.", "description": "Popup: error toast when REC_QUERY threw, so \"not recording\" cannot be trusted." }, "recFailOverlayBlocked": { - "message": "The recording controls couldn\u2019t open on this page \u2014 stop the recording from here.", + "message": "The recording controls couldn’t open on this page — stop the recording from here.", "description": "Popup: error toast when the in-page control bar could not be injected." }, "recFailOverlayLost": { - "message": "The recording controls were lost on this page \u2014 stop the recording from here.", + "message": "The recording controls were lost on this page — stop the recording from here.", "description": "Popup: error toast when the in-page control bar stopped reporting mid-recording." }, "recFailControlUnreachable": { - "message": "That didn\u2019t reach the recorder \u2014 the extension didn\u2019t answer. Try again.", + "message": "That didn’t reach the recorder — the extension didn’t answer. Try again.", "description": "Popup: error toast when stop/cancel never reached the service worker." }, "recFailSessionLoad": { - "message": "That recording couldn\u2019t be opened \u2014 it may be incomplete.", + "message": "That recording couldn’t be opened — it may be incomplete.", "description": "Recorder page: error toast when a session could not be read out of IndexedDB." }, "recFailSegmentSkipped": { - "message": "Part of this recording wouldn\u2019t play and was left out of the export.", + "message": "Part of this recording wouldn’t play and was left out of the export.", "description": "Recorder page: error toast when one or more segments were skipped during an export." }, "recFailExport": { - "message": "Export failed \u2014 no file was saved. Try again.", + "message": "Export failed — no file was saved. Try again.", "description": "Recorder page: error toast when the export threw." }, "recFailSaveInterrupted": { @@ -784,7 +788,7 @@ "description": "Setup page: row title for the camera permission." }, "setupCameraDesc": { - "message": "Adds a bubble to the export \u2014 no live preview while recording.", + "message": "Adds a bubble to the export — no live preview while recording.", "description": "Setup page: row description for the camera permission." }, "setupMic": { @@ -2252,5 +2256,87 @@ "settingsSupport": { "message": "Support", "description": "Popup settings: label for the row holding the Ko-fi and cool-stuff links." + }, + "welcomeTitle": { + "message": "Welcome", + "description": "Welcome page: browser tab title." + }, + "welcomeHeadline": { + "message": "You're in. Click the icon.", + "description": "Welcome page: headline inviting the first icon click." + }, + "welcomeBody": { + "message": "OpenScreenShot captures the whole page in one click. No account. Nothing leaves this machine. Try it on this page right now.", + "description": "Welcome page: one-paragraph pitch under the headline." + }, + "welcomePrimary": { + "message": "Click the OpenScreenShot icon in the toolbar.", + "description": "Welcome page: the primary instruction line." + }, + "welcomeSecondary": { + "message": "Or press Ctrl+Shift+S (Cmd+Shift+S on Mac).", + "description": "Welcome page: the keyboard-shortcut alternative line." + }, + "editorRateLabel": { + "message": "Rate", + "description": "Editor topbar: label on the always-visible Rate star." + }, + "editorRateTooltip": { + "message": "30 seconds. It is how other people find a private screenshot tool.", + "description": "Editor topbar: tooltip on the Rate star." + }, + "ratePromptHeadline": { + "message": "Is this saving you time?", + "description": "One-time post-export rate prompt: headline." + }, + "ratePromptBody": { + "message": "A short rating on the Chrome Web Store is the main reason a private tool gets found. It takes 30 seconds.", + "description": "One-time post-export rate prompt: body." + }, + "ratePromptRate": { + "message": "Rate on Chrome Web Store", + "description": "One-time post-export rate prompt: button that opens the reviews tab." + }, + "ratePromptLater": { + "message": "Not now", + "description": "One-time post-export rate prompt: dismiss button." + }, + "errNoTab": { + "message": "No active tab found.", + "description": "Capture error: no active tab." + }, + "errProtectedPage": { + "message": "Can't screenshot this protected page.", + "description": "Capture error: browser-protected page." + }, + "errBlankPage": { + "message": "This page has no scrollable content.", + "description": "Capture error: nothing scrollable to capture." + }, + "errTooLarge": { + "message": "This page is too tall to capture in one image ($PX$px). Try visible or region mode.", + "description": "Capture error: page taller than one canvas. $PX$ is the height in device pixels.", + "placeholders": { + "px": { + "content": "$1", + "example": "35000" + } + } + }, + "errNoRegion": { + "message": "No saved region fits this screen — select one first.", + "description": "Capture error: repeat-last-region with no region that fits." + }, + "errClipboard": { + "message": "Could not copy the screenshot to the clipboard.", + "description": "Quick-action error: clipboard write failed." + }, + "errSave": { + "message": "Could not save the screenshot to disk.", + "description": "Quick-action error: disk save failed." + }, + "errUnknown": { + "message": "Capture failed unexpectedly.", + "description": "Capture error: unexpected failure." } } diff --git a/public/_locales/es/messages.json b/public/_locales/es/messages.json index 05b3f9a..84c2a94 100644 --- a/public/_locales/es/messages.json +++ b/public/_locales/es/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Captura de página completa y anotaciones - OpenScreenShot", + "message": "Captura de página completa - OpenScreenShot", "description": "Extension name shown in Chrome UI." }, "extDesc": { - "message": "Capturas de página completa, región o área visible, con anotaciones. Exporta PNG, JPEG, WebP, PDF. 100 % privado, sin conexión.", + "message": "Captura de página completa en un clic. Código abierto, 100 % privado, sin cuenta, sin subidas.", "description": "Extension description shown in the Chrome Web Store and extensions page." }, "modeFullPage": { @@ -222,13 +222,17 @@ "description": "Hint shown under the after-capture row when Download is selected." }, "expressLabel": { - "message": "Capturar al pulsar el icono", + "message": "Al hacer clic en el icono se captura la página completa", "description": "Label for the capture-on-icon-click row in the popup, and the icon context-menu checkbox." }, "expressHint": { - "message": "El icono ahora captura la página completa. Haz clic derecho en él para desactivarlo.", + "message": "Desactívalo para elegir cada vez entre Página completa, Área visible o Región.", "description": "Hint shown under the express row when express mode is on." }, + "expressMigratedNote": { + "message": "Ahora el clic en el icono captura la página completa. Otros modos con clic derecho.", + "description": "One-time editor note after the update that made a toolbar click capture the full page." + }, "recOverlayPause": { "message": "Pausar", "description": "Recording control bar: button that pauses the active recording." @@ -2252,5 +2256,87 @@ "settingsSupport": { "message": "Apoyo", "description": "Popup settings: label for the row holding the Ko-fi and cool-stuff links." + }, + "welcomeTitle": { + "message": "Bienvenida", + "description": "Welcome page: browser tab title." + }, + "welcomeHeadline": { + "message": "Listo. Haz clic en el icono.", + "description": "Welcome page: headline inviting the first icon click." + }, + "welcomeBody": { + "message": "OpenScreenShot captura la página completa con un clic. Sin cuenta. Nada sale de este equipo. Pruébalo ahora mismo en esta página.", + "description": "Welcome page: one-paragraph pitch under the headline." + }, + "welcomePrimary": { + "message": "Haz clic en el icono de OpenScreenShot en la barra de herramientas.", + "description": "Welcome page: the primary instruction line." + }, + "welcomeSecondary": { + "message": "O pulsa Ctrl+Mayús+S (Cmd+Mayús+S en Mac).", + "description": "Welcome page: the keyboard-shortcut alternative line." + }, + "editorRateLabel": { + "message": "Valorar", + "description": "Editor topbar: label on the always-visible Rate star." + }, + "editorRateTooltip": { + "message": "30 segundos. Así otras personas encuentran una herramienta privada de capturas.", + "description": "Editor topbar: tooltip on the Rate star." + }, + "ratePromptHeadline": { + "message": "¿Te está ahorrando tiempo?", + "description": "One-time post-export rate prompt: headline." + }, + "ratePromptBody": { + "message": "Una valoración breve en Chrome Web Store es la principal forma de que se encuentre una herramienta privada. Lleva 30 segundos.", + "description": "One-time post-export rate prompt: body." + }, + "ratePromptRate": { + "message": "Valorar en Chrome Web Store", + "description": "One-time post-export rate prompt: button that opens the reviews tab." + }, + "ratePromptLater": { + "message": "Ahora no", + "description": "One-time post-export rate prompt: dismiss button." + }, + "errNoTab": { + "message": "No se encontró ninguna pestaña activa.", + "description": "Capture error: no active tab." + }, + "errProtectedPage": { + "message": "No se puede capturar esta página protegida.", + "description": "Capture error: browser-protected page." + }, + "errBlankPage": { + "message": "Esta página no tiene contenido desplazable.", + "description": "Capture error: nothing scrollable to capture." + }, + "errTooLarge": { + "message": "Esta página es demasiado alta para una sola imagen ($PX$ px). Prueba el área visible o una región.", + "description": "Capture error: page taller than one canvas. $PX$ is the height in device pixels.", + "placeholders": { + "px": { + "content": "$1", + "example": "35000" + } + } + }, + "errNoRegion": { + "message": "Ninguna región guardada cabe en esta pantalla; selecciona una primero.", + "description": "Capture error: repeat-last-region with no region that fits." + }, + "errClipboard": { + "message": "No se pudo copiar la captura al portapapeles.", + "description": "Quick-action error: clipboard write failed." + }, + "errSave": { + "message": "No se pudo guardar la captura en el disco.", + "description": "Quick-action error: disk save failed." + }, + "errUnknown": { + "message": "La captura falló de forma inesperada.", + "description": "Capture error: unexpected failure." } } diff --git a/public/_locales/es_419/messages.json b/public/_locales/es_419/messages.json new file mode 100644 index 0000000..e640618 --- /dev/null +++ b/public/_locales/es_419/messages.json @@ -0,0 +1,116 @@ +{ + "extName": { + "message": "Captura de página completa - OpenScreenShot", + "description": "Extension name shown in Chrome UI." + }, + "extDesc": { + "message": "Captura de página completa en un clic. Código abierto, 100 % privado, sin cuenta, sin subidas.", + "description": "Extension description shown in the Chrome Web Store and extensions page." + }, + "modeFullPage": { + "message": "Página completa", + "description": "Capture mode: capture the entire scrolling page." + }, + "modeFullPageSub": { + "message": "Captura toda la página con desplazamiento", + "description": "Subtitle for the Full Page capture mode." + }, + "modeVisible": { + "message": "Área visible", + "description": "Capture mode: capture what is currently visible." + }, + "modeVisibleSub": { + "message": "Lo que ves ahora mismo", + "description": "Subtitle for the Visible Area capture mode." + }, + "modeRegion": { + "message": "Región", + "description": "Capture mode: capture a user-dragged region." + }, + "modeRegionSub": { + "message": "Haz clic y arrastra para seleccionar", + "description": "Subtitle for the Selected Region capture mode." + }, + "repeatLastRegion": { + "message": "Repetir última región", + "description": "Footer link and context menu item that re-captures the previous region selection." + }, + "settingsTitle": { + "message": "Configuración", + "description": "Settings panel heading." + }, + "expressLabel": { + "message": "Al hacer clic en el ícono se captura la página completa", + "description": "Label for the capture-on-icon-click row in the popup, and the icon context-menu checkbox." + }, + "expressHint": { + "message": "Desactívalo para elegir cada vez entre Página completa, Área visible o Región.", + "description": "Hint shown under the express row when express mode is on." + }, + "expressMigratedNote": { + "message": "Ahora el clic en el ícono captura la página completa. Otros modos con clic derecho.", + "description": "One-time editor note after the update that made a toolbar click capture the full page." + }, + "editorRateLabel": { + "message": "Calificar", + "description": "Editor topbar: label on the always-visible Rate star." + }, + "editorRateTooltip": { + "message": "30 segundos. Así otras personas encuentran una herramienta privada de capturas.", + "description": "Editor topbar: tooltip on the Rate star." + }, + "ratePromptHeadline": { + "message": "¿Te está ahorrando tiempo?", + "description": "One-time post-export rate prompt: headline." + }, + "ratePromptBody": { + "message": "Una calificación breve en Chrome Web Store es la principal forma de que se encuentre una herramienta privada. Toma 30 segundos.", + "description": "One-time post-export rate prompt: body." + }, + "ratePromptRate": { + "message": "Calificar en Chrome Web Store", + "description": "One-time post-export rate prompt: button that opens the reviews tab." + }, + "ratePromptLater": { + "message": "Ahora no", + "description": "One-time post-export rate prompt: dismiss button." + }, + "errNoTab": { + "message": "No se encontró ninguna pestaña activa.", + "description": "Capture error: no active tab." + }, + "errProtectedPage": { + "message": "No se puede capturar esta página protegida.", + "description": "Capture error: browser-protected page." + }, + "errBlankPage": { + "message": "Esta página no tiene contenido desplazable.", + "description": "Capture error: nothing scrollable to capture." + }, + "errTooLarge": { + "message": "Esta página es demasiado alta para una sola imagen ($PX$ px). Prueba el área visible o una región.", + "description": "Capture error: page taller than one canvas. $PX$ is the height in device pixels.", + "placeholders": { + "px": { + "content": "$1", + "example": "35000" + } + } + }, + "errNoRegion": { + "message": "Ninguna región guardada cabe en esta pantalla; selecciona una primero.", + "description": "Capture error: repeat-last-region with no region that fits." + }, + "errClipboard": { + "message": "No se pudo copiar la captura al portapapeles.", + "description": "Quick-action error: clipboard write failed." + }, + "errSave": { + "message": "No se pudo guardar la captura en el disco.", + "description": "Quick-action error: disk save failed." + }, + "errUnknown": { + "message": "La captura falló de forma inesperada.", + "description": "Capture error: unexpected failure." + } +} diff --git a/public/_locales/fr/messages.json b/public/_locales/fr/messages.json index e331f44..65440d8 100644 --- a/public/_locales/fr/messages.json +++ b/public/_locales/fr/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Capture d’écran pleine page et annotations - OpenScreenShot", + "message": "Capture pleine page - OpenScreenShot", "description": "Extension name shown in Chrome UI." }, "extDesc": { - "message": "Captures pleine page, zone ou visible + éditeur d’annotations. Export PNG, JPEG, WebP, PDF. 100 % privé, hors ligne, sans envoi.", + "message": "Capture d’écran pleine page en un clic. Open source, 100 % privé, sans compte, sans envoi.", "description": "Extension description shown in the Chrome Web Store and extensions page." }, "modeFullPage": { @@ -222,13 +222,17 @@ "description": "Hint shown under the after-capture row when Download is selected." }, "expressLabel": { - "message": "Capture au clic sur l’icône", + "message": "Un clic sur l'icône capture la page entière", "description": "Label for the capture-on-icon-click row in the popup, and the icon context-menu checkbox." }, "expressHint": { - "message": "L’icône capture désormais la page entière. Clic droit sur l’icône pour désactiver.", + "message": "Désactivez pour choisir à chaque fois Page entière, Zone visible ou Zone sélectionnée.", "description": "Hint shown under the express row when express mode is on." }, + "expressMigratedNote": { + "message": "Un clic sur l'icône capture désormais la page entière. Clic droit pour les autres modes.", + "description": "One-time editor note after the update that made a toolbar click capture the full page." + }, "recOverlayPause": { "message": "Pause", "description": "Recording control bar: button that pauses the active recording." @@ -2252,5 +2256,87 @@ "settingsSupport": { "message": "Soutenir", "description": "Popup settings: label for the row holding the Ko-fi and cool-stuff links." + }, + "welcomeTitle": { + "message": "Bienvenue", + "description": "Welcome page: browser tab title." + }, + "welcomeHeadline": { + "message": "C’est prêt. Cliquez sur l’icône.", + "description": "Welcome page: headline inviting the first icon click." + }, + "welcomeBody": { + "message": "OpenScreenShot capture la page entière en un clic. Pas de compte. Rien ne quitte cette machine. Essayez tout de suite sur cette page.", + "description": "Welcome page: one-paragraph pitch under the headline." + }, + "welcomePrimary": { + "message": "Cliquez sur l’icône OpenScreenShot dans la barre d’outils.", + "description": "Welcome page: the primary instruction line." + }, + "welcomeSecondary": { + "message": "Ou appuyez sur Ctrl+Maj+S (Cmd+Maj+S sur Mac).", + "description": "Welcome page: the keyboard-shortcut alternative line." + }, + "editorRateLabel": { + "message": "Noter", + "description": "Editor topbar: label on the always-visible Rate star." + }, + "editorRateTooltip": { + "message": "30 secondes. C’est ainsi que d’autres trouvent un outil de capture privé.", + "description": "Editor topbar: tooltip on the Rate star." + }, + "ratePromptHeadline": { + "message": "Vous fait-il gagner du temps ?", + "description": "One-time post-export rate prompt: headline." + }, + "ratePromptBody": { + "message": "Une courte note sur le Chrome Web Store est le principal moyen de faire connaître un outil privé. Cela prend 30 secondes.", + "description": "One-time post-export rate prompt: body." + }, + "ratePromptRate": { + "message": "Noter sur le Chrome Web Store", + "description": "One-time post-export rate prompt: button that opens the reviews tab." + }, + "ratePromptLater": { + "message": "Pas maintenant", + "description": "One-time post-export rate prompt: dismiss button." + }, + "errNoTab": { + "message": "Aucun onglet actif trouvé.", + "description": "Capture error: no active tab." + }, + "errProtectedPage": { + "message": "Impossible de capturer cette page protégée.", + "description": "Capture error: browser-protected page." + }, + "errBlankPage": { + "message": "Cette page n’a pas de contenu défilant.", + "description": "Capture error: nothing scrollable to capture." + }, + "errTooLarge": { + "message": "Cette page est trop haute pour une seule image ($PX$ px). Essayez la zone visible ou une zone sélectionnée.", + "description": "Capture error: page taller than one canvas. $PX$ is the height in device pixels.", + "placeholders": { + "px": { + "content": "$1", + "example": "35000" + } + } + }, + "errNoRegion": { + "message": "Aucune zone enregistrée ne tient sur cet écran — sélectionnez-en une d’abord.", + "description": "Capture error: repeat-last-region with no region that fits." + }, + "errClipboard": { + "message": "Impossible de copier la capture dans le presse-papiers.", + "description": "Quick-action error: clipboard write failed." + }, + "errSave": { + "message": "Impossible d’enregistrer la capture sur le disque.", + "description": "Quick-action error: disk save failed." + }, + "errUnknown": { + "message": "La capture a échoué de manière inattendue.", + "description": "Capture error: unexpected failure." } } diff --git a/public/_locales/hi/messages.json b/public/_locales/hi/messages.json new file mode 100644 index 0000000..1830e42 --- /dev/null +++ b/public/_locales/hi/messages.json @@ -0,0 +1,116 @@ +{ + "extName": { + "message": "पूरे पेज का स्क्रीनशॉट - OpenScreenShot", + "description": "Extension name shown in Chrome UI." + }, + "extDesc": { + "message": "एक क्लिक में पूरे पेज का स्क्रीनशॉट। ओपन सोर्स, 100% निजी, न कोई खाता, न कोई अपलोड।", + "description": "Extension description shown in the Chrome Web Store and extensions page." + }, + "modeFullPage": { + "message": "पूरा पेज", + "description": "Capture mode: capture the entire scrolling page." + }, + "modeFullPageSub": { + "message": "पूरे स्क्रॉल होने वाले पेज को कैप्चर करें", + "description": "Subtitle for the Full Page capture mode." + }, + "modeVisible": { + "message": "दिखाई देने वाला क्षेत्र", + "description": "Capture mode: capture what is currently visible." + }, + "modeVisibleSub": { + "message": "जो आप अभी देख रहे हैं", + "description": "Subtitle for the Visible Area capture mode." + }, + "modeRegion": { + "message": "चुना हुआ क्षेत्र", + "description": "Capture mode: capture a user-dragged region." + }, + "modeRegionSub": { + "message": "चुनने के लिए क्लिक करके खींचें", + "description": "Subtitle for the Selected Region capture mode." + }, + "repeatLastRegion": { + "message": "पिछला क्षेत्र दोहराएँ", + "description": "Footer link and context menu item that re-captures the previous region selection." + }, + "settingsTitle": { + "message": "सेटिंग", + "description": "Settings panel heading." + }, + "expressLabel": { + "message": "टूलबार आइकन पर क्लिक करने से पूरा पेज कैप्चर होता है", + "description": "Label for the capture-on-icon-click row in the popup, and the icon context-menu checkbox." + }, + "expressHint": { + "message": "हर बार पूरा पेज, दिखाई देने वाला क्षेत्र या चुना हुआ क्षेत्र चुनने के लिए बंद करें।", + "description": "Hint shown under the express row when express mode is on." + }, + "expressMigratedNote": { + "message": "आइकन पर क्लिक अब पूरा पेज कैप्चर करता है। अन्य मोड के लिए राइट-क्लिक करें।", + "description": "One-time editor note after the update that made a toolbar click capture the full page." + }, + "editorRateLabel": { + "message": "रेट करें", + "description": "Editor topbar: label on the always-visible Rate star." + }, + "editorRateTooltip": { + "message": "30 सेकंड। इसी तरह दूसरे लोग एक निजी स्क्रीनशॉट टूल खोज पाते हैं।", + "description": "Editor topbar: tooltip on the Rate star." + }, + "ratePromptHeadline": { + "message": "क्या इससे आपका समय बच रहा है?", + "description": "One-time post-export rate prompt: headline." + }, + "ratePromptBody": { + "message": "Chrome वेब स्टोर पर एक छोटी रेटिंग ही वह मुख्य तरीका है जिससे एक निजी टूल लोगों तक पहुँचता है। इसमें 30 सेकंड लगते हैं।", + "description": "One-time post-export rate prompt: body." + }, + "ratePromptRate": { + "message": "Chrome वेब स्टोर पर रेट करें", + "description": "One-time post-export rate prompt: button that opens the reviews tab." + }, + "ratePromptLater": { + "message": "अभी नहीं", + "description": "One-time post-export rate prompt: dismiss button." + }, + "errNoTab": { + "message": "कोई सक्रिय टैब नहीं मिला।", + "description": "Capture error: no active tab." + }, + "errProtectedPage": { + "message": "इस सुरक्षित पेज का स्क्रीनशॉट नहीं लिया जा सकता।", + "description": "Capture error: browser-protected page." + }, + "errBlankPage": { + "message": "इस पेज में स्क्रॉल करने योग्य सामग्री नहीं है।", + "description": "Capture error: nothing scrollable to capture." + }, + "errTooLarge": { + "message": "यह पेज एक छवि के लिए बहुत लंबा है ($PX$ px)। दिखाई देने वाला क्षेत्र या चुना हुआ क्षेत्र आज़माएँ।", + "description": "Capture error: page taller than one canvas. $PX$ is the height in device pixels.", + "placeholders": { + "px": { + "content": "$1", + "example": "35000" + } + } + }, + "errNoRegion": { + "message": "सहेजा गया क्षेत्र इस स्क्रीन में नहीं समाता — पहले एक क्षेत्र चुनें।", + "description": "Capture error: repeat-last-region with no region that fits." + }, + "errClipboard": { + "message": "स्क्रीनशॉट क्लिपबोर्ड पर कॉपी नहीं हो सका।", + "description": "Quick-action error: clipboard write failed." + }, + "errSave": { + "message": "स्क्रीनशॉट डिस्क पर सहेजा नहीं जा सका।", + "description": "Quick-action error: disk save failed." + }, + "errUnknown": { + "message": "कैप्चर अप्रत्याशित रूप से विफल रहा।", + "description": "Capture error: unexpected failure." + } +} diff --git a/public/_locales/id/messages.json b/public/_locales/id/messages.json new file mode 100644 index 0000000..5688aff --- /dev/null +++ b/public/_locales/id/messages.json @@ -0,0 +1,116 @@ +{ + "extName": { + "message": "Screenshot Halaman Penuh - OpenScreenShot", + "description": "Extension name shown in Chrome UI." + }, + "extDesc": { + "message": "Screenshot halaman penuh sekali klik. Open source, 100% privat, tanpa akun, tanpa unggahan.", + "description": "Extension description shown in the Chrome Web Store and extensions page." + }, + "modeFullPage": { + "message": "Halaman penuh", + "description": "Capture mode: capture the entire scrolling page." + }, + "modeFullPageSub": { + "message": "Tangkap seluruh halaman yang bergulir", + "description": "Subtitle for the Full Page capture mode." + }, + "modeVisible": { + "message": "Area terlihat", + "description": "Capture mode: capture what is currently visible." + }, + "modeVisibleSub": { + "message": "Yang Anda lihat sekarang", + "description": "Subtitle for the Visible Area capture mode." + }, + "modeRegion": { + "message": "Area pilihan", + "description": "Capture mode: capture a user-dragged region." + }, + "modeRegionSub": { + "message": "Klik dan seret untuk memilih", + "description": "Subtitle for the Selected Region capture mode." + }, + "repeatLastRegion": { + "message": "Ulangi area terakhir", + "description": "Footer link and context menu item that re-captures the previous region selection." + }, + "settingsTitle": { + "message": "Setelan", + "description": "Settings panel heading." + }, + "expressLabel": { + "message": "Mengklik ikon bilah alat menangkap halaman penuh", + "description": "Label for the capture-on-icon-click row in the popup, and the icon context-menu checkbox." + }, + "expressHint": { + "message": "Matikan untuk memilih Halaman penuh, Area terlihat, atau Area pilihan setiap kali.", + "description": "Hint shown under the express row when express mode is on." + }, + "expressMigratedNote": { + "message": "Klik ikon kini menangkap halaman penuh. Klik kanan untuk mode lain.", + "description": "One-time editor note after the update that made a toolbar click capture the full page." + }, + "editorRateLabel": { + "message": "Beri nilai", + "description": "Editor topbar: label on the always-visible Rate star." + }, + "editorRateTooltip": { + "message": "30 detik. Begitulah orang lain menemukan alat screenshot yang privat.", + "description": "Editor topbar: tooltip on the Rate star." + }, + "ratePromptHeadline": { + "message": "Apakah ini menghemat waktu Anda?", + "description": "One-time post-export rate prompt: headline." + }, + "ratePromptBody": { + "message": "Penilaian singkat di Chrome Web Store adalah cara utama alat privat ditemukan. Hanya 30 detik.", + "description": "One-time post-export rate prompt: body." + }, + "ratePromptRate": { + "message": "Beri nilai di Chrome Web Store", + "description": "One-time post-export rate prompt: button that opens the reviews tab." + }, + "ratePromptLater": { + "message": "Nanti saja", + "description": "One-time post-export rate prompt: dismiss button." + }, + "errNoTab": { + "message": "Tab aktif tidak ditemukan.", + "description": "Capture error: no active tab." + }, + "errProtectedPage": { + "message": "Tidak dapat menangkap halaman yang dilindungi ini.", + "description": "Capture error: browser-protected page." + }, + "errBlankPage": { + "message": "Halaman ini tidak memiliki konten yang dapat digulir.", + "description": "Capture error: nothing scrollable to capture." + }, + "errTooLarge": { + "message": "Halaman ini terlalu tinggi untuk satu gambar ($PX$ px). Coba area terlihat atau area pilihan.", + "description": "Capture error: page taller than one canvas. $PX$ is the height in device pixels.", + "placeholders": { + "px": { + "content": "$1", + "example": "35000" + } + } + }, + "errNoRegion": { + "message": "Area tersimpan tidak muat di layar ini — pilih dulu area baru.", + "description": "Capture error: repeat-last-region with no region that fits." + }, + "errClipboard": { + "message": "Tidak dapat menyalin screenshot ke papan klip.", + "description": "Quick-action error: clipboard write failed." + }, + "errSave": { + "message": "Tidak dapat menyimpan screenshot ke disk.", + "description": "Quick-action error: disk save failed." + }, + "errUnknown": { + "message": "Penangkapan gagal secara tak terduga.", + "description": "Capture error: unexpected failure." + } +} diff --git a/public/_locales/it/messages.json b/public/_locales/it/messages.json index fa282f5..a1f40aa 100644 --- a/public/_locales/it/messages.json +++ b/public/_locales/it/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Screenshot pagina intera e annotazioni - OpenScreenShot", + "message": "Screenshot pagina intera - OpenScreenShot", "description": "Extension name shown in Chrome UI." }, "extDesc": { - "message": "Screenshot a pagina intera, regione o schermo con editor di annotazioni. PNG, JPEG, WebP, PDF. 100% privato, offline, senza upload.", + "message": "Screenshot a pagina intera in un clic. Open source, 100% privato, senza account, senza upload.", "description": "Extension description shown in the Chrome Web Store and extensions page." }, "modeFullPage": { @@ -222,13 +222,17 @@ "description": "Hint shown under the after-capture row when Download is selected." }, "expressLabel": { - "message": "Cattura al clic sull’icona", + "message": "Un clic sull'icona cattura la pagina intera", "description": "Label for the capture-on-icon-click row in the popup, and the icon context-menu checkbox." }, "expressHint": { - "message": "Ora l’icona cattura la pagina intera. Clic destro sull’icona per disattivare.", + "message": "Disattiva per scegliere ogni volta tra Pagina intera, Area visibile o Regione selezionata.", "description": "Hint shown under the express row when express mode is on." }, + "expressMigratedNote": { + "message": "Ora un clic sull'icona cattura la pagina intera. Altri modi con il clic destro.", + "description": "One-time editor note after the update that made a toolbar click capture the full page." + }, "recOverlayPause": { "message": "Pausa", "description": "Recording control bar: button that pauses the active recording." @@ -2252,5 +2256,87 @@ "settingsSupport": { "message": "Sostieni", "description": "Popup settings: label for the row holding the Ko-fi and cool-stuff links." + }, + "welcomeTitle": { + "message": "Benvenuto", + "description": "Welcome page: browser tab title." + }, + "welcomeHeadline": { + "message": "Fatto. Fai clic sull’icona.", + "description": "Welcome page: headline inviting the first icon click." + }, + "welcomeBody": { + "message": "OpenScreenShot cattura la pagina intera con un clic. Nessun account. Niente lascia questo computer. Provalo subito su questa pagina.", + "description": "Welcome page: one-paragraph pitch under the headline." + }, + "welcomePrimary": { + "message": "Fai clic sull’icona di OpenScreenShot nella barra degli strumenti.", + "description": "Welcome page: the primary instruction line." + }, + "welcomeSecondary": { + "message": "Oppure premi Ctrl+Maiusc+S (Cmd+Maiusc+S su Mac).", + "description": "Welcome page: the keyboard-shortcut alternative line." + }, + "editorRateLabel": { + "message": "Valuta", + "description": "Editor topbar: label on the always-visible Rate star." + }, + "editorRateTooltip": { + "message": "30 secondi. È così che altri trovano uno strumento privato per gli screenshot.", + "description": "Editor topbar: tooltip on the Rate star." + }, + "ratePromptHeadline": { + "message": "Ti sta facendo risparmiare tempo?", + "description": "One-time post-export rate prompt: headline." + }, + "ratePromptBody": { + "message": "Una breve valutazione sul Chrome Web Store è il modo principale in cui uno strumento privato viene trovato. Richiede 30 secondi.", + "description": "One-time post-export rate prompt: body." + }, + "ratePromptRate": { + "message": "Valuta sul Chrome Web Store", + "description": "One-time post-export rate prompt: button that opens the reviews tab." + }, + "ratePromptLater": { + "message": "Non ora", + "description": "One-time post-export rate prompt: dismiss button." + }, + "errNoTab": { + "message": "Nessuna scheda attiva trovata.", + "description": "Capture error: no active tab." + }, + "errProtectedPage": { + "message": "Impossibile catturare questa pagina protetta.", + "description": "Capture error: browser-protected page." + }, + "errBlankPage": { + "message": "Questa pagina non ha contenuto scorrevole.", + "description": "Capture error: nothing scrollable to capture." + }, + "errTooLarge": { + "message": "Questa pagina è troppo alta per una sola immagine ($PX$ px). Prova l’area visibile o una regione.", + "description": "Capture error: page taller than one canvas. $PX$ is the height in device pixels.", + "placeholders": { + "px": { + "content": "$1", + "example": "35000" + } + } + }, + "errNoRegion": { + "message": "Nessuna regione salvata rientra in questo schermo: selezionane prima una.", + "description": "Capture error: repeat-last-region with no region that fits." + }, + "errClipboard": { + "message": "Impossibile copiare lo screenshot negli appunti.", + "description": "Quick-action error: clipboard write failed." + }, + "errSave": { + "message": "Impossibile salvare lo screenshot su disco.", + "description": "Quick-action error: disk save failed." + }, + "errUnknown": { + "message": "La cattura non è riuscita in modo imprevisto.", + "description": "Capture error: unexpected failure." } } diff --git a/public/_locales/ja/messages.json b/public/_locales/ja/messages.json index 641487a..5ce1e56 100644 --- a/public/_locales/ja/messages.json +++ b/public/_locales/ja/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "フルページ スクリーンショット&注釈 - OpenScreenShot", + "message": "フルページ スクリーンショット - OpenScreenShot", "description": "Extension name shown in Chrome UI." }, "extDesc": { - "message": "ページ全体・選択範囲・表示領域のスクリーンショットと注釈エディタ。PNG、JPEG、WebP、PDF で書き出し。100% プライベート、オフライン、アップロードなし。", + "message": "ワンクリックでページ全体をスクリーンショット。オープンソース、100% プライベート、アカウント不要、アップロードなし。", "description": "Extension description shown in the Chrome Web Store and extensions page." }, "modeFullPage": { @@ -222,13 +222,17 @@ "description": "Hint shown under the after-capture row when Download is selected." }, "expressLabel": { - "message": "アイコン クリックでキャプチャ", + "message": "アイコンをクリックするとページ全体をキャプチャ", "description": "Label for the capture-on-icon-click row in the popup, and the icon context-menu checkbox." }, "expressHint": { - "message": "アイコンをクリックするとページ全体をキャプチャします。オフにするにはアイコンを右クリックしてください。", + "message": "オフにすると、毎回「ページ全体」「表示領域」「選択範囲」から選べます。", "description": "Hint shown under the express row when express mode is on." }, + "expressMigratedNote": { + "message": "アイコンのクリックでページ全体をキャプチャするようになりました。他のモードは右クリックから。", + "description": "One-time editor note after the update that made a toolbar click capture the full page." + }, "recOverlayPause": { "message": "一時停止", "description": "Recording control bar: button that pauses the active recording." @@ -2252,5 +2256,87 @@ "settingsSupport": { "message": "サポート", "description": "Popup settings: label for the row holding the Ko-fi and cool-stuff links." + }, + "welcomeTitle": { + "message": "ようこそ", + "description": "Welcome page: browser tab title." + }, + "welcomeHeadline": { + "message": "準備完了。アイコンをクリック。", + "description": "Welcome page: headline inviting the first icon click." + }, + "welcomeBody": { + "message": "OpenScreenShot はワンクリックでページ全体をキャプチャします。アカウント不要。このマシンから何も送信されません。今すぐこのページで試してみてください。", + "description": "Welcome page: one-paragraph pitch under the headline." + }, + "welcomePrimary": { + "message": "ツールバーの OpenScreenShot アイコンをクリックしてください。", + "description": "Welcome page: the primary instruction line." + }, + "welcomeSecondary": { + "message": "または Ctrl+Shift+S(Mac では Cmd+Shift+S)を押してください。", + "description": "Welcome page: the keyboard-shortcut alternative line." + }, + "editorRateLabel": { + "message": "評価", + "description": "Editor topbar: label on the always-visible Rate star." + }, + "editorRateTooltip": { + "message": "30秒で完了。プライベートなスクリーンショットツールが見つかるきっかけになります。", + "description": "Editor topbar: tooltip on the Rate star." + }, + "ratePromptHeadline": { + "message": "時間の節約になっていますか?", + "description": "One-time post-export rate prompt: headline." + }, + "ratePromptBody": { + "message": "Chrome ウェブストアでの短い評価が、プライベートなツールが見つかる一番の理由です。30秒で終わります。", + "description": "One-time post-export rate prompt: body." + }, + "ratePromptRate": { + "message": "Chrome ウェブストアで評価する", + "description": "One-time post-export rate prompt: button that opens the reviews tab." + }, + "ratePromptLater": { + "message": "あとで", + "description": "One-time post-export rate prompt: dismiss button." + }, + "errNoTab": { + "message": "アクティブなタブが見つかりません。", + "description": "Capture error: no active tab." + }, + "errProtectedPage": { + "message": "この保護されたページはスクリーンショットできません。", + "description": "Capture error: browser-protected page." + }, + "errBlankPage": { + "message": "このページにはスクロール可能なコンテンツがありません。", + "description": "Capture error: nothing scrollable to capture." + }, + "errTooLarge": { + "message": "このページは高すぎて 1 枚の画像に収まりません($PX$px)。表示領域か選択範囲をお試しください。", + "description": "Capture error: page taller than one canvas. $PX$ is the height in device pixels.", + "placeholders": { + "px": { + "content": "$1", + "example": "35000" + } + } + }, + "errNoRegion": { + "message": "保存された範囲がこの画面に収まりません。先に範囲を選択してください。", + "description": "Capture error: repeat-last-region with no region that fits." + }, + "errClipboard": { + "message": "スクリーンショットをクリップボードにコピーできませんでした。", + "description": "Quick-action error: clipboard write failed." + }, + "errSave": { + "message": "スクリーンショットをディスクに保存できませんでした。", + "description": "Quick-action error: disk save failed." + }, + "errUnknown": { + "message": "キャプチャが予期せず失敗しました。", + "description": "Capture error: unexpected failure." } } diff --git a/public/_locales/ko/messages.json b/public/_locales/ko/messages.json index b38533a..c990298 100644 --- a/public/_locales/ko/messages.json +++ b/public/_locales/ko/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "전체 페이지 스크린샷 및 주석 - OpenScreenShot", + "message": "전체 페이지 스크린샷 - OpenScreenShot", "description": "Extension name shown in Chrome UI." }, "extDesc": { - "message": "전체 페이지, 선택 영역, 보이는 영역 스크린샷과 주석 편집기. PNG, JPEG, WebP, PDF 내보내기. 100% 비공개, 오프라인, 업로드 없음.", + "message": "한 번의 클릭으로 전체 페이지 스크린샷. 오픈 소스, 100% 비공개, 계정 불필요, 업로드 없음.", "description": "Extension description shown in the Chrome Web Store and extensions page." }, "modeFullPage": { @@ -222,13 +222,17 @@ "description": "Hint shown under the after-capture row when Download is selected." }, "expressLabel": { - "message": "아이콘 클릭 시 캡처", + "message": "아이콘을 클릭하면 전체 페이지를 캡처", "description": "Label for the capture-on-icon-click row in the popup, and the icon context-menu checkbox." }, "expressHint": { - "message": "이제 아이콘을 클릭하면 전체 페이지를 캡처해요. 끄려면 아이콘을 마우스 오른쪽 버튼으로 클릭하세요.", + "message": "끄면 매번 전체 페이지, 보이는 영역, 선택 영역 중에서 선택할 수 있습니다.", "description": "Hint shown under the express row when express mode is on." }, + "expressMigratedNote": { + "message": "이제 아이콘을 클릭하면 전체 페이지가 캡처됩니다. 다른 모드는 마우스 오른쪽 버튼으로 선택하세요.", + "description": "One-time editor note after the update that made a toolbar click capture the full page." + }, "recOverlayPause": { "message": "일시중지", "description": "Recording control bar: button that pauses the active recording." @@ -2252,5 +2256,87 @@ "settingsSupport": { "message": "후원", "description": "Popup settings: label for the row holding the Ko-fi and cool-stuff links." + }, + "welcomeTitle": { + "message": "환영합니다", + "description": "Welcome page: browser tab title." + }, + "welcomeHeadline": { + "message": "준비 완료. 아이콘을 클릭하세요.", + "description": "Welcome page: headline inviting the first icon click." + }, + "welcomeBody": { + "message": "OpenScreenShot은 한 번의 클릭으로 전체 페이지를 캡처합니다. 계정이 필요 없고, 이 컴퓨터 밖으로 아무것도 나가지 않습니다. 지금 이 페이지에서 바로 사용해 보세요.", + "description": "Welcome page: one-paragraph pitch under the headline." + }, + "welcomePrimary": { + "message": "툴바에서 OpenScreenShot 아이콘을 클릭하세요.", + "description": "Welcome page: the primary instruction line." + }, + "welcomeSecondary": { + "message": "또는 Ctrl+Shift+S(Mac에서는 Cmd+Shift+S)를 누르세요.", + "description": "Welcome page: the keyboard-shortcut alternative line." + }, + "editorRateLabel": { + "message": "평가", + "description": "Editor topbar: label on the always-visible Rate star." + }, + "editorRateTooltip": { + "message": "30초면 됩니다. 다른 사람들이 프라이빗 스크린샷 도구를 찾는 방법입니다.", + "description": "Editor topbar: tooltip on the Rate star." + }, + "ratePromptHeadline": { + "message": "시간이 절약되고 있나요?", + "description": "One-time post-export rate prompt: headline." + }, + "ratePromptBody": { + "message": "Chrome 웹 스토어의 짧은 평가는 프라이빗 도구가 알려지는 가장 큰 이유입니다. 30초면 충분합니다.", + "description": "One-time post-export rate prompt: body." + }, + "ratePromptRate": { + "message": "Chrome 웹 스토어에서 평가하기", + "description": "One-time post-export rate prompt: button that opens the reviews tab." + }, + "ratePromptLater": { + "message": "나중에", + "description": "One-time post-export rate prompt: dismiss button." + }, + "errNoTab": { + "message": "활성 탭을 찾을 수 없습니다.", + "description": "Capture error: no active tab." + }, + "errProtectedPage": { + "message": "보호된 페이지는 캡처할 수 없습니다.", + "description": "Capture error: browser-protected page." + }, + "errBlankPage": { + "message": "이 페이지에는 스크롤할 콘텐츠가 없습니다.", + "description": "Capture error: nothing scrollable to capture." + }, + "errTooLarge": { + "message": "이 페이지는 너무 길어 한 이미지에 담을 수 없습니다($PX$px). 보이는 영역이나 선택 영역을 사용해 보세요.", + "description": "Capture error: page taller than one canvas. $PX$ is the height in device pixels.", + "placeholders": { + "px": { + "content": "$1", + "example": "35000" + } + } + }, + "errNoRegion": { + "message": "저장된 영역이 이 화면에 맞지 않습니다. 먼저 영역을 선택하세요.", + "description": "Capture error: repeat-last-region with no region that fits." + }, + "errClipboard": { + "message": "스크린샷을 클립보드에 복사하지 못했습니다.", + "description": "Quick-action error: clipboard write failed." + }, + "errSave": { + "message": "스크린샷을 디스크에 저장하지 못했습니다.", + "description": "Quick-action error: disk save failed." + }, + "errUnknown": { + "message": "캡처가 예기치 않게 실패했습니다.", + "description": "Capture error: unexpected failure." } } diff --git a/public/_locales/nl/messages.json b/public/_locales/nl/messages.json new file mode 100644 index 0000000..e53ab22 --- /dev/null +++ b/public/_locales/nl/messages.json @@ -0,0 +1,116 @@ +{ + "extName": { + "message": "Volledige pagina screenshot - OpenScreenShot", + "description": "Extension name shown in Chrome UI." + }, + "extDesc": { + "message": "Screenshot van de volledige pagina met één klik. Open source, 100% privé, geen account, geen uploads.", + "description": "Extension description shown in the Chrome Web Store and extensions page." + }, + "modeFullPage": { + "message": "Volledige pagina", + "description": "Capture mode: capture the entire scrolling page." + }, + "modeFullPageSub": { + "message": "Leg de hele scrollende pagina vast", + "description": "Subtitle for the Full Page capture mode." + }, + "modeVisible": { + "message": "Zichtbaar gebied", + "description": "Capture mode: capture what is currently visible." + }, + "modeVisibleSub": { + "message": "Wat je nu ziet", + "description": "Subtitle for the Visible Area capture mode." + }, + "modeRegion": { + "message": "Geselecteerd gebied", + "description": "Capture mode: capture a user-dragged region." + }, + "modeRegionSub": { + "message": "Klik en sleep om te selecteren", + "description": "Subtitle for the Selected Region capture mode." + }, + "repeatLastRegion": { + "message": "Laatste gebied herhalen", + "description": "Footer link and context menu item that re-captures the previous region selection." + }, + "settingsTitle": { + "message": "Instellingen", + "description": "Settings panel heading." + }, + "expressLabel": { + "message": "Een klik op het werkbalkpictogram legt de volledige pagina vast", + "description": "Label for the capture-on-icon-click row in the popup, and the icon context-menu checkbox." + }, + "expressHint": { + "message": "Zet uit om elke keer te kiezen tussen Volledige pagina, Zichtbaar gebied of Geselecteerd gebied.", + "description": "Hint shown under the express row when express mode is on." + }, + "expressMigratedNote": { + "message": "Een klik op het pictogram legt nu de volledige pagina vast. Rechtsklik voor andere modi.", + "description": "One-time editor note after the update that made a toolbar click capture the full page." + }, + "editorRateLabel": { + "message": "Beoordelen", + "description": "Editor topbar: label on the always-visible Rate star." + }, + "editorRateTooltip": { + "message": "30 seconden. Zo vinden anderen een privé screenshot-tool.", + "description": "Editor topbar: tooltip on the Rate star." + }, + "ratePromptHeadline": { + "message": "Bespaart dit je tijd?", + "description": "One-time post-export rate prompt: headline." + }, + "ratePromptBody": { + "message": "Een korte beoordeling in de Chrome Web Store is de belangrijkste manier waarop een privé tool gevonden wordt. Het duurt 30 seconden.", + "description": "One-time post-export rate prompt: body." + }, + "ratePromptRate": { + "message": "Beoordelen in de Chrome Web Store", + "description": "One-time post-export rate prompt: button that opens the reviews tab." + }, + "ratePromptLater": { + "message": "Niet nu", + "description": "One-time post-export rate prompt: dismiss button." + }, + "errNoTab": { + "message": "Geen actief tabblad gevonden.", + "description": "Capture error: no active tab." + }, + "errProtectedPage": { + "message": "Deze beveiligde pagina kan niet worden vastgelegd.", + "description": "Capture error: browser-protected page." + }, + "errBlankPage": { + "message": "Deze pagina heeft geen scrollbare inhoud.", + "description": "Capture error: nothing scrollable to capture." + }, + "errTooLarge": { + "message": "Deze pagina is te hoog voor één afbeelding ($PX$ px). Probeer het zichtbare of een geselecteerd gebied.", + "description": "Capture error: page taller than one canvas. $PX$ is the height in device pixels.", + "placeholders": { + "px": { + "content": "$1", + "example": "35000" + } + } + }, + "errNoRegion": { + "message": "Geen opgeslagen gebied past op dit scherm — selecteer er eerst een.", + "description": "Capture error: repeat-last-region with no region that fits." + }, + "errClipboard": { + "message": "Kon de screenshot niet naar het klembord kopiëren.", + "description": "Quick-action error: clipboard write failed." + }, + "errSave": { + "message": "Kon de screenshot niet op schijf opslaan.", + "description": "Quick-action error: disk save failed." + }, + "errUnknown": { + "message": "Vastleggen is onverwacht mislukt.", + "description": "Capture error: unexpected failure." + } +} diff --git a/public/_locales/pl/messages.json b/public/_locales/pl/messages.json new file mode 100644 index 0000000..e8e8724 --- /dev/null +++ b/public/_locales/pl/messages.json @@ -0,0 +1,116 @@ +{ + "extName": { + "message": "Zrzut całej strony - OpenScreenShot", + "description": "Extension name shown in Chrome UI." + }, + "extDesc": { + "message": "Zrzut całej strony jednym kliknięciem. Open source, 100% prywatnie, bez konta, bez wysyłania.", + "description": "Extension description shown in the Chrome Web Store and extensions page." + }, + "modeFullPage": { + "message": "Cała strona", + "description": "Capture mode: capture the entire scrolling page." + }, + "modeFullPageSub": { + "message": "Przechwyć całą przewijaną stronę", + "description": "Subtitle for the Full Page capture mode." + }, + "modeVisible": { + "message": "Widoczny obszar", + "description": "Capture mode: capture what is currently visible." + }, + "modeVisibleSub": { + "message": "To, co widzisz teraz", + "description": "Subtitle for the Visible Area capture mode." + }, + "modeRegion": { + "message": "Zaznaczony obszar", + "description": "Capture mode: capture a user-dragged region." + }, + "modeRegionSub": { + "message": "Kliknij i przeciągnij, aby zaznaczyć", + "description": "Subtitle for the Selected Region capture mode." + }, + "repeatLastRegion": { + "message": "Powtórz ostatni obszar", + "description": "Footer link and context menu item that re-captures the previous region selection." + }, + "settingsTitle": { + "message": "Ustawienia", + "description": "Settings panel heading." + }, + "expressLabel": { + "message": "Kliknięcie ikony na pasku narzędzi przechwytuje całą stronę", + "description": "Label for the capture-on-icon-click row in the popup, and the icon context-menu checkbox." + }, + "expressHint": { + "message": "Wyłącz, aby za każdym razem wybierać: Cała strona, Widoczny obszar lub Zaznaczony obszar.", + "description": "Hint shown under the express row when express mode is on." + }, + "expressMigratedNote": { + "message": "Kliknięcie ikony przechwytuje teraz całą stronę. Inne tryby po kliknięciu prawym przyciskiem.", + "description": "One-time editor note after the update that made a toolbar click capture the full page." + }, + "editorRateLabel": { + "message": "Oceń", + "description": "Editor topbar: label on the always-visible Rate star." + }, + "editorRateTooltip": { + "message": "30 sekund. Tak inni znajdują prywatne narzędzie do zrzutów ekranu.", + "description": "Editor topbar: tooltip on the Rate star." + }, + "ratePromptHeadline": { + "message": "Czy to oszczędza Twój czas?", + "description": "One-time post-export rate prompt: headline." + }, + "ratePromptBody": { + "message": "Krótka ocena w Chrome Web Store to główny sposób, w jaki prywatne narzędzie zostaje znalezione. Zajmuje 30 sekund.", + "description": "One-time post-export rate prompt: body." + }, + "ratePromptRate": { + "message": "Oceń w Chrome Web Store", + "description": "One-time post-export rate prompt: button that opens the reviews tab." + }, + "ratePromptLater": { + "message": "Nie teraz", + "description": "One-time post-export rate prompt: dismiss button." + }, + "errNoTab": { + "message": "Nie znaleziono aktywnej karty.", + "description": "Capture error: no active tab." + }, + "errProtectedPage": { + "message": "Nie można wykonać zrzutu tej chronionej strony.", + "description": "Capture error: browser-protected page." + }, + "errBlankPage": { + "message": "Ta strona nie ma przewijanej zawartości.", + "description": "Capture error: nothing scrollable to capture." + }, + "errTooLarge": { + "message": "Ta strona jest zbyt wysoka na jeden obraz ($PX$ px). Spróbuj widocznego lub zaznaczonego obszaru.", + "description": "Capture error: page taller than one canvas. $PX$ is the height in device pixels.", + "placeholders": { + "px": { + "content": "$1", + "example": "35000" + } + } + }, + "errNoRegion": { + "message": "Zapisany obszar nie mieści się na tym ekranie — najpierw zaznacz nowy.", + "description": "Capture error: repeat-last-region with no region that fits." + }, + "errClipboard": { + "message": "Nie udało się skopiować zrzutu do schowka.", + "description": "Quick-action error: clipboard write failed." + }, + "errSave": { + "message": "Nie udało się zapisać zrzutu na dysku.", + "description": "Quick-action error: disk save failed." + }, + "errUnknown": { + "message": "Przechwytywanie nieoczekiwanie się nie powiodło.", + "description": "Capture error: unexpected failure." + } +} diff --git a/public/_locales/pt_BR/messages.json b/public/_locales/pt_BR/messages.json index f07e07b..aee42fb 100644 --- a/public/_locales/pt_BR/messages.json +++ b/public/_locales/pt_BR/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Captura de Página Inteira e Anotações - OpenScreenShot", + "message": "Captura de página inteira - OpenScreenShot", "description": "Extension name shown in Chrome UI." }, "extDesc": { - "message": "Capture a página inteira, uma região ou a área visível e anote. Exporte PNG, JPEG, WebP, PDF. 100% privado, offline, sem uploads.", + "message": "Captura de página inteira em um clique. Código aberto, 100% privado, sem conta, sem uploads.", "description": "Extension description shown in the Chrome Web Store and extensions page." }, "modeFullPage": { @@ -222,13 +222,17 @@ "description": "Hint shown under the after-capture row when Download is selected." }, "expressLabel": { - "message": "Capturar ao clicar no ícone", + "message": "Clicar no ícone captura a página inteira", "description": "Label for the capture-on-icon-click row in the popup, and the icon context-menu checkbox." }, "expressHint": { - "message": "O ícone agora captura a página inteira. Clique com o botão direito no ícone para desativar.", + "message": "Desative para escolher a cada vez entre Página inteira, Área visível ou Região selecionada.", "description": "Hint shown under the express row when express mode is on." }, + "expressMigratedNote": { + "message": "Agora clicar no ícone captura a página inteira. Outros modos com o clique direito.", + "description": "One-time editor note after the update that made a toolbar click capture the full page." + }, "recOverlayPause": { "message": "Pausar", "description": "Recording control bar: button that pauses the active recording." @@ -2252,5 +2256,87 @@ "settingsSupport": { "message": "Apoio", "description": "Popup settings: label for the row holding the Ko-fi and cool-stuff links." + }, + "welcomeTitle": { + "message": "Boas-vindas", + "description": "Welcome page: browser tab title." + }, + "welcomeHeadline": { + "message": "Pronto. Clique no ícone.", + "description": "Welcome page: headline inviting the first icon click." + }, + "welcomeBody": { + "message": "O OpenScreenShot captura a página inteira com um clique. Sem conta. Nada sai desta máquina. Experimente agora mesmo nesta página.", + "description": "Welcome page: one-paragraph pitch under the headline." + }, + "welcomePrimary": { + "message": "Clique no ícone do OpenScreenShot na barra de ferramentas.", + "description": "Welcome page: the primary instruction line." + }, + "welcomeSecondary": { + "message": "Ou pressione Ctrl+Shift+S (Cmd+Shift+S no Mac).", + "description": "Welcome page: the keyboard-shortcut alternative line." + }, + "editorRateLabel": { + "message": "Avaliar", + "description": "Editor topbar: label on the always-visible Rate star." + }, + "editorRateTooltip": { + "message": "30 segundos. É assim que outras pessoas encontram uma ferramenta privada de capturas.", + "description": "Editor topbar: tooltip on the Rate star." + }, + "ratePromptHeadline": { + "message": "Está economizando seu tempo?", + "description": "One-time post-export rate prompt: headline." + }, + "ratePromptBody": { + "message": "Uma avaliação rápida na Chrome Web Store é a principal forma de uma ferramenta privada ser encontrada. Leva 30 segundos.", + "description": "One-time post-export rate prompt: body." + }, + "ratePromptRate": { + "message": "Avaliar na Chrome Web Store", + "description": "One-time post-export rate prompt: button that opens the reviews tab." + }, + "ratePromptLater": { + "message": "Agora não", + "description": "One-time post-export rate prompt: dismiss button." + }, + "errNoTab": { + "message": "Nenhuma aba ativa encontrada.", + "description": "Capture error: no active tab." + }, + "errProtectedPage": { + "message": "Não é possível capturar esta página protegida.", + "description": "Capture error: browser-protected page." + }, + "errBlankPage": { + "message": "Esta página não tem conteúdo rolável.", + "description": "Capture error: nothing scrollable to capture." + }, + "errTooLarge": { + "message": "Esta página é alta demais para uma única imagem ($PX$ px). Tente a área visível ou uma região.", + "description": "Capture error: page taller than one canvas. $PX$ is the height in device pixels.", + "placeholders": { + "px": { + "content": "$1", + "example": "35000" + } + } + }, + "errNoRegion": { + "message": "Nenhuma região salva cabe nesta tela — selecione uma primeiro.", + "description": "Capture error: repeat-last-region with no region that fits." + }, + "errClipboard": { + "message": "Não foi possível copiar a captura para a área de transferência.", + "description": "Quick-action error: clipboard write failed." + }, + "errSave": { + "message": "Não foi possível salvar a captura no disco.", + "description": "Quick-action error: disk save failed." + }, + "errUnknown": { + "message": "A captura falhou inesperadamente.", + "description": "Capture error: unexpected failure." } } diff --git a/public/_locales/ru/messages.json b/public/_locales/ru/messages.json index f273b57..4135ae4 100644 --- a/public/_locales/ru/messages.json +++ b/public/_locales/ru/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "Скриншот всей страницы и аннотации - OpenScreenShot", + "message": "Скриншот всей страницы - OpenScreenShot", "description": "Extension name shown in Chrome UI." }, "extDesc": { - "message": "Скриншот всей страницы, области и экрана с редактором аннотаций. Экспорт PNG, JPEG, WebP, PDF. 100% приватно, офлайн, без загрузок.", + "message": "Скриншот всей страницы в один клик. Открытый код, 100% приватно, без аккаунта, без загрузок.", "description": "Extension description shown in the Chrome Web Store and extensions page." }, "modeFullPage": { @@ -222,13 +222,17 @@ "description": "Hint shown under the after-capture row when Download is selected." }, "expressLabel": { - "message": "Снимать по клику на значок", + "message": "Клик по значку снимает всю страницу", "description": "Label for the capture-on-icon-click row in the popup, and the icon context-menu checkbox." }, "expressHint": { - "message": "Теперь значок снимает всю страницу. Чтобы отключить, нажмите его правой кнопкой.", + "message": "Отключите, чтобы каждый раз выбирать: Вся страница, Видимая часть или Выделенная область.", "description": "Hint shown under the express row when express mode is on." }, + "expressMigratedNote": { + "message": "Теперь клик по значку снимает всю страницу. Другие режимы — по правому клику.", + "description": "One-time editor note after the update that made a toolbar click capture the full page." + }, "recOverlayPause": { "message": "Пауза", "description": "Recording control bar: button that pauses the active recording." @@ -2252,5 +2256,87 @@ "settingsSupport": { "message": "Поддержка", "description": "Popup settings: label for the row holding the Ko-fi and cool-stuff links." + }, + "welcomeTitle": { + "message": "Добро пожаловать", + "description": "Welcome page: browser tab title." + }, + "welcomeHeadline": { + "message": "Готово. Нажмите на значок.", + "description": "Welcome page: headline inviting the first icon click." + }, + "welcomeBody": { + "message": "OpenScreenShot снимает всю страницу в один клик. Без аккаунта. Ничего не покидает этот компьютер. Попробуйте прямо на этой странице.", + "description": "Welcome page: one-paragraph pitch under the headline." + }, + "welcomePrimary": { + "message": "Нажмите на значок OpenScreenShot на панели инструментов.", + "description": "Welcome page: the primary instruction line." + }, + "welcomeSecondary": { + "message": "Или нажмите Ctrl+Shift+S (Cmd+Shift+S на Mac).", + "description": "Welcome page: the keyboard-shortcut alternative line." + }, + "editorRateLabel": { + "message": "Оценить", + "description": "Editor topbar: label on the always-visible Rate star." + }, + "editorRateTooltip": { + "message": "30 секунд. Так другие находят приватный инструмент для скриншотов.", + "description": "Editor topbar: tooltip on the Rate star." + }, + "ratePromptHeadline": { + "message": "Экономит ли это ваше время?", + "description": "One-time post-export rate prompt: headline." + }, + "ratePromptBody": { + "message": "Короткий отзыв в Chrome Web Store — главный способ, которым находят приватный инструмент. Это занимает 30 секунд.", + "description": "One-time post-export rate prompt: body." + }, + "ratePromptRate": { + "message": "Оценить в Chrome Web Store", + "description": "One-time post-export rate prompt: button that opens the reviews tab." + }, + "ratePromptLater": { + "message": "Не сейчас", + "description": "One-time post-export rate prompt: dismiss button." + }, + "errNoTab": { + "message": "Активная вкладка не найдена.", + "description": "Capture error: no active tab." + }, + "errProtectedPage": { + "message": "Нельзя снять эту защищённую страницу.", + "description": "Capture error: browser-protected page." + }, + "errBlankPage": { + "message": "На этой странице нет прокручиваемого содержимого.", + "description": "Capture error: nothing scrollable to capture." + }, + "errTooLarge": { + "message": "Страница слишком высокая для одного изображения ($PX$ px). Попробуйте видимую часть или область.", + "description": "Capture error: page taller than one canvas. $PX$ is the height in device pixels.", + "placeholders": { + "px": { + "content": "$1", + "example": "35000" + } + } + }, + "errNoRegion": { + "message": "Сохранённая область не помещается на этом экране — сначала выделите новую.", + "description": "Capture error: repeat-last-region with no region that fits." + }, + "errClipboard": { + "message": "Не удалось скопировать скриншот в буфер обмена.", + "description": "Quick-action error: clipboard write failed." + }, + "errSave": { + "message": "Не удалось сохранить скриншот на диск.", + "description": "Quick-action error: disk save failed." + }, + "errUnknown": { + "message": "Захват неожиданно завершился ошибкой.", + "description": "Capture error: unexpected failure." } } diff --git a/public/_locales/tr/messages.json b/public/_locales/tr/messages.json new file mode 100644 index 0000000..a198fb6 --- /dev/null +++ b/public/_locales/tr/messages.json @@ -0,0 +1,116 @@ +{ + "extName": { + "message": "Tam Sayfa Ekran Görüntüsü - OpenScreenShot", + "description": "Extension name shown in Chrome UI." + }, + "extDesc": { + "message": "Tek tıkla tam sayfa ekran görüntüsü. Açık kaynak, %100 gizli, hesap yok, yükleme yok.", + "description": "Extension description shown in the Chrome Web Store and extensions page." + }, + "modeFullPage": { + "message": "Tam sayfa", + "description": "Capture mode: capture the entire scrolling page." + }, + "modeFullPageSub": { + "message": "Kaydırılan sayfanın tamamını yakala", + "description": "Subtitle for the Full Page capture mode." + }, + "modeVisible": { + "message": "Görünen alan", + "description": "Capture mode: capture what is currently visible." + }, + "modeVisibleSub": { + "message": "Şu anda gördüğünüz", + "description": "Subtitle for the Visible Area capture mode." + }, + "modeRegion": { + "message": "Seçili bölge", + "description": "Capture mode: capture a user-dragged region." + }, + "modeRegionSub": { + "message": "Seçmek için tıklayıp sürükleyin", + "description": "Subtitle for the Selected Region capture mode." + }, + "repeatLastRegion": { + "message": "Son bölgeyi tekrarla", + "description": "Footer link and context menu item that re-captures the previous region selection." + }, + "settingsTitle": { + "message": "Ayarlar", + "description": "Settings panel heading." + }, + "expressLabel": { + "message": "Araç çubuğu simgesine tıklamak tam sayfayı yakalar", + "description": "Label for the capture-on-icon-click row in the popup, and the icon context-menu checkbox." + }, + "expressHint": { + "message": "Her seferinde Tam sayfa, Görünen alan veya Seçili bölge seçmek için kapatın.", + "description": "Hint shown under the express row when express mode is on." + }, + "expressMigratedNote": { + "message": "Simgeye tıklamak artık tam sayfayı yakalıyor. Diğer modlar için sağ tıklayın.", + "description": "One-time editor note after the update that made a toolbar click capture the full page." + }, + "editorRateLabel": { + "message": "Değerlendir", + "description": "Editor topbar: label on the always-visible Rate star." + }, + "editorRateTooltip": { + "message": "30 saniye. Başkaları gizli bir ekran görüntüsü aracını böyle buluyor.", + "description": "Editor topbar: tooltip on the Rate star." + }, + "ratePromptHeadline": { + "message": "Zaman kazandırıyor mu?", + "description": "One-time post-export rate prompt: headline." + }, + "ratePromptBody": { + "message": "Chrome Web Mağazası'nda kısa bir değerlendirme, gizli bir aracın bulunmasının ana yoludur. 30 saniye sürer.", + "description": "One-time post-export rate prompt: body." + }, + "ratePromptRate": { + "message": "Chrome Web Mağazası'nda değerlendir", + "description": "One-time post-export rate prompt: button that opens the reviews tab." + }, + "ratePromptLater": { + "message": "Şimdi değil", + "description": "One-time post-export rate prompt: dismiss button." + }, + "errNoTab": { + "message": "Etkin sekme bulunamadı.", + "description": "Capture error: no active tab." + }, + "errProtectedPage": { + "message": "Bu korumalı sayfanın ekran görüntüsü alınamaz.", + "description": "Capture error: browser-protected page." + }, + "errBlankPage": { + "message": "Bu sayfada kaydırılabilir içerik yok.", + "description": "Capture error: nothing scrollable to capture." + }, + "errTooLarge": { + "message": "Bu sayfa tek bir görüntü için çok uzun ($PX$ px). Görünen alanı veya bölge modunu deneyin.", + "description": "Capture error: page taller than one canvas. $PX$ is the height in device pixels.", + "placeholders": { + "px": { + "content": "$1", + "example": "35000" + } + } + }, + "errNoRegion": { + "message": "Kayıtlı bölge bu ekrana sığmıyor — önce bir bölge seçin.", + "description": "Capture error: repeat-last-region with no region that fits." + }, + "errClipboard": { + "message": "Ekran görüntüsü panoya kopyalanamadı.", + "description": "Quick-action error: clipboard write failed." + }, + "errSave": { + "message": "Ekran görüntüsü diske kaydedilemedi.", + "description": "Quick-action error: disk save failed." + }, + "errUnknown": { + "message": "Yakalama beklenmedik şekilde başarısız oldu.", + "description": "Capture error: unexpected failure." + } +} diff --git a/public/_locales/vi/messages.json b/public/_locales/vi/messages.json new file mode 100644 index 0000000..107d460 --- /dev/null +++ b/public/_locales/vi/messages.json @@ -0,0 +1,116 @@ +{ + "extName": { + "message": "Chụp toàn trang - OpenScreenShot", + "description": "Extension name shown in Chrome UI." + }, + "extDesc": { + "message": "Chụp toàn trang trong một cú nhấp. Mã nguồn mở, 100% riêng tư, không tài khoản, không tải lên.", + "description": "Extension description shown in the Chrome Web Store and extensions page." + }, + "modeFullPage": { + "message": "Toàn trang", + "description": "Capture mode: capture the entire scrolling page." + }, + "modeFullPageSub": { + "message": "Chụp toàn bộ trang cuộn", + "description": "Subtitle for the Full Page capture mode." + }, + "modeVisible": { + "message": "Vùng hiển thị", + "description": "Capture mode: capture what is currently visible." + }, + "modeVisibleSub": { + "message": "Những gì bạn đang thấy", + "description": "Subtitle for the Visible Area capture mode." + }, + "modeRegion": { + "message": "Vùng chọn", + "description": "Capture mode: capture a user-dragged region." + }, + "modeRegionSub": { + "message": "Nhấp và kéo để chọn", + "description": "Subtitle for the Selected Region capture mode." + }, + "repeatLastRegion": { + "message": "Lặp lại vùng chọn trước", + "description": "Footer link and context menu item that re-captures the previous region selection." + }, + "settingsTitle": { + "message": "Cài đặt", + "description": "Settings panel heading." + }, + "expressLabel": { + "message": "Nhấp vào biểu tượng trên thanh công cụ sẽ chụp toàn trang", + "description": "Label for the capture-on-icon-click row in the popup, and the icon context-menu checkbox." + }, + "expressHint": { + "message": "Tắt để mỗi lần chọn Toàn trang, Vùng hiển thị hoặc Vùng chọn.", + "description": "Hint shown under the express row when express mode is on." + }, + "expressMigratedNote": { + "message": "Nhấp vào biểu tượng giờ sẽ chụp toàn trang. Nhấp chuột phải để chọn chế độ khác.", + "description": "One-time editor note after the update that made a toolbar click capture the full page." + }, + "editorRateLabel": { + "message": "Đánh giá", + "description": "Editor topbar: label on the always-visible Rate star." + }, + "editorRateTooltip": { + "message": "30 giây. Đây là cách người khác tìm thấy một công cụ chụp màn hình riêng tư.", + "description": "Editor topbar: tooltip on the Rate star." + }, + "ratePromptHeadline": { + "message": "Công cụ này có giúp bạn tiết kiệm thời gian không?", + "description": "One-time post-export rate prompt: headline." + }, + "ratePromptBody": { + "message": "Một đánh giá ngắn trên Chrome Web Store là cách chính để một công cụ riêng tư được tìm thấy. Chỉ mất 30 giây.", + "description": "One-time post-export rate prompt: body." + }, + "ratePromptRate": { + "message": "Đánh giá trên Chrome Web Store", + "description": "One-time post-export rate prompt: button that opens the reviews tab." + }, + "ratePromptLater": { + "message": "Để sau", + "description": "One-time post-export rate prompt: dismiss button." + }, + "errNoTab": { + "message": "Không tìm thấy thẻ đang hoạt động.", + "description": "Capture error: no active tab." + }, + "errProtectedPage": { + "message": "Không thể chụp trang được bảo vệ này.", + "description": "Capture error: browser-protected page." + }, + "errBlankPage": { + "message": "Trang này không có nội dung cuộn.", + "description": "Capture error: nothing scrollable to capture." + }, + "errTooLarge": { + "message": "Trang này quá dài cho một ảnh ($PX$ px). Hãy thử vùng hiển thị hoặc vùng chọn.", + "description": "Capture error: page taller than one canvas. $PX$ is the height in device pixels.", + "placeholders": { + "px": { + "content": "$1", + "example": "35000" + } + } + }, + "errNoRegion": { + "message": "Vùng đã lưu không vừa màn hình này — hãy chọn lại trước.", + "description": "Capture error: repeat-last-region with no region that fits." + }, + "errClipboard": { + "message": "Không thể sao chép ảnh chụp vào bảng nhớ tạm.", + "description": "Quick-action error: clipboard write failed." + }, + "errSave": { + "message": "Không thể lưu ảnh chụp vào ổ đĩa.", + "description": "Quick-action error: disk save failed." + }, + "errUnknown": { + "message": "Chụp thất bại ngoài dự kiến.", + "description": "Capture error: unexpected failure." + } +} diff --git a/public/_locales/zh_CN/messages.json b/public/_locales/zh_CN/messages.json index 12d75b6..7a63117 100644 --- a/public/_locales/zh_CN/messages.json +++ b/public/_locales/zh_CN/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "网页整页截图与标注 - OpenScreenShot", + "message": "网页整页截图 - OpenScreenShot", "description": "Extension name shown in Chrome UI." }, "extDesc": { - "message": "整页、区域和可见区域截图,自带标注编辑器。导出 PNG、JPEG、WebP、PDF。100% 私密,离线运行,不上传。", + "message": "一键截取整页截图。开源,100% 私密,无需账号,不上传。", "description": "Extension description shown in the Chrome Web Store and extensions page." }, "modeFullPage": { @@ -222,13 +222,17 @@ "description": "Hint shown under the after-capture row when Download is selected." }, "expressLabel": { - "message": "点击图标即截图", + "message": "点击图标即截取整页", "description": "Label for the capture-on-icon-click row in the popup, and the icon context-menu checkbox." }, "expressHint": { - "message": "现在点击图标会直接截取整页。右键点击图标可关闭。", + "message": "关闭后,每次可选择整页、可见区域或选定区域。", "description": "Hint shown under the express row when express mode is on." }, + "expressMigratedNote": { + "message": "现在点击图标即截取整页。其他模式请右键选择。", + "description": "One-time editor note after the update that made a toolbar click capture the full page." + }, "recOverlayPause": { "message": "暂停", "description": "Recording control bar: button that pauses the active recording." @@ -2252,5 +2256,87 @@ "settingsSupport": { "message": "支持", "description": "Popup settings: label for the row holding the Ko-fi and cool-stuff links." + }, + "welcomeTitle": { + "message": "欢迎", + "description": "Welcome page: browser tab title." + }, + "welcomeHeadline": { + "message": "就绪。点击图标。", + "description": "Welcome page: headline inviting the first icon click." + }, + "welcomeBody": { + "message": "OpenScreenShot 一键截取整页。无需账号,任何数据都不会离开这台电脑。现在就在本页试试吧。", + "description": "Welcome page: one-paragraph pitch under the headline." + }, + "welcomePrimary": { + "message": "点击工具栏中的 OpenScreenShot 图标。", + "description": "Welcome page: the primary instruction line." + }, + "welcomeSecondary": { + "message": "或按 Ctrl+Shift+S(Mac 上为 Cmd+Shift+S)。", + "description": "Welcome page: the keyboard-shortcut alternative line." + }, + "editorRateLabel": { + "message": "评分", + "description": "Editor topbar: label on the always-visible Rate star." + }, + "editorRateTooltip": { + "message": "只需 30 秒。这是其他人发现私密截图工具的方式。", + "description": "Editor topbar: tooltip on the Rate star." + }, + "ratePromptHeadline": { + "message": "它帮你节省时间了吗?", + "description": "One-time post-export rate prompt: headline." + }, + "ratePromptBody": { + "message": "在 Chrome 应用商店留下简短评分,是私密工具被发现的主要途径。只需 30 秒。", + "description": "One-time post-export rate prompt: body." + }, + "ratePromptRate": { + "message": "去 Chrome 应用商店评分", + "description": "One-time post-export rate prompt: button that opens the reviews tab." + }, + "ratePromptLater": { + "message": "暂不", + "description": "One-time post-export rate prompt: dismiss button." + }, + "errNoTab": { + "message": "未找到活动标签页。", + "description": "Capture error: no active tab." + }, + "errProtectedPage": { + "message": "无法截取此受保护的页面。", + "description": "Capture error: browser-protected page." + }, + "errBlankPage": { + "message": "此页面没有可滚动的内容。", + "description": "Capture error: nothing scrollable to capture." + }, + "errTooLarge": { + "message": "页面太长,无法合成一张图片($PX$px)。请尝试可见区域或选定区域。", + "description": "Capture error: page taller than one canvas. $PX$ is the height in device pixels.", + "placeholders": { + "px": { + "content": "$1", + "example": "35000" + } + } + }, + "errNoRegion": { + "message": "保存的区域不适合当前屏幕,请先重新选择。", + "description": "Capture error: repeat-last-region with no region that fits." + }, + "errClipboard": { + "message": "无法将截图复制到剪贴板。", + "description": "Quick-action error: clipboard write failed." + }, + "errSave": { + "message": "无法将截图保存到磁盘。", + "description": "Quick-action error: disk save failed." + }, + "errUnknown": { + "message": "截图意外失败。", + "description": "Capture error: unexpected failure." } } diff --git a/public/_locales/zh_TW/messages.json b/public/_locales/zh_TW/messages.json index 6268b6c..5fd7076 100644 --- a/public/_locales/zh_TW/messages.json +++ b/public/_locales/zh_TW/messages.json @@ -1,10 +1,10 @@ { "extName": { - "message": "全頁截圖與註解 - OpenScreenShot", + "message": "整頁截圖 - OpenScreenShot", "description": "Extension name shown in Chrome UI." }, "extDesc": { - "message": "整頁、選取區域與可見範圍截圖,內建註解編輯器。可匯出 PNG、JPEG、WebP、PDF。100% 私密、離線、不上傳。", + "message": "一鍵擷取整頁截圖。開放原始碼,100% 私密,無需帳號,不上傳。", "description": "Extension description shown in the Chrome Web Store and extensions page." }, "modeFullPage": { @@ -222,13 +222,17 @@ "description": "Hint shown under the after-capture row when Download is selected." }, "expressLabel": { - "message": "點圖示即擷取", + "message": "點一下圖示即擷取整個網頁", "description": "Label for the capture-on-icon-click row in the popup, and the icon context-menu checkbox." }, "expressHint": { - "message": "點圖示即擷取整個網頁。在圖示上按右鍵可關閉。", + "message": "關閉後,每次可選擇整個網頁、可見範圍或選取區域。", "description": "Hint shown under the express row when express mode is on." }, + "expressMigratedNote": { + "message": "現在點一下圖示即可擷取整個網頁。其他模式請按右鍵選擇。", + "description": "One-time editor note after the update that made a toolbar click capture the full page." + }, "recOverlayPause": { "message": "暫停", "description": "Recording control bar: button that pauses the active recording." @@ -2252,5 +2256,87 @@ "settingsSupport": { "message": "支持", "description": "Popup settings: label for the row holding the Ko-fi and cool-stuff links." + }, + "welcomeTitle": { + "message": "歡迎", + "description": "Welcome page: browser tab title." + }, + "welcomeHeadline": { + "message": "就緒。點一下圖示。", + "description": "Welcome page: headline inviting the first icon click." + }, + "welcomeBody": { + "message": "OpenScreenShot 一鍵擷取整個網頁。無需帳號,任何資料都不會離開這台電腦。現在就在本頁試試看。", + "description": "Welcome page: one-paragraph pitch under the headline." + }, + "welcomePrimary": { + "message": "點一下工具列中的 OpenScreenShot 圖示。", + "description": "Welcome page: the primary instruction line." + }, + "welcomeSecondary": { + "message": "或按 Ctrl+Shift+S(Mac 為 Cmd+Shift+S)。", + "description": "Welcome page: the keyboard-shortcut alternative line." + }, + "editorRateLabel": { + "message": "評分", + "description": "Editor topbar: label on the always-visible Rate star." + }, + "editorRateTooltip": { + "message": "只要 30 秒。這是其他人發現私密截圖工具的方式。", + "description": "Editor topbar: tooltip on the Rate star." + }, + "ratePromptHeadline": { + "message": "有幫你節省時間嗎?", + "description": "One-time post-export rate prompt: headline." + }, + "ratePromptBody": { + "message": "在 Chrome 線上應用程式商店留下簡短評分,是私密工具被看見的主要方式。只要 30 秒。", + "description": "One-time post-export rate prompt: body." + }, + "ratePromptRate": { + "message": "前往 Chrome 線上應用程式商店評分", + "description": "One-time post-export rate prompt: button that opens the reviews tab." + }, + "ratePromptLater": { + "message": "暫時不要", + "description": "One-time post-export rate prompt: dismiss button." + }, + "errNoTab": { + "message": "找不到使用中的分頁。", + "description": "Capture error: no active tab." + }, + "errProtectedPage": { + "message": "無法擷取此受保護的網頁。", + "description": "Capture error: browser-protected page." + }, + "errBlankPage": { + "message": "此網頁沒有可捲動的內容。", + "description": "Capture error: nothing scrollable to capture." + }, + "errTooLarge": { + "message": "網頁太長,無法合成一張圖片($PX$px)。請改用可見範圍或選取區域。", + "description": "Capture error: page taller than one canvas. $PX$ is the height in device pixels.", + "placeholders": { + "px": { + "content": "$1", + "example": "35000" + } + } + }, + "errNoRegion": { + "message": "儲存的區域不符合目前螢幕,請先重新選取。", + "description": "Capture error: repeat-last-region with no region that fits." + }, + "errClipboard": { + "message": "無法將截圖複製到剪貼簿。", + "description": "Quick-action error: clipboard write failed." + }, + "errSave": { + "message": "無法將截圖儲存到磁碟。", + "description": "Quick-action error: disk save failed." + }, + "errUnknown": { + "message": "擷取意外失敗。", + "description": "Capture error: unexpected failure." } } diff --git a/scripts/shots/render.mjs b/scripts/shots/render.mjs index ab11dfa..8e9e5dc 100644 --- a/scripts/shots/render.mjs +++ b/scripts/shots/render.mjs @@ -57,6 +57,8 @@ const OUT_DIR = join(ROOT, 'site/src/assets'); // README hero. const LOCALE = process.env.OSS_LOCALE || 'en'; const STORE_DIR = join(ROOT, 'media/store', LOCALE === 'en' ? '' : LOCALE); +// The five store gallery billboards (slide-1..5), uploaded in order. +const SLIDES_DIR = join(ROOT, 'store/screenshots'); // Poster marketing copy, one file per locale, filled into the `{{key}}` // slots of marquee.html and promo-tile.html before they are screenshotted. const COPY_DIR = join(SHOTS_DIR, 'copy'); @@ -873,6 +875,25 @@ async function renderPosters(browser) { console.log(`✓ ${rel(out)} (${w}x${h})`); } + // Store gallery billboards (P0.4): five 1280x800 statements in huge type + // on saturated fields, no UI captures. English only — the founder uploads + // one global set. 24-bit PNG with no alpha, as the store requires. + if (LOCALE === 'en') { + await mkdir(SLIDES_DIR, { recursive: true }); + for (let i = 1; i <= 5; i++) { + const name = `slide-${i}`; + await assertPosterImagesLoad(browser, name); + const png = await screenshotPoster(name, STORE_SIZE.w, STORE_SIZE.h, work); + const out = join(SLIDES_DIR, `${name}.png`); + await sharp(png) + .resize(STORE_SIZE.w, STORE_SIZE.h) + .flatten({ background: '#ffffff' }) + .png({ compressionLevel: 9 }) + .toFile(out); + console.log(`✓ ${rel(out)} (${STORE_SIZE.w}x${STORE_SIZE.h})`); + } + } + // Store promo images and the popup store screenshot: exact sizes, JPEG. // Rendered at 2x like the shots, then downscaled straight into the store // directory. diff --git a/scripts/shots/slide-1.html b/scripts/shots/slide-1.html new file mode 100644 index 0000000..2a56414 --- /dev/null +++ b/scripts/shots/slide-1.html @@ -0,0 +1,22 @@ + + + + + store slide 1 — 1280x800 + + + + +
+

THE WHOLE PAGE.ONE CLICK.

+
+ + OpenScreenShot +
+ + diff --git a/scripts/shots/slide-2.html b/scripts/shots/slide-2.html new file mode 100644 index 0000000..074b3a2 --- /dev/null +++ b/scripts/shots/slide-2.html @@ -0,0 +1,22 @@ + + + + + store slide 2 — 1280x800 + + + + +
+

SCROLLS.STITCHES.DONE.

+
+ + OpenScreenShot +
+ + diff --git a/scripts/shots/slide-3.html b/scripts/shots/slide-3.html new file mode 100644 index 0000000..5d0850c --- /dev/null +++ b/scripts/shots/slide-3.html @@ -0,0 +1,22 @@ + + + + + store slide 3 — 1280x800 + + + + +
+

ANNOTATE.NO PAYWALL.

+
+ + OpenScreenShot +
+ + diff --git a/scripts/shots/slide-4.html b/scripts/shots/slide-4.html new file mode 100644 index 0000000..3e58b24 --- /dev/null +++ b/scripts/shots/slide-4.html @@ -0,0 +1,22 @@ + + + + + store slide 4 — 1280x800 + + + + +
+

WI-FI OFF.STILL WORKS.

+
+ + OpenScreenShot +
+ + diff --git a/scripts/shots/slide-5.html b/scripts/shots/slide-5.html new file mode 100644 index 0000000..d3bb12e --- /dev/null +++ b/scripts/shots/slide-5.html @@ -0,0 +1,22 @@ + + + + + store slide 5 — 1280x800 + + + + +
+

MIT.READ THE SOURCE.

+
+ + OpenScreenShot +
+ + diff --git a/scripts/shots/slide.css b/scripts/shots/slide.css new file mode 100644 index 0000000..9a45954 --- /dev/null +++ b/scripts/shots/slide.css @@ -0,0 +1,64 @@ +/* Store gallery billboards (P0.4): huge type on a saturated field, one + * statement per slide. The winning tiles in CWS search are color blocks + * readable at thumbnail size — these are deliberately not UI captures. + * Each slide-N.html sets --field and --type; everything else is shared. */ +* { + box-sizing: border-box; + margin: 0; +} +html, +body { + width: 1280px; + height: 800px; + overflow: hidden; +} +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + background: var(--field); + color: var(--type); + position: relative; +} +/* A slight corner falloff plus grain, so the field never reads as a flat + * vector fill at gallery size. */ +.ground { + position: absolute; + inset: 0; + background: radial-gradient(140% 110% at 18% 12%, rgba(255, 255, 255, 0.1), rgba(0, 0, 0, 0.12)); +} +.ground::after { + content: ''; + position: absolute; + inset: 0; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.07 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); + mix-blend-mode: multiply; +} +h1 { + position: absolute; + left: 96px; + right: 96px; + top: 50%; + transform: translateY(-54%); + font-size: 118px; + line-height: 1.02; + font-weight: 900; + letter-spacing: -0.022em; + text-wrap: balance; +} +h1 span { + display: block; +} +.wordmark { + position: absolute; + left: 96px; + bottom: 72px; + display: flex; + align-items: center; + gap: 14px; + font-size: 28px; + font-weight: 700; + letter-spacing: -0.01em; +} +.wordmark img { + width: 44px; + height: 44px; +} diff --git a/site/src/assets/shot-4.webp b/site/src/assets/shot-4.webp index fc4c1b4..ac4eb36 100644 Binary files a/site/src/assets/shot-4.webp and b/site/src/assets/shot-4.webp differ diff --git a/site/src/assets/shot-5.webp b/site/src/assets/shot-5.webp index 201334a..118694b 100644 Binary files a/site/src/assets/shot-5.webp and b/site/src/assets/shot-5.webp differ diff --git a/site/src/i18n/en/uninstall.json b/site/src/i18n/en/uninstall.json new file mode 100644 index 0000000..89c5639 --- /dev/null +++ b/site/src/i18n/en/uninstall.json @@ -0,0 +1,23 @@ +{ + "meta": { + "title": "Before you go — OpenScreenShot", + "description": "You uninstalled OpenScreenShot. If it worked, a rating helps other people find a private tool. If it failed, tell us what happened." + }, + "label": "OpenScreenShot", + "heading": "Before you go.", + "lead": "If it worked, a rating is the whole growth engine. If it failed, tell us. We read it.", + "happy": { + "heading": "It worked, I just don't need it.", + "body": "Then a public rating is the highest-leverage thing you can do for an open-source tool. It takes 30 seconds.", + "cta": "Rate OpenScreenShot" + }, + "broken": { + "heading": "Something was wrong.", + "body": "What happened? What did you expect?", + "placeholder": "Example: full page missed the footer on a dashboard. Or: I wanted region capture on first click.", + "cta": "Send feedback", + "thanks": "Thanks. If you are willing, a rating still helps people who need a private option.", + "rateAnyway": "Rate anyway" + }, + "footer": "Source is still public." +} diff --git a/site/src/pages/[...lang]/uninstall/index.astro b/site/src/pages/[...lang]/uninstall/index.astro new file mode 100644 index 0000000..e16af67 --- /dev/null +++ b/site/src/pages/[...lang]/uninstall/index.astro @@ -0,0 +1,94 @@ +--- +/** + * Uninstall landing (rating funnel Surface D). chrome.runtime.setUninstallURL + * points here with ?v=&hl= — nothing else. No analytics, no + * cookies; the feedback textarea sends only if the visitor presses Send, and + * it sends by opening their own mail client (there is no backend). + */ +import Doc from '../../../layouts/Doc.astro'; +import { t, localePaths, localeFromParams } from '../../../i18n'; + +export const getStaticPaths = localePaths; + +const locale = localeFromParams(Astro.params.lang); +const d = t(locale, 'uninstall'); + +const REVIEWS_URL = + 'https://chromewebstore.google.com/detail/hdabbojjccojlapnfjpdppcpfcnhgmdp/reviews'; +const REPO_URL = 'https://github.com/pghqdev/OpenScreenShot'; +--- + + +
+

{d.happy.heading}

+

{d.happy.body}

+

+ {d.happy.cta} +

+
+ +
+

{d.broken.heading}

+

{d.broken.body}

+
+ +

+ +

+
+ +
+ +

+ {d.footer} + github.com/pghqdev/OpenScreenShot +

+
+ + + + diff --git a/src/background/index.ts b/src/background/index.ts index 24190c1..759103d 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -16,6 +16,7 @@ import type { CaptureMode, CaptureRequest, PopupMessage, TileSpec } from '../sha import { getLastRegion, getSettings, + migrateExpressDefault, onSettingsChanged, setLastCapture, setLastRegion, @@ -31,6 +32,7 @@ import { normalizeCaptureDelay, } from '../shared/utils'; import { clampRegionRect, computeScrollPositions, MAX_CANVAS_HEIGHT_PX } from '../shared/geometry'; +import { recordExportSuccess } from '../shared/rating'; import { cropTile, getMetrics, @@ -46,20 +48,53 @@ import { restoreRecBadge } from './recording'; const EDITOR_URL = chrome.runtime.getURL('src/editor/index.html'); const POPUP_URL = 'src/popup/index.html'; +/** First-run page: a tall dummy article that invites the first capture. */ +const WELCOME_URL = chrome.runtime.getURL('src/welcome/index.html'); +/** + * Where an uninstall lands (Surface D of the rating funnel). Query carries + * the extension version and UI locale only — never a page URL, title, or + * capture. + */ +const UNINSTALL_URL = 'https://openscreenshot.app/uninstall'; +/** The popup page opened as a tab, straight into its settings pane. */ +const SETTINGS_TAB_URL = chrome.runtime.getURL('src/popup/index.html?settings=1'); /** Icon context-menu checkbox that toggles express mode. */ const MENU_EXPRESS_ID = 'oss-express'; +/** Page-menu and icon-menu items that open the settings pane in a tab. */ +const MENU_SETTINGS_ID = 'oss-settings'; +const MENU_ICON_SETTINGS_ID = 'oss-icon-settings'; +/** + * Icon context-menu capture items. With express mode hijacking the icon + * click, the icon's right-click menu is where the other modes stay one + * gesture away. Separate ids from MENU_IDS: a menu item cannot sit both + * under the page parent and on the action context. + */ +const ICON_MENU_IDS: Record = { + 'full-page': 'oss-icon-full-page', + visible: 'oss-icon-visible', + region: 'oss-icon-region', +}; /** Minimum gap between `captureVisibleTab` calls — Chrome throttles to ~2/sec. */ const CAPTURE_THROTTLE_MS = 500; /** Time to let the page paint/composite after each scroll before capturing. */ const PAINT_SETTLE_MS = 60; -// No tab opens on install: the one grant a recording needs is asked for from -// the Record click itself, so first run has nothing to walk through. -chrome.runtime.onInstalled.addListener(() => { - void createContextMenus(); +// A fresh install opens the welcome page — a tall article built to make the +// first one-click capture look good. Updates open nothing. The express +// migration runs before the menus so the checkbox reads the post-migration +// value. +chrome.runtime.onInstalled.addListener((details) => { + void migrateExpressDefault(details.reason).then(() => createContextMenus()); + if (details.reason === 'install') void chrome.tabs.create({ url: WELCOME_URL }); }); +// Registered on every worker start so it survives service-worker restarts. +// Version and locale only — see UNINSTALL_URL. +void chrome.runtime.setUninstallURL( + `${UNINSTALL_URL}?v=${chrome.runtime.getManifest().version}&hl=${chrome.i18n.getUILanguage()}`, +); + /** Contexts the capture menu appears in — everywhere on a page. */ const MENU_CONTEXTS: NonNullable = [ 'page', @@ -120,7 +155,23 @@ async function createContextMenusOnce(): Promise { title: titles[mode], contexts: MENU_CONTEXTS, }); + chrome.contextMenus.create({ + id: ICON_MENU_IDS[mode], + title: titles[mode], + contexts: ['action'], + }); } + chrome.contextMenus.create({ + id: MENU_SETTINGS_ID, + parentId: 'oss-parent', + title: chrome.i18n.getMessage('settingsTitle'), + contexts: MENU_CONTEXTS, + }); + chrome.contextMenus.create({ + id: MENU_ICON_SETTINGS_ID, + title: chrome.i18n.getMessage('settingsTitle'), + contexts: ['action'], + }); // Express lives on the icon's right-click menu: once it hijacks the icon // click, this checkbox is the only remaining surface that can turn it off. chrome.contextMenus.create( @@ -192,7 +243,14 @@ chrome.contextMenus.onClicked.addListener((info) => { void handleCapture('region', true).catch(onCaptureError); return; } - const mode = menuIdToMode(id); + if (id === MENU_SETTINGS_ID || id === MENU_ICON_SETTINGS_ID) { + void chrome.tabs.create({ url: SETTINGS_TAB_URL }); + return; + } + const iconMode = (Object.entries(ICON_MENU_IDS) as [CaptureMode, string][]).find( + ([, menuId]) => menuId === id, + )?.[0]; + const mode = iconMode ?? menuIdToMode(id); if (mode) void handleCapture(mode).catch(onCaptureError); }); @@ -211,14 +269,14 @@ chrome.runtime.onMessage.addListener((message: unknown) => { async function handleCapture(mode: CaptureMode, repeatRegion = false): Promise { const tab = await getActiveTab(); if (!tab || tab.id == null) { - broadcast({ type: 'CAPTURE_ERROR', code: 'unknown', message: 'No active tab found.' }); + broadcast({ type: 'CAPTURE_ERROR', code: 'unknown', message: chrome.i18n.getMessage('errNoTab') }); return; } if (isProtectedUrl(tab.url)) { broadcast({ type: 'CAPTURE_ERROR', code: 'protected-page', - message: "Can't screenshot this protected page.", + message: chrome.i18n.getMessage('errProtectedPage'), }); return; } @@ -325,7 +383,7 @@ async function captureRegion(tab: chrome.tabs.Tab, repeat = false): Promise { broadcast({ type: 'CAPTURE_ERROR', code: 'blank-page', - message: 'This page has no scrollable content.', + message: chrome.i18n.getMessage('errBlankPage'), }); return; } @@ -372,7 +430,7 @@ async function captureFullPage(tab: chrome.tabs.Tab): Promise { broadcast({ type: 'CAPTURE_ERROR', code: 'too-large', - message: `This page is too tall to capture in one image (${canvasHeight}px). Try visible or region mode.`, + message: chrome.i18n.getMessage('errTooLarge', String(canvasHeight)), }); return; } @@ -476,10 +534,11 @@ async function deliverCapture( broadcast({ type: 'CAPTURE_ERROR', code: 'quick-action', - message: 'Could not copy the screenshot to the clipboard.', + message: chrome.i18n.getMessage('errClipboard'), }); return false; } + void recordExportSuccess(); void flashDoneBadge(); return true; } @@ -493,10 +552,11 @@ async function deliverCapture( broadcast({ type: 'CAPTURE_ERROR', code: 'quick-action', - message: 'Could not save the screenshot to disk.', + message: chrome.i18n.getMessage('errSave'), }); return false; } + void recordExportSuccess(); void flashDoneBadge(); return true; } @@ -516,7 +576,7 @@ function commandToMode(command: string): CaptureMode | null { function onCaptureError(err: unknown): void { console.error('[OpenScreenShot] capture failed', err); - broadcast({ type: 'CAPTURE_ERROR', code: 'unknown', message: 'Capture failed unexpectedly.' }); + broadcast({ type: 'CAPTURE_ERROR', code: 'unknown', message: chrome.i18n.getMessage('errUnknown') }); } function broadcast(msg: PopupMessage): void { diff --git a/src/editor/App.tsx b/src/editor/App.tsx index cae2844..2a9d3d7 100644 --- a/src/editor/App.tsx +++ b/src/editor/App.tsx @@ -35,12 +35,20 @@ import { IconRedo, IconSelect, IconSpotlight, + IconStar, IconStep, IconText, IconTrash, IconUndo, } from '../shared/icons'; import { getSettings, setSettings } from '../shared/storage'; +import { + CWS_REVIEWS_URL, + markRatedOrDismissed, + markRatePromptShown, + recordExportSuccess, + shouldShowRatePrompt, +} from '../shared/rating'; import { labelForSource } from './capture-label'; import { ZoomMenu } from './ZoomMenu'; import { BeautifyMenu } from './BeautifyMenu'; @@ -159,9 +167,35 @@ export function App() { if (toolbarRef.current) syncRovingTabIndex(toolbarRef.current); }, []); + // The one post-success rate prompt (Surface C). Marked shown the moment it + // renders, so it appears at most once per install however it is answered. + const [ratePrompt, setRatePrompt] = useState(false); + + async function recordSuccess() { + await recordExportSuccess(); + if (await shouldShowRatePrompt()) { + await markRatePromptShown(); + setRatePrompt(true); + } + } + + function openReviews() { + void markRatedOrDismissed(); + setRatePrompt(false); + window.open(CWS_REVIEWS_URL, '_blank', 'noopener'); + } + + function dismissRatePrompt() { + void markRatedOrDismissed(); + setRatePrompt(false); + } + function copyToClipboard() { ed.copyImage() - .then(() => setCopyState('copied')) + .then(() => { + setCopyState('copied'); + void recordSuccess(); + }) .catch(() => setCopyState('failed')) .finally(() => setTimeout(() => setCopyState('idle'), 1500)); } @@ -311,6 +345,15 @@ export function App() { imageSize={ed.imageSize} onChange={ed.setFrame} /> + + + + ) : null} {sheetT.mounted ? ( setSheetOpen(false)} closing={sheetT.closing} /> @@ -760,10 +822,12 @@ function isLight(hex: string): boolean { function ExportDialog({ ed, onClose, + onSuccess, closing, }: { ed: ReturnType; onClose: () => void; + onSuccess: () => void; closing: boolean; }) { const df = ed.settings?.defaultFormat ?? 'png'; @@ -877,6 +941,7 @@ function ExportDialog({ // Swallowed: see comment above. } } + onSuccess(); onClose(); } catch { // Keep the dialog open on failure — there is no other surface for this diff --git a/src/editor/editor.css b/src/editor/editor.css index 802ca68..2d1fad6 100644 --- a/src/editor/editor.css +++ b/src/editor/editor.css @@ -1356,6 +1356,50 @@ body { pointer-events: none; } +/* The always-visible Rate star in the topbar — an icon-btn with a word, + so it widens past the square the base rule assumes. */ +.rate-btn { + width: auto; + padding: 0 10px; + gap: 6px; + display: inline-flex; + align-items: center; + font-size: var(--fs-sm); +} + +/* The one post-success rate prompt (Surface C): a non-blocking card in the + bottom corner. It never covers the export controls and never blocks. */ +.rate-prompt { + position: fixed; + right: var(--s-4); + bottom: var(--s-4); + width: 320px; + padding: var(--s-4); + border: 1px solid var(--border-1); + border-radius: var(--r-md); + background: var(--surface-1); + box-shadow: var(--sh-md); + z-index: 7; + animation: oss-notice-in var(--dur-mid) var(--ease-out); +} + +.rate-prompt-headline { + margin: 0 0 var(--s-2); + font-weight: 600; + font-size: var(--fs-base); +} + +.rate-prompt-body { + margin: 0 0 var(--s-3); + font-size: var(--fs-sm); + color: var(--text-2); +} + +.rate-prompt-actions { + display: flex; + gap: var(--s-2); +} + .modal-text { margin: 0 0 var(--s-4); color: var(--text-2); diff --git a/src/editor/useEditor.ts b/src/editor/useEditor.ts index 0c40058..dd8d178 100644 --- a/src/editor/useEditor.ts +++ b/src/editor/useEditor.ts @@ -110,6 +110,7 @@ import { setDraftImage, setLastCapture, setSettings, + takeExpressNote, } from '../shared/storage'; import { formatFilename } from '../shared/utils'; import { applyTheme, watchSystemTheme } from '../shared/theme'; @@ -987,6 +988,14 @@ export function useEditor() { // Live-update a "system" theme setting when the OS preference flips. useEffect(() => watchSystemTheme(() => void getSettings().then((s) => applyTheme(s.theme))), []); + // One-time note after the express-default migration changed what the icon + // does. `takeExpressNote` clears the flag, so this shows exactly once. + useEffect(() => { + void takeExpressNote().then((pending) => { + if (pending) setStageNotice(t('expressMigratedNote')); + }); + }, []); + // Wheel zoom (non-passive so we can preventDefault trackpad scroll). useEffect(() => { const canvas = canvasRef.current; diff --git a/src/popup/App.tsx b/src/popup/App.tsx index 1e9a02d..38a95eb 100644 --- a/src/popup/App.tsx +++ b/src/popup/App.tsx @@ -215,7 +215,11 @@ const MODES: ModeDef[] = [ export function App() { const [settings, setSettingsState] = useState(DEFAULT_SETTINGS); - const [showSettings, setShowSettings] = useState(false); + // The context menus' "Capture settings" items open this page as a tab with + // ?settings=1 — the settings pane is unreachable by icon click in express mode. + const [showSettings, setShowSettings] = useState( + () => new URLSearchParams(location.search).has('settings'), + ); const [busy, setBusy] = useState(null); const [progress, setProgress] = useState(null); const [toasts, setToasts] = useState([]); diff --git a/src/shared/icons.tsx b/src/shared/icons.tsx index 909512f..e5d245a 100644 --- a/src/shared/icons.tsx +++ b/src/shared/icons.tsx @@ -226,6 +226,15 @@ export function IconHistory(props: IconProps) { ); } +/** A five-point star — the Rate control in the editor header. */ +export function IconStar(props: IconProps) { + return ( + + + + ); +} + /** Outer frame with a small window in the corner — pin in a floating window. */ export function IconPictureInPicture(props: IconProps) { return ( diff --git a/src/shared/rating.ts b/src/shared/rating.ts new file mode 100644 index 0000000..60f9beb --- /dev/null +++ b/src/shared/rating.ts @@ -0,0 +1,47 @@ +/** + * The rating funnel's local state (see agent_docs and the P0 brief, section + * 11). Everything here is a plain local flag — no network call, nothing + * blocks an export, and the one post-success prompt shows at most once per + * install. + */ + +/** The listing's reviews tab — where every Rate surface points. */ +export const CWS_REVIEWS_URL = + 'https://chromewebstore.google.com/detail/hdabbojjccojlapnfjpdppcpfcnhgmdp/reviews'; + +/** Set by the Rate star or either prompt button; ends the funnel for good. */ +const RATED_KEY = 'openscreenshot:rated-or-dismissed'; +/** Successful exports/copies on this install, across editor and quick mode. */ +const SUCCESS_COUNT_KEY = 'openscreenshot:export-success-count'; +/** The one post-success prompt has been shown (regardless of the answer). */ +const PROMPTED_KEY = 'openscreenshot:rate-prompted'; + +/** How many successful exports/copies before the one prompt may appear. */ +export const RATE_PROMPT_AFTER = 3; + +/** Count one successful export or clipboard copy. */ +export async function recordExportSuccess(): Promise { + const stored = await chrome.storage.local.get(SUCCESS_COUNT_KEY); + const count = (stored[SUCCESS_COUNT_KEY] as number | undefined) ?? 0; + await chrome.storage.local.set({ [SUCCESS_COUNT_KEY]: count + 1 }); +} + +/** + * True when the one post-success prompt is due: enough successes, the user + * has never used a Rate surface, and the prompt has not been shown before. + */ +export async function shouldShowRatePrompt(): Promise { + const stored = await chrome.storage.local.get([SUCCESS_COUNT_KEY, RATED_KEY, PROMPTED_KEY]); + if (stored[RATED_KEY] || stored[PROMPTED_KEY]) return false; + return ((stored[SUCCESS_COUNT_KEY] as number | undefined) ?? 0) >= RATE_PROMPT_AFTER; +} + +/** The prompt is on screen — never show it again, whatever the answer. */ +export async function markRatePromptShown(): Promise { + await chrome.storage.local.set({ [PROMPTED_KEY]: true }); +} + +/** The user clicked a Rate surface (or Not now) — the funnel never nags. */ +export async function markRatedOrDismissed(): Promise { + await chrome.storage.local.set({ [RATED_KEY]: true }); +} diff --git a/src/shared/storage.ts b/src/shared/storage.ts index 59027b7..6026f93 100644 --- a/src/shared/storage.ts +++ b/src/shared/storage.ts @@ -20,6 +20,48 @@ export async function setSettings(patch: Partial): Promise { return next; } +const EXPRESS_MIGRATED_KEY = 'openscreenshot:express-default-migrated'; +const EXPRESS_NOTE_KEY = 'openscreenshot:express-note-pending'; + +/** + * One-shot flip of `expressMode` to the new default. Stored settings persist + * the whole object, so an install that ever wrote settings holds + * `expressMode: false` even when the user never touched the toggle — merely + * changing DEFAULT_SETTINGS would leave every such install on the picker. + * Runs from `onInstalled`; the flag makes later runs no-ops, so a user who + * turns express off afterwards stays off. An update also queues the one-time + * editor note (see `takeExpressNote`) when the flip changed behavior. + */ +export async function migrateExpressDefault(reason: string): Promise { + const stored = await chrome.storage.local.get([EXPRESS_MIGRATED_KEY, SETTINGS_KEY]); + if (stored[EXPRESS_MIGRATED_KEY]) return; + if (reason === 'install') { + // Fresh install: the default already applies; write nothing but the flag + // so stored settings stay empty and future default changes flow through. + await chrome.storage.local.set({ [EXPRESS_MIGRATED_KEY]: true }); + return; + } + const partial = (stored[SETTINGS_KEY] ?? {}) as Partial; + const write: Record = { + [SETTINGS_KEY]: { ...partial, expressMode: true }, + [EXPRESS_MIGRATED_KEY]: true, + }; + if (partial.expressMode !== true) write[EXPRESS_NOTE_KEY] = true; + await chrome.storage.local.set(write); +} + +/** + * Read-and-clear the pending "toolbar click now captures the full page" + * note. True at most once per install, on the first editor open after the + * express migration changed what the icon does. + */ +export async function takeExpressNote(): Promise { + const stored = await chrome.storage.local.get(EXPRESS_NOTE_KEY); + if (!stored[EXPRESS_NOTE_KEY]) return false; + await chrome.storage.local.remove(EXPRESS_NOTE_KEY); + return true; +} + /** Run `callback` whenever the stored settings change (any writer, any context). */ export function onSettingsChanged(callback: () => void): void { chrome.storage.onChanged.addListener((changes, area) => { diff --git a/src/shared/types.ts b/src/shared/types.ts index 3838aea..adf4ac1 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -195,7 +195,9 @@ export const DEFAULT_SETTINGS: Settings = { recentColors: [], captureDelay: 0, captureAction: 'editor', - expressMode: false, + // On by default: the toolbar icon is the one-click full-page capture. + // Existing installs are flipped once by migrateExpressDefault (storage.ts). + expressMode: true, beautifyEnabled: false, beautifyPadding: 40, beautifyRadius: 30, diff --git a/src/shared/utils.ts b/src/shared/utils.ts index f9b9d22..5842e36 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -57,6 +57,12 @@ export function sanitizeFilename(name: string): string { /** True for URLs the extension is not allowed to capture. */ export function isProtectedUrl(url: string | undefined): boolean { if (!url) return true; + // The extension's own pages (the welcome page above all — its whole job is + // to be the first one-click capture) are ours to inject into and capture. + // Every other chrome-extension:// origin stays protected below. + if (typeof chrome !== 'undefined' && chrome.runtime?.id) { + if (url.startsWith(`chrome-extension://${chrome.runtime.id}/`)) return false; + } return ( url.startsWith('chrome://') || url.startsWith('chrome-extension://') || diff --git a/src/welcome/index.html b/src/welcome/index.html new file mode 100644 index 0000000..d7f0bed --- /dev/null +++ b/src/welcome/index.html @@ -0,0 +1,86 @@ + + + + + + + Welcome — OpenScreenShot + + + + +
+
+ + OpenScreenShot +
+

You're in. Click the icon.

+

+ OpenScreenShot captures the whole page in one click. No account. Nothing leaves this + machine. Try it on this page right now. +

+

Click the OpenScreenShot icon in the toolbar.

+

Or press Ctrl+Shift+S (Cmd+Shift+S on Mac).

+
+ +
+
+

The whole page.

+

+ Long pages scroll and stitch into one image — sticky headers appear once, nested + scrollers are handled, and the footer makes it in. +

+
+ +
+
+

Annotate

+

Arrows, boxes, text, numbered steps, blur, and crop. All free.

+
+
+

Export

+

PNG, JPEG, WebP, or a multi-page PDF — or straight to the clipboard.

+
+
+

Private

+

No account, no uploads, no telemetry. It works with Wi-Fi off.

+
+
+ +
+

Open source. MIT.

+

+ Every line is public. No standing access to the sites you visit — the only tab it + touches is the one you just captured. +

+
+ +
+

What a capture keeps

+
    +
  1. The hero at the top, exactly once.
  2. +
  3. Every section in between, in order.
  4. +
  5. Charts, cards, and code blocks, unclipped.
  6. +
  7. The long tail most screenshots cut off.
  8. +
  9. This very line, near the bottom of a tall page.
  10. +
+
+ +
+

Scroll no further.

+

If your capture reached this band, it got the whole page. That is the point.

+
+ + +
+ + + diff --git a/src/welcome/main.ts b/src/welcome/main.ts new file mode 100644 index 0000000..f96245e --- /dev/null +++ b/src/welcome/main.ts @@ -0,0 +1,18 @@ +/** + * Fill the welcome hero from the message catalog. The article below the hero + * stays English on purpose — it is a capture playground, not UI chrome. + */ +function t(id: string): string { + return chrome.i18n.getMessage(id) || id; +} + +for (const [id, key] of [ + ['headline', 'welcomeHeadline'], + ['body', 'welcomeBody'], + ['primary', 'welcomePrimary'], + ['secondary', 'welcomeSecondary'], +] as const) { + const node = document.getElementById(id); + if (node) node.textContent = t(key); +} +document.title = `${t('welcomeTitle')} — OpenScreenShot`; diff --git a/src/welcome/welcome.css b/src/welcome/welcome.css new file mode 100644 index 0000000..ea1602b --- /dev/null +++ b/src/welcome/welcome.css @@ -0,0 +1,148 @@ +/* First-run welcome page. Deliberately tall and colorful: it is the page the + * first one-click capture runs on, so it has to look good stitched. Light + * only — it is a marketing surface, not app chrome. */ +* { + box-sizing: border-box; + margin: 0; +} +body { + /* Display rungs for this page only. The shared type scale (tokens.css) + * tops out at UI sizes; this marketing canvas needs billboard type, so it + * declares its own --fs-* rungs — every font-size below still goes + * through a named rung, which is what the font-size guard enforces. */ + --fs-hero: 52px; + --fs-band: 34px; + --fs-title: 24px; + --fs-card-title: 18px; + --fs-lede: 19px; + --fs-body: 17px; + --fs-copy: 16px; + --fs-note: 15px; + --fs-fine: 14px; + --fs-brand: 20px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + background: #faf9f6; + color: #1b1a17; + line-height: 1.55; +} +.hero { + max-width: 720px; + margin: 0 auto; + padding: 72px 24px 48px; + text-align: center; +} +.wordmark { + display: inline-flex; + align-items: center; + gap: 12px; + font-weight: 700; + font-size: var(--fs-brand); + margin-bottom: 40px; +} +.hero h1 { + font-size: var(--fs-hero); + line-height: 1.05; + font-weight: 800; + letter-spacing: -0.02em; + margin-bottom: 20px; +} +.lede { + font-size: var(--fs-lede); + color: #4d4a44; + margin-bottom: 32px; +} +.try { + display: inline-block; + font-size: var(--fs-lede); + font-weight: 700; + background: #e8503a; + color: #fff; + border-radius: 999px; + padding: 12px 26px; + margin-bottom: 12px; +} +.try-alt { + font-size: var(--fs-note); + color: #4d4a44; +} +.article { + max-width: 880px; + margin: 0 auto; + padding: 0 24px 64px; + display: grid; + gap: 28px; +} +.band { + border-radius: 20px; + padding: 56px 48px; + color: #fff; +} +.band h2 { + font-size: var(--fs-band); + font-weight: 800; + letter-spacing: -0.015em; + margin-bottom: 12px; +} +.band p { + max-width: 560px; + font-size: var(--fs-body); + opacity: 0.92; +} +.band-coral { + background: linear-gradient(150deg, #e8503a, #c73a26); +} +.band-ink { + background: linear-gradient(150deg, #23201b, #16130f); +} +.band-amber { + background: linear-gradient(150deg, #f5a623, #dd8c0e); + color: #1b1a17; +} +.cards { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; +} +@media (max-width: 768px) { + .cards { + grid-template-columns: 1fr; + } +} +.card { + border: 1px solid #e6e2d8; + border-radius: 16px; + background: #fff; + padding: 28px 24px; +} +.card h3 { + font-size: var(--fs-card-title); + margin-bottom: 8px; +} +.card p { + font-size: var(--fs-note); + color: #4d4a44; +} +.list { + border: 1px solid #e6e2d8; + border-radius: 20px; + background: #fff; + padding: 40px 48px; +} +.list h2 { + font-size: var(--fs-title); + margin-bottom: 16px; +} +.list ol { + padding-left: 22px; + display: grid; + gap: 10px; + font-size: var(--fs-copy); +} +.foot { + text-align: center; + font-size: var(--fs-fine); + color: #4d4a44; +} +.foot a { + color: #c73a26; +} diff --git a/store/STORE_LISTING.md b/store/STORE_LISTING.md new file mode 100644 index 0000000..f67b623 --- /dev/null +++ b/store/STORE_LISTING.md @@ -0,0 +1,60 @@ +# Chrome Web Store listing — paste-ready copy + +Paste these into the Chrome Web Store Developer Dashboard. The dashboard does +not always take the manifest `extName` as the visible title, so the title is +repeated here. Title max 45 characters; short description max 132. + +## Title (45 chars max) + +Full Page Screenshot - OpenScreenShot + +## Short description (132 chars max) + +Full page screenshot in one click. Open source, 100% private, no account, no uploads. + +## Detailed description + +``` +The full page screenshot you can actually audit. + +Click the icon. Get the entire scrolling page. Annotate, export PNG or PDF, done. + +OpenScreenShot is MIT-licensed, 100 percent private, and asks for no standing access to the sites you visit. No account, no watermark, no uploads, no telemetry. It works with Wi-Fi off. Read the source: https://github.com/pghqdev/OpenScreenShot + +HOW IT WORKS +- Click the OpenScreenShot icon. The page scrolls and stitches itself into one image. +- Annotate with arrows, boxes, text, numbered steps, blur, and crop. All free. +- Export PNG, JPEG, WebP, or PDF, or copy to the clipboard. +- Right-click the page for visible-area or region capture, or to record the tab. + +WHY THIS ONE +- One click. No mode menu in the way of a full page screenshot. +- Open source (MIT). You can read every line. +- No standing site access at install. Access is only the tab you just captured. +- No account, no cloud, no watermark. Captures never leave your machine. +- Sticky headers appear once. Nested scrollers and long pages are handled. +- Annotate, blur, crop, and PDF are free. + +MORE (when you need it) +- Visible area and selected-region capture from the right-click menu or shortcuts. +- Tab recording with auto-zoom, trim, and WebM export. Optional, local, no account. +- Keyboard: Ctrl or Cmd Shift S full page. Other shortcuts stay as they are today. + +OpenScreenShot is free and open source. +Website: https://openscreenshot.app +Source: https://github.com/pghqdev/OpenScreenShot +Privacy: https://openscreenshot.app/privacy/ +``` + +## Gallery slides + +Upload the five billboards from `store/screenshots/`, in this order: + +1. `slide-1.png` — THE WHOLE PAGE. ONE CLICK. +2. `slide-2.png` — SCROLLS. STITCHES. DONE. +3. `slide-3.png` — ANNOTATE. NO PAYWALL. +4. `slide-4.png` — WI-FI OFF. STILL WORKS. +5. `slide-5.png` — MIT. READ THE SOURCE. + +Permission justifications, localized descriptions, and the rest of the +dashboard walkthrough stay in `agent_docs/store-listing.md` (local, untracked). diff --git a/store/screenshots/slide-1.png b/store/screenshots/slide-1.png new file mode 100644 index 0000000..9ea6a8c Binary files /dev/null and b/store/screenshots/slide-1.png differ diff --git a/store/screenshots/slide-2.png b/store/screenshots/slide-2.png new file mode 100644 index 0000000..157d8e8 Binary files /dev/null and b/store/screenshots/slide-2.png differ diff --git a/store/screenshots/slide-3.png b/store/screenshots/slide-3.png new file mode 100644 index 0000000..e525d2e Binary files /dev/null and b/store/screenshots/slide-3.png differ diff --git a/store/screenshots/slide-4.png b/store/screenshots/slide-4.png new file mode 100644 index 0000000..a378325 Binary files /dev/null and b/store/screenshots/slide-4.png differ diff --git a/store/screenshots/slide-5.png b/store/screenshots/slide-5.png new file mode 100644 index 0000000..0b313a8 Binary files /dev/null and b/store/screenshots/slide-5.png differ diff --git a/tests/unit/context-menus-race.test.ts b/tests/unit/context-menus-race.test.ts index d1942e6..8d0881c 100644 --- a/tests/unit/context-menus-race.test.ts +++ b/tests/unit/context-menus-race.test.ts @@ -16,7 +16,18 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; */ /** Every id `createContextMenus()` creates, in no particular order. */ -const ALL_MENU_IDS = ['oss-parent', 'oss-full-page', 'oss-visible', 'oss-region', 'oss-express']; +const ALL_MENU_IDS = [ + 'oss-parent', + 'oss-full-page', + 'oss-visible', + 'oss-region', + 'oss-icon-full-page', + 'oss-icon-visible', + 'oss-icon-region', + 'oss-settings', + 'oss-icon-settings', + 'oss-express', +]; /** Ids actually handed to `chrome.contextMenus.create` across both runs. */ let createdIds: string[]; @@ -37,6 +48,8 @@ function makeFakeChrome() { onMessage: { addListener: vi.fn() }, getURL: vi.fn((path: string) => `chrome-extension://fake/${path}`), sendMessage: vi.fn(() => Promise.resolve()), + setUninstallURL: vi.fn(() => Promise.resolve()), + getManifest: vi.fn(() => ({ version: '1.6.0' })), lastError: undefined as { message: string } | undefined, }, action: { @@ -98,7 +111,7 @@ function makeFakeChrome() { // parked failure removes the key, and the badge has to follow. session: { onChanged: { addListener: vi.fn() } }, }, - i18n: { getMessage: vi.fn((key: string) => key) }, + i18n: { getMessage: vi.fn((key: string) => key), getUILanguage: vi.fn(() => 'en') }, windows: { WINDOW_ID_CURRENT: -2 }, downloads: { download: vi.fn(() => Promise.resolve(1)) }, scripting: { executeScript: vi.fn(() => Promise.resolve([{ result: undefined }])) }, @@ -111,6 +124,19 @@ async function flushMicrotasks(times = 10): Promise { for (let i = 0; i < times; i++) await Promise.resolve(); } +/** + * Release storage reads until no new one appears: a released read can queue + * another (the express migration reads, writes, then the menu build reads + * settings), so a single drain pass is not enough. + */ +async function releaseAllReads(rounds = 10): Promise { + for (let i = 0; i < rounds; i++) { + await flushMicrotasks(); + while (getResolvers.length > 0) getResolvers.shift()?.({}); + } + await flushMicrotasks(); +} + let fakeChrome: ReturnType; describe('createContextMenus concurrency', () => { @@ -137,9 +163,7 @@ describe('createContextMenus concurrency', () => { // Release every pending settings read (however many runs actually made // one — a fixed, single-flight implementation makes only one) and let // both calls run to completion. - while (getResolvers.length > 0) getResolvers.shift()?.({}); - await flushMicrotasks(); - while (getResolvers.length > 0) getResolvers.shift()?.({}); + await releaseAllReads(); await Promise.all([runA, runB]); const counts = Object.fromEntries( @@ -150,12 +174,11 @@ describe('createContextMenus concurrency', () => { }); /** - * Install opens nothing. The whole "the setup page is reached only from a - * failure" deliverable is one hunk in `src/background/index.ts`, and it is - * invisible to every other check here: reverting it leaves lint, types and - * every other test green. This is the one assertion that fails when it comes - * back, so it reads the listener the module actually registered and fires it - * rather than trusting the source. + * A fresh install opens exactly one tab: the welcome page (rating funnel + * Surface A). An update opens nothing — the update-time welcome is the + * pattern users report extensions for. The setup page stays reachable only + * from a failure, as before. These read the listener the module actually + * registered and fire it rather than trusting the source. */ describe('onInstalled', () => { beforeEach(() => { @@ -168,7 +191,7 @@ describe('onInstalled', () => { vi.unstubAllGlobals(); }); - it('opens no tab on a fresh install', async () => { + async function fireOnInstalled(reason: string): Promise { await import('../../src/background/index.ts'); const listener = fakeChrome.runtime.onInstalled.addListener.mock.calls[0]?.[0] as (details: { reason: string; @@ -176,20 +199,25 @@ describe('onInstalled', () => { expect(listener, 'background/index.ts registers an onInstalled listener').toBeTypeOf( 'function', ); - - listener({ reason: 'install' }); - // Let the menu build get as far as its settings read, then release it, so - // a tab created after that await would still be caught below. - await flushMicrotasks(); - while (getResolvers.length > 0) getResolvers.shift()?.({}); - await flushMicrotasks(); - - expect( - fakeChrome.tabs.create.mock.calls.map(([opts]) => (opts as { url: string }).url), - 'a fresh install must not open the setup page (or any other tab)', - ).toEqual([]); + listener({ reason }); + // Let the migration and the menu build run every read they queue, so a + // tab created after any of those awaits would still be caught below. + await releaseAllReads(); + } + + const openedUrls = () => + fakeChrome.tabs.create.mock.calls.map(([opts]) => (opts as { url: string }).url); + + it('opens exactly the welcome page on a fresh install', async () => { + await fireOnInstalled('install'); + expect(openedUrls()).toEqual(['chrome-extension://fake/src/welcome/index.html']); // The install still has to build the menus — the assertion above must not // pass by the listener having stopped doing its real work. expect(new Set(createdIds)).toEqual(new Set(ALL_MENU_IDS)); }); + + it('opens no tab on an update', async () => { + await fireOnInstalled('update'); + expect(openedUrls()).toEqual([]); + }); }); diff --git a/tests/unit/express-default.test.ts b/tests/unit/express-default.test.ts new file mode 100644 index 0000000..1cdd598 --- /dev/null +++ b/tests/unit/express-default.test.ts @@ -0,0 +1,117 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { DEFAULT_SETTINGS } from '../../src/shared/types'; + +/** + * Express mode became the default in 1.6.0: a toolbar click captures the + * full page unless the user turns it off. Stored settings persist the whole + * object, so every install that ever wrote settings holds + * `expressMode: false` even when the user never touched the toggle — + * `migrateExpressDefault` flips those once and never again. These tests pin + * the default, the one-shot flip, and the note handoff to the editor. + */ + +const SETTINGS_KEY = 'openscreenshot:settings'; +const MIGRATED_KEY = 'openscreenshot:express-default-migrated'; +const NOTE_KEY = 'openscreenshot:express-note-pending'; + +/** In-memory `chrome.storage.local` accepting the string-or-array `get` + * shapes the migration and note reads actually use. */ +function makeStorageStub() { + const store = new Map(); + return { + store, + local: { + get: vi.fn(async (keys: string | string[]) => { + const list = Array.isArray(keys) ? keys : [keys]; + const out: Record = {}; + for (const k of list) if (store.has(k)) out[k] = store.get(k); + return out; + }), + set: vi.fn(async (items: Record) => { + for (const [k, v] of Object.entries(items)) store.set(k, v); + }), + remove: vi.fn(async (key: string) => void store.delete(key)), + }, + }; +} + +let store: Map; + +beforeEach(() => { + const stub = makeStorageStub(); + store = stub.store; + vi.stubGlobal('chrome', { storage: { local: stub.local } }); +}); + +afterEach(() => { + vi.unstubAllGlobals(); + vi.resetModules(); +}); + +async function storage() { + return import('../../src/shared/storage'); +} + +describe('expressMode default', () => { + it('is on for a fresh install', () => { + expect(DEFAULT_SETTINGS.expressMode).toBe(true); + }); +}); + +describe('migrateExpressDefault', () => { + it('fresh install: writes only the flag, leaving stored settings empty', async () => { + const { migrateExpressDefault } = await storage(); + await migrateExpressDefault('install'); + expect(store.get(MIGRATED_KEY)).toBe(true); + expect(store.has(SETTINGS_KEY)).toBe(false); + expect(store.has(NOTE_KEY)).toBe(false); + }); + + it('update with stored expressMode:false flips it on and queues the note', async () => { + store.set(SETTINGS_KEY, { expressMode: false, quality: 0.5 }); + const { migrateExpressDefault, getSettings } = await storage(); + await migrateExpressDefault('update'); + const settings = await getSettings(); + expect(settings.expressMode).toBe(true); + expect(settings.quality).toBe(0.5); // other stored values survive + expect(store.get(NOTE_KEY)).toBe(true); + }); + + it('update with no stored settings flips the default on and queues the note', async () => { + const { migrateExpressDefault, getSettings } = await storage(); + await migrateExpressDefault('update'); + expect((await getSettings()).expressMode).toBe(true); + expect(store.get(NOTE_KEY)).toBe(true); + }); + + it('update where the user already turned express on queues no note', async () => { + store.set(SETTINGS_KEY, { expressMode: true }); + const { migrateExpressDefault } = await storage(); + await migrateExpressDefault('update'); + expect(store.has(NOTE_KEY)).toBe(false); + }); + + it('runs once: a later update does not undo the user turning it back off', async () => { + store.set(SETTINGS_KEY, { expressMode: false }); + const { migrateExpressDefault, setSettings, getSettings } = await storage(); + await migrateExpressDefault('update'); + await setSettings({ expressMode: false }); // the user opts back out + await migrateExpressDefault('update'); // next extension update + expect((await getSettings()).expressMode).toBe(false); + }); +}); + +describe('takeExpressNote', () => { + it('is true exactly once after a migration that changed behavior', async () => { + store.set(SETTINGS_KEY, { expressMode: false }); + const { migrateExpressDefault, takeExpressNote } = await storage(); + await migrateExpressDefault('update'); + expect(await takeExpressNote()).toBe(true); + expect(await takeExpressNote()).toBe(false); + }); + + it('is false when nothing queued it', async () => { + const { takeExpressNote } = await storage(); + expect(await takeExpressNote()).toBe(false); + }); +}); diff --git a/tests/unit/express-toolbar.test.ts b/tests/unit/express-toolbar.test.ts new file mode 100644 index 0000000..b8955c9 --- /dev/null +++ b/tests/unit/express-toolbar.test.ts @@ -0,0 +1,166 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +/** + * The toolbar click's branch is the popup *binding*: express mode unbinds + * the popup so `action.onClicked` fires a full-page capture; express off + * rebinds the popup and the listener never fires. These tests read the + * bindings and listeners the background module actually registers, driving + * it with an immediate in-memory `chrome.storage.local` (unlike the race + * suite, nothing here needs reads held open). + */ + +const SETTINGS_KEY = 'openscreenshot:settings'; + +let store: Map; + +function makeFakeChrome() { + const fake = { + runtime: { + onInstalled: { addListener: vi.fn() }, + onStartup: { addListener: vi.fn() }, + onMessage: { addListener: vi.fn() }, + getURL: vi.fn((path: string) => `chrome-extension://fake/${path}`), + sendMessage: vi.fn(() => Promise.resolve()), + setUninstallURL: vi.fn(() => Promise.resolve()), + getManifest: vi.fn(() => ({ version: '1.6.0' })), + lastError: undefined as { message: string } | undefined, + }, + action: { + onClicked: { addListener: vi.fn() }, + setPopup: vi.fn(() => Promise.resolve()), + setBadgeBackgroundColor: vi.fn(() => Promise.resolve()), + setBadgeTextColor: vi.fn(() => Promise.resolve()), + setBadgeText: vi.fn(() => Promise.resolve()), + }, + contextMenus: { + removeAll: vi.fn(() => Promise.resolve()), + create: vi.fn((props: { id: string }, callback?: () => void) => { + callback?.(); + return props.id; + }), + update: vi.fn((_id: string, _props: unknown, callback?: () => void) => callback?.()), + onClicked: { addListener: vi.fn() }, + }, + commands: { onCommand: { addListener: vi.fn() } }, + permissions: { onAdded: { addListener: vi.fn() } }, + tabs: { + onUpdated: { addListener: vi.fn() }, + query: vi.fn(() => Promise.resolve([])), + create: vi.fn(() => Promise.resolve({})), + captureVisibleTab: vi.fn(() => Promise.resolve('data:image/png;base64,')), + }, + storage: { + local: { + get: vi.fn(async (keys: string | string[]) => { + const list = Array.isArray(keys) ? keys : [keys]; + const out: Record = {}; + for (const k of list) if (store.has(k)) out[k] = store.get(k); + return out; + }), + set: vi.fn(async (items: Record) => { + for (const [k, v] of Object.entries(items)) store.set(k, v); + }), + remove: vi.fn(async (key: string) => void store.delete(key)), + getBytesInUse: vi.fn(() => Promise.resolve(0)), + }, + onChanged: { addListener: vi.fn() }, + session: { onChanged: { addListener: vi.fn() } }, + }, + i18n: { getMessage: vi.fn((key: string) => key), getUILanguage: vi.fn(() => 'en') }, + windows: { WINDOW_ID_CURRENT: -2 }, + downloads: { download: vi.fn(() => Promise.resolve(1)) }, + scripting: { executeScript: vi.fn(() => Promise.resolve([{ result: undefined }])) }, + }; + return fake; +} + +async function flushMicrotasks(times = 20): Promise { + for (let i = 0; i < times; i++) await Promise.resolve(); +} + +let fakeChrome: ReturnType; + +async function importBackground() { + const mod = await import('../../src/background/index.ts'); + await flushMicrotasks(); + return mod; +} + +function lastPopupBinding(): string | undefined { + const calls = fakeChrome.action.setPopup.mock.calls; + return (calls[calls.length - 1]?.[0] as { popup: string } | undefined)?.popup; +} + +beforeEach(() => { + vi.resetModules(); + store = new Map(); + fakeChrome = makeFakeChrome(); + vi.stubGlobal('chrome', fakeChrome); +}); + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe('toolbar-click branching', () => { + it('express on (the default): unbinds the popup so the click captures', async () => { + await importBackground(); + expect(lastPopupBinding()).toBe(''); + }); + + it('express off: binds the popup, restoring the picker', async () => { + store.set(SETTINGS_KEY, { expressMode: false }); + await importBackground(); + expect(lastPopupBinding()).toBe('src/popup/index.html'); + }); + + it('an icon click starts a capture attempt against the active tab', async () => { + await importBackground(); + const listener = fakeChrome.action.onClicked.addListener.mock.calls[0]?.[0] as () => void; + expect(listener).toBeTypeOf('function'); + listener(); + await flushMicrotasks(); + expect(fakeChrome.tabs.query).toHaveBeenCalledWith({ active: true, currentWindow: true }); + // No tab in the fake: the attempt must surface as a capture error, which + // proves the click went down the capture path rather than doing nothing. + expect(fakeChrome.runtime.sendMessage).toHaveBeenCalledWith( + expect.objectContaining({ type: 'CAPTURE_ERROR' }), + ); + }); +}); + +describe('context-menu surfaces', () => { + function menuClick(menuItemId: string, checked?: boolean): void { + const listener = fakeChrome.contextMenus.onClicked.addListener.mock.calls[0]?.[0] as (info: { + menuItemId: string; + checked?: boolean; + }) => void; + listener({ menuItemId, checked }); + } + + it('the icon-menu mode items start a capture', async () => { + await importBackground(); + menuClick('oss-icon-visible'); + await flushMicrotasks(); + expect(fakeChrome.tabs.query).toHaveBeenCalledWith({ active: true, currentWindow: true }); + }); + + it('the settings items open the popup page on its settings pane', async () => { + await importBackground(); + for (const id of ['oss-settings', 'oss-icon-settings']) { + fakeChrome.tabs.create.mockClear(); + menuClick(id); + await flushMicrotasks(); + expect(fakeChrome.tabs.create).toHaveBeenCalledWith({ + url: 'chrome-extension://fake/src/popup/index.html?settings=1', + }); + } + }); + + it('the express checkbox persists its new state', async () => { + await importBackground(); + menuClick('oss-express', false); + await flushMicrotasks(); + expect((store.get(SETTINGS_KEY) as { expressMode?: boolean }).expressMode).toBe(false); + }); +}); diff --git a/tests/unit/locales.test.ts b/tests/unit/locales.test.ts index f754f2e..9cc8792 100644 --- a/tests/unit/locales.test.ts +++ b/tests/unit/locales.test.ts @@ -11,6 +11,39 @@ import { join } from 'node:path'; const ROOT = join(process.cwd(), 'public/_locales'); const LOCALES = ['de', 'es', 'fr', 'it', 'pt_BR', 'ja', 'ko', 'zh_CN', 'zh_TW', 'ru']; +/** + * Second tier (P0.6): locales that ship only the store listing and the + * first-session surfaces — Chrome falls back to en per key, so a partial + * catalog degrades to English instead of breaking. Each must carry at + * least MINIMUM_KEYS, and nothing that en does not have. + */ +const PARTIAL_LOCALES = ['ar', 'es_419', 'hi', 'id', 'nl', 'pl', 'tr', 'vi']; +const MINIMUM_KEYS = [ + 'extName', + 'extDesc', + 'modeFullPage', + 'modeVisible', + 'modeRegion', + 'repeatLastRegion', + 'settingsTitle', + 'expressLabel', + 'expressHint', + 'expressMigratedNote', + 'editorRateLabel', + 'editorRateTooltip', + 'ratePromptHeadline', + 'ratePromptBody', + 'ratePromptRate', + 'ratePromptLater', + 'errNoTab', + 'errProtectedPage', + 'errBlankPage', + 'errTooLarge', + 'errNoRegion', + 'errClipboard', + 'errSave', + 'errUnknown', +]; const TITLE_MAX = 75; const SUMMARY_MAX = 132; @@ -39,7 +72,7 @@ describe('locale catalogs', () => { const present = readdirSync(ROOT) .filter((d) => d !== 'en') .sort(); - expect(present).toEqual([...LOCALES].sort()); + expect(present).toEqual([...LOCALES, ...PARTIAL_LOCALES].sort()); }); for (const locale of LOCALES) { @@ -79,4 +112,48 @@ describe('locale catalogs', () => { }); }); } + + for (const locale of PARTIAL_LOCALES) { + describe(`${locale} (partial)`, () => { + let cache: Record | null = null; + const catalog = () => (cache ??= load(locale)); + + it('carries at least the minimum key set', () => { + const missing = MINIMUM_KEYS.filter((k) => !(k in catalog())); + expect(missing).toEqual([]); + }); + + it('has no key en does not have', () => { + const unknown = Object.keys(catalog()).filter((k) => !(k in en)); + expect(unknown).toEqual([]); + }); + + it('has no empty message', () => { + const empty = Object.entries(catalog()) + .filter(([, e]) => typeof e.message !== 'string' || e.message.trim() === '') + .map(([k]) => k); + expect(empty).toEqual([]); + }); + + it('declares the same placeholders as en, and references each one', () => { + const bad: string[] = []; + for (const [key, entry] of Object.entries(catalog())) { + const enEntry = en[key]; + if (!enEntry) continue; // caught by the unknown-key test above + const want = placeholderNames(enEntry); + const have = placeholderNames(entry); + if (want.join() !== have.join()) bad.push(`${key}: declared ${have} vs en ${want}`); + const refs = placeholderRefs(entry.message); + if (refs.join() !== want.join()) + bad.push(`${key}: message references ${refs} vs ${want}`); + } + expect(bad).toEqual([]); + }); + + it('keeps the store title and summary inside the CWS limits', () => { + expect(catalog().extName.message.length).toBeLessThanOrEqual(TITLE_MAX); + expect(catalog().extDesc.message.length).toBeLessThanOrEqual(SUMMARY_MAX); + }); + }); + } }); diff --git a/tests/unit/rating.test.ts b/tests/unit/rating.test.ts new file mode 100644 index 0000000..3feac7a --- /dev/null +++ b/tests/unit/rating.test.ts @@ -0,0 +1,68 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +/** + * The rating funnel's rules (brief, section 11): the one post-success + * prompt is due only after RATE_PROMPT_AFTER successes, never after any + * Rate surface was used, and never twice. + */ + +function makeStorageStub() { + const store = new Map(); + return { + store, + local: { + get: vi.fn(async (keys: string | string[]) => { + const list = Array.isArray(keys) ? keys : [keys]; + const out: Record = {}; + for (const k of list) if (store.has(k)) out[k] = store.get(k); + return out; + }), + set: vi.fn(async (items: Record) => { + for (const [k, v] of Object.entries(items)) store.set(k, v); + }), + remove: vi.fn(async (key: string) => void store.delete(key)), + }, + }; +} + +beforeEach(() => { + vi.stubGlobal('chrome', { storage: { local: makeStorageStub().local } }); +}); + +afterEach(() => { + vi.unstubAllGlobals(); + vi.resetModules(); +}); + +async function rating() { + return import('../../src/shared/rating'); +} + +describe('rate prompt gating', () => { + it('is not due before RATE_PROMPT_AFTER successes', async () => { + const { recordExportSuccess, shouldShowRatePrompt, RATE_PROMPT_AFTER } = await rating(); + for (let i = 0; i < RATE_PROMPT_AFTER - 1; i++) { + await recordExportSuccess(); + expect(await shouldShowRatePrompt()).toBe(false); + } + await recordExportSuccess(); + expect(await shouldShowRatePrompt()).toBe(true); + }); + + it('never comes back once shown, whatever the answer', async () => { + const { recordExportSuccess, shouldShowRatePrompt, markRatePromptShown, RATE_PROMPT_AFTER } = + await rating(); + for (let i = 0; i < RATE_PROMPT_AFTER; i++) await recordExportSuccess(); + await markRatePromptShown(); + await recordExportSuccess(); + expect(await shouldShowRatePrompt()).toBe(false); + }); + + it('never appears after a Rate surface was used', async () => { + const { recordExportSuccess, shouldShowRatePrompt, markRatedOrDismissed, RATE_PROMPT_AFTER } = + await rating(); + await markRatedOrDismissed(); // e.g. the Rate star in the editor header + for (let i = 0; i < RATE_PROMPT_AFTER + 2; i++) await recordExportSuccess(); + expect(await shouldShowRatePrompt()).toBe(false); + }); +}); diff --git a/tests/unit/utils.test.ts b/tests/unit/utils.test.ts index 5194e15..547884c 100644 --- a/tests/unit/utils.test.ts +++ b/tests/unit/utils.test.ts @@ -92,6 +92,15 @@ describe('isProtectedUrl', () => { it('treats missing urls as protected', () => { expect(isProtectedUrl(undefined)).toBe(true); }); + it("allows the extension's own pages but no other extension's", () => { + vi.stubGlobal('chrome', { runtime: { id: 'ownid' } }); + try { + expect(isProtectedUrl('chrome-extension://ownid/src/welcome/index.html')).toBe(false); + expect(isProtectedUrl('chrome-extension://otherid/page.html')).toBe(true); + } finally { + vi.unstubAllGlobals(); + } + }); }); describe('insertToken', () => { diff --git a/vite.config.ts b/vite.config.ts index 24b2fdd..68915e2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -46,6 +46,7 @@ export default defineConfig({ recorder: 'src/recorder/index.html', webcamFrame: 'src/recorder/webcam-frame.html', setup: 'src/setup/index.html', + welcome: 'src/welcome/index.html', }, }, },