[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:
@@ -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 = [];
|
||||
|
||||
Reference in New Issue
Block a user