[Website] i18n module, page-local sections, translatable copy (#21082)

**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 → `<Trans>`** — A lot of headings were split across several
`<HeadingPart>`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 `<Trans>` 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.
This commit is contained in:
Abdullah.
2026-05-31 17:39:35 +05:00
committed by GitHub
parent fc90b4ba8b
commit b027e4bdb1
222 changed files with 2610 additions and 3057 deletions
@@ -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 <Root> — 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 <section> in components/Root.tsx.
path.join(sectionDir, 'components', 'Root.tsx'),
// Single-file sections own it in <Section>.tsx (e.g. TrustedBy/TrustedBy.tsx).
path.join(sectionDir, `${sectionName}.tsx`),
path.join(sectionDir, `${sectionName}.ts`),
// Flat-primitive sections own it in a <Section>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 = [];