···208208209209Ideally, extract utilities into separate files so they can be unit tested. 🙏
210210211211+## Localization (i18n)
212212+213213+npmx.dev uses [@nuxtjs/i18n](https://i18n.nuxtjs.org/) for internationalization. We aim to make the UI accessible to users in their preferred language.
214214+215215+### Approach
216216+217217+- All user-facing strings should use translation keys via `$t()` in templates or `t()` in script
218218+- Translation files live in `i18n/locales/` (e.g., `en.json`)
219219+- We use the `no_prefix` strategy (no `/en/` or `/fr/` in URLs)
220220+- Locale preference is stored in cookies and respected on subsequent visits
221221+222222+### Adding translations
223223+224224+1. Add your translation key to `i18n/locales/en.json` first (English is the source of truth)
225225+2. Use the key in your component:
226226+227227+ ```vue
228228+ <template>
229229+ <p>{{ $t('my.translation.key') }}</p>
230230+ </template>
231231+ ```
232232+233233+ Or in script:
234234+235235+ ```typescript
236236+ const { t } = useI18n()
237237+ const message = t('my.translation.key')
238238+ ```
239239+240240+3. For dynamic values, use interpolation:
241241+242242+ ```json
243243+ { "greeting": "Hello, {name}!" }
244244+ ```
245245+246246+ ```vue
247247+ <p>{{ $t('greeting', { name: userName }) }}</p>
248248+ ```
249249+250250+### Translation key conventions
251251+252252+- Use dot notation for hierarchy: `section.subsection.key`
253253+- Keep keys descriptive but concise
254254+- Group related keys together
255255+- Use `common.*` for shared strings (loading, retry, close, etc.)
256256+- Use component-specific prefixes: `package.card.*`, `settings.*`, `nav.*`
257257+258258+### Using i18n-ally (recommended)
259259+260260+We recommend the [i18n-ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) VSCode extension for a better development experience:
261261+262262+- Inline translation previews in your code
263263+- Auto-completion for translation keys
264264+- Missing translation detection
265265+- Easy navigation to translation files
266266+267267+The extension is included in our workspace recommendations, so VSCode should prompt you to install it.
268268+269269+### Adding a new locale
270270+271271+1. Create a new JSON file in `i18n/locales/` (e.g., `fr.json`)
272272+2. Add the locale to `nuxt.config.ts`:
273273+274274+ ```typescript
275275+ i18n: {
276276+ locales: [
277277+ { code: 'en', language: 'en-US', name: 'English', file: 'en.json' },
278278+ { code: 'fr', language: 'fr-FR', name: 'Francais', file: 'fr.json' },
279279+ ],
280280+ }
281281+ ```
282282+283283+3. Translate all keys from `en.json`
284284+285285+### Formatting with locale
286286+287287+When formatting numbers or dates that should respect the user's locale, pass the locale:
288288+289289+```typescript
290290+const { locale } = useI18n()
291291+const formatted = formatNumber(12345, locale.value) // "12,345" in en-US
292292+```
293293+211294## Testing
212295213296### Unit tests
···11-export function formatNumber(num: number): string {
11+export function formatNumber(num: number, _locale?: string): string {
22+ // TODO: Support different locales (needs care to ensure hydration works correctly)
23 return new Intl.NumberFormat('en-US').format(num)
34}
45
+481
i18n/locales/en.json
···11+{
22+ "seo": {
33+ "home": {
44+ "title": "npmx - Package Browser for the npm Registry",
55+ "description": "A better browser for the npm registry. Search, browse, and explore packages with a modern interface."
66+ }
77+ },
88+ "tagline": "a better browser for the npm registry",
99+ "non_affiliation_disclaimer": "not affiliated with npm, Inc.",
1010+ "trademark_disclaimer": "npm is a registered trademark of npm, Inc. This site is not affiliated with npm, Inc.",
1111+ "footer": {
1212+ "source": "source",
1313+ "social": "social",
1414+ "chat": "chat"
1515+ },
1616+ "search": {
1717+ "label": "Search npm packages",
1818+ "placeholder": "search packages...",
1919+ "button": "search",
2020+ "clear": "Clear search",
2121+ "searching": "Searching...",
2222+ "found_packages": "Found {count} packages",
2323+ "updating": "(updating...)",
2424+ "no_results": "No packages found for \"{query}\"",
2525+ "not_taken": "{name} is not taken",
2626+ "claim_prompt": "Claim this package name on npm",
2727+ "claim_button": "Claim \"{name}\"",
2828+ "want_to_claim": "Want to claim this package name?",
2929+ "start_typing": "Start typing to search packages"
3030+ },
3131+ "nav": {
3232+ "popular_packages": "Popular packages",
3333+ "search": "search",
3434+ "settings": "settings"
3535+ },
3636+ "settings": {
3737+ "relative_dates": "Relative dates",
3838+ "include_types": "Include {'@'}types in install",
3939+ "language": "Language",
4040+ "help_translate": "Help translate npmx"
4141+ },
4242+ "common": {
4343+ "loading": "Loading...",
4444+ "loading_more": "Loading more...",
4545+ "loading_packages": "Loading packages...",
4646+ "end_of_results": "End of results",
4747+ "try_again": "Try again",
4848+ "close": "Close",
4949+ "retry": "Retry",
5050+ "copy": "copy",
5151+ "copied": "copied!",
5252+ "show_more": "show more",
5353+ "warnings": "Warnings:",
5454+ "go_back_home": "Go back home",
5555+ "view_on_npm": "view on npm",
5656+ "per_week": "/ week",
5757+ "sort": {
5858+ "name": "name",
5959+ "role": "role",
6060+ "members": "members"
6161+ },
6262+ "scroll_to_top": "Scroll to top"
6363+ },
6464+ "package": {
6565+ "not_found": "Package Not Found",
6666+ "not_found_message": "The package could not be found.",
6767+ "no_description": "No description provided",
6868+ "show_full_description": "Show full description",
6969+ "not_latest": "(not latest)",
7070+ "verified_provenance": "Verified provenance",
7171+ "view_permalink": "View permalink for this version",
7272+ "deprecation": {
7373+ "package": "This package has been deprecated.",
7474+ "version": "This version has been deprecated.",
7575+ "no_reason": "No reason provided"
7676+ },
7777+ "stats": {
7878+ "license": "License",
7979+ "weekly": "Weekly",
8080+ "deps": "Deps",
8181+ "install_size": "Install Size",
8282+ "updated": "Updated",
8383+ "view_download_trends": "View download trends",
8484+ "view_dependency_graph": "View dependency graph",
8585+ "inspect_dependency_tree": "Inspect dependency tree"
8686+ },
8787+ "links": {
8888+ "repo": "repo",
8989+ "homepage": "homepage",
9090+ "issues": "issues",
9191+ "forks": "{count} fork | {count} forks",
9292+ "jsr": "jsr",
9393+ "socket": "socket.dev",
9494+ "code": "code"
9595+ },
9696+ "install": {
9797+ "title": "Install",
9898+ "pm_label": "Package manager",
9999+ "copy_command": "Copy install command",
100100+ "view_types": "View {package}"
101101+ },
102102+ "readme": {
103103+ "title": "Readme",
104104+ "no_readme": "No README available.",
105105+ "view_on_github": "View on GitHub"
106106+ },
107107+ "keywords_title": "Keywords",
108108+ "compatibility": "Compatibility",
109109+ "card": {
110110+ "publisher": "Publisher",
111111+ "updated": "Updated",
112112+ "weekly_downloads": "Weekly downloads",
113113+ "keywords": "Keywords"
114114+ },
115115+ "versions": {
116116+ "title": "Versions",
117117+ "collapse": "Collapse {tag}",
118118+ "expand": "Expand {tag}",
119119+ "other_versions": "Other versions",
120120+ "more_tagged": "{count} more tagged",
121121+ "all_covered": "All versions are covered by tags above",
122122+ "deprecated_title": "{version} (deprecated)"
123123+ },
124124+ "dependencies": {
125125+ "title": "Dependencies ({count})",
126126+ "list_label": "Package dependencies",
127127+ "show_all": "show all {count} deps",
128128+ "optional": "optional"
129129+ },
130130+ "peer_dependencies": {
131131+ "title": "Peer Dependencies ({count})",
132132+ "list_label": "Package peer dependencies",
133133+ "show_all": "show all {count} peer deps"
134134+ },
135135+ "optional_dependencies": {
136136+ "title": "Optional Dependencies ({count})",
137137+ "list_label": "Package optional dependencies",
138138+ "show_all": "show all {count} optional deps"
139139+ },
140140+ "maintainers": {
141141+ "title": "Maintainers",
142142+ "list_label": "Package maintainers",
143143+ "you": "(you)",
144144+ "via": "via {teams}",
145145+ "remove_owner": "Remove {name} as owner",
146146+ "username_to_add": "Username to add as owner",
147147+ "username_placeholder": "username...",
148148+ "add_button": "add",
149149+ "cancel_add": "Cancel adding owner",
150150+ "add_owner": "+ Add owner"
151151+ },
152152+ "downloads": {
153153+ "title": "Weekly Downloads",
154154+ "date_range": "{start} to {end}"
155155+ },
156156+ "install_scripts": {
157157+ "title": "Install Scripts",
158158+ "script_label": "(script)",
159159+ "npx_packages": "{count} npx package | {count} npx packages",
160160+ "currently": "currently {version}"
161161+ },
162162+ "playgrounds": {
163163+ "title": "Try it out",
164164+ "choose": "choose playground"
165165+ },
166166+ "metrics": {
167167+ "esm": "ES Modules only",
168168+ "cjs": "CommonJS only",
169169+ "dual": "Supports both CommonJS and ES Modules",
170170+ "unknown_format": "Unknown module format",
171171+ "ts_included": "TypeScript types included",
172172+ "types_from": "Types from {package}"
173173+ },
174174+ "license": {
175175+ "view_spdx": "View license text on SPDX"
176176+ },
177177+ "vulnerabilities": {
178178+ "no_description": "No description available",
179179+ "found": "{count} vulnerability found | {count} vulnerabilities found",
180180+ "no_summary": "No summary",
181181+ "view_details": "View vulnerability details",
182182+ "severity": {
183183+ "critical": "critical",
184184+ "high": "high",
185185+ "moderate": "moderate",
186186+ "low": "low"
187187+ }
188188+ },
189189+ "access": {
190190+ "title": "Team Access",
191191+ "refresh": "Refresh team access",
192192+ "list_label": "Team access list",
193193+ "owner": "owner",
194194+ "rw": "rw",
195195+ "ro": "ro",
196196+ "revoke_access": "Revoke {name} access",
197197+ "no_access": "No team access configured",
198198+ "select_team_label": "Select team",
199199+ "loading_teams": "Loading teams...",
200200+ "select_team": "Select team",
201201+ "permission_label": "Permission level",
202202+ "permission": {
203203+ "read_only": "read-only",
204204+ "read_write": "read-write"
205205+ },
206206+ "grant_button": "grant",
207207+ "cancel_grant": "Cancel granting access",
208208+ "grant_access": "+ Grant team access"
209209+ },
210210+ "list": {
211211+ "filter_label": "Filter packages",
212212+ "filter_placeholder": "Filter packages...",
213213+ "sort_label": "Sort packages",
214214+ "showing_count": "Showing {filtered} of {total} packages"
215215+ },
216216+ "skeleton": {
217217+ "loading": "Loading package details",
218218+ "license": "License",
219219+ "weekly": "Weekly",
220220+ "size": "Size",
221221+ "deps": "Deps",
222222+ "updated": "Updated",
223223+ "install": "Install",
224224+ "readme": "Readme",
225225+ "maintainers": "Maintainers",
226226+ "keywords": "Keywords",
227227+ "versions": "Versions",
228228+ "dependencies": "Dependencies"
229229+ },
230230+ "sort": {
231231+ "downloads": "Most downloaded",
232232+ "updated": "Recently updated",
233233+ "name_asc": "Name (A-Z)",
234234+ "name_desc": "Name (Z-A)"
235235+ }
236236+ },
237237+ "connector": {
238238+ "status": {
239239+ "connecting": "connecting...",
240240+ "connected_as": "connected as {'@'}{user}",
241241+ "connected": "connected",
242242+ "connect_cli": "connect local CLI",
243243+ "aria_connecting": "Connecting to local connector",
244244+ "aria_connected": "Connected to local connector",
245245+ "aria_click_to_connect": "Click to connect to local connector",
246246+ "avatar_alt": "{user}'s avatar"
247247+ },
248248+ "modal": {
249249+ "title": "Local Connector",
250250+ "close_modal": "Close modal",
251251+ "close": "Close",
252252+ "connected": "Connected",
253253+ "logged_in_as": "Logged in as {'@'}{user}",
254254+ "connected_hint": "You can now manage packages and organizations from the web UI.",
255255+ "disconnect": "Disconnect",
256256+ "run_hint": "Run the connector on your machine to enable admin features.",
257257+ "copy_command": "Copy command",
258258+ "copied": "Copied",
259259+ "paste_token": "Then paste the token below to connect:",
260260+ "token_label": "Token",
261261+ "token_placeholder": "paste token here...",
262262+ "advanced": "Advanced options",
263263+ "port_label": "Port",
264264+ "warning": "WARNING",
265265+ "warning_text": "This allows npmx to access your npm CLI. Only connect to sites you trust.",
266266+ "connect": "Connect",
267267+ "connecting": "Connecting..."
268268+ }
269269+ },
270270+ "operations": {
271271+ "queue": {
272272+ "title": "Operations Queue",
273273+ "clear_all": "clear all",
274274+ "refresh": "Refresh operations",
275275+ "empty": "No operations queued",
276276+ "empty_hint": "Add operations from package or org pages",
277277+ "active_label": "Active operations",
278278+ "otp_required": "OTP required",
279279+ "otp_prompt": "Enter OTP to continue",
280280+ "otp_placeholder": "Enter OTP code...",
281281+ "otp_label": "One-time password",
282282+ "retry_otp": "Retry with OTP",
283283+ "retrying": "Retrying...",
284284+ "approve_operation": "Approve operation",
285285+ "remove_operation": "Remove operation",
286286+ "approve_all": "Approve All",
287287+ "execute": "Execute",
288288+ "executing": "Executing...",
289289+ "log": "Log",
290290+ "log_label": "Completed operations log",
291291+ "remove_from_log": "Remove from log"
292292+ }
293293+ },
294294+ "org": {
295295+ "teams": {
296296+ "title": "Teams",
297297+ "refresh": "Refresh teams",
298298+ "filter_label": "Filter teams",
299299+ "filter_placeholder": "Filter teams...",
300300+ "sort_by": "Sort by",
301301+ "loading": "Loading teams...",
302302+ "no_teams": "No teams found",
303303+ "list_label": "Organization teams",
304304+ "delete_team": "Delete team {name}",
305305+ "member_count": "{count} member | {count} members",
306306+ "members_of": "Members of {team}",
307307+ "no_members": "No members",
308308+ "remove_user": "Remove {user} from team",
309309+ "username_to_add": "Username to add to {team}",
310310+ "username_placeholder": "username...",
311311+ "add_button": "add",
312312+ "cancel_add_user": "Cancel adding user",
313313+ "add_member": "+ Add member",
314314+ "team_name_label": "Team name",
315315+ "team_name_placeholder": "team-name...",
316316+ "create_button": "create",
317317+ "no_match": "No teams match \"{query}\"",
318318+ "cancel_create": "Cancel creating team",
319319+ "create_team": "+ Create team"
320320+ },
321321+ "members": {
322322+ "title": "Members",
323323+ "refresh": "Refresh members",
324324+ "filter_label": "Filter members",
325325+ "filter_placeholder": "Filter members...",
326326+ "filter_by_role": "Filter by role",
327327+ "filter_by_team": "Filter by team",
328328+ "all_teams": "all teams",
329329+ "sort_by": "Sort by",
330330+ "loading": "Loading members...",
331331+ "no_members": "No members found",
332332+ "list_label": "Organization members",
333333+ "change_role_for": "Change role for {name}",
334334+ "remove_from_org": "Remove {name} from org",
335335+ "view_team": "View {team} team",
336336+ "no_match": "No members match your filters",
337337+ "username_label": "Username",
338338+ "username_placeholder": "username...",
339339+ "role_label": "Role",
340340+ "role": {
341341+ "all": "all",
342342+ "developer": "developer",
343343+ "admin": "admin",
344344+ "owner": "owner"
345345+ },
346346+ "team_label": "Team",
347347+ "no_team": "no team",
348348+ "add_button": "add",
349349+ "cancel_add": "Cancel adding member",
350350+ "add_member": "+ Add member"
351351+ },
352352+ "public_packages": "{count} public package | {count} public packages",
353353+ "page": {
354354+ "packages_title": "Packages",
355355+ "members_tab": "Members",
356356+ "teams_tab": "Teams",
357357+ "no_packages": "No public packages found for",
358358+ "no_packages_hint": "This organization may not exist or has no public packages.",
359359+ "failed_to_load": "Failed to load organization packages",
360360+ "no_match": "No packages match \"{query}\"",
361361+ "not_found": "Organization not found",
362362+ "not_found_message": "The organization \"{'@'}{name}\" does not exist on npm",
363363+ "filter_placeholder": "Filter {count} packages..."
364364+ }
365365+ },
366366+ "user": {
367367+ "combobox": {
368368+ "add_to_org_hint": "(will also add to org)",
369369+ "press_enter_to_add": "Press Enter to add {'@'}{username}",
370370+ "default_placeholder": "username...",
371371+ "suggestions_label": "User suggestions"
372372+ },
373373+ "page": {
374374+ "packages_title": "Packages",
375375+ "no_packages": "No public packages found for",
376376+ "no_packages_hint": "This user may not exist or has no public packages.",
377377+ "failed_to_load": "Failed to load user packages",
378378+ "no_match": "No packages match \"{query}\"",
379379+ "filter_placeholder": "Filter {count} packages..."
380380+ },
381381+ "orgs_page": {
382382+ "title": "Organizations",
383383+ "back_to_profile": "Back to profile",
384384+ "connect_required": "Connect the local CLI to view your organizations.",
385385+ "connect_hint_prefix": "Run",
386386+ "connect_hint_suffix": "to get started.",
387387+ "own_orgs_only": "You can only view your own organizations.",
388388+ "view_your_orgs": "View your organizations",
389389+ "loading": "Loading organizations...",
390390+ "empty": "No organizations found.",
391391+ "empty_hint": "Organizations are detected from your scoped packages.",
392392+ "count": "{count} Organization | {count} Organizations",
393393+ "packages_count": "{count} package | {count} packages"
394394+ }
395395+ },
396396+ "claim": {
397397+ "modal": {
398398+ "title": "Claim Package Name",
399399+ "close_modal": "Close modal",
400400+ "close": "Close",
401401+ "success": "Package claimed!",
402402+ "success_detail": "{name}{'@'}0.0.0 has been published to npm.",
403403+ "success_hint": "You can now publish new versions to this package using npm publish.",
404404+ "view_package": "View Package",
405405+ "invalid_name": "Invalid package name:",
406406+ "available": "This name is available!",
407407+ "taken": "This name is already taken.",
408408+ "similar_warning": "Similar packages exist - npm may reject this name:",
409409+ "related": "Related packages:",
410410+ "scope_warning_title": "Consider using a scoped package instead",
411411+ "scope_warning_text": "Unscoped package names are a shared resource. Only claim a name if you intend to publish and maintain a package. For personal or organizational projects, use a scoped name like {'@'}{username}/{name}.",
412412+ "connect_required": "Connect to the local connector to claim this package name.",
413413+ "connect_button": "Connect to Connector",
414414+ "publish_hint": "This will publish a minimal placeholder package.",
415415+ "preview_json": "Preview package.json",
416416+ "claim_button": "Claim Package Name",
417417+ "publishing": "Publishing...",
418418+ "retry": "Retry",
419419+ "checking": "Checking availability...",
420420+ "failed_to_check": "Failed to check name availability",
421421+ "failed_to_claim": "Failed to claim package"
422422+ }
423423+ },
424424+ "code": {
425425+ "files_label": "Files",
426426+ "no_files": "No files in this directory",
427427+ "select_version": "Select version",
428428+ "root": "root",
429429+ "lines": "{count} lines",
430430+ "toggle_tree": "Toggle file tree",
431431+ "close_tree": "Close file tree",
432432+ "copy_link": "Copy link",
433433+ "raw": "Raw",
434434+ "view_raw": "View raw file",
435435+ "file_too_large": "File too large to preview",
436436+ "file_size_warning": "{size} exceeds the 500KB limit for syntax highlighting",
437437+ "load_anyway": "Load anyway",
438438+ "failed_to_load": "Failed to load file",
439439+ "unavailable_hint": "The file may be too large or unavailable",
440440+ "version_required": "Version is required to browse code",
441441+ "go_to_package": "Go to package",
442442+ "loading_tree": "Loading file tree...",
443443+ "failed_to_load_tree": "Failed to load files for this package version",
444444+ "back_to_package": "Back to package",
445445+ "table": {
446446+ "name": "Name",
447447+ "size": "Size"
448448+ }
449449+ },
450450+ "badges": {
451451+ "provenance": {
452452+ "verified": "verified",
453453+ "verified_title": "Verified provenance",
454454+ "verified_via": "Verified: published via {provider}"
455455+ },
456456+ "jsr": {
457457+ "title": "also available on JSR",
458458+ "label": "jsr"
459459+ }
460460+ },
461461+ "header": {
462462+ "home": "npmx home",
463463+ "github": "GitHub",
464464+ "packages": "packages",
465465+ "packages_dropdown": {
466466+ "title": "Your Packages",
467467+ "loading": "Loading...",
468468+ "error": "Failed to load packages",
469469+ "empty": "No packages found",
470470+ "view_all": "View all"
471471+ },
472472+ "orgs": "orgs",
473473+ "orgs_dropdown": {
474474+ "title": "Your Organizations",
475475+ "loading": "Loading...",
476476+ "error": "Failed to load organizations",
477477+ "empty": "No organizations found",
478478+ "view_all": "View all"
479479+ }
480480+ }
481481+}