Conversation
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 = { |
There was a problem hiding this comment.
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" | |||
There was a problem hiding this comment.
Just a way to avoid repeating these
| @@ -0,0 +1,107 @@ | |||
| import { NextRequest, NextResponse } from "next/server"; | |||
There was a problem hiding this comment.
It's 2026 why the hell Next asks users to configure all this hell when they want path based i18n
* 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>
* 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>
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>
There was a problem hiding this comment.
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 guardingconsole.debugstatement.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,
resolveResolverPathreturns 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 forincludes()check.
SUPPORTED_LOCALESisLocaleCode[]whilecodeisstring. 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 unusedgetLocalePathnameexport 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
TODOcomment 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
📒 Files selected for processing (18)
demo/new-compiler-next16/app/.lingo/locale-resolver-client.tsdemo/new-compiler-next16/app/.lingo/locale-resolver-server.tsdemo/new-compiler-next16/app/[locale]/layout.tsxdemo/new-compiler-next16/app/[locale]/page.tsxdemo/new-compiler-next16/app/[locale]/test/page.tsxdemo/new-compiler-next16/app/layout.tsxdemo/new-compiler-next16/components/LocaleLink.tsxdemo/new-compiler-next16/next.config.tsdemo/new-compiler-next16/proxy.tsdemo/new-compiler-next16/supported-locales.tspackages/new-compiler/src/index.tspackages/new-compiler/src/plugin/next.tspackages/new-compiler/src/plugin/resolve-locale-resolver.tspackages/new-compiler/src/react/shared/LingoProvider.tsxpackages/new-compiler/src/translation-server/translation-server.tspackages/new-compiler/src/types.tspackages/new-compiler/src/virtual/code-generator.tspackages/new-compiler/src/virtual/locale/client.ts
| try { | ||
| const pathname = window.location.pathname; | ||
| const segments = pathname.split("/").filter(Boolean); | ||
| const potentialLocale = segments[0]; | ||
|
|
||
| if (potentialLocale) { | ||
| return potentialLocale as LocaleCode; | ||
| } |
There was a problem hiding this comment.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
🧩 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:
- 1: https://nextjs.org/docs/messages/missing-root-layout-tags
- 2: https://nextjs.org/docs/app/getting-started/layouts-and-pages
- 3: https://beta.nextjs.org/docs/api-reference/file-conventions/layout
- 4: https://beta.nextjs.org/docs/routing/fundamentals
- 5: https://nextjs.org/blog/next-16
- 6: https://nextjs.org/docs/app/guides/upgrading/version-16
- 7: https://www.knrestaurants.com
- 8: https://nextjs.org/docs/13/app/building-your-application/routing/pages-and-layouts
- 9: https://nextjs.org/docs/14/app/api-reference/file-conventions/layout
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.
| // 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; |
There was a problem hiding this comment.
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".
| 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, |
There was a problem hiding this comment.
🧩 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.tsRepository: 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 -20Repository: 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 3Repository: 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 2Repository: 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 -60Repository: 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.tsRepository: 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).
| * For path-based routing, also needs push() for navigation | ||
| */ | ||
| router?: { refresh: () => void }; | ||
| router?: { refresh: () => void; push: (path: string) => void }; |
There was a problem hiding this comment.
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.
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
Testing
Business logic tests added:
Visuals
Required for UI/UX changes:
Checklist
Closes #[issue-number]
Summary by CodeRabbit