diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2383d7d..233d25a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ permissions: jobs: ci: - name: Build + name: Compile, Test, Build runs-on: ubuntu-latest steps: @@ -37,5 +37,28 @@ jobs: - name: Prepare WXT types run: yarn exec wxt prepare + - name: Compile TypeScript + id: compile + continue-on-error: true + shell: bash + run: | + set -o pipefail + yarn compile --pretty false 2>&1 | tee compile.log + + - name: Upload TypeScript diagnostics + if: steps.compile.outcome == 'failure' + uses: actions/upload-artifact@v5 + with: + name: typescript-diagnostics + path: compile.log + if-no-files-found: error + + - name: Fail on TypeScript errors + if: steps.compile.outcome == 'failure' + run: exit 1 + + - name: Run tests + run: yarn test + - name: Build extension run: yarn build diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9d676fd..83abb01 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: "Submit to Web Store" +name: Submit to Web Store permissions: contents: write @@ -7,10 +7,6 @@ on: tags: - "[0-9]+.[0-9]+.[0-9]+" -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - jobs: build-and-release: name: Build and Release @@ -27,32 +23,30 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v7 - - - name: Enable Corepack - run: corepack enable + uses: actions/checkout@v4 - - name: Setup Node.js with Yarn cache - uses: actions/setup-node@v6 + - name: Setup Node.js + uses: actions/setup-node@v4 with: node-version: 24.x - cache: "yarn" + cache: yarn - name: Install dependencies - run: yarn install --immutable --mode=skip-build + run: yarn install --frozen-lockfile - name: Prepare WXT types - run: yarn exec wxt prepare + run: yarn wxt prepare + + - name: Run tests + run: yarn test - - name: Build the extension + - name: Build extension run: | yarn zip - yarn zip:firefox --mv3 - yarn zip -b edge - yarn zip -b opera + yarn zip:firefox - name: Verify build outputs - run: ls -l .output/*.zip + run: ls -lh .output/*.zip - name: Update CHANGELOG id: changelog @@ -69,19 +63,16 @@ jobs: file_pattern: CHANGELOG.md - name: Create GitHub Release - if: startsWith(github.ref, 'refs/tags/') - uses: softprops/action-gh-release@v3 + uses: softprops/action-gh-release@v2 with: - name: "v.${{ github.ref_name }}" - draft: true + name: "v${{ github.ref_name }}" + draft: false generate_release_notes: true - body_path: INSTALL.md files: .output/*.zip - name: Submit to stores - if: startsWith(github.ref, 'refs/tags/') run: | - yarn exec wxt submit \ + yarn wxt submit \ --chrome-zip .output/*-chrome.zip \ --firefox-zip .output/*-firefox.zip \ --firefox-sources-zip .output/*-sources.zip diff --git a/.gitignore b/.gitignore index 38ed388..2f737ed 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ lerna-debug.log* node_modules .output +web/dist/ +*.tsbuildinfo stats.html stats-*.json .wxt diff --git a/CHANGELOG.md b/CHANGELOG.md index abc09d7..6f25c55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,44 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- Added comprehensive JSDoc comments to all functions and components to meet documentation standards +- Fixed async/await anti-patterns by replacing `void` statements with proper `.catch()` error handlers +- Improved variable declarations to use `const` where appropriate +- Optimized JSX component nesting by extracting inline form input into a separate component +- Resolved all DeepSource code quality warnings (24 issues) +- Optimized alias filtering by normalizing search input once and using a set for favorites +- Added regression coverage for plus-addressed Gmail validation and trimmed searches +- Removed generated TypeScript build-info files from version control + +## [1.3.0] - 2026-07-13 + +### Added + +- Added website-aware alias suggestions based on the current hostname +- Added an email input helper with inline icons, suggestion popups, live previews, and explicit Use actions +- Added previous-alias navigation and an information panel explaining supported rules and local-only storage +- Added expanded statistics metrics and Russian and Turkish translations +- Added a product landing page with automated GitHub Pages deployment + +### Changed + +- Enhanced the context menu with dynamic, website-specific alias suggestions +- Updated the History tab to show aliases across websites and store history per email account +- Improved popup navigation, layout, styling, and alias selection behavior +- Reorganized the content script and colocated its email helper styles + +### Fixed + +- Preserved email input width and flex layout when injecting the helper icon +- Improved helper popup positioning and hover behavior to prevent accidental closing +- Hid the Tags statistics tab when there is not enough data for a useful chart +- Hardened content rendering against client-side cross-site scripting +- Resolved code quality, localization, and build workflow issues + ## [1.2.0] - 2026-07-03 ### Added diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f563cdd --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,200 @@ +# Gmail Alias Toolkit Development Guide + +## Code Standards + +### JSDoc Comments (Required) + +All functions, arrow functions, and exported components must have a JSDoc comment. This prevents DeepSource (`JS-D1001`) documentation warnings. + +**Rule:** Every function declaration, arrow function, and exported component needs at minimum a one-line JSDoc comment describing its purpose. + +**Format:** + +```typescript +/** Brief description of what this function does. */ +function myFunction() { + // ... +} +``` + +**Examples:** + +- `/** Renders a bar chart comparing alias creation counts for today, week, and month. */` +- `/** Loads active email account and fetches associated statistics from storage. */` +- `/** Handles storage changes and reloads stats if relevant keys change. */` + +**Why:** DeepSource flags missing documentation as `JS-D1001` errors. Adding JSDoc prevents build/review failures and maintains code clarity. + +--- + +## Project Structure + +- `entrypoints/` - Browser extension entry points +- `src/` - Shared components and utilities +- `lib/` - Core libraries (i18n, etc.) + +--- + +## Phase 1: Website Detection & Auto-suggestions + +**Implemented:** + +- Auto-detect active tab website (hostname normalization) +- Generate 5 alias suggestions per site +- Save website → alias mapping (local storage) +- Show "Previously used" alias in Statistics tab +- Display suggestions in Statistics tab with one-click copy +- Integrated into right-click context menu (3 suggestions) +- Auto-save mapping when suggestion is used + +**Key Files:** + +- `src/utils/hostnameNormalizer.ts` — URL → clean keyword (shopee, github, etc.) +- `src/services/websiteAliasService.ts` — Storage, retrieval, suggestion generation +- `entrypoints/background.ts` — Messaging + dynamic context menu population +- `entrypoints/popup/components/Statistics.tsx` — Website detection UI + +**Privacy Note:** + +Local-first: All mappings stored in browser.storage.local. No server or analytics. + +--- + +## Phase 2: Email Input Helper UI + +**Implemented:** + +- Detect email inputs on any webpage +- Inject blue icon next to each input (like Roboform) +- Popup on icon click showing: + - Previous alias for current website (highlighted in yellow) + - 5 suggestion buttons +- One-click fill + auto-save mapping +- Supports controlled inputs (React, Vue, Angular) +- Watch for dynamically added inputs (SPA support) +- Dark mode styling + +**Key Files:** + +- `entrypoints/content/index.ts` — Content script entry point +- `entrypoints/content/email-input-helper.ts` — Icon injection + popup logic +- `entrypoints/content/email-input-helper.css` — UI styling + +**Detection Logic:** + +- Looks for: `input[type="email"]`, `input[name*="email"]`, `input[placeholder*="email"]` +- Uses MutationObserver to catch dynamically added inputs +- Skips hidden inputs (offsetParent === null) + +--- + +## Testing & CI + +### Automated Code Quality Checks + +Code is checked by DeepSource for: + +- Documentation completeness (JS-D1001) — All functions and components require JSDoc +- Unused imports/variables (JS-0356) +- Array index keys (JS-0437) +- Anti-patterns: void statements, improper variable declarations +- Other code quality issues + +All issues must be resolved before merging to `main`. + +### Unit Testing with Vitest + +The project uses **Vitest** for unit testing. Tests are located in the `tests/` directory mirroring the source structure. + +**Test File Locations:** + +- `tests/services/` — Tests for service layer (websiteAliasService, etc.) +- `tests/utils/` — Tests for utility functions (hostnameNormalizer, inlineSiteSettings, etc.) +- `tests/popup/` — Tests for popup components and utilities +- `tests/content/` — Tests for content script helper functions + +**Running Tests:** + +```bash +npm test # Run all tests +npm test -- [file] # Run specific test file +npm test -- --ui # Run with UI dashboard +npm test -- --coverage # Generate coverage report +``` + +**Test Standards:** + +1. **Coverage Requirements:** + - Utility functions: 100% line coverage + - Service layer: 90%+ line coverage + - Components: 80%+ line coverage + +2. **Test Organization:** + - One test file per module + - Use `describe()` blocks to group related tests + - Test both success and error paths + - Mock external dependencies (browser APIs, storage) + +3. **Best Practices:** + - Use descriptive test names that explain what is being tested + - Test edge cases and boundary conditions + - Mock browser storage and extension APIs + - Arrange → Act → Assert pattern for test structure + - Use `beforeEach()` to reset state between tests + +**Example Test Structure:** + +```typescript +import { describe, expect, it, beforeEach, vi } from "vitest"; + +describe("functionName", () => { + beforeEach(() => { + // Reset state before each test + vi.clearAllMocks(); + }); + + it("should do something specific", () => { + // Arrange: Set up test data + const input = "test"; + + // Act: Call the function + const result = functionName(input); + + // Assert: Verify the result + expect(result).toBe("expected"); + }); +}); +``` + +### Test Coverage + +**Current Coverage Areas:** + +- ✅ Hostname normalization and URL parsing +- ✅ Website alias storage and retrieval +- ✅ Alias suggestion generation +- ✅ Email address normalization +- ✅ HTML escaping (XSS prevention) +- ✅ Inline site settings management +- ✅ Gmail tricks generation (dot tricks, plus tags, etc.) +- ✅ Custom alias creation +- ✅ Popup components (Button, Input, Toggle, etc.) + +**Add Tests For:** + +- New utility functions (100% line coverage) +- Service layer changes (test both happy path and error cases) +- Complex component logic (interaction testing) +- Browser API integrations (with proper mocking) + +### Continuous Integration + +Pull requests to `main` require: + +1. ✅ All tests passing +2. ✅ Code coverage maintained +3. ✅ DeepSource checks passing (zero issues) +4. ✅ TypeScript type checking strict mode +5. ✅ Prettier formatting compliance + +See [CONTRIBUTING.md](CONTRIBUTING.md#publishing-to-extension-stores) for publishing and store submission procedures. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3bcef89..c188e3c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -146,3 +146,73 @@ The repository includes GitHub Actions for: 4. Run `yarn compile` and `yarn test`. 5. Add screenshots or recordings for UI changes. 6. Open a pull request using the provided template. + +## Publishing to Extension Stores + +### Submission Workflow + +The extension submits to **Chrome Web Store** and **Firefox Add-ons**. + +Credentials are stored in `.env.submit` (excluded from git): + +```env +CHROME_EXTENSION_ID="..." +CHROME_CLIENT_ID="..." +CHROME_CLIENT_SECRET="..." +CHROME_REFRESH_TOKEN="..." +FIREFOX_EXTENSION_ID="..." +FIREFOX_JWT_ISSUER="..." +FIREFOX_JWT_SECRET="..." +``` + +### Pre-submission Checklist + +1. **Update version** in `package.json` following semantic versioning +2. **Run tests** — `yarn test` (all tests must pass) +3. **Run quality checks** — `yarn compile` + DeepSource review +4. **Update CHANGELOG.md** with release notes +5. **Verify credentials** — Ensure `.env.submit` has valid Chrome & Firefox credentials + +### Build and Submit + +**For Chrome Web Store:** + +```bash +yarn build +yarn zip + +# Load credentials and submit (dry-run to test) +source .env.submit +yarn wxt submit --dry-run --chrome-zip .output/gmail-alias-toolkit-{version}-chrome.zip + +# Production submit +yarn wxt submit \ + --chrome-zip .output/gmail-alias-toolkit-{version}-chrome.zip \ + --chrome-extension-id $CHROME_EXTENSION_ID \ + --chrome-client-id $CHROME_CLIENT_ID \ + --chrome-client-secret $CHROME_CLIENT_SECRET \ + --chrome-refresh-token $CHROME_REFRESH_TOKEN +``` + +**For Firefox Add-ons:** + +```bash +yarn build:firefox +yarn zip:firefox + +# Load credentials and submit +source .env.submit +yarn wxt submit \ + --firefox-zip .output/gmail-alias-toolkit-{version}-firefox.zip \ + --firefox-sources-zip .output/gmail-alias-toolkit-{version}-sources.zip \ + --firefox-extension-id $FIREFOX_EXTENSION_ID \ + --firefox-jwt-issuer $FIREFOX_JWT_ISSUER \ + --firefox-jwt-secret $FIREFOX_JWT_SECRET +``` + +### After Submission + +- Monitor store review queues for approval (Chrome: 1-2 hours, Firefox: 1-7 days) +- Update GitHub Release with store links once approved +- Monitor user reviews and ratings for feedback +- Keep dependencies updated between releases diff --git a/README.md b/README.md index 90ec139..6e7ad3b 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,13 @@ A lightweight browser extension that generates, manages, and organizes Gmail ali - Complete localization with automatic UI translation across all interfaces. +### Code Quality + +- Comprehensive JSDoc documentation for all functions and components +- Strict TypeScript type checking and anti-pattern detection +- Clean, maintainable code following industry best practices +- Automatic code formatting with Prettier + ## Privacy - Data is stored locally in the browser extension storage. @@ -66,6 +73,6 @@ MIT. See `LICENSE.md`. ## Version -Current version: `1.2.0` +Current version: `1.3.0` See `CHANGELOG.md` for release notes. diff --git a/entrypoints/background.ts b/entrypoints/background.ts index 44d2f1e..081ad31 100644 --- a/entrypoints/background.ts +++ b/entrypoints/background.ts @@ -118,6 +118,31 @@ export default defineBackground(() => { contexts: ["editable"], }); + // Website-specific submenu (populated on demand) + browser.contextMenus.create({ + id: "website-alias-parent", + parentId: "gmail-alias-parent", + title: t("menuForThisWebsite") || "For this website", + contexts: ["editable"], + }); + + // Placeholder for website suggestions (will be replaced dynamically) + browser.contextMenus.create({ + id: "website-loading", + parentId: "website-alias-parent", + title: t("menuLoading") || "Loading...", + contexts: ["editable"], + enabled: false, + }); + + // Separator + browser.contextMenus.create({ + id: "separator-1", + parentId: "gmail-alias-parent", + type: "separator", + contexts: ["editable"], + }); + // Random email submenu browser.contextMenus.create({ id: "fill-random-email", @@ -189,11 +214,97 @@ export default defineBackground(() => { contexts: ["editable"], }); } + + /** Generate random tag based on format. */ + function generateRandomTag(format = "private-mail"): string { + switch (format) { + case "private-mail": { + const chars = "abcdefghijklmnopqrstuvwxyz"; + return Array.from( + { length: 8 }, + () => chars[Math.floor(Math.random() * chars.length)], + ).join(""); + } + case "alphanumeric": { + const alphanum = "abcdefghijklmnopqrstuvwxyz0123456789"; + return Array.from( + { length: 10 }, + () => alphanum[Math.floor(Math.random() * alphanum.length)], + ).join(""); + } + case "words": { + const words = [ + "alpha", + "beta", + "gamma", + "delta", + "echo", + "foxtrot", + "golf", + "hotel", + ]; + const word1 = words[Math.floor(Math.random() * words.length)]; + const word2 = words[Math.floor(Math.random() * words.length)]; + const num = Math.floor(Math.random() * 100); + return `${word1}${word2}${num}`; + } + default: + return Date.now().toString(); + } + } + + /** Handle website suggestion menu item. */ + async function handleWebsiteSuggestion(menuItemId: string): Promise { + const suggestionIndex = parseInt( + String(menuItemId).replace("website-suggestion-", ""), + ); + const cacheResult = (await browser.storage.session?.get?.( + "contextMenuWebsiteSuggestions", + )) as + | { + contextMenuWebsiteSuggestions?: string[] | undefined; + } + | undefined; + const suggestions = (cacheResult?.contextMenuWebsiteSuggestions || + []) as string[]; + return suggestions[suggestionIndex] || ""; + } + + /** Generate email based on menu item. */ + function generateEmail( + menuItemId: string, + username: string, + domain: string, + format = "private-mail", + ): string { + if (menuItemId === "fill-random-email") { + const randomTag = generateRandomTag(format); + return `${username}+${randomTag}@${domain}`; + } + if (String(menuItemId).startsWith("tag-")) { + const tag = String(menuItemId).replace("tag-", ""); + return `${username}+${tag}@${domain}`; + } + if (menuItemId === "trick-dot") { + const pos = Math.floor(Math.random() * (username.length - 1)) + 1; + const dottedUsername = `${username.slice(0, pos)}.${username.slice(pos)}`; + return `${dottedUsername}@${domain}`; + } + if (menuItemId === "trick-googlemail") { + const altDomain = domain === "gmail.com" ? "googlemail.com" : "gmail.com"; + return `${username}@${altDomain}`; + } + if (menuItemId === "trick-nodots") { + const noDots = username.replace(/\./g, ""); + return `${noDots}@${domain}`; + } + return ""; + } + // Handle context menu clicks browser.contextMenus.onClicked.addListener(async (info, tab) => { if (!tab?.id) return; - // Get base email from storage const result = (await browser.storage.local.get([ "email_accounts", "base_email", @@ -217,78 +328,38 @@ export default defineBackground(() => { const [username, domain] = baseEmail.split("@"); let emailToFill = ""; - if (info.menuItemId === "fill-random-email") { - // Generate random email + if (String(info.menuItemId).startsWith("website-suggestion-")) { + emailToFill = await handleWebsiteSuggestion(String(info.menuItemId)); + } else { const format = result.app_settings?.randomFormat || "private-mail"; - let randomTag = ""; - - switch (format) { - case "private-mail": { - const chars = "abcdefghijklmnopqrstuvwxyz"; - randomTag = Array.from( - { length: 8 }, - () => chars[Math.floor(Math.random() * chars.length)], - ).join(""); - break; - } - case "alphanumeric": { - const alphanum = "abcdefghijklmnopqrstuvwxyz0123456789"; - randomTag = Array.from( - { length: 10 }, - () => alphanum[Math.floor(Math.random() * alphanum.length)], - ).join(""); - break; - } - case "words": { - const words = [ - "alpha", - "beta", - "gamma", - "delta", - "echo", - "foxtrot", - "golf", - "hotel", - ]; - const word1 = words[Math.floor(Math.random() * words.length)]; - const word2 = words[Math.floor(Math.random() * words.length)]; - const num = Math.floor(Math.random() * 100); - randomTag = `${word1}${word2}${num}`; - break; - } - case "timestamp": - randomTag = Date.now().toString(); - break; - default: - randomTag = Date.now().toString(); - break; - } - - emailToFill = `${username}+${randomTag}@${domain}`; - } else if (String(info.menuItemId).startsWith("tag-")) { - // Custom tag from preset - const tag = String(info.menuItemId).replace("tag-", ""); - emailToFill = `${username}+${tag}@${domain}`; - } else if (info.menuItemId === "trick-dot") { - // Dot variation - insert dot at random position - const pos = Math.floor(Math.random() * (username.length - 1)) + 1; - const dottedUsername = `${username.slice(0, pos)}.${username.slice(pos)}`; - emailToFill = `${dottedUsername}@${domain}`; - } else if (info.menuItemId === "trick-googlemail") { - // Googlemail domain - const altDomain = domain === "gmail.com" ? "googlemail.com" : "gmail.com"; - emailToFill = `${username}@${altDomain}`; - } else if (info.menuItemId === "trick-nodots") { - // Remove all dots - const noDots = username.replace(/\./g, ""); - emailToFill = `${noDots}@${domain}`; + emailToFill = generateEmail( + String(info.menuItemId), + username, + domain, + format, + ); } if (emailToFill) { - // Save to history and statistics await saveToHistory(emailToFill, result.app_settings?.maxHistory || 20); - // Send message to content script to fill the input + if (String(info.menuItemId).startsWith("website-suggestion-")) { + try { + const { normalizeHostname } = + await import("../src/utils/hostnameNormalizer"); + const { saveWebsiteAlias } = + await import("../src/services/websiteAliasService"); + if (tab.url) { + const normalized = normalizeHostname(tab.url); + if (normalized) { + await saveWebsiteAlias(baseEmail, normalized, emailToFill); + } + } + } catch (error) { + console.debug("Error saving website alias:", error); + } + } + browser.tabs.sendMessage(tab.id, { action: "fillEmail", email: emailToFill, @@ -392,15 +463,32 @@ export default defineBackground(() => { } // Helper function to save email to history and stats - async function saveToHistory(email: string, maxRecent: number) { + async function saveToHistory( + email: string, + maxRecent: number, + accountEmail?: string, + ) { + await saveAliasesToHistory([email], maxRecent, accountEmail); + } + + /** Atomically saves a generated batch to history and statistics. */ + async function saveAliasesToHistory( + emails: string[], + maxRecent: number, + accountEmail?: string, + ) { + const uniqueEmails = [...new Set(emails.filter(Boolean))]; + if (uniqueEmails.length === 0) return; + // Get active account const accountResult = (await browser.storage.local.get([ "email_accounts", "base_email", ])) as { email_accounts?: EmailAccount[]; base_email?: string }; - let activeEmail = "your.email@gmail.com"; + let activeEmail = accountEmail?.trim() || "your.email@gmail.com"; if ( + !accountEmail && accountResult.email_accounts && Array.isArray(accountResult.email_accounts) ) { @@ -410,7 +498,7 @@ export default defineBackground(() => { if (activeAccount) { activeEmail = activeAccount.email; } - } else if (accountResult.base_email) { + } else if (!accountEmail && accountResult.base_email) { activeEmail = accountResult.base_email; } @@ -425,15 +513,16 @@ export default defineBackground(() => { ])) as Record; const recentAliases = (result[historyKey] as Alias[]) || []; - // Add to history (remove duplicates, add to top) - const newAlias: Alias = { + // Add the batch to history (remove duplicates, newest first). + const now = Date.now(); + const newAliases: Alias[] = uniqueEmails.map((email, index) => ({ email, - timestamp: Date.now(), - }; - + timestamp: now - index, + })); + const newEmailSet = new Set(uniqueEmails); const updated = [ - newAlias, - ...recentAliases.filter((a) => a.email !== email), + ...newAliases, + ...recentAliases.filter((alias) => !newEmailSet.has(alias.email)), ].slice(0, maxRecent); // Update statistics @@ -441,15 +530,16 @@ export default defineBackground(() => { total: 0, tags: {}, }; - stats.total = (stats.total || 0) + 1; - - // Extract tag from email (if it has + addressing) - const tagMatch = email.match(/\+([^@]+)@/); - if (tagMatch) { - const tag = tagMatch[1]; - stats.tags = stats.tags || {}; - stats.tags[tag] = (stats.tags[tag] || 0) + 1; - } + stats.total = (stats.total || 0) + uniqueEmails.length; + + stats.tags = stats.tags || {}; + uniqueEmails.forEach((email) => { + const tagMatch = email.match(/\+([^@]+)@/); + if (tagMatch) { + const tag = tagMatch[1]; + stats.tags[tag] = (stats.tags[tag] || 0) + 1; + } + }); // Save to storage with account-specific keys await browser.storage.local.set({ @@ -460,4 +550,138 @@ export default defineBackground(() => { // Update badge await updateBadge(); } + + // Update website suggestions when context menu is shown + const dynamicContextMenus = + browser.contextMenus as typeof browser.contextMenus & { + onShown?: { + addListener( + listener: (info: { pageUrl?: string }) => void | Promise, + ): void; + }; + refresh?: () => void | Promise; + }; + + // Update website suggestions when the context menu is shown. + dynamicContextMenus.onShown?.addListener(async (info) => { + try { + if (!info.pageUrl) return; + + // Import services inline to avoid circular dependencies + const { normalizeHostname } = + await import("../src/utils/hostnameNormalizer"); + const { generateSuggestionsForWebsite } = + await import("../src/services/websiteAliasService"); + + const normalized = normalizeHostname(info.pageUrl); + if (!normalized) return; + + // Get active email + const accountResult = (await browser.storage.local.get([ + "email_accounts", + "base_email", + ])) as { email_accounts?: EmailAccount[]; base_email?: string }; + let activeEmail = "your.email@gmail.com"; + + if ( + accountResult.email_accounts && + Array.isArray(accountResult.email_accounts) + ) { + const activeAccount = accountResult.email_accounts.find( + (acc) => acc.isActive, + ); + if (activeAccount) { + activeEmail = activeAccount.email; + } + } else if (accountResult.base_email) { + activeEmail = accountResult.base_email; + } + + // Generate suggestions + const suggestions = await generateSuggestionsForWebsite( + activeEmail, + info.pageUrl, + ); + if (suggestions.length === 0) return; + + // Store suggestions in session storage for retrieval in click handler + if (browser.storage.session) { + await browser.storage.session.set({ + contextMenuWebsiteSuggestions: suggestions, + }); + } + // Rebuild dynamic items safely. Repeated menu openings otherwise + // reuse the same IDs and leave stale suggestions behind. + const dynamicItemIds = [ + "website-loading", + "website-suggestion-0", + "website-suggestion-1", + "website-suggestion-2", + ]; + await Promise.all( + dynamicItemIds.map(async (id) => { + try { + await browser.contextMenus.remove(id); + } catch { + // The item may not exist on the first or a later menu opening. + } + }), + ); + + for (const [index, suggestion] of suggestions.slice(0, 3).entries()) { + browser.contextMenus.create({ + id: `website-suggestion-${index}`, + parentId: "website-alias-parent", + title: suggestion.split("@")[0], + contexts: ["editable"], + }); + } + + await dynamicContextMenus.refresh?.(); + } catch (error) { + console.debug("Error updating website suggestions:", error); + } + }); + + // Handle messages from popup/content script + browser.runtime.onMessage.addListener( + async (message, sender, sendResponse) => { + try { + if (message.action === "getActiveTabUrl") { + const tabs = await browser.tabs.query({ + active: true, + currentWindow: true, + }); + const tab = tabs[0]; + sendResponse({ + url: tab?.url, + title: tab?.title, + }); + } else if (message.action === "saveWebsiteAlias") { + // Import service inline to avoid circular dependencies + const { saveWebsiteAlias } = + await import("../src/services/websiteAliasService"); + await saveWebsiteAlias( + message.email, + message.normalizedHostname, + message.alias, + ); + sendResponse({ success: true }); + } else if (message.action === "saveAliasToHistory") { + const settingsResult = (await browser.storage.local.get( + "app_settings", + )) as { app_settings?: AppSettings }; + await saveToHistory( + message.alias, + settingsResult.app_settings?.maxHistory || 20, + message.accountEmail, + ); + sendResponse({ success: true }); + } + } catch (error) { + console.error("Message handler error:", error); + sendResponse({ error: String(error) }); + } + }, + ); }); diff --git a/entrypoints/content.ts b/entrypoints/content.ts deleted file mode 100644 index c6dc11b..0000000 --- a/entrypoints/content.ts +++ /dev/null @@ -1,45 +0,0 @@ -export default defineContentScript({ - matches: [""], - main() { - // Listen for messages from background script - browser.runtime.onMessage.addListener((message) => { - if (message.action === "fillEmail" && message.email) { - // Get the active element (the input field that was right-clicked) - const activeElement = document.activeElement as HTMLElement | null; - - if ( - activeElement && - (activeElement.tagName === "INPUT" || - activeElement.tagName === "TEXTAREA" || - activeElement.isContentEditable) - ) { - if ( - activeElement instanceof HTMLInputElement || - activeElement instanceof HTMLTextAreaElement - ) { - // Fill input or textarea - activeElement.value = message.email; - - // Trigger input event for frameworks like React/Vue - activeElement.dispatchEvent(new Event("input", { bubbles: true })); - activeElement.dispatchEvent(new Event("change", { bubbles: true })); - } else if (activeElement.isContentEditable) { - // Fill contentEditable element - activeElement.textContent = message.email; - - // Trigger input event - activeElement.dispatchEvent(new Event("input", { bubbles: true })); - } - - // Flash effect to show it was filled - const originalBg = (activeElement as HTMLElement).style - .backgroundColor; - (activeElement as HTMLElement).style.backgroundColor = "#d1fae5"; - setTimeout(() => { - (activeElement as HTMLElement).style.backgroundColor = originalBg; - }, 500); - } - } - }); - }, -}); diff --git a/entrypoints/content/email-helper.css b/entrypoints/content/email-helper.css new file mode 100644 index 0000000..d47c244 --- /dev/null +++ b/entrypoints/content/email-helper.css @@ -0,0 +1,588 @@ +/* Email input helper icon and popup styles - Tailwind+Custom */ + +/* Icon wrapper styling */ +.gmail-alias-input-icon-container { + overflow: visible; + transition: + box-shadow 160ms ease, + transform 160ms ease; +} + +.gmail-alias-input-icon-container::before, +.gmail-alias-input-icon-container::after { + position: absolute; + left: 50%; + width: 0; + height: 0; + border-right: 6px solid transparent; + border-left: 6px solid transparent; + content: ""; + transform: translateX(-50%); +} + +.gmail-alias-input-icon-container::before { + bottom: -7px; + border-top: 7px solid #d1d5db; +} + +.gmail-alias-input-icon-container::after { + bottom: -5px; + border-right-width: 5px; + border-left-width: 5px; + border-top: 6px solid #fff; +} + +.gmail-alias-input-icon-container.gmail-alias-icon-below::before { + top: -7px; + bottom: auto; + border-top: 0; + border-bottom: 7px solid #d1d5db; +} + +.gmail-alias-input-icon-container.gmail-alias-icon-below::after { + top: -5px; + bottom: auto; + border-top: 0; + border-bottom: 6px solid #fff; +} + +.gmail-alias-input-icon-container.gmail-alias-icon-left::before, +.gmail-alias-input-icon-container.gmail-alias-icon-left::after, +.gmail-alias-input-icon-container.gmail-alias-icon-right::before, +.gmail-alias-input-icon-container.gmail-alias-icon-right::after { + top: 50%; + bottom: auto; + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + transform: translateY(-50%); +} + +.gmail-alias-input-icon-container.gmail-alias-icon-left::before { + right: -7px; + left: auto; + border-right: 0; + border-left: 7px solid #d1d5db; +} + +.gmail-alias-input-icon-container.gmail-alias-icon-left::after { + right: -5px; + left: auto; + border-right: 0; + border-left: 6px solid #fff; +} + +.gmail-alias-input-icon-container.gmail-alias-icon-right::before { + right: auto; + left: -7px; + border-right: 7px solid #d1d5db; + border-left: 0; +} + +.gmail-alias-input-icon-container.gmail-alias-icon-right::after { + right: auto; + left: -5px; + border-right: 6px solid #fff; + border-left: 0; +} + +.gmail-alias-input-icon-container:hover { + box-shadow: + 0 5px 14px rgba(15, 23, 42, 0.2), + 0 2px 4px rgba(15, 23, 42, 0.12) !important; + transform: translateY(-1px); +} + +.gmail-alias-input-icon { + color: #1ad2a4; +} + +.gmail-alias-input-icon:hover { + transform: scale(1.1); + box-shadow: 0 2px 8px rgba(26, 210, 164, 0.25); +} + +.gmail-alias-input-icon:active { + transform: scale(0.95); +} + +/* Input highlight effect */ +input.gmail-alias-input-highlight { + background-color: #ecfdf5 !important; + border-color: #10b981 !important; + color: #064e3b !important; + -webkit-text-fill-color: #064e3b !important; + box-shadow: inset 0 0 0 1px #10b981 !important; +} + +/* Input preview effect */ +input.gmail-alias-input-preview { + background-color: #d1fae5 !important; + border-color: #10b981 !important; + color: #064e3b !important; + -webkit-text-fill-color: #064e3b !important; + box-shadow: inset 0 0 0 1px #10b981 !important; +} + +input.gmail-alias-input-highlight::placeholder, +input.gmail-alias-input-preview::placeholder { + color: #6b7280 !important; + -webkit-text-fill-color: #6b7280 !important; + opacity: 1 !important; +} + +/* Popup base styles */ +.gmail-alias-popup { + display: flex; + flex-direction: column; + overflow: hidden; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; +} + +/* Tab styling */ +.gmail-alias-popup-tab:hover { + background: #f0f0f0; +} + +.gmail-alias-popup-tab.active { + color: #3b82f6; + border-bottom-color: #3b82f6 !important; +} + +/* Tab content visibility */ +.gmail-alias-popup-tab-content { + display: none; +} + +.gmail-alias-popup-tab-content.active { + display: block; +} + +/* Previously used alias styling */ +.gmail-alias-prev-alias { + background: #fef3c7; + border-color: #fcd34d; + color: #92400e; +} + +.gmail-alias-prev-alias:hover { + background: #fce7a8; + border-color: #fbbf24; +} + +/* Suggestion item styling */ +.gmail-alias-suggestion-item { + background: #f0f9ff; + border-color: #bfdbfe; + color: #1e40af; +} + +.gmail-alias-suggestion-item:hover { + background: #e0f2fe; + border-color: #7dd3fc; +} + +/* History item styling */ +.gmail-alias-history-item { + background: #f0f9ff; + border-color: #bfdbfe; + color: #1e40af; +} + +.gmail-alias-history-item:hover { + background: #e0f2fe; + border-color: #7dd3fc; +} + +.gmail-alias-popup-header, +.gmail-alias-popup-tabs, +.gmail-alias-popup-footer { + flex: none; +} + +.gmail-alias-popup-content { + flex: 1 1 auto; + min-height: 0; +} + +.gmail-alias-popup-footer { + position: sticky; + z-index: 1; + bottom: 0; + display: flex; + min-height: 34px; + align-items: center; + gap: 9px; + padding: 0 10px; + border-top: 1px solid #e5e7eb; + background: #f9fafb; + box-shadow: 0 -2px 8px rgba(15, 23, 42, 0.04); +} + +.gmail-alias-popup-home { + display: flex; + width: 22px; + height: 22px; + align-items: center; + justify-content: center; + border-radius: 5px; + color: #6b7280; + text-decoration: none; +} + +.gmail-alias-popup-home:hover { + background: #e5e7eb; + color: #1f2937; +} + +.gmail-alias-popup-footer-divider { + width: 1px; + height: 20px; + background: #d1d5db; +} + +.gmail-alias-popup-review { + flex: 1; + color: #6b7280; + font-size: 11px; + text-align: center; + text-decoration: underline; + text-underline-offset: 2px; +} + +.gmail-alias-popup-review:hover { + color: #2563eb; +} + +.gmail-alias-popup-disable-site { + position: relative; + display: flex; + width: 28px; + height: 28px; + align-items: center; + justify-content: center; + padding: 0; + border: 1px solid #fecaca; + border-radius: 7px; + background: #fef2f2; + color: #dc2626; + cursor: pointer; +} + +.gmail-alias-popup-disable-site:hover { + background: #fee2e2; + border-color: #fca5a5; + color: #b91c1c; +} + +.gmail-alias-popup-disable-site::after { + position: absolute; + z-index: 2; + top: calc(100% + 7px); + right: 0; + width: max-content; + max-width: 220px; + padding: 6px 8px; + border-radius: 6px; + background: #111827; + color: #f9fafb; + box-shadow: 0 4px 12px rgba(15, 23, 42, 0.22); + content: attr(data-tooltip); + font-size: 11px; + font-weight: 500; + line-height: 1.3; + opacity: 0; + pointer-events: none; + text-align: left; + transform: translateY(-2px); + transition: + opacity 120ms ease, + transform 120ms ease; + white-space: normal; +} + +.gmail-alias-popup-disable-site:hover::after, +.gmail-alias-popup-disable-site:focus-visible::after { + opacity: 1; + transform: translateY(0); +} + +.gmail-alias-popup-header-actions .gmail-alias-popup-close { + border-left: 1px solid #e5e7eb !important; + border-radius: 0 !important; + padding-left: 6px !important; + width: 30px !important; +} + +/* Compact quick-access generator and history controls */ +.gmail-alias-generate-mode { + flex: 1; + padding: 7px 8px; + border: 1px solid #e5e7eb; + border-radius: 8px; + background: #fff; + color: #6b7280; + font-size: 11px; + font-weight: 600; + cursor: pointer; +} + +.gmail-alias-generate-mode.active { + border-color: #93c5fd; + background: #eff6ff; + color: #2563eb; +} + +.gmail-alias-generate-panel { + display: none; +} + +.gmail-alias-generate-panel.active { + display: block; +} + +.gmail-alias-generate-panel input, +.gmail-alias-generate-panel select, +.gmail-alias-history-search, +.gmail-alias-history-filters select { + width: 100%; + min-width: 0; + box-sizing: border-box; + padding: 8px 10px; + border: 1px solid #bfdbfe; + border-radius: 8px; + background: #fff; + color: #1f2937; + font-size: 12px; + outline: none; +} + +.gmail-alias-generate-btn, +.gmail-alias-quick-action { + border: 0; + border-radius: 8px; + background: #2563eb; + color: #fff; + font-size: 11px; + font-weight: 600; + cursor: pointer; +} + +.gmail-alias-generate-btn { + padding: 8px 14px; +} + +.gmail-alias-quick-action { + width: 100%; + margin-top: 8px; + padding: 9px 12px; +} + +.gmail-alias-generated-list, +.gmail-alias-trick-list, +.gmail-alias-preset-list { + display: flex; + flex-direction: column; + gap: 6px; + margin-top: 8px; +} + +.gmail-alias-preset-list { + flex-direction: row; + flex-wrap: wrap; +} + +.gmail-alias-quick-label { + width: 100%; + color: #6b7280; + font-size: 11px; + font-weight: 600; +} + +.gmail-alias-preset-item { + padding: 6px 10px; + border: 1px solid #bfdbfe; + border-radius: 999px; + background: #eff6ff; + color: #1d4ed8; + font-size: 11px; + cursor: pointer; +} + +.gmail-alias-quick-alias { + width: 100%; + padding: 8px 10px; + border: 1px solid #bfdbfe; + border-radius: 7px; + background: #f0f9ff; + color: #1e40af; + font-family: Monaco, "Courier New", monospace; + font-size: 11px; + text-align: left; + overflow-wrap: anywhere; + cursor: pointer; +} + +.gmail-alias-quick-alias:hover { + background: #dbeafe; +} + +.gmail-alias-history-section { + display: flex; + flex-direction: column; + gap: 8px; +} + +.gmail-alias-history-filters { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 6px; +} + +.gmail-alias-history-filters select { + padding: 7px 8px; +} + +.gmail-alias-history-pagination { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.gmail-alias-history-pagination[hidden] { + display: none; +} + +.gmail-alias-history-pagination button { + width: 30px; + height: 28px; + border: 1px solid #bfdbfe; + border-radius: 7px; + background: #fff; + color: #1d4ed8; + cursor: pointer; +} + +.gmail-alias-history-pagination button:disabled { + cursor: default; + opacity: 0.35; +} + +.gmail-alias-history-page-info { + min-width: 72px; + color: #6b7280; + font-size: 11px; + text-align: center; +} + +.gmail-alias-quick-empty { + width: 100%; + padding: 10px; + color: #9ca3af; + font-size: 11px; + text-align: center; +} + +/* Dark mode support */ +@media (prefers-color-scheme: dark) { + .gmail-alias-popup { + background: #1f2937 !important; + border-color: #374151 !important; + color: #f3f4f6 !important; + } + + .gmail-alias-popup-footer { + border-top-color: #374151; + background: #111827; + } + + .gmail-alias-popup-footer-divider { + background: #4b5563; + } + + .gmail-alias-popup-home, + .gmail-alias-popup-review { + color: #9ca3af; + } + + .gmail-alias-popup-home:hover { + background: #374151; + color: #f3f4f6; + } + + .gmail-alias-popup-tab { + color: #9ca3af; + } + + .gmail-alias-popup-tab:hover { + background: #374151; + color: #d1d5db; + } + + .gmail-alias-popup-tab.active { + color: #60a5fa; + border-bottom-color: #60a5fa; + } + + .gmail-alias-prev-alias { + background: #7c2d12; + border-color: #92400e; + color: #fecaca; + } + + .gmail-alias-prev-alias:hover { + background: #92400e; + border-color: #b45309; + } + + .gmail-alias-suggestion-item { + background: #0c2340; + border-color: #1e3a5f; + color: #93c5fd; + } + + .gmail-alias-suggestion-item:hover { + background: #1e3a5f; + border-color: #3b82f6; + } + + .gmail-alias-history-item { + background: #0c2340; + border-color: #1e3a5f; + color: #93c5fd; + } + + .gmail-alias-history-item:hover { + background: #1e3a5f; + border-color: #3b82f6; + } + + .gmail-alias-generate-mode, + .gmail-alias-generate-panel input, + .gmail-alias-generate-panel select, + .gmail-alias-history-search, + .gmail-alias-history-filters select, + .gmail-alias-history-pagination button { + border-color: #4b5563; + background: #111827; + color: #e5e7eb; + } + + .gmail-alias-generate-mode.active, + .gmail-alias-preset-item { + border-color: #1d4ed8; + background: #172554; + color: #93c5fd; + } + + .gmail-alias-quick-alias { + border-color: #1e3a5f; + background: #0c2340; + color: #93c5fd; + } + + .gmail-alias-quick-alias:hover { + background: #1e3a5f; + } +} diff --git a/entrypoints/content/index.ts b/entrypoints/content/index.ts new file mode 100644 index 0000000..f9ef2ac --- /dev/null +++ b/entrypoints/content/index.ts @@ -0,0 +1,1540 @@ +import { normalizeHostname } from "src/utils/hostnameNormalizer"; +import { + INLINE_DISABLED_SITES_KEY, + normalizeSiteHostname, + parseDisabledInlineSites, +} from "src/utils/inlineSiteSettings"; +import { t } from "../../lib/i18n"; +import { + getPreviousAliasForWebsite, + generateSuggestionsForWebsite, + saveWebsiteAlias, +} from "src/services/websiteAliasService"; +import { + generateAlias, + generateRandomString, + filterAliases, + getAccountStorageKey, + getAliasTags, + getDotVariationCandidates, + paginateItems, + type RandomFormat, +} from "../popup/utils"; +import "./email-helper.css"; + +const ICON_HTML = ` + + + + + + +`; + +interface EmailInputElement extends HTMLInputElement { + __gmailAliasIcon?: HTMLElement; + __gmailAliasPosition?: () => void; + __gmailAliasCleanup?: () => void; +} + +let inlineDisabledForCurrentSite = false; + +/** Gets the normalized hostname for the current site. */ +const currentSiteHostname = () => + normalizeSiteHostname(window.location.hostname); + +/** Removes all inline helper icons and popups from the page. */ +function removeInlineHelpers() { + document + .querySelectorAll("input") + .forEach((input) => input.__gmailAliasCleanup?.()); + document + .querySelectorAll(".gmail-alias-popup, .gmail-alias-input-icon-container") + .forEach((element) => element.remove()); +} + +/** Disables the inline helper for the current website and removes all helpers. */ +async function disableInlineForCurrentSite() { + const site = currentSiteHostname(); + if (!site) return; + + const result = await browser.storage.local.get(INLINE_DISABLED_SITES_KEY); + const disabledSites = parseDisabledInlineSites( + result[INLINE_DISABLED_SITES_KEY], + ); + await browser.storage.local.set({ + [INLINE_DISABLED_SITES_KEY]: Array.from( + new Set([...disabledSites, site]), + ).sort((a, b) => a.localeCompare(b)), + }); + inlineDisabledForCurrentSite = true; + removeInlineHelpers(); +} + +/** Escape HTML special characters to prevent XSS attacks. */ +function escapeHtml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +interface SuggestionData { + activeEmail: string; + previousAlias: string | null; + suggestions: string[]; + website: string; +} + +interface StoredAccountData { + email_accounts?: Array<{ isActive?: boolean; email: string }>; + base_email?: string; +} + +/** Removes accidental text appended after a configured Gmail domain. */ +function normalizeConfiguredEmail(email: string): string { + const trimmed = email.trim(); + const gmailMatch = trimmed.match( + /^([^@\s]+)@(gmail\.com|googlemail\.com)(?:[^@\s]+)?$/i, + ); + + return gmailMatch + ? `${gmailMatch[1]}@${gmailMatch[2].toLowerCase()}` + : trimmed; +} + +/** Returns the configured active account with safe legacy fallbacks. */ +function resolveActiveEmail(accountData: StoredAccountData): string { + const accounts = Array.isArray(accountData.email_accounts) + ? accountData.email_accounts + : []; + return normalizeConfiguredEmail( + accounts.find((account) => account.isActive)?.email || + accountData.base_email || + accounts[0]?.email || + "your.email@gmail.com", + ); +} + +/** Fetch suggestion data for current page. */ +async function fetchSuggestions(): Promise { + const normalized = normalizeHostname(window.location.href); + if (!normalized) return null; + + let email = "your.email@gmail.com"; + try { + const accountResult = (await browser.storage.local.get([ + "email_accounts", + "base_email", + ])) as StoredAccountData; + email = resolveActiveEmail(accountResult); + } catch (error) { + console.debug("Error resolving active email:", error); + } + + const [previousResult, suggestionsResult] = await Promise.allSettled([ + getPreviousAliasForWebsite(email, window.location.href), + generateSuggestionsForWebsite(email, window.location.href), + ]); + + return { + activeEmail: email, + previousAlias: + previousResult.status === "fulfilled" + ? previousResult.value?.alias || null + : null, + suggestions: + suggestionsResult.status === "fulfilled" ? suggestionsResult.value : [], + website: normalized, + }; +} + +/** Create popup element with suggestions. */ +function createPopup( + data: SuggestionData, + onSelect: (alias: string, recordUsage?: boolean) => void, + input?: EmailInputElement, +) { + const popup = document.createElement("div"); + const isFirefox = + browser.runtime.getURL("").startsWith("moz-extension://") || + navigator.userAgent.includes("Firefox"); + const reviewUrl = isFirefox + ? "https://addons.mozilla.org/en-US/firefox/addon/gmail-alias-toolkit/reviews/" + : "https://chromewebstore.google.com/detail/gmail-alias-toolkit/cbapjlppdfbnfbopdegobofmfijnlibl/reviews"; + const homepageUrl = "https://eplus-dev.github.io/gmail-alias-toolkit/"; + popup.className = "gmail-alias-popup"; + popup.style.cssText = ` + position: fixed; + display: flex; + flex-direction: column; + overflow: hidden; + background: white; + border: 1px solid #e5e7eb; + border-radius: 8px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.12); + z-index: 999999; + min-width: 280px; + max-width: 320px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-size: 14px; + color: #1f2937; + `; + const safeWebsite = escapeHtml(data.website); + const safePreviousAlias = data.previousAlias + ? escapeHtml(data.previousAlias) + : ""; + const labels = { + close: escapeHtml(t("close")), + suggestions: escapeHtml(t("generatedAliases")), + generate: escapeHtml(t("generate")), + history: escapeHtml(t("recentAliases")), + previous: escapeHtml(t("recentAliases")), + random: escapeHtml(t("random")), + tags: escapeHtml(t("tabTagsShort")), + tricks: escapeHtml(t("tabTricksShort")), + privateMail: escapeHtml(t("privateMailFormat")), + alphanumeric: escapeHtml(t("randomCharactersFormat")), + words: escapeHtml(t("randomWordsFormat")), + timestamp: escapeHtml(t("timestampFormat")), + numberOfAliases: escapeHtml(t("numberOfAliases")), + tagPlaceholder: escapeHtml(t("tagPlaceholder")), + select: escapeHtml(t("select")), + dotTrick: escapeHtml(t("dotTrick")), + plusTags: escapeHtml(t("plusTags")), + googlemail: escapeHtml(t("googlemail")), + removeDots: escapeHtml(t("removeDots")), + generateTricks: escapeHtml(t("generateTricks")), + searchAliases: escapeHtml(t("searchAliases")), + favorites: escapeHtml(t("favorites")), + allTags: escapeHtml(t("allTags")), + mostRecent: escapeHtml(t("mostRecent")), + az: escapeHtml(t("az")), + previousPage: escapeHtml(t("previousPage")), + nextPage: escapeHtml(t("nextPage")), + disableInlineForSite: escapeHtml(t("disableInlineForSite")), + extensionName: escapeHtml(t("extensionName")), + reportReview: escapeHtml(t("reportReview")), + }; + popup.innerHTML = ` +
+ ${labels.extensionName} +
+ + +
+
+
+ + + +
+
+
+ ${ + data.previousAlias + ? ` +
+
${labels.previous}:
+ +
+
+ ` + : "" + } +
+
${labels.suggestions}:
+
+ ${data.suggestions + .filter((alias) => alias !== data.previousAlias) + .map((alias) => { + const safeAlias = escapeHtml(alias); + return ` +
+
${safeAlias}
+
+ `; + }) + .join("")} +
+
+
+
+
+ + + +
+
+
+ + +
+ +
+
+
+
+ + +
+
+
+
+
+ + +
+ +
+
+
+
+
+ +
+ + + + +
+
+
+ + 1 / 1 + +
+
+
+
+ + `; + + // Handle close + const closeBtn = popup.querySelector( + ".gmail-alias-popup-close", + ) as HTMLButtonElement; + if (closeBtn) { + closeBtn.addEventListener("click", (e) => { + e.stopPropagation(); + popup.remove(); + }); + } + + const disableSiteBtn = popup.querySelector( + ".gmail-alias-popup-disable-site", + ) as HTMLButtonElement | null; + disableSiteBtn?.addEventListener("click", (event) => { + event.preventDefault(); + event.stopPropagation(); + disableInlineForCurrentSite().catch((error) => { + console.debug("Error disabling inline helper:", error); + }); + }); + + // Handle tab switching + popup.querySelectorAll(".gmail-alias-popup-tab").forEach((tab) => { + tab.addEventListener("click", () => { + const tabName = (tab as HTMLElement).dataset.tab; + + // Remove active from all tabs and contents + popup.querySelectorAll(".gmail-alias-popup-tab").forEach((t) => { + t.classList.remove("active"); + }); + popup.querySelectorAll(".gmail-alias-popup-tab-content").forEach((c) => { + c.classList.remove("active"); + }); + + // Add active to clicked tab and corresponding content + tab.classList.add("active"); + popup + .querySelector(`[data-tab-content="${tabName}"]`) + ?.classList.add("active"); + + if (tabName === "history") { + loadHistory().catch((error) => { + console.debug("Error loading history:", error); + }); + } + }); + }); + + // Switch between the compact Random, Tags, and Tricks generators. + popup.querySelectorAll(".gmail-alias-generate-mode").forEach((button) => { + button.addEventListener("click", () => { + const mode = (button as HTMLElement).dataset.mode; + popup + .querySelectorAll(".gmail-alias-generate-mode") + .forEach((item) => item.classList.remove("active")); + popup + .querySelectorAll(".gmail-alias-generate-panel") + .forEach((panel) => panel.classList.remove("active")); + button.classList.add("active"); + popup + .querySelector(`[data-generate-panel="${mode}"]`) + ?.classList.add("active"); + }); + }); + + // Handle custom generate input + const generateBtn = popup.querySelector( + ".gmail-alias-generate-btn", + ) as HTMLButtonElement; + const generateInput = popup.querySelector( + ".gmail-alias-generate-input", + ) as HTMLInputElement; + if (generateBtn && generateInput) { + generateBtn.addEventListener("click", () => { + const value = generateInput.value.trim(); + if (value) { + onSelect(createCustomAlias(data.activeEmail, value)); + popup.remove(); + } + }); + generateInput.addEventListener("keypress", (e) => { + if (e.key === "Enter") { + const value = generateInput.value.trim(); + if (value) { + onSelect(createCustomAlias(data.activeEmail, value)); + popup.remove(); + } + } + }); + } + + // Load the same custom tag presets used by the main popup. + browser.storage.local.get("app_settings").then((settingsStorage) => { + const presetList = popup.querySelector( + ".gmail-alias-preset-list", + ) as HTMLElement | null; + if (!presetList) return; + + const settings = settingsStorage.app_settings as + | { + customPresets?: Array<{ id?: string; label: string; tag: string }>; + randomFormat?: RandomFormat; + } + | undefined; + const formatSelect = popup.querySelector( + ".gmail-alias-random-format", + ) as HTMLSelectElement | null; + if (formatSelect && settings?.randomFormat) { + formatSelect.value = settings.randomFormat; + } + const presets = settings?.customPresets || []; + presetList.innerHTML = presets.length + ? `
${escapeHtml(t("yourPresets"))}
${presets + .map( + (preset) => + ``, + ) + .join("")}` + : `
${escapeHtml(t("noTagsYet"))}
`; + presetList + .querySelectorAll(".gmail-alias-preset-item") + .forEach((presetButton) => { + presetButton.addEventListener("click", () => { + const tag = (presetButton as HTMLElement).dataset.tag; + if (tag) { + onSelect(createCustomAlias(data.activeEmail, tag)); + popup.remove(); + } + }); + }); + }); + + // Generate a batch with the same formats as the main popup. + const randomBtn = popup.querySelector( + ".gmail-alias-random-btn", + ) as HTMLButtonElement; + const randomFormat = popup.querySelector( + ".gmail-alias-random-format", + ) as HTMLSelectElement; + const randomCount = popup.querySelector( + ".gmail-alias-random-count", + ) as HTMLInputElement; + const generatedList = popup.querySelector( + ".gmail-alias-generated-list", + ) as HTMLElement; + if (randomBtn && data.activeEmail) { + randomFormat?.addEventListener("change", async () => { + const settingsStorage = await browser.storage.local.get("app_settings"); + await browser.storage.local.set({ + app_settings: { + ...(settingsStorage.app_settings || {}), + randomFormat: randomFormat.value, + }, + }); + }); + randomBtn.addEventListener("click", () => { + const format = (randomFormat?.value || "private-mail") as RandomFormat; + const count = clampQuickCount(randomCount?.value); + const aliases = Array.from({ length: count }, (_, index) => + generateAlias( + data.activeEmail, + generateRandomString(format, Date.now() + index), + ), + ).filter((alias): alias is string => Boolean(alias)); + if (generatedList) { + renderQuickAliasList(generatedList, aliases, onSelect, input); + } + }); + } + + // Generate commonly used Gmail trick variations. + const trickBtn = popup.querySelector( + ".gmail-alias-trick-btn", + ) as HTMLButtonElement; + const trickType = popup.querySelector( + ".gmail-alias-trick-type", + ) as HTMLSelectElement; + const trickCount = popup.querySelector( + ".gmail-alias-trick-count", + ) as HTMLInputElement; + const trickList = popup.querySelector( + ".gmail-alias-trick-list", + ) as HTMLElement; + trickBtn?.addEventListener("click", () => { + const aliases = generateQuickTricks( + data.activeEmail, + trickType?.value || "dot", + clampQuickCount(trickCount?.value), + ); + if (trickList) { + renderQuickAliasList(trickList, aliases, onSelect, input); + } + }); + + let currentHistory: Array<{ email: string; timestamp: number }> = []; + let currentFavorites: string[] = []; + let historyPage = 1; + const historySearch = popup.querySelector( + ".gmail-alias-history-search", + ) as HTMLInputElement | null; + const historyView = popup.querySelector( + ".gmail-alias-history-view", + ) as HTMLSelectElement | null; + const historyTag = popup.querySelector( + ".gmail-alias-history-tag", + ) as HTMLSelectElement | null; + const historySort = popup.querySelector( + ".gmail-alias-history-sort", + ) as HTMLSelectElement | null; + const historyPageSize = popup.querySelector( + ".gmail-alias-history-page-size", + ) as HTMLSelectElement | null; + const historyPrev = popup.querySelector( + ".gmail-alias-history-prev", + ) as HTMLButtonElement | null; + const historyNext = popup.querySelector( + ".gmail-alias-history-next", + ) as HTMLButtonElement | null; + const historyPageInfo = popup.querySelector( + ".gmail-alias-history-page-info", + ) as HTMLElement | null; + const historyPagination = popup.querySelector( + ".gmail-alias-history-pagination", + ) as HTMLElement | null; + + /** Filters, sorts, and paginates history using the popup's shared logic. */ + function renderHistory() { + const historyList = popup.querySelector( + ".gmail-alias-history-list", + ) as HTMLElement | null; + if (!historyList) return; + + const filtered = filterAliases(currentHistory, { + viewMode: historyView?.value === "favorites" ? "favorites" : "all", + favorites: currentFavorites, + searchQuery: historySearch?.value || "", + filterTag: historyTag?.value || "all", + sortBy: historySort?.value === "alphabetical" ? "alphabetical" : "recent", + }); + const page = paginateItems( + filtered, + historyPage, + Number(historyPageSize?.value || 5), + ); + historyPage = page.currentPage; + + if (historyPageInfo) { + historyPageInfo.textContent = page.totalItems + ? `${page.startIndex + 1}-${Math.min(page.endIndex, page.totalItems)} / ${page.totalItems}` + : "0 / 0"; + } + if (historyPrev) historyPrev.disabled = historyPage <= 1; + if (historyNext) historyNext.disabled = historyPage >= page.totalPages; + if (historyPagination) { + historyPagination.hidden = page.totalPages <= 1; + } + + if (page.items.length === 0) { + const emptyMessage = + currentHistory.length === 0 + ? t("noResultsFound") + : historyView?.value === "favorites" + ? t("noFavoritesYet") + : t("noResultsFound"); + historyList.innerHTML = `
${emptyMessage}
`; + return; + } + + renderQuickAliasList( + historyList, + page.items.map((item) => item.email), + onSelect, + input, + false, + ); + } + + [ + historySearch, + historyView, + historyTag, + historySort, + historyPageSize, + ].forEach((control) => { + control?.addEventListener( + control === historySearch ? "input" : "change", + () => { + historyPage = 1; + renderHistory(); + }, + ); + }); + historyPrev?.addEventListener("click", () => { + historyPage -= 1; + renderHistory(); + }); + historyNext?.addEventListener("click", () => { + historyPage += 1; + renderHistory(); + }); + + /** Loads the latest account history whenever the popup or History tab opens. */ + async function loadHistory() { + const historyList = popup.querySelector( + ".gmail-alias-history-list", + ) as HTMLElement; + if (!historyList) return; + + try { + // Use the same account that was used to generate this popup's aliases. + const historyKey = getAccountStorageKey( + data.activeEmail, + "gmail_alias_recent", + ); + const favoritesKey = getAccountStorageKey(data.activeEmail, "favorites"); + + const storage = (await browser.storage.local.get([ + historyKey, + favoritesKey, + "gmail_alias_recent", + "favorites", + ])) as Record; + // Older installations may still have history under the global key. + const history = (storage[historyKey] ?? + storage.gmail_alias_recent ?? + []) as Array<{ + email: string; + timestamp: number; + }>; + currentHistory = history + .filter((item) => item && typeof item.email === "string") + .slice() + .sort((a, b) => b.timestamp - a.timestamp); + const favorites = (storage[favoritesKey] ?? storage.favorites ?? []) as + | Array<{ email?: string } | string> + | undefined; + currentFavorites = Array.isArray(favorites) + ? favorites + .map((favorite) => + typeof favorite === "string" ? favorite : favorite.email, + ) + .filter((email): email is string => Boolean(email)) + : []; + + if (historyTag) { + const selectedTag = historyTag.value; + historyTag.innerHTML = [ + ``, + ...getAliasTags(currentHistory).map( + (tag) => + ``, + ), + ].join(""); + historyTag.value = getAliasTags(currentHistory).includes(selectedTag) + ? selectedTag + : "all"; + } + renderHistory(); + } catch (error) { + console.debug("Error loading history:", error); + historyList.innerHTML = `
${escapeHtml(t("noResultsFound"))}
`; + } + } + + loadHistory().catch((error) => { + console.debug("Error loading history:", error); + }); + + // Handle suggestion item hover for preview and click to select + if (input) { + const originalValue = input.value; + let suggestionCommitted = false; + + popup.querySelectorAll(".gmail-alias-suggestion-item").forEach((item) => { + const alias = (item as HTMLElement).dataset.alias; + + item.addEventListener("mouseenter", () => { + if (alias) { + // Show preview in input with visual indicator + fillInput(input, alias); + input.classList.add("gmail-alias-input-preview"); + } + }); + + item.addEventListener("mouseleave", () => { + if (suggestionCommitted) return; + // Restore original input value + fillInput(input, originalValue); + input.classList.remove("gmail-alias-input-preview"); + }); + + item.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + if (alias) { + suggestionCommitted = true; + onSelect(alias); + popup.remove(); + } + }); + }); + } + + // Handle previous alias button hover and click + const prevAliasBtn = popup.querySelector( + ".gmail-alias-prev-alias", + ) as HTMLElement; + if (prevAliasBtn && input) { + const originalValue = input.value; + let previousAliasCommitted = false; + + prevAliasBtn.addEventListener("mouseenter", () => { + const alias = prevAliasBtn.dataset.alias; + if (alias) { + fillInput(input, alias); + input.classList.add("gmail-alias-input-preview"); + } + }); + + prevAliasBtn.addEventListener("mouseleave", () => { + if (previousAliasCommitted) return; + fillInput(input, originalValue); + input.classList.remove("gmail-alias-input-preview"); + }); + + prevAliasBtn.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + const alias = prevAliasBtn.dataset.alias; + if (alias) { + previousAliasCommitted = true; + onSelect(alias); + popup.remove(); + } + }); + } + + // Close on outside click + const handleOutsideClick = (e: MouseEvent) => { + if (!popup.contains(e.target as Node)) { + popup.remove(); + document.removeEventListener("click", handleOutsideClick); + } + }; + + setTimeout(() => { + document.addEventListener("click", handleOutsideClick); + }, 0); + + return popup; +} + +/** Inject icon next to email input. */ +function injectIcon(input: EmailInputElement) { + if (input.__gmailAliasIcon) { + input.__gmailAliasPosition?.(); + return; + } + + try { + // Create icon container (no wrapping, just for the icon) + const iconContainer = document.createElement("div"); + iconContainer.className = "gmail-alias-input-icon-container"; + iconContainer.innerHTML = ICON_HTML; + + const icon = iconContainer.querySelector( + ".gmail-alias-input-icon", + ) as SVGElement; + if (!icon) { + console.error("[Gmail Alias] Failed to create icon SVG"); + return; + } + + // Render the fixed helper at the document root so form wrappers with + // overflow/contain/stacking contexts cannot clip or remove it. + document.body.appendChild(iconContainer); + + console.debug("[Gmail Alias] Icon injected successfully"); + + // Keep the helper outside the website's input and layout. Fixed positioning + // avoids colliding with native suffix icons or adjacent submit buttons. + iconContainer.style.position = "fixed"; + iconContainer.style.pointerEvents = "auto"; + iconContainer.style.display = "flex"; + iconContainer.style.alignItems = "center"; + iconContainer.style.justifyContent = "center"; + iconContainer.style.width = "32px"; + iconContainer.style.height = "32px"; + iconContainer.style.border = "1px solid #e5e7eb"; + iconContainer.style.borderRadius = "9999px"; + iconContainer.style.backgroundColor = "#ffffff"; + iconContainer.style.boxShadow = + "0 3px 10px rgba(15, 23, 42, 0.18), 0 1px 2px rgba(15, 23, 42, 0.1)"; + iconContainer.style.zIndex = "999998"; + + /** Detects compact controls or floating overlays occupying a candidate. */ + const isPlacementBlocked = ( + left: number, + top: number, + iconSize: number, + ) => { + const inset = 4; + const points = [ + [left + iconSize / 2, top + iconSize / 2], + [left + inset, top + inset], + [left + iconSize - inset, top + inset], + [left + inset, top + iconSize - inset], + [left + iconSize - inset, top + iconSize - inset], + ]; + + return points.some(([x, y]) => + document.elementsFromPoint(x, y).some((element) => { + if ( + element === input || + element === document.body || + element === document.documentElement || + iconContainer.contains(element) + ) { + return false; + } + + const rect = element.getBoundingClientRect(); + const style = window.getComputedStyle(element); + const isInteractiveControl = element.matches( + "button, select, textarea, a[href], [role='button'], [role='combobox'], [contenteditable='true'], input", + ); + const isCompactControl = rect.width <= 96 && rect.height <= 96; + const isCompactOverlay = + (style.position === "fixed" || style.position === "absolute") && + rect.width <= 180 && + rect.height <= 180; + + return ( + style.visibility !== "hidden" && + style.display !== "none" && + (isInteractiveControl || isCompactControl || isCompactOverlay) + ); + }), + ); + }; + + const state = { + inputResizeObserver: undefined as ResizeObserver | undefined, + activePopup: null as HTMLElement | null, + iconPlacementDirection: null as + | "left" + | "right" + | "above" + | "below" + | null, + // eslint-disable-next-line @typescript-eslint/no-empty-function + positionIconOutsideInput: (() => {}) as () => void, + }; + + /** Cleans up positioning event listeners, observers, and removes the icon. */ + const cleanupPositioning = () => { + window.removeEventListener("resize", state.positionIconOutsideInput); + window.removeEventListener( + "scroll", + state.positionIconOutsideInput, + true, + ); + state.inputResizeObserver?.disconnect(); + iconContainer.remove(); + input.__gmailAliasIcon = undefined; + input.__gmailAliasPosition = undefined; + input.__gmailAliasCleanup = undefined; + }; + + /** Checks if the input field is within the viewport bounds. */ + const isInputInViewport = (rect: DOMRect) => + rect.width > 0 && + rect.height > 0 && + rect.bottom > 0 && + rect.right > 0 && + rect.top < window.innerHeight && + rect.left < window.innerWidth; + + /** Updates the icon's directional pointer class to match placement. */ + const setIconPointerDirection = ( + direction: "left" | "right" | "above" | "below", + ) => { + iconContainer.classList.remove( + "gmail-alias-icon-left", + "gmail-alias-icon-right", + "gmail-alias-icon-above", + "gmail-alias-icon-below", + ); + iconContainer.classList.add(`gmail-alias-icon-${direction}`); + }; + + /** Determines the best horizontal side (left/right) for the icon and popup. */ + const getPreferredHorizontalSide = (rect: DOMRect) => { + const viewportPadding = 8; + const inputGap = 6; + const iconSize = 32; + const requiredRoom = inputGap + iconSize + 8 + 240; + const centeredTop = rect.top + (rect.height - iconSize) / 2; + const options = [ + { + side: "right" as const, + room: window.innerWidth - rect.right - viewportPadding, + iconLeft: rect.right + inputGap, + }, + { + side: "left" as const, + room: rect.left - viewportPadding, + iconLeft: rect.left - inputGap - iconSize, + }, + ] + .filter( + (option) => + option.room >= requiredRoom && + option.iconLeft >= viewportPadding && + option.iconLeft + iconSize <= window.innerWidth - viewportPadding && + !isPlacementBlocked(option.iconLeft, centeredTop, iconSize), + ) + .sort((a, b) => b.room - a.room); + + return options[0]?.side ?? null; + }; + + /** Places the icon and its popup as one group without covering the input. */ + const positionPopupNextToInput = (popup: HTMLElement) => { + const inputRect = input.getBoundingClientRect(); + if (!isInputInViewport(inputRect)) { + iconContainer.style.display = "none"; + popup.remove(); + state.activePopup = null; + return; + } + + const viewportPadding = 8; + const inputGap = 6; + const popupGap = 8; + const iconSize = 32; + const maxPopupWidth = 320; + const leftRoom = inputRect.left - viewportPadding; + const rightRoom = window.innerWidth - inputRect.right - viewportPadding; + const horizontalChrome = inputGap + iconSize + popupGap; + const preferredHorizontalSide = + state.iconPlacementDirection === "left" || + state.iconPlacementDirection === "right" + ? state.iconPlacementDirection + : null; + const canUseLeft = preferredHorizontalSide === "left"; + const canUseRight = preferredHorizontalSide === "right"; + const centeredIconTop = Math.min( + window.innerHeight - iconSize - viewportPadding, + Math.max( + viewportPadding, + inputRect.top + (inputRect.height - iconSize) / 2, + ), + ); + + let availableHeight = window.innerHeight - viewportPadding * 2; + if (canUseRight || canUseLeft) { + const placeRight = canUseRight; + const room = placeRight ? rightRoom : leftRoom; + const popupWidth = Math.min(maxPopupWidth, room - horizontalChrome); + popup.style.width = `${popupWidth}px`; + popup.style.minWidth = `${popupWidth}px`; + popup.style.maxWidth = `${popupWidth}px`; + + // Measure again after adapting the popup width. + const popupRect = popup.getBoundingClientRect(); + const visiblePopupHeight = Math.min( + popupRect.height, + window.innerHeight - viewportPadding * 2, + ); + const popupTop = Math.min( + window.innerHeight - visiblePopupHeight - viewportPadding, + Math.max( + viewportPadding, + centeredIconTop + iconSize / 2 - visiblePopupHeight / 2, + ), + ); + + if (placeRight) { + const iconLeft = inputRect.right + inputGap; + iconContainer.style.left = `${iconLeft}px`; + popup.style.left = `${iconLeft + iconSize + popupGap}px`; + setIconPointerDirection("right"); + } else { + const iconLeft = inputRect.left - inputGap - iconSize; + iconContainer.style.left = `${iconLeft}px`; + popup.style.left = `${iconLeft - popupGap - popupWidth}px`; + setIconPointerDirection("left"); + } + iconContainer.style.top = `${centeredIconTop}px`; + popup.style.top = `${popupTop}px`; + } else { + const roomBelow = window.innerHeight - inputRect.bottom - inputGap; + const roomAbove = inputRect.top - inputGap; + const placeBelow = + state.iconPlacementDirection === "below" || + (state.iconPlacementDirection !== "above" && roomBelow >= roomAbove); + const popupWidth = Math.min( + maxPopupWidth, + window.innerWidth - viewportPadding * 2, + ); + const currentIconLeft = Number.parseFloat(iconContainer.style.left); + const centeredIconLeft = + inputRect.left + inputRect.width / 2 - iconSize / 2; + const iconLeft = Math.min( + window.innerWidth - iconSize - viewportPadding, + Math.max( + viewportPadding, + Number.isFinite(currentIconLeft) + ? currentIconLeft + : centeredIconLeft, + ), + ); + // For vertical layouts, the popup follows the icon rather than the + // input center so collision-adjusted icons never look detached. + const left = Math.min( + window.innerWidth - popupWidth - viewportPadding, + Math.max(viewportPadding, iconLeft + iconSize / 2 - popupWidth / 2), + ); + popup.style.width = `${popupWidth}px`; + popup.style.minWidth = `${popupWidth}px`; + popup.style.maxWidth = `${popupWidth}px`; + popup.style.left = `${left}px`; + iconContainer.style.left = `${iconLeft}px`; + + if (placeBelow) { + const iconTop = inputRect.bottom + inputGap; + iconContainer.style.top = `${iconTop}px`; + popup.style.top = `${iconTop + iconSize + popupGap}px`; + availableHeight = Math.max( + 120, + window.innerHeight - + (iconTop + iconSize + popupGap) - + viewportPadding, + ); + setIconPointerDirection("below"); + } else { + const iconTop = inputRect.top - inputGap - iconSize; + const popupRect = popup.getBoundingClientRect(); + availableHeight = Math.max(120, iconTop - popupGap - viewportPadding); + iconContainer.style.top = `${iconTop}px`; + popup.style.top = `${Math.max( + viewportPadding, + iconTop - popupGap - Math.min(popupRect.height, availableHeight), + )}px`; + setIconPointerDirection("above"); + } + } + + iconContainer.style.display = "flex"; + popup.style.position = "fixed"; + popup.style.maxHeight = `${availableHeight}px`; + popup.style.overflowY = "hidden"; + popup.style.zIndex = "999999"; + }; + + // Assign the positioning function (declared earlier to fix forward reference) + state.positionIconOutsideInput = () => { + if (!input.isConnected) { + cleanupPositioning(); + return; + } + + const rect = input.getBoundingClientRect(); + if (!isInputInViewport(rect)) { + iconContainer.style.display = "none"; + if (state.activePopup?.isConnected) state.activePopup.remove(); + state.activePopup = null; + return; + } + + iconContainer.style.display = "flex"; + const iconSize = 32; + const gap = 6; + /** Clamps the left position to keep the icon within viewport bounds. */ + const clampLeft = (left: number) => + Math.min(window.innerWidth - iconSize - 4, Math.max(4, left)); + const centeredTop = rect.top + (rect.height - iconSize) / 2; + const right = rect.right + gap; + const left = rect.left - iconSize - gap; + const rightAligned = clampLeft(rect.right - iconSize); + const centeredIconLeft = clampLeft( + rect.left + rect.width / 2 - iconSize / 2, + ); + const above = rect.top - iconSize - gap; + const below = rect.bottom + gap; + const preferredHorizontalSide = getPreferredHorizontalSide(rect); + const roomBelow = window.innerHeight - rect.bottom - gap; + const roomAbove = rect.top - gap; + const verticalCandidate = + roomBelow >= roomAbove + ? { left: centeredIconLeft, top: below, direction: "below" as const } + : { left: centeredIconLeft, top: above, direction: "above" as const }; + const candidates = [ + ...(preferredHorizontalSide === "right" + ? [{ left: right, top: centeredTop, direction: "right" as const }] + : []), + ...(preferredHorizontalSide === "left" + ? [{ left, top: centeredTop, direction: "left" as const }] + : []), + ...(!preferredHorizontalSide ? [verticalCandidate] : []), + { left: rightAligned, top: above, direction: "above" as const }, + { left: rightAligned, top: below, direction: "below" as const }, + ].filter( + (candidate) => + candidate.left >= 4 && + candidate.left + iconSize <= window.innerWidth - 4 && + candidate.top >= 4 && + candidate.top + iconSize <= window.innerHeight - 4, + ); + const placement = + candidates.find( + (candidate) => + !isPlacementBlocked(candidate.left, candidate.top, iconSize), + ) || candidates[0]; + if (!placement) return; + + iconContainer.style.left = `${placement.left}px`; + iconContainer.style.top = `${placement.top}px`; + state.iconPlacementDirection = placement.direction; + setIconPointerDirection(placement.direction); + if (state.activePopup?.isConnected) { + positionPopupNextToInput(state.activePopup); + } + }; + input.__gmailAliasPosition = state.positionIconOutsideInput; + input.__gmailAliasCleanup = cleanupPositioning; + state.positionIconOutsideInput(); + window.addEventListener("resize", state.positionIconOutsideInput); + window.addEventListener("scroll", state.positionIconOutsideInput, true); + state.inputResizeObserver = new ResizeObserver( + state.positionIconOutsideInput, + ); + state.inputResizeObserver.observe(input); + + icon.style.cursor = "pointer"; + icon.style.color = "#3b82f6"; + icon.style.opacity = "1"; + icon.style.pointerEvents = "auto"; + icon.style.width = "18px"; + icon.style.height = "18px"; + + let closeTimer: NodeJS.Timeout; + + iconContainer.addEventListener("mouseenter", async () => { + clearTimeout(closeTimer); + icon.style.opacity = "1"; + input.classList.add("gmail-alias-input-highlight"); + + // Show popup on hover + document + .querySelectorAll(".gmail-alias-popup") + .forEach((p) => p.remove()); + + const data = await fetchSuggestions(); + if (!data) { + return; + } + + const popup = createPopup( + data, + async (alias, recordUsage = true) => { + fillInput(input, alias); + input.classList.remove("gmail-alias-input-preview"); + + try { + const tasks: Promise[] = []; + if (recordUsage) { + tasks.push( + browser.runtime.sendMessage({ + action: "saveAliasToHistory", + alias, + accountEmail: data.activeEmail, + }), + ); + } + + if (data.website) { + tasks.push( + saveWebsiteAlias(data.activeEmail, data.website, alias), + ); + } + + await Promise.all(tasks); + } catch (error) { + console.debug("Error saving selected alias:", error); + } + }, + input, + ); + + document.body.appendChild(popup); + state.activePopup = popup; + positionPopupNextToInput(popup); + + // Keep popup open when hovering over it + popup.addEventListener("mouseenter", () => { + clearTimeout(closeTimer); + }); + popup.addEventListener("focusin", () => { + clearTimeout(closeTimer); + }); + popup.addEventListener("pointerdown", () => { + clearTimeout(closeTimer); + }); + + popup.addEventListener("mouseleave", () => { + closeTimer = setTimeout(() => { + // Native select menus are rendered outside the popup bounds. Their + // control remains focused, so this is still an active popup session. + if (popup.matches(":focus-within")) return; + popup.remove(); + }, 100); + }); + }); + + iconContainer.addEventListener("mouseleave", () => { + icon.style.opacity = "1"; + input.classList.remove("gmail-alias-input-highlight"); + // Delay close to allow mouse movement to popup (with overlap, 250ms buffer) + closeTimer = setTimeout(() => { + document + .querySelectorAll(".gmail-alias-popup") + .forEach((popupElement) => { + if (!popupElement.matches(":focus-within")) { + popupElement.remove(); + } + }); + }, 250); + }); + + input.__gmailAliasIcon = icon as unknown as HTMLElement; + } catch (error) { + console.error("[Gmail Alias] Error injecting icon:", error); + } +} + +/** Restricts quick generator batch sizes to keep the inline popup responsive. */ +function clampQuickCount(value?: string): number { + return Math.min(20, Math.max(1, Number.parseInt(value || "5", 10) || 5)); +} + +/** Renders aliases as quick-fill buttons. */ +function renderQuickAliasList( + container: HTMLElement, + aliases: string[], + onSelect: (alias: string, recordUsage?: boolean) => void, + input?: EmailInputElement, + recordUsage = true, +) { + container.innerHTML = aliases.length + ? aliases + .map( + (alias) => + ``, + ) + .join("") + : `
${escapeHtml(t("noResultsFound"))}
`; + + const originalValue = input?.value || ""; + let committed = false; + + container.querySelectorAll(".gmail-alias-quick-alias").forEach((button) => { + button.addEventListener("mouseenter", () => { + const alias = (button as HTMLElement).dataset.alias; + if (!alias || !input) return; + fillInput(input, alias); + input.classList.add("gmail-alias-input-preview"); + }); + + button.addEventListener("mouseleave", () => { + if (!input || committed) return; + fillInput(input, originalValue); + input.classList.remove("gmail-alias-input-preview"); + }); + + button.addEventListener("click", () => { + const alias = (button as HTMLElement).dataset.alias; + if (!alias) return; + committed = true; + onSelect(alias, recordUsage); + button.closest(".gmail-alias-popup")?.remove(); + }); + }); +} + +/** Generates the most useful Gmail tricks for the compact helper. */ +function generateQuickTricks( + baseEmail: string, + trick: string, + count: number, +): string[] { + const [username, domain] = baseEmail.split("@"); + if (!username || !domain) return []; + + const commonTags = [ + "newsletter", + "shop", + "work", + "personal", + "test", + "promo", + "social", + "finance", + "travel", + "spam", + ]; + if (trick === "plus") { + return commonTags + .slice(0, count) + .map((tag) => `${username}+${tag}@${domain}`); + } + if (trick === "nodots") { + const noDots = username.replace(/\./g, ""); + return [ + `${noDots}@${domain}`, + ...commonTags.map((tag) => `${noDots}+${tag}@${domain}`), + ].slice(0, count); + } + + const outputDomain = + trick === "googlemail" + ? domain.toLowerCase() === "gmail.com" + ? "googlemail.com" + : "gmail.com" + : domain; + return getDotVariationCandidates(username, count).map( + (variation) => `${variation}@${outputDomain}`, + ); +} + +/** Converts a custom tag into an alias, while preserving a complete email. */ +function createCustomAlias(baseEmail: string, value: string): string { + if (value.includes("@")) return value; + + const atIndex = baseEmail.lastIndexOf("@"); + if (atIndex <= 0) return value; + + return `${baseEmail.slice(0, atIndex)}+${value}${baseEmail.slice(atIndex)}`; +} + +/** Fill email input with alias (supports controlled inputs). */ +function fillInput(input: EmailInputElement, alias: string) { + const nativeInputValueSetter = Object.getOwnPropertyDescriptor( + HTMLInputElement.prototype, + "value", + )?.set; + + if (nativeInputValueSetter) { + nativeInputValueSetter.call(input, alias); + } else { + input.value = alias; + } + + ["input", "change"].forEach((eventType) => { + input.dispatchEvent( + new Event(eventType, { bubbles: true, composed: true }), + ); + }); + + input.focus(); +} + +/** Detect email inputs on page. */ +function detectEmailInputs() { + if (inlineDisabledForCurrentSite) { + removeInlineHelpers(); + return; + } + + const emailInputs = document.querySelectorAll( + 'input[type="email"], input[name*="email" i], input[placeholder*="email" i], input[id*="email" i], input[aria-label*="email" i]', + ); + + console.debug(`[Gmail Alias] Found ${emailInputs.length} email inputs`); + + emailInputs.forEach((input, idx) => { + const isVisible = input.offsetParent !== null; + const hasIcon = Boolean(input.__gmailAliasIcon); + const isWideEnough = input.offsetWidth > 50; + + console.debug( + `[Gmail Alias] Input ${idx}: visible=${isVisible}, hasIcon=${hasIcon}, width=${input.offsetWidth}px`, + ); + + if (isVisible && isWideEnough) { + if (!hasIcon) { + console.debug(`[Gmail Alias] Injecting icon for input ${idx}`); + } + injectIcon(input); + } + }); +} + +/** Watch for dynamically added inputs. */ +function observeDOM() { + const observer = new MutationObserver(() => { + detectEmailInputs(); + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ["type", "name", "placeholder"], + }); + + return observer; +} + +export default defineContentScript({ + matches: [""], + async main() { + const disabledSitesResult = await browser.storage.local.get( + INLINE_DISABLED_SITES_KEY, + ); + inlineDisabledForCurrentSite = parseDisabledInlineSites( + disabledSitesResult[INLINE_DISABLED_SITES_KEY], + ).includes(currentSiteHostname()); + + // Setup observer immediately + const observer = observeDOM(); + + // Initial detect + detectEmailInputs(); + + // Periodic fallback scan for SPA/lazy-loaded inputs + const scanInterval = setInterval(() => { + detectEmailInputs(); + }, 2000); + + // Listen for DOM ready events + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", detectEmailInputs); + } + window.addEventListener("load", detectEmailInputs); + + // Listen for context menu fill requests + browser.runtime.onMessage.addListener((message) => { + if (message.action === "fillEmail" && message.email) { + const activeElement = document.activeElement as HTMLElement | null; + + if ( + activeElement && + (activeElement.tagName === "INPUT" || + activeElement.tagName === "TEXTAREA" || + activeElement.isContentEditable) + ) { + if ( + activeElement instanceof HTMLInputElement || + activeElement instanceof HTMLTextAreaElement + ) { + fillInput(activeElement as EmailInputElement, message.email); + activeElement.dispatchEvent(new Event("input", { bubbles: true })); + activeElement.dispatchEvent(new Event("change", { bubbles: true })); + } else if (activeElement.isContentEditable) { + activeElement.textContent = message.email; + activeElement.dispatchEvent(new Event("input", { bubbles: true })); + } + + const originalBg = (activeElement as HTMLElement).style + .backgroundColor; + (activeElement as HTMLElement).style.backgroundColor = "#d1fae5"; + setTimeout(() => { + (activeElement as HTMLElement).style.backgroundColor = originalBg; + }, 500); + } + } + }); + + browser.storage.onChanged.addListener((changes, areaName) => { + if (areaName !== "local" || !changes[INLINE_DISABLED_SITES_KEY]) return; + inlineDisabledForCurrentSite = parseDisabledInlineSites( + changes[INLINE_DISABLED_SITES_KEY].newValue, + ).includes(currentSiteHostname()); + if (inlineDisabledForCurrentSite) removeInlineHelpers(); + else detectEmailInputs(); + }); + + // Cleanup on unload + window.addEventListener("beforeunload", () => { + clearInterval(scanInterval); + observer.disconnect(); + }); + }, +}); diff --git a/entrypoints/popup/App.tsx b/entrypoints/popup/App.tsx index 9a3e7a4..297bc39 100644 --- a/entrypoints/popup/App.tsx +++ b/entrypoints/popup/App.tsx @@ -399,14 +399,15 @@ function App() { // stale-closure / lost-update race that happens when saveRecentAlias is called // N times in a tight loop (e.g. "Copy All"). const saveRecentAliases = (emails: string[]) => { - if (emails.length === 0) return; + const uniqueEmails = [...new Set(emails.filter(Boolean))]; + if (uniqueEmails.length === 0) return; const now = Date.now(); - const newAliases: Alias[] = emails.map((email, i) => ({ + const newAliases: Alias[] = uniqueEmails.map((email, i) => ({ email, timestamp: now - i, })); - const newEmailSet = new Set(emails); + const newEmailSet = new Set(uniqueEmails); const updated = [ ...newAliases, @@ -420,7 +421,7 @@ function App() { browser.storage.local.set({ [historyKey]: updated }); // Update statistics - updateStats(emails); + updateStats(uniqueEmails); }; /** Saves a single alias to recent history. */ diff --git a/entrypoints/popup/components/GeneratorTabs.tsx b/entrypoints/popup/components/GeneratorTabs.tsx index d1e7f1b..90acda8 100644 --- a/entrypoints/popup/components/GeneratorTabs.tsx +++ b/entrypoints/popup/components/GeneratorTabs.tsx @@ -283,7 +283,7 @@ function RandomAliasListHeader({ saveRecentAliases, setToastMessage, }: RandomAliasListHeaderProps) { - /** Copies all generated aliases and records them in recent history. */ + /** Copies all generated aliases and records the copied batch. */ const copyAllAliases = async () => { try { await navigator.clipboard.writeText(generatedRandomList.join("\n")); diff --git a/entrypoints/popup/components/HistorySection.tsx b/entrypoints/popup/components/HistorySection.tsx index 191a518..ae338f2 100644 --- a/entrypoints/popup/components/HistorySection.tsx +++ b/entrypoints/popup/components/HistorySection.tsx @@ -22,6 +22,7 @@ import { Tooltip } from "src/components/motion/tooltip"; import { Table, type TableColumn } from "src/components/motion/table"; import { AnimatedBadge } from "src/components/motion/animated-badge"; import { t } from "../../../lib/i18n"; +import { getAliasTags, paginateItems } from "../utils"; interface Alias { email: string; @@ -305,16 +306,7 @@ export default function HistorySection({ {t("allTags")} - {Array.from( - new Set( - recentAliases - .map((a) => { - const match = a.email.match(/\+([^@]+)@/); - return match ? match[1] : null; - }) - .filter((t): t is string => t !== null), - ), - ).map((tag) => ( + {getAliasTags(recentAliases).map((tag) => ( {tag} @@ -390,11 +382,13 @@ function HistoryList({ setQrAlias: (email: string | null) => void; viewMode: "all" | "favorites"; }) { - const totalItems = filteredAliases.length; - const totalPages = Math.ceil(totalItems / itemsPerPage); - const startIndex = (currentPage - 1) * itemsPerPage; - const endIndex = startIndex + itemsPerPage; - const paginatedAliases = filteredAliases.slice(startIndex, endIndex); + const { + items: paginatedAliases, + totalItems, + totalPages, + startIndex, + endIndex, + } = paginateItems(filteredAliases, currentPage, itemsPerPage); const columns = useMemo[]>( () => [ ...(isSelectMode diff --git a/entrypoints/popup/components/Settings.tsx b/entrypoints/popup/components/Settings.tsx index 7c144c0..9be5741 100644 --- a/entrypoints/popup/components/Settings.tsx +++ b/entrypoints/popup/components/Settings.tsx @@ -14,8 +14,12 @@ import { RadioGroup, RadioGroupItem } from "src/components/motion/radio"; import { Tooltip } from "src/components/motion/tooltip"; import { BouncyAccordion } from "src/components/motion/bouncy-accordion"; import { AnimatedBadge } from "src/components/motion/animated-badge"; -import { getAccountStorageKey } from "../utils"; +import { getAccountStorageKey, validateEmail } from "../utils"; import { t } from "../../../lib/i18n"; +import { + INLINE_DISABLED_SITES_KEY, + parseDisabledInlineSites, +} from "src/utils/inlineSiteSettings"; interface SettingsProps { isOpen: boolean; @@ -74,6 +78,41 @@ const DEFAULT_SETTINGS: AppSettings = { }; const CHANGELOG: ChangelogEntry[] = [ + { + version: "1.3.0", + date: "2026-07-13", + changes: [ + { + type: "Added", + items: [ + "Added website-aware alias suggestions based on the current hostname", + "Added an email input helper with inline icons, suggestion popups, live previews, and explicit Use actions", + "Added previous-alias navigation and an information panel explaining supported rules and local-only storage", + "Added expanded statistics metrics and Russian and Turkish translations", + "Added a product landing page with automated GitHub Pages deployment", + ], + }, + { + type: "Changed", + items: [ + "Enhanced the context menu with dynamic, website-specific alias suggestions", + "Updated the History tab to show aliases across websites and store history per email account", + "Improved popup navigation, layout, styling, and alias selection behavior", + "Reorganized the content script and colocated its email helper styles", + ], + }, + { + type: "Fixed", + items: [ + "Preserved email input width and flex layout when injecting the helper icon", + "Improved helper popup positioning and hover behavior to prevent accidental closing", + "Hid the Tags statistics tab when there is not enough data for a useful chart", + "Hardened content rendering against client-side cross-site scripting", + "Resolved code quality, localization, and build workflow issues", + ], + }, + ], + }, { version: "1.2.0", date: "2026-07-03", @@ -625,6 +664,7 @@ export default function Settings({ "general" | "accounts" | "presets" | "advanced" | "changelog" >("general"); const [emailAccounts, setEmailAccounts] = useState([]); + const [disabledInlineSites, setDisabledInlineSites] = useState([]); const [editingAccountId, setEditingAccountId] = useState(null); const [editingLabel, setEditingLabel] = useState(""); const [editingEmail, setEditingEmail] = useState(""); @@ -671,10 +711,25 @@ export default function Settings({ /** Loads saved app settings from extension storage, merged over defaults. */ const loadSettings = async () => { - const result = await browser.storage.local.get("app_settings"); + const result = await browser.storage.local.get([ + "app_settings", + INLINE_DISABLED_SITES_KEY, + ]); if (result.app_settings) { setSettings({ ...DEFAULT_SETTINGS, ...result.app_settings }); } + setDisabledInlineSites( + parseDisabledInlineSites(result[INLINE_DISABLED_SITES_KEY]), + ); + }; + + /** Re-enables the inline helper for one previously disabled website. */ + const enableInlineForSite = async (site: string) => { + const nextSites = disabledInlineSites.filter((item) => item !== site); + setDisabledInlineSites(nextSites); + await browser.storage.local.set({ + [INLINE_DISABLED_SITES_KEY]: nextSites, + }); }; /** Loads the email accounts list from extension storage. */ @@ -814,11 +869,6 @@ export default function Settings({ /** Deletes an account and all of its stored data after confirmation. */ const handleDeleteAccount = async (account: EmailAccount) => { - if (emailAccounts.length === 1) { - showToast(t("cannotDeleteLastAccountTitle")); - return; - } - const confirmMsg = t("deleteAccountMessage", [ account.label, account.email, @@ -840,8 +890,10 @@ export default function Settings({ ); const statsKey = getAccountStorageKey(account.email, "alias_stats"); const favoritesKey = getAccountStorageKey(account.email, "favorites"); - - await browser.storage.local.remove([historyKey, statsKey, favoritesKey]); + const websiteAliasesKey = getAccountStorageKey( + account.email, + "website_alias_map", + ); // Remove from accounts list let updated = emailAccounts.filter((acc) => acc.id !== account.id); @@ -855,9 +907,22 @@ export default function Settings({ await browser.storage.local.set({ base_email: updated[0].email }); } + await browser.storage.local.remove([ + historyKey, + statsKey, + favoritesKey, + websiteAliasesKey, + ...(updated.length === 0 + ? ["base_email", "gmail_alias_recent", "alias_stats", "favorites"] + : []), + ]); await browser.storage.local.set({ email_accounts: updated }); setEmailAccounts(updated); showToast(t("toastAccountDeleted")); + + if (updated.length === 0) { + onClose(); + } }; /** Enters edit mode for the given account. */ @@ -881,7 +946,12 @@ export default function Settings({ return; } - if (!editingEmail.trim() || !editingEmail.includes("@")) { + let newEmail = editingEmail.trim(); + if (newEmail && !newEmail.includes("@")) { + newEmail += "@gmail.com"; + } + + if (!validateEmail(newEmail).isValid) { showToast(t("errorInvalidEmail")); return; } @@ -890,7 +960,6 @@ export default function Settings({ if (!account) return; const oldEmail = account.email; - const newEmail = editingEmail.trim(); // Check if email changed if (oldEmail !== newEmail) { @@ -922,6 +991,10 @@ export default function Settings({ ); const oldStatsKey = getAccountStorageKey(oldEmail, "alias_stats"); const oldFavoritesKey = getAccountStorageKey(oldEmail, "favorites"); + const oldWebsiteAliasesKey = getAccountStorageKey( + oldEmail, + "website_alias_map", + ); const newHistoryKey = getAccountStorageKey( newEmail, @@ -929,12 +1002,17 @@ export default function Settings({ ); const newStatsKey = getAccountStorageKey(newEmail, "alias_stats"); const newFavoritesKey = getAccountStorageKey(newEmail, "favorites"); + const newWebsiteAliasesKey = getAccountStorageKey( + newEmail, + "website_alias_map", + ); // Get old data const oldData = await browser.storage.local.get([ oldHistoryKey, oldStatsKey, oldFavoritesKey, + oldWebsiteAliasesKey, ]); // Save to new keys @@ -942,6 +1020,7 @@ export default function Settings({ [newHistoryKey]: oldData[oldHistoryKey] || [], [newStatsKey]: oldData[oldStatsKey] || { total: 0, tags: {} }, [newFavoritesKey]: oldData[oldFavoritesKey] || [], + [newWebsiteAliasesKey]: oldData[oldWebsiteAliasesKey] || {}, }); // Delete old keys @@ -949,6 +1028,7 @@ export default function Settings({ oldHistoryKey, oldStatsKey, oldFavoritesKey, + oldWebsiteAliasesKey, ]); // Update base_email if this is the active account @@ -960,7 +1040,7 @@ export default function Settings({ // Update account in list const updated = emailAccounts.map((acc) => acc.id === accountId - ? { ...acc, label: editingLabel.trim(), email: editingEmail.trim() } + ? { ...acc, label: editingLabel.trim(), email: newEmail } : acc, ); @@ -1071,7 +1151,7 @@ export default function Settings({ className={`h-9 min-w-0 rounded-lg px-2 text-xs font-medium transition-colors ${ activeTab === "general" ? "bg-background text-primary shadow-sm" - : "text-muted-foreground hover:text-foreground" + : "text-muted-foreground hover:bg-muted hover:text-foreground" }`} > ), }, + { + id: "inline-helper-sites", + title: t("inlineDisabledSites"), + icon: ( + + + + ), + description: ( +
+

+ {t("inlineDisabledSitesDescription")} +

+ {disabledInlineSites.length === 0 ? ( +

+ {t("noDisabledSites")} +

+ ) : ( +
+ {disabledInlineSites.map((site) => ( +
+ + {site} + + +
+ ))} +
+ )} +
+ ), + }, { id: "alias-generation", title: t("aliasGeneration"), @@ -1459,7 +1595,7 @@ export default function Settings({ @@ -1531,11 +1667,7 @@ export default function Settings({ ); } - // skipcq: JS-0415 return ( - // skipcq: JS-0415
-
-

Statistics

+
+

+ {t("statistics")} +

-
- } - value={} - label="Total Generated" - /> - } - value={} - label="Created Today" - /> - } - value={} - label="This Week" - /> - } - value={stats.mostUsedTag} - label="Top Tag" - /> +
+ {( + [ + { label: t("metricsTab"), value: "metrics" }, + { label: t("timelineTab"), value: "timeline" }, + ...(Object.keys(stats.tags).length >= 2 + ? [{ label: t("tagsTab"), value: "tags" as const }] + : []), + ] as const + ).map((tab) => ( + + ))}
+ + {activeTab === "metrics" && ( +
+ } + value={} + label={t("totalGenerated")} + /> + } + value={} + label={t("createdToday")} + /> + } + value={} + label={t("thisWeek")} + /> + } + value={} + label={t("thisMonth")} + /> + } + value={stats.mostUsedTag} + label={t("topTag")} + /> + } + value={} + label={t("totalTags")} + /> +
+ )} + + {activeTab === "timeline" && ( +
+ +
+ )} + + {activeTab === "tags" && ( +
+ +
+ )}
); } diff --git a/entrypoints/popup/utils.ts b/entrypoints/popup/utils.ts index c3883c6..a9cde13 100644 --- a/entrypoints/popup/utils.ts +++ b/entrypoints/popup/utils.ts @@ -214,18 +214,16 @@ export function filterAliases( sortBy: "recent" | "alphabetical"; }, ): Array<{ email: string; timestamp: number }> { + const normalizedSearchQuery = opts.searchQuery.trim().toLowerCase(); + const favoriteEmails = new Set(opts.favorites); + return aliases .filter((alias) => { - if ( - opts.viewMode === "favorites" && - !opts.favorites.includes(alias.email) - ) + if (opts.viewMode === "favorites" && !favoriteEmails.has(alias.email)) return false; if ( - opts.searchQuery && - !alias.email - .toLowerCase() - .includes(opts.searchQuery.trim().toLowerCase()) + normalizedSearchQuery && + !alias.email.toLowerCase().includes(normalizedSearchQuery) ) return false; if (opts.filterTag !== "all") { @@ -241,3 +239,36 @@ export function filterAliases( : a.email.localeCompare(b.email), ); } + +/** Extracts the unique plus-addressing tags used by a history collection. */ +export function getAliasTags(aliases: Array<{ email: string }>): string[] { + return [ + ...new Set( + aliases + .map((alias) => alias.email.match(/\+([^@]+)@/)?.[1]) + .filter((tag): tag is string => Boolean(tag)), + ), + ]; +} + +/** Returns a safe page slice and its normalized pagination metadata. */ +export function paginateItems( + items: T[], + requestedPage: number, + itemsPerPage: number, +) { + const safeItemsPerPage = Math.max(1, itemsPerPage); + const totalPages = Math.max(1, Math.ceil(items.length / safeItemsPerPage)); + const currentPage = Math.min(Math.max(1, requestedPage), totalPages); + const startIndex = (currentPage - 1) * safeItemsPerPage; + const endIndex = startIndex + safeItemsPerPage; + + return { + items: items.slice(startIndex, endIndex), + currentPage, + totalPages, + startIndex, + endIndex, + totalItems: items.length, + }; +} diff --git a/package.json b/package.json index 2ef8a68..34047ef 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "gmail-alias-toolkit", "description": "Generate and manage Gmail aliases with plus addressing and presets", "private": true, - "version": "1.2.0", + "version": "1.3.0", "author": "dev@eplus.dev", "license": "MIT", "homepage_url": "https://eplus.dev", @@ -37,7 +37,8 @@ "qrcode": "^1.5.4", "react": "^19.2.3", "react-dom": "^19.2.3", - "tailwind-merge": "^2.6.0" + "tailwind-merge": "^2.6.0", + "tldts": "^7.4.8" }, "devDependencies": { "@tailwindcss/postcss": "^4.3.2", diff --git a/public/_locales/de/messages.json b/public/_locales/de/messages.json index 3150b95..81ed7ce 100644 --- a/public/_locales/de/messages.json +++ b/public/_locales/de/messages.json @@ -3,10 +3,10 @@ "message": "Gmail Alias Toolkit" }, "extensionDescription": { - "message": "Gmail-Aliase mit Plus-Adressierung und Vorlagen erstellen und verwalten" + "message": "Gmail-Aliasse mit +tag-Adressen und Vorlagen erstellen und verwalten" }, "headerSubtitle": { - "message": "Aliase mit Plus-Adressierung erstellen" + "message": "Aliasse mit +tag-Adressen erstellen" }, "settings": { "message": "Einstellungen" @@ -45,7 +45,7 @@ "message": "deine.email" }, "accountLabelPlaceholder": { - "message": "Bezeichnung (optional, z. B. Arbeit, Privat)" + "message": "Label (optional, z. B. Arbeit, Privat)" }, "pressTabToAddGmail": { "message": "Drücke $KEY$, um @gmail.com hinzuzufügen", @@ -74,7 +74,7 @@ "message": "Tricks" }, "generating": { - "message": "Wird generiert" + "message": "Wird erstellt" }, "copied": { "message": "Kopiert" @@ -89,7 +89,7 @@ "message": "Alias" }, "copyEmailTooltip": { - "message": "$EMAIL$ - klicken zum Kopieren", + "message": "$EMAIL$ - zum Kopieren klicken", "placeholders": { "email": { "content": "$1" @@ -108,7 +108,7 @@ "message": "Punkt-Trick" }, "plusTags": { - "message": "Plus-Tags (+)" + "message": "Plus- (+) Tags" }, "googlemail": { "message": "Googlemail" @@ -129,13 +129,13 @@ "message": "Punktpositionen zufällig wählen" }, "sequential": { - "message": "Nacheinander" + "message": "Fortlaufend" }, "generateTricks": { - "message": "Tricks generieren" + "message": "Tricks erstellen" }, "generatedVariations": { - "message": "Generierte Varianten" + "message": "Erstellte Varianten" }, "gmailTrickInfoLabel": { "message": "Gmail-Trick:" @@ -159,10 +159,10 @@ "message": "Zeitstempel (1234567890)" }, "numberOfAliases": { - "message": "Anzahl der Aliase" + "message": "Anzahl der Aliasse" }, "generateRandomAliases": { - "message": "$COUNT$ zufällige Aliase generieren", + "message": "$COUNT$ zufällige Alias$PLURAL$ erstellen", "placeholders": { "count": { "content": "$1" @@ -173,7 +173,7 @@ } }, "generatedAliases": { - "message": "Generierte Aliase" + "message": "Erstellte Aliasse" }, "totalCount": { "message": "$COUNT$ insgesamt", @@ -184,7 +184,7 @@ } }, "copiedAliases": { - "message": "$COUNT$ Aliase kopiert!", + "message": "$COUNT$ Aliasse kopiert!", "placeholders": { "count": { "content": "$1" @@ -207,10 +207,10 @@ "message": "Unix-Zeitstempel" }, "tagPlaceholder": { - "message": "Tag eingeben (z. B. shopping, arbeit)" + "message": "Tag eingeben (z. B. shopping, work)" }, "generate": { - "message": "Generieren" + "message": "Erstellen" }, "yourPresets": { "message": "Deine Vorlagen" @@ -222,7 +222,7 @@ "message": "Beispiel kopieren" }, "recentAliases": { - "message": "Aktuelle Aliase" + "message": "Aktuelle Aliasse" }, "favorites": { "message": "Favoriten" @@ -234,7 +234,7 @@ "message": "Als JSON exportieren" }, "selectAliases": { - "message": "Aliase auswählen" + "message": "Aliasse auswählen" }, "select": { "message": "Auswählen" @@ -248,7 +248,7 @@ } }, "deselectAll": { - "message": "Auswahl aufheben" + "message": "Alle abwählen" }, "selectAll": { "message": "Alle auswählen" @@ -286,7 +286,7 @@ } }, "searchAliases": { - "message": "Aliase suchen..." + "message": "Aliasse suchen..." }, "allTags": { "message": "Alle Tags" @@ -319,7 +319,7 @@ "message": "Zu Favoriten hinzufügen" }, "showingRange": { - "message": "$START$–$END$ von $TOTAL$ werden angezeigt", + "message": "Zeige $START$-$END$ von $TOTAL$", "placeholders": { "start": { "content": "$1" @@ -383,13 +383,13 @@ "message": "Badge-Zähler" }, "badgeNone": { - "message": "Keine (Ausgeblendet)" + "message": "Keine (ausgeblendet)" }, "badgeTotal": { "message": "Gesamt im Verlauf" }, "badgeAllTime": { - "message": "Insgesamt generiert" + "message": "Insgesamt erstellt (alle Zeiten)" }, "badgeToday": { "message": "Heute erstellt" @@ -401,16 +401,16 @@ "message": "Kopierbenachrichtigungen" }, "aliasGeneration": { - "message": "Alias-Generierung" + "message": "Alias-Erstellung" }, "randomAliasFormat": { - "message": "Format für zufällige Aliase" + "message": "Format für zufällige Aliasse" }, "autoSaveLimit": { "message": "Limit für automatisches Speichern" }, "aliasesLimit": { - "message": "$COUNT$ Aliase", + "message": "$COUNT$ Aliasse", "placeholders": { "count": { "content": "$1" @@ -421,7 +421,7 @@ "message": "Benutzerdefinierte Vorlagen" }, "label": { - "message": "Bezeichnung" + "message": "Label" }, "tag": { "message": "Tag" @@ -454,16 +454,16 @@ "message": "Zurücksetzen" }, "clearHistoryTitle": { - "message": "Aktuelle Aliase löschen?" + "message": "Aktuelle Aliasse löschen?" }, "clearHistoryMessage": { - "message": "Dadurch werden alle Aliase aus der Liste des aktuellen Verlaufs entfernt." + "message": "Dadurch werden alle Aliasse aus der Liste des aktuellen Verlaufs entfernt." }, "emailAccounts": { "message": "E-Mail-Konten" }, "manageAccountsDescription": { - "message": "Verwalte deine Gmail-Konten. Jedes Konto hat seinen eigenen Verlauf, eigene Statistiken und Favoriten." + "message": "Verwalte deine Gmail-Konten. Jedes Konto hat seinen eigenen Verlauf, Statistiken und Favoriten." }, "noAccountsFound": { "message": "Keine Konten gefunden. Bitte füge ein Konto über den Hauptbildschirm hinzu." @@ -472,7 +472,7 @@ "message": "E-Mail-Adresse" }, "accountLabel": { - "message": "Kontobezeichnung" + "message": "Kontolabel" }, "emailAddressPlaceholder": { "message": "deine.email@gmail.com" @@ -499,7 +499,7 @@ "message": "Konto löschen?" }, "deleteAccountMessage": { - "message": "„$LABEL$“ ($EMAIL$) löschen?\n\nDies löscht dauerhaft:\n- Den gesamten Verlauf dieses Kontos\n- Alle Statistiken\n- Alle Favoriten\n\nDiese Aktion kann nicht rückgängig gemacht werden.", + "message": "\"$LABEL$\" ($EMAIL$) löschen?\n\nDadurch wird dauerhaft gelöscht:\n- Der gesamte Verlauf für dieses Konto\n- Alle Statistiken\n- Alle Favoriten\n\nDiese Aktion kann nicht rückgängig gemacht werden.", "placeholders": { "label": { "content": "$1" @@ -516,7 +516,7 @@ "message": "Konto-E-Mail ändern?" }, "changeAccountEmailMessage": { - "message": "E-Mail ändern von\n$OLD_EMAIL$\nzu\n$NEW_EMAIL$?\n\nDies wird:\n- Den gesamten Verlauf, alle Statistiken und Favoriten zur neuen E-Mail migrieren\n- Die Konto-E-Mail aktualisieren\n- Mit der alten E-Mail verknüpfte Daten löschen\n\nFortfahren?", + "message": "E-Mail ändern von\n$OLD_EMAIL$\nzu\n$NEW_EMAIL$?\n\nDies wird:\n- Den gesamten Verlauf, Statistiken und Favoriten zur neuen E-Mail migrieren\n- Die Konto-E-Mail aktualisieren\n- Daten löschen, die mit der alten E-Mail verknüpft sind\n\nFortfahren?", "placeholders": { "old_email": { "content": "$1" @@ -529,6 +529,12 @@ "changeEmail": { "message": "E-Mail ändern" }, + "menuForThisWebsite": { + "message": "Für diese Website" + }, + "menuLoading": { + "message": "Wird geladen..." + }, "menuRandomEmailAlias": { "message": "Zufälliger E-Mail-Alias" }, @@ -536,13 +542,13 @@ "message": "Benutzerdefinierte Tags" }, "menuNoPresets": { - "message": "Keine Vorlagen – in den Einstellungen hinzufügen" + "message": "Keine Vorlagen - in Einstellungen hinzufügen" }, "menuGmailTricks": { "message": "Gmail-Tricks" }, "menuDotVariation": { - "message": "Punkt-Variation" + "message": "Punktvariante" }, "menuGooglemailDomain": { "message": "Googlemail-Domain" @@ -563,7 +569,7 @@ "message": "Einstellungen importiert" }, "toastImportFailed": { - "message": "Import fehlgeschlagen – ungültige Datei" + "message": "Import fehlgeschlagen - ungültige Datei" }, "toastSettingsReset": { "message": "Einstellungen auf Standard zurückgesetzt" @@ -586,7 +592,7 @@ } }, "errorLabelRequired": { - "message": "Bezeichnung darf nicht leer sein" + "message": "Label darf nicht leer sein" }, "errorInvalidEmail": { "message": "Bitte gib eine gültige E-Mail-Adresse ein" @@ -604,7 +610,7 @@ "message": "QR-Code konnte nicht erstellt werden" }, "toastExportedAliases": { - "message": "$COUNT$ Aliase exportiert", + "message": "$COUNT$ Aliasse exportiert", "placeholders": { "count": { "content": "$1" @@ -612,7 +618,7 @@ } }, "toastDeletedAliases": { - "message": "$COUNT$ Aliase gelöscht", + "message": "$COUNT$ Aliasse gelöscht", "placeholders": { "count": { "content": "$1" @@ -643,7 +649,7 @@ "message": "Willkommen bei Gmail Alias Toolkit" }, "welcomeSubtitle": { - "message": "Erstelle unbegrenzt E-Mail-Aliase für Datenschutz und Organisation" + "message": "Erstelle unbegrenzt E-Mail-Aliasse für Datenschutz und Organisation" }, "letsGetStarted": { "message": "Los geht’s" @@ -660,7 +666,7 @@ } }, "settingUp": { - "message": "Einrichtung läuft..." + "message": "Wird eingerichtet..." }, "getStarted": { "message": "Starten" @@ -672,7 +678,7 @@ "message": "Was du tun kannst:" }, "featurePrivateEmail": { - "message": "Private-E-Mail-Generator" + "message": "Generator für private E-Mails" }, "featureCustomTags": { "message": "Benutzerdefinierte Tags & Vorlagen" @@ -692,6 +698,56 @@ } }, "clickStarHint": { - "message": "Klicke bei einem Alias im Verlauf auf den Stern, um ihn hier hinzuzufügen" + "message": "Klicke im Verlauf bei einem Alias auf den Stern, um ihn hier hinzuzufügen" + }, + "viewStatistics": { + "message": "Statistiken anzeigen" + }, + "statistics": { + "message": "Statistiken" + }, + "metricsTab": { + "message": "Metriken" + }, + "timelineTab": { + "message": "Zeitachse" + }, + "tagsTab": { + "message": "Tags" + }, + "totalGenerated": { + "message": "Insgesamt erstellt" + }, + "createdToday": { + "message": "Heute erstellt" + }, + "thisWeek": { + "message": "Diese Woche" + }, + "thisMonth": { + "message": "Diesen Monat" + }, + "topTag": { + "message": "Top-Tag" + }, + "totalTags": { + "message": "Tags insgesamt" + }, + "noTagsYet": { + "message": "Noch keine Tags" + }, + "disableInlineForSite": { "message": "Inline-Hilfe auf dieser Website deaktivieren" }, + "inlineDisabledSites": { "message": "Websites mit deaktivierter Inline-Hilfe" }, + "inlineDisabledSitesDescription": { "message": "Inline-Symbol und Schnell-Popup sind auf diesen Websites ausgeblendet." }, + "noDisabledSites": { "message": "Keine deaktivierten Websites" }, + "enableInlineForSite": { "message": "Aktivieren" }, + "reportReview": { "message": "Problem melden / Bewerten" }, + "percentLabel": { + "message": "$PERCENT$%", + "placeholders": { + "percent": { + "content": "$1" + } + } } } diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index 940341f..37376cc 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -1,301 +1,753 @@ { - "extensionName": { "message": "Gmail Alias Toolkit" }, + "extensionName": { + "message": "Gmail Alias Toolkit" + }, "extensionDescription": { - "message": "Generate and manage Gmail aliases with plus addressing and presets" - }, - "headerSubtitle": { "message": "Generate aliases with plus addressing" }, - "settings": { "message": "Settings" }, - "back": { "message": "Back" }, - "close": { "message": "Close" }, - "cancel": { "message": "Cancel" }, - "copy": { "message": "Copy" }, - "copyAll": { "message": "Copy All" }, - "copyToClipboard": { "message": "Copy to clipboard" }, - "activeGmailAddress": { "message": "Active Gmail Address" }, - "addNewAccount": { "message": "Add New Account" }, - "addAccount": { "message": "Add Account" }, - "addNewAccountTitle": { "message": "Add New Account" }, - "emailPlaceholder": { "message": "your.email" }, + "message": "Generate and manage Gmail aliases using +tag addressing and presets" + }, + "headerSubtitle": { + "message": "Generate aliases using +tag addressing" + }, + "settings": { + "message": "Settings" + }, + "back": { + "message": "Back" + }, + "close": { + "message": "Close" + }, + "cancel": { + "message": "Cancel" + }, + "copy": { + "message": "Copy" + }, + "copyAll": { + "message": "Copy All" + }, + "copyToClipboard": { + "message": "Copy to clipboard" + }, + "activeGmailAddress": { + "message": "Active Gmail Address" + }, + "addNewAccount": { + "message": "Add New Account" + }, + "addAccount": { + "message": "Add Account" + }, + "addNewAccountTitle": { + "message": "Add New Account" + }, + "emailPlaceholder": { + "message": "your.email" + }, "accountLabelPlaceholder": { "message": "Label (optional, e.g., Work, Personal)" }, "pressTabToAddGmail": { "message": "Press $KEY$ to add @gmail.com", - "placeholders": { "key": { "content": "$1" } } + "placeholders": { + "key": { + "content": "$1" + } + } }, "gmailWarning": { "message": "This doesn't look like a Gmail address. Plus addressing works best with Gmail." }, - "random": { "message": "Random" }, - "customTags": { "message": "Custom Tags" }, - "gmailTricks": { "message": "Gmail Tricks" }, - "tabTagsShort": { "message": "Tags" }, - "tabTricksShort": { "message": "Tricks" }, - "generating": { "message": "Generating" }, - "copied": { "message": "Copied" }, - "switchToLightMode": { "message": "Switch to light mode" }, - "switchToDarkMode": { "message": "Switch to dark mode" }, - "aliasColumn": { "message": "Alias" }, + "random": { + "message": "Random" + }, + "customTags": { + "message": "Custom Tags" + }, + "gmailTricks": { + "message": "Gmail Tricks" + }, + "tabTagsShort": { + "message": "Tags" + }, + "tabTricksShort": { + "message": "Tricks" + }, + "generating": { + "message": "Generating" + }, + "copied": { + "message": "Copied" + }, + "switchToLightMode": { + "message": "Switch to light mode" + }, + "switchToDarkMode": { + "message": "Switch to dark mode" + }, + "aliasColumn": { + "message": "Alias" + }, "copyEmailTooltip": { "message": "$EMAIL$ - click to copy", - "placeholders": { "email": { "content": "$1" } } + "placeholders": { + "email": { + "content": "$1" + } + } }, "pageLabel": { "message": "Page $PAGE$", - "placeholders": { "page": { "content": "$1" } } - }, - "dotTrick": { "message": "Dot Trick" }, - "plusTags": { "message": "Plus (+) Tags" }, - "googlemail": { "message": "Googlemail" }, - "removeDots": { "message": "Remove Dots" }, - "dotPlus": { "message": "Dot + Plus" }, - "allCombos": { "message": "All Combos" }, - "numberOfVariations": { "message": "Number of variations" }, - "randomizeDotPositions": { "message": "Randomize dot positions" }, - "sequential": { "message": "Sequential" }, - "generateTricks": { "message": "Generate Tricks" }, - "generatedVariations": { "message": "Generated Variations" }, - "gmailTrickInfoLabel": { "message": "Gmail trick:" }, + "placeholders": { + "page": { + "content": "$1" + } + } + }, + "dotTrick": { + "message": "Dot Trick" + }, + "plusTags": { + "message": "Plus (+) Tags" + }, + "googlemail": { + "message": "Googlemail" + }, + "removeDots": { + "message": "Remove Dots" + }, + "dotPlus": { + "message": "Dot + Plus" + }, + "allCombos": { + "message": "All Combos" + }, + "numberOfVariations": { + "message": "Number of variations" + }, + "randomizeDotPositions": { + "message": "Randomize dot positions" + }, + "sequential": { + "message": "Sequential" + }, + "generateTricks": { + "message": "Generate Tricks" + }, + "generatedVariations": { + "message": "Generated Variations" + }, + "gmailTrickInfoLabel": { + "message": "Gmail trick:" + }, "gmailTrickInfo": { "message": "Dots are ignored and everything after + goes to the same inbox" }, - "format": { "message": "Format" }, - "privateMailFormat": { "message": "Private Mail (private-mail-xxxx)" }, - "randomCharactersFormat": { "message": "Random Characters (abc123xy)" }, - "randomWordsFormat": { "message": "Random Words (happy-fox-42)" }, - "timestampFormat": { "message": "Timestamp (1234567890)" }, - "numberOfAliases": { "message": "Number of aliases" }, + "format": { + "message": "Format" + }, + "privateMailFormat": { + "message": "Private Mail (private-mail-xxxx)" + }, + "randomCharactersFormat": { + "message": "Random Characters (abc123xy)" + }, + "randomWordsFormat": { + "message": "Random Words (happy-fox-42)" + }, + "timestampFormat": { + "message": "Timestamp (1234567890)" + }, + "numberOfAliases": { + "message": "Number of aliases" + }, "generateRandomAliases": { "message": "Generate $COUNT$ Random Alias$PLURAL$", "placeholders": { - "count": { "content": "$1" }, - "plural": { "content": "$2" } + "count": { + "content": "$1" + }, + "plural": { + "content": "$2" + } } }, - "generatedAliases": { "message": "Generated Aliases" }, + "generatedAliases": { + "message": "Generated Aliases" + }, "totalCount": { "message": "$COUNT$ total", - "placeholders": { "count": { "content": "$1" } } + "placeholders": { + "count": { + "content": "$1" + } + } }, "copiedAliases": { "message": "Copied $COUNT$ aliases!", - "placeholders": { "count": { "content": "$1" } } - }, - "failedToCopy": { "message": "Failed to copy" }, - "formatPrivateMail": { "message": "Format: private-mail-xxxx" }, - "formatAlphanumeric": { "message": "8 random characters" }, - "formatWords": { "message": "2 random words" }, - "formatTimestamp": { "message": "Unix timestamp" }, - "tagPlaceholder": { "message": "Enter tag (e.g., shopping, work)" }, - "generate": { "message": "Generate" }, - "yourPresets": { "message": "Your Presets" }, - "example": { "message": "Example:" }, - "copyExample": { "message": "Copy example" }, - "recentAliases": { "message": "Recent Aliases" }, - "favorites": { "message": "Favorites" }, - "exportAsCsv": { "message": "Export as CSV" }, - "exportAsJson": { "message": "Export as JSON" }, - "selectAliases": { "message": "Select aliases" }, - "select": { "message": "Select" }, + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "failedToCopy": { + "message": "Failed to copy" + }, + "formatPrivateMail": { + "message": "Format: private-mail-xxxx" + }, + "formatAlphanumeric": { + "message": "8 random characters" + }, + "formatWords": { + "message": "2 random words" + }, + "formatTimestamp": { + "message": "Unix timestamp" + }, + "tagPlaceholder": { + "message": "Enter tag (e.g., shopping, work)" + }, + "generate": { + "message": "Generate" + }, + "yourPresets": { + "message": "Your Presets" + }, + "example": { + "message": "Example:" + }, + "copyExample": { + "message": "Copy example" + }, + "recentAliases": { + "message": "Recent Aliases" + }, + "favorites": { + "message": "Favorites" + }, + "exportAsCsv": { + "message": "Export as CSV" + }, + "exportAsJson": { + "message": "Export as JSON" + }, + "selectAliases": { + "message": "Select aliases" + }, + "select": { + "message": "Select" + }, "starredCount": { "message": "$COUNT$ starred", - "placeholders": { "count": { "content": "$1" } } + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "deselectAll": { + "message": "Deselect All" + }, + "selectAll": { + "message": "Select All" }, - "deselectAll": { "message": "Deselect All" }, - "selectAll": { "message": "Select All" }, "selectedCount": { "message": "$COUNT$ selected", - "placeholders": { "count": { "content": "$1" } } + "placeholders": { + "count": { + "content": "$1" + } + } }, "deleteCount": { "message": "Delete $COUNT$", - "placeholders": { "count": { "content": "$1" } } + "placeholders": { + "count": { + "content": "$1" + } + } }, "allCount": { "message": "All ($COUNT$)", - "placeholders": { "count": { "content": "$1" } } + "placeholders": { + "count": { + "content": "$1" + } + } }, "favoritesCount": { "message": "Favorites ($COUNT$)", - "placeholders": { "count": { "content": "$1" } } + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "searchAliases": { + "message": "Search aliases..." + }, + "allTags": { + "message": "All Tags" + }, + "mostRecent": { + "message": "Most Recent" + }, + "az": { + "message": "A-Z" + }, + "noFavoritesYet": { + "message": "No favorites yet" }, - "searchAliases": { "message": "Search aliases..." }, - "allTags": { "message": "All Tags" }, - "mostRecent": { "message": "Most Recent" }, - "az": { "message": "A-Z" }, - "noFavoritesYet": { "message": "No favorites yet" }, "starEmailsHint": { "message": "Star emails from your history to quick access them here" }, - "noResultsFound": { "message": "No results found" }, - "differentSearchHint": { "message": "Try a different search or filter" }, - "showQrCode": { "message": "Show QR code" }, - "removeFromFavorites": { "message": "Remove from favorites" }, - "addToFavorites": { "message": "Add to favorites" }, + "noResultsFound": { + "message": "No results found" + }, + "differentSearchHint": { + "message": "Try a different search or filter" + }, + "showQrCode": { + "message": "Show QR code" + }, + "removeFromFavorites": { + "message": "Remove from favorites" + }, + "addToFavorites": { + "message": "Add to favorites" + }, "showingRange": { "message": "Showing $START$-$END$ of $TOTAL$", "placeholders": { - "start": { "content": "$1" }, - "end": { "content": "$2" }, - "total": { "content": "$3" } + "start": { + "content": "$1" + }, + "end": { + "content": "$2" + }, + "total": { + "content": "$3" + } } }, "perPage": { "message": "$COUNT$ / page", - "placeholders": { "count": { "content": "$1" } } - }, - "firstPage": { "message": "First page" }, - "previousPage": { "message": "Previous page" }, - "nextPage": { "message": "Next page" }, - "lastPage": { "message": "Last page" }, - "scanToCopyAlias": { "message": "Scan to copy alias" }, - "general": { "message": "General" }, - "accounts": { "message": "Accounts" }, - "changelog": { "message": "Changelog" }, - "appearanceDisplay": { "message": "Appearance & Display" }, - "theme": { "message": "Theme" }, - "themeLight": { "message": "Light" }, - "themeDark": { "message": "Dark" }, - "themeAuto": { "message": "System (Auto)" }, - "badgeCounter": { "message": "Badge Counter" }, - "badgeNone": { "message": "None (Hidden)" }, - "badgeTotal": { "message": "Total in History" }, - "badgeAllTime": { "message": "Total Generated (All Time)" }, - "badgeToday": { "message": "Created Today" }, - "badgeWeek": { "message": "This Week" }, - "copyNotifications": { "message": "Copy notifications" }, - "aliasGeneration": { "message": "Alias Generation" }, - "randomAliasFormat": { "message": "Random Alias Format" }, - "autoSaveLimit": { "message": "Auto-save Limit" }, + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "firstPage": { + "message": "First page" + }, + "previousPage": { + "message": "Previous page" + }, + "nextPage": { + "message": "Next page" + }, + "lastPage": { + "message": "Last page" + }, + "scanToCopyAlias": { + "message": "Scan to copy alias" + }, + "general": { + "message": "General" + }, + "accounts": { + "message": "Accounts" + }, + "changelog": { + "message": "Changelog" + }, + "appearanceDisplay": { + "message": "Appearance & Display" + }, + "theme": { + "message": "Theme" + }, + "themeLight": { + "message": "Light" + }, + "themeDark": { + "message": "Dark" + }, + "themeAuto": { + "message": "System (Auto)" + }, + "badgeCounter": { + "message": "Badge Counter" + }, + "badgeNone": { + "message": "None (Hidden)" + }, + "badgeTotal": { + "message": "Total in History" + }, + "badgeAllTime": { + "message": "Total Generated (All Time)" + }, + "badgeToday": { + "message": "Created Today" + }, + "badgeWeek": { + "message": "This Week" + }, + "copyNotifications": { + "message": "Copy notifications" + }, + "aliasGeneration": { + "message": "Alias Generation" + }, + "randomAliasFormat": { + "message": "Random Alias Format" + }, + "autoSaveLimit": { + "message": "Auto-save Limit" + }, "aliasesLimit": { "message": "$COUNT$ aliases", - "placeholders": { "count": { "content": "$1" } } - }, - "customPresets": { "message": "Custom Presets" }, - "label": { "message": "Label" }, - "tag": { "message": "Tag" }, - "addPreset": { "message": "+ Add Preset" }, - "dataManagement": { "message": "Data Management" }, - "export": { "message": "Export" }, - "import": { "message": "Import" }, - "clear": { "message": "Clear" }, - "resetSettings": { "message": "Reset all settings to default" }, - "resetSettingsTitle": { "message": "Reset settings?" }, + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "customPresets": { + "message": "Custom Presets" + }, + "label": { + "message": "Label" + }, + "tag": { + "message": "Tag" + }, + "addPreset": { + "message": "+ Add Preset" + }, + "dataManagement": { + "message": "Data Management" + }, + "export": { + "message": "Export" + }, + "import": { + "message": "Import" + }, + "clear": { + "message": "Clear" + }, + "resetSettings": { + "message": "Reset all settings to default" + }, + "resetSettingsTitle": { + "message": "Reset settings?" + }, "resetSettingsMessage": { "message": "This will restore every setting to its default value." }, - "reset": { "message": "Reset" }, - "clearHistoryTitle": { "message": "Clear recent aliases?" }, + "reset": { + "message": "Reset" + }, + "clearHistoryTitle": { + "message": "Clear recent aliases?" + }, "clearHistoryMessage": { "message": "This removes all aliases from the recent history list." }, - "emailAccounts": { "message": "Email Accounts" }, + "emailAccounts": { + "message": "Email Accounts" + }, "manageAccountsDescription": { "message": "Manage your Gmail accounts. Each account has its own history, statistics, and favorites." }, "noAccountsFound": { "message": "No accounts found. Please add an account from the main screen." }, - "emailAddress": { "message": "Email Address" }, - "accountLabel": { "message": "Account label" }, - "emailAddressPlaceholder": { "message": "your.email@gmail.com" }, + "emailAddress": { + "message": "Email Address" + }, + "accountLabel": { + "message": "Account label" + }, + "emailAddressPlaceholder": { + "message": "your.email@gmail.com" + }, "emailChangeWarning": { "message": "Changing email will migrate all data to the new email address" }, - "saveChanges": { "message": "Save Changes" }, - "active": { "message": "Active" }, - "editAccount": { "message": "Edit account" }, + "saveChanges": { + "message": "Save Changes" + }, + "active": { + "message": "Active" + }, + "editAccount": { + "message": "Edit account" + }, "cannotDeleteLastAccountTitle": { "message": "Cannot delete the last account" }, - "deleteThisAccount": { "message": "Delete this account" }, - "deleteAccountTitle": { "message": "Delete account?" }, + "deleteThisAccount": { + "message": "Delete this account" + }, + "deleteAccountTitle": { + "message": "Delete account?" + }, "deleteAccountMessage": { "message": "Delete \"$LABEL$\" ($EMAIL$)?\n\nThis will permanently delete:\n- All history for this account\n- All statistics\n- All favorites\n\nThis action cannot be undone.", "placeholders": { - "label": { "content": "$1" }, - "email": { "content": "$2" } + "label": { + "content": "$1" + }, + "email": { + "content": "$2" + } } }, - "delete": { "message": "Delete" }, - "changeAccountEmailTitle": { "message": "Change account email?" }, + "delete": { + "message": "Delete" + }, + "changeAccountEmailTitle": { + "message": "Change account email?" + }, "changeAccountEmailMessage": { "message": "Change email from\n$OLD_EMAIL$\nto\n$NEW_EMAIL$?\n\nThis will:\n- Migrate all history, statistics, and favorites to the new email\n- Update the account email\n- Delete data associated with the old email\n\nContinue?", "placeholders": { - "old_email": { "content": "$1" }, - "new_email": { "content": "$2" } + "old_email": { + "content": "$1" + }, + "new_email": { + "content": "$2" + } } }, - "changeEmail": { "message": "Change email" }, - "menuRandomEmailAlias": { "message": "Random Email Alias" }, - "menuCustomTags": { "message": "Custom Tags" }, - "menuNoPresets": { "message": "No presets - Add in Settings" }, - "menuGmailTricks": { "message": "Gmail Tricks" }, - "menuDotVariation": { "message": "Dot Variation" }, - "menuGooglemailDomain": { "message": "Googlemail Domain" }, - "menuRemoveAllDots": { "message": "Remove All Dots" }, - "toastPresetAdded": { "message": "Preset added" }, - "toastPresetRemoved": { "message": "Preset removed" }, - "toastSettingsExported": { "message": "Settings exported" }, - "toastSettingsImported": { "message": "Settings imported" }, - "toastImportFailed": { "message": "Import failed - invalid file" }, - "toastSettingsReset": { "message": "Settings reset to default" }, - "toastAccountSwitched": { "message": "Account switched" }, - "toastAccountDeleted": { "message": "Account deleted" }, - "toastAccountUpdated": { "message": "Account updated" }, + "changeEmail": { + "message": "Change email" + }, + "menuForThisWebsite": { + "message": "For this website" + }, + "menuLoading": { + "message": "Loading..." + }, + "menuRandomEmailAlias": { + "message": "Random Email Alias" + }, + "menuCustomTags": { + "message": "Custom Tags" + }, + "menuNoPresets": { + "message": "No presets - Add in Settings" + }, + "menuGmailTricks": { + "message": "Gmail Tricks" + }, + "menuDotVariation": { + "message": "Dot Variation" + }, + "menuGooglemailDomain": { + "message": "Googlemail Domain" + }, + "menuRemoveAllDots": { + "message": "Remove All Dots" + }, + "toastPresetAdded": { + "message": "Preset added" + }, + "toastPresetRemoved": { + "message": "Preset removed" + }, + "toastSettingsExported": { + "message": "Settings exported" + }, + "toastSettingsImported": { + "message": "Settings imported" + }, + "toastImportFailed": { + "message": "Import failed - invalid file" + }, + "toastSettingsReset": { + "message": "Settings reset to default" + }, + "toastAccountSwitched": { + "message": "Account switched" + }, + "toastAccountDeleted": { + "message": "Account deleted" + }, + "toastAccountUpdated": { + "message": "Account updated" + }, "toastAccountAdded": { "message": "$LABEL$ added", - "placeholders": { "label": { "content": "$1" } } + "placeholders": { + "label": { + "content": "$1" + } + } + }, + "errorLabelRequired": { + "message": "Label cannot be empty" + }, + "errorInvalidEmail": { + "message": "Please enter a valid email address" }, - "errorLabelRequired": { "message": "Label cannot be empty" }, - "errorInvalidEmail": { "message": "Please enter a valid email address" }, "errorDuplicateEmail": { "message": "This email address is already used by another account" }, - "errorEnterEmail": { "message": "Please enter an email address" }, - "errorAccountExists": { "message": "This account already exists" }, - "toastQrFailed": { "message": "Failed to generate QR code" }, + "errorEnterEmail": { + "message": "Please enter an email address" + }, + "errorAccountExists": { + "message": "This account already exists" + }, + "toastQrFailed": { + "message": "Failed to generate QR code" + }, "toastExportedAliases": { "message": "Exported $COUNT$ aliases", - "placeholders": { "count": { "content": "$1" } } + "placeholders": { + "count": { + "content": "$1" + } + } }, "toastDeletedAliases": { "message": "Deleted $COUNT$ aliases", - "placeholders": { "count": { "content": "$1" } } + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "toastHistoryCleared": { + "message": "History cleared" + }, + "toastFavoriteAdded": { + "message": "Added to favorites" + }, + "toastFavoriteRemoved": { + "message": "Removed from favorites" }, - "toastHistoryCleared": { "message": "History cleared" }, - "toastFavoriteAdded": { "message": "Added to favorites" }, - "toastFavoriteRemoved": { "message": "Removed from favorites" }, "toastCopiedEmail": { "message": "Copied $EMAIL$", - "placeholders": { "email": { "content": "$1" } } + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "toastCopyFailed": { + "message": "Failed to copy" + }, + "welcomeTitle": { + "message": "Welcome to Gmail Alias Toolkit" }, - "toastCopyFailed": { "message": "Failed to copy" }, - "welcomeTitle": { "message": "Welcome to Gmail Alias Toolkit" }, "welcomeSubtitle": { "message": "Generate unlimited email aliases for privacy and organization" }, - "letsGetStarted": { "message": "Let's get started" }, - "enterGmailAddress": { "message": "Enter your Gmail address" }, + "letsGetStarted": { + "message": "Let's get started" + }, + "enterGmailAddress": { + "message": "Enter your Gmail address" + }, "pressTabForGmail": { "message": "Press $KEY$ for @gmail.com", - "placeholders": { "key": { "content": "$1" } } - }, - "settingUp": { "message": "Setting up..." }, - "getStarted": { "message": "Get Started" }, - "advancedSetup": { "message": "Advanced Setup in Settings" }, - "whatYouCanDo": { "message": "What you can do:" }, - "featurePrivateEmail": { "message": "Private Email Generator" }, - "featureCustomTags": { "message": "Custom Tags & Presets" }, - "featureGmailTricks": { "message": "Gmail Advanced Tricks" }, + "placeholders": { + "key": { + "content": "$1" + } + } + }, + "settingUp": { + "message": "Setting up..." + }, + "getStarted": { + "message": "Get Started" + }, + "advancedSetup": { + "message": "Advanced Setup in Settings" + }, + "whatYouCanDo": { + "message": "What you can do:" + }, + "featurePrivateEmail": { + "message": "Private Email Generator" + }, + "featureCustomTags": { + "message": "Custom Tags & Presets" + }, + "featureGmailTricks": { + "message": "Gmail Advanced Tricks" + }, "welcomeFooter": { "message": "All data is stored locally. No tracking, no server." }, "favoritesSaved": { "message": "$COUNT$ saved", - "placeholders": { "count": { "content": "$1" } } + "placeholders": { + "count": { + "content": "$1" + } + } }, "clickStarHint": { "message": "Click star on any alias in history to add it here" + }, + "viewStatistics": { + "message": "View Statistics" + }, + "statistics": { + "message": "Statistics" + }, + "metricsTab": { + "message": "Metrics" + }, + "timelineTab": { + "message": "Timeline" + }, + "tagsTab": { + "message": "Tags" + }, + "totalGenerated": { + "message": "Total Generated" + }, + "createdToday": { + "message": "Created Today" + }, + "thisWeek": { + "message": "This Week" + }, + "thisMonth": { + "message": "This Month" + }, + "topTag": { + "message": "Top Tag" + }, + "totalTags": { + "message": "Total Tags" + }, + "noTagsYet": { + "message": "No tags yet" + }, + "disableInlineForSite": { "message": "Turn off the inline helper on this website" }, + "inlineDisabledSites": { "message": "Disabled inline helper sites" }, + "inlineDisabledSitesDescription": { "message": "The inline icon and quick popup are hidden on these websites." }, + "noDisabledSites": { "message": "No disabled websites" }, + "enableInlineForSite": { "message": "Enable" }, + "reportReview": { "message": "Report / Review" }, + "percentLabel": { + "message": "$PERCENT$%", + "placeholders": { + "percent": { + "content": "$1" + } + } } } diff --git a/public/_locales/es/messages.json b/public/_locales/es/messages.json new file mode 100644 index 0000000..54eee57 --- /dev/null +++ b/public/_locales/es/messages.json @@ -0,0 +1,753 @@ +{ + "extensionName": { + "message": "Gmail Alias Toolkit" + }, + "extensionDescription": { + "message": "Genera y administra alias de Gmail con direcciones +tag y preajustes" + }, + "headerSubtitle": { + "message": "Genera alias con direcciones +tag" + }, + "settings": { + "message": "Configuración" + }, + "back": { + "message": "Atrás" + }, + "close": { + "message": "Cerrar" + }, + "cancel": { + "message": "Cancelar" + }, + "copy": { + "message": "Copiar" + }, + "copyAll": { + "message": "Copiar todo" + }, + "copyToClipboard": { + "message": "Copiar al portapapeles" + }, + "activeGmailAddress": { + "message": "Dirección de Gmail activa" + }, + "addNewAccount": { + "message": "Agregar nueva cuenta" + }, + "addAccount": { + "message": "Agregar cuenta" + }, + "addNewAccountTitle": { + "message": "Agregar nueva cuenta" + }, + "emailPlaceholder": { + "message": "tu.email" + }, + "accountLabelPlaceholder": { + "message": "Etiqueta (opcional, p. ej., Trabajo, Personal)" + }, + "pressTabToAddGmail": { + "message": "Pulsa $KEY$ para agregar @gmail.com", + "placeholders": { + "key": { + "content": "$1" + } + } + }, + "gmailWarning": { + "message": "Esto no parece una dirección de Gmail. El direccionamiento plus funciona mejor con Gmail." + }, + "random": { + "message": "Aleatorio" + }, + "customTags": { + "message": "Etiquetas personalizadas" + }, + "gmailTricks": { + "message": "Trucos de Gmail" + }, + "tabTagsShort": { + "message": "Etiquetas" + }, + "tabTricksShort": { + "message": "Trucos" + }, + "generating": { + "message": "Generando" + }, + "copied": { + "message": "Copiado" + }, + "switchToLightMode": { + "message": "Cambiar al modo claro" + }, + "switchToDarkMode": { + "message": "Cambiar al modo oscuro" + }, + "aliasColumn": { + "message": "Alias" + }, + "copyEmailTooltip": { + "message": "$EMAIL$ - haz clic para copiar", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "pageLabel": { + "message": "Página $PAGE$", + "placeholders": { + "page": { + "content": "$1" + } + } + }, + "dotTrick": { + "message": "Truco de puntos" + }, + "plusTags": { + "message": "Etiquetas Plus (+)" + }, + "googlemail": { + "message": "Googlemail" + }, + "removeDots": { + "message": "Quitar puntos" + }, + "dotPlus": { + "message": "Punto + Plus" + }, + "allCombos": { + "message": "Todas las combinaciones" + }, + "numberOfVariations": { + "message": "Número de variaciones" + }, + "randomizeDotPositions": { + "message": "Aleatorizar posiciones de puntos" + }, + "sequential": { + "message": "Secuencial" + }, + "generateTricks": { + "message": "Generar trucos" + }, + "generatedVariations": { + "message": "Variaciones generadas" + }, + "gmailTrickInfoLabel": { + "message": "Truco de Gmail:" + }, + "gmailTrickInfo": { + "message": "Los puntos se ignoran y todo lo que va después de + llega a la misma bandeja de entrada" + }, + "format": { + "message": "Formato" + }, + "privateMailFormat": { + "message": "Correo privado (private-mail-xxxx)" + }, + "randomCharactersFormat": { + "message": "Caracteres aleatorios (abc123xy)" + }, + "randomWordsFormat": { + "message": "Palabras aleatorias (happy-fox-42)" + }, + "timestampFormat": { + "message": "Marca de tiempo (1234567890)" + }, + "numberOfAliases": { + "message": "Número de alias" + }, + "generateRandomAliases": { + "message": "Generar $COUNT$ alias aleatorio$PLURAL$", + "placeholders": { + "count": { + "content": "$1" + }, + "plural": { + "content": "$2" + } + } + }, + "generatedAliases": { + "message": "Alias generados" + }, + "totalCount": { + "message": "$COUNT$ en total", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "copiedAliases": { + "message": "¡Se copiaron $COUNT$ alias!", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "failedToCopy": { + "message": "No se pudo copiar" + }, + "formatPrivateMail": { + "message": "Formato: private-mail-xxxx" + }, + "formatAlphanumeric": { + "message": "8 caracteres aleatorios" + }, + "formatWords": { + "message": "2 palabras aleatorias" + }, + "formatTimestamp": { + "message": "Marca de tiempo Unix" + }, + "tagPlaceholder": { + "message": "Introduce una etiqueta (p. ej., shopping, work)" + }, + "generate": { + "message": "Generar" + }, + "yourPresets": { + "message": "Tus preajustes" + }, + "example": { + "message": "Ejemplo:" + }, + "copyExample": { + "message": "Copiar ejemplo" + }, + "recentAliases": { + "message": "Alias recientes" + }, + "favorites": { + "message": "Favoritos" + }, + "exportAsCsv": { + "message": "Exportar como CSV" + }, + "exportAsJson": { + "message": "Exportar como JSON" + }, + "selectAliases": { + "message": "Seleccionar alias" + }, + "select": { + "message": "Seleccionar" + }, + "starredCount": { + "message": "$COUNT$ con estrella", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "deselectAll": { + "message": "Deseleccionar todo" + }, + "selectAll": { + "message": "Seleccionar todo" + }, + "selectedCount": { + "message": "$COUNT$ seleccionados", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "deleteCount": { + "message": "Eliminar $COUNT$", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "allCount": { + "message": "Todos ($COUNT$)", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "favoritesCount": { + "message": "Favoritos ($COUNT$)", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "searchAliases": { + "message": "Buscar alias..." + }, + "allTags": { + "message": "Todas las etiquetas" + }, + "mostRecent": { + "message": "Más recientes" + }, + "az": { + "message": "A-Z" + }, + "noFavoritesYet": { + "message": "Aún no hay favoritos" + }, + "starEmailsHint": { + "message": "Marca correos de tu historial para acceder rápidamente a ellos aquí" + }, + "noResultsFound": { + "message": "No se encontraron resultados" + }, + "differentSearchHint": { + "message": "Prueba otra búsqueda o filtro" + }, + "showQrCode": { + "message": "Mostrar código QR" + }, + "removeFromFavorites": { + "message": "Quitar de favoritos" + }, + "addToFavorites": { + "message": "Agregar a favoritos" + }, + "showingRange": { + "message": "Mostrando $START$-$END$ de $TOTAL$", + "placeholders": { + "start": { + "content": "$1" + }, + "end": { + "content": "$2" + }, + "total": { + "content": "$3" + } + } + }, + "perPage": { + "message": "$COUNT$ / página", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "firstPage": { + "message": "Primera página" + }, + "previousPage": { + "message": "Página anterior" + }, + "nextPage": { + "message": "Página siguiente" + }, + "lastPage": { + "message": "Última página" + }, + "scanToCopyAlias": { + "message": "Escanear para copiar alias" + }, + "general": { + "message": "General" + }, + "accounts": { + "message": "Cuentas" + }, + "changelog": { + "message": "Registro de cambios" + }, + "appearanceDisplay": { + "message": "Apariencia y visualización" + }, + "theme": { + "message": "Tema" + }, + "themeLight": { + "message": "Claro" + }, + "themeDark": { + "message": "Oscuro" + }, + "themeAuto": { + "message": "Sistema (Auto)" + }, + "badgeCounter": { + "message": "Contador de insignia" + }, + "badgeNone": { + "message": "Ninguno (oculto)" + }, + "badgeTotal": { + "message": "Total en el historial" + }, + "badgeAllTime": { + "message": "Total generado (todo el tiempo)" + }, + "badgeToday": { + "message": "Creado hoy" + }, + "badgeWeek": { + "message": "Esta semana" + }, + "copyNotifications": { + "message": "Notificaciones de copia" + }, + "aliasGeneration": { + "message": "Generación de alias" + }, + "randomAliasFormat": { + "message": "Formato de alias aleatorio" + }, + "autoSaveLimit": { + "message": "Límite de guardado automático" + }, + "aliasesLimit": { + "message": "$COUNT$ alias", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "customPresets": { + "message": "Preajustes personalizados" + }, + "label": { + "message": "Etiqueta" + }, + "tag": { + "message": "Etiqueta" + }, + "addPreset": { + "message": "+ Agregar preajuste" + }, + "dataManagement": { + "message": "Gestión de datos" + }, + "export": { + "message": "Exportar" + }, + "import": { + "message": "Importar" + }, + "clear": { + "message": "Borrar" + }, + "resetSettings": { + "message": "Restablecer toda la configuración a los valores predeterminados" + }, + "resetSettingsTitle": { + "message": "¿Restablecer configuración?" + }, + "resetSettingsMessage": { + "message": "Esto restaurará cada ajuste a su valor predeterminado." + }, + "reset": { + "message": "Restablecer" + }, + "clearHistoryTitle": { + "message": "¿Borrar alias recientes?" + }, + "clearHistoryMessage": { + "message": "Esto elimina todos los alias de la lista de historial reciente." + }, + "emailAccounts": { + "message": "Cuentas de correo" + }, + "manageAccountsDescription": { + "message": "Administra tus cuentas de Gmail. Cada cuenta tiene su propio historial, estadísticas y favoritos." + }, + "noAccountsFound": { + "message": "No se encontraron cuentas. Agrega una cuenta desde la pantalla principal." + }, + "emailAddress": { + "message": "Dirección de correo" + }, + "accountLabel": { + "message": "Etiqueta de cuenta" + }, + "emailAddressPlaceholder": { + "message": "tu.email@gmail.com" + }, + "emailChangeWarning": { + "message": "Cambiar el correo migrará todos los datos a la nueva dirección de correo" + }, + "saveChanges": { + "message": "Guardar cambios" + }, + "active": { + "message": "Activa" + }, + "editAccount": { + "message": "Editar cuenta" + }, + "cannotDeleteLastAccountTitle": { + "message": "No se puede eliminar la última cuenta" + }, + "deleteThisAccount": { + "message": "Eliminar esta cuenta" + }, + "deleteAccountTitle": { + "message": "¿Eliminar cuenta?" + }, + "deleteAccountMessage": { + "message": "¿Eliminar \"$LABEL$\" ($EMAIL$)?\n\nEsto eliminará permanentemente:\n- Todo el historial de esta cuenta\n- Todas las estadísticas\n- Todos los favoritos\n\nEsta acción no se puede deshacer.", + "placeholders": { + "label": { + "content": "$1" + }, + "email": { + "content": "$2" + } + } + }, + "delete": { + "message": "Eliminar" + }, + "changeAccountEmailTitle": { + "message": "¿Cambiar correo de la cuenta?" + }, + "changeAccountEmailMessage": { + "message": "¿Cambiar correo de\n$OLD_EMAIL$\na\n$NEW_EMAIL$?\n\nEsto hará lo siguiente:\n- Migrar todo el historial, las estadísticas y los favoritos al nuevo correo\n- Actualizar el correo de la cuenta\n- Eliminar los datos asociados con el correo anterior\n\n¿Continuar?", + "placeholders": { + "old_email": { + "content": "$1" + }, + "new_email": { + "content": "$2" + } + } + }, + "changeEmail": { + "message": "Cambiar correo" + }, + "menuForThisWebsite": { + "message": "Para este sitio web" + }, + "menuLoading": { + "message": "Cargando..." + }, + "menuRandomEmailAlias": { + "message": "Alias de correo aleatorio" + }, + "menuCustomTags": { + "message": "Etiquetas personalizadas" + }, + "menuNoPresets": { + "message": "Sin preajustes - agrégalos en Configuración" + }, + "menuGmailTricks": { + "message": "Trucos de Gmail" + }, + "menuDotVariation": { + "message": "Variación con puntos" + }, + "menuGooglemailDomain": { + "message": "Dominio Googlemail" + }, + "menuRemoveAllDots": { + "message": "Quitar todos los puntos" + }, + "toastPresetAdded": { + "message": "Preajuste agregado" + }, + "toastPresetRemoved": { + "message": "Preajuste eliminado" + }, + "toastSettingsExported": { + "message": "Configuración exportada" + }, + "toastSettingsImported": { + "message": "Configuración importada" + }, + "toastImportFailed": { + "message": "Error al importar - archivo no válido" + }, + "toastSettingsReset": { + "message": "Configuración restablecida a valores predeterminados" + }, + "toastAccountSwitched": { + "message": "Cuenta cambiada" + }, + "toastAccountDeleted": { + "message": "Cuenta eliminada" + }, + "toastAccountUpdated": { + "message": "Cuenta actualizada" + }, + "toastAccountAdded": { + "message": "$LABEL$ agregada", + "placeholders": { + "label": { + "content": "$1" + } + } + }, + "errorLabelRequired": { + "message": "La etiqueta no puede estar vacía" + }, + "errorInvalidEmail": { + "message": "Introduce una dirección de correo válida" + }, + "errorDuplicateEmail": { + "message": "Esta dirección de correo ya la usa otra cuenta" + }, + "errorEnterEmail": { + "message": "Introduce una dirección de correo" + }, + "errorAccountExists": { + "message": "Esta cuenta ya existe" + }, + "toastQrFailed": { + "message": "No se pudo generar el código QR" + }, + "toastExportedAliases": { + "message": "Se exportaron $COUNT$ alias", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "toastDeletedAliases": { + "message": "Se eliminaron $COUNT$ alias", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "toastHistoryCleared": { + "message": "Historial borrado" + }, + "toastFavoriteAdded": { + "message": "Agregado a favoritos" + }, + "toastFavoriteRemoved": { + "message": "Eliminado de favoritos" + }, + "toastCopiedEmail": { + "message": "Se copió $EMAIL$", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "toastCopyFailed": { + "message": "No se pudo copiar" + }, + "welcomeTitle": { + "message": "Bienvenido a Gmail Alias Toolkit" + }, + "welcomeSubtitle": { + "message": "Genera alias de correo ilimitados para privacidad y organización" + }, + "letsGetStarted": { + "message": "Empecemos" + }, + "enterGmailAddress": { + "message": "Introduce tu dirección de Gmail" + }, + "pressTabForGmail": { + "message": "Pulsa $KEY$ para @gmail.com", + "placeholders": { + "key": { + "content": "$1" + } + } + }, + "settingUp": { + "message": "Configurando..." + }, + "getStarted": { + "message": "Comenzar" + }, + "advancedSetup": { + "message": "Configuración avanzada en Ajustes" + }, + "whatYouCanDo": { + "message": "Qué puedes hacer:" + }, + "featurePrivateEmail": { + "message": "Generador de correo privado" + }, + "featureCustomTags": { + "message": "Etiquetas y preajustes personalizados" + }, + "featureGmailTricks": { + "message": "Trucos avanzados de Gmail" + }, + "welcomeFooter": { + "message": "Todos los datos se almacenan localmente. Sin seguimiento, sin servidor." + }, + "favoritesSaved": { + "message": "$COUNT$ guardados", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "clickStarHint": { + "message": "Haz clic en la estrella de cualquier alias del historial para agregarlo aquí" + }, + "viewStatistics": { + "message": "Ver estadísticas" + }, + "statistics": { + "message": "Estadísticas" + }, + "metricsTab": { + "message": "Métricas" + }, + "timelineTab": { + "message": "Cronología" + }, + "tagsTab": { + "message": "Etiquetas" + }, + "totalGenerated": { + "message": "Total generado" + }, + "createdToday": { + "message": "Creado hoy" + }, + "thisWeek": { + "message": "Esta semana" + }, + "thisMonth": { + "message": "Este mes" + }, + "topTag": { + "message": "Etiqueta principal" + }, + "totalTags": { + "message": "Total de etiquetas" + }, + "noTagsYet": { + "message": "Aún no hay etiquetas" + }, + "disableInlineForSite": { "message": "Desactivar el asistente integrado en este sitio" }, + "inlineDisabledSites": { "message": "Sitios con el asistente integrado desactivado" }, + "inlineDisabledSitesDescription": { "message": "El icono y la ventana rápida se ocultan en estos sitios web." }, + "noDisabledSites": { "message": "No hay sitios desactivados" }, + "enableInlineForSite": { "message": "Activar" }, + "reportReview": { "message": "Informar / Valorar" }, + "percentLabel": { + "message": "$PERCENT$%", + "placeholders": { + "percent": { + "content": "$1" + } + } + } +} diff --git a/public/_locales/fr/messages.json b/public/_locales/fr/messages.json index 0e6523a..3900287 100644 --- a/public/_locales/fr/messages.json +++ b/public/_locales/fr/messages.json @@ -3,10 +3,10 @@ "message": "Gmail Alias Toolkit" }, "extensionDescription": { - "message": "Générez et gérez des alias Gmail avec l’adressage plus et des préréglages" + "message": "Générez et gérez des alias Gmail avec des adresses +tag et des préréglages" }, "headerSubtitle": { - "message": "Générez des alias avec l’adressage plus" + "message": "Générez des alias avec des adresses +tag" }, "settings": { "message": "Paramètres" @@ -89,7 +89,7 @@ "message": "Alias" }, "copyEmailTooltip": { - "message": "$EMAIL$ - cliquer pour copier", + "message": "$EMAIL$ - cliquez pour copier", "placeholders": { "email": { "content": "$1" @@ -105,10 +105,10 @@ } }, "dotTrick": { - "message": "Astuce points" + "message": "Astuce des points" }, "plusTags": { - "message": "Tags +" + "message": "Tags Plus (+)" }, "googlemail": { "message": "Googlemail" @@ -117,7 +117,7 @@ "message": "Supprimer les points" }, "dotPlus": { - "message": "Points + tags" + "message": "Point + Plus" }, "allCombos": { "message": "Toutes les combinaisons" @@ -126,13 +126,13 @@ "message": "Nombre de variantes" }, "randomizeDotPositions": { - "message": "Positions des points aléatoires" + "message": "Positionner les points au hasard" }, "sequential": { "message": "Séquentiel" }, "generateTricks": { - "message": "Générer les astuces" + "message": "Générer des astuces" }, "generatedVariations": { "message": "Variantes générées" @@ -141,13 +141,13 @@ "message": "Astuce Gmail :" }, "gmailTrickInfo": { - "message": "Les points sont ignorés et tout ce qui suit + arrive dans la même boîte" + "message": "Les points sont ignorés et tout ce qui suit + arrive dans la même boîte de réception" }, "format": { "message": "Format" }, "privateMailFormat": { - "message": "Private Mail (private-mail-xxxx)" + "message": "E-mail privé (private-mail-xxxx)" }, "randomCharactersFormat": { "message": "Caractères aléatoires (abc123xy)" @@ -162,7 +162,7 @@ "message": "Nombre d’alias" }, "generateRandomAliases": { - "message": "Générer $COUNT$ alias aléatoire(s)", + "message": "Générer $COUNT$ alias aléatoire$PLURAL$", "placeholders": { "count": { "content": "$1" @@ -207,7 +207,7 @@ "message": "Horodatage Unix" }, "tagPlaceholder": { - "message": "Saisir un tag (ex. shopping, travail)" + "message": "Saisir un tag (ex. shopping, work)" }, "generate": { "message": "Générer" @@ -240,7 +240,7 @@ "message": "Sélectionner" }, "starredCount": { - "message": "$COUNT$ favoris", + "message": "$COUNT$ étoilés", "placeholders": { "count": { "content": "$1" @@ -254,7 +254,7 @@ "message": "Tout sélectionner" }, "selectedCount": { - "message": "$COUNT$ sélectionné(s)", + "message": "$COUNT$ sélectionnés", "placeholders": { "count": { "content": "$1" @@ -292,7 +292,7 @@ "message": "Tous les tags" }, "mostRecent": { - "message": "Les plus récents" + "message": "Plus récents" }, "az": { "message": "A-Z" @@ -319,7 +319,7 @@ "message": "Ajouter aux favoris" }, "showingRange": { - "message": "Affichage de $START$ à $END$ sur $TOTAL$", + "message": "Affichage de $START$-$END$ sur $TOTAL$", "placeholders": { "start": { "content": "$1" @@ -377,7 +377,7 @@ "message": "Sombre" }, "themeAuto": { - "message": "Système (Auto)" + "message": "Système (auto)" }, "badgeCounter": { "message": "Compteur de badge" @@ -389,7 +389,7 @@ "message": "Total dans l’historique" }, "badgeAllTime": { - "message": "Total généré (tous temps)" + "message": "Total généré (depuis le début)" }, "badgeToday": { "message": "Créés aujourd’hui" @@ -448,7 +448,7 @@ "message": "Réinitialiser les paramètres ?" }, "resetSettingsMessage": { - "message": "Tous les paramètres seront restaurés à leur valeur par défaut." + "message": "Cela restaurera chaque paramètre à sa valeur par défaut." }, "reset": { "message": "Réinitialiser" @@ -457,7 +457,7 @@ "message": "Effacer les alias récents ?" }, "clearHistoryMessage": { - "message": "Cela supprime tous les alias de la liste d’historique récent." + "message": "Cela supprime tous les alias de la liste d’historique récente." }, "emailAccounts": { "message": "Comptes e-mail" @@ -499,7 +499,7 @@ "message": "Supprimer le compte ?" }, "deleteAccountMessage": { - "message": "Supprimer « $LABEL$ » ($EMAIL$) ?\n\nCela supprimera définitivement :\n- Tout l’historique de ce compte\n- Toutes les statistiques\n- Tous les favoris\n\nCette action est irréversible.", + "message": "Supprimer \"$LABEL$\" ($EMAIL$) ?\n\nCela supprimera définitivement :\n- Tout l’historique de ce compte\n- Toutes les statistiques\n- Tous les favoris\n\nCette action est irréversible.", "placeholders": { "label": { "content": "$1" @@ -513,7 +513,7 @@ "message": "Supprimer" }, "changeAccountEmailTitle": { - "message": "Changer l’e-mail du compte ?" + "message": "Modifier l’e-mail du compte ?" }, "changeAccountEmailMessage": { "message": "Changer l’e-mail de\n$OLD_EMAIL$\nvers\n$NEW_EMAIL$ ?\n\nCela va :\n- Migrer tout l’historique, les statistiques et les favoris vers le nouvel e-mail\n- Mettre à jour l’e-mail du compte\n- Supprimer les données associées à l’ancien e-mail\n\nContinuer ?", @@ -529,6 +529,12 @@ "changeEmail": { "message": "Changer l’e-mail" }, + "menuForThisWebsite": { + "message": "Pour ce site" + }, + "menuLoading": { + "message": "Chargement..." + }, "menuRandomEmailAlias": { "message": "Alias e-mail aléatoire" }, @@ -536,13 +542,13 @@ "message": "Tags personnalisés" }, "menuNoPresets": { - "message": "Aucun préréglage - Ajoutez-en dans les paramètres" + "message": "Aucun préréglage - ajoutez-en dans Paramètres" }, "menuGmailTricks": { "message": "Astuces Gmail" }, "menuDotVariation": { - "message": "Variation avec points" + "message": "Variante avec points" }, "menuGooglemailDomain": { "message": "Domaine Googlemail" @@ -643,7 +649,7 @@ "message": "Bienvenue dans Gmail Alias Toolkit" }, "welcomeSubtitle": { - "message": "Générez des alias e-mail illimités pour la confidentialité et l’organisation" + "message": "Générez un nombre illimité d’alias e-mail pour la confidentialité et l’organisation" }, "letsGetStarted": { "message": "Commençons" @@ -660,7 +666,7 @@ } }, "settingUp": { - "message": "Configuration en cours..." + "message": "Configuration..." }, "getStarted": { "message": "Commencer" @@ -672,19 +678,19 @@ "message": "Ce que vous pouvez faire :" }, "featurePrivateEmail": { - "message": "Générateur d’e-mail privé" + "message": "Générateur d’e-mails privés" }, "featureCustomTags": { "message": "Tags et préréglages personnalisés" }, "featureGmailTricks": { - "message": "Astuces Gmail avancées" + "message": "Astuces avancées Gmail" }, "welcomeFooter": { "message": "Toutes les données sont stockées localement. Aucun suivi, aucun serveur." }, "favoritesSaved": { - "message": "$COUNT$ enregistré(s)", + "message": "$COUNT$ enregistrés", "placeholders": { "count": { "content": "$1" @@ -693,5 +699,55 @@ }, "clickStarHint": { "message": "Cliquez sur l’étoile d’un alias dans l’historique pour l’ajouter ici" + }, + "viewStatistics": { + "message": "Voir les statistiques" + }, + "statistics": { + "message": "Statistiques" + }, + "metricsTab": { + "message": "Métriques" + }, + "timelineTab": { + "message": "Chronologie" + }, + "tagsTab": { + "message": "Tags" + }, + "totalGenerated": { + "message": "Total généré" + }, + "createdToday": { + "message": "Créés aujourd’hui" + }, + "thisWeek": { + "message": "Cette semaine" + }, + "thisMonth": { + "message": "Ce mois-ci" + }, + "topTag": { + "message": "Top tag" + }, + "totalTags": { + "message": "Total des tags" + }, + "noTagsYet": { + "message": "Aucun tag pour le moment" + }, + "disableInlineForSite": { "message": "Désactiver l’assistant intégré sur ce site" }, + "inlineDisabledSites": { "message": "Sites avec l’assistant intégré désactivé" }, + "inlineDisabledSitesDescription": { "message": "L’icône et la fenêtre rapide sont masquées sur ces sites." }, + "noDisabledSites": { "message": "Aucun site désactivé" }, + "enableInlineForSite": { "message": "Activer" }, + "reportReview": { "message": "Signaler / Évaluer" }, + "percentLabel": { + "message": "$PERCENT$%", + "placeholders": { + "percent": { + "content": "$1" + } + } } } diff --git a/public/_locales/hi/messages.json b/public/_locales/hi/messages.json index 780a725..1b7798d 100644 --- a/public/_locales/hi/messages.json +++ b/public/_locales/hi/messages.json @@ -3,10 +3,10 @@ "message": "Gmail Alias Toolkit" }, "extensionDescription": { - "message": "प्लस एड्रेसिंग और प्रीसेट के साथ Gmail उपनाम बनाएं और प्रबंधित करें" + "message": "+tag पते और प्रीसेट का उपयोग करके Gmail उपनाम बनाएं और प्रबंधित करें" }, "headerSubtitle": { - "message": "प्लस एड्रेसिंग के साथ उपनाम बनाएं" + "message": "+tag पतों से उपनाम बनाएं" }, "settings": { "message": "सेटिंग्स" @@ -56,10 +56,10 @@ } }, "gmailWarning": { - "message": "यह Gmail पता नहीं लग रहा है। प्लस एड्रेसिंग Gmail के साथ सबसे अच्छी तरह काम करती है।" + "message": "यह Gmail पता जैसा नहीं दिखता। प्लस एड्रेसिंग Gmail के साथ सबसे अच्छा काम करती है।" }, "random": { - "message": "यादृच्छिक" + "message": "रैंडम" }, "customTags": { "message": "कस्टम टैग" @@ -74,16 +74,16 @@ "message": "ट्रिक्स" }, "generating": { - "message": "बनाया जा रहा है" + "message": "बन रहा है" }, "copied": { "message": "कॉपी हो गया" }, "switchToLightMode": { - "message": "लाइट मोड पर जाएं" + "message": "लाइट मोड पर स्विच करें" }, "switchToDarkMode": { - "message": "डार्क मोड पर जाएं" + "message": "डार्क मोड पर स्विच करें" }, "aliasColumn": { "message": "उपनाम" @@ -120,40 +120,40 @@ "message": "डॉट + प्लस" }, "allCombos": { - "message": "सभी कॉम्बो" + "message": "सभी संयोजन" }, "numberOfVariations": { - "message": "वेरिएशन की संख्या" + "message": "वैरिएशन की संख्या" }, "randomizeDotPositions": { - "message": "डॉट की स्थिति रैंडम करें" + "message": "डॉट की स्थितियां रैंडम करें" }, "sequential": { - "message": "क्रमवार" + "message": "क्रमिक" }, "generateTricks": { "message": "ट्रिक्स बनाएं" }, "generatedVariations": { - "message": "बने हुए वेरिएशन" + "message": "बने हुए वैरिएशन" }, "gmailTrickInfoLabel": { "message": "Gmail ट्रिक:" }, "gmailTrickInfo": { - "message": "डॉट्स को अनदेखा किया जाता है और + के बाद की हर चीज उसी इनबॉक्स में जाती है" + "message": "डॉट्स को अनदेखा किया जाता है और + के बाद सब कुछ उसी इनबॉक्स में जाता है" }, "format": { - "message": "फ़ॉर्मैट" + "message": "फ़ॉर्मेट" }, "privateMailFormat": { - "message": "Private Mail (private-mail-xxxx)" + "message": "प्राइवेट मेल (private-mail-xxxx)" }, "randomCharactersFormat": { - "message": "यादृच्छिक वर्ण (abc123xy)" + "message": "रैंडम अक्षर (abc123xy)" }, "randomWordsFormat": { - "message": "यादृच्छिक शब्द (happy-fox-42)" + "message": "रैंडम शब्द (happy-fox-42)" }, "timestampFormat": { "message": "टाइमस्टैम्प (1234567890)" @@ -162,7 +162,7 @@ "message": "उपनामों की संख्या" }, "generateRandomAliases": { - "message": "$COUNT$ यादृच्छिक उपनाम बनाएं", + "message": "$COUNT$ रैंडम उपनाम$PLURAL$ बनाएं", "placeholders": { "count": { "content": "$1" @@ -173,7 +173,7 @@ } }, "generatedAliases": { - "message": "बनाए गए उपनाम" + "message": "बने हुए उपनाम" }, "totalCount": { "message": "कुल $COUNT$", @@ -184,7 +184,7 @@ } }, "copiedAliases": { - "message": "$COUNT$ उपनाम कॉपी किए गए!", + "message": "$COUNT$ उपनाम कॉपी हुए!", "placeholders": { "count": { "content": "$1" @@ -195,13 +195,13 @@ "message": "कॉपी करने में विफल" }, "formatPrivateMail": { - "message": "फ़ॉर्मैट: private-mail-xxxx" + "message": "फ़ॉर्मेट: private-mail-xxxx" }, "formatAlphanumeric": { - "message": "8 यादृच्छिक वर्ण" + "message": "8 रैंडम अक्षर" }, "formatWords": { - "message": "2 यादृच्छिक शब्द" + "message": "2 रैंडम शब्द" }, "formatTimestamp": { "message": "Unix टाइमस्टैम्प" @@ -248,7 +248,7 @@ } }, "deselectAll": { - "message": "सभी चयन हटाएं" + "message": "सभी अचयनित करें" }, "selectAll": { "message": "सभी चुनें" @@ -301,13 +301,13 @@ "message": "अभी कोई पसंदीदा नहीं" }, "starEmailsHint": { - "message": "यहां तुरंत पहुंचने के लिए अपने इतिहास से ईमेल को स्टार करें" + "message": "यहां जल्दी पहुंचने के लिए अपने इतिहास के ईमेल को स्टार करें" }, "noResultsFound": { "message": "कोई परिणाम नहीं मिला" }, "differentSearchHint": { - "message": "कोई दूसरी खोज या फ़िल्टर आज़माएं" + "message": "अलग खोज या फ़िल्टर आज़माएं" }, "showQrCode": { "message": "QR कोड दिखाएं" @@ -319,7 +319,7 @@ "message": "पसंदीदा में जोड़ें" }, "showingRange": { - "message": "$TOTAL$ में से $START$-$END$ दिखाए जा रहे हैं", + "message": "$TOTAL$ में से $START$-$END$ दिखा रहे हैं", "placeholders": { "start": { "content": "$1" @@ -333,7 +333,7 @@ } }, "perPage": { - "message": "$COUNT$ / पेज", + "message": "$COUNT$ / पृष्ठ", "placeholders": { "count": { "content": "$1" @@ -341,16 +341,16 @@ } }, "firstPage": { - "message": "पहला पेज" + "message": "पहला पृष्ठ" }, "previousPage": { - "message": "पिछला पेज" + "message": "पिछला पृष्ठ" }, "nextPage": { - "message": "अगला पेज" + "message": "अगला पृष्ठ" }, "lastPage": { - "message": "अंतिम पेज" + "message": "अंतिम पृष्ठ" }, "scanToCopyAlias": { "message": "उपनाम कॉपी करने के लिए स्कैन करें" @@ -362,10 +362,10 @@ "message": "खाते" }, "changelog": { - "message": "परिवर्तन लॉग" + "message": "चेंजलॉग" }, "appearanceDisplay": { - "message": "दिखावट और प्रदर्शन" + "message": "रूप और प्रदर्शन" }, "theme": { "message": "थीम" @@ -404,7 +404,7 @@ "message": "उपनाम बनाना" }, "randomAliasFormat": { - "message": "यादृच्छिक उपनाम फ़ॉर्मैट" + "message": "रैंडम उपनाम फ़ॉर्मेट" }, "autoSaveLimit": { "message": "ऑटो-सेव सीमा" @@ -439,7 +439,7 @@ "message": "आयात" }, "clear": { - "message": "साफ़ करें" + "message": "साफ करें" }, "resetSettings": { "message": "सभी सेटिंग्स को डिफ़ॉल्ट पर रीसेट करें" @@ -448,13 +448,13 @@ "message": "सेटिंग्स रीसेट करें?" }, "resetSettingsMessage": { - "message": "यह हर सेटिंग को उसके डिफ़ॉल्ट मान पर वापस कर देगा।" + "message": "यह हर सेटिंग को उसके डिफ़ॉल्ट मान पर वापस लाएगा।" }, "reset": { "message": "रीसेट" }, "clearHistoryTitle": { - "message": "हाल के उपनाम साफ़ करें?" + "message": "हाल के उपनाम साफ करें?" }, "clearHistoryMessage": { "message": "यह हाल की इतिहास सूची से सभी उपनाम हटा देता है।" @@ -463,7 +463,7 @@ "message": "ईमेल खाते" }, "manageAccountsDescription": { - "message": "अपने Gmail खाते प्रबंधित करें। हर खाते का अपना इतिहास, आंकड़े और पसंदीदा होते हैं।" + "message": "अपने Gmail खातों को प्रबंधित करें। हर खाते का अपना इतिहास, आंकड़े और पसंदीदा होते हैं।" }, "noAccountsFound": { "message": "कोई खाता नहीं मिला। कृपया मुख्य स्क्रीन से खाता जोड़ें।" @@ -478,7 +478,7 @@ "message": "your.email@gmail.com" }, "emailChangeWarning": { - "message": "ईमेल बदलने पर सभी डेटा नए ईमेल पते पर माइग्रेट हो जाएगा" + "message": "ईमेल बदलने से सभी डेटा नए ईमेल पते पर माइग्रेट हो जाएगा" }, "saveChanges": { "message": "बदलाव सहेजें" @@ -499,7 +499,7 @@ "message": "खाता हटाएं?" }, "deleteAccountMessage": { - "message": "“$LABEL$” ($EMAIL$) हटाएं?\n\nयह स्थायी रूप से हटाएगा:\n- इस खाते का पूरा इतिहास\n- सभी आंकड़े\n- सभी पसंदीदा\n\nयह कार्रवाई पूर्ववत नहीं की जा सकती।", + "message": "\"$LABEL$\" ($EMAIL$) हटाएं?\n\nयह स्थायी रूप से हटाएगा:\n- इस खाते का पूरा इतिहास\n- सभी आंकड़े\n- सभी पसंदीदा\n\nयह कार्रवाई पूर्ववत नहीं की जा सकती।", "placeholders": { "label": { "content": "$1" @@ -516,7 +516,7 @@ "message": "खाते का ईमेल बदलें?" }, "changeAccountEmailMessage": { - "message": "ईमेल बदलें\n$OLD_EMAIL$\nसे\n$NEW_EMAIL$\nमें?\n\nयह करेगा:\n- पूरा इतिहास, आंकड़े और पसंदीदा नए ईमेल में माइग्रेट करेगा\n- खाते का ईमेल अपडेट करेगा\n- पुराने ईमेल से जुड़े डेटा को हटा देगा\n\nजारी रखें?", + "message": "ईमेल बदलें\n$OLD_EMAIL$\nसे\n$NEW_EMAIL$\nमें?\n\nयह करेगा:\n- पूरा इतिहास, आंकड़े और पसंदीदा नए ईमेल पर माइग्रेट करेगा\n- खाते का ईमेल अपडेट करेगा\n- पुराने ईमेल से जुड़ा डेटा हटाएगा\n\nजारी रखें?", "placeholders": { "old_email": { "content": "$1" @@ -529,8 +529,14 @@ "changeEmail": { "message": "ईमेल बदलें" }, + "menuForThisWebsite": { + "message": "इस वेबसाइट के लिए" + }, + "menuLoading": { + "message": "लोड हो रहा है..." + }, "menuRandomEmailAlias": { - "message": "यादृच्छिक ईमेल उपनाम" + "message": "रैंडम ईमेल उपनाम" }, "menuCustomTags": { "message": "कस्टम टैग" @@ -557,16 +563,16 @@ "message": "प्रीसेट हटाया गया" }, "toastSettingsExported": { - "message": "सेटिंग्स निर्यात की गईं" + "message": "सेटिंग्स निर्यात हुईं" }, "toastSettingsImported": { - "message": "सेटिंग्स आयात की गईं" + "message": "सेटिंग्स आयात हुईं" }, "toastImportFailed": { "message": "आयात विफल - अमान्य फ़ाइल" }, "toastSettingsReset": { - "message": "सेटिंग्स डिफ़ॉल्ट पर रीसेट की गईं" + "message": "सेटिंग्स डिफ़ॉल्ट पर रीसेट हुईं" }, "toastAccountSwitched": { "message": "खाता बदला गया" @@ -575,7 +581,7 @@ "message": "खाता हटाया गया" }, "toastAccountUpdated": { - "message": "खाता अपडेट किया गया" + "message": "खाता अपडेट हुआ" }, "toastAccountAdded": { "message": "$LABEL$ जोड़ा गया", @@ -589,13 +595,13 @@ "message": "लेबल खाली नहीं हो सकता" }, "errorInvalidEmail": { - "message": "कृपया एक मान्य ईमेल पता दर्ज करें" + "message": "कृपया मान्य ईमेल पता दर्ज करें" }, "errorDuplicateEmail": { - "message": "यह ईमेल पता पहले से किसी दूसरे खाते में उपयोग हो रहा है" + "message": "यह ईमेल पता पहले से किसी अन्य खाते द्वारा उपयोग किया जा रहा है" }, "errorEnterEmail": { - "message": "कृपया एक ईमेल पता दर्ज करें" + "message": "कृपया ईमेल पता दर्ज करें" }, "errorAccountExists": { "message": "यह खाता पहले से मौजूद है" @@ -604,7 +610,7 @@ "message": "QR कोड बनाने में विफल" }, "toastExportedAliases": { - "message": "$COUNT$ उपनाम निर्यात किए गए", + "message": "$COUNT$ उपनाम निर्यात हुए", "placeholders": { "count": { "content": "$1" @@ -620,7 +626,7 @@ } }, "toastHistoryCleared": { - "message": "इतिहास साफ़ किया गया" + "message": "इतिहास साफ किया गया" }, "toastFavoriteAdded": { "message": "पसंदीदा में जोड़ा गया" @@ -629,7 +635,7 @@ "message": "पसंदीदा से हटाया गया" }, "toastCopiedEmail": { - "message": "$EMAIL$ कॉपी किया गया", + "message": "$EMAIL$ कॉपी हुआ", "placeholders": { "email": { "content": "$1" @@ -672,13 +678,13 @@ "message": "आप क्या कर सकते हैं:" }, "featurePrivateEmail": { - "message": "निजी ईमेल जनरेटर" + "message": "प्राइवेट ईमेल जनरेटर" }, "featureCustomTags": { "message": "कस्टम टैग और प्रीसेट" }, "featureGmailTricks": { - "message": "Gmail एडवांस्ड ट्रिक्स" + "message": "Gmail उन्नत ट्रिक्स" }, "welcomeFooter": { "message": "सारा डेटा स्थानीय रूप से संग्रहीत होता है। कोई ट्रैकिंग नहीं, कोई सर्वर नहीं।" @@ -692,6 +698,56 @@ } }, "clickStarHint": { - "message": "इसे यहां जोड़ने के लिए इतिहास में किसी भी उपनाम पर स्टार क्लिक करें" + "message": "यहां जोड़ने के लिए इतिहास में किसी भी उपनाम पर स्टार क्लिक करें" + }, + "viewStatistics": { + "message": "आंकड़े देखें" + }, + "statistics": { + "message": "आंकड़े" + }, + "metricsTab": { + "message": "मेट्रिक्स" + }, + "timelineTab": { + "message": "टाइमलाइन" + }, + "tagsTab": { + "message": "टैग" + }, + "totalGenerated": { + "message": "कुल बनाए गए" + }, + "createdToday": { + "message": "आज बनाए गए" + }, + "thisWeek": { + "message": "इस सप्ताह" + }, + "thisMonth": { + "message": "इस महीने" + }, + "topTag": { + "message": "शीर्ष टैग" + }, + "totalTags": { + "message": "कुल टैग" + }, + "noTagsYet": { + "message": "अभी कोई टैग नहीं" + }, + "disableInlineForSite": { "message": "इस वेबसाइट पर इनलाइन सहायक बंद करें" }, + "inlineDisabledSites": { "message": "इनलाइन सहायक बंद वाली वेबसाइटें" }, + "inlineDisabledSitesDescription": { "message": "इन वेबसाइटों पर इनलाइन आइकन और त्वरित पॉपअप छिपे रहेंगे।" }, + "noDisabledSites": { "message": "कोई वेबसाइट बंद नहीं है" }, + "enableInlineForSite": { "message": "चालू करें" }, + "reportReview": { "message": "रिपोर्ट / समीक्षा" }, + "percentLabel": { + "message": "$PERCENT$%", + "placeholders": { + "percent": { + "content": "$1" + } + } } } diff --git a/public/_locales/it/messages.json b/public/_locales/it/messages.json new file mode 100644 index 0000000..91d98ea --- /dev/null +++ b/public/_locales/it/messages.json @@ -0,0 +1,753 @@ +{ + "extensionName": { + "message": "Gmail Alias Toolkit" + }, + "extensionDescription": { + "message": "Genera e gestisci alias Gmail con indirizzi +tag e preset" + }, + "headerSubtitle": { + "message": "Genera alias con indirizzi +tag" + }, + "settings": { + "message": "Impostazioni" + }, + "back": { + "message": "Indietro" + }, + "close": { + "message": "Chiudi" + }, + "cancel": { + "message": "Annulla" + }, + "copy": { + "message": "Copia" + }, + "copyAll": { + "message": "Copia tutto" + }, + "copyToClipboard": { + "message": "Copia negli appunti" + }, + "activeGmailAddress": { + "message": "Indirizzo Gmail attivo" + }, + "addNewAccount": { + "message": "Aggiungi nuovo account" + }, + "addAccount": { + "message": "Aggiungi account" + }, + "addNewAccountTitle": { + "message": "Aggiungi nuovo account" + }, + "emailPlaceholder": { + "message": "tua.email" + }, + "accountLabelPlaceholder": { + "message": "Etichetta (opzionale, es. Lavoro, Personale)" + }, + "pressTabToAddGmail": { + "message": "Premi $KEY$ per aggiungere @gmail.com", + "placeholders": { + "key": { + "content": "$1" + } + } + }, + "gmailWarning": { + "message": "Questo non sembra un indirizzo Gmail. L’indirizzamento plus funziona meglio con Gmail." + }, + "random": { + "message": "Casuale" + }, + "customTags": { + "message": "Tag personalizzati" + }, + "gmailTricks": { + "message": "Trucchi Gmail" + }, + "tabTagsShort": { + "message": "Tag" + }, + "tabTricksShort": { + "message": "Trucchi" + }, + "generating": { + "message": "Generazione" + }, + "copied": { + "message": "Copiato" + }, + "switchToLightMode": { + "message": "Passa alla modalità chiara" + }, + "switchToDarkMode": { + "message": "Passa alla modalità scura" + }, + "aliasColumn": { + "message": "Alias" + }, + "copyEmailTooltip": { + "message": "$EMAIL$ - clicca per copiare", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "pageLabel": { + "message": "Pagina $PAGE$", + "placeholders": { + "page": { + "content": "$1" + } + } + }, + "dotTrick": { + "message": "Trucco dei punti" + }, + "plusTags": { + "message": "Tag Plus (+)" + }, + "googlemail": { + "message": "Googlemail" + }, + "removeDots": { + "message": "Rimuovi punti" + }, + "dotPlus": { + "message": "Punto + Plus" + }, + "allCombos": { + "message": "Tutte le combinazioni" + }, + "numberOfVariations": { + "message": "Numero di varianti" + }, + "randomizeDotPositions": { + "message": "Rendi casuali le posizioni dei punti" + }, + "sequential": { + "message": "Sequenziale" + }, + "generateTricks": { + "message": "Genera trucchi" + }, + "generatedVariations": { + "message": "Varianti generate" + }, + "gmailTrickInfoLabel": { + "message": "Trucco Gmail:" + }, + "gmailTrickInfo": { + "message": "I punti vengono ignorati e tutto ciò che segue + arriva nella stessa casella di posta" + }, + "format": { + "message": "Formato" + }, + "privateMailFormat": { + "message": "Posta privata (private-mail-xxxx)" + }, + "randomCharactersFormat": { + "message": "Caratteri casuali (abc123xy)" + }, + "randomWordsFormat": { + "message": "Parole casuali (happy-fox-42)" + }, + "timestampFormat": { + "message": "Timestamp (1234567890)" + }, + "numberOfAliases": { + "message": "Numero di alias" + }, + "generateRandomAliases": { + "message": "Genera $COUNT$ alias casuale$PLURAL$", + "placeholders": { + "count": { + "content": "$1" + }, + "plural": { + "content": "$2" + } + } + }, + "generatedAliases": { + "message": "Alias generati" + }, + "totalCount": { + "message": "$COUNT$ in totale", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "copiedAliases": { + "message": "$COUNT$ alias copiati!", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "failedToCopy": { + "message": "Copia non riuscita" + }, + "formatPrivateMail": { + "message": "Formato: private-mail-xxxx" + }, + "formatAlphanumeric": { + "message": "8 caratteri casuali" + }, + "formatWords": { + "message": "2 parole casuali" + }, + "formatTimestamp": { + "message": "Timestamp Unix" + }, + "tagPlaceholder": { + "message": "Inserisci tag (es. shopping, work)" + }, + "generate": { + "message": "Genera" + }, + "yourPresets": { + "message": "I tuoi preset" + }, + "example": { + "message": "Esempio:" + }, + "copyExample": { + "message": "Copia esempio" + }, + "recentAliases": { + "message": "Alias recenti" + }, + "favorites": { + "message": "Preferiti" + }, + "exportAsCsv": { + "message": "Esporta come CSV" + }, + "exportAsJson": { + "message": "Esporta come JSON" + }, + "selectAliases": { + "message": "Seleziona alias" + }, + "select": { + "message": "Seleziona" + }, + "starredCount": { + "message": "$COUNT$ contrassegnati", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "deselectAll": { + "message": "Deseleziona tutto" + }, + "selectAll": { + "message": "Seleziona tutto" + }, + "selectedCount": { + "message": "$COUNT$ selezionati", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "deleteCount": { + "message": "Elimina $COUNT$", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "allCount": { + "message": "Tutti ($COUNT$)", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "favoritesCount": { + "message": "Preferiti ($COUNT$)", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "searchAliases": { + "message": "Cerca alias..." + }, + "allTags": { + "message": "Tutti i tag" + }, + "mostRecent": { + "message": "Più recenti" + }, + "az": { + "message": "A-Z" + }, + "noFavoritesYet": { + "message": "Ancora nessun preferito" + }, + "starEmailsHint": { + "message": "Aggiungi una stella alle email dalla cronologia per accedervi rapidamente qui" + }, + "noResultsFound": { + "message": "Nessun risultato trovato" + }, + "differentSearchHint": { + "message": "Prova una ricerca o un filtro diverso" + }, + "showQrCode": { + "message": "Mostra codice QR" + }, + "removeFromFavorites": { + "message": "Rimuovi dai preferiti" + }, + "addToFavorites": { + "message": "Aggiungi ai preferiti" + }, + "showingRange": { + "message": "Visualizzazione $START$-$END$ di $TOTAL$", + "placeholders": { + "start": { + "content": "$1" + }, + "end": { + "content": "$2" + }, + "total": { + "content": "$3" + } + } + }, + "perPage": { + "message": "$COUNT$ / pagina", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "firstPage": { + "message": "Prima pagina" + }, + "previousPage": { + "message": "Pagina precedente" + }, + "nextPage": { + "message": "Pagina successiva" + }, + "lastPage": { + "message": "Ultima pagina" + }, + "scanToCopyAlias": { + "message": "Scansiona per copiare l’alias" + }, + "general": { + "message": "Generale" + }, + "accounts": { + "message": "Account" + }, + "changelog": { + "message": "Registro modifiche" + }, + "appearanceDisplay": { + "message": "Aspetto e visualizzazione" + }, + "theme": { + "message": "Tema" + }, + "themeLight": { + "message": "Chiaro" + }, + "themeDark": { + "message": "Scuro" + }, + "themeAuto": { + "message": "Sistema (Auto)" + }, + "badgeCounter": { + "message": "Contatore badge" + }, + "badgeNone": { + "message": "Nessuno (nascosto)" + }, + "badgeTotal": { + "message": "Totale nella cronologia" + }, + "badgeAllTime": { + "message": "Totale generato (sempre)" + }, + "badgeToday": { + "message": "Creati oggi" + }, + "badgeWeek": { + "message": "Questa settimana" + }, + "copyNotifications": { + "message": "Notifiche di copia" + }, + "aliasGeneration": { + "message": "Generazione alias" + }, + "randomAliasFormat": { + "message": "Formato alias casuale" + }, + "autoSaveLimit": { + "message": "Limite salvataggio automatico" + }, + "aliasesLimit": { + "message": "$COUNT$ alias", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "customPresets": { + "message": "Preset personalizzati" + }, + "label": { + "message": "Etichetta" + }, + "tag": { + "message": "Tag" + }, + "addPreset": { + "message": "+ Aggiungi preset" + }, + "dataManagement": { + "message": "Gestione dati" + }, + "export": { + "message": "Esporta" + }, + "import": { + "message": "Importa" + }, + "clear": { + "message": "Cancella" + }, + "resetSettings": { + "message": "Ripristina tutte le impostazioni predefinite" + }, + "resetSettingsTitle": { + "message": "Ripristinare impostazioni?" + }, + "resetSettingsMessage": { + "message": "Questo ripristinerà ogni impostazione al valore predefinito." + }, + "reset": { + "message": "Ripristina" + }, + "clearHistoryTitle": { + "message": "Cancellare alias recenti?" + }, + "clearHistoryMessage": { + "message": "Questo rimuove tutti gli alias dall’elenco della cronologia recente." + }, + "emailAccounts": { + "message": "Account email" + }, + "manageAccountsDescription": { + "message": "Gestisci i tuoi account Gmail. Ogni account ha la propria cronologia, statistiche e preferiti." + }, + "noAccountsFound": { + "message": "Nessun account trovato. Aggiungi un account dalla schermata principale." + }, + "emailAddress": { + "message": "Indirizzo email" + }, + "accountLabel": { + "message": "Etichetta account" + }, + "emailAddressPlaceholder": { + "message": "tua.email@gmail.com" + }, + "emailChangeWarning": { + "message": "La modifica dell’email migrerà tutti i dati al nuovo indirizzo email" + }, + "saveChanges": { + "message": "Salva modifiche" + }, + "active": { + "message": "Attivo" + }, + "editAccount": { + "message": "Modifica account" + }, + "cannotDeleteLastAccountTitle": { + "message": "Impossibile eliminare l’ultimo account" + }, + "deleteThisAccount": { + "message": "Elimina questo account" + }, + "deleteAccountTitle": { + "message": "Eliminare account?" + }, + "deleteAccountMessage": { + "message": "Eliminare \"$LABEL$\" ($EMAIL$)?\n\nQuesto eliminerà definitivamente:\n- Tutta la cronologia di questo account\n- Tutte le statistiche\n- Tutti i preferiti\n\nQuesta azione non può essere annullata.", + "placeholders": { + "label": { + "content": "$1" + }, + "email": { + "content": "$2" + } + } + }, + "delete": { + "message": "Elimina" + }, + "changeAccountEmailTitle": { + "message": "Cambiare email account?" + }, + "changeAccountEmailMessage": { + "message": "Cambiare email da\n$OLD_EMAIL$\na\n$NEW_EMAIL$?\n\nQuesto farà quanto segue:\n- Migrerà tutta la cronologia, le statistiche e i preferiti alla nuova email\n- Aggiornerà l’email dell’account\n- Eliminerà i dati associati alla vecchia email\n\nContinuare?", + "placeholders": { + "old_email": { + "content": "$1" + }, + "new_email": { + "content": "$2" + } + } + }, + "changeEmail": { + "message": "Cambia email" + }, + "menuForThisWebsite": { + "message": "Per questo sito" + }, + "menuLoading": { + "message": "Caricamento..." + }, + "menuRandomEmailAlias": { + "message": "Alias email casuale" + }, + "menuCustomTags": { + "message": "Tag personalizzati" + }, + "menuNoPresets": { + "message": "Nessun preset - Aggiungi in Impostazioni" + }, + "menuGmailTricks": { + "message": "Trucchi Gmail" + }, + "menuDotVariation": { + "message": "Variazione con punti" + }, + "menuGooglemailDomain": { + "message": "Dominio Googlemail" + }, + "menuRemoveAllDots": { + "message": "Rimuovi tutti i punti" + }, + "toastPresetAdded": { + "message": "Preset aggiunto" + }, + "toastPresetRemoved": { + "message": "Preset rimosso" + }, + "toastSettingsExported": { + "message": "Impostazioni esportate" + }, + "toastSettingsImported": { + "message": "Impostazioni importate" + }, + "toastImportFailed": { + "message": "Importazione non riuscita - file non valido" + }, + "toastSettingsReset": { + "message": "Impostazioni ripristinate ai valori predefiniti" + }, + "toastAccountSwitched": { + "message": "Account cambiato" + }, + "toastAccountDeleted": { + "message": "Account eliminato" + }, + "toastAccountUpdated": { + "message": "Account aggiornato" + }, + "toastAccountAdded": { + "message": "$LABEL$ aggiunto", + "placeholders": { + "label": { + "content": "$1" + } + } + }, + "errorLabelRequired": { + "message": "L’etichetta non può essere vuota" + }, + "errorInvalidEmail": { + "message": "Inserisci un indirizzo email valido" + }, + "errorDuplicateEmail": { + "message": "Questo indirizzo email è già usato da un altro account" + }, + "errorEnterEmail": { + "message": "Inserisci un indirizzo email" + }, + "errorAccountExists": { + "message": "Questo account esiste già" + }, + "toastQrFailed": { + "message": "Impossibile generare il codice QR" + }, + "toastExportedAliases": { + "message": "Esportati $COUNT$ alias", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "toastDeletedAliases": { + "message": "Eliminati $COUNT$ alias", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "toastHistoryCleared": { + "message": "Cronologia cancellata" + }, + "toastFavoriteAdded": { + "message": "Aggiunto ai preferiti" + }, + "toastFavoriteRemoved": { + "message": "Rimosso dai preferiti" + }, + "toastCopiedEmail": { + "message": "$EMAIL$ copiato", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "toastCopyFailed": { + "message": "Copia non riuscita" + }, + "welcomeTitle": { + "message": "Benvenuto in Gmail Alias Toolkit" + }, + "welcomeSubtitle": { + "message": "Genera alias email illimitati per privacy e organizzazione" + }, + "letsGetStarted": { + "message": "Iniziamo" + }, + "enterGmailAddress": { + "message": "Inserisci il tuo indirizzo Gmail" + }, + "pressTabForGmail": { + "message": "Premi $KEY$ per @gmail.com", + "placeholders": { + "key": { + "content": "$1" + } + } + }, + "settingUp": { + "message": "Configurazione..." + }, + "getStarted": { + "message": "Inizia" + }, + "advancedSetup": { + "message": "Configurazione avanzata nelle Impostazioni" + }, + "whatYouCanDo": { + "message": "Cosa puoi fare:" + }, + "featurePrivateEmail": { + "message": "Generatore di email private" + }, + "featureCustomTags": { + "message": "Tag e preset personalizzati" + }, + "featureGmailTricks": { + "message": "Trucchi Gmail avanzati" + }, + "welcomeFooter": { + "message": "Tutti i dati sono archiviati localmente. Nessun tracciamento, nessun server." + }, + "favoritesSaved": { + "message": "$COUNT$ salvati", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "clickStarHint": { + "message": "Clicca sulla stella di qualsiasi alias nella cronologia per aggiungerlo qui" + }, + "viewStatistics": { + "message": "Visualizza statistiche" + }, + "statistics": { + "message": "Statistiche" + }, + "metricsTab": { + "message": "Metriche" + }, + "timelineTab": { + "message": "Cronologia" + }, + "tagsTab": { + "message": "Tag" + }, + "totalGenerated": { + "message": "Totale generato" + }, + "createdToday": { + "message": "Creati oggi" + }, + "thisWeek": { + "message": "Questa settimana" + }, + "thisMonth": { + "message": "Questo mese" + }, + "topTag": { + "message": "Tag principale" + }, + "totalTags": { + "message": "Tag totali" + }, + "noTagsYet": { + "message": "Ancora nessun tag" + }, + "disableInlineForSite": { "message": "Disattiva l’assistente inline su questo sito" }, + "inlineDisabledSites": { "message": "Siti con assistente inline disattivato" }, + "inlineDisabledSitesDescription": { "message": "L’icona inline e il popup rapido sono nascosti su questi siti." }, + "noDisabledSites": { "message": "Nessun sito disattivato" }, + "enableInlineForSite": { "message": "Attiva" }, + "reportReview": { "message": "Segnala / Recensisci" }, + "percentLabel": { + "message": "$PERCENT$%", + "placeholders": { + "percent": { + "content": "$1" + } + } + } +} diff --git a/public/_locales/ja/messages.json b/public/_locales/ja/messages.json index 9b6c0fb..e00a064 100644 --- a/public/_locales/ja/messages.json +++ b/public/_locales/ja/messages.json @@ -3,10 +3,10 @@ "message": "Gmail Alias Toolkit" }, "extensionDescription": { - "message": "プラスアドレスとプリセットで Gmail エイリアスを生成・管理します" + "message": "+tag アドレスとプリセットで Gmail エイリアスを生成・管理" }, "headerSubtitle": { - "message": "プラスアドレスでエイリアスを生成" + "message": "+tag アドレスでエイリアスを生成" }, "settings": { "message": "設定" @@ -56,7 +56,7 @@ } }, "gmailWarning": { - "message": "これは Gmail アドレスではないようです。プラスアドレスは Gmail で最も効果的に機能します。" + "message": "Gmail アドレスではないようです。プラスアドレスは Gmail で最もよく機能します。" }, "random": { "message": "ランダム" @@ -65,7 +65,7 @@ "message": "カスタムタグ" }, "gmailTricks": { - "message": "Gmail の便利機能" + "message": "Gmail の小技" }, "tabTagsShort": { "message": "タグ" @@ -97,7 +97,7 @@ } }, "pageLabel": { - "message": "$PAGE$ページ", + "message": "ページ $PAGE$", "placeholders": { "page": { "content": "$1" @@ -138,10 +138,10 @@ "message": "生成されたバリエーション" }, "gmailTrickInfoLabel": { - "message": "Gmailの小技:" + "message": "Gmail の小技:" }, "gmailTrickInfo": { - "message": "ドットは無視され、+ 以降はすべて同じ受信トレイに届きます" + "message": "ドットは無視され、+ の後ろはすべて同じ受信トレイに届きます" }, "format": { "message": "形式" @@ -162,7 +162,7 @@ "message": "エイリアス数" }, "generateRandomAliases": { - "message": "$COUNT$ 件のランダムエイリアスを生成", + "message": "$COUNT$ 個のランダムエイリアス$PLURAL$を生成", "placeholders": { "count": { "content": "$1" @@ -176,7 +176,7 @@ "message": "生成されたエイリアス" }, "totalCount": { - "message": "合計 $COUNT$ 件", + "message": "合計 $COUNT$", "placeholders": { "count": { "content": "$1" @@ -184,7 +184,7 @@ } }, "copiedAliases": { - "message": "$COUNT$ 件のエイリアスをコピーしました!", + "message": "$COUNT$ 個のエイリアスをコピーしました!", "placeholders": { "count": { "content": "$1" @@ -195,13 +195,13 @@ "message": "コピーに失敗しました" }, "formatPrivateMail": { - "message": "形式:private-mail-xxxx" + "message": "形式: private-mail-xxxx" }, "formatAlphanumeric": { - "message": "8 文字のランダム文字" + "message": "8 個のランダム文字" }, "formatWords": { - "message": "2 つのランダム単語" + "message": "2 個のランダム単語" }, "formatTimestamp": { "message": "Unix タイムスタンプ" @@ -216,7 +216,7 @@ "message": "あなたのプリセット" }, "example": { - "message": "例:" + "message": "例:" }, "copyExample": { "message": "例をコピー" @@ -240,7 +240,7 @@ "message": "選択" }, "starredCount": { - "message": "$COUNT$ 件をスター付き", + "message": "$COUNT$ 件にスター", "placeholders": { "count": { "content": "$1" @@ -248,13 +248,13 @@ } }, "deselectAll": { - "message": "選択解除" + "message": "すべて選択解除" }, "selectAll": { "message": "すべて選択" }, "selectedCount": { - "message": "$COUNT$ 件選択中", + "message": "$COUNT$ 件を選択", "placeholders": { "count": { "content": "$1" @@ -292,7 +292,7 @@ "message": "すべてのタグ" }, "mostRecent": { - "message": "最新順" + "message": "最新" }, "az": { "message": "A-Z" @@ -301,13 +301,13 @@ "message": "お気に入りはまだありません" }, "starEmailsHint": { - "message": "履歴内のメールにスターを付けると、ここからすばやくアクセスできます" + "message": "履歴のメールにスターを付けると、ここからすばやくアクセスできます" }, "noResultsFound": { "message": "結果が見つかりません" }, "differentSearchHint": { - "message": "別の検索語またはフィルターを試してください" + "message": "別の検索またはフィルターを試してください" }, "showQrCode": { "message": "QR コードを表示" @@ -319,7 +319,7 @@ "message": "お気に入りに追加" }, "showingRange": { - "message": "$TOTAL$ 件中 $START$〜$END$ 件を表示", + "message": "$TOTAL$ 件中 $START$-$END$ を表示", "placeholders": { "start": { "content": "$1" @@ -389,7 +389,7 @@ "message": "履歴内の合計" }, "badgeAllTime": { - "message": "総生成数" + "message": "総生成数(全期間)" }, "badgeToday": { "message": "今日作成" @@ -410,7 +410,7 @@ "message": "自動保存上限" }, "aliasesLimit": { - "message": "$COUNT$ 件のエイリアス", + "message": "$COUNT$ 個のエイリアス", "placeholders": { "count": { "content": "$1" @@ -463,7 +463,7 @@ "message": "メールアカウント" }, "manageAccountsDescription": { - "message": "Gmail アカウントを管理します。各アカウントには個別の履歴、統計、お気に入りがあります。" + "message": "Gmail アカウントを管理します。各アカウントには独自の履歴、統計、お気に入りがあります。" }, "noAccountsFound": { "message": "アカウントが見つかりません。メイン画面からアカウントを追加してください。" @@ -499,7 +499,7 @@ "message": "アカウントを削除しますか?" }, "deleteAccountMessage": { - "message": "「$LABEL$」($EMAIL$)を削除しますか?\n\nこれにより以下が完全に削除されます:\n- このアカウントのすべての履歴\n- すべての統計\n- すべてのお気に入り\n\nこの操作は元に戻せません。", + "message": "\"$LABEL$\"($EMAIL$)を削除しますか?\n\nこれにより次の内容が完全に削除されます:\n- このアカウントのすべての履歴\n- すべての統計\n- すべてのお気に入り\n\nこの操作は元に戻せません。", "placeholders": { "label": { "content": "$1" @@ -516,7 +516,7 @@ "message": "アカウントのメールを変更しますか?" }, "changeAccountEmailMessage": { - "message": "メールを\n$OLD_EMAIL$\nから\n$NEW_EMAIL$\nに変更しますか?\n\nこれにより:\n- すべての履歴、統計、お気に入りが新しいメールに移行されます\n- アカウントのメールが更新されます\n- 古いメールに関連付けられたデータが削除されます\n\n続行しますか?", + "message": "メールを\n$OLD_EMAIL$\nから\n$NEW_EMAIL$\nに変更しますか?\n\nこれにより次の処理が行われます:\n- すべての履歴、統計、お気に入りを新しいメールへ移行\n- アカウントのメールを更新\n- 古いメールに関連するデータを削除\n\n続行しますか?", "placeholders": { "old_email": { "content": "$1" @@ -529,6 +529,12 @@ "changeEmail": { "message": "メールを変更" }, + "menuForThisWebsite": { + "message": "このウェブサイト用" + }, + "menuLoading": { + "message": "読み込み中..." + }, "menuRandomEmailAlias": { "message": "ランダムメールエイリアス" }, @@ -539,10 +545,10 @@ "message": "プリセットなし - 設定で追加" }, "menuGmailTricks": { - "message": "Gmail の便利機能" + "message": "Gmail の小技" }, "menuDotVariation": { - "message": "ドットバリエーション" + "message": "ドットのバリエーション" }, "menuGooglemailDomain": { "message": "Googlemail ドメイン" @@ -566,7 +572,7 @@ "message": "インポートに失敗しました - 無効なファイル" }, "toastSettingsReset": { - "message": "設定をデフォルトに戻しました" + "message": "設定をデフォルトにリセットしました" }, "toastAccountSwitched": { "message": "アカウントを切り替えました" @@ -592,19 +598,19 @@ "message": "有効なメールアドレスを入力してください" }, "errorDuplicateEmail": { - "message": "このメールアドレスは別のアカウントで既に使用されています" + "message": "このメールアドレスは別のアカウントですでに使用されています" }, "errorEnterEmail": { "message": "メールアドレスを入力してください" }, "errorAccountExists": { - "message": "このアカウントは既に存在します" + "message": "このアカウントはすでに存在します" }, "toastQrFailed": { "message": "QR コードの生成に失敗しました" }, "toastExportedAliases": { - "message": "$COUNT$ 件のエイリアスをエクスポートしました", + "message": "$COUNT$ 個のエイリアスをエクスポートしました", "placeholders": { "count": { "content": "$1" @@ -612,7 +618,7 @@ } }, "toastDeletedAliases": { - "message": "$COUNT$ 件のエイリアスを削除しました", + "message": "$COUNT$ 個のエイリアスを削除しました", "placeholders": { "count": { "content": "$1" @@ -649,10 +655,10 @@ "message": "始めましょう" }, "enterGmailAddress": { - "message": "Gmail アドレスを入力してください" + "message": "Gmail アドレスを入力" }, "pressTabForGmail": { - "message": "$KEY$ を押して @gmail.com を入力", + "message": "@gmail.com には $KEY$ を押してください", "placeholders": { "key": { "content": "$1" @@ -663,13 +669,13 @@ "message": "設定中..." }, "getStarted": { - "message": "始める" + "message": "開始" }, "advancedSetup": { "message": "設定で詳細セットアップ" }, "whatYouCanDo": { - "message": "できること:" + "message": "できること:" }, "featurePrivateEmail": { "message": "プライベートメール生成" @@ -678,7 +684,7 @@ "message": "カスタムタグとプリセット" }, "featureGmailTricks": { - "message": "Gmail 高度な便利機能" + "message": "Gmail の高度な小技" }, "welcomeFooter": { "message": "すべてのデータはローカルに保存されます。追跡なし、サーバーなし。" @@ -692,6 +698,56 @@ } }, "clickStarHint": { - "message": "履歴内の任意のエイリアスのスターをクリックすると、ここに追加できます" + "message": "履歴内の任意のエイリアスのスターをクリックして、ここに追加します" + }, + "viewStatistics": { + "message": "統計を表示" + }, + "statistics": { + "message": "統計" + }, + "metricsTab": { + "message": "指標" + }, + "timelineTab": { + "message": "タイムライン" + }, + "tagsTab": { + "message": "タグ" + }, + "totalGenerated": { + "message": "総生成数" + }, + "createdToday": { + "message": "今日作成" + }, + "thisWeek": { + "message": "今週" + }, + "thisMonth": { + "message": "今月" + }, + "topTag": { + "message": "上位タグ" + }, + "totalTags": { + "message": "タグ合計" + }, + "noTagsYet": { + "message": "タグはまだありません" + }, + "disableInlineForSite": { "message": "このサイトでインラインヘルパーを無効にする" }, + "inlineDisabledSites": { "message": "インラインヘルパーを無効にしたサイト" }, + "inlineDisabledSitesDescription": { "message": "これらのサイトではインラインアイコンとクイックポップアップが非表示になります。" }, + "noDisabledSites": { "message": "無効にしたサイトはありません" }, + "enableInlineForSite": { "message": "有効にする" }, + "reportReview": { "message": "報告 / レビュー" }, + "percentLabel": { + "message": "$PERCENT$%", + "placeholders": { + "percent": { + "content": "$1" + } + } } } diff --git a/public/_locales/ko/messages.json b/public/_locales/ko/messages.json new file mode 100644 index 0000000..8406d2f --- /dev/null +++ b/public/_locales/ko/messages.json @@ -0,0 +1,753 @@ +{ + "extensionName": { + "message": "Gmail Alias Toolkit" + }, + "extensionDescription": { + "message": "+tag 주소와 프리셋으로 Gmail 별칭을 생성하고 관리하세요" + }, + "headerSubtitle": { + "message": "+tag 주소로 별칭 생성" + }, + "settings": { + "message": "설정" + }, + "back": { + "message": "뒤로" + }, + "close": { + "message": "닫기" + }, + "cancel": { + "message": "취소" + }, + "copy": { + "message": "복사" + }, + "copyAll": { + "message": "모두 복사" + }, + "copyToClipboard": { + "message": "클립보드에 복사" + }, + "activeGmailAddress": { + "message": "활성 Gmail 주소" + }, + "addNewAccount": { + "message": "새 계정 추가" + }, + "addAccount": { + "message": "계정 추가" + }, + "addNewAccountTitle": { + "message": "새 계정 추가" + }, + "emailPlaceholder": { + "message": "your.email" + }, + "accountLabelPlaceholder": { + "message": "라벨(선택 사항, 예: 업무, 개인)" + }, + "pressTabToAddGmail": { + "message": "@gmail.com을 추가하려면 $KEY$ 키를 누르세요", + "placeholders": { + "key": { + "content": "$1" + } + } + }, + "gmailWarning": { + "message": "Gmail 주소처럼 보이지 않습니다. 플러스 주소 지정은 Gmail에서 가장 잘 작동합니다." + }, + "random": { + "message": "무작위" + }, + "customTags": { + "message": "사용자 지정 태그" + }, + "gmailTricks": { + "message": "Gmail 팁" + }, + "tabTagsShort": { + "message": "태그" + }, + "tabTricksShort": { + "message": "팁" + }, + "generating": { + "message": "생성 중" + }, + "copied": { + "message": "복사됨" + }, + "switchToLightMode": { + "message": "라이트 모드로 전환" + }, + "switchToDarkMode": { + "message": "다크 모드로 전환" + }, + "aliasColumn": { + "message": "별칭" + }, + "copyEmailTooltip": { + "message": "$EMAIL$ - 클릭하여 복사", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "pageLabel": { + "message": "페이지 $PAGE$", + "placeholders": { + "page": { + "content": "$1" + } + } + }, + "dotTrick": { + "message": "점 트릭" + }, + "plusTags": { + "message": "플러스(+) 태그" + }, + "googlemail": { + "message": "Googlemail" + }, + "removeDots": { + "message": "점 제거" + }, + "dotPlus": { + "message": "점 + 플러스" + }, + "allCombos": { + "message": "모든 조합" + }, + "numberOfVariations": { + "message": "변형 수" + }, + "randomizeDotPositions": { + "message": "점 위치 무작위화" + }, + "sequential": { + "message": "순차" + }, + "generateTricks": { + "message": "팁 생성" + }, + "generatedVariations": { + "message": "생성된 변형" + }, + "gmailTrickInfoLabel": { + "message": "Gmail 팁:" + }, + "gmailTrickInfo": { + "message": "점은 무시되며 + 뒤의 모든 내용은 같은 받은편지함으로 전달됩니다" + }, + "format": { + "message": "형식" + }, + "privateMailFormat": { + "message": "비공개 메일(private-mail-xxxx)" + }, + "randomCharactersFormat": { + "message": "무작위 문자(abc123xy)" + }, + "randomWordsFormat": { + "message": "무작위 단어(happy-fox-42)" + }, + "timestampFormat": { + "message": "타임스탬프(1234567890)" + }, + "numberOfAliases": { + "message": "별칭 수" + }, + "generateRandomAliases": { + "message": "무작위 별칭 $COUNT$개$PLURAL$ 생성", + "placeholders": { + "count": { + "content": "$1" + }, + "plural": { + "content": "$2" + } + } + }, + "generatedAliases": { + "message": "생성된 별칭" + }, + "totalCount": { + "message": "총 $COUNT$개", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "copiedAliases": { + "message": "별칭 $COUNT$개를 복사했습니다!", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "failedToCopy": { + "message": "복사 실패" + }, + "formatPrivateMail": { + "message": "형식: private-mail-xxxx" + }, + "formatAlphanumeric": { + "message": "무작위 문자 8개" + }, + "formatWords": { + "message": "무작위 단어 2개" + }, + "formatTimestamp": { + "message": "Unix 타임스탬프" + }, + "tagPlaceholder": { + "message": "태그 입력(예: shopping, work)" + }, + "generate": { + "message": "생성" + }, + "yourPresets": { + "message": "내 프리셋" + }, + "example": { + "message": "예:" + }, + "copyExample": { + "message": "예시 복사" + }, + "recentAliases": { + "message": "최근 별칭" + }, + "favorites": { + "message": "즐겨찾기" + }, + "exportAsCsv": { + "message": "CSV로 내보내기" + }, + "exportAsJson": { + "message": "JSON으로 내보내기" + }, + "selectAliases": { + "message": "별칭 선택" + }, + "select": { + "message": "선택" + }, + "starredCount": { + "message": "별표 표시 $COUNT$개", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "deselectAll": { + "message": "전체 선택 해제" + }, + "selectAll": { + "message": "전체 선택" + }, + "selectedCount": { + "message": "$COUNT$개 선택됨", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "deleteCount": { + "message": "$COUNT$개 삭제", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "allCount": { + "message": "전체($COUNT$)", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "favoritesCount": { + "message": "즐겨찾기($COUNT$)", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "searchAliases": { + "message": "별칭 검색..." + }, + "allTags": { + "message": "모든 태그" + }, + "mostRecent": { + "message": "최신순" + }, + "az": { + "message": "A-Z" + }, + "noFavoritesYet": { + "message": "아직 즐겨찾기가 없습니다" + }, + "starEmailsHint": { + "message": "기록의 이메일에 별표를 표시하면 여기에서 빠르게 접근할 수 있습니다" + }, + "noResultsFound": { + "message": "결과를 찾을 수 없습니다" + }, + "differentSearchHint": { + "message": "다른 검색어나 필터를 시도해 보세요" + }, + "showQrCode": { + "message": "QR 코드 표시" + }, + "removeFromFavorites": { + "message": "즐겨찾기에서 제거" + }, + "addToFavorites": { + "message": "즐겨찾기에 추가" + }, + "showingRange": { + "message": "총 $TOTAL$개 중 $START$-$END$ 표시", + "placeholders": { + "start": { + "content": "$1" + }, + "end": { + "content": "$2" + }, + "total": { + "content": "$3" + } + } + }, + "perPage": { + "message": "$COUNT$ / 페이지", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "firstPage": { + "message": "첫 페이지" + }, + "previousPage": { + "message": "이전 페이지" + }, + "nextPage": { + "message": "다음 페이지" + }, + "lastPage": { + "message": "마지막 페이지" + }, + "scanToCopyAlias": { + "message": "스캔하여 별칭 복사" + }, + "general": { + "message": "일반" + }, + "accounts": { + "message": "계정" + }, + "changelog": { + "message": "변경 로그" + }, + "appearanceDisplay": { + "message": "모양 및 표시" + }, + "theme": { + "message": "테마" + }, + "themeLight": { + "message": "라이트" + }, + "themeDark": { + "message": "다크" + }, + "themeAuto": { + "message": "시스템(자동)" + }, + "badgeCounter": { + "message": "배지 카운터" + }, + "badgeNone": { + "message": "없음(숨김)" + }, + "badgeTotal": { + "message": "기록의 총합" + }, + "badgeAllTime": { + "message": "총 생성 수(전체 기간)" + }, + "badgeToday": { + "message": "오늘 생성됨" + }, + "badgeWeek": { + "message": "이번 주" + }, + "copyNotifications": { + "message": "복사 알림" + }, + "aliasGeneration": { + "message": "별칭 생성" + }, + "randomAliasFormat": { + "message": "무작위 별칭 형식" + }, + "autoSaveLimit": { + "message": "자동 저장 제한" + }, + "aliasesLimit": { + "message": "별칭 $COUNT$개", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "customPresets": { + "message": "사용자 지정 프리셋" + }, + "label": { + "message": "라벨" + }, + "tag": { + "message": "태그" + }, + "addPreset": { + "message": "+ 프리셋 추가" + }, + "dataManagement": { + "message": "데이터 관리" + }, + "export": { + "message": "내보내기" + }, + "import": { + "message": "가져오기" + }, + "clear": { + "message": "지우기" + }, + "resetSettings": { + "message": "모든 설정을 기본값으로 재설정" + }, + "resetSettingsTitle": { + "message": "설정을 재설정할까요?" + }, + "resetSettingsMessage": { + "message": "모든 설정이 기본값으로 복원됩니다." + }, + "reset": { + "message": "재설정" + }, + "clearHistoryTitle": { + "message": "최근 별칭을 지울까요?" + }, + "clearHistoryMessage": { + "message": "최근 기록 목록에서 모든 별칭을 제거합니다." + }, + "emailAccounts": { + "message": "이메일 계정" + }, + "manageAccountsDescription": { + "message": "Gmail 계정을 관리하세요. 각 계정에는 자체 기록, 통계, 즐겨찾기가 있습니다." + }, + "noAccountsFound": { + "message": "계정을 찾을 수 없습니다. 기본 화면에서 계정을 추가하세요." + }, + "emailAddress": { + "message": "이메일 주소" + }, + "accountLabel": { + "message": "계정 라벨" + }, + "emailAddressPlaceholder": { + "message": "your.email@gmail.com" + }, + "emailChangeWarning": { + "message": "이메일을 변경하면 모든 데이터가 새 이메일 주소로 이전됩니다" + }, + "saveChanges": { + "message": "변경 사항 저장" + }, + "active": { + "message": "활성" + }, + "editAccount": { + "message": "계정 편집" + }, + "cannotDeleteLastAccountTitle": { + "message": "마지막 계정은 삭제할 수 없습니다" + }, + "deleteThisAccount": { + "message": "이 계정 삭제" + }, + "deleteAccountTitle": { + "message": "계정을 삭제할까요?" + }, + "deleteAccountMessage": { + "message": "\"$LABEL$\"($EMAIL$)을 삭제할까요?\n\n다음 항목이 영구적으로 삭제됩니다:\n- 이 계정의 모든 기록\n- 모든 통계\n- 모든 즐겨찾기\n\n이 작업은 되돌릴 수 없습니다.", + "placeholders": { + "label": { + "content": "$1" + }, + "email": { + "content": "$2" + } + } + }, + "delete": { + "message": "삭제" + }, + "changeAccountEmailTitle": { + "message": "계정 이메일을 변경할까요?" + }, + "changeAccountEmailMessage": { + "message": "이메일을\n$OLD_EMAIL$\n에서\n$NEW_EMAIL$\n으로 변경할까요?\n\n다음 작업이 수행됩니다:\n- 모든 기록, 통계, 즐겨찾기를 새 이메일로 이전\n- 계정 이메일 업데이트\n- 이전 이메일과 연결된 데이터 삭제\n\n계속할까요?", + "placeholders": { + "old_email": { + "content": "$1" + }, + "new_email": { + "content": "$2" + } + } + }, + "changeEmail": { + "message": "이메일 변경" + }, + "menuForThisWebsite": { + "message": "이 웹사이트용" + }, + "menuLoading": { + "message": "불러오는 중..." + }, + "menuRandomEmailAlias": { + "message": "무작위 이메일 별칭" + }, + "menuCustomTags": { + "message": "사용자 지정 태그" + }, + "menuNoPresets": { + "message": "프리셋 없음 - 설정에서 추가" + }, + "menuGmailTricks": { + "message": "Gmail 팁" + }, + "menuDotVariation": { + "message": "점 변형" + }, + "menuGooglemailDomain": { + "message": "Googlemail 도메인" + }, + "menuRemoveAllDots": { + "message": "모든 점 제거" + }, + "toastPresetAdded": { + "message": "프리셋이 추가되었습니다" + }, + "toastPresetRemoved": { + "message": "프리셋이 제거되었습니다" + }, + "toastSettingsExported": { + "message": "설정을 내보냈습니다" + }, + "toastSettingsImported": { + "message": "설정을 가져왔습니다" + }, + "toastImportFailed": { + "message": "가져오기 실패 - 잘못된 파일" + }, + "toastSettingsReset": { + "message": "설정이 기본값으로 재설정되었습니다" + }, + "toastAccountSwitched": { + "message": "계정이 전환되었습니다" + }, + "toastAccountDeleted": { + "message": "계정이 삭제되었습니다" + }, + "toastAccountUpdated": { + "message": "계정이 업데이트되었습니다" + }, + "toastAccountAdded": { + "message": "$LABEL$ 추가됨", + "placeholders": { + "label": { + "content": "$1" + } + } + }, + "errorLabelRequired": { + "message": "라벨은 비워 둘 수 없습니다" + }, + "errorInvalidEmail": { + "message": "유효한 이메일 주소를 입력하세요" + }, + "errorDuplicateEmail": { + "message": "이 이메일 주소는 이미 다른 계정에서 사용 중입니다" + }, + "errorEnterEmail": { + "message": "이메일 주소를 입력하세요" + }, + "errorAccountExists": { + "message": "이 계정은 이미 존재합니다" + }, + "toastQrFailed": { + "message": "QR 코드 생성 실패" + }, + "toastExportedAliases": { + "message": "별칭 $COUNT$개를 내보냈습니다", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "toastDeletedAliases": { + "message": "별칭 $COUNT$개를 삭제했습니다", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "toastHistoryCleared": { + "message": "기록이 지워졌습니다" + }, + "toastFavoriteAdded": { + "message": "즐겨찾기에 추가됨" + }, + "toastFavoriteRemoved": { + "message": "즐겨찾기에서 제거됨" + }, + "toastCopiedEmail": { + "message": "$EMAIL$ 복사됨", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "toastCopyFailed": { + "message": "복사 실패" + }, + "welcomeTitle": { + "message": "Gmail Alias Toolkit에 오신 것을 환영합니다" + }, + "welcomeSubtitle": { + "message": "개인정보 보호와 정리를 위해 무제한 이메일 별칭을 생성하세요" + }, + "letsGetStarted": { + "message": "시작해 봅시다" + }, + "enterGmailAddress": { + "message": "Gmail 주소를 입력하세요" + }, + "pressTabForGmail": { + "message": "@gmail.com에는 $KEY$ 키를 누르세요", + "placeholders": { + "key": { + "content": "$1" + } + } + }, + "settingUp": { + "message": "설정 중..." + }, + "getStarted": { + "message": "시작하기" + }, + "advancedSetup": { + "message": "설정에서 고급 설정" + }, + "whatYouCanDo": { + "message": "할 수 있는 작업:" + }, + "featurePrivateEmail": { + "message": "비공개 이메일 생성기" + }, + "featureCustomTags": { + "message": "사용자 지정 태그 및 프리셋" + }, + "featureGmailTricks": { + "message": "Gmail 고급 팁" + }, + "welcomeFooter": { + "message": "모든 데이터는 로컬에 저장됩니다. 추적 없음, 서버 없음." + }, + "favoritesSaved": { + "message": "$COUNT$개 저장됨", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "clickStarHint": { + "message": "기록의 별칭에서 별표를 클릭하여 여기에 추가하세요" + }, + "viewStatistics": { + "message": "통계 보기" + }, + "statistics": { + "message": "통계" + }, + "metricsTab": { + "message": "지표" + }, + "timelineTab": { + "message": "타임라인" + }, + "tagsTab": { + "message": "태그" + }, + "totalGenerated": { + "message": "총 생성 수" + }, + "createdToday": { + "message": "오늘 생성됨" + }, + "thisWeek": { + "message": "이번 주" + }, + "thisMonth": { + "message": "이번 달" + }, + "topTag": { + "message": "상위 태그" + }, + "totalTags": { + "message": "전체 태그" + }, + "noTagsYet": { + "message": "아직 태그가 없습니다" + }, + "disableInlineForSite": { "message": "이 웹사이트에서 인라인 도우미 끄기" }, + "inlineDisabledSites": { "message": "인라인 도우미를 끈 웹사이트" }, + "inlineDisabledSitesDescription": { "message": "이 웹사이트에서는 인라인 아이콘과 빠른 팝업이 숨겨집니다." }, + "noDisabledSites": { "message": "비활성화된 웹사이트가 없습니다" }, + "enableInlineForSite": { "message": "사용" }, + "reportReview": { "message": "신고 / 리뷰" }, + "percentLabel": { + "message": "$PERCENT$%", + "placeholders": { + "percent": { + "content": "$1" + } + } + } +} diff --git a/public/_locales/pl/messages.json b/public/_locales/pl/messages.json new file mode 100644 index 0000000..091061e --- /dev/null +++ b/public/_locales/pl/messages.json @@ -0,0 +1,753 @@ +{ + "extensionName": { + "message": "Gmail Alias Toolkit" + }, + "extensionDescription": { + "message": "Twórz aliasy Gmail i zarządzaj nimi za pomocą adresów +tag oraz gotowych ustawień" + }, + "headerSubtitle": { + "message": "Generuj aliasy za pomocą adresów +tag" + }, + "settings": { + "message": "Ustawienia" + }, + "back": { + "message": "Wstecz" + }, + "close": { + "message": "Zamknij" + }, + "cancel": { + "message": "Anuluj" + }, + "copy": { + "message": "Kopiuj" + }, + "copyAll": { + "message": "Kopiuj wszystko" + }, + "copyToClipboard": { + "message": "Kopiuj do schowka" + }, + "activeGmailAddress": { + "message": "Aktywny adres Gmail" + }, + "addNewAccount": { + "message": "Dodaj nowe konto" + }, + "addAccount": { + "message": "Dodaj konto" + }, + "addNewAccountTitle": { + "message": "Dodaj nowe konto" + }, + "emailPlaceholder": { + "message": "twoj.email" + }, + "accountLabelPlaceholder": { + "message": "Etykieta (opcjonalnie, np. Praca, Prywatne)" + }, + "pressTabToAddGmail": { + "message": "Naciśnij $KEY$, aby dodać @gmail.com", + "placeholders": { + "key": { + "content": "$1" + } + } + }, + "gmailWarning": { + "message": "To nie wygląda jak adres Gmail. Adresowanie plus działa najlepiej z Gmail." + }, + "random": { + "message": "Losowo" + }, + "customTags": { + "message": "Niestandardowe tagi" + }, + "gmailTricks": { + "message": "Triki Gmail" + }, + "tabTagsShort": { + "message": "Tagi" + }, + "tabTricksShort": { + "message": "Triki" + }, + "generating": { + "message": "Generowanie" + }, + "copied": { + "message": "Skopiowano" + }, + "switchToLightMode": { + "message": "Przełącz na tryb jasny" + }, + "switchToDarkMode": { + "message": "Przełącz na tryb ciemny" + }, + "aliasColumn": { + "message": "Alias" + }, + "copyEmailTooltip": { + "message": "$EMAIL$ - kliknij, aby skopiować", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "pageLabel": { + "message": "Strona $PAGE$", + "placeholders": { + "page": { + "content": "$1" + } + } + }, + "dotTrick": { + "message": "Trik z kropkami" + }, + "plusTags": { + "message": "Tagi Plus (+)" + }, + "googlemail": { + "message": "Googlemail" + }, + "removeDots": { + "message": "Usuń kropki" + }, + "dotPlus": { + "message": "Kropka + Plus" + }, + "allCombos": { + "message": "Wszystkie kombinacje" + }, + "numberOfVariations": { + "message": "Liczba wariantów" + }, + "randomizeDotPositions": { + "message": "Losuj pozycje kropek" + }, + "sequential": { + "message": "Sekwencyjnie" + }, + "generateTricks": { + "message": "Generuj triki" + }, + "generatedVariations": { + "message": "Wygenerowane warianty" + }, + "gmailTrickInfoLabel": { + "message": "Trik Gmail:" + }, + "gmailTrickInfo": { + "message": "Kropki są ignorowane, a wszystko po + trafia do tej samej skrzynki odbiorczej" + }, + "format": { + "message": "Format" + }, + "privateMailFormat": { + "message": "Prywatna poczta (private-mail-xxxx)" + }, + "randomCharactersFormat": { + "message": "Losowe znaki (abc123xy)" + }, + "randomWordsFormat": { + "message": "Losowe słowa (happy-fox-42)" + }, + "timestampFormat": { + "message": "Znacznik czasu (1234567890)" + }, + "numberOfAliases": { + "message": "Liczba aliasów" + }, + "generateRandomAliases": { + "message": "Generuj $COUNT$ losowy alias$PLURAL$", + "placeholders": { + "count": { + "content": "$1" + }, + "plural": { + "content": "$2" + } + } + }, + "generatedAliases": { + "message": "Wygenerowane aliasy" + }, + "totalCount": { + "message": "$COUNT$ łącznie", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "copiedAliases": { + "message": "Skopiowano $COUNT$ aliasów!", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "failedToCopy": { + "message": "Nie udało się skopiować" + }, + "formatPrivateMail": { + "message": "Format: private-mail-xxxx" + }, + "formatAlphanumeric": { + "message": "8 losowych znaków" + }, + "formatWords": { + "message": "2 losowe słowa" + }, + "formatTimestamp": { + "message": "Znacznik czasu Unix" + }, + "tagPlaceholder": { + "message": "Wpisz tag (np. shopping, work)" + }, + "generate": { + "message": "Generuj" + }, + "yourPresets": { + "message": "Twoje presety" + }, + "example": { + "message": "Przykład:" + }, + "copyExample": { + "message": "Kopiuj przykład" + }, + "recentAliases": { + "message": "Ostatnie aliasy" + }, + "favorites": { + "message": "Ulubione" + }, + "exportAsCsv": { + "message": "Eksportuj jako CSV" + }, + "exportAsJson": { + "message": "Eksportuj jako JSON" + }, + "selectAliases": { + "message": "Wybierz aliasy" + }, + "select": { + "message": "Wybierz" + }, + "starredCount": { + "message": "$COUNT$ oznaczono gwiazdką", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "deselectAll": { + "message": "Odznacz wszystko" + }, + "selectAll": { + "message": "Zaznacz wszystko" + }, + "selectedCount": { + "message": "Wybrano $COUNT$", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "deleteCount": { + "message": "Usuń $COUNT$", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "allCount": { + "message": "Wszystkie ($COUNT$)", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "favoritesCount": { + "message": "Ulubione ($COUNT$)", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "searchAliases": { + "message": "Szukaj aliasów..." + }, + "allTags": { + "message": "Wszystkie tagi" + }, + "mostRecent": { + "message": "Najnowsze" + }, + "az": { + "message": "A-Z" + }, + "noFavoritesYet": { + "message": "Brak ulubionych" + }, + "starEmailsHint": { + "message": "Oznacz e-maile z historii gwiazdką, aby mieć do nich szybki dostęp tutaj" + }, + "noResultsFound": { + "message": "Nie znaleziono wyników" + }, + "differentSearchHint": { + "message": "Spróbuj innego wyszukiwania lub filtra" + }, + "showQrCode": { + "message": "Pokaż kod QR" + }, + "removeFromFavorites": { + "message": "Usuń z ulubionych" + }, + "addToFavorites": { + "message": "Dodaj do ulubionych" + }, + "showingRange": { + "message": "Wyświetlanie $START$-$END$ z $TOTAL$", + "placeholders": { + "start": { + "content": "$1" + }, + "end": { + "content": "$2" + }, + "total": { + "content": "$3" + } + } + }, + "perPage": { + "message": "$COUNT$ / strona", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "firstPage": { + "message": "Pierwsza strona" + }, + "previousPage": { + "message": "Poprzednia strona" + }, + "nextPage": { + "message": "Następna strona" + }, + "lastPage": { + "message": "Ostatnia strona" + }, + "scanToCopyAlias": { + "message": "Zeskanuj, aby skopiować alias" + }, + "general": { + "message": "Ogólne" + }, + "accounts": { + "message": "Konta" + }, + "changelog": { + "message": "Dziennik zmian" + }, + "appearanceDisplay": { + "message": "Wygląd i wyświetlanie" + }, + "theme": { + "message": "Motyw" + }, + "themeLight": { + "message": "Jasny" + }, + "themeDark": { + "message": "Ciemny" + }, + "themeAuto": { + "message": "Systemowy (Auto)" + }, + "badgeCounter": { + "message": "Licznik odznaki" + }, + "badgeNone": { + "message": "Brak (ukryty)" + }, + "badgeTotal": { + "message": "Łącznie w historii" + }, + "badgeAllTime": { + "message": "Łącznie wygenerowane (cały czas)" + }, + "badgeToday": { + "message": "Utworzone dzisiaj" + }, + "badgeWeek": { + "message": "Ten tydzień" + }, + "copyNotifications": { + "message": "Powiadomienia kopiowania" + }, + "aliasGeneration": { + "message": "Generowanie aliasów" + }, + "randomAliasFormat": { + "message": "Format losowego aliasu" + }, + "autoSaveLimit": { + "message": "Limit automatycznego zapisu" + }, + "aliasesLimit": { + "message": "$COUNT$ aliasów", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "customPresets": { + "message": "Niestandardowe presety" + }, + "label": { + "message": "Etykieta" + }, + "tag": { + "message": "Tag" + }, + "addPreset": { + "message": "+ Dodaj preset" + }, + "dataManagement": { + "message": "Zarządzanie danymi" + }, + "export": { + "message": "Eksport" + }, + "import": { + "message": "Import" + }, + "clear": { + "message": "Wyczyść" + }, + "resetSettings": { + "message": "Przywróć wszystkie ustawienia domyślne" + }, + "resetSettingsTitle": { + "message": "Zresetować ustawienia?" + }, + "resetSettingsMessage": { + "message": "To przywróci każde ustawienie do wartości domyślnej." + }, + "reset": { + "message": "Resetuj" + }, + "clearHistoryTitle": { + "message": "Wyczyścić ostatnie aliasy?" + }, + "clearHistoryMessage": { + "message": "To usunie wszystkie aliasy z listy ostatniej historii." + }, + "emailAccounts": { + "message": "Konta e-mail" + }, + "manageAccountsDescription": { + "message": "Zarządzaj swoimi kontami Gmail. Każde konto ma własną historię, statystyki i ulubione." + }, + "noAccountsFound": { + "message": "Nie znaleziono kont. Dodaj konto z ekranu głównego." + }, + "emailAddress": { + "message": "Adres e-mail" + }, + "accountLabel": { + "message": "Etykieta konta" + }, + "emailAddressPlaceholder": { + "message": "twoj.email@gmail.com" + }, + "emailChangeWarning": { + "message": "Zmiana e-maila przeniesie wszystkie dane na nowy adres e-mail" + }, + "saveChanges": { + "message": "Zapisz zmiany" + }, + "active": { + "message": "Aktywne" + }, + "editAccount": { + "message": "Edytuj konto" + }, + "cannotDeleteLastAccountTitle": { + "message": "Nie można usunąć ostatniego konta" + }, + "deleteThisAccount": { + "message": "Usuń to konto" + }, + "deleteAccountTitle": { + "message": "Usunąć konto?" + }, + "deleteAccountMessage": { + "message": "Usunąć \"$LABEL$\" ($EMAIL$)?\n\nTo trwale usunie:\n- Całą historię tego konta\n- Wszystkie statystyki\n- Wszystkie ulubione\n\nTej akcji nie można cofnąć.", + "placeholders": { + "label": { + "content": "$1" + }, + "email": { + "content": "$2" + } + } + }, + "delete": { + "message": "Usuń" + }, + "changeAccountEmailTitle": { + "message": "Zmienić e-mail konta?" + }, + "changeAccountEmailMessage": { + "message": "Zmienić e-mail z\n$OLD_EMAIL$\nna\n$NEW_EMAIL$?\n\nTo spowoduje:\n- Migrację całej historii, statystyk i ulubionych na nowy e-mail\n- Aktualizację e-maila konta\n- Usunięcie danych powiązanych ze starym e-mailem\n\nKontynuować?", + "placeholders": { + "old_email": { + "content": "$1" + }, + "new_email": { + "content": "$2" + } + } + }, + "changeEmail": { + "message": "Zmień e-mail" + }, + "menuForThisWebsite": { + "message": "Dla tej witryny" + }, + "menuLoading": { + "message": "Ładowanie..." + }, + "menuRandomEmailAlias": { + "message": "Losowy alias e-mail" + }, + "menuCustomTags": { + "message": "Niestandardowe tagi" + }, + "menuNoPresets": { + "message": "Brak presetów - dodaj w Ustawieniach" + }, + "menuGmailTricks": { + "message": "Triki Gmail" + }, + "menuDotVariation": { + "message": "Wariant z kropkami" + }, + "menuGooglemailDomain": { + "message": "Domena Googlemail" + }, + "menuRemoveAllDots": { + "message": "Usuń wszystkie kropki" + }, + "toastPresetAdded": { + "message": "Preset dodany" + }, + "toastPresetRemoved": { + "message": "Preset usunięty" + }, + "toastSettingsExported": { + "message": "Ustawienia wyeksportowane" + }, + "toastSettingsImported": { + "message": "Ustawienia zaimportowane" + }, + "toastImportFailed": { + "message": "Import nieudany - nieprawidłowy plik" + }, + "toastSettingsReset": { + "message": "Ustawienia przywrócone do domyślnych" + }, + "toastAccountSwitched": { + "message": "Konto przełączone" + }, + "toastAccountDeleted": { + "message": "Konto usunięte" + }, + "toastAccountUpdated": { + "message": "Konto zaktualizowane" + }, + "toastAccountAdded": { + "message": "Dodano $LABEL$", + "placeholders": { + "label": { + "content": "$1" + } + } + }, + "errorLabelRequired": { + "message": "Etykieta nie może być pusta" + }, + "errorInvalidEmail": { + "message": "Wpisz prawidłowy adres e-mail" + }, + "errorDuplicateEmail": { + "message": "Ten adres e-mail jest już używany przez inne konto" + }, + "errorEnterEmail": { + "message": "Wpisz adres e-mail" + }, + "errorAccountExists": { + "message": "To konto już istnieje" + }, + "toastQrFailed": { + "message": "Nie udało się wygenerować kodu QR" + }, + "toastExportedAliases": { + "message": "Wyeksportowano $COUNT$ aliasów", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "toastDeletedAliases": { + "message": "Usunięto $COUNT$ aliasów", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "toastHistoryCleared": { + "message": "Historia wyczyszczona" + }, + "toastFavoriteAdded": { + "message": "Dodano do ulubionych" + }, + "toastFavoriteRemoved": { + "message": "Usunięto z ulubionych" + }, + "toastCopiedEmail": { + "message": "Skopiowano $EMAIL$", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "toastCopyFailed": { + "message": "Nie udało się skopiować" + }, + "welcomeTitle": { + "message": "Witamy w Gmail Alias Toolkit" + }, + "welcomeSubtitle": { + "message": "Generuj nieograniczone aliasy e-mail dla prywatności i organizacji" + }, + "letsGetStarted": { + "message": "Zaczynajmy" + }, + "enterGmailAddress": { + "message": "Wpisz swój adres Gmail" + }, + "pressTabForGmail": { + "message": "Naciśnij $KEY$ dla @gmail.com", + "placeholders": { + "key": { + "content": "$1" + } + } + }, + "settingUp": { + "message": "Konfiguracja..." + }, + "getStarted": { + "message": "Rozpocznij" + }, + "advancedSetup": { + "message": "Konfiguracja zaawansowana w Ustawieniach" + }, + "whatYouCanDo": { + "message": "Co możesz zrobić:" + }, + "featurePrivateEmail": { + "message": "Generator prywatnych e-maili" + }, + "featureCustomTags": { + "message": "Niestandardowe tagi i presety" + }, + "featureGmailTricks": { + "message": "Zaawansowane triki Gmail" + }, + "welcomeFooter": { + "message": "Wszystkie dane są przechowywane lokalnie. Bez śledzenia, bez serwera." + }, + "favoritesSaved": { + "message": "Zapisano $COUNT$", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "clickStarHint": { + "message": "Kliknij gwiazdkę na dowolnym aliasie w historii, aby dodać go tutaj" + }, + "viewStatistics": { + "message": "Wyświetl statystyki" + }, + "statistics": { + "message": "Statystyki" + }, + "metricsTab": { + "message": "Metryki" + }, + "timelineTab": { + "message": "Oś czasu" + }, + "tagsTab": { + "message": "Tagi" + }, + "totalGenerated": { + "message": "Łącznie wygenerowane" + }, + "createdToday": { + "message": "Utworzone dzisiaj" + }, + "thisWeek": { + "message": "Ten tydzień" + }, + "thisMonth": { + "message": "Ten miesiąc" + }, + "topTag": { + "message": "Najlepszy tag" + }, + "totalTags": { + "message": "Łącznie tagów" + }, + "noTagsYet": { + "message": "Brak tagów" + }, + "disableInlineForSite": { "message": "Wyłącz pomocnika inline w tej witrynie" }, + "inlineDisabledSites": { "message": "Witryny z wyłączonym pomocnikiem inline" }, + "inlineDisabledSitesDescription": { "message": "Ikona inline i szybkie okno są ukryte w tych witrynach." }, + "noDisabledSites": { "message": "Brak wyłączonych witryn" }, + "enableInlineForSite": { "message": "Włącz" }, + "reportReview": { "message": "Zgłoś / Oceń" }, + "percentLabel": { + "message": "$PERCENT$%", + "placeholders": { + "percent": { + "content": "$1" + } + } + } +} diff --git a/public/_locales/pt_BR/messages.json b/public/_locales/pt_BR/messages.json new file mode 100644 index 0000000..10888d7 --- /dev/null +++ b/public/_locales/pt_BR/messages.json @@ -0,0 +1,753 @@ +{ + "extensionName": { + "message": "Gmail Alias Toolkit" + }, + "extensionDescription": { + "message": "Gere e gerencie aliases do Gmail usando endereços +tag e predefinições" + }, + "headerSubtitle": { + "message": "Gere aliases usando endereços +tag" + }, + "settings": { + "message": "Configurações" + }, + "back": { + "message": "Voltar" + }, + "close": { + "message": "Fechar" + }, + "cancel": { + "message": "Cancelar" + }, + "copy": { + "message": "Copiar" + }, + "copyAll": { + "message": "Copiar tudo" + }, + "copyToClipboard": { + "message": "Copiar para a área de transferência" + }, + "activeGmailAddress": { + "message": "Endereço do Gmail ativo" + }, + "addNewAccount": { + "message": "Adicionar nova conta" + }, + "addAccount": { + "message": "Adicionar conta" + }, + "addNewAccountTitle": { + "message": "Adicionar nova conta" + }, + "emailPlaceholder": { + "message": "seu.email" + }, + "accountLabelPlaceholder": { + "message": "Rótulo (opcional, ex.: Trabalho, Pessoal)" + }, + "pressTabToAddGmail": { + "message": "Pressione $KEY$ para adicionar @gmail.com", + "placeholders": { + "key": { + "content": "$1" + } + } + }, + "gmailWarning": { + "message": "Isso não parece um endereço do Gmail. O endereçamento plus funciona melhor com Gmail." + }, + "random": { + "message": "Aleatório" + }, + "customTags": { + "message": "Tags personalizadas" + }, + "gmailTricks": { + "message": "Truques do Gmail" + }, + "tabTagsShort": { + "message": "Tags" + }, + "tabTricksShort": { + "message": "Truques" + }, + "generating": { + "message": "Gerando" + }, + "copied": { + "message": "Copiado" + }, + "switchToLightMode": { + "message": "Alternar para modo claro" + }, + "switchToDarkMode": { + "message": "Alternar para modo escuro" + }, + "aliasColumn": { + "message": "Alias" + }, + "copyEmailTooltip": { + "message": "$EMAIL$ - clique para copiar", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "pageLabel": { + "message": "Página $PAGE$", + "placeholders": { + "page": { + "content": "$1" + } + } + }, + "dotTrick": { + "message": "Truque dos pontos" + }, + "plusTags": { + "message": "Tags Plus (+)" + }, + "googlemail": { + "message": "Googlemail" + }, + "removeDots": { + "message": "Remover pontos" + }, + "dotPlus": { + "message": "Ponto + Plus" + }, + "allCombos": { + "message": "Todas as combinações" + }, + "numberOfVariations": { + "message": "Número de variações" + }, + "randomizeDotPositions": { + "message": "Aleatorizar posições dos pontos" + }, + "sequential": { + "message": "Sequencial" + }, + "generateTricks": { + "message": "Gerar truques" + }, + "generatedVariations": { + "message": "Variações geradas" + }, + "gmailTrickInfoLabel": { + "message": "Truque do Gmail:" + }, + "gmailTrickInfo": { + "message": "Pontos são ignorados e tudo após + vai para a mesma caixa de entrada" + }, + "format": { + "message": "Formato" + }, + "privateMailFormat": { + "message": "E-mail privado (private-mail-xxxx)" + }, + "randomCharactersFormat": { + "message": "Caracteres aleatórios (abc123xy)" + }, + "randomWordsFormat": { + "message": "Palavras aleatórias (happy-fox-42)" + }, + "timestampFormat": { + "message": "Timestamp (1234567890)" + }, + "numberOfAliases": { + "message": "Número de aliases" + }, + "generateRandomAliases": { + "message": "Gerar $COUNT$ alias aleatório$PLURAL$", + "placeholders": { + "count": { + "content": "$1" + }, + "plural": { + "content": "$2" + } + } + }, + "generatedAliases": { + "message": "Aliases gerados" + }, + "totalCount": { + "message": "$COUNT$ no total", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "copiedAliases": { + "message": "$COUNT$ aliases copiados!", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "failedToCopy": { + "message": "Falha ao copiar" + }, + "formatPrivateMail": { + "message": "Formato: private-mail-xxxx" + }, + "formatAlphanumeric": { + "message": "8 caracteres aleatórios" + }, + "formatWords": { + "message": "2 palavras aleatórias" + }, + "formatTimestamp": { + "message": "Timestamp Unix" + }, + "tagPlaceholder": { + "message": "Digite uma tag (ex.: shopping, work)" + }, + "generate": { + "message": "Gerar" + }, + "yourPresets": { + "message": "Suas predefinições" + }, + "example": { + "message": "Exemplo:" + }, + "copyExample": { + "message": "Copiar exemplo" + }, + "recentAliases": { + "message": "Aliases recentes" + }, + "favorites": { + "message": "Favoritos" + }, + "exportAsCsv": { + "message": "Exportar como CSV" + }, + "exportAsJson": { + "message": "Exportar como JSON" + }, + "selectAliases": { + "message": "Selecionar aliases" + }, + "select": { + "message": "Selecionar" + }, + "starredCount": { + "message": "$COUNT$ com estrela", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "deselectAll": { + "message": "Desmarcar tudo" + }, + "selectAll": { + "message": "Selecionar tudo" + }, + "selectedCount": { + "message": "$COUNT$ selecionados", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "deleteCount": { + "message": "Excluir $COUNT$", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "allCount": { + "message": "Todos ($COUNT$)", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "favoritesCount": { + "message": "Favoritos ($COUNT$)", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "searchAliases": { + "message": "Pesquisar aliases..." + }, + "allTags": { + "message": "Todas as tags" + }, + "mostRecent": { + "message": "Mais recentes" + }, + "az": { + "message": "A-Z" + }, + "noFavoritesYet": { + "message": "Ainda sem favoritos" + }, + "starEmailsHint": { + "message": "Marque e-mails do seu histórico com estrela para acessá-los rapidamente aqui" + }, + "noResultsFound": { + "message": "Nenhum resultado encontrado" + }, + "differentSearchHint": { + "message": "Tente outra pesquisa ou filtro" + }, + "showQrCode": { + "message": "Mostrar código QR" + }, + "removeFromFavorites": { + "message": "Remover dos favoritos" + }, + "addToFavorites": { + "message": "Adicionar aos favoritos" + }, + "showingRange": { + "message": "Mostrando $START$-$END$ de $TOTAL$", + "placeholders": { + "start": { + "content": "$1" + }, + "end": { + "content": "$2" + }, + "total": { + "content": "$3" + } + } + }, + "perPage": { + "message": "$COUNT$ / página", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "firstPage": { + "message": "Primeira página" + }, + "previousPage": { + "message": "Página anterior" + }, + "nextPage": { + "message": "Próxima página" + }, + "lastPage": { + "message": "Última página" + }, + "scanToCopyAlias": { + "message": "Escaneie para copiar o alias" + }, + "general": { + "message": "Geral" + }, + "accounts": { + "message": "Contas" + }, + "changelog": { + "message": "Registro de alterações" + }, + "appearanceDisplay": { + "message": "Aparência e exibição" + }, + "theme": { + "message": "Tema" + }, + "themeLight": { + "message": "Claro" + }, + "themeDark": { + "message": "Escuro" + }, + "themeAuto": { + "message": "Sistema (Auto)" + }, + "badgeCounter": { + "message": "Contador do badge" + }, + "badgeNone": { + "message": "Nenhum (oculto)" + }, + "badgeTotal": { + "message": "Total no histórico" + }, + "badgeAllTime": { + "message": "Total gerado (todo o período)" + }, + "badgeToday": { + "message": "Criados hoje" + }, + "badgeWeek": { + "message": "Esta semana" + }, + "copyNotifications": { + "message": "Notificações de cópia" + }, + "aliasGeneration": { + "message": "Geração de aliases" + }, + "randomAliasFormat": { + "message": "Formato de alias aleatório" + }, + "autoSaveLimit": { + "message": "Limite de salvamento automático" + }, + "aliasesLimit": { + "message": "$COUNT$ aliases", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "customPresets": { + "message": "Predefinições personalizadas" + }, + "label": { + "message": "Rótulo" + }, + "tag": { + "message": "Tag" + }, + "addPreset": { + "message": "+ Adicionar predefinição" + }, + "dataManagement": { + "message": "Gerenciamento de dados" + }, + "export": { + "message": "Exportar" + }, + "import": { + "message": "Importar" + }, + "clear": { + "message": "Limpar" + }, + "resetSettings": { + "message": "Redefinir todas as configurações para o padrão" + }, + "resetSettingsTitle": { + "message": "Redefinir configurações?" + }, + "resetSettingsMessage": { + "message": "Isso restaurará cada configuração para seu valor padrão." + }, + "reset": { + "message": "Redefinir" + }, + "clearHistoryTitle": { + "message": "Limpar aliases recentes?" + }, + "clearHistoryMessage": { + "message": "Isso remove todos os aliases da lista de histórico recente." + }, + "emailAccounts": { + "message": "Contas de e-mail" + }, + "manageAccountsDescription": { + "message": "Gerencie suas contas do Gmail. Cada conta tem seu próprio histórico, estatísticas e favoritos." + }, + "noAccountsFound": { + "message": "Nenhuma conta encontrada. Adicione uma conta pela tela principal." + }, + "emailAddress": { + "message": "Endereço de e-mail" + }, + "accountLabel": { + "message": "Rótulo da conta" + }, + "emailAddressPlaceholder": { + "message": "seu.email@gmail.com" + }, + "emailChangeWarning": { + "message": "Alterar o e-mail migrará todos os dados para o novo endereço de e-mail" + }, + "saveChanges": { + "message": "Salvar alterações" + }, + "active": { + "message": "Ativa" + }, + "editAccount": { + "message": "Editar conta" + }, + "cannotDeleteLastAccountTitle": { + "message": "Não é possível excluir a última conta" + }, + "deleteThisAccount": { + "message": "Excluir esta conta" + }, + "deleteAccountTitle": { + "message": "Excluir conta?" + }, + "deleteAccountMessage": { + "message": "Excluir \"$LABEL$\" ($EMAIL$)?\n\nIsso excluirá permanentemente:\n- Todo o histórico desta conta\n- Todas as estatísticas\n- Todos os favoritos\n\nEsta ação não pode ser desfeita.", + "placeholders": { + "label": { + "content": "$1" + }, + "email": { + "content": "$2" + } + } + }, + "delete": { + "message": "Excluir" + }, + "changeAccountEmailTitle": { + "message": "Alterar e-mail da conta?" + }, + "changeAccountEmailMessage": { + "message": "Alterar e-mail de\n$OLD_EMAIL$\npara\n$NEW_EMAIL$?\n\nIsso irá:\n- Migrar todo o histórico, estatísticas e favoritos para o novo e-mail\n- Atualizar o e-mail da conta\n- Excluir dados associados ao e-mail antigo\n\nContinuar?", + "placeholders": { + "old_email": { + "content": "$1" + }, + "new_email": { + "content": "$2" + } + } + }, + "changeEmail": { + "message": "Alterar e-mail" + }, + "menuForThisWebsite": { + "message": "Para este site" + }, + "menuLoading": { + "message": "Carregando..." + }, + "menuRandomEmailAlias": { + "message": "Alias de e-mail aleatório" + }, + "menuCustomTags": { + "message": "Tags personalizadas" + }, + "menuNoPresets": { + "message": "Sem predefinições - adicione em Configurações" + }, + "menuGmailTricks": { + "message": "Truques do Gmail" + }, + "menuDotVariation": { + "message": "Variação com pontos" + }, + "menuGooglemailDomain": { + "message": "Domínio Googlemail" + }, + "menuRemoveAllDots": { + "message": "Remover todos os pontos" + }, + "toastPresetAdded": { + "message": "Predefinição adicionada" + }, + "toastPresetRemoved": { + "message": "Predefinição removida" + }, + "toastSettingsExported": { + "message": "Configurações exportadas" + }, + "toastSettingsImported": { + "message": "Configurações importadas" + }, + "toastImportFailed": { + "message": "Falha na importação - arquivo inválido" + }, + "toastSettingsReset": { + "message": "Configurações redefinidas para o padrão" + }, + "toastAccountSwitched": { + "message": "Conta alternada" + }, + "toastAccountDeleted": { + "message": "Conta excluída" + }, + "toastAccountUpdated": { + "message": "Conta atualizada" + }, + "toastAccountAdded": { + "message": "$LABEL$ adicionada", + "placeholders": { + "label": { + "content": "$1" + } + } + }, + "errorLabelRequired": { + "message": "O rótulo não pode estar vazio" + }, + "errorInvalidEmail": { + "message": "Digite um endereço de e-mail válido" + }, + "errorDuplicateEmail": { + "message": "Este endereço de e-mail já é usado por outra conta" + }, + "errorEnterEmail": { + "message": "Digite um endereço de e-mail" + }, + "errorAccountExists": { + "message": "Esta conta já existe" + }, + "toastQrFailed": { + "message": "Falha ao gerar código QR" + }, + "toastExportedAliases": { + "message": "$COUNT$ aliases exportados", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "toastDeletedAliases": { + "message": "$COUNT$ aliases excluídos", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "toastHistoryCleared": { + "message": "Histórico limpo" + }, + "toastFavoriteAdded": { + "message": "Adicionado aos favoritos" + }, + "toastFavoriteRemoved": { + "message": "Removido dos favoritos" + }, + "toastCopiedEmail": { + "message": "$EMAIL$ copiado", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "toastCopyFailed": { + "message": "Falha ao copiar" + }, + "welcomeTitle": { + "message": "Bem-vindo ao Gmail Alias Toolkit" + }, + "welcomeSubtitle": { + "message": "Gere aliases de e-mail ilimitados para privacidade e organização" + }, + "letsGetStarted": { + "message": "Vamos começar" + }, + "enterGmailAddress": { + "message": "Digite seu endereço do Gmail" + }, + "pressTabForGmail": { + "message": "Pressione $KEY$ para @gmail.com", + "placeholders": { + "key": { + "content": "$1" + } + } + }, + "settingUp": { + "message": "Configurando..." + }, + "getStarted": { + "message": "Começar" + }, + "advancedSetup": { + "message": "Configuração avançada em Configurações" + }, + "whatYouCanDo": { + "message": "O que você pode fazer:" + }, + "featurePrivateEmail": { + "message": "Gerador de e-mail privado" + }, + "featureCustomTags": { + "message": "Tags e predefinições personalizadas" + }, + "featureGmailTricks": { + "message": "Truques avançados do Gmail" + }, + "welcomeFooter": { + "message": "Todos os dados são armazenados localmente. Sem rastreamento, sem servidor." + }, + "favoritesSaved": { + "message": "$COUNT$ salvos", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "clickStarHint": { + "message": "Clique na estrela em qualquer alias no histórico para adicioná-lo aqui" + }, + "viewStatistics": { + "message": "Ver estatísticas" + }, + "statistics": { + "message": "Estatísticas" + }, + "metricsTab": { + "message": "Métricas" + }, + "timelineTab": { + "message": "Linha do tempo" + }, + "tagsTab": { + "message": "Tags" + }, + "totalGenerated": { + "message": "Total gerado" + }, + "createdToday": { + "message": "Criados hoje" + }, + "thisWeek": { + "message": "Esta semana" + }, + "thisMonth": { + "message": "Este mês" + }, + "topTag": { + "message": "Tag principal" + }, + "totalTags": { + "message": "Total de tags" + }, + "noTagsYet": { + "message": "Ainda sem tags" + }, + "disableInlineForSite": { "message": "Desativar o assistente inline neste site" }, + "inlineDisabledSites": { "message": "Sites com assistente inline desativado" }, + "inlineDisabledSitesDescription": { "message": "O ícone inline e o popup rápido ficam ocultos nestes sites." }, + "noDisabledSites": { "message": "Nenhum site desativado" }, + "enableInlineForSite": { "message": "Ativar" }, + "reportReview": { "message": "Relatar / Avaliar" }, + "percentLabel": { + "message": "$PERCENT$%", + "placeholders": { + "percent": { + "content": "$1" + } + } + } +} diff --git a/public/_locales/ru/messages.json b/public/_locales/ru/messages.json new file mode 100644 index 0000000..dc0b1c8 --- /dev/null +++ b/public/_locales/ru/messages.json @@ -0,0 +1,753 @@ +{ + "extensionName": { + "message": "Gmail Alias Toolkit" + }, + "extensionDescription": { + "message": "Создавайте псевдонимы Gmail и управляйте ими с помощью адресов +tag и шаблонов" + }, + "headerSubtitle": { + "message": "Создавайте псевдонимы с адресами +tag" + }, + "settings": { + "message": "Настройки" + }, + "back": { + "message": "Назад" + }, + "close": { + "message": "Закрыть" + }, + "cancel": { + "message": "Отмена" + }, + "copy": { + "message": "Копировать" + }, + "copyAll": { + "message": "Копировать все" + }, + "copyToClipboard": { + "message": "Копировать в буфер обмена" + }, + "activeGmailAddress": { + "message": "Активный адрес Gmail" + }, + "addNewAccount": { + "message": "Добавить новый аккаунт" + }, + "addAccount": { + "message": "Добавить аккаунт" + }, + "addNewAccountTitle": { + "message": "Добавить новый аккаунт" + }, + "emailPlaceholder": { + "message": "your.email" + }, + "accountLabelPlaceholder": { + "message": "Метка (необязательно, например Работа, Личное)" + }, + "pressTabToAddGmail": { + "message": "Нажмите $KEY$, чтобы добавить @gmail.com", + "placeholders": { + "key": { + "content": "$1" + } + } + }, + "gmailWarning": { + "message": "Это не похоже на адрес Gmail. Plus-адресация лучше всего работает с Gmail." + }, + "random": { + "message": "Случайно" + }, + "customTags": { + "message": "Пользовательские теги" + }, + "gmailTricks": { + "message": "Трюки Gmail" + }, + "tabTagsShort": { + "message": "Теги" + }, + "tabTricksShort": { + "message": "Трюки" + }, + "generating": { + "message": "Создание" + }, + "copied": { + "message": "Скопировано" + }, + "switchToLightMode": { + "message": "Переключиться на светлый режим" + }, + "switchToDarkMode": { + "message": "Переключиться на темный режим" + }, + "aliasColumn": { + "message": "Псевдоним" + }, + "copyEmailTooltip": { + "message": "$EMAIL$ - нажмите, чтобы скопировать", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "pageLabel": { + "message": "Страница $PAGE$", + "placeholders": { + "page": { + "content": "$1" + } + } + }, + "dotTrick": { + "message": "Трюк с точками" + }, + "plusTags": { + "message": "Plus-теги (+)" + }, + "googlemail": { + "message": "Googlemail" + }, + "removeDots": { + "message": "Удалить точки" + }, + "dotPlus": { + "message": "Точка + Plus" + }, + "allCombos": { + "message": "Все комбинации" + }, + "numberOfVariations": { + "message": "Количество вариантов" + }, + "randomizeDotPositions": { + "message": "Случайные позиции точек" + }, + "sequential": { + "message": "Последовательно" + }, + "generateTricks": { + "message": "Создать трюки" + }, + "generatedVariations": { + "message": "Созданные варианты" + }, + "gmailTrickInfoLabel": { + "message": "Трюк Gmail:" + }, + "gmailTrickInfo": { + "message": "Точки игнорируются, а все после + попадает в тот же почтовый ящик" + }, + "format": { + "message": "Формат" + }, + "privateMailFormat": { + "message": "Личная почта (private-mail-xxxx)" + }, + "randomCharactersFormat": { + "message": "Случайные символы (abc123xy)" + }, + "randomWordsFormat": { + "message": "Случайные слова (happy-fox-42)" + }, + "timestampFormat": { + "message": "Метка времени (1234567890)" + }, + "numberOfAliases": { + "message": "Количество псевдонимов" + }, + "generateRandomAliases": { + "message": "Создать $COUNT$ случайный псевдоним$PLURAL$", + "placeholders": { + "count": { + "content": "$1" + }, + "plural": { + "content": "$2" + } + } + }, + "generatedAliases": { + "message": "Созданные псевдонимы" + }, + "totalCount": { + "message": "Всего $COUNT$", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "copiedAliases": { + "message": "Скопировано псевдонимов: $COUNT$!", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "failedToCopy": { + "message": "Не удалось скопировать" + }, + "formatPrivateMail": { + "message": "Формат: private-mail-xxxx" + }, + "formatAlphanumeric": { + "message": "8 случайных символов" + }, + "formatWords": { + "message": "2 случайных слова" + }, + "formatTimestamp": { + "message": "Метка времени Unix" + }, + "tagPlaceholder": { + "message": "Введите тег (например, shopping, work)" + }, + "generate": { + "message": "Создать" + }, + "yourPresets": { + "message": "Ваши шаблоны" + }, + "example": { + "message": "Пример:" + }, + "copyExample": { + "message": "Копировать пример" + }, + "recentAliases": { + "message": "Недавние псевдонимы" + }, + "favorites": { + "message": "Избранное" + }, + "exportAsCsv": { + "message": "Экспорт как CSV" + }, + "exportAsJson": { + "message": "Экспорт как JSON" + }, + "selectAliases": { + "message": "Выбрать псевдонимы" + }, + "select": { + "message": "Выбрать" + }, + "starredCount": { + "message": "$COUNT$ отмечено", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "deselectAll": { + "message": "Снять выбор со всех" + }, + "selectAll": { + "message": "Выбрать все" + }, + "selectedCount": { + "message": "Выбрано $COUNT$", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "deleteCount": { + "message": "Удалить $COUNT$", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "allCount": { + "message": "Все ($COUNT$)", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "favoritesCount": { + "message": "Избранное ($COUNT$)", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "searchAliases": { + "message": "Поиск псевдонимов..." + }, + "allTags": { + "message": "Все теги" + }, + "mostRecent": { + "message": "Самые новые" + }, + "az": { + "message": "A-Z" + }, + "noFavoritesYet": { + "message": "Пока нет избранного" + }, + "starEmailsHint": { + "message": "Отмечайте письма из истории звездочкой, чтобы быстро открывать их здесь" + }, + "noResultsFound": { + "message": "Ничего не найдено" + }, + "differentSearchHint": { + "message": "Попробуйте другой поиск или фильтр" + }, + "showQrCode": { + "message": "Показать QR-код" + }, + "removeFromFavorites": { + "message": "Удалить из избранного" + }, + "addToFavorites": { + "message": "Добавить в избранное" + }, + "showingRange": { + "message": "Показано $START$-$END$ из $TOTAL$", + "placeholders": { + "start": { + "content": "$1" + }, + "end": { + "content": "$2" + }, + "total": { + "content": "$3" + } + } + }, + "perPage": { + "message": "$COUNT$ / стр.", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "firstPage": { + "message": "Первая страница" + }, + "previousPage": { + "message": "Предыдущая страница" + }, + "nextPage": { + "message": "Следующая страница" + }, + "lastPage": { + "message": "Последняя страница" + }, + "scanToCopyAlias": { + "message": "Отсканируйте, чтобы скопировать псевдоним" + }, + "general": { + "message": "Общие" + }, + "accounts": { + "message": "Аккаунты" + }, + "changelog": { + "message": "Журнал изменений" + }, + "appearanceDisplay": { + "message": "Внешний вид и отображение" + }, + "theme": { + "message": "Тема" + }, + "themeLight": { + "message": "Светлая" + }, + "themeDark": { + "message": "Темная" + }, + "themeAuto": { + "message": "Система (авто)" + }, + "badgeCounter": { + "message": "Счетчик значка" + }, + "badgeNone": { + "message": "Нет (скрыто)" + }, + "badgeTotal": { + "message": "Всего в истории" + }, + "badgeAllTime": { + "message": "Всего создано (за все время)" + }, + "badgeToday": { + "message": "Создано сегодня" + }, + "badgeWeek": { + "message": "На этой неделе" + }, + "copyNotifications": { + "message": "Уведомления о копировании" + }, + "aliasGeneration": { + "message": "Создание псевдонимов" + }, + "randomAliasFormat": { + "message": "Формат случайного псевдонима" + }, + "autoSaveLimit": { + "message": "Лимит автосохранения" + }, + "aliasesLimit": { + "message": "$COUNT$ псевдонимов", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "customPresets": { + "message": "Пользовательские шаблоны" + }, + "label": { + "message": "Метка" + }, + "tag": { + "message": "Тег" + }, + "addPreset": { + "message": "+ Добавить шаблон" + }, + "dataManagement": { + "message": "Управление данными" + }, + "export": { + "message": "Экспорт" + }, + "import": { + "message": "Импорт" + }, + "clear": { + "message": "Очистить" + }, + "resetSettings": { + "message": "Сбросить все настройки по умолчанию" + }, + "resetSettingsTitle": { + "message": "Сбросить настройки?" + }, + "resetSettingsMessage": { + "message": "Это восстановит все настройки до значений по умолчанию." + }, + "reset": { + "message": "Сбросить" + }, + "clearHistoryTitle": { + "message": "Очистить недавние псевдонимы?" + }, + "clearHistoryMessage": { + "message": "Это удалит все псевдонимы из списка недавней истории." + }, + "emailAccounts": { + "message": "Почтовые аккаунты" + }, + "manageAccountsDescription": { + "message": "Управляйте своими аккаунтами Gmail. У каждого аккаунта своя история, статистика и избранное." + }, + "noAccountsFound": { + "message": "Аккаунты не найдены. Добавьте аккаунт с главного экрана." + }, + "emailAddress": { + "message": "Адрес электронной почты" + }, + "accountLabel": { + "message": "Метка аккаунта" + }, + "emailAddressPlaceholder": { + "message": "your.email@gmail.com" + }, + "emailChangeWarning": { + "message": "При изменении e-mail все данные будут перенесены на новый адрес" + }, + "saveChanges": { + "message": "Сохранить изменения" + }, + "active": { + "message": "Активен" + }, + "editAccount": { + "message": "Редактировать аккаунт" + }, + "cannotDeleteLastAccountTitle": { + "message": "Нельзя удалить последний аккаунт" + }, + "deleteThisAccount": { + "message": "Удалить этот аккаунт" + }, + "deleteAccountTitle": { + "message": "Удалить аккаунт?" + }, + "deleteAccountMessage": { + "message": "Удалить \"$LABEL$\" ($EMAIL$)?\n\nЭто навсегда удалит:\n- Всю историю этого аккаунта\n- Всю статистику\n- Все избранное\n\nЭто действие нельзя отменить.", + "placeholders": { + "label": { + "content": "$1" + }, + "email": { + "content": "$2" + } + } + }, + "delete": { + "message": "Удалить" + }, + "changeAccountEmailTitle": { + "message": "Изменить e-mail аккаунта?" + }, + "changeAccountEmailMessage": { + "message": "Изменить e-mail с\n$OLD_EMAIL$\nна\n$NEW_EMAIL$?\n\nЭто выполнит:\n- Перенос всей истории, статистики и избранного на новый e-mail\n- Обновление e-mail аккаунта\n- Удаление данных, связанных со старым e-mail\n\nПродолжить?", + "placeholders": { + "old_email": { + "content": "$1" + }, + "new_email": { + "content": "$2" + } + } + }, + "changeEmail": { + "message": "Изменить e-mail" + }, + "menuForThisWebsite": { + "message": "Для этого сайта" + }, + "menuLoading": { + "message": "Загрузка..." + }, + "menuRandomEmailAlias": { + "message": "Случайный e-mail псевдоним" + }, + "menuCustomTags": { + "message": "Пользовательские теги" + }, + "menuNoPresets": { + "message": "Нет шаблонов - добавьте в Настройках" + }, + "menuGmailTricks": { + "message": "Трюки Gmail" + }, + "menuDotVariation": { + "message": "Вариант с точками" + }, + "menuGooglemailDomain": { + "message": "Домен Googlemail" + }, + "menuRemoveAllDots": { + "message": "Удалить все точки" + }, + "toastPresetAdded": { + "message": "Шаблон добавлен" + }, + "toastPresetRemoved": { + "message": "Шаблон удален" + }, + "toastSettingsExported": { + "message": "Настройки экспортированы" + }, + "toastSettingsImported": { + "message": "Настройки импортированы" + }, + "toastImportFailed": { + "message": "Ошибка импорта - недопустимый файл" + }, + "toastSettingsReset": { + "message": "Настройки сброшены по умолчанию" + }, + "toastAccountSwitched": { + "message": "Аккаунт переключен" + }, + "toastAccountDeleted": { + "message": "Аккаунт удален" + }, + "toastAccountUpdated": { + "message": "Аккаунт обновлен" + }, + "toastAccountAdded": { + "message": "$LABEL$ добавлен", + "placeholders": { + "label": { + "content": "$1" + } + } + }, + "errorLabelRequired": { + "message": "Метка не может быть пустой" + }, + "errorInvalidEmail": { + "message": "Введите действительный адрес электронной почты" + }, + "errorDuplicateEmail": { + "message": "Этот адрес уже используется другим аккаунтом" + }, + "errorEnterEmail": { + "message": "Введите адрес электронной почты" + }, + "errorAccountExists": { + "message": "Этот аккаунт уже существует" + }, + "toastQrFailed": { + "message": "Не удалось создать QR-код" + }, + "toastExportedAliases": { + "message": "Экспортировано псевдонимов: $COUNT$", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "toastDeletedAliases": { + "message": "Удалено псевдонимов: $COUNT$", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "toastHistoryCleared": { + "message": "История очищена" + }, + "toastFavoriteAdded": { + "message": "Добавлено в избранное" + }, + "toastFavoriteRemoved": { + "message": "Удалено из избранного" + }, + "toastCopiedEmail": { + "message": "Скопировано $EMAIL$", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "toastCopyFailed": { + "message": "Не удалось скопировать" + }, + "welcomeTitle": { + "message": "Добро пожаловать в Gmail Alias Toolkit" + }, + "welcomeSubtitle": { + "message": "Создавайте неограниченное количество e-mail псевдонимов для приватности и порядка" + }, + "letsGetStarted": { + "message": "Начнем" + }, + "enterGmailAddress": { + "message": "Введите ваш адрес Gmail" + }, + "pressTabForGmail": { + "message": "Нажмите $KEY$ для @gmail.com", + "placeholders": { + "key": { + "content": "$1" + } + } + }, + "settingUp": { + "message": "Настройка..." + }, + "getStarted": { + "message": "Начать" + }, + "advancedSetup": { + "message": "Расширенная настройка в Настройках" + }, + "whatYouCanDo": { + "message": "Что можно делать:" + }, + "featurePrivateEmail": { + "message": "Генератор личной почты" + }, + "featureCustomTags": { + "message": "Пользовательские теги и шаблоны" + }, + "featureGmailTricks": { + "message": "Расширенные трюки Gmail" + }, + "welcomeFooter": { + "message": "Все данные хранятся локально. Без отслеживания, без сервера." + }, + "favoritesSaved": { + "message": "Сохранено $COUNT$", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "clickStarHint": { + "message": "Нажмите звезду на любом псевдониме в истории, чтобы добавить его сюда" + }, + "viewStatistics": { + "message": "Показать статистику" + }, + "statistics": { + "message": "Статистика" + }, + "metricsTab": { + "message": "Метрики" + }, + "timelineTab": { + "message": "Хронология" + }, + "tagsTab": { + "message": "Теги" + }, + "totalGenerated": { + "message": "Всего создано" + }, + "createdToday": { + "message": "Создано сегодня" + }, + "thisWeek": { + "message": "На этой неделе" + }, + "thisMonth": { + "message": "В этом месяце" + }, + "topTag": { + "message": "Лучший тег" + }, + "totalTags": { + "message": "Всего тегов" + }, + "noTagsYet": { + "message": "Пока нет тегов" + }, + "disableInlineForSite": { "message": "Отключить встроенного помощника на этом сайте" }, + "inlineDisabledSites": { "message": "Сайты с отключённым встроенным помощником" }, + "inlineDisabledSitesDescription": { "message": "На этих сайтах встроенный значок и быстрое окно будут скрыты." }, + "noDisabledSites": { "message": "Нет отключённых сайтов" }, + "enableInlineForSite": { "message": "Включить" }, + "reportReview": { "message": "Сообщить / Оценить" }, + "percentLabel": { + "message": "$PERCENT$%", + "placeholders": { + "percent": { + "content": "$1" + } + } + } +} diff --git a/public/_locales/tr/messages.json b/public/_locales/tr/messages.json new file mode 100644 index 0000000..68a680f --- /dev/null +++ b/public/_locales/tr/messages.json @@ -0,0 +1,753 @@ +{ + "extensionName": { + "message": "Gmail Alias Toolkit" + }, + "extensionDescription": { + "message": "+tag adresleri ve hazır ayarlarla Gmail takma adları oluşturun ve yönetin" + }, + "headerSubtitle": { + "message": "+tag adresleriyle takma adlar oluşturun" + }, + "settings": { + "message": "Ayarlar" + }, + "back": { + "message": "Geri" + }, + "close": { + "message": "Kapat" + }, + "cancel": { + "message": "İptal" + }, + "copy": { + "message": "Kopyala" + }, + "copyAll": { + "message": "Tümünü kopyala" + }, + "copyToClipboard": { + "message": "Panoya kopyala" + }, + "activeGmailAddress": { + "message": "Etkin Gmail Adresi" + }, + "addNewAccount": { + "message": "Yeni hesap ekle" + }, + "addAccount": { + "message": "Hesap ekle" + }, + "addNewAccountTitle": { + "message": "Yeni hesap ekle" + }, + "emailPlaceholder": { + "message": "email.adresin" + }, + "accountLabelPlaceholder": { + "message": "Etiket (isteğe bağlı, örn. İş, Kişisel)" + }, + "pressTabToAddGmail": { + "message": "@gmail.com eklemek için $KEY$ tuşuna basın", + "placeholders": { + "key": { + "content": "$1" + } + } + }, + "gmailWarning": { + "message": "Bu bir Gmail adresine benzemiyor. Plus adresleme en iyi Gmail ile çalışır." + }, + "random": { + "message": "Rastgele" + }, + "customTags": { + "message": "Özel etiketler" + }, + "gmailTricks": { + "message": "Gmail ipuçları" + }, + "tabTagsShort": { + "message": "Etiketler" + }, + "tabTricksShort": { + "message": "İpuçları" + }, + "generating": { + "message": "Oluşturuluyor" + }, + "copied": { + "message": "Kopyalandı" + }, + "switchToLightMode": { + "message": "Açık moda geç" + }, + "switchToDarkMode": { + "message": "Koyu moda geç" + }, + "aliasColumn": { + "message": "Takma ad" + }, + "copyEmailTooltip": { + "message": "$EMAIL$ - kopyalamak için tıklayın", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "pageLabel": { + "message": "Sayfa $PAGE$", + "placeholders": { + "page": { + "content": "$1" + } + } + }, + "dotTrick": { + "message": "Nokta ipucu" + }, + "plusTags": { + "message": "Plus (+) etiketleri" + }, + "googlemail": { + "message": "Googlemail" + }, + "removeDots": { + "message": "Noktaları kaldır" + }, + "dotPlus": { + "message": "Nokta + Plus" + }, + "allCombos": { + "message": "Tüm kombinasyonlar" + }, + "numberOfVariations": { + "message": "Varyasyon sayısı" + }, + "randomizeDotPositions": { + "message": "Nokta konumlarını rastgele yap" + }, + "sequential": { + "message": "Sıralı" + }, + "generateTricks": { + "message": "İpuçları oluştur" + }, + "generatedVariations": { + "message": "Oluşturulan varyasyonlar" + }, + "gmailTrickInfoLabel": { + "message": "Gmail ipucu:" + }, + "gmailTrickInfo": { + "message": "Noktalar yok sayılır ve + sonrasındaki her şey aynı gelen kutusuna gider" + }, + "format": { + "message": "Biçim" + }, + "privateMailFormat": { + "message": "Özel Posta (private-mail-xxxx)" + }, + "randomCharactersFormat": { + "message": "Rastgele karakterler (abc123xy)" + }, + "randomWordsFormat": { + "message": "Rastgele kelimeler (happy-fox-42)" + }, + "timestampFormat": { + "message": "Zaman damgası (1234567890)" + }, + "numberOfAliases": { + "message": "Takma ad sayısı" + }, + "generateRandomAliases": { + "message": "$COUNT$ rastgele takma ad$PLURAL$ oluştur", + "placeholders": { + "count": { + "content": "$1" + }, + "plural": { + "content": "$2" + } + } + }, + "generatedAliases": { + "message": "Oluşturulan takma adlar" + }, + "totalCount": { + "message": "Toplam $COUNT$", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "copiedAliases": { + "message": "$COUNT$ takma ad kopyalandı!", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "failedToCopy": { + "message": "Kopyalanamadı" + }, + "formatPrivateMail": { + "message": "Biçim: private-mail-xxxx" + }, + "formatAlphanumeric": { + "message": "8 rastgele karakter" + }, + "formatWords": { + "message": "2 rastgele kelime" + }, + "formatTimestamp": { + "message": "Unix zaman damgası" + }, + "tagPlaceholder": { + "message": "Etiket girin (örn. shopping, work)" + }, + "generate": { + "message": "Oluştur" + }, + "yourPresets": { + "message": "Hazır ayarlarınız" + }, + "example": { + "message": "Örnek:" + }, + "copyExample": { + "message": "Örneği kopyala" + }, + "recentAliases": { + "message": "Son takma adlar" + }, + "favorites": { + "message": "Favoriler" + }, + "exportAsCsv": { + "message": "CSV olarak dışa aktar" + }, + "exportAsJson": { + "message": "JSON olarak dışa aktar" + }, + "selectAliases": { + "message": "Takma adları seç" + }, + "select": { + "message": "Seç" + }, + "starredCount": { + "message": "$COUNT$ yıldızlı", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "deselectAll": { + "message": "Tüm seçimleri kaldır" + }, + "selectAll": { + "message": "Tümünü seç" + }, + "selectedCount": { + "message": "$COUNT$ seçildi", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "deleteCount": { + "message": "$COUNT$ sil", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "allCount": { + "message": "Tümü ($COUNT$)", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "favoritesCount": { + "message": "Favoriler ($COUNT$)", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "searchAliases": { + "message": "Takma ad ara..." + }, + "allTags": { + "message": "Tüm etiketler" + }, + "mostRecent": { + "message": "En yeni" + }, + "az": { + "message": "A-Z" + }, + "noFavoritesYet": { + "message": "Henüz favori yok" + }, + "starEmailsHint": { + "message": "Buradan hızlı erişim için geçmişinizdeki e-postalara yıldız ekleyin" + }, + "noResultsFound": { + "message": "Sonuç bulunamadı" + }, + "differentSearchHint": { + "message": "Farklı bir arama veya filtre deneyin" + }, + "showQrCode": { + "message": "QR kodunu göster" + }, + "removeFromFavorites": { + "message": "Favorilerden kaldır" + }, + "addToFavorites": { + "message": "Favorilere ekle" + }, + "showingRange": { + "message": "$TOTAL$ içinden $START$-$END$ gösteriliyor", + "placeholders": { + "start": { + "content": "$1" + }, + "end": { + "content": "$2" + }, + "total": { + "content": "$3" + } + } + }, + "perPage": { + "message": "$COUNT$ / sayfa", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "firstPage": { + "message": "İlk sayfa" + }, + "previousPage": { + "message": "Önceki sayfa" + }, + "nextPage": { + "message": "Sonraki sayfa" + }, + "lastPage": { + "message": "Son sayfa" + }, + "scanToCopyAlias": { + "message": "Takma adı kopyalamak için tara" + }, + "general": { + "message": "Genel" + }, + "accounts": { + "message": "Hesaplar" + }, + "changelog": { + "message": "Değişiklik günlüğü" + }, + "appearanceDisplay": { + "message": "Görünüm ve görüntüleme" + }, + "theme": { + "message": "Tema" + }, + "themeLight": { + "message": "Açık" + }, + "themeDark": { + "message": "Koyu" + }, + "themeAuto": { + "message": "Sistem (Otomatik)" + }, + "badgeCounter": { + "message": "Rozet sayacı" + }, + "badgeNone": { + "message": "Yok (Gizli)" + }, + "badgeTotal": { + "message": "Geçmişte toplam" + }, + "badgeAllTime": { + "message": "Toplam oluşturulan (tüm zamanlar)" + }, + "badgeToday": { + "message": "Bugün oluşturuldu" + }, + "badgeWeek": { + "message": "Bu hafta" + }, + "copyNotifications": { + "message": "Kopyalama bildirimleri" + }, + "aliasGeneration": { + "message": "Takma ad oluşturma" + }, + "randomAliasFormat": { + "message": "Rastgele takma ad biçimi" + }, + "autoSaveLimit": { + "message": "Otomatik kaydetme limiti" + }, + "aliasesLimit": { + "message": "$COUNT$ takma ad", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "customPresets": { + "message": "Özel hazır ayarlar" + }, + "label": { + "message": "Etiket" + }, + "tag": { + "message": "Etiket" + }, + "addPreset": { + "message": "+ Hazır ayar ekle" + }, + "dataManagement": { + "message": "Veri yönetimi" + }, + "export": { + "message": "Dışa aktar" + }, + "import": { + "message": "İçe aktar" + }, + "clear": { + "message": "Temizle" + }, + "resetSettings": { + "message": "Tüm ayarları varsayılana sıfırla" + }, + "resetSettingsTitle": { + "message": "Ayarlar sıfırlansın mı?" + }, + "resetSettingsMessage": { + "message": "Bu işlem tüm ayarları varsayılan değerlerine döndürür." + }, + "reset": { + "message": "Sıfırla" + }, + "clearHistoryTitle": { + "message": "Son takma adlar temizlensin mi?" + }, + "clearHistoryMessage": { + "message": "Bu işlem son geçmiş listesindeki tüm takma adları kaldırır." + }, + "emailAccounts": { + "message": "E-posta hesapları" + }, + "manageAccountsDescription": { + "message": "Gmail hesaplarınızı yönetin. Her hesabın kendi geçmişi, istatistikleri ve favorileri vardır." + }, + "noAccountsFound": { + "message": "Hesap bulunamadı. Lütfen ana ekrandan bir hesap ekleyin." + }, + "emailAddress": { + "message": "E-posta adresi" + }, + "accountLabel": { + "message": "Hesap etiketi" + }, + "emailAddressPlaceholder": { + "message": "email.adresin@gmail.com" + }, + "emailChangeWarning": { + "message": "E-posta değiştirildiğinde tüm veriler yeni e-posta adresine taşınır" + }, + "saveChanges": { + "message": "Değişiklikleri kaydet" + }, + "active": { + "message": "Etkin" + }, + "editAccount": { + "message": "Hesabı düzenle" + }, + "cannotDeleteLastAccountTitle": { + "message": "Son hesap silinemez" + }, + "deleteThisAccount": { + "message": "Bu hesabı sil" + }, + "deleteAccountTitle": { + "message": "Hesap silinsin mi?" + }, + "deleteAccountMessage": { + "message": "\"$LABEL$\" ($EMAIL$) silinsin mi?\n\nBu işlem kalıcı olarak şunları siler:\n- Bu hesaba ait tüm geçmiş\n- Tüm istatistikler\n- Tüm favoriler\n\nBu işlem geri alınamaz.", + "placeholders": { + "label": { + "content": "$1" + }, + "email": { + "content": "$2" + } + } + }, + "delete": { + "message": "Sil" + }, + "changeAccountEmailTitle": { + "message": "Hesap e-postası değiştirilsin mi?" + }, + "changeAccountEmailMessage": { + "message": "E-postayı\n$OLD_EMAIL$\nadresinden\n$NEW_EMAIL$\nadresine değiştirilsin mi?\n\nBu işlem:\n- Tüm geçmişi, istatistikleri ve favorileri yeni e-postaya taşır\n- Hesap e-postasını günceller\n- Eski e-posta ile ilişkili verileri siler\n\nDevam edilsin mi?", + "placeholders": { + "old_email": { + "content": "$1" + }, + "new_email": { + "content": "$2" + } + } + }, + "changeEmail": { + "message": "E-postayı değiştir" + }, + "menuForThisWebsite": { + "message": "Bu web sitesi için" + }, + "menuLoading": { + "message": "Yükleniyor..." + }, + "menuRandomEmailAlias": { + "message": "Rastgele e-posta takma adı" + }, + "menuCustomTags": { + "message": "Özel etiketler" + }, + "menuNoPresets": { + "message": "Hazır ayar yok - Ayarlardan ekleyin" + }, + "menuGmailTricks": { + "message": "Gmail ipuçları" + }, + "menuDotVariation": { + "message": "Nokta varyasyonu" + }, + "menuGooglemailDomain": { + "message": "Googlemail alan adı" + }, + "menuRemoveAllDots": { + "message": "Tüm noktaları kaldır" + }, + "toastPresetAdded": { + "message": "Hazır ayar eklendi" + }, + "toastPresetRemoved": { + "message": "Hazır ayar kaldırıldı" + }, + "toastSettingsExported": { + "message": "Ayarlar dışa aktarıldı" + }, + "toastSettingsImported": { + "message": "Ayarlar içe aktarıldı" + }, + "toastImportFailed": { + "message": "İçe aktarma başarısız - geçersiz dosya" + }, + "toastSettingsReset": { + "message": "Ayarlar varsayılana sıfırlandı" + }, + "toastAccountSwitched": { + "message": "Hesap değiştirildi" + }, + "toastAccountDeleted": { + "message": "Hesap silindi" + }, + "toastAccountUpdated": { + "message": "Hesap güncellendi" + }, + "toastAccountAdded": { + "message": "$LABEL$ eklendi", + "placeholders": { + "label": { + "content": "$1" + } + } + }, + "errorLabelRequired": { + "message": "Etiket boş olamaz" + }, + "errorInvalidEmail": { + "message": "Lütfen geçerli bir e-posta adresi girin" + }, + "errorDuplicateEmail": { + "message": "Bu e-posta adresi başka bir hesap tarafından kullanılıyor" + }, + "errorEnterEmail": { + "message": "Lütfen bir e-posta adresi girin" + }, + "errorAccountExists": { + "message": "Bu hesap zaten var" + }, + "toastQrFailed": { + "message": "QR kodu oluşturulamadı" + }, + "toastExportedAliases": { + "message": "$COUNT$ takma ad dışa aktarıldı", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "toastDeletedAliases": { + "message": "$COUNT$ takma ad silindi", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "toastHistoryCleared": { + "message": "Geçmiş temizlendi" + }, + "toastFavoriteAdded": { + "message": "Favorilere eklendi" + }, + "toastFavoriteRemoved": { + "message": "Favorilerden kaldırıldı" + }, + "toastCopiedEmail": { + "message": "$EMAIL$ kopyalandı", + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "toastCopyFailed": { + "message": "Kopyalanamadı" + }, + "welcomeTitle": { + "message": "Gmail Alias Toolkit’e hoş geldiniz" + }, + "welcomeSubtitle": { + "message": "Gizlilik ve düzen için sınırsız e-posta takma adı oluşturun" + }, + "letsGetStarted": { + "message": "Başlayalım" + }, + "enterGmailAddress": { + "message": "Gmail adresinizi girin" + }, + "pressTabForGmail": { + "message": "@gmail.com için $KEY$ tuşuna basın", + "placeholders": { + "key": { + "content": "$1" + } + } + }, + "settingUp": { + "message": "Ayarlanıyor..." + }, + "getStarted": { + "message": "Başla" + }, + "advancedSetup": { + "message": "Ayarlarda gelişmiş kurulum" + }, + "whatYouCanDo": { + "message": "Yapabilecekleriniz:" + }, + "featurePrivateEmail": { + "message": "Özel e-posta oluşturucu" + }, + "featureCustomTags": { + "message": "Özel etiketler ve hazır ayarlar" + }, + "featureGmailTricks": { + "message": "Gelişmiş Gmail ipuçları" + }, + "welcomeFooter": { + "message": "Tüm veriler yerel olarak saklanır. Takip yok, sunucu yok." + }, + "favoritesSaved": { + "message": "$COUNT$ kaydedildi", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "clickStarHint": { + "message": "Buraya eklemek için geçmişteki herhangi bir takma adın yıldızına tıklayın" + }, + "viewStatistics": { + "message": "İstatistikleri görüntüle" + }, + "statistics": { + "message": "İstatistikler" + }, + "metricsTab": { + "message": "Metrikler" + }, + "timelineTab": { + "message": "Zaman çizelgesi" + }, + "tagsTab": { + "message": "Etiketler" + }, + "totalGenerated": { + "message": "Toplam oluşturulan" + }, + "createdToday": { + "message": "Bugün oluşturuldu" + }, + "thisWeek": { + "message": "Bu hafta" + }, + "thisMonth": { + "message": "Bu ay" + }, + "topTag": { + "message": "En iyi etiket" + }, + "totalTags": { + "message": "Toplam etiket" + }, + "noTagsYet": { + "message": "Henüz etiket yok" + }, + "disableInlineForSite": { "message": "Bu web sitesinde satır içi yardımcıyı kapat" }, + "inlineDisabledSites": { "message": "Satır içi yardımcının kapalı olduğu siteler" }, + "inlineDisabledSitesDescription": { "message": "Bu sitelerde satır içi simge ve hızlı açılır pencere gizlenir." }, + "noDisabledSites": { "message": "Devre dışı bırakılmış site yok" }, + "enableInlineForSite": { "message": "Etkinleştir" }, + "reportReview": { "message": "Bildir / Değerlendir" }, + "percentLabel": { + "message": "$PERCENT$%", + "placeholders": { + "percent": { + "content": "$1" + } + } + } +} diff --git a/public/_locales/vi/messages.json b/public/_locales/vi/messages.json index f4704cc..11e3f4f 100644 --- a/public/_locales/vi/messages.json +++ b/public/_locales/vi/messages.json @@ -1,301 +1,753 @@ { - "extensionName": { "message": "Gmail Alias Toolkit" }, + "extensionName": { + "message": "Gmail Alias Toolkit" + }, "extensionDescription": { - "message": "Tạo và quản lý bí danh Gmail bằng plus addressing và mẫu có sẵn" - }, - "headerSubtitle": { "message": "Tạo bí danh bằng plus addressing" }, - "settings": { "message": "Cài đặt" }, - "back": { "message": "Quay lại" }, - "close": { "message": "Đóng" }, - "cancel": { "message": "Hủy" }, - "copy": { "message": "Sao chép" }, - "copyAll": { "message": "Sao chép tất cả" }, - "copyToClipboard": { "message": "Sao chép vào clipboard" }, - "activeGmailAddress": { "message": "Địa chỉ Gmail đang dùng" }, - "addNewAccount": { "message": "Thêm tài khoản mới" }, - "addAccount": { "message": "Thêm tài khoản" }, - "addNewAccountTitle": { "message": "Thêm tài khoản mới" }, - "emailPlaceholder": { "message": "email.cua.ban" }, + "message": "Tạo và quản lý alias Gmail bằng cú pháp +tag và preset" + }, + "headerSubtitle": { + "message": "Tạo alias bằng cú pháp +tag" + }, + "settings": { + "message": "Cài đặt" + }, + "back": { + "message": "Quay lại" + }, + "close": { + "message": "Đóng" + }, + "cancel": { + "message": "Hủy" + }, + "copy": { + "message": "Sao chép" + }, + "copyAll": { + "message": "Sao chép tất cả" + }, + "copyToClipboard": { + "message": "Sao chép vào clipboard" + }, + "activeGmailAddress": { + "message": "Địa chỉ Gmail đang hoạt động" + }, + "addNewAccount": { + "message": "Thêm tài khoản mới" + }, + "addAccount": { + "message": "Thêm tài khoản" + }, + "addNewAccountTitle": { + "message": "Thêm tài khoản mới" + }, + "emailPlaceholder": { + "message": "email.cua.ban" + }, "accountLabelPlaceholder": { "message": "Nhãn (tùy chọn, ví dụ: Công việc, Cá nhân)" }, "pressTabToAddGmail": { "message": "Nhấn $KEY$ để thêm @gmail.com", - "placeholders": { "key": { "content": "$1" } } + "placeholders": { + "key": { + "content": "$1" + } + } }, "gmailWarning": { - "message": "Địa chỉ này có vẻ không phải Gmail. Plus addressing hoạt động tốt nhất với Gmail." - }, - "random": { "message": "Ngẫu nhiên" }, - "customTags": { "message": "Tag tùy chỉnh" }, - "gmailTricks": { "message": "Mẹo Gmail" }, - "tabTagsShort": { "message": "Tag" }, - "tabTricksShort": { "message": "Mẹo" }, - "generating": { "message": "Đang tạo" }, - "copied": { "message": "Đã sao chép" }, - "switchToLightMode": { "message": "Chuyển sang chế độ sáng" }, - "switchToDarkMode": { "message": "Chuyển sang chế độ tối" }, - "aliasColumn": { "message": "Bí danh" }, + "message": "Địa chỉ này có vẻ không phải Gmail. Địa chỉ cộng hoạt động tốt nhất với Gmail." + }, + "random": { + "message": "Ngẫu nhiên" + }, + "customTags": { + "message": "Thẻ tùy chỉnh" + }, + "gmailTricks": { + "message": "Thủ thuật Gmail" + }, + "tabTagsShort": { + "message": "Thẻ" + }, + "tabTricksShort": { + "message": "Mẹo" + }, + "generating": { + "message": "Đang tạo" + }, + "copied": { + "message": "Đã sao chép" + }, + "switchToLightMode": { + "message": "Chuyển sang chế độ sáng" + }, + "switchToDarkMode": { + "message": "Chuyển sang chế độ tối" + }, + "aliasColumn": { + "message": "Bí danh" + }, "copyEmailTooltip": { - "message": "$EMAIL$ - bấm để sao chép", - "placeholders": { "email": { "content": "$1" } } + "message": "$EMAIL$ - nhấp để sao chép", + "placeholders": { + "email": { + "content": "$1" + } + } }, "pageLabel": { "message": "Trang $PAGE$", - "placeholders": { "page": { "content": "$1" } } - }, - "dotTrick": { "message": "Mẹo dấu chấm" }, - "plusTags": { "message": "Tag dấu +" }, - "googlemail": { "message": "Googlemail" }, - "removeDots": { "message": "Bỏ dấu chấm" }, - "dotPlus": { "message": "Chấm + tag" }, - "allCombos": { "message": "Tất cả kiểu" }, - "numberOfVariations": { "message": "Số biến thể" }, - "randomizeDotPositions": { "message": "Ngẫu nhiên vị trí dấu chấm" }, - "sequential": { "message": "Tuần tự" }, - "generateTricks": { "message": "Tạo mẹo Gmail" }, - "generatedVariations": { "message": "Biến thể đã tạo" }, - "gmailTrickInfoLabel": { "message": "Mẹo Gmail:" }, + "placeholders": { + "page": { + "content": "$1" + } + } + }, + "dotTrick": { + "message": "Thủ thuật dấu chấm" + }, + "plusTags": { + "message": "Thẻ cộng (+)" + }, + "googlemail": { + "message": "Googlemail" + }, + "removeDots": { + "message": "Xóa dấu chấm" + }, + "dotPlus": { + "message": "Dấu chấm + Dấu cộng" + }, + "allCombos": { + "message": "Tất cả kết hợp" + }, + "numberOfVariations": { + "message": "Số biến thể" + }, + "randomizeDotPositions": { + "message": "Ngẫu nhiên vị trí dấu chấm" + }, + "sequential": { + "message": "Tuần tự" + }, + "generateTricks": { + "message": "Tạo thủ thuật" + }, + "generatedVariations": { + "message": "Biến thể đã tạo" + }, + "gmailTrickInfoLabel": { + "message": "Thủ thuật Gmail:" + }, "gmailTrickInfo": { - "message": "Gmail bỏ qua dấu chấm và mọi phần sau dấu + vẫn vào cùng hộp thư" - }, - "format": { "message": "Định dạng" }, - "privateMailFormat": { "message": "Private Mail (private-mail-xxxx)" }, - "randomCharactersFormat": { "message": "Ký tự ngẫu nhiên (abc123xy)" }, - "randomWordsFormat": { "message": "Từ ngẫu nhiên (happy-fox-42)" }, - "timestampFormat": { "message": "Dấu thời gian (1234567890)" }, - "numberOfAliases": { "message": "Số lượng bí danh" }, + "message": "Dấu chấm bị bỏ qua và mọi nội dung sau dấu + sẽ vào cùng một hộp thư" + }, + "format": { + "message": "Định dạng" + }, + "privateMailFormat": { + "message": "Private Mail (private-mail-xxxx)" + }, + "randomCharactersFormat": { + "message": "Ký tự ngẫu nhiên (abc123xy)" + }, + "randomWordsFormat": { + "message": "Từ ngẫu nhiên (happy-fox-42)" + }, + "timestampFormat": { + "message": "Dấu thời gian (1234567890)" + }, + "numberOfAliases": { + "message": "Số bí danh" + }, "generateRandomAliases": { "message": "Tạo $COUNT$ bí danh ngẫu nhiên$PLURAL$", "placeholders": { - "count": { "content": "$1" }, - "plural": { "content": "$2" } + "count": { + "content": "$1" + }, + "plural": { + "content": "$2" + } } }, - "generatedAliases": { "message": "Bí danh đã tạo" }, + "generatedAliases": { + "message": "Bí danh đã tạo" + }, "totalCount": { - "message": "Tổng $COUNT$", - "placeholders": { "count": { "content": "$1" } } + "message": "Tổng cộng $COUNT$", + "placeholders": { + "count": { + "content": "$1" + } + } }, "copiedAliases": { "message": "Đã sao chép $COUNT$ bí danh!", - "placeholders": { "count": { "content": "$1" } } - }, - "failedToCopy": { "message": "Sao chép thất bại" }, - "formatPrivateMail": { "message": "Định dạng: private-mail-xxxx" }, - "formatAlphanumeric": { "message": "8 ký tự ngẫu nhiên" }, - "formatWords": { "message": "2 từ ngẫu nhiên" }, - "formatTimestamp": { "message": "Dấu thời gian Unix" }, - "tagPlaceholder": { "message": "Nhập tag (ví dụ: shopping, work)" }, - "generate": { "message": "Tạo" }, - "yourPresets": { "message": "Mẫu của bạn" }, - "example": { "message": "Ví dụ:" }, - "copyExample": { "message": "Sao chép ví dụ" }, - "recentAliases": { "message": "Bí danh gần đây" }, - "favorites": { "message": "Yêu thích" }, - "exportAsCsv": { "message": "Xuất CSV" }, - "exportAsJson": { "message": "Xuất JSON" }, - "selectAliases": { "message": "Chọn bí danh" }, - "select": { "message": "Chọn" }, + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "failedToCopy": { + "message": "Sao chép thất bại" + }, + "formatPrivateMail": { + "message": "Định dạng: private-mail-xxxx" + }, + "formatAlphanumeric": { + "message": "8 ký tự ngẫu nhiên" + }, + "formatWords": { + "message": "2 từ ngẫu nhiên" + }, + "formatTimestamp": { + "message": "Dấu thời gian Unix" + }, + "tagPlaceholder": { + "message": "Nhập thẻ (ví dụ: shopping, work)" + }, + "generate": { + "message": "Tạo" + }, + "yourPresets": { + "message": "Preset của bạn" + }, + "example": { + "message": "Ví dụ:" + }, + "copyExample": { + "message": "Sao chép ví dụ" + }, + "recentAliases": { + "message": "Bí danh gần đây" + }, + "favorites": { + "message": "Yêu thích" + }, + "exportAsCsv": { + "message": "Xuất CSV" + }, + "exportAsJson": { + "message": "Xuất JSON" + }, + "selectAliases": { + "message": "Chọn bí danh" + }, + "select": { + "message": "Chọn" + }, "starredCount": { - "message": "$COUNT$ đã đánh dấu", - "placeholders": { "count": { "content": "$1" } } + "message": "$COUNT$ đã đánh dấu sao", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "deselectAll": { + "message": "Bỏ chọn tất cả" + }, + "selectAll": { + "message": "Chọn tất cả" }, - "deselectAll": { "message": "Bỏ chọn tất cả" }, - "selectAll": { "message": "Chọn tất cả" }, "selectedCount": { "message": "Đã chọn $COUNT$", - "placeholders": { "count": { "content": "$1" } } + "placeholders": { + "count": { + "content": "$1" + } + } }, "deleteCount": { "message": "Xóa $COUNT$", - "placeholders": { "count": { "content": "$1" } } + "placeholders": { + "count": { + "content": "$1" + } + } }, "allCount": { "message": "Tất cả ($COUNT$)", - "placeholders": { "count": { "content": "$1" } } + "placeholders": { + "count": { + "content": "$1" + } + } }, "favoritesCount": { "message": "Yêu thích ($COUNT$)", - "placeholders": { "count": { "content": "$1" } } + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "searchAliases": { + "message": "Tìm bí danh..." + }, + "allTags": { + "message": "Tất cả thẻ" + }, + "mostRecent": { + "message": "Mới nhất" + }, + "az": { + "message": "A-Z" + }, + "noFavoritesYet": { + "message": "Chưa có mục yêu thích" }, - "searchAliases": { "message": "Tìm bí danh..." }, - "allTags": { "message": "Tất cả tag" }, - "mostRecent": { "message": "Mới nhất" }, - "az": { "message": "A-Z" }, - "noFavoritesYet": { "message": "Chưa có mục yêu thích" }, "starEmailsHint": { - "message": "Đánh dấu sao email trong lịch sử để truy cập nhanh tại đây" + "message": "Đánh dấu sao email từ lịch sử để truy cập nhanh tại đây" + }, + "noResultsFound": { + "message": "Không tìm thấy kết quả" + }, + "differentSearchHint": { + "message": "Thử tìm kiếm hoặc bộ lọc khác" + }, + "showQrCode": { + "message": "Hiển thị mã QR" + }, + "removeFromFavorites": { + "message": "Xóa khỏi yêu thích" + }, + "addToFavorites": { + "message": "Thêm vào yêu thích" }, - "noResultsFound": { "message": "Không tìm thấy kết quả" }, - "differentSearchHint": { "message": "Thử từ khóa tìm kiếm hoặc bộ lọc khác" }, - "showQrCode": { "message": "Hiển thị mã QR" }, - "removeFromFavorites": { "message": "Bỏ khỏi yêu thích" }, - "addToFavorites": { "message": "Thêm vào yêu thích" }, "showingRange": { "message": "Hiển thị $START$-$END$ trên $TOTAL$", "placeholders": { - "start": { "content": "$1" }, - "end": { "content": "$2" }, - "total": { "content": "$3" } + "start": { + "content": "$1" + }, + "end": { + "content": "$2" + }, + "total": { + "content": "$3" + } } }, "perPage": { "message": "$COUNT$ / trang", - "placeholders": { "count": { "content": "$1" } } - }, - "firstPage": { "message": "Trang đầu" }, - "previousPage": { "message": "Trang trước" }, - "nextPage": { "message": "Trang sau" }, - "lastPage": { "message": "Trang cuối" }, - "scanToCopyAlias": { "message": "Quét để sao chép bí danh" }, - "general": { "message": "Chung" }, - "accounts": { "message": "Tài khoản" }, - "changelog": { "message": "Nhật ký thay đổi" }, - "appearanceDisplay": { "message": "Giao diện & hiển thị" }, - "theme": { "message": "Giao diện" }, - "themeLight": { "message": "Sáng" }, - "themeDark": { "message": "Tối" }, - "themeAuto": { "message": "Theo hệ thống" }, - "badgeCounter": { "message": "Bộ đếm huy hiệu" }, - "badgeNone": { "message": "Không hiển thị" }, - "badgeTotal": { "message": "Tổng trong lịch sử" }, - "badgeAllTime": { "message": "Tổng đã tạo" }, - "badgeToday": { "message": "Tạo hôm nay" }, - "badgeWeek": { "message": "Tuần này" }, - "copyNotifications": { "message": "Thông báo khi sao chép" }, - "aliasGeneration": { "message": "Tạo bí danh" }, - "randomAliasFormat": { "message": "Định dạng bí danh ngẫu nhiên" }, - "autoSaveLimit": { "message": "Giới hạn tự lưu" }, + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "firstPage": { + "message": "Trang đầu" + }, + "previousPage": { + "message": "Trang trước" + }, + "nextPage": { + "message": "Trang tiếp" + }, + "lastPage": { + "message": "Trang cuối" + }, + "scanToCopyAlias": { + "message": "Quét để sao chép bí danh" + }, + "general": { + "message": "Chung" + }, + "accounts": { + "message": "Tài khoản" + }, + "changelog": { + "message": "Nhật ký thay đổi" + }, + "appearanceDisplay": { + "message": "Giao diện & hiển thị" + }, + "theme": { + "message": "Chủ đề" + }, + "themeLight": { + "message": "Sáng" + }, + "themeDark": { + "message": "Tối" + }, + "themeAuto": { + "message": "Hệ thống (Tự động)" + }, + "badgeCounter": { + "message": "Bộ đếm huy hiệu" + }, + "badgeNone": { + "message": "Không (Ẩn)" + }, + "badgeTotal": { + "message": "Tổng trong lịch sử" + }, + "badgeAllTime": { + "message": "Tổng đã tạo (Tất cả thời gian)" + }, + "badgeToday": { + "message": "Tạo hôm nay" + }, + "badgeWeek": { + "message": "Tuần này" + }, + "copyNotifications": { + "message": "Thông báo sao chép" + }, + "aliasGeneration": { + "message": "Tạo bí danh" + }, + "randomAliasFormat": { + "message": "Định dạng bí danh ngẫu nhiên" + }, + "autoSaveLimit": { + "message": "Giới hạn tự động lưu" + }, "aliasesLimit": { "message": "$COUNT$ bí danh", - "placeholders": { "count": { "content": "$1" } } - }, - "customPresets": { "message": "Mẫu tùy chỉnh" }, - "label": { "message": "Nhãn" }, - "tag": { "message": "Tag" }, - "addPreset": { "message": "+ Thêm mẫu" }, - "dataManagement": { "message": "Quản lý dữ liệu" }, - "export": { "message": "Xuất" }, - "import": { "message": "Nhập" }, - "clear": { "message": "Xóa" }, - "resetSettings": { "message": "Khôi phục tất cả cài đặt mặc định" }, - "resetSettingsTitle": { "message": "Khôi phục cài đặt?" }, + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "customPresets": { + "message": "Preset tùy chỉnh" + }, + "label": { + "message": "Nhãn" + }, + "tag": { + "message": "Thẻ" + }, + "addPreset": { + "message": "+ Thêm preset" + }, + "dataManagement": { + "message": "Quản lý dữ liệu" + }, + "export": { + "message": "Xuất" + }, + "import": { + "message": "Nhập" + }, + "clear": { + "message": "Xóa" + }, + "resetSettings": { + "message": "Đặt lại tất cả cài đặt về mặc định" + }, + "resetSettingsTitle": { + "message": "Đặt lại cài đặt?" + }, "resetSettingsMessage": { - "message": "Thao tác này sẽ đưa mọi cài đặt về giá trị mặc định." + "message": "Thao tác này sẽ khôi phục mọi cài đặt về giá trị mặc định." + }, + "reset": { + "message": "Đặt lại" + }, + "clearHistoryTitle": { + "message": "Xóa bí danh gần đây?" }, - "reset": { "message": "Khôi phục" }, - "clearHistoryTitle": { "message": "Xóa bí danh gần đây?" }, "clearHistoryMessage": { - "message": "Thao tác này xóa toàn bộ bí danh khỏi lịch sử gần đây." + "message": "Thao tác này xóa tất cả bí danh khỏi danh sách lịch sử gần đây." + }, + "emailAccounts": { + "message": "Tài khoản email" }, - "emailAccounts": { "message": "Tài khoản email" }, "manageAccountsDescription": { - "message": "Quản lý các tài khoản Gmail. Mỗi tài khoản có lịch sử, thống kê và yêu thích riêng." + "message": "Quản lý các tài khoản Gmail của bạn. Mỗi tài khoản có lịch sử, thống kê và mục yêu thích riêng." }, "noAccountsFound": { - "message": "Không tìm thấy tài khoản. Hãy thêm tài khoản từ màn hình chính." + "message": "Không tìm thấy tài khoản. Vui lòng thêm tài khoản từ màn hình chính." + }, + "emailAddress": { + "message": "Địa chỉ email" + }, + "accountLabel": { + "message": "Nhãn tài khoản" + }, + "emailAddressPlaceholder": { + "message": "email.cua.ban@gmail.com" }, - "emailAddress": { "message": "Địa chỉ email" }, - "accountLabel": { "message": "Nhãn tài khoản" }, - "emailAddressPlaceholder": { "message": "email.cua.ban@gmail.com" }, "emailChangeWarning": { - "message": "Đổi email sẽ chuyển toàn bộ dữ liệu sang địa chỉ email mới" + "message": "Thay đổi email sẽ di chuyển toàn bộ dữ liệu sang địa chỉ email mới" + }, + "saveChanges": { + "message": "Lưu thay đổi" + }, + "active": { + "message": "Đang hoạt động" + }, + "editAccount": { + "message": "Sửa tài khoản" }, - "saveChanges": { "message": "Lưu thay đổi" }, - "active": { "message": "Đang dùng" }, - "editAccount": { "message": "Sửa tài khoản" }, "cannotDeleteLastAccountTitle": { "message": "Không thể xóa tài khoản cuối cùng" }, - "deleteThisAccount": { "message": "Xóa tài khoản này" }, - "deleteAccountTitle": { "message": "Xóa tài khoản?" }, + "deleteThisAccount": { + "message": "Xóa tài khoản này" + }, + "deleteAccountTitle": { + "message": "Xóa tài khoản?" + }, "deleteAccountMessage": { - "message": "Xóa \"$LABEL$\" ($EMAIL$)?\n\nThao tác này sẽ xóa vĩnh viễn:\n- Toàn bộ lịch sử của tài khoản này\n- Toàn bộ thống kê\n- Toàn bộ yêu thích\n\nKhông thể hoàn tác.", + "message": "Xóa \"$LABEL$\" ($EMAIL$)?\n\nThao tác này sẽ xóa vĩnh viễn:\n- Toàn bộ lịch sử của tài khoản này\n- Toàn bộ thống kê\n- Toàn bộ mục yêu thích\n\nKhông thể hoàn tác thao tác này.", "placeholders": { - "label": { "content": "$1" }, - "email": { "content": "$2" } + "label": { + "content": "$1" + }, + "email": { + "content": "$2" + } } }, - "delete": { "message": "Xóa" }, - "changeAccountEmailTitle": { "message": "Đổi email tài khoản?" }, + "delete": { + "message": "Xóa" + }, + "changeAccountEmailTitle": { + "message": "Thay đổi email tài khoản?" + }, "changeAccountEmailMessage": { - "message": "Đổi email từ\n$OLD_EMAIL$\nsang\n$NEW_EMAIL$?\n\nThao tác này sẽ:\n- Chuyển toàn bộ lịch sử, thống kê và yêu thích sang email mới\n- Cập nhật email tài khoản\n- Xóa dữ liệu gắn với email cũ\n\nTiếp tục?", + "message": "Thay đổi email từ\n$OLD_EMAIL$\nthành\n$NEW_EMAIL$?\n\nThao tác này sẽ:\n- Di chuyển toàn bộ lịch sử, thống kê và mục yêu thích sang email mới\n- Cập nhật email tài khoản\n- Xóa dữ liệu liên quan đến email cũ\n\nTiếp tục?", "placeholders": { - "old_email": { "content": "$1" }, - "new_email": { "content": "$2" } + "old_email": { + "content": "$1" + }, + "new_email": { + "content": "$2" + } } }, - "changeEmail": { "message": "Đổi email" }, - "menuRandomEmailAlias": { "message": "Bí danh email ngẫu nhiên" }, - "menuCustomTags": { "message": "Tag tùy chỉnh" }, - "menuNoPresets": { "message": "Chưa có mẫu - Thêm trong Cài đặt" }, - "menuGmailTricks": { "message": "Mẹo Gmail" }, - "menuDotVariation": { "message": "Biến thể dấu chấm" }, - "menuGooglemailDomain": { "message": "Tên miền Googlemail" }, - "menuRemoveAllDots": { "message": "Xóa toàn bộ dấu chấm" }, - "toastPresetAdded": { "message": "Đã thêm mẫu" }, - "toastPresetRemoved": { "message": "Đã xóa mẫu" }, - "toastSettingsExported": { "message": "Đã xuất cài đặt" }, - "toastSettingsImported": { "message": "Đã nhập cài đặt" }, - "toastImportFailed": { "message": "Nhập thất bại - tệp không hợp lệ" }, - "toastSettingsReset": { "message": "Đã khôi phục cài đặt mặc định" }, - "toastAccountSwitched": { "message": "Đã chuyển tài khoản" }, - "toastAccountDeleted": { "message": "Đã xóa tài khoản" }, - "toastAccountUpdated": { "message": "Đã cập nhật tài khoản" }, + "changeEmail": { + "message": "Thay đổi email" + }, + "menuForThisWebsite": { + "message": "Cho trang web này" + }, + "menuLoading": { + "message": "Đang tải..." + }, + "menuRandomEmailAlias": { + "message": "Bí danh email ngẫu nhiên" + }, + "menuCustomTags": { + "message": "Thẻ tùy chỉnh" + }, + "menuNoPresets": { + "message": "Chưa có preset - Thêm trong Cài đặt" + }, + "menuGmailTricks": { + "message": "Thủ thuật Gmail" + }, + "menuDotVariation": { + "message": "Biến thể dấu chấm" + }, + "menuGooglemailDomain": { + "message": "Tên miền Googlemail" + }, + "menuRemoveAllDots": { + "message": "Xóa tất cả dấu chấm" + }, + "toastPresetAdded": { + "message": "Đã thêm preset" + }, + "toastPresetRemoved": { + "message": "Đã xóa preset" + }, + "toastSettingsExported": { + "message": "Đã xuất cài đặt" + }, + "toastSettingsImported": { + "message": "Đã nhập cài đặt" + }, + "toastImportFailed": { + "message": "Nhập thất bại - tệp không hợp lệ" + }, + "toastSettingsReset": { + "message": "Cài đặt đã được đặt về mặc định" + }, + "toastAccountSwitched": { + "message": "Đã chuyển tài khoản" + }, + "toastAccountDeleted": { + "message": "Đã xóa tài khoản" + }, + "toastAccountUpdated": { + "message": "Đã cập nhật tài khoản" + }, "toastAccountAdded": { "message": "Đã thêm $LABEL$", - "placeholders": { "label": { "content": "$1" } } + "placeholders": { + "label": { + "content": "$1" + } + } + }, + "errorLabelRequired": { + "message": "Nhãn không được để trống" + }, + "errorInvalidEmail": { + "message": "Vui lòng nhập địa chỉ email hợp lệ" }, - "errorLabelRequired": { "message": "Nhãn không được để trống" }, - "errorInvalidEmail": { "message": "Vui lòng nhập địa chỉ email hợp lệ" }, "errorDuplicateEmail": { - "message": "Địa chỉ email này đã được dùng bởi tài khoản khác" + "message": "Địa chỉ email này đã được tài khoản khác sử dụng" + }, + "errorEnterEmail": { + "message": "Vui lòng nhập địa chỉ email" + }, + "errorAccountExists": { + "message": "Tài khoản này đã tồn tại" + }, + "toastQrFailed": { + "message": "Tạo mã QR thất bại" }, - "errorEnterEmail": { "message": "Vui lòng nhập địa chỉ email" }, - "errorAccountExists": { "message": "Tài khoản này đã tồn tại" }, - "toastQrFailed": { "message": "Tạo mã QR thất bại" }, "toastExportedAliases": { "message": "Đã xuất $COUNT$ bí danh", - "placeholders": { "count": { "content": "$1" } } + "placeholders": { + "count": { + "content": "$1" + } + } }, "toastDeletedAliases": { "message": "Đã xóa $COUNT$ bí danh", - "placeholders": { "count": { "content": "$1" } } + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "toastHistoryCleared": { + "message": "Đã xóa lịch sử" + }, + "toastFavoriteAdded": { + "message": "Đã thêm vào yêu thích" + }, + "toastFavoriteRemoved": { + "message": "Đã xóa khỏi yêu thích" }, - "toastHistoryCleared": { "message": "Đã xóa lịch sử" }, - "toastFavoriteAdded": { "message": "Đã thêm vào yêu thích" }, - "toastFavoriteRemoved": { "message": "Đã bỏ khỏi yêu thích" }, "toastCopiedEmail": { "message": "Đã sao chép $EMAIL$", - "placeholders": { "email": { "content": "$1" } } + "placeholders": { + "email": { + "content": "$1" + } + } + }, + "toastCopyFailed": { + "message": "Sao chép thất bại" + }, + "welcomeTitle": { + "message": "Chào mừng đến với Gmail Alias Toolkit" }, - "toastCopyFailed": { "message": "Sao chép thất bại" }, - "welcomeTitle": { "message": "Chào mừng đến với Bộ công cụ bí danh Gmail" }, "welcomeSubtitle": { - "message": "Tạo bí danh email không giới hạn để bảo vệ riêng tư và sắp xếp hộp thư" + "message": "Tạo bí danh email không giới hạn để bảo vệ riêng tư và sắp xếp tốt hơn" + }, + "letsGetStarted": { + "message": "Bắt đầu thôi" + }, + "enterGmailAddress": { + "message": "Nhập địa chỉ Gmail của bạn" }, - "letsGetStarted": { "message": "Bắt đầu thôi" }, - "enterGmailAddress": { "message": "Nhập địa chỉ Gmail của bạn" }, "pressTabForGmail": { "message": "Nhấn $KEY$ để thêm @gmail.com", - "placeholders": { "key": { "content": "$1" } } - }, - "settingUp": { "message": "Đang thiết lập..." }, - "getStarted": { "message": "Bắt đầu" }, - "advancedSetup": { "message": "Thiết lập nâng cao trong Cài đặt" }, - "whatYouCanDo": { "message": "Bạn có thể:" }, - "featurePrivateEmail": { "message": "Tạo email riêng tư" }, - "featureCustomTags": { "message": "Tag tùy chỉnh & mẫu có sẵn" }, - "featureGmailTricks": { "message": "Mẹo Gmail nâng cao" }, + "placeholders": { + "key": { + "content": "$1" + } + } + }, + "settingUp": { + "message": "Đang thiết lập..." + }, + "getStarted": { + "message": "Bắt đầu" + }, + "advancedSetup": { + "message": "Thiết lập nâng cao trong Cài đặt" + }, + "whatYouCanDo": { + "message": "Bạn có thể làm gì:" + }, + "featurePrivateEmail": { + "message": "Trình tạo email riêng tư" + }, + "featureCustomTags": { + "message": "Thẻ & preset tùy chỉnh" + }, + "featureGmailTricks": { + "message": "Thủ thuật Gmail nâng cao" + }, "welcomeFooter": { - "message": "Mọi dữ liệu được lưu cục bộ. Không theo dõi, không máy chủ." + "message": "Tất cả dữ liệu được lưu cục bộ. Không theo dõi, không máy chủ." }, "favoritesSaved": { "message": "Đã lưu $COUNT$", - "placeholders": { "count": { "content": "$1" } } + "placeholders": { + "count": { + "content": "$1" + } + } }, "clickStarHint": { - "message": "Bấm sao trên bí danh trong lịch sử để thêm vào đây" + "message": "Nhấp vào sao trên bất kỳ bí danh nào trong lịch sử để thêm vào đây" + }, + "viewStatistics": { + "message": "Xem thống kê" + }, + "statistics": { + "message": "Thống kê" + }, + "metricsTab": { + "message": "Chỉ số" + }, + "timelineTab": { + "message": "Dòng thời gian" + }, + "tagsTab": { + "message": "Thẻ" + }, + "totalGenerated": { + "message": "Tổng đã tạo" + }, + "createdToday": { + "message": "Tạo hôm nay" + }, + "thisWeek": { + "message": "Tuần này" + }, + "thisMonth": { + "message": "Tháng này" + }, + "topTag": { + "message": "Thẻ hàng đầu" + }, + "totalTags": { + "message": "Tổng số thẻ" + }, + "noTagsYet": { + "message": "Chưa có thẻ" + }, + "disableInlineForSite": { "message": "Tắt trợ lý nhanh trên website này" }, + "inlineDisabledSites": { "message": "Website đã tắt trợ lý nhanh" }, + "inlineDisabledSitesDescription": { "message": "Biểu tượng và popup thao tác nhanh sẽ bị ẩn trên các website này." }, + "noDisabledSites": { "message": "Chưa tắt trên website nào" }, + "enableInlineForSite": { "message": "Bật lại" }, + "reportReview": { "message": "Báo lỗi / Đánh giá" }, + "percentLabel": { + "message": "$PERCENT$%", + "placeholders": { + "percent": { + "content": "$1" + } + } } } diff --git a/public/_locales/zh_CN/messages.json b/public/_locales/zh_CN/messages.json index ba6bb74..17f184a 100644 --- a/public/_locales/zh_CN/messages.json +++ b/public/_locales/zh_CN/messages.json @@ -3,10 +3,10 @@ "message": "Gmail Alias Toolkit" }, "extensionDescription": { - "message": "使用加号地址和预设生成并管理 Gmail 别名" + "message": "使用 +tag 地址和预设生成并管理 Gmail 别名" }, "headerSubtitle": { - "message": "使用加号地址生成别名" + "message": "使用 +tag 地址生成别名" }, "settings": { "message": "设置" @@ -33,19 +33,19 @@ "message": "当前 Gmail 地址" }, "addNewAccount": { - "message": "添加新账户" + "message": "添加新账号" }, "addAccount": { - "message": "添加账户" + "message": "添加账号" }, "addNewAccountTitle": { - "message": "添加新账户" + "message": "添加新账号" }, "emailPlaceholder": { "message": "your.email" }, "accountLabelPlaceholder": { - "message": "标签(可选,例如工作、个人)" + "message": "标签(可选,例如:工作、个人)" }, "pressTabToAddGmail": { "message": "按 $KEY$ 添加 @gmail.com", @@ -56,7 +56,7 @@ } }, "gmailWarning": { - "message": "这看起来不像 Gmail 地址。加号地址最适合 Gmail。" + "message": "这看起来不像 Gmail 地址。加号地址最适合 Gmail 使用。" }, "random": { "message": "随机" @@ -135,19 +135,19 @@ "message": "生成技巧" }, "generatedVariations": { - "message": "已生成的变体" + "message": "已生成变体" }, "gmailTrickInfoLabel": { "message": "Gmail 技巧:" }, "gmailTrickInfo": { - "message": "点号会被忽略,+ 后面的所有内容都会进入同一个收件箱" + "message": "点号会被忽略,+ 之后的所有内容都会进入同一个收件箱" }, "format": { "message": "格式" }, "privateMailFormat": { - "message": "Private Mail(private-mail-xxxx)" + "message": "私人邮件(private-mail-xxxx)" }, "randomCharactersFormat": { "message": "随机字符(abc123xy)" @@ -162,7 +162,7 @@ "message": "别名数量" }, "generateRandomAliases": { - "message": "生成 $COUNT$ 个随机别名", + "message": "生成 $COUNT$ 个随机别名$PLURAL$", "placeholders": { "count": { "content": "$1" @@ -173,7 +173,7 @@ } }, "generatedAliases": { - "message": "已生成的别名" + "message": "已生成别名" }, "totalCount": { "message": "共 $COUNT$ 个", @@ -207,7 +207,7 @@ "message": "Unix 时间戳" }, "tagPlaceholder": { - "message": "输入标签(例如 shopping、work)" + "message": "输入标签(例如:shopping、work)" }, "generate": { "message": "生成" @@ -222,7 +222,7 @@ "message": "复制示例" }, "recentAliases": { - "message": "最近的别名" + "message": "最近别名" }, "favorites": { "message": "收藏" @@ -240,7 +240,7 @@ "message": "选择" }, "starredCount": { - "message": "$COUNT$ 个已收藏", + "message": "已加星 $COUNT$ 个", "placeholders": { "count": { "content": "$1" @@ -301,7 +301,7 @@ "message": "还没有收藏" }, "starEmailsHint": { - "message": "将历史记录中的邮件加星标,以便在这里快速访问" + "message": "为历史记录中的邮件加星,方便在这里快速访问" }, "noResultsFound": { "message": "未找到结果" @@ -319,7 +319,7 @@ "message": "添加到收藏" }, "showingRange": { - "message": "显示第 $START$-$END$ 项,共 $TOTAL$ 项", + "message": "显示 $START$-$END$,共 $TOTAL$ 个", "placeholders": { "start": { "content": "$1" @@ -353,13 +353,13 @@ "message": "最后一页" }, "scanToCopyAlias": { - "message": "扫码复制别名" + "message": "扫描以复制别名" }, "general": { - "message": "常规" + "message": "通用" }, "accounts": { - "message": "账户" + "message": "账号" }, "changelog": { "message": "更新日志" @@ -386,7 +386,7 @@ "message": "无(隐藏)" }, "badgeTotal": { - "message": "历史记录总数" + "message": "历史总数" }, "badgeAllTime": { "message": "总生成数(全部时间)" @@ -407,7 +407,7 @@ "message": "随机别名格式" }, "autoSaveLimit": { - "message": "自动保存上限" + "message": "自动保存限制" }, "aliasesLimit": { "message": "$COUNT$ 个别名", @@ -448,31 +448,31 @@ "message": "重置设置?" }, "resetSettingsMessage": { - "message": "这会将所有设置恢复为默认值。" + "message": "这会将每项设置恢复为默认值。" }, "reset": { "message": "重置" }, "clearHistoryTitle": { - "message": "清除最近的别名?" + "message": "清除最近别名?" }, "clearHistoryMessage": { "message": "这会从最近历史列表中移除所有别名。" }, "emailAccounts": { - "message": "邮箱账户" + "message": "邮箱账号" }, "manageAccountsDescription": { - "message": "管理你的 Gmail 账户。每个账户都有自己的历史记录、统计数据和收藏。" + "message": "管理你的 Gmail 账号。每个账号都有自己的历史、统计和收藏。" }, "noAccountsFound": { - "message": "未找到账户。请从主屏幕添加账户。" + "message": "未找到账号。请从主屏幕添加账号。" }, "emailAddress": { "message": "邮箱地址" }, "accountLabel": { - "message": "账户标签" + "message": "账号标签" }, "emailAddressPlaceholder": { "message": "your.email@gmail.com" @@ -487,19 +487,19 @@ "message": "当前" }, "editAccount": { - "message": "编辑账户" + "message": "编辑账号" }, "cannotDeleteLastAccountTitle": { - "message": "无法删除最后一个账户" + "message": "无法删除最后一个账号" }, "deleteThisAccount": { - "message": "删除此账户" + "message": "删除此账号" }, "deleteAccountTitle": { - "message": "删除账户?" + "message": "删除账号?" }, "deleteAccountMessage": { - "message": "删除“$LABEL$”($EMAIL$)?\n\n这将永久删除:\n- 此账户的所有历史记录\n- 所有统计数据\n- 所有收藏\n\n此操作无法撤销。", + "message": "删除“$LABEL$”($EMAIL$)?\n\n这将永久删除:\n- 此账号的所有历史记录\n- 所有统计数据\n- 所有收藏\n\n此操作无法撤销。", "placeholders": { "label": { "content": "$1" @@ -513,10 +513,10 @@ "message": "删除" }, "changeAccountEmailTitle": { - "message": "更改账户邮箱?" + "message": "更改账号邮箱?" }, "changeAccountEmailMessage": { - "message": "将邮箱从\n$OLD_EMAIL$\n更改为\n$NEW_EMAIL$?\n\n这将:\n- 将所有历史记录、统计数据和收藏迁移到新邮箱\n- 更新账户邮箱\n- 删除与旧邮箱关联的数据\n\n继续?", + "message": "将邮箱从\n$OLD_EMAIL$\n更改为\n$NEW_EMAIL$?\n\n这将:\n- 将所有历史、统计和收藏迁移到新邮箱\n- 更新账号邮箱\n- 删除与旧邮箱关联的数据\n\n继续?", "placeholders": { "old_email": { "content": "$1" @@ -529,6 +529,12 @@ "changeEmail": { "message": "更改邮箱" }, + "menuForThisWebsite": { + "message": "用于此网站" + }, + "menuLoading": { + "message": "正在加载..." + }, "menuRandomEmailAlias": { "message": "随机邮箱别名" }, @@ -551,10 +557,10 @@ "message": "移除所有点号" }, "toastPresetAdded": { - "message": "已添加预设" + "message": "预设已添加" }, "toastPresetRemoved": { - "message": "已移除预设" + "message": "预设已移除" }, "toastSettingsExported": { "message": "设置已导出" @@ -569,16 +575,16 @@ "message": "设置已重置为默认值" }, "toastAccountSwitched": { - "message": "已切换账户" + "message": "账号已切换" }, "toastAccountDeleted": { - "message": "账户已删除" + "message": "账号已删除" }, "toastAccountUpdated": { - "message": "账户已更新" + "message": "账号已更新" }, "toastAccountAdded": { - "message": "已添加 $LABEL$", + "message": "$LABEL$ 已添加", "placeholders": { "label": { "content": "$1" @@ -592,16 +598,16 @@ "message": "请输入有效的邮箱地址" }, "errorDuplicateEmail": { - "message": "此邮箱地址已被另一个账户使用" + "message": "此邮箱地址已被另一个账号使用" }, "errorEnterEmail": { "message": "请输入邮箱地址" }, "errorAccountExists": { - "message": "此账户已存在" + "message": "此账号已存在" }, "toastQrFailed": { - "message": "生成二维码失败" + "message": "二维码生成失败" }, "toastExportedAliases": { "message": "已导出 $COUNT$ 个别名", @@ -620,13 +626,13 @@ } }, "toastHistoryCleared": { - "message": "历史记录已清除" + "message": "历史已清除" }, "toastFavoriteAdded": { "message": "已添加到收藏" }, "toastFavoriteRemoved": { - "message": "已从收藏中移除" + "message": "已从收藏移除" }, "toastCopiedEmail": { "message": "已复制 $EMAIL$", @@ -643,10 +649,10 @@ "message": "欢迎使用 Gmail Alias Toolkit" }, "welcomeSubtitle": { - "message": "生成无限邮箱别名,用于隐私保护和整理" + "message": "生成无限邮箱别名,保护隐私并便于整理" }, "letsGetStarted": { - "message": "开始使用" + "message": "开始吧" }, "enterGmailAddress": { "message": "输入你的 Gmail 地址" @@ -663,16 +669,16 @@ "message": "正在设置..." }, "getStarted": { - "message": "开始" + "message": "开始使用" }, "advancedSetup": { - "message": "在设置中进行高级设置" + "message": "在设置中进行高级配置" }, "whatYouCanDo": { "message": "你可以做什么:" }, "featurePrivateEmail": { - "message": "私密邮箱生成器" + "message": "私人邮箱生成器" }, "featureCustomTags": { "message": "自定义标签和预设" @@ -692,6 +698,56 @@ } }, "clickStarHint": { - "message": "点击历史记录中任意别名的星标,即可将其添加到这里" + "message": "点击历史中任意别名的星标,即可添加到这里" + }, + "viewStatistics": { + "message": "查看统计" + }, + "statistics": { + "message": "统计" + }, + "metricsTab": { + "message": "指标" + }, + "timelineTab": { + "message": "时间线" + }, + "tagsTab": { + "message": "标签" + }, + "totalGenerated": { + "message": "总生成数" + }, + "createdToday": { + "message": "今天创建" + }, + "thisWeek": { + "message": "本周" + }, + "thisMonth": { + "message": "本月" + }, + "topTag": { + "message": "热门标签" + }, + "totalTags": { + "message": "标签总数" + }, + "noTagsYet": { + "message": "还没有标签" + }, + "disableInlineForSite": { "message": "在此网站上关闭内联助手" }, + "inlineDisabledSites": { "message": "已关闭内联助手的网站" }, + "inlineDisabledSitesDescription": { "message": "在这些网站上将隐藏内联图标和快捷弹窗。" }, + "noDisabledSites": { "message": "没有已关闭的网站" }, + "enableInlineForSite": { "message": "启用" }, + "reportReview": { "message": "报告 / 评价" }, + "percentLabel": { + "message": "$PERCENT$%", + "placeholders": { + "percent": { + "content": "$1" + } + } } } diff --git a/src/components/motion/input.tsx b/src/components/motion/input.tsx index 0c7a3d2..6539f2a 100644 --- a/src/components/motion/input.tsx +++ b/src/components/motion/input.tsx @@ -85,7 +85,13 @@ export const Input = forwardRef(function Input( const fieldRef = useRef(null); const inputRef = useRef(null); const mirrorRef = useRef(null); - useImperativeHandle(ref, () => inputRef.current as HTMLInputElement); + // Keep the forwarded handle stable. Recreating it on every render causes + // callback refs that focus on mount to steal focus from sibling inputs. + useImperativeHandle( + ref, + () => inputRef.current as HTMLInputElement, + [], + ); const hasError = Boolean(error); const errorMessage = typeof error === "string" ? error : null; diff --git a/src/components/motion/select.tsx b/src/components/motion/select.tsx index 0f94d78..727779c 100644 --- a/src/components/motion/select.tsx +++ b/src/components/motion/select.tsx @@ -20,6 +20,7 @@ import { useRef, useState, } from "react"; +import { createPortal } from "react-dom"; import { EASE_OUT } from "src/lib/ease"; import { cn } from "src/lib/utils"; @@ -120,8 +121,14 @@ export function Select({ if (!open) return; const onKey = (e: KeyboardEvent) => e.key === "Escape" && setOpen(false); const onPointer = (e: PointerEvent) => { - if (rootRef.current && !rootRef.current.contains(e.target as Node)) - setOpen(false); + const target = e.target as Node; + const trigger = document.getElementById(`${baseId}-trigger`); + const list = document.getElementById(`${baseId}-list`); + const isClickInside = + (rootRef.current && rootRef.current.contains(target)) || + (trigger && trigger.contains(target)) || + (list && list.contains(target)); + if (!isClickInside) setOpen(false); }; window.addEventListener("keydown", onKey); window.addEventListener("pointerdown", onPointer); @@ -129,7 +136,7 @@ export function Select({ window.removeEventListener("keydown", onKey); window.removeEventListener("pointerdown", onPointer); }; - }, [open]); + }, [open, baseId]); const ctx = useMemo( () => ({ @@ -260,7 +267,9 @@ export interface SelectContentProps { export function SelectContent({ className, children }: SelectContentProps) { const ctx = useSelectContext("SelectContent"); const innerRef = useRef(null); + const contentRef = useRef(null); const [height, setHeight] = useState(0); + const [position, setPosition] = useState({ top: 0, left: 0, width: 0 }); const open = ctx.open; const { setPlacement } = ctx; @@ -274,6 +283,17 @@ export function SelectContent({ className, children }: SelectContentProps) { return () => observer.disconnect(); }); + useLayoutEffect(() => { + const trigger = document.getElementById(ctx.triggerId); + if (!trigger) return; + const rect = trigger.getBoundingClientRect(); + setPosition({ + top: rect.bottom, + left: rect.left, + width: rect.width, + }); + }, [ctx.triggerId, open]); + // On open, flip upward when there isn't room below and there's more above. useLayoutEffect(() => { if (!open) return; @@ -287,10 +307,6 @@ export function SelectContent({ className, children }: SelectContentProps) { setPlacement(below < nodeHeight + 16 && above > below ? "top" : "bottom"); }, [open, ctx.triggerId, setPlacement]); - // Specify EVERY corner + both margins each render. The near edge (facing the - // trigger) animates flat->round and the gap opens on that side; the far edge - // stays rounded and its margin pinned to 0. Setting all of them avoids a - // stranded square corner when the placement flips between opens. const isTop = ctx.placement === "top"; const nearGap = open ? 8 : 0; const nearRadius = open ? 12 : 0; @@ -303,11 +319,9 @@ export function SelectContent({ className, children }: SelectContentProps) { : { duration: 0.16, ease: EASE_OUT }; const instant: Transition = { duration: 0 }; - // Items stay mounted (open just animates the panel) so each item's label - // registration persists — otherwise the trigger would fall back to the - // placeholder the moment the panel closes. - return ( + const content = ( round; far corners stay rounded borderTopLeftRadius: isTop ? 12 : nearRadius, borderTopRightRadius: isTop ? 12 : nearRadius, borderBottomLeftRadius: isTop ? nearRadius : 12, @@ -348,15 +360,17 @@ export function SelectContent({ className, children }: SelectContentProps) { } } style={{ + position: "fixed", + top: isTop ? position.top - height - nearGap : position.top + nearGap, + left: position.left, + width: position.width, transformOrigin: isTop ? "bottom" : "top", overflow: "hidden", pointerEvents: open ? "auto" : "none", + zIndex: 50, }} - // flush against the trigger, then separates into its own rounded pill; - // sits above or below depending on available space className={cn( - "absolute left-0 right-0 z-20 rounded-xl border border-border bg-background shadow-lg", - isTop ? "bottom-full" : "top-full", + "rounded-xl border border-border bg-background shadow-lg", className, )} > @@ -371,6 +385,8 @@ export function SelectContent({ className, children }: SelectContentProps) { ); + + return createPortal(content, document.body); } export interface SelectItemProps { diff --git a/src/services/websiteAliasService.ts b/src/services/websiteAliasService.ts new file mode 100644 index 0000000..cdde8d3 --- /dev/null +++ b/src/services/websiteAliasService.ts @@ -0,0 +1,117 @@ +import { normalizeHostname } from "../utils/hostnameNormalizer"; +import { getAccountStorageKey } from "../../entrypoints/popup/utils"; + +interface WebsiteAliasEntry { + alias: string; + timestamp: number; + generatedCount: number; +} + +type WebsiteAliasMap = Record; + +/** + * Get storage key for website alias map for a given email. + */ +function getWebsiteAliasMapKey(email: string): string { + return getAccountStorageKey(email, "website_alias_map"); +} + +/** + * Retrieve the website alias map for an email account. + */ +export async function getWebsiteAliasMap( + email: string, +): Promise { + const key = getWebsiteAliasMapKey(email); + const result = await browser.storage.local.get([key]); + return (result[key] as WebsiteAliasMap) || {}; +} + +/** + * Save a website-to-alias mapping. + */ +export async function saveWebsiteAlias( + email: string, + normalizedHostname: string, + alias: string, +): Promise { + if (!normalizedHostname || !alias) return; + + const map = await getWebsiteAliasMap(email); + const key = getWebsiteAliasMapKey(email); + + map[normalizedHostname] = { + alias, + timestamp: Date.now(), + generatedCount: (map[normalizedHostname]?.generatedCount || 0) + 1, + }; + + await browser.storage.local.set({ [key]: map }); +} + +/** + * Get the previously used alias for a website (if any). + */ +export async function getPreviousAliasForWebsite( + email: string, + urlOrHostname: string, +): Promise<{ alias: string; timestamp: number } | null> { + const normalized = normalizeHostname(urlOrHostname); + if (!normalized) return null; + + const map = await getWebsiteAliasMap(email); + const entry = map[normalized]; + + return entry ? { alias: entry.alias, timestamp: entry.timestamp } : null; +} + +/** + * Generate alias suggestions for a website. + * Returns 3–5 suggestions based on the normalized hostname and random formats. + */ +export async function generateSuggestionsForWebsite( + baseEmail: string, + urlOrHostname: string, +): Promise { + const normalized = normalizeHostname(urlOrHostname); + if (!normalized) return []; + + // Extract base email part (before @gmail.com) + const emailBase = baseEmail.split("@")[0]; + + const suggestions: string[] = []; + + // 1. Simple: base+normalized + suggestions.push(`${emailBase}+${normalized}@gmail.com`); + + // 2. Counter: base+normalized001 + const map = await getWebsiteAliasMap(baseEmail); + const count = map[normalized]?.generatedCount || 0; + if (count > 0) { + suggestions.push( + `${emailBase}+${normalized}${String(count + 1).padStart(3, "0")}@gmail.com`, + ); + } + + // 3. Short: base+first-3-letters + const shortCode = normalized.slice(0, 3); + suggestions.push(`${emailBase}+${shortCode}@gmail.com`); + + // 4. With date: base+normalized-YYYYMMDD + const today = new Date().toISOString().slice(0, 10).replace(/-/g, ""); + suggestions.push(`${emailBase}+${normalized}-${today}@gmail.com`); + + // 5. Random suffix: base+normalized-XXXX + const randomSuffix = Math.random().toString(36).substring(2, 6); + suggestions.push(`${emailBase}+${normalized}-${randomSuffix}@gmail.com`); + + return suggestions.slice(0, 5); +} + +/** + * Clear all website alias mappings for an email (used for reset/import). + */ +export async function clearWebsiteAliasMap(email: string): Promise { + const key = getWebsiteAliasMapKey(email); + await browser.storage.local.remove([key]); +} diff --git a/src/utils/hostnameNormalizer.ts b/src/utils/hostnameNormalizer.ts new file mode 100644 index 0000000..21f76a1 --- /dev/null +++ b/src/utils/hostnameNormalizer.ts @@ -0,0 +1,72 @@ +/** + * Normalizes a URL/hostname to a clean keyword for alias generation. + * Examples: shopee.com.vn → "shopee", github.com → "github", api.github.com → "github" + */ + +import { parse } from "tldts"; + +/** + * Normalize URL/hostname to a clean keyword. + * Returns null if unable to normalize. + * + * Examples: + * "https://github.com" → "github" + * "https://api.github.com" → "api.github" + * "shopee.com.vn" → "shopee" + * "mail.google.com" → "mail.google" + * "localhost:3000" → "localhost" + */ +export function normalizeHostname(urlOrHostname: string): string | null { + if (!urlOrHostname || typeof urlOrHostname !== "string") { + return null; + } + + const value = urlOrHostname.toLowerCase().trim(); + const result = parse(value, { allowPrivateDomains: true }); + + if (!result.hostname) return null; + if (result.isIp) return "local"; + if (result.hostname === "localhost") return "localhost"; + + // Preserve subdomains by removing only the public suffix + // Examples: api.github.com → api.github, mail.google.com → mail.google + let aliasKey: string | null = null; + + // If we have a domain without suffix (like github when visiting github.com), + // use it directly. Otherwise, extract hostname minus the public suffix. + if (result.domainWithoutSuffix) { + // We have a proper domain (e.g., github.com → github) + aliasKey = result.domainWithoutSuffix.replace(/[^a-z0-9]/g, ""); + // If there's a subdomain, prepend it (e.g., api.github.com → api.github) + // Multiple subdomains are preserved as one unit (e.g., api.v2.example.com → api.v2.example) + if (result.subdomain) { + const cleanSubdomain = result.subdomain.replace(/[^a-z0-9.]/g, ""); + aliasKey = `${cleanSubdomain}.${aliasKey}`; + } + } else if (!result.hostname.includes(".")) { + // Single-label hostname (e.g., "localhost", "github") + aliasKey = result.hostname; + } + + return aliasKey && aliasKey.length > 0 ? aliasKey : null; +} + +/** + * Check if hostname is valid and recognized. + */ +export function isValidHostname(urlOrHostname: string): boolean { + return normalizeHostname(urlOrHostname) !== null; +} + +/** + * Get display name for a normalized hostname. + * Can be used to show user-friendly website names. + */ +export function getDisplayName(normalizedHostname: string): string { + if (!normalizedHostname) return ""; + // Capitalize first letter + add spaces before uppercase (camelCase support) + return normalizedHostname + .replace(/([A-Z])/g, " $1") + .replace(/^./, (str) => str.toUpperCase()) + .trim(); +} diff --git a/src/utils/inlineSiteSettings.ts b/src/utils/inlineSiteSettings.ts new file mode 100644 index 0000000..fd08072 --- /dev/null +++ b/src/utils/inlineSiteSettings.ts @@ -0,0 +1,24 @@ +export const INLINE_DISABLED_SITES_KEY = "inline_disabled_sites"; + +/** Returns a stable, human-readable key for per-site inline-helper settings. */ +export function normalizeSiteHostname(hostname: string): string { + return hostname + .trim() + .toLowerCase() + .replace(/^www\./, "") + .replace(/\.$/, ""); +} + +/** Sanitizes storage values written by current or older extension versions. */ +export function parseDisabledInlineSites(value: unknown): string[] { + if (!Array.isArray(value)) return []; + + return Array.from( + new Set( + value + .filter((site): site is string => typeof site === "string") + .map(normalizeSiteHostname) + .filter(Boolean), + ), + ).sort((a, b) => a.localeCompare(b)); +} diff --git a/tests/content/helpers.test.ts b/tests/content/helpers.test.ts new file mode 100644 index 0000000..aa87507 --- /dev/null +++ b/tests/content/helpers.test.ts @@ -0,0 +1,293 @@ +import { describe, expect, it } from "vitest"; + +/** + * Test utilities for content script helper functions. + * These functions are extracted from entrypoints/content/index.ts for testing. + */ + +// Replicate escapeHtml from content script +function escapeHtml(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +// Replicate normalizeConfiguredEmail +function normalizeConfiguredEmail(email: string): string { + const trimmed = email.trim(); + const gmailMatch = trimmed.match( + /^([^@\s]+)@(gmail\.com|googlemail\.com)(?:[^@\s]+)?$/i, + ); + + return gmailMatch + ? `${gmailMatch[1]}@${gmailMatch[2].toLowerCase()}` + : trimmed; +} + +// Replicate clampQuickCount +function clampQuickCount(value?: string): number { + return Math.min(20, Math.max(1, Number.parseInt(value || "5", 10) || 5)); +} + +// Replicate createCustomAlias +function createCustomAlias(baseEmail: string, value: string): string { + if (value.includes("@")) return value; + + const atIndex = baseEmail.lastIndexOf("@"); + if (atIndex <= 0) return value; + + return `${baseEmail.slice(0, atIndex)}+${value}${baseEmail.slice(atIndex)}`; +} + +// Replicate generateQuickTricks +function generateQuickTricks( + baseEmail: string, + trick: string, + count: number, +): string[] { + const [username, domain] = baseEmail.split("@"); + if (!username || !domain) return []; + + const commonTags = [ + "newsletter", + "shop", + "work", + "personal", + "test", + "promo", + "social", + "finance", + "travel", + "spam", + ]; + + if (trick === "plus") { + return commonTags + .slice(0, count) + .map((tag) => `${username}+${tag}@${domain}`); + } + + if (trick === "nodots") { + const noDots = username.replace(/\./g, ""); + return [ + `${noDots}@${domain}`, + ...commonTags.map((tag) => `${noDots}+${tag}@${domain}`), + ].slice(0, count); + } + + // For dot trick + const outputDomain = + trick === "googlemail" + ? domain.toLowerCase() === "gmail.com" + ? "googlemail.com" + : "gmail.com" + : domain; + + // Simple dot variations + const variations = [ + username, + username.replace(".", ""), + username.split("").join("."), + ]; + + return variations + .filter(Boolean) + .slice(0, count) + .map((v) => `${v}@${outputDomain}`); +} + +describe("Content script helpers", () => { + describe("escapeHtml", () => { + it("escapes HTML special characters", () => { + expect(escapeHtml("")).toBe( + "<script>alert('XSS')</script>", + ); + }); + + it("escapes ampersands", () => { + expect(escapeHtml("A & B")).toBe("A & B"); + }); + + it("escapes double quotes", () => { + expect(escapeHtml('He said "hello"')).toBe("He said "hello""); + }); + + it("escapes single quotes", () => { + expect(escapeHtml("It's a test")).toBe("It's a test"); + }); + + it("handles multiple escapes in one string", () => { + expect(escapeHtml('
A&B\'s "quote"
')).toBe( + "<div class="test">A&B's "quote"</div>", + ); + }); + + it("returns unchanged safe strings", () => { + expect(escapeHtml("Plain text")).toBe("Plain text"); + }); + }); + + describe("normalizeConfiguredEmail", () => { + it("normalizes gmail.com addresses", () => { + expect(normalizeConfiguredEmail("User.Name@GMAIL.COM")).toBe( + "User.Name@gmail.com", + ); + }); + + it("normalizes googlemail.com addresses", () => { + expect(normalizeConfiguredEmail("user@GOOGLEMAIL.COM")).toBe( + "user@googlemail.com", + ); + }); + + it("preserves emails with extra text (only regex match works)", () => { + expect(normalizeConfiguredEmail("user@gmail.com extra")).toBe( + "user@gmail.com extra", + ); + }); + + it("handles whitespace", () => { + expect(normalizeConfiguredEmail(" user@gmail.com ")).toBe( + "user@gmail.com", + ); + }); + + it("returns non-gmail addresses unchanged", () => { + expect(normalizeConfiguredEmail("user@example.com")).toBe( + "user@example.com", + ); + }); + + it("handles invalid email formats", () => { + expect(normalizeConfiguredEmail("not-an-email")).toBe("not-an-email"); + }); + }); + + describe("clampQuickCount", () => { + it("returns default value when undefined", () => { + expect(clampQuickCount()).toBe(5); + }); + + it("clamps minimum to 1 (zero defaults to 5)", () => { + // Note: parseInt("0") returns 0, which is falsy, so defaults to 5 + expect(clampQuickCount("0")).toBe(5); + expect(clampQuickCount("-5")).toBe(1); + expect(clampQuickCount("1")).toBe(1); + }); + + it("clamps maximum to 20", () => { + expect(clampQuickCount("50")).toBe(20); + expect(clampQuickCount("100")).toBe(20); + }); + + it("parses valid numbers", () => { + expect(clampQuickCount("10")).toBe(10); + expect(clampQuickCount("1")).toBe(1); + expect(clampQuickCount("20")).toBe(20); + }); + + it("handles non-numeric input", () => { + expect(clampQuickCount("abc")).toBe(5); + expect(clampQuickCount("")).toBe(5); + }); + }); + + describe("createCustomAlias", () => { + it("creates custom alias with tag", () => { + expect(createCustomAlias("user@gmail.com", "shopping")).toBe( + "user+shopping@gmail.com", + ); + }); + + it("returns full email if value includes @", () => { + expect(createCustomAlias("user@gmail.com", "custom@example.com")).toBe( + "custom@example.com", + ); + }); + + it("handles email without @ in base", () => { + expect(createCustomAlias("invalid", "tag")).toBe("tag"); + }); + + it("preserves domain case", () => { + expect(createCustomAlias("user@GMAIL.COM", "tag")).toBe( + "user+tag@GMAIL.COM", + ); + }); + + it("handles dots in username", () => { + expect(createCustomAlias("first.last@gmail.com", "work")).toBe( + "first.last+work@gmail.com", + ); + }); + + it("handles multiple @ symbols (takes last one)", () => { + expect(createCustomAlias("user+old@domain.com", "new")).toBe( + "user+old+new@domain.com", + ); + }); + }); + + describe("generateQuickTricks", () => { + describe("plus trick", () => { + it("generates plus-tag aliases", () => { + const tricks = generateQuickTricks("user@gmail.com", "plus", 3); + expect(tricks).toEqual([ + "user+newsletter@gmail.com", + "user+shop@gmail.com", + "user+work@gmail.com", + ]); + }); + + it("respects count limit", () => { + const tricks = generateQuickTricks("user@gmail.com", "plus", 1); + expect(tricks).toHaveLength(1); + expect(tricks[0]).toBe("user+newsletter@gmail.com"); + }); + }); + + describe("nodots trick", () => { + it("removes dots from username", () => { + const tricks = generateQuickTricks("first.last@gmail.com", "nodots", 2); + expect(tricks[0]).toBe("firstlast@gmail.com"); + expect(tricks[1]).toMatch(/^firstlast\+\w+@gmail\.com$/); + }); + + it("handles username without dots", () => { + const tricks = generateQuickTricks("user@gmail.com", "nodots", 2); + expect(tricks[0]).toBe("user@gmail.com"); + expect(tricks[1]).toMatch(/^user\+\w+@gmail\.com$/); + }); + }); + + describe("googlemail trick", () => { + it("converts gmail.com to googlemail.com", () => { + const tricks = generateQuickTricks("user@gmail.com", "googlemail", 1); + expect(tricks[0]).toMatch(/@googlemail\.com$/); + }); + + it("converts googlemail.com to gmail.com", () => { + const tricks = generateQuickTricks( + "user@googlemail.com", + "googlemail", + 1, + ); + expect(tricks[0]).toMatch(/@gmail\.com$/); + }); + }); + + describe("invalid input", () => { + it("returns empty array for invalid email", () => { + expect(generateQuickTricks("invalid", "plus", 5)).toEqual([]); + expect(generateQuickTricks("@gmail.com", "plus", 5)).toEqual([]); + }); + + it("returns empty array for zero count", () => { + expect(generateQuickTricks("user@gmail.com", "plus", 0)).toEqual([]); + }); + }); + }); +}); diff --git a/tests/lib/i18n.test.ts b/tests/lib/i18n.test.ts index dcd8b4c..c89327f 100644 --- a/tests/lib/i18n.test.ts +++ b/tests/lib/i18n.test.ts @@ -4,6 +4,15 @@ import { describe, it, expect, vi, afterEach } from "vitest"; import { t } from "../../lib/i18n"; describe("t", () => { + const localesDir = join(process.cwd(), "public", "_locales"); + const readMessages = (locale: string) => + JSON.parse( + readFileSync(join(localesDir, locale, "messages.json"), "utf8"), + ) as Record< + string, + { message: string; placeholders?: Record } + >; + afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); @@ -50,11 +59,6 @@ describe("t", () => { }); it("keeps all locale message key sets aligned with English", () => { - const localesDir = join(process.cwd(), "public", "_locales"); - const readMessages = (locale: string) => - JSON.parse( - readFileSync(join(localesDir, locale, "messages.json"), "utf8"), - ) as Record; const englishKeys = Object.keys(readMessages("en")).sort(); for (const locale of readdirSync(localesDir)) { @@ -64,4 +68,48 @@ describe("t", () => { expect(localeKeys, `${locale} keys should match en`).toEqual(englishKeys); } }); + + it("keeps translations and placeholder definitions complete", () => { + const english = readMessages("en"); + + for (const locale of readdirSync(localesDir)) { + const messages = readMessages(locale); + for (const [key, definition] of Object.entries(messages)) { + expect( + definition.message.trim(), + `${locale}.${key} should not be empty`, + ).not.toBe(""); + expect( + Object.keys(definition.placeholders || {}).sort(), + `${locale}.${key} placeholders should match en`, + ).toEqual(Object.keys(english[key].placeholders || {}).sort()); + } + } + }); + + it("has an English message for every statically referenced translation key", () => { + const english = readMessages("en"); + const sourceRoots = ["entrypoints", "lib", "src"]; + const sourceFiles: string[] = []; + + const collectSources = (directory: string) => { + for (const entry of readdirSync(directory, { withFileTypes: true })) { + const path = join(directory, entry.name); + if (entry.isDirectory()) collectSources(path); + else if (/\.(ts|tsx)$/.test(entry.name)) sourceFiles.push(path); + } + }; + sourceRoots.forEach((root) => collectSources(join(process.cwd(), root))); + + const referencedKeys = new Set(); + for (const file of sourceFiles) { + const source = readFileSync(file, "utf8"); + for (const match of source.matchAll(/\bt\(\s*["']([^"']+)["']/g)) { + referencedKeys.add(match[1]); + } + } + + const missing = [...referencedKeys].filter((key) => !english[key]).sort(); + expect(missing, "static t() keys should exist in en").toEqual([]); + }); }); diff --git a/tests/popup/components/Input.test.tsx b/tests/popup/components/Input.test.tsx index ef243e4..b2957c1 100644 --- a/tests/popup/components/Input.test.tsx +++ b/tests/popup/components/Input.test.tsx @@ -60,4 +60,25 @@ describe("Input", () => { "email", ); }); + + it("does not let a mount-focus ref steal focus after rerender", () => { + const focusOnMount = (element: HTMLInputElement | null) => element?.focus(); + const { rerender } = render( + <> + + + , + ); + const emailInput = screen.getByDisplayValue("user@gmail.com"); + emailInput.focus(); + + rerender( + <> + + + , + ); + + expect(emailInput).toHaveFocus(); + }); }); diff --git a/tests/popup/review-regressions.test.ts b/tests/popup/review-regressions.test.ts new file mode 100644 index 0000000..e35b47a --- /dev/null +++ b/tests/popup/review-regressions.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { filterAliases, validateEmail } from "../../entrypoints/popup/utils"; + +describe("PR review regressions", () => { + it("accepts plus-addressed Gmail usernames", () => { + expect(validateEmail("user+shopping@gmail.com")).toMatchObject({ + isValid: true, + }); + }); + + it("trims and normalizes a search query before filtering", () => { + const aliases = [ + { email: "user+shopping@gmail.com", timestamp: 3000 }, + { email: "user+work@gmail.com", timestamp: 2000 }, + ]; + + const result = filterAliases(aliases, { + viewMode: "all", + favorites: [], + searchQuery: " WORK ", + filterTag: "all", + sortBy: "recent", + }); + + expect(result).toEqual([{ email: "user+work@gmail.com", timestamp: 2000 }]); + }); +}); diff --git a/tests/popup/utils.test.ts b/tests/popup/utils.test.ts index 13972a6..766a041 100644 --- a/tests/popup/utils.test.ts +++ b/tests/popup/utils.test.ts @@ -8,6 +8,8 @@ import { generateDotVariations, getDotVariationCandidates, filterAliases, + getAliasTags, + paginateItems, } from "../../entrypoints/popup/utils"; // ─── getAccountStorageKey ──────────────────────────────────────────────────── @@ -380,3 +382,24 @@ describe("filterAliases", () => { expect(result).toHaveLength(0); }); }); + +describe("shared history helpers", () => { + it("extracts unique plus tags", () => { + expect( + getAliasTags([ + { email: "user+work@gmail.com" }, + { email: "user+shop@gmail.com" }, + { email: "user+work@gmail.com" }, + { email: "user@gmail.com" }, + ]), + ).toEqual(["work", "shop"]); + }); + + it("paginates and clamps an out-of-range page", () => { + const result = paginateItems([1, 2, 3, 4, 5], 9, 2); + expect(result.items).toEqual([5]); + expect(result.currentPage).toBe(3); + expect(result.totalPages).toBe(3); + expect(result.totalItems).toBe(5); + }); +}); diff --git a/tests/services/websiteAliasService.test.ts b/tests/services/websiteAliasService.test.ts new file mode 100644 index 0000000..6a7af22 --- /dev/null +++ b/tests/services/websiteAliasService.test.ts @@ -0,0 +1,299 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + getWebsiteAliasMap, + saveWebsiteAlias, + getPreviousAliasForWebsite, + generateSuggestionsForWebsite, + clearWebsiteAliasMap, +} from "../../src/services/websiteAliasService"; + +// Mock the browser storage API +const mockStorageData: Record = {}; + +type MockBrowser = { + storage: { + local: { + get: ReturnType; + set: ReturnType; + remove: ReturnType; + }; + }; +}; + +vi.stubGlobal("browser", { + storage: { + local: { + get: vi.fn((keys: string[]) => { + const result: Record = {}; + keys.forEach((key) => { + if (key in mockStorageData) { + result[key] = mockStorageData[key]; + } + }); + return Promise.resolve(result); + }), + set: vi.fn((data: Record) => { + Object.assign(mockStorageData, data); + return Promise.resolve(); + }), + remove: vi.fn((keys: string[]) => { + keys.forEach((key) => { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete mockStorageData[key]; + }); + return Promise.resolve(); + }), + }, + }, +} as unknown as MockBrowser); + +// Mock the getAccountStorageKey utility +vi.mock("../../entrypoints/popup/utils", () => ({ + getAccountStorageKey: (email: string, suffix: string) => `${email}:${suffix}`, +})); + +describe("websiteAliasService", () => { + const testEmail = "user@gmail.com"; + const testUrl = "https://github.com/user/repo"; + const testAlias = "user+github@gmail.com"; + + beforeEach(() => { + // Clear mock storage before each test + Object.keys(mockStorageData).forEach((key) => { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete mockStorageData[key]; + }); + vi.clearAllMocks(); + }); + + describe("getWebsiteAliasMap", () => { + it("returns empty object for new email", async () => { + const map = await getWebsiteAliasMap(testEmail); + expect(map).toEqual({}); + }); + + it("retrieves stored website alias map", async () => { + const storedMap = { + github: { alias: testAlias, timestamp: 12345, generatedCount: 1 }, + }; + mockStorageData[`${testEmail}:website_alias_map`] = storedMap; + + const map = await getWebsiteAliasMap(testEmail); + expect(map).toEqual(storedMap); + }); + }); + + describe("saveWebsiteAlias", () => { + it("saves a website alias with metadata", async () => { + const beforeTime = Date.now(); + await saveWebsiteAlias(testEmail, "github", testAlias); + const afterTime = Date.now(); + + const map = await getWebsiteAliasMap(testEmail); + const entry = map.github; + + expect(entry).toBeDefined(); + expect(entry.alias).toBe(testAlias); + expect(entry.generatedCount).toBe(1); + expect(entry.timestamp).toBeGreaterThanOrEqual(beforeTime); + expect(entry.timestamp).toBeLessThanOrEqual(afterTime); + }); + + it("increments generatedCount on repeated saves", async () => { + await saveWebsiteAlias(testEmail, "github", "alias1@gmail.com"); + await saveWebsiteAlias(testEmail, "github", "alias2@gmail.com"); + await saveWebsiteAlias(testEmail, "github", "alias3@gmail.com"); + + const map = await getWebsiteAliasMap(testEmail); + expect(map.github.generatedCount).toBe(3); + expect(map.github.alias).toBe("alias3@gmail.com"); // Latest alias + }); + + it("ignores empty hostname or alias", async () => { + await saveWebsiteAlias(testEmail, "", testAlias); + await saveWebsiteAlias(testEmail, "github", ""); + + const map = await getWebsiteAliasMap(testEmail); + expect(Object.keys(map)).toHaveLength(0); + }); + + it("handles multiple websites independently", async () => { + await saveWebsiteAlias(testEmail, "github", "user+github@gmail.com"); + await saveWebsiteAlias(testEmail, "twitter", "user+twitter@gmail.com"); + await saveWebsiteAlias(testEmail, "linkedin", "user+linkedin@gmail.com"); + + const map = await getWebsiteAliasMap(testEmail); + expect(Object.keys(map)).toHaveLength(3); + expect(map.github.alias).toBe("user+github@gmail.com"); + expect(map.twitter.alias).toBe("user+twitter@gmail.com"); + expect(map.linkedin.alias).toBe("user+linkedin@gmail.com"); + }); + }); + + describe("getPreviousAliasForWebsite", () => { + it("returns null for unmapped website", async () => { + const result = await getPreviousAliasForWebsite(testEmail, "unknown.com"); + expect(result).toBeNull(); + }); + + it("returns previous alias and timestamp", async () => { + const timestamp = 12345; + mockStorageData[`${testEmail}:website_alias_map`] = { + github: { alias: testAlias, timestamp, generatedCount: 1 }, + }; + + const result = await getPreviousAliasForWebsite(testEmail, testUrl); + expect(result).toEqual({ alias: testAlias, timestamp }); + }); + + it("accepts both URL and hostname", async () => { + // Save alias with normalized key "github" + await saveWebsiteAlias(testEmail, "github", testAlias); + + // Test with full URL that normalizes to "github" + const resultUrl = await getPreviousAliasForWebsite( + testEmail, + "https://github.com/user", + ); + expect(resultUrl?.alias).toBe(testAlias); + + // Test with raw hostname + const resultHostname = await getPreviousAliasForWebsite( + testEmail, + "github", + ); + expect(resultHostname?.alias).toBe(testAlias); + }); + + it("returns null for invalid hostname", async () => { + const result = await getPreviousAliasForWebsite( + testEmail, + "not a valid host", + ); + expect(result).toBeNull(); + }); + }); + + describe("generateSuggestionsForWebsite", () => { + it("returns empty array for invalid hostname", async () => { + const suggestions = await generateSuggestionsForWebsite( + testEmail, + "not a valid host", + ); + expect(suggestions).toEqual([]); + }); + + it("generates up to 5 suggestions for new website", async () => { + const suggestions = await generateSuggestionsForWebsite( + testEmail, + "https://github.com", + ); + + expect(suggestions.length).toBeGreaterThan(0); + expect(suggestions.length).toBeLessThanOrEqual(5); + expect(suggestions[0]).toContain("user+github@gmail.com"); + suggestions.forEach((s) => expect(s).toMatch(/@gmail\.com$/)); + }); + + it("includes counter-based suggestion only for repeat visits", async () => { + // First visit: no counter suggestion + let suggestions = await generateSuggestionsForWebsite( + testEmail, + "https://github.com", + ); + expect(suggestions[1]).not.toMatch(/github001/); + + // Save an alias to mark it as visited + await saveWebsiteAlias(testEmail, "github", "user+github@gmail.com"); + + // Second visit: includes counter suggestion + suggestions = await generateSuggestionsForWebsite( + testEmail, + "https://github.com", + ); + expect(suggestions).toContainEqual(expect.stringMatching(/github002/)); + }); + + it("includes date-based suggestion with YYYYMMDD format", async () => { + const suggestions = await generateSuggestionsForWebsite( + testEmail, + "https://github.com", + ); + + const dateRegex = /github-\d{8}@gmail\.com/; + expect(suggestions.some((s) => dateRegex.test(s))).toBe(true); + }); + + it("includes short code based on hostname", async () => { + const suggestions = await generateSuggestionsForWebsite( + testEmail, + "https://github.com", + ); + + // Short code uses first 3 letters of normalized hostname (github -> git, or ghi) + expect( + suggestions.some((s) => s.includes("+git@") || s.includes("+ghi@")), + ).toBe(true); + }); + + it("generates random suffix suggestions", async () => { + const suggestions = await generateSuggestionsForWebsite( + testEmail, + "https://github.com", + ); + + const randomSuffixRegex = /github-[a-z0-9]{4}@gmail\.com/; + const randomSuggestions = suggestions.filter((s) => + randomSuffixRegex.test(s), + ); + expect(randomSuggestions.length).toBeGreaterThan(0); + }); + + it("limits suggestions to 5", async () => { + const suggestions = await generateSuggestionsForWebsite( + testEmail, + "https://github.com", + ); + + expect(suggestions.length).toBeLessThanOrEqual(5); + }); + + it("always uses @gmail.com for suggestions regardless of email domain", async () => { + const suggestions = await generateSuggestionsForWebsite( + "user@example.com", + "https://github.com", + ); + + // Suggestions always use @gmail.com, not the original email domain + suggestions.forEach((s) => expect(s).toMatch(/@gmail\.com$/)); + }); + }); + + describe("clearWebsiteAliasMap", () => { + it("removes all website alias mappings for email", async () => { + await saveWebsiteAlias(testEmail, "github", testAlias); + await saveWebsiteAlias(testEmail, "twitter", "user+twitter@gmail.com"); + + await clearWebsiteAliasMap(testEmail); + + const map = await getWebsiteAliasMap(testEmail); + expect(map).toEqual({}); + }); + + it("does not affect other emails", async () => { + const email1 = "user1@gmail.com"; + const email2 = "user2@gmail.com"; + + await saveWebsiteAlias(email1, "github", "alias1@gmail.com"); + await saveWebsiteAlias(email2, "github", "alias2@gmail.com"); + + await clearWebsiteAliasMap(email1); + + const map1 = await getWebsiteAliasMap(email1); + const map2 = await getWebsiteAliasMap(email2); + + expect(map1).toEqual({}); + expect(map2.github.alias).toBe("alias2@gmail.com"); + }); + }); +}); diff --git a/tests/utils/hostnameNormalizer.test.ts b/tests/utils/hostnameNormalizer.test.ts new file mode 100644 index 0000000..6534e00 --- /dev/null +++ b/tests/utils/hostnameNormalizer.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; +import { normalizeHostname } from "../../src/utils/hostnameNormalizer"; + +describe("normalizeHostname", () => { + it("normalizes a regular domain", () => { + expect(normalizeHostname("https://github.com/path")).toBe("github"); + }); + + it("preserves subdomains in hostname", () => { + expect(normalizeHostname("https://www.github.com/path")).toBe("www.github"); + expect(normalizeHostname("https://api.github.com")).toBe("api.github"); + expect(normalizeHostname("https://mail.google.com")).toBe("mail.google"); + expect(normalizeHostname("https://api.v2.example.com")).toBe( + "api.v2.example", + ); + }); + + it("supports modern TLDs that are not in the legacy allowlist", () => { + expect( + normalizeHostname( + "https://voidzero.dev/posts/whats-new-jun-2026?ref=dailydev", + ), + ).toBe("voidzero"); + expect(normalizeHostname("https://example.app/signup")).toBe("example"); + expect(normalizeHostname("example.anyfuturetld")).toBe("example"); + }); + + it("handles compound and private public suffixes", () => { + expect(normalizeHostname("https://shop.example.co.uk/account")).toBe( + "shop.example", + ); + expect(normalizeHostname("https://shop.example.com.au/account")).toBe( + "shop.example", + ); + expect(normalizeHostname("https://alice.github.io/project")).toBe("alice"); + }); + + it("accepts raw hostnames, localhost, IPs, and IDN punycode", () => { + expect(normalizeHostname("example.dev")).toBe("example"); + expect(normalizeHostname("localhost:3000")).toBe("localhost"); + expect(normalizeHostname("https://127.0.0.1:3000")).toBe("local"); + expect(normalizeHostname("https://xn--mnchen-3ya.de")).toBe("xnmnchen3ya"); + }); + + it("rejects invalid input", () => { + expect(normalizeHostname("not a valid.host")).toBeNull(); + }); +}); diff --git a/tests/utils/inlineSiteSettings.test.ts b/tests/utils/inlineSiteSettings.test.ts new file mode 100644 index 0000000..1325312 --- /dev/null +++ b/tests/utils/inlineSiteSettings.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { + normalizeSiteHostname, + parseDisabledInlineSites, +} from "../../src/utils/inlineSiteSettings"; + +describe("inline site settings", () => { + it("normalizes hostnames used as per-site keys", () => { + expect(normalizeSiteHostname(" WWW.VoidZero.DEV. ")).toBe("voidzero.dev"); + }); + + it("sanitizes, deduplicates, and sorts stored sites", () => { + expect( + parseDisabledInlineSites([ + "www.typeform.com", + null, + "VOIDZERO.DEV", + "typeform.com", + "", + ]), + ).toEqual(["typeform.com", "voidzero.dev"]); + }); + + it("handles invalid legacy storage values", () => { + expect(parseDisabledInlineSites({ site: "voidzero.dev" })).toEqual([]); + }); +}); diff --git a/web/index.html b/web/index.html index 4989eb0..0b78130 100644 --- a/web/index.html +++ b/web/index.html @@ -5,9 +5,54 @@ + + + + + + + + + + + Gmail Alias Toolkit +
diff --git a/web/package.json b/web/package.json index 38edbd4..d44e1ab 100644 --- a/web/package.json +++ b/web/package.json @@ -12,13 +12,13 @@ "clsx": "^2.1.1", "framer-motion": "^12.23.12", "lucide-react": "^0.468.0", - "react": "^18.3.1", - "react-dom": "^18.3.1", + "react": "^19.2.3", + "react-dom": "^19.2.3", "tailwind-merge": "^2.5.5" }, "devDependencies": { - "@types/react": "^18.3.18", - "@types/react-dom": "^18.3.5", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", "postcss": "^8.4.49", diff --git a/web/src/App.tsx b/web/src/App.tsx index f0ff3c9..d28db3d 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,22 +1,26 @@ import type React from "react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { motion } from "framer-motion"; +import extensionIconUrl from "../../assets/icon.png?url"; +import { ThemeToggle } from "src/components/motion/theme-toggle"; import { ArrowRight, AtSign, BadgeCheck, BarChart3, Clipboard, + ChevronDown, Copy, Database, Download, EyeOff, - FileJson, Github, History, + Home, Languages, Mail, Moon, + Plus, QrCode, Search, Settings, @@ -25,11 +29,14 @@ import { Sparkles, Star, Tags, + X, Zap, } from "lucide-react"; const chromeUrl = "https://chromewebstore.google.com/detail/gmail-alias-toolkit/cbapjlppdfbnfbopdegobofmfijnlibl"; +const firefoxUrl = + "https://addons.mozilla.org/en-US/firefox/addon/gmail-alias-toolkit/"; const githubUrl = "https://github.com/ePlus-DEV/gmail-alias-toolkit"; type Locale = "vi" | "en"; @@ -43,16 +50,19 @@ const translations = { vi: { nav: { mock: "Bản mô phỏng", + inline: "Inline Popup", features: "Tính năng", privacy: "Quyền riêng tư", install: "Thêm vào Chrome", + installFirefox: "Thêm vào Firefox", }, hero: { badge: "Tiện ích Chrome cho Gmail plus addressing", titlePrefix: "Quản lý Gmail alias bằng một", titleHighlight: "popup nhỏ gọn.", - desc: "Tạo địa chỉ dạng david+tag@gmail.com, sao chép nhanh, tìm lại trong lịch sử, ghim mục yêu thích và xuất dữ liệu mà không rời khỏi trình duyệt.", + desc: "Tạo địa chỉ dạng david+tag@gmail.com trong popup chính hoặc thao tác ngay cạnh ô email bằng Inline Popup, với gợi ý theo website, lịch sử, yêu thích và xuất dữ liệu.", install: "Cài tiện ích", + installFirefox: "Cài cho Firefox", source: "Xem mã nguồn", stats: [ ["4", "định dạng random"], @@ -118,6 +128,8 @@ const translations = { }, ], mock: { + subtitle: "Tạo & quản lý Gmail alias", + activeEmail: "Địa chỉ Gmail đang dùng", format: "Định dạng", count: "Số lượng", generate: "Tạo và sao chép", @@ -125,11 +137,34 @@ const translations = { recent: "Alias gần đây", search: "tìm kiếm", settings: "Cài đặt", + viewStatistics: "Xem thống kê", + }, + inlineHelper: { + eyebrow: "Inline Popup", + title: "Thao tác ngay tại ô email, không cần mở popup chính.", + desc: "Extension phát hiện trường email trên mọi domain, đặt icon ngoài input và mở một popup thu gọn có đầy đủ gợi ý, generator và lịch sử.", + steps: [ + "Phát hiện input email và tránh các icon của website hoặc password manager.", + "Hover icon để mở popup; vị trí trên, dưới, trái hoặc phải được giữ ổn định.", + "Hover alias để xem trước trong input, bấm để điền và ghi đúng một mục lịch sử.", + "Tắt Inline Helper theo từng website và bật lại bất kỳ lúc nào trong Settings.", + ], + demoTitle: "Tạo tài khoản", + email: "Địa chỉ email", + suggestions: "Alias gợi ý", + generate: "Tạo alias", + history: "Alias gần đây", + generatedLabel: "Alias đã tạo:", + generateAction: "Tạo 5 alias", + search: "Tìm lịch sử...", + allAliases: "Tất cả alias", + mostRecent: "Mới nhất", + previewHint: "Hover một alias để xem trước trực tiếp trong input", }, features: { eyebrow: "Tính năng tiện ích", - title: "Mỗi tính năng đều được mô phỏng như popup thật", - desc: "Trang giới thiệu không chỉ kể tính năng, mà mô phỏng đúng cách extension tạo, sao chép, lưu và xuất alias.", + title: "Đầy đủ từ popup chính đến trợ lý ngay trong form", + desc: "Extension tạo, xem trước, điền và quản lý alias ngay tại ô email; đồng thời giữ đầy đủ generator, history, favorites và công cụ dữ liệu trong popup chính.", items: [ { title: "Tạo alias ngẫu nhiên", @@ -179,6 +214,30 @@ const translations = { icon: ShieldCheck, sample: "local-first", }, + { + title: "Inline Popup thao tác nhanh", + desc: "Tạo alias, dùng preset, Gmail tricks và tìm lịch sử ngay cạnh ô email mà không cần mở popup chính.", + icon: Zap, + sample: "Generate + History", + }, + { + title: "Gợi ý theo website", + desc: "Tự nhận diện domain, đề xuất alias phù hợp và xem trước trực tiếp trong input khi hover.", + icon: Sparkles, + sample: "hover để xem trước", + }, + { + title: "Điều khiển theo từng site", + desc: "Ẩn Inline Helper trên website bất kỳ và bật lại từ danh sách quản lý trong Settings.", + icon: EyeOff, + sample: "tắt / bật theo domain", + }, + { + title: "14 ngôn ngữ", + desc: "Popup chính, Inline Popup và cài đặt được đồng bộ nội dung trên toàn bộ locale hỗ trợ.", + icon: Languages, + sample: "14 locales", + }, ], }, tricks: { @@ -211,21 +270,25 @@ const translations = { title: "Sẵn sàng biến Gmail alias thành workflow gọn gàng?", desc: "Cài Gmail Alias Toolkit để tạo alias traceable cho đăng ký tài khoản, newsletter, testing và bảo vệ inbox mỗi ngày.", install: "Thêm vào Chrome", + installFirefox: "Thêm vào Firefox", }, }, en: { nav: { mock: "Mock", + inline: "Inline Popup", features: "Features", privacy: "Privacy", install: "Add to Chrome", + installFirefox: "Add to Firefox", }, hero: { badge: "Chrome extension for Gmail plus addressing", titlePrefix: "Manage Gmail aliases from one", titleHighlight: "compact popup.", - desc: "Create david+tag@gmail.com aliases, copy them quickly, search history, pin favorites and export data without leaving the browser.", + desc: "Create david+tag@gmail.com aliases in the main popup or work beside any email field with website-aware inline suggestions, history, favorites and export tools.", install: "Install extension", + installFirefox: "Install for Firefox", source: "View source", stats: [ ["4", "random formats"], @@ -291,6 +354,8 @@ const translations = { }, ], mock: { + subtitle: "Generate & manage Gmail aliases", + activeEmail: "Active Gmail Address", format: "Format", count: "Count", generate: "Generate and copy", @@ -298,11 +363,34 @@ const translations = { recent: "Recent aliases", search: "search", settings: "Settings", + viewStatistics: "View Statistics", + }, + inlineHelper: { + eyebrow: "Inline Popup", + title: "Work beside an email field without opening the main popup.", + desc: "The extension detects email fields on every domain, keeps its icon outside the input and opens a compact helper with suggestions, generation and history.", + steps: [ + "Detect email inputs while avoiding site controls and password-manager icons.", + "Hover the icon to open a popup whose top, bottom, left or right anchor stays stable.", + "Hover an alias for a live input preview, then click to fill it and record one history item.", + "Disable the inline helper per website and re-enable it later from Settings.", + ], + demoTitle: "Create your account", + email: "Email address", + suggestions: "Suggested aliases", + generate: "Generate", + history: "Recent aliases", + generatedLabel: "Generated aliases:", + generateAction: "Generate 5 aliases", + search: "Search history...", + allAliases: "All aliases", + mostRecent: "Most recent", + previewHint: "Hover an alias to preview it directly in the input", }, features: { eyebrow: "Extension features", - title: "Every feature is mocked like the real popup", - desc: "The page does more than describe features: it simulates how the extension generates, copies, stores and exports aliases.", + title: "From the main popup to an assistant inside every form", + desc: "Generate, preview, fill and manage aliases beside an email field while keeping the complete generator, history, favorites and data tools in the main popup.", items: [ { title: "Random generator", @@ -352,6 +440,30 @@ const translations = { icon: ShieldCheck, sample: "local-first", }, + { + title: "Inline quick popup", + desc: "Generate aliases, use presets and Gmail tricks, or search history beside an email field without opening the main popup.", + icon: Zap, + sample: "Generate + History", + }, + { + title: "Website-aware suggestions", + desc: "Detect the current domain, suggest relevant aliases and preview a selection directly in the input on hover.", + icon: Sparkles, + sample: "hover to preview", + }, + { + title: "Per-site controls", + desc: "Hide the inline helper on any website and re-enable it later from the managed list in Settings.", + icon: EyeOff, + sample: "disable by domain", + }, + { + title: "14 languages", + desc: "The main popup, inline helper and settings stay localized across every supported locale.", + icon: Languages, + sample: "14 locales", + }, ], }, tricks: { @@ -384,6 +496,7 @@ const translations = { title: "Ready to turn Gmail aliases into a tidy workflow?", desc: "Install Gmail Alias Toolkit to create traceable aliases for signups, newsletters, testing and everyday inbox protection.", install: "Add to Chrome", + installFirefox: "Add to Firefox", }, }, } as const; @@ -531,8 +644,12 @@ function Header({ function HeaderBrand() { return ( - ); } @@ -669,6 +794,9 @@ function HeroActions({ t }: { t: (typeof translations)[Locale] }) { {t.hero.install} + + {t.hero.installFirefox} + {t.hero.source} @@ -1007,6 +1135,10 @@ function InboxNode({ label, email }: { label: string; email: string }) { function ExtensionMockup({ t }: { t: (typeof translations)[Locale] }) { const [activeTab, setActiveTab] = useState<(typeof t.tabs)[number]["id"]>("random"); + const [isDark, setIsDark] = useState(false); + const [settingsOpen, setSettingsOpen] = useState(false); + const [accountOpen, setAccountOpen] = useState(false); + const [activeEmail, setActiveEmail] = useState("david@gmail.com"); const currentTab = t.tabs.find((tab) => tab.id === activeTab) ?? t.tabs[0]; return ( @@ -1016,44 +1148,560 @@ function ExtensionMockup({ t }: { t: (typeof translations)[Locale] }) { animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.65, delay: 0.08 }} whileHover={{ y: -3 }} - className="mx-auto w-full max-w-[390px] rounded-[1.75rem] border border-slate-200 bg-white p-3 shadow-2xl shadow-blue-950/10" + className={`relative mx-auto w-full max-w-[390px] rounded-[1.75rem] border p-3 shadow-2xl shadow-blue-950/10 transition-colors ${ + isDark + ? "border-slate-700 bg-slate-950 text-white" + : "border-slate-200 bg-slate-50" + }`} > -
- - setIsDark((value) => !value)} + onOpenSettings={() => setSettingsOpen(true)} + /> +
+
+ setAccountOpen((value) => !value)} + onSelect={(email) => { + setActiveEmail(email); + setAccountOpen(false); + }} + /> +
+ + +
+ + +
+
+ {settingsOpen ? ( + setSettingsOpen(false)} + /> + ) : null} + + ); +} + +/** Interactive landing-page version of the extension's Settings screen. */ +/** Settings panel header with back button and version. */ +function MockSettingsPanelHeader({ + isDark, + muted, + t, + onClose, +}: { + isDark: boolean; + muted: string; + t: (typeof translations)[Locale]; + onClose: () => void; +}) { + return ( +
+
+ +

{t.mock.settings}

+
+ + v1.3.0 + +
+ ); +} + +/** Settings panel tab navigation. */ +function MockSettingsPanelTabs({ + isDark, + labels, + tab, + setTab, +}: { + isDark: boolean; + labels: Record; + tab: "general" | "accounts" | "changelog"; + setTab: (tab: "general" | "accounts" | "changelog") => void; +}) { + return ( +
+
+ {(["general", "accounts", "changelog"] as const).map((id) => ( + + ))} +
+
+ ); +} + +/** Settings panel with tabs for general, accounts, and changelog. */ +function MockSettingsPanel({ + t, + isDark, + activeEmail, + onClose, +}: { + t: (typeof translations)[Locale]; + isDark: boolean; + activeEmail: string; + onClose: () => void; +}) { + const english = t.mock.settings === "Settings"; + const labels = english + ? { + general: "General", + accounts: "Accounts", + changelog: "Changelog", + appearance: "Appearance & display", + inline: "Inline helper disabled sites", + generation: "Alias generation", + presets: "Custom presets", + data: "Data management", + badge: "Badge counter", + notifications: "Copy notifications", + format: "Random alias format", + limit: "Auto-save limit", + enable: "Enable", + export: "Export", + import: "Import", + clear: "Clear", + add: "Add account", + current: "Active", + } + : { + general: "Chung", + accounts: "Tài khoản", + changelog: "Thay đổi", + appearance: "Giao diện & hiển thị", + inline: "Website đã tắt Inline Helper", + generation: "Tạo alias", + presets: "Preset tùy chỉnh", + data: "Quản lý dữ liệu", + badge: "Bộ đếm trên badge", + notifications: "Thông báo khi sao chép", + format: "Định dạng alias ngẫu nhiên", + limit: "Giới hạn tự lưu", + enable: "Bật lại", + export: "Xuất", + import: "Nhập", + clear: "Xóa", + add: "Thêm tài khoản", + current: "Đang dùng", + }; + const [tab, setTab] = useState<"general" | "accounts" | "changelog">( + "general", + ); + const [section, setSection] = useState("appearance"); + const [notifications, setNotifications] = useState(true); + const [disabledSites, setDisabledSites] = useState(["voidzero.dev"]); + const panel = isDark + ? "border-slate-700 bg-slate-900" + : "border-slate-200 bg-white"; + const muted = isDark ? "bg-slate-950/70" : "bg-slate-50"; + const sections = [ + ["appearance", labels.appearance], + ["inline", labels.inline], + ["generation", labels.generation], + ["presets", labels.presets], + ["data", labels.data], + ]; + + return ( + +
+ -
- - - + +
+ {tab === "general" ? ( +
+ {sections.map(([id, label]) => ( +
+ + {section === id ? ( +
+ {id === "appearance" ? ( +
+ +
+ + {labels.notifications} + + +
+
+ ) : null} + {id === "inline" ? ( + disabledSites.length ? ( + disabledSites.map((site) => ( +
+ {site} + +
+ )) + ) : ( +

+ No disabled sites +

+ ) + ) : null} + {id === "generation" ? ( +
+ + +
+ ) : null} + {id === "presets" ? ( +
+ {["Shopping +shopping", "Development +dev"].map( + (item) => ( +
+ {item} +
+ ), + )} +
+ ) : null} + {id === "data" ? ( +
+ {[labels.export, labels.import, labels.clear].map( + (item) => ( + + ), + )} +
+ ) : null} +
+ ) : null} +
+ ))} +
+ ) : null} + {tab === "accounts" ? ( +
+ + {[activeEmail, "work@gmail.com"].map((email, index) => ( +
+ + + {email} + + {index === 0 ? ( + + {labels.current} + + ) : null} +
+ ))} +
+ ) : null} + {tab === "changelog" ? ( +
+

Version 1.3.0

+

2026-07-13

+
    +
  • • Inline helper on every domain
  • +
  • • Interactive Generate and History
  • +
  • • Unified BeUI styling and dark mode
  • +
+
+ ) : null}
); } +/** Renders a mock settings dropdown select for the extension settings demo. */ +function MockSettingSelect({ + label, + options, + isDark, +}: { + label: string; + options: string[]; + isDark: boolean; +}) { + return ( + + ); +} + +/** Renders a mock toggle switch for the extension settings demo. */ +function MockSwitch({ + enabled, + onChange, +}: { + enabled: boolean; + onChange: (enabled: boolean) => void; +}) { + return ( + + ); +} + /** Renders the header inside the extension popup mockup. */ -function MockHeader() { +function MockHeader({ + t, + isDark, + onToggleDark, + onOpenSettings, +}: { + t: (typeof translations)[Locale]; + isDark: boolean; + onToggleDark: () => void; + onOpenSettings: () => void; +}) { return ( -
+
-
- +
+ +
-

Gmail Alias Toolkit

-

david@gmail.com

+

Gmail Alias Toolkit

+

{t.mock.subtitle}

- + onToggleDark()} + variant="circle" + start="center" + className="h-9 w-9 rounded-2xl text-blue-50 hover:bg-white/15" + iconClassName="h-4 w-4" + /> + +
+
+ ); +} + +/** Mirrors the active-account selector used by the current popup. */ +function MockAccountSwitcher({ + t, + isDark, + activeEmail, + isOpen, + onToggle, + onSelect, +}: { + t: (typeof translations)[Locale]; + isDark: boolean; + activeEmail: string; + isOpen: boolean; + onToggle: () => void; + onSelect: (email: string) => void; +}) { + const accounts = [ + "david@gmail.com", + "work@gmail.com", + "alias.team@gmail.com", + ]; + return ( +
+

- - + {t.mock.activeEmail} +

+
+ + +
+ {isOpen ? ( +
+ {accounts.map((email) => ( + + ))} +
+ ) : null}
); } @@ -1063,15 +1711,17 @@ function MockTabs({ activeTab, setActiveTab, tabs, + isDark, }: { activeTab: (typeof translations)[Locale]["tabs"][number]["id"]; setActiveTab: ( tab: (typeof translations)[Locale]["tabs"][number]["id"], ) => void; tabs: (typeof translations)[Locale]["tabs"]; + isDark: boolean; }) { return ( -
+
{tabs.map((tab) => { const Icon = tab.icon; const active = activeTab === tab.id; @@ -1085,7 +1735,9 @@ function MockTabs({ className={`relative flex h-10 items-center justify-center gap-2 rounded-xl border px-2 text-xs font-extrabold transition ${ active ? "border-blue-300 text-blue-700" - : "border-slate-200 bg-white text-slate-500 hover:text-slate-950" + : isDark + ? "border-slate-700 bg-slate-800 text-slate-300 hover:text-white" + : "border-slate-200 bg-white text-slate-500 hover:text-slate-950" }`} > {active ? ( @@ -1108,9 +1760,11 @@ function MockTabs({ function MockGeneratorPanel({ currentTab, t, + isDark, }: { currentTab: (typeof translations)[Locale]["tabs"][number]; t: (typeof translations)[Locale]; + isDark: boolean; }) { return (
-
+

{t.mock.format}

-

+

{currentTab.title}

-
+

{t.mock.count}

-

10

+

+ 10 +

{t.mock.generate} -
+
{t.mock.generated} @@ -1155,7 +1821,7 @@ function MockGeneratorPanel({ initial={{ opacity: 0.74 }} animate={{ opacity: 1 }} transition={{ duration: 0.25 }} - className="break-all font-mono text-sm font-bold text-slate-950" + className={`break-all font-mono text-sm font-bold ${isDark ? "text-blue-300" : "text-slate-950"}`} > {currentTab.alias} @@ -1168,73 +1834,465 @@ function MockGeneratorPanel({ } /** Renders recent alias rows in the popup mockup. */ -function MockHistory({ t }: { t: (typeof translations)[Locale] }) { +function MockHistory({ + t, + isDark, +}: { + t: (typeof translations)[Locale]; + isDark: boolean; +}) { return ( -
-
-
- {t.mock.recent} +
+
+
+
+ {t.mock.recent} +
+
+ {t.mock.search} +
+
+
+ {historyItems.slice(0, 2).map((item, index) => ( + +
+

+ {item.alias} +

+

+ {item.tag} +

+
+ + +
+ ))}
-
- {t.mock.search} +
+
+ ); +} + +/** Renders compact utility actions in the popup mockup footer. */ +function MockFooter({ + t, + isDark, +}: { + t: (typeof translations)[Locale]; + isDark: boolean; +}) { + return ( +
+ {t.mock.viewStatistics} + +
+ ); +} + +/** Renders the input form for the inline popup demo. */ +function InlineFormInput({ + inputValue, + t, +}: { + inputValue: string; + t: (typeof translations)[Locale]; +}) { + return ( + <> + +
+
+ + {inputValue || t.inlineHelper.email} +
+
-
- {historyItems.map((item, index) => ( - -
-

- {item.alias} -

-

- {item.tag} -

-
- + ); +} + +/** Demo form container for inline helper popup. */ +function InlineDemoContainer({ + t, + inputValue, + hoveredAlias, + activeTab, + setActiveTab, + setHoveredAlias, + setSelectedAlias, + aliases, +}: { + t: (typeof translations)[Locale]; + inputValue: string; + hoveredAlias: string; + activeTab: "suggestions" | "generate" | "history"; + setActiveTab: (tab: "suggestions" | "generate" | "history") => void; + setHoveredAlias: (alias: string) => void; + setSelectedAlias: (alias: string) => void; + aliases: string[]; +}) { + return ( +
+ + + + + + + +
+ +
+ {( + [ + ["suggestions", t.inlineHelper.suggestions], + ["generate", t.inlineHelper.generate], + ["history", t.inlineHelper.history], + ] as const + ).map(([id, label]) => ( + + ))} +
+
+ {activeTab === "suggestions" ? ( + - - - ))} + ) : null} + {activeTab === "generate" ? ( + + ) : null} + {activeTab === "history" ? ( + + ) : null} +
+
+ + + + Report / Review + +
); } -/** Renders compact utility actions in the popup mockup footer. */ -function MockFooter({ t }: { t: (typeof translations)[Locale] }) { - const footerItems = [ - [Settings, t.mock.settings], - [FileJson, "JSON"], - [QrCode, "QR"], - ] as const; +/** Content grid with workflow steps and demo for inline popup. */ +function InlinePopupGrid({ + t, + activeTab, + setActiveTab, + hoveredAlias, + setHoveredAlias, + setSelectedAlias, + aliases, + inputValue, +}: { + t: (typeof translations)[Locale]; + activeTab: "suggestions" | "generate" | "history"; + setActiveTab: (tab: "suggestions" | "generate" | "history") => void; + hoveredAlias: string; + setHoveredAlias: (alias: string) => void; + setSelectedAlias: (alias: string) => void; + aliases: string[]; + inputValue: string; +}) { + return ( +
+ +
+
+

{t.inlineHelper.demoTitle}

+

+ {t.inlineHelper.previewHint} +

+
+ +
+
+ ); +} + +/** Demonstrates the complete inline-helper workflow beside a website form. */ +function InlinePopupSection({ t }: { t: (typeof translations)[Locale] }) { + const [activeTab, setActiveTab] = useState< + "suggestions" | "generate" | "history" + >("suggestions"); + const [hoveredAlias, setHoveredAlias] = useState(""); + const [selectedAlias, setSelectedAlias] = useState(""); + const aliases = [ + "david+website@gmail.com", + "david+website007@gmail.com", + "david+web@gmail.com", + "david+website-20260714@gmail.com", + ]; + const inputValue = hoveredAlias || selectedAlias; return ( -
- {footerItems.map(([Icon, label]) => ( +
+
+ + +
+
+ ); +} + +/** Renders numbered workflow steps for the inline popup demo section. */ +function InlineWorkflowSteps({ steps }: { steps: readonly string[] }) { + return ( +
+ {steps.map((step, index) => ( - - {label} + + {index + 1} + +

+ {step} +

))}
); } +/** Renders the header for the inline popup demo with controls. */ +function InlineDemoHeader() { + return ( +
+ + Gmail Alias Toolkit + +
+ + + + + +
+
+ ); +} + +/** Renders the suggested aliases panel for the inline popup demo. */ +function InlineSuggestions({ + aliases, + label, + onHover, + onSelect, +}: { + aliases: string[]; + label: string; + onHover: (alias: string) => void; + onSelect: (alias: string) => void; +}) { + return ( +
+

+ {label} +

+
+ {aliases.map((alias) => ( + + ))} +
+
+ ); +} + +/** Renders the generate tab demo for the inline popup section. */ +function InlineGenerateDemo({ + t, + aliases, +}: { + t: (typeof translations)[Locale]; + aliases: string[]; +}) { + return ( +
+
+ {["Random", "Tags", "Tricks"].map((label, index) => ( + + {label} + + ))} +
+
+ + Private Mail + + + 5 + +
+ +
+ {aliases.slice(0, 2).map((alias) => ( +
+ {alias} +
+ ))} +
+
+ ); +} + +/** Renders the history tab demo for the inline popup section. */ +function InlineHistoryDemo({ + t, + aliases, +}: { + t: (typeof translations)[Locale]; + aliases: string[]; +}) { + return ( +
+
+ {t.inlineHelper.search} +
+
+ + {t.inlineHelper.allAliases} + + + {t.inlineHelper.mostRecent} + +
+
+ {aliases.map((alias) => ( +
+ {alias} +
+ ))} +
+
+ ); +} + /** Renders the feature grid with active preview and sample rail. */ function FeaturesSection({ t }: { t: (typeof translations)[Locale] }) { const [activeFeatureIndex, setActiveFeatureIndex] = useState(0); @@ -1596,7 +2654,7 @@ function CtaSection({ t }: { t: (typeof translations)[Locale] }) {

{t.cta.desc}

-
+
{t.cta.install} + + {t.cta.installFirefox} +
@@ -1618,6 +2686,16 @@ export function App() { const [locale, setLocale] = useState("en"); const copy = translations[locale]; + useEffect(() => { + let favicon = document.querySelector("link[rel='icon']"); + if (!favicon) { + favicon = document.createElement("link"); + favicon.rel = "icon"; + document.head.appendChild(favicon); + } + favicon.href = extensionIconUrl; + }, []); + return (
+ diff --git a/web/src/lucide-react.d.ts b/web/src/lucide-react.d.ts index c09bc7a..a83e13c 100644 --- a/web/src/lucide-react.d.ts +++ b/web/src/lucide-react.d.ts @@ -1,35 +1,9 @@ -declare module "lucide-react" { - import type { ComponentType, SVGProps } from "react"; - - export type LucideIcon = ComponentType< - SVGProps & { - absoluteStrokeWidth?: boolean; - size?: number | string; - } - >; +declare module "*.svg?url" { + const url: string; + export default url; +} - export const ArrowRight: LucideIcon; - export const AtSign: LucideIcon; - export const BadgeCheck: LucideIcon; - export const BarChart3: LucideIcon; - export const Clipboard: LucideIcon; - export const Copy: LucideIcon; - export const Database: LucideIcon; - export const Download: LucideIcon; - export const EyeOff: LucideIcon; - export const FileJson: LucideIcon; - export const Github: LucideIcon; - export const History: LucideIcon; - export const Languages: LucideIcon; - export const Mail: LucideIcon; - export const Moon: LucideIcon; - export const QrCode: LucideIcon; - export const Search: LucideIcon; - export const Settings: LucideIcon; - export const ShieldCheck: LucideIcon; - export const Shuffle: LucideIcon; - export const Sparkles: LucideIcon; - export const Star: LucideIcon; - export const Tags: LucideIcon; - export const Zap: LucideIcon; +declare module "*.png?url" { + const url: string; + export default url; } diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 8b83e9a..bfcb79a 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -1,6 +1,11 @@ /** @type {import('tailwindcss').Config} */ export default { - content: ["./index.html", "./src/**/*.{ts,tsx}"], + content: [ + "./index.html", + "./src/**/*.{ts,tsx}", + "../src/components/motion/theme-toggle.tsx", + "../src/components/motion/action-swap.tsx", + ], theme: { extend: { fontFamily: { diff --git a/web/tsconfig.json b/web/tsconfig.json index a2d0c94..d7c6e70 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -1 +1 @@ -{"compilerOptions":{"target":"ES2020","useDefineForClassFields":true,"lib":["DOM","DOM.Iterable","ES2020"],"allowJs":false,"skipLibCheck":true,"esModuleInterop":true,"allowSyntheticDefaultImports":true,"strict":true,"forceConsistentCasingInFileNames":true,"module":"ESNext","moduleResolution":"Bundler","resolveJsonModule":true,"isolatedModules":true,"noEmit":true,"jsx":"react-jsx"},"include":["src"],"references":[]} \ No newline at end of file +{"compilerOptions":{"target":"ES2020","useDefineForClassFields":true,"lib":["DOM","DOM.Iterable","ES2020"],"allowJs":false,"skipLibCheck":true,"esModuleInterop":true,"allowSyntheticDefaultImports":true,"strict":true,"forceConsistentCasingInFileNames":true,"module":"ESNext","moduleResolution":"Bundler","resolveJsonModule":true,"isolatedModules":true,"noEmit":true,"jsx":"react-jsx","baseUrl":"..","paths":{"src/*":["src/*"]}},"include":["src"],"references":[]} diff --git a/web/vite.config.ts b/web/vite.config.ts index fc872d6..a885378 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,7 +1,14 @@ import react from "@vitejs/plugin-react"; import { defineConfig } from "vite"; +import { fileURLToPath, URL } from "node:url"; export default defineConfig({ base: "/gmail-alias-toolkit/", plugins: [react()], + resolve: { + alias: { + src: fileURLToPath(new URL("../src", import.meta.url)), + }, + dedupe: ["react", "react-dom"], + }, }); diff --git a/yarn.lock b/yarn.lock index 8f18f51..416c70d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2083,13 +2083,6 @@ __metadata: languageName: node linkType: hard -"@types/prop-types@npm:*": - version: 15.7.15 - resolution: "@types/prop-types@npm:15.7.15" - checksum: 10c0/b59aad1ad19bf1733cf524fd4e618196c6c7690f48ee70a327eb450a42aab8e8a063fbe59ca0a5701aebe2d92d582292c0fb845ea57474f6a15f6994b0e260b2 - languageName: node - linkType: hard - "@types/qrcode@npm:^1.5.6": version: 1.5.6 resolution: "@types/qrcode@npm:1.5.6" @@ -2099,15 +2092,6 @@ __metadata: languageName: node linkType: hard -"@types/react-dom@npm:^18.3.5": - version: 18.3.7 - resolution: "@types/react-dom@npm:18.3.7" - peerDependencies: - "@types/react": ^18.0.0 - checksum: 10c0/8bd309e2c3d1604a28a736a24f96cbadf6c05d5288cfef8883b74f4054c961b6b3a5e997fd5686e492be903c8f3380dba5ec017eff3906b1256529cd2d39603e - languageName: node - linkType: hard - "@types/react-dom@npm:^19.2.3": version: 19.2.3 resolution: "@types/react-dom@npm:19.2.3" @@ -2117,16 +2101,6 @@ __metadata: languageName: node linkType: hard -"@types/react@npm:^18.3.18": - version: 18.3.31 - resolution: "@types/react@npm:18.3.31" - dependencies: - "@types/prop-types": "npm:*" - csstype: "npm:^3.2.2" - checksum: 10c0/44180549dd045f536ececd39e39aacdf828e76adc1c4a90b132f453e23cc370c4648d9102ae401172ebd8fd8b1977a901a39e214e53ec77171b27514b588c179 - languageName: node - linkType: hard - "@types/react@npm:^19.2.7": version: 19.2.7 resolution: "@types/react@npm:19.2.7" @@ -3956,16 +3930,16 @@ __metadata: version: 0.0.0-use.local resolution: "gmail-alias-toolkit-web@workspace:web" dependencies: - "@types/react": "npm:^18.3.18" - "@types/react-dom": "npm:^18.3.5" + "@types/react": "npm:^19.2.7" + "@types/react-dom": "npm:^19.2.3" "@vitejs/plugin-react": "npm:^4.3.4" autoprefixer: "npm:^10.4.20" clsx: "npm:^2.1.1" framer-motion: "npm:^12.23.12" lucide-react: "npm:^0.468.0" postcss: "npm:^8.4.49" - react: "npm:^18.3.1" - react-dom: "npm:^18.3.1" + react: "npm:^19.2.3" + react-dom: "npm:^19.2.3" tailwind-merge: "npm:^2.5.5" tailwindcss: "npm:^3.4.17" typescript: "npm:^5.7.2" @@ -4001,6 +3975,7 @@ __metadata: react-dom: "npm:^19.2.3" tailwind-merge: "npm:^2.6.0" tailwindcss: "npm:^4" + tldts: "npm:^7.4.8" typescript: "npm:^5.9.3" vite: "npm:^8.1.1" vitest: "npm:^4.1.9" @@ -4433,7 +4408,7 @@ __metadata: languageName: node linkType: hard -"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": +"js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0" checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed @@ -4873,17 +4848,6 @@ __metadata: languageName: node linkType: hard -"loose-envify@npm:^1.1.0": - version: 1.4.0 - resolution: "loose-envify@npm:1.4.0" - dependencies: - js-tokens: "npm:^3.0.0 || ^4.0.0" - bin: - loose-envify: cli.js - checksum: 10c0/655d110220983c1a4b9c0c679a2e8016d4b67f6e9c7b5435ff5979ecdb20d0813f4dec0a08674fcbdd4846a3f07edbb50a36811fd37930b94aaa0d9daceb017e - languageName: node - linkType: hard - "lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.1": version: 11.2.7 resolution: "lru-cache@npm:11.2.7" @@ -5928,18 +5892,6 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:^18.3.1": - version: 18.3.1 - resolution: "react-dom@npm:18.3.1" - dependencies: - loose-envify: "npm:^1.1.0" - scheduler: "npm:^0.23.2" - peerDependencies: - react: ^18.3.1 - checksum: 10c0/a752496c1941f958f2e8ac56239172296fcddce1365ce45222d04a1947e0cc5547df3e8447f855a81d6d39f008d7c32eab43db3712077f09e3f67c4874973e85 - languageName: node - linkType: hard - "react-dom@npm:^19.2.3": version: 19.2.3 resolution: "react-dom@npm:19.2.3" @@ -5972,15 +5924,6 @@ __metadata: languageName: node linkType: hard -"react@npm:^18.3.1": - version: 18.3.1 - resolution: "react@npm:18.3.1" - dependencies: - loose-envify: "npm:^1.1.0" - checksum: 10c0/283e8c5efcf37802c9d1ce767f302dd569dd97a70d9bb8c7be79a789b9902451e0d16334b05d73299b20f048cbc3c7d288bbbde10b701fa194e2089c237dbea3 - languageName: node - linkType: hard - "react@npm:^19.2.3": version: 19.2.3 resolution: "react@npm:19.2.3" @@ -6344,15 +6287,6 @@ __metadata: languageName: node linkType: hard -"scheduler@npm:^0.23.2": - version: 0.23.2 - resolution: "scheduler@npm:0.23.2" - dependencies: - loose-envify: "npm:^1.1.0" - checksum: 10c0/26383305e249651d4c58e6705d5f8425f153211aef95f15161c151f7b8de885f24751b377e4a0b3dd42cce09aad3f87a61dab7636859c0d89b7daf1a1e2a5c78 - languageName: node - linkType: hard - "scheduler@npm:^0.27.0": version: 0.27.0 resolution: "scheduler@npm:0.27.0" @@ -6978,6 +6912,13 @@ __metadata: languageName: node linkType: hard +"tldts-core@npm:^7.4.8": + version: 7.4.8 + resolution: "tldts-core@npm:7.4.8" + checksum: 10c0/94edee2a06cb1929fae4a26045628b01dd865ee8e28661df29128228522fb8dcf29c2f68ffa7286ee41a2a2c98da3e6814dbf49a1f6509de3fe2c9448e0db1b7 + languageName: node + linkType: hard + "tldts@npm:^7.0.5": version: 7.4.5 resolution: "tldts@npm:7.4.5" @@ -6989,6 +6930,17 @@ __metadata: languageName: node linkType: hard +"tldts@npm:^7.4.8": + version: 7.4.8 + resolution: "tldts@npm:7.4.8" + dependencies: + tldts-core: "npm:^7.4.8" + bin: + tldts: bin/cli.js + checksum: 10c0/438b379a43b105359311427b8ab9d73572244996b4f6dd1fbe9112bd274c308d4ffb33b6f644302e79c8bc60f2e2539463b00dd91ee1063b92d9fc8b10882b6c + languageName: node + linkType: hard + "tmp@npm:0.2.5": version: 0.2.5 resolution: "tmp@npm:0.2.5"