Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions front_end/messages/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2081,5 +2081,6 @@
"tournamentUnfollowModalSubmit": "Zrušit sledování",
"switchBackToSlidersHint": "přepněte zpět na posuvníky pro plynulé přizpůsobení",
"view": "Zobrazit",
"maxAttainableCoverageExplanation": "Otázky, které se vyřeší dříve, mají své skóre a pokrytí upravené, aby se zachovala správnost skóre. <link>Více se dozvíte zde.</link>",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"thousandsOfOpenQuestions": "20 000+ otevřených otázek"
}
5 changes: 3 additions & 2 deletions front_end/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@
"communityRelativeLegacyScore": "Community Relative Legacy Score",
"communityRelativeLegacyArchivedScore": "Community Relative Legacy Archived Score",
"myCoverage": "My coverage",
"maxAttainableCoverageExplanation": "Questions that resolve early have their scores and coverage truncated to preserve score properness. <link>Learn more here.</link>",
"myWeightedCoverage": "My weighted coverage",
"questionWillOpenAt": "This question will open {{date}}.",
"questionNotOpenYet": "This question is not yet open for predictions.",
Expand Down Expand Up @@ -1567,8 +1568,8 @@
"participations": "Participations",
"participationSummary": "Participation Summary",
"participationSummaryPredictionNrStats": "You made <strong>{userUpdates} {userUpdates, plural, =0 {updates} =1 {update} other {updates} }</strong> vs. a community average of <strong>{communityUpdates} {communityUpdates, plural, =0 {updates} =1 {update} other {updates} }</strong>.",
"participationSummaryCoverageBetterStats": "Your coverage was <strong>{userCoverage}%</strong>, better than the average forecaster on this question (<strong>{averageCoverage}%</strong>).",
"participationSummaryCoverageWorseStats": "Your coverage was <strong>{userCoverage}%</strong>, worse than the average forecaster on this question (<strong>{averageCoverage}%</strong>).",
"participationSummaryCoverageBetterStats": "Your coverage was <strong>{userCoverage}%</strong><maxCoverageDisplay></maxCoverageDisplay>, better than the average forecaster on this question (<strong>{averageCoverage}%</strong>).",
"participationSummaryCoverageWorseStats": "Your coverage was <strong>{userCoverage}%</strong><maxCoverageDisplay></maxCoverageDisplay>, worse than the average forecaster on this question (<strong>{averageCoverage}%</strong>).",
"participationSummaryScoreOutperformance": "Congrats, you <strong>outperformed the Community Prediction</strong> in {scoreTypes}.",
"peer": "Peer",
"bothPeerAndBaseline": "both Peer and Baseline Scores",
Expand Down
1 change: 1 addition & 0 deletions front_end/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -2081,5 +2081,6 @@
"tournamentUnfollowModalSubmit": "Dejar de Seguir",
"switchBackToSlidersHint": "vuelve a los deslizadores para un ajuste suave",
"view": "Ver",
"maxAttainableCoverageExplanation": "Las preguntas que se resuelven pronto tienen sus puntuaciones y cobertura truncadas para preservar la propiedad adecuada de la puntuación. <link>Aprende más aquí.</link>",
"thousandsOfOpenQuestions": "20,000+ preguntas abiertas"
}
1 change: 1 addition & 0 deletions front_end/messages/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -2079,5 +2079,6 @@
"tournamentUnfollowModalSubmit": "Deixar de Seguir",
"switchBackToSlidersHint": "volte para os controles deslizantes para um ajuste suave",
"view": "Visualizar",
"maxAttainableCoverageExplanation": "Questões que são resolvidas cedo têm suas pontuações e coberturas truncadas para preservar a correção da pontuação. <link>Saiba mais aqui.</link>",
Comment thread
lsabor marked this conversation as resolved.
"thousandsOfOpenQuestions": "20.000+ perguntas abertas"
}
1 change: 1 addition & 0 deletions front_end/messages/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -2078,5 +2078,6 @@
"tournamentUnfollowModalSubmit": "取消關注",
"switchBackToSlidersHint": "切換回滑桿以平滑調整",
"view": "檢視",
"maxAttainableCoverageExplanation": "為了保持分數的正確性,早期解決的問題其分數和覆蓋率會被截斷。<link>在這裡了解更多。</link>",
Comment thread
lsabor marked this conversation as resolved.
"thousandsOfOpenQuestions": "20,000+ 開放問題"
}
1 change: 1 addition & 0 deletions front_end/messages/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -2083,5 +2083,6 @@
"tournamentUnfollowModalSubmit": "取消关注",
"switchBackToSlidersHint": "切回滑块以进行更精细的调整",
"view": "查看",
"maxAttainableCoverageExplanation": "为确保分数的准确性,提前解决的问题,其分数和覆盖率会被截断。<link>在此了解更多信息。</link>",
"thousandsOfOpenQuestions": "20,000+ 开放问题"
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { isNil } from "lodash";
import { useTranslations } from "next-intl";
import React, { FC } from "react";
import React, { FC, ReactNode } from "react";

import SectionToggle from "@/components/ui/section_toggle";
import Tooltip from "@/components/ui/tooltip";
import { QuestionWithForecasts, ScoreData } from "@/types/question";
import { TranslationKey } from "@/types/translations";
import cn from "@/utils/core/cn";
Expand All @@ -14,8 +17,10 @@ type Props = {
variant?: Variant;
};

type ScoreRow = { label: string; value: string; valueSuffix?: ReactNode };

const ScoreTable: FC<{
rows: { label: string; value: string }[];
rows: ScoreRow[];
className?: string;
variant?: Variant;
}> = ({ rows, className, variant = "auto" }) => (
Expand All @@ -42,11 +47,12 @@ const ScoreTable: FC<{
</span>
<span
className={cn(
"w-[34%] pl-4 text-center text-base text-gray-800 dark:text-gray-800-dark",
"flex w-[34%] items-center justify-center gap-0.5 pl-4 text-center text-base text-gray-800 dark:text-gray-800-dark",
{ "sm:w-1/2": variant === "auto" }
)}
>
{row.value}
{row.valueSuffix}
</span>
</div>
))}
Expand Down Expand Up @@ -77,6 +83,21 @@ const buildScoreLabelKey = (
return (prefix + toCamel(key) + suffix) as TranslationKey;
};

/**
* Returns the max attainable peer coverage (0–1) for a question that resolved
* before its scheduled close time, or null if not applicable.
*/
const getMaxCoverage = (question: QuestionWithForecasts): number | null => {
const { open_time, actual_close_time, scheduled_close_time } = question;
if (!open_time || !actual_close_time || !scheduled_close_time) return null;
const open = new Date(open_time).getTime();
const actualClose = new Date(actual_close_time).getTime();
const scheduledClose = new Date(scheduled_close_time).getTime();
const totalDuration = scheduledClose - open;
if (totalDuration <= 0) return null;
return (actualClose - open) / totalDuration;
};
Comment thread
lsabor marked this conversation as resolved.
Comment on lines +78 to +87
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getMaxCoverage is defined identically in both additional_scores_table.tsx and participation_summary.tsx. There's already a utils.ts in the same post_score_data/ directory – consider extracting getMaxCoverage there and importing it in both files.


export const AdditionalScoresTable: FC<Props> = ({
question,
separateCoverage,
Expand Down Expand Up @@ -107,8 +128,41 @@ export const AdditionalScoresTable: FC<Props> = ({
"weighted_coverage",
];

const coverageRows: { label: string; value: string }[] = [];
const otherRows: { label: string; value: string }[] = [];
// Peer coverage uses scheduled_close_time as total duration, so early
// resolution reduces the max attainable coverage.
const maxCoverage = getMaxCoverage(question);
let maxCoverageValueSuffix: ReactNode;
if (maxCoverage !== null) {
maxCoverageValueSuffix = (
<span className="text-sm text-gray-600 dark:text-gray-600-dark">
{" (max. "}
{(maxCoverage * 100).toFixed(1)}%
<Tooltip
tooltipContent={t.rich("maxAttainableCoverageExplanation", {
link: (chunks) => (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid hardcoded English in max coverage label.

"(max. ...)" is hardcoded in TSX, so this fragment won’t localize correctly.

Based on learnings: Do not hardcode English strings in TSX components; prefer useTranslations() and i18n strings across components, avoiding hardcoded literals that stall localization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@front_end/src/app/`(main)/questions/[id]/components/post_score_data/additional_scores_table.tsx
around lines 137 - 142, Replace the hardcoded English fragment "(max. ...)" in
the AdditionalScoresTable TSX with a localized string using the translations
hook (useTranslations) so the entire label is translatable; locate the JSX
around maxCoverage and Tooltip in additional_scores_table.tsx and wrap the whole
phrase in t or t.rich (passing the formatted value {(maxCoverage *
100).toFixed(1)}% as an interpolation) instead of the literal " (max. " and
closing ")". Ensure you remove the hardcoded punctuation and pass any
link/Tooltip content to t.rich to preserve existing behavior.

<a
href="https://www.metaculus.com/help/scores-faq/#score-truncation"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
{chunks}
</a>
),
})}
>
<FontAwesomeIcon
icon={faCircleInfo}
className="ml-0.5 cursor-help text-blue-500 dark:text-blue-500-dark"
/>
</Tooltip>
{")"}
</span>
);
}

const coverageRows: ScoreRow[] = [];
const otherRows: ScoreRow[] = [];

for (const key of scoreKeys) {
if (key === peerKey || key === baselineKey) continue;
Expand All @@ -126,6 +180,8 @@ export const AdditionalScoresTable: FC<Props> = ({
targetRows.push({
label: t(buildScoreLabelKey(key, "user")),
value: formattedValue,
// Only peer coverage (key === "coverage") is affected by early resolution
...(key === "coverage" ? { valueSuffix: maxCoverageValueSuffix } : {}),
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { faClock } from "@fortawesome/free-regular-svg-icons";
import { faFire, faRepeat } from "@fortawesome/free-solid-svg-icons";
import {
faCircleInfo,
faFire,
faRepeat,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { isNil } from "lodash";
import { useTranslations } from "next-intl";
import React, { PropsWithChildren, ReactNode } from "react";

import Tooltip from "@/components/ui/tooltip";
import { QuestionWithForecasts } from "@/types/question";
import cn from "@/utils/core/cn";

Expand Down Expand Up @@ -34,6 +39,21 @@ type Props = {
itemClassName?: string;
};

/**
* Returns the max attainable peer coverage (0–1) for a question that resolved
* before its scheduled close time, or null if not applicable.
*/
const getMaxCoverage = (question: QuestionWithForecasts): number | null => {
const { open_time, actual_close_time, scheduled_close_time } = question;
if (!open_time || !actual_close_time || !scheduled_close_time) return null;
const open = new Date(open_time).getTime();
const actualClose = new Date(actual_close_time).getTime();
const scheduledClose = new Date(scheduled_close_time).getTime();
const totalDuration = scheduledClose - open;
if (totalDuration <= 0) return null;
return (actualClose - open) / totalDuration;
};
Comment thread
lsabor marked this conversation as resolved.

export const ParticipationSummary: React.FC<Props> = ({
question,
forecastsCount,
Expand Down Expand Up @@ -91,6 +111,38 @@ export const ParticipationSummary: React.FC<Props> = ({
</span>
);

// Only peer coverage (non-spot) is affected by early resolution.
const maxCoverage = isSpot ? null : getMaxCoverage(question);
const richMaxCoverageDisplay = (_chunks: ReactNode) => {
if (maxCoverage === null) return null;
return (
<span>
{" (max. "}
{Math.round(maxCoverage * 100)}%
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

(max. …) label is still hardcoded English.

This prevents full localization of the participation summary string.

Based on learnings: Do not hardcode English strings in TSX components; prefer useTranslations() and i18n strings across components, avoiding hardcoded literals that stall localization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@front_end/src/app/`(main)/questions/[id]/components/post_score_data/participation_summary.tsx
around lines 120 - 121, The hardcoded "(max. " and "%" in
participation_summary.tsx prevent localization; update the component to use
useTranslations() (e.g., const t = useTranslations('ParticipationSummary')) and
replace the literal bits around Math.round(maxCoverage * 100) with a single
localized string key that accepts the percentage as an interpolated variable
(for example t('max', { pct: Math.round(maxCoverage * 100) })), or use separate
keys if your i18n prefers prefix/suffix; ensure you remove the hardcoded " (max.
" and "%" and add the corresponding i18n key(s) in your locale files.

<Tooltip
tooltipContent={t.rich("maxAttainableCoverageExplanation", {
link: (chunks) => (
<a
href="https://www.metaculus.com/help/scores-faq/#score-truncation"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
{chunks}
</a>
),
})}
>
<FontAwesomeIcon
icon={faCircleInfo}
className="ml-0.5 cursor-help text-blue-500 dark:text-blue-500-dark"
/>
</Tooltip>
{")"}
</span>
);
};

return (
<div className={cn("flex flex-col gap-2", className)}>
<ParticipationItem
Expand All @@ -115,6 +167,7 @@ export const ParticipationSummary: React.FC<Props> = ({
strong: richStrong,
userCoverage: Math.round(userCoverage * 100),
averageCoverage: Math.round(averageCoverage * 100),
maxCoverageDisplay: richMaxCoverageDisplay,
}
)}
</ParticipationItem>
Expand Down
2 changes: 0 additions & 2 deletions front_end/src/components/question/score_card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -466,8 +466,6 @@ export default function ScoreCard({
const t = useTranslations();
const isSpot = type.includes("spot");

console.log("isSpot", isSpot, type);

if (type.includes("peer")) {
return (
<PeerScoreCard
Expand Down
Loading