Skip to content

Commit e503f3c

Browse files
authored
fix(dashboard): Sort languages by their label instead of language label (#3981)
1 parent 9e13d57 commit e503f3c

File tree

6 files changed

+101
-33
lines changed

6 files changed

+101
-33
lines changed

packages/dashboard/src/lib/components/layout/channel-switcher.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { DEFAULT_CHANNEL_CODE } from '@/vdb/constants.js';
1717
import { useChannel } from '@/vdb/hooks/use-channel.js';
1818
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
1919
import { useServerConfig } from '@/vdb/hooks/use-server-config.js';
20+
import { useSortedLanguages } from '@/vdb/hooks/use-sorted-languages.js';
2021
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
2122
import { cn } from '@/vdb/lib/utils.js';
2223
import { Trans } from '@lingui/react/macro';
@@ -65,6 +66,9 @@ export function ChannelSwitcher() {
6566
? [displayChannel, ...channels.filter(ch => ch.id !== displayChannel.id)]
6667
: channels;
6768

69+
// Sort language codes by their formatted names and map to code and label
70+
const sortedLanguages = useSortedLanguages(displayChannel?.availableLanguageCodes);
71+
6872
useEffect(() => {
6973
if (activeChannel?.availableLanguageCodes) {
7074
// Ensure the current content language is a valid one for the active
@@ -150,7 +154,7 @@ export function ChannelSwitcher() {
150154
</div>
151155
</DropdownMenuSubTrigger>
152156
<DropdownMenuSubContent>
153-
{channel.availableLanguageCodes?.map(languageCode => (
157+
{sortedLanguages?.map(({ code: languageCode, label }) => (
154158
<DropdownMenuItem
155159
key={`${channel.code}-${languageCode}`}
156160
onClick={() => setContentLanguage(languageCode)}
@@ -161,7 +165,7 @@ export function ChannelSwitcher() {
161165
{languageCode.toUpperCase()}
162166
</span>
163167
</div>
164-
<span>{formatLanguageName(languageCode)}</span>
168+
<span>{label}</span>
165169
{contentLanguage === languageCode && (
166170
<span className="ml-auto text-xs text-muted-foreground">
167171
<Trans context="active language">

packages/dashboard/src/lib/components/layout/content-language-selector.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/vdb/components/ui/select.js';
2-
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
32
import { useServerConfig } from '@/vdb/hooks/use-server-config.js';
3+
import { useSortedLanguages } from '@/vdb/hooks/use-sorted-languages.js';
44
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
55
import { cn } from '@/vdb/lib/utils.js';
66

@@ -12,14 +12,13 @@ interface ContentLanguageSelectorProps {
1212

1313
export function ContentLanguageSelector({ value, onChange, className }: ContentLanguageSelectorProps) {
1414
const serverConfig = useServerConfig();
15-
const { formatLanguageName } = useLocalFormat();
1615
const {
1716
settings: { contentLanguage },
1817
setContentLanguage,
1918
} = useUserSettings();
2019

21-
// Fallback to empty array if serverConfig is null
22-
const languages = serverConfig?.availableLanguages || [];
20+
// Map languages to code and label, then sort by label
21+
const sortedLanguages = useSortedLanguages(serverConfig?.availableLanguages);
2322

2423
// If no value is provided but languages are available, use the first language
2524
const currentValue = contentLanguage;
@@ -36,9 +35,9 @@ export function ContentLanguageSelector({ value, onChange, className }: ContentL
3635
<SelectValue placeholder="Select language" />
3736
</SelectTrigger>
3837
<SelectContent>
39-
{languages.map(language => (
40-
<SelectItem key={language} value={language}>
41-
{formatLanguageName(language)}
38+
{sortedLanguages.map(({ code, label }) => (
39+
<SelectItem key={code} value={code}>
40+
{label}
4241
</SelectItem>
4342
))}
4443
</SelectContent>

packages/dashboard/src/lib/components/layout/language-dialog.tsx

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { CurrencyCode } from '@/vdb/constants.js';
22
import { useDisplayLocale } from '@/vdb/hooks/use-display-locale.js';
33
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
4+
import { useSortedLanguages } from '@/vdb/hooks/use-sorted-languages.js';
45
import { useUiLanguageLoader } from '@/vdb/hooks/use-ui-language-loader.js';
56
import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
67
import { Trans } from '@lingui/react/macro';
7-
import { useState } from 'react';
8+
import { useMemo, useState } from 'react';
89
import { uiConfig } from 'virtual:vendure-ui-config';
910
import { Button } from '../ui/button.js';
1011
import { DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '../ui/dialog.js';
@@ -18,12 +19,24 @@ export function LanguageDialog() {
1819
const { settings, setDisplayLanguage, setDisplayLocale } = useUserSettings();
1920
const { humanReadableLanguageAndLocale } = useDisplayLocale();
2021
const availableCurrencyCodes = Object.values(CurrencyCode);
21-
const { formatCurrency, formatLanguageName, formatRegionName, formatCurrencyName, formatDate } =
22-
useLocalFormat();
22+
const { formatCurrency, formatRegionName, formatCurrencyName, formatDate } = useLocalFormat();
2323
const [selectedCurrency, setSelectedCurrency] = useState<string>('USD');
2424

25-
const orderedAvailableLanguages = availableLanguages.slice().sort((a, b) => a.localeCompare(b));
26-
const orderedAvailableLocales = availableLocales.slice().sort((a, b) => a.localeCompare(b));
25+
// Map and sort languages by their formatted names
26+
const sortedLanguages = useSortedLanguages(availableLanguages);
27+
28+
// Map and sort locales by their formatted region names
29+
const sortedLocales = useMemo(
30+
() =>
31+
availableLocales
32+
.map(code => ({
33+
code,
34+
label: formatRegionName(code),
35+
}))
36+
.sort((a, b) => a.label.localeCompare(b.label)),
37+
[availableLocales, formatRegionName],
38+
);
39+
2740
const handleLanguageChange = async (value: string) => {
2841
setDisplayLanguage(value);
2942
void loadAndActivateLocale(value);
@@ -46,10 +59,10 @@ export function LanguageDialog() {
4659
<SelectValue placeholder="Select a language" />
4760
</SelectTrigger>
4861
<SelectContent>
49-
{orderedAvailableLanguages.map(language => (
50-
<SelectItem key={language} value={language} className="flex gap-1">
51-
<span className="uppercase text-muted-foreground">{language}</span>
52-
<span>{formatLanguageName(language)}</span>
62+
{sortedLanguages.map(({ code, label }) => (
63+
<SelectItem key={code} value={code} className="flex gap-1">
64+
<span className="uppercase text-muted-foreground">{code}</span>
65+
<span>{label}</span>
5366
</SelectItem>
5467
))}
5568
</SelectContent>
@@ -64,10 +77,10 @@ export function LanguageDialog() {
6477
<SelectValue placeholder="Select a locale" />
6578
</SelectTrigger>
6679
<SelectContent>
67-
{orderedAvailableLocales.map(locale => (
68-
<SelectItem key={locale} value={locale} className="flex gap-1">
69-
<span className="uppercase text-muted-foreground">{locale}</span>
70-
<span>{formatRegionName(locale)}</span>
80+
{sortedLocales.map(({ code, label }) => (
81+
<SelectItem key={code} value={code} className="flex gap-1">
82+
<span className="uppercase text-muted-foreground">{code}</span>
83+
<span>{label}</span>
7184
</SelectItem>
7285
))}
7386
</SelectContent>

packages/dashboard/src/lib/components/layout/manage-languages-dialog.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { graphql } from '@/vdb/graphql/graphql.js';
1717
import { useChannel } from '@/vdb/hooks/use-channel.js';
1818
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
1919
import { usePermissions } from '@/vdb/hooks/use-permissions.js';
20+
import { useSortedLanguages } from '@/vdb/hooks/use-sorted-languages.js';
2021
import { Trans } from '@lingui/react/macro';
2122
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
2223
import { AlertCircle, Lock } from 'lucide-react';
@@ -115,6 +116,9 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
115116
const [channelLanguages, setChannelLanguages] = useState<string[]>([]);
116117
const [channelDefaultLanguage, setChannelDefaultLanguage] = useState<string>('');
117118

119+
// Map and sort channel languages by their formatted names
120+
const sortedChannelLanguages = useSortedLanguages(channelLanguages || []);
121+
118122
// Queries
119123
const {
120124
data: globalSettingsData,
@@ -363,7 +367,7 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
363367
)}
364368
</div>
365369

366-
{channelLanguages.length > 0 && (
370+
{sortedChannelLanguages.length > 0 && (
367371
<div>
368372
<Label className="text-sm font-medium mb-2 block">
369373
<Trans>Default Language</Trans>
@@ -377,10 +381,9 @@ export function ManageLanguagesDialog({ open, onClose }: ManageLanguagesDialogPr
377381
<SelectValue placeholder="Select default language" />
378382
</SelectTrigger>
379383
<SelectContent>
380-
{channelLanguages.map(languageCode => (
381-
<SelectItem key={languageCode} value={languageCode}>
382-
{formatLanguageName(languageCode)} (
383-
{languageCode.toUpperCase()})
384+
{sortedChannelLanguages.map(({ code, label }) => (
385+
<SelectItem key={code} value={code}>
386+
{label} ({code.toUpperCase()})
384387
</SelectItem>
385388
))}
386389
</SelectContent>

packages/dashboard/src/lib/components/shared/language-selector.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { api } from '@/vdb/graphql/api.js';
22
import { graphql } from '@/vdb/graphql/graphql.js';
3-
import { useLocalFormat } from '@/vdb/hooks/use-local-format.js';
3+
import { useSortedLanguages } from '@/vdb/hooks/use-sorted-languages.js';
44
import { useLingui } from '@lingui/react/macro';
55
import { useQuery } from '@tanstack/react-query';
6+
import { useMemo } from 'react';
67
import { MultiSelect } from './multi-select.js';
78

89
const availableGlobalLanguages = graphql(`
@@ -26,14 +27,21 @@ export function LanguageSelector<T extends boolean>(props: LanguageSelectorProps
2627
queryFn: () => api.query(availableGlobalLanguages),
2728
staleTime: 1000 * 60 * 5, // 5 minutes
2829
});
29-
const { formatLanguageName } = useLocalFormat();
3030
const { value, onChange, multiple, availableLanguageCodes } = props;
3131
const { t } = useLingui();
3232

33-
const items = (availableLanguageCodes ?? data?.globalSettings.availableLanguages ?? []).map(language => ({
34-
value: language,
35-
label: formatLanguageName(language),
36-
}));
33+
const sortedLanguages = useSortedLanguages(
34+
availableLanguageCodes ?? data?.globalSettings.availableLanguages ?? undefined,
35+
);
36+
37+
const items = useMemo(
38+
() =>
39+
sortedLanguages.map(language => ({
40+
value: language.code,
41+
label: language.label,
42+
})),
43+
[sortedLanguages],
44+
);
3745

3846
return (
3947
<MultiSelect
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { useMemo } from 'react';
2+
3+
import { useLocalFormat } from './use-local-format.js';
4+
5+
export interface SortedLanguage {
6+
code: string;
7+
label: string;
8+
}
9+
10+
/**
11+
* @description
12+
* This hook takes an array of language codes and returns a sorted array of language objects
13+
* with code and localized label, sorted alphabetically by the label.
14+
*
15+
* @example
16+
* ```ts
17+
* const sortedLanguages = useSortedLanguages(['en', 'fr', 'de']);
18+
* // Returns: [{ code: 'de', label: 'German' }, { code: 'en', label: 'English' }, { code: 'fr', label: 'French' }]
19+
* ```
20+
*
21+
* @param availableLanguages - Array of language codes to sort
22+
* @returns Sorted array of language objects with code and label
23+
*
24+
* @docsCategory hooks
25+
* @docsPage useSortedLanguages
26+
* @docsWeight 0
27+
*/
28+
export function useSortedLanguages(availableLanguages?: string[] | null): SortedLanguage[] {
29+
const { formatLanguageName } = useLocalFormat();
30+
31+
return useMemo(
32+
() =>
33+
(availableLanguages ?? [])
34+
.map(code => ({
35+
code,
36+
label: formatLanguageName(code),
37+
}))
38+
.sort((a, b) => a.label.localeCompare(b.label)),
39+
[availableLanguages, formatLanguageName],
40+
);
41+
}

0 commit comments

Comments
 (0)