From 97e6620d37b949534b0beaecc255a240421a62ae Mon Sep 17 00:00:00 2001 From: leonardthethird Date: Thu, 21 May 2026 16:58:32 -0700 Subject: [PATCH 1/3] feat(notebooks): require feed tile summary when tile preview is noisy When the visible start of a notebook's body would render as raw HTML or table-like pipe noise on a feed tile, require feed_tile_summary so authors set a clean plain-text preview at creation time instead of discovering the broken tile after publish. Implementation: - Reuses the tile's own getMarkdownSummary helper to compute exactly what a reader sees, sized to the widest realistic tile (~900px / ~450 rendered chars). Anything past that point is past every tile size and doesn't trigger the rule, so a clean intro followed by a table past the visible area still submits cleanly. - Scans the rendered preview for two surviving-after-strip-markdown patterns: raw HTML tags (iframes, divs) and 3+ pipes on a line (table-like content MDXEditor didn't promote to a real table). Links, inline code, fenced code, images, emphasis, and lists all render correctly on the tile and are intentionally not flagged. - A 2000 source-char slice bounds remark parse cost on long notebooks. - Adds a zod superRefine on the notebook schema that attaches a ZodIssueCode.custom issue to feed_tile_summary; the existing Textarea + FormError pair already renders field errors as red inline text. - New i18n key feedTileSummaryRequiredForFormattedContent with the explainer copy. Server-side validation was skipped intentionally: this is a UX rule, not a data integrity rule, and NotebookWriteSerializer has no equivalent cross-field validation pattern. Co-Authored-By: Claude Opus 4.7 (1M context) --- front_end/messages/en.json | 3 +- .../questions/components/notebook_form.tsx | 86 ++++++++++++++----- 2 files changed, 66 insertions(+), 23 deletions(-) diff --git a/front_end/messages/en.json b/front_end/messages/en.json index 9f54464212..a0c5c896b5 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -2197,5 +2197,6 @@ "whyTrustMetaculusLessNoiseTitle": "Less noise, more truth", "whyTrustMetaculusLessNoise": "Metaculus's crowd-powered forecasts cut through the noise by grounding every prediction in transparent evidence, accountable scoring, and a decade of demonstrated accuracy. Metaculus equips policymakers, researchers, journalists, and corporate organizations with evidence-based forecasts that surface clear, quantifiable insight into the world's most critical uncertainties. Explore our suite of Business Solutions to learn more about how Metaculus can improve your organization's decision-making.", "publishTimeLockedDescription": "Publish time cannot be changed after creation.", - "feedTileSummaryPlaceholder": "Optional: Enter a custom summary text to display on feed tiles (if not provided, a summary will be auto-generated from the notebook content)" + "feedTileSummaryPlaceholder": "Optional: Enter a custom summary text to display on feed tiles (if not provided, a summary will be auto-generated from the notebook content)", + "feedTileSummaryRequiredForFormattedContent": "Feed tile summary is required because the visible start of your notebook contains a table or raw HTML that wouldn't render cleanly on a feed tile. Add a short plain-text summary so readers see a clean preview." } diff --git a/front_end/src/app/(main)/questions/components/notebook_form.tsx b/front_end/src/app/(main)/questions/components/notebook_form.tsx index 9d3993ccf6..4f306c962e 100644 --- a/front_end/src/app/(main)/questions/components/notebook_form.tsx +++ b/front_end/src/app/(main)/questions/components/notebook_form.tsx @@ -33,36 +33,78 @@ import { getQuestionDraft, saveQuestionDraft, } from "@/utils/drafts/questionForm"; +import { getMarkdownSummary } from "@/utils/markdown"; import { getPostLink } from "@/utils/navigation"; import BacktoCreate from "./back_to_create"; import CategoryPicker from "./category_picker"; import { createQuestionPost, updatePost } from "../actions"; +// Run the same strip-markdown pipeline the feed tile uses (getMarkdownSummary) +// to scan exactly what a reader would see. Sized to the widest realistic tile +// (~900px → ~450 rendered chars); anything past that point is past every tile +// size and doesn't need a summary. MAX_SOURCE_CHARS_TO_SCAN bounds the remark +// parse cost on long notebooks — 2000 source chars maps to well over the +// rendered budget even with markdown-heavy input. +const TILE_PREVIEW_WIDTH = 900; +const TILE_PREVIEW_HEIGHT = 80; +const MAX_SOURCE_CHARS_TO_SCAN = 2000; + +// Patterns whose presence in the *rendered* preview would surface visibly on +// the tile. strip-markdown leaves these in: raw HTML tags (iframes, divs) and +// pipes from table-like content MDXEditor didn't promote to a real table. +// Plain text and `[text](url)` link syntax are accepted: links re-render as +// real tags via the tile's read-mode MarkdownEditor. +const NOISY_TILE_PREVIEW_PATTERNS: RegExp[] = [ + /<[a-zA-Z][^>]*>/, // raw HTML + /\|[^|\n]*\|[^|\n]*\|/, // 3+ pipes on a single line (table-ish) +]; + +const hasNoisyTilePreview = (markdown: string | undefined): boolean => { + if (!markdown) return false; + const preview = getMarkdownSummary({ + markdown: markdown.slice(0, MAX_SOURCE_CHARS_TO_SCAN), + width: TILE_PREVIEW_WIDTH, + height: TILE_PREVIEW_HEIGHT, + }); + return NOISY_TILE_PREVIEW_PATTERNS.some((pattern) => pattern.test(preview)); +}; + const createNotebookSchema = (t: ReturnType) => { - return z.object({ - title: z - .string() - .min(4, { - message: t("errorMinLength", { field: "String", minLength: 4 }), - }) - .max(200, { - message: t("errorMaxLength", { field: "String", maxLength: 200 }), + return z + .object({ + title: z + .string() + .min(4, { + message: t("errorMinLength", { field: "String", minLength: 4 }), + }) + .max(200, { + message: t("errorMaxLength", { field: "String", maxLength: 200 }), + }), + short_title: z + .string() + .min(4, { + message: t("errorMinLength", { field: "String", minLength: 4 }), + }) + .max(80, { + message: t("errorMaxLength", { field: "String", maxLength: 80 }), + }), + default_project: z.number(), + markdown: z.string().min(1, { + message: t("errorMinLength", { field: "String", minLength: 1 }), }), - short_title: z - .string() - .min(4, { - message: t("errorMinLength", { field: "String", minLength: 4 }), - }) - .max(80, { - message: t("errorMaxLength", { field: "String", maxLength: 80 }), - }), - default_project: z.number(), - markdown: z.string().min(1, { - message: t("errorMinLength", { field: "String", minLength: 1 }), - }), - feed_tile_summary: z.string().optional(), - }); + feed_tile_summary: z.string().optional(), + }) + .superRefine((data, ctx) => { + const summary = data.feed_tile_summary?.trim() ?? ""; + if (summary.length === 0 && hasNoisyTilePreview(data.markdown)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: t("feedTileSummaryRequiredForFormattedContent"), + path: ["feed_tile_summary"], + }); + } + }); }; type FormData = z.infer>; From c64bf8733f39cfe624a702baab4cfb0dc59996f0 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 00:51:28 +0000 Subject: [PATCH 2/3] feat(notebooks): flag lists, blockquotes, and code fences as noisy tile previews MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lists (bullet + ordered), blockquotes, and fenced code blocks all have their structural markers stripped by getMarkdownSummary, so they can't be detected in the rendered preview output. But their visual semantics (bullets, numbering, indentation, monospace) are part of the author's intent — without them the tile shows the content as run-on prose, silently stripping meaning. Detect them against the raw source within a tile-visible window instead. Per Sylvain's review feedback on lists; covers the analogous cases (blockquotes, code fences) at the same time. LaTeX is left alone — it re-renders correctly via the tile's read-mode MarkdownEditor. Co-authored-by: leonardthethird --- front_end/messages/en.json | 2 +- .../questions/components/notebook_form.tsx | 32 ++++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/front_end/messages/en.json b/front_end/messages/en.json index a0c5c896b5..a2e7a93115 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -2198,5 +2198,5 @@ "whyTrustMetaculusLessNoise": "Metaculus's crowd-powered forecasts cut through the noise by grounding every prediction in transparent evidence, accountable scoring, and a decade of demonstrated accuracy. Metaculus equips policymakers, researchers, journalists, and corporate organizations with evidence-based forecasts that surface clear, quantifiable insight into the world's most critical uncertainties. Explore our suite of Business Solutions to learn more about how Metaculus can improve your organization's decision-making.", "publishTimeLockedDescription": "Publish time cannot be changed after creation.", "feedTileSummaryPlaceholder": "Optional: Enter a custom summary text to display on feed tiles (if not provided, a summary will be auto-generated from the notebook content)", - "feedTileSummaryRequiredForFormattedContent": "Feed tile summary is required because the visible start of your notebook contains a table or raw HTML that wouldn't render cleanly on a feed tile. Add a short plain-text summary so readers see a clean preview." + "feedTileSummaryRequiredForFormattedContent": "Feed tile summary is required because the visible start of your notebook contains formatting (a list, blockquote, code block, table, or raw HTML) that wouldn't render cleanly on a feed tile. Add a short plain-text summary so readers see a clean preview." } diff --git a/front_end/src/app/(main)/questions/components/notebook_form.tsx b/front_end/src/app/(main)/questions/components/notebook_form.tsx index 4f306c962e..5f55597542 100644 --- a/front_end/src/app/(main)/questions/components/notebook_form.tsx +++ b/front_end/src/app/(main)/questions/components/notebook_form.tsx @@ -45,29 +45,51 @@ import { createQuestionPost, updatePost } from "../actions"; // (~900px → ~450 rendered chars); anything past that point is past every tile // size and doesn't need a summary. MAX_SOURCE_CHARS_TO_SCAN bounds the remark // parse cost on long notebooks — 2000 source chars maps to well over the -// rendered budget even with markdown-heavy input. +// rendered budget even with markdown-heavy input. TILE_VISIBLE_SOURCE_CHARS +// is a tighter window used for raw-source checks (lists, blockquotes, code +// fences): their markers are stripped before render, so we can't see them in +// the preview output and instead approximate "could appear on the tile" by +// scanning the first ~600 source chars. const TILE_PREVIEW_WIDTH = 900; const TILE_PREVIEW_HEIGHT = 80; const MAX_SOURCE_CHARS_TO_SCAN = 2000; +const TILE_VISIBLE_SOURCE_CHARS = 600; -// Patterns whose presence in the *rendered* preview would surface visibly on -// the tile. strip-markdown leaves these in: raw HTML tags (iframes, divs) and +// Patterns whose presence in the *rendered* preview surfaces visibly on the +// tile. strip-markdown leaves these in: raw HTML tags (iframes, divs) and // pipes from table-like content MDXEditor didn't promote to a real table. // Plain text and `[text](url)` link syntax are accepted: links re-render as // real tags via the tile's read-mode MarkdownEditor. -const NOISY_TILE_PREVIEW_PATTERNS: RegExp[] = [ +const NOISY_RENDERED_PATTERNS: RegExp[] = [ /<[a-zA-Z][^>]*>/, // raw HTML /\|[^|\n]*\|[^|\n]*\|/, // 3+ pipes on a single line (table-ish) ]; +// Patterns whose markers strip-markdown removes, but whose visual semantics +// (bullets, numbering, indentation, monospace) are part of the author's +// intent. Without them the tile shows the content as run-on prose, which +// silently strips meaning rather than displaying syntax. Checked against the +// raw source within the tile-visible window. The `{0,3}` allowance matches +// CommonMark's leading-space tolerance for these block constructs. +const NOISY_RAW_SOURCE_PATTERNS: RegExp[] = [ + /^[ \t]{0,3}[-*+][ \t]+\S/m, // bullet list item + /^[ \t]{0,3}\d+[.)][ \t]+\S/m, // ordered list item + /^[ \t]{0,3}>[ \t]?/m, // blockquote + /^[ \t]{0,3}```/m, // fenced code block +]; + const hasNoisyTilePreview = (markdown: string | undefined): boolean => { if (!markdown) return false; + const rawSource = markdown.slice(0, TILE_VISIBLE_SOURCE_CHARS); + if (NOISY_RAW_SOURCE_PATTERNS.some((pattern) => pattern.test(rawSource))) { + return true; + } const preview = getMarkdownSummary({ markdown: markdown.slice(0, MAX_SOURCE_CHARS_TO_SCAN), width: TILE_PREVIEW_WIDTH, height: TILE_PREVIEW_HEIGHT, }); - return NOISY_TILE_PREVIEW_PATTERNS.some((pattern) => pattern.test(preview)); + return NOISY_RENDERED_PATTERNS.some((pattern) => pattern.test(preview)); }; const createNotebookSchema = (t: ReturnType) => { From 3be035d80716e6e4bc4806c2f58b0a6421e609e5 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:20:02 +0000 Subject: [PATCH 3/3] chore: resolve conflicts with main in en.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pulls in i18n keys added on main since the branch diverged and keeps the new `feedTileSummaryRequiredForFormattedContent` key next to `feedTileSummaryPlaceholder`. `notebook_form.tsx` had no conflict — main is unchanged for that file. Co-authored-by: Sylvain <74110469+SylvainChevalier@users.noreply.github.com> --- front_end/messages/en.json | 165 ++++++++++++++++++++++++++++++++++++- 1 file changed, 162 insertions(+), 3 deletions(-) diff --git a/front_end/messages/en.json b/front_end/messages/en.json index a2e7a93115..3f51174576 100644 --- a/front_end/messages/en.json +++ b/front_end/messages/en.json @@ -41,6 +41,7 @@ "hostPrivateInstancesDescription": "Surface insights from within your organization", "questionFeed": "Question Feed", "feed": "Feed", + "newCommentsSortNote": "New Comments only shows questions you have viewed before and that have unread comments.", "collectiveForecastsForPublicGood": "Collective forecasts for the public good", "followImportantTopics": "Follow important topics", "followImportantTopicsDescription": "AI, Geopolitics, Biosecurity, and more", @@ -235,6 +236,7 @@ "metaculus_prediction": "Metaculus Prediction", "metaculus": "Metaculus", "community": "community", + "enterProbability": "Enter probability", "communityPrediction": "Community Prediction", "unweightedAggregate": "Unweighted Aggregate", "firstQuartile": "lower 25%", @@ -272,6 +274,8 @@ "commentsReadGuidelines": "Read our guidelines", "commentsLearnMarkdown": "Learn about our Markdown", "commentDeleted": "Comment deleted", + "deleteCommentConfirmTitle": "Delete comment?", + "deleteCommentConfirmDescription": "This comment will be removed. This can't be undone.", "commentsReportCommentHeading": "Report this comment", "commentsReportCommentDescription": "Is there something wrong with this comment? Please help us keep our community great by reporting it.", "commentsReportSpam": "Report as spam", @@ -327,6 +331,8 @@ "hideCommunityPrediction": "Hide Community Prediction", "viewMetaculusPrediction": "View Metaculus Prediction", "questionSearchPlaceholder": "search questions...", + "feedSearchResultsFor": "{count} results for \"{search}\"", + "feedSearchResultsForWithoutCount": "Results for \"{search}\"", "articlesSearchPlaceholder": "search articles...", "tagSearchPlaceholder": "search tags...", "search": "Search", @@ -351,6 +357,7 @@ "searchOptionActivePrediction": "Active Prediction", "searchOptionWithdrawnPrediction": "Withdrawn", "searchOptionNotPredicted": "Not Predicted", + "searchOptionCommented": "Commented", "searchOptionAuthored": "Authored", "searchOptionUpvoted": "Upvoted", "searchOptionPrivate": "Private", @@ -375,6 +382,7 @@ "estimatedReadingTime": "{minutes} min read", "predictions": "Predictions", "predictors": "Predictors", + "predictorsMentionWarning": "Only curators and admins can notify @predictors. Your mention will not send notifications.", "relativeLog": "Relative Log", "unreadAll": "all unread", "questionsLeft": "{count, plural, =1 {1 question left} other {{count} questions left} }", @@ -384,6 +392,7 @@ "unreadWithTotalCount": "({unread_count_formatted} unread) {total_count_formatted} total", "unreadWithTotalCountXs": "({unread_count_formatted}) {total_count_formatted}", "forecastersWithCount": "{count_formatted} {count, plural, =1 {forecaster} other {forecasters}}", + "histogramBinShare": "{share} weight of forecasts", "or": "or", "signupInviteUsers": "Invite Users", "signupInviteUsersDescription": "Enter the email addresses of users you would like to invite, separated by newlines. They will receive an invitation to sign up by email.", @@ -489,6 +498,7 @@ "reminderErrorPercentLifetimeRequired": "Please set a percent of lifetime", "reminderErrorPercentLifetimeCannotHappen": "This would result in no notification, please select a lower value", "reminderErrorPercentLifetimeWouldNotRepeat": "This would result in no repetition. Consider switching to \"Doesn't Repeat\"", + "reminderErrorDuplicateDate": "This date is already selected. Please choose a different date.", "reminderNotePlaceholder": "Write yourself a note...", "reminderCPChangesDescription1": "Notify me when the forecast changes by more than 10%", "reminderCPChangesDescription2": "For continuous questions, this is measured by a Kolmogorov–Smirnov test.", @@ -601,6 +611,7 @@ "toggleAllTopics": "Toggle all topics", "toggleNewsAnnotations": "Toggle news annotations", "Filter": "Filter", + "sort": "Sort", "done": "done", "clear": "clear", "predicted": "Predicted", @@ -778,6 +789,7 @@ "aboutMetaculusTitle": "About Metaculus", "aboutMetaculusDescription": "Metaculus is an online forecasting platform and aggregation engine working to improve human reasoning and coordination on topics of global importance.", "more": "More", + "nMore": "{count} more", "moreLikely": "percentage points higher", "afterElection": "under {winner}", "ifCandidateElected": "if {candidate} elected", @@ -845,6 +857,7 @@ "userProfile": "{username}'s profile", "unknownUserProfile": "User's profile", "userMedals": "{username}'s medals", + "userContributions": "{username}'s contributions", "unknownUserMedals": "User's medals", "noMedals": "No medals yet", "leaderboard": "Leaderboard", @@ -910,6 +923,7 @@ "parentResolutionCriteria": "Parent Resolution Criteria", "childResolutionCriteria": "Child Resolution Criteria", "loadMoreComments": "Load more comments", + "linkedComment": "Linked comment", "followers": "Followers", "followed": "Followed", "followButton": "Follow", @@ -994,7 +1008,7 @@ "settingsNewEmail": "New Email", "settingsEmailChangeDescription": "Enter a new email address and a password, and we'll send you a link to change your email.", "settingsChangeEmailAddress": "Change email address", - "settingsChangeEmailAddressSuccess": "A confirmation email has been sent to your new address. Please follow the link inside to activate it.", + "settingsChangeEmailAddressSuccess": "A confirmation email has been sent to your old address. Please follow the link inside to activate it.", "emailChangeErrorMessage": "Failed to change email", "emailChangeSuccessMessage": "Your email address has been changed successfully", "socialAccountNoPasswordBanner": "Your account doesn't have a password yet. Set one below to change your email or log in with email and password.", @@ -1093,6 +1107,7 @@ "choicesSeparatedBy": "Choices (separated by ,)", "choicesLockedHelp": "Options can only be changed through the admin panel once forecasting has started.", "projects": "projects", + "projectsTags": "Projects / Tags", "FABPrizePool": "PRIZE POOL", "FABPrizeValue": "$30,000", "FABStartDate": "START DATE", @@ -1210,6 +1225,7 @@ "deselectAll": "deselect all", "forecastDataIsEmpty": "Forecast data is empty", "noForecastsYet": "No forecasts yet", + "noHistogramData": "No histogram data available", "RevealTemporarily": "Reveal Temporarily", "CPIsHidden": "Community Prediction is hidden", "createdByUserOnDate": "Created by {user} on {date}", @@ -1309,6 +1325,9 @@ "youArePostingAPrivateComment": "You are posting a private comment", "unread": "unread", "contentTranslatedHeaderText": "Some content on this page is automatically translated, and may be inaccurate.", + "translatedBy": "translated by", + "listLayout": "List layout", + "gridLayout": "Grid layout", "showOriginalContent": "Show original", "translated_by": "translated by", "nextQuestion": "Next Question", @@ -1323,6 +1342,8 @@ "keyFactor": "Key Factor", "scrollToKeyFactor": "Scroll to key factor", "keyFactors": "Key Factors", + "keyFactorsFor": "Key factors for", + "goToKeyFactor": "Go to key factor {number}", "topKeyFactors": "Top Key Factors", "notAvailable": "Not available", "indexesTitle": "Project", @@ -1383,6 +1404,7 @@ "selectChildQuestion": "Select Child Question", "pickQuestion": "Pick Question", "viewQuestions": "View Questions({count})", + "includingSubquestions": "Including Subquestions: {count}", "predictionFlow": "Prediction Flow", "started": "Started", "starts": "Starts", @@ -1781,6 +1803,7 @@ "unknown": "Unknown", "lowest": "Lowest", "highest": "Highest", + "fanChart": "Fan Chart", "timeline": "Timeline", "inNews": "In the News", "info": "Question Info", @@ -1965,6 +1988,7 @@ "copyQuestionLinkSuccess": "Question link successfully created! You can see them under the Question Links section for each question.", "viewQuestionLinks": "View Question Links", "moreActions": "More actions", + "openOnMetaculus": "Open on Metaculus", "copyQuestionLinkImpactPrefix": "has a", "copyQuestionLinkImpactSuffix": "causal impact on:", "copyQuestionLinkStrengthLabel": "The strength of this impact is:", @@ -1980,6 +2004,7 @@ "loadMore": "Load More", "noPrivateNotes": "No private notes yet", "privateNotes": "Private Notes", + "questionLinks": "Question Links", "justNow": "just now", "cmmButtonShort": "Mind", "myForecastingBots": "My Forecasting Bots", @@ -2026,6 +2051,7 @@ "tournamentTimelineStarts": "Starts {when}", "tournamentTimelineEnds": "Ends {when}", "tournamentTimelineClosed": "Pending resolutions", + "tournamentTimelinePendingWinners": "Pending winners announcement", "tournamentTimelineAllResolved": "All questions resolved", "tournamentRelativeSoon": "soon", "tournamentRelativeUnderMinute": "in under a minute", @@ -2084,7 +2110,7 @@ "readFullReport": "Read full report", "feedTileNewTournament": "New Tournament", "feedTileForecastNewQuestions": "Forecast {count, plural, one {# new question} other {# new questions}}", - "feedTileQuestionsResolved": "{count, plural, one {# question resolved} other {# questions resolved}}", + "feedTileQuestionsRecentlyResolved": "{count, plural, one {# question recently resolved} other {# questions recently resolved}}", "feedTileCheckLeaderboards": "Check out the final leaderboards", "feedTilePopularTournament": "Popular Tournament", "feedTileClosesIn": "Closes in {when}", @@ -2198,5 +2224,138 @@ "whyTrustMetaculusLessNoise": "Metaculus's crowd-powered forecasts cut through the noise by grounding every prediction in transparent evidence, accountable scoring, and a decade of demonstrated accuracy. Metaculus equips policymakers, researchers, journalists, and corporate organizations with evidence-based forecasts that surface clear, quantifiable insight into the world's most critical uncertainties. Explore our suite of Business Solutions to learn more about how Metaculus can improve your organization's decision-making.", "publishTimeLockedDescription": "Publish time cannot be changed after creation.", "feedTileSummaryPlaceholder": "Optional: Enter a custom summary text to display on feed tiles (if not provided, a summary will be auto-generated from the notebook content)", - "feedTileSummaryRequiredForFormattedContent": "Feed tile summary is required because the visible start of your notebook contains formatting (a list, blockquote, code block, table, or raw HTML) that wouldn't render cleanly on a feed tile. Add a short plain-text summary so readers see a clean preview." + "feedTileSummaryRequiredForFormattedContent": "Feed tile summary is required because the visible start of your notebook contains formatting (a list, blockquote, code block, table, or raw HTML) that wouldn't render cleanly on a feed tile. Add a short plain-text summary so readers see a clean preview.", + "laborHubJobsBreadcrumb": "All Jobs", + "laborHubJobsVisitCta": "Visit Jobs", + "laborHubJobsPageTitle": "All Jobs · Labor Automation Forecasting Hub | Metaculus", + "laborHubJobsPageDescription": "Browse 15 US occupations forecasted by the Metaculus community through 2035. See how AI may reshape each one.", + "laborHubJobsHeroTitle": "Which jobs will survive AI?", + "laborHubJobsHeroLead": "Metaculus forecasters track 15 representative US occupations through 2035. Pick a job to see the forecast trajectory, the why behind the numbers, and what comes next.", + "laborHubJobDetailTitle": "Will AI replace {name}? · Metaculus Labor Hub forecast", + "laborHubJobDetailDescription": "Community forecasts for {name} employment, wages and AI exposure through 2035. Live from the Metaculus Labor Automation Forecasting Hub.", + "laborHubJobDetailSubtitle": "Community forecast of employment change versus 2025, across 2027, 2030, and 2035.", + "laborHubJobsClickTileSubtitle": "Click any tile for the full forecast & forecaster commentary", + "laborHubJobsYearToggleLabel": "Forecast year", + "laborHubJobsExploreCta": "Explore the full Labor Hub Dashboard", + "laborHubJobsExploreCtaSub": "Occupations and wages · economy-wide forecasts · forecaster commentary", + "laborHubJobsByYear": "By {year}", + "laborHubJobsJumpToLabel": "Jump to:", + "laborHubJobsFeltenLabel": "Felten et al. exposure", + "laborHubJobsFeltenTooltip": "Academic measure of how much a job's required abilities overlap with current AI capabilities.", + "laborHubJobsMnaLabel": "M&A vulnerability", + "laborHubJobsMnaTooltip": "Academic measure combining a job's AI exposure with how well its workers can adapt.", + "laborHubJobsAoeLabel": "Anthropic Exposure", + "laborHubJobsAoeTooltip": "Grounded in real, anonymized Claude usage — the share of this job's tasks already being done with AI.", + "laborHubJobsRingHigh": "HIGH", + "laborHubJobsRingMed": "MED", + "laborHubJobsRingLow": "LOW", + "laborHubJobsCuratedInsightsTitle": "Curated insights", + "laborHubJobsCuratedInsightsEmpty": "No insights surfaced for {name} yet — check the full Labor Hub for active discussion.", + "laborHubJobsInsightSourceFromAnotherPost": "from a related question", + "laborHubJobsWagesTitle": "Median wage by 2035", + "laborHubJobsWagesSub": "Real change vs 2025", + "laborHubJobsWagesUnavailable": "Per-occupation wage forecast not yet tracked", + "laborHubJobsHoursTitle": "Hours / week · economy-wide", + "laborHubJobsHoursEconomyWideSub": "By 2035, vs {from} hrs/week in 2025", + "laborHubJobsShareTitle": "Share this forecast", + "laborHubJobsShareLead": "Download a share card and share this forecast with your network.", + "laborHubJobsShareYearLabel": "Headline year", + "laborHubJobsShareYearOption": "By {year}", + "laborHubJobsShareSave": "Save Image", + "laborHubJobsShareTweet": "Share on X", + "laborHubJobsShareLinkedin": "Share on LinkedIn", + "laborHubJobsMetricSeeAll": "Click to compare this to other jobs →", + "laborHubJobsMetricPaperLink": "Read the paper →", + "laborHubJobsProForecasterTitle": "Pro Forecaster", + "laborHubJobsMetricChartHead": "How compares", + "laborHubJobsMetricLower": "lower", + "laborHubJobsMetricHigher": "higher", + "laborHubJobsFeltenSource": "Felten, Raj & Seamans — Language modeling AIOE", + "laborHubJobsFeltenAxisLabel": "Felten exposure score (standardized)", + "laborHubJobsFeltenNature1": "The AI Occupational Exposure (AIOE) measures the overlap between AI capabilities and the abilities an occupation requires, aggregated from O*NET ability data.", + "laborHubJobsFeltenNature3": "Higher exposure does not by itself mean a job will be automated — it can equally signal that AI augments the work.", + "laborHubJobsFeltenBounds": "Scale: standardized z-score · the average occupation sits near 0; positive values are more exposed than average, negative values less.", + "laborHubJobsMnaSource": "Manning & Aguirre — Vulnerability score", + "laborHubJobsMnaAxisLabel": "Manning & Aguirre vulnerability (0–1)", + "laborHubJobsMnaNature1": "Academic measure combining exposure to AI with workers' adaptive capacity.", + "laborHubJobsMnaNature3": "Vulnerability scores are related both to the likelihood of displacement (through AI exposure) and its burden (through adaptive capacity).", + "laborHubJobsMnaBounds": "Scale: 0 to 1 · highest in occupations that are both heavily exposed and their workers are poorly cushioned against displacement.", + "laborHubJobsAoeSource": "Massenkoff & McCrory — Observed Exposure (AOE)", + "laborHubJobsAoeAxisLabel": "Observed AI exposure (0–100%)", + "laborHubJobsAoeNature1": "Unlike purely theoretical measures, this is also grounded in real, anonymized Claude usage: what people are actually doing with AI right now, mapped to O*NET occupation tasks.", + "laborHubJobsAoeNature3": "AOE captures observed adoption, not potential — a low value may mean the work simply isn't being brought to Claude yet, not that AI couldn't do it.", + "laborHubJobsAoeBounds": "Scale: 0% to 100% · highest in occupations whose tasks AI can theoretically do and is already being used for.", + "laborHubJobsChartA11y": "Employment forecast by year", + "laborHubJobsChartBaseline": "Baseline", + "midtermsHubMetaTitle": "2026 US Midterm Elections | Metaculus", + "midtermsHubMetaDescription": "Real-time forecasts from the Metaculus community on the 2026 US midterm elections.", + "midtermsHubChamberSenate": "Senate", + "midtermsHubChamberHouse": "House", + "midtermsHubChamberGovernor": "Governor", + "midtermsHubChamberControl": "Chamber control", + "midtermsHubCongressForecast": "Congressional Control", + "midtermsHubChamberCurrent": "Current:", + "midtermsHubChamberForecast": "Forecast:", + "midtermsHubChamberTooltipBody": "Forecasters give {party} a {pct}% chance of holding the most {chamber} seats.", + "midtermsHubChamberTooltipDisclaimer": "Click to view forecasting question", + "midtermsHubPartyDemocrats": "Democrats", + "midtermsHubPartyRepublicans": "Republicans", + "midtermsHubOutcomeRepRep": "Rep House / Rep Senate", + "midtermsHubOutcomeRepDem": "Dem House / Rep Senate", + "midtermsHubOutcomeDemRep": "Rep House / Dem Senate", + "midtermsHubOutcomeDemDem": "Dem House / Dem Senate", + "midtermsHubCongressSummary": "Forecasters' current views on Congress control are reflected in the probabilities above.", + "midtermsHubThingsToWatch": "Key Drivers", + "midtermsHubAbortionAmendment": "Missouri abortion-rights repeal", + "midtermsHubAbortionAmendmentContext": "Missouri voters decide whether to repeal the abortion-rights protections they enshrined in 2024.", + "midtermsHubElectoralConsequences": "Electoral Consequences", + "midtermsHubConsequenceQuestion": "Question", + "midtermsHubConsequenceIfRep": "if Rep Congress", + "midtermsHubConsequenceIfDem": "if Dem Congress", + "midtermsHubConsequenceHeaderRepTitle": "Republican Congress", + "midtermsHubConsequenceHeaderRepSubtitle": "If GOP controls", + "midtermsHubConsequenceHeaderSplitTitle": "Split Congress", + "midtermsHubConsequenceHeaderSplitSubtitle": "If control is split", + "midtermsHubConsequenceIfSplit": "if Split Congress", + "midtermsHubMailInBallots": "Mail-in ballot court challenge", + "midtermsHubMailInBallotsContext": "New USPS restrictions could reshape mail-in voting access, but pending lawsuits may block them before November.", + "midtermsHubElectionEmergency": "Election-integrity national emergency", + "midtermsHubElectionEmergencyContext": "A national emergency declaration could expand federal authority over the election.", + "midtermsHubConsequenceHeaderDemTitle": "Democratic Congress", + "midtermsHubConsequenceHeaderDemSubtitle": "If Dems control", + "midtermsHubConsequenceClimate": "Will major federal climate legislation be passed before 2028?", + "midtermsHubConsequenceMinWage": "Will the federal minimum wage be raised before 2028?", + "midtermsHubConsequenceImmigration": "Will a major immigration reform bill pass before 2028?", + "midtermsHubConsequenceShutdown": "Will there be a government shutdown lasting more than 14 days before 2028?", + "midtermsHubCommunityInsights": "Community Insights", + "midtermsHubScrollLeft": "Scroll insights left", + "midtermsHubScrollRight": "Scroll insights right", + "midtermsHubDemocrat": "Democrat", + "midtermsHubRepublican": "Republican", + "midtermsHubNotContested": "Not contested", + "midtermsHubDemPct": "{pct}% Democrat", + "midtermsHubRepPct": "{pct}% Republican", + "midtermsHubNoForecast": "No forecast", + "midtermsHubFooterDisclaimer": "Not affiliated with any political party.", + "midtermsHubHeroTitleLine1": "2026 US", + "midtermsHubHeroTitleLine2": "Midterm Elections", + "midtermsHubHeroSubtitle": "Forecasts from the Metaculus community on contested races and the policies they will shape.", + "midtermsHubHeroSubtitleMobile": "Real-time forecasts from the Metaculus community.", + "midtermsHubConsequencesSubtitle": "How forecasted outcomes shift depending on which party holds Congress.", + "midtermsHubUpdatedRealtime": "Updated in real time", + "midtermsHubMetaculusUser": "Metaculus User", + "midtermsHubComingSoon": "Coming Soon", + "midtermsHubClickToView": "Click to view question", + "midtermsHubSeatDistributionsTitle": "Seat Distributions", + "midtermsHubSeatDistributionsSubtitle": "The community's forecasts for the final seat advantage in each chamber.", + "midtermsHubSenateSeats": "Senate", + "midtermsHubHouseSeats": "House", + "midtermsHubDemSeatAdvantage": "Democratic Seat Advantage", + "midtermsHubRepSeatAdvantage": "Republican Seat Advantage", + "midtermsHubEven": "EVEN", + "midtermsHubForecastUnavailable": "Forecast unavailable", + "midtermsHubSeatAdvantageTooltip": "{count} seat advantage", + "midtermsHubSeatAdvantageOverTooltip": ">{count} seat advantage", + "midtermsHubProbabilityTooltip": "{value}% probability", + "midtermsHubEvenTooltip": "Even" }