diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..a80574fb --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,18 @@ +name: "lint" +on: [pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Run ESLint + run: bun run lint diff --git a/CONTRIBUTING_TRANSLATIONS.md b/CONTRIBUTING_TRANSLATIONS.md new file mode 100644 index 00000000..d82a142d --- /dev/null +++ b/CONTRIBUTING_TRANSLATIONS.md @@ -0,0 +1,174 @@ +# Contributing Translations to Handy + +Thank you for helping translate Handy! This guide explains how to add or improve translations. + +## Quick Start + +1. Fork the repository +2. Copy the English translation file to your language folder +3. Translate the values (not the keys!) +4. Submit a pull request + +## File Structure + +Translation files are located in: + +``` +src/i18n/locales/ +├── en/ +│ └── translation.json # English (source) +├── vi/ +│ └── translation.json # Vietnamese +├── fr/ +│ └── translation.json # French +└── [your-language]/ + └── translation.json # Your contribution! +``` + +## Adding a New Language + +### Step 1: Create the Language Folder + +Create a new folder using the [ISO 639-1 language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes): + +```bash +mkdir src/i18n/locales/[language-code] +``` + +Examples: + +- `de` for German +- `es` for Spanish +- `ja` for Japanese +- `zh` for Chinese +- `ko` for Korean +- `pt` for Portuguese + +### Step 2: Copy the English File + +```bash +cp src/i18n/locales/en/translation.json src/i18n/locales/[language-code]/translation.json +``` + +### Step 3: Translate the Values + +Open the file and translate only the **values** (right side), not the keys (left side): + +```json +{ + "sidebar": { + "general": "General", // ← Translate this value + "advanced": "Advanced", // ← Translate this value + ... + } +} +``` + +**Important:** + +- Keep all keys exactly the same +- Preserve any `{{variables}}` in the text (e.g., `{{error}}`, `{{model}}`) +- Keep the JSON structure and formatting intact + +### Step 4: Register Your Language + +Edit `src/i18n/languages.ts` and add your language metadata: + +```typescript +export const LANGUAGE_METADATA: Record< + string, + { name: string; nativeName: string } +> = { + en: { name: "English", nativeName: "English" }, + es: { name: "Spanish", nativeName: "Español" }, + fr: { name: "French", nativeName: "Français" }, + vi: { name: "Vietnamese", nativeName: "Tiếng Việt" }, + de: { name: "German", nativeName: "Deutsch" }, // ← Add your language +}; +``` + +### Step 5: Test Your Translation + +1. Run the app: `bun run tauri dev` +2. Go to Settings → General → App Language +3. Select your language +4. Verify all text displays correctly + +### Step 6: Submit a Pull Request + +1. Commit your changes +2. Push to your fork +3. Open a pull request with: + - Language name in the title (e.g., "Add German translation") + - Any notes about the translation + +## Improving Existing Translations + +Found a typo or better translation? + +1. Edit the relevant `translation.json` file +2. Submit a PR with a brief description of the change + +## Translation Guidelines + +### Do: + +- Use natural, native-sounding language +- Keep translations concise (UI space is limited) +- Match the tone of the English text (friendly, clear) +- Preserve technical terms when appropriate (e.g., "API", "GPU") + +### Don't: + +- Translate brand names (Handy, Whisper.cpp, OpenAI) +- Change or remove `{{variables}}` +- Modify JSON keys +- Add extra spaces or formatting + +### Handling Variables + +Some strings contain variables like `{{error}}` or `{{model}}`. Keep these exactly as-is: + +```json +// English +"downloadModel": "Failed to download model: {{error}}" + +// French (correct) +"downloadModel": "Échec du téléchargement du modèle : {{error}}" + +// French (incorrect - don't translate the variable!) +"downloadModel": "Échec du téléchargement du modèle : {{erreur}}" +``` + +### Handling Plurals + +Some languages have complex plural rules. For now, use a general form that works for all cases. We may add proper plural support in the future. + +## Questions? + +- Open an issue on GitHub +- Join the discussion in existing translation PRs + +## Currently Supported Languages + +| Language | Code | Status | +| ---------- | ---- | ----------------- | +| English | `en` | Complete (source) | +| Spanish | `es` | Complete | +| French | `fr` | Complete | +| Vietnamese | `vi` | Complete | + +## Requested Languages + +We'd love help with: + +- German (`de`) +- Japanese (`ja`) +- Chinese (`zh`) +- Korean (`ko`) +- Portuguese (`pt`) +- And more! + +--- + +Thank you for making Handy accessible to more people around the world! diff --git a/bun.lock b/bun.lock index 5b5b03b6..4e594aa7 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "handy-app", @@ -17,9 +16,11 @@ "@tauri-apps/plugin-sql": "~2.3.1", "@tauri-apps/plugin-store": "~2.4.1", "@tauri-apps/plugin-updater": "~2.9.0", + "i18next": "^25.7.2", "lucide-react": "^0.542.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-i18next": "^16.4.1", "react-select": "^5.8.0", "sonner": "^2.0.7", "tailwindcss": "^4.1.16", @@ -33,7 +34,11 @@ "@types/react": "^18.3.26", "@types/react-dom": "^18.3.7", "@types/react-select": "^5.0.1", + "@typescript-eslint/eslint-plugin": "^8.49.0", + "@typescript-eslint/parser": "^8.49.0", "@vitejs/plugin-react": "^4.7.0", + "eslint": "^9.39.1", + "eslint-plugin-i18next": "^6.1.3", "prettier": "^3.6.2", "typescript": "~5.6.3", "vite": "^6.4.1", @@ -155,12 +160,38 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.11", "", { "os": "win32", "cpu": "x64" }, "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.3", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ=="], + + "@eslint/js": ["@eslint/js@9.39.1", "", {}, "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -303,6 +334,8 @@ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="], "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], @@ -317,26 +350,72 @@ "@types/react-transition-group": ["@types/react-transition-group@4.4.12", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.49.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/type-utils": "8.49.0", "@typescript-eslint/utils": "8.49.0", "@typescript-eslint/visitor-keys": "8.49.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.49.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.49.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", "@typescript-eslint/typescript-estree": "8.49.0", "@typescript-eslint/visitor-keys": "8.49.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.49.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.49.0", "@typescript-eslint/types": "^8.49.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.49.0", "", { "dependencies": { "@typescript-eslint/types": "8.49.0", "@typescript-eslint/visitor-keys": "8.49.0" } }, "sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.49.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.49.0", "", { "dependencies": { "@typescript-eslint/types": "8.49.0", "@typescript-eslint/typescript-estree": "8.49.0", "@typescript-eslint/utils": "8.49.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.49.0", "", {}, "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.49.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.49.0", "@typescript-eslint/tsconfig-utils": "8.49.0", "@typescript-eslint/types": "8.49.0", "@typescript-eslint/visitor-keys": "8.49.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.49.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.49.0", "@typescript-eslint/types": "8.49.0", "@typescript-eslint/typescript-estree": "8.49.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.49.0", "", { "dependencies": { "@typescript-eslint/types": "8.49.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="], + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.8.20", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ=="], + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "browserslist": ["browserslist@4.27.0", "", { "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", "electron-to-chromium": "^1.5.238", "node-releases": "^2.0.26", "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" } }, "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw=="], "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], "caniuse-lite": ["caniuse-lite@1.0.30001751", "", {}, "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], @@ -353,38 +432,102 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + "eslint": ["eslint@9.39.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.1", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g=="], + + "eslint-plugin-i18next": ["eslint-plugin-i18next@6.1.3", "", { "dependencies": { "lodash": "^4.17.21", "requireindex": "~1.1.0" } }, "sha512-z/h4oBRd9wI1ET60HqcLSU6XPeAh/EPOrBBTyCdkWeMoYrWAaUVA+DOQkWTiNIyCltG4NTmy62SQisVXxoXurw=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + "find-root": ["find-root@1.1.0", "", {}, "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="], + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], + "html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="], + + "i18next": ["i18next@25.7.2", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-58b4kmLpLv1buWUEwegMDUqZVR5J+rT+WTRFaBGL7lxDuJQQ0NrJFrq+eT2N94aYVR1k1Sr13QITNOL88tZCuw=="], + + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + "lightningcss": ["lightningcss@1.30.2", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.30.2", "lightningcss-darwin-arm64": "1.30.2", "lightningcss-darwin-x64": "1.30.2", "lightningcss-freebsd-x64": "1.30.2", "lightningcss-linux-arm-gnueabihf": "1.30.2", "lightningcss-linux-arm64-gnu": "1.30.2", "lightningcss-linux-arm64-musl": "1.30.2", "lightningcss-linux-x64-gnu": "1.30.2", "lightningcss-linux-x64-musl": "1.30.2", "lightningcss-win32-arm64-msvc": "1.30.2", "lightningcss-win32-x64-msvc": "1.30.2" } }, "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.30.2", "", { "os": "android", "cpu": "arm64" }, "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A=="], @@ -411,6 +554,12 @@ "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], @@ -421,18 +570,32 @@ "memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="], + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "node-releases": ["node-releases@2.0.26", "", {}, "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], @@ -443,14 +606,20 @@ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], + "react-i18next": ["react-i18next@16.4.1", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-GzsYomxb1/uE7nlJm0e1qQ8f+W9I3Xirh9VoycZIahk6C8Pmv/9Fd0ek6zjf1FSgtGLElDGqwi/4FOHEGUbsEQ=="], + "react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="], @@ -459,6 +628,8 @@ "react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="], + "requireindex": ["requireindex@1.1.0", "", {}, "sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg=="], + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], @@ -467,7 +638,11 @@ "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], @@ -475,8 +650,12 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], "tailwindcss": ["tailwindcss@4.1.16", "", {}, "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA=="], @@ -487,26 +666,50 @@ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "ts-api-utils": ["ts-api-utils@2.1.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "typescript": ["typescript@5.6.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="], + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "use-isomorphic-layout-effect": ["use-isomorphic-layout-effect@1.2.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA=="], + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], + "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "zustand": ["zustand@5.0.8", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@emotion/babel-plugin/convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.6.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.6.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA=="], @@ -518,5 +721,11 @@ "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "eslint/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], } } diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..20e027e3 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,38 @@ +import i18next from "eslint-plugin-i18next"; +import tsParser from "@typescript-eslint/parser"; + +export default [ + { + files: ["src/**/*.{ts,tsx}"], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + }, + }, + plugins: { + i18next, + }, + rules: { + // Catch text in JSX that should be translated + "i18next/no-literal-string": [ + "error", + { + markupOnly: true, // Only check JSX content, not all strings + ignoreAttribute: [ + "className", + "style", + "type", + "id", + "name", + "key", + "data-*", + "aria-*", + ], // Ignore common non-translatable attributes + }, + ], + }, + }, +]; diff --git a/package.json b/package.json index 0179112a..f55efb3c 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,8 @@ "build": "tsc && vite build", "preview": "vite preview", "tauri": "tauri", + "lint": "eslint src", + "lint:fix": "eslint src --fix", "format": "prettier --write . && cd src-tauri && cargo fmt", "format:check": "prettier --check . && cd src-tauri && cargo fmt -- --check", "format:frontend": "prettier --write .", @@ -28,9 +30,11 @@ "@tauri-apps/plugin-updater": "~2.9.0", "react-select": "^5.8.0", "tauri-plugin-macos-permissions-api": "2.3.0", + "i18next": "^25.7.2", "lucide-react": "^0.542.0", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-i18next": "^16.4.1", "sonner": "^2.0.7", "tailwindcss": "^4.1.16", "zod": "^3.25.76", @@ -42,7 +46,11 @@ "@types/react": "^18.3.26", "@types/react-dom": "^18.3.7", "@types/react-select": "^5.0.1", + "@typescript-eslint/eslint-plugin": "^8.49.0", + "@typescript-eslint/parser": "^8.49.0", "@vitejs/plugin-react": "^4.7.0", + "eslint": "^9.39.1", + "eslint-plugin-i18next": "^6.1.3", "prettier": "^3.6.2", "typescript": "~5.6.3", "vite": "^6.4.1" diff --git a/src/components/AccessibilityPermissions.tsx b/src/components/AccessibilityPermissions.tsx index e8abfb18..6454c8cc 100644 --- a/src/components/AccessibilityPermissions.tsx +++ b/src/components/AccessibilityPermissions.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { checkAccessibilityPermission, requestAccessibilityPermission, @@ -14,6 +15,7 @@ interface ButtonConfig { } const AccessibilityPermissions: React.FC = () => { + const { t } = useTranslation(); const [hasAccessibility, setHasAccessibility] = useState(false); const [permissionState, setPermissionState] = useState("request"); @@ -61,12 +63,12 @@ const AccessibilityPermissions: React.FC = () => { // Configure button text and style based on state const buttonConfig: Record = { request: { - text: "Grant", + text: t("accessibility.openSettings"), className: "px-2 py-1 text-sm font-semibold bg-mid-gray/10 border border-mid-gray/80 hover:bg-logo-primary/10 rounded cursor-pointer hover:border-logo-primary", }, verify: { - text: "Verify", + text: t("accessibility.openSettings"), className: "bg-gray-100 hover:bg-gray-200 text-gray-800 font-medium py-1 px-3 rounded text-sm flex items-center justify-center cursor-pointer", }, @@ -80,7 +82,7 @@ const AccessibilityPermissions: React.FC = () => {

- Please grant accessibility permissions for Handy + {t("accessibility.permissionsDescription")}

); })} diff --git a/src/components/footer/Footer.tsx b/src/components/footer/Footer.tsx index b901a048..cadeb352 100644 --- a/src/components/footer/Footer.tsx +++ b/src/components/footer/Footer.tsx @@ -32,6 +32,7 @@ const Footer: React.FC = () => {
+ {/* eslint-disable-next-line i18next/no-literal-string */} v{version}
diff --git a/src/components/model-selector/ModelDropdown.tsx b/src/components/model-selector/ModelDropdown.tsx index 4f7477cb..10bb23cb 100644 --- a/src/components/model-selector/ModelDropdown.tsx +++ b/src/components/model-selector/ModelDropdown.tsx @@ -1,6 +1,11 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import type { ModelInfo } from "@/bindings"; import { formatModelSize } from "../../lib/utils/format"; +import { + getTranslatedModelName, + getTranslatedModelDescription, +} from "../../lib/utils/modelTranslation"; import { ProgressBar } from "../shared"; interface DownloadProgress { @@ -29,6 +34,7 @@ const ModelDropdown: React.FC = ({ onModelDelete, onError, }) => { + const { t } = useTranslation(); const availableModels = models.filter((m) => m.is_downloaded); const downloadableModels = models.filter((m) => !m.is_downloaded); const isFirstRun = availableModels.length === 0 && models.length > 0; @@ -65,10 +71,10 @@ const ModelDropdown: React.FC = ({ {isFirstRun && (
- Welcome to Handy! + {t("modelSelector.welcome")}
- Download a model below to get started with transcription. + {t("modelSelector.downloadPrompt")}
)} @@ -77,7 +83,7 @@ const ModelDropdown: React.FC = ({ {availableModels.length > 0 && (
- Available Models + {t("modelSelector.availableModels")}
{availableModels.map((model) => (
= ({ >
-
{model.name}
+
+ {getTranslatedModelName(model, t)} +
- {model.description} + {getTranslatedModelDescription(model, t)}
{currentModelId === model.id && ( -
Active
+
+ {t("modelSelector.active")} +
)} {currentModelId !== model.id && (
diff --git a/src/components/settings/AppLanguageSelector.tsx b/src/components/settings/AppLanguageSelector.tsx new file mode 100644 index 00000000..fd219bb0 --- /dev/null +++ b/src/components/settings/AppLanguageSelector.tsx @@ -0,0 +1,45 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Dropdown } from "../ui/Dropdown"; +import { SettingContainer } from "../ui/SettingContainer"; +import { SUPPORTED_LANGUAGES, type SupportedLanguageCode } from "../../i18n"; + +interface AppLanguageSelectorProps { + descriptionMode?: "inline" | "tooltip"; + grouped?: boolean; +} + +export const AppLanguageSelector: React.FC = + React.memo(({ descriptionMode = "tooltip", grouped = false }) => { + const { t, i18n } = useTranslation(); + + const currentLanguage = i18n.language as SupportedLanguageCode; + + const languageOptions = SUPPORTED_LANGUAGES.map((lang) => ({ + value: lang.code, + label: `${lang.nativeName} (${lang.name})`, + })); + + const handleLanguageChange = (langCode: string) => { + i18n.changeLanguage(langCode); + // Persist to localStorage for next session + localStorage.setItem("handy-app-language", langCode); + }; + + return ( + + + + ); + }); + +AppLanguageSelector.displayName = "AppLanguageSelector"; diff --git a/src/components/settings/AppendTrailingSpace.tsx b/src/components/settings/AppendTrailingSpace.tsx index 9374c458..594b9488 100644 --- a/src/components/settings/AppendTrailingSpace.tsx +++ b/src/components/settings/AppendTrailingSpace.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { ToggleSwitch } from "../ui/ToggleSwitch"; import { useSettings } from "../../hooks/useSettings"; @@ -9,6 +10,7 @@ interface AppendTrailingSpaceProps { export const AppendTrailingSpace: React.FC = React.memo(({ descriptionMode = "tooltip", grouped = false }) => { + const { t } = useTranslation(); const { getSetting, updateSetting, isUpdating } = useSettings(); const enabled = getSetting("append_trailing_space") ?? false; @@ -18,8 +20,8 @@ export const AppendTrailingSpace: React.FC = checked={enabled} onChange={(enabled) => updateSetting("append_trailing_space", enabled)} isUpdating={isUpdating("append_trailing_space")} - label="Append Trailing Space" - description="Automatically add a space at the end of transcribed text, making it easier to dictate multiple sentences in a row." + label={t("settings.debug.appendTrailingSpace.label")} + description={t("settings.debug.appendTrailingSpace.description")} descriptionMode={descriptionMode} grouped={grouped} /> diff --git a/src/components/settings/AudioFeedback.tsx b/src/components/settings/AudioFeedback.tsx index 3889aa98..4ae34824 100644 --- a/src/components/settings/AudioFeedback.tsx +++ b/src/components/settings/AudioFeedback.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { ToggleSwitch } from "../ui/ToggleSwitch"; import { useSettings } from "../../hooks/useSettings"; import { VolumeSlider } from "./VolumeSlider"; @@ -11,6 +12,7 @@ interface AudioFeedbackProps { export const AudioFeedback: React.FC = React.memo( ({ descriptionMode = "tooltip", grouped = false }) => { + const { t } = useTranslation(); const { getSetting, updateSetting, isUpdating } = useSettings(); const audioFeedbackEnabled = getSetting("audio_feedback") || false; @@ -20,8 +22,8 @@ export const AudioFeedback: React.FC = React.memo( checked={audioFeedbackEnabled} onChange={(enabled) => updateSetting("audio_feedback", enabled)} isUpdating={isUpdating("audio_feedback")} - label="Audio Feedback" - description="Play sound when recording starts and stops" + label={t("settings.sound.audioFeedback.label")} + description={t("settings.sound.audioFeedback.description")} descriptionMode={descriptionMode} grouped={grouped} /> diff --git a/src/components/settings/AutostartToggle.tsx b/src/components/settings/AutostartToggle.tsx index c71d9622..c5430d45 100644 --- a/src/components/settings/AutostartToggle.tsx +++ b/src/components/settings/AutostartToggle.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { ToggleSwitch } from "../ui/ToggleSwitch"; import { useSettings } from "../../hooks/useSettings"; @@ -9,6 +10,7 @@ interface AutostartToggleProps { export const AutostartToggle: React.FC = React.memo( ({ descriptionMode = "tooltip", grouped = false }) => { + const { t } = useTranslation(); const { getSetting, updateSetting, isUpdating } = useSettings(); const autostartEnabled = getSetting("autostart_enabled") ?? false; @@ -18,8 +20,8 @@ export const AutostartToggle: React.FC = React.memo( checked={autostartEnabled} onChange={(enabled) => updateSetting("autostart_enabled", enabled)} isUpdating={isUpdating("autostart_enabled")} - label="Launch on Startup" - description="Automatically start Handy when you log in to your computer." + label={t("settings.advanced.autostart.label")} + description={t("settings.advanced.autostart.description")} descriptionMode={descriptionMode} grouped={grouped} /> diff --git a/src/components/settings/ClamshellMicrophoneSelector.tsx b/src/components/settings/ClamshellMicrophoneSelector.tsx index a7023803..c5a11011 100644 --- a/src/components/settings/ClamshellMicrophoneSelector.tsx +++ b/src/components/settings/ClamshellMicrophoneSelector.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { commands } from "@/bindings"; import { Dropdown } from "../ui/Dropdown"; import { SettingContainer } from "../ui/SettingContainer"; @@ -12,6 +13,7 @@ interface ClamshellMicrophoneSelectorProps { export const ClamshellMicrophoneSelector: React.FC = React.memo(({ descriptionMode = "tooltip", grouped = false }) => { + const { t } = useTranslation(); const { getSetting, updateSetting, @@ -67,8 +69,8 @@ export const ClamshellMicrophoneSelector: React.FC @@ -79,8 +81,8 @@ export const ClamshellMicrophoneSelector: React.FC = React.memo(({ descriptionMode = "tooltip", grouped = false }) => { + const { t } = useTranslation(); const { getSetting, updateSetting, isUpdating } = useSettings(); + const clipboardHandlingOptions = [ + { + value: "dont_modify", + label: t("settings.advanced.clipboardHandling.options.dontModify"), + }, + { + value: "copy_to_clipboard", + label: t("settings.advanced.clipboardHandling.options.copyToClipboard"), + }, + ]; + const selectedHandling = (getSetting("clipboard_handling") || "dont_modify") as ClipboardHandling; return ( diff --git a/src/components/settings/CustomWords.tsx b/src/components/settings/CustomWords.tsx index 0f059d33..bdb9e14a 100644 --- a/src/components/settings/CustomWords.tsx +++ b/src/components/settings/CustomWords.tsx @@ -1,4 +1,5 @@ import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; import { useSettings } from "../../hooks/useSettings"; import { Input } from "../ui/Input"; import { Button } from "../ui/Button"; @@ -11,6 +12,7 @@ interface CustomWordsProps { export const CustomWords: React.FC = React.memo( ({ descriptionMode = "tooltip", grouped = false }) => { + const { t } = useTranslation(); const { getSetting, updateSetting, isUpdating } = useSettings(); const [newWord, setNewWord] = useState(""); const customWords = getSetting("custom_words") || []; @@ -46,8 +48,8 @@ export const CustomWords: React.FC = React.memo( return ( <> @@ -58,7 +60,7 @@ export const CustomWords: React.FC = React.memo( value={newWord} onChange={(e) => setNewWord(e.target.value)} onKeyDown={handleKeyPress} - placeholder="Add a word" + placeholder={t("settings.advanced.customWords.placeholder")} variant="compact" disabled={isUpdating("custom_words")} /> @@ -73,7 +75,7 @@ export const CustomWords: React.FC = React.memo( variant="primary" size="md" > - Add + {t("settings.advanced.customWords.add")}
@@ -89,7 +91,7 @@ export const CustomWords: React.FC = React.memo( variant="secondary" size="sm" className="inline-flex items-center gap-1 cursor-pointer" - aria-label={`Remove ${word}`} + aria-label={t("settings.advanced.customWords.remove", { word })} > {word} = ({ shortcutId, disabled = false, }) => { + const { t } = useTranslation(); const { getSetting, updateBinding, resetBinding, isUpdating, isLoading } = useSettings(); const [keyPressed, setKeyPressed] = useState([]); @@ -89,7 +91,7 @@ export const HandyShortcut: React.FC = ({ .catch(console.error); } catch (error) { console.error("Failed to restore original binding:", error); - toast.error("Failed to restore original shortcut"); + toast.error(t("settings.general.shortcut.errors.restore")); } } else if (editingShortcutId) { await commands.resumeBinding(editingShortcutId).catch(console.error); @@ -141,7 +143,11 @@ export const HandyShortcut: React.FC = ({ .catch(console.error); } catch (error) { console.error("Failed to change binding:", error); - toast.error(`Failed to set shortcut: ${error}`); + toast.error( + t("settings.general.shortcut.errors.set", { + error: String(error), + }), + ); // Reset to original binding on error if (originalBinding) { @@ -152,7 +158,7 @@ export const HandyShortcut: React.FC = ({ .catch(console.error); } catch (resetError) { console.error("Failed to reset binding:", resetError); - toast.error("Failed to reset shortcut to original value"); + toast.error(t("settings.general.shortcut.errors.reset")); } } } @@ -180,7 +186,7 @@ export const HandyShortcut: React.FC = ({ .catch(console.error); } catch (error) { console.error("Failed to restore original binding:", error); - toast.error("Failed to restore original shortcut"); + toast.error(t("settings.general.shortcut.errors.restore")); } } else if (editingShortcutId) { commands.resumeBinding(editingShortcutId).catch(console.error); @@ -228,7 +234,8 @@ export const HandyShortcut: React.FC = ({ // Format the current shortcut keys being recorded const formatCurrentKeys = (): string => { - if (recordedKeys.length === 0) return "Press keys..."; + if (recordedKeys.length === 0) + return t("settings.general.shortcut.pressKeys"); // Use the same formatting as the display to ensure consistency return formatKeyCombination(recordedKeys.join("+"), osType); @@ -243,12 +250,14 @@ export const HandyShortcut: React.FC = ({ if (isLoading) { return ( -
Loading shortcuts...
+
+ {t("settings.general.shortcut.loading")} +
); } @@ -257,12 +266,14 @@ export const HandyShortcut: React.FC = ({ if (Object.keys(bindings).length === 0) { return ( -
No shortcuts configured
+
+ {t("settings.general.shortcut.none")} +
); } @@ -271,20 +282,32 @@ export const HandyShortcut: React.FC = ({ if (!binding) { return ( -
No shortcut configured
+
+ {t("settings.general.shortcut.none")} +
); } + // Get translated name and description for the binding + const translatedName = t( + `settings.general.shortcut.bindings.${shortcutId}.name`, + binding.name, + ); + const translatedDescription = t( + `settings.general.shortcut.bindings.${shortcutId}.description`, + binding.description, + ); + return ( = ({ descriptionMode = "inline", grouped = false, }) => { + const { t } = useTranslation(); const { getSetting, updateSetting, isUpdating } = useSettings(); const historyLimit = Number(getSetting("history_limit") ?? "5"); @@ -25,8 +27,8 @@ export const HistoryLimit: React.FC = ({ return ( = ({ disabled={isUpdating("history_limit")} className="w-20" /> - entries + + {t("settings.debug.historyLimit.entries")} +
); diff --git a/src/components/settings/LanguageSelector.tsx b/src/components/settings/LanguageSelector.tsx index bb20ecab..a8cf3fe3 100644 --- a/src/components/settings/LanguageSelector.tsx +++ b/src/components/settings/LanguageSelector.tsx @@ -1,4 +1,5 @@ import React, { useState, useRef, useEffect, useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { listen } from "@tauri-apps/api/event"; import { SettingContainer } from "../ui/SettingContainer"; import { ResetButton } from "../ui/ResetButton"; @@ -17,6 +18,7 @@ export const LanguageSelector: React.FC = ({ descriptionMode = "tooltip", grouped = false, }) => { + const { t } = useTranslation(); const { getSetting, updateSetting, resetSetting, isUpdating } = useSettings(); const { currentModel, loadCurrentModel } = useModels(); const [isOpen, setIsOpen] = useState(false); @@ -70,9 +72,9 @@ export const LanguageSelector: React.FC = ({ ); const selectedLanguageName = isUnsupported - ? "Auto" + ? t("settings.general.language.auto") : LANGUAGES.find((lang) => lang.value === selectedLanguage)?.label || - "Auto"; + t("settings.general.language.auto"); const handleLanguageSelect = async (languageCode: string) => { await updateSetting("selected_language", languageCode); @@ -105,11 +107,11 @@ export const LanguageSelector: React.FC = ({ return ( = ({ value={searchQuery} onChange={handleSearchChange} onKeyDown={handleKeyDown} - placeholder="Search languages..." + placeholder={t("settings.general.language.searchPlaceholder")} className="w-full px-2 py-1 text-sm bg-mid-gray/10 border border-mid-gray/40 rounded focus:outline-none focus:ring-1 focus:ring-logo-primary focus:border-logo-primary" />
@@ -163,7 +165,7 @@ export const LanguageSelector: React.FC = ({
{filteredLanguages.length === 0 ? (
- No languages found + {t("settings.general.language.noResults")}
) : ( filteredLanguages.map((language) => ( diff --git a/src/components/settings/MicrophoneSelector.tsx b/src/components/settings/MicrophoneSelector.tsx index 7626d6c1..0a5f4aca 100644 --- a/src/components/settings/MicrophoneSelector.tsx +++ b/src/components/settings/MicrophoneSelector.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { Dropdown } from "../ui/Dropdown"; import { SettingContainer } from "../ui/SettingContainer"; import { ResetButton } from "../ui/ResetButton"; @@ -11,6 +12,7 @@ interface MicrophoneSelectorProps { export const MicrophoneSelector: React.FC = React.memo( ({ descriptionMode = "tooltip", grouped = false }) => { + const { t } = useTranslation(); const { getSetting, updateSetting, @@ -41,8 +43,8 @@ export const MicrophoneSelector: React.FC = React.memo( return ( @@ -53,8 +55,8 @@ export const MicrophoneSelector: React.FC = React.memo( onSelect={handleMicrophoneSelect} placeholder={ isLoading || audioDevices.length === 0 - ? "Loading..." - : "Select microphone..." + ? t("settings.sound.microphone.loading") + : t("settings.sound.microphone.placeholder") } disabled={ isUpdating("selected_microphone") || diff --git a/src/components/settings/ModelUnloadTimeout.tsx b/src/components/settings/ModelUnloadTimeout.tsx index dd992bf7..4ff9f487 100644 --- a/src/components/settings/ModelUnloadTimeout.tsx +++ b/src/components/settings/ModelUnloadTimeout.tsx @@ -1,4 +1,5 @@ import React, { useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { useSettings } from "../../hooks/useSettings"; import { commands, type ModelUnloadTimeout } from "@/bindings"; import { Dropdown } from "../ui/Dropdown"; @@ -9,27 +10,52 @@ interface ModelUnloadTimeoutProps { grouped?: boolean; } -const timeoutOptions = [ - { value: "never" as ModelUnloadTimeout, label: "Never" }, - { value: "immediately" as ModelUnloadTimeout, label: "Immediately" }, - { value: "min2" as ModelUnloadTimeout, label: "After 2 minutes" }, - { value: "min5" as ModelUnloadTimeout, label: "After 5 minutes" }, - { value: "min10" as ModelUnloadTimeout, label: "After 10 minutes" }, - { value: "min15" as ModelUnloadTimeout, label: "After 15 minutes" }, - { value: "hour1" as ModelUnloadTimeout, label: "After 1 hour" }, -]; - -const debugTimeoutOptions = [ - ...timeoutOptions, - { value: "sec5" as ModelUnloadTimeout, label: "After 5 seconds (Debug)" }, -]; - export const ModelUnloadTimeoutSetting: React.FC = ({ descriptionMode = "inline", grouped = false, }) => { + const { t } = useTranslation(); const { settings, getSetting, updateSetting } = useSettings(); + const timeoutOptions = [ + { + value: "never" as ModelUnloadTimeout, + label: t("settings.advanced.modelUnload.options.never"), + }, + { + value: "immediately" as ModelUnloadTimeout, + label: t("settings.advanced.modelUnload.options.immediately"), + }, + { + value: "min2" as ModelUnloadTimeout, + label: t("settings.advanced.modelUnload.options.min2"), + }, + { + value: "min5" as ModelUnloadTimeout, + label: t("settings.advanced.modelUnload.options.min5"), + }, + { + value: "min10" as ModelUnloadTimeout, + label: t("settings.advanced.modelUnload.options.min10"), + }, + { + value: "min15" as ModelUnloadTimeout, + label: t("settings.advanced.modelUnload.options.min15"), + }, + { + value: "hour1" as ModelUnloadTimeout, + label: t("settings.advanced.modelUnload.options.hour1"), + }, + ]; + + const debugTimeoutOptions = [ + ...timeoutOptions, + { + value: "sec5" as ModelUnloadTimeout, + label: t("settings.advanced.modelUnload.options.sec5"), + }, + ]; + const handleChange = async (event: React.ChangeEvent) => { const newTimeout = event.target.value as ModelUnloadTimeout; @@ -49,8 +75,8 @@ export const ModelUnloadTimeoutSetting: React.FC = ({ return ( diff --git a/src/components/settings/MuteWhileRecording.tsx b/src/components/settings/MuteWhileRecording.tsx index a27b01a0..b3e81542 100644 --- a/src/components/settings/MuteWhileRecording.tsx +++ b/src/components/settings/MuteWhileRecording.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { ToggleSwitch } from "../ui/ToggleSwitch"; import { useSettings } from "../../hooks/useSettings"; @@ -9,6 +10,7 @@ interface MuteWhileRecordingToggleProps { export const MuteWhileRecording: React.FC = React.memo(({ descriptionMode = "tooltip", grouped = false }) => { + const { t } = useTranslation(); const { getSetting, updateSetting, isUpdating } = useSettings(); const muteEnabled = getSetting("mute_while_recording") ?? false; @@ -18,8 +20,8 @@ export const MuteWhileRecording: React.FC = checked={muteEnabled} onChange={(enabled) => updateSetting("mute_while_recording", enabled)} isUpdating={isUpdating("mute_while_recording")} - label="Mute While Recording" - description="Automatically mute all sound output while Handy is recording, then restore it when finished." + label={t("settings.debug.muteWhileRecording.label")} + description={t("settings.debug.muteWhileRecording.description")} descriptionMode={descriptionMode} grouped={grouped} /> diff --git a/src/components/settings/OutputDeviceSelector.tsx b/src/components/settings/OutputDeviceSelector.tsx index ec45ee2a..3df632a8 100644 --- a/src/components/settings/OutputDeviceSelector.tsx +++ b/src/components/settings/OutputDeviceSelector.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { Dropdown } from "../ui/Dropdown"; import { SettingContainer } from "../ui/SettingContainer"; import { ResetButton } from "../ui/ResetButton"; @@ -14,6 +15,7 @@ interface OutputDeviceSelectorProps { export const OutputDeviceSelector: React.FC = React.memo( ({ descriptionMode = "tooltip", grouped = false, disabled = false }) => { + const { t } = useTranslation(); const { getSetting, updateSetting, @@ -44,8 +46,8 @@ export const OutputDeviceSelector: React.FC = return ( = onSelect={handleOutputDeviceSelect} placeholder={ isLoading || outputDevices.length === 0 - ? "Loading..." - : "Select output device..." + ? t("settings.sound.outputDevice.loading") + : t("settings.sound.outputDevice.placeholder") } disabled={ disabled || diff --git a/src/components/settings/PasteMethod.tsx b/src/components/settings/PasteMethod.tsx index 40da9919..383b93ac 100644 --- a/src/components/settings/PasteMethod.tsx +++ b/src/components/settings/PasteMethod.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { type as getOsType } from "@tauri-apps/plugin-os"; import { Dropdown } from "../ui/Dropdown"; import { SettingContainer } from "../ui/SettingContainer"; @@ -10,31 +11,53 @@ interface PasteMethodProps { grouped?: boolean; } -const getPasteMethodOptions = (osType: string) => { - const mod = osType === "macos" ? "Cmd" : "Ctrl"; - - const options = [ - { value: "ctrl_v", label: `Clipboard (${mod}+V)` }, - { value: "direct", label: "Direct" }, - { value: "none", label: "None" }, - ]; - - // Add Shift+Insert and Ctrl+Shift+V options for Windows and Linux only - if (osType === "windows" || osType === "linux") { - options.push( - { value: "ctrl_shift_v", label: "Clipboard (Ctrl+Shift+V)" }, - { value: "shift_insert", label: "Clipboard (Shift+Insert)" }, - ); - } - - return options; -}; - export const PasteMethodSetting: React.FC = React.memo( ({ descriptionMode = "tooltip", grouped = false }) => { + const { t } = useTranslation(); const { getSetting, updateSetting, isUpdating } = useSettings(); const [osType, setOsType] = useState("unknown"); + const getPasteMethodOptions = (osType: string) => { + const mod = osType === "macos" ? "Cmd" : "Ctrl"; + + const options = [ + { + value: "ctrl_v", + label: t("settings.advanced.pasteMethod.options.clipboard", { + modifier: mod, + }), + }, + { + value: "direct", + label: t("settings.advanced.pasteMethod.options.direct"), + }, + { + value: "none", + label: t("settings.advanced.pasteMethod.options.none"), + }, + ]; + + // Add Shift+Insert and Ctrl+Shift+V options for Windows and Linux only + if (osType === "windows" || osType === "linux") { + options.push( + { + value: "ctrl_shift_v", + label: t( + "settings.advanced.pasteMethod.options.clipboardCtrlShiftV", + ), + }, + { + value: "shift_insert", + label: t( + "settings.advanced.pasteMethod.options.clipboardShiftInsert", + ), + }, + ); + } + + return options; + }; + useEffect(() => { setOsType(getOsType()); }, []); @@ -46,8 +69,8 @@ export const PasteMethodSetting: React.FC = React.memo( return ( = React.memo(({ descriptionMode = "tooltip", grouped = false }) => { + const { t } = useTranslation(); const { getSetting, updateSetting, isUpdating } = useSettings(); const enabled = getSetting("post_process_enabled") || false; @@ -18,8 +20,8 @@ export const PostProcessingToggle: React.FC = checked={enabled} onChange={(enabled) => updateSetting("post_process_enabled", enabled)} isUpdating={isUpdating("post_process_enabled")} - label="Post Process" - description="Enable post-processing of transcribed text using language models via OpenAI Compatible API." + label={t("settings.debug.postProcessingToggle.label")} + description={t("settings.debug.postProcessingToggle.description")} descriptionMode={descriptionMode} grouped={grouped} /> diff --git a/src/components/settings/PushToTalk.tsx b/src/components/settings/PushToTalk.tsx index 947eb673..333d26d2 100644 --- a/src/components/settings/PushToTalk.tsx +++ b/src/components/settings/PushToTalk.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { ToggleSwitch } from "../ui/ToggleSwitch"; import { useSettings } from "../../hooks/useSettings"; @@ -9,6 +10,7 @@ interface PushToTalkProps { export const PushToTalk: React.FC = React.memo( ({ descriptionMode = "tooltip", grouped = false }) => { + const { t } = useTranslation(); const { getSetting, updateSetting, isUpdating } = useSettings(); const pttEnabled = getSetting("push_to_talk") || false; @@ -18,8 +20,8 @@ export const PushToTalk: React.FC = React.memo( checked={pttEnabled} onChange={(enabled) => updateSetting("push_to_talk", enabled)} isUpdating={isUpdating("push_to_talk")} - label="Push To Talk" - description="Hold to record, release to stop" + label={t("settings.general.pushToTalk.label")} + description={t("settings.general.pushToTalk.description")} descriptionMode={descriptionMode} grouped={grouped} /> diff --git a/src/components/settings/RecordingRetentionPeriod.tsx b/src/components/settings/RecordingRetentionPeriod.tsx index 819472e1..58f4de6b 100644 --- a/src/components/settings/RecordingRetentionPeriod.tsx +++ b/src/components/settings/RecordingRetentionPeriod.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { Dropdown } from "../ui/Dropdown"; import { SettingContainer } from "../ui/SettingContainer"; import { useSettings } from "../../hooks/useSettings"; @@ -11,6 +12,7 @@ interface RecordingRetentionPeriodProps { export const RecordingRetentionPeriodSelector: React.FC = React.memo(({ descriptionMode = "tooltip", grouped = false }) => { + const { t } = useTranslation(); const { getSetting, updateSetting, isUpdating } = useSettings(); const selectedRetentionPeriod = @@ -25,17 +27,25 @@ export const RecordingRetentionPeriodSelector: React.FC @@ -43,7 +53,7 @@ export const RecordingRetentionPeriodSelector: React.FC diff --git a/src/components/settings/ShowOverlay.tsx b/src/components/settings/ShowOverlay.tsx index f45776dc..526aeff8 100644 --- a/src/components/settings/ShowOverlay.tsx +++ b/src/components/settings/ShowOverlay.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { Dropdown } from "../ui/Dropdown"; import { SettingContainer } from "../ui/SettingContainer"; import { useSettings } from "../../hooks/useSettings"; @@ -9,23 +10,24 @@ interface ShowOverlayProps { grouped?: boolean; } -const overlayOptions = [ - { value: "none", label: "None" }, - { value: "bottom", label: "Bottom" }, - { value: "top", label: "Top" }, -]; - export const ShowOverlay: React.FC = React.memo( ({ descriptionMode = "tooltip", grouped = false }) => { + const { t } = useTranslation(); const { getSetting, updateSetting, isUpdating } = useSettings(); + const overlayOptions = [ + { value: "none", label: t("settings.advanced.overlay.options.none") }, + { value: "bottom", label: t("settings.advanced.overlay.options.bottom") }, + { value: "top", label: t("settings.advanced.overlay.options.top") }, + ]; + const selectedPosition = (getSetting("overlay_position") || "bottom") as OverlayPosition; return ( diff --git a/src/components/settings/StartHidden.tsx b/src/components/settings/StartHidden.tsx index 348e37bd..d06762b3 100644 --- a/src/components/settings/StartHidden.tsx +++ b/src/components/settings/StartHidden.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { ToggleSwitch } from "../ui/ToggleSwitch"; import { useSettings } from "../../hooks/useSettings"; @@ -9,6 +10,7 @@ interface StartHiddenProps { export const StartHidden: React.FC = React.memo( ({ descriptionMode = "tooltip", grouped = false }) => { + const { t } = useTranslation(); const { getSetting, updateSetting, isUpdating } = useSettings(); const startHidden = getSetting("start_hidden") ?? false; @@ -18,8 +20,8 @@ export const StartHidden: React.FC = React.memo( checked={startHidden} onChange={(enabled) => updateSetting("start_hidden", enabled)} isUpdating={isUpdating("start_hidden")} - label="Start Hidden" - description="Launch to system tray without opening the window." + label={t("settings.advanced.startHidden.label")} + description={t("settings.advanced.startHidden.description")} descriptionMode={descriptionMode} grouped={grouped} tooltipPosition="bottom" diff --git a/src/components/settings/TranslateToEnglish.tsx b/src/components/settings/TranslateToEnglish.tsx index a03abf9b..b236cf0b 100644 --- a/src/components/settings/TranslateToEnglish.tsx +++ b/src/components/settings/TranslateToEnglish.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useMemo } from "react"; +import { useTranslation } from "react-i18next"; import { listen } from "@tauri-apps/api/event"; import { ToggleSwitch } from "../ui/ToggleSwitch"; import { useSettings } from "../../hooks/useSettings"; @@ -17,6 +18,7 @@ const unsupportedTranslationModels = [ export const TranslateToEnglish: React.FC = React.memo( ({ descriptionMode = "tooltip", grouped = false }) => { + const { t } = useTranslation(); const { getSetting, updateSetting, isUpdating } = useSettings(); const { currentModel, loadCurrentModel, models } = useModels(); @@ -29,11 +31,16 @@ export const TranslateToEnglish: React.FC = React.memo( const currentModelDisplayName = models.find( (model) => model.id === currentModel, )?.name; - return `Translation is not supported by the ${currentModelDisplayName} model.`; + return t( + "settings.advanced.translateToEnglish.descriptionUnsupported", + { + model: currentModelDisplayName, + }, + ); } - return "Automatically translate speech from other languages to English during transcription."; - }, [models, currentModel, isDisabledTranslation]); + return t("settings.advanced.translateToEnglish.description"); + }, [t, models, currentModel, isDisabledTranslation]); // Listen for model state changes to update UI reactively useEffect(() => { @@ -52,7 +59,7 @@ export const TranslateToEnglish: React.FC = React.memo( onChange={(enabled) => updateSetting("translate_to_english", enabled)} isUpdating={isUpdating("translate_to_english")} disabled={isDisabledTranslation} - label="Translate to English" + label={t("settings.advanced.translateToEnglish.label")} description={description} descriptionMode={descriptionMode} grouped={grouped} diff --git a/src/components/settings/UpdateChecksToggle.tsx b/src/components/settings/UpdateChecksToggle.tsx index cf8f8ecc..675dc3a4 100644 --- a/src/components/settings/UpdateChecksToggle.tsx +++ b/src/components/settings/UpdateChecksToggle.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { ToggleSwitch } from "../ui/ToggleSwitch"; import { useSettings } from "../../hooks/useSettings"; @@ -11,6 +12,7 @@ export const UpdateChecksToggle: React.FC = ({ descriptionMode = "tooltip", grouped = false, }) => { + const { t } = useTranslation(); const { getSetting, updateSetting, isUpdating } = useSettings(); const updateChecksEnabled = getSetting("update_checks_enabled") ?? true; @@ -19,8 +21,8 @@ export const UpdateChecksToggle: React.FC = ({ checked={updateChecksEnabled} onChange={(enabled) => updateSetting("update_checks_enabled", enabled)} isUpdating={isUpdating("update_checks_enabled")} - label="Check for Updates" - description="Allow Handy to automatically check for updates and enable manual checks from the footer or tray menu." + label={t("settings.debug.updateChecks.label")} + description={t("settings.debug.updateChecks.description")} descriptionMode={descriptionMode} grouped={grouped} /> diff --git a/src/components/settings/VolumeSlider.tsx b/src/components/settings/VolumeSlider.tsx index 7a2cd3e2..362627ad 100644 --- a/src/components/settings/VolumeSlider.tsx +++ b/src/components/settings/VolumeSlider.tsx @@ -1,10 +1,12 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { Slider } from "../ui/Slider"; import { useSettings } from "../../hooks/useSettings"; export const VolumeSlider: React.FC<{ disabled?: boolean }> = ({ disabled = false, }) => { + const { t } = useTranslation(); const { getSetting, updateSetting } = useSettings(); const audioFeedbackVolume = getSetting("audio_feedback_volume") ?? 0.5; @@ -17,8 +19,8 @@ export const VolumeSlider: React.FC<{ disabled?: boolean }> = ({ min={0} max={1} step={0.1} - label="Volume" - description="Adjust the volume of audio feedback sounds" + label={t("settings.sound.volume.title")} + description={t("settings.sound.volume.description")} descriptionMode="tooltip" grouped formatValue={(value) => `${Math.round(value * 100)}%`} diff --git a/src/components/settings/about/AboutSettings.tsx b/src/components/settings/about/AboutSettings.tsx index 4fcea3c8..4246f98a 100644 --- a/src/components/settings/about/AboutSettings.tsx +++ b/src/components/settings/about/AboutSettings.tsx @@ -1,12 +1,15 @@ import React, { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; import { getVersion } from "@tauri-apps/api/app"; import { openUrl } from "@tauri-apps/plugin-opener"; import { SettingsGroup } from "../../ui/SettingsGroup"; import { SettingContainer } from "../../ui/SettingContainer"; import { Button } from "../../ui/Button"; import { AppDataDirectory } from "../AppDataDirectory"; +import { AppLanguageSelector } from "../AppLanguageSelector"; export const AboutSettings: React.FC = () => { + const { t } = useTranslation(); const [version, setVersion] = useState(""); useEffect(() => { @@ -33,20 +36,22 @@ export const AboutSettings: React.FC = () => { return (
- + + + {/* eslint-disable-next-line i18next/no-literal-string */} v{version} - +
- Handy uses Whisper.cpp for fast, local speech-to-text processing. - Thanks to the amazing work by Georgi Gerganov and contributors. + {t("settings.about.acknowledgments.whisper.details")}
diff --git a/src/components/settings/advanced/AdvancedSettings.tsx b/src/components/settings/advanced/AdvancedSettings.tsx index b1fbb9bb..7f3bd3c6 100644 --- a/src/components/settings/advanced/AdvancedSettings.tsx +++ b/src/components/settings/advanced/AdvancedSettings.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { ShowOverlay } from "../ShowOverlay"; import { TranslateToEnglish } from "../TranslateToEnglish"; import { ModelUnloadTimeoutSetting } from "../ModelUnloadTimeout"; @@ -10,9 +11,10 @@ import { PasteMethodSetting } from "../PasteMethod"; import { ClipboardHandlingSetting } from "../ClipboardHandling"; export const AdvancedSettings: React.FC = () => { + const { t } = useTranslation(); return (
- + diff --git a/src/components/settings/debug/DebugPaths.tsx b/src/components/settings/debug/DebugPaths.tsx index 52c18342..ad79e750 100644 --- a/src/components/settings/debug/DebugPaths.tsx +++ b/src/components/settings/debug/DebugPaths.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { SettingContainer } from "../../ui/SettingContainer"; interface DebugPathsProps { @@ -10,6 +11,8 @@ export const DebugPaths: React.FC = ({ descriptionMode = "inline", grouped = false, }) => { + const { t } = useTranslation(); + return ( = ({ >
- App Data:{" "} + + {t("settings.debug.paths.appData")} + {" "} + {/* eslint-disable-next-line i18next/no-literal-string */} %APPDATA%/handy
- Models:{" "} + + {t("settings.debug.paths.models")} + {" "} + {/* eslint-disable-next-line i18next/no-literal-string */} %APPDATA%/handy/models
- Settings:{" "} + + {t("settings.debug.paths.settings")} + {" "} + {/* eslint-disable-next-line i18next/no-literal-string */} %APPDATA%/handy/settings_store.json diff --git a/src/components/settings/debug/DebugSettings.tsx b/src/components/settings/debug/DebugSettings.tsx index e81a2bb8..4914010e 100644 --- a/src/components/settings/debug/DebugSettings.tsx +++ b/src/components/settings/debug/DebugSettings.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { type } from "@tauri-apps/plugin-os"; import { WordCorrectionThreshold } from "./WordCorrectionThreshold"; import { LogDirectory } from "./LogDirectory"; @@ -17,19 +18,20 @@ import { UpdateChecksToggle } from "../UpdateChecksToggle"; import { useSettings } from "../../../hooks/useSettings"; export const DebugSettings: React.FC = () => { + const { t } = useTranslation(); const { getSetting } = useSettings(); const pushToTalk = getSetting("push_to_talk"); const isLinux = type() === "linux"; return (
- + diff --git a/src/components/settings/debug/LogDirectory.tsx b/src/components/settings/debug/LogDirectory.tsx index c07f8a92..c2ccd8e3 100644 --- a/src/components/settings/debug/LogDirectory.tsx +++ b/src/components/settings/debug/LogDirectory.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { commands } from "@/bindings"; import { SettingContainer } from "../../ui/SettingContainer"; import { Button } from "../../ui/Button"; @@ -12,6 +13,7 @@ export const LogDirectory: React.FC = ({ descriptionMode = "tooltip", grouped = false, }) => { + const { t } = useTranslation(); const [logDir, setLogDir] = useState(""); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -50,8 +52,8 @@ export const LogDirectory: React.FC = ({ return ( = ({
) : error ? (
- Error loading log directory: {error} + {t("errors.loadDirectory", { error })}
) : (
@@ -76,7 +78,7 @@ export const LogDirectory: React.FC = ({ disabled={!logDir} className="px-3 py-2" > - Open + {t("common.open")}
)} diff --git a/src/components/settings/debug/LogLevelSelector.tsx b/src/components/settings/debug/LogLevelSelector.tsx index b3f3e5d2..2fdee0bd 100644 --- a/src/components/settings/debug/LogLevelSelector.tsx +++ b/src/components/settings/debug/LogLevelSelector.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { SettingContainer } from "../../ui/SettingContainer"; import { Dropdown, type DropdownOption } from "../../ui/Dropdown"; import { useSettings } from "../../../hooks/useSettings"; @@ -21,6 +22,7 @@ export const LogLevelSelector: React.FC = ({ descriptionMode = "tooltip", grouped = false, }) => { + const { t } = useTranslation(); const { settings, updateSetting, isUpdating } = useSettings(); const currentLevel = settings?.log_level ?? "debug"; @@ -36,8 +38,8 @@ export const LogLevelSelector: React.FC = ({ return ( = ({ descriptionMode = "tooltip", grouped = false }) => { + const { t } = useTranslation(); const { settings, updateSetting } = useSettings(); const handleThresholdChange = (value: number) => { @@ -22,8 +24,8 @@ export const WordCorrectionThreshold: React.FC< onChange={handleThresholdChange} min={0.0} max={1.0} - label="Word Correction Threshold" - description="Controls how aggressively custom words are applied. Lower values mean fewer corrections will be made, higher values mean more corrections. Range: 0 (least aggressive) to 1 (most aggressive)." + label={t("settings.debug.wordCorrectionThreshold.title")} + description={t("settings.debug.wordCorrectionThreshold.description")} descriptionMode={descriptionMode} grouped={grouped} /> diff --git a/src/components/settings/general/GeneralSettings.tsx b/src/components/settings/general/GeneralSettings.tsx index 57bd9ec0..b3a0ad67 100644 --- a/src/components/settings/general/GeneralSettings.tsx +++ b/src/components/settings/general/GeneralSettings.tsx @@ -1,4 +1,5 @@ import React from "react"; +import { useTranslation } from "react-i18next"; import { MicrophoneSelector } from "../MicrophoneSelector"; import { LanguageSelector } from "../LanguageSelector"; import { HandyShortcut } from "../HandyShortcut"; @@ -10,15 +11,16 @@ import { useSettings } from "../../../hooks/useSettings"; import { VolumeSlider } from "../VolumeSlider"; export const GeneralSettings: React.FC = () => { + const { t } = useTranslation(); const { audioFeedbackEnabled } = useSettings(); return (
- + - + void; + label: string; } const OpenRecordingsButton: React.FC = ({ onClick, + label, }) => ( ); export const HistorySettings: React.FC = () => { + const { t } = useTranslation(); const [historyEntries, setHistoryEntries] = useState([]); const [loading, setLoading] = useState(true); @@ -121,14 +126,17 @@ export const HistorySettings: React.FC = () => {

- History + {t("settings.history.title")}

- +
- Loading history... + {t("settings.history.loading")}
@@ -143,14 +151,17 @@ export const HistorySettings: React.FC = () => {

- History + {t("settings.history.title")}

- +
- No transcriptions yet. Start recording to build your history! + {t("settings.history.empty")}
@@ -164,10 +175,13 @@ export const HistorySettings: React.FC = () => {

- History + {t("settings.history.title")}

- +
@@ -203,6 +217,7 @@ const HistoryEntryComponent: React.FC = ({ getAudioUrl, deleteAudio, }) => { + const { t, i18n } = useTranslation(); const [audioUrl, setAudioUrl] = useState(null); const [showCopied, setShowCopied] = useState(false); @@ -229,15 +244,17 @@ const HistoryEntryComponent: React.FC = ({ } }; + const formattedDate = formatDateTime(entry.timestamp, i18n.language); + return (
-

{entry.title}

+

{formattedDate}

diff --git a/src/components/settings/post-processing/PostProcessingSettings.tsx b/src/components/settings/post-processing/PostProcessingSettings.tsx index e697266c..52ad96dc 100644 --- a/src/components/settings/post-processing/PostProcessingSettings.tsx +++ b/src/components/settings/post-processing/PostProcessingSettings.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { RefreshCcw } from "lucide-react"; import { commands } from "@/bindings"; @@ -26,13 +27,13 @@ const DisabledNotice: React.FC<{ children: React.ReactNode }> = ({ ); const PostProcessingSettingsApiComponent: React.FC = () => { + const { t } = useTranslation(); const state = usePostProcessProviderState(); if (!state.enabled) { return ( - Post processing is currently disabled. Enable it in Debug settings to - configure. + {t("settings.postProcessing.disabledNotice")} ); } @@ -40,8 +41,8 @@ const PostProcessingSettingsApiComponent: React.FC = () => { return ( <> { {state.isAppleProvider ? ( - Requires an Apple Silicon Mac running macOS Tahoe (26.0) or later. - Apple Intelligence must be enabled in System Settings. + {t("settings.postProcessing.api.appleIntelligence.requirements")} ) : ( <> { { { @@ -112,13 +118,13 @@ const PostProcessingSettingsApiComponent: React.FC = () => { )} { isLoading={state.isFetchingModels} placeholder={ state.isAppleProvider - ? "Apple Intelligence" + ? t("settings.postProcessing.api.model.placeholderApple") : state.modelOptions.length > 0 - ? "Search or select a model" - : "Type a model name" + ? t( + "settings.postProcessing.api.model.placeholderWithOptions", + ) + : t("settings.postProcessing.api.model.placeholderNoOptions") } onSelect={state.handleModelSelect} onCreate={state.handleModelCreate} @@ -145,7 +153,7 @@ const PostProcessingSettingsApiComponent: React.FC = () => { { }; const PostProcessingSettingsPromptsComponent: React.FC = () => { + const { t } = useTranslation(); const { getSetting, updateSetting, isUpdating, refreshSettings } = useSettings(); const [isCreating, setIsCreating] = useState(false); @@ -259,8 +268,7 @@ const PostProcessingSettingsPromptsComponent: React.FC = () => { if (!enabled) { return ( - Post processing is currently disabled. Enable it in Debug settings to - configure. + {t("settings.postProcessing.disabledNotice")} ); } @@ -273,8 +281,10 @@ const PostProcessingSettingsPromptsComponent: React.FC = () => { return ( { }))} onSelect={(value) => handlePromptSelect(value)} placeholder={ - prompts.length === 0 ? "No prompts available" : "Select a prompt" + prompts.length === 0 + ? t("settings.postProcessing.prompts.noPrompts") + : t("settings.postProcessing.prompts.selectPrompt") } disabled={ isUpdating("post_process_selected_prompt_id") || isCreating @@ -302,39 +314,44 @@ const PostProcessingSettingsPromptsComponent: React.FC = () => { size="md" disabled={isCreating} > - Create New Prompt + {t("settings.postProcessing.prompts.createNew")}
{!isCreating && hasPrompts && selectedPrompt && (
- + setDraftName(e.target.value)} - placeholder="Enter prompt name" + placeholder={t( + "settings.postProcessing.prompts.promptLabelPlaceholder", + )} variant="compact" />