Source: core/reducers/languageReducer.es.js

/**
 * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 */

import {FieldSupport} from 'dynamic-data-mapping-form-builder';

import {
	generateInstanceId,
	generateName,
	getRepeatedIndex,
} from '../../util/repeatable.es';
import {PagesVisitor} from '../../util/visitors.es';
import {EVENT_TYPES} from '../actions/eventTypes.es';

const deleteLanguageId = (languageId, pages) => {
	const visitor = new PagesVisitor(pages);

	return visitor.mapFields((field) => {
		const {localizedValue} = field;
		const newLocalizedValue = {...localizedValue};

		delete newLocalizedValue[languageId];

		return {
			...field,
			localizedValue: newLocalizedValue,
		};
	});
};

const getLocalizedValue = ({
	defaultLanguageId,
	editingLanguageId,
	localizable,
	localizedValue,
	localizedValueEdited,
	type,
	value,
}) => {
	if (!localizable) {
		return value;
	}

	let _value;

	const defaultValue = localizedValue[defaultLanguageId];

	if (localizedValue) {
		if (localizedValue[editingLanguageId] != null) {
			if (
				Array.isArray(localizedValue[editingLanguageId]) &&
				!localizedValue[editingLanguageId]?.length &&
				!localizedValueEdited?.[editingLanguageId]
			) {
				_value = defaultValue;
			}
			else {
				_value = localizedValue[editingLanguageId];
			}
		}
		else if (defaultValue) {
			_value = defaultValue;
		}
	}

	try {
		_value = JSON.parse(_value);
	}
	catch (e) {}

	if (type === 'image') {
		try {
			return JSON.parse(value);
		}
		catch (e) {}
	}

	return _value;
};

const getLocalizedPages = (pages, defaultLanguageId, editingLanguageId) => {
	const settingsVisitor = new PagesVisitor(pages);

	return settingsVisitor.mapFields((field) =>
		FieldSupport.localizeField(field, defaultLanguageId, editingLanguageId)
	);
};

const updateFieldLanguage = ({
	availableLanguageIds,
	dataSourceType,
	defaultLanguageId,
	editingLanguageId,
	instanceId,
	name,
	options,
	settingsContext,
	type,
}) => {
	const newSettingsContext = {
		...settingsContext,
		availableLanguageIds,
		defaultLanguageId,
		pages: getLocalizedPages(
			settingsContext.pages,
			defaultLanguageId,
			editingLanguageId
		),
	};

	const newField = {
		...FieldSupport.getFieldProperties(
			newSettingsContext,
			defaultLanguageId,
			editingLanguageId
		),
		name: generateName(name, {
			instanceId: instanceId || generateInstanceId(),
			repeatedIndex: getRepeatedIndex(name),
		}),
		settingsContext: newSettingsContext,
	};

	if (
		type === 'select' &&
		dataSourceType &&
		dataSourceType.includes('data-provider')
	) {
		return {
			...newField,
			options,
		};
	}

	return newField;
};

/**
 * NOTE: This is a literal copy of the old LayoutProvider logic. Small changes
 * were made only to adapt to the reducer.
 */
export default (state, action) => {
	switch (action.type) {
		case EVENT_TYPES.LANGUAGE.CHANGE: {
			const {
				availableLanguageIds,
				defaultLanguageId: prevDefaultLanguageId,
				focusedField,
			} = state;
			const {
				defaultLanguageId = prevDefaultLanguageId,
				editingLanguageId,
				pages,
			} = action.payload;

			const visitor = new PagesVisitor(pages ?? state.pages);

			let newFocusedField = focusedField;

			const newPages = visitor.mapFields(
				({
					localizable,
					localizedValue,
					localizedValueEdited,
					value: previousValue,
					...field
				}) => {

					// When languageReducer is used in the context of the
					// Form Builder, the fields contain settingsContext
					// which we also need to update but do not exist within
					// the fields in the settingsContext structure.

					if (field.settingsContext) {
						const newField = {
							...field,
							...updateFieldLanguage({
								...field,
								availableLanguageIds,
								defaultLanguageId,
								editingLanguageId,
							}),
							value: previousValue,
						};

						if (field.fieldName === newFocusedField.fieldName) {
							newFocusedField = newField;
						}

						return newField;
					}

					return {
						value: getLocalizedValue({
							defaultLanguageId,
							editingLanguageId,
							localizable,
							localizedValue,
							localizedValueEdited,
							type: field.type,
							value: previousValue,
						}),
					};
				},
				true,
				true,
				true
			);

			return {
				defaultLanguageId,
				editingLanguageId,
				focusedField: newFocusedField,
				pages: newPages,
			};
		}
		case EVENT_TYPES.LANGUAGE.ADD: {
			const {languageId} = action.payload;
			const {availableLanguageIds} = state;

			if (availableLanguageIds.includes(languageId)) {
				return state;
			}

			return {
				availableLanguageIds: [...availableLanguageIds, languageId],
			};
		}
		case EVENT_TYPES.LANGUAGE.DELETE: {
			const {languageIds} = action.payload;
			const {availableLanguageIds, focusedField, pages} = state;

			// The TranslationManager API does not return the removed languageId
			// but the list of new locales available but the operation will always
			// be synchronous, the user removes a locale and `availableLocales` is
			// triggered and then we call the reducer, it will not be an asynchronous
			// operation where the locales are removed are accumulated and sent in
			// a batch.

			const [languageId] = languageIds;

			let newFocusedField = focusedField;

			if (newFocusedField.settingsContext) {
				newFocusedField = {
					...newFocusedField,
					settingsContext: {
						...newFocusedField.settingsContext,
						pages: deleteLanguageId(
							languageId,
							newFocusedField.settingsContext.pages
						),
					},
				};
			}

			const visitor = new PagesVisitor(pages);

			const newPages = visitor.mapPages((page) => {
				const {
					contentRenderer,
					localizedDescription,
					localizedTitle,
				} = page;

				if (contentRenderer === 'success') {
					return page;
				}

				delete localizedDescription[languageId];
				delete localizedTitle[languageId];

				return {
					...page,
					localizedDescription,
					localizedTitle,
				};
			});

			visitor.setPages(newPages);

			return {
				availableLanguageIds: availableLanguageIds.filter(
					(id) => !languageIds.includes(id)
				),
				focusedField: newFocusedField,
				pages: visitor.mapFields((field) => {
					const {settingsContext} = field;

					return {
						...field,
						settingsContext: {
							...settingsContext,
							pages: deleteLanguageId(
								languageId,
								settingsContext.pages
							),
						},
					};
				}),
			};
		}
		default:
			return state;
	}
};