Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 97 additions & 17 deletions api/src/services/contentful.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,36 @@ const mapLocales = ({ masterLocale, locale, locales, isNull = false }: any) => {
}
}

function resolveEntryFieldKey(entry: Record<string, unknown>, baseKey: string): string | undefined {
if (baseKey in entry) return baseKey;
const snake = baseKey.replace(/([A-Z])/g, (m) => `_${m.toLowerCase()}`);
if (snake in entry) return snake;
return undefined;
}

/**
* Maps Contentful content type id → field id → whether that field is localized in the export schema.
* Used so we only fan out values for fields with `localized: false`, not for localized fields that
* happen to have a single locale in the entry (missing translations).
*/
function buildContentfulFieldLocalizedByContentType(
contentTypesFromPackage: any[]
): Map<string, Map<string, boolean>> {
const byCt = new Map<string, Map<string, boolean>>();
for (const ct of contentTypesFromPackage ?? []) {
const ctId = ct?.sys?.id;
if (!ctId) continue;
const byField = new Map<string, boolean>();
for (const f of ct?.fields ?? []) {
if (f?.id != null) {
byField.set(f.id, f.localized === true);
}
}
byCt.set(ctId, byField);
}
return byCt;
}

const transformCloudinaryObject = (input: any) => {
const result: any = [];
if (!Array.isArray(input)) {
Expand Down Expand Up @@ -777,6 +807,7 @@ const createEntry = async (packagePath: any, destination_stack_id: string, proje
const data = await fs.promises.readFile(packagePath, "utf8");
const entries = JSON.parse(data)?.entries;
const content = JSON.parse(data)?.contentTypes;
const cfFieldLocalizedByCt = buildContentfulFieldLocalizedByContentType(content);
const LocaleMapper = { masterLocale: project?.master_locale ?? LOCALE_MAPPER?.masterLocale, ...project?.locales ?? {} };
if (entries && entries.length > 0) {
const assetId = await readFile(assetsSave, ASSETS_SCHEMA_FILE) ?? [];
Expand Down Expand Up @@ -814,7 +845,7 @@ const createEntry = async (packagePath: any, destination_stack_id: string, proje
entryData[name][lang] ??= {};
entryData[name][lang][id] ??= {};
locales.push(lang);
const fieldData = currentCT?.fieldMapping?.find?.((item: any) => (key === item?.uid) && (!["text", "url"]?.includes?.(item?.backupFieldType)));
const fieldData = currentCT?.fieldMapping?.find?.((item: any) => key === item?.uid);
const newId = fieldData?.contentstackFieldUid ?? `${key}`?.replace?.(/[^a-zA-Z0-9]+/g, "_");
entryData[name][lang][id][newId] = processField(
langValue,
Expand Down Expand Up @@ -860,18 +891,73 @@ const createEntry = async (packagePath: any, destination_stack_id: string, proje
);
});
});

// Non-localized Contentful fields (`localized: false` in the content type) only appear under
// one locale in exports. Copy them to every other locale branch so each slice is complete.
// Do not infer non-localized-ness from a single locale key — localized fields can legitimately
// have only one locale when translations are missing.
const entryLocaleKeys = new Set<string>();
for (const [, v] of Object?.entries?.(fields)) {
for (const lang of Object.keys(v as object)) {
entryLocaleKeys.add(lang);
}
}
const ct = contentTypes?.find((c: any) => c?.otherCmsUid === name);
for (const [key, value] of Object?.entries?.(fields)) {
const langs = Object?.keys(value as object);
if (langs?.length !== 1) continue;
const fd = ct?.fieldMapping?.find?.((item: any) => key === item?.uid);
const localizedInCf = cfFieldLocalizedByCt.get(name)?.get(key);
const explicitlyNonLocalized =
localizedInCf === false ||
(localizedInCf === undefined && fd?.advanced?.nonLocalizable === true);
if (!explicitlyNonLocalized) continue;
const srcLang = langs[0];
const newId = fd?.contentstackFieldUid ?? `${key}`?.replace?.(/[^a-zA-Z0-9]+/g, "_");
const srcEntry = entryData[name][srcLang]?.[id] as Record<string, unknown> | undefined;
if (!srcEntry) continue;
const fk = resolveEntryFieldKey(srcEntry, newId);
if (fk === undefined) continue;
for (const tgtLang of entryLocaleKeys) {
if (tgtLang === srcLang) continue;
entryData[name][tgtLang] ??= {};
entryData[name][tgtLang][id] ??= {};
const tgt = entryData[name][tgtLang][id] as Record<string, unknown>;
if (tgt[fk] === undefined) {
tgt[fk] = srcEntry[fk];
}
}
}

return entryData;
},
{}
);
for await (const [newKey, values] of Object.entries(result)) {
const currentCT = contentTypes?.find((ct: any) => ct?.otherCmsUid === newKey);
const ctName = currentCT?.contentstackUid in mapperKeys ?
mapperKeys?.[currentCT?.contentstackUid] : (currentCT?.contentstackUid ?? newKey.replace(/([A-Z])/g, "_$1").toLowerCase());
for await (const [localeKey, localeValues] of Object.entries(
values as { [key: string]: any }
)) {
const localeCode = mapLocales({ masterLocale: master_locale, locale: localeKey, locales: LocaleMapper, isNull: true });
mapperKeys?.[currentCT?.contentstackUid] : (currentCT?.contentstackUid ?? newKey?.replace?.(/([A-Z])/g, "_$1")?.toLowerCase?.());
const valuesByCfLocale = values as { [key: string]: { [uid: string]: Record<string, unknown> } };
const mergedByDestinationLocale: { [localeCode: string]: { [uid: string]: Record<string, unknown> } } = {};
for (const localeKey of Object.keys(valuesByCfLocale)) {
const localeValues = valuesByCfLocale[localeKey];
if (!localeValues) continue;
const localeCode = mapLocales({
masterLocale: master_locale,
locale: localeKey,
locales: LocaleMapper,
isNull: true,
});
if (!localeCode) continue;
mergedByDestinationLocale[localeCode] ??= {};
for (const [uid, entry] of Object.entries(localeValues)) {
mergedByDestinationLocale[localeCode][uid] = {
...(mergedByDestinationLocale[localeCode][uid] ?? {}),
...(entry ?? {}),
};
}
}
for await (const [localeCode, localeValues] of Object.entries(mergedByDestinationLocale)) {
const chunks = makeChunks(localeValues);
for (const [entryKey, entryValue] of Object.entries(localeValues)) {
const message = getLogMessage(
Expand All @@ -883,18 +969,12 @@ const createEntry = async (packagePath: any, destination_stack_id: string, proje
}
const refs: { [key: string]: any } = {};
let chunkIndex = 1;
if (localeCode) {
const filePath = path.join(
entriesSave,
ctName,
localeCode
);
for await (const [chunkId, chunkData] of Object.entries(chunks)) {
refs[chunkIndex++] = `${chunkId}-entries.json`;
await writeFile(filePath, `${chunkId}-entries.json`, chunkData);
}
await writeFile(filePath, ENTRIES_MASTER_FILE, refs);
const filePath = path.join(entriesSave, ctName, localeCode);
for await (const [chunkId, chunkData] of Object.entries(chunks)) {
refs[chunkIndex++] = `${chunkId}-entries.json`;
await writeFile(filePath, `${chunkId}-entries.json`, chunkData);
}
await writeFile(filePath, ENTRIES_MASTER_FILE, refs);
}
}
} else {
Expand Down
9 changes: 5 additions & 4 deletions ui/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"sass": "^1.68.0",
"socket.io-client": "^4.7.5",
"typescript": "^4.9.5",
"vite": "^7.3.1",
"vite": "^7.3.2",
"vite-tsconfig-paths": "^6.1.1"
},
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/ContentMapper/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ const Fields: MappingFields = {
'taxonomy':{
label: 'Taxonomy',
options: {'Taxonomy':'taxonomy'},
type:'taxonomy'
type:''
}
}
type contentMapperProps = {
Expand Down
Loading