Skip to content
Draft
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: 5 additions & 2 deletions app/components/Package/TableRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ const score = computed(() => props.result.score)

const updatedDate = computed(() => props.result.package.date)

const compactNumberFormatter = useCompactNumberFormatter()

function formatDownloads(count?: number): string {
if (count === undefined) return '-'
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`
if (count >= 1_000_000 || count >= 1_000) return compactNumberFormatter.value.format(count)
// if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`
// if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`
return count.toString()
}

Expand Down
8 changes: 6 additions & 2 deletions app/components/Package/WeeklyDownloadStats.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const { settings } = useSettings()

const chartModal = useModal('chart-modal')
const hasChartModalTransitioned = shallowRef(false)
const numberFormatter = useNumberFormatter()

const modalTitle = computed(() => {
const facet = route.query.facet as string | undefined
Expand Down Expand Up @@ -203,8 +204,8 @@ const dataset = computed<VueUiSparklineDatasetItem[]>(() =>
correctedDownloads.value.map(d => ({
value: d?.value ?? 0,
period: $t('package.trends.date_range', {
start: d.weekStart ?? '-',
end: d.weekEnd ?? '-',
start: d.weekStart ? $d(d.weekStart, 'shortDate') : '-',
end: d.weekEnd ? $d(d.weekEnd, 'shortDate') : '-',
}),
})),
)
Expand Down Expand Up @@ -250,6 +251,9 @@ const config = computed<VueUiSparklineConfig>(() => {
fontSize: 28,
bold: false,
color: colors.value.fg,
formatter: ({ value }) => {
return numberFormatter.value.format(value)
},
},
line: {
color: colors.value.borderHover,
Expand Down
63 changes: 45 additions & 18 deletions app/composables/useNumberFormatter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
export function useNumberFormatter(options?: Intl.NumberFormatOptions) {
const { locale } = useI18n()
const { userLocale } = useUserLocale()

return computed(() => new Intl.NumberFormat(locale.value, options))
return computed(
() =>
new Intl.NumberFormat(
userLocale.value,
options ?? {
maximumFractionDigits: 0,
},
),
)
}

export const useCompactNumberFormatter = () =>
Expand All @@ -12,26 +20,45 @@ export const useCompactNumberFormatter = () =>
})

export const useBytesFormatter = () => {
const { t } = useI18n()
const decimalNumberFormatter = useNumberFormatter({
maximumFractionDigits: 1,
const { userLocale } = useUserLocale()

const units = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte', 'petabyte']

// Create formatters reactively based on the user's preferred locale.
// This ensures that when the locale (or the setting) changes, all formatters are recreated.
const formatters = computed(() => {
const locale = userLocale.value
const map = new Map<string, Intl.NumberFormat>()

units.forEach(unit => {
map.set(
unit,
new Intl.NumberFormat(locale, {
style: 'unit',
unit,
unitDisplay: 'short',
maximumFractionDigits: 2,
}),
)
})

return map
})
const KB = 1000
const MB = 1000 * 1000

return {
format: (bytes: number) => {
if (bytes < KB)
return t('package.size.b', {
size: decimalNumberFormatter.value.format(bytes),
})
if (bytes < MB)
return t('package.size.kb', {
size: decimalNumberFormatter.value.format(bytes / KB),
})
return t('package.size.mb', {
size: decimalNumberFormatter.value.format(bytes / MB),
})
let value = bytes
let unitIndex = 0

// Use 1_000 as base (SI units) instead of 1_024.
while (value >= 1_000 && unitIndex < units.length - 1) {
value /= 1_000
unitIndex++
}

const unit = units[unitIndex]!
// Accessing formatters.value here establishes the reactive dependency
return formatters.value.get(unit)!.format(value)
},
}
}
3 changes: 3 additions & 0 deletions app/composables/useSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export interface AppSettings {
hidePlatformPackages: boolean
/** User-selected locale */
selectedLocale: LocaleObject['code'] | null
/** Use the browser's locale for number and date formatting instead of the app's locale */
useSystemLocaleForFormatting: boolean
/** Search provider for package search */
searchProvider: SearchProvider
/** Enable/disable keyboard shortcuts */
Expand All @@ -53,6 +55,7 @@ const DEFAULT_SETTINGS: AppSettings = {
accentColorId: null,
hidePlatformPackages: true,
selectedLocale: null,
useSystemLocaleForFormatting: false,
preferredBackgroundTheme: null,
searchProvider: import.meta.test ? 'npm' : 'algolia',
keyboardShortcuts: true,
Expand Down
26 changes: 26 additions & 0 deletions app/composables/useUserLocale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { usePreferredLanguages } from '@vueuse/core'

/**
* Composable to determine the best locale for formatting numbers and dates.
* It respects the user's preference to use the system/browser locale or the app's selected locale.
*/
export const useUserLocale = () => {
const { locale } = useI18n()
const { settings } = useSettings()
const languages = usePreferredLanguages()

const userLocale = computed(() => {
// If the user wants to use the system locale and we are on the client side with available languages
if (settings.value.useSystemLocaleForFormatting && languages.value.length > 0) {
return languages.value[0]
}

// Fallback to the app's selected locale (also used during SSR to avoid hydration mismatch if possible,
// though formatting might change on client hydration if system locale differs)
return locale.value
})

return {
userLocale,
}
}
12 changes: 12 additions & 0 deletions app/pages/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
const { currentLocaleStatus, isSourceLocale } = useI18nStatus()
const keyboardShortcutsEnabled = useKeyboardShortcuts()

const rtl = new Map<string, boolean>(locales.value.map(l => [l.code, l.dir === 'rtl']))

Check failure on line 10 in app/pages/settings.vue

View workflow job for this annotation

GitHub Actions / 💪 Type check

'rtl' is declared but its value is never read.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't remove this, maybe we'll need it


// Escape to go back (but not when focused on form elements or modal is open)
onKeyStroke(
e =>
Expand Down Expand Up @@ -142,6 +144,16 @@
:description="$t('settings.hide_platform_packages_description')"
v-model="settings.hidePlatformPackages"
/>

<!-- Divider -->
<div class="border-t border-border my-4" />

<!-- System locale toggle -->
<SettingsToggle
:label="$t('settings.use_system_locale')"
:description="$t('settings.use_system_locale_description')"
v-model="settings.useSystemLocaleForFormatting"
/>
</div>
</section>

Expand Down
2 changes: 2 additions & 0 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@
"theme_dark": "Dark",
"theme_system": "System",
"language": "Language",
"use_system_locale": "Use system locale for formatting",
"use_system_locale_description": "Use your browser's locale for number and date formatting instead of the app's language",
"help_translate": "Help translate npmx",
"accent_colors": "Accent colors",
"clear_accent": "Clear accent color",
Expand Down
6 changes: 6 additions & 0 deletions i18n/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,12 @@
"language": {
"type": "string"
},
"use_system_locale": {
"type": "string"
},
"use_system_locale_description": {
"type": "string"
},
"help_translate": {
"type": "string"
},
Expand Down
2 changes: 2 additions & 0 deletions lunaria/files/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@
"theme_dark": "Dark",
"theme_system": "System",
"language": "Language",
"use_system_locale": "Use system locale for formatting",
"use_system_locale_description": "Use your browser's locale for number and date formatting instead of the app's language",
"help_translate": "Help translate npmx",
"accent_colors": "Accent colors",
"clear_accent": "Clear accent colour",
Expand Down
2 changes: 2 additions & 0 deletions lunaria/files/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@
"theme_dark": "Dark",
"theme_system": "System",
"language": "Language",
"use_system_locale": "Use system locale for formatting",
"use_system_locale_description": "Use your browser's locale for number and date formatting instead of the app's language",
"help_translate": "Help translate npmx",
"accent_colors": "Accent colors",
"clear_accent": "Clear accent color",
Expand Down
Loading