fix(client): localize external curriculum structure (#67638)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sem Bauke 2026-06-01 11:47:33 +02:00 committed by GitHub
parent 94ee73951d
commit 5cd4e8abe6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 227 additions and 189 deletions

View File

@ -2,7 +2,7 @@ import path from 'path';
import fs, { readFileSync } from 'fs';
import readdirp from 'readdirp';
import { describe, test, expect } from 'vitest';
import { afterEach, describe, test, expect, vi } from 'vitest';
import {
chapterBasedSuperBlocks,
@ -10,6 +10,7 @@ import {
SuperBlockStage,
superBlockStages
} from '@freecodecamp/shared/config/curriculum';
import { Languages } from '@freecodecamp/shared/config/i18n';
import {
superblockSchemaValidator,
availableSuperBlocksValidator
@ -22,7 +23,8 @@ import {
type GeneratedChapterBasedCurriculumProps,
type ChapterBasedCurriculumIntros,
orderedSuperBlockInfo,
OrderedSuperBlocks
OrderedSuperBlocks,
readCurriculumIntros
} from './build-external-curricula-data-v2';
const VERSION = 'v2';
@ -34,6 +36,9 @@ const intros = JSON.parse(
) as CurriculumIntros;
describe('external curriculum data build', () => {
afterEach(() => {
vi.unstubAllEnvs();
});
const clientStaticPath = path.resolve(__dirname, '../../../client/static');
const validateSuperBlock = superblockSchemaValidator();
@ -274,13 +279,14 @@ describe('external curriculum data build', () => {
next: SuperBlockStage.Next
};
const stages = Object.keys(orderedSuperBlockInfo);
const info = orderedSuperBlockInfo();
const stages = Object.keys(info);
expect(stages).not.toContain('next');
expect(stages).not.toContain('upcoming');
for (const stage of stages) {
const superBlockDashedNames = orderedSuperBlockInfo[stage]?.map(
const superBlockDashedNames = info[stage]?.map(
superBlock => superBlock.dashedName
);
@ -306,4 +312,20 @@ describe('external curriculum data build', () => {
).length
).toBeGreaterThan(0);
});
test('available superblocks should use the configured curriculum locale', () => {
vi.stubEnv('CURRICULUM_LOCALE', Languages.Espanol);
const spanishOrderedSuperBlockInfo = orderedSuperBlockInfo();
const spanishIntros = readCurriculumIntros(Languages.Espanol);
const englishIntros = readCurriculumIntros(Languages.English);
expect(spanishOrderedSuperBlockInfo.core[0]).toMatchObject({
dashedName: SuperBlocks.RespWebDesignV9,
title: spanishIntros[SuperBlocks.RespWebDesignV9].title
});
expect(spanishOrderedSuperBlockInfo.core[0]?.title).not.toEqual(
englishIntros[SuperBlocks.RespWebDesignV9].title
);
});
});

View File

@ -7,6 +7,7 @@ import {
SuperBlocks,
chapterBasedSuperBlocks
} from '@freecodecamp/shared/config/curriculum';
import { availableLangs, Languages } from '@freecodecamp/shared/config/i18n';
import type { Chapter } from '@freecodecamp/shared/config/chapters';
import { getSuperblockStructure } from '@freecodecamp/curriculum/file-handler';
import {
@ -117,203 +118,218 @@ const ver = 'v2';
const staticFolderPath = resolve(__dirname, '../../../client/static');
const dataPath = `${staticFolderPath}/curriculum-data/`;
const blockIntroPath = resolve(
__dirname,
'../../../client/i18n/locales/english/intro.json'
);
const intros = JSON.parse(
readFileSync(blockIntroPath, 'utf-8')
) as CurriculumIntros;
const intros = readCurriculumIntros(getCurriculumLocale());
export const orderedSuperBlockInfo: OrderedSuperBlocks = {
[SuperBlockStage.Core]: [
{
dashedName: SuperBlocks.RespWebDesignV9,
public: true,
title: intros[SuperBlocks.RespWebDesignV9].title
},
{
dashedName: SuperBlocks.JsV9,
public: true,
title: intros[SuperBlocks.JsV9].title
},
{
dashedName: SuperBlocks.PythonV9,
public: true,
title: intros[SuperBlocks.PythonV9].title
},
{
dashedName: SuperBlocks.FrontEndDevLibsV9,
public: false,
title: intros[SuperBlocks.FrontEndDevLibsV9].title
},
{
dashedName: SuperBlocks.RelationalDbV9,
public: false,
title: intros[SuperBlocks.RelationalDbV9].title
},
{
dashedName: SuperBlocks.BackEndDevApisV9,
public: false,
title: intros[SuperBlocks.BackEndDevApisV9].title
},
{
dashedName: SuperBlocks.FullStackDeveloperV9,
public: false,
title: intros[SuperBlocks.FullStackDeveloperV9].title
}
],
export function getCurriculumLocale(): Languages {
const { CURRICULUM_LOCALE } = process.env;
[SuperBlockStage.English]: [
{
dashedName: SuperBlocks.A2English,
public: true,
title: intros[SuperBlocks.A2English].title
},
{
dashedName: SuperBlocks.B1English,
public: true,
title: intros[SuperBlocks.B1English].title
}
],
return availableLangs.curriculum.includes(CURRICULUM_LOCALE as Languages)
? (CURRICULUM_LOCALE as Languages)
: Languages.English;
}
[SuperBlockStage.Spanish]: [
{
dashedName: SuperBlocks.A1Spanish,
public: true,
title: intros[SuperBlocks.A1Spanish].title
}
],
export function readCurriculumIntros(lang: Languages): CurriculumIntros {
const blockIntroPath = resolve(
__dirname,
`../../../client/i18n/locales/${lang}/intro.json`
);
[SuperBlockStage.Chinese]: [
{
dashedName: SuperBlocks.A1Chinese,
public: false,
title: intros[SuperBlocks.A1Chinese].title
}
],
return JSON.parse(readFileSync(blockIntroPath, 'utf-8')) as CurriculumIntros;
}
[SuperBlockStage.Extra]: [
{
dashedName: SuperBlocks.TheOdinProject,
public: true,
title: intros[SuperBlocks.TheOdinProject].title
},
{
dashedName: SuperBlocks.CodingInterviewPrep,
public: false,
title: intros[SuperBlocks.CodingInterviewPrep].title
},
{
dashedName: SuperBlocks.ProjectEuler,
public: false,
title: intros[SuperBlocks.ProjectEuler].title
},
{
dashedName: SuperBlocks.RosettaCode,
public: false,
title: intros[SuperBlocks.RosettaCode].title
}
],
export function orderedSuperBlockInfo(
intros: CurriculumIntros = readCurriculumIntros(getCurriculumLocale())
): OrderedSuperBlocks {
return {
[SuperBlockStage.Core]: [
{
dashedName: SuperBlocks.RespWebDesignV9,
public: true,
title: intros[SuperBlocks.RespWebDesignV9].title
},
{
dashedName: SuperBlocks.JsV9,
public: true,
title: intros[SuperBlocks.JsV9].title
},
{
dashedName: SuperBlocks.PythonV9,
public: true,
title: intros[SuperBlocks.PythonV9].title
},
{
dashedName: SuperBlocks.FrontEndDevLibsV9,
public: false,
title: intros[SuperBlocks.FrontEndDevLibsV9].title
},
{
dashedName: SuperBlocks.RelationalDbV9,
public: false,
title: intros[SuperBlocks.RelationalDbV9].title
},
{
dashedName: SuperBlocks.BackEndDevApisV9,
public: false,
title: intros[SuperBlocks.BackEndDevApisV9].title
},
{
dashedName: SuperBlocks.FullStackDeveloperV9,
public: false,
title: intros[SuperBlocks.FullStackDeveloperV9].title
}
],
[SuperBlockStage.Legacy]: [
{
dashedName: SuperBlocks.RespWebDesignNew,
public: true,
title: intros[SuperBlocks.RespWebDesignNew].title
},
{
dashedName: SuperBlocks.JsAlgoDataStructNew,
public: false,
title: intros[SuperBlocks.JsAlgoDataStructNew].title
},
{
dashedName: SuperBlocks.FrontEndDevLibs,
public: false,
title: intros[SuperBlocks.FrontEndDevLibs].title
},
{
dashedName: SuperBlocks.DataVis,
public: false,
title: intros[SuperBlocks.DataVis].title
},
{
dashedName: SuperBlocks.RelationalDb,
public: false,
title: intros[SuperBlocks.RelationalDb].title
},
{
dashedName: SuperBlocks.BackEndDevApis,
public: false,
title: intros[SuperBlocks.BackEndDevApis].title
},
{
dashedName: SuperBlocks.QualityAssurance,
public: false,
title: intros[SuperBlocks.QualityAssurance].title
},
{
dashedName: SuperBlocks.SciCompPy,
public: false,
title: intros[SuperBlocks.SciCompPy].title
},
{
dashedName: SuperBlocks.DataAnalysisPy,
public: true,
title: intros[SuperBlocks.DataAnalysisPy].title
},
{
dashedName: SuperBlocks.InfoSec,
public: false,
title: intros[SuperBlocks.InfoSec].title
},
{
dashedName: SuperBlocks.MachineLearningPy,
public: true,
title: intros[SuperBlocks.MachineLearningPy].title
},
{
dashedName: SuperBlocks.CollegeAlgebraPy,
public: true,
title: intros[SuperBlocks.CollegeAlgebraPy].title
},
{
dashedName: SuperBlocks.RespWebDesign,
public: true,
title: intros[SuperBlocks.RespWebDesign].title
},
{
dashedName: SuperBlocks.JsAlgoDataStruct,
public: false,
title: intros[SuperBlocks.JsAlgoDataStruct].title
},
{
dashedName: SuperBlocks.PythonForEverybody,
public: true,
title: intros[SuperBlocks.PythonForEverybody].title
}
],
[SuperBlockStage.English]: [
{
dashedName: SuperBlocks.A2English,
public: true,
title: intros[SuperBlocks.A2English].title
},
{
dashedName: SuperBlocks.B1English,
public: true,
title: intros[SuperBlocks.B1English].title
}
],
[SuperBlockStage.Professional]: [
{
dashedName: SuperBlocks.FoundationalCSharp,
public: false,
title: intros[SuperBlocks.FoundationalCSharp].title
}
]
};
[SuperBlockStage.Spanish]: [
{
dashedName: SuperBlocks.A1Spanish,
public: true,
title: intros[SuperBlocks.A1Spanish].title
}
],
export const superBlockDashedNames = Object.keys(orderedSuperBlockInfo).reduce(
(acc, superBlockStage) => {
const dashedNames = orderedSuperBlockInfo[superBlockStage].map(
[SuperBlockStage.Chinese]: [
{
dashedName: SuperBlocks.A1Chinese,
public: false,
title: intros[SuperBlocks.A1Chinese].title
}
],
[SuperBlockStage.Extra]: [
{
dashedName: SuperBlocks.TheOdinProject,
public: true,
title: intros[SuperBlocks.TheOdinProject].title
},
{
dashedName: SuperBlocks.CodingInterviewPrep,
public: false,
title: intros[SuperBlocks.CodingInterviewPrep].title
},
{
dashedName: SuperBlocks.ProjectEuler,
public: false,
title: intros[SuperBlocks.ProjectEuler].title
},
{
dashedName: SuperBlocks.RosettaCode,
public: false,
title: intros[SuperBlocks.RosettaCode].title
}
],
[SuperBlockStage.Legacy]: [
{
dashedName: SuperBlocks.RespWebDesignNew,
public: true,
title: intros[SuperBlocks.RespWebDesignNew].title
},
{
dashedName: SuperBlocks.JsAlgoDataStructNew,
public: false,
title: intros[SuperBlocks.JsAlgoDataStructNew].title
},
{
dashedName: SuperBlocks.FrontEndDevLibs,
public: false,
title: intros[SuperBlocks.FrontEndDevLibs].title
},
{
dashedName: SuperBlocks.DataVis,
public: false,
title: intros[SuperBlocks.DataVis].title
},
{
dashedName: SuperBlocks.RelationalDb,
public: false,
title: intros[SuperBlocks.RelationalDb].title
},
{
dashedName: SuperBlocks.BackEndDevApis,
public: false,
title: intros[SuperBlocks.BackEndDevApis].title
},
{
dashedName: SuperBlocks.QualityAssurance,
public: false,
title: intros[SuperBlocks.QualityAssurance].title
},
{
dashedName: SuperBlocks.SciCompPy,
public: false,
title: intros[SuperBlocks.SciCompPy].title
},
{
dashedName: SuperBlocks.DataAnalysisPy,
public: true,
title: intros[SuperBlocks.DataAnalysisPy].title
},
{
dashedName: SuperBlocks.InfoSec,
public: false,
title: intros[SuperBlocks.InfoSec].title
},
{
dashedName: SuperBlocks.MachineLearningPy,
public: true,
title: intros[SuperBlocks.MachineLearningPy].title
},
{
dashedName: SuperBlocks.CollegeAlgebraPy,
public: true,
title: intros[SuperBlocks.CollegeAlgebraPy].title
},
{
dashedName: SuperBlocks.RespWebDesign,
public: true,
title: intros[SuperBlocks.RespWebDesign].title
},
{
dashedName: SuperBlocks.JsAlgoDataStruct,
public: false,
title: intros[SuperBlocks.JsAlgoDataStruct].title
},
{
dashedName: SuperBlocks.PythonForEverybody,
public: true,
title: intros[SuperBlocks.PythonForEverybody].title
}
],
[SuperBlockStage.Professional]: [
{
dashedName: SuperBlocks.FoundationalCSharp,
public: false,
title: intros[SuperBlocks.FoundationalCSharp].title
}
]
};
}
export const superBlockDashedNames = (() => {
const info = orderedSuperBlockInfo();
return Object.keys(info).reduce((acc, superBlockStage) => {
const dashedNames = info[superBlockStage].map(
superBlock => superBlock.dashedName
);
acc.push(...dashedNames);
return acc;
},
[] as SuperBlocks[]
);
}, [] as SuperBlocks[]);
})();
export function buildExtCurriculumDataV2(
curriculum: Curriculum<CurriculumProps>
@ -330,7 +346,7 @@ export function buildExtCurriculumDataV2(
);
writeToFile('available-superblocks', {
superblocks: orderedSuperBlockInfo
superblocks: orderedSuperBlockInfo()
});
for (const superBlockKey of superBlockKeys) {