Skip to content

Next.js 16.2 Upgrade#4563

Open
ncarazon wants to merge 1 commit intomainfrom
feat/nextjs-16-upgrade
Open

Next.js 16.2 Upgrade#4563
ncarazon wants to merge 1 commit intomainfrom
feat/nextjs-16-upgrade

Conversation

@ncarazon
Copy link
Copy Markdown
Contributor

@ncarazon ncarazon commented Mar 31, 2026

Closes #4547

This PR upgrades the frontend from Next.js 15 to 16.2 with full Turbopack adoption, along with
companion upgrades for React, Sentry, next-intl, ESLint, and Storybook.

What Changed and Why

Turbopack Migration (next.config.mjs)

I removed entire webpack callback (SVG handling + buildId injection + eslint config).
Addedturbopack.rules with condition-based SVG handling:

  • for *.svg?url imports – type: "asset" (returns URL string)
  • for *.svg imports – @svgr/webpack loader (returns React component)

This uses Turbopack 16.2's new condition.query feature to replicate the webpack resourceQuery behavior.

Replaced webpack.DefinePlugin with generateBuildId() + env.BUILD_ID. A UUID is generated at config evaluation time, used both as Next.js build ID and injected as an environment variable for the version checker.

Removed eslint: { ignoreDuringBuilds: true } – this config option was removed in Next.js 16 (along with the next lint command).


Middleware to Proxy (middleware.ts to proxy.ts)

Next.js 16 renames the middleware convention to "proxy" to clarify its role as a network boundary layer. The file was renamed and the exported function changed from middleware to proxy. Behavior is identical.


ESLint Flat Config (.eslintrc.json to eslint.config.mjs)

  • next lint was removed in Next.js 16 – lint:js script now calls eslint . directly
  • eslint-config-next v16 exports native flat config arrays – imported directly instead of through FlatCompat (which caused circular reference errors with the React plugin)
  • FlatCompat is still used for prettier and storybook plugins that don't yet have native flat config exports
  • New React Compiler lint rules (react-hooks/set-state-in-effect, react-hooks/purity, etc.) are set to warn for incremental adoption

Sentry v10 (@sentry/nextjs)

Required upgrade because Sentry v9's withSentryConfig injected experimental.turbo (the old config key), causing Next.js 16 to show "unrecognized key" warnings and breaking the next-intl plugin.


next-intl v4

Required upgrade because next-intl v3's plugin checked process.env.TURBOPACK and injected experimental.turbo.resolveAlias – the deprecated config key that Next.js 16 doesn't understand. This caused the "Couldn't find next-intl config file" build error.


Performance

Metric Next.js 15 (webpack) Next.js 16.2 (Turbopack)
next dev startup ~1.5-2s ~250ms
next build compile ~30-40s ~12s
Bundler webpack Turbopack (stable)

Summary by CodeRabbit

Release Notes

  • New Features

    • Updated development environment with Next.js 16.2.0, React 19.2.0, and enhanced build tooling.
  • Bug Fixes

    • Fixed undefined value handling in medal rankings, numeric inputs, forecast predictions, and various UI components to ensure consistent display behavior.
    • Improved null-safety in translation features across multiple components.
  • Chores

    • Migrated ESLint configuration to modern flat config format.
    • Upgraded dependencies including next-intl, Storybook, and related tooling.
    • Updated development scripts for streamlined build processes.
    • Enhanced TypeScript configuration for improved type checking.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 31, 2026

Warning

Rate limit exceeded

@ncarazon has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 0 minutes and 31 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 0 minutes and 31 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b69188c4-e949-4468-9bda-925deb5f3067

📥 Commits

Reviewing files that changed from the base of the PR and between 48eb0f0 and 0da490b.

⛔ Files ignored due to path filters (1)
  • front_end/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (34)
  • front_end/.eslintignore
  • front_end/.eslintrc.json
  • front_end/eslint.config.mjs
  • front_end/next.config.mjs
  • front_end/package.json
  • front_end/src/app/(futureeval)/futureeval/components/benchmark/performance-over-time/collision-aware-labels.tsx
  • front_end/src/app/(main)/(leaderboards)/medals/components/medal_card.tsx
  • front_end/src/app/(main)/accounts/profile/[id]/medals/page.tsx
  • front_end/src/app/(main)/aggregation-explorer/components/question_metadata.tsx
  • front_end/src/app/(main)/questions/[id]/components/sidebar/sidebar_question_info.tsx
  • front_end/src/app/(main)/questions/actions.ts
  • front_end/src/app/(main)/questions/components/coherence_links/link_strength_component.tsx
  • front_end/src/app/(main)/questions/components/numeric_question_input.tsx
  • front_end/src/app/(main)/services/components/case_studies/case_study_card.tsx
  • front_end/src/components/forecast_maker/conditional_forecast_table.tsx
  • front_end/src/components/forecast_maker/continuous_group_accordion/accordion_open_button.tsx
  • front_end/src/components/forecast_maker/continuous_group_accordion/group_forecast_accordion_item.tsx
  • front_end/src/components/forecast_maker/forecast_expiration.tsx
  • front_end/src/components/forecast_maker/forecast_text_input.tsx
  • front_end/src/components/onboarding/steps/step_1.tsx
  • front_end/src/components/onboarding/steps/step_2.tsx
  • front_end/src/components/popover_filter/combobox_filter.tsx
  • front_end/src/components/search_input.tsx
  • front_end/src/components/ui/button.tsx
  • front_end/src/components/ui/chip.tsx
  • front_end/src/components/ui/info_toggle_container.tsx
  • front_end/src/components/ui/inline_select.tsx
  • front_end/src/components/ui/listbox.tsx
  • front_end/src/components/ui/select.tsx
  • front_end/src/components/ui/switch.tsx
  • front_end/src/proxy.ts
  • front_end/src/sentry/options.ts
  • front_end/src/types/translations.ts
  • front_end/tsconfig.json
📝 Walkthrough

Walkthrough

This PR upgrades Next.js from 15.5.14 to 16.2.0, migrating ESLint configuration to flat config format, updating TypeScript compilation settings, adding "use client" directives across components, removing webpack customizations, and applying defensive null/undefined handling throughout the codebase.

Changes

Cohort / File(s) Summary
ESLint Configuration Migration
front_end/.eslintignore, front_end/.eslintrc.json, front_end/eslint.config.mjs
Removed legacy .eslintignore entries and entire .eslintrc.json config; introduced new flat config file eslint.config.mjs with Next.js, Prettier, and Storybook presets, custom rule overrides, and ignore patterns.
Next.js & Build Configuration
front_end/next.config.mjs, front_end/package.json, front_end/tsconfig.json
Upgraded dependencies (Next.js 15.5.14→16.2.0, next-intl 3.26.4→4.8.3, React 19.0.0→19.2.0); removed custom webpack override and eslint.ignoreDuringBuilds; added turbopack SVG rules; changed lint script to eslint .; updated JSX runtime from "preserve" to "react-jsx" and added Next.js dev type includes.
Client Component Directives
front_end/src/components/forecast_maker/continuous_group_accordion/*, front_end/src/components/forecast_maker/forecast_*.tsx, front_end/src/components/ui/*, front_end/src/components/search_input.tsx, front_end/src/components/popover_filter/combobox_filter.tsx
Added "use client" directive to 14 component files for explicit client-side rendering in React Server Components context.
Null/Undefined Handling
front_end/src/app/(futureeval)/futureeval/components/benchmark/.../collision-aware-labels.tsx, front_end/src/app/(main)/(leaderboards)/medals/components/medal_card.tsx, front_end/src/app/(main)/accounts/profile/[id]/medals/page.tsx, front_end/src/app/(main)/questions/components/numeric_question_input.tsx, front_end/src/app/(main)/services/components/case_studies/case_study_card.tsx, front_end/src/components/forecast_maker/conditional_forecast_table.tsx, front_end/src/components/forecast_maker/forecast_expiration.tsx, front_end/src/components/markdown_editor/__tests__/initialized_editor.test.tsx, front_end/src/components/onboarding/steps/step_*.tsx
Applied nullish coalescing operators (??) to prevent undefined/null values from being passed to translation functions and render methods; updated type narrowing with explicit ternary for coauthor counts.
Type System & Exports
front_end/src/app/(main)/questions/[id]/components/sidebar/sidebar_question_info.tsx, front_end/src/app/(main)/aggregation-explorer/components/question_metadata.tsx, front_end/src/app/(main)/questions/components/coherence_links/link_strength_component.tsx, front_end/src/types/translations.ts, front_end/src/proxy.ts, front_end/src/sentry/options.ts
Relaxed translation key type constraints from keyof IntlMessages to string; renamed middleware function to proxy; removed VercelEdgeOptions from Sentry generic constraint; updated nextIntl provider initialization.
Cache & Query Invalidation
front_end/src/app/(main)/questions/actions.ts
Updated revalidateTag call to include "max" parameter for related-articles cache revalidation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Key Factors Redesign 1st Iteration #4414: Both PRs modify the Key Factors front-end question components with overlapping changes to types, messages, and utilities in front_end/src/app/(main)/questions/....
  • Services page #4104: Both PRs update case-studies UI components and translations, particularly the publishedWithLabel fallback handling in case study cards.
  • Redesigned Aggregation Explorer #4398: Main PR's type change in question_metadata.tsx overlaps with the Aggregation Explorer refactor that modifies the same file.

Suggested reviewers

  • hlbmtc
  • elisescu
  • cemreinanc

Poem

🐰 Next.js leaps to sixteen with a bound,
ESLint's config flat upon the ground,
"use client" whispers spread their cheer,
Nullish coalescing—no undefined fear!
From turbopack to TypeScript's jsx,
This upgrade's done—next hurdle's next! 🚀

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Next.js 16.2 Upgrade' clearly and concisely summarizes the main objective of the changeset: upgrading Next.js to version 16.2.
Linked Issues check ✅ Passed All objectives from issue #4547 are met: Next.js upgraded from 15.5.12 to 16.2, breaking changes resolved (middleware→proxy, ESLint flat config migration, Turbopack adoption), related tooling updated (React, Sentry, next-intl), and configuration changes implemented for asset/SVG handling and performance improvements.
Out of Scope Changes check ✅ Passed All changes are directly related to the Next.js 16.2 upgrade and resolving compatibility issues: config migrations, dependency updates, client component directives, nullish coalescing in translations, and type constraint relaxations are all necessary for the upgrade and related tooling compatibility.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/nextjs-16-upgrade

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 31, 2026

🚀 Preview Environment

Your preview environment is ready!

Resource Details
🌐 Preview URL https://metaculus-pr-4563-feat-nextjs-16-upgrade-preview.mtcl.cc
📦 Docker Image ghcr.io/metaculus/metaculus:feat-nextjs-16-upgrade-0da490b
🗄️ PostgreSQL NeonDB branch preview/pr-4563-feat-nextjs-16-upgrade
Redis Fly Redis mtc-redis-pr-4563-feat-nextjs-16-upgrade

Details

  • Commit: d70a6084fa3bf1631587120af94874faa6b47a07
  • Branch: feat/nextjs-16-upgrade
  • Fly App: metaculus-pr-4563-feat-nextjs-16-upgrade

ℹ️ Preview Environment Info

Isolation:

  • PostgreSQL and Redis are fully isolated from production
  • Each PR gets its own database branch and Redis instance
  • Changes pushed to this PR will trigger a new deployment

Limitations:

  • Background workers and cron jobs are not deployed in preview environments
  • If you need to test background jobs, use Heroku staging environments

Cleanup:

  • This preview will be automatically destroyed when the PR is closed

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
front_end/src/app/(main)/questions/actions.ts (1)

309-312: Replace revalidateTag with updateTag for immediate consistency after deletion.

After removeRelatedArticle(articleId) completes, the cached related-articles data should be immediately fresh, not stale-while-revalidate. While the component uses local state to provide instant UI feedback, using updateTag("related-articles") ensures backend consistency across all views and edge cases (multi-tab refresh, navigation, other components).

Change:

revalidateTag("related-articles", "max");

to:

updateTag("related-articles");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@front_end/src/app/`(main)/questions/actions.ts around lines 309 - 312, The
code currently calls revalidateTag("related-articles", "max") after awaiting
ServerPostsApi.removeRelatedArticle(articleId) in removeRelatedArticle; replace
that call with updateTag("related-articles") so the cache for the
"related-articles" tag is updated immediately across clients. Locate the
removeRelatedArticle function and change the revalidateTag invocation to
updateTag("related-articles") to ensure immediate consistency after
ServerPostsApi.removeRelatedArticle completes.
front_end/src/app/(main)/questions/components/numeric_question_input.tsx (1)

172-187: Minor inconsistency in null-handling pattern.

The Date branch still uses (min ? min : 0) / (max ? max : 0) (lines 179, 183) while the Numeric branch now uses ?? 0. For Date questions this is fine since timestamps are always > 0, but consider aligning to ?? 0 for consistency with the rest of the PR.

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

In `@front_end/src/app/`(main)/questions/components/numeric_question_input.tsx
around lines 172 - 187, The null-handling in the Date formatting inside
current_errors.push uses (min ? min : 0) and (max ? max : 0); change those to
use the nullish coalescing operator (min ?? 0) and (max ?? 0) to match the
Numeric branch and existing PR style. Locate the call to
t.rich("zeroPointError1", { zeroPoint: format(new Date(zeroPoint * 1000), ...),
min: format(new Date((min ? min : 0) * 1000), ...), max: format(new Date((max ?
max : 0) * 1000), ...) }) and replace the ternaries with ?? 0 for min and max so
date formatting uses the same null-handling pattern.
front_end/next.config.mjs (1)

31-45: Limit the SVG transform to non-foreign imports.

These conditions currently rewrite every *.svg import, including node_modules and some Next internals. Turbopack documents foreign for scoping loaders away from those modules; without it, a dependency import can be transformed into your app’s SVGR/asset behavior unexpectedly. I’d also match the ?url flag exactly instead of any query that happens to contain url. (nextjs.org)

Safer Turbopack rule shape
   turbopack: {
     rules: {
       "*.svg": [
         {
-          condition: { query: /\burl\b/ },
+          condition: {
+            all: [{ not: "foreign" }, { query: /[?&]url(?=&|$)/ }],
+          },
           type: "asset",
         },
         {
-          condition: { not: { query: /\burl\b/ } },
+          condition: {
+            all: [{ not: "foreign" }, { not: { query: /[?&]url(?=&|$)/ } }],
+          },
           loaders: ["@svgr/webpack"],
           as: "*.js",
         },
       ],
     },
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@front_end/next.config.mjs` around lines 31 - 45, Update the "*.svg" turbopack
rule so it doesn't apply to foreign (external/dependency/Next-internal) imports
and only treats exact ?url queries as URL imports: add a condition excluding
foreign resources (e.g., wrap each branch with a condition like { not: {
resource: { origin: "foreign" } } } or equivalent turbopack "foreign" scoping)
and replace the loose /\burl\b/ query tests with an exact match for the ?url
flag (e.g., /\?url$/) in the two condition entries inside the
turbopack.rules["*.svg"] rule so the asset/loader branches only target your app
sources and exact ?url imports.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@front_end/next.config.mjs`:
- Line 1: The current generateBuildId uses crypto.randomUUID() causing a new
build ID on each build; change generateBuildId to return a stable identifier
sourced from CI/GIT (e.g., process.env.BUILD_ID, GIT_COMMIT_SHA, or a CI
deployment id) and fall back to that order rather than generating a UUID; also
set the Next.js deploymentId to the same stable value so the runtime skew
protection sees identical IDs; update any code that relies on
process.env.BUILD_ID to use that env var (or the git/CI fallback) and ensure
generateBuildId and deploymentId consistently return the same stable identifier
across replicas.

In
`@front_end/src/app/`(main)/questions/[id]/components/sidebar/sidebar_question_info.tsx:
- Around line 27-30: The pluralization logic is inverted: change the count
passed to the i18n call in sidebar_question_info.tsx so it reflects the actual
total number of authors rather than presence of coauthors; replace the current
count expression using postData.coauthors.length > 0 with a total authors value
(e.g., 1 + postData.coauthors.length) when calling t("authorWithCount", { count:
... }) so the ICU pattern in t() receives 1 for single author and 2+ for
multiple authors.

In `@front_end/src/components/search_input.tsx`:
- Line 1: SearchInput was marked a Client Component which creates an RSC
boundary that breaks because the Server Component question_picker is passing
inline function props (onChange, onErase) across the boundary; either convert
question_picker to a Client Component so it owns the interactive state and can
pass handlers directly to SearchInput (add "use client" and move the
onChange/onErase state/handlers into the question_picker component), or keep
question_picker as a Server Component and add a small Client wrapper (e.g.,
QuestionPickerClient or SearchInputWrapper) that lives inside question_picker,
owns the search state and implements/onChange and onErase handlers, and then
forwards those handlers as props to SearchInput so no server-to-client function
props are passed.

In `@front_end/src/types/translations.ts`:
- Line 1: The change making TranslationKey an unconstrained string removes
compile-time i18n key checking; either document this decision and add
runtime/test safeguards or restore stricter typing via next-intl AppConfig
augmentation. Update the repo by (1) adding a short comment in front of the
TranslationKey type explaining the intentional choice to keep it as string
(reference: TranslationKey) and why dynamic key patterns in files like
dataset.ts and additional_scores_table.tsx require it, (2) if leaving it as
string, add runtime validation or unit/integration tests to assert expected keys
exist before rendering (or point to the test file/location), and (3) if you
prefer compile-time safety, reintroduce the AppConfig augmentation approach in
the i18n types to narrow TranslationKey. Ensure the chosen approach is
documented in-code and covered by tests or augmentation to prevent missing
translation keys at runtime.

---

Nitpick comments:
In `@front_end/next.config.mjs`:
- Around line 31-45: Update the "*.svg" turbopack rule so it doesn't apply to
foreign (external/dependency/Next-internal) imports and only treats exact ?url
queries as URL imports: add a condition excluding foreign resources (e.g., wrap
each branch with a condition like { not: { resource: { origin: "foreign" } } }
or equivalent turbopack "foreign" scoping) and replace the loose /\burl\b/ query
tests with an exact match for the ?url flag (e.g., /\?url$/) in the two
condition entries inside the turbopack.rules["*.svg"] rule so the asset/loader
branches only target your app sources and exact ?url imports.

In `@front_end/src/app/`(main)/questions/actions.ts:
- Around line 309-312: The code currently calls
revalidateTag("related-articles", "max") after awaiting
ServerPostsApi.removeRelatedArticle(articleId) in removeRelatedArticle; replace
that call with updateTag("related-articles") so the cache for the
"related-articles" tag is updated immediately across clients. Locate the
removeRelatedArticle function and change the revalidateTag invocation to
updateTag("related-articles") to ensure immediate consistency after
ServerPostsApi.removeRelatedArticle completes.

In `@front_end/src/app/`(main)/questions/components/numeric_question_input.tsx:
- Around line 172-187: The null-handling in the Date formatting inside
current_errors.push uses (min ? min : 0) and (max ? max : 0); change those to
use the nullish coalescing operator (min ?? 0) and (max ?? 0) to match the
Numeric branch and existing PR style. Locate the call to
t.rich("zeroPointError1", { zeroPoint: format(new Date(zeroPoint * 1000), ...),
min: format(new Date((min ? min : 0) * 1000), ...), max: format(new Date((max ?
max : 0) * 1000), ...) }) and replace the ternaries with ?? 0 for min and max so
date formatting uses the same null-handling pattern.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 03625de1-b768-48d5-a735-1518960e4a87

📥 Commits

Reviewing files that changed from the base of the PR and between 12b0564 and 48eb0f0.

⛔ Files ignored due to path filters (1)
  • front_end/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (35)
  • front_end/.eslintignore
  • front_end/.eslintrc.json
  • front_end/eslint.config.mjs
  • front_end/next.config.mjs
  • front_end/package.json
  • front_end/src/app/(futureeval)/futureeval/components/benchmark/performance-over-time/collision-aware-labels.tsx
  • front_end/src/app/(main)/(leaderboards)/medals/components/medal_card.tsx
  • front_end/src/app/(main)/accounts/profile/[id]/medals/page.tsx
  • front_end/src/app/(main)/aggregation-explorer/components/question_metadata.tsx
  • front_end/src/app/(main)/questions/[id]/components/sidebar/sidebar_question_info.tsx
  • front_end/src/app/(main)/questions/actions.ts
  • front_end/src/app/(main)/questions/components/coherence_links/link_strength_component.tsx
  • front_end/src/app/(main)/questions/components/numeric_question_input.tsx
  • front_end/src/app/(main)/services/components/case_studies/case_study_card.tsx
  • front_end/src/components/forecast_maker/conditional_forecast_table.tsx
  • front_end/src/components/forecast_maker/continuous_group_accordion/accordion_open_button.tsx
  • front_end/src/components/forecast_maker/continuous_group_accordion/group_forecast_accordion_item.tsx
  • front_end/src/components/forecast_maker/forecast_expiration.tsx
  • front_end/src/components/forecast_maker/forecast_text_input.tsx
  • front_end/src/components/markdown_editor/__tests__/initialized_editor.test.tsx
  • front_end/src/components/onboarding/steps/step_1.tsx
  • front_end/src/components/onboarding/steps/step_2.tsx
  • front_end/src/components/popover_filter/combobox_filter.tsx
  • front_end/src/components/search_input.tsx
  • front_end/src/components/ui/button.tsx
  • front_end/src/components/ui/chip.tsx
  • front_end/src/components/ui/info_toggle_container.tsx
  • front_end/src/components/ui/inline_select.tsx
  • front_end/src/components/ui/listbox.tsx
  • front_end/src/components/ui/select.tsx
  • front_end/src/components/ui/switch.tsx
  • front_end/src/proxy.ts
  • front_end/src/sentry/options.ts
  • front_end/src/types/translations.ts
  • front_end/tsconfig.json
💤 Files with no reviewable changes (2)
  • front_end/.eslintignore
  • front_end/.eslintrc.json

Copy link
Copy Markdown
Contributor

@cemreinanc cemreinanc left a comment

Choose a reason for hiding this comment

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

spotted 2 regressions and commented about them.

also we need to test about reverting this change: #2369 and discussion about it on vercel/next.js#82651

Maybe its solved in upstream turbopack but I cant find a solid result about it so we need to explore.

],
},
},
images: {
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.

this is from migration doc:

If you specify a quality prop not included in the image.qualities array, the quality will be coerced to the closest value in images.qualities.

so many quality 100 images in our codebase will be rendered at 75.
add qualities: [75, 100] to prevent not configured quality errors or remove quality=100 configs from these images if that's not necessary.

import { MessageKeys } from "next-intl";

export type TranslationKey = MessageKeys<IntlMessages, keyof IntlMessages>;
export type TranslationKey = string;
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.

TranslationKey type regression — Type safety silently broken

Root cause

next-intl v4 changed its type augmentation mechanism. The project is still using the v3 pattern which v4 no longer reads:

v3 (current in global.d.ts):

declare global {
  interface IntlMessages extends Messages {}  // v4 ignores this
}

v4 (what the library now expects):

declare module 'next-intl' {
  interface AppConfig {
    Messages: typeof en;
  }
}

In v4, AppConfig in use-intl/dist/types/core/AppConfig.d.ts resolves Messages like this:

export type Messages = AppConfig extends {
    Messages: infer AppMessages;
} ? AppMessages : Record<string, any>;  // <-- falls back to untyped

Since AppConfig is never augmented in the project, all translation keys fall back to Record<string, any> — meaning t("anyTypo") compiles without error. The TranslationKey = string change was likely a consequence of the old MessageKeys<IntlMessages, ...> silently breaking.

Impact

  • useTranslations()t() accepts any string with no autocomplete or compile-time validation
  • TranslationKey is string — no typo detection
  • keyof IntlMessages references in question_metadata.tsx and link_strength_component.tsx were also replaced with string

Fix

Update global.d.ts to use the v4 AppConfig pattern:

import { RowData } from "@tanstack/table-core";
import en from "./messages/en.json";
import "@tanstack/react-table";

declare module 'next-intl' {
  interface AppConfig {
    Messages: typeof en;
  }
}

declare module "@tanstack/table-core" {
  interface ColumnMeta<TData extends RowData, TValue> {
    className: string;
  }
}

Once that's done, MessageKeys is still exported from use-intl/core in v4, so you can restore typed TranslationKey:

import { MessageKeys } from "next-intl";

export type TranslationKey = MessageKeys<Messages, keyof Messages>;

Or just use keyof IntlMessages pattern again once AppConfig makes the types flow through.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Upgrade Next.js to v16.2

2 participants