Skip to content

feat(compiler): custom resolvers#2024

Open
AleksandrSl wants to merge 6 commits intomainfrom
aleksandrsl/custom-resolvers
Open

feat(compiler): custom resolvers#2024
AleksandrSl wants to merge 6 commits intomainfrom
aleksandrsl/custom-resolvers

Conversation

@AleksandrSl
Copy link
Collaborator

@AleksandrSl AleksandrSl commented Mar 1, 2026

Summary

It's an example of having custom resolvers and what we have to do to support them. Ofc we have to make path based i18n support internally along with the cookies, without custom resolvers.

There are both changes on the user side (demo) and our side (in the compiler, because I removed the custom resolver part for simplicity until we have requests, but PR also demonstrates how we can support path based i18n, probably it can also be done a bit cleaner, and we can provide middleware configuration as exported function, so users don't have to do it, similar how next-intl does this. suprisingly they have a ton of code around these)

Changes

  • [Key change 1]
  • [Key change 2]

Testing

Business logic tests added:

  • [Describe test 1 - what behavior it validates]
  • [Describe test 2 - what edge case it covers]
  • All tests pass locally

Visuals

Required for UI/UX changes:

  • Before/after screenshots attached
  • Video demo for interactions (< 30s)

Checklist

  • Changeset added (if version bump needed)
  • Tests cover business logic (not just happy path)
  • No breaking changes (or documented below)

Closes #[issue-number]

Summary by CodeRabbit

  • New Features
    • Added path-based locale routing through URL segments (e.g., /en/, /es/, /de/).
    • Implemented automatic locale detection from browser language preferences.
    • Enabled custom locale resolvers for flexible locale-switching behavior.
    • Enhanced navigation with locale-aware link handling.

There were a bunch of leftover from experiments. e.g. _lingoConfig was a hack, a field with a config copy added to the config object which can be read by the translation-server cli to parse the arguments. But bundlers were always complaining about it, plus in next with our async setup it would not work. So there is no way to parse the config now. We could add some though if needed, but honestly parsing the file and finding our config section.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 1, 2026

📝 Walkthrough

Walkthrough

This pull request introduces a custom locale resolver system for Next.js 16 that enables path-based locale routing instead of cookie-based persistence. It includes new server and client-side locale resolver utilities, a locale-aware link component, middleware for Accept-Language header parsing and locale detection, and updates to the compiler's virtual module generation and type system.

Changes

Cohort / File(s) Summary
Custom Locale Resolvers
demo/new-compiler-next16/app/.lingo/locale-resolver-client.ts, demo/new-compiler-next16/app/.lingo/locale-resolver-server.ts
New client and server locale resolvers. Client-side resolver derives locale from pathname or persists locale changes by returning locale-prefixed URLs. Server-side resolver reads locale from x-lingo-locale header set by middleware.
Locale Configuration
demo/new-compiler-next16/supported-locales.ts
New file exporting sourceLocale ("en"), targetLocales (["es", "de", "ru"]), and supportedLocales array combining both.
Middleware & Request Handling
demo/new-compiler-next16/proxy.ts
New middleware for locale detection and request routing. Detects locale from URL path or parses Accept-Language header with quality weighting; redirects requests to locale-prefixed paths when needed.
Layout Structure
demo/new-compiler-next16/app/layout.tsx, demo/new-compiler-next16/app/[locale]/layout.tsx
Restructured layout hierarchy. Root layout simplified to pass through children; new locale layout handles metadata, font setup, LingoProvider initialization with detected locale, and HTML lang attribute.
Navigation Component
demo/new-compiler-next16/components/LocaleLink.tsx, demo/new-compiler-next16/app/[locale]/page.tsx
New LocaleLink component that wraps Next.js Link and automatically prefixes relative URLs with current locale. Updated page to use LocaleLink instead of Next.js Link.
Compiler Configuration
demo/new-compiler-next16/next.config.ts
Updated to derive locale configuration from supported-locales and enable custom locale persistence via localePersistence: { type: "custom" }.
Resolver Path Resolution
packages/new-compiler/src/plugin/resolve-locale-resolver.ts
New utility module to locate and normalize custom locale resolver files for Turbopack; resolves .ts, .tsx, .js, .jsx files and returns paths relative to project root.
Next.js Plugin Updates
packages/new-compiler/src/plugin/next.ts
Extended Turbopack configuration to register virtual module aliases for custom locale resolvers when localePersistence.type === "custom".
LingoProvider Routing
packages/new-compiler/src/react/shared/LingoProvider.tsx
Updated provider to support path-based routing. Router prop now includes push(path) method; setLocale calls router.push() with new locale-prefixed URL from persistLocale() when available.
Type & Code Generation
packages/new-compiler/src/types.ts, packages/new-compiler/src/virtual/code-generator.ts, packages/new-compiler/src/virtual/locale/client.ts
Updated LocalePersistenceConfig to support { type: "custom" } variant. Code generator conditionally re-exports custom resolvers or generates default cookie-based implementations. Updated persistLocale return type to string | undefined.
Type Exports
packages/new-compiler/src/index.ts
Re-exported LocaleCode type from spec package for public API access.
Minor Formatting
packages/new-compiler/src/translation-server/translation-server.ts
Adjusted indentation in debug log statement.

Sequence Diagram

sequenceDiagram
    actor Browser
    participant Middleware
    participant Server as Server<br/>(Layout)
    participant Provider as LingoProvider
    participant Client as Client<br/>(LocaleLink)

    Browser->>Middleware: GET /
    Middleware->>Middleware: Extract locale from path<br/>or Accept-Language header
    Middleware->>Browser: Redirect to /en/test
    Browser->>Middleware: GET /en/test
    Middleware->>Server: Request with<br/>x-lingo-locale header
    Server->>Server: Read locale<br/>from header
    Server->>Provider: Wrap with LingoProvider<br/>initialLocale="en"
    Provider->>Provider: Initialize context<br/>with locale
    Browser->>Client: Hover/click LocaleLink
    Client->>Client: Get current locale<br/>from context
    Client->>Client: Prefix href with<br/>/${locale}
    Client->>Browser: Navigate to prefixed URL
    Browser->>Middleware: Follow new locale path
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

community

Suggested reviewers

  • cherkanovart

Poem

🐰 Hops excitedly

A path-based router hath arrived,
With locales in URLs now thrived!
No more cookie crumbs to track,
The rabbit's locale resolver's back—
Custom resolvers make routing stack! 🌍

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.19% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description provides a clear summary and acknowledges both demo and compiler changes, but the Changes section lists only placeholders ('Key change 1', 'Key change 2') and the Testing/Checklist sections remain incomplete with unchecked boxes and placeholders. Replace placeholder text in Changes section with actual key changes (custom resolver support, path-based i18n, virtual module aliasing, etc.), and either complete the Testing/Checklist sections or remove the template placeholders if testing was not done.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(compiler): custom resolvers' directly and clearly summarizes the main feature addition across the changeset—implementing support for custom locale resolvers in the compiler alongside a demo example.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch aleksandrsl/custom-resolvers

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.

@AleksandrSl AleksandrSl changed the title Aleksandrsl/custom resolvers feat(compiler): custom resolvers Mar 1, 2026
@AleksandrSl AleksandrSl changed the base branch from main to aleksandrsl/docs-update March 1, 2026 22:30
@@ -1 +1,2 @@
export type { PartialLingoConfig } from "./types";
export type { LocaleCode } from "lingo.dev/spec";
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Makes sense to reexport I think, because it's used in many places, and compiler could be the single dependency.

Comment on lines +74 to +77

export function getLocalePathname(locale) {
return null; // Not used for cookie-based routing
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Suggested change
export function getLocalePathname(locale) {
return null; // Not used for cookie-based routing
}

if (router) {
router.refresh();
if (newUrl) {
router.push(newUrl);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I missed that for the path based i18n we will have to do navigation. I don't think we need to add a separate method for that, just returning newUrl form persistLocale if redirect is needed sounds reasonable to me.

Next needs a push, because we have to trigger a middleware so that server components get a correct locale

} {
const baseDir = path.join(projectRoot, sourceRoot, lingoDir);

const serverPath = resolveResolverPath("locale-resolver-server", baseDir);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The whole file is a way to check that the file are present where we expect them to be. In this PR I expect them to be in the lingo directory, but we are free to come up with any pattern we want.

Also a nice error if one of the files is missing when custom locale resolver is chosen in settings would be good

process.cwd(),
);

customResolverAliases = {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For vite and unplugin there should already be a similar thing.

See

function tryLocalOrReturnVirtual(
  config: LingoConfig,
  fileName: string,
  virtualName: string,
) {
  const customPath = path.join(config.sourceRoot, config.lingoDir, fileName);
  if (fs.existsSync(customPath)) {
    return customPath;
  }
  return virtualName;
}

I don't remember if I checked it working

@@ -0,0 +1,4 @@
import type { LocaleCode } from "@lingo.dev/compiler"
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Just a way to avoid repeating these

@@ -0,0 +1,107 @@
import { NextRequest, NextResponse } from "next/server";
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It's 2026 why the hell Next asks users to configure all this hell when they want path based i18n

@AndreyHirsa AndreyHirsa marked this pull request as ready for review March 4, 2026 08:30
Base automatically changed from aleksandrsl/docs-update to main March 4, 2026 08:34
Copy link
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: 5

🧹 Nitpick comments (5)
demo/new-compiler-next16/app/.lingo/locale-resolver-server.ts (1)

25-29: Avoid hardcoded fallback locale in server resolver.

At Line 28, "en" can diverge from configured defaults. Prefer pulling fallback from shared locale config to keep resolver behavior consistent.

Proposed refactor
 import { headers } from "next/headers";
+import { sourceLocale } from "../../supported-locales";
 ...
 export async function getServerLocale(): Promise<string> {
   const headersList = await headers();
   const locale = headersList.get("x-lingo-locale");
-  return locale || "en";
+  return locale || sourceLocale;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@demo/new-compiler-next16/app/.lingo/locale-resolver-server.ts` around lines
25 - 29, getServerLocale currently falls back to a hardcoded "en" which can
diverge from app defaults; update getServerLocale to pull the fallback from the
shared locale configuration (e.g. use the exported DEFAULT_LOCALE constant or
getDefaultLocale() function) instead of the literal "en", and ensure the module
imports that symbol and returns it when headers().get("x-lingo-locale") is
falsy.
demo/new-compiler-next16/app/[locale]/layout.tsx (1)

32-32: Consider removing or guarding console.debug statement.

This debug log will appear in production server logs. Consider removing it or using a conditional/logger utility that respects environment.

🔧 Proposed fix
-  console.debug("LocaleLayout", { locale });

Or if debugging is needed:

-  console.debug("LocaleLayout", { locale });
+  if (process.env.NODE_ENV === "development") {
+    console.debug("LocaleLayout", { locale });
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@demo/new-compiler-next16/app/`[locale]/layout.tsx at line 32, Remove or guard
the console.debug call so it doesn't leak debug output to production logs:
either delete the line console.debug("LocaleLayout", { locale }) in the layout
(or LocaleLayout component) or wrap it behind an environment check/logger that
only emits debug in development (e.g., process.env.NODE_ENV === "development" or
using the project's logger utility) so production servers won't log the locale
debug message.
packages/new-compiler/src/plugin/resolve-locale-resolver.ts (1)

26-41: Silent fallback when resolver file doesn't exist.

When no resolver file is found, resolveResolverPath returns the base path without extension (line 40). This will cause a confusing Turbopack error downstream rather than a clear "resolver file not found" message.

Consider adding validation or a clearer error:

🔧 Proposed improvement
 function resolveResolverPath(basePath: string, projectRoot: string): string {
   // Try with the provided extension first
   const absolutePath = path.resolve(projectRoot, basePath);
   if (fs.existsSync(absolutePath)) {
     return absolutePath;
   }
 
   for (const ext of EXTENSIONS) {
     const pathWithExt = absolutePath + ext;
     if (fs.existsSync(pathWithExt)) {
       return pathWithExt;
     }
   }
 
-  return absolutePath;
+  throw new Error(
+    `Custom locale resolver not found: ${basePath}\n` +
+    `Expected one of: ${EXTENSIONS.map(ext => basePath + ext).join(', ')}\n` +
+    `Searched in: ${projectRoot}`
+  );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/new-compiler/src/plugin/resolve-locale-resolver.ts` around lines 26
- 41, The resolveResolverPath function currently returns the untranslated
absolutePath when no file exists, causing unclear downstream errors; update
resolveResolverPath to validate that a resolver file was found and throw a clear
Error (or return a distinct failure) if none of the checked paths exist.
Specifically, after trying absolutePath and the loop over EXTENSIONS
(referencing resolveResolverPath and EXTENSIONS), construct a descriptive error
message that includes the attempted basePath, projectRoot, and the candidate
paths checked, then throw that Error so callers get an explicit "resolver file
not found" message instead of a confusing Turbopack error.
demo/new-compiler-next16/proxy.ts (1)

28-32: Type narrowing may be needed for includes() check.

SUPPORTED_LOCALES is LocaleCode[] while code is string. Depending on TypeScript strictness settings, includes(code) might not properly narrow the type. This works at runtime but could benefit from explicit typing.

🔧 Optional type-safe alternative
   // Find first supported locale
   for (const { code } of languages) {
-    if (SUPPORTED_LOCALES.includes(code)) {
+    if ((SUPPORTED_LOCALES as readonly string[]).includes(code)) {
       return code;
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@demo/new-compiler-next16/proxy.ts` around lines 28 - 32, The includes check
compares a string `code` from `languages` against `SUPPORTED_LOCALES:
LocaleCode[]` and may not narrow types under strict TS; update the check to be
type-safe by either (A) narrowing `code` to `LocaleCode` with a type guard
before returning, or (B) cast/assert `code as LocaleCode` when calling
`SUPPORTED_LOCALES.includes`, or better yet adjust the `languages` item type so
`code` is typed as `LocaleCode`; locate the loop using `for (const { code } of
languages)` and change the check to use one of these approaches so TypeScript
understands the result is a valid `LocaleCode`.
packages/new-compiler/src/virtual/code-generator.ts (1)

74-77: Remove unused getLocalePathname export or clarify intent with TODO comment.

This function is exported but never imported anywhere in the codebase. The existing comment ("Not used for cookie-based routing") suggests it's an intentional placeholder, but either remove it to avoid dead code or add a TODO comment if it's reserved for future use.

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

In `@packages/new-compiler/src/virtual/code-generator.ts` around lines 74 - 77,
The getLocalePathname function is exported but unused; either delete the
exported function entirely or keep it but clarify intent: if removing, remove
the export and function declaration for getLocalePathname; if reserving for
future use, add a clear TODO/JSDoc above getLocalePathname explaining why it is
a placeholder for cookie-based routing and retain the export to avoid linter
noise (e.g., "TODO: reserved for future cookie-based routing — keep exported
until feature implemented"); update the signature or comment accordingly so the
purpose is explicit.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@demo/new-compiler-next16/app/.lingo/locale-resolver-client.ts`:
- Around line 29-36: The resolver currently returns the first path segment
(potentialLocale) without checking it against the supported list; update the
logic in locale-resolver-client.ts to validate potentialLocale with
SUPPORTED_LOCALES (e.g., if (potentialLocale &&
SUPPORTED_LOCALES.includes(potentialLocale))) before returning it as LocaleCode,
otherwise fall back to the existing fallback/undefined behavior so the client
matches the server's locale validation.

In `@demo/new-compiler-next16/app/layout.tsx`:
- Around line 7-13: The RootLayout component currently returns children directly
which causes missing-root-layout-tags errors; update the export default function
RootLayout to return a root HTML structure by wrapping children in <html> and
<body> elements (e.g., return <html><body>{children}</body></html>), ensuring
the component still accepts the existing Readonly<{ children: ReactNode }> props
and exports as before.

In `@demo/new-compiler-next16/components/LocaleLink.tsx`:
- Around line 13-16: The locale-prefix check in LocaleLink.tsx (the
localizedHref logic) only prevents adding the current locale and can produce
double prefixes like "/es/en/about"; update the condition to detect any
supported locale prefix before prepending: replace the
!href.startsWith(`/${locale}`) check with a test against the app's supported
locales (e.g., supportedLocales or i18n.locales) so you only prepend
`/${locale}` when href does not start with "/" + anySupportedLocale (handle both
"/{locale}" and "/{locale}/" cases) and still skip external URLs starting with
"http".

In `@packages/new-compiler/src/plugin/next.ts`:
- Around line 262-287: The webpack config is missing the same alias wiring
applied to Turbopack: when lingoConfig.localePersistence.type === "custom" the
customResolverAliases are merged into mergedResolveAlias but that merged map
isn't applied to webpack's resolve.alias, causing imports like
'@lingo.dev/compiler/virtual/locale-server' to fail; fix by assigning
mergedResolveAlias to the webpack configuration's resolve.alias (or merging it
into any existing webpackResolveAlias) where the webpack config is
built/returned so webpack uses the same aliases as Turbopack (reference
existingResolveAlias, customResolverAliases, mergedResolveAlias).

In `@packages/new-compiler/src/react/shared/LingoProvider.tsx`:
- Around line 41-43: The router type in LingoProvider.tsx was changed to require
push which is a breaking API; make push optional on the router property (change
router?: { refresh: () => void; push?: (path: string) => void }) and update all
usages inside the LingoProvider component (and any helper functions referenced
there) to guard calls to push (use optional chaining or an existence check
before calling) so existing consumers that only provide refresh won't break.

---

Nitpick comments:
In `@demo/new-compiler-next16/app/.lingo/locale-resolver-server.ts`:
- Around line 25-29: getServerLocale currently falls back to a hardcoded "en"
which can diverge from app defaults; update getServerLocale to pull the fallback
from the shared locale configuration (e.g. use the exported DEFAULT_LOCALE
constant or getDefaultLocale() function) instead of the literal "en", and ensure
the module imports that symbol and returns it when
headers().get("x-lingo-locale") is falsy.

In `@demo/new-compiler-next16/app/`[locale]/layout.tsx:
- Line 32: Remove or guard the console.debug call so it doesn't leak debug
output to production logs: either delete the line console.debug("LocaleLayout",
{ locale }) in the layout (or LocaleLayout component) or wrap it behind an
environment check/logger that only emits debug in development (e.g.,
process.env.NODE_ENV === "development" or using the project's logger utility) so
production servers won't log the locale debug message.

In `@demo/new-compiler-next16/proxy.ts`:
- Around line 28-32: The includes check compares a string `code` from
`languages` against `SUPPORTED_LOCALES: LocaleCode[]` and may not narrow types
under strict TS; update the check to be type-safe by either (A) narrowing `code`
to `LocaleCode` with a type guard before returning, or (B) cast/assert `code as
LocaleCode` when calling `SUPPORTED_LOCALES.includes`, or better yet adjust the
`languages` item type so `code` is typed as `LocaleCode`; locate the loop using
`for (const { code } of languages)` and change the check to use one of these
approaches so TypeScript understands the result is a valid `LocaleCode`.

In `@packages/new-compiler/src/plugin/resolve-locale-resolver.ts`:
- Around line 26-41: The resolveResolverPath function currently returns the
untranslated absolutePath when no file exists, causing unclear downstream
errors; update resolveResolverPath to validate that a resolver file was found
and throw a clear Error (or return a distinct failure) if none of the checked
paths exist. Specifically, after trying absolutePath and the loop over
EXTENSIONS (referencing resolveResolverPath and EXTENSIONS), construct a
descriptive error message that includes the attempted basePath, projectRoot, and
the candidate paths checked, then throw that Error so callers get an explicit
"resolver file not found" message instead of a confusing Turbopack error.

In `@packages/new-compiler/src/virtual/code-generator.ts`:
- Around line 74-77: The getLocalePathname function is exported but unused;
either delete the exported function entirely or keep it but clarify intent: if
removing, remove the export and function declaration for getLocalePathname; if
reserving for future use, add a clear TODO/JSDoc above getLocalePathname
explaining why it is a placeholder for cookie-based routing and retain the
export to avoid linter noise (e.g., "TODO: reserved for future cookie-based
routing — keep exported until feature implemented"); update the signature or
comment accordingly so the purpose is explicit.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 17389229-3324-4733-9de9-60737c78f642

📥 Commits

Reviewing files that changed from the base of the PR and between d18be81 and 3c4fa7a.

📒 Files selected for processing (18)
  • demo/new-compiler-next16/app/.lingo/locale-resolver-client.ts
  • demo/new-compiler-next16/app/.lingo/locale-resolver-server.ts
  • demo/new-compiler-next16/app/[locale]/layout.tsx
  • demo/new-compiler-next16/app/[locale]/page.tsx
  • demo/new-compiler-next16/app/[locale]/test/page.tsx
  • demo/new-compiler-next16/app/layout.tsx
  • demo/new-compiler-next16/components/LocaleLink.tsx
  • demo/new-compiler-next16/next.config.ts
  • demo/new-compiler-next16/proxy.ts
  • demo/new-compiler-next16/supported-locales.ts
  • packages/new-compiler/src/index.ts
  • packages/new-compiler/src/plugin/next.ts
  • packages/new-compiler/src/plugin/resolve-locale-resolver.ts
  • packages/new-compiler/src/react/shared/LingoProvider.tsx
  • packages/new-compiler/src/translation-server/translation-server.ts
  • packages/new-compiler/src/types.ts
  • packages/new-compiler/src/virtual/code-generator.ts
  • packages/new-compiler/src/virtual/locale/client.ts

Comment on lines +29 to +36
try {
const pathname = window.location.pathname;
const segments = pathname.split("/").filter(Boolean);
const potentialLocale = segments[0];

if (potentialLocale) {
return potentialLocale as LocaleCode;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing validation against supported locales.

Unlike proxy.ts which validates SUPPORTED_LOCALES.includes(potentialLocale), this function returns any first path segment as the locale without validation. This could cause client/server mismatch if an invalid locale somehow reaches the URL.

Consider validating against the supported locales list:

🛡️ Proposed fix
 import type { LocaleCode } from "@lingo.dev/compiler"
-import { sourceLocale } from "../../supported-locales";
+import { sourceLocale, supportedLocales } from "../../supported-locales";
 
 // ... in getClientLocale():
     const potentialLocale = segments[0];
 
-    if (potentialLocale) {
-      return potentialLocale as LocaleCode;
+    if (potentialLocale && supportedLocales.includes(potentialLocale as LocaleCode)) {
+      return potentialLocale as LocaleCode;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@demo/new-compiler-next16/app/.lingo/locale-resolver-client.ts` around lines
29 - 36, The resolver currently returns the first path segment (potentialLocale)
without checking it against the supported list; update the logic in
locale-resolver-client.ts to validate potentialLocale with SUPPORTED_LOCALES
(e.g., if (potentialLocale && SUPPORTED_LOCALES.includes(potentialLocale)))
before returning it as LocaleCode, otherwise fall back to the existing
fallback/undefined behavior so the client matches the server's locale
validation.

Comment on lines 7 to 13
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
children: ReactNode;
}>) {
return (
<LingoProvider>
<html>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
</LingoProvider>
);
return children;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Next.js 16 App Router: does the top-level app/layout.tsx root layout still require both <html> and <body> tags?

💡 Result:

Yes, the top-level app/layout.tsx root layout in Next.js 16 App Router still requires both <html> and <body> tags.

Citations:


Add <html> and <body> tags to the root layout.

The top-level app/layout.tsx must render <html> and <body> elements. Returning children directly will cause missing-root-layout-tags errors at runtime.

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

In `@demo/new-compiler-next16/app/layout.tsx` around lines 7 - 13, The RootLayout
component currently returns children directly which causes
missing-root-layout-tags errors; update the export default function RootLayout
to return a root HTML structure by wrapping children in <html> and <body>
elements (e.g., return <html><body>{children}</body></html>), ensuring the
component still accepts the existing Readonly<{ children: ReactNode }> props and
exports as before.

Comment on lines +13 to +16
// If href is already locale-prefixed or external, use as-is
const localizedHref = typeof href === "string" && !href.startsWith("http") && !href.startsWith(`/${locale}`)
? `/${locale}${href.startsWith("/") ? "" : "/"}${href}`
: href;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Locale prefix check only guards against current locale, not all supported locales.

If locale is "es" and href is "/en/about" (already prefixed with a different locale), this would incorrectly produce "/es/en/about".

Consider checking against all supported locales:

🐛 Proposed fix
+"use client";
+
+import NextLink from "next/link";
+import { useLingoContext } from "@lingo.dev/compiler/react";
+import type { ComponentProps } from "react";
+import { supportedLocales } from "@/supported-locales";
+
+/**
+ * Check if href already starts with a supported locale prefix
+ */
+function hasLocalePrefix(href: string): boolean {
+  return supportedLocales.some(
+    (loc) => href === `/${loc}` || href.startsWith(`/${loc}/`)
+  );
+}
+
 /**
  * Link component that automatically prefixes hrefs with current locale
  */
 export function Link({ href, ...props }: ComponentProps<typeof NextLink>) {
   const { locale } = useLingoContext();
 
-  // If href is already locale-prefixed or external, use as-is
-  const localizedHref = typeof href === "string" && !href.startsWith("http") && !href.startsWith(`/${locale}`)
+  // If href is already locale-prefixed or external, use as-is
+  const localizedHref = typeof href === "string" && !href.startsWith("http") && !hasLocalePrefix(href)
     ? `/${locale}${href.startsWith("/") ? "" : "/"}${href}`
     : href;
 
   return <NextLink href={localizedHref} {...props} />;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@demo/new-compiler-next16/components/LocaleLink.tsx` around lines 13 - 16, The
locale-prefix check in LocaleLink.tsx (the localizedHref logic) only prevents
adding the current locale and can produce double prefixes like "/es/en/about";
update the condition to detect any supported locale prefix before prepending:
replace the !href.startsWith(`/${locale}`) check with a test against the app's
supported locales (e.g., supportedLocales or i18n.locales) so you only prepend
`/${locale}` when href does not start with "/" + anySupportedLocale (handle both
"/{locale}" and "/{locale}/" cases) and still skip external URLs starting with
"http".

Comment on lines 262 to +287
const existingResolveAlias = existingTurbopackConfig.resolveAlias;
let customResolverAliases = {};

// Custom locale resolvers:
// When using custom resolvers (localePersistence.type === "custom"),
// we map abstract module paths to the user's actual files via Turbopack resolveAlias.
// This allows virtual modules to import from '@lingo.dev/compiler/virtual/locale-*'
// which Turbopack resolves to the user's actual locale resolver files.
//
// Convention: Resolver files must be at <sourceRoot>/<lingoDir>/locale-resolver-{server|client}.ts
if (lingoConfig.localePersistence.type === "custom") {
const resolvedPaths = resolveCustomResolverPaths(
lingoConfig.sourceRoot,
lingoConfig.lingoDir,
process.cwd(),
);

customResolverAliases = {
"@lingo.dev/compiler/virtual/locale-server": resolvedPaths.serverResolver,
"@lingo.dev/compiler/virtual/locale-client": resolvedPaths.clientResolver,
};
}

const mergedResolveAlias = {
...existingResolveAlias,
// TODO (AleksandrSl 08/12/2025): Describe what have to be done to support custom resolvers
...customResolverAliases,
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether webpack path has equivalent custom resolver handling.
# Expected: either webpack resolve.alias is configured, or locale loaders directly resolve user custom resolver files.

rg -n "resolveCustomResolverPaths|virtual/locale-server|virtual/locale-client|locale-resolver" packages/new-compiler/src

fd 'next-locale-.*loader\.ts$' packages/new-compiler/src --exec sed -n '1,260p' {}

rg -n "webpack\\s*:\\s*\\(|resolve\\s*:\\s*\\{\\s*alias|resolveAlias" packages/new-compiler/src/plugin/next.ts

Repository: lingodotdev/lingo.dev

Length of output: 3468


🏁 Script executed:

cat -n packages/new-compiler/src/plugin/next.ts | sed -n '260,320p'

Repository: lingodotdev/lingo.dev

Length of output: 2410


🏁 Script executed:

# Check if webpack configuration exists in the file
rg -n "webpack" packages/new-compiler/src/plugin/next.ts | head -20

Repository: lingodotdev/lingo.dev

Length of output: 493


🏁 Script executed:

# Understand the unplugin approach and how it handles resolvers
cat -n packages/new-compiler/src/plugin/unplugin.ts | sed -n '1,100p'

Repository: lingodotdev/lingo.dev

Length of output: 3892


🏁 Script executed:

cat -n packages/new-compiler/src/plugin/next.ts | sed -n '340,400p'

Repository: lingodotdev/lingo.dev

Length of output: 1324


🏁 Script executed:

# Check if webpack resolve.alias is set anywhere in the file
rg -n "resolve.*alias|webpack.*alias" packages/new-compiler/src/plugin/next.ts -A 3

Repository: lingodotdev/lingo.dev

Length of output: 47


🏁 Script executed:

# Understand the relationship between unplugin and webpack/turbopack
cat -n packages/new-compiler/src/plugin/next.ts | sed -n '1,50p'

Repository: lingodotdev/lingo.dev

Length of output: 2083


🏁 Script executed:

# Search for webpack resolve.alias in entire plugin directory
rg -n "resolve.*alias" packages/new-compiler/src/plugin/ -A 2

Repository: lingodotdev/lingo.dev

Length of output: 47


🏁 Script executed:

# Check what the loaders return - do they provide resolve.alias?
cat -n packages/new-compiler/src/plugin/next.ts | sed -n '22,120p'

Repository: lingodotdev/lingo.dev

Length of output: 3061


🏁 Script executed:

# Understand the issue better: search for how virtual module imports are handled in webpack context
rg -n "@lingo.dev/compiler/virtual" packages/new-compiler/src --type ts -B 2 -A 2 | head -60

Repository: lingodotdev/lingo.dev

Length of output: 5491


🏁 Script executed:

# Check if Next.js webpack automatically resolves `@lingo.dev/`* packages
rg -n "resolve.*modules|moduleNameMapper|alias" packages/new-compiler/src/plugin/next.ts

Repository: lingodotdev/lingo.dev

Length of output: 47


🏁 Script executed:

# Verify: Can webpack loaders alone handle virtual imports like `@lingo.dev/compiler/virtual/`*, or is alias needed?
# Check if there's any webpack resolve configuration that might handle this
cat -n packages/new-compiler/src/plugin/next.ts | sed -n '345,376p'

Repository: lingodotdev/lingo.dev

Length of output: 1240


Add webpack resolve.alias configuration for custom resolver paths.

Custom resolver aliasing is configured for Turbopack (lines 280–281, injected at 296 and 308), but webpack has no equivalent resolve.alias wiring. When localePersistence.type === "custom", webpack will fail to resolve the @lingo.dev/compiler/virtual/locale-* imports to the user's custom resolver files. Apply mergedResolveAlias to the webpack config's resolve.alias to maintain feature parity across bundlers.

Also applies to: 290–311

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

In `@packages/new-compiler/src/plugin/next.ts` around lines 262 - 287, The webpack
config is missing the same alias wiring applied to Turbopack: when
lingoConfig.localePersistence.type === "custom" the customResolverAliases are
merged into mergedResolveAlias but that merged map isn't applied to webpack's
resolve.alias, causing imports like '@lingo.dev/compiler/virtual/locale-server'
to fail; fix by assigning mergedResolveAlias to the webpack configuration's
resolve.alias (or merging it into any existing webpackResolveAlias) where the
webpack config is built/returned so webpack uses the same aliases as Turbopack
(reference existingResolveAlias, customResolverAliases, mergedResolveAlias).

Comment on lines +41 to +43
* For path-based routing, also needs push() for navigation
*/
router?: { refresh: () => void };
router?: { refresh: () => void; push: (path: string) => void };
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Breaking change: router interface now requires push method.

Adding push to the required interface is a breaking change for existing consumers that only provided { refresh: () => void }. This will cause TypeScript errors for current integrations.

Consider making push optional if backward compatibility is needed:

🔧 Proposed fix for backward compatibility
-  router?: { refresh: () => void; push: (path: string) => void };
+  router?: { refresh: () => void; push?: (path: string) => void };

Then update the usage:

       if (newUrl) {
-        router.push(newUrl);
+        router.push?.(newUrl) ?? router.refresh();
       } else {
         router.refresh();
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/new-compiler/src/react/shared/LingoProvider.tsx` around lines 41 -
43, The router type in LingoProvider.tsx was changed to require push which is a
breaking API; make push optional on the router property (change router?: {
refresh: () => void; push?: (path: string) => void }) and update all usages
inside the LingoProvider component (and any helper functions referenced there)
to guard calls to push (use optional chaining or an existence check before
calling) so existing consumers that only provide refresh won't break.

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.

2 participants