From 6ec5271302ae33e043bf42f2c5d045729ab5286c Mon Sep 17 00:00:00 2001 From: nyatinte Date: Sat, 14 Mar 2026 17:34:36 +0900 Subject: [PATCH 1/2] feat: add paraglide i18n for cli --- .gitignore | 1 + messages/en.json | 10 ++ messages/ja.json | 10 ++ package.json | 7 +- pnpm-lock.yaml | 193 +++++++++++++++++++++++++++++++++++ project.inlang/settings.json | 12 +++ src/cli.test.ts | 103 +++++++++++++++---- src/cli.ts | 13 +-- src/i18n.test.ts | 65 ++++++++++++ src/i18n.ts | 39 +++++++ src/index.ts | 2 + src/ui.ts | 9 +- src/workspace.test.ts | 5 +- src/workspace.ts | 3 +- 14 files changed, 438 insertions(+), 34 deletions(-) create mode 100644 messages/en.json create mode 100644 messages/ja.json create mode 100644 project.inlang/settings.json create mode 100644 src/i18n.test.ts create mode 100644 src/i18n.ts 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/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 119813c..e27406f 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,9 @@ "prw": "./dist/bin.mjs" }, "scripts": { - "build": "tsdown", - "test": "vitest", + "paraglide:compile": "paraglide-js compile --strategy globalVariable baseLocale --emit-ts-declarations", + "build": "pnpm paraglide:compile && tsdown", + "test": "pnpm paraglide:compile && vitest", "prepublishOnly": "pnpm build", "changeset": "changeset", "version-packages": "changeset version", @@ -47,6 +48,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", "lefthook": "^2.1.2", @@ -59,6 +61,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 c212c4b..621f1d8 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 @@ -359,6 +365,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'} @@ -371,6 +388,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'} @@ -381,6 +401,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==} @@ -717,6 +744,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==} @@ -770,6 +804,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'} @@ -788,6 +827,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'} @@ -837,10 +879,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'} @@ -854,6 +908,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'} @@ -1010,6 +1072,9 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + js-sha256@0.11.1: + resolution: {integrity: sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==} + js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} @@ -1026,12 +1091,21 @@ 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==} jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + 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] @@ -1139,6 +1213,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==} @@ -1313,6 +1391,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==} @@ -1425,6 +1508,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'} @@ -1435,6 +1522,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} @@ -1508,6 +1606,9 @@ packages: jsdom: optional: true + 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'} @@ -1825,6 +1926,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 @@ -1837,6 +1964,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': {} @@ -1846,6 +1978,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 @@ -2050,6 +2196,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 @@ -2116,6 +2266,8 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 + acorn@8.16.0: {} + ansi-colors@4.1.3: {} ansi-regex@5.0.1: {} @@ -2128,6 +2280,8 @@ snapshots: argparse@2.0.1: {} + array-timsort@1.0.3: {} + array-union@2.1.0: {} assertion-error@2.0.1: {} @@ -2170,8 +2324,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 @@ -2182,6 +2345,8 @@ snapshots: dependencies: ms: 2.1.3 + dedent@1.5.1: {} + deep-eql@5.0.2: {} deepmerge@4.3.1: {} @@ -2337,6 +2502,8 @@ snapshots: isexe@2.0.0: {} + js-sha256@0.11.1: {} + js-tokens@9.0.1: {} js-yaml@3.14.2: @@ -2350,12 +2517,16 @@ snapshots: jsesc@3.1.0: {} + json5@2.2.3: {} + jsonc-parser@3.3.1: {} jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 + kysely@0.27.6: {} + lefthook-darwin-arm64@2.1.2: optional: true @@ -2440,6 +2611,8 @@ snapshots: obug@2.1.1: {} + os-locale@8.0.0: {} + outdent@0.5.0: {} p-filter@2.1.0: @@ -2629,6 +2802,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: {} @@ -2726,10 +2904,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)(tsx@4.21.0): dependencies: cac: 6.7.14 @@ -2805,6 +2996,8 @@ snapshots: - tsx - yaml + 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..1be96e6 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -1,5 +1,18 @@ 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 { @@ -18,6 +31,9 @@ vi.mock("./workspace", async (importOriginal) => { import { resolveScript, selectPackageByArgs } from "./cli"; import type { HistoryEntry } from "./history"; +import { m } from "./i18n"; +import { isCancel } from "@clack/prompts"; +import { setLocale } from "./paraglide/runtime.js"; import { selectPackage, selectScript } from "./ui"; import type { Package } from "./workspace"; import { getScripts } from "./workspace"; @@ -30,12 +46,46 @@ const packages: Package[] = [ const history: HistoryEntry[] = []; +function resetCliTestState(): void { + vi.clearAllMocks(); + void 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 expectPackageSelectionError( + args: string[], + expectedMessage: string +): Promise { + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + await expect(selectPackageByArgs(packages, history, args)).rejects.toThrow( + "process.exit(1)" + ); + + expect(errorSpy).toHaveBeenCalledWith(expectedMessage); +} + +async function expectResolveScriptError( + root: string, + pkg: Package, + expectedMessage: string +): Promise { + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + await expect(resolveScript(root, pkg, undefined, [])).rejects.toThrow( + "process.exit(1)" + ); + + expect(errorSpy).toHaveBeenCalledWith(expectedMessage); +} + describe("selectPackageByArgs", () => { beforeEach(() => { - vi.clearAllMocks(); - vi.spyOn(process, "exit").mockImplementation((code?: string | number) => { - throw new Error(`process.exit(${code})`); - }); + resetCliTestState(); }); describe("prw — single arg", () => { @@ -46,9 +96,10 @@ describe("selectPackageByArgs", () => { }); it("exits with code 1 when no packages match", async () => { - await expect( - selectPackageByArgs(packages, history, ["nonexistent"]) - ).rejects.toThrow("process.exit(1)"); + await expectPackageSelectionError( + ["nonexistent"], + m.no_packages_match({ query: "nonexistent" }) + ); }); it("calls selectPackage UI when multiple packages match", async () => { @@ -60,6 +111,19 @@ 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(() => {}); + 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