408 lines
12 KiB
TypeScript
408 lines
12 KiB
TypeScript
import { Map, List } from 'immutable';
|
|
import { set, trimEnd, groupBy } from 'lodash';
|
|
import { Collection, Entry, EntryDraft, EntryField, EntryMap } from '../types/redux';
|
|
import { selectEntrySlug } from '../reducers/collections';
|
|
import { EntryValue } from '../valueObjects/Entry';
|
|
|
|
export const I18N = 'i18n';
|
|
|
|
export enum I18N_STRUCTURE {
|
|
MULTIPLE_FOLDERS = 'multiple_folders',
|
|
MULTIPLE_FILES = 'multiple_files',
|
|
SINGLE_FILE = 'single_file',
|
|
}
|
|
|
|
export enum I18N_FIELD {
|
|
TRANSLATE = 'translate',
|
|
DUPLICATE = 'duplicate',
|
|
NONE = 'none',
|
|
}
|
|
|
|
export const hasI18n = (collection: Collection) => {
|
|
return collection.has(I18N);
|
|
};
|
|
|
|
type I18nInfo = {
|
|
locales: string[];
|
|
defaultLocale: string;
|
|
structure: I18N_STRUCTURE;
|
|
};
|
|
|
|
export const getI18nInfo = (collection: Collection) => {
|
|
if (!hasI18n(collection)) {
|
|
return {};
|
|
}
|
|
const { structure, locales, default_locale: defaultLocale } = collection.get(I18N).toJS();
|
|
return { structure, locales, defaultLocale } as I18nInfo;
|
|
};
|
|
|
|
export const getI18nFilesDepth = (collection: Collection, depth: number) => {
|
|
const { structure } = getI18nInfo(collection) as I18nInfo;
|
|
if (structure === I18N_STRUCTURE.MULTIPLE_FOLDERS) {
|
|
return depth + 1;
|
|
}
|
|
return depth;
|
|
};
|
|
|
|
export const isFieldTranslatable = (field: EntryField, locale: string, defaultLocale: string) => {
|
|
const isTranslatable = locale !== defaultLocale && field.get(I18N) === I18N_FIELD.TRANSLATE;
|
|
return isTranslatable;
|
|
};
|
|
|
|
export const isFieldDuplicate = (field: EntryField, locale: string, defaultLocale: string) => {
|
|
const isDuplicate = locale !== defaultLocale && field.get(I18N) === I18N_FIELD.DUPLICATE;
|
|
return isDuplicate;
|
|
};
|
|
|
|
export const isFieldHidden = (field: EntryField, locale: string, defaultLocale: string) => {
|
|
const isHidden = locale !== defaultLocale && field.get(I18N) === I18N_FIELD.NONE;
|
|
return isHidden;
|
|
};
|
|
|
|
export const getLocaleDataPath = (locale: string) => {
|
|
return [I18N, locale, 'data'];
|
|
};
|
|
|
|
export const getDataPath = (locale: string, defaultLocale: string) => {
|
|
const dataPath = locale !== defaultLocale ? getLocaleDataPath(locale) : ['data'];
|
|
return dataPath;
|
|
};
|
|
|
|
export const getFilePath = (
|
|
structure: I18N_STRUCTURE,
|
|
extension: string,
|
|
path: string,
|
|
slug: string,
|
|
locale: string,
|
|
) => {
|
|
switch (structure) {
|
|
case I18N_STRUCTURE.MULTIPLE_FOLDERS:
|
|
return path.replace(`/${slug}`, `/${locale}/${slug}`);
|
|
case I18N_STRUCTURE.MULTIPLE_FILES:
|
|
return path.replace(extension, `${locale}.${extension}`);
|
|
case I18N_STRUCTURE.SINGLE_FILE:
|
|
default:
|
|
return path;
|
|
}
|
|
};
|
|
|
|
export const getLocaleFromPath = (structure: I18N_STRUCTURE, extension: string, path: string) => {
|
|
switch (structure) {
|
|
case I18N_STRUCTURE.MULTIPLE_FOLDERS: {
|
|
const parts = path.split('/');
|
|
// filename
|
|
parts.pop();
|
|
// locale
|
|
return parts.pop();
|
|
}
|
|
case I18N_STRUCTURE.MULTIPLE_FILES: {
|
|
const parts = trimEnd(path, `.${extension}`);
|
|
return parts.split('.').pop();
|
|
}
|
|
case I18N_STRUCTURE.SINGLE_FILE:
|
|
default:
|
|
return '';
|
|
}
|
|
};
|
|
|
|
export const getFilePaths = (
|
|
collection: Collection,
|
|
extension: string,
|
|
path: string,
|
|
slug: string,
|
|
) => {
|
|
const { structure, locales } = getI18nInfo(collection) as I18nInfo;
|
|
const paths = locales.map(locale =>
|
|
getFilePath(structure as I18N_STRUCTURE, extension, path, slug, locale),
|
|
);
|
|
|
|
return paths;
|
|
};
|
|
|
|
export const normalizeFilePath = (structure: I18N_STRUCTURE, path: string, locale: string) => {
|
|
switch (structure) {
|
|
case I18N_STRUCTURE.MULTIPLE_FOLDERS:
|
|
return path.replace(`${locale}/`, '');
|
|
case I18N_STRUCTURE.MULTIPLE_FILES:
|
|
return path.replace(`.${locale}`, '');
|
|
case I18N_STRUCTURE.SINGLE_FILE:
|
|
default:
|
|
return path;
|
|
}
|
|
};
|
|
|
|
export const getI18nFiles = (
|
|
collection: Collection,
|
|
extension: string,
|
|
entryDraft: EntryMap,
|
|
entryToRaw: (entryDraft: EntryMap) => string,
|
|
path: string,
|
|
slug: string,
|
|
newPath?: string,
|
|
) => {
|
|
const { structure, defaultLocale, locales } = getI18nInfo(collection) as I18nInfo;
|
|
|
|
if (structure === I18N_STRUCTURE.SINGLE_FILE) {
|
|
const data = locales.reduce((map, locale) => {
|
|
const dataPath = getDataPath(locale, defaultLocale);
|
|
return map.set(locale, entryDraft.getIn(dataPath));
|
|
}, Map<string, unknown>({}));
|
|
const draft = entryDraft.set('data', data);
|
|
|
|
return [
|
|
{
|
|
path: getFilePath(structure, extension, path, slug, locales[0]),
|
|
slug,
|
|
raw: entryToRaw(draft),
|
|
...(newPath && {
|
|
newPath: getFilePath(structure, extension, newPath, slug, locales[0]),
|
|
}),
|
|
},
|
|
];
|
|
}
|
|
|
|
const dataFiles = locales
|
|
.map(locale => {
|
|
const dataPath = getDataPath(locale, defaultLocale);
|
|
const draft = entryDraft.set('data', entryDraft.getIn(dataPath));
|
|
return {
|
|
path: getFilePath(structure, extension, path, slug, locale),
|
|
slug,
|
|
raw: draft.get('data') ? entryToRaw(draft) : '',
|
|
...(newPath && {
|
|
newPath: getFilePath(structure, extension, newPath, slug, locale),
|
|
}),
|
|
};
|
|
})
|
|
.filter(dataFile => dataFile.raw);
|
|
return dataFiles;
|
|
};
|
|
|
|
export const getI18nBackup = (
|
|
collection: Collection,
|
|
entry: EntryMap,
|
|
entryToRaw: (entry: EntryMap) => string,
|
|
) => {
|
|
const { locales, defaultLocale } = getI18nInfo(collection) as I18nInfo;
|
|
|
|
const i18nBackup = locales
|
|
.filter(l => l !== defaultLocale)
|
|
.reduce((acc, locale) => {
|
|
const dataPath = getDataPath(locale, defaultLocale);
|
|
const data = entry.getIn(dataPath);
|
|
if (!data) {
|
|
return acc;
|
|
}
|
|
const draft = entry.set('data', data);
|
|
return { ...acc, [locale]: { raw: entryToRaw(draft) } };
|
|
}, {} as Record<string, { raw: string }>);
|
|
|
|
return i18nBackup;
|
|
};
|
|
|
|
export const formatI18nBackup = (
|
|
i18nBackup: Record<string, { raw: string }>,
|
|
formatRawData: (raw: string) => EntryValue,
|
|
) => {
|
|
const i18n = Object.entries(i18nBackup).reduce((acc, [locale, { raw }]) => {
|
|
const entry = formatRawData(raw);
|
|
return { ...acc, [locale]: { data: entry.data } };
|
|
}, {});
|
|
|
|
return i18n;
|
|
};
|
|
|
|
const mergeValues = (
|
|
collection: Collection,
|
|
structure: I18N_STRUCTURE,
|
|
defaultLocale: string,
|
|
values: { locale: string; value: EntryValue }[],
|
|
) => {
|
|
let defaultEntry = values.find(e => e.locale === defaultLocale);
|
|
if (!defaultEntry) {
|
|
defaultEntry = values[0];
|
|
console.warn(`Could not locale entry for default locale '${defaultLocale}'`);
|
|
}
|
|
const i18n = values
|
|
.filter(e => e.locale !== defaultEntry!.locale)
|
|
.reduce((acc, { locale, value }) => {
|
|
const dataPath = getLocaleDataPath(locale);
|
|
return set(acc, dataPath, value.data);
|
|
}, {});
|
|
|
|
const path = normalizeFilePath(structure, defaultEntry.value.path, defaultLocale);
|
|
const slug = selectEntrySlug(collection, path) as string;
|
|
const entryValue: EntryValue = {
|
|
...defaultEntry.value,
|
|
raw: '',
|
|
...i18n,
|
|
path,
|
|
slug,
|
|
};
|
|
|
|
return entryValue;
|
|
};
|
|
|
|
const mergeSingleFileValue = (entryValue: EntryValue, defaultLocale: string, locales: string[]) => {
|
|
const data = entryValue.data[defaultLocale];
|
|
const i18n = locales
|
|
.filter(l => l !== defaultLocale)
|
|
.map(l => ({ locale: l, value: entryValue.data[l] }))
|
|
.filter(e => e.value)
|
|
.reduce((acc, e) => {
|
|
return { ...acc, [e.locale]: { data: e.value } };
|
|
}, {});
|
|
|
|
return {
|
|
...entryValue,
|
|
data,
|
|
i18n,
|
|
raw: '',
|
|
};
|
|
};
|
|
|
|
export const getI18nEntry = async (
|
|
collection: Collection,
|
|
extension: string,
|
|
path: string,
|
|
slug: string,
|
|
getEntryValue: (path: string) => Promise<EntryValue>,
|
|
) => {
|
|
const { structure, locales, defaultLocale } = getI18nInfo(collection) as I18nInfo;
|
|
|
|
let entryValue: EntryValue;
|
|
if (structure === I18N_STRUCTURE.SINGLE_FILE) {
|
|
entryValue = mergeSingleFileValue(await getEntryValue(path), defaultLocale, locales);
|
|
} else {
|
|
const entryValues = await Promise.all(
|
|
locales.map(async locale => {
|
|
const entryPath = getFilePath(structure, extension, path, slug, locale);
|
|
const value = await getEntryValue(entryPath).catch(() => null);
|
|
return { value, locale };
|
|
}),
|
|
);
|
|
|
|
const nonNullValues = entryValues.filter(e => e.value !== null) as {
|
|
value: EntryValue;
|
|
locale: string;
|
|
}[];
|
|
|
|
entryValue = mergeValues(collection, structure, defaultLocale, nonNullValues);
|
|
}
|
|
|
|
return entryValue;
|
|
};
|
|
|
|
export const groupEntries = (collection: Collection, extension: string, entries: EntryValue[]) => {
|
|
const { structure, defaultLocale, locales } = getI18nInfo(collection) as I18nInfo;
|
|
if (structure === I18N_STRUCTURE.SINGLE_FILE) {
|
|
return entries.map(e => mergeSingleFileValue(e, defaultLocale, locales));
|
|
}
|
|
|
|
const grouped = groupBy(
|
|
entries.map(e => ({
|
|
locale: getLocaleFromPath(structure, extension, e.path) as string,
|
|
value: e,
|
|
})),
|
|
({ locale, value: e }) => {
|
|
return normalizeFilePath(structure, e.path, locale);
|
|
},
|
|
);
|
|
|
|
const groupedEntries = Object.values(grouped).reduce((acc, values) => {
|
|
const entryValue = mergeValues(collection, structure, defaultLocale, values);
|
|
return [...acc, entryValue];
|
|
}, [] as EntryValue[]);
|
|
|
|
return groupedEntries;
|
|
};
|
|
|
|
export const getI18nDataFiles = (
|
|
collection: Collection,
|
|
extension: string,
|
|
path: string,
|
|
slug: string,
|
|
diffFiles: { path: string; id: string; newFile: boolean }[],
|
|
) => {
|
|
const { structure } = getI18nInfo(collection) as I18nInfo;
|
|
if (structure === I18N_STRUCTURE.SINGLE_FILE) {
|
|
return diffFiles;
|
|
}
|
|
const paths = getFilePaths(collection, extension, path, slug);
|
|
const dataFiles = paths.reduce((acc, path) => {
|
|
const dataFile = diffFiles.find(file => file.path === path);
|
|
if (dataFile) {
|
|
return [...acc, dataFile];
|
|
} else {
|
|
return [...acc, { path, id: '', newFile: false }];
|
|
}
|
|
}, [] as { path: string; id: string; newFile: boolean }[]);
|
|
|
|
return dataFiles;
|
|
};
|
|
|
|
export const duplicateI18nFields = (
|
|
entryDraft: EntryDraft,
|
|
field: EntryField,
|
|
locales: string[],
|
|
defaultLocale: string,
|
|
fieldPath: string[] = [field.get('name')],
|
|
) => {
|
|
const value = entryDraft.getIn(['entry', 'data', ...fieldPath]);
|
|
if (field.get(I18N) === I18N_FIELD.DUPLICATE) {
|
|
locales
|
|
.filter(l => l !== defaultLocale)
|
|
.forEach(l => {
|
|
entryDraft = entryDraft.setIn(
|
|
['entry', ...getDataPath(l, defaultLocale), ...fieldPath],
|
|
value,
|
|
);
|
|
});
|
|
}
|
|
|
|
if (field.has('field') && !List.isList(value)) {
|
|
const fields = [field.get('field') as EntryField];
|
|
fields.forEach(field => {
|
|
entryDraft = duplicateI18nFields(entryDraft, field, locales, defaultLocale, [
|
|
...fieldPath,
|
|
field.get('name'),
|
|
]);
|
|
});
|
|
} else if (field.has('fields') && !List.isList(value)) {
|
|
const fields = field.get('fields')!.toArray() as EntryField[];
|
|
fields.forEach(field => {
|
|
entryDraft = duplicateI18nFields(entryDraft, field, locales, defaultLocale, [
|
|
...fieldPath,
|
|
field.get('name'),
|
|
]);
|
|
});
|
|
}
|
|
|
|
return entryDraft;
|
|
};
|
|
|
|
export const getPreviewEntry = (entry: EntryMap, locale: string, defaultLocale: string) => {
|
|
if (locale === defaultLocale) {
|
|
return entry;
|
|
}
|
|
return entry.set('data', entry.getIn([I18N, locale, 'data']));
|
|
};
|
|
|
|
export const serializeI18n = (
|
|
collection: Collection,
|
|
entry: Entry,
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
serializeValues: (data: any) => any,
|
|
) => {
|
|
const { locales, defaultLocale } = getI18nInfo(collection) as I18nInfo;
|
|
|
|
locales
|
|
.filter(locale => locale !== defaultLocale)
|
|
.forEach(locale => {
|
|
const dataPath = getLocaleDataPath(locale);
|
|
entry = entry.setIn(dataPath, serializeValues(entry.getIn(dataPath)));
|
|
});
|
|
|
|
return entry;
|
|
};
|