├── .gitignore ├── .DS_Store ├── public ├── icon.png ├── icon128.png ├── icon16.png ├── icon32.png └── icon48.png ├── pnpm-workspace.yaml ├── postcss.config.js ├── src ├── utils │ ├── cn.ts │ ├── i18n.ts │ ├── cn.test.ts │ ├── rtl.ts │ ├── useChromeLocalStorage.ts │ └── epubParser.ts ├── shared │ ├── settings.ts │ ├── messages.ts │ ├── languages.ts │ └── streaming.ts ├── popup │ ├── popup.html │ └── popup.tsx ├── sidePanel │ └── sidePanel.html ├── components │ └── ui │ │ ├── label.tsx │ │ ├── slider.tsx │ │ ├── progress.tsx │ │ ├── textarea.tsx │ │ ├── badge.tsx │ │ ├── switch.tsx │ │ ├── button.tsx │ │ ├── tabs.tsx │ │ ├── card.tsx │ │ ├── alert.tsx │ │ └── select.tsx ├── styles │ └── tailwind.css ├── manifest.json └── scripts │ └── background.ts ├── test └── setup.ts ├── vitest.config.ts ├── FUNDING.yml ├── biome.json ├── .cursor └── rules │ ├── 50-scripts-and-pnpm.mdc │ ├── 60-coding-style.mdc │ ├── 30-i18n-and-rtl.mdc │ ├── 10-typescript-and-alias.mdc │ ├── 40-extension-build-and-contracts.mdc │ ├── 20-ui-and-tailwind.mdc │ ├── 00-project-structure.mdc │ └── 00-project-structure-and-entries.mdc ├── LICENSE ├── package.json ├── .github └── workflows │ └── release-on-tag.yml ├── AGENTS.md ├── README.zh-CN.md ├── develop.md ├── rspack.config.js ├── README.md ├── _locales ├── zh_CN │ └── messages.json ├── ar │ └── messages.json ├── hi │ └── messages.json ├── vi │ └── messages.json ├── th │ └── messages.json ├── tr │ └── messages.json ├── id │ └── messages.json ├── nl │ └── messages.json ├── pt │ └── messages.json ├── es │ └── messages.json └── en │ └── messages.json ├── QWEN.md ├── CLAUDE.md └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | *.zip -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zh30/native-translate/HEAD/.DS_Store -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zh30/native-translate/HEAD/public/icon.png -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - '@tailwindcss/oxide' 3 | - esbuild 4 | -------------------------------------------------------------------------------- /public/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zh30/native-translate/HEAD/public/icon128.png -------------------------------------------------------------------------------- /public/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zh30/native-translate/HEAD/public/icon16.png -------------------------------------------------------------------------------- /public/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zh30/native-translate/HEAD/public/icon32.png -------------------------------------------------------------------------------- /public/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zh30/native-translate/HEAD/public/icon48.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | }; -------------------------------------------------------------------------------- /src/utils/cn.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]): string { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | import { cleanup } from '@testing-library/react'; 3 | import { afterEach } from 'vitest'; 4 | 5 | // Cleanup after each test case (e.g. clearing jsdom) 6 | afterEach(() => { 7 | cleanup(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/shared/settings.ts: -------------------------------------------------------------------------------- 1 | import type { LanguageCode } from '@/shared/languages'; 2 | 3 | export const POPUP_SETTINGS_KEY = 'nativeTranslate.settings' as const; 4 | 5 | export interface PopupSettings { 6 | targetLanguage: LanguageCode; 7 | hotkeyModifier?: 'alt' | 'control' | 'shift'; 8 | inputTargetLanguage?: LanguageCode; 9 | } 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Popup 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/sidePanel/sidePanel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Side Panel 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/utils/i18n.ts: -------------------------------------------------------------------------------- 1 | export function t(key: string, substitutions?: Array): string { 2 | try { 3 | // chrome.i18n.getMessage 第二个参数可为 string 或 string[] 4 | const value = chrome?.i18n?.getMessage?.( 5 | key, 6 | (substitutions ?? []) as unknown as string | string[] 7 | ); 8 | return value || key; 9 | } catch (_e) { 10 | return key; 11 | } 12 | } 13 | 14 | 15 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import path from 'path'; 3 | 4 | export default defineConfig({ 5 | test: { 6 | environment: 'jsdom', 7 | globals: true, 8 | setupFiles: ['./test/setup.ts'], 9 | include: ['src/**/*.test.{ts,tsx}', 'test/**/*.test.{ts,tsx}'], 10 | alias: { 11 | '@': path.resolve(__dirname, './src'), 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as RadixLabel from '@radix-ui/react-label'; 3 | import { cn } from '@/utils/cn'; 4 | 5 | export const Label = React.forwardRef< 6 | React.ElementRef, 7 | React.ComponentPropsWithoutRef 8 | >(({ className, ...props }, ref) => ( 9 | 14 | )); 15 | Label.displayName = RadixLabel.Root.displayName; 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface SliderProps { 4 | value: number; 5 | min?: number; 6 | max?: number; 7 | step?: number; 8 | onChange: (value: number) => void; 9 | className?: string; 10 | } 11 | 12 | export const Slider: React.FC = ({ value, min = 1, max = 12, step = 1, onChange, className }) => { 13 | return ( 14 | ) => onChange(Number(e.target.value))} 21 | className={className} 22 | /> 23 | ); 24 | }; 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/utils/cn.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { cn } from './cn'; 3 | 4 | describe('cn utility', () => { 5 | it('merges class names correctly', () => { 6 | expect(cn('c1', 'c2')).toBe('c1 c2'); 7 | }); 8 | 9 | it('handles conditional classes', () => { 10 | expect(cn('c1', true && 'c2', false && 'c3')).toBe('c1 c2'); 11 | }); 12 | 13 | it('merges tailwind classes using tailwind-merge', () => { 14 | // p-4 overrides p-2 in tailwind-merge 15 | expect(cn('p-2', 'p-4')).toBe('p-4'); 16 | }); 17 | 18 | it('handles arrays and objects', () => { 19 | expect(cn('c1', ['c2', 'c3'], { c4: true, c5: false })).toBe('c1 c2 c3 c4'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | /* 4 | The default border color has changed to `currentColor` in Tailwind CSS v4, 5 | so we've added these compatibility styles to make sure everything still 6 | looks the same as it did with Tailwind CSS v3. 7 | 8 | If we ever want to remove these styles, we need to add an explicit border 9 | color utility to any element that depends on these defaults. 10 | */ 11 | @layer base { 12 | *, 13 | ::after, 14 | ::before, 15 | ::backdrop, 16 | ::file-selector-button { 17 | border-color: var(--color-gray-200, currentColor); 18 | } 19 | } 20 | 21 | @layer base { 22 | body { 23 | direction: __MSG_ @@bidi_dir__; 24 | color-scheme: light dark; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { cn } from '@/utils/cn'; 3 | 4 | export interface ProgressProps extends React.HTMLAttributes { 5 | value?: number; 6 | } 7 | 8 | export function Progress({ value = 0, className, ...props }: ProgressProps) { 9 | const pct = Math.max(0, Math.min(100, value)); 10 | return ( 11 |
12 |
16 |
17 | ); 18 | } 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/utils/cn" 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 |