Skip to content

Commit d7f37ac

Browse files
committed
feat(i18n): add browser locale support for formatting
1 parent 365bd9f commit d7f37ac

File tree

10 files changed

+107
-20
lines changed

10 files changed

+107
-20
lines changed

app/components/Package/TableRow.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@ const score = computed(() => props.result.score)
1818
1919
const updatedDate = computed(() => props.result.package.date)
2020
21+
const compactNumberFormatter = useCompactNumberFormatter()
22+
2123
function formatDownloads(count?: number): string {
2224
if (count === undefined) return '-'
23-
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`
24-
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`
25+
if (count >= 1_000_000 || count >= 1_000) return compactNumberFormatter.value.format(count)
26+
// if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`
27+
// if (count >= 1_000) return `${(count / 1_000).toFixed(1)}K`
2528
return count.toString()
2629
}
2730

app/components/Package/WeeklyDownloadStats.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const { settings } = useSettings()
2020
2121
const chartModal = useModal('chart-modal')
2222
const hasChartModalTransitioned = shallowRef(false)
23+
const numberFormatter = useNumberFormatter()
2324
2425
const modalTitle = computed(() => {
2526
const facet = route.query.facet as string | undefined
@@ -244,6 +245,9 @@ const config = computed<VueUiSparklineConfig>(() => {
244245
fontSize: 28,
245246
bold: false,
246247
color: colors.value.fg,
248+
formatter: ({ value }) => {
249+
return numberFormatter.value.format(value)
250+
},
247251
},
248252
line: {
249253
color: colors.value.borderHover,
Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
export function useNumberFormatter(options?: Intl.NumberFormatOptions) {
2-
const { locale } = useI18n()
2+
const { userLocale } = useUserLocale()
33

4-
return computed(() => new Intl.NumberFormat(locale.value, options))
4+
return computed(
5+
() =>
6+
new Intl.NumberFormat(
7+
userLocale.value,
8+
options ?? {
9+
maximumFractionDigits: 0,
10+
},
11+
),
12+
)
513
}
614

715
export const useCompactNumberFormatter = () =>
@@ -12,26 +20,45 @@ export const useCompactNumberFormatter = () =>
1220
})
1321

1422
export const useBytesFormatter = () => {
15-
const { t } = useI18n()
16-
const decimalNumberFormatter = useNumberFormatter({
17-
maximumFractionDigits: 1,
23+
const { userLocale } = useUserLocale()
24+
25+
const units = ['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte', 'petabyte']
26+
27+
// Create formatters reactively based on the user's preferred locale.
28+
// This ensures that when the locale (or the setting) changes, all formatters are recreated.
29+
const formatters = computed(() => {
30+
const locale = userLocale.value
31+
const map = new Map<string, Intl.NumberFormat>()
32+
33+
units.forEach(unit => {
34+
map.set(
35+
unit,
36+
new Intl.NumberFormat(locale, {
37+
style: 'unit',
38+
unit,
39+
unitDisplay: 'short',
40+
maximumFractionDigits: 2,
41+
}),
42+
)
43+
})
44+
45+
return map
1846
})
19-
const KB = 1000
20-
const MB = 1000 * 1000
2147

2248
return {
2349
format: (bytes: number) => {
24-
if (bytes < KB)
25-
return t('package.size.b', {
26-
size: decimalNumberFormatter.value.format(bytes),
27-
})
28-
if (bytes < MB)
29-
return t('package.size.kb', {
30-
size: decimalNumberFormatter.value.format(bytes / KB),
31-
})
32-
return t('package.size.mb', {
33-
size: decimalNumberFormatter.value.format(bytes / MB),
34-
})
50+
let value = bytes
51+
let unitIndex = 0
52+
53+
// Use 1_000 as base (SI units) instead of 1_024.
54+
while (value >= 1_000 && unitIndex < units.length - 1) {
55+
value /= 1_000
56+
unitIndex++
57+
}
58+
59+
const unit = units[unitIndex]!
60+
// Accessing formatters.value here establishes the reactive dependency
61+
return formatters.value.get(unit)!.format(value)
3562
},
3663
}
3764
}

app/composables/useSettings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ export interface AppSettings {
2727
hidePlatformPackages: boolean
2828
/** User-selected locale */
2929
selectedLocale: LocaleObject['code'] | null
30+
/** Use the browser's locale for number and date formatting instead of the app's locale */
31+
useSystemLocaleForFormatting: boolean
3032
/** Search provider for package search */
3133
searchProvider: SearchProvider
3234
/** Enable/disable keyboard shortcuts */
@@ -53,6 +55,7 @@ const DEFAULT_SETTINGS: AppSettings = {
5355
accentColorId: null,
5456
hidePlatformPackages: true,
5557
selectedLocale: null,
58+
useSystemLocaleForFormatting: false,
5659
preferredBackgroundTheme: null,
5760
searchProvider: import.meta.test ? 'npm' : 'algolia',
5861
keyboardShortcuts: true,

app/composables/useUserLocale.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { usePreferredLanguages } from '@vueuse/core'
2+
3+
/**
4+
* Composable to determine the best locale for formatting numbers and dates.
5+
* It respects the user's preference to use the system/browser locale or the app's selected locale.
6+
*/
7+
export const useUserLocale = () => {
8+
const { locale } = useI18n()
9+
const { settings } = useSettings()
10+
const languages = usePreferredLanguages()
11+
12+
const userLocale = computed(() => {
13+
// If the user wants to use the system locale and we are on the client side with available languages
14+
if (settings.value.useSystemLocaleForFormatting && languages.value.length > 0) {
15+
return languages.value[0]
16+
}
17+
18+
// Fallback to the app's selected locale (also used during SSR to avoid hydration mismatch if possible,
19+
// though formatting might change on client hydration if system locale differs)
20+
return locale.value
21+
})
22+
23+
return {
24+
userLocale,
25+
}
26+
}

app/pages/settings.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const colorMode = useColorMode()
77
const { currentLocaleStatus, isSourceLocale } = useI18nStatus()
88
const keyboardShortcutsEnabled = useKeyboardShortcuts()
99
10+
const rtl = new Map<string, boolean>(locales.value.map(l => [l.code, l.dir === 'rtl']))
11+
1012
// Escape to go back (but not when focused on form elements or modal is open)
1113
onKeyStroke(
1214
e =>
@@ -142,6 +144,16 @@ const setLocale: typeof setNuxti18nLocale = locale => {
142144
:description="$t('settings.hide_platform_packages_description')"
143145
v-model="settings.hidePlatformPackages"
144146
/>
147+
148+
<!-- Divider -->
149+
<div class="border-t border-border my-4" />
150+
151+
<!-- System locale toggle -->
152+
<SettingsToggle
153+
:label="$t('settings.use_system_locale')"
154+
:description="$t('settings.use_system_locale_description')"
155+
v-model="settings.useSystemLocaleForFormatting"
156+
/>
145157
</div>
146158
</section>
147159

i18n/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@
106106
"theme_dark": "Dark",
107107
"theme_system": "System",
108108
"language": "Language",
109+
"use_system_locale": "Use system locale for formatting",
110+
"use_system_locale_description": "Use your browser's locale for number and date formatting instead of the app's language",
109111
"help_translate": "Help translate npmx",
110112
"accent_colors": "Accent colors",
111113
"clear_accent": "Clear accent color",

i18n/schema.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,12 @@
322322
"language": {
323323
"type": "string"
324324
},
325+
"use_system_locale": {
326+
"type": "string"
327+
},
328+
"use_system_locale_description": {
329+
"type": "string"
330+
},
325331
"help_translate": {
326332
"type": "string"
327333
},

lunaria/files/en-GB.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@
105105
"theme_dark": "Dark",
106106
"theme_system": "System",
107107
"language": "Language",
108+
"use_system_locale": "Use system locale for formatting",
109+
"use_system_locale_description": "Use your browser's locale for number and date formatting instead of the app's language",
108110
"help_translate": "Help translate npmx",
109111
"accent_colors": "Accent colors",
110112
"clear_accent": "Clear accent colour",

lunaria/files/en-US.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@
105105
"theme_dark": "Dark",
106106
"theme_system": "System",
107107
"language": "Language",
108+
"use_system_locale": "Use system locale for formatting",
109+
"use_system_locale_description": "Use your browser's locale for number and date formatting instead of the app's language",
108110
"help_translate": "Help translate npmx",
109111
"accent_colors": "Accent colors",
110112
"clear_accent": "Clear accent color",

0 commit comments

Comments
 (0)