diff --git a/.gitignore b/.gitignore index 5b16a5d..73d64bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ dist/ +src/paraglide/ *.tsbuildinfo .DS_Store .env diff --git a/README.ja.md b/README.ja.md index 43b76fa..2dea58b 100644 --- a/README.ja.md +++ b/README.ja.md @@ -13,6 +13,9 @@ pnpm workspace で、実行したい package と script をルートから選んで実行します。 使うのは既存の `package.json` scripts だけです。 +prompt や error message はシステム locale に合わせて表示されます。 +現在サポートしている locale は英語 (`en`) と日本語 (`ja`) です。 +未対応の locale は英語にフォールバックします。 ## インストール @@ -63,12 +66,12 @@ package と script が特定できれば、そのまま実行します。 ```text $ prw -? Select package +? パッケージを選択 ❯ (root) @myapp/web apps/web @myapp/api apps/api -? Select script +? スクリプトを選択 ❯ dev build test diff --git a/README.md b/README.md index f1b1f95..4be22a6 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,9 @@ English | [日本語](./README.ja.md) `prw` lets you pick a package and a script from your pnpm workspace root, then runs it. It only uses scripts already defined in `package.json`. +Prompt and error messages follow your system locale. +Currently supported locales are English (`en`) and Japanese (`ja`). +Unsupported locales fall back to English. ## Installation @@ -73,6 +76,8 @@ $ prw test ``` +With a Japanese locale, the prompts and errors are shown in Japanese. + ## Workspace Layout ```text diff --git a/messages/en.json b/messages/en.json new file mode 100644 index 0000000..653bfa1 --- /dev/null +++ b/messages/en.json @@ -0,0 +1,10 @@ +{ + "select_package": "Select package", + "select_script": "Select script", + "cancelled": "Cancelled.", + "no_packages_match": "No packages match: {query}", + "multiple_packages_match": "Multiple packages match: {query}. Be more specific.", + "no_scripts_in_package": "No scripts in {packageName}", + "workspace_root_required": "Run prw from workspace root.", + "package_not_found": "Package \"{selected}\" not found" +} diff --git a/messages/ja.json b/messages/ja.json new file mode 100644 index 0000000..d6c3923 --- /dev/null +++ b/messages/ja.json @@ -0,0 +1,10 @@ +{ + "select_package": "パッケージを選択", + "select_script": "スクリプトを選択", + "cancelled": "キャンセルしました。", + "no_packages_match": "一致するパッケージがありません: {query}", + "multiple_packages_match": "一致するパッケージが複数あります: {query}。より具体的に指定してください。", + "no_scripts_in_package": "{packageName} にスクリプトがありません", + "workspace_root_required": "ワークスペースのルートで prw を実行してください。", + "package_not_found": "パッケージ \"{selected}\" が見つかりません" +} diff --git a/package.json b/package.json index ffa143a..297d82b 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,10 @@ "prw": "./dist/bin.mjs" }, "scripts": { - "build": "tsdown", - "typecheck": "tsgo --noEmit -p tsconfig.json", - "test": "vitest", + "paraglide:compile": "paraglide-js compile --strategy globalVariable baseLocale --emit-ts-declarations", + "build": "pnpm paraglide:compile && tsdown", + "typecheck": "pnpm paraglide:compile && tsgo --noEmit -p tsconfig.json", + "test": "pnpm paraglide:compile && vitest", "knip": "knip", "prepublishOnly": "pnpm build", "changeset": "changeset", @@ -59,6 +60,7 @@ "devDependencies": { "@biomejs/biome": "2.4.0", "@changesets/cli": "2.30.0", + "@inlang/paraglide-js": "^2.15.0", "@types/js-yaml": "^4.0.9", "@types/node": "^24.12.0", "@typescript/native-preview": "7.0.0-dev.20260313.1", @@ -73,6 +75,7 @@ "@clack/prompts": "^1.1.0", "fast-glob": "^3.3.3", "js-yaml": "^4.1.1", + "os-locale": "^8.0.0", "picocolors": "^1.1.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 208d545..5309728 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: js-yaml: specifier: ^4.1.1 version: 4.1.1 + os-locale: + specifier: ^8.0.0 + version: 8.0.0 picocolors: specifier: ^1.1.1 version: 1.1.1 @@ -27,6 +30,9 @@ importers: '@changesets/cli': specifier: 2.30.0 version: 2.30.0(@types/node@24.12.0) + '@inlang/paraglide-js': + specifier: ^2.15.0 + version: 2.15.0 '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -365,6 +371,17 @@ packages: cpu: [x64] os: [win32] + '@inlang/paraglide-js@2.15.0': + resolution: {integrity: sha512-2ZOa9nssVn4tjkKskqb88KP5A7cTIjo8AiM9xnPvH+vBhRIRenO+ftAbVOHhHcHjcFxy2QFcOfBAH/Cw1LIsUg==} + hasBin: true + + '@inlang/recommend-sherlock@0.2.1': + resolution: {integrity: sha512-ckv8HvHy/iTqaVAEKrr+gnl+p3XFNwe5D2+6w6wJk2ORV2XkcRkKOJ/XsTUJbPSiyi4PI+p+T3bqbmNx/rDUlg==} + + '@inlang/sdk@2.8.0': + resolution: {integrity: sha512-w1jysvUDTMgCaONklIgOJAp9dUDl0UhLbsdqfWEwY/GIqoc9IwpuHsrP3pzC+h3DfOpkMMDnDkTpPv8kIZ98iA==} + engines: {node: '>=18.0.0'} + '@inquirer/external-editor@1.0.3': resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} engines: {node: '>=18'} @@ -377,6 +394,9 @@ packages: '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -387,6 +407,13 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@lix-js/sdk@0.4.7': + resolution: {integrity: sha512-pRbW+joG12L0ULfMiWYosIW0plmW4AsUdiPCp+Z8rAsElJ+wJ6in58zhD3UwUcd4BNcpldEGjg6PdA7e0RgsDQ==} + engines: {node: '>=18'} + + '@lix-js/server-protocol-schema@0.1.1': + resolution: {integrity: sha512-jBeALB6prAbtr5q4vTuxnRZZv1M2rKe8iNqRQhFJ4Tv7150unEa0vKyz0hs8Gl3fUGsWaNJBh3J8++fpbrpRBQ==} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -831,6 +858,13 @@ packages: cpu: [x64] os: [win32] + '@sinclair/typebox@0.31.28': + resolution: {integrity: sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==} + + '@sqlite.org/sqlite-wasm@3.48.0-build4': + resolution: {integrity: sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ==} + hasBin: true + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -923,6 +957,11 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} @@ -941,6 +980,9 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -990,10 +1032,22 @@ packages: citty@0.2.1: resolution: {integrity: sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==} + commander@11.1.0: + resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} + engines: {node: '>=16'} + commander@14.0.3: resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} + comment-json@4.6.2: + resolution: {integrity: sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==} + engines: {node: '>= 6'} + + consola@3.4.0: + resolution: {integrity: sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA==} + engines: {node: ^14.18.0 || >=16.10.0} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1007,6 +1061,14 @@ packages: supports-color: optional: true + dedent@1.5.1: + resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -1175,6 +1237,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + js-sha256@0.11.1: + resolution: {integrity: sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==} + js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} @@ -1191,6 +1256,11 @@ packages: engines: {node: '>=6'} hasBin: true + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + jsonc-parser@3.3.1: resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} @@ -1205,6 +1275,10 @@ packages: '@types/node': '>=18' typescript: '>=5.0.4 <7' + kysely@0.27.6: + resolution: {integrity: sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==} + engines: {node: '>=14.0.0'} + lefthook-darwin-arm64@2.1.2: resolution: {integrity: sha512-AgHu93YuJtj1l9bcKlCbo4Tg8N8xFl9iD6BjXCGaGMu46LSjFiXbJFlkUdpgrL8fIbwoCjJi5FNp3POpqs4Wdw==} cpu: [arm64] @@ -1315,6 +1389,10 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + os-locale@8.0.0: + resolution: {integrity: sha512-0hdyXRn0wSKMTHCo2CkPJdTKzIhg1Wb7DxJtkeD8M8rRXSwP2ZFRu4UiWBnkjPdRbL5X+AnUVXBE2SZOmkm3dg==} + engines: {node: '>=20'} + outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} @@ -1496,6 +1574,11 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sqlite-wasm-kysely@0.3.0: + resolution: {integrity: sha512-TzjBNv7KwRw6E3pdKdlRyZiTmUIE0UttT/Sl56MVwVARl/u5gp978KepazCJZewFUnlWHz9i3NQd4kOtP/Afdg==} + peerDependencies: + kysely: '*' + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -1616,6 +1699,10 @@ packages: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} + unplugin@2.3.11: + resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} + engines: {node: '>=18.12.0'} + unrun@0.2.29: resolution: {integrity: sha512-smM+bKuQiyfmKr0/3H+/MRxSoAPDjDQRMZYYWlJGDRUero4jRu0UKuYHkQXLnKDE2XUihM8Y6bWi2fI0/JwBSw==} engines: {node: '>=20.19.0'} @@ -1626,6 +1713,17 @@ packages: synckit: optional: true + urlpattern-polyfill@10.1.0: + resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==} + + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + + uuid@13.0.0: + resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} + hasBin: true + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -1703,6 +1801,9 @@ packages: resolution: {integrity: sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==} engines: {node: 20 || >=22} + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2028,6 +2129,32 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true + '@inlang/paraglide-js@2.15.0': + dependencies: + '@inlang/recommend-sherlock': 0.2.1 + '@inlang/sdk': 2.8.0 + commander: 11.1.0 + consola: 3.4.0 + json5: 2.2.3 + unplugin: 2.3.11 + urlpattern-polyfill: 10.1.0 + transitivePeerDependencies: + - babel-plugin-macros + + '@inlang/recommend-sherlock@0.2.1': + dependencies: + comment-json: 4.6.2 + + '@inlang/sdk@2.8.0': + dependencies: + '@lix-js/sdk': 0.4.7 + '@sinclair/typebox': 0.31.28 + kysely: 0.27.6 + sqlite-wasm-kysely: 0.3.0(kysely@0.27.6) + uuid: 13.0.0 + transitivePeerDependencies: + - babel-plugin-macros + '@inquirer/external-editor@1.0.3(@types/node@24.12.0)': dependencies: chardet: 2.1.1 @@ -2040,6 +2167,11 @@ snapshots: '@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/sourcemap-codec@1.5.5': {} @@ -2049,6 +2181,20 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@lix-js/sdk@0.4.7': + dependencies: + '@lix-js/server-protocol-schema': 0.1.1 + dedent: 1.5.1 + human-id: 4.1.3 + js-sha256: 0.11.1 + kysely: 0.27.6 + sqlite-wasm-kysely: 0.3.0(kysely@0.27.6) + uuid: 10.0.0 + transitivePeerDependencies: + - babel-plugin-macros + + '@lix-js/server-protocol-schema@0.1.1': {} + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.28.6 @@ -2315,6 +2461,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.59.0': optional: true + '@sinclair/typebox@0.31.28': {} + + '@sqlite.org/sqlite-wasm@3.48.0-build4': {} + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -2412,6 +2562,8 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 + acorn@8.16.0: {} + ansi-colors@4.1.3: {} ansi-regex@5.0.1: {} @@ -2424,6 +2576,8 @@ snapshots: argparse@2.0.1: {} + array-timsort@1.0.3: {} + array-union@2.1.0: {} assertion-error@2.0.1: {} @@ -2466,8 +2620,17 @@ snapshots: citty@0.2.1: {} + commander@11.1.0: {} + commander@14.0.3: {} + comment-json@4.6.2: + dependencies: + array-timsort: 1.0.3 + esprima: 4.0.1 + + consola@3.4.0: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2478,6 +2641,8 @@ snapshots: dependencies: ms: 2.1.3 + dedent@1.5.1: {} + deep-eql@5.0.2: {} deepmerge@4.3.1: {} @@ -2645,6 +2810,8 @@ snapshots: jiti@2.6.1: {} + js-sha256@0.11.1: {} + js-tokens@9.0.1: {} js-yaml@3.14.2: @@ -2658,6 +2825,8 @@ snapshots: jsesc@3.1.0: {} + json5@2.2.3: {} + jsonc-parser@3.3.1: {} jsonfile@4.0.0: @@ -2682,6 +2851,8 @@ snapshots: yaml: 2.8.2 zod: 4.3.6 + kysely@0.27.6: {} + lefthook-darwin-arm64@2.1.2: optional: true @@ -2768,6 +2939,8 @@ snapshots: obug@2.1.1: {} + os-locale@8.0.0: {} + outdent@0.5.0: {} oxc-resolver@11.19.1: @@ -2983,6 +3156,11 @@ snapshots: sprintf-js@1.0.3: {} + sqlite-wasm-kysely@0.3.0(kysely@0.27.6): + dependencies: + '@sqlite.org/sqlite-wasm': 3.48.0-build4 + kysely: 0.27.6 + stackback@0.0.2: {} std-env@3.10.0: {} @@ -3084,10 +3262,23 @@ snapshots: universalify@0.1.2: {} + unplugin@2.3.11: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.16.0 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + unrun@0.2.29: dependencies: rolldown: 1.0.0-rc.6 + urlpattern-polyfill@10.1.0: {} + + uuid@10.0.0: {} + + uuid@13.0.0: {} + vite-node@3.2.4(@types/node@24.12.0)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: cac: 6.7.14 @@ -3167,6 +3358,8 @@ snapshots: walk-up-path@4.0.0: {} + webpack-virtual-modules@0.6.2: {} + which@2.0.2: dependencies: isexe: 2.0.0 diff --git a/project.inlang/settings.json b/project.inlang/settings.json new file mode 100644 index 0000000..a0e2565 --- /dev/null +++ b/project.inlang/settings.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://inlang.com/schema/project-settings", + "baseLocale": "en", + "locales": ["en", "ja"], + "modules": [ + "https://cdn.jsdelivr.net/npm/@inlang/plugin-message-format@4/dist/index.js", + "https://cdn.jsdelivr.net/npm/@inlang/plugin-m-function-matcher@2/dist/index.js" + ], + "plugin.inlang.messageFormat": { + "pathPattern": "./messages/{locale}.json" + } +} diff --git a/src/cli.test.ts b/src/cli.test.ts index 23da31b..761f2f0 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -1,5 +1,16 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - +// TODO: UI prompt i18n should be covered from a higher-level entrypoint/E2E test +// once the current test structure stops mocking ./ui. +vi.mock("@clack/prompts", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + isCancel: vi.fn(), + log: { + ...actual.log, + message: vi.fn(), + }, + }; +}); vi.mock("./ui", async (importOriginal) => { const actual = await importOriginal(); return { @@ -16,8 +27,12 @@ vi.mock("./workspace", async (importOriginal) => { }; }); +import { isCancel } from "@clack/prompts"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { resolveScript, selectPackageByArgs } from "./cli"; import type { HistoryEntry } from "./history"; +import { m } from "./i18n"; +import { setLocale } from "./paraglide/runtime.js"; import { selectPackage, selectScript } from "./ui"; import type { Package } from "./workspace"; import { getScripts } from "./workspace"; @@ -30,12 +45,58 @@ const packages: Package[] = [ const history: HistoryEntry[] = []; +async function resetCliTestState(): Promise { + vi.clearAllMocks(); + await setLocale("en", { reload: false }); + // UI helpers are mocked in this file, so cancellation is opt-in per test. + vi.mocked(isCancel).mockReturnValue(false); + vi.spyOn(process, "exit").mockImplementation((code?: string | number) => { + throw new Error(`process.exit(${code})`); + }); +} + +async function capturePackageSelectionError(args: string[]): Promise<{ + errorSpy: ReturnType; + thrown: unknown; +}> { + const errorSpy = vi + .spyOn(console, "error") + .mockImplementation(() => undefined); + let thrown: unknown; + + try { + await selectPackageByArgs(packages, history, args); + } catch (error) { + thrown = error; + } + + return { errorSpy, thrown }; +} + +async function captureResolveScriptError( + root: string, + pkg: Package +): Promise<{ + errorSpy: ReturnType; + thrown: unknown; +}> { + const errorSpy = vi + .spyOn(console, "error") + .mockImplementation(() => undefined); + let thrown: unknown; + + try { + await resolveScript(root, pkg, undefined, []); + } catch (error) { + thrown = error; + } + + return { errorSpy, thrown }; +} + describe("selectPackageByArgs", () => { - beforeEach(() => { - vi.clearAllMocks(); - vi.spyOn(process, "exit").mockImplementation((code?: string | number) => { - throw new Error(`process.exit(${code})`); - }); + beforeEach(async () => { + await resetCliTestState(); }); describe("prw — single arg", () => { @@ -46,9 +107,14 @@ describe("selectPackageByArgs", () => { }); it("exits with code 1 when no packages match", async () => { - await expect( - selectPackageByArgs(packages, history, ["nonexistent"]) - ).rejects.toThrow("process.exit(1)"); + const { errorSpy, thrown } = await capturePackageSelectionError([ + "nonexistent", + ]); + + expect(thrown).toEqual(new Error("process.exit(1)")); + expect(errorSpy).toHaveBeenCalledWith( + m.no_packages_match({ query: "nonexistent" }) + ); }); it("calls selectPackage UI when multiple packages match", async () => { @@ -60,6 +126,21 @@ describe("selectPackageByArgs", () => { expect(selectPackage).toHaveBeenCalled(); expect(result.pkg.name).toBe("@myapp/web"); }); + + it("logs localized cancel message when package picker is cancelled", async () => { + const cancelSymbol = Symbol("cancel"); + const logSpy = vi + .spyOn(console, "log") + .mockImplementation(() => undefined); + vi.mocked(selectPackage).mockResolvedValue(cancelSymbol); + vi.mocked(isCancel).mockImplementation((value) => value === cancelSymbol); + + await expect(selectPackageByArgs(packages, history, [])).rejects.toThrow( + "process.exit(0)" + ); + + expect(logSpy).toHaveBeenCalledWith(m.cancelled()); + }); }); describe("prw