[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add i18n support (#166)

authored by

Matteo Gabriele and committed by
GitHub
04f94fed 205556c6

+2028 -515
+1 -1
.vscode/extensions.json
··· 1 1 { 2 - "recommendations": ["oxc.oxc-vscode", "Vue.volar"] 2 + "recommendations": ["oxc.oxc-vscode", "Vue.volar", "lokalise.i18n-ally"] 3 3 }
+4
.vscode/settings.json
··· 1 + { 2 + "i18n-ally.localesPaths": ["./i18n/locales"], 3 + "i18n-ally.keystyle": "nested" 4 + }
+83
CONTRIBUTING.md
··· 208 208 209 209 Ideally, extract utilities into separate files so they can be unit tested. 🙏 210 210 211 + ## Localization (i18n) 212 + 213 + npmx.dev uses [@nuxtjs/i18n](https://i18n.nuxtjs.org/) for internationalization. We aim to make the UI accessible to users in their preferred language. 214 + 215 + ### Approach 216 + 217 + - All user-facing strings should use translation keys via `$t()` in templates or `t()` in script 218 + - Translation files live in `i18n/locales/` (e.g., `en.json`) 219 + - We use the `no_prefix` strategy (no `/en/` or `/fr/` in URLs) 220 + - Locale preference is stored in cookies and respected on subsequent visits 221 + 222 + ### Adding translations 223 + 224 + 1. Add your translation key to `i18n/locales/en.json` first (English is the source of truth) 225 + 2. Use the key in your component: 226 + 227 + ```vue 228 + <template> 229 + <p>{{ $t('my.translation.key') }}</p> 230 + </template> 231 + ``` 232 + 233 + Or in script: 234 + 235 + ```typescript 236 + const { t } = useI18n() 237 + const message = t('my.translation.key') 238 + ``` 239 + 240 + 3. For dynamic values, use interpolation: 241 + 242 + ```json 243 + { "greeting": "Hello, {name}!" } 244 + ``` 245 + 246 + ```vue 247 + <p>{{ $t('greeting', { name: userName }) }}</p> 248 + ``` 249 + 250 + ### Translation key conventions 251 + 252 + - Use dot notation for hierarchy: `section.subsection.key` 253 + - Keep keys descriptive but concise 254 + - Group related keys together 255 + - Use `common.*` for shared strings (loading, retry, close, etc.) 256 + - Use component-specific prefixes: `package.card.*`, `settings.*`, `nav.*` 257 + 258 + ### Using i18n-ally (recommended) 259 + 260 + We recommend the [i18n-ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) VSCode extension for a better development experience: 261 + 262 + - Inline translation previews in your code 263 + - Auto-completion for translation keys 264 + - Missing translation detection 265 + - Easy navigation to translation files 266 + 267 + The extension is included in our workspace recommendations, so VSCode should prompt you to install it. 268 + 269 + ### Adding a new locale 270 + 271 + 1. Create a new JSON file in `i18n/locales/` (e.g., `fr.json`) 272 + 2. Add the locale to `nuxt.config.ts`: 273 + 274 + ```typescript 275 + i18n: { 276 + locales: [ 277 + { code: 'en', language: 'en-US', name: 'English', file: 'en.json' }, 278 + { code: 'fr', language: 'fr-FR', name: 'Francais', file: 'fr.json' }, 279 + ], 280 + } 281 + ``` 282 + 283 + 3. Translate all keys from `en.json` 284 + 285 + ### Formatting with locale 286 + 287 + When formatting numbers or dates that should respect the user's locale, pass the locale: 288 + 289 + ```typescript 290 + const { locale } = useI18n() 291 + const formatted = formatNumber(12345, locale.value) // "12,345" in en-US 292 + ``` 293 + 211 294 ## Testing 212 295 213 296 ### Unit tests
+6 -6
app/components/AppFooter.vue
··· 85 85 > 86 86 <div class="container py-2 sm:py-6 flex flex-col gap-1 sm:gap-3 text-fg-subtle text-sm"> 87 87 <div class="flex flex-row items-center justify-between gap-2 sm:gap-4"> 88 - <p class="font-mono m-0 hidden sm:block">a better browser for the npm registry</p> 88 + <p class="font-mono m-0 hidden sm:block">{{ $t('tagline') }}</p> 89 89 <!-- On mobile, show disclaimer here instead of tagline --> 90 - <p class="text-xs text-fg-muted m-0 sm:hidden">not affiliated with npm, Inc.</p> 90 + <p class="text-xs text-fg-muted m-0 sm:hidden">{{ $t('non_affiliation_disclaimer') }}</p> 91 91 <div class="flex items-center gap-4 sm:gap-6"> 92 92 <a 93 93 href="https://repo.npmx.dev" 94 94 rel="noopener noreferrer" 95 95 class="link-subtle font-mono text-xs min-h-11 min-w- flex items-center" 96 96 > 97 - source 97 + {{ $t('footer.source') }} 98 98 </a> 99 99 <a 100 100 href="https://social.npmx.dev" 101 101 rel="noopener noreferrer" 102 102 class="link-subtle font-mono text-xs min-h-11 min-w-11 flex items-center" 103 103 > 104 - social 104 + {{ $t('footer.social') }} 105 105 </a> 106 106 <a 107 107 href="https://chat.npmx.dev" 108 108 rel="noopener noreferrer" 109 109 class="link-subtle font-mono text-xs min-h-11 min-w-11 flex items-center" 110 110 > 111 - chat 111 + {{ $t('footer.chat') }} 112 112 </a> 113 113 </div> 114 114 </div> 115 115 <p class="text-xs text-fg-muted text-center sm:text-left m-0 hidden sm:block"> 116 - npm is a registered trademark of npm, Inc. This site is not affiliated with npm, Inc. 116 + {{ $t('trademark_disclaimer') }} 117 117 </p> 118 118 </div> 119 119 </footer>
+3 -3
app/components/AppHeader.vue
··· 21 21 <NuxtLink 22 22 v-if="showLogo" 23 23 to="/" 24 - aria-label="npmx home" 24 + :aria-label="$t('header.home')" 25 25 class="header-logo font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring rounded" 26 26 > 27 27 <span class="text-fg-subtle"><span style="letter-spacing: -0.2em">.</span>/</span>npmx ··· 38 38 class="link-subtle font-mono text-sm inline-flex items-center gap-2" 39 39 aria-keyshortcuts="/" 40 40 > 41 - search 41 + {{ $t('nav.search') }} 42 42 <kbd 43 43 class="hidden sm:inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded" 44 44 aria-hidden="true" ··· 74 74 target="_blank" 75 75 rel="noopener noreferrer" 76 76 class="link-subtle" 77 - aria-label="GitHub repository" 77 + :aria-label="$t('header.github')" 78 78 > 79 79 <span class="i-carbon-logo-github w-5 h-5" aria-hidden="true" /> 80 80 </a>
+35 -31
app/components/ClaimPackageModal.vue
··· 8 8 9 9 const open = defineModel<boolean>('open', { default: false }) 10 10 11 + const { t } = useI18n() 12 + 11 13 const { 12 14 isConnected, 13 15 state, ··· 32 34 try { 33 35 checkResult.value = await checkPackageName(props.packageName) 34 36 } catch (err) { 35 - publishError.value = err instanceof Error ? err.message : 'Failed to check name availability' 37 + publishError.value = err instanceof Error ? err.message : t('claim.modal.failed_to_check') 36 38 } finally { 37 39 isChecking.value = false 38 40 } ··· 82 84 connectorModalOpen.value = true 83 85 } 84 86 } catch (err) { 85 - publishError.value = err instanceof Error ? err.message : 'Failed to claim package' 87 + publishError.value = err instanceof Error ? err.message : t('claim.modal.failed_to_claim') 86 88 } finally { 87 89 isPublishing.value = false 88 90 } ··· 141 143 <button 142 144 type="button" 143 145 class="absolute inset-0 bg-black/60 cursor-default" 144 - aria-label="Close modal" 146 + :aria-label="$t('claim.modal.close_modal')" 145 147 @click="open = false" 146 148 /> 147 149 ··· 155 157 <div class="p-6"> 156 158 <div class="flex items-center justify-between mb-6"> 157 159 <h2 id="claim-modal-title" class="font-mono text-lg font-medium"> 158 - Claim Package Name 160 + {{ $t('claim.modal.title') }} 159 161 </h2> 160 162 <button 161 163 type="button" 162 164 class="text-fg-subtle hover:text-fg transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded" 163 - aria-label="Close" 165 + :aria-label="$t('claim.modal.close')" 164 166 @click="open = false" 165 167 > 166 168 <span class="i-carbon-close block w-5 h-5" aria-hidden="true" /> ··· 169 171 170 172 <!-- Loading state --> 171 173 <div v-if="isChecking" class="py-8 text-center"> 172 - <LoadingSpinner text="Checking availability…" /> 174 + <LoadingSpinner :text="t('claim.modal.checking')" /> 173 175 </div> 174 176 175 177 <!-- Success state --> ··· 179 181 > 180 182 <span class="i-carbon-checkmark-filled text-green-500 w-6 h-6" aria-hidden="true" /> 181 183 <div> 182 - <p class="font-mono text-sm text-fg">Package claimed!</p> 184 + <p class="font-mono text-sm text-fg">{{ $t('claim.modal.success') }}</p> 183 185 <p class="text-xs text-fg-muted"> 184 - {{ packageName }}@0.0.0 has been published to npm. 186 + {{ $t('claim.modal.success_detail', { name: packageName }) }} 185 187 </p> 186 188 </div> 187 189 </div> 188 190 189 191 <p class="text-sm text-fg-muted"> 190 - You can now publish new versions to this package using 191 - <code class="font-mono bg-bg-subtle px-1 rounded">npm publish</code>. 192 + {{ $t('claim.modal.success_hint') }} 192 193 </p> 193 194 194 195 <div class="flex gap-3"> ··· 197 198 class="flex-1 px-4 py-2 font-mono text-sm text-center text-bg bg-fg rounded-md transition-colors duration-200 hover:bg-fg/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 198 199 @click="open = false" 199 200 > 200 - View Package 201 + {{ $t('claim.modal.view_package') }} 201 202 </NuxtLink> 202 203 <button 203 204 type="button" 204 205 class="flex-1 px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 205 206 @click="open = false" 206 207 > 207 - Close 208 + {{ $t('claim.modal.close') }} 208 209 </button> 209 210 </div> 210 211 </div> ··· 222 223 class="p-3 text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-md" 223 224 role="alert" 224 225 > 225 - <p class="font-medium mb-1">Invalid package name:</p> 226 + <p class="font-medium mb-1">{{ $t('claim.modal.invalid_name') }}</p> 226 227 <ul class="list-disc list-inside space-y-1"> 227 228 <li v-for="err in checkResult.validationErrors" :key="err">{{ err }}</li> 228 229 </ul> ··· 234 235 class="p-3 text-sm text-yellow-400 bg-yellow-500/10 border border-yellow-500/20 rounded-md" 235 236 role="alert" 236 237 > 237 - <p class="font-medium mb-1">Warnings:</p> 238 + <p class="font-medium mb-1">{{ $t('common.warnings') }}</p> 238 239 <ul class="list-disc list-inside space-y-1"> 239 240 <li v-for="warn in checkResult.validationWarnings" :key="warn">{{ warn }}</li> 240 241 </ul> ··· 250 251 class="i-carbon-checkmark-filled text-green-500 w-5 h-5" 251 252 aria-hidden="true" 252 253 /> 253 - <p class="font-mono text-sm text-fg">This name is available!</p> 254 + <p class="font-mono text-sm text-fg">{{ $t('claim.modal.available') }}</p> 254 255 </div> 255 256 256 257 <div ··· 258 259 class="flex items-center gap-3 p-4 bg-red-500/10 border border-red-500/20 rounded-lg" 259 260 > 260 261 <span class="i-carbon-close-filled text-red-500 w-5 h-5" aria-hidden="true" /> 261 - <p class="font-mono text-sm text-fg">This name is already taken.</p> 262 + <p class="font-mono text-sm text-fg">{{ $t('claim.modal.taken') }}</p> 262 263 </div> 263 264 </div> 264 265 ··· 277 278 class="text-sm font-medium mb-3" 278 279 > 279 280 <span v-if="hasDangerousSimilarPackages"> 280 - Similar packages exist - npm may reject this name: 281 + {{ $t('claim.modal.similar_warning') }} 281 282 </span> 282 - <span v-else> Related packages: </span> 283 + <span v-else>{{ $t('claim.modal.related') }}</span> 283 284 </p> 284 285 <ul class="space-y-2"> 285 286 <li ··· 331 332 v-if="!isScoped" 332 333 class="p-3 text-sm text-yellow-400 bg-yellow-500/10 border border-yellow-500/20 rounded-md" 333 334 > 334 - <p class="font-medium mb-1">Consider using a scoped package instead</p> 335 + <p class="font-medium mb-1">{{ $t('claim.modal.scope_warning_title') }}</p> 335 336 <p class="text-xs text-yellow-400/80"> 336 - Unscoped package names are a shared resource. Only claim a name if you intend to 337 - publish and maintain a package. For personal or organizational projects, use a 338 - scoped name like 339 - <code class="font-mono">@{{ npmUser || 'username' }}/{{ packageName }}</code 340 - >. 337 + {{ 338 + $t('claim.modal.scope_warning_text', { 339 + username: npmUser || 'username', 340 + name: packageName, 341 + }) 342 + }} 341 343 </p> 342 344 </div> 343 345 ··· 346 348 <div 347 349 class="p-3 text-sm text-yellow-400 bg-yellow-500/10 border border-yellow-500/20 rounded-md" 348 350 > 349 - <p>Connect to the local connector to claim this package name.</p> 351 + <p>{{ $t('claim.modal.connect_required') }}</p> 350 352 </div> 351 353 <button 352 354 type="button" 353 355 class="w-full px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-colors duration-200 hover:bg-fg/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 354 356 @click="connectorModalOpen = true" 355 357 > 356 - Connect to Connector 358 + {{ $t('claim.modal.connect_button') }} 357 359 </button> 358 360 </div> 359 361 360 362 <!-- Claim button --> 361 363 <div v-else class="space-y-3"> 362 364 <p class="text-sm text-fg-muted"> 363 - This will publish a minimal placeholder package. 365 + {{ $t('claim.modal.publish_hint') }} 364 366 </p> 365 367 366 368 <!-- Expandable package.json preview --> ··· 368 370 <summary 369 371 class="px-3 py-2 text-sm text-fg-muted bg-bg-subtle cursor-pointer hover:text-fg transition-colors select-none" 370 372 > 371 - Preview package.json 373 + {{ $t('claim.modal.preview_json') }} 372 374 </summary> 373 375 <pre class="p-3 text-xs font-mono text-fg-muted bg-[#0d0d0d] overflow-x-auto">{{ 374 376 JSON.stringify(previewPackageJson, null, 2) ··· 381 383 class="w-full px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-colors duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 382 384 @click="handleClaim" 383 385 > 384 - {{ isPublishing ? 'Publishing…' : 'Claim Package Name' }} 386 + {{ 387 + isPublishing ? $t('claim.modal.publishing') : $t('claim.modal.claim_button') 388 + }} 385 389 </button> 386 390 </div> 387 391 </div> ··· 393 397 class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 394 398 @click="open = false" 395 399 > 396 - Close 400 + {{ $t('claim.modal.close') }} 397 401 </button> 398 402 </div> 399 403 ··· 410 414 class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 411 415 @click="checkAvailability" 412 416 > 413 - Retry 417 + {{ $t('claim.modal.retry') }} 414 418 </button> 415 419 </div> 416 420 </div>
+3 -3
app/components/CodeDirectoryListing.vue
··· 48 48 <div class="directory-listing"> 49 49 <!-- Empty state --> 50 50 <div v-if="currentContents.length === 0" class="py-20 text-center text-fg-muted"> 51 - <p>No files in this directory</p> 51 + <p>{{ $t('code.no_files') }}</p> 52 52 </div> 53 53 54 54 <!-- File list --> 55 55 <table v-else class="w-full"> 56 56 <thead class="sr-only"> 57 57 <tr> 58 - <th>Name</th> 59 - <th>Size</th> 58 + <th>{{ $t('code.table.name') }}</th> 59 + <th>{{ $t('code.table.size') }}</th> 60 60 </tr> 61 61 </thead> 62 62 <tbody>
+3 -3
app/components/CodeMobileTreeDrawer.vue
··· 38 38 <button 39 39 type="button" 40 40 class="md:hidden fixed bottom-4 right-4 z-40 w-12 h-12 bg-bg-elevated border border-border rounded-full shadow-lg flex items-center justify-center text-fg-muted hover:text-fg transition-colors" 41 - aria-label="Toggle file tree" 41 + :aria-label="$t('code.toggle_tree')" 42 42 @click="isOpen = !isOpen" 43 43 > 44 44 <span class="w-5 h-5" :class="isOpen ? 'i-carbon-close' : 'i-carbon-folder'" /> ··· 72 72 <div 73 73 class="sticky top-0 bg-bg-subtle border-b border-border px-4 py-3 flex items-center justify-between" 74 74 > 75 - <span class="font-mono text-sm text-fg-muted">Files</span> 75 + <span class="font-mono text-sm text-fg-muted">{{ $t('code.files_label') }}</span> 76 76 <button 77 77 type="button" 78 78 class="text-fg-muted hover:text-fg transition-colors" 79 - aria-label="Close file tree" 79 + :aria-label="$t('code.close_tree')" 80 80 @click="isOpen = false" 81 81 > 82 82 <span class="i-carbon-close w-5 h-5" />
+23 -18
app/components/ConnectorModal.vue
··· 63 63 <button 64 64 type="button" 65 65 class="absolute inset-0 bg-black/60 cursor-default" 66 - aria-label="Close modal" 66 + :aria-label="$t('connector.modal.close_modal')" 67 67 @click="open = false" 68 68 /> 69 69 ··· 78 78 <div class="p-6"> 79 79 <div class="flex items-center justify-between mb-6"> 80 80 <h2 id="connector-modal-title" class="font-mono text-lg font-medium"> 81 - Local Connector 81 + {{ $t('connector.modal.title') }} 82 82 </h2> 83 83 <button 84 84 type="button" 85 85 class="text-fg-subtle hover:text-fg transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded" 86 - aria-label="Close" 86 + :aria-label="$t('connector.modal.close')" 87 87 @click="open = false" 88 88 > 89 89 <span class="i-carbon-close block w-5 h-5" aria-hidden="true" /> ··· 95 95 <div class="flex items-center gap-3 p-4 bg-bg-subtle border border-border rounded-lg"> 96 96 <span class="w-3 h-3 rounded-full bg-green-500" aria-hidden="true" /> 97 97 <div> 98 - <p class="font-mono text-sm text-fg">Connected</p> 98 + <p class="font-mono text-sm text-fg">{{ $t('connector.modal.connected') }}</p> 99 99 <p v-if="npmUser" class="font-mono text-xs text-fg-muted"> 100 - Logged in as @{{ npmUser }} 100 + {{ $t('connector.modal.logged_in_as', { user: npmUser }) }} 101 101 </p> 102 102 </div> 103 103 </div> ··· 106 106 <OperationsQueue /> 107 107 108 108 <div v-if="!hasOperations" class="text-sm text-fg-muted"> 109 - You can now manage packages, organizations, and teams through the npmx.dev 110 - interface. 109 + {{ $t('connector.modal.connected_hint') }} 111 110 </div> 112 111 113 112 <button ··· 115 114 class="w-full px-4 py-2 font-mono text-sm text-fg-muted bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 116 115 @click="handleDisconnect" 117 116 > 118 - Disconnect 117 + {{ $t('connector.modal.disconnect') }} 119 118 </button> 120 119 </div> 121 120 122 121 <!-- Disconnected state --> 123 122 <form v-else class="space-y-4" @submit.prevent="handleConnect"> 124 123 <p class="text-sm text-fg-muted"> 125 - Run the connector on your machine to enable admin features: 124 + {{ $t('connector.modal.run_hint') }} 126 125 </p> 127 126 128 127 <div ··· 132 131 <span class="text-fg ml-2">{{ executeNpmxConnectorCommand }}</span> 133 132 <button 134 133 type="button" 135 - :aria-label="copied ? 'Copied' : 'Copy command'" 134 + :aria-label=" 135 + copied ? $t('connector.modal.copied') : $t('connector.modal.copy_command') 136 + " 136 137 class="ml-auto text-fg-subtle hover:text-fg transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded" 137 138 @click="copyCommand" 138 139 > ··· 145 146 </button> 146 147 </div> 147 148 148 - <p class="text-sm text-fg-muted">Then paste the token shown in your terminal:</p> 149 + <p class="text-sm text-fg-muted">{{ $t('connector.modal.paste_token') }}</p> 149 150 150 151 <div class="space-y-3"> 151 152 <div> ··· 153 154 for="connector-token" 154 155 class="block text-xs text-fg-subtle uppercase tracking-wider mb-1.5" 155 156 > 156 - Token 157 + {{ $t('connector.modal.token_label') }} 157 158 </label> 158 159 <input 159 160 id="connector-token" 160 161 v-model="tokenInput" 161 162 type="password" 162 163 name="connector-token" 163 - placeholder="paste token here…" 164 + :placeholder="$t('connector.modal.token_placeholder')" 164 165 autocomplete="off" 165 166 spellcheck="false" 166 167 class="w-full px-3 py-2 font-mono text-sm bg-bg-subtle border border-border rounded-md text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" ··· 171 172 <summary 172 173 class="text-fg-subtle cursor-pointer hover:text-fg-muted transition-colors duration-200" 173 174 > 174 - Advanced options 175 + {{ $t('connector.modal.advanced') }} 175 176 </summary> 176 177 <div class="mt-3"> 177 178 <label 178 179 for="connector-port" 179 180 class="block text-xs text-fg-subtle uppercase tracking-wider mb-1.5" 180 181 > 181 - Port 182 + {{ $t('connector.modal.port_label') }} 182 183 </label> 183 184 <input 184 185 id="connector-port" ··· 207 208 role="alert" 208 209 class="p-3 text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-md" 209 210 > 210 - <p class="font-mono text-sm text-fg font-bold">WARNING</p> 211 + <p class="font-mono text-sm text-fg font-bold"> 212 + {{ $t('connector.modal.warning') }} 213 + </p> 211 214 <p class="text-sm text-fg-muted"> 212 - This allows npmx to access your npm cli and any authenticated contexts. 215 + {{ $t('connector.modal.warning_text') }} 213 216 </p> 214 217 </div> 215 218 ··· 218 221 :disabled="!tokenInput.trim() || isConnecting" 219 222 class="w-full px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-all duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 focus-visible:ring-offset-2 focus-visible:ring-offset-bg" 220 223 > 221 - {{ isConnecting ? 'Connecting…' : 'Connect' }} 224 + {{ 225 + isConnecting ? $t('connector.modal.connecting') : $t('connector.modal.connect') 226 + }} 222 227 </button> 223 228 </form> 224 229 </div>
+9 -7
app/components/ConnectorStatus.client.vue
··· 12 12 const showModal = shallowRef(false) 13 13 const showTooltip = shallowRef(false) 14 14 15 + const { t } = useI18n() 16 + 15 17 const tooltipText = computed(() => { 16 - if (isConnecting.value) return 'connecting…' 17 - if (isConnected.value) return 'connected' 18 - return 'connect local CLI' 18 + if (isConnecting.value) return t('connector.status.connecting') 19 + if (isConnected.value) return t('connector.status.connected') 20 + return t('connector.status.connect_cli') 19 21 }) 20 22 21 23 const statusColor = computed(() => { ··· 29 31 30 32 const ariaLabel = computed(() => { 31 33 if (error.value) return error.value 32 - if (isConnecting.value) return 'Connecting to local connector' 33 - if (isConnected.value) return 'Connected to local connector' 34 - return 'Click to connect to local connector' 34 + if (isConnecting.value) return t('connector.status.aria_connecting') 35 + if (isConnected.value) return t('connector.status.aria_connected') 36 + return t('connector.status.aria_click_to_connect') 35 37 }) 36 38 </script> 37 39 ··· 60 62 <img 61 63 v-if="isConnected && avatar" 62 64 :src="avatar" 63 - :alt="`${npmUser}'s avatar`" 65 + :alt="t('connector.status.avatar_alt', { user: npmUser })" 64 66 width="24" 65 67 height="24" 66 68 class="w-6 h-6 rounded-full"
+11 -8
app/components/HeaderOrgsDropdown.vue
··· 3 3 username: string 4 4 }>() 5 5 6 + const { t } = useI18n() 6 7 const { listUserOrgs } = useConnector() 7 8 8 9 const isOpen = ref(false) ··· 22 23 // Already sorted alphabetically by server, take top 10 23 24 orgs.value = orgList.slice(0, 10) 24 25 } else { 25 - error.value = 'Failed to load organizations' 26 + error.value = t('header.orgs_dropdown.error') 26 27 } 27 28 hasLoaded.value = true 28 29 } catch { 29 - error.value = 'Failed to load organizations' 30 + error.value = t('header.orgs_dropdown.error') 30 31 } finally { 31 32 isLoading.value = false 32 33 } ··· 61 62 :to="`/~${username}/orgs`" 62 63 class="link-subtle font-mono text-sm inline-flex items-center gap-1" 63 64 > 64 - orgs 65 + {{ t('header.orgs') }} 65 66 <span 66 67 class="i-carbon-chevron-down w-3 h-3 transition-transform duration-200" 67 68 :class="{ 'rotate-180': isOpen }" ··· 78 79 <div v-if="isOpen" class="absolute right-0 top-full pt-2 w-56 z-50"> 79 80 <div class="bg-bg-elevated border border-border rounded-lg shadow-lg overflow-hidden"> 80 81 <div class="px-3 py-2 border-b border-border"> 81 - <span class="font-mono text-xs text-fg-subtle">Your Organizations</span> 82 + <span class="font-mono text-xs text-fg-subtle">{{ 83 + t('header.orgs_dropdown.title') 84 + }}</span> 82 85 </div> 83 86 84 87 <div v-if="isLoading" class="px-3 py-4 text-center"> 85 - <span class="text-fg-muted text-sm">Loading…</span> 88 + <span class="text-fg-muted text-sm">{{ t('header.orgs_dropdown.loading') }}</span> 86 89 </div> 87 90 88 91 <div v-else-if="error" class="px-3 py-4 text-center"> 89 - <span class="text-fg-muted text-sm">{{ error }}</span> 92 + <span class="text-fg-muted text-sm">{{ t('header.orgs_dropdown.error') }}</span> 90 93 </div> 91 94 92 95 <ul v-else-if="orgs.length > 0" class="py-1 max-h-80 overflow-y-auto"> ··· 101 104 </ul> 102 105 103 106 <div v-else class="px-3 py-4 text-center"> 104 - <span class="text-fg-muted text-sm">No organizations found</span> 107 + <span class="text-fg-muted text-sm">{{ t('header.orgs_dropdown.empty') }}</span> 105 108 </div> 106 109 107 110 <div class="px-3 py-2 border-t border-border"> ··· 109 112 :to="`/~${username}/orgs`" 110 113 class="link-subtle font-mono text-xs inline-flex items-center gap-1" 111 114 > 112 - View all 115 + {{ t('header.orgs_dropdown.view_all') }} 113 116 <span class="i-carbon-arrow-right w-3 h-3" aria-hidden="true" /> 114 117 </NuxtLink> 115 118 </div>
+11 -8
app/components/HeaderPackagesDropdown.vue
··· 3 3 username: string 4 4 }>() 5 5 6 + const { t } = useI18n() 6 7 const { listUserPackages } = useConnector() 7 8 8 9 const isOpen = ref(false) ··· 22 23 // Sort alphabetically and take top 10 23 24 packages.value = Object.keys(pkgMap).sort().slice(0, 10) 24 25 } else { 25 - error.value = 'Failed to load packages' 26 + error.value = t('header.packages_dropdown.error') 26 27 } 27 28 hasLoaded.value = true 28 29 } catch { 29 - error.value = 'Failed to load packages' 30 + error.value = t('header.packages_dropdown.error') 30 31 } finally { 31 32 isLoading.value = false 32 33 } ··· 61 62 :to="`/~${username}`" 62 63 class="link-subtle font-mono text-sm inline-flex items-center gap-1" 63 64 > 64 - packages 65 + {{ t('header.packages') }} 65 66 <span 66 67 class="i-carbon-chevron-down w-3 h-3 transition-transform duration-200" 67 68 :class="{ 'rotate-180': isOpen }" ··· 78 79 <div v-if="isOpen" class="absolute right-0 top-full pt-2 w-64 z-50"> 79 80 <div class="bg-bg-elevated border border-border rounded-lg shadow-lg overflow-hidden"> 80 81 <div class="px-3 py-2 border-b border-border"> 81 - <span class="font-mono text-xs text-fg-subtle">Your Packages</span> 82 + <span class="font-mono text-xs text-fg-subtle">{{ 83 + t('header.packages_dropdown.title') 84 + }}</span> 82 85 </div> 83 86 84 87 <div v-if="isLoading" class="px-3 py-4 text-center"> 85 - <span class="text-fg-muted text-sm">Loading…</span> 88 + <span class="text-fg-muted text-sm">{{ t('header.packages_dropdown.loading') }}</span> 86 89 </div> 87 90 88 91 <div v-else-if="error" class="px-3 py-4 text-center"> 89 - <span class="text-fg-muted text-sm">{{ error }}</span> 92 + <span class="text-fg-muted text-sm">{{ t('header.packages_dropdown.error') }}</span> 90 93 </div> 91 94 92 95 <ul v-else-if="packages.length > 0" class="py-1 max-h-80 overflow-y-auto"> ··· 101 104 </ul> 102 105 103 106 <div v-else class="px-3 py-4 text-center"> 104 - <span class="text-fg-muted text-sm">No packages found</span> 107 + <span class="text-fg-muted text-sm">{{ t('header.packages_dropdown.empty') }}</span> 105 108 </div> 106 109 107 110 <div class="px-3 py-2 border-t border-border"> ··· 109 112 :to="`/~${username}`" 110 113 class="link-subtle font-mono text-xs inline-flex items-center gap-1" 111 114 > 112 - View all 115 + {{ t('header.packages_dropdown.view_all') }} 113 116 <span class="i-carbon-arrow-right w-3 h-3" aria-hidden="true" /> 114 117 </NuxtLink> 115 118 </div>
+2 -2
app/components/JsrBadge.vue
··· 13 13 target="_blank" 14 14 rel="noopener noreferrer" 15 15 class="inline-flex items-center gap-1 text-xs font-mono text-fg-muted hover:text-fg transition-colors duration-200" 16 - title="also available on JSR" 16 + :title="$t('badges.jsr.title')" 17 17 > 18 18 <span 19 19 class="i-simple-icons-jsr shrink-0" 20 20 :class="compact ? 'w-3.5 h-3.5' : 'w-4 h-4'" 21 21 aria-hidden="true" 22 22 /> 23 - <span v-if="!compact" class="sr-only sm:not-sr-only">jsr</span> 23 + <span v-if="!compact" class="sr-only sm:not-sr-only">{{ $t('badges.jsr.label') }}</span> 24 24 </a> 25 25 </template>
+1 -1
app/components/LicenseDisplay.vue
··· 19 19 target="_blank" 20 20 rel="noopener noreferrer" 21 21 class="link-subtle" 22 - title="View license text on SPDX" 22 + :title="$t('package.license.view_spdx')" 23 23 > 24 24 {{ token.value }} 25 25 </a>
+3 -1
app/components/LoadingSpinner.vue
··· 3 3 /** Text to display next to the spinner */ 4 4 text?: string 5 5 }>() 6 + 7 + const { t } = useI18n() 6 8 </script> 7 9 8 10 <template> 9 11 <div aria-busy="true" class="flex items-center gap-3 text-fg-muted font-mono text-sm py-8"> 10 12 <span class="w-4 h-4 border-2 border-fg-subtle border-t-fg rounded-full animate-spin" /> 11 - {{ text ?? 'Loading...' }} 13 + {{ text ?? t('common.loading') }} 12 14 </div> 13 15 </template>
+29 -19
app/components/OperationsQueue.vue
··· 125 125 <!-- Header --> 126 126 <div class="flex items-center justify-between"> 127 127 <h3 class="font-mono text-sm font-medium text-fg"> 128 - Operations Queue 128 + {{ $t('operations.queue.title') }} 129 129 <span v-if="hasActiveOperations" class="text-fg-muted" 130 130 >({{ activeOperations.length }})</span 131 131 > ··· 135 135 v-if="hasOperations" 136 136 type="button" 137 137 class="px-2 py-1 font-mono text-xs text-fg-muted hover:text-fg bg-bg-subtle border border-border rounded transition-colors duration-200 hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 138 - aria-label="Clear all operations" 138 + :aria-label="$t('operations.queue.clear_all')" 139 139 @click="handleClearAll" 140 140 > 141 - clear all 141 + {{ $t('operations.queue.clear_all') }} 142 142 </button> 143 143 <button 144 144 type="button" 145 145 class="p-1 text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 146 - aria-label="Refresh operations" 146 + :aria-label="$t('operations.queue.refresh')" 147 147 @click="refreshState" 148 148 > 149 149 <span class="i-carbon-renew block w-4 h-4" aria-hidden="true" /> ··· 153 153 154 154 <!-- Empty state --> 155 155 <div v-if="!hasActiveOperations && !hasCompletedOperations" class="py-8 text-center"> 156 - <p class="font-mono text-sm text-fg-subtle">No operations queued</p> 157 - <p class="font-mono text-xs text-fg-subtle mt-1">Add operations from package or org pages</p> 156 + <p class="font-mono text-sm text-fg-subtle">{{ $t('operations.queue.empty') }}</p> 157 + <p class="font-mono text-xs text-fg-subtle mt-1">{{ $t('operations.queue.empty_hint') }}</p> 158 158 </div> 159 159 160 160 <!-- Active operations list --> 161 - <ul v-if="hasActiveOperations" class="space-y-2" aria-label="Active operations"> 161 + <ul 162 + v-if="hasActiveOperations" 163 + class="space-y-2" 164 + :aria-label="$t('operations.queue.active_label')" 165 + > 162 166 <li 163 167 v-for="op in activeOperations" 164 168 :key="op.id" ··· 189 193 v-if="op.result?.requiresOtp && op.status === 'failed'" 190 194 class="mt-1 text-xs text-amber-400" 191 195 > 192 - OTP required 196 + {{ $t('operations.queue.otp_required') }} 193 197 </p> 194 198 <!-- Result output for completed/failed --> 195 199 <div ··· 211 215 v-if="op.status === 'pending'" 212 216 type="button" 213 217 class="p-1 text-fg-muted hover:text-green-400 transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 214 - aria-label="Approve operation" 218 + :aria-label="$t('operations.queue.approve_operation')" 215 219 @click="approveOperation(op.id)" 216 220 > 217 221 <span class="i-carbon-checkmark block w-4 h-4" aria-hidden="true" /> ··· 220 224 v-if="op.status !== 'running'" 221 225 type="button" 222 226 class="p-1 text-fg-muted hover:text-red-400 transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 223 - aria-label="Remove operation" 227 + :aria-label="$t('operations.queue.remove_operation')" 224 228 @click="removeOperation(op.id)" 225 229 > 226 230 <span class="i-carbon-close block w-4 h-4" aria-hidden="true" /> ··· 237 241 > 238 242 <div class="flex items-center gap-2 mb-2"> 239 243 <span class="i-carbon-locked block w-4 h-4 text-amber-400 shrink-0" aria-hidden="true" /> 240 - <span class="font-mono text-sm text-amber-400"> Enter OTP to continue </span> 244 + <span class="font-mono text-sm text-amber-400"> 245 + {{ $t('operations.queue.otp_prompt') }} 246 + </span> 241 247 </div> 242 248 <form class="flex items-center gap-2" @submit.prevent="handleRetryWithOtp"> 243 - <label for="otp-input" class="sr-only">One-time password</label> 249 + <label for="otp-input" class="sr-only">{{ $t('operations.queue.otp_label') }}</label> 244 250 <input 245 251 id="otp-input" 246 252 v-model="otpInput" ··· 248 254 name="otp-code" 249 255 inputmode="numeric" 250 256 pattern="[0-9]*" 251 - placeholder="Enter OTP code…" 257 + :placeholder="$t('operations.queue.otp_placeholder')" 252 258 autocomplete="one-time-code" 253 259 spellcheck="false" 254 260 class="flex-1 px-3 py-1.5 font-mono text-sm bg-bg border border-border rounded text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" ··· 258 264 :disabled="!otpInput.trim() || isExecuting" 259 265 class="px-3 py-1.5 font-mono text-xs text-bg bg-amber-500 rounded transition-all duration-200 hover:bg-amber-400 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/50" 260 266 > 261 - {{ isExecuting ? 'Retrying…' : 'Retry with OTP' }} 267 + {{ isExecuting ? $t('operations.queue.retrying') : $t('operations.queue.retry_otp') }} 262 268 </button> 263 269 </form> 264 270 </div> ··· 271 277 class="flex-1 px-4 py-2 font-mono text-sm text-fg bg-bg-subtle border border-border rounded-md transition-colors duration-200 hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 272 278 @click="handleApproveAll" 273 279 > 274 - Approve All ({{ pendingOperations.length }}) 280 + {{ $t('operations.queue.approve_all') }} ({{ pendingOperations.length }}) 275 281 </button> 276 282 <button 277 283 v-if="hasApprovedOperations && !hasOtpFailures" ··· 280 286 class="flex-1 px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-all duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 focus-visible:ring-offset-2 focus-visible:ring-offset-bg" 281 287 @click="handleExecute()" 282 288 > 283 - {{ isExecuting ? 'Executing…' : `Execute (${approvedOperations.length})` }} 289 + {{ 290 + isExecuting 291 + ? $t('operations.queue.executing') 292 + : `${$t('operations.queue.execute')} (${approvedOperations.length})` 293 + }} 284 294 </button> 285 295 </div> 286 296 ··· 293 303 class="i-carbon-chevron-right block w-3 h-3 transition-transform duration-200 [[open]>&]:rotate-90" 294 304 aria-hidden="true" 295 305 /> 296 - Log ({{ completedOperations.length }}) 306 + {{ $t('operations.queue.log') }} ({{ completedOperations.length }}) 297 307 </summary> 298 - <ul class="mt-2 space-y-1" aria-label="Completed operations log"> 308 + <ul class="mt-2 space-y-1" :aria-label="$t('operations.queue.log_label')"> 299 309 <li 300 310 v-for="op in completedOperations" 301 311 :key="op.id" ··· 323 333 <button 324 334 type="button" 325 335 class="p-0.5 text-fg-subtle hover:text-fg-muted transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 326 - aria-label="Remove from log" 336 + :aria-label="$t('operations.queue.remove_from_log')" 327 337 @click="removeOperation(op.id)" 328 338 > 329 339 <span class="i-carbon-close block w-3 h-3" aria-hidden="true" />
+47 -35
app/components/OrgMembersPanel.vue
··· 10 10 'select-team': [teamName: string] 11 11 }>() 12 12 13 + const { t } = useI18n() 14 + 13 15 const { 14 16 isConnected, 15 17 lastExecutionTime, ··· 295 297 <div class="flex items-center justify-between p-4 border-b border-border"> 296 298 <h2 id="members-heading" class="font-mono text-sm font-medium flex items-center gap-2"> 297 299 <span class="i-carbon-user-multiple w-4 h-4 text-fg-muted" aria-hidden="true" /> 298 - Members 300 + {{ $t('org.members.title') }} 299 301 <span v-if="memberList.length > 0" class="text-fg-muted">({{ memberList.length }})</span> 300 302 </h2> 301 303 <button 302 304 type="button" 303 305 class="p-1.5 text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 304 - aria-label="Refresh members" 306 + :aria-label="$t('org.members.refresh')" 305 307 :disabled="isLoading" 306 308 @click="refreshData" 307 309 > ··· 320 322 class="absolute left-2 top-1/2 -translate-y-1/2 i-carbon-search w-3.5 h-3.5 text-fg-subtle" 321 323 aria-hidden="true" 322 324 /> 323 - <label for="members-search" class="sr-only">Filter members</label> 325 + <label for="members-search" class="sr-only">{{ $t('org.members.filter_label') }}</label> 324 326 <input 325 327 id="members-search" 326 328 v-model="searchQuery" 327 329 type="search" 328 330 name="members-search" 329 - placeholder="Filter members…" 331 + :placeholder="$t('org.members.filter_placeholder')" 330 332 autocomplete="off" 331 333 class="w-full pl-7 pr-2 py-1.5 font-mono text-sm bg-bg-subtle border border-border rounded text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 332 334 /> 333 335 </div> 334 - <div class="flex items-center gap-1" role="group" aria-label="Filter by role"> 336 + <div 337 + class="flex items-center gap-1" 338 + role="group" 339 + :aria-label="$t('org.members.filter_by_role')" 340 + > 335 341 <button 336 342 v-for="role in ['all', 'owner', 'admin', 'developer'] as const" 337 343 :key="role" ··· 341 347 :aria-pressed="filterRole === role" 342 348 @click="filterRole = role" 343 349 > 344 - {{ role }} 350 + {{ t(`org.members.role.${role}`) }} 345 351 <span v-if="role !== 'all'" class="text-fg-subtle">({{ roleCounts[role] }})</span> 346 352 </button> 347 353 </div> 348 354 <!-- Team filter --> 349 355 <div v-if="teamNames.length > 0"> 350 - <label for="team-filter" class="sr-only">Filter by team</label> 356 + <label for="team-filter" class="sr-only">{{ $t('org.members.filter_by_team') }}</label> 351 357 <select 352 358 id="team-filter" 353 359 v-model="filterTeam" 354 360 name="team-filter" 355 361 class="px-2 py-1 font-mono text-xs bg-bg-subtle border border-border rounded text-fg transition-colors duration-200 focus:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 356 362 > 357 - <option :value="null">all teams</option> 363 + <option :value="null">{{ $t('org.members.all_teams') }}</option> 358 364 <option v-for="team in teamNames" :key="team" :value="team"> 359 365 {{ team }} 360 366 </option> 361 367 </select> 362 368 </div> 363 - <div class="flex items-center gap-1 text-xs" role="group" aria-label="Sort by"> 369 + <div 370 + class="flex items-center gap-1 text-xs" 371 + role="group" 372 + :aria-label="$t('org.members.sort_by')" 373 + > 364 374 <button 365 375 type="button" 366 376 class="px-2 py-1 font-mono rounded transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" ··· 368 378 :aria-pressed="sortBy === 'name'" 369 379 @click="toggleSort('name')" 370 380 > 371 - name 381 + {{ $t('common.sort.name') }} 372 382 <span v-if="sortBy === 'name'">{{ sortOrder === 'asc' ? '↑' : '↓' }}</span> 373 383 </button> 374 384 <button ··· 378 388 :aria-pressed="sortBy === 'role'" 379 389 @click="toggleSort('role')" 380 390 > 381 - role 391 + {{ $t('common.sort.role') }} 382 392 <span v-if="sortBy === 'role'">{{ sortOrder === 'asc' ? '↑' : '↓' }}</span> 383 393 </button> 384 394 </div> ··· 390 400 class="i-carbon-rotate-180 block w-5 h-5 text-fg-muted animate-spin mx-auto" 391 401 aria-hidden="true" 392 402 /> 393 - <p class="font-mono text-sm text-fg-muted mt-2">Loading members…</p> 403 + <p class="font-mono text-sm text-fg-muted mt-2">{{ $t('org.members.loading') }}</p> 394 404 </div> 395 405 396 406 <!-- Error state --> ··· 403 413 class="mt-2 font-mono text-xs text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 404 414 @click="loadMembers" 405 415 > 406 - Try again 416 + {{ $t('common.try_again') }} 407 417 </button> 408 418 </div> 409 419 410 420 <!-- Empty state --> 411 421 <div v-else-if="memberList.length === 0" class="p-8 text-center"> 412 - <p class="font-mono text-sm text-fg-muted">No members found</p> 422 + <p class="font-mono text-sm text-fg-muted">{{ $t('org.members.no_members') }}</p> 413 423 </div> 414 424 415 425 <!-- Members list --> 416 426 <ul 417 427 v-else 418 428 class="divide-y divide-border max-h-[400px] overflow-y-auto" 419 - aria-label="Organization members" 429 + :aria-label="$t('org.members.list_label')" 420 430 > 421 431 <li 422 432 v-for="member in filteredMembers" ··· 440 450 </div> 441 451 <div class="flex items-center gap-1"> 442 452 <!-- Role selector --> 443 - <label :for="`role-${member.name}`" class="sr-only" 444 - >Change role for {{ member.name }}</label 445 - > 453 + <label :for="`role-${member.name}`" class="sr-only">{{ 454 + $t('org.members.change_role_for', { name: member.name }) 455 + }}</label> 446 456 <select 447 457 :id="`role-${member.name}`" 448 458 :value="member.role" ··· 455 465 ) 456 466 " 457 467 > 458 - <option value="developer">developer</option> 459 - <option value="admin">admin</option> 460 - <option value="owner">owner</option> 468 + <option value="developer">{{ t('org.members.role.developer') }}</option> 469 + <option value="admin">{{ t('org.members.role.admin') }}</option> 470 + <option value="owner">{{ t('org.members.role.owner') }}</option> 461 471 </select> 462 472 <!-- Remove button --> 463 473 <button 464 474 type="button" 465 475 class="p-1 text-fg-subtle hover:text-red-400 transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 466 - :aria-label="`Remove ${member.name} from org`" 476 + :aria-label="$t('org.members.remove_from_org', { name: member.name })" 467 477 @click="handleRemoveMember(member.name)" 468 478 > 469 479 <span class="i-carbon-close block w-4 h-4" aria-hidden="true" /> ··· 477 487 :key="team" 478 488 type="button" 479 489 class="inline-flex items-center gap-1 px-1.5 py-0.5 font-mono text-xs text-fg-muted border border-border rounded hover:text-fg hover:border-border-hover transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 480 - :aria-label="`View ${team} team`" 490 + :aria-label="$t('org.members.view_team', { team })" 481 491 @click="handleTeamClick(team)" 482 492 > 483 493 {{ team }} ··· 488 498 489 499 <!-- No results --> 490 500 <div v-if="memberList.length > 0 && filteredMembers.length === 0" class="p-4 text-center"> 491 - <p class="font-mono text-sm text-fg-muted">No members match your filters</p> 501 + <p class="font-mono text-sm text-fg-muted">{{ $t('org.members.no_match') }}</p> 492 502 </div> 493 503 494 504 <!-- Add member --> 495 505 <div class="p-3 border-t border-border"> 496 506 <div v-if="showAddMember"> 497 507 <form class="space-y-2" @submit.prevent="handleAddMember"> 498 - <label for="new-member-username" class="sr-only">Username</label> 508 + <label for="new-member-username" class="sr-only">{{ 509 + $t('org.members.username_label') 510 + }}</label> 499 511 <input 500 512 id="new-member-username" 501 513 v-model="newUsername" 502 514 type="text" 503 515 name="new-member-username" 504 - placeholder="username…" 516 + :placeholder="$t('org.members.username_placeholder')" 505 517 autocomplete="off" 506 518 spellcheck="false" 507 519 class="w-full px-2 py-1.5 font-mono text-sm bg-bg border border-border rounded text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 508 520 /> 509 521 <div class="flex items-center gap-2"> 510 - <label for="new-member-role" class="sr-only">Role</label> 522 + <label for="new-member-role" class="sr-only">{{ $t('org.members.role_label') }}</label> 511 523 <select 512 524 id="new-member-role" 513 525 v-model="newRole" 514 526 name="new-member-role" 515 527 class="flex-1 px-2 py-1.5 font-mono text-sm bg-bg border border-border rounded text-fg transition-colors duration-200 focus:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 516 528 > 517 - <option value="developer">developer</option> 518 - <option value="admin">admin</option> 519 - <option value="owner">owner</option> 529 + <option value="developer">{{ t('org.members.role.developer') }}</option> 530 + <option value="admin">{{ t('org.members.role.admin') }}</option> 531 + <option value="owner">{{ t('org.members.role.owner') }}</option> 520 532 </select> 521 533 <!-- Team selection --> 522 - <label for="new-member-team" class="sr-only">Team</label> 534 + <label for="new-member-team" class="sr-only">{{ $t('org.members.team_label') }}</label> 523 535 <select 524 536 id="new-member-team" 525 537 v-model="newTeam" 526 538 name="new-member-team" 527 539 class="flex-1 px-2 py-1.5 font-mono text-sm bg-bg border border-border rounded text-fg transition-colors duration-200 focus:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 528 540 > 529 - <option value="">no team</option> 541 + <option value="">{{ $t('org.members.no_team') }}</option> 530 542 <option v-for="team in teamNames" :key="team" :value="team"> 531 543 {{ team }} 532 544 </option> ··· 536 548 :disabled="!newUsername.trim() || isAddingMember" 537 549 class="px-3 py-1.5 font-mono text-xs text-bg bg-fg rounded transition-all duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 538 550 > 539 - {{ isAddingMember ? '…' : 'add' }} 551 + {{ isAddingMember ? '…' : $t('org.members.add_button') }} 540 552 </button> 541 553 <button 542 554 type="button" 543 555 class="p-1.5 text-fg-subtle hover:text-fg transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 544 - aria-label="Cancel adding member" 556 + :aria-label="$t('org.members.cancel_add')" 545 557 @click="showAddMember = false" 546 558 > 547 559 <span class="i-carbon-close block w-4 h-4" aria-hidden="true" /> ··· 555 567 class="w-full px-3 py-2 font-mono text-sm text-fg-muted bg-bg border border-border rounded transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 556 568 @click="showAddMember = true" 557 569 > 558 - + Add member 570 + {{ $t('org.members.add_member') }} 559 571 </button> 560 572 </div> 561 573 </section>
+40 -30
app/components/OrgTeamsPanel.vue
··· 264 264 <div class="flex items-center justify-between p-4 border-b border-border"> 265 265 <h2 id="teams-heading" class="font-mono text-sm font-medium flex items-center gap-2"> 266 266 <span class="i-carbon-group w-4 h-4 text-fg-muted" aria-hidden="true" /> 267 - Teams 267 + {{ $t('org.teams.title') }} 268 268 <span v-if="teams.length > 0" class="text-fg-muted">({{ teams.length }})</span> 269 269 </h2> 270 270 <button 271 271 type="button" 272 272 class="p-1.5 text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 273 - aria-label="Refresh teams" 273 + :aria-label="$t('org.teams.refresh')" 274 274 :disabled="isLoadingTeams" 275 275 @click="loadTeams" 276 276 > ··· 289 289 class="absolute left-2 top-1/2 -translate-y-1/2 i-carbon-search w-3.5 h-3.5 text-fg-subtle" 290 290 aria-hidden="true" 291 291 /> 292 - <label for="teams-search" class="sr-only">Filter teams</label> 292 + <label for="teams-search" class="sr-only">{{ $t('org.teams.filter_label') }}</label> 293 293 <input 294 294 id="teams-search" 295 295 v-model="searchQuery" 296 296 type="search" 297 297 name="teams-search" 298 - placeholder="Filter teams…" 298 + :placeholder="$t('org.teams.filter_placeholder')" 299 299 autocomplete="off" 300 300 class="w-full pl-7 pr-2 py-1.5 font-mono text-sm bg-bg-subtle border border-border rounded text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 301 301 /> 302 302 </div> 303 - <div class="flex items-center gap-1 text-xs" role="group" aria-label="Sort by"> 303 + <div 304 + class="flex items-center gap-1 text-xs" 305 + role="group" 306 + :aria-label="$t('org.teams.sort_by')" 307 + > 304 308 <button 305 309 type="button" 306 310 class="px-2 py-1 font-mono rounded transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" ··· 308 312 :aria-pressed="sortBy === 'name'" 309 313 @click="toggleSort('name')" 310 314 > 311 - name 315 + {{ $t('common.sort.name') }} 312 316 <span v-if="sortBy === 'name'">{{ sortOrder === 'asc' ? '↑' : '↓' }}</span> 313 317 </button> 314 318 <button ··· 318 322 :aria-pressed="sortBy === 'members'" 319 323 @click="toggleSort('members')" 320 324 > 321 - members 325 + {{ $t('common.sort.members') }} 322 326 <span v-if="sortBy === 'members'">{{ sortOrder === 'asc' ? '↑' : '↓' }}</span> 323 327 </button> 324 328 </div> ··· 330 334 class="i-carbon-rotate-180 block w-5 h-5 text-fg-muted animate-spin mx-auto" 331 335 aria-hidden="true" 332 336 /> 333 - <p class="font-mono text-sm text-fg-muted mt-2">Loading teams…</p> 337 + <p class="font-mono text-sm text-fg-muted mt-2">{{ $t('org.teams.loading') }}</p> 334 338 </div> 335 339 336 340 <!-- Error state --> ··· 343 347 class="mt-2 font-mono text-xs text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 344 348 @click="loadTeams" 345 349 > 346 - Try again 350 + {{ $t('common.try_again') }} 347 351 </button> 348 352 </div> 349 353 350 354 <!-- Empty state --> 351 355 <div v-else-if="teams.length === 0" class="p-8 text-center"> 352 - <p class="font-mono text-sm text-fg-muted">No teams found</p> 356 + <p class="font-mono text-sm text-fg-muted">{{ $t('org.teams.no_teams') }}</p> 353 357 </div> 354 358 355 359 <!-- Teams list --> 356 - <ul v-else class="divide-y divide-border" aria-label="Organization teams"> 360 + <ul v-else class="divide-y divide-border" :aria-label="$t('org.teams.list_label')"> 357 361 <li v-for="teamName in filteredTeams" :key="teamName" class="bg-bg"> 358 362 <!-- Team header --> 359 363 <div ··· 376 380 /> 377 381 <span class="font-mono text-sm text-fg">{{ teamName }}</span> 378 382 <span v-if="teamUsers[teamName]" class="font-mono text-xs text-fg-subtle"> 379 - ({{ teamUsers[teamName].length }} member{{ 380 - teamUsers[teamName].length === 1 ? '' : 's' 383 + ({{ 384 + $t( 385 + 'org.teams.member_count', 386 + { count: teamUsers[teamName].length }, 387 + teamUsers[teamName].length, 388 + ) 381 389 }}) 382 390 </span> 383 391 <span ··· 389 397 <button 390 398 type="button" 391 399 class="p-1 text-fg-subtle hover:text-red-400 transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 392 - :aria-label="`Delete team ${teamName}`" 400 + :aria-label="$t('org.teams.delete_team', { name: teamName })" 393 401 @click.stop="handleDestroyTeam(teamName)" 394 402 > 395 403 <span class="i-carbon-trash-can block w-4 h-4" aria-hidden="true" /> ··· 406 414 <ul 407 415 v-if="teamUsers[teamName]?.length" 408 416 class="space-y-1 mb-2" 409 - :aria-label="`Members of ${teamName}`" 417 + :aria-label="$t('org.teams.members_of', { team: teamName })" 410 418 > 411 419 <li 412 420 v-for="user in teamUsers[teamName]" ··· 422 430 <button 423 431 type="button" 424 432 class="p-1 text-fg-subtle hover:text-red-400 transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 425 - :aria-label="`Remove ${user} from team`" 433 + :aria-label="$t('org.teams.remove_user', { user })" 426 434 @click="handleRemoveUser(teamName, user)" 427 435 > 428 436 <span class="i-carbon-close block w-3.5 h-3.5" aria-hidden="true" /> ··· 430 438 </li> 431 439 </ul> 432 440 <p v-else-if="!isLoadingUsers[teamName]" class="font-mono text-xs text-fg-subtle py-1"> 433 - No members 441 + {{ $t('org.teams.no_members') }} 434 442 </p> 435 443 436 444 <!-- Add user form --> 437 445 <div v-if="showAddUserFor === teamName" class="mt-2"> 438 446 <form class="flex items-center gap-2" @submit.prevent="handleAddUser(teamName)"> 439 - <label :for="`add-user-${teamName}`" class="sr-only" 440 - >Username to add to {{ teamName }}</label 441 - > 447 + <label :for="`add-user-${teamName}`" class="sr-only">{{ 448 + $t('org.teams.username_to_add', { team: teamName }) 449 + }}</label> 442 450 <input 443 451 :id="`add-user-${teamName}`" 444 452 v-model="newUserUsername" 445 453 type="text" 446 454 :name="`add-user-${teamName}`" 447 - placeholder="username…" 455 + :placeholder="$t('org.teams.username_placeholder')" 448 456 autocomplete="off" 449 457 spellcheck="false" 450 458 class="flex-1 px-2 py-1 font-mono text-sm bg-bg-subtle border border-border rounded text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" ··· 454 462 :disabled="!newUserUsername.trim() || isAddingUser" 455 463 class="px-2 py-1 font-mono text-xs text-bg bg-fg rounded transition-all duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 456 464 > 457 - {{ isAddingUser ? '…' : 'add' }} 465 + {{ isAddingUser ? '…' : $t('org.teams.add_button') }} 458 466 </button> 459 467 <button 460 468 type="button" 461 469 class="p-1 text-fg-subtle hover:text-fg transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 462 - aria-label="Cancel adding user" 470 + :aria-label="$t('org.teams.cancel_add_user')" 463 471 @click="showAddUserFor = null" 464 472 > 465 473 <span class="i-carbon-close block w-4 h-4" aria-hidden="true" /> ··· 472 480 class="mt-2 px-2 py-1 font-mono text-xs text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 473 481 @click="showAddUserFor = teamName" 474 482 > 475 - + Add member 483 + {{ $t('org.teams.add_member') }} 476 484 </button> 477 485 </div> 478 486 </li> ··· 480 488 481 489 <!-- No results --> 482 490 <div v-if="teams.length > 0 && filteredTeams.length === 0" class="p-4 text-center"> 483 - <p class="font-mono text-sm text-fg-muted">No teams match "{{ searchQuery }}"</p> 491 + <p class="font-mono text-sm text-fg-muted"> 492 + {{ $t('org.teams.no_match', { query: searchQuery }) }} 493 + </p> 484 494 </div> 485 495 486 496 <!-- Create team --> ··· 493 503 > 494 504 {{ orgName }}: 495 505 </span> 496 - <label for="new-team-name" class="sr-only">Team name</label> 506 + <label for="new-team-name" class="sr-only">{{ $t('org.teams.team_name_label') }}</label> 497 507 <input 498 508 id="new-team-name" 499 509 v-model="newTeamName" 500 510 type="text" 501 511 name="new-team-name" 502 - placeholder="team-name…" 512 + :placeholder="$t('org.teams.team_name_placeholder')" 503 513 autocomplete="off" 504 514 spellcheck="false" 505 515 class="flex-1 px-2 py-1.5 font-mono text-sm bg-bg border border-border rounded-r text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" ··· 510 520 :disabled="!newTeamName.trim() || isCreatingTeam" 511 521 class="px-3 py-1.5 font-mono text-xs text-bg bg-fg rounded transition-all duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 512 522 > 513 - {{ isCreatingTeam ? '…' : 'create' }} 523 + {{ isCreatingTeam ? '…' : $t('org.teams.create_button') }} 514 524 </button> 515 525 <button 516 526 type="button" 517 527 class="p-1.5 text-fg-subtle hover:text-fg transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 518 - aria-label="Cancel creating team" 528 + :aria-label="$t('org.teams.cancel_create')" 519 529 @click="showCreateTeam = false" 520 530 > 521 531 <span class="i-carbon-close block w-4 h-4" aria-hidden="true" /> ··· 528 538 class="w-full px-3 py-2 font-mono text-sm text-fg-muted bg-bg border border-border rounded transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 529 539 @click="showCreateTeam = true" 530 540 > 531 - + Create team 541 + {{ $t('org.teams.create_team') }} 532 542 </button> 533 543 </div> 534 544 </section>
+29 -15
app/components/PackageAccessControls.vue
··· 153 153 <section v-if="isConnected && orgName" aria-labelledby="access-heading"> 154 154 <div class="flex items-center justify-between mb-3"> 155 155 <h2 id="access-heading" class="text-xs text-fg-subtle uppercase tracking-wider"> 156 - Team Access 156 + {{ $t('package.access.title') }} 157 157 </h2> 158 158 <button 159 159 type="button" 160 160 class="p-1 text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 161 - aria-label="Refresh team access" 161 + :aria-label="$t('package.access.refresh')" 162 162 :disabled="isLoadingCollaborators" 163 163 @click="loadCollaborators" 164 164 > ··· 184 184 </div> 185 185 186 186 <!-- Collaborators list --> 187 - <ul v-if="collaboratorList.length > 0" class="space-y-1 mb-3" aria-label="Team access list"> 187 + <ul 188 + v-if="collaboratorList.length > 0" 189 + class="space-y-1 mb-3" 190 + :aria-label="$t('package.access.list_label')" 191 + > 188 192 <li 189 193 v-for="collab in collaboratorList" 190 194 :key="collab.name" ··· 212 216 : 'bg-fg-subtle/20 text-fg-muted' 213 217 " 214 218 > 215 - {{ collab.permission === 'read-write' ? 'rw' : 'ro' }} 219 + {{ 220 + collab.permission === 'read-write' ? $t('package.access.rw') : $t('package.access.ro') 221 + }} 216 222 </span> 217 223 </div> 218 224 <!-- Only show revoke for teams (users are managed via owners) --> ··· 220 226 v-if="collab.isTeam" 221 227 type="button" 222 228 class="p-1 text-fg-subtle hover:text-red-400 transition-colors duration-200 shrink-0 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 223 - :aria-label="`Revoke ${collab.displayName} access`" 229 + :aria-label="$t('package.access.revoke_access', { name: collab.displayName })" 224 230 @click="handleRevokeAccess(collab.name)" 225 231 > 226 232 <span class="i-carbon-close block w-3.5 h-3.5" aria-hidden="true" /> 227 233 </button> 228 - <span v-else class="text-xs text-fg-subtle"> owner </span> 234 + <span v-else class="text-xs text-fg-subtle"> {{ $t('package.access.owner') }} </span> 229 235 </li> 230 236 </ul> 231 237 232 238 <p v-else-if="!isLoadingCollaborators && !error" class="text-xs text-fg-subtle mb-3"> 233 - No team access configured 239 + {{ $t('package.access.no_access') }} 234 240 </p> 235 241 236 242 <!-- Grant access form --> 237 243 <div v-if="showGrantAccess"> 238 244 <form class="space-y-2" @submit.prevent="handleGrantAccess"> 239 245 <div class="flex items-center gap-2"> 240 - <label for="grant-team-select" class="sr-only">Select team</label> 246 + <label for="grant-team-select" class="sr-only">{{ 247 + $t('package.access.select_team_label') 248 + }}</label> 241 249 <select 242 250 id="grant-team-select" 243 251 v-model="selectedTeam" ··· 246 254 :disabled="isLoadingTeams" 247 255 > 248 256 <option value="" disabled> 249 - {{ isLoadingTeams ? 'Loading teams…' : 'Select team' }} 257 + {{ 258 + isLoadingTeams 259 + ? $t('package.access.loading_teams') 260 + : $t('package.access.select_team') 261 + }} 250 262 </option> 251 263 <option v-for="team in teams" :key="team" :value="team"> 252 264 {{ orgName }}:{{ team }} ··· 254 266 </select> 255 267 </div> 256 268 <div class="flex items-center gap-2"> 257 - <label for="grant-permission-select" class="sr-only">Permission level</label> 269 + <label for="grant-permission-select" class="sr-only">{{ 270 + $t('package.access.permission_label') 271 + }}</label> 258 272 <select 259 273 id="grant-permission-select" 260 274 v-model="permission" 261 275 name="grant-permission" 262 276 class="flex-1 px-2 py-1.5 font-mono text-sm bg-bg-subtle border border-border rounded text-fg transition-colors duration-200 focus:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 263 277 > 264 - <option value="read-only">read-only</option> 265 - <option value="read-write">read-write</option> 278 + <option value="read-only">{{ $t('package.access.permission.read_only') }}</option> 279 + <option value="read-write">{{ $t('package.access.permission.read_write') }}</option> 266 280 </select> 267 281 <button 268 282 type="submit" 269 283 :disabled="!selectedTeam || isGranting" 270 284 class="px-3 py-1.5 font-mono text-xs text-bg bg-fg rounded transition-all duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 271 285 > 272 - {{ isGranting ? '…' : 'grant' }} 286 + {{ isGranting ? '…' : $t('package.access.grant_button') }} 273 287 </button> 274 288 <button 275 289 type="button" 276 290 class="p-1.5 text-fg-subtle hover:text-fg transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 277 - aria-label="Cancel granting access" 291 + :aria-label="$t('package.access.cancel_grant')" 278 292 @click="showGrantAccess = false" 279 293 > 280 294 <span class="i-carbon-close block w-4 h-4" aria-hidden="true" /> ··· 288 302 class="w-full px-3 py-1.5 font-mono text-xs text-fg-muted bg-bg-subtle border border-border rounded transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 289 303 @click="showGrantAccess = true" 290 304 > 291 - + Grant team access 305 + {{ $t('package.access.grant_access') }} 292 306 </button> 293 307 </section> 294 308 </template>
+5 -5
app/components/PackageCard.vue
··· 70 70 v-if="showPublisher && result.package.publisher?.username" 71 71 class="flex items-center gap-1.5" 72 72 > 73 - <dt class="sr-only">Publisher</dt> 73 + <dt class="sr-only">{{ $t('package.card.publisher') }}</dt> 74 74 <dd class="font-mono">@{{ result.package.publisher.username }}</dd> 75 75 </div> 76 76 <div v-if="result.package.date" class="flex items-center gap-1.5"> 77 - <dt class="sr-only">Updated</dt> 77 + <dt class="sr-only">{{ $t('package.card.updated') }}</dt> 78 78 <dd> 79 79 <DateTime 80 80 :datetime="result.package.date" ··· 96 96 class="sm:hidden flex items-center gap-4 mt-2 text-xs text-fg-subtle m-0" 97 97 > 98 98 <div class="flex items-center gap-1.5"> 99 - <dt class="sr-only">Weekly downloads</dt> 99 + <dt class="sr-only">{{ $t('package.card.weekly_downloads') }}</dt> 100 100 <dd class="flex items-center gap-1.5"> 101 101 <span class="i-carbon-chart-line w-3.5 h-3.5 inline-block" aria-hidden="true" /> 102 102 <span class="font-mono">{{ formatNumber(result.downloads.weekly) }}/w</span> ··· 133 133 > 134 134 <span class="i-carbon-chart-line w-3.5 h-3.5 inline-block" aria-hidden="true" /> 135 135 <span class="font-mono text-xs"> 136 - {{ formatNumber(result.downloads.weekly) }} / week 136 + {{ formatNumber(result.downloads.weekly) }} {{ $t('common.per_week') }} 137 137 </span> 138 138 </div> 139 139 </div> ··· 141 141 142 142 <ul 143 143 v-if="result.package.keywords?.length" 144 - aria-label="Keywords" 144 + :aria-label="$t('package.card.keywords')" 145 145 class="relative z-10 flex flex-wrap gap-1.5 mt-3 pt-3 border-t border-border list-none m-0 p-0" 146 146 > 147 147 <li v-for="keyword in result.package.keywords.slice(0, 5)" :key="keyword" class="tag">
+21 -11
app/components/PackageDependencies.vue
··· 50 50 <!-- Dependencies --> 51 51 <section v-if="sortedDependencies.length > 0" aria-labelledby="dependencies-heading"> 52 52 <h2 id="dependencies-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-3"> 53 - Dependencies ({{ sortedDependencies.length }}) 53 + {{ $t('package.dependencies.title', { count: sortedDependencies.length }) }} 54 54 </h2> 55 - <ul class="space-y-1 list-none m-0 p-0" aria-label="Package dependencies"> 55 + <ul class="space-y-1 list-none m-0 p-0" :aria-label="$t('package.dependencies.list_label')"> 56 56 <li 57 57 v-for="[dep, version] in sortedDependencies.slice(0, depsExpanded ? undefined : 10)" 58 58 :key="dep" ··· 94 94 class="mt-2 font-mono text-xs text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 95 95 @click="depsExpanded = true" 96 96 > 97 - show all {{ sortedDependencies.length }} deps 97 + {{ $t('package.dependencies.show_all', { count: sortedDependencies.length }) }} 98 98 </button> 99 99 </section> 100 100 ··· 104 104 id="peer-dependencies-heading" 105 105 class="text-xs text-fg-subtle uppercase tracking-wider mb-3" 106 106 > 107 - Peer Dependencies ({{ sortedPeerDependencies.length }}) 107 + {{ $t('package.peer_dependencies.title', { count: sortedPeerDependencies.length }) }} 108 108 </h2> 109 - <ul class="space-y-1 list-none m-0 p-0" aria-label="Package peer dependencies"> 109 + <ul 110 + class="space-y-1 list-none m-0 p-0" 111 + :aria-label="$t('package.peer_dependencies.list_label')" 112 + > 110 113 <li 111 114 v-for="peer in sortedPeerDependencies.slice(0, peerDepsExpanded ? undefined : 10)" 112 115 :key="peer.name" ··· 122 125 <span 123 126 v-if="peer.optional" 124 127 class="px-1 py-0.5 font-mono text-[10px] text-fg-subtle bg-bg-muted border border-border rounded shrink-0" 125 - title="Optional peer dependency" 128 + :title="$t('package.dependencies.optional')" 126 129 > 127 - optional 130 + {{ $t('package.dependencies.optional') }} 128 131 </span> 129 132 </div> 130 133 <NuxtLink ··· 145 148 class="mt-2 font-mono text-xs text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 146 149 @click="peerDepsExpanded = true" 147 150 > 148 - show all {{ sortedPeerDependencies.length }} peer deps 151 + {{ $t('package.peer_dependencies.show_all', { count: sortedPeerDependencies.length }) }} 149 152 </button> 150 153 </section> 151 154 ··· 158 161 id="optional-dependencies-heading" 159 162 class="text-xs text-fg-subtle uppercase tracking-wider mb-3" 160 163 > 161 - Optional Dependencies ({{ sortedOptionalDependencies.length }}) 164 + {{ 165 + $t('package.optional_dependencies.title', { count: sortedOptionalDependencies.length }) 166 + }} 162 167 </h2> 163 - <ul class="space-y-1 list-none m-0 p-0" aria-label="Package optional dependencies"> 168 + <ul 169 + class="space-y-1 list-none m-0 p-0" 170 + :aria-label="$t('package.optional_dependencies.list_label')" 171 + > 164 172 <li 165 173 v-for="[dep, version] in sortedOptionalDependencies.slice( 166 174 0, ··· 190 198 class="mt-2 font-mono text-xs text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 191 199 @click="optionalDepsExpanded = true" 192 200 > 193 - show all {{ sortedOptionalDependencies.length }} optional deps 201 + {{ 202 + $t('package.optional_dependencies.show_all', { count: sortedOptionalDependencies.length }) 203 + }} 194 204 </button> 195 205 </section> 196 206 </div>
+6 -2
app/components/PackageDownloadStats.vue
··· 1 1 <script setup lang="ts"> 2 2 import { VueUiSparkline } from 'vue-data-ui/vue-ui-sparkline' 3 3 4 + const { t } = useI18n() 5 + 4 6 const props = defineProps<{ 5 7 downloads?: Array<{ 6 8 downloads: number | null ··· 12 14 const dataset = computed(() => 13 15 props.downloads?.map(d => ({ 14 16 value: d?.downloads ?? 0, 15 - period: `${d.weekStart ?? '-'} to ${d.weekEnd ?? '-'}`, 17 + period: t('package.downloads.date_range', { start: d.weekStart ?? '-', end: d.weekEnd ?? '-' }), 16 18 })), 17 19 ) 18 20 ··· 75 77 <!-- Download stats --> 76 78 <section> 77 79 <div class="flex items-center justify-between mb-3"> 78 - <h2 class="text-xs text-fg-subtle uppercase tracking-wider">Weekly Downloads</h2> 80 + <h2 class="text-xs text-fg-subtle uppercase tracking-wider"> 81 + {{ $t('package.downloads.title') }} 82 + </h2> 79 83 </div> 80 84 <div class="w-full overflow-hidden"> 81 85 <ClientOnly>
+14 -4
app/components/PackageInstallScripts.vue
··· 8 8 } 9 9 }>() 10 10 11 + const { t } = useI18n() 12 + 11 13 const outdatedNpxDeps = useOutdatedDependencies(() => props.installScripts.npxDependencies) 12 14 const hasNpxDeps = computed(() => Object.keys(props.installScripts.npxDependencies).length > 0) 13 15 const sortedNpxDeps = computed(() => { ··· 24 26 class="text-xs text-fg-subtle uppercase tracking-wider mb-3 flex items-center gap-2" 25 27 > 26 28 <span class="i-carbon-warning-alt w-3 h-3 text-yellow-500" aria-hidden="true" /> 27 - Install Scripts 29 + {{ $t('package.install_scripts.title') }} 28 30 </h2> 29 31 30 32 <!-- Script list: name as label, content below --> ··· 36 38 class="font-mono text-sm text-fg-subtle m-0 truncate focus:whitespace-normal focus:overflow-visible cursor-help rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 37 39 :title="installScripts.content?.[scriptName]" 38 40 > 39 - {{ installScripts.content?.[scriptName] || '(script)' }} 41 + {{ installScripts.content?.[scriptName] || $t('package.install_scripts.script_label') }} 40 42 </dd> 41 43 </div> 42 44 </dl> ··· 55 57 :class="{ 'rotate-90': isExpanded }" 56 58 aria-hidden="true" 57 59 /> 58 - {{ sortedNpxDeps.length }} npx package{{ sortedNpxDeps.length !== 1 ? 's' : '' }} 60 + {{ 61 + t( 62 + 'package.install_scripts.npx_packages', 63 + { count: sortedNpxDeps.length }, 64 + sortedNpxDeps.length, 65 + ) 66 + }} 59 67 </button> 60 68 61 69 <ul ··· 93 101 :title=" 94 102 outdatedNpxDeps[dep] 95 103 ? outdatedNpxDeps[dep].resolved === outdatedNpxDeps[dep].latest 96 - ? `currently ${outdatedNpxDeps[dep].latest}` 104 + ? t('package.install_scripts.currently', { 105 + version: outdatedNpxDeps[dep].latest, 106 + }) 97 107 : getOutdatedTooltip(outdatedNpxDeps[dep]) 98 108 : version 99 109 "
+2 -2
app/components/PackageList.vue
··· 123 123 <div v-if="isLoading" class="py-4 flex items-center justify-center"> 124 124 <div class="flex items-center gap-3 text-fg-muted font-mono text-sm"> 125 125 <span class="w-4 h-4 border-2 border-fg-subtle border-t-fg rounded-full animate-spin" /> 126 - Loading more... 126 + {{ $t('common.loading_more') }} 127 127 </div> 128 128 </div> 129 129 ··· 132 132 v-else-if="!hasMore && results.length > 0" 133 133 class="py-4 text-center text-fg-subtle font-mono text-sm" 134 134 > 135 - End of results 135 + {{ $t('common.end_of_results') }} 136 136 </p> 137 137 </div> 138 138 </template>
+15 -10
app/components/PackageListControls.vue
··· 19 19 'update:sort': [value: SortOption] 20 20 }>() 21 21 22 + const { t } = useI18n() 23 + 22 24 const filterValue = computed({ 23 25 get: () => props.filter, 24 26 set: value => emit('update:filter', value), ··· 29 31 set: value => emit('update:sort', value), 30 32 }) 31 33 32 - const sortOptions = [ 33 - { value: 'downloads', label: 'Most downloaded' }, 34 - { value: 'updated', label: 'Recently updated' }, 35 - { value: 'name-asc', label: 'Name (A-Z)' }, 36 - { value: 'name-desc', label: 'Name (Z-A)' }, 37 - ] as const 34 + const sortOptions = computed( 35 + () => 36 + [ 37 + { value: 'downloads', label: t('package.sort.downloads') }, 38 + { value: 'updated', label: t('package.sort.updated') }, 39 + { value: 'name-asc', label: t('package.sort.name_asc') }, 40 + { value: 'name-desc', label: t('package.sort.name_desc') }, 41 + ] as const, 42 + ) 38 43 39 44 // Show filter count when filtering is active 40 45 const showFilteredCount = computed(() => { ··· 51 56 <div class="flex flex-col sm:flex-row gap-3 mb-6"> 52 57 <!-- Filter input --> 53 58 <div class="flex-1 relative"> 54 - <label for="package-filter" class="sr-only">Filter packages</label> 59 + <label for="package-filter" class="sr-only">{{ $t('package.list.filter_label') }}</label> 55 60 <div 56 61 class="absolute h-full w-10 flex items-center justify-center text-fg-subtle pointer-events-none" 57 62 aria-hidden="true" ··· 62 67 id="package-filter" 63 68 v-model="filterValue" 64 69 type="search" 65 - :placeholder="placeholder ?? 'Filter packages...'" 70 + :placeholder="placeholder ?? t('package.list.filter_placeholder')" 66 71 autocomplete="off" 67 72 class="w-full bg-bg-subtle border border-border rounded-lg pl-10 pr-4 py-2 font-mono text-sm text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:(border-border-hover outline-none)" 68 73 /> ··· 70 75 71 76 <!-- Sort select --> 72 77 <div class="relative shrink-0 flex"> 73 - <label for="package-sort" class="sr-only">Sort packages</label> 78 + <label for="package-sort" class="sr-only">{{ $t('package.list.sort_label') }}</label> 74 79 <div class="relative"> 75 80 <select 76 81 id="package-sort" ··· 93 98 94 99 <!-- Filtered count indicator --> 95 100 <p v-if="showFilteredCount" class="text-fg-subtle text-xs font-mono mb-4"> 96 - Showing {{ filteredCount }} of {{ totalCount }} packages 101 + {{ $t('package.list.showing_count', { filtered: filteredCount, total: totalCount }) }} 97 102 </p> 98 103 </template>
+12 -10
app/components/PackageMaintainers.vue
··· 155 155 <template> 156 156 <section v-if="maintainers?.length" aria-labelledby="maintainers-heading"> 157 157 <h2 id="maintainers-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-3"> 158 - Maintainers 158 + {{ $t('package.maintainers.title') }} 159 159 </h2> 160 - <ul class="space-y-2 list-none m-0 p-0" aria-label="Package maintainers"> 160 + <ul class="space-y-2 list-none m-0 p-0" :aria-label="$t('package.maintainers.list_label')"> 161 161 <li 162 162 v-for="maintainer in maintainerAccess.slice(0, canManageOwners ? undefined : 5)" 163 163 :key="maintainer.name ?? maintainer.email" ··· 178 178 v-if="isConnected && maintainer.accessVia?.length && !isLoadingAccess" 179 179 class="text-xs text-fg-subtle truncate" 180 180 > 181 - via {{ maintainer.accessVia.join(', ') }} 181 + {{ $t('package.maintainers.via', { teams: maintainer.accessVia.join(', ') }) }} 182 182 </span> 183 183 <span 184 184 v-if="canManageOwners && maintainer.name === npmUser" 185 185 class="text-xs text-fg-subtle shrink-0" 186 - >(you)</span 186 + >{{ $t('package.maintainers.you') }}</span 187 187 > 188 188 </div> 189 189 ··· 192 192 v-if="canManageOwners && maintainer.name && maintainer.name !== npmUser" 193 193 type="button" 194 194 class="p-1 text-fg-subtle hover:text-red-400 transition-colors duration-200 shrink-0 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 195 - :aria-label="`Remove ${maintainer.name} as owner`" 195 + :aria-label="$t('package.maintainers.remove_owner', { name: maintainer.name })" 196 196 @click="handleRemoveOwner(maintainer.name)" 197 197 > 198 198 <span class="i-carbon-close block w-3.5 h-3.5" aria-hidden="true" /> ··· 204 204 <div v-if="canManageOwners" class="mt-3"> 205 205 <div v-if="showAddOwner"> 206 206 <form class="flex items-center gap-2" @submit.prevent="handleAddOwner"> 207 - <label for="add-owner-username" class="sr-only">Username to add as owner</label> 207 + <label for="add-owner-username" class="sr-only">{{ 208 + $t('package.maintainers.username_to_add') 209 + }}</label> 208 210 <input 209 211 id="add-owner-username" 210 212 v-model="newOwnerUsername" 211 213 type="text" 212 214 name="add-owner-username" 213 - placeholder="username…" 215 + :placeholder="$t('package.maintainers.username_placeholder')" 214 216 autocomplete="off" 215 217 spellcheck="false" 216 218 class="flex-1 px-2 py-1 font-mono text-sm bg-bg-subtle border border-border rounded text-fg placeholder:text-fg-subtle transition-colors duration-200 focus:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" ··· 220 222 :disabled="!newOwnerUsername.trim() || isAdding" 221 223 class="px-2 py-1 font-mono text-xs text-bg bg-fg rounded transition-all duration-200 hover:bg-fg/90 disabled:opacity-50 disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 222 224 > 223 - {{ isAdding ? '…' : 'add' }} 225 + {{ isAdding ? '…' : $t('package.maintainers.add_button') }} 224 226 </button> 225 227 <button 226 228 type="button" 227 229 class="p-1 text-fg-subtle hover:text-fg transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 228 - aria-label="Cancel adding owner" 230 + :aria-label="$t('package.maintainers.cancel_add')" 229 231 @click="showAddOwner = false" 230 232 > 231 233 <span class="i-carbon-close block w-4 h-4" aria-hidden="true" /> ··· 238 240 class="w-full px-3 py-1.5 font-mono text-xs text-fg-muted bg-bg-subtle border border-border rounded transition-colors duration-200 hover:text-fg hover:border-border-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 239 241 @click="showAddOwner = true" 240 242 > 241 - + Add owner 243 + {{ $t('package.maintainers.add_owner') }} 242 244 </button> 243 245 </div> 244 246 </section>
+8 -6
app/components/PackageMetricsBadges.vue
··· 25 25 } 26 26 }) 27 27 28 + const { t } = useI18n() 29 + 28 30 const moduleFormatTooltip = computed(() => { 29 31 if (!analysis.value) return '' 30 32 switch (analysis.value.moduleFormat) { 31 33 case 'esm': 32 - return 'ES Modules only' 34 + return t('package.metrics.esm') 33 35 case 'cjs': 34 - return 'CommonJS only' 36 + return t('package.metrics.cjs') 35 37 case 'dual': 36 - return 'Supports both CommonJS and ES Modules' 38 + return t('package.metrics.dual') 37 39 default: 38 - return 'Unknown module format' 40 + return t('package.metrics.unknown_format') 39 41 } 40 42 }) 41 43 ··· 48 50 if (!analysis.value) return '' 49 51 switch (analysis.value.types?.kind) { 50 52 case 'included': 51 - return 'TypeScript types included' 53 + return t('package.metrics.ts_included') 52 54 case '@types': 53 - return `Types from ${analysis.value.types.packageName}` 55 + return t('package.metrics.types_from', { package: analysis.value.types.packageName }) 54 56 default: 55 57 return '' 56 58 }
+4 -2
app/components/PackagePlaygrounds.vue
··· 110 110 <template> 111 111 <section v-if="links.length > 0" aria-labelledby="playgrounds-heading"> 112 112 <h2 id="playgrounds-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-3"> 113 - Try it out 113 + {{ $t('package.playgrounds.title') }} 114 114 </h2> 115 115 116 116 <div ref="dropdownRef" class="relative"> ··· 142 142 > 143 143 <span class="flex items-center gap-2"> 144 144 <span class="i-carbon-play w-4 h-4 shrink-0 text-fg-muted" aria-hidden="true" /> 145 - <span class="text-fg-muted">choose playground ({{ links.length }})</span> 145 + <span class="text-fg-muted" 146 + >{{ $t('package.playgrounds.choose') }} ({{ links.length }})</span 147 + > 146 148 </span> 147 149 <span 148 150 class="i-carbon-chevron-down w-3 h-3 text-fg-subtle transition-transform duration-200 motion-reduce:transition-none"
+26 -12
app/components/PackageSkeleton.vue
··· 1 + <script setup lang="ts"> 2 + const { t } = useI18n() 3 + </script> 4 + 1 5 <template> 2 - <article aria-busy="true" aria-label="Loading package details" class="animate-fade-in"> 6 + <article aria-busy="true" :aria-label="t('package.skeleton.loading')" class="animate-fade-in"> 3 7 <!-- Package header - matches header in [...name].vue --> 4 8 <header class="mb-8 pb-8 border-b border-border"> 5 9 <div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4 mb-4"> ··· 26 30 <dl class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-4 mt-6"> 27 31 <!-- License --> 28 32 <div class="space-y-1"> 29 - <dt class="text-xs text-fg-subtle uppercase tracking-wider">License</dt> 33 + <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 34 + {{ t('package.skeleton.license') }} 35 + </dt> 30 36 <dd class="font-mono text-sm"> 31 37 <span class="skeleton inline-block h-5 w-12" /> 32 38 </dd> ··· 34 40 35 41 <!-- Weekly --> 36 42 <div class="space-y-1"> 37 - <dt class="text-xs text-fg-subtle uppercase tracking-wider">Weekly</dt> 43 + <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 44 + {{ t('package.skeleton.weekly') }} 45 + </dt> 38 46 <dd class="font-mono text-sm"> 39 47 <span class="skeleton inline-block h-5 w-20" /> 40 48 </dd> ··· 42 50 43 51 <!-- Size --> 44 52 <div class="space-y-1"> 45 - <dt class="text-xs text-fg-subtle uppercase tracking-wider">Size</dt> 53 + <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 54 + {{ t('package.skeleton.size') }} 55 + </dt> 46 56 <dd class="font-mono text-sm"> 47 57 <span class="skeleton inline-block h-5 w-16" /> 48 58 </dd> ··· 50 60 51 61 <!-- Deps --> 52 62 <div class="space-y-1"> 53 - <dt class="text-xs text-fg-subtle uppercase tracking-wider">Deps</dt> 63 + <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 64 + {{ t('package.skeleton.deps') }} 65 + </dt> 54 66 <dd class="font-mono text-sm"> 55 67 <span class="skeleton inline-block h-5 w-8" /> 56 68 </dd> ··· 58 70 59 71 <!-- Updated --> 60 72 <div class="space-y-1 col-span-2"> 61 - <dt class="text-xs text-fg-subtle uppercase tracking-wider">Updated</dt> 73 + <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 74 + {{ t('package.skeleton.updated') }} 75 + </dt> 62 76 <dd class="font-mono text-sm"> 63 77 <span class="skeleton inline-block h-5 w-28" /> 64 78 </dd> ··· 90 104 id="install-heading-skeleton" 91 105 class="text-xs text-fg-subtle uppercase tracking-wider mb-3" 92 106 > 93 - Install 107 + {{ t('package.skeleton.install') }} 94 108 </h2> 95 109 <!-- code-block with relative positioning for copy button --> 96 110 <div class="relative"> ··· 110 124 id="readme-heading-skeleton" 111 125 class="text-xs text-fg-subtle uppercase tracking-wider mb-4" 112 126 > 113 - Readme 127 + {{ t('package.skeleton.readme') }} 114 128 </h2> 115 129 <!-- Simulated README content --> 116 130 <div class="space-y-4"> ··· 141 155 id="maintainers-heading-skeleton" 142 156 class="text-xs text-fg-subtle uppercase tracking-wider mb-3" 143 157 > 144 - Maintainers 158 + {{ t('package.skeleton.maintainers') }} 145 159 </h2> 146 160 <ul class="space-y-2 list-none m-0 p-0"> 147 161 <li> ··· 159 173 id="keywords-heading-skeleton" 160 174 class="text-xs text-fg-subtle uppercase tracking-wider mb-3" 161 175 > 162 - Keywords 176 + {{ t('package.skeleton.keywords') }} 163 177 </h2> 164 178 <!-- flex flex-wrap gap-1.5 --> 165 179 <ul class="flex flex-wrap gap-1.5 list-none m-0 p-0"> ··· 178 192 id="versions-heading-skeleton" 179 193 class="text-xs text-fg-subtle uppercase tracking-wider mb-3" 180 194 > 181 - Versions 195 + {{ t('package.skeleton.versions') }} 182 196 </h2> 183 197 <!-- space-y-1, each row: flex items-center justify-between py-1.5 text-sm --> 184 198 <div class="space-y-1"> ··· 211 225 id="dependencies-heading-skeleton" 212 226 class="text-xs text-fg-subtle uppercase tracking-wider mb-3" 213 227 > 214 - Dependencies 228 + {{ t('package.skeleton.dependencies') }} 215 229 </h2> 216 230 <!-- space-y-1, each: flex items-center justify-between py-1 text-sm --> 217 231 <ul class="space-y-1 list-none m-0 p-0">
+30 -10
app/components/PackageVersions.vue
··· 10 10 } from '~/utils/versions' 11 11 import { fetchAllPackageVersions } from '~/composables/useNpmRegistry' 12 12 13 + const { t } = useI18n() 14 + 13 15 const props = defineProps<{ 14 16 packageName: string 15 17 versions: Record<string, PackumentVersion> ··· 293 295 <template> 294 296 <section v-if="allTagRows.length > 0" aria-labelledby="versions-heading" class="overflow-hidden"> 295 297 <h2 id="versions-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-3"> 296 - Versions 298 + {{ $t('package.versions.title') }} 297 299 </h2> 298 300 299 301 <div class="space-y-0.5 min-w-0"> ··· 306 308 type="button" 307 309 class="w-4 h-4 flex items-center justify-center text-fg-subtle hover:text-fg transition-colors" 308 310 :aria-expanded="expandedTags.has(row.tag)" 309 - :aria-label="expandedTags.has(row.tag) ? `Collapse ${row.tag}` : `Expand ${row.tag}`" 311 + :aria-label=" 312 + expandedTags.has(row.tag) 313 + ? $t('package.versions.collapse', { tag: row.tag }) 314 + : $t('package.versions.expand', { tag: row.tag }) 315 + " 310 316 @click="expandTagRow(row.tag)" 311 317 > 312 318 <span ··· 336 342 " 337 343 :title=" 338 344 row.primaryVersion.deprecated 339 - ? `${row.primaryVersion.version} (deprecated)` 345 + ? t('package.versions.deprecated_title', { 346 + version: row.primaryVersion.version, 347 + }) 340 348 : row.primaryVersion.version 341 349 " 342 350 > ··· 387 395 ? 'text-red-400 hover:text-red-300' 388 396 : 'text-fg-subtle hover:text-fg-muted' 389 397 " 390 - :title="v.deprecated ? `${v.version} (deprecated)` : v.version" 398 + :title=" 399 + v.deprecated 400 + ? t('package.versions.deprecated_title', { version: v.version }) 401 + : v.version 402 + " 391 403 > 392 404 {{ v.version }} 393 405 </NuxtLink> ··· 444 456 /> 445 457 </span> 446 458 <span class="text-xs text-fg-muted py-1.5"> 447 - Other versions 459 + {{ $t('package.versions.other_versions') }} 448 460 <span v-if="hiddenTagRows.length > 0" class="text-fg-subtle"> 449 - ({{ hiddenTagRows.length }} more tagged) 461 + ({{ $t('package.versions.more_tagged', { count: hiddenTagRows.length }) }}) 450 462 </span> 451 463 </span> 452 464 </button> ··· 466 478 " 467 479 :title=" 468 480 row.primaryVersion.deprecated 469 - ? `${row.primaryVersion.version} (deprecated)` 481 + ? t('package.versions.deprecated_title', { 482 + version: row.primaryVersion.version, 483 + }) 470 484 : row.primaryVersion.version 471 485 " 472 486 > ··· 552 566 " 553 567 :title=" 554 568 group.versions[0].deprecated 555 - ? `${group.versions[0].version} (deprecated)` 569 + ? t('package.versions.deprecated_title', { 570 + version: group.versions[0].version, 571 + }) 556 572 : group.versions[0].version 557 573 " 558 574 > ··· 585 601 ? 'text-red-400 hover:text-red-300' 586 602 : 'text-fg-subtle hover:text-fg-muted' 587 603 " 588 - :title="v.deprecated ? `${v.version} (deprecated)` : v.version" 604 + :title=" 605 + v.deprecated 606 + ? t('package.versions.deprecated_title', { version: v.version }) 607 + : v.version 608 + " 589 609 > 590 610 {{ v.version }} 591 611 </NuxtLink> ··· 623 643 v-else-if="hasLoadedAll && hiddenTagRows.length === 0" 624 644 class="py-1 text-xs text-fg-subtle" 625 645 > 626 - All versions are covered by tags above 646 + {{ $t('package.versions.all_covered') }} 627 647 </div> 628 648 </div> 629 649 </div>
+17 -8
app/components/PackageVulnerabilities.vue
··· 92 92 return `https://osv.dev/vulnerability/${vuln.id}` 93 93 } 94 94 95 + const { t } = useI18n() 96 + 95 97 function toVulnerabilitySummary(vuln: OsvVulnerability): VulnerabilitySummary { 96 98 return { 97 99 id: vuln.id, 98 - summary: vuln.summary || 'No description available', 100 + summary: vuln.summary || t('package.vulnerabilities.no_description'), 99 101 severity: getSeverityLevel(vuln), 100 102 aliases: vuln.aliases || [], 101 103 url: getVulnerabilityUrl(vuln), ··· 139 141 const summaryText = computed(() => { 140 142 const counts = vulnData.value.counts 141 143 const parts: string[] = [] 142 - if (counts.critical > 0) parts.push(`${counts.critical} critical`) 143 - if (counts.high > 0) parts.push(`${counts.high} high`) 144 - if (counts.moderate > 0) parts.push(`${counts.moderate} moderate`) 145 - if (counts.low > 0) parts.push(`${counts.low} low`) 144 + if (counts.critical > 0) 145 + parts.push(`${counts.critical} ${t('package.vulnerabilities.severity.critical')}`) 146 + if (counts.high > 0) parts.push(`${counts.high} ${t('package.vulnerabilities.severity.high')}`) 147 + if (counts.moderate > 0) 148 + parts.push(`${counts.moderate} ${t('package.vulnerabilities.severity.moderate')}`) 149 + if (counts.low > 0) parts.push(`${counts.low} ${t('package.vulnerabilities.severity.low')}`) 146 150 return parts.join(', ') 147 151 }) 148 152 </script> ··· 166 170 <div class="flex items-center gap-2 min-w-0"> 167 171 <span class="i-carbon-warning-alt w-4 h-4 shrink-0" aria-hidden="true" /> 168 172 <span class="font-mono text-sm font-medium truncate"> 169 - {{ vulnData.counts.total }} 170 - {{ vulnData.counts.total === 1 ? 'vulnerability' : 'vulnerabilities' }} found 173 + {{ 174 + $t( 175 + 'package.vulnerabilities.found', 176 + { count: vulnData.counts.total }, 177 + vulnData.counts.total, 178 + ) 179 + }} 171 180 </span> 172 181 </div> 173 182 <div class="flex items-center gap-2 shrink-0"> ··· 228 237 target="_blank" 229 238 rel="noopener noreferrer" 230 239 class="shrink-0 p-1.5 text-fg-subtle hover:text-fg transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 231 - aria-label="View vulnerability details" 240 + :aria-label="$t('package.vulnerabilities.view_details')" 232 241 > 233 242 <span class="i-carbon-launch w-3.5 h-3.5" aria-hidden="true" /> 234 243 </a>
+19 -13
app/components/ProvenanceBadge.vue
··· 1 1 <script setup lang="ts"> 2 - defineProps<{ 2 + const props = defineProps<{ 3 3 /** Provider ID (e.g., "github", "gitlab") */ 4 4 provider?: string 5 5 /** Package name for linking to npmjs.com provenance page */ ··· 12 12 linked?: boolean 13 13 }>() 14 14 15 + const { t } = useI18n() 16 + 15 17 const providerLabels: Record<string, string> = { 16 18 github: 'GitHub Actions', 17 19 gitlab: 'GitLab CI', 18 20 } 21 + 22 + const title = computed(() => 23 + props.provider 24 + ? t('badges.provenance.verified_via', { 25 + provider: providerLabels[props.provider] ?? props.provider, 26 + }) 27 + : t('badges.provenance.verified_title'), 28 + ) 19 29 </script> 20 30 21 31 <template> ··· 25 35 target="_blank" 26 36 rel="noopener noreferrer" 27 37 class="inline-flex items-center justify-center gap-1 text-xs font-mono text-fg-muted hover:text-fg transition-colors duration-200 min-w-6 min-h-6" 28 - :title=" 29 - provider 30 - ? `Verified: published via ${providerLabels[provider] ?? provider}` 31 - : 'Verified provenance' 32 - " 38 + :title="title" 33 39 > 34 40 <span 35 41 class="i-solar-shield-check-outline shrink-0" 36 42 :class="compact ? 'w-3.5 h-3.5' : 'w-4 h-4'" 37 43 /> 38 - <span v-if="!compact" class="sr-only sm:not-sr-only">verified</span> 44 + <span v-if="!compact" class="sr-only sm:not-sr-only">{{ 45 + t('badges.provenance.verified') 46 + }}</span> 39 47 </a> 40 48 <span 41 49 v-else 42 50 class="inline-flex items-center gap-1 text-xs font-mono text-fg-muted" 43 - :title=" 44 - provider 45 - ? `Verified: published via ${providerLabels[provider] ?? provider}` 46 - : 'Verified provenance' 47 - " 51 + :title="title" 48 52 > 49 53 <span 50 54 class="i-solar-shield-check-outline shrink-0" 51 55 :class="compact ? 'w-3.5 h-3.5' : 'w-4 h-4'" 52 56 /> 53 - <span v-if="!compact" class="sr-only sm:not-sr-only">verified</span> 57 + <span v-if="!compact" class="sr-only sm:not-sr-only">{{ 58 + t('badges.provenance.verified') 59 + }}</span> 54 60 </span> 55 61 </template>
+2 -2
app/components/ScrollToTop.vue
··· 37 37 v-if="isActive && supportsScrollStateQueries" 38 38 type="button" 39 39 class="scroll-to-top-css fixed bottom-4 right-4 z-50 w-12 h-12 bg-bg-elevated border border-border rounded-full shadow-lg md:hidden flex items-center justify-center text-fg-muted hover:text-fg transition-colors active:scale-95" 40 - aria-label="Scroll to top" 40 + :aria-label="$t('common.scroll_to_top')" 41 41 @click="scrollToTop" 42 42 > 43 43 <span class="i-carbon-arrow-up w-5 h-5" aria-hidden="true" /> ··· 57 57 v-if="isActive && isMounted && isVisible" 58 58 type="button" 59 59 class="fixed bottom-4 right-4 z-50 w-12 h-12 bg-bg-elevated border border-border rounded-full shadow-lg md:hidden flex items-center justify-center text-fg-muted hover:text-fg transition-colors active:scale-95" 60 - aria-label="Scroll to top" 60 + :aria-label="$t('common.scroll_to_top')" 61 61 @click="scrollToTop" 62 62 > 63 63 <span class="i-carbon-arrow-up w-5 h-5" aria-hidden="true" />
+40 -5
app/components/SettingsMenu.vue
··· 2 2 import { onKeyStroke, onClickOutside } from '@vueuse/core' 3 3 4 4 const { settings } = useSettings() 5 + const { locale, locales, setLocale } = useI18n() 6 + 7 + const availableLocales = computed(() => 8 + locales.value.map(l => (typeof l === 'string' ? { code: l, name: l } : l)), 9 + ) 5 10 6 11 const isOpen = ref(false) 7 12 const menuRef = useTemplateRef('menuRef') ··· 46 51 class="link-subtle font-mono text-sm inline-flex items-center justify-center gap-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded" 47 52 :aria-expanded="isOpen" 48 53 aria-haspopup="menu" 49 - aria-label="Settings" 54 + :aria-label="$t('nav.settings')" 50 55 aria-keyshortcuts="," 51 56 @click="toggle" 52 57 > 53 58 <span class="i-carbon-settings w-4 h-4 sm:hidden" aria-hidden="true" /> 54 - <span class="hidden sm:inline">settings</span> 59 + <span class="hidden sm:inline">{{ $t('nav.settings') }}</span> 55 60 <kbd 56 61 class="hidden sm:inline-flex items-center justify-center w-5 h-5 text-xs bg-bg-muted border border-border rounded" 57 62 aria-hidden="true" ··· 76 81 class="absolute right-0 top-full mt-2 w-64 bg-bg-elevated border border-border rounded-lg shadow-lg z-50 overflow-hidden" 77 82 > 78 83 <div class="px-3 py-2 border-b border-border"> 79 - <h2 class="text-xs text-fg-subtle uppercase tracking-wider">Settings</h2> 84 + <h2 class="text-xs text-fg-subtle uppercase tracking-wider">{{ $t('nav.settings') }}</h2> 80 85 </div> 81 86 82 87 <div class="p-2 space-y-1"> ··· 88 93 :aria-checked="settings.relativeDates" 89 94 @click="settings.relativeDates = !settings.relativeDates" 90 95 > 91 - <span class="text-sm text-fg select-none">Relative dates</span> 96 + <span class="text-sm text-fg select-none">{{ $t('settings.relative_dates') }}</span> 92 97 <span 93 98 class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border-2 border-transparent transition-[background-color] duration-200 ease-in-out motion-reduce:transition-none" 94 99 :class="settings.relativeDates ? 'bg-fg' : 'bg-bg-subtle'" ··· 111 116 :aria-checked="settings.includeTypesInInstall" 112 117 @click="settings.includeTypesInInstall = !settings.includeTypesInInstall" 113 118 > 114 - <span class="text-sm text-fg select-none">Include @types in install</span> 119 + <span class="text-sm text-fg select-none">{{ $t('settings.include_types') }}</span> 115 120 <span 116 121 class="relative inline-flex h-5 w-9 shrink-0 items-center rounded-full border-2 border-transparent transition-[background-color] duration-200 ease-in-out motion-reduce:transition-none" 117 122 :class="settings.includeTypesInInstall ? 'bg-fg' : 'bg-bg-subtle'" ··· 127 132 /> 128 133 </span> 129 134 </button> 135 + 136 + <!-- Language selector --> 137 + <div class="pt-2 mt-2 border-t border-border"> 138 + <div class="px-2 py-1"> 139 + <label for="language-select" class="text-xs text-fg-subtle uppercase tracking-wider"> 140 + {{ $t('settings.language') }} 141 + </label> 142 + </div> 143 + <div class="px-2 py-1"> 144 + <select 145 + id="language-select" 146 + :value="locale" 147 + class="w-full bg-bg-muted border border-border rounded-md px-2 py-1.5 text-sm text-fg focus:outline-none focus:ring-2 focus:ring-fg/50 cursor-pointer" 148 + @change="setLocale(($event.target as HTMLSelectElement).value as typeof locale)" 149 + > 150 + <option v-for="loc in availableLocales" :key="loc.code" :value="loc.code"> 151 + {{ loc.name }} 152 + </option> 153 + </select> 154 + </div> 155 + <a 156 + href="https://github.com/npmx-dev/npmx.dev/tree/main/i18n/locales" 157 + target="_blank" 158 + rel="noopener noreferrer" 159 + class="flex items-center gap-1.5 px-2 py-1.5 text-xs text-fg-muted hover:text-fg transition-colors" 160 + > 161 + <span class="i-carbon-translate w-3.5 h-3.5" aria-hidden="true" /> 162 + {{ $t('settings.help_translate') }} 163 + </a> 164 + </div> 130 165 </div> 131 166 </div> 132 167 </Transition>
+8 -4
app/components/UserCombobox.vue
··· 143 143 :id="inputId" 144 144 v-model="inputValue" 145 145 type="text" 146 - :placeholder="placeholder ?? 'username…'" 146 + :placeholder="placeholder ?? $t('user.combobox.default_placeholder')" 147 147 :disabled="disabled" 148 148 autocomplete="off" 149 149 spellcheck="false" ··· 176 176 :id="listboxId" 177 177 ref="listRef" 178 178 role="listbox" 179 - :aria-label="label ?? 'User suggestions'" 179 + :aria-label="label ?? $t('user.combobox.suggestions_label')" 180 180 class="absolute z-50 w-full mt-1 py-1 bg-bg-elevated border border-border rounded shadow-lg max-h-48 overflow-y-auto" 181 181 > 182 182 <!-- Suggestions from org --> ··· 209 209 class="i-carbon-information w-3 h-3 inline-block mr-1 align-middle" 210 210 aria-hidden="true" 211 211 /> 212 - Press Enter to add @{{ inputValue.trim().replace(/^@/, '') }} 213 - <span class="text-amber-400">(will also add to org)</span> 212 + {{ 213 + $t('user.combobox.press_enter_to_add', { 214 + username: inputValue.trim().replace(/^@/, ''), 215 + }) 216 + }} 217 + <span class="text-amber-400">{{ $t('user.combobox.add_to_org_hint') }}</span> 214 218 </li> 215 219 </ul> 216 220 </Transition>
+19 -16
app/pages/@[org].vue
··· 2 2 import { formatNumber } from '#imports' 3 3 import { debounce } from 'perfect-debounce' 4 4 5 + const { t } = useI18n() 6 + 5 7 definePageMeta({ 6 8 name: 'org', 7 9 alias: ['/org/:org()'], ··· 42 44 if (status.value === 'error' && error.value?.statusCode === 404) { 43 45 throw createError({ 44 46 statusCode: 404, 45 - statusMessage: 'Organization not found', 46 - message: `The organization "@${orgName.value}" does not exist on npm`, 47 + statusMessage: t('org.page.not_found'), 48 + message: t('org.page.not_found_message', { name: orgName.value }), 47 49 }) 48 50 } 49 51 ··· 133 135 <div> 134 136 <h1 class="font-mono text-2xl sm:text-3xl font-medium">@{{ orgName }}</h1> 135 137 <p v-if="status === 'success'" class="text-fg-muted text-sm mt-1"> 136 - {{ formatNumber(packageCount) }} public package{{ packageCount === 1 ? '' : 's' }} 138 + {{ t('org.public_packages', { count: formatNumber(packageCount) }, packageCount) }} 137 139 </p> 138 140 </div> 139 141 </div> ··· 147 149 class="link-subtle font-mono text-sm inline-flex items-center gap-1.5" 148 150 > 149 151 <span class="i-carbon-cube w-4 h-4" /> 150 - view on npm 152 + {{ t('common.view_on_npm') }} 151 153 </a> 152 154 </nav> 153 155 </header> ··· 167 169 " 168 170 @click="activeTab = 'members'" 169 171 > 170 - Members 172 + {{ t('org.page.members_tab') }} 171 173 </button> 172 174 <button 173 175 type="button" ··· 179 181 " 180 182 @click="activeTab = 'teams'" 181 183 > 182 - Teams 184 + {{ t('org.page.teams_tab') }} 183 185 </button> 184 186 </div> 185 187 ··· 190 192 </ClientOnly> 191 193 192 194 <!-- Loading state --> 193 - <LoadingSpinner v-if="status === 'pending'" text="Loading packages..." /> 195 + <LoadingSpinner v-if="status === 'pending'" :text="t('common.loading_packages')" /> 194 196 195 197 <!-- Error state --> 196 198 <div v-else-if="status === 'error'" role="alert" class="py-12 text-center"> 197 199 <p class="text-fg-muted mb-4"> 198 - {{ error?.message ?? 'Failed to load organization packages' }} 200 + {{ error?.message ?? t('org.page.failed_to_load') }} 199 201 </p> 200 - <NuxtLink to="/" class="btn"> Go back home </NuxtLink> 202 + <NuxtLink to="/" class="btn">{{ t('common.go_back_home') }}</NuxtLink> 201 203 </div> 202 204 203 205 <!-- Empty state --> 204 206 <div v-else-if="packageCount === 0" class="py-12 text-center"> 205 207 <p class="text-fg-muted font-mono"> 206 - No public packages found for <span class="text-fg">@{{ orgName }}</span> 208 + {{ t('org.page.no_packages') }} <span class="text-fg">@{{ orgName }}</span> 207 209 </p> 208 210 <p class="text-fg-subtle text-sm mt-2"> 209 - This organization may not exist or has no public packages. 211 + {{ t('org.page.no_packages_hint') }} 210 212 </p> 211 213 </div> 212 214 213 215 <!-- Package list --> 214 - <section v-else-if="packages.length > 0" aria-label="Organization packages"> 215 - <h2 class="text-xs text-fg-subtle uppercase tracking-wider mb-4">Packages</h2> 216 + <section v-else-if="packages.length > 0" :aria-label="t('org.page.packages_title')"> 217 + <h2 class="text-xs text-fg-subtle uppercase tracking-wider mb-4"> 218 + {{ t('org.page.packages_title') }} 219 + </h2> 216 220 217 221 <!-- Filter and sort controls --> 218 222 <PackageListControls 219 223 v-model:filter="filterText" 220 224 v-model:sort="sortOption" 221 - :placeholder="`Filter ${packageCount} packages...`" 225 + :placeholder="t('org.page.filter_placeholder', { count: packageCount })" 222 226 :total-count="packageCount" 223 227 :filtered-count="filteredCount" 224 228 /> ··· 228 232 v-if="filteredAndSortedPackages.length === 0" 229 233 class="text-fg-muted py-8 text-center font-mono" 230 234 > 231 - No packages match "<span class="text-fg">{{ filterText }}</span 232 - >" 235 + {{ t('org.page.no_match', { query: filterText }) }} 233 236 </p> 234 237 235 238 <PackageList v-else :results="filteredAndSortedPackages" />
+60 -44
app/pages/[...package].vue
··· 6 6 import { joinURL } from 'ufo' 7 7 import { areUrlsEquivalent } from '#shared/utils/url' 8 8 9 + const { t } = useI18n() 10 + 9 11 definePageMeta({ 10 12 name: 'package', 11 13 alias: ['/package/:package(.*)*'], ··· 431 433 <NuxtLink 432 434 v-if="resolvedVersion !== requestedVersion" 433 435 :to="`/${pkg.name}/v/${displayVersion.version}`" 434 - title="View permalink for this version" 436 + :title="t('package.view_permalink')" 435 437 >{{ displayVersion.version }}</NuxtLink 436 438 > 437 439 <span v-else>v{{ displayVersion.version }}</span> ··· 442 444 target="_blank" 443 445 rel="noopener noreferrer" 444 446 class="inline-flex items-center justify-center gap-1.5 text-fg-muted hover:text-fg transition-colors duration-200 min-w-6 min-h-6" 445 - title="Verified provenance" 447 + :title="t('package.verified_provenance')" 446 448 > 447 449 <span 448 450 class="i-solar-shield-check-outline w-3.5 h-3.5 shrink-0" ··· 456 458 displayVersion.version !== latestVersion.version 457 459 " 458 460 class="text-fg-subtle text-sm shrink-0" 459 - >(not latest)</span 461 + >{{ t('package.not_latest') }}</span 460 462 > 461 463 </span> 462 464 ··· 481 483 target="_blank" 482 484 rel="noopener noreferrer" 483 485 class="link-subtle font-mono text-sm inline-flex items-center gap-1.5 ml-auto shrink-0 self-center" 484 - title="View on npm" 486 + :title="t('common.view_on_npm')" 485 487 > 486 488 <span class="i-carbon-logo-npm w-4 h-4" aria-hidden="true" /> 487 489 <span class="hidden sm:inline">npm</span> 488 - <span class="sr-only sm:hidden">View on npm</span> 490 + <span class="sr-only sm:hidden">{{ t('common.view_on_npm') }}</span> 489 491 </a> 490 492 </div> 491 493 ··· 498 500 > 499 501 <MarkdownText :text="pkg.description" /> 500 502 </p> 501 - <p v-else class="text-fg-subtle text-base m-0 italic">No description provided</p> 503 + <p v-else class="text-fg-subtle text-base m-0 italic"> 504 + {{ t('package.no_description') }} 505 + </p> 502 506 <!-- Fade overlay with show more button - only when collapsed and overflowing --> 503 507 <div 504 508 v-if="pkg.description && descriptionOverflows && !descriptionExpanded" ··· 507 511 <button 508 512 type="button" 509 513 class="font-mono text-xs text-fg-muted hover:text-fg bg-bg px-1 transition-colors duration-200 rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 510 - aria-label="Show full description" 514 + :aria-label="t('package.show_full_description')" 511 515 @click="descriptionExpanded = true" 512 516 > 513 - show more 517 + {{ t('common.show_more') }} 514 518 </button> 515 519 </div> 516 520 </div> ··· 523 527 <h2 class="font-medium mb-2"> 524 528 {{ 525 529 deprecationNotice.type === 'package' 526 - ? 'This package has been deprecated.' 527 - : 'This version has been deprecated.' 530 + ? t('package.deprecation.package') 531 + : t('package.deprecation.version') 528 532 }} 529 533 </h2> 530 534 <p v-if="deprecationNotice.message" class="text-base m-0"> 531 535 <MarkdownText :text="deprecationNotice.message" /> 532 536 </p> 533 - <p v-else class="text-base m-0 italic">No reason provided</p> 537 + <p v-else class="text-base m-0 italic">{{ t('package.deprecation.no_reason') }}</p> 534 538 </div> 535 539 536 540 <!-- Stats grid --> 537 541 <dl class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-3 sm:gap-4 mt-4 sm:mt-6"> 538 542 <div v-if="pkg.license" class="space-y-1"> 539 - <dt class="text-xs text-fg-subtle uppercase tracking-wider">License</dt> 543 + <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 544 + {{ t('package.stats.license') }} 545 + </dt> 540 546 <dd class="font-mono text-sm text-fg"> 541 547 <LicenseDisplay :license="pkg.license" /> 542 548 </dd> 543 549 </div> 544 550 545 551 <div v-if="downloads" class="space-y-1"> 546 - <dt class="text-xs text-fg-subtle uppercase tracking-wider">Weekly</dt> 552 + <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 553 + {{ t('package.stats.weekly') }} 554 + </dt> 547 555 <dd class="font-mono text-sm text-fg flex items-center justify-start gap-2"> 548 556 {{ formatNumber(downloads.downloads) }} 549 557 <a ··· 551 559 target="_blank" 552 560 rel="noopener noreferrer" 553 561 class="text-fg-subtle hover:text-fg transition-colors duration-200 inline-flex items-center justify-center min-w-6 min-h-6 -m-1 p-1" 554 - title="View download trends" 562 + :title="t('package.stats.view_download_trends')" 555 563 > 556 564 <span class="i-carbon-chart-line w-3.5 h-3.5 inline-block" aria-hidden="true" /> 557 - <span class="sr-only">View download trends</span> 565 + <span class="sr-only">{{ t('package.stats.view_download_trends') }}</span> 558 566 </a> 559 567 </dd> 560 568 </div> 561 569 562 570 <div class="space-y-1"> 563 - <dt class="text-xs text-fg-subtle uppercase tracking-wider">Deps</dt> 571 + <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 572 + {{ t('package.stats.deps') }} 573 + </dt> 564 574 <dd class="font-mono text-sm text-fg flex items-center justify-start gap-2"> 565 575 {{ getDependencyCount(displayVersion) }} 566 576 <a ··· 569 579 target="_blank" 570 580 rel="noopener noreferrer" 571 581 class="text-fg-subtle hover:text-fg transition-colors duration-200 inline-flex items-center justify-center min-w-6 min-h-6 -m-1 p-1" 572 - title="View dependency graph" 582 + :title="t('package.stats.view_dependency_graph')" 573 583 > 574 584 <span class="i-carbon-network-3 w-3.5 h-3.5 inline-block" aria-hidden="true" /> 575 - <span class="sr-only">View dependency graph</span> 585 + <span class="sr-only">{{ t('package.stats.view_dependency_graph') }}</span> 576 586 </a> 577 587 578 588 <a ··· 581 591 target="_blank" 582 592 rel="noopener noreferrer" 583 593 class="text-fg-subtle hover:text-fg transition-colors duration-200 inline-flex items-center justify-center min-w-6 min-h-6 -m-1 p-1" 584 - title="Inspect dependency tree on node-modules.dev" 594 + :title="t('package.stats.inspect_dependency_tree')" 585 595 > 586 596 <span 587 597 class="i-solar-eye-scan-outline w-3.5 h-3.5 inline-block" 588 598 aria-hidden="true" 589 599 /> 590 - <span class="sr-only">Inspect dependency tree</span> 600 + <span class="sr-only">{{ t('package.stats.inspect_dependency_tree') }}</span> 591 601 </a> 592 602 </dd> 593 603 </div> 594 604 595 605 <div class="space-y-1 sm:col-span-2"> 596 606 <dt class="text-xs text-fg-subtle uppercase tracking-wider flex items-center gap-1"> 597 - Install Size 607 + {{ t('package.stats.install_size') }} 598 608 <span 599 609 class="i-carbon-information w-3 h-3 text-fg-subtle" 600 610 aria-hidden="true" ··· 630 640 </div> 631 641 632 642 <div v-if="pkg.time?.modified" class="space-y-1"> 633 - <dt class="text-xs text-fg-subtle uppercase tracking-wider sm:text-right">Updated</dt> 643 + <dt class="text-xs text-fg-subtle uppercase tracking-wider sm:text-right"> 644 + {{ t('package.stats.updated') }} 645 + </dt> 634 646 <dd class="font-mono text-sm text-fg sm:text-right"> 635 647 <DateTime :datetime="pkg.time.modified" date-style="medium" /> 636 648 </dd> ··· 651 663 <span v-if="repoRef"> 652 664 {{ repoRef.owner }}<span class="opacity-50">/</span>{{ repoRef.repo }} 653 665 </span> 654 - <span v-else>repo</span> 666 + <span v-else>{{ t('package.links.repo') }}</span> 655 667 </a> 656 668 </li> 657 669 <li v-if="repositoryUrl && repoMeta && starsLink"> ··· 673 685 class="link-subtle font-mono text-sm inline-flex items-center gap-1.5" 674 686 > 675 687 <span class="i-carbon-link w-4 h-4" aria-hidden="true" /> 676 - homepage 688 + {{ t('package.links.homepage') }} 677 689 </a> 678 690 </li> 679 691 <li v-if="displayVersion?.bugs?.url"> ··· 684 696 class="link-subtle font-mono text-sm inline-flex items-center gap-1.5" 685 697 > 686 698 <span class="i-carbon-warning w-4 h-4" aria-hidden="true" /> 687 - issues 699 + {{ t('package.links.issues') }} 688 700 </a> 689 701 </li> 690 702 ··· 698 710 <span class="i-carbon-fork w-4 h-4" aria-hidden="true" /> 699 711 <span> 700 712 {{ formatCompactNumber(forks, { decimals: 1 }) }} 701 - {{ forks === 1 ? 'fork' : 'forks' }} 713 + {{ t('package.links.forks', { count: forks }, forks) }} 702 714 </span> 703 715 </a> 704 716 </li> ··· 709 721 target="_blank" 710 722 rel="noopener noreferrer" 711 723 class="link-subtle font-mono text-sm inline-flex items-center gap-1.5" 712 - title="Also available on JSR" 724 + :title="t('badges.jsr.title')" 713 725 > 714 726 <span class="i-simple-icons-jsr w-4 h-4" aria-hidden="true" /> 715 - jsr 727 + {{ t('package.links.jsr') }} 716 728 </a> 717 729 </li> 718 730 <li class="sm:flex-grow"> ··· 723 735 class="link-subtle font-mono text-sm inline-flex items-center gap-1.5" 724 736 > 725 737 <span class="i-simple-icons-socket w-4 h-4" aria-hidden="true" /> 726 - socket.dev 738 + {{ t('package.links.socket') }} 727 739 </a> 728 740 </li> 729 741 ··· 736 748 class="link-subtle font-mono text-sm inline-flex items-center gap-1.5" 737 749 aria-keyshortcuts="." 738 750 > 739 - code 751 + {{ t('package.links.code') }} 740 752 <kbd 741 753 class="hidden sm:inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded" 742 754 aria-hidden="true" ··· 760 772 <section aria-labelledby="install-heading" class="mb-8"> 761 773 <div class="flex flex-wrap items-center justify-between mb-3"> 762 774 <h2 id="install-heading" class="text-xs text-fg-subtle uppercase tracking-wider"> 763 - Install 775 + {{ t('package.install.title') }} 764 776 </h2> 765 777 <!-- Package manager tabs --> 766 778 <div 767 779 class="flex items-center gap-1 p-0.5 bg-bg-subtle border border-border rounded-md" 768 780 role="tablist" 769 - aria-label="Package manager" 781 + :aria-label="t('package.install.pm_label')" 770 782 > 771 783 <ClientOnly> 772 784 <button ··· 838 850 v-if="typesPackageName" 839 851 :to="`/${typesPackageName}`" 840 852 class="text-fg-subtle hover:text-fg-muted text-xs transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded" 841 - title="View @types package" 853 + :title="t('package.install.view_types', { package: typesPackageName })" 842 854 > 843 855 <span class="i-carbon-arrow-right w-3 h-3" aria-hidden="true" /> 844 856 <span class="sr-only">View {{ typesPackageName }}</span> ··· 849 861 <button 850 862 type="button" 851 863 class="absolute top-3 right-3 px-2 py-1 font-mono text-xs text-fg-muted bg-bg-subtle/80 border border-border rounded transition-colors duration-200 hover:(text-fg border-border-hover) active:scale-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 852 - aria-label="Copy install command" 864 + :aria-label="t('package.install.copy_command')" 853 865 @click="copyInstallCommand" 854 866 > 855 - <span aria-live="polite">{{ copied ? 'copied!' : 'copy' }}</span> 867 + <span aria-live="polite">{{ copied ? t('common.copied') : t('common.copy') }}</span> 856 868 </button> 857 869 </div> 858 870 </section> ··· 863 875 <div class="lg:col-span-2 order-2 lg:order-1 min-w-0"> 864 876 <section aria-labelledby="readme-heading"> 865 877 <h2 id="readme-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-4"> 866 - Readme 878 + {{ t('package.readme.title') }} 867 879 </h2> 868 880 <!-- eslint-disable vue/no-v-html -- HTML is sanitized server-side --> 869 881 <div ··· 872 884 v-html="readmeData.html" 873 885 /> 874 886 <p v-else class="text-fg-subtle italic"> 875 - No README available. 876 - <a v-if="repositoryUrl" :href="repositoryUrl" rel="noopener noreferrer" class="link" 877 - >View on GitHub</a 887 + {{ t('package.readme.no_readme') }} 888 + <a 889 + v-if="repositoryUrl" 890 + :href="repositoryUrl" 891 + rel="noopener noreferrer" 892 + class="link" 893 + >{{ t('package.readme.view_on_github') }}</a 878 894 > 879 895 </p> 880 896 </section> ··· 893 909 <!-- Keywords --> 894 910 <section v-if="displayVersion?.keywords?.length" aria-labelledby="keywords-heading"> 895 911 <h2 id="keywords-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-3"> 896 - Keywords 912 + {{ t('package.keywords_title') }} 897 913 </h2> 898 914 <ul class="flex flex-wrap gap-1.5 list-none m-0 p-0"> 899 915 <li v-for="keyword in displayVersion.keywords.slice(0, 15)" :key="keyword"> ··· 923 939 id="compatibility-heading" 924 940 class="text-xs text-fg-subtle uppercase tracking-wider mb-3" 925 941 > 926 - Compatibility 942 + {{ t('package.compatibility') }} 927 943 </h2> 928 944 <dl class="space-y-2"> 929 945 <div v-if="displayVersion.engines.node" class="flex justify-between gap-4 py-1"> ··· 978 994 979 995 <!-- Error state --> 980 996 <div v-else-if="status === 'error'" role="alert" class="py-20 text-center"> 981 - <h1 class="font-mono text-2xl font-medium mb-4">Package Not Found</h1> 997 + <h1 class="font-mono text-2xl font-medium mb-4">{{ t('package.not_found') }}</h1> 982 998 <p class="text-fg-muted mb-8"> 983 - {{ error?.message ?? 'The package could not be found.' }} 999 + {{ error?.message ?? t('package.not_found_message') }} 984 1000 </p> 985 - <NuxtLink to="/" class="btn"> Go back home </NuxtLink> 1001 + <NuxtLink to="/" class="btn">{{ t('common.go_back_home') }}</NuxtLink> 986 1002 </div> 987 1003 </main> 988 1004 </template>
+20 -19
app/pages/code/[...path].vue
··· 5 5 PackageFileContentResponse, 6 6 } from '#shared/types' 7 7 8 + const { t } = useI18n() 9 + 8 10 definePageMeta({ 9 11 name: 'code', 10 12 alias: ['/package/code/:path(.*)*'], ··· 306 308 </NuxtLink> 307 309 <!-- Version selector --> 308 310 <div v-if="version && availableVersions.length > 0" class="relative shrink-0"> 309 - <label for="version-select" class="sr-only">Select version</label> 311 + <label for="version-select" class="sr-only">{{ t('code.select_version') }}</label> 310 312 <select 311 313 id="version-select" 312 314 :value="version" ··· 343 345 :to="getCodeUrl()" 344 346 class="text-fg-muted hover:text-fg transition-colors shrink-0" 345 347 > 346 - root 348 + {{ t('code.root') }} 347 349 </NuxtLink> 348 - <span v-else class="text-fg shrink-0">root</span> 350 + <span v-else class="text-fg shrink-0">{{ t('code.root') }}</span> 349 351 <template v-for="(crumb, i) in breadcrumbs" :key="crumb.path"> 350 352 <span class="text-fg-subtle">/</span> 351 353 <NuxtLink ··· 363 365 364 366 <!-- Error: no version --> 365 367 <div v-if="!version" class="container py-20 text-center"> 366 - <p class="text-fg-muted mb-4">Version is required to browse code</p> 367 - <NuxtLink :to="packageRoute()" class="btn"> Go to package </NuxtLink> 368 + <p class="text-fg-muted mb-4">{{ t('code.version_required') }}</p> 369 + <NuxtLink :to="packageRoute()" class="btn">{{ t('code.go_to_package') }}</NuxtLink> 368 370 </div> 369 371 370 372 <!-- Loading state --> 371 373 <div v-else-if="treeStatus === 'pending'" class="container py-20 text-center"> 372 374 <div class="i-svg-spinners-ring-resize w-8 h-8 mx-auto text-fg-muted" /> 373 - <p class="mt-4 text-fg-muted">Loading file tree...</p> 375 + <p class="mt-4 text-fg-muted">{{ t('code.loading_tree') }}</p> 374 376 </div> 375 377 376 378 <!-- Error state --> 377 379 <div v-else-if="treeStatus === 'error'" class="container py-20 text-center" role="alert"> 378 - <p class="text-fg-muted mb-4">Failed to load files for this package version</p> 379 - <NuxtLink :to="packageRoute(version)" class="btn"> Back to package </NuxtLink> 380 + <p class="text-fg-muted mb-4">{{ t('code.failed_to_load_tree') }}</p> 381 + <NuxtLink :to="packageRoute(version)" class="btn">{{ t('code.back_to_package') }}</NuxtLink> 380 382 </div> 381 383 382 384 <!-- Main content: file tree + file viewer --> ··· 402 404 class="sticky top-0 bg-bg border-b border-border px-4 py-2 flex items-center justify-between" 403 405 > 404 406 <div class="flex items-center gap-3 text-sm"> 405 - <span class="text-fg-muted">{{ fileContent.lines }} lines</span> 407 + <span class="text-fg-muted">{{ t('code.lines', { count: fileContent.lines }) }}</span> 406 408 <span v-if="currentNode?.size" class="text-fg-subtle">{{ 407 409 formatBytes(currentNode.size) 408 410 }}</span> ··· 414 416 class="px-2 py-1 font-mono text-xs text-fg-muted bg-bg-subtle border border-border rounded hover:text-fg hover:border-border-hover transition-colors" 415 417 @click="copyPermalink" 416 418 > 417 - Copy link 419 + {{ t('code.copy_link') }} 418 420 </button> 419 421 <a 420 422 :href="`https://cdn.jsdelivr.net/npm/${packageName}@${version}/${filePath}`" ··· 422 424 rel="noopener noreferrer" 423 425 class="px-2 py-1 font-mono text-xs text-fg-muted bg-bg-subtle border border-border rounded hover:text-fg hover:border-border-hover transition-colors inline-flex items-center gap-1" 424 426 > 425 - Raw 427 + {{ t('code.raw') }} 426 428 <span class="i-carbon-launch w-3 h-3" /> 427 429 </a> 428 430 </div> ··· 438 440 <!-- File too large warning --> 439 441 <div v-else-if="isViewingFile && isFileTooLarge" class="py-20 text-center"> 440 442 <div class="i-carbon-document w-12 h-12 mx-auto text-fg-subtle mb-4" /> 441 - <p class="text-fg-muted mb-2">File too large to preview</p> 443 + <p class="text-fg-muted mb-2">{{ t('code.file_too_large') }}</p> 442 444 <p class="text-fg-subtle text-sm mb-4"> 443 - {{ formatBytes(currentNode?.size ?? 0) }} exceeds the 500KB limit for syntax 444 - highlighting 445 + {{ t('code.file_size_warning', { size: formatBytes(currentNode?.size ?? 0) }) }} 445 446 </p> 446 447 <a 447 448 :href="`https://cdn.jsdelivr.net/npm/${packageName}@${version}/${filePath}`" ··· 449 450 rel="noopener noreferrer" 450 451 class="btn inline-flex items-center gap-2" 451 452 > 452 - View raw file 453 + {{ t('code.view_raw') }} 453 454 <span class="i-carbon-launch w-4 h-4" /> 454 455 </a> 455 456 </div> ··· 459 460 v-else-if="filePath && fileStatus === 'pending'" 460 461 class="flex min-h-full" 461 462 aria-busy="true" 462 - aria-label="Loading file content" 463 + :aria-label="t('common.loading')" 463 464 > 464 465 <!-- Fake line numbers column --> 465 466 <div class="shrink-0 bg-bg-subtle border-r border-border w-14 py-0"> ··· 495 496 <!-- Error loading file --> 496 497 <div v-else-if="filePath && fileStatus === 'error'" class="py-20 text-center" role="alert"> 497 498 <div class="i-carbon-warning-alt w-8 h-8 mx-auto text-fg-subtle mb-4" /> 498 - <p class="text-fg-muted mb-2">Failed to load file</p> 499 - <p class="text-fg-subtle text-sm mb-4">The file may be too large or unavailable</p> 499 + <p class="text-fg-muted mb-2">{{ t('code.failed_to_load') }}</p> 500 + <p class="text-fg-subtle text-sm mb-4">{{ t('code.unavailable_hint') }}</p> 500 501 <a 501 502 :href="`https://cdn.jsdelivr.net/npm/${packageName}@${version}/${filePath}`" 502 503 target="_blank" 503 504 rel="noopener noreferrer" 504 505 class="btn inline-flex items-center gap-2" 505 506 > 506 - View raw file 507 + {{ t('code.view_raw') }} 507 508 <span class="i-carbon-launch w-4 h-4" /> 508 509 </a> 509 510 </div>
+10 -8
app/pages/index.vue
··· 10 10 }) 11 11 } 12 12 13 + const { t } = useI18n() 13 14 useSeoMeta({ 14 - title: 'npmx - Package Browser for the npm Registry', 15 - description: 16 - 'A better browser for the npm registry. Search, browse, and explore packages with a modern interface.', 15 + title: () => t('seo.home.title'), 16 + description: () => t('seo.home.description'), 17 17 }) 18 18 19 19 defineOgImageComponent('Default') ··· 34 34 class="text-fg-muted text-lg sm:text-xl max-w-md mb-12 animate-slide-up animate-fill-both" 35 35 style="animation-delay: 0.1s" 36 36 > 37 - a better browser for the npm registry 37 + {{ $t('tagline') }} 38 38 </p> 39 39 40 40 <!-- Search form with micro-interactions --> ··· 43 43 style="animation-delay: 0.2s" 44 44 > 45 45 <form role="search" class="relative" @submit.prevent="handleSearch"> 46 - <label for="home-search" class="sr-only">Search npm packages</label> 46 + <label for="home-search" class="sr-only"> 47 + {{ $t('search.label') }} 48 + </label> 47 49 48 50 <!-- Search input with glow effect on focus --> 49 51 <div class="relative group" :class="{ 'is-focused': isSearchFocused }"> ··· 64 66 v-model="searchQuery" 65 67 type="search" 66 68 name="q" 67 - placeholder="search packages..." 69 + :placeholder="$t('search.placeholder')" 68 70 autocomplete="off" 69 71 class="w-full bg-bg-subtle border border-border rounded-lg pl-8 pr-24 py-4 font-mono text-base text-fg placeholder:text-fg-subtle transition-all duration-300 focus:(border-border-hover outline-none)" 70 72 @input="handleSearch" ··· 76 78 type="submit" 77 79 class="absolute right-2 px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-all duration-200 hover:bg-fg/90 active:scale-95" 78 80 > 79 - search 81 + {{ $t('search.button') }} 80 82 </button> 81 83 </div> 82 84 </div> ··· 86 88 87 89 <!-- Popular packages --> 88 90 <nav 89 - aria-label="Popular packages" 91 + :aria-label="$t('nav.popular_packages')" 90 92 class="pb-20 text-center animate-fade-in animate-fill-both" 91 93 style="animation-delay: 0.3s" 92 94 >
+17 -16
app/pages/search.vue
··· 3 3 import { debounce } from 'perfect-debounce' 4 4 import { isValidNewPackageName, checkPackageExists } from '~/utils/package-name' 5 5 6 + const { t } = useI18n() 6 7 const route = useRoute() 7 8 const router = useRouter() 8 9 ··· 345 346 346 347 <search> 347 348 <form role="search" class="relative" @submit.prevent> 348 - <label for="search-input" class="sr-only">Search npm packages</label> 349 + <label for="search-input" class="sr-only">{{ t('search.label') }}</label> 349 350 350 351 <div class="relative group" :class="{ 'is-focused': isSearchFocused }"> 351 352 <!-- Subtle glow effect --> ··· 366 367 v-model="inputValue" 367 368 type="search" 368 369 name="q" 369 - placeholder="search packages…" 370 + :placeholder="t('search.placeholder')" 370 371 autocapitalize="off" 371 372 autocomplete="off" 372 373 autocorrect="off" ··· 380 381 v-show="inputValue" 381 382 type="button" 382 383 class="absolute right-3 text-fg-subtle hover:text-fg transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded" 383 - aria-label="Clear search" 384 + :aria-label="t('search.clear')" 384 385 @click="inputValue = ''" 385 386 > 386 387 <span class="i-carbon-close-large block w-3.5 h-3.5" aria-hidden="true" /> 387 388 </button> 388 389 <!-- Hidden submit button for accessibility (form must have submit button per WCAG) --> 389 - <button type="submit" class="sr-only">Search</button> 390 + <button type="submit" class="sr-only">{{ t('search.button') }}</button> 390 391 </div> 391 392 </div> 392 393 </form> ··· 398 399 <div class="container pt-20 pb-6"> 399 400 <section v-if="query" aria-label="Search results" @keydown="handleResultsKeydown"> 400 401 <!-- Initial loading (only after user interaction, not during view transition) --> 401 - <LoadingSpinner v-if="showSearching" text="Searching…" /> 402 + <LoadingSpinner v-if="showSearching" :text="t('search.searching')" /> 402 403 403 404 <div v-else-if="visibleResults"> 404 405 <!-- Claim prompt - shown at top when valid name but no exact match --> ··· 408 409 > 409 410 <div class="flex-1 min-w-0"> 410 411 <p class="font-mono text-sm text-fg"> 411 - "<span class="text-fg font-medium">{{ query }}</span 412 - >" is not taken 412 + {{ t('search.not_taken', { name: query }) }} 413 413 </p> 414 - <p class="text-xs text-fg-muted mt-0.5">Claim this package name on npm</p> 414 + <p class="text-xs text-fg-muted mt-0.5">{{ t('search.claim_prompt') }}</p> 415 415 </div> 416 416 <button 417 417 type="button" 418 418 class="shrink-0 px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-colors duration-200 hover:bg-fg/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 419 419 @click="claimModalOpen = true" 420 420 > 421 - Claim "{{ query }}" 421 + {{ t('search.claim_button', { name: query }) }} 422 422 </button> 423 423 </div> 424 424 ··· 427 427 role="status" 428 428 class="text-fg-muted text-sm mb-6 font-mono" 429 429 > 430 - Found <span class="text-fg">{{ formatNumber(visibleResults.total) }}</span> packages 431 - <span v-if="status === 'pending'" class="text-fg-subtle">(updating…)</span> 430 + {{ t('search.found_packages', { count: formatNumber(visibleResults.total) }) }} 431 + <span v-if="status === 'pending'" class="text-fg-subtle">{{ 432 + t('search.updating') 433 + }}</span> 432 434 </p> 433 435 434 436 <!-- No results found --> 435 437 <div v-else-if="status !== 'pending'" role="status" class="py-12 text-center"> 436 438 <p class="text-fg-muted font-mono mb-6"> 437 - No packages found for "<span class="text-fg">{{ query }}</span 438 - >" 439 + {{ t('search.no_results', { query }) }} 439 440 </p> 440 441 441 442 <!-- Offer to claim the package name if it's valid --> 442 443 <div v-if="showClaimPrompt" class="max-w-md mx-auto"> 443 444 <div class="p-4 bg-bg-subtle border border-border rounded-lg"> 444 - <p class="text-sm text-fg-muted mb-3">Want to claim this package name?</p> 445 + <p class="text-sm text-fg-muted mb-3">{{ t('search.want_to_claim') }}</p> 445 446 <button 446 447 type="button" 447 448 class="px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md transition-colors duration-200 hover:bg-fg/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 448 449 @click="claimModalOpen = true" 449 450 > 450 - Claim "{{ query }}" 451 + {{ t('search.claim_button', { name: query }) }} 451 452 </button> 452 453 </div> 453 454 </div> ··· 472 473 </section> 473 474 474 475 <section v-else class="py-20 text-center"> 475 - <p class="text-fg-subtle font-mono text-sm">Start typing to search packages</p> 476 + <p class="text-fg-subtle font-mono text-sm">{{ t('search.start_typing') }}</p> 476 477 </section> 477 478 </div> 478 479
+16 -11
app/pages/~[username]/index.vue
··· 2 2 import { formatNumber } from '#imports' 3 3 import { debounce } from 'perfect-debounce' 4 4 5 + const { t } = useI18n() 5 6 const route = useRoute('~username') 6 7 const router = useRouter() 7 8 ··· 187 188 <div> 188 189 <h1 class="font-mono text-2xl sm:text-3xl font-medium">@{{ username }}</h1> 189 190 <p v-if="results?.total" class="text-fg-muted text-sm mt-1"> 190 - {{ formatNumber(results.total) }} public package{{ results.total === 1 ? '' : 's' }} 191 + {{ t('org.public_packages', { count: formatNumber(results.total) }, results.total) }} 191 192 </p> 192 193 </div> 193 194 </div> ··· 201 202 class="link-subtle font-mono text-sm inline-flex items-center gap-1.5" 202 203 > 203 204 <span class="i-carbon-cube w-4 h-4" /> 204 - view on npm 205 + {{ t('common.view_on_npm') }} 205 206 </a> 206 207 </nav> 207 208 </header> 208 209 209 210 <!-- Loading state --> 210 - <LoadingSpinner v-if="status === 'pending' && loadedPages === 1" text="Loading packages..." /> 211 + <LoadingSpinner 212 + v-if="status === 'pending' && loadedPages === 1" 213 + :text="t('common.loading_packages')" 214 + /> 211 215 212 216 <!-- Error state --> 213 217 <div v-else-if="status === 'error'" role="alert" class="py-12 text-center"> 214 218 <p class="text-fg-muted mb-4"> 215 - {{ error?.message ?? 'Failed to load user packages' }} 219 + {{ error?.message ?? t('user.page.failed_to_load') }} 216 220 </p> 217 - <NuxtLink to="/" class="btn"> Go back home </NuxtLink> 221 + <NuxtLink to="/" class="btn">{{ t('common.go_back_home') }}</NuxtLink> 218 222 </div> 219 223 220 224 <!-- Empty state --> 221 225 <div v-else-if="results && results.total === 0" class="py-12 text-center"> 222 226 <p class="text-fg-muted font-mono"> 223 - No public packages found for <span class="text-fg">@{{ username }}</span> 227 + {{ t('user.page.no_packages') }} <span class="text-fg">@{{ username }}</span> 224 228 </p> 225 - <p class="text-fg-subtle text-sm mt-2">This user may not exist or has no public packages.</p> 229 + <p class="text-fg-subtle text-sm mt-2">{{ t('user.page.no_packages_hint') }}</p> 226 230 </div> 227 231 228 232 <!-- Package list --> 229 233 <section v-else-if="results && packages.length > 0" aria-label="User packages"> 230 - <h2 class="text-xs text-fg-subtle uppercase tracking-wider mb-4">Packages</h2> 234 + <h2 class="text-xs text-fg-subtle uppercase tracking-wider mb-4"> 235 + {{ t('user.page.packages_title') }} 236 + </h2> 231 237 232 238 <!-- Filter and sort controls --> 233 239 <PackageListControls 234 240 v-model:filter="filterText" 235 241 v-model:sort="sortOption" 236 - :placeholder="`Filter ${packageCount} packages...`" 242 + :placeholder="t('user.page.filter_placeholder', { count: packageCount })" 237 243 :total-count="packageCount" 238 244 :filtered-count="filteredCount" 239 245 /> ··· 243 249 v-if="filteredAndSortedPackages.length === 0" 244 250 class="text-fg-muted py-8 text-center font-mono" 245 251 > 246 - No packages match "<span class="text-fg">{{ filterText }}</span 247 - >" 252 + {{ t('user.page.no_match', { query: filterText }) }} 248 253 </p> 249 254 250 255 <PackageList
+26 -16
app/pages/~[username]/orgs.vue
··· 1 1 <script setup lang="ts"> 2 + const { t } = useI18n() 2 3 const route = useRoute('~username-orgs') 3 4 4 5 const username = computed(() => route.params.username) ··· 72 73 // Load details for each org in parallel 73 74 await Promise.all(orgs.value.map(org => loadOrgDetails(org))) 74 75 } else { 75 - error.value = 'Failed to load organizations' 76 + error.value = t('header.orgs_dropdown.error') 76 77 } 77 78 } catch (e) { 78 - error.value = e instanceof Error ? e.message : 'Failed to load organizations' 79 + error.value = e instanceof Error ? e.message : t('header.orgs_dropdown.error') 79 80 } finally { 80 81 isLoading.value = false 81 82 } ··· 119 120 </div> 120 121 <div> 121 122 <h1 class="font-mono text-2xl sm:text-3xl font-medium">@{{ username }}</h1> 122 - <p class="text-fg-muted text-sm mt-1">Organizations</p> 123 + <p class="text-fg-muted text-sm mt-1">{{ t('user.orgs_page.title') }}</p> 123 124 </div> 124 125 </div> 125 126 ··· 130 131 class="link-subtle font-mono text-sm inline-flex items-center gap-1.5" 131 132 > 132 133 <span class="i-carbon-arrow-left w-4 h-4" aria-hidden="true" /> 133 - Back to profile 134 + {{ t('user.orgs_page.back_to_profile') }} 134 135 </NuxtLink> 135 136 </nav> 136 137 </header> ··· 138 139 <!-- Not connected state --> 139 140 <ClientOnly> 140 141 <div v-if="!isConnected" class="py-12 text-center"> 141 - <p class="text-fg-muted mb-4">Connect the local CLI to view your organizations.</p> 142 + <p class="text-fg-muted mb-4">{{ t('user.orgs_page.connect_required') }}</p> 142 143 <p class="text-fg-subtle text-sm"> 143 - Run <code class="font-mono bg-bg-subtle px-1.5 py-0.5 rounded">npx @npmx.dev/cli</code> to 144 - get started. 144 + {{ t('user.orgs_page.connect_hint_prefix') }} 145 + <code class="font-mono bg-bg-subtle px-1.5 py-0.5 rounded">npx @npmx.dev/cli</code> 146 + {{ t('user.orgs_page.connect_hint_suffix') }} 145 147 </p> 146 148 </div> 147 149 148 150 <!-- Not own profile state --> 149 151 <div v-else-if="!isOwnProfile" class="py-12 text-center"> 150 - <p class="text-fg-muted">You can only view your own organizations.</p> 151 - <NuxtLink :to="`/~${npmUser}/orgs`" class="btn mt-4">View your organizations</NuxtLink> 152 + <p class="text-fg-muted">{{ t('user.orgs_page.own_orgs_only') }}</p> 153 + <NuxtLink :to="`/~${npmUser}/orgs`" class="btn mt-4">{{ 154 + t('user.orgs_page.view_your_orgs') 155 + }}</NuxtLink> 152 156 </div> 153 157 154 158 <!-- Loading state --> 155 - <LoadingSpinner v-else-if="isLoading" text="Loading organizations..." /> 159 + <LoadingSpinner v-else-if="isLoading" :text="t('user.orgs_page.loading')" /> 156 160 157 161 <!-- Error state --> 158 162 <div v-else-if="error" role="alert" class="py-12 text-center"> 159 163 <p class="text-fg-muted mb-4">{{ error }}</p> 160 - <button type="button" class="btn" @click="loadOrgs">Try again</button> 164 + <button type="button" class="btn" @click="loadOrgs">{{ t('common.try_again') }}</button> 161 165 </div> 162 166 163 167 <!-- Empty state --> 164 168 <div v-else-if="orgs.length === 0" class="py-12 text-center"> 165 - <p class="text-fg-muted">No organizations found.</p> 169 + <p class="text-fg-muted">{{ t('user.orgs_page.empty') }}</p> 166 170 <p class="text-fg-subtle text-sm mt-2"> 167 - Organizations are detected from your scoped packages. 171 + {{ t('user.orgs_page.empty_hint') }} 168 172 </p> 169 173 </div> 170 174 171 175 <!-- Orgs list --> 172 - <section v-else aria-label="Organizations"> 176 + <section v-else :aria-label="t('user.orgs_page.title')"> 173 177 <h2 class="text-xs text-fg-subtle uppercase tracking-wider mb-4"> 174 - {{ orgs.length }} Organization{{ orgs.length === 1 ? '' : 's' }} 178 + {{ t('user.orgs_page.count', { count: orgs.length }, orgs.length) }} 175 179 </h2> 176 180 177 181 <ul class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> ··· 212 216 <div class="flex items-center gap-1.5"> 213 217 <span class="i-carbon-cube w-4 h-4" aria-hidden="true" /> 214 218 <span v-if="org.packageCount !== null"> 215 - {{ org.packageCount }} package{{ org.packageCount === 1 ? '' : 's' }} 219 + {{ 220 + t( 221 + 'user.orgs_page.packages_count', 222 + { count: org.packageCount }, 223 + org.packageCount, 224 + ) 225 + }} 216 226 </span> 217 227 <span v-else-if="org.isLoadingDetails" class="skeleton inline-block h-4 w-20" /> 218 228 <span v-else class="text-fg-subtle">—</span>
+2 -1
app/utils/formatters.ts
··· 1 - export function formatNumber(num: number): string { 1 + export function formatNumber(num: number, _locale?: string): string { 2 + // TODO: Support different locales (needs care to ensure hydration works correctly) 2 3 return new Intl.NumberFormat('en-US').format(num) 3 4 } 4 5
+481
i18n/locales/en.json
··· 1 + { 2 + "seo": { 3 + "home": { 4 + "title": "npmx - Package Browser for the npm Registry", 5 + "description": "A better browser for the npm registry. Search, browse, and explore packages with a modern interface." 6 + } 7 + }, 8 + "tagline": "a better browser for the npm registry", 9 + "non_affiliation_disclaimer": "not affiliated with npm, Inc.", 10 + "trademark_disclaimer": "npm is a registered trademark of npm, Inc. This site is not affiliated with npm, Inc.", 11 + "footer": { 12 + "source": "source", 13 + "social": "social", 14 + "chat": "chat" 15 + }, 16 + "search": { 17 + "label": "Search npm packages", 18 + "placeholder": "search packages...", 19 + "button": "search", 20 + "clear": "Clear search", 21 + "searching": "Searching...", 22 + "found_packages": "Found {count} packages", 23 + "updating": "(updating...)", 24 + "no_results": "No packages found for \"{query}\"", 25 + "not_taken": "{name} is not taken", 26 + "claim_prompt": "Claim this package name on npm", 27 + "claim_button": "Claim \"{name}\"", 28 + "want_to_claim": "Want to claim this package name?", 29 + "start_typing": "Start typing to search packages" 30 + }, 31 + "nav": { 32 + "popular_packages": "Popular packages", 33 + "search": "search", 34 + "settings": "settings" 35 + }, 36 + "settings": { 37 + "relative_dates": "Relative dates", 38 + "include_types": "Include {'@'}types in install", 39 + "language": "Language", 40 + "help_translate": "Help translate npmx" 41 + }, 42 + "common": { 43 + "loading": "Loading...", 44 + "loading_more": "Loading more...", 45 + "loading_packages": "Loading packages...", 46 + "end_of_results": "End of results", 47 + "try_again": "Try again", 48 + "close": "Close", 49 + "retry": "Retry", 50 + "copy": "copy", 51 + "copied": "copied!", 52 + "show_more": "show more", 53 + "warnings": "Warnings:", 54 + "go_back_home": "Go back home", 55 + "view_on_npm": "view on npm", 56 + "per_week": "/ week", 57 + "sort": { 58 + "name": "name", 59 + "role": "role", 60 + "members": "members" 61 + }, 62 + "scroll_to_top": "Scroll to top" 63 + }, 64 + "package": { 65 + "not_found": "Package Not Found", 66 + "not_found_message": "The package could not be found.", 67 + "no_description": "No description provided", 68 + "show_full_description": "Show full description", 69 + "not_latest": "(not latest)", 70 + "verified_provenance": "Verified provenance", 71 + "view_permalink": "View permalink for this version", 72 + "deprecation": { 73 + "package": "This package has been deprecated.", 74 + "version": "This version has been deprecated.", 75 + "no_reason": "No reason provided" 76 + }, 77 + "stats": { 78 + "license": "License", 79 + "weekly": "Weekly", 80 + "deps": "Deps", 81 + "install_size": "Install Size", 82 + "updated": "Updated", 83 + "view_download_trends": "View download trends", 84 + "view_dependency_graph": "View dependency graph", 85 + "inspect_dependency_tree": "Inspect dependency tree" 86 + }, 87 + "links": { 88 + "repo": "repo", 89 + "homepage": "homepage", 90 + "issues": "issues", 91 + "forks": "{count} fork | {count} forks", 92 + "jsr": "jsr", 93 + "socket": "socket.dev", 94 + "code": "code" 95 + }, 96 + "install": { 97 + "title": "Install", 98 + "pm_label": "Package manager", 99 + "copy_command": "Copy install command", 100 + "view_types": "View {package}" 101 + }, 102 + "readme": { 103 + "title": "Readme", 104 + "no_readme": "No README available.", 105 + "view_on_github": "View on GitHub" 106 + }, 107 + "keywords_title": "Keywords", 108 + "compatibility": "Compatibility", 109 + "card": { 110 + "publisher": "Publisher", 111 + "updated": "Updated", 112 + "weekly_downloads": "Weekly downloads", 113 + "keywords": "Keywords" 114 + }, 115 + "versions": { 116 + "title": "Versions", 117 + "collapse": "Collapse {tag}", 118 + "expand": "Expand {tag}", 119 + "other_versions": "Other versions", 120 + "more_tagged": "{count} more tagged", 121 + "all_covered": "All versions are covered by tags above", 122 + "deprecated_title": "{version} (deprecated)" 123 + }, 124 + "dependencies": { 125 + "title": "Dependencies ({count})", 126 + "list_label": "Package dependencies", 127 + "show_all": "show all {count} deps", 128 + "optional": "optional" 129 + }, 130 + "peer_dependencies": { 131 + "title": "Peer Dependencies ({count})", 132 + "list_label": "Package peer dependencies", 133 + "show_all": "show all {count} peer deps" 134 + }, 135 + "optional_dependencies": { 136 + "title": "Optional Dependencies ({count})", 137 + "list_label": "Package optional dependencies", 138 + "show_all": "show all {count} optional deps" 139 + }, 140 + "maintainers": { 141 + "title": "Maintainers", 142 + "list_label": "Package maintainers", 143 + "you": "(you)", 144 + "via": "via {teams}", 145 + "remove_owner": "Remove {name} as owner", 146 + "username_to_add": "Username to add as owner", 147 + "username_placeholder": "username...", 148 + "add_button": "add", 149 + "cancel_add": "Cancel adding owner", 150 + "add_owner": "+ Add owner" 151 + }, 152 + "downloads": { 153 + "title": "Weekly Downloads", 154 + "date_range": "{start} to {end}" 155 + }, 156 + "install_scripts": { 157 + "title": "Install Scripts", 158 + "script_label": "(script)", 159 + "npx_packages": "{count} npx package | {count} npx packages", 160 + "currently": "currently {version}" 161 + }, 162 + "playgrounds": { 163 + "title": "Try it out", 164 + "choose": "choose playground" 165 + }, 166 + "metrics": { 167 + "esm": "ES Modules only", 168 + "cjs": "CommonJS only", 169 + "dual": "Supports both CommonJS and ES Modules", 170 + "unknown_format": "Unknown module format", 171 + "ts_included": "TypeScript types included", 172 + "types_from": "Types from {package}" 173 + }, 174 + "license": { 175 + "view_spdx": "View license text on SPDX" 176 + }, 177 + "vulnerabilities": { 178 + "no_description": "No description available", 179 + "found": "{count} vulnerability found | {count} vulnerabilities found", 180 + "no_summary": "No summary", 181 + "view_details": "View vulnerability details", 182 + "severity": { 183 + "critical": "critical", 184 + "high": "high", 185 + "moderate": "moderate", 186 + "low": "low" 187 + } 188 + }, 189 + "access": { 190 + "title": "Team Access", 191 + "refresh": "Refresh team access", 192 + "list_label": "Team access list", 193 + "owner": "owner", 194 + "rw": "rw", 195 + "ro": "ro", 196 + "revoke_access": "Revoke {name} access", 197 + "no_access": "No team access configured", 198 + "select_team_label": "Select team", 199 + "loading_teams": "Loading teams...", 200 + "select_team": "Select team", 201 + "permission_label": "Permission level", 202 + "permission": { 203 + "read_only": "read-only", 204 + "read_write": "read-write" 205 + }, 206 + "grant_button": "grant", 207 + "cancel_grant": "Cancel granting access", 208 + "grant_access": "+ Grant team access" 209 + }, 210 + "list": { 211 + "filter_label": "Filter packages", 212 + "filter_placeholder": "Filter packages...", 213 + "sort_label": "Sort packages", 214 + "showing_count": "Showing {filtered} of {total} packages" 215 + }, 216 + "skeleton": { 217 + "loading": "Loading package details", 218 + "license": "License", 219 + "weekly": "Weekly", 220 + "size": "Size", 221 + "deps": "Deps", 222 + "updated": "Updated", 223 + "install": "Install", 224 + "readme": "Readme", 225 + "maintainers": "Maintainers", 226 + "keywords": "Keywords", 227 + "versions": "Versions", 228 + "dependencies": "Dependencies" 229 + }, 230 + "sort": { 231 + "downloads": "Most downloaded", 232 + "updated": "Recently updated", 233 + "name_asc": "Name (A-Z)", 234 + "name_desc": "Name (Z-A)" 235 + } 236 + }, 237 + "connector": { 238 + "status": { 239 + "connecting": "connecting...", 240 + "connected_as": "connected as {'@'}{user}", 241 + "connected": "connected", 242 + "connect_cli": "connect local CLI", 243 + "aria_connecting": "Connecting to local connector", 244 + "aria_connected": "Connected to local connector", 245 + "aria_click_to_connect": "Click to connect to local connector", 246 + "avatar_alt": "{user}'s avatar" 247 + }, 248 + "modal": { 249 + "title": "Local Connector", 250 + "close_modal": "Close modal", 251 + "close": "Close", 252 + "connected": "Connected", 253 + "logged_in_as": "Logged in as {'@'}{user}", 254 + "connected_hint": "You can now manage packages and organizations from the web UI.", 255 + "disconnect": "Disconnect", 256 + "run_hint": "Run the connector on your machine to enable admin features.", 257 + "copy_command": "Copy command", 258 + "copied": "Copied", 259 + "paste_token": "Then paste the token below to connect:", 260 + "token_label": "Token", 261 + "token_placeholder": "paste token here...", 262 + "advanced": "Advanced options", 263 + "port_label": "Port", 264 + "warning": "WARNING", 265 + "warning_text": "This allows npmx to access your npm CLI. Only connect to sites you trust.", 266 + "connect": "Connect", 267 + "connecting": "Connecting..." 268 + } 269 + }, 270 + "operations": { 271 + "queue": { 272 + "title": "Operations Queue", 273 + "clear_all": "clear all", 274 + "refresh": "Refresh operations", 275 + "empty": "No operations queued", 276 + "empty_hint": "Add operations from package or org pages", 277 + "active_label": "Active operations", 278 + "otp_required": "OTP required", 279 + "otp_prompt": "Enter OTP to continue", 280 + "otp_placeholder": "Enter OTP code...", 281 + "otp_label": "One-time password", 282 + "retry_otp": "Retry with OTP", 283 + "retrying": "Retrying...", 284 + "approve_operation": "Approve operation", 285 + "remove_operation": "Remove operation", 286 + "approve_all": "Approve All", 287 + "execute": "Execute", 288 + "executing": "Executing...", 289 + "log": "Log", 290 + "log_label": "Completed operations log", 291 + "remove_from_log": "Remove from log" 292 + } 293 + }, 294 + "org": { 295 + "teams": { 296 + "title": "Teams", 297 + "refresh": "Refresh teams", 298 + "filter_label": "Filter teams", 299 + "filter_placeholder": "Filter teams...", 300 + "sort_by": "Sort by", 301 + "loading": "Loading teams...", 302 + "no_teams": "No teams found", 303 + "list_label": "Organization teams", 304 + "delete_team": "Delete team {name}", 305 + "member_count": "{count} member | {count} members", 306 + "members_of": "Members of {team}", 307 + "no_members": "No members", 308 + "remove_user": "Remove {user} from team", 309 + "username_to_add": "Username to add to {team}", 310 + "username_placeholder": "username...", 311 + "add_button": "add", 312 + "cancel_add_user": "Cancel adding user", 313 + "add_member": "+ Add member", 314 + "team_name_label": "Team name", 315 + "team_name_placeholder": "team-name...", 316 + "create_button": "create", 317 + "no_match": "No teams match \"{query}\"", 318 + "cancel_create": "Cancel creating team", 319 + "create_team": "+ Create team" 320 + }, 321 + "members": { 322 + "title": "Members", 323 + "refresh": "Refresh members", 324 + "filter_label": "Filter members", 325 + "filter_placeholder": "Filter members...", 326 + "filter_by_role": "Filter by role", 327 + "filter_by_team": "Filter by team", 328 + "all_teams": "all teams", 329 + "sort_by": "Sort by", 330 + "loading": "Loading members...", 331 + "no_members": "No members found", 332 + "list_label": "Organization members", 333 + "change_role_for": "Change role for {name}", 334 + "remove_from_org": "Remove {name} from org", 335 + "view_team": "View {team} team", 336 + "no_match": "No members match your filters", 337 + "username_label": "Username", 338 + "username_placeholder": "username...", 339 + "role_label": "Role", 340 + "role": { 341 + "all": "all", 342 + "developer": "developer", 343 + "admin": "admin", 344 + "owner": "owner" 345 + }, 346 + "team_label": "Team", 347 + "no_team": "no team", 348 + "add_button": "add", 349 + "cancel_add": "Cancel adding member", 350 + "add_member": "+ Add member" 351 + }, 352 + "public_packages": "{count} public package | {count} public packages", 353 + "page": { 354 + "packages_title": "Packages", 355 + "members_tab": "Members", 356 + "teams_tab": "Teams", 357 + "no_packages": "No public packages found for", 358 + "no_packages_hint": "This organization may not exist or has no public packages.", 359 + "failed_to_load": "Failed to load organization packages", 360 + "no_match": "No packages match \"{query}\"", 361 + "not_found": "Organization not found", 362 + "not_found_message": "The organization \"{'@'}{name}\" does not exist on npm", 363 + "filter_placeholder": "Filter {count} packages..." 364 + } 365 + }, 366 + "user": { 367 + "combobox": { 368 + "add_to_org_hint": "(will also add to org)", 369 + "press_enter_to_add": "Press Enter to add {'@'}{username}", 370 + "default_placeholder": "username...", 371 + "suggestions_label": "User suggestions" 372 + }, 373 + "page": { 374 + "packages_title": "Packages", 375 + "no_packages": "No public packages found for", 376 + "no_packages_hint": "This user may not exist or has no public packages.", 377 + "failed_to_load": "Failed to load user packages", 378 + "no_match": "No packages match \"{query}\"", 379 + "filter_placeholder": "Filter {count} packages..." 380 + }, 381 + "orgs_page": { 382 + "title": "Organizations", 383 + "back_to_profile": "Back to profile", 384 + "connect_required": "Connect the local CLI to view your organizations.", 385 + "connect_hint_prefix": "Run", 386 + "connect_hint_suffix": "to get started.", 387 + "own_orgs_only": "You can only view your own organizations.", 388 + "view_your_orgs": "View your organizations", 389 + "loading": "Loading organizations...", 390 + "empty": "No organizations found.", 391 + "empty_hint": "Organizations are detected from your scoped packages.", 392 + "count": "{count} Organization | {count} Organizations", 393 + "packages_count": "{count} package | {count} packages" 394 + } 395 + }, 396 + "claim": { 397 + "modal": { 398 + "title": "Claim Package Name", 399 + "close_modal": "Close modal", 400 + "close": "Close", 401 + "success": "Package claimed!", 402 + "success_detail": "{name}{'@'}0.0.0 has been published to npm.", 403 + "success_hint": "You can now publish new versions to this package using npm publish.", 404 + "view_package": "View Package", 405 + "invalid_name": "Invalid package name:", 406 + "available": "This name is available!", 407 + "taken": "This name is already taken.", 408 + "similar_warning": "Similar packages exist - npm may reject this name:", 409 + "related": "Related packages:", 410 + "scope_warning_title": "Consider using a scoped package instead", 411 + "scope_warning_text": "Unscoped package names are a shared resource. Only claim a name if you intend to publish and maintain a package. For personal or organizational projects, use a scoped name like {'@'}{username}/{name}.", 412 + "connect_required": "Connect to the local connector to claim this package name.", 413 + "connect_button": "Connect to Connector", 414 + "publish_hint": "This will publish a minimal placeholder package.", 415 + "preview_json": "Preview package.json", 416 + "claim_button": "Claim Package Name", 417 + "publishing": "Publishing...", 418 + "retry": "Retry", 419 + "checking": "Checking availability...", 420 + "failed_to_check": "Failed to check name availability", 421 + "failed_to_claim": "Failed to claim package" 422 + } 423 + }, 424 + "code": { 425 + "files_label": "Files", 426 + "no_files": "No files in this directory", 427 + "select_version": "Select version", 428 + "root": "root", 429 + "lines": "{count} lines", 430 + "toggle_tree": "Toggle file tree", 431 + "close_tree": "Close file tree", 432 + "copy_link": "Copy link", 433 + "raw": "Raw", 434 + "view_raw": "View raw file", 435 + "file_too_large": "File too large to preview", 436 + "file_size_warning": "{size} exceeds the 500KB limit for syntax highlighting", 437 + "load_anyway": "Load anyway", 438 + "failed_to_load": "Failed to load file", 439 + "unavailable_hint": "The file may be too large or unavailable", 440 + "version_required": "Version is required to browse code", 441 + "go_to_package": "Go to package", 442 + "loading_tree": "Loading file tree...", 443 + "failed_to_load_tree": "Failed to load files for this package version", 444 + "back_to_package": "Back to package", 445 + "table": { 446 + "name": "Name", 447 + "size": "Size" 448 + } 449 + }, 450 + "badges": { 451 + "provenance": { 452 + "verified": "verified", 453 + "verified_title": "Verified provenance", 454 + "verified_via": "Verified: published via {provider}" 455 + }, 456 + "jsr": { 457 + "title": "also available on JSR", 458 + "label": "jsr" 459 + } 460 + }, 461 + "header": { 462 + "home": "npmx home", 463 + "github": "GitHub", 464 + "packages": "packages", 465 + "packages_dropdown": { 466 + "title": "Your Packages", 467 + "loading": "Loading...", 468 + "error": "Failed to load packages", 469 + "empty": "No packages found", 470 + "view_all": "View all" 471 + }, 472 + "orgs": "orgs", 473 + "orgs_dropdown": { 474 + "title": "Your Organizations", 475 + "loading": "Loading...", 476 + "error": "Failed to load organizations", 477 + "empty": "No organizations found", 478 + "view_all": "View all" 479 + } 480 + } 481 + }
+23
nuxt.config.ts
··· 7 7 nuxt.options.pwa.pwaAssets.disabled = true 8 8 } 9 9 }, 10 + // Workaround for Nuxt 4.3.0 regression: https://github.com/nuxt/nuxt/issues/34140 11 + // shared-imports.d.ts pulls in app composables during type-checking of shared context, 12 + // but the shared context doesn't have access to auto-import globals. 13 + // TODO: Remove when Nuxt fixes this upstream 14 + function (_, nuxt) { 15 + nuxt.hook('prepare:types', ({ sharedReferences }) => { 16 + const idx = sharedReferences.findIndex( 17 + ref => 'path' in ref && ref.path.endsWith('shared-imports.d.ts'), 18 + ) 19 + if (idx !== -1) { 20 + sharedReferences.splice(idx, 1) 21 + } 22 + }) 23 + }, 10 24 '@unocss/nuxt', 11 25 '@nuxtjs/html-validator', 12 26 '@nuxt/scripts', ··· 16 30 '@nuxt/test-utils', 17 31 '@vite-pwa/nuxt', 18 32 '@vueuse/nuxt', 33 + '@nuxtjs/i18n', 19 34 ], 20 35 21 36 devtools: { enabled: true }, ··· 130 145 'validate-npm-package-name', 131 146 ], 132 147 }, 148 + }, 149 + 150 + i18n: { 151 + defaultLocale: 'en', 152 + strategy: 'no_prefix', 153 + detectBrowserLanguage: false, 154 + langDir: 'locales', 155 + locales: [{ code: 'en', language: 'en-US', name: 'English', file: 'en.json' }], 133 156 }, 134 157 })
+1
package.json
··· 32 32 "@nuxt/fonts": "^0.13.0", 33 33 "@nuxt/scripts": "^0.13.2", 34 34 "@nuxtjs/html-validator": "^2.1.0", 35 + "@nuxtjs/i18n": "10.2.1", 35 36 "@shikijs/langs": "^3.21.0", 36 37 "@shikijs/themes": "^3.21.0", 37 38 "@vueuse/core": "^14.1.0",
+780 -87
pnpm-lock.yaml
··· 33 33 '@nuxtjs/html-validator': 34 34 specifier: ^2.1.0 35 35 version: 2.1.0(@voidzero-dev/vite-plus-test@0.0.0-ffb4d08a8edafe855c59736c0a38ee85a2373ebb(@types/node@25.0.10)(esbuild@0.27.2)(happy-dom@20.3.5)(jiti@2.6.1)(jsdom@27.4.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(magicast@0.5.1) 36 + '@nuxtjs/i18n': 37 + specifier: 10.2.1 38 + version: 10.2.1(@vue/compiler-dom@3.5.27)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(rollup@4.56.0)(vue@3.5.27(typescript@5.9.3)) 36 39 '@shikijs/langs': 37 40 specifier: ^3.21.0 38 41 version: 3.21.0 ··· 1400 1403 cpu: [x64] 1401 1404 os: [win32] 1402 1405 1406 + '@intlify/bundle-utils@11.0.3': 1407 + resolution: {integrity: sha512-dURCDz1rQXwAb1+Hv4NDit6aZSRaAt4zUYBPEeaDCe3FSs8dMtdF6kEvgd9JwsYFSTAHcvbTs2CqwBjjt9Ltsw==} 1408 + engines: {node: '>= 20'} 1409 + peerDependencies: 1410 + petite-vue-i18n: '*' 1411 + vue-i18n: '*' 1412 + peerDependenciesMeta: 1413 + petite-vue-i18n: 1414 + optional: true 1415 + vue-i18n: 1416 + optional: true 1417 + 1418 + '@intlify/core-base@11.2.8': 1419 + resolution: {integrity: sha512-nBq6Y1tVkjIUsLsdOjDSJj4AsjvD0UG3zsg9Fyc+OivwlA/oMHSKooUy9tpKj0HqZ+NWFifweHavdljlBLTwdA==} 1420 + engines: {node: '>= 16'} 1421 + 1422 + '@intlify/core@11.2.8': 1423 + resolution: {integrity: sha512-su9kRlQAkG+SBP5cufTYmwPnqjur8etZVa2lnR80CgE5JqA0pXwGUF7W08dR/a6T2oDoYPh53/S8O0CGbfx1qg==} 1424 + engines: {node: '>= 16'} 1425 + 1426 + '@intlify/h3@0.7.4': 1427 + resolution: {integrity: sha512-BtL5+U3Dd9Qz6so+ArOMQWZ+nV21rOqqYUXnqwvW6J3VUXr66A9+9+vUFb/NAQvOU4kdfkO3c/9LMRGU9WZ8vw==} 1428 + engines: {node: '>= 20'} 1429 + 1430 + '@intlify/message-compiler@11.2.8': 1431 + resolution: {integrity: sha512-A5n33doOjmHsBtCN421386cG1tWp5rpOjOYPNsnpjIJbQ4POF0QY2ezhZR9kr0boKwaHjbOifvyQvHj2UTrDFQ==} 1432 + engines: {node: '>= 16'} 1433 + 1434 + '@intlify/shared@11.2.8': 1435 + resolution: {integrity: sha512-l6e4NZyUgv8VyXXH4DbuucFOBmxLF56C/mqh2tvApbzl2Hrhi1aTDcuv5TKdxzfHYmpO3UB0Cz04fgDT9vszfw==} 1436 + engines: {node: '>= 16'} 1437 + 1438 + '@intlify/unplugin-vue-i18n@11.0.3': 1439 + resolution: {integrity: sha512-iQuik0nXfdVZ5ab+IEyBFEuvMQ213zfbUpBXaEdHPk8DV+qB2CT/SdFuDhfUDRRBZc/e0qoLlfmc9urhnRYVWw==} 1440 + engines: {node: '>= 20'} 1441 + peerDependencies: 1442 + petite-vue-i18n: '*' 1443 + vue: ^3.2.25 1444 + vue-i18n: '*' 1445 + peerDependenciesMeta: 1446 + petite-vue-i18n: 1447 + optional: true 1448 + vue-i18n: 1449 + optional: true 1450 + 1451 + '@intlify/utils@0.13.0': 1452 + resolution: {integrity: sha512-8i3uRdAxCGzuHwfmHcVjeLQBtysQB2aXl/ojoagDut5/gY5lvWCQ2+cnl2TiqE/fXj/D8EhWG/SLKA7qz4a3QA==} 1453 + engines: {node: '>= 18'} 1454 + 1455 + '@intlify/vue-i18n-extensions@8.0.0': 1456 + resolution: {integrity: sha512-w0+70CvTmuqbskWfzeYhn0IXxllr6mU+IeM2MU0M+j9OW64jkrvqY+pYFWrUnIIC9bEdij3NICruicwd5EgUuQ==} 1457 + engines: {node: '>= 18'} 1458 + peerDependencies: 1459 + '@intlify/shared': ^9.0.0 || ^10.0.0 || ^11.0.0 1460 + '@vue/compiler-dom': ^3.0.0 1461 + vue: ^3.0.0 1462 + vue-i18n: ^9.0.0 || ^10.0.0 || ^11.0.0 1463 + peerDependenciesMeta: 1464 + '@intlify/shared': 1465 + optional: true 1466 + '@vue/compiler-dom': 1467 + optional: true 1468 + vue: 1469 + optional: true 1470 + vue-i18n: 1471 + optional: true 1472 + 1403 1473 '@ioredis/commands@1.5.0': 1404 1474 resolution: {integrity: sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==} 1405 1475 ··· 1449 1519 engines: {node: '>=18'} 1450 1520 hasBin: true 1451 1521 1522 + '@miyaneee/rollup-plugin-json5@1.2.0': 1523 + resolution: {integrity: sha512-JjTIaXZp9WzhUHpElrqPnl1AzBi/rvRs065F71+aTmlqvTMVkdbjZ8vfFl4nRlgJy+TPBw69ZK4pwFdmOAt4aA==} 1524 + peerDependencies: 1525 + rollup: ^1.20.0 || ^2.0.0 || ^3.0.0 || ^4.0.0 1526 + 1452 1527 '@napi-rs/wasm-runtime@1.1.1': 1453 1528 resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} 1454 1529 ··· 1635 1710 '@nuxtjs/html-validator@2.1.0': 1636 1711 resolution: {integrity: sha512-ldo8ioSsH3OEumtgwDMokTxlhjgO9FxjJWViAxisq5l/wjvaVX8SYTQ02wjtQcQQPSvS6BwgypAp400RlyFHng==} 1637 1712 1713 + '@nuxtjs/i18n@10.2.1': 1714 + resolution: {integrity: sha512-/CHAIpYbFgobxeMsnKcD8xBUHxBpqipRMjaI3ol9MVZKscJM+IetYdNL9lGNFdEtlxzkV8COxnoa60rE4sPjuQ==} 1715 + engines: {node: '>=20.11.1'} 1716 + 1638 1717 '@one-ini/wasm@0.1.1': 1639 1718 resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} 1640 1719 ··· 1769 1848 cpu: [arm64] 1770 1849 os: [android] 1771 1850 1851 + '@oxc-parser/binding-android-arm64@0.95.0': 1852 + resolution: {integrity: sha512-dZyxhhvJigwWL1wgnLlqyEiSeuqO0WdDH9H+if0dPcBM4fKa5fjVkaUcJT1jBMcBTnkjxMwTXYZy5TK60N0fjg==} 1853 + engines: {node: ^20.19.0 || >=22.12.0} 1854 + cpu: [arm64] 1855 + os: [android] 1856 + 1772 1857 '@oxc-parser/binding-darwin-arm64@0.110.0': 1773 1858 resolution: {integrity: sha512-jPBsXPc8hwmsUQyLMg7a5Ll/j/8rWCDFoB8WzLP6C0qQKX0zWQxbfSdLFg9GGNPuRo8J8ma9WfBQN5RmbFxNJA==} 1859 + engines: {node: ^20.19.0 || >=22.12.0} 1860 + cpu: [arm64] 1861 + os: [darwin] 1862 + 1863 + '@oxc-parser/binding-darwin-arm64@0.95.0': 1864 + resolution: {integrity: sha512-zun9+V33kyCtNec9oUSWwb0qi3fB8pXwum1yGFECPEr55g/CrWju6/Jv4hwwNBeW2tK9Ch/PRstEtYmOLMhHpg==} 1774 1865 engines: {node: ^20.19.0 || >=22.12.0} 1775 1866 cpu: [arm64] 1776 1867 os: [darwin] 1777 1868 1778 1869 '@oxc-parser/binding-darwin-x64@0.110.0': 1779 1870 resolution: {integrity: sha512-jt5G1eZj4sdMGc7Q0c6kfPRmqY1Mn3yzo6xuRr8EXozkh93O8KGFflABY7t56WIrmP+cloaCQkLcjlm6vdhzcQ==} 1871 + engines: {node: ^20.19.0 || >=22.12.0} 1872 + cpu: [x64] 1873 + os: [darwin] 1874 + 1875 + '@oxc-parser/binding-darwin-x64@0.95.0': 1876 + resolution: {integrity: sha512-9djMQ/t6Ns/UXtziwUe562uVJMbhtuLtCj+Xav+HMVT/rhV9gWO8PQOG7AwDLUBjJanItsrfqrGtqhNxtZ701w==} 1780 1877 engines: {node: ^20.19.0 || >=22.12.0} 1781 1878 cpu: [x64] 1782 1879 os: [darwin] ··· 1787 1884 cpu: [x64] 1788 1885 os: [freebsd] 1789 1886 1887 + '@oxc-parser/binding-freebsd-x64@0.95.0': 1888 + resolution: {integrity: sha512-GK6k0PgCVkkeRZtHgcosCYbXIRySpJpuPw/OInfLGFh8f3x9gp2l8Fbcfx+YO+ZOHFBCd2NNedGqw8wMgouxfA==} 1889 + engines: {node: ^20.19.0 || >=22.12.0} 1890 + cpu: [x64] 1891 + os: [freebsd] 1892 + 1790 1893 '@oxc-parser/binding-linux-arm-gnueabihf@0.110.0': 1791 1894 resolution: {integrity: sha512-w3OZ0pLKktM7k4qEbVj3dHnCvSMFnWugYxHfhpwncYUOxwDNL3mw++EOIrw997QYiEuJ+H6Od8K6mbj1p6Ae8w==} 1792 1895 engines: {node: ^20.19.0 || >=22.12.0} 1793 1896 cpu: [arm] 1794 1897 os: [linux] 1795 1898 1899 + '@oxc-parser/binding-linux-arm-gnueabihf@0.95.0': 1900 + resolution: {integrity: sha512-+g/lFITtyHHEk69cunOHuiT5cX+mpUTcbGYNe8suguZ7FqgNwai+PnGv0ctCvtgxBPVfckfUK8c3RvFKo+vi/w==} 1901 + engines: {node: ^20.19.0 || >=22.12.0} 1902 + cpu: [arm] 1903 + os: [linux] 1904 + 1796 1905 '@oxc-parser/binding-linux-arm-musleabihf@0.110.0': 1797 1906 resolution: {integrity: sha512-BIaoW4W6QKb8Q6p3DErDtsAuDRAnr0W+gtwo7fQQkbAJpoPII0ZJXZn+tcQGCyNGKWSsilRNWHyd/XZfXXXpzw==} 1798 1907 engines: {node: ^20.19.0 || >=22.12.0} 1799 1908 cpu: [arm] 1800 1909 os: [linux] 1801 1910 1911 + '@oxc-parser/binding-linux-arm-musleabihf@0.95.0': 1912 + resolution: {integrity: sha512-SXNasDtPw8ycNV7VEvFxb4LETmykvWKUhHR7K3us818coXYpDj54P8WEx8hJobP/9skuuiFuKHmtYLdjX8wntA==} 1913 + engines: {node: ^20.19.0 || >=22.12.0} 1914 + cpu: [arm] 1915 + os: [linux] 1916 + 1802 1917 '@oxc-parser/binding-linux-arm64-gnu@0.110.0': 1803 1918 resolution: {integrity: sha512-3EQDJze28t0HdxXjMKBU6utNscXJePg2YV0Kd/ZnHx24VcIyfkNH6NKzBh0NeaWHovDTkpzYHPtF2tOevtbbfw==} 1804 1919 engines: {node: ^20.19.0 || >=22.12.0} 1805 1920 cpu: [arm64] 1806 1921 os: [linux] 1807 1922 1923 + '@oxc-parser/binding-linux-arm64-gnu@0.95.0': 1924 + resolution: {integrity: sha512-0LzebARTU0ROfD6pDK4h1pFn+09meErCZ0MA2TaW08G72+GNneEsksPufOuI+9AxVSRa+jKE3fu0wavvhZgSkg==} 1925 + engines: {node: ^20.19.0 || >=22.12.0} 1926 + cpu: [arm64] 1927 + os: [linux] 1928 + 1808 1929 '@oxc-parser/binding-linux-arm64-musl@0.110.0': 1809 1930 resolution: {integrity: sha512-5xwm1hPrGGvjCVtTWNGJ39MmQGnyipoIDShneGBgSrnDh0XX+COAO7AZKajgNipqgNq5rGEItpzFkMtSDyx0bQ==} 1810 1931 engines: {node: ^20.19.0 || >=22.12.0} 1811 1932 cpu: [arm64] 1812 1933 os: [linux] 1813 1934 1935 + '@oxc-parser/binding-linux-arm64-musl@0.95.0': 1936 + resolution: {integrity: sha512-Pvi1lGe/G+mJZ3hUojMP/aAHAzHA25AEtVr8/iuz7UV72t/15NOgJYr9kELMUMNjPqpr3vKUgXTFmTtAxp11Qw==} 1937 + engines: {node: ^20.19.0 || >=22.12.0} 1938 + cpu: [arm64] 1939 + os: [linux] 1940 + 1814 1941 '@oxc-parser/binding-linux-ppc64-gnu@0.110.0': 1815 1942 resolution: {integrity: sha512-I8Xop7z+enuvW1xe0AcRQ9XqFNkUYgeXusyGjCyW6TstRb62P90h+nL1AoGaUMy0E0518DJam5vRYVRgXaAzYg==} 1816 1943 engines: {node: ^20.19.0 || >=22.12.0} ··· 1823 1950 cpu: [riscv64] 1824 1951 os: [linux] 1825 1952 1953 + '@oxc-parser/binding-linux-riscv64-gnu@0.95.0': 1954 + resolution: {integrity: sha512-pUEVHIOVNDfhk4sTlLhn6mrNENhE4/dAwemxIfqpcSyBlYG0xYZND1F3jjR2yWY6DakXZ6VSuDbtiv1LPNlOLw==} 1955 + engines: {node: ^20.19.0 || >=22.12.0} 1956 + cpu: [riscv64] 1957 + os: [linux] 1958 + 1826 1959 '@oxc-parser/binding-linux-riscv64-musl@0.110.0': 1827 1960 resolution: {integrity: sha512-ylJIuJyMzAqR191QeCwZLEkyo4Sx817TNILjNhT0W1EDQusGicOYKSsGXM/2DHCNYGcidV+MQ8pUVzNeVmuM6g==} 1828 1961 engines: {node: ^20.19.0 || >=22.12.0} ··· 1835 1968 cpu: [s390x] 1836 1969 os: [linux] 1837 1970 1971 + '@oxc-parser/binding-linux-s390x-gnu@0.95.0': 1972 + resolution: {integrity: sha512-5+olaepHTE3J/+w7g0tr3nocvv5BKilAJnzj4L8tWBCLEZbL6olJcGVoldUO+3cgg1SO1xJywP5BuLhT0mDUDw==} 1973 + engines: {node: ^20.19.0 || >=22.12.0} 1974 + cpu: [s390x] 1975 + os: [linux] 1976 + 1838 1977 '@oxc-parser/binding-linux-x64-gnu@0.110.0': 1839 1978 resolution: {integrity: sha512-+e6ws5JLpFehdK+wh6q8icx1iM3Ao+9dtItVWFcRiXxSvGcIlS9viWcMvXKrmcsyVDUf81dnvuMSBigNslxhIQ==} 1840 1979 engines: {node: ^20.19.0 || >=22.12.0} 1841 1980 cpu: [x64] 1842 1981 os: [linux] 1843 1982 1983 + '@oxc-parser/binding-linux-x64-gnu@0.95.0': 1984 + resolution: {integrity: sha512-8huzHlK/N98wrnYKxIcYsK8ZGBWomQchu/Mzi6m+CtbhjWOv9DmK0jQ2fUWImtluQVpTwS0uZT06d3g7XIkJrA==} 1985 + engines: {node: ^20.19.0 || >=22.12.0} 1986 + cpu: [x64] 1987 + os: [linux] 1988 + 1844 1989 '@oxc-parser/binding-linux-x64-musl@0.110.0': 1845 1990 resolution: {integrity: sha512-6DiYhVdXKOzB01+j/tyrB6/d2o6b4XYFQvcbBRNbVHIimS6nl992y3V3mGG3NaA+uCZAzhT3M3btTdKAxE4A3A==} 1991 + engines: {node: ^20.19.0 || >=22.12.0} 1992 + cpu: [x64] 1993 + os: [linux] 1994 + 1995 + '@oxc-parser/binding-linux-x64-musl@0.95.0': 1996 + resolution: {integrity: sha512-bWnrLfGDcx/fab0+UQnFbVFbiykof/btImbYf+cI2pU/1Egb2x+OKSmM5Qt0nEUiIpM5fgJmYXxTopybSZOKYA==} 1846 1997 engines: {node: ^20.19.0 || >=22.12.0} 1847 1998 cpu: [x64] 1848 1999 os: [linux] ··· 1858 2009 engines: {node: '>=14.0.0'} 1859 2010 cpu: [wasm32] 1860 2011 2012 + '@oxc-parser/binding-wasm32-wasi@0.95.0': 2013 + resolution: {integrity: sha512-0JLyqkZu1HnQIZ4e5LBGOtzqua1QwFEUOoMSycdoerXqayd4LK2b7WMfAx8eCIf+jGm1Uj6f3R00nlsx8g1faQ==} 2014 + engines: {node: '>=14.0.0'} 2015 + cpu: [wasm32] 2016 + 1861 2017 '@oxc-parser/binding-win32-arm64-msvc@0.110.0': 1862 2018 resolution: {integrity: sha512-ZW393ysGT5oZeGJRyw2JAz4tIfyTjVCSxuZoh8e+7J7e0QPDH/SAmyxJXb/aMxarIVa3OcYZ5p/Q6eooHZ0i1Q==} 2019 + engines: {node: ^20.19.0 || >=22.12.0} 2020 + cpu: [arm64] 2021 + os: [win32] 2022 + 2023 + '@oxc-parser/binding-win32-arm64-msvc@0.95.0': 2024 + resolution: {integrity: sha512-RWvaA6s1SYlBj9CxwHfNn0CRlkPdv9fEUAXfZkGQPdP5e1ppIaO2KYE0sUov/zzp9hPTMMsTMHl4dcIbb+pHCQ==} 1863 2025 engines: {node: ^20.19.0 || >=22.12.0} 1864 2026 cpu: [arm64] 1865 2027 os: [win32] ··· 1876 2038 cpu: [x64] 1877 2039 os: [win32] 1878 2040 2041 + '@oxc-parser/binding-win32-x64-msvc@0.95.0': 2042 + resolution: {integrity: sha512-BQpgl7rDjFvCIHudmUR0dCwc4ylBYZl4CPVinlD3NhkMif4WD5dADckoo5ES/KOpFyvwcbKZX+grP63cjHi26g==} 2043 + engines: {node: ^20.19.0 || >=22.12.0} 2044 + cpu: [x64] 2045 + os: [win32] 2046 + 1879 2047 '@oxc-project/runtime@0.110.0': 1880 2048 resolution: {integrity: sha512-4t5lYmPneAGKGN7zDhK2iQrn+Ax3DXLCNqVr3z6K2VqemKWfQTlLyzjgjilxZmwFAKe65qI4WG7Bsj05UgUHaA==} 1881 2049 engines: {node: ^20.19.0 || >=22.12.0} 1882 2050 1883 2051 '@oxc-project/types@0.110.0': 1884 2052 resolution: {integrity: sha512-6Ct21OIlrEnFEJk5LT4e63pk3btsI6/TusD/GStLi7wYlGJNOl1GI9qvXAnRAxQU9zqA2Oz+UwhfTOU2rPZVow==} 2053 + 2054 + '@oxc-project/types@0.95.0': 2055 + resolution: {integrity: sha512-vACy7vhpMPhjEJhULNxrdR0D943TkA/MigMpJCHmBHvMXxRStRi/dPtTlfQ3uDwWSzRpT8z+7ImjZVf8JWBocQ==} 1885 2056 1886 2057 '@oxc-transform/binding-android-arm-eabi@0.110.0': 1887 2058 resolution: {integrity: sha512-sE9dxvqqAax1YYJ3t7j+h5ZSI9jl6dYuDfngl6ieZUrIy5P89/8JKVgAzgp8o3wQSo7ndpJvYsi1K4ZqrmbP7w==} ··· 1895 2066 cpu: [arm64] 1896 2067 os: [android] 1897 2068 2069 + '@oxc-transform/binding-android-arm64@0.95.0': 2070 + resolution: {integrity: sha512-eW+BCgRWOsMrDiz7FEV7BjAmaF9lGIc2ueGdRUYjRUMq4f5FSGS7gMBTYDxajdoIB3L5Gnksh1CWkIlgg95UVA==} 2071 + engines: {node: ^20.19.0 || >=22.12.0} 2072 + cpu: [arm64] 2073 + os: [android] 2074 + 1898 2075 '@oxc-transform/binding-darwin-arm64@0.110.0': 1899 2076 resolution: {integrity: sha512-oeSeHnL4Z4cMXtc8V0/rwoVn0dgwlS9q0j6LcHn9dIhtFEdp3W0iSBF8YmMQA+E7sILeLDjsHmHE4Kp0sOScXw==} 1900 2077 engines: {node: ^20.19.0 || >=22.12.0} 1901 2078 cpu: [arm64] 1902 2079 os: [darwin] 1903 2080 2081 + '@oxc-transform/binding-darwin-arm64@0.95.0': 2082 + resolution: {integrity: sha512-OUUaYZVss8tyDZZ7TGr2vnH3+i3Ouwsx0frQRGkiePNatXxaJJ3NS5+Kwgi9hh3WryXaQz2hWji4AM2RHYE7Cg==} 2083 + engines: {node: ^20.19.0 || >=22.12.0} 2084 + cpu: [arm64] 2085 + os: [darwin] 2086 + 1904 2087 '@oxc-transform/binding-darwin-x64@0.110.0': 1905 2088 resolution: {integrity: sha512-nL9K5x7OuZydobAGPylsEW9d4APs2qEkIBLMgQPA+kY8dtVD3IR87QsTbs4l4DBQYyun/+ay6qVCDlxqxdX2Jg==} 1906 2089 engines: {node: ^20.19.0 || >=22.12.0} 1907 2090 cpu: [x64] 1908 2091 os: [darwin] 1909 2092 2093 + '@oxc-transform/binding-darwin-x64@0.95.0': 2094 + resolution: {integrity: sha512-49UPEgIlgWUndwcP3LH6dvmOewZ92DxCMpFMo11JhUlmNJxA3sjVImEBRB56/tJ+XF+xnya9kB1oCW4yRY+mRw==} 2095 + engines: {node: ^20.19.0 || >=22.12.0} 2096 + cpu: [x64] 2097 + os: [darwin] 2098 + 1910 2099 '@oxc-transform/binding-freebsd-x64@0.110.0': 1911 2100 resolution: {integrity: sha512-GS29zXXirDQhZEUq8xKJ1azAWMuUy3Ih3W5Bc5ddk12LRthO5wRLFcKIyeHpAXCoXymQ+LmxbMtbPf84GPxouw==} 1912 2101 engines: {node: ^20.19.0 || >=22.12.0} 1913 2102 cpu: [x64] 1914 2103 os: [freebsd] 1915 2104 2105 + '@oxc-transform/binding-freebsd-x64@0.95.0': 2106 + resolution: {integrity: sha512-lNKrHKaDEm8pbKlVbn0rv2L97O0lbA0Tsrxx4GF/HhmdW+NgwGU1pMzZ4tB2QcylbqgKxOB+v9luebHyh1jfgA==} 2107 + engines: {node: ^20.19.0 || >=22.12.0} 2108 + cpu: [x64] 2109 + os: [freebsd] 2110 + 1916 2111 '@oxc-transform/binding-linux-arm-gnueabihf@0.110.0': 1917 2112 resolution: {integrity: sha512-glzDHak8ISyZJemCUi7RCvzNSl+MQ1ly9RceT2qRufhUsvNZ4C/2QLJ1HJwd2N6E88bO4laYn+RofdRzNnGGEA==} 1918 2113 engines: {node: ^20.19.0 || >=22.12.0} 1919 2114 cpu: [arm] 1920 2115 os: [linux] 1921 2116 2117 + '@oxc-transform/binding-linux-arm-gnueabihf@0.95.0': 2118 + resolution: {integrity: sha512-+VWcLeeizI8IjU+V+o8AmzPuIMiTrGr0vrmXU3CEsV05MrywCuJU+f6ilPs3JBKno9VIwqvRpHB/z39sQabHWg==} 2119 + engines: {node: ^20.19.0 || >=22.12.0} 2120 + cpu: [arm] 2121 + os: [linux] 2122 + 1922 2123 '@oxc-transform/binding-linux-arm-musleabihf@0.110.0': 1923 2124 resolution: {integrity: sha512-8JThvgJ2FRoTVfbp7e4wqeZqCZbtudM06SfZmNzND9kPNu/LVYygIR+72RWs+xm4bWkuYHg/islo/boNPtMT5Q==} 1924 2125 engines: {node: ^20.19.0 || >=22.12.0} 1925 2126 cpu: [arm] 1926 2127 os: [linux] 1927 2128 2129 + '@oxc-transform/binding-linux-arm-musleabihf@0.95.0': 2130 + resolution: {integrity: sha512-a59xPw84t6VwlvNEGcmuw3feGcKcWOC7uB8oePJ/BVSAV1yayLoB3k6JASwLTZ7N/PNPNUhcw1jDxowgAfBJfg==} 2131 + engines: {node: ^20.19.0 || >=22.12.0} 2132 + cpu: [arm] 2133 + os: [linux] 2134 + 1928 2135 '@oxc-transform/binding-linux-arm64-gnu@0.110.0': 1929 2136 resolution: {integrity: sha512-IRh21Ub/g4bkHoErZ0AUWMlWfoZaS0A6EaOVtbcY70RSYIMlrsbjiFwJCzM+b/1DD1rXbH5tsGcH7GweTbfRqg==} 1930 2137 engines: {node: ^20.19.0 || >=22.12.0} 1931 2138 cpu: [arm64] 1932 2139 os: [linux] 1933 2140 2141 + '@oxc-transform/binding-linux-arm64-gnu@0.95.0': 2142 + resolution: {integrity: sha512-NLdrFuEHlmbiC1M1WESFV4luUcB/84GXi+cbnRXhgMjIW/CThRVJ989eTJy59QivkVlLcJSKTiKiKCt0O6TTlQ==} 2143 + engines: {node: ^20.19.0 || >=22.12.0} 2144 + cpu: [arm64] 2145 + os: [linux] 2146 + 1934 2147 '@oxc-transform/binding-linux-arm64-musl@0.110.0': 1935 2148 resolution: {integrity: sha512-e5JN94/oy+wevk76q+LMr+2klTTcO60uXa+Wkq558Ms7mdF2TvkKFI++d/JeiuIwJLTi/BxQ4qdT5FWcsHM/ug==} 1936 2149 engines: {node: ^20.19.0 || >=22.12.0} 1937 2150 cpu: [arm64] 1938 2151 os: [linux] 1939 2152 2153 + '@oxc-transform/binding-linux-arm64-musl@0.95.0': 2154 + resolution: {integrity: sha512-GL0ffCPW8JlFI0/jeSgCY665yDdojHxA0pbYG+k8oEHOWCYZUZK9AXL+r0oerNEWYJ8CRB+L5Yq87ZtU/YUitw==} 2155 + engines: {node: ^20.19.0 || >=22.12.0} 2156 + cpu: [arm64] 2157 + os: [linux] 2158 + 1940 2159 '@oxc-transform/binding-linux-ppc64-gnu@0.110.0': 1941 2160 resolution: {integrity: sha512-Y3/Tnnz1GvDpmv8FXBIKtdZPsdZklOEPdrL6NHrN5i2u54BOkybFaDSptgWF53wOrJlTrcmAVSE6fRKK9XCM2Q==} 1942 2161 engines: {node: ^20.19.0 || >=22.12.0} ··· 1949 2168 cpu: [riscv64] 1950 2169 os: [linux] 1951 2170 2171 + '@oxc-transform/binding-linux-riscv64-gnu@0.95.0': 2172 + resolution: {integrity: sha512-tbH7LaClSmN3YFVo1UjMSe7D6gkb5f+CMIbj9i873UUZomVRmAjC4ygioObfzM+sj/tX0WoTXx5L1YOfQkHL6Q==} 2173 + engines: {node: ^20.19.0 || >=22.12.0} 2174 + cpu: [riscv64] 2175 + os: [linux] 2176 + 1952 2177 '@oxc-transform/binding-linux-riscv64-musl@0.110.0': 1953 2178 resolution: {integrity: sha512-JOUSYFfHjBUs7xp2FHmZHb8eTYD/oEu0NklS6JgUauqnoXZHiTLPLVW2o2uVCqldnabYHcomuwI2iqVFYJNhTw==} 1954 2179 engines: {node: ^20.19.0 || >=22.12.0} ··· 1961 2186 cpu: [s390x] 1962 2187 os: [linux] 1963 2188 2189 + '@oxc-transform/binding-linux-s390x-gnu@0.95.0': 2190 + resolution: {integrity: sha512-8jMqiURWa0iTiPMg7BWaln89VdhhWzNlPyKM90NaFVVhBIKCr2UEhrQWdpBw/E9C8uWf/4VabBEhfPMK+0yS4w==} 2191 + engines: {node: ^20.19.0 || >=22.12.0} 2192 + cpu: [s390x] 2193 + os: [linux] 2194 + 1964 2195 '@oxc-transform/binding-linux-x64-gnu@0.110.0': 1965 2196 resolution: {integrity: sha512-YQ2joGWCVDZVEU2cD/r/w49hVjDm/Qu1BvC/7zs8LvprzdLS/HyMXGF2oA0puw0b+AqgYaz3bhwKB2xexHyITQ==} 1966 2197 engines: {node: ^20.19.0 || >=22.12.0} 1967 2198 cpu: [x64] 1968 2199 os: [linux] 1969 2200 2201 + '@oxc-transform/binding-linux-x64-gnu@0.95.0': 2202 + resolution: {integrity: sha512-D5ULJ2uWipsTgfvHIvqmnGkCtB3Fyt2ZN7APRjVO+wLr+HtmnaWddKsLdrRWX/m/6nQ2xQdoQekdJrokYK9LtQ==} 2203 + engines: {node: ^20.19.0 || >=22.12.0} 2204 + cpu: [x64] 2205 + os: [linux] 2206 + 1970 2207 '@oxc-transform/binding-linux-x64-musl@0.110.0': 1971 2208 resolution: {integrity: sha512-fkjr5qE632ULmNgvFXWDR/8668WxERz3tU7TQFp6JebPBneColitjSkdx6VKNVXEoMmQnOvBIGeP5tUNT384oA==} 1972 2209 engines: {node: ^20.19.0 || >=22.12.0} 1973 2210 cpu: [x64] 1974 2211 os: [linux] 1975 2212 2213 + '@oxc-transform/binding-linux-x64-musl@0.95.0': 2214 + resolution: {integrity: sha512-DmCGU+FzRezES5wVAGVimZGzYIjMOapXbWpxuz8M8p3nMrfdBEQ5/tpwBp2vRlIohhABy4vhHJByl4c64ENCGQ==} 2215 + engines: {node: ^20.19.0 || >=22.12.0} 2216 + cpu: [x64] 2217 + os: [linux] 2218 + 1976 2219 '@oxc-transform/binding-openharmony-arm64@0.110.0': 1977 2220 resolution: {integrity: sha512-HWH9Zj+lMrdSTqFRCZsvDWMz7OnMjbdGsm3xURXWfRZpuaz0bVvyuZNDQXc4FyyhRDsemICaJbU1bgeIpUJDGw==} 1978 2221 engines: {node: ^20.19.0 || >=22.12.0} ··· 1984 2227 engines: {node: '>=14.0.0'} 1985 2228 cpu: [wasm32] 1986 2229 2230 + '@oxc-transform/binding-wasm32-wasi@0.95.0': 2231 + resolution: {integrity: sha512-tSo1EU4Whd1gXyae7cwSDouhppkuz6Jkd5LY8Uch9VKsHVSRhDLDW19Mq6VSwtyPxDPTJnJ2jYJWm+n8SYXiXQ==} 2232 + engines: {node: '>=14.0.0'} 2233 + cpu: [wasm32] 2234 + 1987 2235 '@oxc-transform/binding-win32-arm64-msvc@0.110.0': 1988 2236 resolution: {integrity: sha512-9VTwpXCZs7xkV+mKhQ62dVk7KLnLXtEUxNS2T4nLz3iMl1IJbA4h5oltK0JoobtiUAnbkV53QmMVGW8+Nh3bDQ==} 1989 2237 engines: {node: ^20.19.0 || >=22.12.0} 1990 2238 cpu: [arm64] 1991 2239 os: [win32] 1992 2240 2241 + '@oxc-transform/binding-win32-arm64-msvc@0.95.0': 2242 + resolution: {integrity: sha512-6eaxlgj+J5n8zgJTSugqdPLBtKGRqvxYLcvHN8b+U9hVhF/2HG/JCOrcSYV/XgWGNPQiaRVzpR3hGhmFro9QTw==} 2243 + engines: {node: ^20.19.0 || >=22.12.0} 2244 + cpu: [arm64] 2245 + os: [win32] 2246 + 1993 2247 '@oxc-transform/binding-win32-ia32-msvc@0.110.0': 1994 2248 resolution: {integrity: sha512-5y0fzuNON7/F2hh2P94vANFaRPJ/3DI1hVl5rseCT8VUVqOGIjWaza0YS/D1g6t1WwycW2LWDMi2raOKoWU5GQ==} 1995 2249 engines: {node: ^20.19.0 || >=22.12.0} ··· 1998 2252 1999 2253 '@oxc-transform/binding-win32-x64-msvc@0.110.0': 2000 2254 resolution: {integrity: sha512-QROrowwlrApI1fEScMknGWKM6GTM/Z2xwMnDqvSaEmzNazBsDUlE08Jasw610hFEsYAVU2K5sp/YaCa9ORdP4A==} 2255 + engines: {node: ^20.19.0 || >=22.12.0} 2256 + cpu: [x64] 2257 + os: [win32] 2258 + 2259 + '@oxc-transform/binding-win32-x64-msvc@0.95.0': 2260 + resolution: {integrity: sha512-Y8JY79A7fTuBjEXZFu+mHbHzgsV3uJDUuUKeGffpOwI1ayOGCKeBJTiMhksYkiir1xS+DkGLEz73+xse9Is9rw==} 2001 2261 engines: {node: ^20.19.0 || >=22.12.0} 2002 2262 cpu: [x64] 2003 2263 os: [win32] ··· 2475 2735 rollup: 2476 2736 optional: true 2477 2737 2738 + '@rollup/plugin-yaml@4.1.2': 2739 + resolution: {integrity: sha512-RpupciIeZMUqhgFE97ba0s98mOFS7CWzN3EJNhJkqSv9XLlWYtwVdtE6cDw6ASOF/sZVFS7kRJXftaqM2Vakdw==} 2740 + engines: {node: '>=14.0.0'} 2741 + peerDependencies: 2742 + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 2743 + peerDependenciesMeta: 2744 + rollup: 2745 + optional: true 2746 + 2478 2747 '@rollup/pluginutils@3.1.0': 2479 2748 resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} 2480 2749 engines: {node: '>= 8.0.0'} ··· 2740 3009 '@types/ws@8.18.1': 2741 3010 resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} 2742 3011 3012 + '@typescript-eslint/project-service@8.54.0': 3013 + resolution: {integrity: sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==} 3014 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 3015 + peerDependencies: 3016 + typescript: '>=4.8.4 <6.0.0' 3017 + 3018 + '@typescript-eslint/scope-manager@8.54.0': 3019 + resolution: {integrity: sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==} 3020 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 3021 + 3022 + '@typescript-eslint/tsconfig-utils@8.54.0': 3023 + resolution: {integrity: sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==} 3024 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 3025 + peerDependencies: 3026 + typescript: '>=4.8.4 <6.0.0' 3027 + 3028 + '@typescript-eslint/types@8.54.0': 3029 + resolution: {integrity: sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==} 3030 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 3031 + 3032 + '@typescript-eslint/typescript-estree@8.54.0': 3033 + resolution: {integrity: sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==} 3034 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 3035 + peerDependencies: 3036 + typescript: '>=4.8.4 <6.0.0' 3037 + 3038 + '@typescript-eslint/visitor-keys@8.54.0': 3039 + resolution: {integrity: sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==} 3040 + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 3041 + 2743 3042 '@ungap/structured-clone@1.3.0': 2744 3043 resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} 2745 3044 ··· 3988 4287 resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} 3989 4288 engines: {node: '>=12'} 3990 4289 4290 + escodegen@2.1.0: 4291 + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} 4292 + engines: {node: '>=6.0'} 4293 + hasBin: true 4294 + 3991 4295 eslint-scope@5.1.1: 3992 4296 resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} 3993 4297 engines: {node: '>=8.0.0'} ··· 4017 4321 espree@10.4.0: 4018 4322 resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} 4019 4323 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 4324 + 4325 + espree@9.6.1: 4326 + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} 4327 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 4328 + 4329 + esprima@4.0.1: 4330 + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} 4331 + engines: {node: '>=4'} 4332 + hasBin: true 4020 4333 4021 4334 esquery@1.7.0: 4022 4335 resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} ··· 4806 5119 engines: {node: '>=6'} 4807 5120 hasBin: true 4808 5121 5122 + jsonc-eslint-parser@2.4.2: 5123 + resolution: {integrity: sha512-1e4qoRgnn448pRuMvKGsFFymUCquZV0mpGgOyIKNgD3JVDTsVJyRBGH/Fm0tBb8WsWGgmB1mDe6/yJMQM37DUA==} 5124 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 5125 + 4809 5126 jsonfile@6.2.0: 4810 5127 resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} 4811 5128 ··· 5221 5538 nth-check@2.1.1: 5222 5539 resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} 5223 5540 5541 + nuxt-define@1.0.0: 5542 + resolution: {integrity: sha512-CYZ2WjU+KCyCDVzjYUM4eEpMF0rkPmkpiFrybTqqQCRpUbPt2h3snswWIpFPXTi+osRCY6Og0W/XLAQgDL4FfQ==} 5543 + 5224 5544 nuxt-og-image@5.1.13: 5225 5545 resolution: {integrity: sha512-H9kqGlmcEb9agWURwT5iFQjbr7Ec7tcQHZZaYSpC/JXKq2/dFyRyAoo6oXTk6ob20dK9aNjkJDcX2XmgZy67+w==} 5226 5546 engines: {node: '>=18.0.0'} ··· 5319 5639 resolution: {integrity: sha512-GijUR3K1Ln/QwMyYXRsBtOyzqGaCs9ce5pOug1UtrMg8dSiE7VuuRuIcyYD4nyJbasat3K0YljiKt/PSFPdSBA==} 5320 5640 engines: {node: ^20.19.0 || >=22.12.0} 5321 5641 5642 + oxc-parser@0.95.0: 5643 + resolution: {integrity: sha512-Te8fE/SmiiKWIrwBwxz5Dod87uYvsbcZ9JAL5ylPg1DevyKgTkxCXnPEaewk1Su2qpfNmry5RHoN+NywWFCG+A==} 5644 + engines: {node: ^20.19.0 || >=22.12.0} 5645 + 5322 5646 oxc-transform@0.110.0: 5323 5647 resolution: {integrity: sha512-/fymQNzzUoKZweH0nC5yvbI2eR0yWYusT9TEKDYVgOgYrf9Qmdez9lUFyvxKR9ycx+PTHi/reIOzqf3wkShQsw==} 5324 5648 engines: {node: ^20.19.0 || >=22.12.0} 5325 5649 5650 + oxc-transform@0.95.0: 5651 + resolution: {integrity: sha512-SmS5aThb5K0SoUZgzGbikNBjrGHfOY4X5TEqBlaZb1uy5YgXbUSbpakpZJ13yW36LNqy8Im5+y+sIk5dlzpZ/w==} 5652 + engines: {node: ^20.19.0 || >=22.12.0} 5653 + 5654 + oxc-walker@0.5.2: 5655 + resolution: {integrity: sha512-XYoZqWwApSKUmSDEFeOKdy3Cdh95cOcSU8f7yskFWE4Rl3cfL5uwyY+EV7Brk9mdNLy+t5SseJajd6g7KncvlA==} 5656 + peerDependencies: 5657 + oxc-parser: '>=0.72.0' 5658 + 5326 5659 oxc-walker@0.7.0: 5327 5660 resolution: {integrity: sha512-54B4KUhrzbzc4sKvKwVYm7E2PgeROpGba0/2nlNZMqfDyca+yOor5IMb4WLGBatGDT0nkzYdYuzylg7n3YfB7A==} 5328 5661 peerDependencies: ··· 6302 6635 resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} 6303 6636 engines: {node: '>=0.6'} 6304 6637 6638 + tosource@2.0.0-alpha.3: 6639 + resolution: {integrity: sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug==} 6640 + engines: {node: '>=10'} 6641 + 6305 6642 totalist@3.0.1: 6306 6643 resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} 6307 6644 engines: {node: '>=6'} ··· 6326 6663 6327 6664 trim-lines@3.0.1: 6328 6665 resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} 6666 + 6667 + ts-api-utils@2.4.0: 6668 + resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} 6669 + engines: {node: '>=18.12'} 6670 + peerDependencies: 6671 + typescript: '>=4.8.4' 6329 6672 6330 6673 tsdown@0.20.1: 6331 6674 resolution: {integrity: sha512-Wo1BzqNQVZ6SFQV8rjQBwMmNubO+yV3F+vp2WNTjEaS4S5CT1C1dHtUbeFMrCEasZpGy5w6TshpehNnfTe8QBQ==} ··· 6506 6849 unplugin-utils@0.3.1: 6507 6850 resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} 6508 6851 engines: {node: '>=20.19.0'} 6852 + 6853 + unplugin-vue-router@0.16.2: 6854 + resolution: {integrity: sha512-lE6ZjnHaXfS2vFI/PSEwdKcdOo5RwAbCKUnPBIN9YwLgSWas3x+qivzQvJa/uxhKzJldE6WK43aDKjGj9Rij9w==} 6855 + peerDependencies: 6856 + '@vue/compiler-sfc': ^3.5.17 6857 + vue-router: ^4.6.0 6858 + peerDependenciesMeta: 6859 + vue-router: 6860 + optional: true 6509 6861 6510 6862 unplugin-vue-router@0.19.2: 6511 6863 resolution: {integrity: sha512-u5dgLBarxE5cyDK/hzJGfpCTLIAyiTXGlo85COuD4Nssj6G7NxS+i9mhCWz/1p/ud1eMwdcUbTXehQe41jYZUA==} ··· 6812 7164 vue-flow-layout@0.2.0: 6813 7165 resolution: {integrity: sha512-zKgsWWkXq0xrus7H4Mc+uFs1ESrmdTXlO0YNbR6wMdPaFvosL3fMB8N7uTV308UhGy9UvTrGhIY7mVz9eN+L0Q==} 6814 7166 7167 + vue-i18n@11.2.8: 7168 + resolution: {integrity: sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg==} 7169 + engines: {node: '>= 16'} 7170 + peerDependencies: 7171 + vue: ^3.0.0 7172 + 6815 7173 vue-router@4.6.4: 6816 7174 resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} 6817 7175 peerDependencies: ··· 7012 7370 yallist@5.0.0: 7013 7371 resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} 7014 7372 engines: {node: '>=18'} 7373 + 7374 + yaml-eslint-parser@1.3.2: 7375 + resolution: {integrity: sha512-odxVsHAkZYYglR30aPYRY4nUGJnoJ2y1ww2HDvZALo0BDETv9kWbi16J52eHs+PWRNmF4ub6nZqfVOeesOvntg==} 7376 + engines: {node: ^14.17.0 || >=16.0.0} 7015 7377 7016 7378 yaml@2.8.2: 7017 7379 resolution: {integrity: sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==} ··· 8050 8412 dependencies: 8051 8413 eslint: 9.39.2(jiti@2.6.1) 8052 8414 eslint-visitor-keys: 3.4.3 8053 - optional: true 8054 8415 8055 - '@eslint-community/regexpp@4.12.2': 8056 - optional: true 8416 + '@eslint-community/regexpp@4.12.2': {} 8057 8417 8058 8418 '@eslint/config-array@0.21.1': 8059 8419 dependencies: ··· 8062 8422 minimatch: 3.1.2 8063 8423 transitivePeerDependencies: 8064 8424 - supports-color 8065 - optional: true 8066 8425 8067 8426 '@eslint/config-helpers@0.4.2': 8068 8427 dependencies: 8069 8428 '@eslint/core': 0.17.0 8070 - optional: true 8071 8429 8072 8430 '@eslint/core@0.17.0': 8073 8431 dependencies: 8074 8432 '@types/json-schema': 7.0.15 8075 - optional: true 8076 8433 8077 8434 '@eslint/eslintrc@3.3.3': 8078 8435 dependencies: ··· 8087 8444 strip-json-comments: 3.1.1 8088 8445 transitivePeerDependencies: 8089 8446 - supports-color 8090 - optional: true 8091 8447 8092 - '@eslint/js@9.39.2': 8093 - optional: true 8448 + '@eslint/js@9.39.2': {} 8094 8449 8095 - '@eslint/object-schema@2.1.7': 8096 - optional: true 8450 + '@eslint/object-schema@2.1.7': {} 8097 8451 8098 8452 '@eslint/plugin-kit@0.4.1': 8099 8453 dependencies: 8100 8454 '@eslint/core': 0.17.0 8101 8455 levn: 0.4.1 8102 - optional: true 8103 8456 8104 8457 '@exodus/bytes@1.9.0': 8105 8458 optional: true ··· 8108 8461 dependencies: 8109 8462 kleur: 4.1.5 8110 8463 8111 - '@humanfs/core@0.19.1': 8112 - optional: true 8464 + '@humanfs/core@0.19.1': {} 8113 8465 8114 8466 '@humanfs/node@0.16.7': 8115 8467 dependencies: 8116 8468 '@humanfs/core': 0.19.1 8117 8469 '@humanwhocodes/retry': 0.4.3 8118 - optional: true 8119 8470 8120 - '@humanwhocodes/module-importer@1.0.1': 8121 - optional: true 8471 + '@humanwhocodes/module-importer@1.0.1': {} 8122 8472 8123 - '@humanwhocodes/retry@0.4.3': 8124 - optional: true 8473 + '@humanwhocodes/retry@0.4.3': {} 8125 8474 8126 8475 '@iconify-json/carbon@1.2.18': 8127 8476 dependencies: ··· 8243 8592 '@img/sharp-win32-x64@0.34.5': 8244 8593 optional: true 8245 8594 8595 + '@intlify/bundle-utils@11.0.3(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))': 8596 + dependencies: 8597 + '@intlify/message-compiler': 11.2.8 8598 + '@intlify/shared': 11.2.8 8599 + acorn: 8.15.0 8600 + esbuild: 0.25.12 8601 + escodegen: 2.1.0 8602 + estree-walker: 2.0.2 8603 + jsonc-eslint-parser: 2.4.2 8604 + source-map-js: 1.2.1 8605 + yaml-eslint-parser: 1.3.2 8606 + optionalDependencies: 8607 + vue-i18n: 11.2.8(vue@3.5.27(typescript@5.9.3)) 8608 + 8609 + '@intlify/core-base@11.2.8': 8610 + dependencies: 8611 + '@intlify/message-compiler': 11.2.8 8612 + '@intlify/shared': 11.2.8 8613 + 8614 + '@intlify/core@11.2.8': 8615 + dependencies: 8616 + '@intlify/core-base': 11.2.8 8617 + '@intlify/shared': 11.2.8 8618 + 8619 + '@intlify/h3@0.7.4': 8620 + dependencies: 8621 + '@intlify/core': 11.2.8 8622 + '@intlify/utils': 0.13.0 8623 + 8624 + '@intlify/message-compiler@11.2.8': 8625 + dependencies: 8626 + '@intlify/shared': 11.2.8 8627 + source-map-js: 1.2.1 8628 + 8629 + '@intlify/shared@11.2.8': {} 8630 + 8631 + '@intlify/unplugin-vue-i18n@11.0.3(@vue/compiler-dom@3.5.27)(eslint@9.39.2(jiti@2.6.1))(rollup@4.56.0)(typescript@5.9.3)(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))': 8632 + dependencies: 8633 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) 8634 + '@intlify/bundle-utils': 11.0.3(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3))) 8635 + '@intlify/shared': 11.2.8 8636 + '@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.2.8)(@vue/compiler-dom@3.5.27)(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3)) 8637 + '@rollup/pluginutils': 5.3.0(rollup@4.56.0) 8638 + '@typescript-eslint/scope-manager': 8.54.0 8639 + '@typescript-eslint/typescript-estree': 8.54.0(typescript@5.9.3) 8640 + debug: 4.4.3 8641 + fast-glob: 3.3.3 8642 + pathe: 2.0.3 8643 + picocolors: 1.1.1 8644 + unplugin: 2.3.11 8645 + vue: 3.5.27(typescript@5.9.3) 8646 + optionalDependencies: 8647 + vue-i18n: 11.2.8(vue@3.5.27(typescript@5.9.3)) 8648 + transitivePeerDependencies: 8649 + - '@vue/compiler-dom' 8650 + - eslint 8651 + - rollup 8652 + - supports-color 8653 + - typescript 8654 + 8655 + '@intlify/utils@0.13.0': {} 8656 + 8657 + '@intlify/vue-i18n-extensions@8.0.0(@intlify/shared@11.2.8)(@vue/compiler-dom@3.5.27)(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3))': 8658 + dependencies: 8659 + '@babel/parser': 7.28.6 8660 + optionalDependencies: 8661 + '@intlify/shared': 11.2.8 8662 + '@vue/compiler-dom': 3.5.27 8663 + vue: 3.5.27(typescript@5.9.3) 8664 + vue-i18n: 11.2.8(vue@3.5.27(typescript@5.9.3)) 8665 + 8246 8666 '@ioredis/commands@1.5.0': {} 8247 8667 8248 8668 '@isaacs/balanced-match@4.0.1': {} ··· 8308 8728 transitivePeerDependencies: 8309 8729 - encoding 8310 8730 - supports-color 8731 + 8732 + '@miyaneee/rollup-plugin-json5@1.2.0(rollup@4.56.0)': 8733 + dependencies: 8734 + '@rollup/pluginutils': 5.3.0(rollup@4.56.0) 8735 + json5: 2.2.3 8736 + rollup: 4.56.0 8311 8737 8312 8738 '@napi-rs/wasm-runtime@1.1.1': 8313 8739 dependencies: ··· 8832 9258 - magicast 8833 9259 - vitest 8834 9260 9261 + '@nuxtjs/i18n@10.2.1(@vue/compiler-dom@3.5.27)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.9.2)(magicast@0.5.1)(rollup@4.56.0)(vue@3.5.27(typescript@5.9.3))': 9262 + dependencies: 9263 + '@intlify/core': 11.2.8 9264 + '@intlify/h3': 0.7.4 9265 + '@intlify/shared': 11.2.8 9266 + '@intlify/unplugin-vue-i18n': 11.0.3(@vue/compiler-dom@3.5.27)(eslint@9.39.2(jiti@2.6.1))(rollup@4.56.0)(typescript@5.9.3)(vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3)) 9267 + '@intlify/utils': 0.13.0 9268 + '@miyaneee/rollup-plugin-json5': 1.2.0(rollup@4.56.0) 9269 + '@nuxt/kit': 4.3.0(magicast@0.5.1) 9270 + '@rollup/plugin-yaml': 4.1.2(rollup@4.56.0) 9271 + '@vue/compiler-sfc': 3.5.27 9272 + defu: 6.1.4 9273 + devalue: 5.6.2 9274 + h3: 1.15.5 9275 + knitwork: 1.3.0 9276 + magic-string: 0.30.21 9277 + mlly: 1.8.0 9278 + nuxt-define: 1.0.0 9279 + ohash: 2.0.11 9280 + oxc-parser: 0.95.0 9281 + oxc-transform: 0.95.0 9282 + oxc-walker: 0.5.2(oxc-parser@0.95.0) 9283 + pathe: 2.0.3 9284 + typescript: 5.9.3 9285 + ufo: 1.6.3 9286 + unplugin: 2.3.11 9287 + unplugin-vue-router: 0.16.2(@vue/compiler-sfc@3.5.27)(vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3)) 9288 + unstorage: 1.17.4(db0@0.3.4)(ioredis@5.9.2) 9289 + vue-i18n: 11.2.8(vue@3.5.27(typescript@5.9.3)) 9290 + vue-router: 4.6.4(vue@3.5.27(typescript@5.9.3)) 9291 + transitivePeerDependencies: 9292 + - '@azure/app-configuration' 9293 + - '@azure/cosmos' 9294 + - '@azure/data-tables' 9295 + - '@azure/identity' 9296 + - '@azure/keyvault-secrets' 9297 + - '@azure/storage-blob' 9298 + - '@capacitor/preferences' 9299 + - '@deno/kv' 9300 + - '@netlify/blobs' 9301 + - '@planetscale/database' 9302 + - '@upstash/redis' 9303 + - '@vercel/blob' 9304 + - '@vercel/functions' 9305 + - '@vercel/kv' 9306 + - '@vue/compiler-dom' 9307 + - aws4fetch 9308 + - db0 9309 + - eslint 9310 + - idb-keyval 9311 + - ioredis 9312 + - magicast 9313 + - petite-vue-i18n 9314 + - rollup 9315 + - supports-color 9316 + - uploadthing 9317 + - vue 9318 + 8835 9319 '@one-ini/wasm@0.1.1': {} 8836 9320 8837 9321 '@oxc-minify/binding-android-arm-eabi@0.110.0': ··· 8902 9386 '@oxc-parser/binding-android-arm64@0.110.0': 8903 9387 optional: true 8904 9388 9389 + '@oxc-parser/binding-android-arm64@0.95.0': 9390 + optional: true 9391 + 8905 9392 '@oxc-parser/binding-darwin-arm64@0.110.0': 9393 + optional: true 9394 + 9395 + '@oxc-parser/binding-darwin-arm64@0.95.0': 8906 9396 optional: true 8907 9397 8908 9398 '@oxc-parser/binding-darwin-x64@0.110.0': 8909 9399 optional: true 8910 9400 9401 + '@oxc-parser/binding-darwin-x64@0.95.0': 9402 + optional: true 9403 + 8911 9404 '@oxc-parser/binding-freebsd-x64@0.110.0': 8912 9405 optional: true 8913 9406 9407 + '@oxc-parser/binding-freebsd-x64@0.95.0': 9408 + optional: true 9409 + 8914 9410 '@oxc-parser/binding-linux-arm-gnueabihf@0.110.0': 8915 9411 optional: true 8916 9412 9413 + '@oxc-parser/binding-linux-arm-gnueabihf@0.95.0': 9414 + optional: true 9415 + 8917 9416 '@oxc-parser/binding-linux-arm-musleabihf@0.110.0': 8918 9417 optional: true 8919 9418 9419 + '@oxc-parser/binding-linux-arm-musleabihf@0.95.0': 9420 + optional: true 9421 + 8920 9422 '@oxc-parser/binding-linux-arm64-gnu@0.110.0': 9423 + optional: true 9424 + 9425 + '@oxc-parser/binding-linux-arm64-gnu@0.95.0': 8921 9426 optional: true 8922 9427 8923 9428 '@oxc-parser/binding-linux-arm64-musl@0.110.0': 8924 9429 optional: true 8925 9430 9431 + '@oxc-parser/binding-linux-arm64-musl@0.95.0': 9432 + optional: true 9433 + 8926 9434 '@oxc-parser/binding-linux-ppc64-gnu@0.110.0': 8927 9435 optional: true 8928 9436 8929 9437 '@oxc-parser/binding-linux-riscv64-gnu@0.110.0': 8930 9438 optional: true 8931 9439 9440 + '@oxc-parser/binding-linux-riscv64-gnu@0.95.0': 9441 + optional: true 9442 + 8932 9443 '@oxc-parser/binding-linux-riscv64-musl@0.110.0': 8933 9444 optional: true 8934 9445 8935 9446 '@oxc-parser/binding-linux-s390x-gnu@0.110.0': 8936 9447 optional: true 8937 9448 9449 + '@oxc-parser/binding-linux-s390x-gnu@0.95.0': 9450 + optional: true 9451 + 8938 9452 '@oxc-parser/binding-linux-x64-gnu@0.110.0': 8939 9453 optional: true 8940 9454 9455 + '@oxc-parser/binding-linux-x64-gnu@0.95.0': 9456 + optional: true 9457 + 8941 9458 '@oxc-parser/binding-linux-x64-musl@0.110.0': 8942 9459 optional: true 8943 9460 9461 + '@oxc-parser/binding-linux-x64-musl@0.95.0': 9462 + optional: true 9463 + 8944 9464 '@oxc-parser/binding-openharmony-arm64@0.110.0': 8945 9465 optional: true 8946 9466 ··· 8949 9469 '@napi-rs/wasm-runtime': 1.1.1 8950 9470 optional: true 8951 9471 9472 + '@oxc-parser/binding-wasm32-wasi@0.95.0': 9473 + dependencies: 9474 + '@napi-rs/wasm-runtime': 1.1.1 9475 + optional: true 9476 + 8952 9477 '@oxc-parser/binding-win32-arm64-msvc@0.110.0': 8953 9478 optional: true 8954 9479 9480 + '@oxc-parser/binding-win32-arm64-msvc@0.95.0': 9481 + optional: true 9482 + 8955 9483 '@oxc-parser/binding-win32-ia32-msvc@0.110.0': 8956 9484 optional: true 8957 9485 8958 9486 '@oxc-parser/binding-win32-x64-msvc@0.110.0': 8959 9487 optional: true 8960 9488 9489 + '@oxc-parser/binding-win32-x64-msvc@0.95.0': 9490 + optional: true 9491 + 8961 9492 '@oxc-project/runtime@0.110.0': {} 8962 9493 8963 9494 '@oxc-project/types@0.110.0': {} 8964 9495 9496 + '@oxc-project/types@0.95.0': {} 9497 + 8965 9498 '@oxc-transform/binding-android-arm-eabi@0.110.0': 8966 9499 optional: true 8967 9500 8968 9501 '@oxc-transform/binding-android-arm64@0.110.0': 9502 + optional: true 9503 + 9504 + '@oxc-transform/binding-android-arm64@0.95.0': 8969 9505 optional: true 8970 9506 8971 9507 '@oxc-transform/binding-darwin-arm64@0.110.0': 9508 + optional: true 9509 + 9510 + '@oxc-transform/binding-darwin-arm64@0.95.0': 8972 9511 optional: true 8973 9512 8974 9513 '@oxc-transform/binding-darwin-x64@0.110.0': 8975 9514 optional: true 8976 9515 9516 + '@oxc-transform/binding-darwin-x64@0.95.0': 9517 + optional: true 9518 + 8977 9519 '@oxc-transform/binding-freebsd-x64@0.110.0': 8978 9520 optional: true 8979 9521 9522 + '@oxc-transform/binding-freebsd-x64@0.95.0': 9523 + optional: true 9524 + 8980 9525 '@oxc-transform/binding-linux-arm-gnueabihf@0.110.0': 8981 9526 optional: true 8982 9527 9528 + '@oxc-transform/binding-linux-arm-gnueabihf@0.95.0': 9529 + optional: true 9530 + 8983 9531 '@oxc-transform/binding-linux-arm-musleabihf@0.110.0': 8984 9532 optional: true 8985 9533 9534 + '@oxc-transform/binding-linux-arm-musleabihf@0.95.0': 9535 + optional: true 9536 + 8986 9537 '@oxc-transform/binding-linux-arm64-gnu@0.110.0': 8987 9538 optional: true 8988 9539 9540 + '@oxc-transform/binding-linux-arm64-gnu@0.95.0': 9541 + optional: true 9542 + 8989 9543 '@oxc-transform/binding-linux-arm64-musl@0.110.0': 9544 + optional: true 9545 + 9546 + '@oxc-transform/binding-linux-arm64-musl@0.95.0': 8990 9547 optional: true 8991 9548 8992 9549 '@oxc-transform/binding-linux-ppc64-gnu@0.110.0': ··· 8995 9552 '@oxc-transform/binding-linux-riscv64-gnu@0.110.0': 8996 9553 optional: true 8997 9554 9555 + '@oxc-transform/binding-linux-riscv64-gnu@0.95.0': 9556 + optional: true 9557 + 8998 9558 '@oxc-transform/binding-linux-riscv64-musl@0.110.0': 8999 9559 optional: true 9000 9560 9001 9561 '@oxc-transform/binding-linux-s390x-gnu@0.110.0': 9002 9562 optional: true 9003 9563 9564 + '@oxc-transform/binding-linux-s390x-gnu@0.95.0': 9565 + optional: true 9566 + 9004 9567 '@oxc-transform/binding-linux-x64-gnu@0.110.0': 9005 9568 optional: true 9006 9569 9570 + '@oxc-transform/binding-linux-x64-gnu@0.95.0': 9571 + optional: true 9572 + 9007 9573 '@oxc-transform/binding-linux-x64-musl@0.110.0': 9574 + optional: true 9575 + 9576 + '@oxc-transform/binding-linux-x64-musl@0.95.0': 9008 9577 optional: true 9009 9578 9010 9579 '@oxc-transform/binding-openharmony-arm64@0.110.0': ··· 9015 9584 '@napi-rs/wasm-runtime': 1.1.1 9016 9585 optional: true 9017 9586 9587 + '@oxc-transform/binding-wasm32-wasi@0.95.0': 9588 + dependencies: 9589 + '@napi-rs/wasm-runtime': 1.1.1 9590 + optional: true 9591 + 9018 9592 '@oxc-transform/binding-win32-arm64-msvc@0.110.0': 9019 9593 optional: true 9020 9594 9595 + '@oxc-transform/binding-win32-arm64-msvc@0.95.0': 9596 + optional: true 9597 + 9021 9598 '@oxc-transform/binding-win32-ia32-msvc@0.110.0': 9022 9599 optional: true 9023 9600 9024 9601 '@oxc-transform/binding-win32-x64-msvc@0.110.0': 9602 + optional: true 9603 + 9604 + '@oxc-transform/binding-win32-x64-msvc@0.95.0': 9025 9605 optional: true 9026 9606 9027 9607 '@oxfmt/darwin-arm64@0.26.0': ··· 9366 9946 optionalDependencies: 9367 9947 rollup: 4.56.0 9368 9948 9949 + '@rollup/plugin-yaml@4.1.2(rollup@4.56.0)': 9950 + dependencies: 9951 + '@rollup/pluginutils': 5.3.0(rollup@4.56.0) 9952 + js-yaml: 4.1.1 9953 + tosource: 2.0.0-alpha.3 9954 + optionalDependencies: 9955 + rollup: 4.56.0 9956 + 9369 9957 '@rollup/pluginutils@3.1.0(rollup@2.79.2)': 9370 9958 dependencies: 9371 9959 '@types/estree': 0.0.39 ··· 9601 10189 dependencies: 9602 10190 '@types/node': 24.10.9 9603 10191 10192 + '@typescript-eslint/project-service@8.54.0(typescript@5.9.3)': 10193 + dependencies: 10194 + '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) 10195 + '@typescript-eslint/types': 8.54.0 10196 + debug: 4.4.3 10197 + typescript: 5.9.3 10198 + transitivePeerDependencies: 10199 + - supports-color 10200 + 10201 + '@typescript-eslint/scope-manager@8.54.0': 10202 + dependencies: 10203 + '@typescript-eslint/types': 8.54.0 10204 + '@typescript-eslint/visitor-keys': 8.54.0 10205 + 10206 + '@typescript-eslint/tsconfig-utils@8.54.0(typescript@5.9.3)': 10207 + dependencies: 10208 + typescript: 5.9.3 10209 + 10210 + '@typescript-eslint/types@8.54.0': {} 10211 + 10212 + '@typescript-eslint/typescript-estree@8.54.0(typescript@5.9.3)': 10213 + dependencies: 10214 + '@typescript-eslint/project-service': 8.54.0(typescript@5.9.3) 10215 + '@typescript-eslint/tsconfig-utils': 8.54.0(typescript@5.9.3) 10216 + '@typescript-eslint/types': 8.54.0 10217 + '@typescript-eslint/visitor-keys': 8.54.0 10218 + debug: 4.4.3 10219 + minimatch: 9.0.5 10220 + semver: 7.7.3 10221 + tinyglobby: 0.2.15 10222 + ts-api-utils: 2.4.0(typescript@5.9.3) 10223 + typescript: 5.9.3 10224 + transitivePeerDependencies: 10225 + - supports-color 10226 + 10227 + '@typescript-eslint/visitor-keys@8.54.0': 10228 + dependencies: 10229 + '@typescript-eslint/types': 8.54.0 10230 + eslint-visitor-keys: 4.2.1 10231 + 9604 10232 '@ungap/structured-clone@1.3.0': {} 9605 10233 9606 10234 '@unhead/vue@2.1.2(vue@3.5.27(typescript@5.9.3))': ··· 10259 10887 acorn-jsx@5.3.2(acorn@8.15.0): 10260 10888 dependencies: 10261 10889 acorn: 8.15.0 10262 - optional: true 10263 10890 10264 10891 acorn@8.15.0: {} 10265 10892 ··· 10280 10907 fast-json-stable-stringify: 2.1.0 10281 10908 json-schema-traverse: 0.4.1 10282 10909 uri-js: 4.4.1 10283 - optional: true 10284 10910 10285 10911 ajv@8.17.1: 10286 10912 dependencies: ··· 10335 10961 - bare-abort-controller 10336 10962 - react-native-b4a 10337 10963 10338 - argparse@2.0.1: 10339 - optional: true 10964 + argparse@2.0.1: {} 10340 10965 10341 10966 array-buffer-byte-length@1.0.2: 10342 10967 dependencies: ··· 10455 11080 dependencies: 10456 11081 balanced-match: 1.0.2 10457 11082 concat-map: 0.0.1 10458 - optional: true 10459 11083 10460 11084 brace-expansion@2.0.2: 10461 11085 dependencies: ··· 10526 11150 call-bind-apply-helpers: 1.0.2 10527 11151 get-intrinsic: 1.3.0 10528 11152 10529 - callsites@3.1.0: 10530 - optional: true 11153 + callsites@3.1.0: {} 10531 11154 10532 11155 camelize@1.0.1: {} 10533 11156 ··· 10546 11169 dependencies: 10547 11170 ansi-styles: 4.3.0 10548 11171 supports-color: 7.2.0 10549 - optional: true 10550 11172 10551 11173 character-entities-html4@2.1.0: {} 10552 11174 ··· 10638 11260 normalize-path: 3.0.0 10639 11261 readable-stream: 4.7.0 10640 11262 10641 - concat-map@0.0.1: 10642 - optional: true 11263 + concat-map@0.0.1: {} 10643 11264 10644 11265 confbox@0.1.8: {} 10645 11266 ··· 10835 11456 decode-bmp: 0.2.1 10836 11457 to-data-view: 1.1.0 10837 11458 10838 - deep-is@0.1.4: 10839 - optional: true 11459 + deep-is@0.1.4: {} 10840 11460 10841 11461 deepmerge@4.3.1: {} 10842 11462 ··· 11121 11741 11122 11742 escape-string-regexp@5.0.0: {} 11123 11743 11744 + escodegen@2.1.0: 11745 + dependencies: 11746 + esprima: 4.0.1 11747 + estraverse: 5.3.0 11748 + esutils: 2.0.3 11749 + optionalDependencies: 11750 + source-map: 0.6.1 11751 + 11124 11752 eslint-scope@5.1.1: 11125 11753 dependencies: 11126 11754 esrecurse: 4.3.0 ··· 11130 11758 dependencies: 11131 11759 esrecurse: 4.3.0 11132 11760 estraverse: 5.3.0 11133 - optional: true 11134 11761 11135 - eslint-visitor-keys@3.4.3: 11136 - optional: true 11762 + eslint-visitor-keys@3.4.3: {} 11137 11763 11138 - eslint-visitor-keys@4.2.1: 11139 - optional: true 11764 + eslint-visitor-keys@4.2.1: {} 11140 11765 11141 11766 eslint@9.39.2(jiti@2.6.1): 11142 11767 dependencies: ··· 11178 11803 jiti: 2.6.1 11179 11804 transitivePeerDependencies: 11180 11805 - supports-color 11181 - optional: true 11182 11806 11183 11807 espree@10.4.0: 11184 11808 dependencies: 11185 11809 acorn: 8.15.0 11186 11810 acorn-jsx: 5.3.2(acorn@8.15.0) 11187 11811 eslint-visitor-keys: 4.2.1 11188 - optional: true 11812 + 11813 + espree@9.6.1: 11814 + dependencies: 11815 + acorn: 8.15.0 11816 + acorn-jsx: 5.3.2(acorn@8.15.0) 11817 + eslint-visitor-keys: 3.4.3 11818 + 11819 + esprima@4.0.1: {} 11189 11820 11190 11821 esquery@1.7.0: 11191 11822 dependencies: 11192 11823 estraverse: 5.3.0 11193 - optional: true 11194 11824 11195 11825 esrecurse@4.3.0: 11196 11826 dependencies: ··· 11269 11899 11270 11900 fast-json-stable-stringify@2.1.0: {} 11271 11901 11272 - fast-levenshtein@2.0.6: 11273 - optional: true 11902 + fast-levenshtein@2.0.6: {} 11274 11903 11275 11904 fast-npm-meta@0.4.8: {} 11276 11905 ··· 11293 11922 file-entry-cache@8.0.0: 11294 11923 dependencies: 11295 11924 flat-cache: 4.0.1 11296 - optional: true 11297 11925 11298 11926 file-uri-to-path@1.0.0: {} 11299 11927 ··· 11309 11937 dependencies: 11310 11938 locate-path: 6.0.0 11311 11939 path-exists: 4.0.0 11312 - optional: true 11313 11940 11314 11941 flat-cache@4.0.1: 11315 11942 dependencies: 11316 11943 flatted: 3.3.3 11317 11944 keyv: 4.5.4 11318 - optional: true 11319 11945 11320 - flatted@3.3.3: 11321 - optional: true 11946 + flatted@3.3.3: {} 11322 11947 11323 11948 fontaine@0.7.0: 11324 11949 dependencies: ··· 11507 12132 glob-parent@6.0.2: 11508 12133 dependencies: 11509 12134 is-glob: 4.0.3 11510 - optional: true 11511 12135 11512 12136 glob-to-regexp@0.4.1: {} 11513 12137 ··· 11541 12165 11542 12166 globals@11.12.0: {} 11543 12167 11544 - globals@14.0.0: 11545 - optional: true 12168 + globals@14.0.0: {} 11546 12169 11547 12170 globalthis@1.0.4: 11548 12171 dependencies: ··· 11722 12345 11723 12346 ieee754@1.2.1: {} 11724 12347 11725 - ignore@5.3.2: 11726 - optional: true 12348 + ignore@5.3.2: {} 11727 12349 11728 12350 ignore@7.0.5: {} 11729 12351 ··· 11735 12357 dependencies: 11736 12358 parent-module: 1.0.1 11737 12359 resolve-from: 4.0.0 11738 - optional: true 11739 12360 11740 12361 import-without-cache@0.2.5: {} 11741 12362 ··· 11747 12368 unplugin: 2.3.11 11748 12369 unplugin-utils: 0.2.5 11749 12370 11750 - imurmurhash@0.1.4: 11751 - optional: true 12371 + imurmurhash@0.1.4: {} 11752 12372 11753 12373 inherits@2.0.4: {} 11754 12374 ··· 12012 12632 js-yaml@4.1.1: 12013 12633 dependencies: 12014 12634 argparse: 2.0.1 12015 - optional: true 12016 12635 12017 12636 jsdom@27.4.0: 12018 12637 dependencies: ··· 12045 12664 12046 12665 jsesc@3.1.0: {} 12047 12666 12048 - json-buffer@3.0.1: 12049 - optional: true 12667 + json-buffer@3.0.1: {} 12050 12668 12051 12669 json-parse-even-better-errors@2.3.1: {} 12052 12670 12053 - json-schema-traverse@0.4.1: 12054 - optional: true 12671 + json-schema-traverse@0.4.1: {} 12055 12672 12056 12673 json-schema-traverse@1.0.0: {} 12057 12674 12058 12675 json-schema@0.4.0: {} 12059 12676 12060 - json-stable-stringify-without-jsonify@1.0.1: 12061 - optional: true 12677 + json-stable-stringify-without-jsonify@1.0.1: {} 12062 12678 12063 12679 json5@2.2.3: {} 12064 12680 12681 + jsonc-eslint-parser@2.4.2: 12682 + dependencies: 12683 + acorn: 8.15.0 12684 + eslint-visitor-keys: 3.4.3 12685 + espree: 9.6.1 12686 + semver: 7.7.3 12687 + 12065 12688 jsonfile@6.2.0: 12066 12689 dependencies: 12067 12690 universalify: 2.0.1 ··· 12073 12696 keyv@4.5.4: 12074 12697 dependencies: 12075 12698 json-buffer: 3.0.1 12076 - optional: true 12077 12699 12078 12700 kleur@3.0.3: {} 12079 12701 ··· 12098 12720 dependencies: 12099 12721 prelude-ls: 1.2.1 12100 12722 type-check: 0.4.0 12101 - optional: true 12102 12723 12103 12724 lighthouse-logger@2.0.2: 12104 12725 dependencies: ··· 12214 12835 locate-path@6.0.0: 12215 12836 dependencies: 12216 12837 p-locate: 5.0.0 12217 - optional: true 12218 12838 12219 12839 lodash.debounce@4.0.8: {} 12220 12840 ··· 12224 12844 12225 12845 lodash.memoize@4.1.2: {} 12226 12846 12227 - lodash.merge@4.6.2: 12228 - optional: true 12847 + lodash.merge@4.6.2: {} 12229 12848 12230 12849 lodash.sortby@4.7.0: {} 12231 12850 ··· 12354 12973 minimatch@3.1.2: 12355 12974 dependencies: 12356 12975 brace-expansion: 1.1.12 12357 - optional: true 12358 12976 12359 12977 minimatch@5.1.6: 12360 12978 dependencies: ··· 12401 13019 12402 13020 nanotar@0.2.0: {} 12403 13021 12404 - natural-compare@1.4.0: 12405 - optional: true 13022 + natural-compare@1.4.0: {} 12406 13023 12407 13024 neo-async@2.6.2: {} 12408 13025 ··· 12548 13165 nth-check@2.1.1: 12549 13166 dependencies: 12550 13167 boolbase: 1.0.0 13168 + 13169 + nuxt-define@1.0.0: {} 12551 13170 12552 13171 nuxt-og-image@5.1.13(@unhead/vue@2.1.2(vue@3.5.27(typescript@5.9.3)))(magicast@0.5.1)(unstorage@1.17.4(db0@0.3.4)(ioredis@5.9.2))(vite@8.0.0-beta.10(@types/node@25.0.10)(esbuild@0.27.2)(jiti@2.6.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3)): 12553 13172 dependencies: ··· 12812 13431 prelude-ls: 1.2.1 12813 13432 type-check: 0.4.0 12814 13433 word-wrap: 1.2.5 12815 - optional: true 12816 13434 12817 13435 own-keys@1.0.1: 12818 13436 dependencies: ··· 12868 13486 '@oxc-parser/binding-win32-ia32-msvc': 0.110.0 12869 13487 '@oxc-parser/binding-win32-x64-msvc': 0.110.0 12870 13488 13489 + oxc-parser@0.95.0: 13490 + dependencies: 13491 + '@oxc-project/types': 0.95.0 13492 + optionalDependencies: 13493 + '@oxc-parser/binding-android-arm64': 0.95.0 13494 + '@oxc-parser/binding-darwin-arm64': 0.95.0 13495 + '@oxc-parser/binding-darwin-x64': 0.95.0 13496 + '@oxc-parser/binding-freebsd-x64': 0.95.0 13497 + '@oxc-parser/binding-linux-arm-gnueabihf': 0.95.0 13498 + '@oxc-parser/binding-linux-arm-musleabihf': 0.95.0 13499 + '@oxc-parser/binding-linux-arm64-gnu': 0.95.0 13500 + '@oxc-parser/binding-linux-arm64-musl': 0.95.0 13501 + '@oxc-parser/binding-linux-riscv64-gnu': 0.95.0 13502 + '@oxc-parser/binding-linux-s390x-gnu': 0.95.0 13503 + '@oxc-parser/binding-linux-x64-gnu': 0.95.0 13504 + '@oxc-parser/binding-linux-x64-musl': 0.95.0 13505 + '@oxc-parser/binding-wasm32-wasi': 0.95.0 13506 + '@oxc-parser/binding-win32-arm64-msvc': 0.95.0 13507 + '@oxc-parser/binding-win32-x64-msvc': 0.95.0 13508 + 12871 13509 oxc-transform@0.110.0: 12872 13510 optionalDependencies: 12873 13511 '@oxc-transform/binding-android-arm-eabi': 0.110.0 ··· 12891 13529 '@oxc-transform/binding-win32-ia32-msvc': 0.110.0 12892 13530 '@oxc-transform/binding-win32-x64-msvc': 0.110.0 12893 13531 13532 + oxc-transform@0.95.0: 13533 + optionalDependencies: 13534 + '@oxc-transform/binding-android-arm64': 0.95.0 13535 + '@oxc-transform/binding-darwin-arm64': 0.95.0 13536 + '@oxc-transform/binding-darwin-x64': 0.95.0 13537 + '@oxc-transform/binding-freebsd-x64': 0.95.0 13538 + '@oxc-transform/binding-linux-arm-gnueabihf': 0.95.0 13539 + '@oxc-transform/binding-linux-arm-musleabihf': 0.95.0 13540 + '@oxc-transform/binding-linux-arm64-gnu': 0.95.0 13541 + '@oxc-transform/binding-linux-arm64-musl': 0.95.0 13542 + '@oxc-transform/binding-linux-riscv64-gnu': 0.95.0 13543 + '@oxc-transform/binding-linux-s390x-gnu': 0.95.0 13544 + '@oxc-transform/binding-linux-x64-gnu': 0.95.0 13545 + '@oxc-transform/binding-linux-x64-musl': 0.95.0 13546 + '@oxc-transform/binding-wasm32-wasi': 0.95.0 13547 + '@oxc-transform/binding-win32-arm64-msvc': 0.95.0 13548 + '@oxc-transform/binding-win32-x64-msvc': 0.95.0 13549 + 13550 + oxc-walker@0.5.2(oxc-parser@0.95.0): 13551 + dependencies: 13552 + magic-regexp: 0.10.0 13553 + oxc-parser: 0.95.0 13554 + 12894 13555 oxc-walker@0.7.0(oxc-parser@0.110.0): 12895 13556 dependencies: 12896 13557 magic-regexp: 0.10.0 ··· 12933 13594 p-limit@3.1.0: 12934 13595 dependencies: 12935 13596 yocto-queue: 0.1.0 12936 - optional: true 12937 13597 12938 13598 p-locate@5.0.0: 12939 13599 dependencies: 12940 13600 p-limit: 3.1.0 12941 - optional: true 12942 13601 12943 13602 package-json-from-dist@1.0.1: {} 12944 13603 ··· 12949 13608 parent-module@1.0.1: 12950 13609 dependencies: 12951 13610 callsites: 3.1.0 12952 - optional: true 12953 13611 12954 13612 parse-css-color@0.2.1: 12955 13613 dependencies: ··· 12978 13636 12979 13637 path-browserify@1.0.1: {} 12980 13638 12981 - path-exists@4.0.0: 12982 - optional: true 13639 + path-exists@4.0.0: {} 12983 13640 12984 13641 path-key@3.1.1: {} 12985 13642 ··· 13201 13858 picocolors: 1.1.1 13202 13859 source-map-js: 1.2.1 13203 13860 13204 - prelude-ls@1.2.1: 13205 - optional: true 13861 + prelude-ls@1.2.1: {} 13206 13862 13207 13863 prettier@3.8.1: {} 13208 13864 ··· 13341 13997 13342 13998 require-from-string@2.0.2: {} 13343 13999 13344 - resolve-from@4.0.0: 13345 - optional: true 14000 + resolve-from@4.0.0: {} 13346 14001 13347 14002 resolve-from@5.0.0: {} 13348 14003 ··· 13847 14502 13848 14503 strip-final-newline@4.0.0: {} 13849 14504 13850 - strip-json-comments@3.1.1: 13851 - optional: true 14505 + strip-json-comments@3.1.1: {} 13852 14506 13853 14507 strip-literal@3.1.0: 13854 14508 dependencies: ··· 13980 14634 13981 14635 toidentifier@1.0.1: {} 13982 14636 14637 + tosource@2.0.0-alpha.3: {} 14638 + 13983 14639 totalist@3.0.1: {} 13984 14640 13985 14641 tough-cookie@6.0.0: ··· 14002 14658 14003 14659 trim-lines@3.0.1: {} 14004 14660 14661 + ts-api-utils@2.4.0(typescript@5.9.3): 14662 + dependencies: 14663 + typescript: 5.9.3 14664 + 14005 14665 tsdown@0.20.1(typescript@5.9.3)(vue-tsc@3.2.2(typescript@5.9.3)): 14006 14666 dependencies: 14007 14667 ansis: 4.2.0 ··· 14042 14702 type-check@0.4.0: 14043 14703 dependencies: 14044 14704 prelude-ls: 1.2.1 14045 - optional: true 14046 14705 14047 14706 type-fest@0.16.0: {} 14048 14707 ··· 14245 14904 pathe: 2.0.3 14246 14905 picomatch: 4.0.3 14247 14906 14907 + unplugin-vue-router@0.16.2(@vue/compiler-sfc@3.5.27)(vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3)): 14908 + dependencies: 14909 + '@babel/generator': 7.28.6 14910 + '@vue-macros/common': 3.1.2(vue@3.5.27(typescript@5.9.3)) 14911 + '@vue/compiler-sfc': 3.5.27 14912 + '@vue/language-core': 3.2.2 14913 + ast-walker-scope: 0.8.3 14914 + chokidar: 4.0.3 14915 + json5: 2.2.3 14916 + local-pkg: 1.1.2 14917 + magic-string: 0.30.21 14918 + mlly: 1.8.0 14919 + muggle-string: 0.4.1 14920 + pathe: 2.0.3 14921 + picomatch: 4.0.3 14922 + scule: 1.3.0 14923 + tinyglobby: 0.2.15 14924 + unplugin: 2.3.11 14925 + unplugin-utils: 0.3.1 14926 + yaml: 2.8.2 14927 + optionalDependencies: 14928 + vue-router: 4.6.4(vue@3.5.27(typescript@5.9.3)) 14929 + transitivePeerDependencies: 14930 + - vue 14931 + 14248 14932 unplugin-vue-router@0.19.2(@vue/compiler-sfc@3.5.27)(vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3)): 14249 14933 dependencies: 14250 14934 '@babel/generator': 7.28.6 ··· 14331 15015 uri-js@4.4.1: 14332 15016 dependencies: 14333 15017 punycode: 2.3.1 14334 - optional: true 14335 15018 14336 15019 util-deprecate@1.0.2: {} 14337 15020 ··· 14541 15224 14542 15225 vue-flow-layout@0.2.0: {} 14543 15226 15227 + vue-i18n@11.2.8(vue@3.5.27(typescript@5.9.3)): 15228 + dependencies: 15229 + '@intlify/core-base': 11.2.8 15230 + '@intlify/shared': 11.2.8 15231 + '@vue/devtools-api': 6.6.4 15232 + vue: 3.5.27(typescript@5.9.3) 15233 + 14544 15234 vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)): 14545 15235 dependencies: 14546 15236 '@vue/devtools-api': 6.6.4 ··· 14689 15379 dependencies: 14690 15380 isexe: 3.1.1 14691 15381 14692 - word-wrap@1.2.5: 14693 - optional: true 15382 + word-wrap@1.2.5: {} 14694 15383 14695 15384 workbox-background-sync@7.4.0: 14696 15385 dependencies: ··· 14841 15530 14842 15531 yallist@5.0.0: {} 14843 15532 15533 + yaml-eslint-parser@1.3.2: 15534 + dependencies: 15535 + eslint-visitor-keys: 3.4.3 15536 + yaml: 2.8.2 15537 + 14844 15538 yaml@2.8.2: {} 14845 15539 14846 15540 yargs-parser@21.1.1: {} ··· 14855 15549 y18n: 5.0.8 14856 15550 yargs-parser: 21.1.1 14857 15551 14858 - yocto-queue@0.1.0: 14859 - optional: true 15552 + yocto-queue@0.1.0: {} 14860 15553 14861 15554 yoctocolors@2.1.2: {} 14862 15555
+1
shared/utils/npm.ts
··· 1 + import { createError } from 'h3' 1 2 import validatePackageName from 'validate-npm-package-name' 2 3 3 4 /**