import { unflatten } from "flat"; import { constants } from "../../../constants"; import type { TLocalisationSource } from "../../../types/localisation"; type TLocaleSource = TLocalisationSource; type TLocaleDictionary = TLocaleSource[string]; type TLocaleValue = TLocaleDictionary[string][number]; type TFlatLocaleSource = Record; /** * Тип для одного языка в формате i18next resources. * Структура: { [ns: string]: { [key: string]: TLocaleValue } } */ type TLanguageResources = Record>; /** * Полный формат resources для i18next. * Структура: { [lang: string]: { [type: string]: TLanguageResources } } */ type TResources = Record>; /** * Проверяет, является ли значение обычным объектом (не null, не массив) * @returns {boolean} */ const isRecord = (value: unknown): value is Record => { return typeof value === "object" && value !== null && !Array.isArray(value); }; /** * Проверяет, является ли значение допустимым значением локали (строка или функция) * @returns {boolean} */ const isLocaleValue = (value: unknown): value is TLocaleValue => { return typeof value === "string" || typeof value === "function"; }; /** * Получает плоский объект ресурсов для указанного namespace. * Преобразует вложенную структуру { name: { key: value } } в плоскую { "name.key": value }. * * @param values{TLocaleSource} исходные данные локализации * @param ns{string} пространство имён * @returns {TFlatLocaleSource} плоский объект с ключами вида "name.key" * @example * const source = { * translation: { * forms: { invalidInput: ["Ошибка", "Error"] }, * buttons: { submit: ["Отправить", "Submit"] } * } * }; * getFlatResourceValues(source, "translation"); * // { "forms.invalidInput": ["Ошибка", "Error"], "buttons.submit": ["Отправить", "Submit"] } */ const getFlatResourceValues = (values: TLocaleSource = {}, ns: string = ""): TFlatLocaleSource => { const result: Record = {}; if (!ns) { return result; } const namespacedValues = values[ns]; if (!isRecord(namespacedValues)) { return result; } Object.entries(namespacedValues).forEach(([ name, value ]) => { if (!isRecord(value)) { return; } Object.entries(value).forEach(([ key, val ]) => { result[`${name}.${key}`] = val; }); }); return result; }; /** * Получает данные локализации из объекта и преобразует их в формат i18next resources. * * @param data{TLocaleSource} объект с локалями в формате { ns: { key: [lang1Value, lang2Value, ...] } } * @param type{string} тип ресурса (обычно "translation") * @param langKeys{string[]} массив кодов языков * @returns {TResources} объект ресурсов в формате i18next * @example * const data = { * translation: { * greeting: ["Привет", "Hello"], * farewell: ["Пока", "Bye"] * } * }; * getResourcesFromObj({ data, langKeys: ["ru", "en"] }); * // { ru: { translation: { translation: { greeting: "Привет", farewell: "Пока" } } }, ... } */ const getResourcesFromObj = ({ data = {}, type = "translation", langKeys = constants.defaultLangKeys, }: { data?: TLocaleSource; type?: string; langKeys?: string[]; }): TResources => { try { const locales: TResources = {}; // Инициализируем структуру для каждого языка langKeys.forEach((localeKey) => { locales[localeKey] = { [type]: {} }; }); // Заполняем данные Object.entries(data).forEach(([ ns, values ]) => { if (!isRecord(values)) { return; } Object.entries(values).forEach(([ key, value ]) => { if (!Array.isArray(value)) { return; } value.forEach((val, i) => { const currentLang = langKeys[i]; if (!currentLang) { return; } if (!isLocaleValue(val)) { return; } // Получаем хранилище типа для текущего языка const typeStorage = locales[currentLang][type]; // Получаем или создаём хранилище namespace if (!typeStorage[ns]) { typeStorage[ns] = {}; } // Устанавливаем значение typeStorage[ns][key] = val; }); }); }); return locales; } catch (e) { console.error(`[Localisation] Error while parsing locales:`, e); return {}; } }; /** * Получает пользовательские атрибуты в виде объекта с одним уровнем вложенности. * Преобразует плоский объект с ключами-путями в обычный вложенный объект. * * @param obj{Record} плоский объект с ключами-путями (например, { "style.color": "red" }) * @returns {Record} вложенный объект (например, { style: { color: "red" } }) * @example * getUserAttrs({ "style.color": "red", "style.fontSize": 16 }); * // { style: { color: "red", fontSize: 16 } } */ const getUserAttrs = (obj: Record): Record => { if (!obj || Object.keys(obj).length === 0) { return {}; } const attrs = unflatten(obj, { object: true, }); return isRecord(attrs) ? attrs : {}; }; /** * Преобразует TLanguageResources в плоский объект с составными ключами. * Например, { new: { dynamic: "value" } } преобразуется в * { "new.dynamic": "value" }. * @returns {TFlatLocaleSource} */ const getFlattenLanguageResources = ( resources: TLanguageResources ): TFlatLocaleSource => { const result: TFlatLocaleSource = {}; Object.entries(resources).forEach(([ name, values ]) => { if (!isRecord(values)) { return; } Object.entries(values).forEach(([ key, value ]) => { if (isLocaleValue(value)) { result[`${name}.${key}`] = value; } }); }); return result; }; export type { TFlatLocaleSource, TLanguageResources, TLocaleDictionary, TLocaleSource, TLocaleValue, TResources, }; export { getFlatResourceValues, getFlattenLanguageResources, getResourcesFromObj, getUserAttrs, isLocaleValue, isRecord, };