Skip to content

Commit 9fdc22e

Browse files
kevinortiz43claude
andcommitted
Pass user locale to localeCompare so sorts honor the selected language
localeCompare with no locale used the runtime default, mis-collating sorted lists (community names, chat members, profile tags, language picker) for non-English users. Pass useTranslation's i18n.language wherever available. CommunitySearch sorts via useMemo so it re-sorts on locale change without refetching. Fixes #8817. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7211732 commit 9fdc22e

5 files changed

Lines changed: 45 additions & 35 deletions

File tree

app/web/features/communities/CommunitiesPage/CommunitySearch.tsx

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { COMMUNITIES } from "i18n/namespaces";
1212
import { useRouter } from "next/router";
1313
import Sentry from "platform/sentry";
1414
import { CommunitySummary } from "proto/communities_pb";
15-
import { useEffect, useState } from "react";
15+
import { useEffect, useMemo, useState } from "react";
1616
import { communityCreationFormURL, routeToCommunity } from "routes";
1717
import { listAllCommunities } from "service/communities";
1818

@@ -31,7 +31,7 @@ const StyledAutocomplete = styled(
3131
}));
3232

3333
export default function CommunitySearch() {
34-
const { t } = useTranslation(COMMUNITIES);
34+
const { t, i18n } = useTranslation(COMMUNITIES);
3535
const router = useRouter();
3636
const { data: accountInfo } = useAccountInfo();
3737
const [inputValue, setInputValue] = useState("");
@@ -60,28 +60,7 @@ export default function CommunitySearch() {
6060
: "",
6161
}));
6262

63-
// Sort hierarchically: first by depth (number of parents), then by full parent path, then by name
64-
const sortedCommunities = communitiesWithRegion.sort((a, b) => {
65-
// First, sort by depth in hierarchy (fewer parents = higher in tree)
66-
const depthCompare = a.parentsList.length - b.parentsList.length;
67-
if (depthCompare !== 0) return depthCompare;
68-
69-
// Then sort by the full path through the hierarchy
70-
const aPath = a.parentsList
71-
.map((p) => p.community?.name || "")
72-
.join("/");
73-
const bPath = b.parentsList
74-
.map((p) => p.community?.name || "")
75-
.join("/");
76-
const pathCompare = aPath.localeCompare(bPath);
77-
if (pathCompare !== 0) return pathCompare;
78-
79-
// Finally sort by community name
80-
return a.name.localeCompare(b.name);
81-
});
82-
83-
setAllCommunities(sortedCommunities);
84-
setFilteredOptions(sortedCommunities);
63+
setAllCommunities(communitiesWithRegion);
8564
} catch (error) {
8665
Sentry.captureException(error, {
8766
tags: {
@@ -99,22 +78,42 @@ export default function CommunitySearch() {
9978
fetchAllCommunities();
10079
}, []);
10180

81+
// Sort hierarchically: depth, then full parent path, then name.
82+
// Recomputes on locale change without refetching.
83+
const sortedCommunities = useMemo(
84+
() =>
85+
[...allCommunities].sort((a, b) => {
86+
const depthCompare = a.parentsList.length - b.parentsList.length;
87+
if (depthCompare !== 0) return depthCompare;
88+
const aPath = a.parentsList
89+
.map((p) => p.community?.name || "")
90+
.join("/");
91+
const bPath = b.parentsList
92+
.map((p) => p.community?.name || "")
93+
.join("/");
94+
const pathCompare = aPath.localeCompare(bPath, i18n.language);
95+
if (pathCompare !== 0) return pathCompare;
96+
return a.name.localeCompare(b.name, i18n.language);
97+
}),
98+
[allCommunities, i18n.language],
99+
);
100+
102101
// Filter communities based on input
103102
useEffect(() => {
104103
if (!inputValue) {
105-
setFilteredOptions(allCommunities);
104+
setFilteredOptions(sortedCommunities);
106105
return;
107106
}
108107

109108
const lowercaseInput = inputValue.toLowerCase();
110-
const filtered = allCommunities.filter(
109+
const filtered = sortedCommunities.filter(
111110
(community) =>
112111
community.name.toLowerCase().includes(lowercaseInput) ||
113112
(community.regionName &&
114113
community.regionName.toLowerCase().includes(lowercaseInput)),
115114
);
116115
setFilteredOptions(filtered);
117-
}, [inputValue, allCommunities]);
116+
}, [inputValue, sortedCommunities]);
118117

119118
const handleInputChange = (_event: React.SyntheticEvent, value: string) => {
120119
setInputValue(value);

app/web/features/messages/groupchats/AdminsDialog.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ export default function AdminsDialog({
176176
groupChat,
177177
...props
178178
}: AdminsDialogProps) {
179-
const { t } = useTranslation([GLOBAL, MESSAGES]);
179+
const { t, i18n } = useTranslation([GLOBAL, MESSAGES]);
180180
const [error, setError] = useState("");
181181

182182
const nonAdminIds = groupChat?.memberUserIdsList.filter(
@@ -213,7 +213,10 @@ export default function AdminsDialog({
213213
<CenteredSpinner />
214214
) : (
215215
Array.from(admins.data?.values() ?? [])
216-
.sort((a, b) => b?.name.localeCompare(a?.name ?? "") ?? 0)
216+
.sort(
217+
(a, b) =>
218+
b?.name.localeCompare(a?.name ?? "", i18n.language) ?? 0,
219+
)
217220
.map((user) =>
218221
user ? (
219222
<AdminListItem
@@ -242,7 +245,10 @@ export default function AdminsDialog({
242245
<CenteredSpinner />
243246
) : (
244247
Array.from(nonAdmins.data?.values() ?? [])
245-
.sort((a, b) => b?.name.localeCompare(a?.name ?? "") ?? 0)
248+
.sort(
249+
(a, b) =>
250+
b?.name.localeCompare(a?.name ?? "", i18n.language) ?? 0,
251+
)
246252
.map((user) =>
247253
user ? (
248254
<AdminListItem

app/web/features/profile/ProfileTagInput.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ export default function ProfileTagInput({
153153
className,
154154
inputFieldProps,
155155
}: ProfileTagInputProps) {
156-
const { t } = useTranslation(PROFILE);
156+
const { t, i18n } = useTranslation(PROFILE);
157157

158158
// In case some value doesn't map to an option, add a fake option for it.
159159
// For example if value is [en, xx] and options is { en: "English", fr: "French" },
@@ -272,7 +272,7 @@ export default function ProfileTagInput({
272272
disablePortal
273273
options={Object.entries(effectiveOptions)
274274
.map(([key, label]) => ({ key, label }))
275-
.sort((a, b) => a.label.localeCompare(b.label))}
275+
.sort((a, b) => a.label.localeCompare(b.label, i18n.language))}
276276
renderOption={(props, option, { selected }) => {
277277
const { key, ...rest } = props;
278278

app/web/features/translate/LanguagePickerSelect.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export default function LanguagePickerSelect({
7373
const isAuthenticated = authState.authenticated;
7474

7575
const isMobile = useMediaQuery(theme.breakpoints.down("md"));
76-
const { t } = useTranslation([GLOBAL]);
76+
const { t, i18n } = useTranslation([GLOBAL]);
7777

7878
const { data: languages, isLoading, error } = useWeblateStats();
7979
const { showAllLanguages } = useShowAllLanguages();
@@ -124,7 +124,11 @@ export default function LanguagePickerSelect({
124124

125125
// Languages with < 50% translated are hidden from language selector (unless showAllLanguages is enabled)
126126
// Languages with < 80% translated are greyed out
127-
const availableLanguages = getAvailableLanguages(languages, showAllLanguages);
127+
const availableLanguages = getAvailableLanguages(
128+
languages,
129+
showAllLanguages,
130+
i18n.language,
131+
);
128132

129133
const menuItems: React.ReactNode[] | undefined = isLoading
130134
? []

app/web/features/translate/utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export function isLanguageProductionReady(
4040
export function getAvailableLanguages(
4141
languages: WeblateLanguage[] | undefined,
4242
showAll = false,
43+
locale?: string,
4344
): WeblateLanguage[] {
4445
if (!languages) {
4546
return [];
@@ -63,6 +64,6 @@ export function getAvailableLanguages(
6364
b.translated_percent >= ALMOST_DONE_CUTOFF
6465
)
6566
return 1;
66-
return a.code.localeCompare(b.code);
67+
return a.code.localeCompare(b.code, locale);
6768
});
6869
}

0 commit comments

Comments
 (0)