├── example ├── src │ ├── vite-env.d.ts │ ├── main.tsx │ ├── App.tsx │ ├── components │ │ ├── Header.tsx │ │ ├── ProductCard.tsx │ │ ├── ContactForm.tsx │ │ └── Footer.tsx │ ├── index.css │ ├── assets │ │ └── react.svg │ └── App.css ├── tsconfig.json ├── vite.config.ts ├── .gitignore ├── index.html ├── eslint.config.js ├── tsconfig.node.json ├── package.json ├── tsconfig.app.json ├── public │ └── vite.svg └── package-lock.json ├── .gitignore ├── tsconfig.json ├── package.json ├── README.md └── index.ts /example/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /example/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /example/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App.tsx' 5 | 6 | createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies (bun install) 2 | node_modules 3 | 4 | # output 5 | out 6 | dist 7 | *.tgz 8 | 9 | # code coverage 10 | coverage 11 | *.lcov 12 | 13 | # logs 14 | logs 15 | _.log 16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 17 | 18 | # dotenv environment variable files 19 | .env 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | .env.local 24 | 25 | # caches 26 | .eslintcache 27 | .cache 28 | *.tsbuildinfo 29 | 30 | # IntelliJ based IDEs 31 | .idea 32 | 33 | # Finder (MacOS) folder config 34 | .DS_Store 35 | -------------------------------------------------------------------------------- /example/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | import { globalIgnores } from 'eslint/config' 7 | 8 | export default tseslint.config([ 9 | globalIgnores(['dist']), 10 | { 11 | files: ['**/*.{ts,tsx}'], 12 | extends: [ 13 | js.configs.recommended, 14 | tseslint.configs.recommended, 15 | reactHooks.configs['recommended-latest'], 16 | reactRefresh.configs.vite, 17 | ], 18 | languageOptions: { 19 | ecmaVersion: 2020, 20 | globals: globals.browser, 21 | }, 22 | }, 23 | ]) 24 | -------------------------------------------------------------------------------- /example/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2023", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "verbatimModuleSyntax": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "erasableSyntaxOnly": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedSideEffectImports": true 23 | }, 24 | "include": ["vite.config.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "react": "^19.1.0", 14 | "react-dom": "^19.1.0" 15 | }, 16 | "devDependencies": { 17 | "@eslint/js": "^9.30.1", 18 | "@types/react": "^19.1.8", 19 | "@types/react-dom": "^19.1.6", 20 | "@vitejs/plugin-react": "^4.6.0", 21 | "eslint": "^9.30.1", 22 | "eslint-plugin-react-hooks": "^5.2.0", 23 | "eslint-plugin-react-refresh": "^0.4.20", 24 | "globals": "^16.3.0", 25 | "typescript": "~5.8.3", 26 | "typescript-eslint": "^8.35.1", 27 | "vite": "^7.0.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 4 | "target": "ES2022", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "verbatimModuleSyntax": true, 14 | "moduleDetection": "force", 15 | "noEmit": true, 16 | "jsx": "react-jsx", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "erasableSyntaxOnly": true, 23 | "noFallthroughCasesInSwitch": true, 24 | "noUncheckedSideEffectImports": true 25 | }, 26 | "include": ["src"] 27 | } 28 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from "./components/Header"; 2 | import { ContactForm } from "./components/ContactForm"; 3 | import { ProductGrid } from "./components/ProductCard"; 4 | import { Footer } from "./components/Footer"; 5 | import "./App.css"; 6 | 7 | function App() { 8 | return ( 9 |
10 |
11 | 12 |
13 |
14 |
15 |

Welcome to Our Amazing Store

16 |

Discover quality products at unbeatable prices

17 | 20 |
21 |
22 | 23 | 24 | 25 |
26 | 27 |
28 |
29 | ); 30 | } 31 | 32 | export default App; 33 | -------------------------------------------------------------------------------- /example/src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export const Header: React.FC = () => { 4 | return ( 5 |
6 |
7 |

My Awesome App

8 | 32 | 33 |
34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2022"], 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "allowJs": true, 10 | "outDir": "./dist", 11 | "rootDir": "./", 12 | 13 | "strict": true, 14 | "noImplicitAny": true, 15 | "strictNullChecks": true, 16 | "strictFunctionTypes": true, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "noUncheckedIndexedAccess": true, 20 | 21 | "moduleDetection": "force", 22 | "resolveJsonModule": true, 23 | "isolatedModules": true, 24 | "verbatimModuleSyntax": false, 25 | "skipLibCheck": true, 26 | "forceConsistentCasingInFileNames": true, 27 | 28 | "declaration": true, 29 | "declarationMap": true, 30 | "sourceMap": true, 31 | "preserveConstEnums": true, 32 | "removeComments": false 33 | }, 34 | "include": [ 35 | "*.ts" 36 | ], 37 | "exclude": [ 38 | "node_modules", 39 | "dist" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "i18n-scan", 3 | "version": "1.1.1", 4 | "description": "Extract raw text from JSX for i18n", 5 | "main": "dist/index.js", 6 | "type": "module", 7 | "bin": { 8 | "i18n-scan": "dist/index.js" 9 | }, 10 | "files": [ 11 | "dist/", 12 | "README.md" 13 | ], 14 | "scripts": { 15 | "build": "tsc", 16 | "dev": "tsx index.ts", 17 | "start": "node dist/index.js", 18 | "prepublishOnly": "npm run build", 19 | "test": "npm run build && node dist/index.js example/src" 20 | }, 21 | "dependencies": { 22 | "commander": "^11.1.0", 23 | "@babel/parser": "^7.23.0", 24 | "@babel/traverse": "^7.23.0", 25 | "@babel/types": "^7.23.0" 26 | }, 27 | "devDependencies": { 28 | "@types/node": "^20.0.0", 29 | "@types/babel__traverse": "^7.20.0", 30 | "typescript": "^5.0.0", 31 | "tsx": "^4.0.0" 32 | }, 33 | "keywords": [ 34 | "i18n", 35 | "jsx", 36 | "react", 37 | "typescript", 38 | "cli", 39 | "babel" 40 | ], 41 | "author": "hexxt", 42 | "license": "MIT", 43 | "engines": { 44 | "node": ">=18.0.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /example/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color: #213547; 7 | background-color: #ffffff; 8 | 9 | font-synthesis: none; 10 | text-rendering: optimizeLegibility; 11 | -webkit-font-smoothing: antialiased; 12 | -moz-osx-font-smoothing: grayscale; 13 | } 14 | 15 | a { 16 | font-weight: 500; 17 | color: #646cff; 18 | text-decoration: inherit; 19 | } 20 | a:hover { 21 | color: #535bf2; 22 | } 23 | 24 | body { 25 | margin: 0; 26 | min-height: 100vh; 27 | } 28 | 29 | h1 { 30 | font-size: 3.2em; 31 | line-height: 1.1; 32 | } 33 | 34 | button { 35 | border-radius: 8px; 36 | border: 1px solid transparent; 37 | padding: 0.6em 1.2em; 38 | font-size: 1em; 39 | font-weight: 500; 40 | font-family: inherit; 41 | background-color: #1a1a1a; 42 | cursor: pointer; 43 | transition: border-color 0.25s; 44 | } 45 | button:hover { 46 | border-color: #646cff; 47 | } 48 | button:focus, 49 | button:focus-visible { 50 | outline: 4px auto -webkit-focus-ring-color; 51 | } 52 | 53 | @media (prefers-color-scheme: light) { 54 | :root { 55 | color: #213547; 56 | background-color: #ffffff; 57 | } 58 | a:hover { 59 | color: #747bff; 60 | } 61 | button { 62 | background-color: #f9f9f9; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /example/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # i18n-scan 2 | 3 | A CLI tool to extract raw text from JSX/TSX files for internationalization (i18n). 4 | 5 | Best paired with agentic ai tools like Cursor. 6 | 7 | ## Features 8 | 9 | - Extracts text content from JSX elements 10 | - Extracts text from specified JSX attributes (title, alt, placeholder, etc.) 11 | - Supports TypeScript and JavaScript JSX files 12 | - Configurable file extensions and attribute names 13 | - Shows file path and line numbers for each extracted text 14 | 15 | ## Installation 16 | 17 | 1. Install dependencies: 18 | 19 | ```bash 20 | npm install 21 | ``` 22 | 23 | 2. Build the project: 24 | 25 | ```bash 26 | npm run build 27 | ``` 28 | 29 | 3. (Optional) Install globally: 30 | 31 | ```bash 32 | npm install -g . 33 | ``` 34 | 35 | ## Usage 36 | 37 | ### Basic Usage 38 | 39 | ```bash 40 | # Use with npx (recommended) 41 | npx i18n-scan 42 | 43 | # If installed globally 44 | i18n-scan 45 | 46 | # Run locally during development 47 | npm run dev 48 | 49 | # Run built version locally 50 | npm start 51 | ``` 52 | 53 | ### Examples 54 | 55 | ```bash 56 | # Scan all .tsx and .jsx files in src directory 57 | npm run dev ./src 58 | 59 | # Scan with custom file extensions 60 | npm run dev ./src --ext .tsx,.jsx,.ts,.js 61 | 62 | # Scan with custom attributes 63 | npm run dev ./src --attributes title,alt,placeholder,aria-label 64 | ``` 65 | 66 | ### Options 67 | 68 | - `-e, --ext `: Comma-separated list of file extensions (default: `.tsx,.jsx`) 69 | - `-a, --attributes `: Comma-separated list of attribute names to extract (default: `title,alt,placeholder`) 70 | 71 | ## Example Output 72 | 73 | ``` 74 | [src/components/Button.tsx:5] Click me 75 | [src/components/Button.tsx:6] Submit form 76 | [src/components/Form.tsx:12] Enter your name 77 | [src/components/Form.tsx:13] This field is required 78 | ``` 79 | 80 | ## Development 81 | 82 | - `npm run dev `: Run from TypeScript source 83 | - `npm run build`: Compile TypeScript to JavaScript 84 | - `npm start `: Run compiled version 85 | 86 | ## What it extracts 87 | 88 | 1. **JSX Text Content**: Any text between JSX tags 89 | 90 | ```jsx 91 | // Extracts: "Click me" 92 |

Hello world

// Extracts: "Hello world" 93 | ``` 94 | 95 | 2. **JSX Attributes**: Text from specified attributes 96 | ```jsx 97 | // Extracts: "Enter name" 98 | Profile picture // Extracts: "Profile picture" 99 | 51 | 52 | 55 | 56 | 57 | 58 | ); 59 | }; 60 | 61 | // Example component showing multiple products 62 | export const ProductGrid: React.FC = () => { 63 | const sampleProducts: Product[] = [ 64 | { 65 | id: 1, 66 | name: "Wireless Headphones", 67 | price: 99.99, 68 | description: 69 | "High-quality wireless headphones with noise cancellation and 20-hour battery life.", 70 | image: "https://picsum.photos/300/200?random=1", 71 | inStock: true, 72 | }, 73 | { 74 | id: 2, 75 | name: "Smart Watch", 76 | price: 299.99, 77 | description: 78 | "Feature-rich smartwatch with health monitoring, GPS, and water resistance.", 79 | image: "https://picsum.photos/300/200?random=2", 80 | inStock: false, 81 | }, 82 | { 83 | id: 3, 84 | name: "Laptop Backpack", 85 | price: 49.99, 86 | description: 87 | "Durable and stylish backpack designed specifically for laptops up to 15 inches.", 88 | image: "https://picsum.photos/300/200?random=3", 89 | inStock: true, 90 | }, 91 | ]; 92 | 93 | return ( 94 |
95 |
96 |

Featured Products

97 |

Discover our best-selling items and latest arrivals

98 | 99 |
100 | {sampleProducts.map((product) => ( 101 | 102 | ))} 103 |
104 |
105 |
106 | ); 107 | }; 108 | -------------------------------------------------------------------------------- /example/src/components/ContactForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | export const ContactForm: React.FC = () => { 4 | const [formData, setFormData] = useState({ 5 | name: "", 6 | email: "", 7 | subject: "", 8 | message: "", 9 | }); 10 | 11 | return ( 12 |
13 |
14 |

Get In Touch

15 |

16 | We'd love to hear from you. Send us a message and we'll respond as 17 | soon as possible. 18 |

19 | 20 |
21 |
22 |
23 | 26 | 34 | setFormData({ ...formData, name: e.target.value }) 35 | } 36 | required 37 | /> 38 |
39 | 40 |
41 | 44 | 52 | setFormData({ ...formData, email: e.target.value }) 53 | } 54 | required 55 | /> 56 |
57 | 58 |
59 | 62 | 70 | setFormData({ ...formData, subject: e.target.value }) 71 | } 72 | /> 73 |
74 | 75 |
76 | 79 |