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

refactor(i18n): use `$t` in script (#182)

authored by

Bobbie Goede and committed by
GitHub
17b07e0a 0d28eb47

+214 -245
+4 -3
CONTRIBUTING.md
··· 214 214 215 215 ### Approach 216 216 217 - - All user-facing strings should use translation keys via `$t()` in templates or `t()` in script 217 + - All user-facing strings should use translation keys via `$t()` in templates and script 218 218 - Translation files live in `i18n/locales/` (e.g., `en.json`) 219 219 - We use the `no_prefix` strategy (no `/en/` or `/fr/` in URLs) 220 220 - Locale preference is stored in cookies and respected on subsequent visits ··· 233 233 Or in script: 234 234 235 235 ```typescript 236 - const { t } = useI18n() 237 - const message = t('my.translation.key') 236 + <script setup lang="ts"> 237 + const message = computed(() => $t('my.translation.key')) 238 + </script> 238 239 ``` 239 240 240 241 3. For dynamic values, use interpolation:
+3 -5
app/components/ClaimPackageModal.vue
··· 8 8 9 9 const open = defineModel<boolean>('open', { default: false }) 10 10 11 - const { t } = useI18n() 12 - 13 11 const { 14 12 isConnected, 15 13 state, ··· 34 32 try { 35 33 checkResult.value = await checkPackageName(props.packageName) 36 34 } catch (err) { 37 - publishError.value = err instanceof Error ? err.message : t('claim.modal.failed_to_check') 35 + publishError.value = err instanceof Error ? err.message : $t('claim.modal.failed_to_check') 38 36 } finally { 39 37 isChecking.value = false 40 38 } ··· 84 82 connectorModalOpen.value = true 85 83 } 86 84 } catch (err) { 87 - publishError.value = err instanceof Error ? err.message : t('claim.modal.failed_to_claim') 85 + publishError.value = err instanceof Error ? err.message : $t('claim.modal.failed_to_claim') 88 86 } finally { 89 87 isPublishing.value = false 90 88 } ··· 171 169 172 170 <!-- Loading state --> 173 171 <div v-if="isChecking" class="py-8 text-center"> 174 - <LoadingSpinner :text="t('claim.modal.checking')" /> 172 + <LoadingSpinner :text="$t('claim.modal.checking')" /> 175 173 </div> 176 174 177 175 <!-- Success state -->
+7 -9
app/components/ConnectorStatus.client.vue
··· 12 12 const showModal = shallowRef(false) 13 13 const showTooltip = shallowRef(false) 14 14 15 - const { t } = useI18n() 16 - 17 15 const tooltipText = computed(() => { 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') 16 + if (isConnecting.value) return $t('connector.status.connecting') 17 + if (isConnected.value) return $t('connector.status.connected') 18 + return $t('connector.status.connect_cli') 21 19 }) 22 20 23 21 const statusColor = computed(() => { ··· 31 29 32 30 const ariaLabel = computed(() => { 33 31 if (error.value) return error.value 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') 32 + if (isConnecting.value) return $t('connector.status.aria_connecting') 33 + if (isConnected.value) return $t('connector.status.aria_connected') 34 + return $t('connector.status.aria_click_to_connect') 37 35 }) 38 36 </script> 39 37 ··· 62 60 <img 63 61 v-if="isConnected && avatar" 64 62 :src="avatar" 65 - :alt="t('connector.status.avatar_alt', { user: npmUser })" 63 + :alt="$t('connector.status.avatar_alt', { user: npmUser })" 66 64 width="24" 67 65 height="24" 68 66 class="w-6 h-6 rounded-full"
+8 -9
app/components/HeaderOrgsDropdown.vue
··· 3 3 username: string 4 4 }>() 5 5 6 - const { t } = useI18n() 7 6 const { listUserOrgs } = useConnector() 8 7 9 8 const isOpen = ref(false) ··· 23 22 // Already sorted alphabetically by server, take top 10 24 23 orgs.value = orgList.slice(0, 10) 25 24 } else { 26 - error.value = t('header.orgs_dropdown.error') 25 + error.value = $t('header.orgs_dropdown.error') 27 26 } 28 27 hasLoaded.value = true 29 28 } catch { 30 - error.value = t('header.orgs_dropdown.error') 29 + error.value = $t('header.orgs_dropdown.error') 31 30 } finally { 32 31 isLoading.value = false 33 32 } ··· 62 61 :to="`/~${username}/orgs`" 63 62 class="link-subtle font-mono text-sm inline-flex items-center gap-1" 64 63 > 65 - {{ t('header.orgs') }} 64 + {{ $t('header.orgs') }} 66 65 <span 67 66 class="i-carbon-chevron-down w-3 h-3 transition-transform duration-200" 68 67 :class="{ 'rotate-180': isOpen }" ··· 80 79 <div class="bg-bg-elevated border border-border rounded-lg shadow-lg overflow-hidden"> 81 80 <div class="px-3 py-2 border-b border-border"> 82 81 <span class="font-mono text-xs text-fg-subtle">{{ 83 - t('header.orgs_dropdown.title') 82 + $t('header.orgs_dropdown.title') 84 83 }}</span> 85 84 </div> 86 85 87 86 <div v-if="isLoading" class="px-3 py-4 text-center"> 88 - <span class="text-fg-muted text-sm">{{ t('header.orgs_dropdown.loading') }}</span> 87 + <span class="text-fg-muted text-sm">{{ $t('header.orgs_dropdown.loading') }}</span> 89 88 </div> 90 89 91 90 <div v-else-if="error" class="px-3 py-4 text-center"> 92 - <span class="text-fg-muted text-sm">{{ t('header.orgs_dropdown.error') }}</span> 91 + <span class="text-fg-muted text-sm">{{ $t('header.orgs_dropdown.error') }}</span> 93 92 </div> 94 93 95 94 <ul v-else-if="orgs.length > 0" class="py-1 max-h-80 overflow-y-auto"> ··· 104 103 </ul> 105 104 106 105 <div v-else class="px-3 py-4 text-center"> 107 - <span class="text-fg-muted text-sm">{{ t('header.orgs_dropdown.empty') }}</span> 106 + <span class="text-fg-muted text-sm">{{ $t('header.orgs_dropdown.empty') }}</span> 108 107 </div> 109 108 110 109 <div class="px-3 py-2 border-t border-border"> ··· 112 111 :to="`/~${username}/orgs`" 113 112 class="link-subtle font-mono text-xs inline-flex items-center gap-1" 114 113 > 115 - {{ t('header.orgs_dropdown.view_all') }} 114 + {{ $t('header.orgs_dropdown.view_all') }} 116 115 <span class="i-carbon-arrow-right w-3 h-3" aria-hidden="true" /> 117 116 </NuxtLink> 118 117 </div>
+8 -9
app/components/HeaderPackagesDropdown.vue
··· 3 3 username: string 4 4 }>() 5 5 6 - const { t } = useI18n() 7 6 const { listUserPackages } = useConnector() 8 7 9 8 const isOpen = ref(false) ··· 23 22 // Sort alphabetically and take top 10 24 23 packages.value = Object.keys(pkgMap).sort().slice(0, 10) 25 24 } else { 26 - error.value = t('header.packages_dropdown.error') 25 + error.value = $t('header.packages_dropdown.error') 27 26 } 28 27 hasLoaded.value = true 29 28 } catch { 30 - error.value = t('header.packages_dropdown.error') 29 + error.value = $t('header.packages_dropdown.error') 31 30 } finally { 32 31 isLoading.value = false 33 32 } ··· 62 61 :to="`/~${username}`" 63 62 class="link-subtle font-mono text-sm inline-flex items-center gap-1" 64 63 > 65 - {{ t('header.packages') }} 64 + {{ $t('header.packages') }} 66 65 <span 67 66 class="i-carbon-chevron-down w-3 h-3 transition-transform duration-200" 68 67 :class="{ 'rotate-180': isOpen }" ··· 80 79 <div class="bg-bg-elevated border border-border rounded-lg shadow-lg overflow-hidden"> 81 80 <div class="px-3 py-2 border-b border-border"> 82 81 <span class="font-mono text-xs text-fg-subtle">{{ 83 - t('header.packages_dropdown.title') 82 + $t('header.packages_dropdown.title') 84 83 }}</span> 85 84 </div> 86 85 87 86 <div v-if="isLoading" class="px-3 py-4 text-center"> 88 - <span class="text-fg-muted text-sm">{{ t('header.packages_dropdown.loading') }}</span> 87 + <span class="text-fg-muted text-sm">{{ $t('header.packages_dropdown.loading') }}</span> 89 88 </div> 90 89 91 90 <div v-else-if="error" class="px-3 py-4 text-center"> 92 - <span class="text-fg-muted text-sm">{{ t('header.packages_dropdown.error') }}</span> 91 + <span class="text-fg-muted text-sm">{{ $t('header.packages_dropdown.error') }}</span> 93 92 </div> 94 93 95 94 <ul v-else-if="packages.length > 0" class="py-1 max-h-80 overflow-y-auto"> ··· 104 103 </ul> 105 104 106 105 <div v-else class="px-3 py-4 text-center"> 107 - <span class="text-fg-muted text-sm">{{ t('header.packages_dropdown.empty') }}</span> 106 + <span class="text-fg-muted text-sm">{{ $t('header.packages_dropdown.empty') }}</span> 108 107 </div> 109 108 110 109 <div class="px-3 py-2 border-t border-border"> ··· 112 111 :to="`/~${username}`" 113 112 class="link-subtle font-mono text-xs inline-flex items-center gap-1" 114 113 > 115 - {{ t('header.packages_dropdown.view_all') }} 114 + {{ $t('header.packages_dropdown.view_all') }} 116 115 <span class="i-carbon-arrow-right w-3 h-3" aria-hidden="true" /> 117 116 </NuxtLink> 118 117 </div>
+1 -3
app/components/LoadingSpinner.vue
··· 3 3 /** Text to display next to the spinner */ 4 4 text?: string 5 5 }>() 6 - 7 - const { t } = useI18n() 8 6 </script> 9 7 10 8 <template> ··· 12 10 <span 13 11 class="w-4 h-4 border-2 border-fg-subtle border-t-fg rounded-full motion-safe:animate-spin" 14 12 /> 15 - {{ text ?? t('common.loading') }} 13 + {{ text ?? $t('common.loading') }} 16 14 </div> 17 15 </template>
+7 -9
app/components/OrgMembersPanel.vue
··· 10 10 'select-team': [teamName: string] 11 11 }>() 12 12 13 - const { t } = useI18n() 14 - 15 13 const { 16 14 isConnected, 17 15 lastExecutionTime, ··· 347 345 :aria-pressed="filterRole === role" 348 346 @click="filterRole = role" 349 347 > 350 - {{ t(`org.members.role.${role}`) }} 348 + {{ $t(`org.members.role.${role}`) }} 351 349 <span v-if="role !== 'all'" class="text-fg-subtle">({{ roleCounts[role] }})</span> 352 350 </button> 353 351 </div> ··· 465 463 ) 466 464 " 467 465 > 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> 466 + <option value="developer">{{ $t('org.members.role.developer') }}</option> 467 + <option value="admin">{{ $t('org.members.role.admin') }}</option> 468 + <option value="owner">{{ $t('org.members.role.owner') }}</option> 471 469 </select> 472 470 <!-- Remove button --> 473 471 <button ··· 526 524 name="new-member-role" 527 525 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 526 > 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> 527 + <option value="developer">{{ $t('org.members.role.developer') }}</option> 528 + <option value="admin">{{ $t('org.members.role.admin') }}</option> 529 + <option value="owner">{{ $t('org.members.role.owner') }}</option> 532 530 </select> 533 531 <!-- Team selection --> 534 532 <label for="new-member-team" class="sr-only">{{ $t('org.members.team_label') }}</label>
+10 -12
app/components/PackageDownloadAnalytics.vue
··· 4 4 import { VueUiXy } from 'vue-data-ui/vue-ui-xy' 5 5 import { useDebounceFn } from '@vueuse/core' 6 6 7 - const { t } = useI18n() 8 - 9 7 const { 10 8 weeklyDownloads, 11 9 inModal = false, ··· 398 396 grid: { 399 397 labels: { 400 398 axis: { 401 - yLabel: t('package.downloads.y_axis_label', { granularity: selectedGranularity.value }), 399 + yLabel: $t('package.downloads.y_axis_label', { granularity: selectedGranularity.value }), 402 400 xLabel: packageName, 403 401 yLabelOffsetX: 12, 404 402 fontSize: 24, ··· 469 467 for="granularity" 470 468 class="text-[10px] font-mono text-fg-subtle tracking-wide uppercase" 471 469 > 472 - {{ t('package.downloads.granularity') }} 470 + {{ $t('package.downloads.granularity') }} 473 471 </label> 474 472 475 473 <div ··· 480 478 v-model="selectedGranularity" 481 479 class="w-full bg-transparent font-mono text-sm text-fg outline-none" 482 480 > 483 - <option value="daily">{{ t('package.downloads.granularity_daily') }}</option> 484 - <option value="weekly">{{ t('package.downloads.granularity_weekly') }}</option> 485 - <option value="monthly">{{ t('package.downloads.granularity_monthly') }}</option> 486 - <option value="yearly">{{ t('package.downloads.granularity_yearly') }}</option> 481 + <option value="daily">{{ $t('package.downloads.granularity_daily') }}</option> 482 + <option value="weekly">{{ $t('package.downloads.granularity_weekly') }}</option> 483 + <option value="monthly">{{ $t('package.downloads.granularity_monthly') }}</option> 484 + <option value="yearly">{{ $t('package.downloads.granularity_yearly') }}</option> 487 485 </select> 488 486 </div> 489 487 </div> ··· 495 493 for="startDate" 496 494 class="text-[10px] font-mono text-fg-subtle tracking-wide uppercase" 497 495 > 498 - {{ t('package.downloads.start_date') }} 496 + {{ $t('package.downloads.start_date') }} 499 497 </label> 500 498 <div 501 499 class="flex items-center gap-2 px-2.5 py-1.75 bg-bg-subtle border border-border rounded-md focus-within:(border-border-hover ring-2 ring-fg/50)" ··· 515 513 for="endDate" 516 514 class="text-[10px] font-mono text-fg-subtle tracking-wide uppercase" 517 515 > 518 - {{ t('package.downloads.end_date') }} 516 + {{ $t('package.downloads.end_date') }} 519 517 </label> 520 518 <div 521 519 class="flex items-center gap-2 px-2.5 py-1.75 bg-bg-subtle border border-border rounded-md focus-within:(border-border-hover ring-2 ring-fg/50)" ··· 636 634 v-if="inModal && !chartData.dataset && !pending" 637 635 class="min-h-[260px] flex items-center justify-center text-fg-subtle font-mono text-sm" 638 636 > 639 - {{ t('package.downloads.no_data') }} 637 + {{ $t('package.downloads.no_data') }} 640 638 </div> 641 639 642 640 <div ··· 645 643 aria-live="polite" 646 644 class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-xs text-fg-subtle font-mono bg-bg/70 backdrop-blur px-3 py-2 rounded-md border border-border" 647 645 > 648 - {{ t('package.downloads.loading') }} 646 + {{ $t('package.downloads.loading') }} 649 647 </div> 650 648 </div> 651 649 </template>
+2 -4
app/components/PackageInstallScripts.vue
··· 8 8 } 9 9 }>() 10 10 11 - const { t } = useI18n() 12 - 13 11 const outdatedNpxDeps = useOutdatedDependencies(() => props.installScripts.npxDependencies) 14 12 const hasNpxDeps = computed(() => Object.keys(props.installScripts.npxDependencies).length > 0) 15 13 const sortedNpxDeps = computed(() => { ··· 58 56 aria-hidden="true" 59 57 /> 60 58 {{ 61 - t( 59 + $t( 62 60 'package.install_scripts.npx_packages', 63 61 { count: sortedNpxDeps.length }, 64 62 sortedNpxDeps.length, ··· 101 99 :title=" 102 100 outdatedNpxDeps[dep] 103 101 ? outdatedNpxDeps[dep].resolved === outdatedNpxDeps[dep].latest 104 - ? t('package.install_scripts.currently', { 102 + ? $t('package.install_scripts.currently', { 105 103 version: outdatedNpxDeps[dep].latest, 106 104 }) 107 105 : getOutdatedTooltip(outdatedNpxDeps[dep])
+5 -7
app/components/PackageListControls.vue
··· 19 19 'update:sort': [value: SortOption] 20 20 }>() 21 21 22 - const { t } = useI18n() 23 - 24 22 const filterValue = computed({ 25 23 get: () => props.filter, 26 24 set: value => emit('update:filter', value), ··· 34 32 const sortOptions = computed( 35 33 () => 36 34 [ 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') }, 35 + { value: 'downloads', label: $t('package.sort.downloads') }, 36 + { value: 'updated', label: $t('package.sort.updated') }, 37 + { value: 'name-asc', label: $t('package.sort.name_asc') }, 38 + { value: 'name-desc', label: $t('package.sort.name_desc') }, 41 39 ] as const, 42 40 ) 43 41 ··· 67 65 id="package-filter" 68 66 v-model="filterValue" 69 67 type="search" 70 - :placeholder="placeholder ?? t('package.list.filter_placeholder')" 68 + :placeholder="placeholder ?? $t('package.list.filter_placeholder')" 71 69 autocomplete="off" 72 70 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)" 73 71 />
+6 -8
app/components/PackageMetricsBadges.vue
··· 25 25 } 26 26 }) 27 27 28 - const { t } = useI18n() 29 - 30 28 const moduleFormatTooltip = computed(() => { 31 29 if (!analysis.value) return '' 32 30 switch (analysis.value.moduleFormat) { 33 31 case 'esm': 34 - return t('package.metrics.esm') 32 + return $t('package.metrics.esm') 35 33 case 'cjs': 36 - return t('package.metrics.cjs') 34 + return $t('package.metrics.cjs') 37 35 case 'dual': 38 - return t('package.metrics.dual') 36 + return $t('package.metrics.dual') 39 37 default: 40 - return t('package.metrics.unknown_format') 38 + return $t('package.metrics.unknown_format') 41 39 } 42 40 }) 43 41 ··· 50 48 if (!analysis.value) return '' 51 49 switch (analysis.value.types?.kind) { 52 50 case 'included': 53 - return t('package.metrics.ts_included') 51 + return $t('package.metrics.ts_included') 54 52 case '@types': 55 - return t('package.metrics.types_from', { package: analysis.value.types.packageName }) 53 + return $t('package.metrics.types_from', { package: analysis.value.types.packageName }) 56 54 default: 57 55 return '' 58 56 }
+13 -15
app/components/PackageSkeleton.vue
··· 1 - <script setup lang="ts"> 2 - const { t } = useI18n() 3 - </script> 1 + <script setup lang="ts"></script> 4 2 5 3 <template> 6 4 <article 7 5 aria-busy="true" 8 - :aria-label="t('package.skeleton.loading')" 6 + :aria-label="$t('package.skeleton.loading')" 9 7 class="motion-safe:animate-fade-in" 10 8 > 11 9 <!-- Package header - matches header in [...name].vue --> ··· 35 33 <!-- License --> 36 34 <div class="space-y-1"> 37 35 <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 38 - {{ t('package.skeleton.license') }} 36 + {{ $t('package.skeleton.license') }} 39 37 </dt> 40 38 <dd class="font-mono text-sm"> 41 39 <span class="skeleton inline-block h-5 w-12" /> ··· 45 43 <!-- Weekly --> 46 44 <div class="space-y-1"> 47 45 <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 48 - {{ t('package.skeleton.weekly') }} 46 + {{ $t('package.skeleton.weekly') }} 49 47 </dt> 50 48 <dd class="font-mono text-sm"> 51 49 <span class="skeleton inline-block h-5 w-20" /> ··· 55 53 <!-- Size --> 56 54 <div class="space-y-1"> 57 55 <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 58 - {{ t('package.skeleton.size') }} 56 + {{ $t('package.skeleton.size') }} 59 57 </dt> 60 58 <dd class="font-mono text-sm"> 61 59 <span class="skeleton inline-block h-5 w-16" /> ··· 65 63 <!-- Deps --> 66 64 <div class="space-y-1"> 67 65 <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 68 - {{ t('package.skeleton.deps') }} 66 + {{ $t('package.skeleton.deps') }} 69 67 </dt> 70 68 <dd class="font-mono text-sm"> 71 69 <span class="skeleton inline-block h-5 w-8" /> ··· 75 73 <!-- Updated --> 76 74 <div class="space-y-1 col-span-2"> 77 75 <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 78 - {{ t('package.skeleton.updated') }} 76 + {{ $t('package.skeleton.updated') }} 79 77 </dt> 80 78 <dd class="font-mono text-sm"> 81 79 <span class="skeleton inline-block h-5 w-28" /> ··· 108 106 id="install-heading-skeleton" 109 107 class="text-xs text-fg-subtle uppercase tracking-wider mb-3" 110 108 > 111 - {{ t('package.skeleton.install') }} 109 + {{ $t('package.skeleton.install') }} 112 110 </h2> 113 111 <!-- code-block with relative positioning for copy button --> 114 112 <div class="relative"> ··· 128 126 id="readme-heading-skeleton" 129 127 class="text-xs text-fg-subtle uppercase tracking-wider mb-4" 130 128 > 131 - {{ t('package.skeleton.readme') }} 129 + {{ $t('package.skeleton.readme') }} 132 130 </h2> 133 131 <!-- Simulated README content --> 134 132 <div class="space-y-4"> ··· 159 157 id="maintainers-heading-skeleton" 160 158 class="text-xs text-fg-subtle uppercase tracking-wider mb-3" 161 159 > 162 - {{ t('package.skeleton.maintainers') }} 160 + {{ $t('package.skeleton.maintainers') }} 163 161 </h2> 164 162 <ul class="space-y-2 list-none m-0 p-0"> 165 163 <li> ··· 177 175 id="keywords-heading-skeleton" 178 176 class="text-xs text-fg-subtle uppercase tracking-wider mb-3" 179 177 > 180 - {{ t('package.skeleton.keywords') }} 178 + {{ $t('package.skeleton.keywords') }} 181 179 </h2> 182 180 <!-- flex flex-wrap gap-1.5 --> 183 181 <ul class="flex flex-wrap gap-1.5 list-none m-0 p-0"> ··· 196 194 id="versions-heading-skeleton" 197 195 class="text-xs text-fg-subtle uppercase tracking-wider mb-3" 198 196 > 199 - {{ t('package.skeleton.versions') }} 197 + {{ $t('package.skeleton.versions') }} 200 198 </h2> 201 199 <!-- space-y-1, each row: flex items-center justify-between py-1.5 text-sm --> 202 200 <div class="space-y-1"> ··· 229 227 id="dependencies-heading-skeleton" 230 228 class="text-xs text-fg-subtle uppercase tracking-wider mb-3" 231 229 > 232 - {{ t('package.skeleton.dependencies') }} 230 + {{ $t('package.skeleton.dependencies') }} 233 231 </h2> 234 232 <!-- space-y-1, each: flex items-center justify-between py-1 text-sm --> 235 233 <ul class="space-y-1 list-none m-0 p-0">
+6 -8
app/components/PackageVersions.vue
··· 10 10 } from '~/utils/versions' 11 11 import { fetchAllPackageVersions } from '~/composables/useNpmRegistry' 12 12 13 - const { t } = useI18n() 14 - 15 13 const props = defineProps<{ 16 14 packageName: string 17 15 versions: Record<string, PackumentVersion> ··· 345 343 " 346 344 :title=" 347 345 row.primaryVersion.deprecated 348 - ? t('package.versions.deprecated_title', { 346 + ? $t('package.versions.deprecated_title', { 349 347 version: row.primaryVersion.version, 350 348 }) 351 349 : row.primaryVersion.version ··· 400 398 " 401 399 :title=" 402 400 v.deprecated 403 - ? t('package.versions.deprecated_title', { version: v.version }) 401 + ? $t('package.versions.deprecated_title', { version: v.version }) 404 402 : v.version 405 403 " 406 404 > ··· 492 490 " 493 491 :title=" 494 492 row.primaryVersion.deprecated 495 - ? t('package.versions.deprecated_title', { 493 + ? $t('package.versions.deprecated_title', { 496 494 version: row.primaryVersion.version, 497 495 }) 498 496 : row.primaryVersion.version ··· 562 560 " 563 561 :title=" 564 562 group.versions[0]?.deprecated 565 - ? t('package.versions.deprecated_title', { 563 + ? $t('package.versions.deprecated_title', { 566 564 version: group.versions[0]?.version, 567 565 }) 568 566 : group.versions[0]?.version ··· 618 616 " 619 617 :title=" 620 618 group.versions[0]?.deprecated 621 - ? t('package.versions.deprecated_title', { 619 + ? $t('package.versions.deprecated_title', { 622 620 version: group.versions[0]?.version, 623 621 }) 624 622 : group.versions[0]?.version ··· 672 670 " 673 671 :title=" 674 672 v.deprecated 675 - ? t('package.versions.deprecated_title', { version: v.version }) 673 + ? $t('package.versions.deprecated_title', { version: v.version }) 676 674 : v.version 677 675 " 678 676 >
+5 -7
app/components/PackageVulnerabilities.vue
··· 92 92 return `https://osv.dev/vulnerability/${vuln.id}` 93 93 } 94 94 95 - const { t } = useI18n() 96 - 97 95 function toVulnerabilitySummary(vuln: OsvVulnerability): VulnerabilitySummary { 98 96 return { 99 97 id: vuln.id, 100 - summary: vuln.summary || t('package.vulnerabilities.no_description'), 98 + summary: vuln.summary || $t('package.vulnerabilities.no_description'), 101 99 severity: getSeverityLevel(vuln), 102 100 aliases: vuln.aliases || [], 103 101 url: getVulnerabilityUrl(vuln), ··· 142 140 const counts = vulnData.value.counts 143 141 const parts: string[] = [] 144 142 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')}`) 143 + parts.push(`${counts.critical} ${$t('package.vulnerabilities.severity.critical')}`) 144 + if (counts.high > 0) parts.push(`${counts.high} ${$t('package.vulnerabilities.severity.high')}`) 147 145 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 + parts.push(`${counts.moderate} ${$t('package.vulnerabilities.severity.moderate')}`) 147 + if (counts.low > 0) parts.push(`${counts.low} ${$t('package.vulnerabilities.severity.low')}`) 150 148 return parts.join(', ') 151 149 }) 152 150 </script>
+6 -4
app/components/PackageWeeklyDownloadStats.vue
··· 6 6 packageName: string 7 7 }>() 8 8 9 - const { t } = useI18n() 10 9 const showModal = ref(false) 11 10 12 11 const { data: packument } = usePackage(() => packageName) ··· 43 42 const dataset = computed(() => 44 43 weeklyDownloads.value.map(d => ({ 45 44 value: d?.downloads ?? 0, 46 - period: t('package.downloads.date_range', { start: d.weekStart ?? '-', end: d.weekEnd ?? '-' }), 45 + period: $t('package.downloads.date_range', { 46 + start: d.weekStart ?? '-', 47 + end: d.weekEnd ?? '-', 48 + }), 47 49 })), 48 50 ) 49 51 ··· 85 87 type="button" 86 88 @click="showModal = true" 87 89 class="link-subtle font-mono text-sm inline-flex items-center gap-1.5 ml-auto shrink-0 self-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded" 88 - :title="t('package.downloads.analyze')" 90 + :title="$t('package.downloads.analyze')" 89 91 > 90 92 <span class="i-carbon-data-analytics w-4 h-4" aria-hidden="true" /> 91 - <span class="sr-only">{{ t('package.downloads.analyze') }}</span> 93 + <span class="sr-only">{{ $t('package.downloads.analyze') }}</span> 92 94 </button> 93 95 </div> 94 96
+4 -6
app/components/ProvenanceBadge.vue
··· 12 12 linked?: boolean 13 13 }>() 14 14 15 - const { t } = useI18n() 16 - 17 15 const providerLabels: Record<string, string> = { 18 16 github: 'GitHub Actions', 19 17 gitlab: 'GitLab CI', ··· 21 19 22 20 const title = computed(() => 23 21 props.provider 24 - ? t('badges.provenance.verified_via', { 22 + ? $t('badges.provenance.verified_via', { 25 23 provider: providerLabels[props.provider] ?? props.provider, 26 24 }) 27 - : t('badges.provenance.verified_title'), 25 + : $t('badges.provenance.verified_title'), 28 26 ) 29 27 </script> 30 28 ··· 42 40 :class="compact ? 'w-3.5 h-3.5' : 'w-4 h-4'" 43 41 /> 44 42 <span v-if="!compact" class="sr-only sm:not-sr-only">{{ 45 - t('badges.provenance.verified') 43 + $t('badges.provenance.verified') 46 44 }}</span> 47 45 </a> 48 46 <span ··· 55 53 :class="compact ? 'w-3.5 h-3.5' : 'w-4 h-4'" 56 54 /> 57 55 <span v-if="!compact" class="sr-only sm:not-sr-only">{{ 58 - t('badges.provenance.verified') 56 + $t('badges.provenance.verified') 59 57 }}</span> 60 58 </span> 61 59 </template>
+15 -17
app/pages/@[org].vue
··· 2 2 import { formatNumber } from '#imports' 3 3 import { debounce } from 'perfect-debounce' 4 4 5 - const { t } = useI18n() 6 - 7 5 definePageMeta({ 8 6 name: 'org', 9 7 alias: ['/org/:org()'], ··· 44 42 if (status.value === 'error' && error.value?.statusCode === 404) { 45 43 throw createError({ 46 44 statusCode: 404, 47 - statusMessage: t('org.page.not_found'), 48 - message: t('org.page.not_found_message', { name: orgName.value }), 45 + statusMessage: $t('org.page.not_found'), 46 + message: $t('org.page.not_found_message', { name: orgName.value }), 49 47 }) 50 48 } 51 49 ··· 135 133 <div> 136 134 <h1 class="font-mono text-2xl sm:text-3xl font-medium">@{{ orgName }}</h1> 137 135 <p v-if="status === 'success'" class="text-fg-muted text-sm mt-1"> 138 - {{ t('org.public_packages', { count: formatNumber(packageCount) }, packageCount) }} 136 + {{ $t('org.public_packages', { count: formatNumber(packageCount) }, packageCount) }} 139 137 </p> 140 138 </div> 141 139 </div> ··· 149 147 class="link-subtle font-mono text-sm inline-flex items-center gap-1.5" 150 148 > 151 149 <span class="i-carbon-cube w-4 h-4" /> 152 - {{ t('common.view_on_npm') }} 150 + {{ $t('common.view_on_npm') }} 153 151 </a> 154 152 </nav> 155 153 </header> ··· 169 167 " 170 168 @click="activeTab = 'members'" 171 169 > 172 - {{ t('org.page.members_tab') }} 170 + {{ $t('org.page.members_tab') }} 173 171 </button> 174 172 <button 175 173 type="button" ··· 181 179 " 182 180 @click="activeTab = 'teams'" 183 181 > 184 - {{ t('org.page.teams_tab') }} 182 + {{ $t('org.page.teams_tab') }} 185 183 </button> 186 184 </div> 187 185 ··· 192 190 </ClientOnly> 193 191 194 192 <!-- Loading state --> 195 - <LoadingSpinner v-if="status === 'pending'" :text="t('common.loading_packages')" /> 193 + <LoadingSpinner v-if="status === 'pending'" :text="$t('common.loading_packages')" /> 196 194 197 195 <!-- Error state --> 198 196 <div v-else-if="status === 'error'" role="alert" class="py-12 text-center"> 199 197 <p class="text-fg-muted mb-4"> 200 - {{ error?.message ?? t('org.page.failed_to_load') }} 198 + {{ error?.message ?? $t('org.page.failed_to_load') }} 201 199 </p> 202 - <NuxtLink to="/" class="btn">{{ t('common.go_back_home') }}</NuxtLink> 200 + <NuxtLink to="/" class="btn">{{ $t('common.go_back_home') }}</NuxtLink> 203 201 </div> 204 202 205 203 <!-- Empty state --> 206 204 <div v-else-if="packageCount === 0" class="py-12 text-center"> 207 205 <p class="text-fg-muted font-mono"> 208 - {{ t('org.page.no_packages') }} <span class="text-fg">@{{ orgName }}</span> 206 + {{ $t('org.page.no_packages') }} <span class="text-fg">@{{ orgName }}</span> 209 207 </p> 210 208 <p class="text-fg-subtle text-sm mt-2"> 211 - {{ t('org.page.no_packages_hint') }} 209 + {{ $t('org.page.no_packages_hint') }} 212 210 </p> 213 211 </div> 214 212 215 213 <!-- Package list --> 216 - <section v-else-if="packages.length > 0" :aria-label="t('org.page.packages_title')"> 214 + <section v-else-if="packages.length > 0" :aria-label="$t('org.page.packages_title')"> 217 215 <h2 class="text-xs text-fg-subtle uppercase tracking-wider mb-4"> 218 - {{ t('org.page.packages_title') }} 216 + {{ $t('org.page.packages_title') }} 219 217 </h2> 220 218 221 219 <!-- Filter and sort controls --> 222 220 <PackageListControls 223 221 v-model:filter="filterText" 224 222 v-model:sort="sortOption" 225 - :placeholder="t('org.page.filter_placeholder', { count: packageCount })" 223 + :placeholder="$t('org.page.filter_placeholder', { count: packageCount })" 226 224 :total-count="packageCount" 227 225 :filtered-count="filteredCount" 228 226 /> ··· 232 230 v-if="filteredAndSortedPackages.length === 0" 233 231 class="text-fg-muted py-8 text-center font-mono" 234 232 > 235 - {{ t('org.page.no_match', { query: filterText }) }} 233 + {{ $t('org.page.no_match', { query: filterText }) }} 236 234 </p> 237 235 238 236 <PackageList v-else :results="filteredAndSortedPackages" />
+42 -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 - 11 9 definePageMeta({ 12 10 name: 'package', 13 11 alias: ['/package/:package(.*)*'], ··· 432 430 <NuxtLink 433 431 v-if="resolvedVersion !== requestedVersion" 434 432 :to="`/${pkg.name}/v/${displayVersion.version}`" 435 - :title="t('package.view_permalink')" 433 + :title="$t('package.view_permalink')" 436 434 >{{ displayVersion.version }}</NuxtLink 437 435 > 438 436 <span v-else>v{{ displayVersion.version }}</span> ··· 443 441 target="_blank" 444 442 rel="noopener noreferrer" 445 443 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" 446 - :title="t('package.verified_provenance')" 444 + :title="$t('package.verified_provenance')" 447 445 > 448 446 <span 449 447 class="i-solar-shield-check-outline w-3.5 h-3.5 shrink-0" ··· 457 455 displayVersion.version !== latestVersion.version 458 456 " 459 457 class="text-fg-subtle text-sm shrink-0" 460 - >{{ t('package.not_latest') }}</span 458 + >{{ $t('package.not_latest') }}</span 461 459 > 462 460 </span> 463 461 ··· 482 480 target="_blank" 483 481 rel="noopener noreferrer" 484 482 class="link-subtle font-mono text-sm inline-flex items-center gap-1.5 ml-auto shrink-0 self-center" 485 - :title="t('common.view_on_npm')" 483 + :title="$t('common.view_on_npm')" 486 484 > 487 485 <span class="i-carbon-logo-npm w-4 h-4" aria-hidden="true" /> 488 486 <span class="hidden sm:inline">npm</span> 489 - <span class="sr-only sm:hidden">{{ t('common.view_on_npm') }}</span> 487 + <span class="sr-only sm:hidden">{{ $t('common.view_on_npm') }}</span> 490 488 </a> 491 489 </div> 492 490 ··· 500 498 <MarkdownText :text="pkg.description" /> 501 499 </p> 502 500 <p v-else class="text-fg-subtle text-base m-0 italic"> 503 - {{ t('package.no_description') }} 501 + {{ $t('package.no_description') }} 504 502 </p> 505 503 <!-- Fade overlay with show more button - only when collapsed and overflowing --> 506 504 <div ··· 510 508 <button 511 509 type="button" 512 510 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" 513 - :aria-label="t('package.show_full_description')" 511 + :aria-label="$t('package.show_full_description')" 514 512 @click="descriptionExpanded = true" 515 513 > 516 - {{ t('common.show_more') }} 514 + {{ $t('common.show_more') }} 517 515 </button> 518 516 </div> 519 517 </div> ··· 526 524 <h2 class="font-medium mb-2"> 527 525 {{ 528 526 deprecationNotice.type === 'package' 529 - ? t('package.deprecation.package') 530 - : t('package.deprecation.version') 527 + ? $t('package.deprecation.package') 528 + : $t('package.deprecation.version') 531 529 }} 532 530 </h2> 533 531 <p v-if="deprecationNotice.message" class="text-base m-0"> 534 532 <MarkdownText :text="deprecationNotice.message" /> 535 533 </p> 536 - <p v-else class="text-base m-0 italic">{{ t('package.deprecation.no_reason') }}</p> 534 + <p v-else class="text-base m-0 italic">{{ $t('package.deprecation.no_reason') }}</p> 537 535 </div> 538 536 539 537 <!-- Stats grid --> 540 538 <dl class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-3 sm:gap-4 mt-4 sm:mt-6"> 541 539 <div v-if="pkg.license" class="space-y-1"> 542 540 <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 543 - {{ t('package.stats.license') }} 541 + {{ $t('package.stats.license') }} 544 542 </dt> 545 543 <dd class="font-mono text-sm text-fg"> 546 544 <LicenseDisplay :license="pkg.license" /> ··· 549 547 550 548 <div v-if="downloads" class="space-y-1"> 551 549 <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 552 - {{ t('package.stats.weekly') }} 550 + {{ $t('package.stats.weekly') }} 553 551 </dt> 554 552 <dd class="font-mono text-sm text-fg flex items-center justify-start gap-2"> 555 553 {{ formatNumber(downloads.downloads) }} ··· 558 556 target="_blank" 559 557 rel="noopener noreferrer" 560 558 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" 561 - :title="t('package.stats.view_download_trends')" 559 + :title="$t('package.stats.view_download_trends')" 562 560 > 563 561 <span class="i-carbon-chart-line w-3.5 h-3.5 inline-block" aria-hidden="true" /> 564 - <span class="sr-only">{{ t('package.stats.view_download_trends') }}</span> 562 + <span class="sr-only">{{ $t('package.stats.view_download_trends') }}</span> 565 563 </a> 566 564 </dd> 567 565 </div> 568 566 569 567 <div class="space-y-1"> 570 568 <dt class="text-xs text-fg-subtle uppercase tracking-wider"> 571 - {{ t('package.stats.deps') }} 569 + {{ $t('package.stats.deps') }} 572 570 </dt> 573 571 <dd class="font-mono text-sm text-fg flex items-center justify-start gap-2"> 574 572 {{ getDependencyCount(displayVersion) }} ··· 578 576 target="_blank" 579 577 rel="noopener noreferrer" 580 578 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" 581 - :title="t('package.stats.view_dependency_graph')" 579 + :title="$t('package.stats.view_dependency_graph')" 582 580 > 583 581 <span class="i-carbon-network-3 w-3.5 h-3.5 inline-block" aria-hidden="true" /> 584 - <span class="sr-only">{{ t('package.stats.view_dependency_graph') }}</span> 582 + <span class="sr-only">{{ $t('package.stats.view_dependency_graph') }}</span> 585 583 </a> 586 584 587 585 <a ··· 590 588 target="_blank" 591 589 rel="noopener noreferrer" 592 590 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" 593 - :title="t('package.stats.inspect_dependency_tree')" 591 + :title="$t('package.stats.inspect_dependency_tree')" 594 592 > 595 593 <span 596 594 class="i-solar-eye-scan-outline w-3.5 h-3.5 inline-block" 597 595 aria-hidden="true" 598 596 /> 599 - <span class="sr-only">{{ t('package.stats.inspect_dependency_tree') }}</span> 597 + <span class="sr-only">{{ $t('package.stats.inspect_dependency_tree') }}</span> 600 598 </a> 601 599 </dd> 602 600 </div> 603 601 604 602 <div class="space-y-1 sm:col-span-2"> 605 603 <dt class="text-xs text-fg-subtle uppercase tracking-wider flex items-center gap-1"> 606 - {{ t('package.stats.install_size') }} 604 + {{ $t('package.stats.install_size') }} 607 605 <span 608 606 class="i-carbon-information w-3 h-3 text-fg-subtle" 609 607 aria-hidden="true" ··· 640 638 641 639 <div v-if="pkg.time?.modified" class="space-y-1"> 642 640 <dt class="text-xs text-fg-subtle uppercase tracking-wider sm:text-right"> 643 - {{ t('package.stats.updated') }} 641 + {{ $t('package.stats.updated') }} 644 642 </dt> 645 643 <dd class="font-mono text-sm text-fg sm:text-right"> 646 644 <DateTime :datetime="pkg.time.modified" date-style="medium" /> ··· 662 660 <span v-if="repoRef"> 663 661 {{ repoRef.owner }}<span class="opacity-50">/</span>{{ repoRef.repo }} 664 662 </span> 665 - <span v-else>{{ t('package.links.repo') }}</span> 663 + <span v-else>{{ $t('package.links.repo') }}</span> 666 664 </a> 667 665 </li> 668 666 <li v-if="repositoryUrl && repoMeta && starsLink"> ··· 684 682 class="link-subtle font-mono text-sm inline-flex items-center gap-1.5" 685 683 > 686 684 <span class="i-carbon-link w-4 h-4" aria-hidden="true" /> 687 - {{ t('package.links.homepage') }} 685 + {{ $t('package.links.homepage') }} 688 686 </a> 689 687 </li> 690 688 <li v-if="displayVersion?.bugs?.url"> ··· 695 693 class="link-subtle font-mono text-sm inline-flex items-center gap-1.5" 696 694 > 697 695 <span class="i-carbon-warning w-4 h-4" aria-hidden="true" /> 698 - {{ t('package.links.issues') }} 696 + {{ $t('package.links.issues') }} 699 697 </a> 700 698 </li> 701 699 ··· 709 707 <span class="i-carbon-fork w-4 h-4" aria-hidden="true" /> 710 708 <span> 711 709 {{ formatCompactNumber(forks, { decimals: 1 }) }} 712 - {{ t('package.links.forks', { count: forks }, forks) }} 710 + {{ $t('package.links.forks', { count: forks }, forks) }} 713 711 </span> 714 712 </a> 715 713 </li> ··· 720 718 target="_blank" 721 719 rel="noopener noreferrer" 722 720 class="link-subtle font-mono text-sm inline-flex items-center gap-1.5" 723 - :title="t('badges.jsr.title')" 721 + :title="$t('badges.jsr.title')" 724 722 > 725 723 <span class="i-simple-icons-jsr w-4 h-4" aria-hidden="true" /> 726 - {{ t('package.links.jsr') }} 724 + {{ $t('package.links.jsr') }} 727 725 </a> 728 726 </li> 729 727 <li v-if="displayVersion" class="sm:ml-auto"> ··· 736 734 aria-keyshortcuts="." 737 735 > 738 736 <span class="i-carbon-code w-4 h-4 sm:invisible" aria-hidden="true" /> 739 - {{ t('package.links.code') }} 737 + {{ $t('package.links.code') }} 740 738 <kbd 741 739 class="hidden sm:inline-flex items-center justify-center w-4 h-4 text-xs bg-bg-muted border border-border rounded" 742 740 aria-hidden="true" ··· 760 758 <section aria-labelledby="install-heading" class="mb-8"> 761 759 <div class="flex flex-wrap items-center justify-between mb-3"> 762 760 <h2 id="install-heading" class="text-xs text-fg-subtle uppercase tracking-wider"> 763 - {{ t('package.install.title') }} 761 + {{ $t('package.install.title') }} 764 762 </h2> 765 763 <!-- Package manager tabs --> 766 764 <div 767 765 class="flex items-center gap-1 p-0.5 bg-bg-subtle border border-border rounded-md" 768 766 role="tablist" 769 - :aria-label="t('package.install.pm_label')" 767 + :aria-label="$t('package.install.pm_label')" 770 768 > 771 769 <ClientOnly> 772 770 <button ··· 838 836 v-if="typesPackageName" 839 837 :to="`/${typesPackageName}`" 840 838 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="t('package.install.view_types', { package: typesPackageName })" 839 + :title="$t('package.install.view_types', { package: typesPackageName })" 842 840 > 843 841 <span class="i-carbon-arrow-right w-3 h-3" aria-hidden="true" /> 844 842 <span class="sr-only">View {{ typesPackageName }}</span> ··· 849 847 <button 850 848 type="button" 851 849 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="t('package.install.copy_command')" 850 + :aria-label="$t('package.install.copy_command')" 853 851 @click="copyInstallCommand" 854 852 > 855 - <span aria-live="polite">{{ copied ? t('common.copied') : t('common.copy') }}</span> 853 + <span aria-live="polite">{{ copied ? $t('common.copied') : $t('common.copy') }}</span> 856 854 </button> 857 855 </div> 858 856 </section> ··· 863 861 <div class="lg:col-span-2 order-2 lg:order-1 min-w-0"> 864 862 <section aria-labelledby="readme-heading"> 865 863 <h2 id="readme-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-4"> 866 - {{ t('package.readme.title') }} 864 + {{ $t('package.readme.title') }} 867 865 </h2> 868 866 <!-- eslint-disable vue/no-v-html -- HTML is sanitized server-side --> 869 867 <div ··· 872 870 v-html="readmeData.html" 873 871 /> 874 872 <p v-else class="text-fg-subtle italic"> 875 - {{ t('package.readme.no_readme') }} 873 + {{ $t('package.readme.no_readme') }} 876 874 <a 877 875 v-if="repositoryUrl" 878 876 :href="repositoryUrl" 879 877 rel="noopener noreferrer" 880 878 class="link" 881 - >{{ t('package.readme.view_on_github') }}</a 879 + >{{ $t('package.readme.view_on_github') }}</a 882 880 > 883 881 </p> 884 882 </section> ··· 897 895 <!-- Keywords --> 898 896 <section v-if="displayVersion?.keywords?.length" aria-labelledby="keywords-heading"> 899 897 <h2 id="keywords-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-3"> 900 - {{ t('package.keywords_title') }} 898 + {{ $t('package.keywords_title') }} 901 899 </h2> 902 900 <ul class="flex flex-wrap gap-1.5 list-none m-0 p-0"> 903 901 <li v-for="keyword in displayVersion.keywords.slice(0, 15)" :key="keyword"> ··· 927 925 id="compatibility-heading" 928 926 class="text-xs text-fg-subtle uppercase tracking-wider mb-3" 929 927 > 930 - {{ t('package.compatibility') }} 928 + {{ $t('package.compatibility') }} 931 929 </h2> 932 930 <dl class="space-y-2"> 933 931 <div v-if="displayVersion.engines.node" class="flex justify-between gap-4 py-1"> ··· 982 980 983 981 <!-- Error state --> 984 982 <div v-else-if="status === 'error'" role="alert" class="py-20 text-center"> 985 - <h1 class="font-mono text-2xl font-medium mb-4">{{ t('package.not_found') }}</h1> 983 + <h1 class="font-mono text-2xl font-medium mb-4">{{ $t('package.not_found') }}</h1> 986 984 <p class="text-fg-muted mb-8"> 987 - {{ error?.message ?? t('package.not_found_message') }} 985 + {{ error?.message ?? $t('package.not_found_message') }} 988 986 </p> 989 - <NuxtLink to="/" class="btn">{{ t('common.go_back_home') }}</NuxtLink> 987 + <NuxtLink to="/" class="btn">{{ $t('common.go_back_home') }}</NuxtLink> 990 988 </div> 991 989 </main> 992 990 </template>
+20 -20
app/pages/code/[...path].vue
··· 5 5 PackageFileContentResponse, 6 6 } from '#shared/types' 7 7 8 - const { t } = useI18n() 9 - 10 8 definePageMeta({ 11 9 name: 'code', 12 10 alias: ['/package/code/:path(.*)*'], ··· 308 306 </NuxtLink> 309 307 <!-- Version selector --> 310 308 <div v-if="version && availableVersions.length > 0" class="relative shrink-0"> 311 - <label for="version-select" class="sr-only">{{ t('code.select_version') }}</label> 309 + <label for="version-select" class="sr-only">{{ $t('code.select_version') }}</label> 312 310 <select 313 311 id="version-select" 314 312 :value="version" ··· 345 343 :to="getCodeUrl()" 346 344 class="text-fg-muted hover:text-fg transition-colors shrink-0" 347 345 > 348 - {{ t('code.root') }} 346 + {{ $t('code.root') }} 349 347 </NuxtLink> 350 - <span v-else class="text-fg shrink-0">{{ t('code.root') }}</span> 348 + <span v-else class="text-fg shrink-0">{{ $t('code.root') }}</span> 351 349 <template v-for="(crumb, i) in breadcrumbs" :key="crumb.path"> 352 350 <span class="text-fg-subtle">/</span> 353 351 <NuxtLink ··· 365 363 366 364 <!-- Error: no version --> 367 365 <div v-if="!version" class="container py-20 text-center"> 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> 366 + <p class="text-fg-muted mb-4">{{ $t('code.version_required') }}</p> 367 + <NuxtLink :to="packageRoute()" class="btn">{{ $t('code.go_to_package') }}</NuxtLink> 370 368 </div> 371 369 372 370 <!-- Loading state --> 373 371 <div v-else-if="treeStatus === 'pending'" class="container py-20 text-center"> 374 372 <div class="i-svg-spinners-ring-resize w-8 h-8 mx-auto text-fg-muted" /> 375 - <p class="mt-4 text-fg-muted">{{ t('code.loading_tree') }}</p> 373 + <p class="mt-4 text-fg-muted">{{ $t('code.loading_tree') }}</p> 376 374 </div> 377 375 378 376 <!-- Error state --> 379 377 <div v-else-if="treeStatus === 'error'" class="container py-20 text-center" role="alert"> 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> 378 + <p class="text-fg-muted mb-4">{{ $t('code.failed_to_load_tree') }}</p> 379 + <NuxtLink :to="packageRoute(version)" class="btn">{{ $t('code.back_to_package') }}</NuxtLink> 382 380 </div> 383 381 384 382 <!-- Main content: file tree + file viewer --> ··· 404 402 class="sticky top-0 bg-bg border-b border-border px-4 py-2 flex items-center justify-between" 405 403 > 406 404 <div class="flex items-center gap-3 text-sm"> 407 - <span class="text-fg-muted">{{ t('code.lines', { count: fileContent.lines }) }}</span> 405 + <span class="text-fg-muted">{{ 406 + $t('code.lines', { count: fileContent.lines }) 407 + }}</span> 408 408 <span v-if="currentNode?.size" class="text-fg-subtle">{{ 409 409 formatBytes(currentNode.size) 410 410 }}</span> ··· 416 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" 417 417 @click="copyPermalink" 418 418 > 419 - {{ t('code.copy_link') }} 419 + {{ $t('code.copy_link') }} 420 420 </button> 421 421 <a 422 422 :href="`https://cdn.jsdelivr.net/npm/${packageName}@${version}/${filePath}`" ··· 424 424 rel="noopener noreferrer" 425 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" 426 426 > 427 - {{ t('code.raw') }} 427 + {{ $t('code.raw') }} 428 428 <span class="i-carbon-launch w-3 h-3" /> 429 429 </a> 430 430 </div> ··· 440 440 <!-- File too large warning --> 441 441 <div v-else-if="isViewingFile && isFileTooLarge" class="py-20 text-center"> 442 442 <div class="i-carbon-document w-12 h-12 mx-auto text-fg-subtle mb-4" /> 443 - <p class="text-fg-muted mb-2">{{ t('code.file_too_large') }}</p> 443 + <p class="text-fg-muted mb-2">{{ $t('code.file_too_large') }}</p> 444 444 <p class="text-fg-subtle text-sm mb-4"> 445 - {{ t('code.file_size_warning', { size: formatBytes(currentNode?.size ?? 0) }) }} 445 + {{ $t('code.file_size_warning', { size: formatBytes(currentNode?.size ?? 0) }) }} 446 446 </p> 447 447 <a 448 448 :href="`https://cdn.jsdelivr.net/npm/${packageName}@${version}/${filePath}`" ··· 450 450 rel="noopener noreferrer" 451 451 class="btn inline-flex items-center gap-2" 452 452 > 453 - {{ t('code.view_raw') }} 453 + {{ $t('code.view_raw') }} 454 454 <span class="i-carbon-launch w-4 h-4" /> 455 455 </a> 456 456 </div> ··· 460 460 v-else-if="filePath && fileStatus === 'pending'" 461 461 class="flex min-h-full" 462 462 aria-busy="true" 463 - :aria-label="t('common.loading')" 463 + :aria-label="$t('common.loading')" 464 464 > 465 465 <!-- Fake line numbers column --> 466 466 <div class="shrink-0 bg-bg-subtle border-r border-border w-14 py-0"> ··· 496 496 <!-- Error loading file --> 497 497 <div v-else-if="filePath && fileStatus === 'error'" class="py-20 text-center" role="alert"> 498 498 <div class="i-carbon-warning-alt w-8 h-8 mx-auto text-fg-subtle mb-4" /> 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> 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> 501 501 <a 502 502 :href="`https://cdn.jsdelivr.net/npm/${packageName}@${version}/${filePath}`" 503 503 target="_blank" 504 504 rel="noopener noreferrer" 505 505 class="btn inline-flex items-center gap-2" 506 506 > 507 - {{ t('code.view_raw') }} 507 + {{ $t('code.view_raw') }} 508 508 <span class="i-carbon-launch w-4 h-4" /> 509 509 </a> 510 510 </div>
+2 -3
app/pages/index.vue
··· 10 10 }) 11 11 } 12 12 13 - const { t } = useI18n() 14 13 useSeoMeta({ 15 - title: () => t('seo.home.title'), 16 - description: () => t('seo.home.description'), 14 + title: () => $t('seo.home.title'), 15 + description: () => $t('seo.home.description'), 17 16 }) 18 17 19 18 defineOgImageComponent('Default')
+14 -15
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() 7 6 const route = useRoute() 8 7 const router = useRouter() 9 8 ··· 346 345 347 346 <search> 348 347 <form role="search" class="relative" @submit.prevent> 349 - <label for="search-input" class="sr-only">{{ t('search.label') }}</label> 348 + <label for="search-input" class="sr-only">{{ $t('search.label') }}</label> 350 349 351 350 <div class="relative group" :class="{ 'is-focused': isSearchFocused }"> 352 351 <!-- Subtle glow effect --> ··· 367 366 v-model="inputValue" 368 367 type="search" 369 368 name="q" 370 - :placeholder="t('search.placeholder')" 369 + :placeholder="$t('search.placeholder')" 371 370 autocapitalize="off" 372 371 autocomplete="off" 373 372 autocorrect="off" ··· 381 380 v-show="inputValue" 382 381 type="button" 383 382 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" 384 - :aria-label="t('search.clear')" 383 + :aria-label="$t('search.clear')" 385 384 @click="inputValue = ''" 386 385 > 387 386 <span class="i-carbon-close-large block w-3.5 h-3.5" aria-hidden="true" /> 388 387 </button> 389 388 <!-- Hidden submit button for accessibility (form must have submit button per WCAG) --> 390 - <button type="submit" class="sr-only">{{ t('search.button') }}</button> 389 + <button type="submit" class="sr-only">{{ $t('search.button') }}</button> 391 390 </div> 392 391 </div> 393 392 </form> ··· 399 398 <div class="container pt-20 pb-6"> 400 399 <section v-if="query" aria-label="Search results" @keydown="handleResultsKeydown"> 401 400 <!-- Initial loading (only after user interaction, not during view transition) --> 402 - <LoadingSpinner v-if="showSearching" :text="t('search.searching')" /> 401 + <LoadingSpinner v-if="showSearching" :text="$t('search.searching')" /> 403 402 404 403 <div v-else-if="visibleResults"> 405 404 <!-- Claim prompt - shown at top when valid name but no exact match --> ··· 409 408 > 410 409 <div class="flex-1 min-w-0"> 411 410 <p class="font-mono text-sm text-fg"> 412 - {{ t('search.not_taken', { name: query }) }} 411 + {{ $t('search.not_taken', { name: query }) }} 413 412 </p> 414 - <p class="text-xs text-fg-muted mt-0.5">{{ t('search.claim_prompt') }}</p> 413 + <p class="text-xs text-fg-muted mt-0.5">{{ $t('search.claim_prompt') }}</p> 415 414 </div> 416 415 <button 417 416 type="button" 418 417 class="shrink-0 px-4 py-2 font-mono text-sm text-bg bg-fg rounded-md motion-safe:transition-colors motion-safe:duration-200 hover:bg-fg/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50" 419 418 @click="claimModalOpen = true" 420 419 > 421 - {{ t('search.claim_button', { name: query }) }} 420 + {{ $t('search.claim_button', { name: query }) }} 422 421 </button> 423 422 </div> 424 423 ··· 427 426 role="status" 428 427 class="text-fg-muted text-sm mb-6 font-mono" 429 428 > 430 - {{ t('search.found_packages', { count: formatNumber(visibleResults.total) }) }} 429 + {{ $t('search.found_packages', { count: formatNumber(visibleResults.total) }) }} 431 430 <span v-if="status === 'pending'" class="text-fg-subtle">{{ 432 - t('search.updating') 431 + $t('search.updating') 433 432 }}</span> 434 433 </p> 435 434 436 435 <!-- No results found --> 437 436 <div v-else-if="status !== 'pending'" role="status" class="py-12 text-center"> 438 437 <p class="text-fg-muted font-mono mb-6"> 439 - {{ t('search.no_results', { query }) }} 438 + {{ $t('search.no_results', { query }) }} 440 439 </p> 441 440 442 441 <!-- Offer to claim the package name if it's valid --> 443 442 <div v-if="showClaimPrompt" class="max-w-md mx-auto"> 444 443 <div class="p-4 bg-bg-subtle border border-border rounded-lg"> 445 - <p class="text-sm text-fg-muted mb-3">{{ t('search.want_to_claim') }}</p> 444 + <p class="text-sm text-fg-muted mb-3">{{ $t('search.want_to_claim') }}</p> 446 445 <button 447 446 type="button" 448 447 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" 449 448 @click="claimModalOpen = true" 450 449 > 451 - {{ t('search.claim_button', { name: query }) }} 450 + {{ $t('search.claim_button', { name: query }) }} 452 451 </button> 453 452 </div> 454 453 </div> ··· 473 472 </section> 474 473 475 474 <section v-else class="py-20 text-center"> 476 - <p class="text-fg-subtle font-mono text-sm">{{ t('search.start_typing') }}</p> 475 + <p class="text-fg-subtle font-mono text-sm">{{ $t('search.start_typing') }}</p> 477 476 </section> 478 477 </div> 479 478
+10 -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() 6 5 const route = useRoute('~username') 7 6 const router = useRouter() 8 7 ··· 188 187 <div> 189 188 <h1 class="font-mono text-2xl sm:text-3xl font-medium">@{{ username }}</h1> 190 189 <p v-if="results?.total" class="text-fg-muted text-sm mt-1"> 191 - {{ t('org.public_packages', { count: formatNumber(results.total) }, results.total) }} 190 + {{ $t('org.public_packages', { count: formatNumber(results.total) }, results.total) }} 192 191 </p> 193 192 </div> 194 193 </div> ··· 202 201 class="link-subtle font-mono text-sm inline-flex items-center gap-1.5" 203 202 > 204 203 <span class="i-carbon-cube w-4 h-4" /> 205 - {{ t('common.view_on_npm') }} 204 + {{ $t('common.view_on_npm') }} 206 205 </a> 207 206 </nav> 208 207 </header> ··· 210 209 <!-- Loading state --> 211 210 <LoadingSpinner 212 211 v-if="status === 'pending' && loadedPages === 1" 213 - :text="t('common.loading_packages')" 212 + :text="$t('common.loading_packages')" 214 213 /> 215 214 216 215 <!-- Error state --> 217 216 <div v-else-if="status === 'error'" role="alert" class="py-12 text-center"> 218 217 <p class="text-fg-muted mb-4"> 219 - {{ error?.message ?? t('user.page.failed_to_load') }} 218 + {{ error?.message ?? $t('user.page.failed_to_load') }} 220 219 </p> 221 - <NuxtLink to="/" class="btn">{{ t('common.go_back_home') }}</NuxtLink> 220 + <NuxtLink to="/" class="btn">{{ $t('common.go_back_home') }}</NuxtLink> 222 221 </div> 223 222 224 223 <!-- Empty state --> 225 224 <div v-else-if="results && results.total === 0" class="py-12 text-center"> 226 225 <p class="text-fg-muted font-mono"> 227 - {{ t('user.page.no_packages') }} <span class="text-fg">@{{ username }}</span> 226 + {{ $t('user.page.no_packages') }} <span class="text-fg">@{{ username }}</span> 228 227 </p> 229 - <p class="text-fg-subtle text-sm mt-2">{{ t('user.page.no_packages_hint') }}</p> 228 + <p class="text-fg-subtle text-sm mt-2">{{ $t('user.page.no_packages_hint') }}</p> 230 229 </div> 231 230 232 231 <!-- Package list --> 233 232 <section v-else-if="results && packages.length > 0" aria-label="User packages"> 234 233 <h2 class="text-xs text-fg-subtle uppercase tracking-wider mb-4"> 235 - {{ t('user.page.packages_title') }} 234 + {{ $t('user.page.packages_title') }} 236 235 </h2> 237 236 238 237 <!-- Filter and sort controls --> 239 238 <PackageListControls 240 239 v-model:filter="filterText" 241 240 v-model:sort="sortOption" 242 - :placeholder="t('user.page.filter_placeholder', { count: packageCount })" 241 + :placeholder="$t('user.page.filter_placeholder', { count: packageCount })" 243 242 :total-count="packageCount" 244 243 :filtered-count="filteredCount" 245 244 /> ··· 249 248 v-if="filteredAndSortedPackages.length === 0" 250 249 class="text-fg-muted py-8 text-center font-mono" 251 250 > 252 - {{ t('user.page.no_match', { query: filterText }) }} 251 + {{ $t('user.page.no_match', { query: filterText }) }} 253 252 </p> 254 253 255 254 <PackageList
+16 -17
app/pages/~[username]/orgs.vue
··· 1 1 <script setup lang="ts"> 2 - const { t } = useI18n() 3 2 const route = useRoute('~username-orgs') 4 3 5 4 const username = computed(() => route.params.username) ··· 73 72 // Load details for each org in parallel 74 73 await Promise.all(orgs.value.map(org => loadOrgDetails(org))) 75 74 } else { 76 - error.value = t('header.orgs_dropdown.error') 75 + error.value = $t('header.orgs_dropdown.error') 77 76 } 78 77 } catch (e) { 79 - error.value = e instanceof Error ? e.message : t('header.orgs_dropdown.error') 78 + error.value = e instanceof Error ? e.message : $t('header.orgs_dropdown.error') 80 79 } finally { 81 80 isLoading.value = false 82 81 } ··· 120 119 </div> 121 120 <div> 122 121 <h1 class="font-mono text-2xl sm:text-3xl font-medium">@{{ username }}</h1> 123 - <p class="text-fg-muted text-sm mt-1">{{ t('user.orgs_page.title') }}</p> 122 + <p class="text-fg-muted text-sm mt-1">{{ $t('user.orgs_page.title') }}</p> 124 123 </div> 125 124 </div> 126 125 ··· 131 130 class="link-subtle font-mono text-sm inline-flex items-center gap-1.5" 132 131 > 133 132 <span class="i-carbon-arrow-left w-4 h-4" aria-hidden="true" /> 134 - {{ t('user.orgs_page.back_to_profile') }} 133 + {{ $t('user.orgs_page.back_to_profile') }} 135 134 </NuxtLink> 136 135 </nav> 137 136 </header> ··· 139 138 <!-- Not connected state --> 140 139 <ClientOnly> 141 140 <div v-if="!isConnected" class="py-12 text-center"> 142 - <p class="text-fg-muted mb-4">{{ t('user.orgs_page.connect_required') }}</p> 141 + <p class="text-fg-muted mb-4">{{ $t('user.orgs_page.connect_required') }}</p> 143 142 <p class="text-fg-subtle text-sm"> 144 - {{ t('user.orgs_page.connect_hint_prefix') }} 143 + {{ $t('user.orgs_page.connect_hint_prefix') }} 145 144 <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 + {{ $t('user.orgs_page.connect_hint_suffix') }} 147 146 </p> 148 147 </div> 149 148 150 149 <!-- Not own profile state --> 151 150 <div v-else-if="!isOwnProfile" class="py-12 text-center"> 152 - <p class="text-fg-muted">{{ t('user.orgs_page.own_orgs_only') }}</p> 151 + <p class="text-fg-muted">{{ $t('user.orgs_page.own_orgs_only') }}</p> 153 152 <NuxtLink :to="`/~${npmUser}/orgs`" class="btn mt-4">{{ 154 - t('user.orgs_page.view_your_orgs') 153 + $t('user.orgs_page.view_your_orgs') 155 154 }}</NuxtLink> 156 155 </div> 157 156 158 157 <!-- Loading state --> 159 - <LoadingSpinner v-else-if="isLoading" :text="t('user.orgs_page.loading')" /> 158 + <LoadingSpinner v-else-if="isLoading" :text="$t('user.orgs_page.loading')" /> 160 159 161 160 <!-- Error state --> 162 161 <div v-else-if="error" role="alert" class="py-12 text-center"> 163 162 <p class="text-fg-muted mb-4">{{ error }}</p> 164 - <button type="button" class="btn" @click="loadOrgs">{{ t('common.try_again') }}</button> 163 + <button type="button" class="btn" @click="loadOrgs">{{ $t('common.try_again') }}</button> 165 164 </div> 166 165 167 166 <!-- Empty state --> 168 167 <div v-else-if="orgs.length === 0" class="py-12 text-center"> 169 - <p class="text-fg-muted">{{ t('user.orgs_page.empty') }}</p> 168 + <p class="text-fg-muted">{{ $t('user.orgs_page.empty') }}</p> 170 169 <p class="text-fg-subtle text-sm mt-2"> 171 - {{ t('user.orgs_page.empty_hint') }} 170 + {{ $t('user.orgs_page.empty_hint') }} 172 171 </p> 173 172 </div> 174 173 175 174 <!-- Orgs list --> 176 - <section v-else :aria-label="t('user.orgs_page.title')"> 175 + <section v-else :aria-label="$t('user.orgs_page.title')"> 177 176 <h2 class="text-xs text-fg-subtle uppercase tracking-wider mb-4"> 178 - {{ t('user.orgs_page.count', { count: orgs.length }, orgs.length) }} 177 + {{ $t('user.orgs_page.count', { count: orgs.length }, orgs.length) }} 179 178 </h2> 180 179 181 180 <ul class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"> ··· 217 216 <span class="i-carbon-cube w-4 h-4" aria-hidden="true" /> 218 217 <span v-if="org.packageCount !== null"> 219 218 {{ 220 - t( 219 + $t( 221 220 'user.orgs_page.packages_count', 222 221 { count: org.packageCount }, 223 222 org.packageCount,