Skip to content

Conversation

@antoinekm
Copy link
Member

@antoinekm antoinekm commented Dec 28, 2025

Summary by CodeRabbit

  • New Features

    • Full English/French internationalization across site (pages, nav, footer, agency, services, projects, careers, glossary, contact, NPO)
    • Language switcher in footer and localized routing
    • Locale-aware sitemaps and robots for SEO; per-locale sitemap endpoints
  • Enhancements

    • Services catalog, project pages and agency landing pages localized with richer content, FAQs and metadata
    • Expanded glossary and localized marketing content
  • Bug Fixes

    • Improved metadata and sitemap generation for translated pages
  • Documentation

    • Locales/translation guidelines and examples added for contributors

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings December 28, 2025 06:05
@vercel
Copy link

vercel bot commented Dec 28, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
onruntime-website Ready Ready Preview, Comment Dec 31, 2025 0:09am

@coderabbitai
Copy link

coderabbitai bot commented Dec 28, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds full i18n support across the web app: new translations packages and locale JSONs, locale-aware routing/proxy and layouts, server/client translation loaders, widespread conversion of UI and content to translation keys, refactors data models to key-driven schemas, replaces next-sitemap with @onruntime/next-sitemap and adds sitemap/robots routes.

Changes

Cohort / File(s) Summary
Package & Build
apps/web/package.json, .changeset/*
Swap next-sitemap@onruntime/next-sitemap, add @onruntime/translations, @radix-ui/react-dropdown-menu, server-only; remove postbuild script; add changeset config.
Root & Locale Layouts
apps/web/src/app/layout.tsx, apps/web/src/app/[locale]/layout.tsx, apps/web/src/app/[locale]/providers.tsx
Introduce locale-aware layout, font, analytics injection, generateStaticParams and a client Providers wrapping AppTranslationProvider; simplify RootLayout to passthrough.
Locale Runtime & Server Helpers
apps/web/src/lib/translations.ts, apps/web/src/lib/translations.server.ts, apps/web/src/proxy.ts
Add locales/defaults, LOCALE_COOKIE, load/getPreferredLocale helpers, server getTranslation and a proxy middleware/route handler with redirect/rewrite and x-locale header handling.
Routing / Sitemap / Robots
apps/web/next.config.ts, apps/web/next-sitemap.config.js (removed), apps/web/src/app/sitemap.xml/**, apps/web/src/app/robots.ts
Remove legacy sitemap config; add sitemap index and per-id sitemap handlers using @onruntime/next-sitemap, robots route, and rewrite for sitemap-:id.xml.
Translations & Locale Data
apps/web/src/locales/en/**, apps/web/src/locales/fr/**, apps/web/src/locales/README.md
Add ~200+ JSON locale bundles for EN/FR covering pages, components, services, agencies, projects and constants; add README with conventions.
Components — Navigation & UI
apps/web/src/components/layout/footer/footer.tsx, apps/web/src/components/marketing/navbar/**, apps/web/src/components/ui/dropdown-menu.tsx
Convert nav/footer to use useTranslation/useLocale and translation-aware Link; add dropdown-menu Radix wrappers; footer adds language selector.
Components — Pages & Marketing
apps/web/src/components/marketing/**, apps/web/src/screens/marketing/**
Convert many marketing components/pages to async/server components that load translations and use t(...); update Links to translations-aware Link; add metadata generation using translations.
Components — Agency & Localized Sections
apps/web/src/components/marketing/agency/**, apps/web/src/components/marketing/agency/*.tsx
Add async server components (CityHeroSection, ContactCTA, AgencyCard, local expertise/testimonials/FAQ) and switch to translation keys.
Glossary & MDX
apps/web/src/lib/mdx.tsx, apps/web/src/lib/glossary.tsx, apps/web/src/components/glossary/**, apps/web/src/content/**
Make MDX and glossary locale-aware, add locale param to getPageContent, convert glossary components to async server components and add new MDX entries.
Services & Features Refactor
apps/web/src/constants/services/*.ts, apps/web/src/components/marketing/services/*
Replace verbose service objects with key-based entries; update Feature/FAQ/Testimonials sections to source from Services catalog and translation keys (API surface of some props changed).
Agencies & Projects Data Model
apps/web/src/constants/agencies/*.ts, apps/web/src/constants/projects/*.ts
Migrate agency and project data objects from descriptive text fields to key-driven schemas (shape changes across many exported objects).
Careers API & Utilities
apps/web/src/app/api/careers/**, apps/web/src/lib/utils/careers.ts
Make careers API locale-aware and cached per-locale; change job response shape; remove formatEmploymentType; refactor formatSalary to structured object.
Metadata Utilities
apps/web/src/lib/utils/metadata.ts, apps/web/src/lib/utils/metadata.server.ts, apps/web/src/app/api/og/route.ts
Move constructMetadata to server variant, add toOgLocale/getPathWithoutLocale helpers and server-side metadata builder that uses translations and locale alternates.
JSON‑LD Schema Components
apps/web/src/components/json-ld/*.tsx, apps/web/src/components/json-ld/constants.ts
Add PersonSchema, ProjectTeamSchema; default schema URLs now derive from siteConfig; siteConfig constant added.
Env & Config
apps/web/env.ts, apps/web/src/constants/site-config.ts
Move NEXT_PUBLIC_APP_URL default into client schema and expose siteConfig from env.
Misc
apps/web/src/components/custom-mdx.tsx, apps/web/src/mdx-components.tsx, many small components
Replace Next.js Link imports with translations-aware Link; wire translations across many small components and pages (metadata generateMetadata added/updated).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Proxy as Middleware/Proxy
    participant NextReq as Next Request Pipeline
    participant LocaleLayout
    participant Providers
    participant TranslationSrv as Translation Loader
    participant Page

    Client->>Proxy: GET /services or /
    Proxy->>Proxy: detect locale (path/cookie/Accept-Language)
    alt redirect to prefixed locale
        Proxy-->>Client: 302 to /{locale}/...
    else rewrite/forward with x-locale
        Proxy->>NextReq: rewritten request + x-locale header
    end
    NextReq->>LocaleLayout: render [locale] layout
    LocaleLayout->>Providers: mount Providers(locale)
    Providers->>TranslationSrv: load namespaces/locales
    TranslationSrv-->>Providers: { t }
    Providers->>Page: render children with t
    Page->>TranslationSrv: getTranslation(namespace, locale)
    TranslationSrv-->>Page: localized strings
    Page-->>Client: HTML with localized content
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • ✨ add i18n #365 — Adds the core @onruntime/translations package and provider APIs; directly related because this PR integrates that package across the web app.
  • 📝 add claude.md #364 — Updates CLAUDE.md and docs; related due to overlapping documentation edits and translations guidance.

Poem

🐰 I hopped through keys and locale streams,

Swapping French strings for translation dreams.
Routes now listen for cookies and head,
Pages dressed in Figtree, t(...) spread.
Hop, hop — locales sing, the site now beams.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.46% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding translations/i18n support to the web application. It is specific, uses minimal words, and accurately reflects the extensive internationalization work evident throughout the changeset.
✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 94b0075 and f61b2da.

📒 Files selected for processing (1)
  • apps/web/src/app/robots.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/app/robots.ts

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

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request adds internationalization (i18n) support to the web application by introducing locale routing, translation infrastructure, and restructuring the app directory to support multiple languages (English and French).

Key Changes:

  • Adds Next.js middleware for locale detection, routing, and URL rewriting
  • Restructures app directory from flat structure to [locale]-based routing
  • Integrates @onruntime/translations package for translation management
  • Converts Footer component to use translations with client-side rendering

Reviewed changes

Copilot reviewed 9 out of 33 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pnpm-lock.yaml Adds @onruntime/translations workspace dependency
apps/web/package.json Adds @onruntime/translations to dependencies
apps/web/src/middleware.ts New middleware for locale detection, redirects, and URL rewriting
apps/web/src/lib/translations.ts Translation loader and utility functions setup
apps/web/src/locales/en/footer.json English translations for footer component
apps/web/src/locales/fr/footer.json French translations for footer component
apps/web/src/components/layout/footer/footer.tsx Converts footer to use client-side translations
apps/web/src/app/layout.tsx Simplified root layout that delegates to locale-specific layout
apps/web/src/app/[locale]/layout.tsx New locale-specific layout with all page structure and providers
apps/web/src/app/[locale]/providers.tsx Translation provider wrapper for client components
apps/web/src/app/[locale]/**/*.tsx Multiple new page routes under locale directory including services, projects, glossary, agency, careers, contact, npo, and legal pages
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@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: 3

🧹 Nitpick comments (2)
apps/web/src/middleware.ts (1)

6-22: LGTM! Consider adding clarifying comments.

The middleware correctly implements locale canonicalization:

  • Redirects default locale URLs to clean paths (e.g., /en/about/about)
  • Preserves non-default locale prefixes (e.g., /fr/about)
  • Rewrites root paths to include the default locale internally

The logic is sound and aligns with Next.js i18n best practices.

Optional: Add inline comments for clarity
 export function middleware(request: NextRequest) {
   const { pathname } = request.nextUrl;
 
+  // Check if the pathname starts with a supported locale
   const pathnameLocale = locales.find(
     (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`,
   );
 
+  // Redirect default locale to clean URLs (e.g., /en/about → /about)
   if (pathnameLocale === defaultLocale) {
     const newPathname = pathname.replace(`/${defaultLocale}`, "") || "/";
     return NextResponse.redirect(new URL(newPathname, request.url));
   }
 
+  // Let non-default locales pass through unchanged
   if (pathnameLocale) return;
 
+  // Rewrite paths without a locale to include the default locale internally
   request.nextUrl.pathname = `/${defaultLocale}${pathname}`;
   return NextResponse.rewrite(request.nextUrl);
 }
apps/web/src/lib/translations.ts (1)

17-23: Correctly handles async params for Next.js 15.

The function properly awaits the params Promise before extracting the locale, which aligns with Next.js 15's breaking change to async request APIs.


Consider validating the locale against the supported locales array.

The locale from params is used directly without checking if it's in the locales array. While the middleware likely validates this, adding a defensive check would improve resilience and make the function more self-contained.

🔎 Optional validation to add
 export const getTranslation = async (
   params: Promise<{ locale: string }>,
   namespace = "common",
 ) => {
   const { locale } = await params;
+  
+  // Fallback to default locale if the provided locale is not supported
+  const validLocale = locales.includes(locale) ? locale : defaultLocale;
+  
-  return getTranslationCore(load, locale, namespace);
+  return getTranslationCore(load, validLocale, namespace);
 };
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 446e0c7 and cd9ad58.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (32)
  • apps/web/package.json
  • apps/web/src/app/[locale]/(landing)/customer/page.ts
  • apps/web/src/app/[locale]/(landing)/layout.tsx
  • apps/web/src/app/[locale]/(landing)/page.ts
  • apps/web/src/app/[locale]/(legal)/company/page.tsx
  • apps/web/src/app/[locale]/(legal)/privacy/page.tsx
  • apps/web/src/app/[locale]/(legal)/terms/page.tsx
  • apps/web/src/app/[locale]/[...not_found]/page.ts
  • apps/web/src/app/[locale]/agency/[city]/page.tsx
  • apps/web/src/app/[locale]/agency/page.tsx
  • apps/web/src/app/[locale]/careers/[id]/page.tsx
  • apps/web/src/app/[locale]/careers/page.tsx
  • apps/web/src/app/[locale]/contact/page.tsx
  • apps/web/src/app/[locale]/glossary/[letter]/[term]/page.tsx
  • apps/web/src/app/[locale]/glossary/[letter]/page.tsx
  • apps/web/src/app/[locale]/glossary/page.tsx
  • apps/web/src/app/[locale]/glossary/tag/[tag]/page.tsx
  • apps/web/src/app/[locale]/glossary/tags/page.tsx
  • apps/web/src/app/[locale]/layout.tsx
  • apps/web/src/app/[locale]/npo/page.tsx
  • apps/web/src/app/[locale]/projects/[id]/page.tsx
  • apps/web/src/app/[locale]/projects/page.tsx
  • apps/web/src/app/[locale]/providers.tsx
  • apps/web/src/app/[locale]/services/[category]/[service]/page.tsx
  • apps/web/src/app/[locale]/services/[category]/page.tsx
  • apps/web/src/app/[locale]/services/page.tsx
  • apps/web/src/app/layout.tsx
  • apps/web/src/components/layout/footer/footer.tsx
  • apps/web/src/lib/translations.ts
  • apps/web/src/locales/en/footer.json
  • apps/web/src/locales/fr/footer.json
  • apps/web/src/middleware.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-12-28T00:50:00.153Z
Learnt from: CR
Repo: onRuntime/onruntime PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T00:50:00.153Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : Use Next.js unstable_cache for caching external API responses in API routes

Applied to files:

  • apps/web/src/middleware.ts
📚 Learning: 2025-12-28T00:50:00.153Z
Learnt from: CR
Repo: onRuntime/onruntime PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T00:50:00.153Z
Learning: Applies to apps/web/src/services/**/*.{ts,tsx} : Use lazy initialization for external API clients to avoid build failures when environment variables are missing in CI, via Proxy pattern or client object pattern

Applied to files:

  • apps/web/src/app/[locale]/providers.tsx
📚 Learning: 2025-12-28T00:50:00.153Z
Learnt from: CR
Repo: onRuntime/onruntime PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T00:50:00.153Z
Learning: Organize web app source code into: app/ (pages and API routes), components/ (ui, layout, marketing), services/ (API clients), constants/ (static data), content/ (MDX), lib/ (utilities), and types/ (TypeScript definitions)

Applied to files:

  • apps/web/src/app/[locale]/layout.tsx
🧬 Code graph analysis (4)
apps/web/src/middleware.ts (1)
apps/web/src/lib/translations.ts (2)
  • locales (6-6)
  • defaultLocale (7-7)
apps/web/src/app/[locale]/providers.tsx (1)
apps/web/src/lib/translations.ts (2)
  • locales (6-6)
  • load (9-15)
apps/web/src/app/[locale]/layout.tsx (2)
apps/web/src/lib/translations.ts (1)
  • locales (6-6)
apps/web/src/app/[locale]/providers.tsx (1)
  • Providers (13-19)
apps/web/src/lib/translations.ts (1)
packages/translations/src/core/types.ts (1)
  • TranslationLoader (12-15)
🪛 ast-grep (0.40.3)
apps/web/src/components/layout/footer/footer.tsx

[warning] 52-52: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

apps/web/src/app/[locale]/layout.tsx

[warning] 75-75: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)


[warning] 92-92: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)
apps/web/src/components/layout/footer/footer.tsx

[error] 53-53: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

apps/web/src/app/[locale]/layout.tsx

[error] 76-76: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)


[error] 93-93: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Agent
🔇 Additional comments (11)
apps/web/package.json (1)

14-14: LGTM!

The workspace dependency addition properly integrates the translations package into the web app.

apps/web/src/app/[locale]/providers.tsx (1)

1-19: LGTM!

The Providers component is well-structured, properly uses the "use client" directive, and correctly wires the translation context for locale-aware rendering.

apps/web/src/app/layout.tsx (1)

1-9: LGTM!

The simplified root layout correctly delegates to the locale-aware layout at [locale]/layout.tsx. This is the appropriate pattern for implementing i18n routing in Next.js 15.

apps/web/src/middleware.ts (1)

24-26: LGTM!

The matcher configuration correctly excludes Next.js internals, API routes, and static assets from locale processing.

apps/web/src/app/[locale]/layout.tsx (3)

25-27: LGTM!

The generateStaticParams function correctly generates static paths for all supported locales, enabling proper static generation at build time.


34-56: LGTM!

The locale-aware layout properly:

  • Extracts the locale from async params
  • Sets the HTML lang attribute
  • Wraps children with translation providers
  • Composes the page structure with Navbar, Footer, and Toaster

The implementation follows Next.js 15 conventions for async params and locale routing.


59-103: Static analytics scripts are safe despite linter warnings.

Static analysis tools flag the use of dangerouslySetInnerHTML on lines 76 and 93. However, these warnings are false positives in this context because:

  1. The injected content is static, hardcoded initialization code for Google Analytics and Google Ads
  2. No user input or dynamic data is involved
  3. This is the standard implementation pattern recommended by Google's documentation

The conditional rendering (non-development only) is appropriate for analytics scripts.

If you want to eliminate the linter warnings entirely, you could extract these scripts to separate components or use Next.js Script component's strategy prop, but the current implementation is functionally correct and secure.

apps/web/src/components/layout/footer/footer.tsx (1)

10-40: LGTM!

The footer successfully integrates with the translation system:

  • Properly uses useTranslation hook with the "footer" namespace
  • Dynamically builds navigation structure from translated strings
  • Maintains clean separation between content and presentation
apps/web/src/locales/fr/footer.json (1)

3-3: This security concern does not apply. The location field uses dangerouslySetInnerHTML intentionally to render simple formatting tags (<strong>) from static translation files. The HTML content is developer-controlled and not derived from user input, making this a legitimate and safe pattern. No security vulnerability exists here.

Likely an incorrect or invalid review comment.

apps/web/src/lib/translations.ts (2)

1-7: LGTM! Clean locale configuration.

The imports and locale setup are straightforward. Using locales[0] for the default is a reasonable pattern that keeps the configuration DRY.


9-15: and

Copy link

@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: 1

♻️ Duplicate comments (3)
apps/web/src/locales/en/layout/footer.json (1)

1-25: LGTM!

The translation structure follows the documented conventions with proper key naming and nesting. The HTML markup in the location field (line 3) was already addressed in a previous review regarding documentation guidelines.

apps/web/src/locales/fr/layout/footer.json (1)

18-18: Translate "Status" to French.

The link label "Status" remains in English. For consistency with other French translations, use "Statut".

-    "status": "Status",
+    "status": "Statut",
apps/web/src/components/layout/footer/footer.tsx (1)

10-40: Translation integration looks correct.

The navigation object is appropriately constructed with translation keys, and the namespace "layout/footer" aligns with the locale file structure (locales/{locale}/layout/footer.json).

Note: The "Github" → "GitHub" spelling issue on line 35 and the dangerouslySetInnerHTML security concern on lines 52-55 have already been raised in previous review comments.

🧹 Nitpick comments (6)
apps/web/src/locales/README.md (1)

7-48: Consider adding a language identifier to the fenced code block.

The directory structure diagram could use a language identifier for consistency, though text or plaintext would work since it's not actual code.

🔎 Suggested fix
-```
+```text
 locales/
 ├── en/
examples/translations/next-app/proxy.ts (1)

25-64: Verify mutation of request.nextUrl before rewrite.

The logic flow is sound, but line 60 mutates request.nextUrl.pathname directly before passing it to NextResponse.rewrite(). While this typically works, it modifies the original request object. Consider creating a new URL to be explicit:

🔎 Suggested alternative
   // Use default locale (rewrite internally)
   const requestHeaders = new Headers(request.headers);
   requestHeaders.set("x-locale", defaultLocale);
-  request.nextUrl.pathname = `/${defaultLocale}${pathname}`;
-  return NextResponse.rewrite(request.nextUrl, {
+  const rewriteUrl = new URL(`/${defaultLocale}${pathname}`, request.url);
+  return NextResponse.rewrite(rewriteUrl, {
     request: { headers: requestHeaders },
   });
apps/web/src/proxy.ts (1)

66-68: Consider escaping the dot in the matcher regex.

The \\. in the matcher works, but the regex pattern could be clearer. Currently .*\\..* matches any path containing a dot (files with extensions). This is correct but ensure test paths like /api.v1/endpoint (without extension) are intentionally excluded if they match.

apps/web/src/components/marketing/navbar/navigation/agencies/index.tsx (1)

54-59: Inconsistent agency name rendering.

Line 57 uses t("agencies.agency", { name: agency.name }) for interpolation, while lines 80 and 99 render agency.name directly. If the interpolation pattern at line 57 is intentional (e.g., for adding context like "Agence {name}"), consider applying it consistently across all agency name displays.

Also applies to: 78-84

apps/web/src/components/marketing/navbar/navigation/services/index.tsx (1)

34-45: Service names and descriptions are not translated.

The navigation services use service.name, service.description, and subService.* directly from the Services constant. If full i18n coverage is desired, consider translating these via translation keys or using a pattern like t(\services.${service.id}.name`)`.

This may be intentional if service data is meant to be locale-independent or will be addressed in a follow-up.

Also applies to: 71-76

packages/translations/README.md (1)

125-127: Consider documenting additional excluded paths in the matcher.

The matcher ["/((?!_next|favicon.ico).*)"] excludes Next.js internals and favicon. Depending on the app structure, you may also want to exclude API routes, static assets, or other paths that shouldn't go through locale detection.

// Example: exclude api routes and static files
matcher: ["/((?!api|_next|favicon.ico|.*\\..*).*)"]
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cd9ad58 and a4d48ec.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (23)
  • CLAUDE.md
  • apps/web/package.json
  • apps/web/src/components/layout/footer/footer.tsx
  • apps/web/src/components/marketing/navbar/index.tsx
  • apps/web/src/components/marketing/navbar/navigation/agencies/index.tsx
  • apps/web/src/components/marketing/navbar/navigation/index.tsx
  • apps/web/src/components/marketing/navbar/navigation/projects/index.tsx
  • apps/web/src/components/marketing/navbar/navigation/services/index.tsx
  • apps/web/src/lib/translations.server.ts
  • apps/web/src/lib/translations.ts
  • apps/web/src/locales/README.md
  • apps/web/src/locales/en/layout/footer.json
  • apps/web/src/locales/en/layout/navbar.json
  • apps/web/src/locales/fr/layout/footer.json
  • apps/web/src/locales/fr/layout/navbar.json
  • apps/web/src/proxy.ts
  • examples/translations/next-app/app/[lang]/page.tsx
  • examples/translations/next-app/lib/translations.server.ts
  • examples/translations/next-app/lib/translations.ts
  • examples/translations/next-app/middleware.ts
  • examples/translations/next-app/package.json
  • examples/translations/next-app/proxy.ts
  • packages/translations/README.md
💤 Files with no reviewable changes (1)
  • examples/translations/next-app/middleware.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/web/src/locales/en/layout/navbar.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/lib/translations.ts
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-12-28T00:50:00.153Z
Learnt from: CR
Repo: onRuntime/onruntime PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T00:50:00.153Z
Learning: Organize web app source code into: app/ (pages and API routes), components/ (ui, layout, marketing), services/ (API clients), constants/ (static data), content/ (MDX), lib/ (utilities), and types/ (TypeScript definitions)

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-12-28T00:50:00.153Z
Learnt from: CR
Repo: onRuntime/onruntime PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T00:50:00.153Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : Use Next.js unstable_cache for caching external API responses in API routes

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-12-28T00:50:00.153Z
Learnt from: CR
Repo: onRuntime/onruntime PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T00:50:00.153Z
Learning: Applies to apps/web/env.ts : Manage environment variables via t3-oss/env-nextjs in env.ts with validation skipped in CI (skipValidation: !!process.env.CI)

Applied to files:

  • CLAUDE.md
📚 Learning: 2025-12-28T00:50:00.153Z
Learnt from: CR
Repo: onRuntime/onruntime PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T00:50:00.153Z
Learning: Applies to apps/web/src/services/**/*.{ts,tsx} : Use lazy initialization for external API clients to avoid build failures when environment variables are missing in CI, via Proxy pattern or client object pattern

Applied to files:

  • CLAUDE.md
🧬 Code graph analysis (2)
examples/translations/next-app/app/[lang]/page.tsx (4)
examples/translations/next-pages/pages/index.tsx (1)
  • Home (6-34)
apps/web/src/lib/translations.server.ts (1)
  • getTranslation (8-12)
examples/translations/next-app/lib/translations.server.ts (1)
  • getTranslation (11-15)
packages/translations/src/core/loader.ts (1)
  • getTranslation (14-22)
apps/web/src/lib/translations.server.ts (3)
examples/translations/next-app/lib/translations.server.ts (1)
  • getTranslation (11-15)
apps/web/src/lib/translations.ts (2)
  • defaultLocale (4-4)
  • load (6-12)
examples/translations/next-app/lib/translations.ts (2)
  • defaultLocale (4-4)
  • load (6-12)
🪛 ast-grep (0.40.3)
apps/web/src/components/layout/footer/footer.tsx

[warning] 52-52: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)
apps/web/src/components/layout/footer/footer.tsx

[error] 53-53: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🪛 markdownlint-cli2 (0.18.1)
apps/web/src/locales/README.md

22-22: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🔇 Additional comments (19)
apps/web/package.json (1)

18-18: LGTM!

The new dependencies are appropriate for the translations feature:

  • @onruntime/translations as a workspace package for shared translation utilities
  • server-only to enforce server-side boundaries for translation loading

Also applies to: 46-46

examples/translations/next-app/package.json (1)

14-15: LGTM!

The server-only dependency addition aligns with the translations infrastructure pattern used in the main web app.

CLAUDE.md (2)

33-39: LGTM!

The documentation updates correctly reflect the new locale-aware routing structure and translation file organization.


67-76: LGTM!

Clear and concise examples for both server and client translation usage patterns. The distinction between getTranslation (server) and useTranslation (client) is well documented.

apps/web/src/locales/README.md (1)

93-102: LGTM with a note on HTML conventions.

The conventions section is well-documented. The HTML interpolation note at line 102 appropriately mentions dangerouslySetInnerHTML. This aligns with the previous review comment requesting documentation of acceptable HTML patterns in translation files.

apps/web/src/lib/translations.server.ts (1)

1-12: LGTM!

Clean implementation of the server-side translation helper:

  • Correctly uses server-only to enforce server-side execution
  • Properly awaits the async headers() API (Next.js 15 pattern)
  • Falls back to defaultLocale when x-locale header is missing
  • Delegates to the core translation function from the shared package
examples/translations/next-app/proxy.ts (2)

6-23: LGTM!

The getPreferredLocale function correctly parses the Accept-Language header with quality values, sorts by priority, and finds the first matching supported locale.


66-68: LGTM!

The matcher correctly excludes Next.js internal routes (_next) and static assets (favicon.ico).

apps/web/src/locales/fr/layout/navbar.json (1)

1-33: LGTM!

The French navbar translations are well-structured:

  • Follows the documented conventions for key naming and nesting
  • Uses proper interpolation syntax ({name}) at line 27
  • Content mirrors the English counterpart ensuring translation parity
examples/translations/next-app/lib/translations.ts (1)

1-12: LGTM!

The translation loader is well-structured with proper type-only import for TranslationLoader, clear locale configuration, and appropriate error handling that returns undefined when a translation file is not found.

apps/web/src/components/marketing/navbar/navigation/index.tsx (1)

1-44: LGTM!

The translation integration is well-implemented with proper "use client" directive, correct hook initialization with the "layout/navbar" namespace, and clean usage of t("links.npo") for the localized link text.

examples/translations/next-app/lib/translations.server.ts (1)

1-15: LGTM!

The server-side translation helper is well-designed with:

  • "server-only" import ensuring this code cannot leak to the client bundle
  • Proper async handling of headers()
  • Sensible fallback to defaultLocale when x-locale header is absent
  • Clear documentation explaining the proxy integration
apps/web/src/proxy.ts (2)

6-23: LGTM!

The Accept-Language parsing is well-implemented with proper handling of quality values, priority sorting, and fallback to defaultLocale.


25-64: LGTM!

The proxy logic correctly handles the three routing scenarios:

  1. Redirects away from default locale prefix (clean URLs)
  2. Forwards non-default locale with x-locale header
  3. Auto-detects and redirects/rewrites for locale-less URLs
apps/web/src/components/marketing/navbar/navigation/agencies/index.tsx (1)

1-2: LGTM!

The translation integration follows the established pattern with proper "use client" directive and correct namespace initialization.

Also applies to: 16-19

apps/web/src/components/marketing/navbar/navigation/services/index.tsx (1)

1-2: LGTM!

The translation integration is properly implemented with the "use client" directive and correct hook initialization.

Also applies to: 16-17, 31-33

apps/web/src/components/marketing/navbar/navigation/projects/index.tsx (1)

19-73: Translation implementation is well-structured.

Static UI labels are correctly translated via t() while dynamic project data (project.name, project.shortDescription) appropriately remains sourced from the Projects constant. This is the right approach since project content is domain data that may have its own localization strategy separate from UI strings.

apps/web/src/components/marketing/navbar/index.tsx (1)

48-87: Good use of translation interpolation for agencies.

The pattern on line 78 using t("agencies.agency", { name: agency.name }) properly handles agency names through interpolation, allowing the translation template to control how the name is displayed in each locale (e.g., "Agency: {name}" in English vs. "Agence : {name}" in French).

The consistent approach of translating UI labels while keeping domain data (services, projects) from constants aligns with the pattern in other components.

packages/translations/README.md (1)

89-123: Documentation accurately reflects the proxy pattern.

The proxy implementation correctly:

  1. Redirects away from default locale in URL (e.g., /en/page/page)
  2. Passes through explicit locale paths with x-locale header
  3. Redirects to preferred non-default locale based on Accept-Language
  4. Rewrites to default locale path while setting x-locale header

One note: Line 119 mutates request.nextUrl.pathname directly before calling NextResponse.rewrite(). This works but could be clearer by constructing a new URL instead.

Copy link

@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: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
apps/web/src/screens/marketing/services/index.tsx (1)

72-133: Hard-coded French strings should be replaced with translation keys.

The component contains multiple hard-coded French strings (titles, descriptions, button text) that prevent internationalization. Given that this PR aims to add translations and the AI summary indicates other marketing components are using translation hooks (useTranslation/useLocale), this screen should be similarly updated.

Consider importing and using the useTranslation hook to load translated strings:

import { useTranslation } from '@onruntime/translations';

Then replace hard-coded strings with translation keys throughout the component (e.g., t('services.hero.title'), t('services.hero.description'), etc.).

apps/web/src/screens/marketing/contact/index.tsx (1)

92-159: Validation error messages must be localized to match the UI.

Form labels, placeholders, and button text correctly use translation keys. However, the contactFormSchema at apps/web/src/lib/schema/contact.ts defines validation error messages as hardcoded French strings (lines 4-6), while the form displays these errors at lines 108, 128, and 147. This creates an UX inconsistency where UI text is localized but validation errors remain in French regardless of the user's language setting.

Externalize the validation error messages to the i18n translation system to ensure consistent localization across the entire form experience.

apps/web/src/components/marketing/services/service-overview.tsx (1)

1-276: Replace hardcoded French strings with translation keys.

While the Link component has been correctly updated to use the translations package, the component still contains numerous hardcoded French strings throughout (e.g., "Discuter de votre projet", "Commencer maintenant", "Une expertise pointue en", etc.). This prevents the component from being properly internationalized and blocks non-French speakers from using the application.

All user-facing strings should be replaced with translation keys using the useTranslation hook or similar translation utilities introduced in this PR.

Example refactor pattern
+import { useTranslation } from "@onruntime/translations/react";
+
 const ServiceOverviewPage: React.FC<ServiceOverviewPageProps> = ({
   service,
   benefits,
   processList,
   accentColor = "onruntime-blue",
 }) => {
+  const { t } = useTranslation("components/marketing/services/service-overview");
+
   return (
     <main className="min-h-screen pt-32 pb-16 w-full">
       <div className="px-4 md:px-0 max-w-5xl mx-auto">
         {/* ... */}
         <Link href={Routes.contact}>
           <Button size="lg">
-            Discuter de votre projet
+            {t("discuss_project")}
             <ArrowRight className="ml-2 w-5 h-5" />
           </Button>
         </Link>

You'll need to create corresponding locale JSON files with the French and English translations.

apps/web/src/components/glossary/entry-page.tsx (1)

1-77: Replace hardcoded French strings with translation keys.

The Link import has been correctly updated, but the component contains hardcoded French strings ("Retour au glossaire" on line 25, "Termes associés" on line 57) that prevent proper internationalization.

Replace these with translation keys using the useTranslation hook.

Suggested refactor
+import { useTranslation } from "@onruntime/translations/react";
+
 const GlossaryEntryPage: React.FC<GlossaryEntryPageProps> = ({
   entry,
   relatedEntries = []
 }) => {
+  const { t } = useTranslation("components/glossary/entry-page");
+
   return (
     <main className="min-h-screen pt-32 pb-16">
       <div className="px-4 md:px-0 max-w-3xl mx-auto">
         <div className="mb-8">
           <Link href="/glossary">
             <Button variant="ghost" className="pl-0 hover:pl-0">
               <ChevronLeft className="mr-2 h-4 w-4" />
-              Retour au glossaire
+              {t("back_to_glossary")}
             </Button>
           </Link>
         </div>
         {/* ... */}
         {relatedEntries.length > 0 && (
           <div className="mt-12 pt-8 border-t">
-            <h2 className="text-2xl font-semibold text-foreground mb-4">Termes associés</h2>
+            <h2 className="text-2xl font-semibold text-foreground mb-4">{t("related_terms")}</h2>

Create the corresponding locale files with translations for both en and fr.

apps/web/src/app/[locale]/glossary/page.tsx (1)

20-147: Component contains hardcoded French strings that should use translations.

The page component has multiple hardcoded French strings (lines 45-50, 98, 138-142) that prevent proper localization. Since this is a locale-aware route under [locale], all user-facing text should support multiple languages.

🔎 Recommended approach

Load translations at the component level:

 export default async function GlossaryPage() {
+  const { t } = await getTranslation("app/glossary/page");
   const entries = await getAllGlossaryEntries();

Then replace hardcoded strings:

  • Line 45: "Glossaire"t("hero.title")
  • Lines 48-50: Description text → t("hero.description")
  • Line 98: "Voir tous"t("view-all")
  • Lines 138-142: Empty state messages → t("empty.message") and t("empty.hint")
packages/translations/README.md (1)

134-136: Matcher in README differs from actual implementation.

The README shows matcher: ["/((?!_next|favicon.ico).*)"] but the actual apps/web/src/proxy.ts uses ["/((?!_next|favicon.ico|api|.*\\.).*)"] which additionally excludes api and paths with dots.

🔎 Proposed fix
 export const config = {
-  matcher: ["/((?!_next|favicon.ico).*)"],
+  matcher: ["/((?!_next|favicon.ico|api|.*\\.).*)"],
 };
♻️ Duplicate comments (1)
apps/web/src/components/layout/footer/footer.tsx (1)

61-64: Existing XSS concern with dangerouslySetInnerHTML (already flagged).

This usage of dangerouslySetInnerHTML with translation content has been extensively reviewed in past comments. The security concern remains: translation files containing <strong> tags create a trust dependency where contributors with write access could inject XSS payloads.

As previously suggested, consider either:

  1. Splitting the translation into separate keys (location.prefix and location.bold)
  2. Sanitizing with DOMPurify
  3. Documenting translation files as requiring security review
🧹 Nitpick comments (21)
apps/web/src/services/join.ts (1)

6-36: Consider minor schema refinements.

The schema extensions for i18n look good overall. Two optional improvements:

  1. Line 14: The z.union([z.string(), z.number(), z.null()]).optional() pattern is verbose. Consider using .nullable().optional() or z.union([z.string(), z.number()]).nullable().optional() for clarity.

  2. Lines 19-20: The comment indicates these are enums (HYBRID, REMOTE, ON_SITE), but the schema accepts any string. If the API contract is stable, consider using z.enum() for type safety:

    workplaceType: z.enum(['HYBRID', 'REMOTE', 'ON_SITE']).nullable().optional(),
🔎 Proposed refinements
  office: z.object({
-   id: z.union([z.string(), z.number(), z.null()]).optional(),
+   id: z.union([z.string(), z.number()]).nullable().optional(),
    name: z.string().nullable().optional(),
    city: z.string().nullable().optional(),
    countryIso: z.string().nullable().optional(),
  }).optional(),
- workplaceType: z.string().nullable().optional(), // HYBRID, REMOTE, ON_SITE
- remoteType: z.string().nullable().optional(),
+ workplaceType: z.enum(['HYBRID', 'REMOTE', 'ON_SITE']).nullable().optional(),
+ remoteType: z.enum([/* add known values */]).nullable().optional(),
apps/web/src/screens/marketing/projects/index.tsx (2)

3-3: Optional: Remove explicit React import.

React 19 with modern JSX transform doesn't require explicit React import for JSX. This import can be safely removed.

🔎 Optional cleanup
-import React from "react";

179-197: Consider named keys instead of array indices for testimonials.

Using numeric indices (items.0, items.1) works but is less maintainable if you need to reorder, add, or remove testimonials. Consider using explicit named keys:

{
  "testimonials": {
    "items": {
      "client1": { "quote": "...", "author": "..." },
      "client2": { "quote": "...", "author": "..." }
    }
  }
}

This approach improves clarity and makes it easier to manage testimonials over time.

apps/web/src/lib/mdx.tsx (1)

19-28: Locale-aware path construction is correct.

The addition of the optional locale parameter and the conditional path construction logic work correctly. When a locale is provided, the path is properly prefixed (e.g., fr/legal/privacy.mdx).

The file existence check on Line 27 throws an error that is immediately caught by the catch block on Line 47, where all errors (file not found, parse errors, etc.) are handled identically. While the current graceful degradation is acceptable and errors are logged for debugging, consider distinguishing between error types for clearer error reporting:

💡 Optional: Differentiate error types in error handling
 export async function getPageContent(filename: string, locale?: string): Promise<PageContent> {
   // Prepend locale to path: "legal/privacy" -> "fr/legal/privacy"
   const filePath = locale
     ? path.join(contentDirectory, locale, `${filename}.mdx`)
     : path.join(contentDirectory, `${filename}.mdx`)
 
   try {
     if (!fs.existsSync(filePath)) {
-      throw new Error(`File not found: ${filePath}`)
+      console.error(`MDX file not found: ${filePath}`)
+      return {
+        frontmatter: {
+          title: "Content Not Found",
+          description: "The requested content could not be found",
+          lastUpdated: new Date().toISOString(),
+        },
+        content: <p>The requested content could not be found.</p>,
+      }
     }
 
     const fileContents = fs.readFileSync(filePath, "utf8")
     const { frontmatter, content } = await evaluate<PageContent["frontmatter"]>({
       source: fileContents,
       options: { parseFrontmatter: true },
     })
 
     const mdxProps: MDXProps = {
       components: {},
       ...((content as React.ReactElement).props || {}),
     }
 
     const clonedContent = React.cloneElement(content as React.ReactElement<MDXProps>, mdxProps)
 
     return {
       frontmatter,
       content: <CustomMDX>{clonedContent}</CustomMDX>,
     }
   } catch (error) {
-    console.error(`Error reading file ${filePath}:`, error)
+    console.error(`Error processing MDX file ${filePath}:`, error)
     return {
       frontmatter: {
-        title: "Error",
-        description: "Unable to load content",
+        title: "Processing Error",
+        description: "An error occurred while processing the content",
         lastUpdated: new Date().toISOString(),
       },
-      content: <p>There was an error loading the content.</p>,
+      content: <p>There was an error processing the content.</p>,
     }
   }
 }
apps/web/src/components/marketing/npo/donations/index.tsx (2)

21-25: Consider using interpolation instead of string concatenation for toast messages.

The current pattern of concatenating prefix + amount + suffix may not work correctly for all languages due to different word orders. Consider using a single translation key with interpolation support (e.g., t("toast.description", { amount })), which is more i18n-friendly.

🔎 Suggested approach
-      const amount = selectedAmount || customAmount;
       toast({
         title: t("toast.title"),
-        description: `${t("toast.description-prefix")} ${amount}€ ${t("toast.description-suffix")}`,
+        description: t("toast.description", { amount: selectedAmount || customAmount }),
       });

Then update the locale file to use interpolation:

{
  "toast": {
    "description": "Thank you for your donation of {{amount}}€!"
  }
}

68-70: Redundant setCustomAmount("") call.

When customAmount is falsy, setting it to an empty string has no effect since it's already empty (initialized as "" on line 13).

🔎 Proposed fix
           onClick={() => {
             setSelectedAmount(null);
-            if (!customAmount) setCustomAmount("");
           }}
apps/web/src/components/marketing/landing/customer/featured/index.tsx (1)

20-24: Title parsing is fragile if translation format changes.

The split("<accent>") and split("</accent>") approach will produce unexpected results if the translation doesn't contain the expected tags (e.g., returns undefined from array access).

🔎 Proposed safer approach
+const parseAccentedTitle = (title: string) => {
+  const match = title.match(/^(.*?)<accent>(.*?)<\/accent>(.*)$/);
+  if (!match) return { before: title, accent: "", after: "" };
+  return { before: match[1], accent: match[2], after: match[3] };
+};
+
 const Featured: React.FC = async () => {
   const { t } = await getTranslation("components/marketing/landing/customer/featured");
+  const titleParts = parseAccentedTitle(t("title"));

   return (
     // ...
           <h1 className="font-semibold text-5xl md:text-6xl text-foreground text-center">
             <Balancer>
-              {t("title").split("<accent>")[0]}
-              <span className="text-onruntime-magenta inline">
-                {t("title").split("<accent>")[1]?.split("</accent>")[0]}
-              </span>
-              {t("title").split("</accent>")[1]}
+              {titleParts.before}
+              {titleParts.accent && (
+                <span className="text-onruntime-magenta inline">
+                  {titleParts.accent}
+                </span>
+              )}
+              {titleParts.after}
             </Balancer>
           </h1>
apps/web/src/components/marketing/landing/visitor/featured/index.tsx (1)

22-27: Consider more robust title parsing.

The title splitting logic relies on translation strings containing <accent> and </accent> markers. If a translation is missing these markers or formats them incorrectly, the rendering could break or display oddly.

💡 Alternative approach using separate translation keys

Instead of embedding markup in translations, consider splitting into separate keys:

- {t("title").split("<accent>")[0]}
- <span className="text-onruntime-blue inline">
-   {t("title").split("<accent>")[1]?.split("</accent>")[0]}
- </span>
- {t("title").split("</accent>")[1]}
+ {t("title.before")}
+ <span className="text-onruntime-blue inline">
+   {t("title.accent")}
+ </span>
+ {t("title.after")}

This makes translations more maintainable and less error-prone.

apps/web/src/screens/marketing/projects/details/index.tsx (1)

199-205: Consider a more scalable locale mapping for date formatting.

The current ternary works for two locales but won't scale if more are added. Consider creating a locale mapping utility or using the locale value directly if it matches standard BCP 47 tags.

apps/web/src/components/marketing/agency/local-portfolio.tsx (1)

74-74: Parent component should be async to properly render async children.

LocalPortfolio is a synchronous component that renders LocalPortfolioProject which is async. While this may work in some cases with React Server Components, for consistency and to avoid potential hydration issues, consider making LocalPortfolio async as well.

🔎 Proposed fix
-const LocalPortfolio: React.FC<LocalPortfolioProps> = ({ agency }) => {
+const LocalPortfolio: React.FC<LocalPortfolioProps> = async ({ agency }) => {
apps/web/src/locales/en/components/marketing/landing/visitor/team.json (1)

2-3: Polish the English phrasing for clarity.

The current text has some grammatical quirks:

  • Line 2: "to shine" reads as an incomplete infinitive; consider "that shines" or "ready to shine your projects."
  • Line 3: Starting with "Composed of..." creates a sentence fragment. Consider: "We're composed of..." or "Our team is composed of..."
🔎 Proposed refinement
 {
-  "title": "A team of passionate creatives to shine.",
-  "description": "Composed of varied talents, from developers to passionate designers. Each brings their unique touch to our projects. Together, we form a collective where innovation is at the heart of everything we do.",
+  "title": "A team of passionate creatives ready to shine.",
+  "description": "We're composed of varied talents, from developers to passionate designers. Each brings their unique touch to our projects. Together, we form a collective where innovation is at the heart of everything we do.",
   "stats": {
     "experience": "Years of experience",
     "projects": "Projects completed",
packages/translations/src/next/hooks/use-pathname.ts (1)

11-19: Consider adding edge case handling for empty or malformed paths.

The logic correctly strips locale prefixes in typical cases. However, consider these edge cases:

  • Empty pathname "" results in an empty segments array where segments[0] is undefined
  • While this likely works (returns the original pathname), explicit handling would improve robustness
🔎 Proposed defensive check
 export const usePathname = () => {
   const pathname = useNextPathname();
   const { locales } = useTranslationContext();
 
+  if (!pathname || pathname === "/") {
+    return pathname;
+  }
+
   const segments = pathname.split("/").filter(Boolean);
   const hasLocalePrefix = locales.includes(segments[0]);
 
   return hasLocalePrefix ? `/${segments.slice(1).join("/")}` : pathname;
 };
apps/web/src/app/[locale]/projects/[id]/page.tsx (1)

27-35: Consider a smarter truncation for metadata description.

Using .slice(0, 100) on the short-description may cut mid-word or mid-sentence, resulting in awkward metadata. Consider truncating at a word boundary or adding an ellipsis.

🔎 Proposed truncation helper
-      shortDescription: tProject("short-description").slice(0, 100),
+      shortDescription: truncateText(tProject("short-description"), 100),

Add a helper function:

function truncateText(text: string, maxLength: number): string {
  if (text.length <= maxLength) return text;
  const truncated = text.slice(0, maxLength);
  const lastSpace = truncated.lastIndexOf(" ");
  return lastSpace > 0 ? truncated.slice(0, lastSpace) + "…" : truncated + "…";
}
apps/web/src/lib/utils/careers.ts (1)

5-7: Update docstring to reflect new behavior.

The docstring says "Formats salary with locale-aware number formatting" but the function now returns a raw object instead of a formatted string. The formatting responsibility has been moved to the consumer.

🔎 Suggested docstring update
 /**
- * Formats salary with locale-aware number formatting
+ * Extracts and normalizes salary data for locale-aware formatting by consumers
  */
packages/translations/src/next/contexts/pages-context.tsx (1)

29-39: Consider adding SameSite attribute to cookie.

The cookie lacks an explicit SameSite attribute. While modern browsers default to SameSite=Lax, explicitly setting it improves clarity and avoids potential browser console warnings.

🔎 Suggested cookie update
-      document.cookie = `NEXT_LOCALE=${newLocale};path=/;max-age=31536000`;
+      document.cookie = `NEXT_LOCALE=${newLocale};path=/;max-age=31536000;SameSite=Lax`;
apps/web/src/app/[locale]/careers/[id]/page.tsx (1)

9-27: Consider deduplicating the job fetch between generateMetadata and Page.

The job is fetched twice per request: once in generateMetadata and once in Page. While the underlying API route uses unstable_cache, you could wrap getJobById with React's cache() to deduplicate within the same request.

🔎 Proposed optimization
+import { cache } from "react";
+
-async function getJobById(id: string, locale: string): Promise<JobPosting | null> {
+const getJobById = cache(async (id: string, locale: string): Promise<JobPosting | null> => {
   try {
     const baseUrl = env.NEXT_PUBLIC_APP_URL;
     // ...
   } catch (error) {
     console.error("Error fetching job:", error);
     return null;
   }
-}
+});
apps/web/src/screens/marketing/careers/job-details.tsx (2)

46-59: Consider using locale-aware number formatting.

The formatSalary function uses toLocaleString() without specifying a locale, which will use the browser's default. For consistent i18n behavior, consider passing the current locale.

🔎 Proposed improvement
-const formatSalary = (salary: JobPosting["salary"]) => {
+const formatSalary = (salary: JobPosting["salary"], locale?: string) => {
   if (!salary) return null;
   const { min, max, currency } = salary;
   const symbol = currency === "EUR" ? "€" : currency;

   if (min && max) {
-    return `${min.toLocaleString()} ${t("salary-format.range")} ${max.toLocaleString()} ${symbol}`;
+    return `${min.toLocaleString(locale)} ${t("salary-format.range")} ${max.toLocaleString(locale)} ${symbol}`;
   } else if (min) {
-    return `${t("salary-format.from")} ${min.toLocaleString()} ${symbol}`;
+    return `${t("salary-format.from")} ${min.toLocaleString(locale)} ${symbol}`;
   } else if (max) {
-    return `${t("salary-format.up-to")} ${max.toLocaleString()} ${symbol}`;
+    return `${t("salary-format.up-to")} ${max.toLocaleString(locale)} ${symbol}`;
   }
   return null;
 };

72-72: Replace alert() with a toast or non-blocking notification.

Using alert() blocks the UI thread and provides a poor user experience. Consider using a toast notification library that's likely already in use elsewhere in the app.

apps/web/src/lib/utils/metadata.ts (2)

150-154: Locale check uses array includes which may have type issues.

The locales array is typed and segments[0] could be undefined if the path is /. This would pass TypeScript but could behave unexpectedly.

🔎 Proposed improvement
 const segments = normalizedPath.split("/").filter(Boolean);
-const hasLocalePrefix = locales.includes(segments[0]);
+const hasLocalePrefix = segments[0] ? locales.includes(segments[0]) : false;

134-161: Add logging to understand which pathname fallback is being used in production.

The custom headers x-pathname and x-invoke-path are referenced but never set anywhere in the codebase—there's no middleware, proxy configuration, or header setup that populates them. The function will reliably fall back to the referer header or "/", but this means the canonical URL will likely be generated from referer (which may be from a different origin or missing altogether) or default to "/" for most requests. Consider adding a debug log or warning when falling back to "/" to make it visible when pathname information is lost.

apps/web/src/app/api/careers/[id]/route.ts (1)

6-41: Potential caching inefficiency: unstable_cache instantiated on every request.

Creating a new unstable_cache wrapper on each call to getCachedJob(locale) may defeat the caching optimization. The cache key ["join-job", locale] should ensure correct behavior, but instantiating the wrapper per-request adds overhead.

Consider memoizing the cached function per locale or restructuring to include locale in the cache key array while keeping a single unstable_cache instance:

🔎 Proposed refactor
-const getCachedJob = (locale: string) =>
-  unstable_cache(
-    async (jobId: string) => {
-      const job = await joinClient.job(jobId, locale);
+const getCachedJob = unstable_cache(
+  async (jobId: string, locale: string) => {
+    const job = await joinClient.job(jobId, locale);
+    
+    if (!job) {
+      return null;
+    }
 
-      if (!job) {
-        return null;
-      }
-
-      return {
-        // ... transformation
-      };
-    },
-    ["join-job", locale],
-    {
-      revalidate: 300,
-      tags: ["careers"],
-    }
-  );
+    return {
+      // ... transformation
+    };
+  },
+  ["join-job"],
+  {
+    revalidate: 300,
+    tags: ["careers"],
+  }
+);

Then call it as:

const job = await getCachedJob(id, locale);

This aligns with the pattern in apps/web/src/app/api/careers/route.ts at lines 31-48, where a similar curried pattern is used. You may want to apply this refactor consistently across both routes.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a4d48ec and 72a6e91.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (149)
  • apps/web/package.json
  • apps/web/src/app/[locale]/(landing)/customer/page.ts
  • apps/web/src/app/[locale]/(landing)/page.ts
  • apps/web/src/app/[locale]/(legal)/company/page.tsx
  • apps/web/src/app/[locale]/(legal)/privacy/page.tsx
  • apps/web/src/app/[locale]/(legal)/terms/page.tsx
  • apps/web/src/app/[locale]/agency/[city]/page.tsx
  • apps/web/src/app/[locale]/agency/page.tsx
  • apps/web/src/app/[locale]/careers/[id]/page.tsx
  • apps/web/src/app/[locale]/careers/page.tsx
  • apps/web/src/app/[locale]/contact/page.tsx
  • apps/web/src/app/[locale]/glossary/[letter]/[term]/page.tsx
  • apps/web/src/app/[locale]/glossary/[letter]/page.tsx
  • apps/web/src/app/[locale]/glossary/page.tsx
  • apps/web/src/app/[locale]/glossary/tag/[tag]/page.tsx
  • apps/web/src/app/[locale]/glossary/tags/page.tsx
  • apps/web/src/app/[locale]/layout.tsx
  • apps/web/src/app/[locale]/npo/page.tsx
  • apps/web/src/app/[locale]/projects/[id]/page.tsx
  • apps/web/src/app/[locale]/projects/page.tsx
  • apps/web/src/app/[locale]/services/[category]/[service]/page.tsx
  • apps/web/src/app/[locale]/services/[category]/page.tsx
  • apps/web/src/app/[locale]/services/page.tsx
  • apps/web/src/app/api/careers/[id]/route.ts
  • apps/web/src/app/api/careers/route.ts
  • apps/web/src/components/custom-mdx.tsx
  • apps/web/src/components/glossary/entry-page.tsx
  • apps/web/src/components/layout/footer/footer.tsx
  • apps/web/src/components/marketing/agency/local-portfolio.tsx
  • apps/web/src/components/marketing/landing/customer/featured/index.tsx
  • apps/web/src/components/marketing/landing/page-switcher/index.tsx
  • apps/web/src/components/marketing/landing/visitor/featured/index.tsx
  • apps/web/src/components/marketing/landing/visitor/featured/partners/index.tsx
  • apps/web/src/components/marketing/landing/visitor/projects/card/index.tsx
  • apps/web/src/components/marketing/landing/visitor/projects/index.tsx
  • apps/web/src/components/marketing/landing/visitor/team/grid/card/index.tsx
  • apps/web/src/components/marketing/landing/visitor/team/index.tsx
  • apps/web/src/components/marketing/legal/page.tsx
  • apps/web/src/components/marketing/navbar/index.tsx
  • apps/web/src/components/marketing/navbar/navigation/agencies/index.tsx
  • apps/web/src/components/marketing/navbar/navigation/index.tsx
  • apps/web/src/components/marketing/navbar/navigation/projects/index.tsx
  • apps/web/src/components/marketing/navbar/navigation/services/index.tsx
  • apps/web/src/components/marketing/npo/donations/index.tsx
  • apps/web/src/components/marketing/projects/sections/index.tsx
  • apps/web/src/components/marketing/services/sections/index.tsx
  • apps/web/src/components/marketing/services/service-layout.tsx
  • apps/web/src/components/marketing/services/service-overview.tsx
  • apps/web/src/components/ui/dropdown-menu.tsx
  • apps/web/src/constants/projects/dark-theme-instagram.ts
  • apps/web/src/constants/projects/expatfacilities.ts
  • apps/web/src/constants/projects/kartrak.ts
  • apps/web/src/constants/projects/kitchn.ts
  • apps/web/src/constants/projects/shadowbonus.ts
  • apps/web/src/constants/projects/tonightpass.ts
  • apps/web/src/content/en/legal/company.mdx
  • apps/web/src/content/en/legal/privacy.mdx
  • apps/web/src/content/en/legal/terms.mdx
  • apps/web/src/content/fr/legal/company.mdx
  • apps/web/src/content/fr/legal/privacy.mdx
  • apps/web/src/content/fr/legal/terms.mdx
  • apps/web/src/lib/mdx.tsx
  • apps/web/src/lib/utils/careers.ts
  • apps/web/src/lib/utils/metadata.ts
  • apps/web/src/locales/README.md
  • apps/web/src/locales/en/app/careers/[id]/page.json
  • apps/web/src/locales/en/app/careers/page.json
  • apps/web/src/locales/en/app/contact/page.json
  • apps/web/src/locales/en/app/landing/customer/page.json
  • apps/web/src/locales/en/app/landing/page.json
  • apps/web/src/locales/en/app/npo/page.json
  • apps/web/src/locales/en/app/projects/[id]/page.json
  • apps/web/src/locales/en/app/projects/page.json
  • apps/web/src/locales/en/components/marketing/careers/job-details.json
  • apps/web/src/locales/en/components/marketing/careers/job-list.json
  • apps/web/src/locales/en/components/marketing/landing/customer/featured.json
  • apps/web/src/locales/en/components/marketing/landing/page-switcher.json
  • apps/web/src/locales/en/components/marketing/landing/visitor/featured.json
  • apps/web/src/locales/en/components/marketing/landing/visitor/projects.json
  • apps/web/src/locales/en/components/marketing/landing/visitor/team.json
  • apps/web/src/locales/en/components/marketing/legal/page.json
  • apps/web/src/locales/en/components/marketing/npo/donations.json
  • apps/web/src/locales/en/components/marketing/projects/card.json
  • apps/web/src/locales/en/components/marketing/projects/sections.json
  • apps/web/src/locales/en/constants/projects/dark-theme-instagram.json
  • apps/web/src/locales/en/constants/projects/expatfacilities.json
  • apps/web/src/locales/en/constants/projects/kartrak.json
  • apps/web/src/locales/en/constants/projects/kitchn.json
  • apps/web/src/locales/en/constants/projects/shadowbonus.json
  • apps/web/src/locales/en/constants/projects/tonightpass.json
  • apps/web/src/locales/en/layout/footer.json
  • apps/web/src/locales/en/layout/navbar.json
  • apps/web/src/locales/en/screens/marketing/careers.json
  • apps/web/src/locales/en/screens/marketing/contact.json
  • apps/web/src/locales/en/screens/marketing/npo.json
  • apps/web/src/locales/en/screens/marketing/projects.json
  • apps/web/src/locales/en/screens/marketing/projects/details.json
  • apps/web/src/locales/fr/app/careers/[id]/page.json
  • apps/web/src/locales/fr/app/careers/page.json
  • apps/web/src/locales/fr/app/contact/page.json
  • apps/web/src/locales/fr/app/landing/customer/page.json
  • apps/web/src/locales/fr/app/landing/page.json
  • apps/web/src/locales/fr/app/npo/page.json
  • apps/web/src/locales/fr/app/projects/[id]/page.json
  • apps/web/src/locales/fr/app/projects/page.json
  • apps/web/src/locales/fr/components/marketing/careers/job-details.json
  • apps/web/src/locales/fr/components/marketing/careers/job-list.json
  • apps/web/src/locales/fr/components/marketing/landing/customer/featured.json
  • apps/web/src/locales/fr/components/marketing/landing/page-switcher.json
  • apps/web/src/locales/fr/components/marketing/landing/visitor/featured.json
  • apps/web/src/locales/fr/components/marketing/landing/visitor/projects.json
  • apps/web/src/locales/fr/components/marketing/landing/visitor/team.json
  • apps/web/src/locales/fr/components/marketing/legal/page.json
  • apps/web/src/locales/fr/components/marketing/npo/donations.json
  • apps/web/src/locales/fr/components/marketing/projects/card.json
  • apps/web/src/locales/fr/components/marketing/projects/sections.json
  • apps/web/src/locales/fr/constants/projects/dark-theme-instagram.json
  • apps/web/src/locales/fr/constants/projects/expatfacilities.json
  • apps/web/src/locales/fr/constants/projects/kartrak.json
  • apps/web/src/locales/fr/constants/projects/kitchn.json
  • apps/web/src/locales/fr/constants/projects/shadowbonus.json
  • apps/web/src/locales/fr/constants/projects/tonightpass.json
  • apps/web/src/locales/fr/layout/footer.json
  • apps/web/src/locales/fr/screens/marketing/careers.json
  • apps/web/src/locales/fr/screens/marketing/contact.json
  • apps/web/src/locales/fr/screens/marketing/npo.json
  • apps/web/src/locales/fr/screens/marketing/projects.json
  • apps/web/src/locales/fr/screens/marketing/projects/details.json
  • apps/web/src/mdx-components.tsx
  • apps/web/src/proxy.ts
  • apps/web/src/screens/marketing/careers/index.tsx
  • apps/web/src/screens/marketing/careers/job-details.tsx
  • apps/web/src/screens/marketing/careers/job-list.tsx
  • apps/web/src/screens/marketing/contact/index.tsx
  • apps/web/src/screens/marketing/npo/index.tsx
  • apps/web/src/screens/marketing/projects/details/index.tsx
  • apps/web/src/screens/marketing/projects/index.tsx
  • apps/web/src/screens/marketing/services/index.tsx
  • apps/web/src/services/join.ts
  • apps/web/src/types/job.ts
  • apps/web/src/types/project.ts
  • examples/translations/next-app/proxy.ts
  • packages/translations/README.md
  • packages/translations/src/next/app-provider.tsx
  • packages/translations/src/next/components/link.tsx
  • packages/translations/src/next/contexts/app-context.tsx
  • packages/translations/src/next/contexts/pages-context.tsx
  • packages/translations/src/next/hooks/use-pathname.ts
  • packages/translations/src/next/index.ts
💤 Files with no reviewable changes (1)
  • packages/translations/src/next/app-provider.tsx
✅ Files skipped from review due to trivial changes (27)
  • apps/web/src/locales/fr/app/landing/customer/page.json
  • apps/web/src/content/en/legal/privacy.mdx
  • apps/web/src/locales/en/app/landing/customer/page.json
  • apps/web/src/locales/fr/components/marketing/landing/page-switcher.json
  • apps/web/src/locales/en/components/marketing/landing/visitor/projects.json
  • apps/web/src/locales/en/components/marketing/careers/job-details.json
  • apps/web/src/locales/en/screens/marketing/projects/details.json
  • apps/web/src/locales/en/components/marketing/careers/job-list.json
  • apps/web/src/app/[locale]/services/[category]/[service]/page.tsx
  • apps/web/src/locales/fr/components/marketing/legal/page.json
  • apps/web/src/locales/fr/constants/projects/kartrak.json
  • apps/web/src/locales/en/app/projects/[id]/page.json
  • apps/web/src/locales/en/constants/projects/expatfacilities.json
  • apps/web/src/locales/en/screens/marketing/careers.json
  • apps/web/src/locales/README.md
  • apps/web/src/locales/fr/screens/marketing/careers.json
  • apps/web/src/locales/en/screens/marketing/contact.json
  • apps/web/src/locales/fr/app/contact/page.json
  • apps/web/src/locales/fr/components/marketing/careers/job-list.json
  • apps/web/src/locales/en/constants/projects/tonightpass.json
  • apps/web/src/locales/fr/constants/projects/expatfacilities.json
  • apps/web/src/locales/fr/app/careers/page.json
  • apps/web/src/locales/fr/screens/marketing/projects/details.json
  • apps/web/src/locales/fr/components/marketing/projects/card.json
  • apps/web/src/locales/fr/app/npo/page.json
  • apps/web/src/locales/en/components/marketing/projects/sections.json
  • apps/web/src/locales/fr/components/marketing/npo/donations.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/web/package.json
  • apps/web/src/locales/en/layout/footer.json
  • apps/web/src/components/marketing/navbar/navigation/services/index.tsx
  • apps/web/src/locales/en/layout/navbar.json
  • apps/web/src/locales/fr/layout/footer.json
🧰 Additional context used
📓 Path-based instructions (2)
apps/web/src/app/**/route.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Next.js unstable_cache for caching external API responses in API routes

Files:

  • apps/web/src/app/api/careers/[id]/route.ts
  • apps/web/src/app/api/careers/route.ts
apps/web/src/services/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use lazy initialization for external API clients to avoid build failures when environment variables are missing in CI, via Proxy pattern or client object pattern

Files:

  • apps/web/src/services/join.ts
🧠 Learnings (2)
📚 Learning: 2025-12-28T00:50:00.153Z
Learnt from: CR
Repo: onRuntime/onruntime PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T00:50:00.153Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : Use Next.js unstable_cache for caching external API responses in API routes

Applied to files:

  • apps/web/src/app/[locale]/(landing)/page.ts
  • examples/translations/next-app/proxy.ts
  • apps/web/src/app/[locale]/careers/[id]/page.tsx
  • apps/web/src/app/api/careers/[id]/route.ts
  • apps/web/src/components/marketing/navbar/index.tsx
  • apps/web/src/app/api/careers/route.ts
  • apps/web/src/proxy.ts
📚 Learning: 2025-12-28T00:50:00.153Z
Learnt from: CR
Repo: onRuntime/onruntime PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T00:50:00.153Z
Learning: Applies to apps/web/src/services/**/*.{ts,tsx} : Use lazy initialization for external API clients to avoid build failures when environment variables are missing in CI, via Proxy pattern or client object pattern

Applied to files:

  • apps/web/src/proxy.ts
🧬 Code graph analysis (33)
apps/web/src/app/[locale]/careers/page.tsx (10)
apps/web/src/app/[locale]/careers/[id]/page.tsx (1)
  • generateMetadata (29-50)
apps/web/src/app/[locale]/projects/page.tsx (1)
  • generateMetadata (5-12)
apps/web/src/app/[locale]/contact/page.tsx (1)
  • generateMetadata (5-11)
apps/web/src/app/[locale]/npo/page.tsx (1)
  • generateMetadata (5-11)
apps/web/src/app/[locale]/(landing)/page.ts (1)
  • generateMetadata (5-11)
apps/web/src/app/[locale]/agency/page.tsx (1)
  • generateMetadata (13-18)
apps/web/src/app/[locale]/glossary/[letter]/page.tsx (1)
  • generateMetadata (13-30)
apps/web/src/app/[locale]/glossary/page.tsx (1)
  • generateMetadata (13-18)
apps/web/src/app/[locale]/glossary/tag/[tag]/page.tsx (1)
  • generateMetadata (13-23)
apps/web/src/lib/utils/metadata.ts (1)
  • constructMetadata (41-128)
apps/web/src/components/layout/footer/footer.tsx (2)
apps/web/src/components/ui/dropdown-menu.tsx (4)
  • DropdownMenu (186-186)
  • DropdownMenuTrigger (187-187)
  • DropdownMenuContent (188-188)
  • DropdownMenuItem (189-189)
apps/web/src/lib/translations.ts (1)
  • locales (3-3)
apps/web/src/components/marketing/landing/visitor/projects/card/index.tsx (2)
apps/web/src/types/project.ts (1)
  • Project (51-78)
packages/translations/src/next/components/link.tsx (1)
  • Link (14-57)
packages/translations/src/next/contexts/app-context.tsx (3)
packages/translations/src/next/index.ts (3)
  • AppTranslationProviderProps (9-9)
  • AppTranslationProvider (8-8)
  • usePathname (16-16)
packages/translations/src/core/types.ts (1)
  • TranslationLoader (12-15)
packages/translations/src/next/hooks/use-pathname.ts (1)
  • usePathname (11-19)
apps/web/src/app/[locale]/glossary/[letter]/[term]/page.tsx (2)
apps/web/src/app/[locale]/glossary/[letter]/page.tsx (1)
  • generateMetadata (13-30)
apps/web/src/app/[locale]/glossary/page.tsx (1)
  • generateMetadata (13-18)
examples/translations/next-app/proxy.ts (2)
examples/translations/next-app/lib/translations.ts (2)
  • locales (3-3)
  • defaultLocale (4-4)
apps/web/src/proxy.ts (2)
  • proxy (34-73)
  • config (75-77)
apps/web/src/app/[locale]/glossary/tags/page.tsx (5)
apps/web/src/app/[locale]/npo/page.tsx (1)
  • generateMetadata (5-11)
apps/web/src/app/[locale]/glossary/[letter]/page.tsx (1)
  • generateMetadata (13-30)
apps/web/src/app/[locale]/glossary/page.tsx (1)
  • generateMetadata (13-18)
apps/web/src/app/[locale]/glossary/tag/[tag]/page.tsx (1)
  • generateMetadata (13-23)
apps/web/src/lib/utils/metadata.ts (1)
  • constructMetadata (41-128)
apps/web/src/components/marketing/landing/customer/featured/index.tsx (2)
packages/translations/src/next/components/link.tsx (1)
  • Link (14-57)
packages/translations/src/next/index.ts (1)
  • Link (13-13)
apps/web/src/app/[locale]/projects/page.tsx (13)
apps/web/src/app/[locale]/careers/[id]/page.tsx (1)
  • generateMetadata (29-50)
apps/web/src/app/[locale]/projects/[id]/page.tsx (1)
  • generateMetadata (11-36)
apps/web/src/app/[locale]/careers/page.tsx (1)
  • generateMetadata (5-12)
apps/web/src/app/[locale]/contact/page.tsx (1)
  • generateMetadata (5-11)
apps/web/src/app/[locale]/npo/page.tsx (1)
  • generateMetadata (5-11)
apps/web/src/app/[locale]/(landing)/customer/page.ts (1)
  • generateMetadata (5-11)
apps/web/src/app/[locale]/(landing)/page.ts (1)
  • generateMetadata (5-11)
apps/web/src/app/[locale]/agency/page.tsx (1)
  • generateMetadata (13-18)
apps/web/src/app/[locale]/glossary/[letter]/[term]/page.tsx (1)
  • generateMetadata (14-31)
apps/web/src/app/[locale]/glossary/page.tsx (1)
  • generateMetadata (13-18)
apps/web/src/app/[locale]/glossary/tag/[tag]/page.tsx (1)
  • generateMetadata (13-23)
apps/web/src/app/[locale]/glossary/tags/page.tsx (1)
  • generateMetadata (8-13)
apps/web/src/lib/utils/metadata.ts (1)
  • constructMetadata (41-128)
apps/web/src/app/[locale]/contact/page.tsx (8)
apps/web/src/app/[locale]/projects/page.tsx (1)
  • generateMetadata (5-12)
apps/web/src/app/[locale]/careers/page.tsx (1)
  • generateMetadata (5-12)
apps/web/src/app/[locale]/npo/page.tsx (1)
  • generateMetadata (5-11)
apps/web/src/app/[locale]/(landing)/page.ts (1)
  • generateMetadata (5-11)
apps/web/src/app/[locale]/agency/page.tsx (1)
  • generateMetadata (13-18)
apps/web/src/app/[locale]/glossary/page.tsx (1)
  • generateMetadata (13-18)
apps/web/src/app/[locale]/glossary/tags/page.tsx (1)
  • generateMetadata (8-13)
apps/web/src/lib/utils/metadata.ts (1)
  • constructMetadata (41-128)
apps/web/src/app/[locale]/glossary/tag/[tag]/page.tsx (3)
apps/web/src/app/[locale]/careers/[id]/page.tsx (1)
  • generateMetadata (29-50)
apps/web/src/app/[locale]/glossary/[letter]/[term]/page.tsx (1)
  • generateMetadata (14-31)
apps/web/src/app/[locale]/glossary/[letter]/page.tsx (1)
  • generateMetadata (13-30)
apps/web/src/components/marketing/landing/visitor/projects/index.tsx (2)
packages/translations/src/next/components/link.tsx (1)
  • Link (14-57)
packages/translations/src/next/index.ts (1)
  • Link (13-13)
apps/web/src/components/marketing/projects/sections/index.tsx (2)
packages/translations/src/next/components/link.tsx (1)
  • Link (14-57)
packages/translations/src/next/index.ts (1)
  • Link (13-13)
apps/web/src/components/marketing/landing/page-switcher/index.tsx (3)
packages/translations/src/next/hooks/use-pathname.ts (1)
  • usePathname (11-19)
packages/translations/src/next/index.ts (2)
  • usePathname (16-16)
  • Link (13-13)
packages/translations/src/next/components/link.tsx (1)
  • Link (14-57)
apps/web/src/screens/marketing/npo/index.tsx (2)
packages/translations/src/next/components/link.tsx (1)
  • Link (14-57)
packages/translations/src/next/index.ts (1)
  • Link (13-13)
apps/web/src/components/marketing/navbar/navigation/index.tsx (2)
packages/translations/src/next/components/link.tsx (1)
  • Link (14-57)
packages/translations/src/next/index.ts (1)
  • Link (13-13)
apps/web/src/app/[locale]/careers/[id]/page.tsx (5)
apps/web/src/types/job.ts (1)
  • JobPosting (1-39)
apps/web/src/app/[locale]/careers/page.tsx (1)
  • generateMetadata (5-12)
apps/web/src/app/[locale]/contact/page.tsx (1)
  • generateMetadata (5-11)
apps/web/src/app/[locale]/npo/page.tsx (1)
  • generateMetadata (5-11)
apps/web/src/lib/utils/metadata.ts (1)
  • constructMetadata (41-128)
packages/translations/src/next/hooks/use-pathname.ts (1)
packages/translations/src/next/index.ts (1)
  • usePathname (16-16)
apps/web/src/app/[locale]/services/page.tsx (3)
apps/web/src/app/[locale]/services/[category]/[service]/page.tsx (1)
  • generateMetadata (25-41)
apps/web/src/app/[locale]/services/[category]/page.tsx (1)
  • generateMetadata (14-30)
apps/web/src/lib/utils/metadata.ts (1)
  • constructMetadata (41-128)
apps/web/src/lib/utils/metadata.ts (1)
apps/web/env.ts (1)
  • env (4-24)
apps/web/src/app/[locale]/glossary/[letter]/page.tsx (3)
apps/web/src/app/[locale]/glossary/[letter]/[term]/page.tsx (1)
  • generateMetadata (14-31)
apps/web/src/app/[locale]/glossary/page.tsx (1)
  • generateMetadata (13-18)
apps/web/src/app/[locale]/glossary/tag/[tag]/page.tsx (1)
  • generateMetadata (13-23)
apps/web/src/components/marketing/agency/local-portfolio.tsx (1)
apps/web/src/types/project.ts (1)
  • Project (51-78)
apps/web/src/app/[locale]/(legal)/terms/page.tsx (3)
apps/web/src/app/[locale]/(legal)/company/page.tsx (1)
  • generateMetadata (12-19)
apps/web/src/app/[locale]/(legal)/privacy/page.tsx (1)
  • generateMetadata (12-19)
apps/web/src/lib/mdx.tsx (1)
  • getPageContent (19-58)
apps/web/src/screens/marketing/careers/index.tsx (2)
packages/translations/src/next/components/link.tsx (1)
  • Link (14-57)
packages/translations/src/next/index.ts (1)
  • Link (13-13)
apps/web/src/components/marketing/landing/visitor/featured/index.tsx (2)
packages/translations/src/next/components/link.tsx (1)
  • Link (14-57)
packages/translations/src/next/index.ts (1)
  • Link (13-13)
apps/web/src/app/api/careers/[id]/route.ts (3)
apps/web/src/services/join.ts (2)
  • job (79-101)
  • joinClient (58-102)
apps/web/src/lib/utils/careers.ts (2)
  • formatSalary (8-20)
  • extractTags (25-37)
apps/web/src/app/api/careers/route.ts (1)
  • GET (32-49)
apps/web/src/components/marketing/navbar/index.tsx (2)
packages/translations/src/next/components/link.tsx (1)
  • Link (14-57)
packages/translations/src/next/index.ts (1)
  • Link (13-13)
apps/web/src/app/[locale]/agency/[city]/page.tsx (1)
apps/web/src/lib/utils/metadata.ts (1)
  • constructMetadata (41-128)
apps/web/src/app/[locale]/glossary/page.tsx (9)
apps/web/src/app/[locale]/projects/page.tsx (1)
  • generateMetadata (5-12)
apps/web/src/app/[locale]/careers/page.tsx (1)
  • generateMetadata (5-12)
apps/web/src/app/[locale]/contact/page.tsx (1)
  • generateMetadata (5-11)
apps/web/src/app/[locale]/npo/page.tsx (1)
  • generateMetadata (5-11)
apps/web/src/app/[locale]/(landing)/page.ts (1)
  • generateMetadata (5-11)
apps/web/src/app/[locale]/agency/page.tsx (1)
  • generateMetadata (13-18)
apps/web/src/app/[locale]/glossary/[letter]/[term]/page.tsx (1)
  • generateMetadata (14-31)
apps/web/src/app/[locale]/glossary/[letter]/page.tsx (1)
  • generateMetadata (13-30)
apps/web/src/app/[locale]/glossary/tags/page.tsx (1)
  • generateMetadata (8-13)
apps/web/src/app/api/careers/route.ts (1)
apps/web/src/services/join.ts (2)
  • jobs (59-77)
  • job (79-101)
apps/web/src/components/marketing/legal/page.tsx (2)
packages/translations/src/next/components/link.tsx (1)
  • Link (14-57)
packages/translations/src/next/index.ts (1)
  • Link (13-13)
apps/web/src/services/join.ts (1)
apps/web/env.ts (1)
  • env (4-24)
apps/web/src/app/[locale]/layout.tsx (2)
apps/web/src/lib/translations.ts (1)
  • locales (3-3)
apps/web/src/app/[locale]/providers.tsx (1)
  • Providers (13-19)
🪛 ast-grep (0.40.3)
apps/web/src/components/layout/footer/footer.tsx

[warning] 61-61: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

apps/web/src/components/marketing/landing/customer/featured/index.tsx

[warning] 29-29: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

apps/web/src/app/[locale]/layout.tsx

[warning] 75-75: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)


[warning] 92-92: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)
apps/web/src/components/layout/footer/footer.tsx

[error] 62-62: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

apps/web/src/components/marketing/landing/customer/featured/index.tsx

[error] 30-30: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

apps/web/src/app/[locale]/layout.tsx

[error] 76-76: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)


[error] 93-93: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

Copy link

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/translations/README.md (1)

141-143: Update matcher to match the actual implementation.

The documented matcher is missing exclusions for API routes and static files that are present in the actual proxy.ts implementation (line 83).

🔎 Proposed fix for matcher documentation
 export const config = {
-  matcher: ["/((?!_next|favicon.ico).*)"],
+  matcher: ["/((?!_next|favicon.ico|api|.*\\.).*)"],
 };
🧹 Nitpick comments (3)
apps/web/src/lib/utils/metadata.ts (2)

11-17: Consider validating locale input or documenting assumptions.

The toOgLocale function assumes simple locale codes like "en" or "fr". If a compound locale like "pt-BR" is passed, the result would be "pt-BR_PT-BR". While this may not be an issue with the current locale configuration, adding a check or documenting the expected input format would make the code more defensive.

🔎 Optional: Add validation to ensure locale is in the expected format
 function toOgLocale(locale: string): string {
+  // Ensure we're working with a simple locale code (e.g., "en", "fr")
+  const simpleLocale = locale.split(/[-_]/)[0];
   const countryOverrides: Record<string, string> = {
     en: "US", // English defaults to US
   };
-  const country = countryOverrides[locale] || locale.toUpperCase();
-  return `${locale}_${country}`;
+  const country = countryOverrides[simpleLocale] || simpleLocale.toUpperCase();
+  return `${simpleLocale}_${country}`;
 }

53-70: Good! This addresses the past review comment about dynamic locale.

The function now correctly derives locale, canonical URLs, and alternates from request context rather than using hardcoded values. The async signature aligns with Next.js 15+ requirements.

Consider adding error handling for robustness.

The async helper calls could potentially fail if headers are unavailable or malformed. Consider wrapping these in try-catch blocks with sensible fallbacks.

🔎 Recommended: Add defensive error handling
 } = {}): Promise<Metadata> {
-  const canonical = await generateCanonical();
-  const languages = await generateAlternates();
-  const currentLocale = await getCurrentLocale();
+  let canonical: string;
+  let languages: Record<string, string>;
+  let currentLocale: string;
+  
+  try {
+    canonical = await generateCanonical();
+    languages = await generateAlternates();
+    currentLocale = await getCurrentLocale();
+  } catch (error) {
+    // Fallback to defaults if headers are unavailable
+    console.error("Failed to generate locale-aware metadata:", error);
+    canonical = siteConfig.url;
+    languages = { [defaultLocale]: siteConfig.url, "x-default": siteConfig.url };
+    currentLocale = defaultLocale;
+  }
apps/web/src/proxy.ts (1)

43-43: Consider using .slice() for clearer prefix removal.

While .replace() works here due to the startsWith check, using .slice() more explicitly conveys the intent of removing a known prefix.

🔎 Suggested refactor
-    const newPathname = pathname.replace(`/${defaultLocale}`, "") || "/";
+    const newPathname = pathname.slice(`/${defaultLocale}`.length) || "/";
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 72a6e91 and 8654804.

📒 Files selected for processing (4)
  • apps/web/src/lib/utils/metadata.ts
  • apps/web/src/proxy.ts
  • examples/translations/next-app/proxy.ts
  • packages/translations/README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/translations/next-app/proxy.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-28T00:50:00.153Z
Learnt from: CR
Repo: onRuntime/onruntime PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T00:50:00.153Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : Use Next.js unstable_cache for caching external API responses in API routes

Applied to files:

  • apps/web/src/proxy.ts
📚 Learning: 2025-12-28T00:50:00.153Z
Learnt from: CR
Repo: onRuntime/onruntime PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T00:50:00.153Z
Learning: Applies to apps/web/src/services/**/*.{ts,tsx} : Use lazy initialization for external API clients to avoid build failures when environment variables are missing in CI, via Proxy pattern or client object pattern

Applied to files:

  • apps/web/src/proxy.ts
🧬 Code graph analysis (2)
apps/web/src/lib/utils/metadata.ts (1)
apps/web/env.ts (1)
  • env (4-24)
apps/web/src/proxy.ts (3)
apps/web/src/lib/translations.ts (2)
  • locales (3-3)
  • defaultLocale (4-4)
examples/translations/next-app/lib/translations.ts (2)
  • locales (3-3)
  • defaultLocale (4-4)
examples/translations/next-app/proxy.ts (2)
  • proxy (34-80)
  • config (82-84)
🔇 Additional comments (9)
apps/web/src/lib/utils/metadata.ts (4)

2-5: LGTM! Imports support i18n functionality.

The new imports properly support the locale-aware metadata generation. The headers import is necessary for detecting locale and pathname from request context, while the env and translations imports provide configuration.


22-22: LGTM! Using env module for better type safety.

Switching from process.env to the env module provides better type safety and validation while maintaining the same fallback behavior.


173-177: LGTM! Locale prefix stripping logic is correct.

The function correctly identifies and removes locale prefixes by checking against the valid locales array. The logic handles both localized paths (e.g., "/en/about") and non-localized paths (e.g., "/about") appropriately.


183-220: LGTM! Canonical and alternates generation follow SEO best practices.

Both helper functions are well-implemented:

  • generateCanonical properly normalizes URLs by handling multiple slashes and trailing slashes while preserving the protocol.
  • generateAlternates correctly implements the locale prefix strategy (no prefix for default locale, prefix for others) and includes the x-default fallback per hreflang best practices.

The edge case handling for root paths is also correct.

apps/web/src/proxy.ts (3)

72-79: LGTM! Clean default locale handling.

The rewrite logic correctly injects the default locale into the pathname and propagates it via headers without exposing it in the user's browser URL.


82-84: LGTM! Appropriate matcher configuration.

The matcher correctly excludes Next.js internals, API routes, and static assets while applying locale handling to page routes.


56-56: The x-pathname header is already consumed downstream.

The header is actively used in apps/web/src/lib/utils/metadata.ts at line 161, where it serves as the primary fallback for determining the pathname in a chain of alternatives (headersList.get("x-pathname")headersList.get("x-invoke-path")headersList.get("referer")"/"). This header is necessary and should be retained.

Likely an incorrect or invalid review comment.

packages/translations/README.md (2)

23-57: LGTM! Clear documentation of server/client separation.

The documentation clearly explains the separation between shared config and server-only translation helpers, with helpful notes about installing the server-only package to prevent accidental client imports.


186-197: LGTM! Usage example reflects the new server-side API correctly.

The example properly demonstrates importing from @/lib/translations.server, awaiting the async getTranslation() call, and destructuring the result.

Copy link

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/components/marketing/agency/local-expertise.tsx (1)

31-31: Dynamic Tailwind classes will not be generated.

Tailwind's JIT compiler scans source code at build time and cannot detect classes constructed with template literals. Classes like bg-${accent}/10 and text-onruntime-${accent} will not be generated unless explicitly safelisted in your Tailwind configuration.

🔎 Recommended solutions

Option 1: Use a mapping object (preferred)

+const accentClasses = {
+  blue: {
+    bg: 'bg-blue/10 text-onruntime-blue',
+    icon: 'p-1 rounded-md bg-blue/10 text-onruntime-blue mt-0.5'
+  },
+  // Add other accent colors used in your app
+};
+
 const LocalExpertise = async ({ agency }: LocalExpertiseProps) => {
   const { t } = await getTranslation(`constants/agencies/${agency.id}`);
   const { t: tComponent } = await getTranslation('components/marketing/agency/local-expertise');
 
   const accent = agency.accentColor || "blue";
+  const classes = accentClasses[accent] || accentClasses.blue;

Then use the mapped classes:

-            <div key={index} className="flex flex-col items-center text-center p-6 border rounded-lg bg-card hover:border-onruntime-blue transition-colors">
-              {Icon && (
-                <div className={`p-3 rounded-md bg-${accent}/10 text-onruntime-${accent} mb-4`}>
+              {Icon && (
+                <div className={`p-3 rounded-md mb-4 ${classes.bg}`}>

Option 2: Safelist in tailwind.config

Add to your Tailwind config:

module.exports = {
  safelist: [
    'bg-blue/10',
    'text-onruntime-blue',
    // Add all possible accent color combinations
  ],
  // ...
}

Also applies to: 36-36, 60-60

♻️ Duplicate comments (1)
apps/web/src/constants/agencies/montpellier.ts (1)

26-29: Previous review issues have been addressed.

Both critical issues from past reviews are now fixed:

  1. primaryStat.key is correctly set to "primary-stat" (matching the locale file structure)
  2. The hardcoded French string "Santé" has been removed

The label will now be properly resolved from translation files at runtime.

🧹 Nitpick comments (1)
apps/web/src/constants/agencies/lyon.ts (1)

26-55: Consider using TypeScript string literal types for translation keys to enable compile-time validation.

While all keys in this file currently exist in the locale files, the string-based key approach lacks compile-time safety. Keys are resolved at runtime using template literals like tAgency(focused-services.${service.key}.name), which means typos or mismatches between the TypeScript constants and locale files would only surface at runtime. This pattern is used consistently across all agency files. Consider using TypeScript string literal types or a code generation approach to create a type-safe mapping between agency data keys and their corresponding locale file structures. This would catch key mismatches at compile time and prevent potential translation failures when new agencies or keys are added.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ea43c5c and 8685ac8.

📒 Files selected for processing (32)
  • apps/web/src/app/[locale]/agency/[city]/page.tsx
  • apps/web/src/components/marketing/agency/city-hero-section.tsx
  • apps/web/src/components/marketing/agency/local-expertise.tsx
  • apps/web/src/constants/agencies/lyon.ts
  • apps/web/src/constants/agencies/marseille.ts
  • apps/web/src/constants/agencies/montpellier.ts
  • apps/web/src/constants/agencies/nantes.ts
  • apps/web/src/constants/agencies/nice.ts
  • apps/web/src/constants/agencies/paris.ts
  • apps/web/src/constants/agencies/rouen.ts
  • apps/web/src/constants/agencies/strasbourg.ts
  • apps/web/src/constants/agencies/toulouse.ts
  • apps/web/src/locales/en/constants/agencies/lyon.json
  • apps/web/src/locales/en/constants/agencies/marseille.json
  • apps/web/src/locales/en/constants/agencies/montpellier.json
  • apps/web/src/locales/en/constants/agencies/nantes.json
  • apps/web/src/locales/en/constants/agencies/nice.json
  • apps/web/src/locales/en/constants/agencies/paris.json
  • apps/web/src/locales/en/constants/agencies/rouen.json
  • apps/web/src/locales/en/constants/agencies/strasbourg.json
  • apps/web/src/locales/en/constants/agencies/toulouse.json
  • apps/web/src/locales/fr/constants/agencies/lyon.json
  • apps/web/src/locales/fr/constants/agencies/marseille.json
  • apps/web/src/locales/fr/constants/agencies/montpellier.json
  • apps/web/src/locales/fr/constants/agencies/nantes.json
  • apps/web/src/locales/fr/constants/agencies/nice.json
  • apps/web/src/locales/fr/constants/agencies/paris.json
  • apps/web/src/locales/fr/constants/agencies/rouen.json
  • apps/web/src/locales/fr/constants/agencies/strasbourg.json
  • apps/web/src/locales/fr/constants/agencies/toulouse.json
  • apps/web/src/types/agency.ts
  • apps/web/tailwind.config.ts
🚧 Files skipped from review as they are similar to previous changes (14)
  • apps/web/src/locales/fr/constants/agencies/nice.json
  • apps/web/src/locales/fr/constants/agencies/nantes.json
  • apps/web/src/locales/fr/constants/agencies/paris.json
  • apps/web/src/locales/en/constants/agencies/paris.json
  • apps/web/src/locales/fr/constants/agencies/toulouse.json
  • apps/web/src/types/agency.ts
  • apps/web/src/locales/en/constants/agencies/rouen.json
  • apps/web/src/locales/en/constants/agencies/montpellier.json
  • apps/web/src/locales/fr/constants/agencies/montpellier.json
  • apps/web/src/locales/en/constants/agencies/lyon.json
  • apps/web/src/locales/fr/constants/agencies/marseille.json
  • apps/web/src/locales/fr/constants/agencies/strasbourg.json
  • apps/web/src/components/marketing/agency/city-hero-section.tsx
  • apps/web/src/locales/en/constants/agencies/marseille.json
🧰 Additional context used
🧬 Code graph analysis (2)
apps/web/src/app/[locale]/agency/[city]/page.tsx (3)
apps/web/src/lib/utils/metadata.server.ts (1)
  • constructMetadata (20-114)
apps/web/src/components/json-ld/constants.ts (1)
  • ORGANIZATION_DATA (3-29)
packages/translations/src/next/components/link.tsx (1)
  • Link (14-57)
apps/web/src/components/marketing/agency/local-expertise.tsx (1)
apps/web/src/types/agency.ts (1)
  • Agency (29-61)
🔇 Additional comments (56)
apps/web/src/constants/agencies/montpellier.ts (3)

10-23: LGTM!

The translation keys for keyBusinessSectors, localChallenges, and benefits are properly defined. Per the past review verification, these keys exist in both en/fr locale files.


31-42: LGTM!

The strengths and focusedServices arrays use proper translation keys with appropriate icon and route references. Structure is consistent with the i18n-first approach.


44-63: LGTM!

The remaining fields are properly structured:

  • stats correctly pairs translation keys with static numeric values
  • contactInfo, geo, and nearbyLocations appropriately contain non-translatable data (contact details, coordinates, and city identifiers)
apps/web/src/constants/agencies/paris.ts (1)

5-64: Clean refactor to key-based data model for i18n support.

The migration from hardcoded strings to translation keys is well-executed and consistent. All translation keys used in this file (startups-tech, startup-solutions, luxury-experiences, finance-fintech, etc.) are properly defined in the corresponding paris.json locale files. The kebab-case naming convention is applied uniformly across all fields.

apps/web/src/locales/en/constants/agencies/strasbourg.json (1)

8-8: Previous grammar issue has been resolved.

The missing article "a" before "border city" has been added as suggested in the previous review. The text now correctly reads "European capital and a border city with rich heritage."

apps/web/src/constants/agencies/strasbourg.ts (1)

5-65: LGTM! Clean i18n refactoring with proper key alignment.

The refactoring from text-based to key-based schema follows a proper i18n pattern. All keys correctly reference the corresponding entries in the strasbourg.json locale file:

  • keyBusinessSectors, localChallenges, and benefits arrays
  • primaryStat, strengths, focusedServices, and stats objects

This enables proper translation support while maintaining type safety.

apps/web/src/locales/fr/constants/agencies/rouen.json (3)

1-29: LGTM! Well-structured French translations for Rouen agency.

The JSON structure is valid, and the French translations are professional and contextually appropriate. The content effectively highlights Rouen/Normandy-specific business sectors and challenges (industry, agrifood, logistics, Seine axis).


33-84: Well-structured service descriptions and contact information.

The strengths, focused-services, contact-info, and stats sections are properly structured with consistent key naming and comprehensive French descriptions. The stats section appropriately provides descriptive labels rather than hardcoded values.


30-32: No action needed. The primary-stat object in rouen.json follows the same structure as all other French agency locale files (Lyon, Paris, Nantes, Marseille, Nice, Toulouse, Strasbourg, Montpellier), which contain only a label field without numeric values. This is the intended design, not an incomplete implementation.

Likely an incorrect or invalid review comment.

apps/web/src/app/[locale]/agency/[city]/page.tsx (7)

4-6: LGTM! Import updates align with i18n architecture.

The imports now use locale-aware components (Link from translations package), server-side translation utilities, and centralized organization data. This addresses the previous comment about using the locale-aware Link component.

Also applies to: 15-15


23-47: LGTM! Metadata generation now fully internationalized.

The not-found metadata now uses translation keys (t('metadata.not-found.title') and t('metadata.not-found.description')), resolving the previous issue with hardcoded French strings. Agency-specific metadata properly uses the agency namespace for localized titles and descriptions.


59-79: LGTM! Translation loading and FAQ setup follow proper i18n patterns.

The component correctly loads both page-level and agency-specific translations using getTranslation. FAQ items properly interpolate dynamic values (city, region) into translation keys, maintaining consistency with the broader i18n architecture.


84-111: LGTM! JSON-LD schemas properly use centralized organization data and translations.

The schemas correctly reference ORGANIZATION_DATA.url for constructing IDs and URLs, ensuring consistency across the application. Breadcrumb and business descriptions are now properly internationalized using translation keys.


113-178: LGTM! Challenges section fully internationalized with proper Link usage.

All section headings, descriptions, and dynamic content (challenges, services) now use translation keys with appropriate interpolation. The Link component at line 167 correctly uses the locale-aware version from @onruntime/translations/next, addressing the previous comment about maintaining locale during navigation.


180-228: LGTM! FAQ, testimonials, and CTA sections properly internationalized.

The FAQ section correctly uses translations with city interpolation. Testimonials appropriately separate translated content (testimonial text) from non-translated metadata (author names, roles, companies), which is the expected pattern for proper nouns and identifying information.


234-241: LGTM! Static params generation follows Next.js conventions.

The function correctly uses dynamic import to load agencies and maps them to route parameters for static page generation at build time.

apps/web/tailwind.config.ts (1)

11-23: Safelist patterns are correct and complete.

The regex patterns accurately cover all dynamic class usage throughout the codebase:

  • bg-onruntime-(blue|magenta)/(5|10|15|20) matches all background opacity variants used in components
  • border-onruntime-(blue|magenta)/(10|20|30|40) matches all border opacity variants including dynamic template construction
  • text-onruntime-(blue|magenta) correctly covers text colors without opacity, which aligns with actual usage (text colors are never combined with opacity modifiers)
  • Explicit bg-onruntime-blue and bg-onruntime-magenta entries are necessary since the regex patterns require opacity modifiers
apps/web/src/constants/agencies/nice.ts (6)

31-36: LGTM: Strengths simplified for translation.

The strengths array has been properly refactored to use translation keys, maintaining consistency with the overall i18n pattern.


38-42: LGTM: Services refactored with preserved navigation links.

The focusedServices array correctly maintains the link property while converting textual content to translation keys.


51-55: LGTM: Stats values preserved with i18n keys.

The stats array correctly preserves the numeric values while converting labels to translation keys, enabling proper localization of stat descriptions.


46-49: LGTM: Contact info simplified.

The contactInfo has been streamlined to include only phone and email, with meetingOptions removed. This is appropriate as contact details are language-agnostic.


26-29: Structure correctly matches the Agency type definition.

The primaryStat structure with { key, icon } is properly typed. The key-based approach enables translation lookup while the icon provides visual representation.


10-23: Consistent i18n key transformation with complete translation coverage.

The conversion from descriptive strings to translation keys follows a clear, consistent pattern with proper kebab-case naming. The Agency type definition supports the new structure, and all referenced translation keys are defined in both English and French translation files (apps/web/src/locales/en/constants/agencies/nice.json and fr variant).

apps/web/src/locales/en/constants/agencies/nice.json (1)

71-84: The stats structure is correct as implemented. Numeric values (expertise: "10+", satisfaction: "99%", average-time: "6-8") are appropriately defined in the TypeScript constants file (apps/web/src/constants/agencies/nice.ts), while the JSON locale file contains only the translatable labels and descriptions. This separation follows standard internationalization practices.

apps/web/src/locales/en/constants/agencies/toulouse.json (1)

1-85: AI summary incorrectly identifies locale.

The AI summary states "Adds a new French locale JSON" but the file path clearly shows apps/web/src/locales/en/ (English locale) and the content is in English.

apps/web/src/constants/agencies/toulouse.ts (6)

10-10: LGTM!

The keyBusinessSectors keys correctly match the translation keys defined in the English locale JSON.


11-23: LGTM!

The localChallenges and benefits arrays use keys that correctly match the corresponding sections in the English locale JSON.


26-29: LGTM!

The primaryStat refactoring to use a key-based approach aligns with the i18n pattern. The translation lookup will resolve the label from the locale JSON.


31-36: LGTM!

The strengths array uses keys that correctly match the translation keys in the English locale JSON, each with appropriate icons.


38-42: LGTM!

The focusedServices array uses keys that correctly match the translation keys in the English locale JSON, with appropriate links and icons.


51-55: LGTM!

The stats array uses keys that correctly match the translation keys in the English locale JSON, with appropriate hardcoded values.

apps/web/src/constants/agencies/nantes.ts (3)

5-65: No action needed. The removed fields (introText, expertiseText, whyChooseUs) are not part of the Agency interface and were never expected to be in this file. These fields belong to the City interface. The nantesAgency object correctly implements the Agency interface with all required fields present.

Likely an incorrect or invalid review comment.


10-23: All translation keys are properly defined in localization files.

The refactor to key-based data arrays is complete and valid. All keys (tech-digital, ecological-transition, creative-industries, agrifood, digital-ecosystem-competition, startup-scaleup-needs, ecological-transition-digitalization, traditional-industries-transformation, nantes-ecosystem-solutions, innovative-eco-technologies, agile-approach, digital-transition-knowledge, personalized-support, primary-stat, innovative-tech, eco-design, maritime-industry, renewable-energy, web-applications, eco-designed-websites, business-platforms, expertise, satisfaction, average-time) are defined in both English and French translation files at apps/web/src/locales/{en,fr}/constants/agencies/nantes.json.


26-55: All structural requirements are properly met. The Agency type definition correctly supports the new key-based structure for primaryStat, strengths, focusedServices, and stats. All translation keys exist in both English and French locales, with proper nesting matching component access patterns (e.g., strengths.${key}.title, stats.${key}.label). No additional action needed.

apps/web/src/constants/agencies/marseille.ts (8)

26-29: Structure change for primaryStat looks good.

The refactoring from { icon, value, label } to { key, icon } correctly externalizes the value and label to translation files. Ensure the "primary-stat" key exists in your locale files (included in the broader verification script).


31-36: Strengths array refactored appropriately for i18n.

The conversion to { key, icon } format is consistent with the overall i18n strategy. The translation keys for strengths ("tourism-experience", "local-ecommerce", "maritime-sector", "mediterranean-design") should be verified as part of the comprehensive translation key check.


38-42: FocusedServices structure correctly preserves links.

The refactoring to { key, link, icon } maintains the routing information while externalizing the descriptive text. The route references look correct. Ensure translation keys ("tourism-websites", "provencal-ecommerce", "mobile-applications") are present in locale files.


46-60: Contact info and geo data appropriately preserved.

The essential contact information (phone, email) and geographic coordinates don't require i18n treatment and are correctly retained in their original format.


62-65: LGTM - nearby locations and export.

The nearbyLocations array uses location identifiers (not translation keys), which is appropriate for cross-referencing. The default export preserves the public API.


1-65: Agency type definition fully supports the refactored structure. All fields used in marseille.ts (and all other agency files) are correctly defined in the Agency interface at @/types/agency.ts. Consistency across all 10 agency files is confirmed—each follows the identical structure with the same properties in the same order.


51-55: No changes needed. The stats array with hardcoded values follows the consistent pattern used across all agency files (Toulouse, Rouen, Paris, Strasbourg, Nice, Nantes, Montpellier, Lyon). Each agency specifies its own metric values to reflect agency-specific performance data, while keys link to translation labels. This is the intentional design.


10-23: All translation keys in the Marseille agency file are properly defined in both English and French locale files. The transformation to key-based identifiers is complete and functional.

apps/web/src/constants/agencies/rouen.ts (9)

10-10: LGTM: Business sectors converted to translation keys.

The transformation from French descriptive strings to English translation keys follows the i18n pattern correctly.


11-16: LGTM: Local challenges properly keyed for translation.

The challenge descriptions are now properly structured as translation keys following the established naming convention.


17-23: LGTM: Benefits array properly internationalized.

The benefits have been successfully converted to translation keys with clear, descriptive names.


31-36: LGTM: Strengths properly structured for i18n.

The strengths array has been cleanly refactored with translation keys while preserving the icon associations.


38-42: LGTM: Focused services correctly internationalized.

The services array maintains proper route references while successfully adopting the translation key pattern.


46-49: LGTM: Contact info appropriately static.

Contact information correctly remains as static data rather than translation keys, which is the appropriate pattern for phone numbers and email addresses.


51-55: LGTM: Stats array follows correct i18n pattern.

The stats properly separate translation keys (for labels) from static display values (percentages and numbers), which is the correct approach for internationalization.


65-65: LGTM: Standard default export.


26-29: No action needed — primaryStat structure is correct.

The hardcoded French string "Industrie" has been successfully removed. The current structure of primaryStat with only key and icon fields aligns with the type definition in apps/web/src/types/agency.ts (lines 55–58). This is intentional and distinct from the stats array, which uses AgencyStat with both key and value fields. The implementation in rouen.ts is correct.

apps/web/src/constants/agencies/lyon.ts (1)

10-23: Excellent i18n refactoring with key-based structure.

The refactoring from inline content to translation keys follows i18n best practices. All keys correctly match the structure defined in the French locale file (lyon.json).

apps/web/src/locales/fr/constants/agencies/lyon.json (1)

1-85: LGTM! Well-structured French locale file with matching English counterpart.

The JSON structure is clean, valid, and follows i18n best practices with kebab-case keys and organized nested objects. French content is properly formatted and comprehensive. Verification confirms the English locale file exists with identical key structure across all sections including strengths and focused-services.

apps/web/src/components/marketing/agency/local-expertise.tsx (3)

8-10: LGTM! Server Component pattern correctly implemented.

The async function conversion and dual translation namespaces (agency-specific and component UI) are well-structured for server-side rendering.


18-21: LGTM! Translation usage is correct and consistent.

The distinction between component UI translations (tComponent) and agency-specific content (t) is clear. The interpolation parameters for city names are properly structured.

Also applies to: 54-55


46-47: Translation keys are complete and properly structured. All strength and benefit keys used in the component have corresponding translations in the agency locale files. Verified across multiple agencies (Paris, Lyon, Marseille) that every dynamic key reference has a matching entry in the translations. The current implementation correctly handles this pattern without issues.

Likely an incorrect or invalid review comment.

Copy link

@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: 0

🧹 Nitpick comments (1)
apps/web/src/locales/fr/constants/agencies/lyon.json (1)

30-32: Confirm primary-stat structure is complete.

The primary-stat object contains only a label field. Verify whether an icon field (or other metadata) is needed to match the expected structure, or if the icon is provided elsewhere in the implementation.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8685ac8 and 752b994.

📒 Files selected for processing (18)
  • apps/web/src/locales/en/constants/agencies/lyon.json
  • apps/web/src/locales/en/constants/agencies/marseille.json
  • apps/web/src/locales/en/constants/agencies/montpellier.json
  • apps/web/src/locales/en/constants/agencies/nantes.json
  • apps/web/src/locales/en/constants/agencies/nice.json
  • apps/web/src/locales/en/constants/agencies/paris.json
  • apps/web/src/locales/en/constants/agencies/rouen.json
  • apps/web/src/locales/en/constants/agencies/strasbourg.json
  • apps/web/src/locales/en/constants/agencies/toulouse.json
  • apps/web/src/locales/fr/constants/agencies/lyon.json
  • apps/web/src/locales/fr/constants/agencies/marseille.json
  • apps/web/src/locales/fr/constants/agencies/montpellier.json
  • apps/web/src/locales/fr/constants/agencies/nantes.json
  • apps/web/src/locales/fr/constants/agencies/nice.json
  • apps/web/src/locales/fr/constants/agencies/paris.json
  • apps/web/src/locales/fr/constants/agencies/rouen.json
  • apps/web/src/locales/fr/constants/agencies/strasbourg.json
  • apps/web/src/locales/fr/constants/agencies/toulouse.json
✅ Files skipped from review due to trivial changes (1)
  • apps/web/src/locales/en/constants/agencies/nantes.json
🚧 Files skipped from review as they are similar to previous changes (11)
  • apps/web/src/locales/en/constants/agencies/toulouse.json
  • apps/web/src/locales/fr/constants/agencies/nice.json
  • apps/web/src/locales/en/constants/agencies/strasbourg.json
  • apps/web/src/locales/en/constants/agencies/paris.json
  • apps/web/src/locales/fr/constants/agencies/nantes.json
  • apps/web/src/locales/en/constants/agencies/montpellier.json
  • apps/web/src/locales/fr/constants/agencies/marseille.json
  • apps/web/src/locales/fr/constants/agencies/toulouse.json
  • apps/web/src/locales/en/constants/agencies/lyon.json
  • apps/web/src/locales/en/constants/agencies/nice.json
  • apps/web/src/locales/fr/constants/agencies/rouen.json
🔇 Additional comments (8)
apps/web/src/locales/fr/constants/agencies/strasbourg.json (2)

1-79: Well-structured French locale file for Strasbourg agency.

The JSON structure is valid, and the French translations are comprehensive and contextually appropriate for Strasbourg's European and cross-border business environment. The content effectively emphasizes the city's unique position as a European capital and its multilingual, international context.


1-79: AI summary mentions non-existent "contact-info" section.

The AI-generated summary claims the file includes a "contact-info" section, but this section is not present in the actual file. The file contains: title, description, hero, intro, expertise, why-choose-us, key-business-sectors, local-challenges, benefits, primary-stat, strengths, focused-services, and stats.

apps/web/src/locales/fr/constants/agencies/montpellier.json (1)

1-79: LGTM! Well-structured French locale resource for Montpellier agency page.

The JSON structure is valid, consistently formatted, and follows the established pattern for agency locale files. The French translations are professional, grammatically correct, and appropriately tailored to the Montpellier regional context (biotechnology sector, universities, Languedoc terroir, Mediterranean tourism).

Key strengths:

  • Consistent kebab-case naming for multi-word keys
  • Logical grouping of related content (strengths, services, stats)
  • Proper encoding of French special characters
  • Contextually relevant content specific to the Montpellier market
apps/web/src/locales/en/constants/agencies/marseille.json (1)

1-79: LGTM! Well-structured locale data file.

The JSON structure is valid and complete, with professionally written English content appropriately tailored to the Marseille/Provençal market context. The schema is consistent with other agency locale files in this PR (Lyon, Paris, Nice, etc.), ensuring uniformity across the translation layer.

apps/web/src/locales/en/constants/agencies/rouen.json (1)

1-79: LGTM! Well-structured locale data file.

The JSON structure is valid and complete, with professionally written English content appropriately tailored to the Rouen/Normandy market context. The schema consistently matches other agency locale files in this PR, maintaining structural uniformity across the translation system.

apps/web/src/locales/fr/constants/agencies/lyon.json (2)

1-79: LGTM! Well-structured French locale file.

The JSON structure is valid, content is appropriately localized for Lyon's business ecosystem, and the organization is clear and logical.


33-64: No changes needed. Field naming is consistent and intentional.

The apparent inconsistency between "title" in the strengths section and "name" in the focused-services section is deliberate and consistently applied across all agency files in both English and French locales. This pattern is used identically in all nine agency configuration files, indicating the different field names reflect intentional design choices tied to their respective UI components.

apps/web/src/locales/fr/constants/agencies/paris.json (1)

1-80: Excellent French localization quality.

The JSON structure is valid, and the French translations are professional and contextually appropriate for the Paris tech market. The mix of French text and English tech terms (SaaS, fintech, UI/UX, startup, tech, e-commerce, premium) follows French tech industry conventions and enhances clarity.

Schema consistency across all agency locale files has been verified—all 9 French agency files (Bordeaux, Lyon, Marseille, Montpellier, Nantes, Nice, Paris, Rouen, Strasbourg, Toulouse) maintain identical top-level key structures, with location-specific values in nested objects like strengths. No schema conflicts detected.

Copy link

@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: 9

🧹 Nitpick comments (11)
apps/web/src/locales/en/constants/services/integration/webflow.json (1)

3-3: Consider expanding the root description field.

The description "Creation of modern websites with Webflow" reads as a terse fragment. For better consistency with the marketing tone used throughout the rest of this file (e.g., the hero section), consider expanding it to a more complete sentence or value proposition.

Example alternatives
-  "description": "Creation of modern websites with Webflow",
+  "description": "Create stunning, modern websites with Webflow's powerful visual development platform",

or

-  "description": "Creation of modern websites with Webflow",
+  "description": "Build modern, responsive websites with Webflow's intuitive visual design tools",
apps/web/src/locales/en/constants/services/frontend/pwa.json (2)

2-2: Consider capitalizing "Web" in "Progressive Web Apps".

"Progressive Web Apps" is typically capitalized as it's a proper technology term. The current lowercase "web" works but may appear inconsistent with standard industry usage.

🔎 Proposed capitalization adjustment
-  "name": "Progressive web apps",
+  "name": "Progressive Web Apps",
-    "title": "Progressive web apps (PWA)",
+    "title": "Progressive Web Apps (PWA)",

Also applies to: 5-5


51-51: Clarify "better referencing" with "better SEO".

The phrase "better referencing" appears to be a translation artifact (from French "référencement"). In English marketing/tech contexts, "better SEO" or "improved search engine visibility" would be clearer and more familiar to readers.

🔎 Proposed wording improvement
-      "answer": "PWAs offer several advantages: no need to go through stores, automatic updates, faster and less expensive development, better referencing, and a single codebase for all platforms."
+      "answer": "PWAs offer several advantages: no need to go through stores, automatic updates, faster and less expensive development, better SEO, and a single codebase for all platforms."
apps/web/src/locales/fr/constants/services/frontend/desktop.json (1)

76-76: Consider localizing "Cloud sync" for complete French translation.

While tech terms in English are commonly used in French contexts, for a fully localized experience you might consider:

  • "Synchronisation cloud"
  • "Synchronisation dans le cloud"
  • "Synchro cloud" (more casual)

The current "Cloud sync" is acceptable in tech contexts, but localizing it would provide a more consistent French experience.

apps/web/src/locales/en/constants/services/design/moodboard.json (1)

1-80: LGTM! Well-structured locale data with professional content.

The JSON structure is clean, valid, and follows consistent patterns across all sections. The content is professional and appropriate for the moodboard service offering.

Optional: Minor wording suggestion for line 47

Consider replacing "visual assembly" with "visual composition" or "visual collection" for slightly clearer terminology:

-      "answer": "A moodboard is a visual assembly that defines the artistic direction of your project. It combines colors, typography, images and textures to create a coherent visual reference. It's an essential tool for communicating your vision and ensuring the consistency of your visual identity."
+      "answer": "A moodboard is a visual composition that defines the artistic direction of your project. It combines colors, typography, images and textures to create a coherent visual reference. It's an essential tool for communicating your vision and ensuring the consistency of your visual identity."
apps/web/src/components/marketing/services/service-overview.tsx (3)

105-108: Consider using JSX for cleaner icon rendering.

The React.createElement pattern works but JSX would be more readable and consistent with the rest of the file.

🔎 Suggested refactor
-      {subService.icon && React.createElement(
-        subService.icon,
-        { className: `w-5 h-5 text-${accentColor} mb-2` }
-      )}
+      {subService.icon && (
+        <subService.icon className={`w-5 h-5 text-${accentColor} mb-2`} />
+      )}

130-134: Consider adding defensive handling for invalid categoryId.

If Services.find() returns undefined, the component renders with empty arrays but no user feedback. Consider adding an early return or error boundary for this edge case.

🔎 Suggested approach
  const categoryData = Services.find(s => s.id === categoryId);
+ 
+ if (!categoryData) {
+   console.error(`Service category not found: ${categoryId}`);
+   return null; // or a fallback UI
+ }
+ 
  const ServiceIcon = categoryData?.icon;
- const benefits = categoryData?.benefits || [];
- const processList = categoryData?.processList || [];
- const subServices = categoryData?.subServices || [];
+ const benefits = categoryData.benefits || [];
+ const processList = categoryData.processList || [];
+ const subServices = categoryData.subServices || [];

220-227: Use benefit.key instead of array index for stable keys.

Using array indices as React keys can cause issues with component state and reconciliation if items are reordered. Since ServiceBenefit has a key property, use that for stable identity.

🔎 Suggested fix
              {benefits.slice(0, 3).map((benefit, index) => (
                <BenefitBadge
-                  key={index}
+                  key={benefit.key}
                  categoryId={categoryId}
                  benefit={benefit}
                  accentColor={accentColor}
                />
              ))}

Apply the same pattern at lines 283-290 and 301-308.

apps/web/src/constants/services/integration.ts (1)

48-235: Consider adding JSDoc comments for translation key structure.

While the refactoring is well-executed, consider adding JSDoc comments to document the expected translation structure for each sub-service. This would help developers understand where to add translations and what the expected content should be.

📝 Example JSDoc documentation
+/**
+ * Integration service category data with translation keys.
+ * 
+ * Translation structure for sub-services:
+ * - features: services.integration.[platform].features.[key].{title, description}
+ * - benefits: services.integration.[platform].benefits.[key].{title, description}
+ * - faqItems: services.integration.[platform].faq.[key].{question, answer}
+ * - complementaryServices: services.[key].{title, description}
+ */
 const integrationCategory: ServiceCategoryData = {
   id: ServiceCategory.INTEGRATION,
   icon: Layers,
apps/web/src/components/marketing/services/testimonials-section.tsx (1)

84-91: Consider using testimonial.key instead of index for the React key.

Using testimonial.key as the key prop would provide more stable identity for list reconciliation, similar to how FAQSection uses item.key.

🔎 Suggested improvement
        {testimonials.map((testimonial, index) => (
          <TestimonialCard
-           key={index}
+           key={testimonial.key}
            categoryId={categoryId}
            serviceId={serviceId}
            testimonial={testimonial}
          />
        ))}
apps/web/src/components/marketing/navbar/index.tsx (1)

22-68: Consider extracting shared SubServiceLink logic.

Both this file and apps/web/src/components/marketing/navbar/navigation/services/index.tsx define a SubServiceLink component with the same translation pattern (constants/services/${categoryId}/${subService.id}). While the styling differs slightly between mobile and desktop versions, the core translation logic could be shared to reduce duplication.

Potential refactor approach

Extract the translation logic into a shared hook or component that accepts className as a prop:

// shared-service-link.tsx
const useServiceTranslation = (categoryId: string, subServiceId: string) => {
  const { t } = useTranslation(`constants/services/${categoryId}/${subServiceId}`);
  return t;
};

Then both mobile and desktop versions can use this hook with their respective styling.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 752b994 and 42ce2fd.

📒 Files selected for processing (73)
  • apps/web/src/app/[locale]/services/[category]/[service]/page.tsx
  • apps/web/src/app/[locale]/services/[category]/page.tsx
  • apps/web/src/app/[locale]/services/page.tsx
  • apps/web/src/components/marketing/navbar/index.tsx
  • apps/web/src/components/marketing/navbar/navigation/services/index.tsx
  • apps/web/src/components/marketing/services/faq-section.tsx
  • apps/web/src/components/marketing/services/feature-section.tsx
  • apps/web/src/components/marketing/services/service-layout.tsx
  • apps/web/src/components/marketing/services/service-overview.tsx
  • apps/web/src/components/marketing/services/testimonials-section.tsx
  • apps/web/src/constants/services/backend.ts
  • apps/web/src/constants/services/design.ts
  • apps/web/src/constants/services/frontend.ts
  • apps/web/src/constants/services/integration.ts
  • apps/web/src/locales/en/app/services/[category]/[service]/page.json
  • apps/web/src/locales/en/app/services/page.json
  • apps/web/src/locales/en/components/marketing/services/service-layout.json
  • apps/web/src/locales/en/components/marketing/services/service-overview.json
  • apps/web/src/locales/en/constants/services/backend.json
  • apps/web/src/locales/en/constants/services/backend/api.json
  • apps/web/src/locales/en/constants/services/backend/bots.json
  • apps/web/src/locales/en/constants/services/backend/cloud.json
  • apps/web/src/locales/en/constants/services/backend/database.json
  • apps/web/src/locales/en/constants/services/backend/microservices.json
  • apps/web/src/locales/en/constants/services/design.json
  • apps/web/src/locales/en/constants/services/design/audit.json
  • apps/web/src/locales/en/constants/services/design/branding.json
  • apps/web/src/locales/en/constants/services/design/moodboard.json
  • apps/web/src/locales/en/constants/services/design/ui.json
  • apps/web/src/locales/en/constants/services/design/wireframes.json
  • apps/web/src/locales/en/constants/services/frontend.json
  • apps/web/src/locales/en/constants/services/frontend/desktop.json
  • apps/web/src/locales/en/constants/services/frontend/mobile.json
  • apps/web/src/locales/en/constants/services/frontend/pwa.json
  • apps/web/src/locales/en/constants/services/frontend/web.json
  • apps/web/src/locales/en/constants/services/integration.json
  • apps/web/src/locales/en/constants/services/integration/prestashop.json
  • apps/web/src/locales/en/constants/services/integration/shopify.json
  • apps/web/src/locales/en/constants/services/integration/squarespace.json
  • apps/web/src/locales/en/constants/services/integration/strapi.json
  • apps/web/src/locales/en/constants/services/integration/webflow.json
  • apps/web/src/locales/en/constants/services/integration/wordpress.json
  • apps/web/src/locales/fr/app/services/[category]/[service]/page.json
  • apps/web/src/locales/fr/app/services/page.json
  • apps/web/src/locales/fr/components/marketing/services/service-layout.json
  • apps/web/src/locales/fr/components/marketing/services/service-overview.json
  • apps/web/src/locales/fr/constants/services/backend.json
  • apps/web/src/locales/fr/constants/services/backend/api.json
  • apps/web/src/locales/fr/constants/services/backend/bots.json
  • apps/web/src/locales/fr/constants/services/backend/cloud.json
  • apps/web/src/locales/fr/constants/services/backend/database.json
  • apps/web/src/locales/fr/constants/services/backend/microservices.json
  • apps/web/src/locales/fr/constants/services/design.json
  • apps/web/src/locales/fr/constants/services/design/audit.json
  • apps/web/src/locales/fr/constants/services/design/branding.json
  • apps/web/src/locales/fr/constants/services/design/moodboard.json
  • apps/web/src/locales/fr/constants/services/design/ui.json
  • apps/web/src/locales/fr/constants/services/design/wireframes.json
  • apps/web/src/locales/fr/constants/services/frontend.json
  • apps/web/src/locales/fr/constants/services/frontend/desktop.json
  • apps/web/src/locales/fr/constants/services/frontend/mobile.json
  • apps/web/src/locales/fr/constants/services/frontend/pwa.json
  • apps/web/src/locales/fr/constants/services/frontend/web.json
  • apps/web/src/locales/fr/constants/services/integration.json
  • apps/web/src/locales/fr/constants/services/integration/prestashop.json
  • apps/web/src/locales/fr/constants/services/integration/shopify.json
  • apps/web/src/locales/fr/constants/services/integration/squarespace.json
  • apps/web/src/locales/fr/constants/services/integration/strapi.json
  • apps/web/src/locales/fr/constants/services/integration/webflow.json
  • apps/web/src/locales/fr/constants/services/integration/wordpress.json
  • apps/web/src/proxy.ts
  • apps/web/src/screens/marketing/services/index.tsx
  • apps/web/src/types/service.ts
✅ Files skipped from review due to trivial changes (22)
  • apps/web/src/locales/en/components/marketing/services/service-overview.json
  • apps/web/src/locales/en/app/services/[category]/[service]/page.json
  • apps/web/src/locales/fr/constants/services/backend.json
  • apps/web/src/locales/fr/constants/services/integration.json
  • apps/web/src/locales/fr/constants/services/design/moodboard.json
  • apps/web/src/locales/fr/constants/services/frontend/mobile.json
  • apps/web/src/locales/en/constants/services/design.json
  • apps/web/src/locales/fr/constants/services/frontend.json
  • apps/web/src/locales/en/constants/services/backend/cloud.json
  • apps/web/src/locales/fr/constants/services/design/audit.json
  • apps/web/src/locales/en/constants/services/integration.json
  • apps/web/src/locales/fr/components/marketing/services/service-layout.json
  • apps/web/src/locales/fr/constants/services/design/ui.json
  • apps/web/src/locales/en/constants/services/integration/wordpress.json
  • apps/web/src/locales/en/constants/services/integration/strapi.json
  • apps/web/src/locales/fr/constants/services/integration/prestashop.json
  • apps/web/src/locales/en/constants/services/integration/shopify.json
  • apps/web/src/locales/en/constants/services/integration/squarespace.json
  • apps/web/src/locales/en/constants/services/frontend/desktop.json
  • apps/web/src/locales/en/constants/services/frontend/mobile.json
  • apps/web/src/locales/en/components/marketing/services/service-layout.json
  • apps/web/src/locales/fr/constants/services/design.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/src/locales/en/app/services/page.json
  • apps/web/src/components/marketing/services/service-layout.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-28T00:50:00.164Z
Learnt from: CR
Repo: onRuntime/onruntime PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T00:50:00.164Z
Learning: Organize web app source code into: app/ (pages and API routes), components/ (ui, layout, marketing), services/ (API clients), constants/ (static data), content/ (MDX), lib/ (utilities), and types/ (TypeScript definitions)

Applied to files:

  • apps/web/src/locales/en/constants/services/frontend/web.json
  • apps/web/src/constants/services/frontend.ts
🧬 Code graph analysis (9)
apps/web/src/app/[locale]/services/[category]/page.tsx (1)
apps/web/src/lib/utils/metadata.server.ts (1)
  • constructMetadata (20-114)
apps/web/src/components/marketing/services/testimonials-section.tsx (1)
apps/web/src/types/service.ts (1)
  • ServiceTestimonial (10-18)
apps/web/src/constants/services/frontend.ts (1)
apps/web/src/types/service.ts (1)
  • ServiceBenefit (29-32)
apps/web/src/components/marketing/services/feature-section.tsx (1)
apps/web/src/types/service.ts (1)
  • ServiceFeature (24-27)
apps/web/src/constants/services/backend.ts (1)
apps/web/src/types/service.ts (3)
  • ServiceBenefit (29-32)
  • ServiceProcessStep (34-37)
  • ServiceCategoryData (51-58)
apps/web/src/constants/services/integration.ts (1)
apps/web/src/types/service.ts (3)
  • ServiceBenefit (29-32)
  • ServiceProcessStep (34-37)
  • ServiceCategoryData (51-58)
apps/web/src/components/marketing/navbar/navigation/services/index.tsx (2)
apps/web/src/types/service.ts (2)
  • SubService (39-49)
  • ServiceCategoryData (51-58)
packages/translations/src/next/components/link.tsx (1)
  • Link (14-57)
apps/web/src/constants/services/design.ts (1)
apps/web/src/types/service.ts (3)
  • ServiceBenefit (29-32)
  • ServiceProcessStep (34-37)
  • ServiceCategoryData (51-58)
apps/web/src/app/[locale]/services/[category]/[service]/page.tsx (4)
apps/web/src/app/[locale]/services/[category]/page.tsx (2)
  • generateMetadata (13-32)
  • generateStaticParams (54-58)
apps/web/src/app/[locale]/services/page.tsx (1)
  • generateMetadata (6-13)
apps/web/src/lib/utils/metadata.server.ts (1)
  • constructMetadata (20-114)
apps/web/src/app/[locale]/layout.tsx (1)
  • generateStaticParams (25-27)
🔇 Additional comments (46)
apps/web/src/locales/fr/app/services/page.json (1)

1-19: LGTM! French translations are accurate and professional.

The JSON structure is valid, and all French translations are grammatically correct and appropriately professional for a business services page. The terminology choices ("digitaux," "stack technologique") align with modern French tech/marketing conventions.

apps/web/src/locales/en/constants/services/frontend/pwa.json (1)

1-80: Well-structured localization file with professional content.

The JSON structure is valid, key naming is consistent (kebab-case), and the content is clear and professional. The translations provide comprehensive coverage of PWA concepts, features, benefits, and FAQs.

apps/web/src/locales/en/constants/services/design/branding.json (1)

1-80: LGTM! Well-structured English translations.

The JSON structure is clean, logical, and comprehensive. The content is professional and appropriate for a branding service offering. Key naming follows consistent kebab-case convention, and all sections (hero, features, benefits, faq, complementary) are well-organized with appropriate nesting.

apps/web/src/locales/fr/constants/services/design/branding.json (1)

1-80: LGTM! French translations are consistent and complete.

The French translation file perfectly mirrors the English version's structure with all keys matching. The translations appear professional and appropriate. Technical terms like "Design system" (line 22) and "Marketing Kit" (line 68) are appropriately handled, maintaining the English terminology where commonly used in the industry.

apps/web/src/locales/fr/app/services/[category]/[service]/page.json (1)

1-11: LGTM! Well-structured French translations.

The JSON structure is correct, and the French translations in these sections are natural and professional. The placeholder usage is consistent throughout.

Also applies to: 16-27

apps/web/src/locales/en/constants/services/backend/database.json (1)

67-70: French text in English locale file.

Line 69 contains French text "Transfer sécurisé" which should be translated to English. This should likely read "Secure transfer from your existing systems."

🔎 Proposed fix
     "data-migration": {
       "title": "Data migration",
-      "description": "Transfer sécurisé depuis vos systèmes existants."
+      "description": "Secure transfer from your existing systems."
     },

Likely an incorrect or invalid review comment.

apps/web/src/locales/fr/constants/services/backend/cloud.json (1)

1-80: LGTM! Translations appear well-structured and professional.

The JSON structure shown is valid and well-organized. The French translations appear grammatically correct and appropriate, with technical terms (AWS, GCP, Azure, IAM, VPC, WAF, etc.) properly handled. The mix of French and English terminology is standard practice in French technical content.

Structure and content alignment with the English source file should be verified to ensure all keys and sections are complete and match exactly.

apps/web/src/locales/en/constants/services/design/wireframes.json (1)

1-80: LGTM! Excellent wireframes service locale data.

The content is well-structured, technically accurate, and maintains consistency with the other service locale files. The FAQ section provides comprehensive answers that will be helpful for users.

apps/web/src/locales/en/constants/services/design/ui.json (1)

1-80: LGTM! High-quality UI design service locale data.

The JSON structure is clean and the content is professional and comprehensive. The file maintains excellent consistency with the other service locale files.

Note: The concept "design-system" appears in three sections (features line 13, benefits line 35, complementary line 67) with slightly different descriptions. While this isn't a technical conflict, you may want to verify this is the intended structure and that the different descriptions serve distinct purposes in your UI.

apps/web/src/components/marketing/services/service-overview.tsx (5)

1-14: LGTM!

The "use client" directive is appropriate since the component uses client-side hooks (useTranslation). The import changes correctly bring in the i18n-aware Link and translation utilities.


45-55: Skip: Dynamic class issue already flagged above.


63-88: LGTM!

Good use of conditional rendering for the optional icon and proper separation of translation namespaces between the sub-service content and overview labels.


275-312: LGTM on conditional rendering.

Good use of conditional guards (benefits.length > 0) to prevent rendering empty sections. The benefits section structure is well organized.


314-328: LGTM on process section.

The process section correctly uses the translatedProcessList derived earlier, and the conditional rendering guard handles the empty case appropriately.

apps/web/src/locales/en/constants/services/integration/prestashop.json (1)

1-80: File structure and content are consistent with other integration pages and require no changes.

Verification confirms that prestashop.json follows the identical structure as all other integration locale files (shopify.json, squarespace.json, strapi.json, webflow.json, wordpress.json), with matching top-level keys and section counts (4 features, 4 benefits, 5 FAQs, 3 complementary items). The JSON is syntactically valid, English content is clear and grammatically correct, and key naming conventions are consistent throughout.

apps/web/src/locales/fr/constants/services/frontend/pwa.json (1)

1-80: Excellent French translations with structure verified.

The French translations are well-written, grammatically correct, and technically accurate. The content properly describes PWA capabilities, benefits, and implementation details. Technical terms like "Progressive Web Apps", "Service Workers", "Analytics", and "SEO" are appropriately kept in English, which aligns with French technical documentation conventions.

Structure verification confirms this file matches the English locale file exactly—all translation keys are consistent between apps/web/src/locales/fr/constants/services/frontend/pwa.json and the corresponding English version. The "name" field value "Progressive web apps" is intentionally in English in both locales, following the same pattern as the English file.

apps/web/src/locales/fr/constants/services/design/wireframes.json (1)

1-80: LGTM! Well-structured French translations with professional content.

The JSON structure is clean and consistent, with properly organized sections for metadata, hero, features, benefits, FAQ, and complementary services. The French translations are grammatically correct, maintain a professional tone throughout using formal "vous", and the technical terminology is accurate.

Verification confirms that this file structure matches the English locale counterpart across all sections (features, benefits, FAQ, and complementary), and is properly placed within the path-based locale system where it will be automatically discovered by the i18n system.

apps/web/src/locales/en/constants/services/design/audit.json (1)

1-80: LGTM! Well-structured locale data.

The JSON structure is clean, consistent, and comprehensive. The content is grammatically correct with a professional tone. Key naming conventions (kebab-case) align well with the PR's move to key-based identifiers, and the hierarchical organization supports clear presentation of the UX/UI Audit service. The corresponding French translation file mirrors this structure with all keys matching perfectly.

apps/web/src/locales/fr/components/marketing/services/service-overview.json (1)

1-29: The French translations in this file are grammatically correct and well-structured. Both the English and French counterpart files exist with identical placeholder patterns, and the {service} placeholder interpolation is already properly implemented in the component via the @onruntime/translations framework, which accepts a second parameter object ({ service: name }) passed to the t() function for dynamic value replacement.

apps/web/src/constants/services/integration.ts (2)

1-30: LGTM! Clean import organization.

The imports are well-structured and all necessary types and icons are properly imported to support the new key-based data structure.


48-235: Translation keys and consumer updates verified across all integration sub-services.

All 6 integration sub-services (Shopify, WordPress, Webflow, Squarespace, PrestaShop, Strapi) have properly implemented the key-based translation structure with complete locale coverage:

  1. Translation keys: All keys verified to exist in corresponding locale files (en/fr) with proper structure (features, benefits, faq, complementaryServices organized by key).
  2. Consuming components updated: All service components correctly use the new structure—feature-section.tsx, faq-section.tsx, service-overview.tsx, and testimonials-section.tsx properly access keys and call useTranslation with correct namespace paths.
  3. No breaking references: No remaining usage of removed fields (heroTitle, heroDescription, title, description, name) found in the codebase.

The refactoring is complete and properly integrated.

apps/web/src/locales/fr/constants/services/frontend/web.json (1)

1-80: Well-structured French localization file.

The JSON structure is consistent and follows the established pattern with properly organized sections (hero, features, benefits, faq, complementary). The French content is appropriately localized.

apps/web/src/locales/en/constants/services/frontend/web.json (1)

1-80: English locale file mirrors French structure correctly.

The key structure is consistent with the French counterpart, ensuring reliable translation lookups across locales.

apps/web/src/components/marketing/services/feature-section.tsx (1)

129-141: Clean feature lookup implementation.

The type-driven feature selection pattern with early return for empty features is well implemented.

apps/web/src/locales/fr/constants/services/integration/shopify.json (1)

1-80: Well-structured Shopify integration translations.

The file follows the established pattern and provides comprehensive French localization for the Shopify service page.

apps/web/src/locales/en/constants/services/frontend.json (1)

1-56: Category-level translation file properly structured.

The file provides comprehensive translations for the frontend service category including process steps and associated services.

apps/web/src/app/[locale]/services/[category]/page.tsx (2)

13-31: Translation-driven metadata correctly implemented.

The generateMetadata function now properly uses getTranslation() for both the not-found fallback and valid category metadata, addressing the previous review feedback about hardcoded strings.


34-52: Page component correctly integrates translations.

The component validates the category exists before rendering and passes translated content to ServiceOverviewPage.

apps/web/src/app/[locale]/services/page.tsx (1)

1-17: Services page correctly implements the translation pattern.

The page now uses getTranslation() for metadata, addressing the previous review feedback about hardcoded French strings. The implementation is consistent with other locale-aware pages in the PR.

apps/web/src/constants/services/backend.ts (2)

1-47: Well-structured migration to key-based i18n model.

The refactored benefits and process list align correctly with the ServiceBenefit and ServiceProcessStep type definitions. Keys use consistent kebab-case naming convention.


63-213: Translation coverage verified for all backend services.

All keys defined in the backend.ts file are present in the corresponding translation files for both English and French. The structure is consistent across all five subServices (api, database, bots, microservices, cloud) with each having proper features, benefits, faqItems, and complementaryServices arrays. The empty testimonials: [] arrays serve as appropriate placeholders for future expansion.

apps/web/src/components/marketing/services/faq-section.tsx (2)

21-37: Clean extraction of FAQ item rendering into a separate component.

The FAQItem component correctly encapsulates translation logic and renders accordion items. Using item.key as the React key (line 73) is better than using index for list stability.


54-60: Good defensive programming with null check.

The early return when faqItems.length === 0 prevents rendering an empty FAQ section, which is the correct UX behavior.

apps/web/src/app/[locale]/services/[category]/[service]/page.tsx (3)

17-21: Good extraction of service lookup logic.

The findService helper function promotes code reuse between generateMetadata and ServicePage, following DRY principles.


23-47: Proper metadata generation with not-found handling.

The metadata function correctly handles missing categories/services by returning noIndex: true, preventing search engine indexing of invalid routes. This aligns with the pattern used in apps/web/src/app/[locale]/services/[category]/page.tsx.


117-129: Correct static params generation for all service combinations.

The generateStaticParams function correctly generates all category/service path combinations for static site generation. This follows the established pattern from the category page.

apps/web/src/constants/services/frontend.ts (2)

34-48: Consistent key-based model for frontend benefits and process list.

The structure correctly implements ServiceBenefit and ServiceProcessStep interfaces with kebab-case keys matching the backend service pattern.


50-177: Well-structured frontend subServices definition.

All four subServices (web, mobile, desktop, pwa) consistently implement the required structure with features, benefits, faqItems, testimonials, and complementaryServices. Based on learnings, this follows the recommended organization for constants/static data in the web app.

apps/web/src/components/marketing/services/testimonials-section.tsx (1)

17-50: Clean TestimonialCard component with proper translation integration.

The component correctly separates translated content (testimonials.${testimonial.key}.content) from static author metadata. Author information (name, role, company) appropriately remains untranslated as these are proper nouns.

apps/web/src/constants/services/design.ts (2)

36-50: Consistent implementation of key-based benefits and process list.

The design service correctly implements the same pattern as backend and frontend services, maintaining consistency across the codebase.


52-209: Complete and consistent design subServices implementation.

All five design subServices (moodboard, wireframes, ui, branding, audit) follow the established key-based structure with proper type compliance. The migration to translation keys is thorough and consistent with other service category files.

apps/web/src/components/marketing/navbar/index.tsx (3)

3-16: LGTM! Clean translation integration.

The imports correctly integrate the translation infrastructure with locale-aware Link component and useTranslation hook.


212-239: LGTM! Services mobile menu implementation is solid.

The special handling for the services section with expand/collapse functionality is well-implemented. The component correctly:

  • Uses the isServices flag to differentiate services from other dropdowns
  • Manages expanded state appropriately
  • Renders translated service categories via MobileServiceCategory
  • Provides proper accessibility attributes with aria-expanded

114-125: No action needed—agency names are properly localized.

The translation pattern t("agencies.agency", { name: agency.name }) correctly handles localization. The template "agency": "Agency {name}" (and "agency": "Agence {name}" in French) translates the label while preserving city names as proper nouns, which should not be translated. This matches the approach used for project names (line 109) and is the correct implementation.

Likely an incorrect or invalid review comment.

apps/web/src/components/marketing/navbar/navigation/services/index.tsx (2)

83-132: LGTM! Navigation services cleanly refactored for i18n.

The NavigationServices component correctly:

  • Uses the layout/navbar namespace for navigation-level translations
  • Applies translation keys to all user-facing text (trigger, title, description, CTA)
  • Delegates to translation-aware child components for rendering service details
  • Maintains the original layout structure while adding full i18n support

18-81: Code structure and translation integration verified.

The refactoring introduces clean, well-separated components (SubServiceLink, ServiceCategoryPreview, ServiceCategoryCard) with a consistent translation namespace pattern. Each component correctly uses dynamic namespaces like constants/services/${categoryId}/${subService.id} for sub-services and constants/services/${service.id} for categories.

All required translation files exist with the proper structure:

  • Category-level translations: constants/services/{categoryId}.json (backend, design, frontend, integration)
  • Sub-service translations: constants/services/{categoryId}/{subServiceId}.json for all 20 sub-services across both English and French

Translation keys ("name", "description") align with component usage, and the implementation is production-ready.

Copy link

@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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/app/[locale]/agency/[city]/page.tsx (1)

19-21: Add locale to the params type definition across this file.

The route structure [locale]/agency/[city] makes both locale and city available in the params object. The type definition should include both segments for accuracy, consistent with other dynamic routes like [locale]/careers/[id] (which includes locale: string in its params type). While getTranslation retrieves locale from HTTP headers, the type should reflect all available route parameters.

Apply to all occurrences

Update lines 19-21, 23-31 (generateMetadata), and 49-60 (default export) from:

params: Promise<{ city: string }>

to:

params: Promise<{ locale: string; city: string }>
🧹 Nitpick comments (1)
apps/web/src/app/[locale]/agency/[city]/page.tsx (1)

62-79: Consider extracting FAQ items construction to avoid duplication.

The faqItems array construction duplicates the FAQ keys and translation logic that's already in AgencyFAQSection (lines 14-19, 50-65). While this array is needed for FAQPageSchema structured data, consider extracting the FAQ_KEYS constant to a shared location or creating a helper function to generate the items for both the component and structured data.

🔎 Suggested refactor approach

Option 1: Extract FAQ_KEYS to a shared constant file:

// In a shared constants file (e.g., @/constants/agency-faqs.ts)
export const AGENCY_FAQ_KEYS = [
  "choose-agency",
  "project-costs", 
  "development-time",
  "remote-work",
] as const;

Then import and use in both files to ensure consistency.

Option 2: Create a helper function that generates faqItems given translation functions:

// In a shared utils file
export function generateAgencyFaqItems(
  t: TranslationFunction,
  agency: Agency
) {
  return AGENCY_FAQ_KEYS.map(key => ({
    questionName: t(`faq.questions.${key}.question`, { 
      city: agency.name,
      region: agency.region 
    }),
    acceptedAnswerText: t(`faq.questions.${key}.answer`, { 
      city: agency.name,
      region: agency.region 
    })
  }));
}

This eliminates the need to manually keep the arrays in sync.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 42ce2fd and cad9e83.

📒 Files selected for processing (2)
  • apps/web/src/app/[locale]/agency/[city]/page.tsx
  • apps/web/src/components/marketing/agency/agency-faq-section.tsx
🔇 Additional comments (3)
apps/web/src/components/marketing/agency/agency-faq-section.tsx (1)

1-72: LGTM! Clean translation-based FAQ component.

The component correctly uses client-side translation hooks, handles missing agencies gracefully, and dynamically interpolates city/region context into FAQ questions and answers. The implementation follows React best practices and maintains accessibility.

apps/web/src/app/[locale]/agency/[city]/page.tsx (2)

33-39: ✅ Past review comment properly addressed.

The hardcoded French strings have been correctly replaced with translation keys using t('metadata.not-found.title') and t('metadata.not-found.description').


167-175: ✅ Past review comment properly addressed.

The <a> tag has been correctly replaced with locale-aware Link component from @onruntime/translations/next.

Copy link

@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: 2

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d39684c and 94b0075.

📒 Files selected for processing (8)
  • .changeset/add-sitemap-options.md
  • .changeset/config.json
  • apps/web/next-sitemap.config.js
  • apps/web/next.config.ts
  • apps/web/package.json
  • apps/web/src/app/robots.ts
  • apps/web/src/app/sitemap.xml/[id]/route.ts
  • apps/web/src/app/sitemap.xml/route.ts
💤 Files with no reviewable changes (1)
  • apps/web/next-sitemap.config.js
✅ Files skipped from review due to trivial changes (1)
  • .changeset/add-sitemap-options.md
🧰 Additional context used
📓 Path-based instructions (1)
apps/web/src/app/**/route.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Next.js unstable_cache for caching external API responses in API routes

Files:

  • apps/web/src/app/sitemap.xml/[id]/route.ts
  • apps/web/src/app/sitemap.xml/route.ts
🧠 Learnings (1)
📚 Learning: 2025-12-28T00:50:00.164Z
Learnt from: CR
Repo: onRuntime/onruntime PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-28T00:50:00.164Z
Learning: Applies to apps/web/src/app/**/route.{ts,tsx} : Use Next.js unstable_cache for caching external API responses in API routes

Applied to files:

  • apps/web/src/app/robots.ts
  • apps/web/src/app/sitemap.xml/[id]/route.ts
  • apps/web/src/app/sitemap.xml/route.ts
🧬 Code graph analysis (1)
apps/web/src/app/sitemap.xml/route.ts (3)
apps/web/src/app/sitemap.xml/[id]/route.ts (1)
  • dynamic (5-5)
packages/next-sitemap/src/app/index.ts (1)
  • createSitemapIndexHandler (138-159)
apps/web/src/constants/site-config.ts (1)
  • siteConfig (3-14)
🔇 Additional comments (6)
apps/web/package.json (3)

5-10: LGTM! Sitemap generation refactored to workspace package.

The removal of the postbuild script and migration to @onruntime/next-sitemap (workspace package) aligns well with the i18n overhaul and centralizes sitemap generation logic.


17-18: Workspace dependencies are properly configured.

The @onruntime/next-sitemap and @onruntime/translations packages exist in the monorepo at packages/next-sitemap and packages/translations with correct names and exports. The pnpm-workspace.yaml configuration includes packages/*, and the workspace:* references in apps/web/package.json are valid. The @radix-ui/react-dropdown-menu@2.1.16 dependency is compatible with React 19 as verified in the lock file.


21-21: No action needed. @radix-ui/react-dropdown-menu@^2.1.16 explicitly supports React 19 via its peer dependencies (^19.0 || ^19.0.0-rc), so the current dependency is compatible.

Likely an incorrect or invalid review comment.

apps/web/src/app/sitemap.xml/route.ts (1)

1-17: LGTM!

The sitemap index route is well-structured and correctly uses createSitemapIndexHandler with site configuration and locale data. The force-static export ensures the sitemap is generated at build time.

Note: The retrieved learning about unstable_cache for external API responses doesn't apply here since this route generates sitemaps from local route context rather than fetching external data.

apps/web/next.config.ts (1)

19-26: LGTM!

The sitemap rewrite rule correctly maps the traditional /sitemap-:id.xml pattern to the app router /sitemap.xml/:id structure, ensuring backward compatibility with standard sitemap conventions.

apps/web/src/app/sitemap.xml/[id]/route.ts (1)

1-17: LGTM!

The dynamic sitemap route correctly uses createSitemapHandler (not the index variant) and exports both generateStaticParams and GET for static generation. The route properly integrates with the locale-aware routing structure.

Note: The retrieved learning about unstable_cache for external API responses doesn't apply here since this route generates sitemaps from local route context.

@antoinekm antoinekm merged commit 77c7a36 into master Dec 31, 2025
4 checks passed
@antoinekm antoinekm deleted the antoinekm/i18n-web branch December 31, 2025 00:12
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