;
+};
+
+export default async function CaseStudiesCatalogPage({
+ params,
+}: CaseStudiesCatalogPageProps) {
+ const [i18n, stats] = await Promise.all([
+ getRouteI18n(params),
+ fetchCommunityStats(),
+ ]);
+ const renderText = createMessageDescriptorRenderer(i18n);
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
return (
@@ -86,41 +91,75 @@ export default async function CaseStudiesCatalogPage() {
-
-
+
+
+ {renderText(msg`See how teams`)}
+
+
+
+ {renderText(msg`build`)}
+ {' '}
+
+ {renderText(msg`on Twenty`)}
+
+
+
-
+
-
+
-
-
+
+
+ {renderText(msg`Ready to build`)}
+
+
+
+ {renderText(msg`your own story?`)}
+
+
+
@@ -128,19 +167,30 @@ export default async function CaseStudiesCatalogPage() {
-
-
+
+
+
+ {renderText(msg`Stop fighting custom.`)}
+
+
+
+ {renderText(msg`Start building, with Twenty`)}
+
+
diff --git a/packages/twenty-website-new/src/app/[locale]/customers/w3villa/page.tsx b/packages/twenty-website-new/src/app/[locale]/customers/w3villa/page.tsx
index eff21a97ba0..0314103f803 100644
--- a/packages/twenty-website-new/src/app/[locale]/customers/w3villa/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/customers/w3villa/page.tsx
@@ -1,7 +1,13 @@
+import { msg } from '@lingui/core/macro';
import { MENU_DATA } from '@/sections/Menu/data';
import { CustomersCaseStudySignoff } from '@/app/[locale]/customers/_components/CustomersCaseStudySignoff';
import { getCaseStudyPalette, type CaseStudyData } from '@/lib/customers';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
+import { createMessageDescriptorRenderer } from '@/lib/i18n/create-message-descriptor-renderer';
+import {
+ getRouteI18n,
+ type LocaleRouteParams,
+} from '@/lib/i18n/get-route-i18n';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudy } from '@/sections/CaseStudy/components';
import { Menu } from '@/sections/Menu/components';
@@ -13,89 +19,92 @@ const PLACEHOLDER_HERO =
const CASE_STUDY: CaseStudyData = {
meta: {
- title:
- 'When your CRM is the product: W3Grads on Twenty | W3villa Technologies',
- description:
- 'How W3villa Technologies shipped W3Grads, an AI mock interview platform for institutions, on Twenty as the operational backbone.',
+ title: msg`When your CRM is the product: W3Grads on Twenty | W3villa Technologies`,
+ description: msg`How W3villa Technologies shipped W3Grads, an AI mock interview platform for institutions, on Twenty as the operational backbone.`,
},
hero: {
readingTime: '8 min',
title: [
- { text: 'When your CRM is ', fontFamily: 'serif' },
- { text: 'the product', fontFamily: 'sans', newLine: true },
+ { text: msg`When your CRM is`, fontFamily: 'serif' },
+ { text: msg`the product`, fontFamily: 'sans', newLine: true },
],
author: 'Amrendra Pratap Singh',
authorAvatarSrc: '/images/partner/testimonials/amrendra-singh.webp',
- authorRole: 'VP of Engineering, W3villa Technologies',
+ authorRole: msg`VP of Engineering, W3villa Technologies`,
clientIcon: 'w3villa',
heroImageSrc: PLACEHOLDER_HERO,
- industry: 'EdTech',
- kpis: [{ value: 'Zero', label: 'Manual work at core' }],
+ industry: msg`EdTech`,
+ kpis: [{ value: msg`Zero`, label: msg`Manual work at core` }],
},
sections: [
{
type: 'text',
- eyebrow: 'W3Grads',
+ eyebrow: msg`W3Grads`,
heading: [
- { text: 'Scale without ', fontFamily: 'serif' },
- { text: 'breaking operations', fontFamily: 'sans' },
+ { text: msg`Scale without`, fontFamily: 'serif' },
+ { text: msg`breaking operations`, fontFamily: 'sans' },
],
paragraphs: [
- 'Running mock interview programs for hundreds of students sounds straightforward. In practice, universities and training institutes hit the same wall: registrations entered by hand, interview links sent one by one, faculty reviewing every session without scoring or classification. At real scale, it breaks.',
- 'W3villa Technologies set out to solve it properly, not with a workaround, but with a product.',
+ msg`Running mock interview programs for hundreds of students sounds straightforward. In practice, universities and training institutes hit the same wall: registrations entered by hand, interview links sent one by one, faculty reviewing every session without scoring or classification. At real scale, it breaks.`,
+ msg`W3villa Technologies set out to solve it properly, not with a workaround, but with a product.`,
],
callout:
'"We did not want to patch over the problem. We wanted to build something institutions could rely on at scale, and that meant starting from a foundation solid enough to support the full complexity of what we had in mind." - Amrendra Pratap Singh, VP of Engineering, W3villa Technologies',
},
{
type: 'text',
- eyebrow: 'Architecture',
+ eyebrow: msg`Architecture`,
heading: [
- { text: 'Focus on the use case, not the ', fontFamily: 'serif' },
- { text: 'plumbing', fontFamily: 'sans' },
+ {
+ text: msg`Focus on the use case, not the`,
+ fontFamily: 'serif',
+ },
+ { text: msg`plumbing`, fontFamily: 'sans' },
],
paragraphs: [
- 'W3villa built W3Grads (w3grads.com), an AI-powered mock interview platform for universities and training institutes, using Twenty as its operational backbone.',
- 'The key decision was not to build everything from scratch. Twenty covers the data model, permissions, authentication, and workflow engine, the parts that would have taken months to rebuild, so the team could focus on product-specific logic.',
- 'When a student registers via QR at a campus event, the system assigns a plan, generates an interview session, and sends a link. The AI conducts the interview, scores the candidate, and classifies the result. Faculty see where each student stands without manually reviewing every session. Building and iterating on these workflows was faster with AI in the loop.',
+ msg`W3villa built W3Grads (w3grads.com), an AI-powered mock interview platform for universities and training institutes, using Twenty as its operational backbone.`,
+ msg`The key decision was not to build everything from scratch. Twenty covers the data model, permissions, authentication, and workflow engine, the parts that would have taken months to rebuild, so the team could focus on product-specific logic.`,
+ msg`When a student registers via QR at a campus event, the system assigns a plan, generates an interview session, and sends a link. The AI conducts the interview, scores the candidate, and classifies the result. Faculty see where each student stands without manually reviewing every session. Building and iterating on these workflows was faster with AI in the loop.`,
],
},
{
type: 'text',
- eyebrow: 'Scale',
+ eyebrow: msg`Scale`,
heading: [
- { text: 'A platform ready to ', fontFamily: 'serif' },
- { text: 'grow', fontFamily: 'sans' },
+ {
+ text: msg`A platform ready to`,
+ fontFamily: 'serif',
+ },
+ { text: msg`grow`, fontFamily: 'sans' },
],
paragraphs: [
- 'Because the foundation is solid, W3Grads is architected for what comes next, including a payment layer for future paid interview plans and nationwide scale without structural rewrites.',
+ msg`Because the foundation is solid, W3Grads is architected for what comes next, including a payment layer for future paid interview plans and nationwide scale without structural rewrites.`,
],
callout:
'"Twenty gave us the flexibility to model the entire interview lifecycle as custom objects and workflows. We could build something genuinely complex without fighting the platform to do it." - Piyush Khandelwal, Director, W3villa Technologies, Partner',
},
{
type: 'text',
- eyebrow: 'The result',
+ eyebrow: msg`The result`,
heading: [
- { text: 'Zero manual work', fontFamily: 'sans' },
- { text: ' at the core', fontFamily: 'serif' },
+ { text: msg`Zero manual work`, fontFamily: 'sans' },
+ { text: msg`at the core`, fontFamily: 'serif' },
],
paragraphs: [
- 'Programs that previously needed heavy manual coordination now run end-to-end with automation. Institutions get a scalable, intelligent system; students get faster preparation for interviews that matter; W3villa shipped a product institutions can build revenue around.',
- 'Zero manual work at the core. Full automation. Built on Twenty.',
+ msg`Programs that previously needed heavy manual coordination now run end-to-end with automation. Institutions get a scalable, intelligent system; students get faster preparation for interviews that matter; W3villa shipped a product institutions can build revenue around.`,
+ msg`Zero manual work at the core. Full automation. Built on Twenty.`,
],
},
],
tableOfContents: [
- 'Scale without breaking operations',
- 'Focus on the use case, not the plumbing',
- 'A platform ready to grow',
- 'The result',
+ msg`Scale without breaking operations`,
+ msg`Focus on the use case, not the plumbing`,
+ msg`A platform ready to grow`,
+ msg`The result`,
],
catalogCard: {
- summary:
- 'W3villa shipped W3Grads on Twenty for AI interviews, scoring, and institution-scale workflows without rebuilding CRM plumbing.',
- date: '2025',
+ summary: msg`W3villa shipped W3Grads on Twenty for AI interviews, scoring, and institution-scale workflows without rebuilding CRM plumbing.`,
+ date: msg`2025`,
},
};
@@ -105,8 +114,18 @@ export const generateMetadata = buildLocalizedMetadata({
description: CASE_STUDY.meta.description,
});
-export default async function W3villaCaseStudyPage() {
- const stats = await fetchCommunityStats();
+type CaseStudyPageProps = {
+ params: Promise;
+};
+
+export default async function W3villaCaseStudyPage({
+ params,
+}: CaseStudyPageProps) {
+ const [i18n, stats] = await Promise.all([
+ getRouteI18n(params),
+ fetchCommunityStats(),
+ ]);
+ const renderText = createMessageDescriptorRenderer(i18n);
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
const palette = getCaseStudyPalette('/customers/w3villa');
@@ -120,6 +139,7 @@ export default async function W3villaCaseStudyPage() {
key={index}
block={block}
isLast={index === CASE_STUDY.sections.length - 1}
+ renderText={renderText}
sectionId={sectionId}
/>
);
@@ -151,17 +171,19 @@ export default async function W3villaCaseStudyPage() {
dashColor={palette.dashColor}
hero={CASE_STUDY.hero}
hoverDashColor={palette.hoverDashColor}
+ renderText={renderText}
/>
{sectionBlocks}
-
+
>
);
}
diff --git a/packages/twenty-website-new/src/app/[locale]/enterprise/activate/EnterpriseActivateClient.tsx b/packages/twenty-website-new/src/app/[locale]/enterprise/activate/EnterpriseActivateClient.tsx
index f7a0c89a770..27ef2657920 100644
--- a/packages/twenty-website-new/src/app/[locale]/enterprise/activate/EnterpriseActivateClient.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/enterprise/activate/EnterpriseActivateClient.tsx
@@ -1,10 +1,12 @@
'use client';
+import { msg } from '@lingui/core/macro';
import {
BaseButton,
buttonBaseStyles,
} from '@/design-system/components/Button/BaseButton';
import { Body, Heading } from '@/design-system/components';
+import { useRenderMessage } from '@/lib/i18n/use-render-message';
import { useTimeoutRegistry } from '@/lib/react';
import { theme } from '@/theme';
import { css } from '@linaria/core';
@@ -46,6 +48,12 @@ const LicenseeRow = styled.div`
gap: ${theme.spacing(1)};
`;
+const LicenseeValue = styled.span`
+ font-family: ${theme.font.family.sans};
+ font-size: ${theme.font.size(4)};
+ line-height: 1.55;
+`;
+
const KeySection = styled.div`
display: flex;
flex-direction: column;
@@ -132,6 +140,7 @@ const nextStepItemClassName = css`
`;
export function EnterpriseActivateClient() {
+ const renderText = useRenderMessage();
const searchParams = useSearchParams();
const sessionId = searchParams.get('session_id');
const timeoutRegistry = useTimeoutRegistry();
@@ -214,7 +223,8 @@ export function EnterpriseActivateClient() {
{loading && (
@@ -226,9 +236,10 @@ export function EnterpriseActivateClient() {
<>
@@ -236,24 +247,27 @@ export function EnterpriseActivateClient() {
-
+ />{' '}
+ {result.licensee}
@@ -270,7 +284,7 @@ export function EnterpriseActivateClient() {
>
@@ -281,28 +295,32 @@ export function EnterpriseActivateClient() {
diff --git a/packages/twenty-website-new/src/app/[locale]/enterprise/activate/page.tsx b/packages/twenty-website-new/src/app/[locale]/enterprise/activate/page.tsx
index 058a7047f44..c53a9edad74 100644
--- a/packages/twenty-website-new/src/app/[locale]/enterprise/activate/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/enterprise/activate/page.tsx
@@ -1,9 +1,19 @@
+import { msg } from '@lingui/core/macro';
import { MENU_DATA } from '@/sections/Menu/data';
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';
+import {
+ Body,
+ Container,
+ Eyebrow,
+ HeadingPart,
+} from '@/design-system/components';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
+import { createMessageDescriptorRenderer } from '@/lib/i18n/create-message-descriptor-renderer';
+import {
+ getRouteI18n,
+ type LocaleRouteParams,
+} from '@/lib/i18n/get-route-i18n';
+import { Pages } from '@/lib/pages';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { Hero } from '@/sections/Hero/components';
import { Menu } from '@/sections/Menu/components';
@@ -14,13 +24,8 @@ import { styled } from '@linaria/react';
export const generateMetadata = buildRouteMetadata('enterpriseActivate');
-const ENTERPRISE_ACTIVATE_HEADING: HeadingType[] = [
- { text: 'Enterprise ', fontFamily: 'serif' },
- { text: 'activation', fontFamily: 'sans' },
-];
-
const ENTERPRISE_ACTIVATE_BODY = {
- text: 'Your checkout is complete. Follow the steps below to copy your license key into your Twenty instance.',
+ text: msg`Your checkout is complete. Follow the steps below to copy your license key into your Twenty instance.`,
};
const ActivatePageContent = styled.section`
@@ -38,18 +43,30 @@ const ActivateContentInner = styled.div`
width: 100%;
`;
-function EnterpriseActivateFallback() {
+type EnterpriseActivateFallbackProps = {
+ loadingLabel: string;
+};
+
+function EnterpriseActivateFallback({
+ loadingLabel,
+}: EnterpriseActivateFallbackProps) {
return (
-
+
);
}
-export default async function EnterpriseActivatePage() {
- const stats = await fetchCommunityStats();
+type EnterpriseActivatePageProps = {
+ params: Promise;
+};
+
+export default async function EnterpriseActivatePage({
+ params,
+}: EnterpriseActivatePageProps) {
+ const [i18n, stats] = await Promise.all([
+ getRouteI18n(params),
+ fetchCommunityStats(),
+ ]);
+ const renderText = createMessageDescriptorRenderer(i18n);
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
return (
@@ -72,19 +89,34 @@ export default async function EnterpriseActivatePage() {
>
-
+
+ {renderText(msg`Enterprise`)}
+ {' '}
+
+ {renderText(msg`activation`)}
+
+
+
-
- }>
+
+ }
+ >
diff --git a/packages/twenty-website-new/src/app/[locale]/layout.tsx b/packages/twenty-website-new/src/app/[locale]/layout.tsx
index 5fddedb5759..016dbfc9bf8 100644
--- a/packages/twenty-website-new/src/app/[locale]/layout.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/layout.tsx
@@ -11,12 +11,14 @@ import { type ReactNode } from 'react';
import { FooterVisibilityGate } from '@/app/_components/FooterVisibilityGate';
import { ScrollToTopOnRouteChange } from '@/app/_components/ScrollToTopOnRouteChange';
import { ContactCalModalRoot } from '@/lib/contact-cal';
+import { createMessageDescriptorRenderer } from '@/lib/i18n/create-message-descriptor-renderer';
import {
I18nProvider,
PUBLIC_APP_LOCALE_LIST,
- getLocaleMessages,
resolveLocaleParam,
} from '@/lib/i18n';
+import { getLocaleMessages } from '@/lib/i18n/messages-by-locale';
+import { setServerI18n } from '@/lib/i18n/set-server-i18n';
import { PartnerApplicationModalRoot } from '@/lib/partner-application';
import { Footer } from '@/sections/Footer/components';
import { FOOTER_DATA } from '@/sections/Footer/data';
@@ -124,6 +126,8 @@ const LocaleLayout = async ({
}) => {
const { locale: rawLocale } = await params;
const locale = resolveLocaleParam(rawLocale);
+ const i18n = setServerI18n(locale);
+ const renderText = createMessageDescriptorRenderer(i18n);
const messages = getLocaleMessages(locale);
return (
@@ -147,10 +151,14 @@ const LocaleLayout = async ({
-
+
diff --git a/packages/twenty-website-new/src/app/[locale]/partners/components/PartnerApplication/BecomePartnerButton.tsx b/packages/twenty-website-new/src/app/[locale]/partners/components/PartnerApplication/BecomePartnerButton.tsx
index a7b54b02ba6..c3d7c525db3 100644
--- a/packages/twenty-website-new/src/app/[locale]/partners/components/PartnerApplication/BecomePartnerButton.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/partners/components/PartnerApplication/BecomePartnerButton.tsx
@@ -4,7 +4,10 @@ import {
BaseButton,
buttonBaseStyles,
} from '@/design-system/components/Button/BaseButton';
+import { useRenderMessage } from '@/lib/i18n/use-render-message';
import { usePartnerApplicationModal } from '@/lib/partner-application';
+import type { MessageDescriptor } from '@lingui/core';
+import { msg } from '@lingui/core/macro';
import { styled } from '@linaria/react';
const StyledTrigger = styled.button`
@@ -13,15 +16,16 @@ const StyledTrigger = styled.button`
type BecomePartnerButtonProps = {
color?: 'primary' | 'secondary';
- label?: string;
+ label?: MessageDescriptor;
variant?: 'contained' | 'outlined';
};
export function BecomePartnerButton({
color = 'secondary',
- label = 'Become a partner',
+ label = msg`Become a partner`,
variant = 'contained',
}: BecomePartnerButtonProps) {
+ const renderText = useRenderMessage();
const { openPartnerApplicationModal } = usePartnerApplicationModal();
return (
@@ -33,7 +37,7 @@ export function BecomePartnerButton({
openPartnerApplicationModal();
}}
>
-
+
);
}
diff --git a/packages/twenty-website-new/src/app/[locale]/partners/components/PartnerApplication/PartnerHeroCtas.tsx b/packages/twenty-website-new/src/app/[locale]/partners/components/PartnerApplication/PartnerHeroCtas.tsx
index 83a9a009b56..94b395327af 100644
--- a/packages/twenty-website-new/src/app/[locale]/partners/components/PartnerApplication/PartnerHeroCtas.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/partners/components/PartnerApplication/PartnerHeroCtas.tsx
@@ -1,6 +1,7 @@
'use client';
import { TalkToUsButton } from '@/lib/contact-cal';
+import { msg } from '@lingui/core/macro';
import { BecomePartnerButton } from './BecomePartnerButton';
@@ -10,7 +11,7 @@ export function PartnerHeroCtas() {
>
diff --git a/packages/twenty-website-new/src/app/[locale]/partners/components/PartnerApplication/PartnerSignoffCtas.tsx b/packages/twenty-website-new/src/app/[locale]/partners/components/PartnerApplication/PartnerSignoffCtas.tsx
index 9382601c2eb..b103637dfe2 100644
--- a/packages/twenty-website-new/src/app/[locale]/partners/components/PartnerApplication/PartnerSignoffCtas.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/partners/components/PartnerApplication/PartnerSignoffCtas.tsx
@@ -1,6 +1,7 @@
'use client';
import { TalkToUsButton } from '@/lib/contact-cal';
+import { msg } from '@lingui/core/macro';
import { BecomePartnerButton } from './BecomePartnerButton';
@@ -10,7 +11,7 @@ export function PartnerSignoffCtas() {
>
diff --git a/packages/twenty-website-new/src/app/[locale]/partners/engagement-band.data.ts b/packages/twenty-website-new/src/app/[locale]/partners/engagement-band.data.ts
deleted file mode 100644
index 7af2efc3e91..00000000000
--- a/packages/twenty-website-new/src/app/[locale]/partners/engagement-band.data.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { EngagementBandDataType } from '@/sections/EngagementBand/types';
-
-export const ENGAGEMENT_BAND_DATA: EngagementBandDataType = {
- heading: {
- text: 'See what they built',
- fontFamily: 'serif',
- },
- body: {
- text: 'Discover how our partners implement, customize, and scale Twenty in real-world deployments.',
- },
-};
diff --git a/packages/twenty-website-new/src/app/[locale]/partners/hero.data.ts b/packages/twenty-website-new/src/app/[locale]/partners/hero.data.ts
index 0d82b9f3177..c3b4c098e23 100644
--- a/packages/twenty-website-new/src/app/[locale]/partners/hero.data.ts
+++ b/packages/twenty-website-new/src/app/[locale]/partners/hero.data.ts
@@ -1,11 +1,5 @@
-import type { HeroBaseDataType } from '@/sections/Hero/types';
+import { msg } from '@lingui/core/macro';
-export const HERO_DATA = {
- heading: [
- { text: 'Become ', fontFamily: 'serif' },
- { text: 'our partner', fontFamily: 'sans', newLine: true },
- ],
- body: {
- text: "We're building the #1 open source CRM, but we can't do it alone. Join our partner ecosystem and grow with us.",
- },
-} satisfies HeroBaseDataType;
+export const HERO_COPY = {
+ body: msg`We're building the #1 open source CRM, but we can't do it alone. Join our partner ecosystem and grow with us.`,
+};
diff --git a/packages/twenty-website-new/src/app/[locale]/partners/page.tsx b/packages/twenty-website-new/src/app/[locale]/partners/page.tsx
index dfaaed3021d..d2e1c695a12 100644
--- a/packages/twenty-website-new/src/app/[locale]/partners/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/partners/page.tsx
@@ -1,19 +1,31 @@
+import { msg } from '@lingui/core/macro';
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 { CASE_STUDY_CATALOG_ENTRIES } from '@/lib/customers';
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 { HERO_COPY } from '@/app/[locale]/partners/hero.data';
+import { SIGNOFF_COPY } from '@/app/[locale]/partners/signoff.data';
import { TESTIMONIALS_DATA } from '@/app/[locale]/partners/testimonials.data';
import {
PartnerHeroCtas,
PartnerSignoffCtas,
} from '@/app/[locale]/partners/components/PartnerApplication';
-import { Body, Eyebrow, Heading, LinkButton } from '@/design-system/components';
-import { Pages } from '@/lib/pages';
+import {
+ Body,
+ Eyebrow,
+ Heading,
+ HeadingPart,
+ LinkButton,
+} from '@/design-system/components';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
+import { createMessageDescriptorRenderer } from '@/lib/i18n/create-message-descriptor-renderer';
+import {
+ getRouteI18n,
+ type LocaleRouteParams,
+} from '@/lib/i18n/get-route-i18n';
+import { Pages } from '@/lib/pages';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudyCatalog } from '@/sections/CaseStudyCatalog/components';
import { Faq } from '@/sections/Faq/components';
@@ -47,8 +59,16 @@ const PromoSpacing = styled.div`
export const generateMetadata = buildRouteMetadata('partners');
-export default async function PartnerPage() {
- const stats = await fetchCommunityStats();
+type PartnerPageProps = {
+ params: Promise;
+};
+
+export default async function PartnerPage({ params }: PartnerPageProps) {
+ const [i18n, stats] = await Promise.all([
+ getRouteI18n(params),
+ fetchCommunityStats(),
+ ]);
+ const renderText = createMessageDescriptorRenderer(i18n);
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
return (
@@ -66,8 +86,20 @@ export default async function PartnerPage() {
-
-
+
+
+ {renderText(msg`Become`)}
+
+
+
+ {renderText(msg`our partner`)}
+
+
+
@@ -78,15 +110,22 @@ export default async function PartnerPage() {
backgroundColor={theme.colors.primary.background[100]}
compactBottom
>
-
+
-
+
@@ -95,13 +134,23 @@ export default async function PartnerPage() {
-
-
+
+
+ {renderText(msg`Find the program that fits your business`)}
+ {' '}
+
+ {renderText(msg`and unlock new opportunities with Twenty`)}
+
+
+ {THREE_CARDS_ILLUSTRATION_DATA.body && (
+
+ )}
-
+
+ {renderText(msg`Ready to grow`)}
+
+
+
+ {renderText(msg`with Twenty?`)}
+
+
+
-
@@ -140,19 +198,30 @@ export default async function PartnerPage() {
-
-
+
+
+
+ {renderText(msg`Stop fighting custom.`)}
+
+
+
+ {renderText(msg`Start building, with Twenty`)}
+
+
diff --git a/packages/twenty-website-new/src/app/[locale]/partners/signoff.data.ts b/packages/twenty-website-new/src/app/[locale]/partners/signoff.data.ts
index 206557a25e9..c35eebf9ca6 100644
--- a/packages/twenty-website-new/src/app/[locale]/partners/signoff.data.ts
+++ b/packages/twenty-website-new/src/app/[locale]/partners/signoff.data.ts
@@ -1,11 +1,5 @@
-import type { SignoffDataType } from '@/sections/Signoff/types';
+import { msg } from '@lingui/core/macro';
-export const SIGNOFF_DATA: SignoffDataType = {
- heading: [
- { text: 'Ready to grow\n', fontFamily: 'serif' },
- { text: 'with Twenty?', fontFamily: 'sans' },
- ],
- body: {
- text: 'Join our partner ecosystem and help businesses\ntake control of their CRM.',
- },
+export const SIGNOFF_COPY = {
+ body: msg`Join our partner ecosystem and help businesses\ntake control of their CRM.`,
};
diff --git a/packages/twenty-website-new/src/app/[locale]/partners/testimonials.data.ts b/packages/twenty-website-new/src/app/[locale]/partners/testimonials.data.ts
index 8c53b9379cc..a0d7bd4a469 100644
--- a/packages/twenty-website-new/src/app/[locale]/partners/testimonials.data.ts
+++ b/packages/twenty-website-new/src/app/[locale]/partners/testimonials.data.ts
@@ -1,18 +1,22 @@
+import { msg } from '@lingui/core/macro';
import type { TestimonialsDataType } from '@/sections/Testimonials/types';
export const TESTIMONIALS_DATA: TestimonialsDataType = {
eyebrow: {
- heading: { text: 'Join our growing partner ecosystem', fontFamily: 'sans' },
+ heading: {
+ text: msg`Join our growing partner ecosystem`,
+ fontFamily: 'sans',
+ },
},
testimonials: [
{
heading: {
- text: "Twenty gives you the kind of flexibility that actually changes what you can offer your clients. The dev experience is clean, the APIs are open, and when something needs to be customized, you can just do it. There's no fighting the platform.",
+ text: msg`Twenty gives you the kind of flexibility that actually changes what you can offer your clients. The dev experience is clean, the APIs are open, and when something needs to be customized, you can just do it. There's no fighting the platform.`,
fontFamily: 'sans',
},
author: {
- name: { text: 'Benjamin Reynolds' },
- designation: { text: 'Principal and Founder, Alternative Partners' },
+ name: { text: msg`Benjamin Reynolds` },
+ designation: { text: msg`Principal and Founder, Alternative Partners` },
avatar: {
src: '/images/partner/testimonials/benjamin-reynolds.webp',
alt: 'Portrait of Benjamin Reynolds',
@@ -21,12 +25,12 @@ export const TESTIMONIALS_DATA: TestimonialsDataType = {
},
{
heading: {
- text: "The flexibility is just amazing. Literally, there's nothing you cannot do. You can create objects, access everything through the API, pull notes and send them to the portal. Try doing that in HubSpot. No way. It's the true ability to build exactly what's actually needed.",
+ text: msg`The flexibility is just amazing. Literally, there's nothing you cannot do. You can create objects, access everything through the API, pull notes and send them to the portal. Try doing that in HubSpot. No way. It's the true ability to build exactly what's actually needed.`,
fontFamily: 'sans',
},
author: {
- name: { text: 'Bertrams' },
- designation: { text: 'Founder, Wintactix' },
+ name: { text: msg`Bertrams` },
+ designation: { text: msg`Founder, Wintactix` },
avatar: {
src: '/images/partner/testimonials/bertrams.jpeg',
alt: 'Portrait of Bertrams',
@@ -35,12 +39,12 @@ export const TESTIMONIALS_DATA: TestimonialsDataType = {
},
{
heading: {
- text: "Twenty Apps opens the door to building products, not just implementations. For example, we're developing a WhatsApp Business integration that any Twenty’s client could get. That's a recurring revenue stream we wouldn't have if we were just configuring someone else's platform.",
+ text: msg`Twenty Apps opens the door to building products, not just implementations. For example, we're developing a WhatsApp Business integration that any Twenty’s client could get. That's a recurring revenue stream we wouldn't have if we were just configuring someone else's platform.`,
fontFamily: 'sans',
},
author: {
- name: { text: 'Mike Babiy' },
- designation: { text: 'Founder, Nine Dots Ventures' },
+ name: { text: msg`Mike Babiy` },
+ designation: { text: msg`Founder, Nine Dots Ventures` },
avatar: {
src: '/images/partner/testimonials/mike-babiy.png',
alt: 'Photo featuring Mike Babiy',
diff --git a/packages/twenty-website-new/src/app/[locale]/partners/three-cards-illustration.data.ts b/packages/twenty-website-new/src/app/[locale]/partners/three-cards-illustration.data.ts
index 5a1c23b9d2c..651202b8767 100644
--- a/packages/twenty-website-new/src/app/[locale]/partners/three-cards-illustration.data.ts
+++ b/packages/twenty-website-new/src/app/[locale]/partners/three-cards-illustration.data.ts
@@ -1,67 +1,66 @@
+import { msg } from '@lingui/core/macro';
import type { ThreeCardsIllustrationDataType } from '@/sections/ThreeCards/types';
export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
eyebrow: {
heading: {
- text: 'Which partner program is right for you?',
+ text: msg`Which partner program is right for you?`,
fontFamily: 'sans',
},
},
- heading: [
- { text: 'Find the program that fits your business ', fontFamily: 'serif' },
- { text: 'and unlock new opportunities with Twenty', fontFamily: 'sans' },
- ],
- body: { text: '' },
illustrationCards: [
{
- heading: { text: 'Technology Partners', fontFamily: 'sans' },
+ heading: { text: msg`Technology Partners`, fontFamily: 'sans' },
body: {
- text: 'Build integrations that connect Twenty with the tools your customers already use. Help us expand the Twenty ecosystem.',
+ text: msg`Build integrations that connect Twenty with the tools your customers already use. Help us expand the Twenty ecosystem.`,
},
benefits: [
- { text: 'Co-marketing opportunities', icon: 'users' },
- { text: 'Listing on Twenty integrations page', icon: 'search' },
- { text: 'Soon: earn revenue', icon: 'tag' },
+ { text: msg`Co-marketing opportunities`, icon: 'users' },
+ { text: msg`Listing on Twenty integrations page`, icon: 'search' },
+ { text: msg`Soon: earn revenue`, icon: 'tag' },
],
action: {
kind: 'partnerApplication',
- label: 'Become a Technology Partner',
+ label: msg`Become a Technology Partner`,
programId: 'technology',
},
attribution: undefined,
illustration: 'programming',
},
{
- heading: { text: 'Content & Community Partners', fontFamily: 'sans' },
+ heading: { text: msg`Content & Community Partners`, fontFamily: 'sans' },
body: {
- text: "Share Twenty with your audience and help shape the future of the #1 open source CRM. We're looking for creators, educators, and community builders who want to showcase great software.",
+ text: msg`Share Twenty with your audience and help shape the future of the #1 open source CRM. We're looking for creators, educators, and community builders who want to showcase great software.`,
},
benefits: [
- { text: 'Revenue share for referred customers', icon: 'tag' },
- { text: 'Exclusive content collaboration opportunities', icon: 'edit' },
- { text: 'Marketing assets & brand resources', icon: 'book' },
+ { text: msg`Revenue share for referred customers`, icon: 'tag' },
+ {
+ text: msg`Exclusive content collaboration opportunities`,
+ icon: 'edit',
+ },
+ { text: msg`Marketing assets & brand resources`, icon: 'book' },
],
action: {
kind: 'partnerApplication',
- label: 'Become a Content Partner',
+ label: msg`Become a Content Partner`,
programId: 'content',
},
attribution: undefined,
illustration: 'connect',
},
{
- heading: { text: 'Solutions Partners', fontFamily: 'sans' },
+ heading: { text: msg`Solutions Partners`, fontFamily: 'sans' },
body: {
- text: 'Help customers implement, customize, and succeed with Twenty. Combine sales and services to grow your business.',
+ text: msg`Help customers implement, customize, and succeed with Twenty. Combine sales and services to grow your business.`,
},
benefits: [
- { text: 'Resale discounts & revenue share', icon: 'tag' },
- { text: 'Marketplace listing', icon: 'search' },
- { text: 'Dedicated partner support', icon: 'users' },
+ { text: msg`Resale discounts & revenue share`, icon: 'tag' },
+ { text: msg`Marketplace listing`, icon: 'search' },
+ { text: msg`Dedicated partner support`, icon: 'users' },
],
action: {
kind: 'partnerApplication',
- label: 'Become a Solution Partner',
+ label: msg`Become a Solution Partner`,
programId: 'solutions',
},
attribution: undefined,
diff --git a/packages/twenty-website-new/src/app/[locale]/pricing/engagement-band.data.ts b/packages/twenty-website-new/src/app/[locale]/pricing/engagement-band.data.ts
index d0a10ead894..b516b399f41 100644
--- a/packages/twenty-website-new/src/app/[locale]/pricing/engagement-band.data.ts
+++ b/packages/twenty-website-new/src/app/[locale]/pricing/engagement-band.data.ts
@@ -1,11 +1,6 @@
-import type { EngagementBandDataType } from '@/sections/EngagementBand/types';
+import { msg } from '@lingui/core/macro';
-export const ENGAGEMENT_BAND_DATA: EngagementBandDataType = {
- heading: {
- text: 'Need help with customization?',
- fontFamily: 'serif',
- },
- body: {
- text: 'Find the right partner to implement, customize, and tailor Twenty to your team.',
- },
+export const ENGAGEMENT_BAND_COPY = {
+ body: msg`Find the right partner to implement, customize, and tailor Twenty to your team.`,
+ heading: msg`Need help with customization?`,
};
diff --git a/packages/twenty-website-new/src/app/[locale]/pricing/hero.data.ts b/packages/twenty-website-new/src/app/[locale]/pricing/hero.data.ts
index 2cee64b6ee1..be0371d4c8f 100644
--- a/packages/twenty-website-new/src/app/[locale]/pricing/hero.data.ts
+++ b/packages/twenty-website-new/src/app/[locale]/pricing/hero.data.ts
@@ -1,13 +1,5 @@
-import type { HeroBaseDataType } from '@/sections/Hero/types';
+import { msg } from '@lingui/core/macro';
-const PRICING_HERO_SUBTAGLINE = {
- text: 'Start your free trial today\nwithout credit card.',
+export const HERO_COPY = {
+ body: msg`Start your free trial today\nwithout credit card.`,
};
-
-export const HERO_DATA = {
- heading: [
- { text: 'Simple', fontFamily: 'serif' },
- { text: 'Pricing', fontFamily: 'sans', newLine: true },
- ],
- body: PRICING_HERO_SUBTAGLINE,
-} satisfies HeroBaseDataType;
diff --git a/packages/twenty-website-new/src/app/[locale]/pricing/page.tsx b/packages/twenty-website-new/src/app/[locale]/pricing/page.tsx
index 2670ee32593..b92096479a3 100644
--- a/packages/twenty-website-new/src/app/[locale]/pricing/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/pricing/page.tsx
@@ -1,14 +1,20 @@
+import { msg } from '@lingui/core/macro';
import { FAQ_DATA } from '@/sections/Faq/data';
import { MENU_DATA } from '@/sections/Menu/data';
import { TalkToUsButton } from '@/lib/contact-cal';
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 { ENGAGEMENT_BAND_COPY } from '@/app/[locale]/pricing/engagement-band.data';
+import { HERO_COPY } 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 { Eyebrow, HeadingPart, LinkButton } from '@/design-system/components';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
+import { createMessageDescriptorRenderer } from '@/lib/i18n/create-message-descriptor-renderer';
+import {
+ getRouteI18n,
+ type LocaleRouteParams,
+} from '@/lib/i18n/get-route-i18n';
+import { Pages } from '@/lib/pages';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { EngagementBand } from '@/sections/EngagementBand/components';
import { Faq } from '@/sections/Faq/components';
@@ -36,8 +42,16 @@ const PricingBannerContainer = styled.div`
export const generateMetadata = buildRouteMetadata('pricing');
-export default async function PricingPage() {
- const stats = await fetchCommunityStats();
+type PricingPageProps = {
+ params: Promise;
+};
+
+export default async function PricingPage({ params }: PricingPageProps) {
+ const [i18n, stats] = await Promise.all([
+ getRouteI18n(params),
+ fetchCommunityStats(),
+ ]);
+ const renderText = createMessageDescriptorRenderer(i18n);
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
return (
@@ -55,11 +69,20 @@ export default async function PricingPage() {
-
+
+
+ {renderText(msg`Simple`)}
+
+
+
+ {renderText(msg`Pricing`)}
+
+
@@ -81,14 +104,21 @@ export default async function PricingPage() {
>
+
-
@@ -106,25 +136,40 @@ export default async function PricingPage() {
+ >
+
+ {renderText(msg`Trust the n°1 CRM,`)}
+ {' '}
+ {renderText(msg`or not !`)}
+
-
-
+
+
+
+ {renderText(msg`Stop fighting custom.`)}
+
+
+
+ {renderText(msg`Start building, with Twenty`)}
+
+
diff --git a/packages/twenty-website-new/src/app/[locale]/pricing/plan-table.data.ts b/packages/twenty-website-new/src/app/[locale]/pricing/plan-table.data.ts
index 19287f9e010..f944671fa66 100644
--- a/packages/twenty-website-new/src/app/[locale]/pricing/plan-table.data.ts
+++ b/packages/twenty-website-new/src/app/[locale]/pricing/plan-table.data.ts
@@ -1,402 +1,403 @@
+import { msg } from '@lingui/core/macro';
import type { PlanTableDataType } from '@/sections/PlanTable/types';
export const PLAN_TABLE_DATA: PlanTableDataType = {
- featureColumnLabel: 'Name',
+ featureColumnLabel: msg`Name`,
rows: [
{
- featureLabel: 'Price',
+ featureLabel: msg`Price`,
selfHostTiers: {
- organization: { kind: 'text', text: '$19' },
- pro: { kind: 'text', text: '$0' },
+ organization: { kind: 'text', text: msg`$19` },
+ pro: { kind: 'text', text: msg`$0` },
},
tiers: {
- organization: { kind: 'text', text: '$19' },
- pro: { kind: 'text', text: '$9' },
+ organization: { kind: 'text', text: msg`$19` },
+ pro: { kind: 'text', text: msg`$9` },
},
type: 'row',
},
{
- featureLabel: 'Seats limit',
+ featureLabel: msg`Seats limit`,
tiers: {
- organization: { kind: 'text', text: 'Unlimited' },
- pro: { kind: 'text', text: 'Unlimited' },
+ organization: { kind: 'text', text: msg`Unlimited` },
+ pro: { kind: 'text', text: msg`Unlimited` },
},
type: 'row',
},
{
- title: 'Workspace',
+ title: msg`Workspace`,
type: 'category',
},
{
- featureLabel: 'Custom objects',
+ featureLabel: msg`Custom objects`,
tiers: {
- organization: { kind: 'text', text: 'Unlimited' },
- pro: { kind: 'text', text: 'Unlimited' },
+ organization: { kind: 'text', text: msg`Unlimited` },
+ pro: { kind: 'text', text: msg`Unlimited` },
},
type: 'row',
},
{
- featureLabel: 'Custom fields',
+ featureLabel: msg`Custom fields`,
tiers: {
- organization: { kind: 'text', text: 'Unlimited' },
- pro: { kind: 'text', text: 'Unlimited' },
+ organization: { kind: 'text', text: msg`Unlimited` },
+ pro: { kind: 'text', text: msg`Unlimited` },
},
type: 'row',
},
{
- featureLabel: 'Custom views',
+ featureLabel: msg`Custom views`,
tiers: {
- organization: { kind: 'text', text: 'Unlimited' },
- pro: { kind: 'text', text: 'Unlimited' },
+ organization: { kind: 'text', text: msg`Unlimited` },
+ pro: { kind: 'text', text: msg`Unlimited` },
},
type: 'row',
},
{
- featureLabel: 'View types',
+ featureLabel: msg`View types`,
tiers: {
- organization: { kind: 'text', text: 'Table, Kanban, Calendar' },
- pro: { kind: 'text', text: 'Table, Kanban, Calendar' },
+ organization: { kind: 'text', text: msg`Table, Kanban, Calendar` },
+ pro: { kind: 'text', text: msg`Table, Kanban, Calendar` },
},
type: 'row',
},
{
- featureLabel: 'Custom layout',
+ featureLabel: msg`Custom layout`,
tiers: {
- organization: { kind: 'text', text: 'Unlimited' },
- pro: { kind: 'text', text: 'Unlimited' },
+ organization: { kind: 'text', text: msg`Unlimited` },
+ pro: { kind: 'text', text: msg`Unlimited` },
},
type: 'row',
},
{
- featureLabel: 'Records',
+ featureLabel: msg`Records`,
tiers: {
- organization: { kind: 'text', text: 'Unlimited' },
- pro: { kind: 'text', text: 'Unlimited' },
+ organization: { kind: 'text', text: msg`Unlimited` },
+ pro: { kind: 'text', text: msg`Unlimited` },
},
type: 'row',
},
{
- featureLabel: 'CSV import & export',
+ featureLabel: msg`CSV import & export`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
- pro: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
+ pro: { kind: 'yes', label: msg`Yes` },
},
type: 'row',
},
{
- featureLabel: 'Languages',
+ featureLabel: msg`Languages`,
tiers: {
- organization: { kind: 'text', text: '30+' },
- pro: { kind: 'text', text: '30+' },
+ organization: { kind: 'text', text: msg`30+` },
+ pro: { kind: 'text', text: msg`30+` },
},
type: 'row',
},
{
- title: 'Reports',
+ title: msg`Reports`,
type: 'category',
},
{
- featureLabel: 'Number of dashboards',
+ featureLabel: msg`Number of dashboards`,
tiers: {
- organization: { kind: 'text', text: 'Unlimited' },
- pro: { kind: 'text', text: 'Unlimited' },
+ organization: { kind: 'text', text: msg`Unlimited` },
+ pro: { kind: 'text', text: msg`Unlimited` },
},
type: 'row',
},
{
- title: 'Emails & Calendar',
+ title: msg`Emails & Calendar`,
type: 'category',
},
{
- featureLabel: 'Internet accounts per user',
+ featureLabel: msg`Internet accounts per user`,
tiers: {
- organization: { kind: 'text', text: 'Unlimited' },
- pro: { kind: 'text', text: 'Unlimited' },
+ organization: { kind: 'text', text: msg`Unlimited` },
+ pro: { kind: 'text', text: msg`Unlimited` },
},
type: 'row',
},
{
- featureLabel: 'Folder/Label import',
+ featureLabel: msg`Folder/Label import`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
- pro: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
+ pro: { kind: 'yes', label: msg`Yes` },
},
type: 'row',
},
{
- featureLabel: 'Email sharing',
+ featureLabel: msg`Email sharing`,
tiers: {
- organization: { kind: 'text', text: 'Fully customizable' },
- pro: { kind: 'text', text: 'Fully customizable' },
+ organization: { kind: 'text', text: msg`Fully customizable` },
+ pro: { kind: 'text', text: msg`Fully customizable` },
},
type: 'row',
},
{
- title: 'AI & Automations',
+ title: msg`AI & Automations`,
type: 'category',
},
{
- featureLabel: 'Workflows',
+ featureLabel: msg`Workflows`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
- pro: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
+ pro: { kind: 'yes', label: msg`Yes` },
},
type: 'row',
},
{
- featureLabel: 'AI agents',
+ featureLabel: msg`AI agents`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
- pro: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
+ pro: { kind: 'yes', label: msg`Yes` },
},
type: 'row',
},
{
- featureLabel: 'Custom AI models',
+ featureLabel: msg`Custom AI models`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
pro: { kind: 'dash' },
},
type: 'row',
},
{
- title: 'Security',
+ title: msg`Security`,
type: 'category',
},
{
- featureLabel: 'Two-factor authentication',
+ featureLabel: msg`Two-factor authentication`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
- pro: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
+ pro: { kind: 'yes', label: msg`Yes` },
},
type: 'row',
},
{
- featureLabel: 'User roles',
+ featureLabel: msg`User roles`,
tiers: {
- organization: { kind: 'text', text: 'Unlimited' },
- pro: { kind: 'text', text: 'Unlimited' },
+ organization: { kind: 'text', text: msg`Unlimited` },
+ pro: { kind: 'text', text: msg`Unlimited` },
},
type: 'row',
},
{
- featureLabel: 'Read/Edit/Delete permissions',
+ featureLabel: msg`Read/Edit/Delete permissions`,
tiers: {
- organization: { kind: 'text', text: 'Unlimited' },
- pro: { kind: 'text', text: 'Unlimited' },
+ organization: { kind: 'text', text: msg`Unlimited` },
+ pro: { kind: 'text', text: msg`Unlimited` },
},
type: 'row',
},
{
- featureLabel: 'Field-level permissions',
+ featureLabel: msg`Field-level permissions`,
tiers: {
- organization: { kind: 'text', text: 'Unlimited' },
- pro: { kind: 'text', text: 'Unlimited' },
+ organization: { kind: 'text', text: msg`Unlimited` },
+ pro: { kind: 'text', text: msg`Unlimited` },
},
type: 'row',
},
{
- featureLabel: 'Row-level permissions',
+ featureLabel: msg`Row-level permissions`,
tiers: {
- organization: { kind: 'text', text: 'Unlimited' },
+ organization: { kind: 'text', text: msg`Unlimited` },
pro: { kind: 'dash' },
},
type: 'row',
},
{
- featureLabel: 'SSO',
+ featureLabel: msg`SSO`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
pro: { kind: 'dash' },
},
type: 'row',
},
{
- featureLabel: 'Advanced Encryption',
+ featureLabel: msg`Advanced Encryption`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
pro: { kind: 'dash' },
},
type: 'row',
},
{
- featureLabel: 'Audit logs',
+ featureLabel: msg`Audit logs`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
pro: { kind: 'dash' },
},
type: 'row',
},
{
- featureLabel: 'Environments',
+ featureLabel: msg`Environments`,
tiers: {
- organization: { kind: 'text', text: 'Local, Production' },
- pro: { kind: 'text', text: 'Local, Production' },
+ organization: { kind: 'text', text: msg`Local, Production` },
+ pro: { kind: 'text', text: msg`Local, Production` },
},
type: 'row',
},
{
- featureLabel: 'Impersonate users',
+ featureLabel: msg`Impersonate users`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
- pro: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
+ pro: { kind: 'yes', label: msg`Yes` },
},
type: 'row',
},
{
- title: 'Support',
+ title: msg`Support`,
type: 'category',
},
{
- featureLabel: 'Community',
+ featureLabel: msg`Community`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
- pro: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
+ pro: { kind: 'yes', label: msg`Yes` },
},
type: 'row',
},
{
- featureLabel: 'Help center',
+ featureLabel: msg`Help center`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
- pro: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
+ pro: { kind: 'yes', label: msg`Yes` },
},
type: 'row',
},
{
- featureLabel: 'Email and Chat',
+ featureLabel: msg`Email and Chat`,
selfHostTiers: {
- organization: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
pro: { kind: 'dash' },
},
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
- pro: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
+ pro: { kind: 'yes', label: msg`Yes` },
},
type: 'row',
},
{
- featureLabel: 'Priority support',
+ featureLabel: msg`Priority support`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
pro: { kind: 'dash' },
},
type: 'row',
},
{
- featureLabel: 'Onboarding Packs',
+ featureLabel: msg`Onboarding Packs`,
selfHostTiers: {
- organization: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
pro: { kind: 'dash' },
},
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
- pro: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
+ pro: { kind: 'yes', label: msg`Yes` },
},
type: 'row',
},
{
- featureLabel: 'Implementation partners',
+ featureLabel: msg`Implementation partners`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
pro: { kind: 'dash' },
},
type: 'row',
},
{
- title: 'Customization',
+ title: msg`Customization`,
type: 'category',
},
{
- featureLabel: 'Custom apps',
+ featureLabel: msg`Custom apps`,
tiers: {
- organization: { kind: 'text', text: 'Unlimited' },
- pro: { kind: 'text', text: 'Unlimited' },
+ organization: { kind: 'text', text: msg`Unlimited` },
+ pro: { kind: 'text', text: msg`Unlimited` },
},
type: 'row',
},
{
appliesTo: 'cloud',
- featureLabel: 'Subdomain (yourco.twenty.com)',
+ featureLabel: msg`Subdomain (yourco.twenty.com)`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
- pro: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
+ pro: { kind: 'yes', label: msg`Yes` },
},
type: 'row',
},
{
appliesTo: 'cloud',
- featureLabel: 'Custom domain (crm.yourco.com)',
+ featureLabel: msg`Custom domain (crm.yourco.com)`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
- pro: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
+ pro: { kind: 'yes', label: msg`Yes` },
},
type: 'row',
},
{
- title: 'Developers',
+ title: msg`Developers`,
type: 'category',
},
{
- featureLabel: 'REST & GraphQL API',
+ featureLabel: msg`REST & GraphQL API`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
- pro: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
+ pro: { kind: 'yes', label: msg`Yes` },
},
type: 'row',
},
{
- featureLabel: 'Webhooks',
+ featureLabel: msg`Webhooks`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
- pro: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
+ pro: { kind: 'yes', label: msg`Yes` },
},
type: 'row',
},
{
- featureLabel: 'MCP server',
+ featureLabel: msg`MCP server`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
- pro: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
+ pro: { kind: 'yes', label: msg`Yes` },
},
type: 'row',
},
{
- featureLabel: 'Install shared tarball app',
+ featureLabel: msg`Install shared tarball app`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
pro: { kind: 'dash' },
},
type: 'row',
},
{
appliesTo: 'cloud',
- featureLabel: 'API calls',
+ featureLabel: msg`API calls`,
tiers: {
- organization: { kind: 'text', text: '200 per minute' },
- pro: { kind: 'text', text: '100 per minute' },
+ organization: { kind: 'text', text: msg`200 per minute` },
+ pro: { kind: 'text', text: msg`100 per minute` },
},
type: 'row',
},
{
appliesTo: 'selfHost',
- title: 'Self-hosting',
+ title: msg`Self-hosting`,
type: 'category',
},
{
appliesTo: 'selfHost',
- featureLabel: 'Source code access',
+ featureLabel: msg`Source code access`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
- pro: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
+ pro: { kind: 'yes', label: msg`Yes` },
},
type: 'row',
},
{
appliesTo: 'selfHost',
- featureLabel: 'Commercial license (no AGPL obligations)',
+ featureLabel: msg`Commercial license (no AGPL obligations)`,
tiers: {
- organization: { kind: 'yes', label: 'Yes' },
+ organization: { kind: 'yes', label: msg`Yes` },
pro: { kind: 'dash' },
},
type: 'row',
@@ -404,11 +405,11 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
],
initialVisibleRowCount: 15,
seeMoreFeaturesCta: {
- collapseLabel: 'Show less',
- expandLabel: 'See more features',
+ collapseLabel: msg`Show less`,
+ expandLabel: msg`See more features`,
},
tierColumns: [
- { id: 'pro', label: 'Pro' },
- { id: 'organization', label: 'Organization' },
+ { id: 'pro', label: msg`Pro` },
+ { id: 'organization', label: msg`Organization` },
],
};
diff --git a/packages/twenty-website-new/src/app/[locale]/pricing/salesforce.data.ts b/packages/twenty-website-new/src/app/[locale]/pricing/salesforce.data.ts
index 35c567845c3..0191b7c68fa 100644
--- a/packages/twenty-website-new/src/app/[locale]/pricing/salesforce.data.ts
+++ b/packages/twenty-website-new/src/app/[locale]/pricing/salesforce.data.ts
@@ -1,51 +1,48 @@
+import { msg } from '@lingui/core/macro';
import type { SalesforceDataType } from '@/sections/Salesforce/types';
-const SALESFORCE_POPUP_TITLE = 'Good choice!';
+const SALESFORCE_POPUP_TITLE = msg`Good choice!`;
export const SALESFORCE_DATA: SalesforceDataType = {
body: {
- text: "Some call this enterprise pricing. We prefer a CRM where API access, webhooks, and workflows don't show up as surprise add-ons.",
+ text: msg`Some call this enterprise pricing. We prefer a CRM where API access, webhooks, and workflows don't show up as surprise add-ons.`,
},
- heading: [
- { text: 'Trust the n°1 CRM,', fontFamily: 'serif' },
- { text: ' or not !', fontFamily: 'sans' },
- ],
pricing: {
addons: [
{
cost: 35,
id: 'api-access',
- label: 'API access',
+ label: msg`API access`,
popup: {
- body: 'APIs are extra. Simplicity has a price.',
+ body: msg`APIs are extra. Simplicity has a price.`,
titleBar: SALESFORCE_POPUP_TITLE,
},
- rightLabel: '+$35/user per month',
+ rightLabel: msg`+$35/user per month`,
},
{
cost: 0,
fixedCost: 7000,
id: 'webhooks',
- label: 'Webhooks (Change Data Capture)',
+ label: msg`Webhooks (Change Data Capture)`,
popup: {
- body: 'Real-time changes? That will be a premium surprise.',
+ body: msg`Real-time changes? That will be a premium surprise.`,
titleBar: SALESFORCE_POPUP_TITLE,
},
- rightLabel: '+$7000/org per month',
+ rightLabel: msg`+$7000/org per month`,
},
{
cost: 0,
disabled: true,
id: 'live-updates',
- label: 'Live updates',
+ label: msg`Live updates`,
popup: {
- body: 'Live updates are unavailable, which is almost more honest.',
+ body: msg`Live updates are unavailable, which is almost more honest.`,
titleBar: SALESFORCE_POPUP_TITLE,
},
- rightLabel: 'Unavailable',
+ rightLabel: msg`Unavailable`,
tooltip: {
- title: 'Unavailable',
- body: 'Real-time is a state of mind, not a feature.',
+ title: msg`Unavailable`,
+ body: msg`Real-time is a state of mind, not a feature.`,
},
},
{
@@ -53,71 +50,71 @@ export const SALESFORCE_DATA: SalesforceDataType = {
defaultChecked: true,
disabled: true,
id: 'ui-theme',
- label: 'UI theme',
+ label: msg`UI theme`,
popup: {
- body: 'A retro theme as a paid add-on is somehow the most believable part.',
+ body: msg`A retro theme as a paid add-on is somehow the most believable part.`,
titleBar: SALESFORCE_POPUP_TITLE,
},
- rightLabel: 'Retro 2015',
+ rightLabel: msg`Retro 2015`,
tooltip: {
- title: 'Included!',
- body: 'Better than Liquid Glass!',
+ title: msg`Included!`,
+ body: msg`Better than Liquid Glass!`,
},
},
{
cost: 5,
id: 'sso',
- label: 'SSO',
+ label: msg`SSO`,
popup: {
- body: 'Only $5 for SSO. Practically a charity program.',
+ body: msg`Only $5 for SSO. Practically a charity program.`,
titleBar: SALESFORCE_POPUP_TITLE,
},
- rightLabel: '+$5/user per month',
+ rightLabel: msg`+$5/user per month`,
},
{
cost: 75,
id: 'permissions',
- label: '11 permissions\ngroups',
+ label: msg`11 permissions\ngroups`,
popup: {
- body: 'Experience enterprise-grade granularity, starting with an 11th permission.',
+ body: msg`Experience enterprise-grade granularity, starting with an 11th permission.`,
titleBar: SALESFORCE_POPUP_TITLE,
},
- rightLabel: '+$75/user per month\nSwitch to enterprise!',
+ rightLabel: msg`+$75/user per month\nSwitch to enterprise!`,
sharedCostKey: 'enterprise-plan',
},
{
cost: 105,
id: 'maps',
- label: 'Maps view',
+ label: msg`Maps view`,
popup: {
- body: 'Visualize your customers on a map!',
+ body: msg`Visualize your customers on a map!`,
titleBar: SALESFORCE_POPUP_TITLE,
},
- rightLabel: '+$105/user per month',
+ rightLabel: msg`+$105/user per month`,
},
{
cost: 75,
id: 'workflows',
- label: '6 workflows',
+ label: msg`6 workflows`,
popup: {
- body: 'Start automating at huge scale!',
+ body: msg`Start automating at huge scale!`,
titleBar: SALESFORCE_POPUP_TITLE,
},
- rightLabel: '+$75/user per month\nSwitch to enterprise!',
+ rightLabel: msg`+$75/user per month\nSwitch to enterprise!`,
sharedCostKey: 'enterprise-plan',
},
{
cost: 0,
id: 'lock-in',
- label: 'Lock-in',
+ label: msg`Lock-in`,
popup: {
- body: 'They call it customer loyalty. We call it a very affectionate cage.',
+ body: msg`They call it customer loyalty. We call it a very affectionate cage.`,
titleBar: SALESFORCE_POPUP_TITLE,
},
- rightLabel: '3 2 years contract\n-33% off',
+ rightLabel: msg`3 2 years contract\n-33% off`,
rightLabelParts: [
- [{ strike: true, text: '3' }, { text: ' 2 years contract' }],
- [{ text: '-33% off' }],
+ [{ strike: true, text: msg`3` }, { text: msg`2 years contract` }],
+ [{ text: msg`-33% off` }],
],
},
{
@@ -125,30 +122,30 @@ export const SALESFORCE_DATA: SalesforceDataType = {
defaultChecked: true,
disabled: true,
id: 'apex-tutorials',
- label: 'APEX tutorials',
+ label: msg`APEX tutorials`,
popup: {
- body: 'Even the training material is a feature worth celebrating.',
+ body: msg`Even the training material is a feature worth celebrating.`,
titleBar: SALESFORCE_POPUP_TITLE,
},
- rightLabel: 'Free for you!',
+ rightLabel: msg`Free for you!`,
tooltip: {
- title: 'Included!',
- body: 'Available on YouTube!',
+ title: msg`Included!`,
+ body: msg`Available on YouTube!`,
},
},
{
cost: 0,
disabled: true,
id: 'self-hosting',
- label: 'Self-hosting',
+ label: msg`Self-hosting`,
popup: {
- body: 'Owning your stack remains mysteriously out of stock.',
+ body: msg`Owning your stack remains mysteriously out of stock.`,
titleBar: SALESFORCE_POPUP_TITLE,
},
- rightLabel: 'Out of stock',
+ rightLabel: msg`Out of stock`,
tooltip: {
- title: 'Out of stock',
- body: 'Self-hosting, now for rent!',
+ title: msg`Out of stock`,
+ body: msg`Self-hosting, now for rent!`,
},
},
{
@@ -156,81 +153,79 @@ export const SALESFORCE_DATA: SalesforceDataType = {
defaultChecked: true,
disabled: true,
id: 'salesforce-classic',
- label: 'Salesforce Classic',
+ label: msg`Salesforce Classic`,
popup: {
- body: 'Classic never dies. It just gets extended one more time.',
+ body: msg`Classic never dies. It just gets extended one more time.`,
titleBar: SALESFORCE_POPUP_TITLE,
},
- rightLabel: 'Extended run!',
+ rightLabel: msg`Extended run!`,
tooltip: {
- title: 'Included!',
- body: 'Outlived every redesign since 2004.',
+ title: msg`Included!`,
+ body: msg`Outlived every redesign since 2004.`,
},
},
{
cost: 75,
id: 'flow-orchestration',
- label: 'Flow\norchestration',
+ label: msg`Flow\norchestration`,
popup: {
- body: 'Because true orchestration means putting a dollar sign on every dramatic entrance.',
+ body: msg`Because true orchestration means putting a dollar sign on every dramatic entrance.`,
titleBar: SALESFORCE_POPUP_TITLE,
},
- rightLabel:
- '$1/orchestration run/org\n+$75/user per month\nSwitch to enterprise!',
+ rightLabel: msg`$1/orchestration run/org\n+$75/user per month\nSwitch to enterprise!`,
sharedCostKey: 'enterprise-plan',
},
{
cost: 0,
disabled: true,
id: 'infinite-scroll',
- label: 'Infinite scroll',
+ label: msg`Infinite scroll`,
popup: {
- body: 'Infinite scroll is still coming soon, unlike the invoice.',
+ body: msg`Infinite scroll is still coming soon, unlike the invoice.`,
titleBar: SALESFORCE_POPUP_TITLE,
},
- rightLabel: 'Coming soon!',
+ rightLabel: msg`Coming soon!`,
tooltip: {
- title: 'Coming soon!',
- body: 'Pagination builds character.',
+ title: msg`Coming soon!`,
+ body: msg`Pagination builds character.`,
},
},
{
cost: 75,
id: 'ai-einstein',
- label: 'AI (Einstein)',
+ label: msg`AI (Einstein)`,
popup: {
- body: 'become a genius!',
+ body: msg`become a genius!`,
titleBar: SALESFORCE_POPUP_TITLE,
},
- rightLabel: '+$75/user per month\nSwitch to enterprise!',
+ rightLabel: msg`+$75/user per month\nSwitch to enterprise!`,
sharedCostKey: 'enterprise-plan',
},
{
cost: 75,
id: 'encrypt-data',
- label: 'Encrypt your data',
+ label: msg`Encrypt your data`,
netSpendRate: 0.2,
popup: {
- body: 'Because apparently privacy feels more premium with a surcharge.',
+ body: msg`Because apparently privacy feels more premium with a surcharge.`,
titleBar: SALESFORCE_POPUP_TITLE,
},
- rightLabel:
- '+20% of net spend\n+$75/user per month\nSwitch to enterprise!',
+ rightLabel: msg`+20% of net spend\n+$75/user per month\nSwitch to enterprise!`,
sharedCostKey: 'enterprise-plan',
},
],
basePriceAmount: 100,
- promoTag: '1‑800‑YES‑SOFTWARE',
- featureSectionHeading: 'Add-ons',
+ promoTag: msg`1‑800‑YES‑SOFTWARE`,
+ featureSectionHeading: msg`Add-ons`,
productIconAlt: 'Retro help document icon',
productIconSrc: '/images/pricing/salesforce/help-icon.webp',
- priceSuffix: ' / seat / month - billed yearly',
- productTitle: 'Salesfarce Pro',
- secondaryCtaNote: 'More options available!',
+ priceSuffix: msg`/ seat / month - billed yearly`,
+ productTitle: msg`Salesfarce Pro`,
+ secondaryCtaNote: msg`More options available!`,
secondaryCtaHref:
'https://www.salesforce.com/en-us/wp-content/uploads/sites/4/documents/pricing/all-add-ons.pdf',
- secondaryCtaLabel: 'Check more add-ons',
- totalPriceLabel: 'total per month with fixed cost',
- windowTitle: 'Salesfarce Add-on Center',
+ secondaryCtaLabel: msg`Check more add-ons`,
+ totalPriceLabel: msg`total per month with fixed cost`,
+ windowTitle: msg`Salesfarce Add-on Center`,
},
};
diff --git a/packages/twenty-website-new/src/app/[locale]/product/feature.data.ts b/packages/twenty-website-new/src/app/[locale]/product/feature.data.ts
index 2439a44922c..b75f5077371 100644
--- a/packages/twenty-website-new/src/app/[locale]/product/feature.data.ts
+++ b/packages/twenty-website-new/src/app/[locale]/product/feature.data.ts
@@ -1,86 +1,83 @@
+import { msg } from '@lingui/core/macro';
import type { FeatureDataType } from '@/sections/Feature/types';
export const FEATURE_DATA: FeatureDataType = {
eyebrow: {
heading: {
- text: 'Core Features',
+ text: msg`Core Features`,
fontFamily: 'sans',
},
},
- heading: [
- { text: 'Everything you need,', fontFamily: 'serif' },
- { text: ' out of the box', fontFamily: 'sans' },
- ],
mask: { src: '/images/product/feature/mask.webp', alt: '' },
tiles: [
{
icon: 'check',
image: { src: '/images/product/feature/dashboards.webp', alt: '' },
- heading: { text: 'Reports & Dashboards', fontFamily: 'sans' },
+ heading: { text: msg`Reports & Dashboards`, fontFamily: 'sans' },
bullets: [
- { text: 'Build custom dashboards' },
- { text: 'Aggregate, bar, line, pie, and gauge widgets' },
- { text: 'Filtered metrics from live CRM data' },
+ { text: msg`Build custom dashboards` },
+ { text: msg`Aggregate, bar, line, pie, and gauge widgets` },
+ { text: msg`Filtered metrics from live CRM data` },
],
},
{
icon: 'check',
image: { src: '/images/product/feature/tasks.webp', alt: '' },
- heading: { text: 'Tasks & Activities', fontFamily: 'sans' },
+ heading: { text: msg`Tasks & Activities`, fontFamily: 'sans' },
bullets: [
- { text: 'Create tasks from records' },
- { text: 'Assign owners and due dates' },
- { text: 'Rich notes attached to records' },
+ { text: msg`Create tasks from records` },
+ { text: msg`Assign owners and due dates` },
+ { text: msg`Rich notes attached to records` },
],
},
{
icon: 'check',
image: { src: '/images/product/feature/emails.webp', alt: '' },
- heading: { text: 'Email & Calendar', fontFamily: 'sans' },
+ heading: { text: msg`Email & Calendar`, fontFamily: 'sans' },
bullets: [
- { text: 'Connect Google or Microsoft accounts' },
- { text: 'Emails and events linked to CRM records' },
- { text: 'Full communication history in one place' },
+ { text: msg`Connect Google or Microsoft accounts` },
+ { text: msg`Emails and events linked to CRM records` },
+ { text: msg`Full communication history in one place` },
],
},
{
icon: 'check',
image: { src: '/images/product/feature/contacts.webp', alt: '' },
- heading: { text: 'Contacts & Companies', fontFamily: 'sans' },
+ heading: { text: msg`Contacts & Companies`, fontFamily: 'sans' },
bullets: [
- { text: 'Custom fields and relationships' },
- { text: 'Unified timeline (emails, events, tasks, notes, files)' },
- { text: 'Email/calendar activity on each record' },
+ { text: msg`Custom fields and relationships` },
+ { text: msg`Unified timeline (emails, events, tasks, notes, files)` },
+ { text: msg`Email/calendar activity on each record` },
],
},
{
icon: 'check',
image: { src: '/images/product/feature/pipeline.webp', alt: '' },
- heading: { text: 'Pipeline Management', fontFamily: 'sans' },
+ heading: { text: msg`Pipeline Management`, fontFamily: 'sans' },
bullets: [
- { text: 'Custom deal stages for your process' },
- { text: 'Drag-and-drop deals between stages' },
- { text: 'Track amount and close date' },
+ { text: msg`Custom deal stages for your process` },
+ { text: msg`Drag-and-drop deals between stages` },
+ { text: msg`Track amount and close date` },
],
},
{
icon: 'check',
image: { src: '/images/product/feature/files.webp', alt: '' },
- heading: { text: 'Files', fontFamily: 'sans' },
+ heading: { text: msg`Files`, fontFamily: 'sans' },
bullets: [
- { text: 'Multi-file upload on records' },
- { text: 'Rename, download, and delete attachments' },
- { text: 'In-app preview for supported file types (when enabled)' },
+ { text: msg`Multi-file upload on records` },
+ { text: msg`Rename, download, and delete attachments` },
+ { text: msg`In-app preview for supported file types (when enabled)` },
],
},
{
icon: 'check',
image: { src: '/images/product/feature/data.webp', alt: '' },
- heading: { text: 'Data import', fontFamily: 'sans' },
+ heading: { text: msg`Data import`, fontFamily: 'sans' },
bullets: [
- { text: 'CSV import flow' },
- { text: 'Column-to-field mapping (including relations)' },
- { text: 'CSV export anytime' },
+ { text: msg`CSV import flow` },
+ { text: msg`Column-to-field mapping (including relations)` },
+ { text: msg`CSV export anytime` },
],
},
],
diff --git a/packages/twenty-website-new/src/app/[locale]/product/hero.data.ts b/packages/twenty-website-new/src/app/[locale]/product/hero.data.ts
index 6fdd3768e0c..1c2eeedcd17 100644
--- a/packages/twenty-website-new/src/app/[locale]/product/hero.data.ts
+++ b/packages/twenty-website-new/src/app/[locale]/product/hero.data.ts
@@ -1,12 +1,5 @@
-import type { HeroBaseDataType } from '@/sections/Hero/types';
+import { msg } from '@lingui/core/macro';
-export const HERO_DATA = {
- heading: [
- { text: 'A CRM for teams', fontFamily: 'serif' },
- { text: 'that ', fontFamily: 'serif', newLine: true },
- { text: 'moves fast', fontFamily: 'sans' },
- ],
- body: {
- text: 'Track relationships, manage pipelines, and take action quickly with a CRM that feels intuitive from day one.',
- },
-} satisfies HeroBaseDataType;
+export const HERO_COPY = {
+ body: msg`Track relationships, manage pipelines, and take action quickly with a CRM that feels intuitive from day one.`,
+};
diff --git a/packages/twenty-website-new/src/app/[locale]/product/page.tsx b/packages/twenty-website-new/src/app/[locale]/product/page.tsx
index 343d7619a9f..864850bf81f 100644
--- a/packages/twenty-website-new/src/app/[locale]/product/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/product/page.tsx
@@ -1,15 +1,27 @@
+import { msg } from '@lingui/core/macro';
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/[locale]/product/feature.data';
-import { HERO_DATA } from '@/app/[locale]/product/hero.data';
-import { SIGNOFF_DATA } from '@/app/[locale]/product/signoff.data';
+import { HERO_COPY } from '@/app/[locale]/product/hero.data';
+import { SIGNOFF_COPY } 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 {
+ Body,
+ Eyebrow,
+ Heading,
+ HeadingPart,
+ LinkButton,
+} from '@/design-system/components';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
+import { createMessageDescriptorRenderer } from '@/lib/i18n/create-message-descriptor-renderer';
+import {
+ getRouteI18n,
+ type LocaleRouteParams,
+} from '@/lib/i18n/get-route-i18n';
+import { Pages } from '@/lib/pages';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { Faq } from '@/sections/Faq/components';
import { Feature } from '@/sections/Feature/components';
@@ -24,8 +36,16 @@ import { buildRouteMetadata } from '@/lib/seo';
export const generateMetadata = buildRouteMetadata('product');
-export default async function ProductPage() {
- const stats = await fetchCommunityStats();
+type ProductPageProps = {
+ params: Promise;
+};
+
+export default async function ProductPage({ params }: ProductPageProps) {
+ const [i18n, stats] = await Promise.all([
+ getRouteI18n(params),
+ fetchCommunityStats(),
+ ]);
+ const renderText = createMessageDescriptorRenderer(i18n);
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
return (
@@ -53,14 +73,28 @@ export default async function ProductPage() {
-
-
+
+
+ {renderText(msg`A CRM for teams`)}
+
+
+
+ {renderText(msg`that`)}
+ {' '}
+
+ {renderText(msg`moves fast`)}
+
+
+
@@ -68,9 +102,15 @@ export default async function ProductPage() {
-
+
-
+
@@ -78,10 +118,22 @@ export default async function ProductPage() {
-
+
+
+ {renderText(msg`Everything you need,`)}
+ {' '}
+
+ {renderText(msg`out of the box`)}
+
+
-
+
@@ -89,13 +141,23 @@ export default async function ProductPage() {
-
-
+
+
+ {renderText(msg`A modern CRM with`)}
+ {' '}
+
+ {renderText(msg`an intuitive interface`)}
+
+
+ {THREE_CARDS_ILLUSTRATION_DATA.body && (
+
+ )}
+ >
+
+ {renderText(msg`Go the extra mile`)}
+ {' '}
+
+ {renderText(msg`with no-code`)}
+
+
-
-
+
+
+ {renderText(msg`Ready to grow`)}
+
+
+
+ {renderText(msg`with`)}
+ {' '}
+
+ {renderText(msg`Twenty?`)}
+
+
+
@@ -134,19 +216,30 @@ export default async function ProductPage() {
-
-
+
+
+
+ {renderText(msg`Stop fighting custom.`)}
+
+
+
+ {renderText(msg`Start building, with Twenty`)}
+
+
diff --git a/packages/twenty-website-new/src/app/[locale]/product/signoff.data.ts b/packages/twenty-website-new/src/app/[locale]/product/signoff.data.ts
index f670876e9a7..390f828f570 100644
--- a/packages/twenty-website-new/src/app/[locale]/product/signoff.data.ts
+++ b/packages/twenty-website-new/src/app/[locale]/product/signoff.data.ts
@@ -1,12 +1,5 @@
-import type { SignoffDataType } from '@/sections/Signoff/types';
+import { msg } from '@lingui/core/macro';
-export const SIGNOFF_DATA: SignoffDataType = {
- heading: [
- { text: 'Ready to grow', fontFamily: 'serif' },
- { text: 'with ', fontFamily: 'serif', newLine: true },
- { text: 'Twenty?', fontFamily: 'sans' },
- ],
- body: {
- text: 'Join the teams that chose to own their CRM. Start building with Twenty today.',
- },
+export const SIGNOFF_COPY = {
+ body: msg`Join the teams that chose to own their CRM. Start building with Twenty today.`,
};
diff --git a/packages/twenty-website-new/src/app/[locale]/product/stepper.data.ts b/packages/twenty-website-new/src/app/[locale]/product/stepper.data.ts
index 048e33f6ad7..cf282a3c2d0 100644
--- a/packages/twenty-website-new/src/app/[locale]/product/stepper.data.ts
+++ b/packages/twenty-website-new/src/app/[locale]/product/stepper.data.ts
@@ -1,34 +1,25 @@
+import { msg } from '@lingui/core/macro';
import type { ProductStepperDataType } from '@/sections/ProductStepper/types';
export const STEPPER_DATA: ProductStepperDataType = {
eyebrow: {
heading: {
- text: 'Customization',
+ text: msg`Customization`,
fontFamily: 'sans',
},
},
- heading: [
- {
- text: 'Go the extra mile',
- fontFamily: 'serif',
- },
- {
- text: ' with no-code',
- fontFamily: 'sans',
- },
- ],
body: {
- text: 'Need a quick change? Skip the engineering ticket. Customize your workspace in minutes.',
+ text: msg`Need a quick change? Skip the engineering ticket. Customize your workspace in minutes.`,
},
steps: [
{
icon: 'users',
heading: {
- text: 'Data model',
+ text: msg`Data model`,
fontFamily: 'sans',
},
body: {
- text: 'Add objects and fields',
+ text: msg`Add objects and fields`,
},
image: {
src: '/images/product/stepper/step-one.webp',
@@ -38,11 +29,11 @@ export const STEPPER_DATA: ProductStepperDataType = {
{
icon: 'check',
heading: {
- text: 'Automation',
+ text: msg`Automation`,
fontFamily: 'sans',
},
body: {
- text: 'Create a workflow',
+ text: msg`Create a workflow`,
},
image: {
src: '/images/product/stepper/step-two.webp',
@@ -52,11 +43,11 @@ export const STEPPER_DATA: ProductStepperDataType = {
{
icon: 'eye',
heading: {
- text: 'Layout',
+ text: msg`Layout`,
fontFamily: 'sans',
},
body: {
- text: 'Tailor record pages, menus, and views',
+ text: msg`Tailor record pages, menus, and views`,
},
image: {
src: '/images/product/stepper/step-three.webp',
diff --git a/packages/twenty-website-new/src/app/[locale]/product/three-cards.data.ts b/packages/twenty-website-new/src/app/[locale]/product/three-cards.data.ts
index 046e0922a3f..d27dbf21147 100644
--- a/packages/twenty-website-new/src/app/[locale]/product/three-cards.data.ts
+++ b/packages/twenty-website-new/src/app/[locale]/product/three-cards.data.ts
@@ -1,37 +1,33 @@
+import { msg } from '@lingui/core/macro';
import type { ThreeCardsIllustrationDataType } from '@/sections/ThreeCards/types';
export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
eyebrow: {
- heading: { text: 'Stop settling for trade-offs.', fontFamily: 'sans' },
+ heading: { text: msg`Stop settling for trade-offs.`, fontFamily: 'sans' },
},
- heading: [
- { text: 'A modern CRM with ', fontFamily: 'serif' },
- { text: 'an intuitive interface', fontFamily: 'sans' },
- ],
- body: { text: '' },
illustrationCards: [
{
- heading: { text: 'Built for speed', fontFamily: 'sans' },
+ heading: { text: msg`Built for speed`, fontFamily: 'sans' },
body: {
- text: 'Fly through your workspace with shortcuts and short load times.',
+ text: msg`Fly through your workspace with shortcuts and short load times.`,
},
benefits: undefined,
attribution: undefined,
illustration: 'speed',
},
{
- heading: { text: 'Real-time data', fontFamily: 'sans' },
+ heading: { text: msg`Real-time data`, fontFamily: 'sans' },
body: {
- text: 'See updates as they happen. Work with your team and agents seamlessly.',
+ text: msg`See updates as they happen. Work with your team and agents seamlessly.`,
},
benefits: undefined,
attribution: undefined,
illustration: 'eye',
},
{
- heading: { text: 'Stay in Flow', fontFamily: 'sans' },
+ heading: { text: msg`Stay in Flow`, fontFamily: 'sans' },
body: {
- text: 'AI chat, settings, and records in a side panels for fast, single-screen access.',
+ text: msg`AI chat, settings, and records in a side panels for fast, single-screen access.`,
},
benefits: undefined,
attribution: undefined,
diff --git a/packages/twenty-website-new/src/app/[locale]/releases/hero.data.ts b/packages/twenty-website-new/src/app/[locale]/releases/hero.data.ts
index 3072c831133..a10c4231f4f 100644
--- a/packages/twenty-website-new/src/app/[locale]/releases/hero.data.ts
+++ b/packages/twenty-website-new/src/app/[locale]/releases/hero.data.ts
@@ -1,11 +1,5 @@
-import type { BodyType } from '@/design-system/components/Body';
-import type { HeadingType } from '@/design-system/components/Heading';
+import { msg } from '@lingui/core/macro';
-export const RELEASE_NOTES_HERO_HEADING: HeadingType[] = [
- { fontFamily: 'serif', text: 'Latest ' },
- { fontFamily: 'sans', text: 'Releases', newLine: true },
-];
-
-export const RELEASE_NOTES_HERO_BODY: BodyType = {
- text: 'Discover the newest features and improvements in Twenty,\nthe #1 open source CRM.',
+export const RELEASE_NOTES_HERO_COPY = {
+ body: msg`Discover the newest features and improvements in Twenty,\nthe #1 open source CRM.`,
};
diff --git a/packages/twenty-website-new/src/app/[locale]/releases/page.tsx b/packages/twenty-website-new/src/app/[locale]/releases/page.tsx
index 92481719561..ea0adf2d71c 100644
--- a/packages/twenty-website-new/src/app/[locale]/releases/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/releases/page.tsx
@@ -1,12 +1,15 @@
+import { msg } from '@lingui/core/macro';
import { MENU_DATA } from '@/sections/Menu/data';
-import {
- RELEASE_NOTES_HERO_BODY,
- RELEASE_NOTES_HERO_HEADING,
-} from '@/app/[locale]/releases/hero.data';
-import { LinkButton } from '@/design-system/components';
-import { Pages } from '@/lib/pages';
+import { RELEASE_NOTES_HERO_COPY } from '@/app/[locale]/releases/hero.data';
+import { HeadingPart, LinkButton } from '@/design-system/components';
import { GitHubIcon } from '@/icons';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
+import { createMessageDescriptorRenderer } from '@/lib/i18n/create-message-descriptor-renderer';
+import {
+ getRouteI18n,
+ type LocaleRouteParams,
+} from '@/lib/i18n/get-route-i18n';
+import { Pages } from '@/lib/pages';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { fetchLatestGithubReleaseTag } from '@/lib/releases/fetch-latest-release-tag';
import { getVisibleReleaseNotes } from '@/lib/releases/get-visible-releases';
@@ -20,12 +23,18 @@ import { Fragment } from 'react';
export const generateMetadata = buildRouteMetadata('releases');
-export default async function ReleasesPage() {
+type ReleasesPageProps = {
+ params: Promise;
+};
+
+export default async function ReleasesPage({ params }: ReleasesPageProps) {
const allNotes = loadLocalReleaseNotes();
- const [latestTag, stats] = await Promise.all([
+ const [i18n, latestTag, stats] = await Promise.all([
+ getRouteI18n(params),
fetchLatestGithubReleaseTag(),
fetchCommunityStats(),
]);
+ const renderText = createMessageDescriptorRenderer(i18n);
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
const visibleNotes =
process.env.NODE_ENV === 'development'
@@ -56,24 +65,27 @@ export default async function ReleasesPage() {
-
+
+
+ {renderText(msg`Latest`)}
+
+
+
+ {renderText(msg`Releases`)}
+
+
}
- type="anchor"
variant="outlined"
/>
diff --git a/packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-four.data.ts b/packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-four.data.ts
index 2de905bad41..b0fd2a50c02 100644
--- a/packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-four.data.ts
+++ b/packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-four.data.ts
@@ -1,28 +1,19 @@
+import { msg } from '@lingui/core/macro';
import type { EditorialDataType } from '@/sections/Editorial/types/EditorialData';
export const EDITORIAL_FOUR: EditorialDataType = {
eyebrow: {
heading: {
fontFamily: 'sans',
- text: 'What this means',
+ text: msg`What this means`,
},
},
- heading: [
- {
- fontFamily: 'serif',
- text: 'Differentiation now ',
- },
- {
- fontFamily: 'sans',
- text: 'lives in the code you own.',
- },
- ],
body: [
{
- text: "You don't buy your deployment pipeline off the shelf. You don't rent your data warehouse from a vendor who decides the schema. You build it, you own it, you iterate on it every week. CRM is going the same way. The teams that treat it as infrastructure they own will compound an advantage every quarter.",
+ text: msg`You don't buy your deployment pipeline off the shelf. You don't rent your data warehouse from a vendor who decides the schema. You build it, you own it, you iterate on it every week. CRM is going the same way. The teams that treat it as infrastructure they own will compound an advantage every quarter.`,
},
{
- text: 'Tuesday your team learns that deals with a technical champion close 3x faster. Wednesday you add the field, wire up the scoring, adjust the workflow. By Thursday your agents are acting on it. That feedback loop is the edge. And it only works if the CRM is yours.',
+ text: msg`Tuesday your team learns that deals with a technical champion close 3x faster. Wednesday you add the field, wire up the scoring, adjust the workflow. By Thursday your agents are acting on it. That feedback loop is the edge. And it only works if the CRM is yours.`,
},
],
};
diff --git a/packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-one.data.ts b/packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-one.data.ts
index d7c6276597f..b4f2d4b5b2c 100644
--- a/packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-one.data.ts
+++ b/packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-one.data.ts
@@ -1,22 +1,19 @@
+import { msg } from '@lingui/core/macro';
import type { EditorialDataType } from '@/sections/Editorial/types/EditorialData';
export const EDITORIAL_ONE: EditorialDataType = {
eyebrow: {
heading: {
fontFamily: 'sans',
- text: 'The shift',
+ text: msg`The shift`,
},
},
- heading: [
- { fontFamily: 'serif', text: 'CRM was a ledger.' },
- { fontFamily: 'sans', text: ' AI turned it into an operating system.' },
- ],
body: [
{
- text: "For twenty years, CRM meant the same thing: a place to log calls, track deals, and pull reports on Friday. The real work happened in people's heads, in Slack threads, in hallway conversations. The CRM kept score. Nobody expected more from it.",
+ text: msg`For twenty years, CRM meant the same thing: a place to log calls, track deals, and pull reports on Friday. The real work happened in people's heads, in Slack threads, in hallway conversations. The CRM kept score. Nobody expected more from it.`,
},
{
- text: 'AI agents are starting to draft outreach, score leads, research accounts, write follow-ups, update deal stages. Every one of these actions reads from and writes to the CRM. The scoreboard became the playbook. The database became the brain.',
+ text: msg`AI agents are starting to draft outreach, score leads, research accounts, write follow-ups, update deal stages. Every one of these actions reads from and writes to the CRM. The scoreboard became the playbook. The database became the brain.`,
},
],
};
diff --git a/packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-three.data.ts b/packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-three.data.ts
index 3f7a1917edd..7d287dd5201 100644
--- a/packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-three.data.ts
+++ b/packages/twenty-website-new/src/app/[locale]/why-twenty/editorial-three.data.ts
@@ -1,25 +1,16 @@
+import { msg } from '@lingui/core/macro';
import type { EditorialDataType } from '@/sections/Editorial/types/EditorialData';
export const EDITORIAL_THREE: EditorialDataType = {
eyebrow: {
- heading: { fontFamily: 'sans', text: 'The opportunity' },
+ heading: { fontFamily: 'sans', text: msg`The opportunity` },
},
- heading: [
- {
- fontFamily: 'serif',
- text: 'Build it in an afternoon.',
- },
- {
- fontFamily: 'sans',
- text: ' AI made the gap that small.',
- },
- ],
body: [
{
- text: 'A year ago, customizing your CRM meant hiring a Salesforce consultant, learning Apex, waiting months. The gap between "I want this" and "it\'s live" was measured in quarters and invoices. So people settled. They bent their process to fit the tool and called it adoption.',
+ text: msg`A year ago, customizing your CRM meant hiring a Salesforce consultant, learning Apex, waiting months. The gap between "I want this" and "it\'s live" was measured in quarters and invoices. So people settled. They bent their process to fit the tool and called it adoption.`,
},
{
- text: "Now a developer can describe what they want to Claude Code and have a working app in an afternoon. A custom object, a scoring workflow, a new view, an integration. The bottleneck isn't building anymore. It's whether your platform lets you.",
+ text: msg`Now a developer can describe what they want to Claude Code and have a working app in an afternoon. A custom object, a scoring workflow, a new view, an integration. The bottleneck isn't building anymore. It's whether your platform lets you.`,
},
],
};
diff --git a/packages/twenty-website-new/src/app/[locale]/why-twenty/hero.data.ts b/packages/twenty-website-new/src/app/[locale]/why-twenty/hero.data.ts
index fbdda1834c1..4ca1f72b78b 100644
--- a/packages/twenty-website-new/src/app/[locale]/why-twenty/hero.data.ts
+++ b/packages/twenty-website-new/src/app/[locale]/why-twenty/hero.data.ts
@@ -1,14 +1,5 @@
-import type { HeroWhyTwentyDataType } from '@/sections/Hero/types';
+import { msg } from '@lingui/core/macro';
-export const HERO_DATA: HeroWhyTwentyDataType = {
- heading: [
- { text: 'The future of CRM is built,', fontFamily: 'serif' },
- { text: ' not bought.', fontFamily: 'sans' },
- ],
- body: {
- text:
- 'CRM was a database you filled on Fridays. ' +
- 'AI turned it into the system that runs your go-to-market. ' +
- "To differentiate, you have to build what your competitors can't buy.",
- },
+export const HERO_COPY = {
+ body: msg`CRM was a database you filled on Fridays. AI turned it into the system that runs your go-to-market. To differentiate, you have to build what your competitors can't buy.`,
};
diff --git a/packages/twenty-website-new/src/app/[locale]/why-twenty/marquee.data.ts b/packages/twenty-website-new/src/app/[locale]/why-twenty/marquee.data.ts
index f02ac704fcf..4f837794903 100644
--- a/packages/twenty-website-new/src/app/[locale]/why-twenty/marquee.data.ts
+++ b/packages/twenty-website-new/src/app/[locale]/why-twenty/marquee.data.ts
@@ -1,9 +1,10 @@
+import { msg } from '@lingui/core/macro';
import type { MarqueeDataType } from '@/sections/Marquee/types';
export const MARQUEE_DATA: MarqueeDataType = {
heading: [
- { fontFamily: 'serif', text: 'Same CRM' },
- { fontFamily: 'sans', text: 'Same output' },
- { fontFamily: 'serif', text: 'Same results' },
+ { fontFamily: 'serif', text: msg`Same CRM` },
+ { fontFamily: 'sans', text: msg`Same output` },
+ { fontFamily: 'serif', text: msg`Same results` },
],
};
diff --git a/packages/twenty-website-new/src/app/[locale]/why-twenty/page.tsx b/packages/twenty-website-new/src/app/[locale]/why-twenty/page.tsx
index 069a3e00e65..e46aaa4cd82 100644
--- a/packages/twenty-website-new/src/app/[locale]/why-twenty/page.tsx
+++ b/packages/twenty-website-new/src/app/[locale]/why-twenty/page.tsx
@@ -1,13 +1,19 @@
+import { msg } from '@lingui/core/macro';
import { MENU_DATA } from '@/sections/Menu/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 { HERO_COPY } 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 { SIGNOFF_COPY } from '@/app/[locale]/why-twenty/signoff.data';
+import { HeadingPart, LinkButton } from '@/design-system/components';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
+import { createMessageDescriptorRenderer } from '@/lib/i18n/create-message-descriptor-renderer';
+import {
+ getRouteI18n,
+ type LocaleRouteParams,
+} from '@/lib/i18n/get-route-i18n';
+import { Pages } from '@/lib/pages';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { Editorial } from '@/sections/Editorial/components';
import { Hero } from '@/sections/Hero/components';
@@ -59,8 +65,16 @@ const sectionCrosshairRight = {
export const generateMetadata = buildRouteMetadata('whyTwenty');
-export default async function WhyTwentyPage() {
- const stats = await fetchCommunityStats();
+type WhyTwentyPageProps = {
+ params: Promise;
+};
+
+export default async function WhyTwentyPage({ params }: WhyTwentyPageProps) {
+ const [i18n, stats] = await Promise.all([
+ getRouteI18n(params),
+ fetchCommunityStats(),
+ ]);
+ const renderText = createMessageDescriptorRenderer(i18n);
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
return (
@@ -91,12 +105,19 @@ export default async function WhyTwentyPage() {
backgroundColor={theme.colors.secondary.background[100]}
colorScheme="secondary"
>
-
+
+ {renderText(msg`The future of CRM is built,`)}
+ {' '}
+
+ {renderText(msg`not bought.`)}
+
+
+
-
@@ -109,13 +130,22 @@ export default async function WhyTwentyPage() {
-
+
+
+ {renderText(msg`CRM was a ledger.`)}
+ {' '}
+
+ {renderText(msg`AI turned it into an operating system.`)}
+
+
@@ -128,6 +158,7 @@ export default async function WhyTwentyPage() {
body={EDITORIAL_TWO.body}
color={theme.colors.secondary.text[60]}
layout="centered"
+ renderText={renderText}
/>
*/}
@@ -141,13 +172,22 @@ export default async function WhyTwentyPage() {
-
+
+
+ {renderText(msg`Differentiation now`)}
+ {' '}
+
+ {renderText(msg`lives in the code you own.`)}
+
+
@@ -160,19 +200,29 @@ export default async function WhyTwentyPage() {
-
+
+
+ {renderText(msg`Build it in an afternoon.`)}
+ {' '}
+
+ {renderText(msg`AI made the gap that small.`)}
+
+
@@ -182,17 +232,24 @@ export default async function WhyTwentyPage() {
color={theme.colors.secondary.text[100]}
page={Pages.WhyTwenty}
>
-
+
+ {renderText(msg`Build a CRM your competitors`)}
+ {' '}
+
+ {renderText(msg`can't buy.`)}
+
+
+
-
diff --git a/packages/twenty-website-new/src/app/[locale]/why-twenty/signoff.data.ts b/packages/twenty-website-new/src/app/[locale]/why-twenty/signoff.data.ts
index 052e68e7bc2..7f8efcb4c94 100644
--- a/packages/twenty-website-new/src/app/[locale]/why-twenty/signoff.data.ts
+++ b/packages/twenty-website-new/src/app/[locale]/why-twenty/signoff.data.ts
@@ -1,11 +1,5 @@
-import type { SignoffDataType } from '@/sections/Signoff/types';
+import { msg } from '@lingui/core/macro';
-export const SIGNOFF_DATA: SignoffDataType = {
- heading: [
- { text: 'Build a CRM your competitors ', fontFamily: 'serif' },
- { text: "can't buy.", fontFamily: 'sans' },
- ],
- body: {
- text: 'Open-source, AI-ready, and yours to shape.',
- },
+export const SIGNOFF_COPY = {
+ body: msg`Open-source, AI-ready, and yours to shape.`,
};
diff --git a/packages/twenty-website-new/src/app/__tests__/sitemap.test.ts b/packages/twenty-website-new/src/app/__tests__/sitemap.test.ts
index 9ef4b1ab2cd..168b767c370 100644
--- a/packages/twenty-website-new/src/app/__tests__/sitemap.test.ts
+++ b/packages/twenty-website-new/src/app/__tests__/sitemap.test.ts
@@ -9,8 +9,9 @@ describe('sitemap', () => {
expect(pathnames).toContain('/');
expect(pathnames).toContain('/product');
+ expect(pathnames).toContain('/fr-FR/product');
expect(pathnames).toContain('/customers/9dots');
- expect(pathnames).not.toContain('/fr-FR/product');
+ expect(pathnames).not.toContain('/de-DE/product');
expect(pathnames).not.toContain('/halftone');
expect(pathnames).not.toContain('/enterprise/activate');
});
@@ -22,8 +23,9 @@ describe('sitemap', () => {
expect(productEntry?.alternates?.languages).toMatchObject({
en: expect.stringMatching(/\/product$/),
+ 'fr-FR': expect.stringMatching(/\/fr-FR\/product$/),
'x-default': expect.stringMatching(/\/product$/),
});
- expect(productEntry?.alternates?.languages).not.toHaveProperty('fr-FR');
+ expect(productEntry?.alternates?.languages).not.toHaveProperty('de-DE');
});
});
diff --git a/packages/twenty-website-new/src/design-system/components/Body.tsx b/packages/twenty-website-new/src/design-system/components/Body.tsx
index 8c9d1e2d009..110fae5208e 100644
--- a/packages/twenty-website-new/src/design-system/components/Body.tsx
+++ b/packages/twenty-website-new/src/design-system/components/Body.tsx
@@ -1,8 +1,9 @@
import { theme } from '@/theme';
import { css } from '@linaria/core';
+import type { ReactNode } from 'react';
-export type BodyType = {
- text: string;
+export type BodyType = {
+ text: TText;
};
const bodyClassName = css`
@@ -80,26 +81,33 @@ export type BodyWeight = 'light' | 'regular' | 'medium';
export type BodySize = 'md' | 'sm' | 'xs';
export type BodyVariant = 'default' | 'body-paragraph';
-export type BodyProps = {
+type BodyTextRenderer = [TText] extends [ReactNode]
+ ? { renderText?: (text: TText) => ReactNode }
+ : { renderText: (text: TText) => ReactNode };
+
+export type BodyProps = {
as?: BodyAs;
- body: BodyType;
+ body: BodyType;
family?: BodyFamily;
weight?: BodyWeight;
size?: BodySize;
variant?: BodyVariant;
className?: string;
-};
+} & BodyTextRenderer;
-export function Body({
+export function Body({
as: Tag = 'p',
body,
family = 'sans',
+ renderText,
weight = 'regular',
size = 'md',
variant = 'default',
className,
-}: BodyProps) {
+}: BodyProps) {
const rootClassName = [bodyClassName, className].filter(Boolean).join(' ');
+ const content =
+ renderText === undefined ? (body.text as ReactNode) : renderText(body.text);
return (
- {body.text}
+ {content}
);
}
diff --git a/packages/twenty-website-new/src/design-system/components/Button/BaseButton.tsx b/packages/twenty-website-new/src/design-system/components/Button/BaseButton.tsx
index 63788e21c64..9b399a1363a 100644
--- a/packages/twenty-website-new/src/design-system/components/Button/BaseButton.tsx
+++ b/packages/twenty-website-new/src/design-system/components/Button/BaseButton.tsx
@@ -92,7 +92,7 @@ const Label = styled.span`
export type BaseButtonProps = {
color: 'primary' | 'secondary';
- label: string;
+ label: ReactNode;
leadingIcon?: ReactNode;
size?: ButtonSize;
variant: 'contained' | 'outlined';
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 66690cc0352..b63e33d8fe6 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,5 +1,4 @@
import type { LinkButtonType } from '@/design-system/components/Button/types/LinkButtonType';
-import { LocalizedLink } from '@/lib/i18n';
import { styled } from '@linaria/react';
import {
BaseButton,
@@ -11,14 +10,7 @@ const StyledButtonAnchor = styled.a`
${buttonBaseStyles}
`;
-const StyledButtonLink = styled(LocalizedLink)`
- ${buttonBaseStyles}
-`;
-
-export type LinkButtonPresentation = 'anchor' | 'link';
-
-export type LinkButtonProps = Omit &
- LinkButtonType & { type: LinkButtonPresentation };
+export type LinkButtonProps = Omit & LinkButtonType;
export function LinkButton({
color,
@@ -26,7 +18,6 @@ export function LinkButton({
label,
leadingIcon,
size = 'regular',
- type,
variant,
}: LinkButtonProps) {
const inner = (
@@ -39,29 +30,16 @@ export function LinkButton({
/>
);
- if (type === 'anchor') {
- return (
-
- {inner}
-
- );
- }
-
return (
-
{inner}
-
+
);
}
diff --git a/packages/twenty-website-new/src/design-system/components/Button/types/LinkButtonType.ts b/packages/twenty-website-new/src/design-system/components/Button/types/LinkButtonType.ts
index e2d671cf605..1c2a409cd91 100644
--- a/packages/twenty-website-new/src/design-system/components/Button/types/LinkButtonType.ts
+++ b/packages/twenty-website-new/src/design-system/components/Button/types/LinkButtonType.ts
@@ -1 +1,3 @@
-export type LinkButtonType = { href: string; label: string };
+import type { ReactNode } from 'react';
+
+export type LinkButtonType = { href: string; label: ReactNode };
diff --git a/packages/twenty-website-new/src/design-system/components/Button/types/SubmitButtonType.ts b/packages/twenty-website-new/src/design-system/components/Button/types/SubmitButtonType.ts
index 4e98c8ecc2e..610819aeaff 100644
--- a/packages/twenty-website-new/src/design-system/components/Button/types/SubmitButtonType.ts
+++ b/packages/twenty-website-new/src/design-system/components/Button/types/SubmitButtonType.ts
@@ -1 +1,3 @@
-export type SubmitButtonType = { label: string };
+import type { ReactNode } from 'react';
+
+export type SubmitButtonType = { label: ReactNode };
diff --git a/packages/twenty-website-new/src/design-system/components/Eyebrow.tsx b/packages/twenty-website-new/src/design-system/components/Eyebrow.tsx
index 64e8dfbf902..a6672378415 100644
--- a/packages/twenty-website-new/src/design-system/components/Eyebrow.tsx
+++ b/packages/twenty-website-new/src/design-system/components/Eyebrow.tsx
@@ -1,10 +1,11 @@
import { Heading, type HeadingType } from '@/design-system/components/Heading';
import { RectangleFillIcon } from '@/icons';
-
-export type EyebrowType = { heading: HeadingType };
import { theme } from '@/theme';
import { css } from '@linaria/core';
import { styled } from '@linaria/react';
+import type { ReactNode } from 'react';
+
+export type EyebrowType = { heading: HeadingType };
const EyebrowRow = styled.div`
align-items: center;
@@ -37,22 +38,31 @@ const eyebrowLabelClassName = css`
}
`;
-type EyebrowProps = {
- heading: HeadingType;
+type EyebrowTextRenderer = [TText] extends [ReactNode]
+ ? { renderText?: (text: TText) => ReactNode }
+ : { renderText: (text: TText) => ReactNode };
+
+type EyebrowProps = {
+ heading: HeadingType;
colorScheme: 'primary' | 'secondary';
markerHeight?: number;
markerWidth?: number;
-};
+} & EyebrowTextRenderer;
-export function Eyebrow({
+export function Eyebrow({
heading,
colorScheme,
markerHeight,
markerWidth,
-}: EyebrowProps) {
+ renderText,
+}: EyebrowProps) {
const colorClassName =
colorScheme === 'primary' ? eyebrowColorPrimary : eyebrowColorSecondary;
const headingClassName = [eyebrowLabelClassName, colorClassName].join(' ');
+ const headingSegment = {
+ fontFamily: heading.fontFamily,
+ text: heading.text,
+ };
return (
@@ -64,13 +74,24 @@ export function Eyebrow({
width={markerWidth}
/>
-
+ {renderText === undefined ? (
+
+ ) : (
+
+ as="h3"
+ className={headingClassName}
+ renderText={renderText}
+ segments={headingSegment}
+ size="xs"
+ weight="medium"
+ />
+ )}
);
}
diff --git a/packages/twenty-website-new/src/design-system/components/Heading.tsx b/packages/twenty-website-new/src/design-system/components/Heading.tsx
index 70b6bfe9707..29b5177a121 100644
--- a/packages/twenty-website-new/src/design-system/components/Heading.tsx
+++ b/packages/twenty-website-new/src/design-system/components/Heading.tsx
@@ -1,11 +1,11 @@
import { theme } from '@/theme';
import { css } from '@linaria/core';
import { styled } from '@linaria/react';
-import { Fragment } from 'react';
+import { Fragment, type ReactNode } from 'react';
-export type HeadingType = {
+export type HeadingType = {
fontFamily: 'sans' | 'serif' | 'mono';
- text: string;
+ text: TText;
fontWeight?: 'light' | 'regular' | 'medium';
newLine?: boolean;
lineBreakBefore?: boolean;
@@ -108,57 +108,91 @@ const StyledSpan = styled.span`
}
`;
+type HeadingPartProps = {
+ children: ReactNode;
+ fontFamily: HeadingFamily;
+ fontWeight?: HeadingWeight;
+};
+
+export function HeadingPart({
+ children,
+ fontFamily,
+ fontWeight,
+}: HeadingPartProps) {
+ return (
+
+ {children}
+
+ );
+}
+
export type HeadingAs = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
export type HeadingFamily = 'sans' | 'serif' | 'mono';
export type HeadingWeight = 'light' | 'regular' | 'medium';
export type HeadingSize = 'xl' | 'lg' | 'md' | 'sm' | 'xs';
-export type HeadingProps = {
+type HeadingTextRenderer = [TText] extends [ReactNode]
+ ? { renderText?: (text: TText) => ReactNode }
+ : { renderText: (text: TText) => ReactNode };
+
+export type HeadingProps = {
as?: HeadingAs;
- segments: HeadingType | HeadingType[];
+ children?: ReactNode;
+ inlineSegmentSeparator?: ReactNode;
+ segments?: HeadingType | HeadingType[];
weight?: HeadingWeight;
size?: HeadingSize;
className?: string;
-};
+} & HeadingTextRenderer;
-export function Heading({
+export function Heading({
as: Tag = 'h1',
+ children,
+ inlineSegmentSeparator = ' ',
+ renderText,
segments,
weight = 'regular',
size = 'md',
className,
-}: HeadingProps) {
+}: HeadingProps) {
const rootClassName = [headingRootClassName, className]
.filter(Boolean)
.join(' ');
+ const renderSegmentText = (text: TText) =>
+ renderText === undefined ? (text as ReactNode) : renderText(text);
return (
- {Array.isArray(segments) ? (
+ {children !== undefined ? (
+ children
+ ) : Array.isArray(segments) ? (
segments.map((segment, index) => {
const lineBreakBefore =
segment.newLine === true || segment.lineBreakBefore === true;
+ const joinWithPreviousInlineSegment =
+ index > 0 && lineBreakBefore === false;
return (
{lineBreakBefore ?
: null}
+ {joinWithPreviousInlineSegment ? inlineSegmentSeparator : null}
- {segment.text}
+ {renderSegmentText(segment.text)}
);
})
- ) : (
+ ) : segments !== undefined ? (
- {segments.text}
+ {renderSegmentText(segments.text)}
- )}
+ ) : null}
);
}
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 38590db7abd..18438593c75 100644
--- a/packages/twenty-website-new/src/design-system/components/IconButton.tsx
+++ b/packages/twenty-website-new/src/design-system/components/IconButton.tsx
@@ -1,4 +1,3 @@
-import { LocalizedLink } from '@/lib/i18n';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import type { ComponentType } from 'react';
@@ -15,7 +14,6 @@ interface IconButtonProps {
iconSize: number;
iconStrokeColor: string;
size: number;
- href?: string;
onClick?: (e: React.MouseEvent) => void;
ariaExpanded?: boolean;
}
@@ -59,14 +57,6 @@ const StyledButton = styled.button`
width: ${({ $size }) => `${$size}px`};
`;
-const StyledIconLink = styled(LocalizedLink)`
- ${iconButtonSurfaceStyles}
- border: 1px solid ${({ $borderColor }) => $borderColor};
- color: inherit;
- height: ${({ $size }) => `${$size}px`};
- width: ${({ $size }) => `${$size}px`};
-`;
-
export function IconButton({
icon: Icon,
ariaLabel,
@@ -75,7 +65,6 @@ export function IconButton({
iconSize,
iconStrokeColor,
size,
- href,
onClick,
ariaExpanded,
}: IconButtonProps) {
@@ -88,19 +77,6 @@ export function IconButton({
/>
);
- if (href !== undefined && href !== '') {
- return (
-
- {icon}
-
- );
- }
-
return (
-
+
);
}
diff --git a/packages/twenty-website-new/src/lib/customers/case-study-catalog.ts b/packages/twenty-website-new/src/lib/customers/case-study-catalog.ts
index 8cb0ec145b5..83eac66d855 100644
--- a/packages/twenty-website-new/src/lib/customers/case-study-catalog.ts
+++ b/packages/twenty-website-new/src/lib/customers/case-study-catalog.ts
@@ -1,3 +1,4 @@
+import { msg } from '@lingui/core/macro';
import type { CaseStudyCatalogEntry } from '@/lib/customers/types';
import { theme } from '@/theme';
@@ -38,23 +39,23 @@ export function getCaseStudyPalette(href: string) {
export const CASE_STUDY_CATALOG_ENTRIES: CaseStudyCatalogEntry[] = [
{
href: '/customers/9dots',
- industry: 'Real Estate',
- authorRole: 'Founder, Nine Dots Ventures',
+ industry: msg`Real Estate`,
+ authorRole: msg`Founder, Nine Dots Ventures`,
kpis: [
- { value: '150 hrs', label: 'Saved / month' },
- { value: '2,000+', label: 'Daily messages' },
- { value: 'Q1 2026', label: 'Record quarter' },
+ { value: msg`150 hrs`, label: msg`Saved / month` },
+ { value: msg`2,000+`, label: msg`Daily messages` },
+ { value: msg`Q1 2026`, label: msg`Record quarter` },
],
quote: {
- text: 'Twenty lets us build a CRM around the business and not the business around the CRM.',
+ text: msg`Twenty lets us build a CRM around the business and not the business around the CRM.`,
author: 'Mike Babiy',
- role: 'Founder, Nine Dots Ventures',
+ role: msg`Founder, Nine Dots Ventures`,
},
hero: {
readingTime: '9 min',
title: [
- { text: 'A real estate agency on WhatsApp ', fontFamily: 'serif' },
- { text: 'built a CRM around it', fontFamily: 'sans' },
+ { text: msg`A real estate agency on WhatsApp`, fontFamily: 'serif' },
+ { text: msg`built a CRM around it`, fontFamily: 'sans' },
],
author: 'Mike Babiy & Azmat Parveen',
authorAvatarSrc: '/images/partner/testimonials/mike-babiy.png',
@@ -62,26 +63,25 @@ export const CASE_STUDY_CATALOG_ENTRIES: CaseStudyCatalogEntry[] = [
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
- summary:
- "Nine Dots put Twenty at the center of Homeseller's stack with APIs, automation, and AI on top of WhatsApp-heavy operations.",
- date: 'Jul 2025',
+ summary: msg`Nine Dots put Twenty at the center of Homeseller's stack with APIs, automation, and AI on top of WhatsApp-heavy operations.`,
+ date: msg`Jul 2025`,
coverImageSrc:
'https://images.unsplash.com/photo-1733244766159-f58f4184fd38?w=1600&q=80',
},
},
{
href: '/customers/alternative-partners',
- industry: 'Consulting',
- authorRole: 'Principal and Founder, Alternative Partners',
+ industry: msg`Consulting`,
+ authorRole: msg`Principal and Founder, Alternative Partners`,
kpis: [
- { value: 'AI-assisted', label: 'Salesforce migration' },
- { value: 'Self-hosted', label: 'Full ownership' },
+ { value: msg`AI-assisted`, label: msg`Salesforce migration` },
+ { value: msg`Self-hosted`, label: msg`Full ownership` },
],
hero: {
readingTime: '7 min',
title: [
- { text: 'From Salesforce to ', fontFamily: 'serif' },
- { text: 'self-hosted Twenty', fontFamily: 'sans' },
+ { text: msg`From Salesforce to`, fontFamily: 'serif' },
+ { text: msg`self-hosted Twenty`, fontFamily: 'sans' },
],
author: 'Benjamin Reynolds',
authorAvatarSrc: '/images/partner/testimonials/benjamin-reynolds.webp',
@@ -89,26 +89,25 @@ export const CASE_STUDY_CATALOG_ENTRIES: CaseStudyCatalogEntry[] = [
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
- summary:
- 'Alternative Partners replaced Salesforce with self-hosted Twenty, using agentic AI to compress migration work.',
- date: '2025',
+ summary: msg`Alternative Partners replaced Salesforce with self-hosted Twenty, using agentic AI to compress migration work.`,
+ date: msg`2025`,
coverImageSrc:
'https://images.unsplash.com/photo-1702047149248-a6049168d2a8?w=1600&q=80',
},
},
{
href: '/customers/netzero',
- industry: 'Agribusiness',
- authorRole: 'Co-founder, NetZero',
+ industry: msg`Agribusiness`,
+ authorRole: msg`Co-founder, NetZero`,
kpis: [
- { value: '3 product lines', label: 'On a single CRM' },
- { value: 'No-code', label: 'Customizations' },
+ { value: msg`3 product lines`, label: msg`On a single CRM` },
+ { value: msg`No-code`, label: msg`Customizations` },
],
hero: {
readingTime: '8 min',
title: [
- { text: 'A CRM that ', fontFamily: 'serif' },
- { text: 'grows with you', fontFamily: 'sans' },
+ { text: msg`A CRM that`, fontFamily: 'serif' },
+ { text: msg`grows with you`, fontFamily: 'sans' },
],
author: 'Olivier Reinaud',
authorAvatarSrc: '/images/partner/testimonials/olivier-reinaud.jpg',
@@ -116,23 +115,22 @@ export const CASE_STUDY_CATALOG_ENTRIES: CaseStudyCatalogEntry[] = [
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
- summary:
- 'NetZero uses Twenty as a modular CRM across product lines and countries, with a roadmap into AI-assisted workflows.',
- date: '2025',
+ summary: msg`NetZero uses Twenty as a modular CRM across product lines and countries, with a roadmap into AI-assisted workflows.`,
+ date: msg`2025`,
coverImageSrc:
'https://images.unsplash.com/photo-1744830343976-ce690ba2a67c?w=1600&q=80',
},
},
{
href: '/customers/act-education',
- industry: 'Education',
- authorRole: 'CRM Engineer, AC&T Education Migration',
- kpis: [{ value: '90%+', label: 'Lower CRM cost' }],
+ industry: msg`Education`,
+ authorRole: msg`CRM Engineer, AC&T Education Migration`,
+ kpis: [{ value: msg`90%+`, label: msg`Lower CRM cost` }],
hero: {
readingTime: '7 min',
title: [
- { text: 'A CRM they ', fontFamily: 'serif' },
- { text: 'actually own', fontFamily: 'sans' },
+ { text: msg`A CRM they`, fontFamily: 'serif' },
+ { text: msg`actually own`, fontFamily: 'sans' },
],
author: 'Joseph Chiang',
authorAvatarSrc: '/images/partner/testimonials/joseph-chiang.jpg',
@@ -140,23 +138,22 @@ export const CASE_STUDY_CATALOG_ENTRIES: CaseStudyCatalogEntry[] = [
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
- summary:
- 'AC&T and Flycoder moved from a dead vendor export to self-hosted Twenty, with over 90% lower CRM cost and full control.',
- date: '2025',
+ summary: msg`AC&T and Flycoder moved from a dead vendor export to self-hosted Twenty, with over 90% lower CRM cost and full control.`,
+ date: msg`2025`,
coverImageSrc:
'https://images.unsplash.com/photo-1687600154329-150952c73169?w=1600&q=80',
},
},
{
href: '/customers/w3villa',
- industry: 'EdTech',
- authorRole: 'VP of Engineering, W3villa Technologies',
- kpis: [{ value: 'Zero', label: 'Manual work at core' }],
+ industry: msg`EdTech`,
+ authorRole: msg`VP of Engineering, W3villa Technologies`,
+ kpis: [{ value: msg`Zero`, label: msg`Manual work at core` }],
hero: {
readingTime: '8 min',
title: [
- { text: 'When your CRM ', fontFamily: 'serif' },
- { text: 'is the product', fontFamily: 'sans' },
+ { text: msg`When your CRM`, fontFamily: 'serif' },
+ { text: msg`is the product`, fontFamily: 'sans' },
],
author: 'Amrendra Pratap Singh',
authorAvatarSrc: '/images/partner/testimonials/amrendra-singh.webp',
@@ -164,41 +161,39 @@ export const CASE_STUDY_CATALOG_ENTRIES: CaseStudyCatalogEntry[] = [
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
- summary:
- 'W3villa shipped W3Grads on Twenty for AI interviews, scoring, and institution-scale workflows without rebuilding CRM plumbing.',
- date: '2025',
+ summary: msg`W3villa shipped W3Grads on Twenty for AI interviews, scoring, and institution-scale workflows without rebuilding CRM plumbing.`,
+ date: msg`2025`,
coverImageSrc:
'https://images.unsplash.com/photo-1756830231350-3b501f63c5c1?w=1600&q=80',
},
},
{
href: '/customers/elevate-consulting',
- industry: 'Management Consulting',
- authorRole: 'Director of Digital and Information, Elevate Consulting',
+ industry: msg`Management Consulting`,
+ authorRole: msg`Director of Digital and Information, Elevate Consulting`,
kpis: [
- { value: '1 click', label: 'Proposal automation' },
- { value: '4 tools', label: 'Connected via API' },
- { value: 'API-first', label: 'Tool integration' },
+ { value: msg`1 click`, label: msg`Proposal automation` },
+ { value: msg`4 tools`, label: msg`Connected via API` },
+ { value: msg`API-first`, label: msg`Tool integration` },
],
quote: {
- text: 'It is just such a nicer experience than dealing with a Salesforce or a HubSpot. My mission has been to get every tool API-accessible, so everything talks to each other.',
+ text: msg`It is just such a nicer experience than dealing with a Salesforce or a HubSpot. My mission has been to get every tool API-accessible, so everything talks to each other.`,
author: 'Justin Beadle',
- role: 'Director of Digital and Information, Elevate Consulting',
+ role: msg`Director of Digital and Information, Elevate Consulting`,
},
hero: {
readingTime: '8 min',
title: [
- { text: 'Twenty as the API backbone ', fontFamily: 'serif' },
- { text: 'of a go-to-market stack', fontFamily: 'sans' },
+ { text: msg`Twenty as the API backbone`, fontFamily: 'serif' },
+ { text: msg`of a go-to-market stack`, fontFamily: 'sans' },
],
author: 'Justin Beadle',
clientIcon: 'elevate-consulting',
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
- summary:
- 'Elevate Consulting uses Twenty as the API backbone connecting billing, Teams, resourcing, and a custom front end around client and opportunity data.',
- date: 'Jun 2025',
+ summary: msg`Elevate Consulting uses Twenty as the API backbone connecting billing, Teams, resourcing, and a custom front end around client and opportunity data.`,
+ date: msg`Jun 2025`,
coverImageSrc:
'https://images.unsplash.com/photo-1758873269035-aae0e1fd3422?w=1600&q=80',
},
diff --git a/packages/twenty-website-new/src/lib/customers/types.ts b/packages/twenty-website-new/src/lib/customers/types.ts
index e102fc63e3e..5b71853a642 100644
--- a/packages/twenty-website-new/src/lib/customers/types.ts
+++ b/packages/twenty-website-new/src/lib/customers/types.ts
@@ -1,10 +1,11 @@
-import type { HeadingType } from '@/design-system/components/Heading';
+import type { MessageHeadingSegment } from '@/lib/i18n/message-heading-segment';
+import type { MessageDescriptor } from '@lingui/core';
export type CaseStudyTextBlock = {
type: 'text';
- eyebrow?: string;
- heading: HeadingType[];
- paragraphs: string[];
+ eyebrow?: MessageDescriptor;
+ heading: MessageHeadingSegment[];
+ paragraphs: MessageDescriptor[];
callout?: string;
};
@@ -17,44 +18,44 @@ export type CaseStudyVisualBlock = {
export type CaseStudyContentBlock = CaseStudyTextBlock | CaseStudyVisualBlock;
export type CaseStudyKpi = {
- value: string;
- label: string;
+ value: MessageDescriptor;
+ label: MessageDescriptor;
};
export type CaseStudyQuote = {
- text: string;
+ text: MessageDescriptor;
author: string;
- role: string;
+ role: MessageDescriptor;
};
export type CaseStudyData = {
- meta: { title: string; description: string };
+ meta: { title: MessageDescriptor; description: MessageDescriptor };
hero: {
readingTime: string;
- title: HeadingType[];
+ title: MessageHeadingSegment[];
author: string;
authorAvatarSrc?: string;
clientIcon: string;
heroImageSrc: string;
- industry?: string;
- authorRole?: string;
+ industry?: MessageDescriptor;
+ authorRole?: MessageDescriptor;
kpis?: CaseStudyKpi[];
quote?: CaseStudyQuote;
};
sections: CaseStudyContentBlock[];
- tableOfContents: string[];
+ tableOfContents: MessageDescriptor[];
catalogCard: {
- summary: string;
- date: string;
+ summary: MessageDescriptor;
+ date: MessageDescriptor;
coverImageSrc?: string;
};
};
export type CaseStudyCatalogEntry = {
href: string;
- industry: string;
+ industry: MessageDescriptor;
kpis: CaseStudyKpi[];
- authorRole: string;
+ authorRole: MessageDescriptor;
quote?: CaseStudyQuote;
hero: CaseStudyData['hero'];
catalogCard: CaseStudyData['catalogCard'];
diff --git a/packages/twenty-website-new/src/lib/i18n/LocalizedLinkButton.tsx b/packages/twenty-website-new/src/lib/i18n/LocalizedLinkButton.tsx
new file mode 100644
index 00000000000..22f1753df95
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/LocalizedLinkButton.tsx
@@ -0,0 +1,44 @@
+'use client';
+
+import {
+ BaseButton,
+ type BaseButtonProps,
+ buttonBaseStyles,
+} from '@/design-system/components/Button/BaseButton';
+import type { LinkButtonType } from '@/design-system/components/Button/types/LinkButtonType';
+import { styled } from '@linaria/react';
+
+import { LocalizedLink } from './LocalizedLink';
+
+const StyledButtonLink = styled(LocalizedLink)`
+ ${buttonBaseStyles}
+`;
+
+export type LocalizedLinkButtonProps = Omit &
+ LinkButtonType;
+
+export function LocalizedLinkButton({
+ color,
+ href,
+ label,
+ leadingIcon,
+ size = 'regular',
+ variant,
+}: LocalizedLinkButtonProps) {
+ 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
index 0385ae71ad4..7224111afad 100644
--- 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
@@ -24,15 +24,15 @@ describe('detectLocale', () => {
detectLocale({
acceptLanguageHeader: 'en;q=0.5,fr-FR;q=0.9,de;q=0.7',
}),
- ).toBe('en');
+ ).toBe('fr-FR');
});
- it('falls back to the source locale when a regional language is not published', () => {
+ it('matches a regional language to a published locale with the same language subtag', () => {
expect(
detectLocale({
acceptLanguageHeader: 'fr-CA',
}),
- ).toBe('en');
+ ).toBe('fr-FR');
});
it('falls back to the source locale when a bare language is not published', () => {
@@ -68,6 +68,6 @@ describe('detectLocale', () => {
detectLocale({
acceptLanguageHeader: 'en;q=0.1, fr-FR ; q=0.9, de-DE ; q=0.5',
}),
- ).toBe('en');
+ ).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
index 6ba2cf03477..a47cea35aa9 100644
--- 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
@@ -2,17 +2,21 @@ import { localizeHref, stripLocale } from '../localize-href';
describe('localizeHref', () => {
it('does not emit locale prefixes for locales the website does not publish yet', () => {
- expect(localizeHref('fr-FR', '/pricing')).toBe('/pricing');
expect(localizeHref('de-DE', '/pricing')).toBe('/pricing');
});
+ it('emits locale prefixes for published non-default locales', () => {
+ expect(localizeHref('fr-FR', '/pricing')).toBe('/fr-FR/pricing');
+ expect(localizeHref('fr-FR', '/')).toBe('/fr-FR');
+ });
+
it('returns paths unprefixed for the default locale (English at root)', () => {
expect(localizeHref('en', '/pricing')).toBe('/pricing');
expect(localizeHref('en', '/')).toBe('/');
});
it('keeps the root path unprefixed for unpublished locales', () => {
- expect(localizeHref('fr-FR', '/')).toBe('/');
+ expect(localizeHref('de-DE', '/')).toBe('/');
});
it('preserves query strings and hash fragments', () => {
@@ -25,8 +29,8 @@ describe('localizeHref', () => {
});
it('strips legacy locale prefixes when targeting an unpublished locale', () => {
- expect(localizeHref('fr-FR', '/de-DE/why-twenty')).toBe('/why-twenty');
- expect(localizeHref('fr-FR', '/fr-FR/pricing')).toBe('/pricing');
+ expect(localizeHref('de-DE', '/fr-FR/why-twenty')).toBe('/why-twenty');
+ expect(localizeHref('de-DE', '/de-DE/pricing')).toBe('/pricing');
});
it('strips a redundant /en prefix when targeting the default locale', () => {
@@ -35,8 +39,8 @@ describe('localizeHref', () => {
});
it('strips an /en-prefixed path when targeting an unpublished locale', () => {
- expect(localizeHref('fr-FR', '/en/why-twenty')).toBe('/why-twenty');
- expect(localizeHref('fr-FR', '/en')).toBe('/');
+ expect(localizeHref('de-DE', '/en/why-twenty')).toBe('/why-twenty');
+ expect(localizeHref('de-DE', '/en')).toBe('/');
});
it('passes external https URLs through unchanged', () => {
@@ -66,14 +70,14 @@ describe('localizeHref', () => {
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('/?ref=hero');
- expect(localizeHref('fr-FR', '/de-DE?ref=hero')).toBe('/?ref=hero');
+ expect(localizeHref('de-DE', '/en?ref=hero')).toBe('/?ref=hero');
+ expect(localizeHref('de-DE', '/fr-FR?ref=hero')).toBe('/?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('/#anchor');
- expect(localizeHref('fr-FR', '/de-DE#anchor')).toBe('/#anchor');
+ expect(localizeHref('de-DE', '/en#anchor')).toBe('/#anchor');
+ expect(localizeHref('de-DE', '/fr-FR#anchor')).toBe('/#anchor');
});
});
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
index 7e858b07128..1aa554a97ce 100644
--- a/packages/twenty-website-new/src/lib/i18n/app-locale-set.ts
+++ b/packages/twenty-website-new/src/lib/i18n/app-locale-set.ts
@@ -1,8 +1,8 @@
-import {
- APP_LOCALES,
- SOURCE_LOCALE,
- type AppLocale,
-} from 'twenty-shared/translations';
+import { APP_LOCALES, type AppLocale } from 'twenty-shared/translations';
+
+import { WEBSITE_LOCALE_LIST } from './website-locale-list';
+
+export { WEBSITE_LOCALE_LIST } from './website-locale-list';
const APP_LOCALE_VALUES: readonly AppLocale[] = Object.values(APP_LOCALES);
@@ -12,8 +12,6 @@ const isKnownPublicLocale = (locale: AppLocale): boolean =>
export const KNOWN_PUBLIC_APP_LOCALE_LIST: readonly AppLocale[] =
APP_LOCALE_VALUES.filter(isKnownPublicLocale);
-export const WEBSITE_LOCALE_LIST: readonly AppLocale[] = [SOURCE_LOCALE];
-
const WEBSITE_LOCALE_SET: ReadonlySet = new Set(WEBSITE_LOCALE_LIST);
export const isPublicAppLocale = (locale: AppLocale): boolean =>
diff --git a/packages/twenty-website-new/src/lib/i18n/create-message-descriptor-renderer.ts b/packages/twenty-website-new/src/lib/i18n/create-message-descriptor-renderer.ts
new file mode 100644
index 00000000000..3f10838227c
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/create-message-descriptor-renderer.ts
@@ -0,0 +1,6 @@
+import type { I18n, MessageDescriptor } from '@lingui/core';
+
+export const createMessageDescriptorRenderer =
+ (i18n: I18n) =>
+ (descriptor: MessageDescriptor): string =>
+ i18n._(descriptor);
diff --git a/packages/twenty-website-new/src/lib/i18n/detect-locale.ts b/packages/twenty-website-new/src/lib/i18n/detect-locale.ts
index df20fb13228..8ca333ea7b5 100644
--- a/packages/twenty-website-new/src/lib/i18n/detect-locale.ts
+++ b/packages/twenty-website-new/src/lib/i18n/detect-locale.ts
@@ -48,7 +48,8 @@ export const detectLocale = ({
}
if (acceptLanguageHeader !== undefined && acceptLanguageHeader.length > 0) {
- for (const { tag } of parseAcceptLanguage(acceptLanguageHeader)) {
+ for (const { tag, quality } of parseAcceptLanguage(acceptLanguageHeader)) {
+ if (quality <= 0) continue;
const match = matchTag(tag);
if (match !== undefined) return match;
}
diff --git a/packages/twenty-website-new/src/lib/i18n/get-message-descriptor-source.ts b/packages/twenty-website-new/src/lib/i18n/get-message-descriptor-source.ts
new file mode 100644
index 00000000000..e800a65f799
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/get-message-descriptor-source.ts
@@ -0,0 +1,5 @@
+import type { MessageDescriptor } from '@lingui/core';
+
+export const getMessageDescriptorSource = (
+ descriptor: MessageDescriptor,
+): string => descriptor.message ?? descriptor.id;
diff --git a/packages/twenty-website-new/src/lib/i18n/get-route-i18n.ts b/packages/twenty-website-new/src/lib/i18n/get-route-i18n.ts
new file mode 100644
index 00000000000..cdbd68ce5f9
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/get-route-i18n.ts
@@ -0,0 +1,17 @@
+import type { AppLocale } from 'twenty-shared/translations';
+
+import { createI18nInstance } from './create-i18n-instance';
+import { resolveLocaleParam } from './resolve-locale-param';
+
+export type LocaleRouteParams = {
+ locale: string;
+};
+
+export const getRouteI18n = async (
+ params: Promise,
+): Promise> => {
+ const { locale: rawLocale } = await params;
+ const locale: AppLocale = resolveLocaleParam(rawLocale);
+
+ return createI18nInstance(locale);
+};
diff --git a/packages/twenty-website-new/src/lib/i18n/index.ts b/packages/twenty-website-new/src/lib/i18n/index.ts
index 7b34b19dd20..355f93fca2c 100644
--- a/packages/twenty-website-new/src/lib/i18n/index.ts
+++ b/packages/twenty-website-new/src/lib/i18n/index.ts
@@ -9,12 +9,15 @@ export {
isPublicAppLocale,
} from './app-locale-set';
export { createI18nInstance } from './create-i18n-instance';
+export { createMessageDescriptorRenderer } from './create-message-descriptor-renderer';
export { detectLocale, LOCALE_COOKIE_NAME } from './detect-locale';
+export { getRouteI18n, type LocaleRouteParams } from './get-route-i18n';
export { I18nProvider } from './I18nProvider';
export { LocaleContext } from './LocaleContext';
export { LocalizedLink } from './LocalizedLink';
+export { LocalizedLinkButton } from './LocalizedLinkButton';
export { localizeHref, stripLocale } from './localize-href';
-export { getLocaleMessages } from './messages-by-locale';
export { resolveLocaleParam } from './resolve-locale-param';
export { useLocale } from './use-locale';
+export { useRenderMessage } from './use-render-message';
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
index 9928cc6e568..42365fd6f9e 100644
--- a/packages/twenty-website-new/src/lib/i18n/localize-href.ts
+++ b/packages/twenty-website-new/src/lib/i18n/localize-href.ts
@@ -32,9 +32,11 @@ export const localizeHref = (locale: AppLocale, href: string): string => {
? buildTailFromSegmentEnd(href, segmentEnd)
: href;
- return locale === SOURCE_LOCALE || !isPublicAppLocale(locale)
- ? unprefixed
- : `/${locale}${unprefixed}`;
+ if (locale === SOURCE_LOCALE || !isPublicAppLocale(locale)) {
+ return unprefixed;
+ }
+
+ return unprefixed === '/' ? `/${locale}` : `/${locale}${unprefixed}`;
};
export const stripLocale = (pathname: string): string => {
diff --git a/packages/twenty-website-new/src/lib/i18n/message-body.ts b/packages/twenty-website-new/src/lib/i18n/message-body.ts
new file mode 100644
index 00000000000..f85cbc18173
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/message-body.ts
@@ -0,0 +1,5 @@
+import type { MessageDescriptor } from '@lingui/core';
+
+export type MessageBody = {
+ text: MessageDescriptor;
+};
diff --git a/packages/twenty-website-new/src/lib/i18n/message-eyebrow.ts b/packages/twenty-website-new/src/lib/i18n/message-eyebrow.ts
new file mode 100644
index 00000000000..b21c7b546fc
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/message-eyebrow.ts
@@ -0,0 +1,5 @@
+import type { MessageHeadingSegment } from './message-heading-segment';
+
+export type MessageEyebrow = {
+ heading: MessageHeadingSegment;
+};
diff --git a/packages/twenty-website-new/src/lib/i18n/message-heading-segment.ts b/packages/twenty-website-new/src/lib/i18n/message-heading-segment.ts
new file mode 100644
index 00000000000..670d4a3d706
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/message-heading-segment.ts
@@ -0,0 +1,9 @@
+import type { MessageDescriptor } from '@lingui/core';
+
+export type MessageHeadingSegment = {
+ fontFamily: 'sans' | 'serif' | 'mono';
+ text: MessageDescriptor;
+ fontWeight?: 'light' | 'regular' | 'medium';
+ newLine?: boolean;
+ lineBreakBefore?: boolean;
+};
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
index fcd445babe1..9c99ccbc10d 100644
--- a/packages/twenty-website-new/src/lib/i18n/messages-by-locale.ts
+++ b/packages/twenty-website-new/src/lib/i18n/messages-by-locale.ts
@@ -2,9 +2,11 @@ import { type Messages } from '@lingui/core';
import { SOURCE_LOCALE, type AppLocale } from 'twenty-shared/translations';
import { messages as enMessages } from '@/locales/generated/en';
+import { messages as frMessages } from '@/locales/generated/fr-FR';
const MESSAGES_BY_LOCALE: Partial> = {
en: enMessages,
+ 'fr-FR': frMessages,
};
export const getLocaleMessages = (locale: AppLocale): Messages =>
diff --git a/packages/twenty-website-new/src/lib/i18n/set-server-i18n.ts b/packages/twenty-website-new/src/lib/i18n/set-server-i18n.ts
new file mode 100644
index 00000000000..15040b89b8d
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/set-server-i18n.ts
@@ -0,0 +1,10 @@
+import { setI18n } from '@lingui/react/server';
+import type { AppLocale } from 'twenty-shared/translations';
+
+import { createI18nInstance } from './create-i18n-instance';
+
+export const setServerI18n = (locale: AppLocale) => {
+ const i18n = createI18nInstance(locale);
+ setI18n(i18n);
+ return i18n;
+};
diff --git a/packages/twenty-website-new/src/lib/i18n/use-render-message.ts b/packages/twenty-website-new/src/lib/i18n/use-render-message.ts
new file mode 100644
index 00000000000..a51c5de4242
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/use-render-message.ts
@@ -0,0 +1,13 @@
+'use client';
+
+import type { MessageDescriptor } from '@lingui/core';
+import { useLingui } from '@lingui/react';
+import { useCallback } from 'react';
+
+export const useRenderMessage = () => {
+ const { i18n } = useLingui();
+ return useCallback(
+ (descriptor: MessageDescriptor) => i18n._(descriptor),
+ [i18n],
+ );
+};
diff --git a/packages/twenty-website-new/src/lib/i18n/website-locale-list.ts b/packages/twenty-website-new/src/lib/i18n/website-locale-list.ts
new file mode 100644
index 00000000000..f1e80c4fadb
--- /dev/null
+++ b/packages/twenty-website-new/src/lib/i18n/website-locale-list.ts
@@ -0,0 +1,12 @@
+import {
+ APP_LOCALES,
+ SOURCE_LOCALE,
+ type AppLocale,
+} from 'twenty-shared/translations';
+
+// Website localization is intentionally rolled out separately from the app.
+// Keep this allowlist small until Crowdin sync, SEO signals, and QA are proven.
+export const WEBSITE_LOCALE_LIST = [
+ SOURCE_LOCALE,
+ APP_LOCALES['fr-FR'],
+] as const satisfies readonly AppLocale[];
diff --git a/packages/twenty-website-new/src/lib/partner-application/PartnerApplicationModal.tsx b/packages/twenty-website-new/src/lib/partner-application/PartnerApplicationModal.tsx
index 9aa3caf492c..89f4ac74dfb 100644
--- a/packages/twenty-website-new/src/lib/partner-application/PartnerApplicationModal.tsx
+++ b/packages/twenty-website-new/src/lib/partner-application/PartnerApplicationModal.tsx
@@ -6,9 +6,11 @@ import {
buttonBaseStyles,
} from '@/design-system/components/Button/BaseButton';
import { ButtonShape } from '@/design-system/components/Button/ButtonShape';
+import { useRenderMessage } from '@/lib/i18n/use-render-message';
import {
PARTNER_APPLICATION_MODAL_COPY,
- PARTNER_PROGRAM_OPTIONS,
+ PARTNER_PROGRAM_IDS,
+ PARTNER_PROGRAM_LABELS,
type PartnerProgramId,
} from '@/lib/partner-application/partner-application-modal-data';
import { theme } from '@/theme';
@@ -266,6 +268,7 @@ export function PartnerApplicationModal({
onClose,
initialProgramId = 'technology',
}: PartnerApplicationModalProps) {
+ const renderText = useRenderMessage();
const formRef = useRef(null);
const dropdownRef = useRef(null);
const [programId, setProgramId] =
@@ -273,6 +276,7 @@ export function PartnerApplicationModal({
const [dropdownOpen, setDropdownOpen] = useState(false);
const [submitError, setSubmitError] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
+ const copy = PARTNER_APPLICATION_MODAL_COPY;
useEffect(() => {
if (open) {
@@ -336,12 +340,12 @@ export function PartnerApplicationModal({
setSubmitError(null);
if (!name || !email || !company || !website || !message) {
- setSubmitError(validationCopy.incompleteForm);
+ setSubmitError(renderText(validationCopy.incompleteForm));
return;
}
if (!emailLooksValid) {
- setSubmitError(validationCopy.invalidEmail);
+ setSubmitError(renderText(validationCopy.invalidEmail));
return;
}
@@ -363,22 +367,20 @@ export function PartnerApplicationModal({
});
if (!response.ok) {
- setSubmitError(validationCopy.submitFailed);
+ setSubmitError(renderText(validationCopy.submitFailed));
return;
}
onClose();
} catch {
- setSubmitError(validationCopy.submitFailed);
+ setSubmitError(renderText(validationCopy.submitFailed));
} finally {
setIsSubmitting(false);
}
},
- [isSubmitting, onClose, programId],
+ [isSubmitting, onClose, programId, renderText],
);
- const copy = PARTNER_APPLICATION_MODAL_COPY;
-
return (
@@ -415,8 +418,16 @@ export function PartnerApplicationModal({
-
-
+
+
}
/>
@@ -424,19 +435,19 @@ export function PartnerApplicationModal({