From b027e4bdb14106595ad0ab898e8b70b07e082eba Mon Sep 17 00:00:00 2001 From: "Abdullah." <125115953+mabdullahabaid@users.noreply.github.com> Date: Sun, 31 May 2026 17:39:35 +0500 Subject: [PATCH] [Website] i18n module, page-local sections, translatable copy (#21082) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **i18n** — collapsed the ~22 scattered i18n files into a single module and turned on Spanish alongside French. **Sections** — dropped the old compound pattern (`Section.Root`, `Section.Heading`, …). Reusable layout shells moved to `src/templates/`, atomic bits stay in `design-system/`, and each page now owns its copy in local `_components` blocks instead of pulling it out of shared sections. Data files hold arrays only, no prose. **Copy → ``** — A lot of headings were split across several ``s just for font styling, which meant each piece was a separate translation string. A translator got "Build your Enterprise CRM" and "at AI Speed" as two unrelated strings and had no way to reorder them for their language. Those are now single `` units with placeholders. Same idea for the old `\n` + `white-space: pre-line` line-break trick: replaced with a small `ResponsiveLineBreak` element so the break is doesn't quietly rot, and did a dead-code pass. The de-fragmentation changes the message IDs, so around 60 strings will fall back to English in fr/es until Crowdin re-syncs. --- packages/twenty-website/jest.config.mjs | 1 + .../twenty-website/jest/server-only-stub.ts | 5 + packages/twenty-website/lingui.config.ts | 2 +- packages/twenty-website/next.config.ts | 14 +- .../scripts/check-boundaries.mjs | 30 ++ .../scripts/check-section-shape.mjs | 78 +---- .../_components/HomeFeatureThreeCards.tsx | 31 ++ .../[locale]/(home)/_components/HomeHero.tsx | 82 +++++ .../HomeIllustrationThreeCards.tsx | 83 +++++ .../(home)/_components/HomeProblem.tsx | 57 +++ .../(home)/_components/HomeStepperSection.tsx | 52 +++ .../(home)/_components/HomeTestimonials.tsx | 24 ++ .../src/app/[locale]/(home)/page.tsx | 331 ++---------------- .../src/app/[locale]/articles/[slug]/page.tsx | 20 +- .../articles/_components/ArticlesHero.tsx | 20 ++ .../src/app/[locale]/articles/page.tsx | 50 +-- .../src/app/[locale]/customers/9dots/page.tsx | 24 +- ...t.tsx => CustomersCaseStudyPageLayout.tsx} | 37 +- .../_components/CustomersCaseStudySignoff.tsx | 67 ++-- .../_components/CustomersCatalogHero.tsx | 25 ++ .../_components/CustomersCatalogSignoff.tsx | 48 +++ .../[locale]/customers/act-education/page.tsx | 17 +- .../customers/alternative-partners/page.tsx | 13 +- .../customers/elevate-consulting/page.tsx | 21 +- .../app/[locale]/customers/netzero/page.tsx | 22 +- .../src/app/[locale]/customers/page.tsx | 124 +------ .../app/[locale]/customers/w3villa/page.tsx | 20 +- .../activate/EnterpriseActivateClient.tsx | 32 +- .../_components/EnterpriseActivateHero.tsx | 28 ++ .../app/[locale]/enterprise/activate/page.tsx | 46 +-- .../src/app/[locale]/layout.tsx | 18 +- .../partners/components/PartnerHero.tsx | 30 ++ .../partners/components/PartnerSignoff.tsx | 30 ++ .../components/PartnerTestimonials.tsx | 28 ++ .../partners/components/PartnerThreeCards.tsx | 46 +++ .../partners/list/components/EmptyState.tsx | 13 +- .../partners/list/components/FilterBar.tsx | 13 +- .../list/components/MarketplaceHeader.tsx | 23 +- .../src/app/[locale]/partners/list/page.tsx | 16 +- .../src/app/[locale]/partners/page.tsx | 175 ++------- .../_components/PricingEngagementBand.tsx | 55 +++ .../pricing/_components/PricingHero.tsx | 25 ++ .../pricing/_components/PricingSalesforce.tsx | 20 ++ .../src/app/[locale]/pricing/page.tsx | 139 ++------ .../src/app/[locale]/privacy-policy/page.tsx | 6 +- .../product/_components/ProductDemo.tsx} | 61 +++- .../product/_components/ProductHero.tsx | 47 +++ .../product/_components/ProductThreeCards.tsx | 27 ++ .../feature}/FeatureScrollEntrance.tsx | 0 .../feature}/InteractiveVisualMount.tsx | 0 .../_components/feature/ProductFeature.tsx} | 38 +- .../_components/feature}/TileContent.tsx | 4 +- .../_components/feature}/TileVisual.tsx | 0 .../feature/feature-tiles.data.ts} | 16 +- .../product/_components/feature/index.ts | 1 + .../_components/feature}/visuals/BarChart.tsx | 0 .../feature}/visuals/ContactsVisual.tsx | 0 .../feature}/visuals/DashboardVisual.tsx | 0 .../feature}/visuals/DonutChart.tsx | 0 .../feature}/visuals/EmailsVisual.tsx | 0 .../feature}/visuals/FilesVisual.tsx | 0 .../feature}/visuals/ImportVisual.tsx | 0 .../feature}/visuals/PipelineVisual.tsx | 0 .../feature}/visuals/TasksVisual.tsx | 0 .../feature}/visuals/WindowChrome.tsx | 0 .../feature}/visuals/dashboard-visual.data.ts | 0 .../feature}/visuals/visual-tokens.ts | 0 .../_components/product-stepper}/Content.tsx | 0 .../_components/product-stepper}/Flow.tsx | 0 .../product-stepper/ProductStepperSection.tsx | 62 ++++ .../product-stepper}/StepperVisualFrame.tsx | 0 .../_components/product-stepper}/Visual.tsx | 0 .../_components/product-stepper/index.ts | 1 + .../_components/product-stepper}/types.ts | 0 .../visuals/AppPreviewShell.tsx | 0 .../visuals/DataModelVisual.tsx | 0 .../product-stepper}/visuals/DrawEdge.tsx | 0 .../product-stepper}/visuals/LayoutVisual.tsx | 0 .../visuals/WorkflowVisual.tsx | 0 .../visuals/data/DataModel.data.tsx | 0 .../visuals/data/layout.data.ts | 0 .../visuals/data/workflow.data.ts | 0 .../visuals/icons/DataModelIcons.tsx | 0 .../visuals/icons/LayoutIcons.tsx | 0 .../visuals/icons/WorkflowIcons.tsx | 0 .../product-stepper}/visuals/layout-styles.ts | 0 .../visuals/stepper-visual-tokens.ts | 0 .../visuals/use-workflow-animation.ts | 0 .../src/app/[locale]/product/page.tsx | 233 +----------- .../releases/_components/ReleasesHero.tsx | 41 +++ .../src/app/[locale]/releases/page.tsx | 72 ++-- .../src/app/[locale]/terms/page.tsx | 6 +- .../why-twenty/_components/WhyTwentyHero.tsx | 34 ++ .../_components/WhyTwentyMarquee.tsx} | 46 +-- .../_components/WhyTwentyMeaningEditorial.tsx | 42 +++ .../WhyTwentyOpportunityEditorial.tsx | 43 +++ .../_components/WhyTwentyShiftEditorial.tsx | 41 +++ .../_components/WhyTwentySignoff.tsx | 32 ++ .../src/app/[locale]/why-twenty/page.tsx | 198 ++--------- packages/twenty-website/src/app/sitemap.ts | 4 +- .../src/design-system/components/Heading.tsx | 4 + .../components/ResponsiveLineBreak.tsx | 16 + .../src/design-system/components/index.ts | 1 + ...alizedLinkButton.tsx => LocalizedLink.tsx} | 15 +- .../lib/i18n/__tests__/detect-locale.test.ts | 73 ---- .../__tests__/locale-display-names.test.ts | 5 +- .../lib/i18n/__tests__/localize-href.test.ts | 2 +- .../twenty-website/src/lib/i18n/client.tsx | 55 +++ .../src/lib/i18n/components/I18nProvider.tsx | 33 -- .../src/lib/i18n/components/LocaleContext.tsx | 6 - .../src/lib/i18n/components/LocalizedLink.tsx | 17 - .../src/lib/i18n/hooks/use-locale.ts | 16 - .../i18n/hooks/use-unlocalized-pathname.ts | 10 - packages/twenty-website/src/lib/i18n/index.ts | 44 +-- .../twenty-website/src/lib/i18n/locales.ts | 131 +++++++ .../twenty-website/src/lib/i18n/messages.ts | 39 +++ .../twenty-website/src/lib/i18n/server.ts | 46 +++ .../src/lib/i18n/utils/app-locale-set.ts | 42 --- .../lib/i18n/utils/create-i18n-instance.ts | 5 - .../src/lib/i18n/utils/detect-locale.ts | 59 ---- .../utils/get-message-descriptor-source.ts | 5 - .../src/lib/i18n/utils/get-route-i18n.ts | 17 - .../src/lib/i18n/utils/get-server-i18n.ts | 13 - .../lib/i18n/utils/locale-display-names.ts | 19 - .../src/lib/i18n/utils/localize-href.ts | 53 --- .../src/lib/i18n/utils/messages-by-locale.ts | 15 - .../lib/i18n/utils/resolve-locale-param.ts | 10 - .../src/lib/i18n/utils/set-server-i18n.ts | 10 - .../src/lib/i18n/utils/website-locale-list.ts | 12 - .../lib/i18n/utils/website-locale-segments.ts | 14 - .../src/lib/seo/build-localized-metadata.ts | 4 +- .../src/lib/seo/build-page-metadata.ts | 16 +- .../src/lib/seo/json-ld-builders.ts | 2 +- .../src/locales/generated/es-ES.ts | 1 + .../src/sections/Articles/components/index.ts | 7 - .../src/sections/Articles/index.ts | 3 +- .../sections/CaseStudy/components/Hero.tsx | 2 +- .../CaseStudy/components/Highlights.tsx | 2 +- .../CaseStudy/components/TextBlock.tsx | 2 +- .../sections/CaseStudy/components/index.ts | 15 - .../src/sections/CaseStudy/index.ts | 7 +- .../CaseStudyCatalog/components/Card.tsx | 2 +- .../components/CardFooter.tsx | 2 +- .../CaseStudyCatalog/components/CardKpis.tsx | 2 +- .../CaseStudyCatalog/components/Promo.tsx | 29 +- .../CaseStudyCatalog/components/index.ts | 5 - .../src/sections/CaseStudyCatalog/index.ts | 3 +- .../src/sections/Demo/components/Cta.tsx | 16 - .../src/sections/Demo/components/Preview.tsx | 28 -- .../src/sections/Demo/components/index.ts | 5 - .../twenty-website/src/sections/Demo/index.ts | 1 - .../sections/Editorial/components/Body.tsx | 92 ----- .../sections/Editorial/components/Eyebrow.tsx | 11 - .../sections/Editorial/components/Heading.tsx | 23 -- .../sections/Editorial/components/Intro.tsx | 22 -- .../sections/Editorial/components/Root.tsx | 89 ----- .../sections/Editorial/components/index.ts | 7 - .../src/sections/Editorial/index.ts | 1 - .../EngagementBand/components/index.ts | 15 - .../src/sections/EngagementBand/index.ts | 7 +- .../twenty-website/src/sections/Faq/Faq.tsx | 91 +++-- .../src/sections/Feature/index.ts | 1 - .../src/sections/Footer/components/Bottom.tsx | 2 +- .../src/sections/Footer/components/Footer.tsx | 19 + .../Footer/components/FooterNavCta.tsx | 2 +- .../Footer/components/LocaleSwitcher.tsx | 10 +- .../src/sections/Footer/components/Nav.tsx | 2 +- .../__tests__/LocaleSwitcher.test.tsx | 7 +- .../src/sections/Footer/components/index.ts | 6 - .../src/sections/Footer/index.ts | 2 +- .../src/sections/Helped/Helped.tsx | 24 +- .../sections/Helped/components/HelpedCard.tsx | 2 +- .../Helped/components/HelpedScene.tsx | 22 +- .../components/__tests__/Heading.test.tsx | 25 -- .../src/sections/Hero/components/index.ts | 23 -- .../twenty-website/src/sections/Hero/index.ts | 5 +- .../sections/HomeStepper/components/index.ts | 15 - .../src/sections/HomeStepper/index.ts | 2 +- .../components/LegalDocumentPage.tsx | 11 +- .../LegalDocument/components/index.ts | 3 - .../src/sections/LegalDocument/index.ts | 2 +- .../src/sections/Marquee/index.ts | 1 - .../src/sections/Menu/components/Menu.tsx | 34 ++ .../src/sections/Menu/components/index.ts | 8 +- .../sections/PlanTable/components/Content.tsx | 7 +- .../sections/PlanTable/components/index.ts | 7 - .../src/sections/PlanTable/index.ts | 3 +- .../Plans/components/PlanFeatureList.tsx | 2 +- .../src/sections/Plans/components/index.ts | 15 - .../components/use-feature-transition.ts | 2 +- .../src/sections/Plans/index.ts | 3 +- .../src/sections/Problem/Problem.tsx | 22 +- .../src/sections/Problem/ProblemPoints.tsx | 2 +- .../src/sections/Problem/index.ts | 8 +- .../ProductStepper/ProductStepper.tsx | 5 - .../src/sections/ProductStepper/index.ts | 2 - .../sections/ReleaseNotes/components/index.ts | 10 - .../src/sections/ReleaseNotes/index.ts | 7 +- .../Salesforce/components/PricingWindow.tsx | 4 +- .../sections/Salesforce/components/index.ts | 7 - .../src/sections/Salesforce/index.ts | 2 +- .../src/sections/Signoff/Signoff.tsx | 244 ------------- .../src/sections/Testimonials/index.ts | 5 +- .../sections/ThreeCards/components/index.ts | 15 - .../src/sections/ThreeCards/index.ts | 3 +- .../src/sections/TrustedBy/TrustedBy.tsx | 44 +-- .../src/sections/TrustedBy/index.ts | 1 - .../src/templates/Editorial/Editorial.tsx | 240 +++++++++++++ .../src/templates/Editorial/index.ts | 2 + .../Body.tsx => templates/Hero/HeroBody.tsx} | 50 +-- .../Cta.tsx => templates/Hero/HeroCta.tsx} | 4 +- .../Hero/HeroHeading.tsx} | 17 +- .../Hero/HeroSection.tsx} | 45 +-- .../Hero/__tests__/HeroHeading.test.tsx | 20 ++ .../src/templates/Hero/index.ts | 4 + .../src/templates/Signoff/Signoff.tsx | 147 ++++++++ .../{sections => templates}/Signoff/index.ts | 0 .../Testimonials/TestimonialsSection.tsx} | 45 +-- .../src/templates/Testimonials/index.ts | 1 + .../ThreeCards/ThreeCardsIntro.tsx} | 26 +- .../ThreeCards/ThreeCardsSection.tsx} | 27 +- .../src/templates/ThreeCards/index.ts | 2 + 222 files changed, 2610 insertions(+), 3057 deletions(-) create mode 100644 packages/twenty-website/jest/server-only-stub.ts create mode 100644 packages/twenty-website/src/app/[locale]/(home)/_components/HomeFeatureThreeCards.tsx create mode 100644 packages/twenty-website/src/app/[locale]/(home)/_components/HomeHero.tsx create mode 100644 packages/twenty-website/src/app/[locale]/(home)/_components/HomeIllustrationThreeCards.tsx create mode 100644 packages/twenty-website/src/app/[locale]/(home)/_components/HomeProblem.tsx create mode 100644 packages/twenty-website/src/app/[locale]/(home)/_components/HomeStepperSection.tsx create mode 100644 packages/twenty-website/src/app/[locale]/(home)/_components/HomeTestimonials.tsx create mode 100644 packages/twenty-website/src/app/[locale]/articles/_components/ArticlesHero.tsx rename packages/twenty-website/src/app/[locale]/customers/_components/{CaseStudyPageLayout.tsx => CustomersCaseStudyPageLayout.tsx} (69%) create mode 100644 packages/twenty-website/src/app/[locale]/customers/_components/CustomersCatalogHero.tsx create mode 100644 packages/twenty-website/src/app/[locale]/customers/_components/CustomersCatalogSignoff.tsx create mode 100644 packages/twenty-website/src/app/[locale]/enterprise/activate/_components/EnterpriseActivateHero.tsx create mode 100644 packages/twenty-website/src/app/[locale]/partners/components/PartnerHero.tsx create mode 100644 packages/twenty-website/src/app/[locale]/partners/components/PartnerSignoff.tsx create mode 100644 packages/twenty-website/src/app/[locale]/partners/components/PartnerTestimonials.tsx create mode 100644 packages/twenty-website/src/app/[locale]/partners/components/PartnerThreeCards.tsx create mode 100644 packages/twenty-website/src/app/[locale]/pricing/_components/PricingEngagementBand.tsx create mode 100644 packages/twenty-website/src/app/[locale]/pricing/_components/PricingHero.tsx create mode 100644 packages/twenty-website/src/app/[locale]/pricing/_components/PricingSalesforce.tsx rename packages/twenty-website/src/{sections/Demo/components/Root.tsx => app/[locale]/product/_components/ProductDemo.tsx} (51%) create mode 100644 packages/twenty-website/src/app/[locale]/product/_components/ProductHero.tsx create mode 100644 packages/twenty-website/src/app/[locale]/product/_components/ProductThreeCards.tsx rename packages/twenty-website/src/{sections/Feature => app/[locale]/product/_components/feature}/FeatureScrollEntrance.tsx (100%) rename packages/twenty-website/src/{sections/Feature => app/[locale]/product/_components/feature}/InteractiveVisualMount.tsx (100%) rename packages/twenty-website/src/{sections/Feature/Feature.tsx => app/[locale]/product/_components/feature/ProductFeature.tsx} (87%) rename packages/twenty-website/src/{sections/Feature => app/[locale]/product/_components/feature}/TileContent.tsx (97%) rename packages/twenty-website/src/{sections/Feature => app/[locale]/product/_components/feature}/TileVisual.tsx (100%) rename packages/twenty-website/src/app/[locale]/product/{feature.data.ts => _components/feature/feature-tiles.data.ts} (86%) create mode 100644 packages/twenty-website/src/app/[locale]/product/_components/feature/index.ts rename packages/twenty-website/src/{sections/Feature => app/[locale]/product/_components/feature}/visuals/BarChart.tsx (100%) rename packages/twenty-website/src/{sections/Feature => app/[locale]/product/_components/feature}/visuals/ContactsVisual.tsx (100%) rename packages/twenty-website/src/{sections/Feature => app/[locale]/product/_components/feature}/visuals/DashboardVisual.tsx (100%) rename packages/twenty-website/src/{sections/Feature => app/[locale]/product/_components/feature}/visuals/DonutChart.tsx (100%) rename packages/twenty-website/src/{sections/Feature => app/[locale]/product/_components/feature}/visuals/EmailsVisual.tsx (100%) rename packages/twenty-website/src/{sections/Feature => app/[locale]/product/_components/feature}/visuals/FilesVisual.tsx (100%) rename packages/twenty-website/src/{sections/Feature => app/[locale]/product/_components/feature}/visuals/ImportVisual.tsx (100%) rename packages/twenty-website/src/{sections/Feature => app/[locale]/product/_components/feature}/visuals/PipelineVisual.tsx (100%) rename packages/twenty-website/src/{sections/Feature => app/[locale]/product/_components/feature}/visuals/TasksVisual.tsx (100%) rename packages/twenty-website/src/{sections/Feature => app/[locale]/product/_components/feature}/visuals/WindowChrome.tsx (100%) rename packages/twenty-website/src/{sections/Feature => app/[locale]/product/_components/feature}/visuals/dashboard-visual.data.ts (100%) rename packages/twenty-website/src/{sections/Feature => app/[locale]/product/_components/feature}/visuals/visual-tokens.ts (100%) rename packages/twenty-website/src/{sections/ProductStepper => app/[locale]/product/_components/product-stepper}/Content.tsx (100%) rename packages/twenty-website/src/{sections/ProductStepper => app/[locale]/product/_components/product-stepper}/Flow.tsx (100%) create mode 100644 packages/twenty-website/src/app/[locale]/product/_components/product-stepper/ProductStepperSection.tsx rename packages/twenty-website/src/{sections/ProductStepper => app/[locale]/product/_components/product-stepper}/StepperVisualFrame.tsx (100%) rename packages/twenty-website/src/{sections/ProductStepper => app/[locale]/product/_components/product-stepper}/Visual.tsx (100%) create mode 100644 packages/twenty-website/src/app/[locale]/product/_components/product-stepper/index.ts rename packages/twenty-website/src/{sections/ProductStepper => app/[locale]/product/_components/product-stepper}/types.ts (100%) rename packages/twenty-website/src/{sections/ProductStepper => app/[locale]/product/_components/product-stepper}/visuals/AppPreviewShell.tsx (100%) rename packages/twenty-website/src/{sections/ProductStepper => app/[locale]/product/_components/product-stepper}/visuals/DataModelVisual.tsx (100%) rename packages/twenty-website/src/{sections/ProductStepper => app/[locale]/product/_components/product-stepper}/visuals/DrawEdge.tsx (100%) rename packages/twenty-website/src/{sections/ProductStepper => app/[locale]/product/_components/product-stepper}/visuals/LayoutVisual.tsx (100%) rename packages/twenty-website/src/{sections/ProductStepper => app/[locale]/product/_components/product-stepper}/visuals/WorkflowVisual.tsx (100%) rename packages/twenty-website/src/{sections/ProductStepper => app/[locale]/product/_components/product-stepper}/visuals/data/DataModel.data.tsx (100%) rename packages/twenty-website/src/{sections/ProductStepper => app/[locale]/product/_components/product-stepper}/visuals/data/layout.data.ts (100%) rename packages/twenty-website/src/{sections/ProductStepper => app/[locale]/product/_components/product-stepper}/visuals/data/workflow.data.ts (100%) rename packages/twenty-website/src/{sections/ProductStepper => app/[locale]/product/_components/product-stepper}/visuals/icons/DataModelIcons.tsx (100%) rename packages/twenty-website/src/{sections/ProductStepper => app/[locale]/product/_components/product-stepper}/visuals/icons/LayoutIcons.tsx (100%) rename packages/twenty-website/src/{sections/ProductStepper => app/[locale]/product/_components/product-stepper}/visuals/icons/WorkflowIcons.tsx (100%) rename packages/twenty-website/src/{sections/ProductStepper => app/[locale]/product/_components/product-stepper}/visuals/layout-styles.ts (100%) rename packages/twenty-website/src/{sections/ProductStepper => app/[locale]/product/_components/product-stepper}/visuals/stepper-visual-tokens.ts (100%) rename packages/twenty-website/src/{sections/ProductStepper => app/[locale]/product/_components/product-stepper}/visuals/use-workflow-animation.ts (100%) create mode 100644 packages/twenty-website/src/app/[locale]/releases/_components/ReleasesHero.tsx create mode 100644 packages/twenty-website/src/app/[locale]/why-twenty/_components/WhyTwentyHero.tsx rename packages/twenty-website/src/{sections/Marquee/Marquee.tsx => app/[locale]/why-twenty/_components/WhyTwentyMarquee.tsx} (78%) create mode 100644 packages/twenty-website/src/app/[locale]/why-twenty/_components/WhyTwentyMeaningEditorial.tsx create mode 100644 packages/twenty-website/src/app/[locale]/why-twenty/_components/WhyTwentyOpportunityEditorial.tsx create mode 100644 packages/twenty-website/src/app/[locale]/why-twenty/_components/WhyTwentyShiftEditorial.tsx create mode 100644 packages/twenty-website/src/app/[locale]/why-twenty/_components/WhyTwentySignoff.tsx create mode 100644 packages/twenty-website/src/design-system/components/ResponsiveLineBreak.tsx rename packages/twenty-website/src/lib/i18n/{components/LocalizedLinkButton.tsx => LocalizedLink.tsx} (64%) delete mode 100644 packages/twenty-website/src/lib/i18n/__tests__/detect-locale.test.ts create mode 100644 packages/twenty-website/src/lib/i18n/client.tsx delete mode 100644 packages/twenty-website/src/lib/i18n/components/I18nProvider.tsx delete mode 100644 packages/twenty-website/src/lib/i18n/components/LocaleContext.tsx delete mode 100644 packages/twenty-website/src/lib/i18n/components/LocalizedLink.tsx delete mode 100644 packages/twenty-website/src/lib/i18n/hooks/use-locale.ts delete mode 100644 packages/twenty-website/src/lib/i18n/hooks/use-unlocalized-pathname.ts create mode 100644 packages/twenty-website/src/lib/i18n/locales.ts create mode 100644 packages/twenty-website/src/lib/i18n/messages.ts create mode 100644 packages/twenty-website/src/lib/i18n/server.ts delete mode 100644 packages/twenty-website/src/lib/i18n/utils/app-locale-set.ts delete mode 100644 packages/twenty-website/src/lib/i18n/utils/create-i18n-instance.ts delete mode 100644 packages/twenty-website/src/lib/i18n/utils/detect-locale.ts delete mode 100644 packages/twenty-website/src/lib/i18n/utils/get-message-descriptor-source.ts delete mode 100644 packages/twenty-website/src/lib/i18n/utils/get-route-i18n.ts delete mode 100644 packages/twenty-website/src/lib/i18n/utils/get-server-i18n.ts delete mode 100644 packages/twenty-website/src/lib/i18n/utils/locale-display-names.ts delete mode 100644 packages/twenty-website/src/lib/i18n/utils/localize-href.ts delete mode 100644 packages/twenty-website/src/lib/i18n/utils/messages-by-locale.ts delete mode 100644 packages/twenty-website/src/lib/i18n/utils/resolve-locale-param.ts delete mode 100644 packages/twenty-website/src/lib/i18n/utils/set-server-i18n.ts delete mode 100644 packages/twenty-website/src/lib/i18n/utils/website-locale-list.ts delete mode 100644 packages/twenty-website/src/lib/i18n/utils/website-locale-segments.ts create mode 100644 packages/twenty-website/src/locales/generated/es-ES.ts delete mode 100644 packages/twenty-website/src/sections/Articles/components/index.ts delete mode 100644 packages/twenty-website/src/sections/CaseStudy/components/index.ts delete mode 100644 packages/twenty-website/src/sections/CaseStudyCatalog/components/index.ts delete mode 100644 packages/twenty-website/src/sections/Demo/components/Cta.tsx delete mode 100644 packages/twenty-website/src/sections/Demo/components/Preview.tsx delete mode 100644 packages/twenty-website/src/sections/Demo/components/index.ts delete mode 100644 packages/twenty-website/src/sections/Demo/index.ts delete mode 100644 packages/twenty-website/src/sections/Editorial/components/Body.tsx delete mode 100644 packages/twenty-website/src/sections/Editorial/components/Eyebrow.tsx delete mode 100644 packages/twenty-website/src/sections/Editorial/components/Heading.tsx delete mode 100644 packages/twenty-website/src/sections/Editorial/components/Intro.tsx delete mode 100644 packages/twenty-website/src/sections/Editorial/components/Root.tsx delete mode 100644 packages/twenty-website/src/sections/Editorial/components/index.ts delete mode 100644 packages/twenty-website/src/sections/Editorial/index.ts delete mode 100644 packages/twenty-website/src/sections/EngagementBand/components/index.ts delete mode 100644 packages/twenty-website/src/sections/Feature/index.ts create mode 100644 packages/twenty-website/src/sections/Footer/components/Footer.tsx delete mode 100644 packages/twenty-website/src/sections/Footer/components/index.ts delete mode 100644 packages/twenty-website/src/sections/Hero/components/__tests__/Heading.test.tsx delete mode 100644 packages/twenty-website/src/sections/Hero/components/index.ts delete mode 100644 packages/twenty-website/src/sections/HomeStepper/components/index.ts delete mode 100644 packages/twenty-website/src/sections/LegalDocument/components/index.ts delete mode 100644 packages/twenty-website/src/sections/Marquee/index.ts create mode 100644 packages/twenty-website/src/sections/Menu/components/Menu.tsx delete mode 100644 packages/twenty-website/src/sections/PlanTable/components/index.ts delete mode 100644 packages/twenty-website/src/sections/Plans/components/index.ts delete mode 100644 packages/twenty-website/src/sections/ProductStepper/ProductStepper.tsx delete mode 100644 packages/twenty-website/src/sections/ProductStepper/index.ts delete mode 100644 packages/twenty-website/src/sections/ReleaseNotes/components/index.ts delete mode 100644 packages/twenty-website/src/sections/Salesforce/components/index.ts delete mode 100644 packages/twenty-website/src/sections/Signoff/Signoff.tsx delete mode 100644 packages/twenty-website/src/sections/ThreeCards/components/index.ts create mode 100644 packages/twenty-website/src/templates/Editorial/Editorial.tsx create mode 100644 packages/twenty-website/src/templates/Editorial/index.ts rename packages/twenty-website/src/{sections/Hero/components/Body.tsx => templates/Hero/HeroBody.tsx} (57%) rename packages/twenty-website/src/{sections/Hero/components/Cta.tsx => templates/Hero/HeroCta.tsx} (75%) rename packages/twenty-website/src/{sections/Hero/components/Heading.tsx => templates/Hero/HeroHeading.tsx} (60%) rename packages/twenty-website/src/{sections/Hero/components/Root.tsx => templates/Hero/HeroSection.tsx} (60%) create mode 100644 packages/twenty-website/src/templates/Hero/__tests__/HeroHeading.test.tsx create mode 100644 packages/twenty-website/src/templates/Hero/index.ts create mode 100644 packages/twenty-website/src/templates/Signoff/Signoff.tsx rename packages/twenty-website/src/{sections => templates}/Signoff/index.ts (100%) rename packages/twenty-website/src/{sections/Testimonials/Testimonials.tsx => templates/Testimonials/TestimonialsSection.tsx} (65%) create mode 100644 packages/twenty-website/src/templates/Testimonials/index.ts rename packages/twenty-website/src/{sections/ThreeCards/components/Intro.tsx => templates/ThreeCards/ThreeCardsIntro.tsx} (54%) rename packages/twenty-website/src/{sections/ThreeCards/components/Root.tsx => templates/ThreeCards/ThreeCardsSection.tsx} (60%) create mode 100644 packages/twenty-website/src/templates/ThreeCards/index.ts diff --git a/packages/twenty-website/jest.config.mjs b/packages/twenty-website/jest.config.mjs index b1662645a9..04bf99188b 100644 --- a/packages/twenty-website/jest.config.mjs +++ b/packages/twenty-website/jest.config.mjs @@ -40,6 +40,7 @@ const jestConfig = { ], }, moduleNameMapper: { + '^server-only$': '/jest/server-only-stub.ts', ...pathsToModuleNameMapper(tsConfig.compilerOptions.paths, { prefix: '/', }), diff --git a/packages/twenty-website/jest/server-only-stub.ts b/packages/twenty-website/jest/server-only-stub.ts new file mode 100644 index 0000000000..55493fc8aa --- /dev/null +++ b/packages/twenty-website/jest/server-only-stub.ts @@ -0,0 +1,5 @@ +// jest stub for the `server-only` package. `server-only` is a Next.js build +// guard (errors if a server module is pulled into a client bundle); it has no +// behaviour at runtime and no resolution under the jest node environment, so we +// map it to this empty module. The real guard still runs during `next build`. +export {}; diff --git a/packages/twenty-website/lingui.config.ts b/packages/twenty-website/lingui.config.ts index 215989deb6..7393f65cbe 100644 --- a/packages/twenty-website/lingui.config.ts +++ b/packages/twenty-website/lingui.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from '@lingui/conf'; import { formatter } from '@lingui/format-po'; import { SOURCE_LOCALE } from 'twenty-shared/translations'; -import { WEBSITE_LOCALE_LIST } from './src/lib/i18n/utils/website-locale-list'; +import { WEBSITE_LOCALE_LIST } from './src/lib/i18n/locales'; export default defineConfig({ sourceLocale: SOURCE_LOCALE, diff --git a/packages/twenty-website/next.config.ts b/packages/twenty-website/next.config.ts index 1fc51e3310..8e660117db 100644 --- a/packages/twenty-website/next.config.ts +++ b/packages/twenty-website/next.config.ts @@ -3,9 +3,17 @@ import { initOpenNextCloudflareForDev } from '@opennextjs/cloudflare'; import withLinaria, { type LinariaConfig } from 'next-with-linaria'; import { APP_LOCALES } from 'twenty-shared/translations'; -// Locale URL segments that are actually served (others are normalised away). -// Mirrors LOCALE_BY_URL_SEGMENT keys in src/lib/i18n/utils/website-locale-segments.ts. -const DEPLOYED_LOCALE_URL_SEGMENTS = ['en', 'fr'] as const; +import { + WEBSITE_LOCALE_LIST, + localeToUrlSegment, +} from './src/lib/i18n/locales'; + +// Locale URL segments that are actually served (others are normalised away), +// derived from the single WEBSITE_LOCALE_LIST source of truth so adding a +// locale never requires editing this config. These are exactly the keys of +// LOCALE_BY_URL_SEGMENT in src/lib/i18n/locales.ts. +const DEPLOYED_LOCALE_URL_SEGMENTS = + WEBSITE_LOCALE_LIST.map(localeToUrlSegment); // Raw locale codes (e.g. fr-FR, de-DE) that should redirect to the un-prefixed // path. Excludes pseudo-* locales and the deployed URL segments themselves. diff --git a/packages/twenty-website/scripts/check-boundaries.mjs b/packages/twenty-website/scripts/check-boundaries.mjs index 3b77818510..7224db7bdb 100644 --- a/packages/twenty-website/scripts/check-boundaries.mjs +++ b/packages/twenty-website/scripts/check-boundaries.mjs @@ -52,6 +52,36 @@ const RULES = [ 'element visibility, cleanup, and render failures are handled consistently.', ].join('\n '), }, + { + id: 'design-system-no-upward-import', + description: + 'design-system is the lowest UI layer — it must not import from @/sections, @/templates, or @/app.', + pattern: /from\s+['"]@\/(sections|templates|app)(\/|['"])/, + appliesTo: (rel) => + rel.startsWith('src/design-system/') && /\.(ts|tsx)$/.test(rel), + exempt: (rel) => rel.includes('__tests__') || rel.endsWith('.d.ts'), + help: [ + 'design-system holds atomic, domain-agnostic primitives. It may depend on', + '@/theme, @/icons and @/lib, but never on the higher layers (sections,', + 'templates, app). If a primitive needs section/page data, lift that data up', + 'to the consumer and pass it in as a prop.', + ].join('\n '), + }, + { + id: 'templates-no-upward-import', + description: + 'templates (section layout shells) must not import from @/sections or @/app — only @/design-system and @/theme.', + pattern: /from\s+['"]@\/(sections|app)(\/|['"])/, + appliesTo: (rel) => + rel.startsWith('src/templates/') && /\.(ts|tsx)$/.test(rel), + exempt: (rel) => rel.includes('__tests__') || rel.endsWith('.d.ts'), + help: [ + 'templates are reusable section layout shells composed from design-system', + 'primitives. They must not reach into sections (visuals) or app (pages).', + 'Page-local blocks compose a template with its section visual — inject the', + 'visual as children/a prop rather than importing it into the template.', + ].join('\n '), + }, ]; const SKIP_DIRS = new Set([ diff --git a/packages/twenty-website/scripts/check-section-shape.mjs b/packages/twenty-website/scripts/check-section-shape.mjs index 8f8106f70f..6c829acd7e 100644 --- a/packages/twenty-website/scripts/check-section-shape.mjs +++ b/packages/twenty-website/scripts/check-section-shape.mjs @@ -7,8 +7,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(__dirname, '..'); const SECTIONS_DIR = path.join(ROOT, 'src', 'sections'); -const SECTIONS_USING_NAMED_SLOTS = new Map([]); - const LEAF_SECTIONS = new Set([ 'CaseStudy', 'CaseStudyCatalog', @@ -16,6 +14,11 @@ const LEAF_SECTIONS = new Set([ 'LegalDocument', 'PartnerApplication', 'Stepper', + // Visual-only modules: their layout shells moved to src/templates/, so they + // no longer own a section — they just expose visual components. + 'Hero', + 'ThreeCards', + 'Testimonials', ]); async function listSections() { @@ -59,9 +62,14 @@ async function findBarrel(sectionDir) { async function findRoot(sectionDir) { const sectionName = path.basename(sectionDir); const candidates = [ + // Legacy compound sections own the
in components/Root.tsx. path.join(sectionDir, 'components', 'Root.tsx'), + // Single-file sections own it in
.tsx (e.g. TrustedBy/TrustedBy.tsx). path.join(sectionDir, `${sectionName}.tsx`), path.join(sectionDir, `${sectionName}.ts`), + // Flat-primitive sections own it in a
Section shell + // (e.g. Hero/components/HeroSection.tsx) consumed by page-local blocks. + path.join(sectionDir, 'components', `${sectionName}Section.tsx`), ]; for (const candidate of candidates) { if (await fileExists(candidate)) return candidate; @@ -69,22 +77,6 @@ async function findRoot(sectionDir) { return null; } -function parseSlotIdentifiers(barrelContents) { - const exportMatch = barrelContents.match( - /export\s+const\s+\w+\s*=\s*\{([^}]+)\}/m, - ); - if (!exportMatch) return null; - const body = exportMatch[1]; - return body - .split(',') - .map((entry) => entry.trim()) - .filter((entry) => entry.length > 0) - .map((entry) => { - const colon = entry.indexOf(':'); - return colon === -1 ? entry : entry.slice(0, colon).trim(); - }); -} - const TOARRAY_REGEX = /Children\.toArray\s*\(/; function stripComments(source) { @@ -100,7 +92,7 @@ async function checkSection(name) { const barrel = await findBarrel(sectionDir); if (barrel === null) { violations.push( - `${name}: missing components/index.{ts,tsx} barrel — every section must expose a single compound export.`, + `${name}: missing a barrel (index.{ts,tsx}) — every section must expose its public API through one barrel (flat named exports; no compound objects).`, ); return violations; } @@ -124,57 +116,9 @@ async function checkSection(name) { } } - const slotsToCheck = SECTIONS_USING_NAMED_SLOTS.get(name); - if (slotsToCheck !== undefined) { - const barrelContents = await readFileOrNull(barrel); - const exportedSlotNames = barrelContents - ? parseSlotIdentifiers(barrelContents) - : null; - for (const slot of slotsToCheck) { - if (exportedSlotNames !== null && !exportedSlotNames.includes(slot)) { - violations.push( - `${name}: slot "${slot}" is declared in SECTIONS_USING_NAMED_SLOTS but is not exported from ${path.relative( - ROOT, - barrel, - )}. Either export it or remove the entry from check-section-shape.mjs.`, - ); - continue; - } - const expected = `${name}.${slot}`; - const slotFile = await locateSlotFile(sectionDir, slot); - if (slotFile === null) { - violations.push( - `${name}: slot "${slot}" is declared in SECTIONS_USING_NAMED_SLOTS but no source file matches the conventional path (components/${slot}.tsx or components/${slot}/${slot}.tsx).`, - ); - continue; - } - const contents = await readFileOrNull(slotFile); - if (contents === null) continue; - if (!contents.includes(`displayName = '${expected}'`)) { - violations.push( - `${name}: slot "${slot}" source (${path.relative( - ROOT, - slotFile, - )}) does not set ${slot}.displayName = '${expected}'. Root looks slots up by displayName; without it the slot silently fails to render.`, - ); - } - } - } - return violations; } -async function locateSlotFile(sectionDir, slot) { - const candidates = [ - path.join(sectionDir, 'components', `${slot}.tsx`), - path.join(sectionDir, 'components', slot, `${slot}.tsx`), - ]; - for (const candidate of candidates) { - if (await fileExists(candidate)) return candidate; - } - return null; -} - async function main() { const sections = await listSections(); const allViolations = []; diff --git a/packages/twenty-website/src/app/[locale]/(home)/_components/HomeFeatureThreeCards.tsx b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeFeatureThreeCards.tsx new file mode 100644 index 0000000000..8a3097ce06 --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeFeatureThreeCards.tsx @@ -0,0 +1,31 @@ +import { Trans } from '@lingui/react/macro'; + +import { FEATURE_CARDS } from '@/app/[locale]/(home)/three-cards-feature.data'; +import { Eyebrow, Heading, HeadingPart } from '@/design-system/components'; +import { FeatureCards } from '@/sections/ThreeCards'; +import { ThreeCardsIntro, ThreeCardsSection } from '@/templates/ThreeCards'; + +export function HomeFeatureThreeCards() { + return ( + + + + + Skip the clunky UX that always comes with custom. + + + + + + Make your GTM team happy + +
+ with + a CRM they'll love +
+
+
+ +
+ ); +} diff --git a/packages/twenty-website/src/app/[locale]/(home)/_components/HomeHero.tsx b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeHero.tsx new file mode 100644 index 0000000000..d9a77f5714 --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeHero.tsx @@ -0,0 +1,82 @@ +import { msg } from '@lingui/core/macro'; +import { Trans } from '@lingui/react/macro'; +import { styled } from '@linaria/react'; + +import { APP_PREVIEW_DATA } from '@/app/[locale]/(home)/app-preview.data'; +import { HeadingPart, LinkButton } from '@/design-system/components'; +import { getServerI18n } from '@/lib/i18n/server'; +import { WebGlMount } from '@/lib/visual-runtime'; +import { AppPreview } from '@/sections/AppPreview'; +import { TalkToUsButton } from '@/sections/ContactCal'; +import { HeroBody, HeroCta, HeroHeading, HeroSection } from '@/templates/Hero'; +import { HomeBackgroundHalftone } from '@/sections/Hero/visuals/components/HomeBackgroundHalftone'; +import { theme } from '@/theme'; + +const HeroHeadingGroup = styled.div` + align-items: center; + display: flex; + flex-direction: column; + gap: ${theme.spacing(3)}; + width: 100%; + + > *:last-child { + margin-top: 0; + } +`; + +const HeroIntroGroup = styled.div` + align-items: center; + display: flex; + flex-direction: column; + gap: ${theme.spacing(8)}; + width: 100%; +`; + +export function HomeHero() { + const i18n = getServerI18n(); + + return ( + + + + } + > + + + + + + Build your Enterprise CRM + + at AI Speed + + + + + Twenty gives technical teams the building blocks for a custom CRM + that meets complex business needs and quickly adapts as the + business evolves. + + + + + + + + + + + ); +} diff --git a/packages/twenty-website/src/app/[locale]/(home)/_components/HomeIllustrationThreeCards.tsx b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeIllustrationThreeCards.tsx new file mode 100644 index 0000000000..60d1516e47 --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeIllustrationThreeCards.tsx @@ -0,0 +1,83 @@ +import { Trans } from '@lingui/react/macro'; +import { css } from '@linaria/core'; +import { styled } from '@linaria/react'; + +import { ILLUSTRATION_CARDS } from '@/app/[locale]/(home)/three-cards-illustration.data'; +import { + Body, + Eyebrow, + Heading, + HeadingPart, +} from '@/design-system/components'; +import { IllustrationCards } from '@/sections/ThreeCards'; +import { ThreeCardsIntro, ThreeCardsSection } from '@/templates/ThreeCards'; +import { theme } from '@/theme'; + +const IntroContent = styled.div` + display: grid; + grid-template-columns: 1fr; + row-gap: ${theme.spacing(2)}; + width: 100%; +`; + +const IntroHeader = styled.div` + display: grid; + grid-template-columns: 1fr; + row-gap: ${theme.spacing(6)}; + width: 100%; +`; + +const headingClassName = css` + width: 100%; + + @media (min-width: ${theme.breakpoints.md}px) { + max-width: ${theme.layout.editorial}; + } + + [data-family='sans'] { + letter-spacing: -0.02em; + } +`; + +const bodyClassName = css` + width: 100%; + + @media (min-width: ${theme.breakpoints.md}px) { + max-width: 571px; + } +`; + +export function HomeIllustrationThreeCards() { + return ( + + + + + + + Stop settling for trade-offs. + + + + + + Assemble, iterate and adapt a robust CRM, + + + that's quick to flex + + + + + + + Compose your CRM and internal apps with a single extensibility + toolkit. + + + + + + + ); +} diff --git a/packages/twenty-website/src/app/[locale]/(home)/_components/HomeProblem.tsx b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeProblem.tsx new file mode 100644 index 0000000000..130c435eb6 --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeProblem.tsx @@ -0,0 +1,57 @@ +import { msg } from '@lingui/core/macro'; +import { Trans } from '@lingui/react/macro'; + +import { Eyebrow, HeadingPart } from '@/design-system/components'; +import { + ProblemContent, + ProblemHeading, + ProblemPoints, + ProblemSection, + ProblemVisual, + type ProblemPointType, +} from '@/sections/Problem'; + +export function HomeProblem() { + const points: ProblemPointType[] = [ + { + heading: ( + + The Giant Monolith + + ), + body: msg`Proprietary languages, slow deployment cycles, and "black box" logic.`, + }, + { + heading: ( + + The In-house Burden + + ), + body: msg`It's fragile. V1 ships quickly, but maintaining and making changes is a long term burden.`, + }, + ]; + + return ( + + + + + + The Problem. + + + + + + A custom CRM gives your org an edge, + + but building one + comes with + tradeoffs + + + + + + ); +} diff --git a/packages/twenty-website/src/app/[locale]/(home)/_components/HomeStepperSection.tsx b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeStepperSection.tsx new file mode 100644 index 0000000000..f9b515e404 --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeStepperSection.tsx @@ -0,0 +1,52 @@ +import { msg } from '@lingui/core/macro'; +import { Trans } from '@lingui/react/macro'; + +import { Heading, HeadingPart } from '@/design-system/components'; +import { + HomeStepperScrollSection, + type HomeStepperStepType, +} from '@/sections/HomeStepper'; + +export function HomeStepperSection() { + const steps: HomeStepperStepType[] = [ + { + heading: ( + + + + Begin with production-grade + + building blocks + + + ), + body: msg`Compose your CRM and internal apps with a single extensibility toolkit. Data model, layout, and automation.`, + }, + { + heading: ( + + + Continue iteration + without friction + + + ), + body: msg`Enjoy unlimited customization using the AI coding tools you already love. Adapt your CRM to fit the way your business grows and wins.`, + }, + { + heading: ( + + + + Stay in control with our + + open-source software + + + ), + body: msg`Don't get locked into someone else's ecosystem. Twenty's developer experience looks like normal software, with local setup, real data, live testing, and no proprietary tooling.`, + }, + ]; + + return ; +} diff --git a/packages/twenty-website/src/app/[locale]/(home)/_components/HomeTestimonials.tsx b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeTestimonials.tsx new file mode 100644 index 0000000000..0887bf6990 --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeTestimonials.tsx @@ -0,0 +1,24 @@ +import { msg } from '@lingui/core/macro'; + +import { HOME_TESTIMONIALS } from '@/app/[locale]/(home)/testimonials.data'; +import { getServerI18n } from '@/lib/i18n/server'; +import { + TestimonialsCarousel, + TestimonialsHourglassVisual, +} from '@/sections/Testimonials'; +import { TestimonialsSection } from '@/templates/Testimonials'; + +export function HomeTestimonials() { + const i18n = getServerI18n(); + + return ( + + + + + + ); +} diff --git a/packages/twenty-website/src/app/[locale]/(home)/page.tsx b/packages/twenty-website/src/app/[locale]/(home)/page.tsx index f18c4a04ae..eecae88495 100644 --- a/packages/twenty-website/src/app/[locale]/(home)/page.tsx +++ b/packages/twenty-website/src/app/[locale]/(home)/page.tsx @@ -1,96 +1,23 @@ -import { msg } from '@lingui/core/macro'; import { HELPED_CARDS } from '@/app/[locale]/(home)/helped.data'; -import { APP_PREVIEW_DATA } from '@/app/[locale]/(home)/app-preview.data'; -import { Problem, type ProblemPointType } from '@/sections/Problem'; -import { HOME_TESTIMONIALS } from '@/app/[locale]/(home)/testimonials.data'; -import { FEATURE_CARDS } from '@/app/[locale]/(home)/three-cards-feature.data'; -import { ILLUSTRATION_CARDS } from '@/app/[locale]/(home)/three-cards-illustration.data'; -import { TalkToUsButton } from '@/sections/ContactCal'; +import { HomeProblem } from '@/app/[locale]/(home)/_components/HomeProblem'; import { Faq, FAQ_QUESTIONS } from '@/sections/Faq'; -import { TRUSTED_BY_LOGOS, TrustedBy } from '@/sections/TrustedBy'; -import { - Body, - Eyebrow, - Heading, - HeadingPart, - LinkButton, -} from '@/design-system/components'; +import { TrustedBy } from '@/sections/TrustedBy'; import { fetchCommunityStats } from '@/lib/community/fetch-community-stats'; -import { - getRouteI18n, - type LocaleRouteParams, -} from '@/lib/i18n/utils/get-route-i18n'; -import { Pages } from '@/lib/pages'; +import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server'; import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels'; import { Helped } from '@/sections/Helped'; -import { Hero } from '@/sections/Hero'; -import { HomeStepper, type HomeStepperStepType } from '@/sections/HomeStepper'; +import { HomeHero } from '@/app/[locale]/(home)/_components/HomeHero'; +import { HomeStepperSection } from '@/app/[locale]/(home)/_components/HomeStepperSection'; import { Menu, MENU_DATA } from '@/sections/Menu'; -import { Testimonials } from '@/sections/Testimonials'; -import { ThreeCards } from '@/sections/ThreeCards'; +import { HomeTestimonials } from '@/app/[locale]/(home)/_components/HomeTestimonials'; +import { HomeFeatureThreeCards } from '@/app/[locale]/(home)/_components/HomeFeatureThreeCards'; +import { HomeIllustrationThreeCards } from '@/app/[locale]/(home)/_components/HomeIllustrationThreeCards'; import { buildFaqPageJsonLd, buildRouteMetadata, JsonLd } from '@/lib/seo'; -import { theme } from '@/theme'; -import { css } from '@linaria/core'; -import { styled } from '@linaria/react'; export const generateMetadata = buildRouteMetadata('home'); const HOME_TOP_BACKGROUND_COLOR = '#F4F4F4'; -const HeroHeadingGroup = styled.div` - align-items: center; - display: flex; - flex-direction: column; - gap: ${theme.spacing(3)}; - width: 100%; - - > *:last-child { - margin-top: 0; - } -`; - -const HeroIntroGroup = styled.div` - align-items: center; - display: flex; - flex-direction: column; - gap: ${theme.spacing(8)}; - width: 100%; -`; - -const ThreeCardsIllustrationIntroContent = styled.div` - display: grid; - grid-template-columns: 1fr; - row-gap: ${theme.spacing(2)}; - width: 100%; -`; - -const ThreeCardsIllustrationIntroHeader = styled.div` - display: grid; - grid-template-columns: 1fr; - row-gap: ${theme.spacing(6)}; - width: 100%; -`; - -const threeCardsIllustrationHeadingClassName = css` - width: 100%; - - @media (min-width: ${theme.breakpoints.md}px) { - max-width: ${theme.layout.editorial}; - } - - [data-family='sans'] { - letter-spacing: -0.02em; - } -`; - -const threeCardsIllustrationBodyClassName = css` - width: 100%; - - @media (min-width: ${theme.breakpoints.md}px) { - max-width: 571px; - } -`; - type HomePageProps = { params: Promise; }; @@ -101,66 +28,6 @@ export default async function HomePage({ params }: HomePageProps) { fetchCommunityStats(), ]); - const PROBLEM_POINTS: ProblemPointType[] = [ - { - heading: ( - - {i18n._(msg`The Giant Monolith`)} - - ), - body: msg`Proprietary languages, slow deployment cycles, and "black box" logic.`, - }, - { - heading: ( - - {i18n._(msg`The In-house Burden`)} - - ), - body: msg`It's fragile. V1 ships quickly, but maintaining and making changes is a long term burden.`, - }, - ]; - - const HOME_STEPPER_STEPS: HomeStepperStepType[] = [ - { - heading: ( - - - {i18n._(msg`Begin with production-grade`)} - {' '} - - {i18n._(msg`building blocks`)} - - - ), - body: msg`Compose your CRM and internal apps with a single extensibility toolkit. Data model, layout, and automation.`, - }, - { - heading: ( - - - {i18n._(msg`Continue iteration`)} - {' '} - - {i18n._(msg`without friction`)} - - - ), - body: msg`Enjoy unlimited customization using the AI coding tools you already love. Adapt your CRM to fit the way your business grows and wins.`, - }, - { - heading: ( - - - {i18n._(msg`Stay in control with our`)} - {' '} - - {i18n._(msg`open-source software`)} - - - ), - body: msg`Don't get locked into someone else's ecosystem. Twenty's developer experience looks like normal software, with local setup, real data, live testing, and no proprietary tooling.`, - }, - ]; const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats); return ( @@ -192,186 +59,28 @@ export default async function HomePage({ params }: HomePageProps) { href="/illustrations/home/helped/money.glb" as="fetch" /> - - - - - - - - - - - - - {i18n._(msg`Build your Enterprise CRM`)} - {' '} - - {i18n._(msg`at\u00A0AI\u00A0Speed`)} - - - - {i18n._( - msg`Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves.`, - )} - - - - - - - - - - - - - - - - - {i18n._(msg`The Problem.`)} - - - - - {i18n._(msg`A custom CRM gives your org an edge,`)} - {' '} - - {i18n._(msg`but building one`)} - {' '} - - {i18n._(msg`comes with`)} - {' '} - - {i18n._(msg`tradeoffs`)} - - - - - + - - - - - - - {i18n._(msg`Stop settling for trade-offs.`)} - - - - - {i18n._(msg`Assemble, iterate and adapt a robust CRM,`)} - {' '} - - {i18n._(msg`that's quick to flex`)} - - - - - {i18n._( - msg`Compose your CRM and internal apps with a single extensibility toolkit.`, - )} - - - - - + - + - - - - - {i18n._(msg`Skip the clunky UX that always comes with custom.`)} - - - - - {i18n._(msg`Make your GTM team happy`)} - -
- - {i18n._(msg`with`)} - {' '} - - {i18n._(msg`a CRM they'll love`)} - -
-
- -
+ - - - + - - - - - + - - - - - {i18n._(msg`Any Questions?`)} - - - - - {i18n._(msg`Stop fighting custom.`)} - -
- - {i18n._(msg`Start building, with Twenty`)} - -
- - - - -
- -
+ + + + + ); } diff --git a/packages/twenty-website/src/app/[locale]/articles/[slug]/page.tsx b/packages/twenty-website/src/app/[locale]/articles/[slug]/page.tsx index 86dbd30539..5fe88633ba 100644 --- a/packages/twenty-website/src/app/[locale]/articles/[slug]/page.tsx +++ b/packages/twenty-website/src/app/[locale]/articles/[slug]/page.tsx @@ -4,12 +4,11 @@ import { SOURCE_LOCALE } from 'twenty-shared/translations'; import { getPublishedArticle, getPublishedArticles } from '@/lib/articles'; import { fetchCommunityStats } from '@/lib/community/fetch-community-stats'; -import { type LocaleRouteParams } from '@/lib/i18n/utils/get-route-i18n'; -import { localeToUrlSegment } from '@/lib/i18n/utils/website-locale-segments'; -import { resolveLocaleParam } from '@/lib/i18n'; +import { type LocaleRouteParams } from '@/lib/i18n/server'; +import { localeToUrlSegment, resolveLocaleParam } from '@/lib/i18n'; import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels'; import { buildArticleJsonLd, buildPageMetadata, JsonLd } from '@/lib/seo'; -import { Articles } from '@/sections/Articles'; +import { ArticlesArticle } from '@/sections/Articles'; import { Menu, MENU_DATA } from '@/sections/Menu'; import { theme } from '@/theme'; @@ -68,18 +67,11 @@ export default async function ArticlePage({ params }: ArticlePageProps) { return ( <> - - - - - - - + /> + ); } diff --git a/packages/twenty-website/src/app/[locale]/articles/_components/ArticlesHero.tsx b/packages/twenty-website/src/app/[locale]/articles/_components/ArticlesHero.tsx new file mode 100644 index 0000000000..4bfe000755 --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/articles/_components/ArticlesHero.tsx @@ -0,0 +1,20 @@ +import { HeadingPart } from '@/design-system/components'; +import { HeroBody, HeroHeading, HeroSection } from '@/templates/Hero'; + +// Articles is English-only (SOURCE_LOCALE-gated), so the copy is literal. +export function ArticlesHero() { + return ( + + + Ideas on +
+ open-source + CRM +
+ + Ideas from the team building Twenty on open source CRM, GTM systems, and + building software that lasts. + +
+ ); +} diff --git a/packages/twenty-website/src/app/[locale]/articles/page.tsx b/packages/twenty-website/src/app/[locale]/articles/page.tsx index 486d8d29b4..e7df6bba64 100644 --- a/packages/twenty-website/src/app/[locale]/articles/page.tsx +++ b/packages/twenty-website/src/app/[locale]/articles/page.tsx @@ -1,27 +1,21 @@ -import { msg } from '@lingui/core/macro'; import { notFound } from 'next/navigation'; import { SOURCE_LOCALE } from 'twenty-shared/translations'; -import { HeadingPart } from '@/design-system/components'; import { getPublishedArticles } from '@/lib/articles'; import { fetchCommunityStats } from '@/lib/community/fetch-community-stats'; import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels'; import { resolveLocaleParam } from '@/lib/i18n'; -import { - getRouteI18n, - type LocaleRouteParams, -} from '@/lib/i18n/utils/get-route-i18n'; -import { Pages } from '@/lib/pages'; +import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server'; import { buildArticleListJsonLd, buildBreadcrumbListJsonLd, buildRouteMetadata, JsonLd, } from '@/lib/seo'; -import { Articles } from '@/sections/Articles'; -import { Hero } from '@/sections/Hero'; +import { ArticlesIndex } from '@/sections/Articles'; +import { ArticlesHero } from '@/app/[locale]/articles/_components/ArticlesHero'; import { Menu, MENU_DATA } from '@/sections/Menu'; -import { TRUSTED_BY_LOGOS, TrustedBy } from '@/sections/TrustedBy'; +import { TrustedBy } from '@/sections/TrustedBy'; import { css } from '@linaria/core'; export const generateMetadata = buildRouteMetadata('articles'); @@ -66,7 +60,10 @@ export default async function ArticlesPage({ params }: ArticlesPageProps) { notFound(); } - const [i18n, stats] = await Promise.all([ + // getRouteI18n sets the request-scoped i18n context that TrustedBy/Menu/ + // Articles read; this page's own copy is English-only (SOURCE_LOCALE-gated + // above), so the returned instance is intentionally unused here. + const [, stats] = await Promise.all([ getRouteI18n(params), fetchCommunityStats(), ]); @@ -87,41 +84,20 @@ export default async function ArticlesPage({ params }: ArticlesPageProps) { {posts.length > 0 ? ( ) : null} - - - - - - + />
- - - Ideas on -
- open-source{' '} - CRM -
- - Ideas from the team building Twenty on open source CRM, GTM systems, - and building software that lasts. - -
- +
- + ); } diff --git a/packages/twenty-website/src/app/[locale]/customers/9dots/page.tsx b/packages/twenty-website/src/app/[locale]/customers/9dots/page.tsx index 4ade697420..85c76b68cf 100644 --- a/packages/twenty-website/src/app/[locale]/customers/9dots/page.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/9dots/page.tsx @@ -1,10 +1,7 @@ -import { CaseStudyPageLayout } from '@/app/[locale]/customers/_components/CaseStudyPageLayout'; +import { CustomersCaseStudyPageLayout } from '@/app/[locale]/customers/_components/CustomersCaseStudyPageLayout'; import { Heading, HeadingPart } from '@/design-system/components'; import { CASE_STUDY_HERO_IMAGES, type CaseStudyData } from '@/lib/customers'; -import { - getRouteI18n, - type LocaleRouteParams, -} from '@/lib/i18n/utils/get-route-i18n'; +import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server'; import { buildLocalizedMetadata } from '@/lib/seo'; import { msg } from '@lingui/core/macro'; @@ -39,10 +36,10 @@ export default async function NineDotsCaseStudyPage({ {i18n._(msg`A real estate agency on WhatsApp`)} - {' '} + {i18n._(msg`built a`)}
- {i18n._(msg`CRM`)}{' '} + {i18n._(msg`CRM`)} {i18n._(msg`around it`)}
), @@ -71,7 +68,7 @@ export default async function NineDotsCaseStudyPage({ {i18n._(msg`When the channel is`)} - {' '} + {i18n._(msg`the business`)} @@ -92,7 +89,7 @@ export default async function NineDotsCaseStudyPage({ {i18n._(msg`The CRM as a`)} - {' '} + {i18n._(msg`control hub`)} @@ -112,7 +109,7 @@ export default async function NineDotsCaseStudyPage({ {i18n._(msg`150 hours`)} - {' '} + {i18n._(msg`saved every month`)} @@ -135,5 +132,10 @@ export default async function NineDotsCaseStudyPage({ }, }; - return ; + return ( + + ); } diff --git a/packages/twenty-website/src/app/[locale]/customers/_components/CaseStudyPageLayout.tsx b/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCaseStudyPageLayout.tsx similarity index 69% rename from packages/twenty-website/src/app/[locale]/customers/_components/CaseStudyPageLayout.tsx rename to packages/twenty-website/src/app/[locale]/customers/_components/CustomersCaseStudyPageLayout.tsx index 2a0ae8bf80..f081ef9dda 100644 --- a/packages/twenty-website/src/app/[locale]/customers/_components/CaseStudyPageLayout.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCaseStudyPageLayout.tsx @@ -1,21 +1,28 @@ import { getCaseStudyPalette, type CaseStudyData } from '@/lib/customers'; import { fetchCommunityStats } from '@/lib/community/fetch-community-stats'; import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels'; -import { CaseStudy } from '@/sections/CaseStudy'; +import { + CaseStudyBody, + CaseStudyHero, + CaseStudyHighlights, + CaseStudySectionNav, + CaseStudyTextBlock, + CaseStudyVisualBlock, +} from '@/sections/CaseStudy'; import { Menu, MENU_DATA } from '@/sections/Menu'; import { theme } from '@/theme'; import { CustomersCaseStudySignoff } from './CustomersCaseStudySignoff'; -type CaseStudyPageLayoutProps = { +type CustomersCaseStudyPageLayoutProps = { caseStudy: CaseStudyData; path: string; }; -export async function CaseStudyPageLayout({ +export async function CustomersCaseStudyPageLayout({ caseStudy, path, -}: CaseStudyPageLayoutProps) { +}: CustomersCaseStudyPageLayoutProps) { const stats = await fetchCommunityStats(); const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats); const palette = getCaseStudyPalette(path); @@ -26,7 +33,7 @@ export async function CaseStudyPageLayout({ const sectionId = `case-study-section-${storySectionIndex}`; storySectionIndex += 1; return ( - - - - - - - + /> - - - {sectionBlocks} + {sectionBlocks} - + ); diff --git a/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCaseStudySignoff.tsx b/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCaseStudySignoff.tsx index 1783a952f6..9c4dd64d18 100644 --- a/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCaseStudySignoff.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCaseStudySignoff.tsx @@ -1,37 +1,50 @@ import { msg } from '@lingui/core/macro'; +import { Trans } from '@lingui/react/macro'; + +import { + HeadingPart, + LinkButton, + ResponsiveLineBreak, +} from '@/design-system/components'; +import { getServerI18n } from '@/lib/i18n/server'; import { TalkToUsButton } from '@/sections/ContactCal'; -import { HeadingPart, LinkButton } from '@/design-system/components'; -import { getServerI18n } from '@/lib/i18n/utils/get-server-i18n'; -import { Pages } from '@/lib/pages'; -import { Signoff } from '@/sections/Signoff'; +import { Signoff } from '@/templates/Signoff'; export function CustomersCaseStudySignoff() { const i18n = getServerI18n(); + return ( - - + - {i18n._(msg`Ready to grow\nwith Twenty?`)} + + Ready to grow + + with Twenty? + - - - {i18n._( - msg`Join the teams that chose to own their CRM.\nStart building with Twenty today.`, - )} - - - - - - + } + body={ + + Join the teams that chose to own their CRM. + + Start building with Twenty today. + + } + > + + + ); } diff --git a/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCatalogHero.tsx b/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCatalogHero.tsx new file mode 100644 index 0000000000..a62183315d --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCatalogHero.tsx @@ -0,0 +1,25 @@ +import { Trans } from '@lingui/react/macro'; + +import { HeadingPart } from '@/design-system/components'; +import { HeroBody, HeroHeading, HeroSection } from '@/templates/Hero'; + +export function CustomersCatalogHero() { + return ( + + + + See how teams +
+ build + on Twenty +
+
+ + + Real stories from real teams about how they shaped Twenty to fit their + workflow and accelerated their growth. + + +
+ ); +} diff --git a/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCatalogSignoff.tsx b/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCatalogSignoff.tsx new file mode 100644 index 0000000000..cbdfb2bfdb --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCatalogSignoff.tsx @@ -0,0 +1,48 @@ +import { msg } from '@lingui/core/macro'; +import { Trans } from '@lingui/react/macro'; + +import { + HeadingPart, + LinkButton, + ResponsiveLineBreak, +} from '@/design-system/components'; +import { getServerI18n } from '@/lib/i18n/server'; +import { TalkToUsButton } from '@/sections/ContactCal'; +import { Signoff } from '@/templates/Signoff'; + +export function CustomersCatalogSignoff() { + const i18n = getServerI18n(); + + return ( + + Ready to build +
+ your own story? + + } + body={ + + Join the teams that chose to own their CRM. + + Start building with Twenty today. + + } + > + + +
+ ); +} diff --git a/packages/twenty-website/src/app/[locale]/customers/act-education/page.tsx b/packages/twenty-website/src/app/[locale]/customers/act-education/page.tsx index 81941d6cfe..9e0e0eb117 100644 --- a/packages/twenty-website/src/app/[locale]/customers/act-education/page.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/act-education/page.tsx @@ -1,12 +1,9 @@ import { msg } from '@lingui/core/macro'; import { CASE_STUDY_HERO_IMAGES, type CaseStudyData } from '@/lib/customers'; -import { - getRouteI18n, - type LocaleRouteParams, -} from '@/lib/i18n/utils/get-route-i18n'; +import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server'; import { buildLocalizedMetadata } from '@/lib/seo'; import { Heading, HeadingPart } from '@/design-system/components'; -import { CaseStudyPageLayout } from '@/app/[locale]/customers/_components/CaseStudyPageLayout'; +import { CustomersCaseStudyPageLayout } from '@/app/[locale]/customers/_components/CustomersCaseStudyPageLayout'; const HERO_IMAGE = CASE_STUDY_HERO_IMAGES.actEducation; @@ -62,7 +59,7 @@ export default async function ActEducationCaseStudyPage({ {i18n._(msg`When the vendor`)} - {' '} + {i18n._(msg`pulled the plug`)} @@ -82,7 +79,7 @@ export default async function ActEducationCaseStudyPage({ {i18n._(msg`No more renting someone else's`)} - {' '} + {i18n._(msg`structure`)} @@ -101,7 +98,7 @@ export default async function ActEducationCaseStudyPage({ {i18n._(msg`Control without`)} - {' '} + {i18n._(msg`the overhead`)} @@ -119,7 +116,7 @@ export default async function ActEducationCaseStudyPage({ {i18n._(msg`Costs down more than`)} - {' '} + {i18n._(msg`90%`)} ), @@ -142,7 +139,7 @@ export default async function ActEducationCaseStudyPage({ }; return ( - diff --git a/packages/twenty-website/src/app/[locale]/customers/alternative-partners/page.tsx b/packages/twenty-website/src/app/[locale]/customers/alternative-partners/page.tsx index 932ded4797..4012f0f1dc 100644 --- a/packages/twenty-website/src/app/[locale]/customers/alternative-partners/page.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/alternative-partners/page.tsx @@ -1,12 +1,9 @@ import { msg } from '@lingui/core/macro'; import { Heading, HeadingPart } from '@/design-system/components'; import { CASE_STUDY_HERO_IMAGES, type CaseStudyData } from '@/lib/customers'; -import { - getRouteI18n, - type LocaleRouteParams, -} from '@/lib/i18n/utils/get-route-i18n'; +import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server'; import { buildLocalizedMetadata } from '@/lib/seo'; -import { CaseStudyPageLayout } from '@/app/[locale]/customers/_components/CaseStudyPageLayout'; +import { CustomersCaseStudyPageLayout } from '@/app/[locale]/customers/_components/CustomersCaseStudyPageLayout'; const HERO_IMAGE = CASE_STUDY_HERO_IMAGES.alternativePartners; @@ -65,7 +62,7 @@ export default async function AlternativePartnersCaseStudyPage({ {i18n._(msg`AI in the`)} - {' '} + {i18n._(msg`migration workflow`)} @@ -84,7 +81,7 @@ export default async function AlternativePartnersCaseStudyPage({ {i18n._(msg`Self-hosted`)} - {' '} + {i18n._(msg`means control`)} @@ -106,7 +103,7 @@ export default async function AlternativePartnersCaseStudyPage({ }; return ( - diff --git a/packages/twenty-website/src/app/[locale]/customers/elevate-consulting/page.tsx b/packages/twenty-website/src/app/[locale]/customers/elevate-consulting/page.tsx index d4880c51d7..1a0bbb6717 100644 --- a/packages/twenty-website/src/app/[locale]/customers/elevate-consulting/page.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/elevate-consulting/page.tsx @@ -1,12 +1,9 @@ import { msg } from '@lingui/core/macro'; import { CASE_STUDY_HERO_IMAGES, type CaseStudyData } from '@/lib/customers'; -import { - getRouteI18n, - type LocaleRouteParams, -} from '@/lib/i18n/utils/get-route-i18n'; +import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server'; import { buildLocalizedMetadata } from '@/lib/seo'; import { Heading, HeadingPart } from '@/design-system/components'; -import { CaseStudyPageLayout } from '@/app/[locale]/customers/_components/CaseStudyPageLayout'; +import { CustomersCaseStudyPageLayout } from '@/app/[locale]/customers/_components/CustomersCaseStudyPageLayout'; const HERO_IMAGE = CASE_STUDY_HERO_IMAGES.elevateConsulting; @@ -43,7 +40,7 @@ export default async function ElevateConsultingCaseStudyPage({
{i18n._(msg`API backbone`)} - {' '} + {i18n._(msg`of a go-to-market stack`)} @@ -73,7 +70,7 @@ export default async function ElevateConsultingCaseStudyPage({ {i18n._(msg`From documents to`)} - {' '} + {i18n._(msg`open APIs`)} @@ -92,8 +89,8 @@ export default async function ElevateConsultingCaseStudyPage({ eyebrow: msg`Integration`, heading: ( - {i18n._(msg`One`)}{' '} - {i18n._(msg`API`)}{' '} + {i18n._(msg`One`)} + {i18n._(msg`API`)} {i18n._(msg`to rule them all`)} @@ -114,7 +111,7 @@ export default async function ElevateConsultingCaseStudyPage({ {i18n._(msg`Workflows that`)} - {' '} + {i18n._(msg`actually get used`)} @@ -131,7 +128,7 @@ export default async function ElevateConsultingCaseStudyPage({ eyebrow: msg`What is next`, heading: ( - {i18n._(msg`Beyond`)}{' '} + {i18n._(msg`Beyond`)} {i18n._(msg`internal rollout`)} @@ -156,7 +153,7 @@ export default async function ElevateConsultingCaseStudyPage({ }; return ( - diff --git a/packages/twenty-website/src/app/[locale]/customers/netzero/page.tsx b/packages/twenty-website/src/app/[locale]/customers/netzero/page.tsx index 7b0f751983..0460f63a9e 100644 --- a/packages/twenty-website/src/app/[locale]/customers/netzero/page.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/netzero/page.tsx @@ -1,12 +1,9 @@ import { msg } from '@lingui/core/macro'; import { CASE_STUDY_HERO_IMAGES, type CaseStudyData } from '@/lib/customers'; -import { - getRouteI18n, - type LocaleRouteParams, -} from '@/lib/i18n/utils/get-route-i18n'; +import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server'; import { buildLocalizedMetadata } from '@/lib/seo'; import { Heading, HeadingPart } from '@/design-system/components'; -import { CaseStudyPageLayout } from '@/app/[locale]/customers/_components/CaseStudyPageLayout'; +import { CustomersCaseStudyPageLayout } from '@/app/[locale]/customers/_components/CustomersCaseStudyPageLayout'; const HERO_IMAGE = CASE_STUDY_HERO_IMAGES.netzero; @@ -41,7 +38,7 @@ export default async function NetZeroCaseStudyPage({ {i18n._(msg`A CRM that`)}
- {i18n._(msg`grows`)}{' '} + {i18n._(msg`grows`)} {i18n._(msg`with you`)}
), @@ -64,7 +61,7 @@ export default async function NetZeroCaseStudyPage({ {i18n._(msg`The right`)} - {' '} + {i18n._(msg`foundation`)} @@ -83,7 +80,7 @@ export default async function NetZeroCaseStudyPage({ {i18n._(msg`A business that does not fit a`)} - {' '} + {i18n._(msg`template`)} ), @@ -101,7 +98,7 @@ export default async function NetZeroCaseStudyPage({ {i18n._(msg`From simple to`)} - {' '} + {i18n._(msg`advanced`)} ), @@ -118,7 +115,7 @@ export default async function NetZeroCaseStudyPage({ {i18n._(msg`The bet is`)} - {' '} + {i18n._(msg`paying off`)} @@ -143,6 +140,9 @@ export default async function NetZeroCaseStudyPage({ }; return ( - + ); } diff --git a/packages/twenty-website/src/app/[locale]/customers/page.tsx b/packages/twenty-website/src/app/[locale]/customers/page.tsx index 0612ce01f2..bc99e1f063 100644 --- a/packages/twenty-website/src/app/[locale]/customers/page.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/page.tsx @@ -1,20 +1,13 @@ -import { msg } from '@lingui/core/macro'; -import { Faq, FAQ_QUESTIONS } from '@/sections/Faq'; -import { TRUSTED_BY_LOGOS, TrustedBy } from '@/sections/TrustedBy'; -import { TalkToUsButton } from '@/sections/ContactCal'; +import { Faq } from '@/sections/Faq'; +import { TrustedBy } from '@/sections/TrustedBy'; import { CASE_STUDY_CATALOG_ENTRIES } from '@/lib/customers'; -import { Eyebrow, HeadingPart, LinkButton } from '@/design-system/components'; import { fetchCommunityStats } from '@/lib/community/fetch-community-stats'; -import { - getRouteI18n, - type LocaleRouteParams, -} from '@/lib/i18n/utils/get-route-i18n'; -import { Pages } from '@/lib/pages'; +import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server'; import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels'; -import { CaseStudyCatalog } from '@/sections/CaseStudyCatalog'; -import { Hero } from '@/sections/Hero'; +import { CaseStudyCatalogGrid } from '@/sections/CaseStudyCatalog'; +import { CustomersCatalogHero } from '@/app/[locale]/customers/_components/CustomersCatalogHero'; import { Menu, MENU_DATA } from '@/sections/Menu'; -import { Signoff } from '@/sections/Signoff'; +import { CustomersCatalogSignoff } from '@/app/[locale]/customers/_components/CustomersCatalogSignoff'; import { buildRouteMetadata } from '@/lib/seo'; import { css } from '@linaria/core'; @@ -55,7 +48,9 @@ type CaseStudiesCatalogPageProps = { export default async function CaseStudiesCatalogPage({ params, }: CaseStudiesCatalogPageProps) { - const [i18n, stats] = await Promise.all([ + // getRouteI18n sets the request-scoped i18n context the page's components + // read; the page renders no copy of its own, so the instance is unused here. + const [, stats] = await Promise.all([ getRouteI18n(params), fetchCommunityStats(), ]); @@ -63,111 +58,24 @@ export default async function CaseStudiesCatalogPage({ return ( <> - - - - - - + />
- - - - {i18n._(msg`See how teams`)} - -
- - {i18n._(msg`build`)} - {' '} - - {i18n._(msg`on Twenty`)} - -
- - {i18n._( - msg`Real stories from real teams about how they shaped Twenty to fit their workflow and accelerated their growth.`, - )} - -
- +
- + - - - - {i18n._(msg`Ready to build`)} - -
- - {i18n._(msg`your own story?`)} - -
- - {i18n._( - msg`Join the teams that chose to own their CRM.\nStart building with Twenty today.`, - )} - - - - - -
+ - - - - - {i18n._(msg`Any Questions?`)} - - - - - {i18n._(msg`Stop fighting custom.`)} - -
- - {i18n._(msg`Start building, with Twenty`)} - -
- - - - -
- -
+ ); } diff --git a/packages/twenty-website/src/app/[locale]/customers/w3villa/page.tsx b/packages/twenty-website/src/app/[locale]/customers/w3villa/page.tsx index c7ad5f1576..0f3451f552 100644 --- a/packages/twenty-website/src/app/[locale]/customers/w3villa/page.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/w3villa/page.tsx @@ -1,12 +1,9 @@ import { msg } from '@lingui/core/macro'; import { CASE_STUDY_HERO_IMAGES, type CaseStudyData } from '@/lib/customers'; -import { - getRouteI18n, - type LocaleRouteParams, -} from '@/lib/i18n/utils/get-route-i18n'; +import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server'; import { buildLocalizedMetadata } from '@/lib/seo'; import { Heading, HeadingPart } from '@/design-system/components'; -import { CaseStudyPageLayout } from '@/app/[locale]/customers/_components/CaseStudyPageLayout'; +import { CustomersCaseStudyPageLayout } from '@/app/[locale]/customers/_components/CustomersCaseStudyPageLayout'; const HERO_IMAGE = CASE_STUDY_HERO_IMAGES.w3villa; @@ -62,7 +59,7 @@ export default async function W3villaCaseStudyPage({ {i18n._(msg`Scale without`)} - {' '} + {i18n._(msg`breaking operations`)} @@ -82,7 +79,7 @@ export default async function W3villaCaseStudyPage({ {i18n._(msg`Focus on the use case, not the`)} - {' '} + {i18n._(msg`plumbing`)} ), @@ -99,7 +96,7 @@ export default async function W3villaCaseStudyPage({ {i18n._(msg`A platform ready to`)} - {' '} + {i18n._(msg`grow`)} ), @@ -116,7 +113,7 @@ export default async function W3villaCaseStudyPage({ {i18n._(msg`Zero manual work`)} - {' '} + {i18n._(msg`at the core`)} @@ -141,6 +138,9 @@ export default async function W3villaCaseStudyPage({ }; return ( - + ); } diff --git a/packages/twenty-website/src/app/[locale]/enterprise/activate/EnterpriseActivateClient.tsx b/packages/twenty-website/src/app/[locale]/enterprise/activate/EnterpriseActivateClient.tsx index bd02d9ffe8..0f0ca3f0f8 100644 --- a/packages/twenty-website/src/app/[locale]/enterprise/activate/EnterpriseActivateClient.tsx +++ b/packages/twenty-website/src/app/[locale]/enterprise/activate/EnterpriseActivateClient.tsx @@ -1,6 +1,7 @@ 'use client'; import { msg } from '@lingui/core/macro'; +import { Trans } from '@lingui/react/macro'; import { BaseButton, buttonBaseStyles, @@ -159,7 +160,7 @@ export function EnterpriseActivateClient() { {loading && ( - {i18n._(msg`Activating your enterprise license…`)} + Activating your enterprise license… )} @@ -168,14 +169,14 @@ export function EnterpriseActivateClient() { {result !== null && ( <> - {i18n._( - msg`Your enterprise license has been activated successfully.`, - )} + + Your enterprise license has been activated successfully. + - {i18n._(msg`Licensee:`)} + Licensee: {' '} {result.licensee} @@ -183,13 +184,14 @@ export function EnterpriseActivateClient() { - {i18n._(msg`Your enterprise key`)} + Your enterprise key - {i18n._( - msg`Copy this key and paste it into your Twenty self-hosted instance settings.`, - )} + + Copy this key and paste it into your Twenty self-hosted instance + settings. + @@ -215,25 +217,25 @@ export function EnterpriseActivateClient() { - {i18n._(msg`Next steps`)} + Next steps
  • - {i18n._(msg`Copy the enterprise key above.`)} + Copy the enterprise key above.
  • - {i18n._( - msg`Open your Twenty self-hosted instance Settings → Enterprise.`, - )} + + Open your Twenty self-hosted instance Settings → Enterprise. +
  • - {i18n._(msg`Paste the key and click Activate.`)} + Paste the key and click Activate.
  • diff --git a/packages/twenty-website/src/app/[locale]/enterprise/activate/_components/EnterpriseActivateHero.tsx b/packages/twenty-website/src/app/[locale]/enterprise/activate/_components/EnterpriseActivateHero.tsx new file mode 100644 index 0000000000..d4c68f53a9 --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/enterprise/activate/_components/EnterpriseActivateHero.tsx @@ -0,0 +1,28 @@ +import { Trans } from '@lingui/react/macro'; + +import { Eyebrow, HeadingPart } from '@/design-system/components'; +import { HeroBody, HeroHeading, HeroSection } from '@/templates/Hero'; + +export function EnterpriseActivateHero() { + return ( + + + + Self-hosting + + + + + Enterprise + activation + + + + + Your checkout is complete. Follow the steps below to copy your license + key into your Twenty instance. + + + + ); +} diff --git a/packages/twenty-website/src/app/[locale]/enterprise/activate/page.tsx b/packages/twenty-website/src/app/[locale]/enterprise/activate/page.tsx index dceb039881..71c8856cf5 100644 --- a/packages/twenty-website/src/app/[locale]/enterprise/activate/page.tsx +++ b/packages/twenty-website/src/app/[locale]/enterprise/activate/page.tsx @@ -1,19 +1,10 @@ import { msg } from '@lingui/core/macro'; import { EnterpriseActivateClient } from '@/app/[locale]/enterprise/activate/EnterpriseActivateClient'; -import { - Body, - Container, - Eyebrow, - HeadingPart, -} from '@/design-system/components'; +import { Body, Container } from '@/design-system/components'; import { fetchCommunityStats } from '@/lib/community/fetch-community-stats'; -import { - getRouteI18n, - type LocaleRouteParams, -} from '@/lib/i18n/utils/get-route-i18n'; -import { Pages } from '@/lib/pages'; +import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server'; import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels'; -import { Hero } from '@/sections/Hero'; +import { EnterpriseActivateHero } from '@/app/[locale]/enterprise/activate/_components/EnterpriseActivateHero'; import { Menu, MENU_DATA } from '@/sections/Menu'; import { theme } from '@/theme'; import { buildRouteMetadata } from '@/lib/seo'; @@ -66,36 +57,9 @@ export default async function EnterpriseActivatePage({ return ( <> - - - - - - + - - - - {i18n._(msg`Self-hosting`)} - - - - - {i18n._(msg`Enterprise`)} - {' '} - {i18n._(msg`activation`)} - - - {i18n._( - msg`Your checkout is complete. Follow the steps below to copy your license key into your Twenty instance.`, - )} - - + diff --git a/packages/twenty-website/src/app/[locale]/layout.tsx b/packages/twenty-website/src/app/[locale]/layout.tsx index e5d34fcad7..c31bcdc082 100644 --- a/packages/twenty-website/src/app/[locale]/layout.tsx +++ b/packages/twenty-website/src/app/[locale]/layout.tsx @@ -18,13 +18,12 @@ import { ScrollToTopOnRouteChange } from '@/app/_components/ScrollToTopOnRouteCh import { I18nProvider, localeToUrlSegment, - PUBLIC_APP_LOCALE_LIST, + WEBSITE_LOCALE_LIST, resolveLocaleParam, } from '@/lib/i18n'; -import { getLocaleMessages } from '@/lib/i18n/utils/messages-by-locale'; -import { setServerI18n } from '@/lib/i18n/utils/set-server-i18n'; +import { getLocaleMessages, setServerI18n } from '@/lib/i18n/server'; import { ContactCalModalRoot } from '@/sections/ContactCal'; -import { Footer, FOOTER_DATA } from '@/sections/Footer'; +import { Footer } from '@/sections/Footer'; import { PartnerApplicationModalRoot } from '@/sections/PartnerApplication'; const hostGrotesk = Host_Grotesk({ @@ -124,7 +123,7 @@ type LocaleLayoutParams = { locale: string }; export const dynamicParams = false; export const generateStaticParams = (): LocaleLayoutParams[] => - PUBLIC_APP_LOCALE_LIST.map((locale) => ({ + WEBSITE_LOCALE_LIST.map((locale) => ({ locale: localeToUrlSegment(locale), })); @@ -162,14 +161,7 @@ const LocaleLayout = async ({ {children} - - - - - +