Skip to content

Commit bae9656

Browse files
authored
fix(agora): localize language control label (#1135)
* fix(agora): localize language control label Use the existing localized language label utility for the new-conversation control bar so detected language names follow the active display language instead of always rendering English metadata. This makes French UI labels render values such as "Langues : français" rather than "Langues : French" and removes the now-unused English-only helper. Deploy: agora * fix(load-testing): guard restricted conversations Scenario 1 only generates fresh guest UCAN identities, so restricted conversations fail opinion creation with participation-gate reasons like account_required instead of creating statements. Add a setup preflight that fetches conversation metadata and aborts early when the target conversation is not guest-enabled or requires an event ticket. Also make the initial conversation page fetch configurable separately from in-flow page fetches, while preserving the ability for CONVERSATION_PAGE_FETCH_PROBABILITY=0 to disable both. Document the guest-only scenario constraint and the new initial page-fetch override. Deploy: none * fix(api): backfill AI description locales Restore detected conversation languages as effective translation targets for existing conversations. A previous backfill removed target-language rows matching the conversation content source language, which is correct for content translation but prevents AI group descriptions from being translated back into the conversation language because descriptions are generated in English.\n\nThe migration derives valid target rows from the display-language enum and skips conversations that already have an active row.\n\nDeploy: api
1 parent 33c88b1 commit bae9656

6 files changed

Lines changed: 259 additions & 56 deletions

File tree

services/agora/src/components/newConversation/NewConversationControlBar.vue

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,8 @@ import { usePremiumFeatureApi } from "src/utils/api/premiumFeature";
143143
import { processEnv } from "src/utils/processEnv";
144144
import { computed, ref, watch } from "vue";
145145
146-
import {
147-
formatLanguageControlLabel,
148-
getDisplayLanguageName,
149-
} from "./conversationLanguageControlLabel";
146+
import { formatLanguageControlLabel } from "./conversationLanguageControlLabel";
147+
import { getLanguageLabel } from "./dialog/conversationLanguageSettings.utils";
150148
import MaxDiffSourceDialog from "./dialog/MaxDiffSourceDialog.vue";
151149
import PostTypeDialog from "./dialog/PostTypeDialog.vue";
152150
import {
@@ -190,7 +188,7 @@ const props = withDefaults(defineProps<Props>(), {
190188
hideLanguageSetting: false,
191189
});
192190
193-
const { t } = useComponentI18n<NewConversationControlBarTranslations>(
191+
const { t, locale } = useComponentI18n<NewConversationControlBarTranslations>(
194192
newConversationControlBarTranslations
195193
);
196194
@@ -609,7 +607,10 @@ const languageSettingLabel = computed(() => {
609607
const detectedLanguage =
610608
props.detectedLanguageCode === null || props.detectedLanguageCode === undefined
611609
? t("languagePrimaryAuto")
612-
: getDisplayLanguageName({ languageCode: props.detectedLanguageCode });
610+
: getLanguageLabel({
611+
languageCode: props.detectedLanguageCode,
612+
locale: locale.value,
613+
});
613614
614615
return formatLanguageControlLabel({
615616
languagesLabel: t("languagesLabel"),

services/agora/src/components/newConversation/conversationLanguageControlLabel.ts

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,5 @@
1-
import {
2-
type DisplayLanguageMetadata,
3-
type LanguageMetadata,
4-
type SupportedDisplayLanguageCodes,
5-
SupportedSpokenLanguageMetadataList,
6-
} from "src/shared/languages";
71
import type { ConversationMultilingualSetting } from "src/shared/types/zod";
82

9-
interface GetDisplayLanguageNameParams {
10-
languageCode: SupportedDisplayLanguageCodes;
11-
}
12-
133
interface FormatCompactLanguageSummaryParams {
144
primaryLanguage: string;
155
multilingualSetting: ConversationMultilingualSetting;
@@ -21,22 +11,6 @@ interface FormatLanguageControlLabelParams extends FormatCompactLanguageSummaryP
2111
languagesLabel: string;
2212
}
2313

24-
function isDisplayLanguageMetadata(
25-
language: LanguageMetadata
26-
): language is DisplayLanguageMetadata {
27-
return language.displaySupported;
28-
}
29-
30-
export function getDisplayLanguageName({
31-
languageCode,
32-
}: GetDisplayLanguageNameParams): string {
33-
return (
34-
SupportedSpokenLanguageMetadataList.filter(isDisplayLanguageMetadata).find(
35-
(language) => language.code === languageCode
36-
)?.englishName ?? languageCode
37-
);
38-
}
39-
4014
export function formatCompactLanguageSummary({
4115
primaryLanguage,
4216
multilingualSetting,
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
-- AI group descriptions are generated in English and then translated into the
2+
-- conversation's effective display languages. V0073.1 removed target-language
3+
-- rows matching the content source language, which is correct for content
4+
-- translation but prevents eager AI-description translation back into the
5+
-- conversation's detected language for existing conversations.
6+
WITH supported_display_language AS (
7+
SELECT unnest(enum_range(NULL::display_language_code)) AS language_code
8+
)
9+
INSERT INTO conversation_translation_target_language (
10+
conversation_id,
11+
language_code,
12+
created_at
13+
)
14+
SELECT
15+
conversation.id,
16+
supported_display_language.language_code,
17+
now()
18+
FROM conversation
19+
JOIN conversation_content
20+
ON conversation_content.id = conversation.current_content_id
21+
JOIN supported_display_language
22+
ON supported_display_language.language_code::text = conversation_content.source_language_code::text
23+
WHERE conversation_content.source_language_code IS NOT NULL
24+
AND NOT EXISTS (
25+
SELECT 1
26+
FROM conversation_translation_target_language existing_target_language
27+
WHERE existing_target_language.conversation_id = conversation.id
28+
AND existing_target_language.language_code = supported_display_language.language_code
29+
AND existing_target_language.deleted_at IS NULL
30+
)
31+
ON CONFLICT DO NOTHING;

services/load-testing/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ Tests voting load on one or more conversations:
150150
- Each user votes on 50-100% of fetched opinions
151151
- Users occasionally create new opinions while voting
152152
- Page fetches are mixed in to simulate realistic browsing behavior
153+
- Requires conversations with `participationMode: "guest"` and no event-ticket requirement. The scenario generates fresh guest identities and does not perform account registration, email verification, or strong verification.
153154

154155
**What it tests:**
155156

@@ -168,6 +169,7 @@ Edit scenario files to adjust:
168169
- `OPINION_CREATOR_COUNT` / `ADDITIONAL_VOTERS` (default: 50/50)
169170
- `INTERMITTENT_OPINION_CREATION_PROBABILITY` (default: 0.1)
170171
- `MAIN_PAGE_FETCH_PROBABILITY` / `CONVERSATION_PAGE_FETCH_PROBABILITY`
172+
- `INITIAL_CONVERSATION_PAGE_FETCH_PROBABILITY` to override the initial frontend page-load probability. If omitted, an explicit `CONVERSATION_PAGE_FETCH_PROBABILITY` value is reused; otherwise the initial page load defaults to always on.
171173
- `SLEEP_BETWEEN_ACTIONS` and retry constants in the scenario file
172174

173175
### Monitoring Configuration (Local Only)

services/load-testing/src/scenario1-single-conversation.ts

Lines changed: 90 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { createDidIfDoesNotExist } from "./crypto/ucan/operation.js";
2525
import {
2626
createOpinion,
2727
fetchOpinions,
28+
fetchConversationMetadata,
2829
fetchConversationPage,
2930
fetchSurveyForm,
3031
saveSurveyAnswer,
@@ -77,6 +78,16 @@ function getBooleanEnv({
7778
return fallback;
7879
}
7980

81+
function getEnvValueOrFallback({
82+
value,
83+
fallback,
84+
}: {
85+
value: string | undefined;
86+
fallback: string | undefined;
87+
}): string | undefined {
88+
return value === undefined || value === "" ? fallback : value;
89+
}
90+
8091
function getVotingPatternEnv(value: string | undefined): VotingPattern {
8192
if (value === undefined || value === "") {
8293
return "random";
@@ -136,6 +147,15 @@ const CONVERSATION_PAGE_FETCH_PROBABILITY = getNumberEnv({
136147
value: __ENV.CONVERSATION_PAGE_FETCH_PROBABILITY,
137148
fallback: 0.15,
138149
}); // 15% chance to fetch conversation page during actions (higher because users refresh to see updates)
150+
const INITIAL_CONVERSATION_PAGE_FETCH_PROBABILITY = clampProbability(
151+
getNumberEnv({
152+
value: getEnvValueOrFallback({
153+
value: __ENV.INITIAL_CONVERSATION_PAGE_FETCH_PROBABILITY,
154+
fallback: __ENV.CONVERSATION_PAGE_FETCH_PROBABILITY,
155+
}),
156+
fallback: 1,
157+
}),
158+
);
139159
const AUTO_FILL_SURVEY = getBooleanEnv({
140160
value: __ENV.AUTO_FILL_SURVEY || __ENV.SURVEY_AUTO_FILL,
141161
fallback: true,
@@ -241,6 +261,8 @@ logLoadEvent({
241261
votingOutlierRate: VOTING_PATTERN_CONFIG.outlierRate,
242262
clusterableVoteTargetPerConversation:
243263
CLUSTERABLE_VOTE_TARGET_PER_CONVERSATION,
264+
initialConversationPageFetchProbability:
265+
INITIAL_CONVERSATION_PAGE_FETCH_PROBABILITY,
244266
},
245267
});
246268

@@ -670,6 +692,39 @@ function getUserId(iterationIndex: number): string {
670692
*/
671693
export function setup(): void {
672694
console.log("=== Setup: user keypairs will be generated by each VU ===");
695+
for (const conversationSlugId of CONVERSATION_SLUG_IDS) {
696+
const metadataResult = fetchConversationMetadata({ conversationSlugId });
697+
if (!metadataResult.success) {
698+
throw new Error(
699+
`Failed to fetch metadata for conversation ${conversationSlugId}: ${metadataResult.error}`,
700+
);
701+
}
702+
703+
if (metadataResult.participationMode !== "guest") {
704+
throw new Error(
705+
`Conversation ${conversationSlugId} uses participationMode=${metadataResult.participationMode}. Scenario 1 creates guest users only; use a guest-enabled conversation or extend the load test to register/verify users.`,
706+
);
707+
}
708+
709+
if (metadataResult.requiresEventTicket !== undefined) {
710+
throw new Error(
711+
`Conversation ${conversationSlugId} requires event ticket ${metadataResult.requiresEventTicket}. Scenario 1 cannot satisfy event-ticket participation gates.`,
712+
);
713+
}
714+
715+
logLoadEvent({
716+
phase: "setup",
717+
action: "check_conversation_participation",
718+
outcome: "success",
719+
conversationSlugId,
720+
responseTimeMs: metadataResult.responseTime,
721+
metadata: {
722+
participationMode: metadataResult.participationMode,
723+
requiresEventTicket: null,
724+
},
725+
});
726+
}
727+
673728
logLoadEvent({
674729
phase: "setup",
675730
action: "prepare_users",
@@ -713,39 +768,50 @@ export default async function () {
713768
responseTimeMs: Date.now() - credentialStartTime,
714769
});
715770

716-
// Step 2: Fetch conversation page (simulates user landing on the frontend page)
717-
// This will trigger the frontend to make multiple API calls to the backend
718-
console.log(`[${userId}] Fetching conversation page...`);
719-
const conversationPageResult = fetchConversationPage({
720-
conversationSlugId: assignedConversationSlugId,
721-
});
722-
723-
conversationPageFetches.add(1);
724-
conversationPageResponseTime.add(conversationPageResult.responseTime);
725-
726-
if (!conversationPageResult.success) {
727-
console.error(
728-
`[${userId}] Failed to fetch conversation page: ${String(conversationPageResult.error)}`,
729-
);
730-
logLoadEvent({
731-
phase: "page_fetch",
732-
action: "fetch_conversation_page",
733-
outcome: "failure",
734-
userId,
735-
iterationIndex,
771+
// Step 2: Optionally fetch the conversation page to simulate landing on the frontend.
772+
if (Math.random() < INITIAL_CONVERSATION_PAGE_FETCH_PROBABILITY) {
773+
console.log(`[${userId}] Fetching conversation page...`);
774+
const conversationPageResult = fetchConversationPage({
736775
conversationSlugId: assignedConversationSlugId,
737-
responseTimeMs: conversationPageResult.responseTime,
738-
error: String(conversationPageResult.error),
739776
});
777+
778+
conversationPageFetches.add(1);
779+
conversationPageResponseTime.add(conversationPageResult.responseTime);
780+
781+
if (!conversationPageResult.success) {
782+
console.error(
783+
`[${userId}] Failed to fetch conversation page: ${String(conversationPageResult.error)}`,
784+
);
785+
logLoadEvent({
786+
phase: "page_fetch",
787+
action: "fetch_conversation_page",
788+
outcome: "failure",
789+
userId,
790+
iterationIndex,
791+
conversationSlugId: assignedConversationSlugId,
792+
responseTimeMs: conversationPageResult.responseTime,
793+
error: String(conversationPageResult.error),
794+
});
795+
} else {
796+
logLoadEvent({
797+
phase: "page_fetch",
798+
action: "fetch_conversation_page",
799+
outcome: "success",
800+
userId,
801+
iterationIndex,
802+
conversationSlugId: assignedConversationSlugId,
803+
responseTimeMs: conversationPageResult.responseTime,
804+
});
805+
}
740806
} else {
741807
logLoadEvent({
742808
phase: "page_fetch",
743809
action: "fetch_conversation_page",
744-
outcome: "success",
810+
outcome: "skip",
745811
userId,
746812
iterationIndex,
747813
conversationSlugId: assignedConversationSlugId,
748-
responseTimeMs: conversationPageResult.responseTime,
814+
metadata: { skipReason: "initial_page_fetch_probability" },
749815
});
750816
}
751817

0 commit comments

Comments
 (0)