Skip to content

feat(compiler): custom resolvers#2024

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

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

Conversation

@AleksandrSl
Copy link
Copy Markdown
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.

github-actions bot and others added 30 commits November 17, 2025 20:17
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat: add support for stringSet to xcode-xcstrings and xcode-xcstrings-v2

* chore: add changeset
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat: add support for substitutions

* fix: fix test

* chore: add changeset
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix: fix ICU input

* chore: add changeset
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…1626)

* fix: preserve YAML literal block scalars without backslash escaping

* chore: add changeset
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* chore: add syncpack for dependency version management

- Add syncpack as dev dependency
- Configure semver groups to enforce exact versions (no ^ or ~)
- Add prebuild script to validate pinned dependencies

* fix: pin all dependencies to exact versions

Remove caret (^) and tilde (~) ranges from all dependencies
to prevent supply chain attacks from automatic dependency updates.
All dependencies are now pinned to their currently resolved versions.

This affects:
- All workspace packages (cli, compiler, locales, logging, react, sdk, spec)
- Demo applications (adonisjs, next-app, react-router-app, vite-project)
- Integrations (directus)
- Legacy packages

* chore: add version field to private packages

Add version: 1.0.0 to packages that were missing it:
- demo/react-router-app
- scripts/docs

Required for syncpack validation to pass.

* chore: enforce dependency pinning in build pipeline

Add prebuild hook that runs syncpack lint to validate all dependencies
are pinned to exact versions. Build will fail if any ^ or ~ ranges are
detected, preventing unpinned dependencies from being introduced.

* chore: add changeset for dependency pinning

* chore: update lockfile with exact version specifiers

* fix: update dependencies to resolve critical security vulnerabilities

Updated packages to fix CVEs:
- vitest: 2.1.8 → 3.2.4 (CVE: RCE vulnerability)
- @directus/extensions-sdk: 12.1.4 → 17.0.3 (includes rollup, axios, form-data fixes)
- glob: 11.0.0 → 11.1.0 (CVE: command injection)

Security improvements:
- 3 critical vulnerabilities → 0
- 4 high severity → 0
- 40 moderate → 27
- 28 low → 25

All updated dependencies pinned to exact versions.

* ci: update Node.js version to 20.19 for directus compatibility

* test: fix vitest v4 compatibility in compiler tests

Updated test mocking syntax for vitest v4:
- Replace vi.mocked() with vi.hoisted() for ESM module mocks
- Use vi.fn() factories in hoisted scope for fs and prettier mocks
- Fix PostHog mock to use function declaration for proper constructor mocking
- Mock LCPCache.writeLocaleDictionary in server tests to prevent filesystem access

All 226 tests now pass with vitest v4.0.13.

* fix: correct package name in changeset
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Legacy packages are deprecated wrappers that should always use the
latest lingo.dev version. Using wildcard (*) allows them to resolve
to the local workspace version without breaking CI when version bumps
happen via changesets.

This fixes the frozen-lockfile error in CI after version bumps.
* WIP

* chore: fix types

* chore: add changeset

---------

Co-authored-by: Veronica Prilutskaya <veranika.prilutskaya@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat: preserve formatting in yaml files

* feat: preserve mixed key quoting

* fix: yaml-room-key detection

* chore: add changeset
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix: preserve formatting in YAML files

* chore: add changeset
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix: preserve list format in YAML files

* chore: add changeset
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat: localize logical blocks together

* fix: localize noscript, and text inside inner tags

* feat: allow localizing HTML blocks not just entire docs

* chore: add changeset
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix: prevent HTML tag duplication in Android XML strings

* chore: add changeset
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* feat: add mjml bucket

* fix(mjml): content extraction to prevent duplication

* chore: mjml examples

* chore: add changeset

* chore: add missing i18n.lock for mjml demo

- Add i18n.lock file for mjml demo bucket to pass tests

* chore: clean up i18n.json

* chore: clean up code

* chore: refactor code
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
process.cwd(),
);

customResolverAliases = {
Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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

AleksandrSl and others added 4 commits March 1, 2026 23:41
* fix(cli): exit with non-zero code when localization tasks fail

Previously, partial localization errors (e.g., API timeout on one locale)
were caught per-task and stored in results, but the process still exited
with code 0. This caused CI/CD pipelines to silently pass on failures.

Now sets process.exitCode = 1 when any task has error status, in both
the `run` and deprecated `i18n` commands. Also fixes the `run` command
to play the failure sound instead of success when there are errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add changeset for CLI exit code fix

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: extract exit code logic into shared helper

Addresses CodeRabbit review — tests now import and verify the actual
production function instead of duplicating the logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Max Prilutskiy <maks.prilutskiy@gmail.com>
@AndreyHirsa AndreyHirsa marked this pull request as ready for review March 4, 2026 08:30
* chore: cleanup translation-server

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.

* fix: add missing notion about url in the lingo config

* Update packages/new-compiler/src/translation-server/translation-server.ts

* fix: missing closing parenthesis

* chore: add changeset

---------

Co-authored-by: AndreyHirsa <58431746+AndreyHirsa@users.noreply.github.com>
Base automatically changed from aleksandrsl/docs-update to main March 4, 2026 08:34
github-actions bot and others added 18 commits March 4, 2026 11:48
Co-authored-by: Max Prilutskiy <maks.prilutskiy@gmail.com>
Co-authored-by: Max Prilutskiy <maks.prilutskiy@gmail.com>
* feat: vNext migration
Co-authored-by: Max Prilutskiy <maks.prilutskiy@gmail.com>
Co-authored-by: Lingo.dev <support@lingo.dev>
* fix: bump biome dependencies to latest

Bump @biomejs/js-api from 3.0.0 to 4.0.0 and @biomejs/wasm-nodejs from
2.3.7 to 2.4.6 to support modern biome config keys.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: add changeset for biome dependency bump

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Veronica Prilutskaya <veronica@lingo.dev>
Co-authored-by: Max Prilutskiy <maks.prilutskiy@gmail.com>
* chore: fix posthog emails

* chore: fix posthog

* chore: add alias
Co-authored-by: Max Prilutskiy <maks.prilutskiy@gmail.com>
Co-authored-by: Max Prilutskiy <5614659+maxprilutskiy@users.noreply.github.com>
* fix: improve API error messages and auth failure UX
Co-authored-by: Max Prilutskiy <maks.prilutskiy@gmail.com>
* fix(cli): checkout i18n branch from remote instead of HEAD

* fix(cli): add changeset
Co-authored-by: Max Prilutskiy <maks.prilutskiy@gmail.com>
Co-authored-by: Lingo.dev <support@lingo.dev>
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: 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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 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.