import type { LocaleCode } from 'contentful'; import type { Asset, AssetRaw, ContentfulData, Entry, EntryField, EntryFieldRaw, EntryRaw, Locale, RuntimeContext, } from '../types.js'; import Listr from 'listr'; import { mapAsync } from '../lib/array.js'; import { convertToMap, getContentTypeId } from '../lib/contentful.js'; /** * Get an ordered list of locales to use for translation based on fallback locales * @param {String} code Locale code * @param {Array} locales Array of contentful locale objects * @returns {Array} E.g. ['en-US', 'en-GB', 'de-DE'] */ export const getLocaleList = (code: string | undefined, locales: Locale[] = []): string[] => { const locale = locales.find((locale) => locale.code === code); return locale ? [locale.code, ...getLocaleList(locale.fallbackCode, locales)] : []; }; /** * Try to get the localized value. Stops at the first valid locale in codes * @param {Object} field Object with values for different locales { 'de-DE': '...', 'en-US': '...' } * @param {...any} codes Array with codes generated by getLocaleList */ export const localizeField = ( field: T, ...codes: LocaleCode[] ): EntryField => { const [code, ...fallbackCodes] = codes; if (code && Object.prototype.hasOwnProperty.call(field, code)) { return field[code] as EntryField; } if (fallbackCodes.length) { return localizeField(field, ...fallbackCodes); } }; /** * Localize fields from contentful entry * @param {Array} fields Contetful fields array * @param {String} code Locale code e.g. 'de-DE' * @param {Object} data Object containing locales & content types from contentful */ export const localizeEntry = < T extends EntryRaw | AssetRaw, R extends T extends EntryRaw ? Entry : Asset, >( node: T, code: string, data: Partial, ): R => { const { locales, fieldSettings } = data; const { fields } = node; const contentType = getContentTypeId(node); const { [contentType]: settings } = fieldSettings || {}; const { code: defaultCode = 'unknown' } = (locales || []).find((locale) => locale.default) || {}; const localeCodes = getLocaleList(code, locales); const isLocalized = (key: string) => settings?.[key]?.localized ?? false; return { ...node, fields: Object.fromEntries( Object.entries(fields).map(([key, field]) => isLocalized(key) ? [key, localizeField(field, ...localeCodes)] : [key, localizeField(field, defaultCode)], ), ), } as unknown as R; }; /** * Localize all entries * @param context * @returns */ export const localize = async (context: RuntimeContext) => { const { locales, entries, assets, contentTypes, fieldSettings } = context.data; context.localized = new Map(); return new Listr( locales.map((locale) => ({ title: `${locale.code}`, async task() { const localizedAssets = await mapAsync(assets, async (asset) => localizeEntry(asset, locale.code, { locales, contentTypes, fieldSettings, }), ); const localizedEntries = await mapAsync(entries, async (entry) => localizeEntry(entry, locale.code, { locales, contentTypes, fieldSettings, }), ); context.localized.set(locale.code, { assets: localizedAssets, entries: localizedEntries, assetMap: convertToMap(localizedAssets), entryMap: convertToMap(localizedEntries), }); }, })), { concurrent: true }, ); };