Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ npmx.dev uses [@nuxtjs/i18n](https://i18n.nuxtjs.org/) for internationalization.

### Approach

- All user-facing strings should use translation keys via `$t()` in templates or `t()` in script
- All user-facing strings should use translation keys via `$t()` in templates and script
- Translation files live in `i18n/locales/` (e.g., `en.json`)
- We use the `no_prefix` strategy (no `/en/` or `/fr/` in URLs)
- Locale preference is stored in cookies and respected on subsequent visits
Expand All @@ -233,8 +233,9 @@ npmx.dev uses [@nuxtjs/i18n](https://i18n.nuxtjs.org/) for internationalization.
Or in script:

```typescript
const { t } = useI18n()
const message = t('my.translation.key')
<script setup lang="ts">
const message = computed(() => $t('my.translation.key'))
</script>
```

3. For dynamic values, use interpolation:
Expand Down
8 changes: 3 additions & 5 deletions app/components/ClaimPackageModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ const props = defineProps<{

const open = defineModel<boolean>('open', { default: false })

const { t } = useI18n()

const {
isConnected,
state,
Expand All @@ -34,7 +32,7 @@ async function checkAvailability() {
try {
checkResult.value = await checkPackageName(props.packageName)
} catch (err) {
publishError.value = err instanceof Error ? err.message : t('claim.modal.failed_to_check')
publishError.value = err instanceof Error ? err.message : $t('claim.modal.failed_to_check')
} finally {
isChecking.value = false
}
Expand Down Expand Up @@ -84,7 +82,7 @@ async function handleClaim() {
connectorModalOpen.value = true
}
} catch (err) {
publishError.value = err instanceof Error ? err.message : t('claim.modal.failed_to_claim')
publishError.value = err instanceof Error ? err.message : $t('claim.modal.failed_to_claim')
} finally {
isPublishing.value = false
}
Expand Down Expand Up @@ -171,7 +169,7 @@ const connectorModalOpen = shallowRef(false)

<!-- Loading state -->
<div v-if="isChecking" class="py-8 text-center">
<LoadingSpinner :text="t('claim.modal.checking')" />
<LoadingSpinner :text="$t('claim.modal.checking')" />
</div>

<!-- Success state -->
Expand Down
16 changes: 7 additions & 9 deletions app/components/ConnectorStatus.client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,10 @@ const {
const showModal = shallowRef(false)
const showTooltip = shallowRef(false)

const { t } = useI18n()

const tooltipText = computed(() => {
if (isConnecting.value) return t('connector.status.connecting')
if (isConnected.value) return t('connector.status.connected')
return t('connector.status.connect_cli')
if (isConnecting.value) return $t('connector.status.connecting')
if (isConnected.value) return $t('connector.status.connected')
return $t('connector.status.connect_cli')
})

const statusColor = computed(() => {
Expand All @@ -31,9 +29,9 @@ const operationCount = computed(() => activeOperations.value.length)

const ariaLabel = computed(() => {
if (error.value) return error.value
if (isConnecting.value) return t('connector.status.aria_connecting')
if (isConnected.value) return t('connector.status.aria_connected')
return t('connector.status.aria_click_to_connect')
if (isConnecting.value) return $t('connector.status.aria_connecting')
if (isConnected.value) return $t('connector.status.aria_connected')
return $t('connector.status.aria_click_to_connect')
})
</script>

Expand Down Expand Up @@ -62,7 +60,7 @@ const ariaLabel = computed(() => {
<img
v-if="isConnected && avatar"
:src="avatar"
:alt="t('connector.status.avatar_alt', { user: npmUser })"
:alt="$t('connector.status.avatar_alt', { user: npmUser })"
width="24"
height="24"
class="w-6 h-6 rounded-full"
Expand Down
17 changes: 8 additions & 9 deletions app/components/HeaderOrgsDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ const props = defineProps<{
username: string
}>()

const { t } = useI18n()
const { listUserOrgs } = useConnector()

const isOpen = ref(false)
Expand All @@ -23,11 +22,11 @@ async function loadOrgs() {
// Already sorted alphabetically by server, take top 10
orgs.value = orgList.slice(0, 10)
} else {
error.value = t('header.orgs_dropdown.error')
error.value = $t('header.orgs_dropdown.error')
}
hasLoaded.value = true
} catch {
error.value = t('header.orgs_dropdown.error')
error.value = $t('header.orgs_dropdown.error')
} finally {
isLoading.value = false
}
Expand Down Expand Up @@ -62,7 +61,7 @@ function handleKeydown(event: KeyboardEvent) {
:to="`/~${username}/orgs`"
class="link-subtle font-mono text-sm inline-flex items-center gap-1"
>
{{ t('header.orgs') }}
{{ $t('header.orgs') }}
<span
class="i-carbon-chevron-down w-3 h-3 transition-transform duration-200"
:class="{ 'rotate-180': isOpen }"
Expand All @@ -80,16 +79,16 @@ function handleKeydown(event: KeyboardEvent) {
<div class="bg-bg-elevated border border-border rounded-lg shadow-lg overflow-hidden">
<div class="px-3 py-2 border-b border-border">
<span class="font-mono text-xs text-fg-subtle">{{
t('header.orgs_dropdown.title')
$t('header.orgs_dropdown.title')
}}</span>
</div>

<div v-if="isLoading" class="px-3 py-4 text-center">
<span class="text-fg-muted text-sm">{{ t('header.orgs_dropdown.loading') }}</span>
<span class="text-fg-muted text-sm">{{ $t('header.orgs_dropdown.loading') }}</span>
</div>

<div v-else-if="error" class="px-3 py-4 text-center">
<span class="text-fg-muted text-sm">{{ t('header.orgs_dropdown.error') }}</span>
<span class="text-fg-muted text-sm">{{ $t('header.orgs_dropdown.error') }}</span>
</div>

<ul v-else-if="orgs.length > 0" class="py-1 max-h-80 overflow-y-auto">
Expand All @@ -104,15 +103,15 @@ function handleKeydown(event: KeyboardEvent) {
</ul>

<div v-else class="px-3 py-4 text-center">
<span class="text-fg-muted text-sm">{{ t('header.orgs_dropdown.empty') }}</span>
<span class="text-fg-muted text-sm">{{ $t('header.orgs_dropdown.empty') }}</span>
</div>

<div class="px-3 py-2 border-t border-border">
<NuxtLink
:to="`/~${username}/orgs`"
class="link-subtle font-mono text-xs inline-flex items-center gap-1"
>
{{ t('header.orgs_dropdown.view_all') }}
{{ $t('header.orgs_dropdown.view_all') }}
<span class="i-carbon-arrow-right w-3 h-3" aria-hidden="true" />
</NuxtLink>
</div>
Expand Down
17 changes: 8 additions & 9 deletions app/components/HeaderPackagesDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ const props = defineProps<{
username: string
}>()

const { t } = useI18n()
const { listUserPackages } = useConnector()

const isOpen = ref(false)
Expand All @@ -23,11 +22,11 @@ async function loadPackages() {
// Sort alphabetically and take top 10
packages.value = Object.keys(pkgMap).sort().slice(0, 10)
} else {
error.value = t('header.packages_dropdown.error')
error.value = $t('header.packages_dropdown.error')
}
hasLoaded.value = true
} catch {
error.value = t('header.packages_dropdown.error')
error.value = $t('header.packages_dropdown.error')
} finally {
isLoading.value = false
}
Expand Down Expand Up @@ -62,7 +61,7 @@ function handleKeydown(event: KeyboardEvent) {
:to="`/~${username}`"
class="link-subtle font-mono text-sm inline-flex items-center gap-1"
>
{{ t('header.packages') }}
{{ $t('header.packages') }}
<span
class="i-carbon-chevron-down w-3 h-3 transition-transform duration-200"
:class="{ 'rotate-180': isOpen }"
Expand All @@ -80,16 +79,16 @@ function handleKeydown(event: KeyboardEvent) {
<div class="bg-bg-elevated border border-border rounded-lg shadow-lg overflow-hidden">
<div class="px-3 py-2 border-b border-border">
<span class="font-mono text-xs text-fg-subtle">{{
t('header.packages_dropdown.title')
$t('header.packages_dropdown.title')
}}</span>
</div>

<div v-if="isLoading" class="px-3 py-4 text-center">
<span class="text-fg-muted text-sm">{{ t('header.packages_dropdown.loading') }}</span>
<span class="text-fg-muted text-sm">{{ $t('header.packages_dropdown.loading') }}</span>
</div>

<div v-else-if="error" class="px-3 py-4 text-center">
<span class="text-fg-muted text-sm">{{ t('header.packages_dropdown.error') }}</span>
<span class="text-fg-muted text-sm">{{ $t('header.packages_dropdown.error') }}</span>
</div>

<ul v-else-if="packages.length > 0" class="py-1 max-h-80 overflow-y-auto">
Expand All @@ -104,15 +103,15 @@ function handleKeydown(event: KeyboardEvent) {
</ul>

<div v-else class="px-3 py-4 text-center">
<span class="text-fg-muted text-sm">{{ t('header.packages_dropdown.empty') }}</span>
<span class="text-fg-muted text-sm">{{ $t('header.packages_dropdown.empty') }}</span>
</div>

<div class="px-3 py-2 border-t border-border">
<NuxtLink
:to="`/~${username}`"
class="link-subtle font-mono text-xs inline-flex items-center gap-1"
>
{{ t('header.packages_dropdown.view_all') }}
{{ $t('header.packages_dropdown.view_all') }}
<span class="i-carbon-arrow-right w-3 h-3" aria-hidden="true" />
</NuxtLink>
</div>
Expand Down
4 changes: 1 addition & 3 deletions app/components/LoadingSpinner.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,13 @@ defineProps<{
/** Text to display next to the spinner */
text?: string
}>()

const { t } = useI18n()
</script>

<template>
<div aria-busy="true" class="flex items-center gap-3 text-fg-muted font-mono text-sm py-8">
<span
class="w-4 h-4 border-2 border-fg-subtle border-t-fg rounded-full motion-safe:animate-spin"
/>
{{ text ?? t('common.loading') }}
{{ text ?? $t('common.loading') }}
</div>
</template>
16 changes: 7 additions & 9 deletions app/components/OrgMembersPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ const emit = defineEmits<{
'select-team': [teamName: string]
}>()

const { t } = useI18n()

const {
isConnected,
lastExecutionTime,
Expand Down Expand Up @@ -347,7 +345,7 @@ watch(lastExecutionTime, () => {
:aria-pressed="filterRole === role"
@click="filterRole = role"
>
{{ t(`org.members.role.${role}`) }}
{{ $t(`org.members.role.${role}`) }}
<span v-if="role !== 'all'" class="text-fg-subtle">({{ roleCounts[role] }})</span>
</button>
</div>
Expand Down Expand Up @@ -465,9 +463,9 @@ watch(lastExecutionTime, () => {
)
"
>
<option value="developer">{{ t('org.members.role.developer') }}</option>
<option value="admin">{{ t('org.members.role.admin') }}</option>
<option value="owner">{{ t('org.members.role.owner') }}</option>
<option value="developer">{{ $t('org.members.role.developer') }}</option>
<option value="admin">{{ $t('org.members.role.admin') }}</option>
<option value="owner">{{ $t('org.members.role.owner') }}</option>
</select>
<!-- Remove button -->
<button
Expand Down Expand Up @@ -526,9 +524,9 @@ watch(lastExecutionTime, () => {
name="new-member-role"
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"
>
<option value="developer">{{ t('org.members.role.developer') }}</option>
<option value="admin">{{ t('org.members.role.admin') }}</option>
<option value="owner">{{ t('org.members.role.owner') }}</option>
<option value="developer">{{ $t('org.members.role.developer') }}</option>
<option value="admin">{{ $t('org.members.role.admin') }}</option>
<option value="owner">{{ $t('org.members.role.owner') }}</option>
</select>
<!-- Team selection -->
<label for="new-member-team" class="sr-only">{{ $t('org.members.team_label') }}</label>
Expand Down
22 changes: 10 additions & 12 deletions app/components/PackageDownloadAnalytics.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import type { VueUiXyDatasetItem } from 'vue-data-ui'
import { VueUiXy } from 'vue-data-ui/vue-ui-xy'
import { useDebounceFn } from '@vueuse/core'

const { t } = useI18n()

const {
weeklyDownloads,
inModal = false,
Expand Down Expand Up @@ -398,7 +396,7 @@ const config = computed(() => ({
grid: {
labels: {
axis: {
yLabel: t('package.downloads.y_axis_label', { granularity: selectedGranularity.value }),
yLabel: $t('package.downloads.y_axis_label', { granularity: selectedGranularity.value }),
xLabel: packageName,
yLabelOffsetX: 12,
fontSize: 24,
Expand Down Expand Up @@ -469,7 +467,7 @@ const config = computed(() => ({
for="granularity"
class="text-[10px] font-mono text-fg-subtle tracking-wide uppercase"
>
{{ t('package.downloads.granularity') }}
{{ $t('package.downloads.granularity') }}
</label>

<div
Expand All @@ -480,10 +478,10 @@ const config = computed(() => ({
v-model="selectedGranularity"
class="w-full bg-transparent font-mono text-sm text-fg outline-none"
>
<option value="daily">{{ t('package.downloads.granularity_daily') }}</option>
<option value="weekly">{{ t('package.downloads.granularity_weekly') }}</option>
<option value="monthly">{{ t('package.downloads.granularity_monthly') }}</option>
<option value="yearly">{{ t('package.downloads.granularity_yearly') }}</option>
<option value="daily">{{ $t('package.downloads.granularity_daily') }}</option>
<option value="weekly">{{ $t('package.downloads.granularity_weekly') }}</option>
<option value="monthly">{{ $t('package.downloads.granularity_monthly') }}</option>
<option value="yearly">{{ $t('package.downloads.granularity_yearly') }}</option>
</select>
</div>
</div>
Expand All @@ -495,7 +493,7 @@ const config = computed(() => ({
for="startDate"
class="text-[10px] font-mono text-fg-subtle tracking-wide uppercase"
>
{{ t('package.downloads.start_date') }}
{{ $t('package.downloads.start_date') }}
</label>
<div
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)"
Expand All @@ -515,7 +513,7 @@ const config = computed(() => ({
for="endDate"
class="text-[10px] font-mono text-fg-subtle tracking-wide uppercase"
>
{{ t('package.downloads.end_date') }}
{{ $t('package.downloads.end_date') }}
</label>
<div
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)"
Expand Down Expand Up @@ -636,7 +634,7 @@ const config = computed(() => ({
v-if="inModal && !chartData.dataset && !pending"
class="min-h-[260px] flex items-center justify-center text-fg-subtle font-mono text-sm"
>
{{ t('package.downloads.no_data') }}
{{ $t('package.downloads.no_data') }}
</div>

<div
Expand All @@ -645,7 +643,7 @@ const config = computed(() => ({
aria-live="polite"
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"
>
{{ t('package.downloads.loading') }}
{{ $t('package.downloads.loading') }}
</div>
</div>
</template>
Expand Down
6 changes: 2 additions & 4 deletions app/components/PackageInstallScripts.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ const props = defineProps<{
}
}>()

const { t } = useI18n()

const outdatedNpxDeps = useOutdatedDependencies(() => props.installScripts.npxDependencies)
const hasNpxDeps = computed(() => Object.keys(props.installScripts.npxDependencies).length > 0)
const sortedNpxDeps = computed(() => {
Expand Down Expand Up @@ -58,7 +56,7 @@ const isExpanded = shallowRef(false)
aria-hidden="true"
/>
{{
t(
$t(
'package.install_scripts.npx_packages',
{ count: sortedNpxDeps.length },
sortedNpxDeps.length,
Expand Down Expand Up @@ -101,7 +99,7 @@ const isExpanded = shallowRef(false)
:title="
outdatedNpxDeps[dep]
? outdatedNpxDeps[dep].resolved === outdatedNpxDeps[dep].latest
? t('package.install_scripts.currently', {
? $t('package.install_scripts.currently', {
version: outdatedNpxDeps[dep].latest,
})
: getOutdatedTooltip(outdatedNpxDeps[dep])
Expand Down
Loading