diff --git a/.github/workflows/i18n-pull.yaml b/.github/workflows/i18n-pull.yaml
index 6d3cdbf179..d88ff9e1ca 100644
--- a/.github/workflows/i18n-pull.yaml
+++ b/.github/workflows/i18n-pull.yaml
@@ -58,6 +58,7 @@ jobs:
npx nx run twenty-server:lingui:compile --strict
npx nx run twenty-emails:lingui:compile --strict
npx nx run twenty-front:lingui:compile --strict
+ npx nx run twenty-website-new:lingui:compile --strict
continue-on-error: true
- name: Stash any changes before pulling translations
@@ -115,6 +116,7 @@ jobs:
npx nx run twenty-server:lingui:compile
npx nx run twenty-emails:lingui:compile
npx nx run twenty-front:lingui:compile
+ npx nx run twenty-website-new:lingui:compile
git status
git add .
if ! git diff --staged --quiet --exit-code; then
diff --git a/.github/workflows/i18n-push.yaml b/.github/workflows/i18n-push.yaml
index 2f7229674d..73d402acbc 100644
--- a/.github/workflows/i18n-push.yaml
+++ b/.github/workflows/i18n-push.yaml
@@ -41,6 +41,7 @@ jobs:
npx nx run twenty-server:lingui:extract
npx nx run twenty-emails:lingui:extract
npx nx run twenty-front:lingui:extract
+ npx nx run twenty-website-new:lingui:extract
- name: Check and commit extracted files
id: check_extract_changes
@@ -60,6 +61,7 @@ jobs:
npx nx run twenty-server:lingui:compile
npx nx run twenty-emails:lingui:compile
npx nx run twenty-front:lingui:compile
+ npx nx run twenty-website-new:lingui:compile
- name: Check and commit compiled files
id: check_compile_changes
diff --git a/packages/twenty-emails/src/utils/i18n.utils.ts b/packages/twenty-emails/src/utils/i18n.utils.ts
index 975652bd53..ccd8d67ded 100644
--- a/packages/twenty-emails/src/utils/i18n.utils.ts
+++ b/packages/twenty-emails/src/utils/i18n.utils.ts
@@ -1,6 +1,6 @@
-import { setupI18n, type I18n, type Messages } from '@lingui/core';
+import { type Messages } from '@lingui/core';
+import { createI18nInstanceFactory } from 'twenty-shared/i18n';
import { type APP_LOCALES } from 'twenty-shared/translations';
-import { isDefined } from 'twenty-shared/utils';
import { messages as afMessages } from '@/locales/generated/af-ZA';
import { messages as arMessages } from '@/locales/generated/ar-SA';
import { messages as caMessages } from '@/locales/generated/ca-ES';
@@ -67,20 +67,4 @@ const messages: Record = {
'zh-TW': zhHantMessages,
};
-const i18nInstancesMap: Partial> = {};
-
-export const createI18nInstance = (locale: keyof typeof APP_LOCALES): I18n => {
- if (isDefined(i18nInstancesMap[locale])) {
- return i18nInstancesMap[locale];
- }
-
- const i18nInstance = setupI18n();
- const localeMessages = messages[locale] ?? messages.en;
-
- i18nInstance.load(locale, localeMessages);
- i18nInstance.activate(locale);
-
- i18nInstancesMap[locale] = i18nInstance;
-
- return i18nInstance;
-};
+export const createI18nInstance = createI18nInstanceFactory(messages);
diff --git a/packages/twenty-shared/package.json b/packages/twenty-shared/package.json
index 9ffa195e1a..18c8f80c72 100644
--- a/packages/twenty-shared/package.json
+++ b/packages/twenty-shared/package.json
@@ -66,6 +66,11 @@
"import": "./dist/database-events.mjs",
"require": "./dist/database-events.cjs"
},
+ "./i18n": {
+ "types": "./dist/i18n/index.d.ts",
+ "import": "./dist/i18n.mjs",
+ "require": "./dist/i18n.cjs"
+ },
"./logic-function": {
"types": "./dist/logic-function/index.d.ts",
"import": "./dist/logic-function.mjs",
@@ -118,6 +123,7 @@
"application",
"constants",
"database-events",
+ "i18n",
"logic-function",
"metadata",
"testing",
@@ -142,6 +148,9 @@
"database-events": [
"dist/database-events/index.d.ts"
],
+ "i18n": [
+ "dist/i18n/index.d.ts"
+ ],
"logic-function": [
"dist/logic-function/index.d.ts"
],
diff --git a/packages/twenty-shared/project.json b/packages/twenty-shared/project.json
index 8d7cbf53bf..9d42cea4df 100644
--- a/packages/twenty-shared/project.json
+++ b/packages/twenty-shared/project.json
@@ -20,6 +20,8 @@
"{projectRoot}/constants/dist",
"{projectRoot}/database-events/package.json",
"{projectRoot}/database-events/dist",
+ "{projectRoot}/i18n/package.json",
+ "{projectRoot}/i18n/dist",
"{projectRoot}/logic-function/package.json",
"{projectRoot}/logic-function/dist",
"{projectRoot}/metadata/package.json",
diff --git a/packages/twenty-shared/src/i18n/create-i18n-instance-factory.ts b/packages/twenty-shared/src/i18n/create-i18n-instance-factory.ts
new file mode 100644
index 0000000000..560c9cea19
--- /dev/null
+++ b/packages/twenty-shared/src/i18n/create-i18n-instance-factory.ts
@@ -0,0 +1,29 @@
+import { setupI18n, type I18n, type Messages } from '@lingui/core';
+
+import { type AppLocale } from '@/translations/constants/AppLocales';
+import { SOURCE_LOCALE } from '@/translations/constants/SourceLocale';
+
+export type LocaleMessagesMap = Partial>;
+
+export const createI18nInstanceFactory = (
+ messagesByLocale: LocaleMessagesMap,
+) => {
+ const cache: Partial> = {};
+
+ return (locale: AppLocale): I18n => {
+ const cached = cache[locale];
+ if (cached !== undefined) {
+ return cached;
+ }
+
+ const fallbackMessages = messagesByLocale[SOURCE_LOCALE] ?? {};
+ const localeMessages = messagesByLocale[locale] ?? fallbackMessages;
+
+ const i18n = setupI18n();
+ i18n.load(locale, localeMessages);
+ i18n.activate(locale);
+
+ cache[locale] = i18n;
+ return i18n;
+ };
+};
diff --git a/packages/twenty-shared/src/i18n/index.ts b/packages/twenty-shared/src/i18n/index.ts
new file mode 100644
index 0000000000..dba3968836
--- /dev/null
+++ b/packages/twenty-shared/src/i18n/index.ts
@@ -0,0 +1,11 @@
+/*
+ * _____ _
+ *|_ _|_ _____ _ __ | |_ _ _
+ * | | \ \ /\ / / _ \ '_ \| __| | | | Auto-generated file
+ * | | \ V V / __/ | | | |_| |_| | Any edits to this will be overridden
+ * |_| \_/\_/ \___|_| |_|\__|\__, |
+ * |___/
+ */
+
+export type { LocaleMessagesMap } from './create-i18n-instance-factory';
+export { createI18nInstanceFactory } from './create-i18n-instance-factory';
diff --git a/packages/twenty-website-new/.oxlintrc.json b/packages/twenty-website-new/.oxlintrc.json
index f366c9222f..b5af6b9985 100644
--- a/packages/twenty-website-new/.oxlintrc.json
+++ b/packages/twenty-website-new/.oxlintrc.json
@@ -4,7 +4,7 @@
"categories": {
"correctness": "error"
},
- "ignorePatterns": ["node_modules"],
+ "ignorePatterns": ["node_modules", "src/locales/generated"],
"rules": {
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"no-console": "off",
diff --git a/packages/twenty-website-new/.prettierignore b/packages/twenty-website-new/.prettierignore
new file mode 100644
index 0000000000..23c4124138
--- /dev/null
+++ b/packages/twenty-website-new/.prettierignore
@@ -0,0 +1 @@
+src/locales/generated
diff --git a/packages/twenty-website-new/lingui.config.ts b/packages/twenty-website-new/lingui.config.ts
new file mode 100644
index 0000000000..debcc9c7ac
--- /dev/null
+++ b/packages/twenty-website-new/lingui.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from '@lingui/conf';
+import { formatter } from '@lingui/format-po';
+import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
+
+export default defineConfig({
+ sourceLocale: SOURCE_LOCALE,
+ locales: Object.values(APP_LOCALES),
+ pseudoLocale: 'pseudo-en',
+ fallbackLocales: {
+ 'pseudo-en': 'en',
+ default: SOURCE_LOCALE,
+ },
+ catalogs: [
+ {
+ path: '/src/locales/{locale}',
+ include: ['src'],
+ },
+ ],
+ catalogsMergePath: '/src/locales/generated/{locale}',
+ compileNamespace: 'ts',
+ format: formatter({ lineNumbers: false, printLinguiId: true }),
+});
diff --git a/packages/twenty-website-new/next.config.ts b/packages/twenty-website-new/next.config.ts
index a1fed9ad44..1ab5ac8f15 100644
--- a/packages/twenty-website-new/next.config.ts
+++ b/packages/twenty-website-new/next.config.ts
@@ -36,6 +36,19 @@ const nextConfig: LinariaConfig = {
configFile: path.resolve(__dirname, 'wyw-in-js.config.cjs'),
},
reactCompiler: true,
+ experimental: {
+ swcPlugins: [
+ [
+ '@lingui/swc-plugin',
+ {
+ runtimeModules: {
+ i18n: ['@lingui/core', 'i18n'],
+ trans: ['@lingui/react', 'Trans'],
+ },
+ },
+ ],
+ ],
+ },
async headers() {
return [
{
diff --git a/packages/twenty-website-new/package.json b/packages/twenty-website-new/package.json
index 42478f69dd..af9040eb91 100644
--- a/packages/twenty-website-new/package.json
+++ b/packages/twenty-website-new/package.json
@@ -15,6 +15,8 @@
"@gsap/react": "2.1.2",
"@linaria/core": "^7.0.0",
"@linaria/react": "^7.0.1",
+ "@lingui/core": "^5.1.2",
+ "@lingui/react": "^5.1.2",
"@lottiefiles/dotlottie-react": "^0.18.10",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
@@ -31,9 +33,14 @@
"sharp": "^0.33.5",
"stripe": "^20.3.1",
"three": "^0.183.2",
+ "twenty-shared": "workspace:*",
"zod": "^4.1.11"
},
"devDependencies": {
+ "@lingui/cli": "^5.1.2",
+ "@lingui/conf": "5.1.2",
+ "@lingui/format-po": "5.1.2",
+ "@lingui/swc-plugin": "^5.11.0",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
diff --git a/packages/twenty-website-new/project.json b/packages/twenty-website-new/project.json
index 1b73738b73..d34b1d7665 100644
--- a/packages/twenty-website-new/project.json
+++ b/packages/twenty-website-new/project.json
@@ -117,6 +117,22 @@
"configurations": {
"fix": {}
}
+ },
+ "lingui:extract": {
+ "executor": "nx:run-commands",
+ "dependsOn": ["^build"],
+ "options": {
+ "cwd": "{projectRoot}",
+ "command": "lingui extract --overwrite --clean"
+ }
+ },
+ "lingui:compile": {
+ "executor": "nx:run-commands",
+ "dependsOn": ["^build"],
+ "options": {
+ "cwd": "{projectRoot}",
+ "command": "lingui compile --typescript"
+ }
}
}
}
diff --git a/packages/twenty-website-new/src/app/(home)/helped.data.ts b/packages/twenty-website-new/src/app/[locale]/(home)/helped.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/(home)/helped.data.ts
rename to packages/twenty-website-new/src/app/[locale]/(home)/helped.data.ts
diff --git a/packages/twenty-website-new/src/app/(home)/hero.data.ts b/packages/twenty-website-new/src/app/[locale]/(home)/hero.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/(home)/hero.data.ts
rename to packages/twenty-website-new/src/app/[locale]/(home)/hero.data.ts
diff --git a/packages/twenty-website-new/src/app/(home)/home-stepper.data.ts b/packages/twenty-website-new/src/app/[locale]/(home)/home-stepper.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/(home)/home-stepper.data.ts
rename to packages/twenty-website-new/src/app/[locale]/(home)/home-stepper.data.ts
diff --git a/packages/twenty-website-new/src/app/(home)/page.tsx b/packages/twenty-website-new/src/app/[locale]/(home)/page.tsx
similarity index 91%
rename from packages/twenty-website-new/src/app/(home)/page.tsx
rename to packages/twenty-website-new/src/app/[locale]/(home)/page.tsx
index 7814a5c74f..38bcdb6b3e 100644
--- a/packages/twenty-website-new/src/app/(home)/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/(home)/page.tsx
@@ -1,10 +1,10 @@
-import { HELPED_DATA } from '@/app/(home)/helped.data';
-import { HERO_DATA } from '@/app/(home)/hero.data';
-import { HOME_STEPPER_DATA } from '@/app/(home)/home-stepper.data';
-import { PROBLEM_DATA } from '@/app/(home)/problem.data';
-import { TESTIMONIALS_DATA } from '@/app/(home)/testimonials.data';
-import { THREE_CARDS_FEATURE_DATA } from '@/app/(home)/three-cards-feature.data';
-import { THREE_CARDS_ILLUSTRATION_DATA } from '@/app/(home)/three-cards-illustration.data';
+import { HELPED_DATA } from '@/app/[locale]/(home)/helped.data';
+import { HERO_DATA } from '@/app/[locale]/(home)/hero.data';
+import { HOME_STEPPER_DATA } from '@/app/[locale]/(home)/home-stepper.data';
+import { PROBLEM_DATA } from '@/app/[locale]/(home)/problem.data';
+import { TESTIMONIALS_DATA } from '@/app/[locale]/(home)/testimonials.data';
+import { THREE_CARDS_FEATURE_DATA } from '@/app/[locale]/(home)/three-cards-feature.data';
+import { THREE_CARDS_ILLUSTRATION_DATA } from '@/app/[locale]/(home)/three-cards-illustration.data';
import { TalkToUsButton } from '@/lib/contact-cal';
import { FAQ_DATA } from '@/sections/Faq/data';
import { MENU_DATA } from '@/sections/Menu/data';
@@ -22,15 +22,18 @@ import { Menu } from '@/sections/Menu/components';
import { Problem } from '@/sections/Problem/components';
import { Testimonials } from '@/sections/Testimonials/components';
import { ThreeCards } from '@/sections/ThreeCards/components';
+import { buildLocalizedMetadata } from '@/lib/seo';
import { TrustedBy } from '@/sections/TrustedBy/components';
import { theme } from '@/theme';
import { css } from '@linaria/core';
import { styled } from '@linaria/react';
-import type { Metadata } from 'next';
-export const metadata: Metadata = {
- alternates: { canonical: '/' },
-};
+export const generateMetadata = buildLocalizedMetadata({
+ path: '/',
+ title: 'Twenty | #1 open source CRM',
+ description:
+ 'The #1 open source CRM for modern teams. Modular, scalable, and built to fit your business.',
+});
const HOME_TOP_BACKGROUND_COLOR = '#F4F4F4';
const PRODUCT_HUNT_LAUNCH_URL =
diff --git a/packages/twenty-website-new/src/app/(home)/problem.data.ts b/packages/twenty-website-new/src/app/[locale]/(home)/problem.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/(home)/problem.data.ts
rename to packages/twenty-website-new/src/app/[locale]/(home)/problem.data.ts
diff --git a/packages/twenty-website-new/src/app/(home)/testimonials.data.ts b/packages/twenty-website-new/src/app/[locale]/(home)/testimonials.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/(home)/testimonials.data.ts
rename to packages/twenty-website-new/src/app/[locale]/(home)/testimonials.data.ts
diff --git a/packages/twenty-website-new/src/app/(home)/three-cards-feature.data.ts b/packages/twenty-website-new/src/app/[locale]/(home)/three-cards-feature.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/(home)/three-cards-feature.data.ts
rename to packages/twenty-website-new/src/app/[locale]/(home)/three-cards-feature.data.ts
diff --git a/packages/twenty-website-new/src/app/(home)/three-cards-illustration.data.ts b/packages/twenty-website-new/src/app/[locale]/(home)/three-cards-illustration.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/(home)/three-cards-illustration.data.ts
rename to packages/twenty-website-new/src/app/[locale]/(home)/three-cards-illustration.data.ts
diff --git a/packages/twenty-website-new/src/app/customers/9dots/page.tsx b/packages/twenty-website-new/src/app/[locale]/customers/9dots/page.tsx
similarity index 96%
rename from packages/twenty-website-new/src/app/customers/9dots/page.tsx
rename to packages/twenty-website-new/src/app/[locale]/customers/9dots/page.tsx
index 93732df179..55ed77a0d8 100644
--- a/packages/twenty-website-new/src/app/customers/9dots/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/customers/9dots/page.tsx
@@ -1,13 +1,12 @@
import { MENU_DATA } from '@/sections/Menu/data';
-import { CustomersCaseStudySignoff } from '@/app/customers/_components/CustomersCaseStudySignoff';
+import { CustomersCaseStudySignoff } from '@/app/[locale]/customers/_components/CustomersCaseStudySignoff';
import { getCaseStudyPalette, type CaseStudyData } from '@/lib/customers';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudy } from '@/sections/CaseStudy/components';
import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme';
-import { buildPageMetadata } from '@/lib/seo';
-import type { Metadata } from 'next';
+import { buildLocalizedMetadata } from '@/lib/seo';
const PLACEHOLDER_HERO =
'https://images.unsplash.com/photo-1733244766159-f58f4184fd38?w=1600&q=80';
@@ -99,7 +98,7 @@ const CASE_STUDY: CaseStudyData = {
},
};
-export const metadata: Metadata = buildPageMetadata({
+export const generateMetadata = buildLocalizedMetadata({
path: '/customers/9dots',
title: CASE_STUDY.meta.title,
description: CASE_STUDY.meta.description,
diff --git a/packages/twenty-website-new/src/app/customers/_components/CustomersCaseStudySignoff.tsx b/packages/twenty-website-new/src/app/[locale]/customers/_components/CustomersCaseStudySignoff.tsx
similarity index 100%
rename from packages/twenty-website-new/src/app/customers/_components/CustomersCaseStudySignoff.tsx
rename to packages/twenty-website-new/src/app/[locale]/customers/_components/CustomersCaseStudySignoff.tsx
diff --git a/packages/twenty-website-new/src/app/customers/act-education/page.tsx b/packages/twenty-website-new/src/app/[locale]/customers/act-education/page.tsx
similarity index 96%
rename from packages/twenty-website-new/src/app/customers/act-education/page.tsx
rename to packages/twenty-website-new/src/app/[locale]/customers/act-education/page.tsx
index 3e020a6fe6..f97eee6938 100644
--- a/packages/twenty-website-new/src/app/customers/act-education/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/customers/act-education/page.tsx
@@ -1,13 +1,12 @@
import { MENU_DATA } from '@/sections/Menu/data';
-import { CustomersCaseStudySignoff } from '@/app/customers/_components/CustomersCaseStudySignoff';
+import { CustomersCaseStudySignoff } from '@/app/[locale]/customers/_components/CustomersCaseStudySignoff';
import { getCaseStudyPalette, type CaseStudyData } from '@/lib/customers';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudy } from '@/sections/CaseStudy/components';
import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme';
-import { buildPageMetadata } from '@/lib/seo';
-import type { Metadata } from 'next';
+import { buildLocalizedMetadata } from '@/lib/seo';
const PLACEHOLDER_HERO =
'https://images.unsplash.com/photo-1687600154329-150952c73169?w=1600&q=80';
@@ -99,7 +98,7 @@ const CASE_STUDY: CaseStudyData = {
},
};
-export const metadata: Metadata = buildPageMetadata({
+export const generateMetadata = buildLocalizedMetadata({
path: '/customers/act-education',
title: CASE_STUDY.meta.title,
description: CASE_STUDY.meta.description,
diff --git a/packages/twenty-website-new/src/app/customers/alternative-partners/page.tsx b/packages/twenty-website-new/src/app/[locale]/customers/alternative-partners/page.tsx
similarity index 95%
rename from packages/twenty-website-new/src/app/customers/alternative-partners/page.tsx
rename to packages/twenty-website-new/src/app/[locale]/customers/alternative-partners/page.tsx
index f79c0b87e4..dbc185cb73 100644
--- a/packages/twenty-website-new/src/app/customers/alternative-partners/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/customers/alternative-partners/page.tsx
@@ -1,13 +1,12 @@
import { MENU_DATA } from '@/sections/Menu/data';
-import { CustomersCaseStudySignoff } from '@/app/customers/_components/CustomersCaseStudySignoff';
+import { CustomersCaseStudySignoff } from '@/app/[locale]/customers/_components/CustomersCaseStudySignoff';
import { getCaseStudyPalette, type CaseStudyData } from '@/lib/customers';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudy } from '@/sections/CaseStudy/components';
import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme';
-import { buildPageMetadata } from '@/lib/seo';
-import type { Metadata } from 'next';
+import { buildLocalizedMetadata } from '@/lib/seo';
const PLACEHOLDER_HERO =
'https://images.unsplash.com/photo-1702047149248-a6049168d2a8?w=1600&q=80';
@@ -73,7 +72,7 @@ const CASE_STUDY: CaseStudyData = {
},
};
-export const metadata: Metadata = buildPageMetadata({
+export const generateMetadata = buildLocalizedMetadata({
path: '/customers/alternative-partners',
title: CASE_STUDY.meta.title,
description: CASE_STUDY.meta.description,
diff --git a/packages/twenty-website-new/src/app/customers/elevate-consulting/page.tsx b/packages/twenty-website-new/src/app/[locale]/customers/elevate-consulting/page.tsx
similarity index 97%
rename from packages/twenty-website-new/src/app/customers/elevate-consulting/page.tsx
rename to packages/twenty-website-new/src/app/[locale]/customers/elevate-consulting/page.tsx
index b3fd65cd9a..6b5499cdc1 100644
--- a/packages/twenty-website-new/src/app/customers/elevate-consulting/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/customers/elevate-consulting/page.tsx
@@ -1,13 +1,12 @@
import { MENU_DATA } from '@/sections/Menu/data';
-import { CustomersCaseStudySignoff } from '@/app/customers/_components/CustomersCaseStudySignoff';
+import { CustomersCaseStudySignoff } from '@/app/[locale]/customers/_components/CustomersCaseStudySignoff';
import { getCaseStudyPalette, type CaseStudyData } from '@/lib/customers';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudy } from '@/sections/CaseStudy/components';
import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme';
-import { buildPageMetadata } from '@/lib/seo';
-import type { Metadata } from 'next';
+import { buildLocalizedMetadata } from '@/lib/seo';
const PLACEHOLDER_HERO =
'https://images.unsplash.com/photo-1758873269035-aae0e1fd3422?w=1600&q=80';
@@ -113,7 +112,7 @@ const CASE_STUDY: CaseStudyData = {
},
};
-export const metadata: Metadata = buildPageMetadata({
+export const generateMetadata = buildLocalizedMetadata({
path: '/customers/elevate-consulting',
title: CASE_STUDY.meta.title,
description: CASE_STUDY.meta.description,
diff --git a/packages/twenty-website-new/src/app/customers/netzero/page.tsx b/packages/twenty-website-new/src/app/[locale]/customers/netzero/page.tsx
similarity index 96%
rename from packages/twenty-website-new/src/app/customers/netzero/page.tsx
rename to packages/twenty-website-new/src/app/[locale]/customers/netzero/page.tsx
index 0bd7aafad4..f25f3903ac 100644
--- a/packages/twenty-website-new/src/app/customers/netzero/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/customers/netzero/page.tsx
@@ -1,13 +1,12 @@
import { MENU_DATA } from '@/sections/Menu/data';
-import { CustomersCaseStudySignoff } from '@/app/customers/_components/CustomersCaseStudySignoff';
+import { CustomersCaseStudySignoff } from '@/app/[locale]/customers/_components/CustomersCaseStudySignoff';
import { getCaseStudyPalette, type CaseStudyData } from '@/lib/customers';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudy } from '@/sections/CaseStudy/components';
import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme';
-import { buildPageMetadata } from '@/lib/seo';
-import type { Metadata } from 'next';
+import { buildLocalizedMetadata } from '@/lib/seo';
const PLACEHOLDER_HERO =
'https://images.unsplash.com/photo-1744830343976-ce690ba2a67c?w=1600&q=80';
@@ -103,7 +102,7 @@ const CASE_STUDY: CaseStudyData = {
},
};
-export const metadata: Metadata = buildPageMetadata({
+export const generateMetadata = buildLocalizedMetadata({
path: '/customers/netzero',
title: CASE_STUDY.meta.title,
description: CASE_STUDY.meta.description,
diff --git a/packages/twenty-website-new/src/app/customers/page.tsx b/packages/twenty-website-new/src/app/[locale]/customers/page.tsx
similarity index 97%
rename from packages/twenty-website-new/src/app/customers/page.tsx
rename to packages/twenty-website-new/src/app/[locale]/customers/page.tsx
index 0a176f1340..98ee905a50 100644
--- a/packages/twenty-website-new/src/app/customers/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/customers/page.tsx
@@ -14,11 +14,10 @@ import { Menu } from '@/sections/Menu/components';
import { Signoff } from '@/sections/Signoff/components';
import { TrustedBy } from '@/sections/TrustedBy/components';
import { theme } from '@/theme';
-import { buildPageMetadata } from '@/lib/seo';
+import { buildLocalizedMetadata } from '@/lib/seo';
import { css } from '@linaria/core';
-import type { Metadata } from 'next';
-export const metadata: Metadata = buildPageMetadata({
+export const generateMetadata = buildLocalizedMetadata({
path: '/customers',
title: 'Customers | Twenty',
description:
diff --git a/packages/twenty-website-new/src/app/customers/w3villa/page.tsx b/packages/twenty-website-new/src/app/[locale]/customers/w3villa/page.tsx
similarity index 96%
rename from packages/twenty-website-new/src/app/customers/w3villa/page.tsx
rename to packages/twenty-website-new/src/app/[locale]/customers/w3villa/page.tsx
index 90ac376afa..eff21a97ba 100644
--- a/packages/twenty-website-new/src/app/customers/w3villa/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/customers/w3villa/page.tsx
@@ -1,13 +1,12 @@
import { MENU_DATA } from '@/sections/Menu/data';
-import { CustomersCaseStudySignoff } from '@/app/customers/_components/CustomersCaseStudySignoff';
+import { CustomersCaseStudySignoff } from '@/app/[locale]/customers/_components/CustomersCaseStudySignoff';
import { getCaseStudyPalette, type CaseStudyData } from '@/lib/customers';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudy } from '@/sections/CaseStudy/components';
import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme';
-import { buildPageMetadata } from '@/lib/seo';
-import type { Metadata } from 'next';
+import { buildLocalizedMetadata } from '@/lib/seo';
const PLACEHOLDER_HERO =
'https://images.unsplash.com/photo-1756830231350-3b501f63c5c1?w=1600&q=80';
@@ -100,7 +99,7 @@ const CASE_STUDY: CaseStudyData = {
},
};
-export const metadata: Metadata = buildPageMetadata({
+export const generateMetadata = buildLocalizedMetadata({
path: '/customers/w3villa',
title: CASE_STUDY.meta.title,
description: CASE_STUDY.meta.description,
diff --git a/packages/twenty-website-new/src/app/enterprise/activate/EnterpriseActivateClient.tsx b/packages/twenty-website-new/src/app/[locale]/enterprise/activate/EnterpriseActivateClient.tsx
similarity index 100%
rename from packages/twenty-website-new/src/app/enterprise/activate/EnterpriseActivateClient.tsx
rename to packages/twenty-website-new/src/app/[locale]/enterprise/activate/EnterpriseActivateClient.tsx
diff --git a/packages/twenty-website-new/src/app/enterprise/activate/page.tsx b/packages/twenty-website-new/src/app/[locale]/enterprise/activate/page.tsx
similarity index 92%
rename from packages/twenty-website-new/src/app/enterprise/activate/page.tsx
rename to packages/twenty-website-new/src/app/[locale]/enterprise/activate/page.tsx
index 7c2f5e031e..299932ff16 100644
--- a/packages/twenty-website-new/src/app/enterprise/activate/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/enterprise/activate/page.tsx
@@ -1,5 +1,5 @@
import { MENU_DATA } from '@/sections/Menu/data';
-import { EnterpriseActivateClient } from '@/app/enterprise/activate/EnterpriseActivateClient';
+import { EnterpriseActivateClient } from '@/app/[locale]/enterprise/activate/EnterpriseActivateClient';
import { Body, Container, Eyebrow } from '@/design-system/components';
import type { HeadingType } from '@/design-system/components/Heading';
import { Pages } from '@/lib/pages';
@@ -8,12 +8,11 @@ import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels'
import { Hero } from '@/sections/Hero/components';
import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme';
-import { buildPageMetadata } from '@/lib/seo';
-import type { Metadata } from 'next';
+import { buildLocalizedMetadata } from '@/lib/seo';
import { Suspense } from 'react';
import { styled } from '@linaria/react';
-export const metadata: Metadata = buildPageMetadata({
+export const generateMetadata = buildLocalizedMetadata({
path: '/enterprise/activate',
title: 'Enterprise activation | Twenty',
description:
diff --git a/packages/twenty-website-new/src/app/halftone/_components/ControlsPanel.tsx b/packages/twenty-website-new/src/app/[locale]/halftone/_components/ControlsPanel.tsx
similarity index 98%
rename from packages/twenty-website-new/src/app/halftone/_components/ControlsPanel.tsx
rename to packages/twenty-website-new/src/app/[locale]/halftone/_components/ControlsPanel.tsx
index c3d3d9230f..223715f717 100644
--- a/packages/twenty-website-new/src/app/halftone/_components/ControlsPanel.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/halftone/_components/ControlsPanel.tsx
@@ -1,6 +1,6 @@
'use client';
-import type { ReactExportSettings } from '@/app/halftone/_lib/exporters';
+import type { ReactExportSettings } from '@/app/[locale]/halftone/_lib/exporters';
import { IconLayoutSidebarRightCollapse, IconShare } from '@tabler/icons-react';
import { styled } from '@linaria/react';
import type {
diff --git a/packages/twenty-website-new/src/app/halftone/_components/HalftoneStudio.tsx b/packages/twenty-website-new/src/app/[locale]/halftone/_components/HalftoneStudio.tsx
similarity index 99%
rename from packages/twenty-website-new/src/app/halftone/_components/HalftoneStudio.tsx
rename to packages/twenty-website-new/src/app/[locale]/halftone/_components/HalftoneStudio.tsx
index 4a93ba086c..fe4b9433b0 100644
--- a/packages/twenty-website-new/src/app/halftone/_components/HalftoneStudio.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/halftone/_components/HalftoneStudio.tsx
@@ -1,6 +1,6 @@
'use client';
-import { ControlsPanel } from '@/app/halftone/_components/ControlsPanel';
+import { ControlsPanel } from '@/app/[locale]/halftone/_components/ControlsPanel';
import {
DEFAULT_REACT_EXPORT_SETTINGS,
deriveExportComponentName,
@@ -9,14 +9,14 @@ import {
getExportedModelFile,
parseExportedPreset,
type ReactExportSettings,
-} from '@/app/halftone/_lib/exporters';
-import { resolveExportArtifactNames } from '@/app/halftone/_lib/exportNames';
-import { generateImageHalftoneSvg } from '@/app/halftone/_lib/imageSvgExport';
+} from '@/app/[locale]/halftone/_lib/exporters';
+import { resolveExportArtifactNames } from '@/app/[locale]/halftone/_lib/exportNames';
+import { generateImageHalftoneSvg } from '@/app/[locale]/halftone/_lib/imageSvgExport';
import {
buildShareUrl,
decodeShareState,
encodeShareState,
-} from '@/app/halftone/_lib/share';
+} from '@/app/[locale]/halftone/_lib/share';
import {
HalftoneCanvas,
type HalftoneSnapshotFn,
@@ -39,9 +39,9 @@ import {
normalizeHalftoneStudioSettings,
} from '@/lib/halftone/state';
import { Logo as LogoIcon } from '@/icons';
+import { LocalizedLink } from '@/lib/i18n';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
-import Link from 'next/link';
import {
useCallback,
useEffect,
@@ -89,7 +89,7 @@ const CanvasLayer = styled.div`
width: 100%;
`;
-const LogoLink = styled(Link)`
+const LogoLink = styled(LocalizedLink)`
display: grid;
left: 24px;
position: absolute;
diff --git a/packages/twenty-website-new/src/app/halftone/_components/controls/AnimationsTab.tsx b/packages/twenty-website-new/src/app/[locale]/halftone/_components/controls/AnimationsTab.tsx
similarity index 99%
rename from packages/twenty-website-new/src/app/halftone/_components/controls/AnimationsTab.tsx
rename to packages/twenty-website-new/src/app/[locale]/halftone/_components/controls/AnimationsTab.tsx
index e158cda298..e1e281122a 100644
--- a/packages/twenty-website-new/src/app/halftone/_components/controls/AnimationsTab.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/halftone/_components/controls/AnimationsTab.tsx
@@ -4,7 +4,7 @@ import {
formatAngle,
formatDecimal,
formatPercent,
-} from '@/app/halftone/_lib/formatters';
+} from '@/app/[locale]/halftone/_lib/formatters';
import type { HalftoneStudioSettings } from '@/lib/halftone/state';
import {
ColorControlLabel,
diff --git a/packages/twenty-website-new/src/app/halftone/_components/controls/DesignTab.tsx b/packages/twenty-website-new/src/app/[locale]/halftone/_components/controls/DesignTab.tsx
similarity index 99%
rename from packages/twenty-website-new/src/app/halftone/_components/controls/DesignTab.tsx
rename to packages/twenty-website-new/src/app/[locale]/halftone/_components/controls/DesignTab.tsx
index 7b873370cd..cd11a060ec 100644
--- a/packages/twenty-website-new/src/app/halftone/_components/controls/DesignTab.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/halftone/_components/controls/DesignTab.tsx
@@ -1,6 +1,9 @@
'use client';
-import { formatAngle, formatDecimal } from '@/app/halftone/_lib/formatters';
+import {
+ formatAngle,
+ formatDecimal,
+} from '@/app/[locale]/halftone/_lib/formatters';
import {
DEFAULT_GLASS_ANIMATION_SETTINGS,
DEFAULT_GLASS_LIGHTING_SETTINGS,
diff --git a/packages/twenty-website-new/src/app/halftone/_components/controls/ExportTab.tsx b/packages/twenty-website-new/src/app/[locale]/halftone/_components/controls/ExportTab.tsx
similarity index 98%
rename from packages/twenty-website-new/src/app/halftone/_components/controls/ExportTab.tsx
rename to packages/twenty-website-new/src/app/[locale]/halftone/_components/controls/ExportTab.tsx
index 43074066a8..d35b66ffa8 100644
--- a/packages/twenty-website-new/src/app/halftone/_components/controls/ExportTab.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/halftone/_components/controls/ExportTab.tsx
@@ -1,8 +1,8 @@
'use client';
-import type { ReactExportSettings } from '@/app/halftone/_lib/exporters';
-import { resolveExportArtifactNames } from '@/app/halftone/_lib/exportNames';
-import { formatAnimationName } from '@/app/halftone/_lib/formatters';
+import type { ReactExportSettings } from '@/app/[locale]/halftone/_lib/exporters';
+import { resolveExportArtifactNames } from '@/app/[locale]/halftone/_lib/exportNames';
+import { formatAnimationName } from '@/app/[locale]/halftone/_lib/formatters';
import type {
HalftoneGeometrySpec,
HalftoneStudioSettings,
diff --git a/packages/twenty-website-new/src/app/halftone/_components/controls/controls-ui.tsx b/packages/twenty-website-new/src/app/[locale]/halftone/_components/controls/controls-ui.tsx
similarity index 100%
rename from packages/twenty-website-new/src/app/halftone/_components/controls/controls-ui.tsx
rename to packages/twenty-website-new/src/app/[locale]/halftone/_components/controls/controls-ui.tsx
diff --git a/packages/twenty-website-new/src/app/halftone/_lib/exportNames.ts b/packages/twenty-website-new/src/app/[locale]/halftone/_lib/exportNames.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/halftone/_lib/exportNames.ts
rename to packages/twenty-website-new/src/app/[locale]/halftone/_lib/exportNames.ts
diff --git a/packages/twenty-website-new/src/app/halftone/_lib/exporters.test.ts b/packages/twenty-website-new/src/app/[locale]/halftone/_lib/exporters.test.ts
similarity index 98%
rename from packages/twenty-website-new/src/app/halftone/_lib/exporters.test.ts
rename to packages/twenty-website-new/src/app/[locale]/halftone/_lib/exporters.test.ts
index 32f97f61f1..0003e1e3db 100644
--- a/packages/twenty-website-new/src/app/halftone/_lib/exporters.test.ts
+++ b/packages/twenty-website-new/src/app/[locale]/halftone/_lib/exporters.test.ts
@@ -3,8 +3,8 @@ import {
generateStandaloneHtml,
parseExportedPreset,
type ReactExportSettings,
-} from '@/app/halftone/_lib/exporters';
-import { resolveExportArtifactNames } from '@/app/halftone/_lib/exportNames';
+} from '@/app/[locale]/halftone/_lib/exporters';
+import { resolveExportArtifactNames } from '@/app/[locale]/halftone/_lib/exportNames';
import { REFERENCE_PREVIEW_DISTANCE } from '@/lib/halftone/footprint';
import {
DEFAULT_HALFTONE_SETTINGS,
diff --git a/packages/twenty-website-new/src/app/halftone/_lib/exporters.ts b/packages/twenty-website-new/src/app/[locale]/halftone/_lib/exporters.ts
similarity index 99%
rename from packages/twenty-website-new/src/app/halftone/_lib/exporters.ts
rename to packages/twenty-website-new/src/app/[locale]/halftone/_lib/exporters.ts
index 3419084c55..1dd512a252 100644
--- a/packages/twenty-website-new/src/app/halftone/_lib/exporters.ts
+++ b/packages/twenty-website-new/src/app/[locale]/halftone/_lib/exporters.ts
@@ -1,5 +1,5 @@
-import { normalizeExportComponentName } from '@/app/halftone/_lib/exportNames';
-import { GLASS_ENVIRONMENT_DATA_URL } from '@/app/halftone/_lib/glassEnvironmentData';
+import { normalizeExportComponentName } from '@/app/[locale]/halftone/_lib/exportNames';
+import { GLASS_ENVIRONMENT_DATA_URL } from '@/app/[locale]/halftone/_lib/glassEnvironmentData';
import {
HALFTONE_FOOTPRINT_RUNTIME_SOURCE,
REFERENCE_PREVIEW_DISTANCE,
diff --git a/packages/twenty-website-new/src/app/halftone/_lib/formatters.ts b/packages/twenty-website-new/src/app/[locale]/halftone/_lib/formatters.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/halftone/_lib/formatters.ts
rename to packages/twenty-website-new/src/app/[locale]/halftone/_lib/formatters.ts
diff --git a/packages/twenty-website-new/src/app/halftone/_lib/glassEnvironmentData.ts b/packages/twenty-website-new/src/app/[locale]/halftone/_lib/glassEnvironmentData.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/halftone/_lib/glassEnvironmentData.ts
rename to packages/twenty-website-new/src/app/[locale]/halftone/_lib/glassEnvironmentData.ts
diff --git a/packages/twenty-website-new/src/app/halftone/_lib/imageSvgExport.ts b/packages/twenty-website-new/src/app/[locale]/halftone/_lib/imageSvgExport.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/halftone/_lib/imageSvgExport.ts
rename to packages/twenty-website-new/src/app/[locale]/halftone/_lib/imageSvgExport.ts
diff --git a/packages/twenty-website-new/src/app/halftone/_lib/share.ts b/packages/twenty-website-new/src/app/[locale]/halftone/_lib/share.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/halftone/_lib/share.ts
rename to packages/twenty-website-new/src/app/[locale]/halftone/_lib/share.ts
diff --git a/packages/twenty-website-new/src/app/[locale]/halftone/page.tsx b/packages/twenty-website-new/src/app/[locale]/halftone/page.tsx
new file mode 100644
index 0000000000..efdcf19690
--- /dev/null
+++ b/packages/twenty-website-new/src/app/[locale]/halftone/page.tsx
@@ -0,0 +1,12 @@
+import { HalftoneStudio } from '@/app/[locale]/halftone/_components/HalftoneStudio';
+import { buildLocalizedMetadata } from '@/lib/seo';
+
+export const generateMetadata = buildLocalizedMetadata({
+ path: '/halftone',
+ title: 'Halftone Generator | Twenty',
+ description: 'Interactive halftone generator exported from Twenty.',
+});
+
+export default function HalftonePage() {
+ return ;
+}
diff --git a/packages/twenty-website-new/src/app/layout.tsx b/packages/twenty-website-new/src/app/[locale]/layout.tsx
similarity index 67%
rename from packages/twenty-website-new/src/app/layout.tsx
rename to packages/twenty-website-new/src/app/[locale]/layout.tsx
index 9a6bf9c157..5fddedb575 100644
--- a/packages/twenty-website-new/src/app/layout.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/layout.tsx
@@ -1,17 +1,25 @@
-import { FooterVisibilityGate } from '@/app/_components/FooterVisibilityGate';
-import { ScrollToTopOnRouteChange } from '@/app/_components/ScrollToTopOnRouteChange';
-import { FOOTER_DATA } from '@/sections/Footer/data';
-import { ContactCalModalRoot } from '@/lib/contact-cal';
-import { PartnerApplicationModalRoot } from '@/lib/partner-application';
import { getSiteUrl } from '@/lib/seo';
import { DRACO_DECODER_ORIGIN } from '@/lib/visual-runtime/draco-decoder-path';
-import { Footer } from '@/sections/Footer/components';
import { theme } from '@/theme';
import { cssVariables } from '@/theme/css-variables';
import { css } from '@linaria/core';
import { styled } from '@linaria/react';
import type { Metadata } from 'next';
import { Aleo, Azeret_Mono, Host_Grotesk, VT323 } from 'next/font/google';
+import { type ReactNode } from 'react';
+
+import { FooterVisibilityGate } from '@/app/_components/FooterVisibilityGate';
+import { ScrollToTopOnRouteChange } from '@/app/_components/ScrollToTopOnRouteChange';
+import { ContactCalModalRoot } from '@/lib/contact-cal';
+import {
+ I18nProvider,
+ PUBLIC_APP_LOCALE_LIST,
+ getLocaleMessages,
+ resolveLocaleParam,
+} from '@/lib/i18n';
+import { PartnerApplicationModalRoot } from '@/lib/partner-application';
+import { Footer } from '@/sections/Footer/components';
+import { FOOTER_DATA } from '@/sections/Footer/data';
const hostGrotesk = Host_Grotesk({
subsets: ['latin'],
@@ -59,9 +67,6 @@ const _globalStyles = css`
display: flex;
font-family: ${theme.font.family.sans};
flex-direction: column;
- /* dvh keeps the footer pinned to the visible viewport bottom on mobile
- * Safari (where 100vh = large viewport with chrome hidden, leaving a
- * gap when the URL bar is showing). vh fallback for older browsers. */
min-height: 100vh;
min-height: 100dvh;
-webkit-font-smoothing: antialiased;
@@ -70,6 +75,8 @@ const _globalStyles = css`
`;
const StyledMain = styled.main`
+ display: flex;
+ flex-direction: column;
flex-grow: 1;
`;
@@ -101,17 +108,27 @@ export const metadata: Metadata = {
},
};
-export default function RootLayout({
+type LocaleLayoutParams = { locale: string };
+
+export const dynamicParams = false;
+
+export const generateStaticParams = (): LocaleLayoutParams[] =>
+ PUBLIC_APP_LOCALE_LIST.map((locale) => ({ locale }));
+
+const LocaleLayout = async ({
children,
-}: Readonly<{ children: React.ReactNode }>) {
+ params,
+}: {
+ children: ReactNode;
+ params: Promise;
+}) => {
+ const { locale: rawLocale } = await params;
+ const locale = resolveLocaleParam(rawLocale);
+ const messages = getLocaleMessages(locale);
+
return (
-
+
- {/*
- * Warm up the connection to the DRACO decoder host so the first 3D
- * model on the page does not pay the full TLS handshake cost the
- * moment it starts decoding.
- */}
-
-
-
- {children}
-
-
-
-
-
-
-
-
-
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+
);
-}
+};
+
+export default LocaleLayout;
diff --git a/packages/twenty-website-new/src/app/partners/components/PartnerApplication/BecomePartnerButton.tsx b/packages/twenty-website-new/src/app/[locale]/partners/components/PartnerApplication/BecomePartnerButton.tsx
similarity index 100%
rename from packages/twenty-website-new/src/app/partners/components/PartnerApplication/BecomePartnerButton.tsx
rename to packages/twenty-website-new/src/app/[locale]/partners/components/PartnerApplication/BecomePartnerButton.tsx
diff --git a/packages/twenty-website-new/src/app/partners/components/PartnerApplication/PartnerHeroCtas.tsx b/packages/twenty-website-new/src/app/[locale]/partners/components/PartnerApplication/PartnerHeroCtas.tsx
similarity index 100%
rename from packages/twenty-website-new/src/app/partners/components/PartnerApplication/PartnerHeroCtas.tsx
rename to packages/twenty-website-new/src/app/[locale]/partners/components/PartnerApplication/PartnerHeroCtas.tsx
diff --git a/packages/twenty-website-new/src/app/partners/components/PartnerApplication/PartnerSignoffCtas.tsx b/packages/twenty-website-new/src/app/[locale]/partners/components/PartnerApplication/PartnerSignoffCtas.tsx
similarity index 100%
rename from packages/twenty-website-new/src/app/partners/components/PartnerApplication/PartnerSignoffCtas.tsx
rename to packages/twenty-website-new/src/app/[locale]/partners/components/PartnerApplication/PartnerSignoffCtas.tsx
diff --git a/packages/twenty-website-new/src/app/partners/components/PartnerApplication/index.ts b/packages/twenty-website-new/src/app/[locale]/partners/components/PartnerApplication/index.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/partners/components/PartnerApplication/index.ts
rename to packages/twenty-website-new/src/app/[locale]/partners/components/PartnerApplication/index.ts
diff --git a/packages/twenty-website-new/src/app/partners/engagement-band.data.ts b/packages/twenty-website-new/src/app/[locale]/partners/engagement-band.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/partners/engagement-band.data.ts
rename to packages/twenty-website-new/src/app/[locale]/partners/engagement-band.data.ts
diff --git a/packages/twenty-website-new/src/app/partners/hero.data.ts b/packages/twenty-website-new/src/app/[locale]/partners/hero.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/partners/hero.data.ts
rename to packages/twenty-website-new/src/app/[locale]/partners/hero.data.ts
diff --git a/packages/twenty-website-new/src/app/partners/page.tsx b/packages/twenty-website-new/src/app/[locale]/partners/page.tsx
similarity index 92%
rename from packages/twenty-website-new/src/app/partners/page.tsx
rename to packages/twenty-website-new/src/app/[locale]/partners/page.tsx
index a1e6a484a7..b4dd6590d5 100644
--- a/packages/twenty-website-new/src/app/partners/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/partners/page.tsx
@@ -3,14 +3,14 @@ import { MENU_DATA } from '@/sections/Menu/data';
import { TRUSTED_BY_DATA } from '@/sections/TrustedBy/data';
import { TalkToUsButton } from '@/lib/contact-cal';
import { CASE_STUDY_CATALOG_ENTRIES } from '@/lib/customers';
-import { THREE_CARDS_ILLUSTRATION_DATA } from '@/app/partners/three-cards-illustration.data';
-import { HERO_DATA } from '@/app/partners/hero.data';
-import { SIGNOFF_DATA } from '@/app/partners/signoff.data';
-import { TESTIMONIALS_DATA } from '@/app/partners/testimonials.data';
+import { THREE_CARDS_ILLUSTRATION_DATA } from '@/app/[locale]/partners/three-cards-illustration.data';
+import { HERO_DATA } from '@/app/[locale]/partners/hero.data';
+import { SIGNOFF_DATA } from '@/app/[locale]/partners/signoff.data';
+import { TESTIMONIALS_DATA } from '@/app/[locale]/partners/testimonials.data';
import {
PartnerHeroCtas,
PartnerSignoffCtas,
-} from '@/app/partners/components/PartnerApplication';
+} from '@/app/[locale]/partners/components/PartnerApplication';
import { Body, Eyebrow, Heading, LinkButton } from '@/design-system/components';
import { Pages } from '@/lib/pages';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
@@ -25,9 +25,8 @@ import { ThreeCards } from '@/sections/ThreeCards/components';
import { TrustedBy } from '@/sections/TrustedBy/components';
import type { ThreeCardsScrollLayoutOptions } from '@/sections/ThreeCards/utils/three-cards-scroll-layout';
import { theme } from '@/theme';
-import { buildPageMetadata } from '@/lib/seo';
+import { buildLocalizedMetadata } from '@/lib/seo';
import { styled } from '@linaria/react';
-import type { Metadata } from 'next';
const PARTNER_ILLUSTRATION_CARDS_SCROLL_LAYOUT_OPTIONS: ThreeCardsScrollLayoutOptions =
{
@@ -46,7 +45,7 @@ const PromoSpacing = styled.div`
}
`;
-export const metadata: Metadata = buildPageMetadata({
+export const generateMetadata = buildLocalizedMetadata({
path: '/partners',
title: 'Partners | Twenty',
description:
diff --git a/packages/twenty-website-new/src/app/partners/signoff.data.ts b/packages/twenty-website-new/src/app/[locale]/partners/signoff.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/partners/signoff.data.ts
rename to packages/twenty-website-new/src/app/[locale]/partners/signoff.data.ts
diff --git a/packages/twenty-website-new/src/app/partners/testimonials.data.ts b/packages/twenty-website-new/src/app/[locale]/partners/testimonials.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/partners/testimonials.data.ts
rename to packages/twenty-website-new/src/app/[locale]/partners/testimonials.data.ts
diff --git a/packages/twenty-website-new/src/app/partners/three-cards-illustration.data.ts b/packages/twenty-website-new/src/app/[locale]/partners/three-cards-illustration.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/partners/three-cards-illustration.data.ts
rename to packages/twenty-website-new/src/app/[locale]/partners/three-cards-illustration.data.ts
diff --git a/packages/twenty-website-new/src/app/pricing/engagement-band.data.ts b/packages/twenty-website-new/src/app/[locale]/pricing/engagement-band.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/pricing/engagement-band.data.ts
rename to packages/twenty-website-new/src/app/[locale]/pricing/engagement-band.data.ts
diff --git a/packages/twenty-website-new/src/app/pricing/hero.data.ts b/packages/twenty-website-new/src/app/[locale]/pricing/hero.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/pricing/hero.data.ts
rename to packages/twenty-website-new/src/app/[locale]/pricing/hero.data.ts
diff --git a/packages/twenty-website-new/src/app/pricing/page.tsx b/packages/twenty-website-new/src/app/[locale]/pricing/page.tsx
similarity index 90%
rename from packages/twenty-website-new/src/app/pricing/page.tsx
rename to packages/twenty-website-new/src/app/[locale]/pricing/page.tsx
index 868337e1a0..6b53568fbd 100644
--- a/packages/twenty-website-new/src/app/pricing/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/pricing/page.tsx
@@ -1,11 +1,11 @@
import { FAQ_DATA } from '@/sections/Faq/data';
import { MENU_DATA } from '@/sections/Menu/data';
import { TalkToUsButton } from '@/lib/contact-cal';
-import { BecomePartnerButton } from '@/app/partners/components/PartnerApplication';
-import { ENGAGEMENT_BAND_DATA } from '@/app/pricing/engagement-band.data';
-import { HERO_DATA } from '@/app/pricing/hero.data';
-import { PLAN_TABLE_DATA } from '@/app/pricing/plan-table.data';
-import { SALESFORCE_DATA } from '@/app/pricing/salesforce.data';
+import { BecomePartnerButton } from '@/app/[locale]/partners/components/PartnerApplication';
+import { ENGAGEMENT_BAND_DATA } from '@/app/[locale]/pricing/engagement-band.data';
+import { HERO_DATA } from '@/app/[locale]/pricing/hero.data';
+import { PLAN_TABLE_DATA } from '@/app/[locale]/pricing/plan-table.data';
+import { SALESFORCE_DATA } from '@/app/[locale]/pricing/salesforce.data';
import { Eyebrow, LinkButton } from '@/design-system/components';
import { Pages } from '@/lib/pages';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
@@ -19,9 +19,8 @@ import { PricingStateProvider } from '@/sections/Plans/context/PricingStateConte
import { PlanTable } from '@/sections/PlanTable/components';
import { Salesforce } from '@/sections/Salesforce/components';
import { theme } from '@/theme';
-import { buildPageMetadata } from '@/lib/seo';
+import { buildLocalizedMetadata } from '@/lib/seo';
import { styled } from '@linaria/react';
-import type { Metadata } from 'next';
const PricingPlansContainer = styled.div`
display: grid;
@@ -35,7 +34,7 @@ const PricingBannerContainer = styled.div`
width: 100%;
`;
-export const metadata: Metadata = buildPageMetadata({
+export const generateMetadata = buildLocalizedMetadata({
path: '/pricing',
title: 'Pricing | Twenty',
description:
diff --git a/packages/twenty-website-new/src/app/pricing/plan-table.data.ts b/packages/twenty-website-new/src/app/[locale]/pricing/plan-table.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/pricing/plan-table.data.ts
rename to packages/twenty-website-new/src/app/[locale]/pricing/plan-table.data.ts
diff --git a/packages/twenty-website-new/src/app/pricing/salesforce.data.ts b/packages/twenty-website-new/src/app/[locale]/pricing/salesforce.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/pricing/salesforce.data.ts
rename to packages/twenty-website-new/src/app/[locale]/pricing/salesforce.data.ts
diff --git a/packages/twenty-website-new/src/app/privacy-policy/_components/PrivacyPolicyDocument.tsx b/packages/twenty-website-new/src/app/[locale]/privacy-policy/_components/PrivacyPolicyDocument.tsx
similarity index 99%
rename from packages/twenty-website-new/src/app/privacy-policy/_components/PrivacyPolicyDocument.tsx
rename to packages/twenty-website-new/src/app/[locale]/privacy-policy/_components/PrivacyPolicyDocument.tsx
index 3ee3b042fb..010fb2a4ab 100644
--- a/packages/twenty-website-new/src/app/privacy-policy/_components/PrivacyPolicyDocument.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/privacy-policy/_components/PrivacyPolicyDocument.tsx
@@ -1,4 +1,4 @@
-import Link from 'next/link';
+import { LocalizedLink } from '@/lib/i18n';
export function PrivacyPolicyDocument() {
return (
@@ -56,7 +56,7 @@ export function PrivacyPolicyDocument() {
{
'By using the Service, you agree to the collection and use of information in accordance with this Privacy Policy and our '
}
- {'Terms of Service'}
+ {'Terms of Service'}
{'.'}
diff --git a/packages/twenty-website-new/src/app/privacy-policy/_components/index.ts b/packages/twenty-website-new/src/app/[locale]/privacy-policy/_components/index.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/privacy-policy/_components/index.ts
rename to packages/twenty-website-new/src/app/[locale]/privacy-policy/_components/index.ts
diff --git a/packages/twenty-website-new/src/app/privacy-policy/page.tsx b/packages/twenty-website-new/src/app/[locale]/privacy-policy/page.tsx
similarity index 86%
rename from packages/twenty-website-new/src/app/privacy-policy/page.tsx
rename to packages/twenty-website-new/src/app/[locale]/privacy-policy/page.tsx
index 5d79252f7e..c165a7ae5d 100644
--- a/packages/twenty-website-new/src/app/privacy-policy/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/privacy-policy/page.tsx
@@ -1,14 +1,12 @@
-import type { Metadata } from 'next';
-
import { MENU_DATA } from '@/sections/Menu/data';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { LegalDocument } from '@/sections/LegalDocument/components';
-import { buildPageMetadata } from '@/lib/seo';
+import { buildLocalizedMetadata } from '@/lib/seo';
import { PrivacyPolicyDocument } from './_components';
-export const metadata: Metadata = buildPageMetadata({
+export const generateMetadata = buildLocalizedMetadata({
path: '/privacy-policy',
title: 'Privacy Policy | Twenty',
description:
diff --git a/packages/twenty-website-new/src/app/product/feature.data.ts b/packages/twenty-website-new/src/app/[locale]/product/feature.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/product/feature.data.ts
rename to packages/twenty-website-new/src/app/[locale]/product/feature.data.ts
diff --git a/packages/twenty-website-new/src/app/product/hero.data.ts b/packages/twenty-website-new/src/app/[locale]/product/hero.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/product/hero.data.ts
rename to packages/twenty-website-new/src/app/[locale]/product/hero.data.ts
diff --git a/packages/twenty-website-new/src/app/product/page.tsx b/packages/twenty-website-new/src/app/[locale]/product/page.tsx
similarity index 92%
rename from packages/twenty-website-new/src/app/product/page.tsx
rename to packages/twenty-website-new/src/app/[locale]/product/page.tsx
index 5b1a6f4349..0aada93b71 100644
--- a/packages/twenty-website-new/src/app/product/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/product/page.tsx
@@ -2,11 +2,11 @@ import { FAQ_DATA } from '@/sections/Faq/data';
import { MENU_DATA } from '@/sections/Menu/data';
import { TRUSTED_BY_DATA } from '@/sections/TrustedBy/data';
import { TalkToUsButton } from '@/lib/contact-cal';
-import { FEATURE_DATA } from '@/app/product/feature.data';
-import { HERO_DATA } from '@/app/product/hero.data';
-import { SIGNOFF_DATA } from '@/app/product/signoff.data';
-import { STEPPER_DATA } from '@/app/product/stepper.data';
-import { THREE_CARDS_ILLUSTRATION_DATA } from '@/app/product/three-cards.data';
+import { FEATURE_DATA } from '@/app/[locale]/product/feature.data';
+import { HERO_DATA } from '@/app/[locale]/product/hero.data';
+import { SIGNOFF_DATA } from '@/app/[locale]/product/signoff.data';
+import { STEPPER_DATA } from '@/app/[locale]/product/stepper.data';
+import { THREE_CARDS_ILLUSTRATION_DATA } from '@/app/[locale]/product/three-cards.data';
import { Body, Eyebrow, Heading, LinkButton } from '@/design-system/components';
import { Pages } from '@/lib/pages';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
@@ -20,10 +20,9 @@ import { Signoff } from '@/sections/Signoff/components';
import { ThreeCards } from '@/sections/ThreeCards/components';
import { TrustedBy } from '@/sections/TrustedBy/components';
import { theme } from '@/theme';
-import { buildPageMetadata } from '@/lib/seo';
-import type { Metadata } from 'next';
+import { buildLocalizedMetadata } from '@/lib/seo';
-export const metadata: Metadata = buildPageMetadata({
+export const generateMetadata = buildLocalizedMetadata({
path: '/product',
title: 'Product | Twenty',
description:
diff --git a/packages/twenty-website-new/src/app/product/signoff.data.ts b/packages/twenty-website-new/src/app/[locale]/product/signoff.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/product/signoff.data.ts
rename to packages/twenty-website-new/src/app/[locale]/product/signoff.data.ts
diff --git a/packages/twenty-website-new/src/app/product/stepper.data.ts b/packages/twenty-website-new/src/app/[locale]/product/stepper.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/product/stepper.data.ts
rename to packages/twenty-website-new/src/app/[locale]/product/stepper.data.ts
diff --git a/packages/twenty-website-new/src/app/product/three-cards.data.ts b/packages/twenty-website-new/src/app/[locale]/product/three-cards.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/product/three-cards.data.ts
rename to packages/twenty-website-new/src/app/[locale]/product/three-cards.data.ts
diff --git a/packages/twenty-website-new/src/app/releases/hero.data.ts b/packages/twenty-website-new/src/app/[locale]/releases/hero.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/releases/hero.data.ts
rename to packages/twenty-website-new/src/app/[locale]/releases/hero.data.ts
diff --git a/packages/twenty-website-new/src/app/releases/page.tsx b/packages/twenty-website-new/src/app/[locale]/releases/page.tsx
similarity index 95%
rename from packages/twenty-website-new/src/app/releases/page.tsx
rename to packages/twenty-website-new/src/app/[locale]/releases/page.tsx
index 4030c8d124..59434698a1 100644
--- a/packages/twenty-website-new/src/app/releases/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/releases/page.tsx
@@ -2,7 +2,7 @@ import { MENU_DATA } from '@/sections/Menu/data';
import {
RELEASE_NOTES_HERO_BODY,
RELEASE_NOTES_HERO_HEADING,
-} from '@/app/releases/hero.data';
+} from '@/app/[locale]/releases/hero.data';
import { LinkButton } from '@/design-system/components';
import { Pages } from '@/lib/pages';
import { GitHubIcon } from '@/icons';
@@ -15,11 +15,10 @@ import { Hero } from '@/sections/Hero/components';
import { Menu } from '@/sections/Menu/components';
import { ReleaseNotes } from '@/sections/ReleaseNotes/components';
import { theme } from '@/theme';
-import { buildPageMetadata } from '@/lib/seo';
-import type { Metadata } from 'next';
+import { buildLocalizedMetadata } from '@/lib/seo';
import { Fragment } from 'react';
-export const metadata: Metadata = buildPageMetadata({
+export const generateMetadata = buildLocalizedMetadata({
path: '/releases',
title: 'Releases | Twenty',
description:
diff --git a/packages/twenty-website-new/src/app/terms/_components/TermsDocument.tsx b/packages/twenty-website-new/src/app/[locale]/terms/_components/TermsDocument.tsx
similarity index 99%
rename from packages/twenty-website-new/src/app/terms/_components/TermsDocument.tsx
rename to packages/twenty-website-new/src/app/[locale]/terms/_components/TermsDocument.tsx
index 727a848754..c624de4920 100644
--- a/packages/twenty-website-new/src/app/terms/_components/TermsDocument.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/terms/_components/TermsDocument.tsx
@@ -1,4 +1,4 @@
-import Link from 'next/link';
+import { LocalizedLink } from '@/lib/i18n';
export function TermsDocument() {
return (
@@ -20,7 +20,7 @@ export function TermsDocument() {
Our Privacy Policy also governs your use of our Service and explains how
we collect, safeguard and disclose information that results from your
use of our web pages. Please read it here:{' '}
- Privacy Policy.
+ Privacy Policy.
Your agreement with us includes these Terms and our Privacy Policy
diff --git a/packages/twenty-website-new/src/app/terms/_components/index.ts b/packages/twenty-website-new/src/app/[locale]/terms/_components/index.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/terms/_components/index.ts
rename to packages/twenty-website-new/src/app/[locale]/terms/_components/index.ts
diff --git a/packages/twenty-website-new/src/app/terms/page.tsx b/packages/twenty-website-new/src/app/[locale]/terms/page.tsx
similarity index 86%
rename from packages/twenty-website-new/src/app/terms/page.tsx
rename to packages/twenty-website-new/src/app/[locale]/terms/page.tsx
index 5d8c8c7d61..16c1d024e6 100644
--- a/packages/twenty-website-new/src/app/terms/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/terms/page.tsx
@@ -1,14 +1,12 @@
-import type { Metadata } from 'next';
-
import { MENU_DATA } from '@/sections/Menu/data';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { LegalDocument } from '@/sections/LegalDocument/components';
-import { buildPageMetadata } from '@/lib/seo';
+import { buildLocalizedMetadata } from '@/lib/seo';
import { TermsDocument } from './_components';
-export const metadata: Metadata = buildPageMetadata({
+export const generateMetadata = buildLocalizedMetadata({
path: '/terms',
title: 'Terms of Service | Twenty',
description:
diff --git a/packages/twenty-website-new/src/app/why-twenty/editorial-four.data.ts b/packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-four.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/why-twenty/editorial-four.data.ts
rename to packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-four.data.ts
diff --git a/packages/twenty-website-new/src/app/why-twenty/editorial-one.data.ts b/packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-one.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/why-twenty/editorial-one.data.ts
rename to packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-one.data.ts
diff --git a/packages/twenty-website-new/src/app/why-twenty/editorial-three.data.ts b/packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-three.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/why-twenty/editorial-three.data.ts
rename to packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-three.data.ts
diff --git a/packages/twenty-website-new/src/app/why-twenty/hero.data.ts b/packages/twenty-website-new/src/app/[locale]/why-twenty/hero.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/why-twenty/hero.data.ts
rename to packages/twenty-website-new/src/app/[locale]/why-twenty/hero.data.ts
diff --git a/packages/twenty-website-new/src/app/why-twenty/marquee.data.ts b/packages/twenty-website-new/src/app/[locale]/why-twenty/marquee.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/why-twenty/marquee.data.ts
rename to packages/twenty-website-new/src/app/[locale]/why-twenty/marquee.data.ts
diff --git a/packages/twenty-website-new/src/app/why-twenty/page.tsx b/packages/twenty-website-new/src/app/[locale]/why-twenty/page.tsx
similarity index 92%
rename from packages/twenty-website-new/src/app/why-twenty/page.tsx
rename to packages/twenty-website-new/src/app/[locale]/why-twenty/page.tsx
index 3c3d6703d2..80b82d27e1 100644
--- a/packages/twenty-website-new/src/app/why-twenty/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/why-twenty/page.tsx
@@ -1,10 +1,10 @@
import { MENU_DATA } from '@/sections/Menu/data';
-import { EDITORIAL_FOUR } from '@/app/why-twenty/editorial-four.data';
-import { EDITORIAL_ONE } from '@/app/why-twenty/editorial-one.data';
-import { EDITORIAL_THREE } from '@/app/why-twenty/editorial-three.data';
-import { HERO_DATA } from '@/app/why-twenty/hero.data';
-import { MARQUEE_DATA } from '@/app/why-twenty/marquee.data';
-import { SIGNOFF_DATA } from '@/app/why-twenty/signoff.data';
+import { EDITORIAL_FOUR } from '@/app/[locale]/why-twenty/editorial-four.data';
+import { EDITORIAL_ONE } from '@/app/[locale]/why-twenty/editorial-one.data';
+import { EDITORIAL_THREE } from '@/app/[locale]/why-twenty/editorial-three.data';
+import { HERO_DATA } from '@/app/[locale]/why-twenty/hero.data';
+import { MARQUEE_DATA } from '@/app/[locale]/why-twenty/marquee.data';
+import { SIGNOFF_DATA } from '@/app/[locale]/why-twenty/signoff.data';
import { LinkButton } from '@/design-system/components';
import { Pages } from '@/lib/pages';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
@@ -15,9 +15,8 @@ import { Marquee } from '@/sections/Marquee/components';
import { Menu } from '@/sections/Menu/components';
import { Signoff } from '@/sections/Signoff/components';
import { theme } from '@/theme';
-import { buildPageMetadata } from '@/lib/seo';
+import { buildLocalizedMetadata } from '@/lib/seo';
import { css } from '@linaria/core';
-import type { Metadata } from 'next';
const editorialOneIntroClass = css`
margin-bottom: ${theme.spacing(4)};
@@ -58,7 +57,7 @@ const sectionCrosshairRight = {
lineColor: crosshairLineColor,
};
-export const metadata: Metadata = buildPageMetadata({
+export const generateMetadata = buildLocalizedMetadata({
path: '/why-twenty',
title: 'Why Twenty | Twenty',
description:
diff --git a/packages/twenty-website-new/src/app/why-twenty/signoff.data.ts b/packages/twenty-website-new/src/app/[locale]/why-twenty/signoff.data.ts
similarity index 100%
rename from packages/twenty-website-new/src/app/why-twenty/signoff.data.ts
rename to packages/twenty-website-new/src/app/[locale]/why-twenty/signoff.data.ts
diff --git a/packages/twenty-website-new/src/app/_components/FooterVisibilityGate.tsx b/packages/twenty-website-new/src/app/_components/FooterVisibilityGate.tsx
index fe1f37df53..c53661f48c 100644
--- a/packages/twenty-website-new/src/app/_components/FooterVisibilityGate.tsx
+++ b/packages/twenty-website-new/src/app/_components/FooterVisibilityGate.tsx
@@ -1,16 +1,17 @@
'use client';
-import { usePathname } from 'next/navigation';
import type { ReactNode } from 'react';
+import { useUnlocalizedPathname } from '@/lib/i18n';
+
type FooterVisibilityGateProps = {
children: ReactNode;
};
export function FooterVisibilityGate({ children }: FooterVisibilityGateProps) {
- const pathname = usePathname();
+ const route = useUnlocalizedPathname();
- if (pathname === '/halftone') {
+ if (route === '/halftone') {
return null;
}
diff --git a/packages/twenty-website-new/src/app/halftone/page.tsx b/packages/twenty-website-new/src/app/halftone/page.tsx
deleted file mode 100644
index 19809ce3d1..0000000000
--- a/packages/twenty-website-new/src/app/halftone/page.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import { HalftoneStudio } from '@/app/halftone/_components/HalftoneStudio';
-import { buildPageMetadata } from '@/lib/seo';
-import type { Metadata } from 'next';
-
-export const metadata: Metadata = buildPageMetadata({
- path: '/halftone',
- title: 'Halftone Generator | Twenty',
- description: 'Interactive halftone generator exported from Twenty.',
-});
-
-export default function HalftonePage() {
- return ;
-}
diff --git a/packages/twenty-website-new/src/app/robots.ts b/packages/twenty-website-new/src/app/robots.ts
index 9519a8f8e4..5e33a5ffa0 100644
--- a/packages/twenty-website-new/src/app/robots.ts
+++ b/packages/twenty-website-new/src/app/robots.ts
@@ -10,7 +10,13 @@ export default function robots(): MetadataRoute.Robots {
{
userAgent: '*',
allow: '/',
- disallow: ['/halftone', '/enterprise/activate', '/api/'],
+ disallow: [
+ '/api/',
+ '/halftone',
+ '/*/halftone',
+ '/enterprise/activate',
+ '/*/enterprise/activate',
+ ],
},
],
sitemap: `${SITE_URL}/sitemap.xml`,
diff --git a/packages/twenty-website-new/src/app/sitemap.ts b/packages/twenty-website-new/src/app/sitemap.ts
index 7a05f6d620..0641da580d 100644
--- a/packages/twenty-website-new/src/app/sitemap.ts
+++ b/packages/twenty-website-new/src/app/sitemap.ts
@@ -1,15 +1,19 @@
import type { MetadataRoute } from 'next';
+import { SOURCE_LOCALE, type AppLocale } from 'twenty-shared/translations';
import { CASE_STUDY_CATALOG_ENTRIES } from '@/lib/customers';
+import { PUBLIC_APP_LOCALE_LIST } from '@/lib/i18n';
import { getSiteUrl } from '@/lib/seo';
const SITE_URL = getSiteUrl();
-const STATIC_ROUTES: ReadonlyArray<{
+type StaticRoute = {
path: string;
changeFrequency: MetadataRoute.Sitemap[number]['changeFrequency'];
priority: number;
-}> = [
+};
+
+const STATIC_ROUTES: readonly StaticRoute[] = [
{ path: '/', changeFrequency: 'weekly', priority: 1.0 },
{ path: '/why-twenty', changeFrequency: 'monthly', priority: 0.8 },
{ path: '/product', changeFrequency: 'monthly', priority: 0.8 },
@@ -21,20 +25,42 @@ const STATIC_ROUTES: ReadonlyArray<{
{ path: '/terms', changeFrequency: 'yearly', priority: 0.3 },
];
+const buildLocalizedUrl = (locale: AppLocale, path: string): string => {
+ const prefix = locale === SOURCE_LOCALE ? '' : `/${locale}`;
+ const tail = path === '/' ? '' : path;
+ return `${SITE_URL}${prefix}${tail}`;
+};
+
+const buildLanguageAlternates = (path: string): Record => {
+ const alternates: Record = {};
+ for (const locale of PUBLIC_APP_LOCALE_LIST) {
+ alternates[locale] = buildLocalizedUrl(locale, path);
+ }
+ alternates['x-default'] = buildLocalizedUrl(SOURCE_LOCALE, path);
+ return alternates;
+};
+
+const localize = (
+ path: string,
+ changeFrequency: MetadataRoute.Sitemap[number]['changeFrequency'],
+ priority: number,
+): MetadataRoute.Sitemap =>
+ PUBLIC_APP_LOCALE_LIST.map((locale) => ({
+ url: buildLocalizedUrl(locale, path),
+ changeFrequency,
+ priority,
+ alternates: { languages: buildLanguageAlternates(path) },
+ }));
+
export default function sitemap(): MetadataRoute.Sitemap {
- const staticEntries = STATIC_ROUTES.map(
- ({ path, changeFrequency, priority }) => ({
- url: `${SITE_URL}${path}`,
- changeFrequency,
- priority,
- }),
+ const staticEntries = STATIC_ROUTES.flatMap(
+ ({ path, changeFrequency, priority }) =>
+ localize(path, changeFrequency, priority),
);
- const caseStudyEntries = CASE_STUDY_CATALOG_ENTRIES.map((entry) => ({
- url: `${SITE_URL}${entry.href}`,
- changeFrequency: 'yearly' as const,
- priority: 0.5,
- }));
+ const caseStudyEntries = CASE_STUDY_CATALOG_ENTRIES.flatMap((entry) =>
+ localize(entry.href, 'yearly', 0.5),
+ );
return [...staticEntries, ...caseStudyEntries];
}
diff --git a/packages/twenty-website-new/src/design-system/components/Button/LinkButton.tsx b/packages/twenty-website-new/src/design-system/components/Button/LinkButton.tsx
index c5b6007882..66690cc035 100644
--- a/packages/twenty-website-new/src/design-system/components/Button/LinkButton.tsx
+++ b/packages/twenty-website-new/src/design-system/components/Button/LinkButton.tsx
@@ -1,6 +1,6 @@
import type { LinkButtonType } from '@/design-system/components/Button/types/LinkButtonType';
+import { LocalizedLink } from '@/lib/i18n';
import { styled } from '@linaria/react';
-import Link from 'next/link';
import {
BaseButton,
type BaseButtonProps,
@@ -11,7 +11,7 @@ const StyledButtonAnchor = styled.a`
${buttonBaseStyles}
`;
-const StyledButtonLink = styled(Link)`
+const StyledButtonLink = styled(LocalizedLink)`
${buttonBaseStyles}
`;
diff --git a/packages/twenty-website-new/src/design-system/components/IconButton.tsx b/packages/twenty-website-new/src/design-system/components/IconButton.tsx
index 8d1956ec18..38590db7ab 100644
--- a/packages/twenty-website-new/src/design-system/components/IconButton.tsx
+++ b/packages/twenty-website-new/src/design-system/components/IconButton.tsx
@@ -1,6 +1,6 @@
+import { LocalizedLink } from '@/lib/i18n';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
-import Link from 'next/link';
import type { ComponentType } from 'react';
type IconComponent =
@@ -59,7 +59,7 @@ const StyledButton = styled.button`
width: ${({ $size }) => `${$size}px`};
`;
-const StyledIconLink = styled(Link)`
+const StyledIconLink = styled(LocalizedLink)`
${iconButtonSurfaceStyles}
border: 1px solid ${({ $borderColor }) => $borderColor};
color: inherit;
diff --git a/packages/twenty-website-new/src/lib/i18n/I18nProvider.tsx b/packages/twenty-website-new/src/lib/i18n/I18nProvider.tsx
new file mode 100644
index 0000000000..c36e89754f
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/I18nProvider.tsx
@@ -0,0 +1,33 @@
+'use client';
+
+import { setupI18n, type Messages } from '@lingui/core';
+import { I18nProvider as LinguiI18nProvider } from '@lingui/react';
+import { useMemo, type ReactNode } from 'react';
+import { type AppLocale } from 'twenty-shared/translations';
+
+import { LocaleContext } from './LocaleContext';
+
+type I18nProviderProps = {
+ locale: AppLocale;
+ messages: Messages;
+ children: ReactNode;
+};
+
+export const I18nProvider = ({
+ locale,
+ messages,
+ children,
+}: I18nProviderProps) => {
+ const i18n = useMemo(() => {
+ const instance = setupI18n();
+ instance.load(locale, messages);
+ instance.activate(locale);
+ return instance;
+ }, [locale, messages]);
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/twenty-website-new/src/lib/i18n/LocaleContext.tsx b/packages/twenty-website-new/src/lib/i18n/LocaleContext.tsx
new file mode 100644
index 0000000000..ecb53d808a
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/LocaleContext.tsx
@@ -0,0 +1,6 @@
+'use client';
+
+import { createContext } from 'react';
+import { type AppLocale } from 'twenty-shared/translations';
+
+export const LocaleContext = createContext(null);
diff --git a/packages/twenty-website-new/src/lib/i18n/LocalizedLink.tsx b/packages/twenty-website-new/src/lib/i18n/LocalizedLink.tsx
new file mode 100644
index 0000000000..8d52b46a7e
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/LocalizedLink.tsx
@@ -0,0 +1,17 @@
+'use client';
+
+import Link from 'next/link';
+import { type ComponentProps } from 'react';
+
+import { localizeHref } from './localize-href';
+import { useLocale } from './use-locale';
+
+type LocalizedLinkProps = Omit, 'href'> & {
+ href: string;
+};
+
+export const LocalizedLink = ({ href, ...rest }: LocalizedLinkProps) => {
+ const locale = useLocale();
+ // oxlint-disable-next-line eslint-plugin-react(jsx-props-no-spreading)
+ return ;
+};
diff --git a/packages/twenty-website-new/src/lib/i18n/__tests__/detect-locale.test.ts b/packages/twenty-website-new/src/lib/i18n/__tests__/detect-locale.test.ts
new file mode 100644
index 0000000000..a44b1e9394
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/__tests__/detect-locale.test.ts
@@ -0,0 +1,73 @@
+import { detectLocale } from '../detect-locale';
+
+describe('detectLocale', () => {
+ it('returns the cookie value when it is a supported locale', () => {
+ expect(
+ detectLocale({
+ cookieValue: 'fr-FR',
+ acceptLanguageHeader: 'de-DE,de;q=0.9',
+ }),
+ ).toBe('fr-FR');
+ });
+
+ it('ignores an unsupported cookie value and falls back to Accept-Language', () => {
+ expect(
+ detectLocale({
+ cookieValue: 'xx-YY',
+ acceptLanguageHeader: 'de-DE,de;q=0.9',
+ }),
+ ).toBe('de-DE');
+ });
+
+ it('matches the highest-quality Accept-Language entry first', () => {
+ expect(
+ detectLocale({
+ acceptLanguageHeader: 'en;q=0.5,fr-FR;q=0.9,de;q=0.7',
+ }),
+ ).toBe('fr-FR');
+ });
+
+ it('falls back from a regional tag to the language family', () => {
+ expect(
+ detectLocale({
+ acceptLanguageHeader: 'fr-CA',
+ }),
+ ).toBe('fr-FR');
+ });
+
+ it('falls back from a bare language tag to the first matching regional locale', () => {
+ expect(
+ detectLocale({
+ acceptLanguageHeader: 'pt;q=1.0,en;q=0.5',
+ }),
+ ).toBe('pt-BR');
+ });
+
+ it('returns the source locale when no input is provided', () => {
+ expect(detectLocale({})).toBe('en');
+ });
+
+ it('returns the source locale when nothing in Accept-Language is supported', () => {
+ expect(
+ detectLocale({
+ acceptLanguageHeader: 'xx-YY,zz-ZZ;q=0.5',
+ }),
+ ).toBe('en');
+ });
+
+ it('treats a malformed quality value as zero (deprioritized)', () => {
+ expect(
+ detectLocale({
+ acceptLanguageHeader: 'fr-FR;q=not-a-number,de-DE;q=0.5',
+ }),
+ ).toBe('de-DE');
+ });
+
+ it('parses quality values when parameters carry leading whitespace around the semicolon', () => {
+ expect(
+ detectLocale({
+ acceptLanguageHeader: 'en;q=0.1, fr-FR ; q=0.9, de-DE ; q=0.5',
+ }),
+ ).toBe('fr-FR');
+ });
+});
diff --git a/packages/twenty-website-new/src/lib/i18n/__tests__/localize-href.test.ts b/packages/twenty-website-new/src/lib/i18n/__tests__/localize-href.test.ts
new file mode 100644
index 0000000000..c11453a062
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/__tests__/localize-href.test.ts
@@ -0,0 +1,109 @@
+import { localizeHref, stripLocale } from '../localize-href';
+
+describe('localizeHref', () => {
+ it('prefixes a non-default locale onto an internal absolute path', () => {
+ expect(localizeHref('fr-FR', '/pricing')).toBe('/fr-FR/pricing');
+ });
+
+ it('returns paths unprefixed for the default locale (English at root)', () => {
+ expect(localizeHref('en', '/pricing')).toBe('/pricing');
+ expect(localizeHref('en', '/')).toBe('/');
+ });
+
+ it('prefixes a non-default locale onto the root path', () => {
+ expect(localizeHref('fr-FR', '/')).toBe('/fr-FR/');
+ });
+
+ it('preserves query strings and hash fragments', () => {
+ expect(localizeHref('de-DE', '/customers?ref=hero#top')).toBe(
+ '/de-DE/customers?ref=hero#top',
+ );
+ expect(localizeHref('en', '/customers?ref=hero#top')).toBe(
+ '/customers?ref=hero#top',
+ );
+ });
+
+ it('does not double-prefix paths that already start with a non-default locale', () => {
+ expect(localizeHref('fr-FR', '/de-DE/why-twenty')).toBe(
+ '/de-DE/why-twenty',
+ );
+ expect(localizeHref('fr-FR', '/fr-FR/pricing')).toBe('/fr-FR/pricing');
+ });
+
+ it('strips a redundant /en prefix when targeting the default locale', () => {
+ expect(localizeHref('en', '/en/why-twenty')).toBe('/why-twenty');
+ expect(localizeHref('en', '/en')).toBe('/');
+ });
+
+ it('rewrites an /en-prefixed path onto the active non-default locale', () => {
+ expect(localizeHref('fr-FR', '/en/why-twenty')).toBe('/fr-FR/why-twenty');
+ expect(localizeHref('fr-FR', '/en')).toBe('/fr-FR/');
+ });
+
+ it('passes external https URLs through unchanged', () => {
+ expect(localizeHref('en', 'https://docs.twenty.com')).toBe(
+ 'https://docs.twenty.com',
+ );
+ });
+
+ it('passes protocol-relative URLs through unchanged', () => {
+ expect(localizeHref('en', '//cdn.twenty.com/asset.png')).toBe(
+ '//cdn.twenty.com/asset.png',
+ );
+ });
+
+ it('passes mailto and tel links through unchanged', () => {
+ expect(localizeHref('en', 'mailto:contact@twenty.com')).toBe(
+ 'mailto:contact@twenty.com',
+ );
+ expect(localizeHref('en', 'tel:+1234567890')).toBe('tel:+1234567890');
+ });
+
+ it('passes hash-only and relative hrefs through unchanged', () => {
+ expect(localizeHref('en', '#top')).toBe('#top');
+ expect(localizeHref('en', './sibling')).toBe('./sibling');
+ expect(localizeHref('en', '../parent')).toBe('../parent');
+ });
+
+ it('handles a locale segment immediately followed by a query string', () => {
+ expect(localizeHref('en', '/en?ref=hero')).toBe('/?ref=hero');
+ expect(localizeHref('fr-FR', '/en?ref=hero')).toBe('/fr-FR/?ref=hero');
+ expect(localizeHref('fr-FR', '/de-DE?ref=hero')).toBe('/de-DE?ref=hero');
+ });
+
+ it('handles a locale segment immediately followed by a hash fragment', () => {
+ expect(localizeHref('en', '/en#anchor')).toBe('/#anchor');
+ expect(localizeHref('fr-FR', '/en#anchor')).toBe('/fr-FR/#anchor');
+ expect(localizeHref('fr-FR', '/de-DE#anchor')).toBe('/de-DE#anchor');
+ });
+});
+
+describe('stripLocale', () => {
+ it('removes a known locale prefix from the pathname', () => {
+ expect(stripLocale('/en/why-twenty')).toBe('/why-twenty');
+ expect(stripLocale('/fr-FR/customers/9dots')).toBe('/customers/9dots');
+ });
+
+ it('returns the root path when the pathname is just the locale segment', () => {
+ expect(stripLocale('/en')).toBe('/');
+ expect(stripLocale('/zh-CN')).toBe('/');
+ });
+
+ it('returns the pathname unchanged when no known locale prefix is present', () => {
+ expect(stripLocale('/why-twenty')).toBe('/why-twenty');
+ expect(stripLocale('/')).toBe('/');
+ });
+
+ it('returns the pathname unchanged when the input does not start with a slash', () => {
+ expect(stripLocale('not-a-path')).toBe('not-a-path');
+ expect(stripLocale('')).toBe('');
+ });
+
+ it('preserves query and hash when the locale segment is immediately followed by them', () => {
+ expect(stripLocale('/en?ref=hero')).toBe('/?ref=hero');
+ expect(stripLocale('/fr-FR#anchor')).toBe('/#anchor');
+ expect(stripLocale('/fr-FR/customers?ref=hero#top')).toBe(
+ '/customers?ref=hero#top',
+ );
+ });
+});
diff --git a/packages/twenty-website-new/src/lib/i18n/app-locale-set.ts b/packages/twenty-website-new/src/lib/i18n/app-locale-set.ts
new file mode 100644
index 0000000000..c7c089b589
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/app-locale-set.ts
@@ -0,0 +1,28 @@
+import { APP_LOCALES, type AppLocale } from 'twenty-shared/translations';
+
+const APP_LOCALE_VALUES: readonly AppLocale[] = Object.values(APP_LOCALES);
+
+export const isPublicAppLocale = (locale: AppLocale): boolean =>
+ !locale.startsWith('pseudo-');
+
+export const APP_LOCALE_LIST: readonly AppLocale[] = APP_LOCALE_VALUES;
+
+export const PUBLIC_APP_LOCALE_LIST: readonly AppLocale[] =
+ APP_LOCALE_VALUES.filter(isPublicAppLocale);
+
+export const APP_LOCALE_BY_RAW: ReadonlyMap = new Map(
+ PUBLIC_APP_LOCALE_LIST.map((locale) => [locale, locale]),
+);
+
+export const APP_LOCALE_BY_LANGUAGE: ReadonlyMap = (() => {
+ const byLanguage = new Map();
+ for (const locale of PUBLIC_APP_LOCALE_LIST) {
+ const [languageSubtag] = locale.split('-');
+ if (languageSubtag === undefined) continue;
+ const key = languageSubtag.toLowerCase();
+ if (!byLanguage.has(key)) {
+ byLanguage.set(key, locale);
+ }
+ }
+ return byLanguage;
+})();
diff --git a/packages/twenty-website-new/src/lib/i18n/create-i18n-instance.ts b/packages/twenty-website-new/src/lib/i18n/create-i18n-instance.ts
new file mode 100644
index 0000000000..a47f937c1a
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/create-i18n-instance.ts
@@ -0,0 +1,5 @@
+import { createI18nInstanceFactory } from 'twenty-shared/i18n';
+
+import { MESSAGES_BY_LOCALE } from './messages-by-locale';
+
+export const createI18nInstance = createI18nInstanceFactory(MESSAGES_BY_LOCALE);
diff --git a/packages/twenty-website-new/src/lib/i18n/detect-locale.ts b/packages/twenty-website-new/src/lib/i18n/detect-locale.ts
new file mode 100644
index 0000000000..df20fb1322
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/detect-locale.ts
@@ -0,0 +1,58 @@
+import { SOURCE_LOCALE, type AppLocale } from 'twenty-shared/translations';
+
+import { APP_LOCALE_BY_LANGUAGE, APP_LOCALE_BY_RAW } from './app-locale-set';
+
+export const LOCALE_COOKIE_NAME = 'NEXT_LOCALE';
+
+type AcceptLanguageEntry = { tag: string; quality: number };
+
+const parseAcceptLanguage = (header: string): AcceptLanguageEntry[] =>
+ header
+ .split(',')
+ .flatMap((part): AcceptLanguageEntry[] => {
+ const [rawTag, ...params] = part.trim().split(';');
+ if (rawTag === undefined || rawTag.length === 0) return [];
+ const qParam = params.find((p) => p.trim().startsWith('q='))?.trim();
+ const parsedQuality =
+ qParam !== undefined ? Number.parseFloat(qParam.slice(2)) : 1;
+ return [
+ {
+ tag: rawTag.trim(),
+ quality: Number.isFinite(parsedQuality) ? parsedQuality : 0,
+ },
+ ];
+ })
+ .sort((a, b) => b.quality - a.quality);
+
+const matchTag = (tag: string): AppLocale | undefined => {
+ const exact = APP_LOCALE_BY_RAW.get(tag);
+ if (exact !== undefined) return exact;
+
+ const [languageSubtag] = tag.split('-');
+ if (languageSubtag === undefined) return undefined;
+ return APP_LOCALE_BY_LANGUAGE.get(languageSubtag.toLowerCase());
+};
+
+type DetectLocaleInput = {
+ cookieValue?: string;
+ acceptLanguageHeader?: string;
+};
+
+export const detectLocale = ({
+ cookieValue,
+ acceptLanguageHeader,
+}: DetectLocaleInput): AppLocale => {
+ if (cookieValue !== undefined) {
+ const fromCookie = APP_LOCALE_BY_RAW.get(cookieValue);
+ if (fromCookie !== undefined) return fromCookie;
+ }
+
+ if (acceptLanguageHeader !== undefined && acceptLanguageHeader.length > 0) {
+ for (const { tag } of parseAcceptLanguage(acceptLanguageHeader)) {
+ const match = matchTag(tag);
+ if (match !== undefined) return match;
+ }
+ }
+
+ return SOURCE_LOCALE;
+};
diff --git a/packages/twenty-website-new/src/lib/i18n/index.ts b/packages/twenty-website-new/src/lib/i18n/index.ts
new file mode 100644
index 0000000000..7bd9dbf54a
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/index.ts
@@ -0,0 +1,17 @@
+export {
+ APP_LOCALE_BY_LANGUAGE,
+ APP_LOCALE_BY_RAW,
+ APP_LOCALE_LIST,
+ PUBLIC_APP_LOCALE_LIST,
+ isPublicAppLocale,
+} from './app-locale-set';
+export { createI18nInstance } from './create-i18n-instance';
+export { detectLocale, LOCALE_COOKIE_NAME } from './detect-locale';
+export { I18nProvider } from './I18nProvider';
+export { LocaleContext } from './LocaleContext';
+export { LocalizedLink } from './LocalizedLink';
+export { localizeHref, stripLocale } from './localize-href';
+export { getLocaleMessages } from './messages-by-locale';
+export { resolveLocaleParam } from './resolve-locale-param';
+export { useLocale } from './use-locale';
+export { useUnlocalizedPathname } from './use-unlocalized-pathname';
diff --git a/packages/twenty-website-new/src/lib/i18n/localize-href.ts b/packages/twenty-website-new/src/lib/i18n/localize-href.ts
new file mode 100644
index 0000000000..6a0d999143
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/localize-href.ts
@@ -0,0 +1,47 @@
+import { SOURCE_LOCALE, type AppLocale } from 'twenty-shared/translations';
+
+import { APP_LOCALE_BY_RAW } from './app-locale-set';
+
+const findFirstSegmentEnd = (path: string): number => {
+ for (let i = 1; i < path.length; i += 1) {
+ const ch = path[i];
+ if (ch === '/' || ch === '?' || ch === '#') return i;
+ }
+ return path.length;
+};
+
+const buildTailFromSegmentEnd = (path: string, segmentEnd: number): string => {
+ const tail = path.slice(segmentEnd);
+ if (tail.length === 0) return '/';
+ if (tail.startsWith('?') || tail.startsWith('#')) return `/${tail}`;
+ return tail;
+};
+
+export const localizeHref = (locale: AppLocale, href: string): string => {
+ if (!href.startsWith('/') || href.startsWith('//')) return href;
+
+ const segmentEnd = findFirstSegmentEnd(href);
+ const firstSegment = href.slice(1, segmentEnd);
+ const existingLocale = APP_LOCALE_BY_RAW.get(firstSegment);
+
+ if (existingLocale !== undefined && existingLocale !== SOURCE_LOCALE) {
+ return href;
+ }
+
+ const unprefixed =
+ existingLocale === SOURCE_LOCALE
+ ? buildTailFromSegmentEnd(href, segmentEnd)
+ : href;
+
+ return locale === SOURCE_LOCALE ? unprefixed : `/${locale}${unprefixed}`;
+};
+
+export const stripLocale = (pathname: string): string => {
+ if (!pathname.startsWith('/')) return pathname;
+
+ const segmentEnd = findFirstSegmentEnd(pathname);
+ const firstSegment = pathname.slice(1, segmentEnd);
+ if (!APP_LOCALE_BY_RAW.has(firstSegment)) return pathname;
+
+ return buildTailFromSegmentEnd(pathname, segmentEnd);
+};
diff --git a/packages/twenty-website-new/src/lib/i18n/messages-by-locale.ts b/packages/twenty-website-new/src/lib/i18n/messages-by-locale.ts
new file mode 100644
index 0000000000..fcd445babe
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/messages-by-locale.ts
@@ -0,0 +1,13 @@
+import { type Messages } from '@lingui/core';
+import { SOURCE_LOCALE, type AppLocale } from 'twenty-shared/translations';
+
+import { messages as enMessages } from '@/locales/generated/en';
+
+const MESSAGES_BY_LOCALE: Partial> = {
+ en: enMessages,
+};
+
+export const getLocaleMessages = (locale: AppLocale): Messages =>
+ MESSAGES_BY_LOCALE[locale] ?? MESSAGES_BY_LOCALE[SOURCE_LOCALE] ?? {};
+
+export { MESSAGES_BY_LOCALE };
diff --git a/packages/twenty-website-new/src/lib/i18n/resolve-locale-param.ts b/packages/twenty-website-new/src/lib/i18n/resolve-locale-param.ts
new file mode 100644
index 0000000000..b348fa4f04
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/resolve-locale-param.ts
@@ -0,0 +1,10 @@
+import { notFound } from 'next/navigation';
+import { type AppLocale } from 'twenty-shared/translations';
+
+import { APP_LOCALE_BY_RAW } from './app-locale-set';
+
+export const resolveLocaleParam = (raw: string): AppLocale => {
+ const locale = APP_LOCALE_BY_RAW.get(raw);
+ if (locale === undefined) notFound();
+ return locale;
+};
diff --git a/packages/twenty-website-new/src/lib/i18n/use-locale.ts b/packages/twenty-website-new/src/lib/i18n/use-locale.ts
new file mode 100644
index 0000000000..81d16034a7
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/use-locale.ts
@@ -0,0 +1,16 @@
+'use client';
+
+import { useContext } from 'react';
+import { type AppLocale } from 'twenty-shared/translations';
+
+import { LocaleContext } from './LocaleContext';
+
+export const useLocale = (): AppLocale => {
+ const locale = useContext(LocaleContext);
+ if (locale === null) {
+ throw new Error(
+ 'useLocale must be used inside a LocaleContext.Provider (mounted by [locale]/layout.tsx)',
+ );
+ }
+ return locale;
+};
diff --git a/packages/twenty-website-new/src/lib/i18n/use-unlocalized-pathname.ts b/packages/twenty-website-new/src/lib/i18n/use-unlocalized-pathname.ts
new file mode 100644
index 0000000000..a1a27d3afb
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/use-unlocalized-pathname.ts
@@ -0,0 +1,10 @@
+'use client';
+
+import { usePathname } from 'next/navigation';
+
+import { stripLocale } from './localize-href';
+
+export const useUnlocalizedPathname = (): string => {
+ const pathname = usePathname();
+ return stripLocale(pathname ?? '');
+};
diff --git a/packages/twenty-website-new/src/lib/seo/__tests__/build-page-metadata.test.ts b/packages/twenty-website-new/src/lib/seo/__tests__/build-page-metadata.test.ts
index cc4ee01b9c..704a133e5b 100644
--- a/packages/twenty-website-new/src/lib/seo/__tests__/build-page-metadata.test.ts
+++ b/packages/twenty-website-new/src/lib/seo/__tests__/build-page-metadata.test.ts
@@ -11,19 +11,21 @@ afterAll(() => {
});
describe('buildPageMetadata', () => {
- it('produces a baseline with canonical, openGraph, and twitter populated', () => {
+ it('emits an unprefixed canonical for the default locale (English at root)', () => {
const metadata = buildPageMetadata({
+ locale: 'en',
path: '/product',
title: 'Product | Twenty',
description: 'Product page description.',
});
- expect(metadata.alternates).toEqual({ canonical: '/product' });
+ expect(metadata.alternates).toMatchObject({ canonical: '/product' });
expect(metadata.openGraph).toMatchObject({
title: 'Product | Twenty',
description: 'Product page description.',
url: '/product',
siteName: 'Twenty',
+ locale: 'en',
type: 'website',
});
expect(metadata.twitter).toMatchObject({
@@ -33,8 +35,68 @@ describe('buildPageMetadata', () => {
});
});
+ it('emits a prefixed canonical for non-default locales', () => {
+ const metadata = buildPageMetadata({
+ locale: 'fr-FR',
+ path: '/product',
+ title: 't',
+ description: 'd',
+ });
+
+ expect(metadata.alternates).toMatchObject({ canonical: '/fr-FR/product' });
+ });
+
+ it('emits hreflang language alternates: English unprefixed, others prefixed, x-default unprefixed', () => {
+ const metadata = buildPageMetadata({
+ locale: 'fr-FR',
+ path: '/pricing',
+ title: 't',
+ description: 'd',
+ });
+
+ const languages = metadata.alternates?.languages as
+ | Record
+ | undefined;
+ expect(languages?.en).toBe('/pricing');
+ expect(languages?.['fr-FR']).toBe('/fr-FR/pricing');
+ expect(languages?.['zh-CN']).toBe('/zh-CN/pricing');
+ expect(languages?.['x-default']).toBe('/pricing');
+ });
+
+ it('omits pseudo locales from hreflang language alternates', () => {
+ const metadata = buildPageMetadata({
+ locale: 'en',
+ path: '/pricing',
+ title: 't',
+ description: 'd',
+ });
+
+ const languages = metadata.alternates?.languages as
+ | Record
+ | undefined;
+ expect(languages?.['pseudo-en']).toBeUndefined();
+ });
+
+ it('localizes the root path correctly (no trailing slash duplication)', () => {
+ const metadata = buildPageMetadata({
+ locale: 'de-DE',
+ path: '/',
+ title: 't',
+ description: 'd',
+ });
+
+ expect(metadata.alternates).toMatchObject({ canonical: '/de-DE' });
+ const languages = metadata.alternates?.languages as
+ | Record
+ | undefined;
+ expect(languages?.['de-DE']).toBe('/de-DE');
+ expect(languages?.en).toBe('/');
+ expect(languages?.['x-default']).toBe('/');
+ });
+
it('absolutises a root-relative ogImage against the configured site URL', () => {
const metadata = buildPageMetadata({
+ locale: 'en',
path: '/x',
title: 't',
description: 'd',
@@ -49,6 +111,7 @@ describe('buildPageMetadata', () => {
it('passes through an absolute ogImage unchanged', () => {
const metadata = buildPageMetadata({
+ locale: 'en',
path: '/x',
title: 't',
description: 'd',
@@ -62,6 +125,7 @@ describe('buildPageMetadata', () => {
it('deep-merges openGraph: extending title preserves description, url, and images', () => {
const metadata = buildPageMetadata({
+ locale: 'en',
path: '/x',
title: 'Default title',
description: 'Default description',
@@ -82,6 +146,7 @@ describe('buildPageMetadata', () => {
it('deep-merges twitter: extending card preserves handle, title, images', () => {
const metadata = buildPageMetadata({
+ locale: 'en',
path: '/x',
title: 't',
description: 'd',
@@ -100,24 +165,29 @@ describe('buildPageMetadata', () => {
});
});
- it('deep-merges alternates so canonical survives a languages override', () => {
+ it('deep-merges alternates so canonical and languages survive an override of one field', () => {
const metadata = buildPageMetadata({
+ locale: 'en',
path: '/x',
title: 't',
description: 'd',
extend: {
- alternates: { languages: { fr: '/fr/x' } },
+ alternates: { canonical: '/custom-canonical' },
},
});
- expect(metadata.alternates).toEqual({
- canonical: '/x',
- languages: { fr: '/fr/x' },
+ expect(metadata.alternates).toMatchObject({
+ canonical: '/custom-canonical',
});
+ const languages = metadata.alternates?.languages as
+ | Record
+ | undefined;
+ expect(languages?.en).toBe('/x');
});
it('shallow-merges unrelated top-level keys (e.g. robots)', () => {
const metadata = buildPageMetadata({
+ locale: 'en',
path: '/x',
title: 't',
description: 'd',
@@ -127,6 +197,6 @@ describe('buildPageMetadata', () => {
});
expect(metadata.robots).toEqual({ index: false, follow: false });
- expect(metadata.alternates).toEqual({ canonical: '/x' });
+ expect(metadata.alternates).toMatchObject({ canonical: '/x' });
});
});
diff --git a/packages/twenty-website-new/src/lib/seo/build-localized-metadata.ts b/packages/twenty-website-new/src/lib/seo/build-localized-metadata.ts
new file mode 100644
index 0000000000..2753ec437d
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/seo/build-localized-metadata.ts
@@ -0,0 +1,20 @@
+import type { Metadata } from 'next';
+
+import { resolveLocaleParam } from '@/lib/i18n/resolve-locale-param';
+
+import {
+ buildPageMetadata,
+ type BuildPageMetadataInput,
+} from './build-page-metadata';
+
+type LocalizedMetadataArgs = {
+ params: Promise<{ locale: string }>;
+};
+
+export const buildLocalizedMetadata =
+ (input: Omit) =>
+ async ({ params }: LocalizedMetadataArgs): Promise => {
+ const { locale: rawLocale } = await params;
+ const locale = resolveLocaleParam(rawLocale);
+ return buildPageMetadata({ ...input, locale });
+ };
diff --git a/packages/twenty-website-new/src/lib/seo/build-page-metadata.ts b/packages/twenty-website-new/src/lib/seo/build-page-metadata.ts
index a63d519501..10180e4e39 100644
--- a/packages/twenty-website-new/src/lib/seo/build-page-metadata.ts
+++ b/packages/twenty-website-new/src/lib/seo/build-page-metadata.ts
@@ -1,4 +1,7 @@
import type { Metadata } from 'next';
+import { SOURCE_LOCALE, type AppLocale } from 'twenty-shared/translations';
+
+import { PUBLIC_APP_LOCALE_LIST } from '@/lib/i18n/app-locale-set';
import { getSiteUrl } from './site-url';
@@ -6,6 +9,7 @@ const SITE_NAME = 'Twenty';
const TWITTER_HANDLE = '@twentycrm';
export type BuildPageMetadataInput = {
+ locale: AppLocale;
path: string;
title: string;
description: string;
@@ -14,7 +18,29 @@ export type BuildPageMetadataInput = {
extend?: Metadata;
};
+const normalizePath = (path: string): string =>
+ path.startsWith('/') ? path : `/${path}`;
+
+const localizePath = (locale: AppLocale, normalizedPath: string): string => {
+ if (locale === SOURCE_LOCALE) {
+ return normalizedPath;
+ }
+ return normalizedPath === '/' ? `/${locale}` : `/${locale}${normalizedPath}`;
+};
+
+const buildLanguageAlternates = (
+ normalizedPath: string,
+): Record => {
+ const languages: Record = {};
+ for (const locale of PUBLIC_APP_LOCALE_LIST) {
+ languages[locale] = localizePath(locale, normalizedPath);
+ }
+ languages['x-default'] = localizePath(SOURCE_LOCALE, normalizedPath);
+ return languages;
+};
+
export function buildPageMetadata({
+ locale,
path,
title,
description,
@@ -23,7 +49,8 @@ export function buildPageMetadata({
extend,
}: BuildPageMetadataInput): Metadata {
const siteUrl = getSiteUrl();
- const canonical = path.startsWith('/') ? path : `/${path}`;
+ const normalizedPath = normalizePath(path);
+ const canonical = localizePath(locale, normalizedPath);
const ogImages =
ogImage === undefined
@@ -39,12 +66,16 @@ export function buildPageMetadata({
const baseMetadata: Metadata = {
title,
description,
- alternates: { canonical },
+ alternates: {
+ canonical,
+ languages: buildLanguageAlternates(normalizedPath),
+ },
openGraph: {
title,
description,
url: canonical,
siteName: SITE_NAME,
+ locale,
type,
...(ogImages && { images: ogImages }),
},
diff --git a/packages/twenty-website-new/src/lib/seo/index.ts b/packages/twenty-website-new/src/lib/seo/index.ts
index 2391cc6ba3..067bf5b00e 100644
--- a/packages/twenty-website-new/src/lib/seo/index.ts
+++ b/packages/twenty-website-new/src/lib/seo/index.ts
@@ -1,5 +1,6 @@
-export { getSiteUrl, getAbsoluteUrl } from './site-url';
+export { buildLocalizedMetadata } from './build-localized-metadata';
export {
buildPageMetadata,
type BuildPageMetadataInput,
} from './build-page-metadata';
+export { getAbsoluteUrl, getSiteUrl } from './site-url';
diff --git a/packages/twenty-website-new/src/locales/af-ZA.po b/packages/twenty-website-new/src/locales/af-ZA.po
new file mode 100644
index 0000000000..f0d391a4b0
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/af-ZA.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: af-ZA\n"
diff --git a/packages/twenty-website-new/src/locales/ar-SA.po b/packages/twenty-website-new/src/locales/ar-SA.po
new file mode 100644
index 0000000000..5cbdb8535e
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/ar-SA.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: ar-SA\n"
diff --git a/packages/twenty-website-new/src/locales/ca-ES.po b/packages/twenty-website-new/src/locales/ca-ES.po
new file mode 100644
index 0000000000..ba48ff2edf
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/ca-ES.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: ca-ES\n"
diff --git a/packages/twenty-website-new/src/locales/cs-CZ.po b/packages/twenty-website-new/src/locales/cs-CZ.po
new file mode 100644
index 0000000000..847e070a9e
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/cs-CZ.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: cs-CZ\n"
diff --git a/packages/twenty-website-new/src/locales/da-DK.po b/packages/twenty-website-new/src/locales/da-DK.po
new file mode 100644
index 0000000000..d24527f334
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/da-DK.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: da-DK\n"
diff --git a/packages/twenty-website-new/src/locales/de-DE.po b/packages/twenty-website-new/src/locales/de-DE.po
new file mode 100644
index 0000000000..35b8ac1c78
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/de-DE.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: de-DE\n"
diff --git a/packages/twenty-website-new/src/locales/el-GR.po b/packages/twenty-website-new/src/locales/el-GR.po
new file mode 100644
index 0000000000..0f3ed50c7d
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/el-GR.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: el-GR\n"
diff --git a/packages/twenty-website-new/src/locales/en.po b/packages/twenty-website-new/src/locales/en.po
new file mode 100644
index 0000000000..b876953b4d
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/en.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: en\n"
diff --git a/packages/twenty-website-new/src/locales/es-ES.po b/packages/twenty-website-new/src/locales/es-ES.po
new file mode 100644
index 0000000000..4a202ea6eb
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/es-ES.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: es-ES\n"
diff --git a/packages/twenty-website-new/src/locales/fi-FI.po b/packages/twenty-website-new/src/locales/fi-FI.po
new file mode 100644
index 0000000000..a64e5dafc9
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/fi-FI.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: fi-FI\n"
diff --git a/packages/twenty-website-new/src/locales/fr-FR.po b/packages/twenty-website-new/src/locales/fr-FR.po
new file mode 100644
index 0000000000..05d9faaabf
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/fr-FR.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: fr-FR\n"
diff --git a/packages/twenty-website-new/src/locales/generated/af-ZA.ts b/packages/twenty-website-new/src/locales/generated/af-ZA.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/af-ZA.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/ar-SA.ts b/packages/twenty-website-new/src/locales/generated/ar-SA.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/ar-SA.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/ca-ES.ts b/packages/twenty-website-new/src/locales/generated/ca-ES.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/ca-ES.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/cs-CZ.ts b/packages/twenty-website-new/src/locales/generated/cs-CZ.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/cs-CZ.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/da-DK.ts b/packages/twenty-website-new/src/locales/generated/da-DK.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/da-DK.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/de-DE.ts b/packages/twenty-website-new/src/locales/generated/de-DE.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/de-DE.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/el-GR.ts b/packages/twenty-website-new/src/locales/generated/el-GR.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/el-GR.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/en.ts b/packages/twenty-website-new/src/locales/generated/en.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/en.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/es-ES.ts b/packages/twenty-website-new/src/locales/generated/es-ES.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/es-ES.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/fi-FI.ts b/packages/twenty-website-new/src/locales/generated/fi-FI.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/fi-FI.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/fr-FR.ts b/packages/twenty-website-new/src/locales/generated/fr-FR.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/fr-FR.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/he-IL.ts b/packages/twenty-website-new/src/locales/generated/he-IL.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/he-IL.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/hu-HU.ts b/packages/twenty-website-new/src/locales/generated/hu-HU.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/hu-HU.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/it-IT.ts b/packages/twenty-website-new/src/locales/generated/it-IT.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/it-IT.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/ja-JP.ts b/packages/twenty-website-new/src/locales/generated/ja-JP.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/ja-JP.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/ko-KR.ts b/packages/twenty-website-new/src/locales/generated/ko-KR.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/ko-KR.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/nl-NL.ts b/packages/twenty-website-new/src/locales/generated/nl-NL.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/nl-NL.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/no-NO.ts b/packages/twenty-website-new/src/locales/generated/no-NO.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/no-NO.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/pl-PL.ts b/packages/twenty-website-new/src/locales/generated/pl-PL.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/pl-PL.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/pseudo-en.ts b/packages/twenty-website-new/src/locales/generated/pseudo-en.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/pseudo-en.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/pt-BR.ts b/packages/twenty-website-new/src/locales/generated/pt-BR.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/pt-BR.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/pt-PT.ts b/packages/twenty-website-new/src/locales/generated/pt-PT.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/pt-PT.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/ro-RO.ts b/packages/twenty-website-new/src/locales/generated/ro-RO.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/ro-RO.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/ru-RU.ts b/packages/twenty-website-new/src/locales/generated/ru-RU.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/ru-RU.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/sr-Cyrl.ts b/packages/twenty-website-new/src/locales/generated/sr-Cyrl.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/sr-Cyrl.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/sv-SE.ts b/packages/twenty-website-new/src/locales/generated/sv-SE.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/sv-SE.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/tr-TR.ts b/packages/twenty-website-new/src/locales/generated/tr-TR.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/tr-TR.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/uk-UA.ts b/packages/twenty-website-new/src/locales/generated/uk-UA.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/uk-UA.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/vi-VN.ts b/packages/twenty-website-new/src/locales/generated/vi-VN.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/vi-VN.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/zh-CN.ts b/packages/twenty-website-new/src/locales/generated/zh-CN.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/zh-CN.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/generated/zh-TW.ts b/packages/twenty-website-new/src/locales/generated/zh-TW.ts
new file mode 100644
index 0000000000..76934b72de
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/generated/zh-TW.ts
@@ -0,0 +1 @@
+/*eslint-disable*/import type{Messages}from"@lingui/core";export const messages=JSON.parse("{}")as Messages;
\ No newline at end of file
diff --git a/packages/twenty-website-new/src/locales/he-IL.po b/packages/twenty-website-new/src/locales/he-IL.po
new file mode 100644
index 0000000000..1ded1f937c
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/he-IL.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: he-IL\n"
diff --git a/packages/twenty-website-new/src/locales/hu-HU.po b/packages/twenty-website-new/src/locales/hu-HU.po
new file mode 100644
index 0000000000..8145050817
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/hu-HU.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: hu-HU\n"
diff --git a/packages/twenty-website-new/src/locales/it-IT.po b/packages/twenty-website-new/src/locales/it-IT.po
new file mode 100644
index 0000000000..5f702a185c
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/it-IT.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: it-IT\n"
diff --git a/packages/twenty-website-new/src/locales/ja-JP.po b/packages/twenty-website-new/src/locales/ja-JP.po
new file mode 100644
index 0000000000..53d95d4a81
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/ja-JP.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: ja-JP\n"
diff --git a/packages/twenty-website-new/src/locales/ko-KR.po b/packages/twenty-website-new/src/locales/ko-KR.po
new file mode 100644
index 0000000000..35658bf40b
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/ko-KR.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: ko-KR\n"
diff --git a/packages/twenty-website-new/src/locales/nl-NL.po b/packages/twenty-website-new/src/locales/nl-NL.po
new file mode 100644
index 0000000000..6c805962df
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/nl-NL.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: nl-NL\n"
diff --git a/packages/twenty-website-new/src/locales/no-NO.po b/packages/twenty-website-new/src/locales/no-NO.po
new file mode 100644
index 0000000000..b64c6d43e4
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/no-NO.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: no-NO\n"
diff --git a/packages/twenty-website-new/src/locales/pl-PL.po b/packages/twenty-website-new/src/locales/pl-PL.po
new file mode 100644
index 0000000000..2ff9b760b4
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/pl-PL.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: pl-PL\n"
diff --git a/packages/twenty-website-new/src/locales/pseudo-en.po b/packages/twenty-website-new/src/locales/pseudo-en.po
new file mode 100644
index 0000000000..4a88161ab7
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/pseudo-en.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: pseudo-en\n"
diff --git a/packages/twenty-website-new/src/locales/pt-BR.po b/packages/twenty-website-new/src/locales/pt-BR.po
new file mode 100644
index 0000000000..32bfcc0d8f
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/pt-BR.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: pt-BR\n"
diff --git a/packages/twenty-website-new/src/locales/pt-PT.po b/packages/twenty-website-new/src/locales/pt-PT.po
new file mode 100644
index 0000000000..f450ab91e2
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/pt-PT.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: pt-PT\n"
diff --git a/packages/twenty-website-new/src/locales/ro-RO.po b/packages/twenty-website-new/src/locales/ro-RO.po
new file mode 100644
index 0000000000..f611ae1e1e
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/ro-RO.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: ro-RO\n"
diff --git a/packages/twenty-website-new/src/locales/ru-RU.po b/packages/twenty-website-new/src/locales/ru-RU.po
new file mode 100644
index 0000000000..0925e2a7ed
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/ru-RU.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: ru-RU\n"
diff --git a/packages/twenty-website-new/src/locales/sr-Cyrl.po b/packages/twenty-website-new/src/locales/sr-Cyrl.po
new file mode 100644
index 0000000000..b8407e9b7a
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/sr-Cyrl.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: sr-Cyrl\n"
diff --git a/packages/twenty-website-new/src/locales/sv-SE.po b/packages/twenty-website-new/src/locales/sv-SE.po
new file mode 100644
index 0000000000..e39a66153f
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/sv-SE.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: sv-SE\n"
diff --git a/packages/twenty-website-new/src/locales/tr-TR.po b/packages/twenty-website-new/src/locales/tr-TR.po
new file mode 100644
index 0000000000..9f3ebf6d70
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/tr-TR.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: tr-TR\n"
diff --git a/packages/twenty-website-new/src/locales/uk-UA.po b/packages/twenty-website-new/src/locales/uk-UA.po
new file mode 100644
index 0000000000..9b24caca66
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/uk-UA.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: uk-UA\n"
diff --git a/packages/twenty-website-new/src/locales/vi-VN.po b/packages/twenty-website-new/src/locales/vi-VN.po
new file mode 100644
index 0000000000..21cc3a8d2e
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/vi-VN.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: vi-VN\n"
diff --git a/packages/twenty-website-new/src/locales/zh-CN.po b/packages/twenty-website-new/src/locales/zh-CN.po
new file mode 100644
index 0000000000..66bcb5b439
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/zh-CN.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: zh-CN\n"
diff --git a/packages/twenty-website-new/src/locales/zh-TW.po b/packages/twenty-website-new/src/locales/zh-TW.po
new file mode 100644
index 0000000000..265077a521
--- /dev/null
+++ b/packages/twenty-website-new/src/locales/zh-TW.po
@@ -0,0 +1,8 @@
+msgid ""
+msgstr ""
+"POT-Creation-Date: 2026-04-27 14:31+0500\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: @lingui/cli\n"
+"Language: zh-TW\n"
diff --git a/packages/twenty-website-new/src/middleware.ts b/packages/twenty-website-new/src/middleware.ts
new file mode 100644
index 0000000000..ea2c56e4c3
--- /dev/null
+++ b/packages/twenty-website-new/src/middleware.ts
@@ -0,0 +1,71 @@
+import { NextResponse, type NextRequest } from 'next/server';
+import { SOURCE_LOCALE, type AppLocale } from 'twenty-shared/translations';
+
+import { APP_LOCALE_BY_RAW } from '@/lib/i18n/app-locale-set';
+import { LOCALE_COOKIE_NAME, detectLocale } from '@/lib/i18n/detect-locale';
+
+const LOCALE_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
+
+const setLocaleCookie = (response: NextResponse, locale: AppLocale) => {
+ response.cookies.set({
+ name: LOCALE_COOKIE_NAME,
+ value: locale,
+ path: '/',
+ maxAge: LOCALE_COOKIE_MAX_AGE,
+ sameSite: 'lax',
+ });
+};
+
+export const middleware = (request: NextRequest) => {
+ const { pathname, search } = request.nextUrl;
+
+ const firstSlash = pathname.indexOf('/', 1);
+ const firstSegment =
+ firstSlash === -1 ? pathname.slice(1) : pathname.slice(1, firstSlash);
+ const localeFromPath = APP_LOCALE_BY_RAW.get(firstSegment);
+
+ if (localeFromPath === SOURCE_LOCALE) {
+ const canonicalPath = firstSlash === -1 ? '/' : pathname.slice(firstSlash);
+ const target = request.nextUrl.clone();
+ target.pathname = canonicalPath;
+ target.search = search;
+ return NextResponse.redirect(target, 301);
+ }
+
+ if (localeFromPath !== undefined) {
+ const cookieLocale = request.cookies.get(LOCALE_COOKIE_NAME)?.value;
+ if (cookieLocale === localeFromPath) {
+ return NextResponse.next();
+ }
+ const response = NextResponse.next();
+ setLocaleCookie(response, localeFromPath);
+ return response;
+ }
+
+ const detected = detectLocale({
+ cookieValue: request.cookies.get(LOCALE_COOKIE_NAME)?.value,
+ acceptLanguageHeader: request.headers.get('accept-language') ?? undefined,
+ });
+
+ if (detected === SOURCE_LOCALE) {
+ const target = request.nextUrl.clone();
+ target.pathname = `/${SOURCE_LOCALE}${pathname === '/' ? '' : pathname}`;
+ const response = NextResponse.rewrite(target);
+ const cookieLocale = request.cookies.get(LOCALE_COOKIE_NAME)?.value;
+ if (cookieLocale !== SOURCE_LOCALE) {
+ setLocaleCookie(response, SOURCE_LOCALE);
+ }
+ return response;
+ }
+
+ const target = request.nextUrl.clone();
+ target.pathname = `/${detected}${pathname === '/' ? '' : pathname}`;
+ target.search = search;
+ return NextResponse.redirect(target, 308);
+};
+
+export const config = {
+ matcher: [
+ '/((?!api|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml|illustrations|lottie|fonts|.*\\..*).*)',
+ ],
+};
diff --git a/packages/twenty-website-new/src/sections/CaseStudy/components/Hero.tsx b/packages/twenty-website-new/src/sections/CaseStudy/components/Hero.tsx
index 7298617e52..fdce472b37 100644
--- a/packages/twenty-website-new/src/sections/CaseStudy/components/Hero.tsx
+++ b/packages/twenty-website-new/src/sections/CaseStudy/components/Hero.tsx
@@ -1,12 +1,12 @@
import type { CaseStudyData } from '@/lib/customers';
import { Container, Heading } from '@/design-system/components';
import { CLIENT_ICONS } from '@/icons';
+import { LocalizedLink } from '@/lib/i18n';
import { CustomerCasesCover } from '@/sections/CaseStudyCatalog/visuals/CustomerCasesCover';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import { IconArrowLeft, IconClock } from '@tabler/icons-react';
import Image from 'next/image';
-import Link from 'next/link';
const CATALOG_LOGO_WIDTHS: Record = {
'nine-dots': 72,
@@ -46,7 +46,7 @@ const StyledContainer = styled(Container)`
}
`;
-const BackLink = styled(Link)`
+const BackLink = styled(LocalizedLink)`
align-items: center;
color: ${theme.colors.secondary.text[80]};
display: inline-flex;
diff --git a/packages/twenty-website-new/src/sections/CaseStudyCatalog/components/Card.tsx b/packages/twenty-website-new/src/sections/CaseStudyCatalog/components/Card.tsx
index a5ef06d0db..beb2bf84e8 100644
--- a/packages/twenty-website-new/src/sections/CaseStudyCatalog/components/Card.tsx
+++ b/packages/twenty-website-new/src/sections/CaseStudyCatalog/components/Card.tsx
@@ -1,15 +1,15 @@
import type { CaseStudyCatalogEntry } from '@/lib/customers';
import { ArrowRightIcon, CLIENT_ICONS } from '@/icons';
+import { LocalizedLink } from '@/lib/i18n';
import { CustomerCasesCover } from '@/sections/CaseStudyCatalog/visuals/CustomerCasesCover';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import Image from 'next/image';
-import Link from 'next/link';
import { type ComponentProps } from 'react';
type CardVariant = 'default' | 'large';
-type CardLinkBaseProps = ComponentProps & {
+type CardLinkBaseProps = ComponentProps & {
$cardIndex: number;
variant: CardVariant;
};
@@ -19,7 +19,7 @@ function CardLinkBase({
variant: _variant,
...linkProps
}: CardLinkBaseProps) {
- return ;
+ return ;
}
const CardLink = styled(CardLinkBase)<{
diff --git a/packages/twenty-website-new/src/sections/Footer/components/Nav.tsx b/packages/twenty-website-new/src/sections/Footer/components/Nav.tsx
index 442f2b1343..97e9e80e1e 100644
--- a/packages/twenty-website-new/src/sections/Footer/components/Nav.tsx
+++ b/packages/twenty-website-new/src/sections/Footer/components/Nav.tsx
@@ -1,10 +1,10 @@
import { FooterNavCta } from '@/sections/Footer/components/FooterNavCta';
import { PlusIcon, RectangleFillIcon } from '@/icons';
+import { LocalizedLink } from '@/lib/i18n';
import type { FooterNavGroupType } from '@/sections/Footer/types';
import { theme } from '@/theme';
import { NavigationMenu } from '@base-ui/react/navigation-menu';
import { styled } from '@linaria/react';
-import Link from 'next/link';
import React from 'react';
const FooterNav = styled.nav`
@@ -161,7 +161,7 @@ export function Nav({ groups }: NavProps) {
rel="noopener noreferrer"
/>
) : (
-
+
)
}
>
diff --git a/packages/twenty-website-new/src/sections/Menu/components/Drawer.tsx b/packages/twenty-website-new/src/sections/Menu/components/Drawer.tsx
index a02707e2a4..5dcb549e24 100644
--- a/packages/twenty-website-new/src/sections/Menu/components/Drawer.tsx
+++ b/packages/twenty-website-new/src/sections/Menu/components/Drawer.tsx
@@ -11,8 +11,7 @@ import { theme } from '@/theme';
import { Drawer } from '@base-ui/react/drawer';
import { Separator } from '@base-ui/react/separator';
import { styled } from '@linaria/react';
-import Link from 'next/link';
-import { usePathname } from 'next/navigation';
+import { LocalizedLink, useUnlocalizedPathname } from '@/lib/i18n';
import React, { useState } from 'react';
const StyledDrawerContent = styled.div`
@@ -93,7 +92,7 @@ const navItemStyles = `
}
`;
-const NavItem = styled(Link)`
+const NavItem = styled(LocalizedLink)`
${navItemStyles}
`;
@@ -169,7 +168,7 @@ const NavChildItemStyles = `
}
`;
-const NavChildItem = styled(Link)`
+const NavChildItem = styled(LocalizedLink)`
${NavChildItemStyles}
`;
@@ -284,14 +283,14 @@ const Divider = styled(Separator)`
type NavGroupProps = {
item: MenuNavItemType;
- pathname: string | null;
+ pathname: string;
scheme: MenuScheme;
};
function NavGroup({ item, pathname, scheme }: NavGroupProps) {
const hasActiveChild =
item.children?.some(
- (child) => !child.external && pathname?.startsWith(child.href),
+ (child) => !child.external && pathname.startsWith(child.href),
) ?? false;
const [isOpen, setIsOpen] = useState(hasActiveChild);
@@ -345,9 +344,7 @@ function NavGroup({ item, pathname, scheme }: NavGroupProps) {
) : (
)
@@ -375,7 +372,7 @@ type MenuDrawerProps = {
};
export function MenuDrawer({ navItems, scheme, socialLinks }: MenuDrawerProps) {
- const pathname = usePathname();
+ const pathname = useUnlocalizedPathname();
const buttonColor = scheme === 'primary' ? 'secondary' : 'primary';
const iconFillColor =
@@ -406,7 +403,7 @@ export function MenuDrawer({ navItems, scheme, socialLinks }: MenuDrawerProps) {
diff --git a/packages/twenty-website-new/src/sections/Menu/components/Logo.tsx b/packages/twenty-website-new/src/sections/Menu/components/Logo.tsx
index 1812225f34..160364726a 100644
--- a/packages/twenty-website-new/src/sections/Menu/components/Logo.tsx
+++ b/packages/twenty-website-new/src/sections/Menu/components/Logo.tsx
@@ -1,16 +1,16 @@
import { Logo as LogoIcon } from '@/icons';
+import { LocalizedLink } from '@/lib/i18n';
import type { MenuScheme } from '@/sections/Menu/types';
import { theme } from '@/theme';
import { Drawer } from '@base-ui/react/drawer';
import { styled } from '@linaria/react';
-import Link from 'next/link';
const LogoContainer = styled.div`
align-items: center;
display: grid;
`;
-const LogoLink = styled(Link)`
+const LogoLink = styled(LocalizedLink)`
display: grid;
text-decoration: none;
diff --git a/packages/twenty-website-new/src/sections/Menu/components/Nav.tsx b/packages/twenty-website-new/src/sections/Menu/components/Nav.tsx
index e6f79b1707..2679c61fc8 100644
--- a/packages/twenty-website-new/src/sections/Menu/components/Nav.tsx
+++ b/packages/twenty-website-new/src/sections/Menu/components/Nav.tsx
@@ -10,9 +10,8 @@ import { theme } from '@/theme';
import { NavigationMenu } from '@base-ui/react/navigation-menu';
import { Separator } from '@base-ui/react/separator';
import { styled } from '@linaria/react';
+import { LocalizedLink, useUnlocalizedPathname } from '@/lib/i18n';
import Image from 'next/image';
-import Link from 'next/link';
-import { usePathname } from 'next/navigation';
import React, { useState } from 'react';
const NavList = styled(NavigationMenu.List)`
@@ -412,7 +411,7 @@ const DropdownDescription = styled.span`
type DropdownContentProps = {
items: MenuNavChildItemType[];
- pathname: string | null;
+ pathname: string;
scheme: MenuScheme;
};
@@ -451,7 +450,7 @@ function DropdownContent({ items, pathname, scheme }: DropdownContentProps) {
) : (
-
+
)
}
>
@@ -533,7 +532,7 @@ type NavProps = {
};
export function Nav({ navItems, scheme }: NavProps) {
- const pathname = usePathname();
+ const pathname = useUnlocalizedPathname();
const hasDropdown = navItems.some((item) => item.children);
@@ -553,7 +552,7 @@ export function Nav({ navItems, scheme }: NavProps) {
data-active={
item.children.some(
(child) =>
- !child.external && pathname?.startsWith(child.href),
+ !child.external && pathname.startsWith(child.href),
) || undefined
}
>
@@ -588,8 +587,8 @@ export function Nav({ navItems, scheme }: NavProps) {
item.href && (
}
+ data-active={pathname.startsWith(item.href) || undefined}
+ render={}
>
{item.label}
diff --git a/packages/twenty-website-new/src/sections/ThreeCards/components/IllustrationCard/IllustrationCard.tsx b/packages/twenty-website-new/src/sections/ThreeCards/components/IllustrationCard/IllustrationCard.tsx
index 88b7196d60..b2e38dc289 100644
--- a/packages/twenty-website-new/src/sections/ThreeCards/components/IllustrationCard/IllustrationCard.tsx
+++ b/packages/twenty-website-new/src/sections/ThreeCards/components/IllustrationCard/IllustrationCard.tsx
@@ -9,9 +9,9 @@ import { WebGlMount } from '@/lib/visual-runtime';
import type { ThreeCardsIllustrationCardType } from '@/sections/ThreeCards/types';
import { THREE_CARDS_VISUALS } from '@/sections/ThreeCards/visuals';
import { theme } from '@/theme';
+import { LocalizedLink } from '@/lib/i18n';
import { css } from '@linaria/core';
import { styled } from '@linaria/react';
-import Link from 'next/link';
import { ThreeCardsCardShape } from './CardShape';
const CARD_OUTLINE_COLOR = theme.colors.primary.border[20];
@@ -212,7 +212,7 @@ const PartnerActionIconButton = styled.button`
}
`;
-const PartnerActionIconLink = styled(Link)`
+const PartnerActionIconLink = styled(LocalizedLink)`
align-items: center;
appearance: none;
background: transparent;
diff --git a/yarn.lock b/yarn.lock
index ee13d05810..cdcfe5d875 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -60912,6 +60912,12 @@ __metadata:
"@gsap/react": "npm:2.1.2"
"@linaria/core": "npm:^7.0.0"
"@linaria/react": "npm:^7.0.1"
+ "@lingui/cli": "npm:^5.1.2"
+ "@lingui/conf": "npm:5.1.2"
+ "@lingui/core": "npm:^5.1.2"
+ "@lingui/format-po": "npm:5.1.2"
+ "@lingui/react": "npm:^5.1.2"
+ "@lingui/swc-plugin": "npm:^5.11.0"
"@lottiefiles/dotlottie-react": "npm:^0.18.10"
"@react-three/drei": "npm:^10.7.7"
"@react-three/fiber": "npm:^9.5.0"
@@ -60933,6 +60939,7 @@ __metadata:
sharp: "npm:^0.33.5"
stripe: "npm:^20.3.1"
three: "npm:^0.183.2"
+ twenty-shared: "workspace:*"
zod: "npm:^4.1.11"
languageName: unknown
linkType: soft