workspace_getLocale.js

/**
## /workspace/getLocale
The getLocale module exports the getLocale method which is required by the getLayer and workspace modules.

@requires /utils/envReplace
@requires /utils/merge
@requires /utils/telemetry
@requires /workspace/cache
@requires /workspace/composeObj
@requires /workspace/getTemplate

@module /workspace/getLocale
*/

import envReplace from '../utils/envReplace.js';
import merge from '../utils/merge.js';
import { withSpan } from '../utils/telemetry.js';
import workspaceCache from './cache.js';
import composeObj from './composeObj.js';
import getLayer from './getLayer.js';
import getTemplate from './getTemplate.js';

/**
@function getLocale
@async

@description
Nested locales can be requested by providing an array of locale keys. The first locale key will be used to retrieve the locale object and the remaining locale keys will be merged into the first locale as nested locales.

The getLocale method will return an error if the requesting user does not have access to the locale.

@param {Object} params
@param {Object} [parentLocale] Locale will be merged into optional parentLocale to create a nested locale.
@property {string} [params.locale] Locale key.
@property {array} [params.locale] An array of locale keys to be merged as a nested locale.
@property {Object} [params.user] Requesting user.
@property {Array} [user.roles] User roles.
@property {Boolean} [params.layers] Whether to retrieve layers for the locale.

@returns {Promise<Object|Error>} JSON Locale.
*/
export default function getLocale(params, parentLocale) {
  return withSpan(
    'workspace.getLocale',
    {
      'xyz.locale': localeAttribute(params.locale),
      'xyz.locale.layers': !!params.layers,
      'xyz.locale.nested': !!parentLocale,
    },
    () => resolveLocale(params, parentLocale),
  );
}

/**
@function localeAttribute

@description
Returns the locale param as a string span attribute.

@param {string|array|object} [locale] The locale param.
@returns {string} The locale key.
*/
function localeAttribute(locale) {
  if (Array.isArray(locale)) return locale.join(',');

  if (typeof locale === 'object') return locale?.key;

  return locale;
}

/**
@function resolveLocale
@async

@description
Resolves the locale for the getLocale method.

@param {Object} params
@param {Object} [parentLocale] Locale will be merged into optional parentLocale to create a nested locale.
@returns {Promise<Object|Error>} JSON Locale.
*/
async function resolveLocale(params, parentLocale) {
  const workspace = await workspaceCache();

  let locale = await workspaceLocale(workspace, params);

  // Failed to getTemplate localeKey.
  if (locale instanceof Error) {
    return locale;
  }

  const defaults =
    !parentLocale && locale.key !== 'locale'
      ? structuredClone(workspace.locale)
      : undefined;

  if (parentLocale) {
    locale.parentRoles = parentLocale.parentRoles.length
      ? parentLocale.parentRoles
      : [parentLocale.role];
  }

  locale = await composeObj(locale, params.user, defaults);

  if (locale instanceof Error) {
    return locale;
  }

  locale.name ??= locale.key;

  locale = mergeParentLocale(locale, parentLocale);

  if (Array.isArray(params.locale) && params.locale.length > 0) {
    // Recursively call getLocale with locale as parentLocale to merge nested locales.
    locale = await getLocale(params, locale);

    // The locale must be returned to prevent processing plugins and layers on a parent locale prior to merging the nested locale.
    return locale;
  }

  if (Array.isArray(locale.keys)) {
    locale.key = locale.keys.join(',');
  }

  if (Array.isArray(locale.plugins)) {
    locale.plugins = locale.plugins.map((plugin) => envReplace(plugin));
  }

  await localeLayers(locale, params);

  return locale;
}

/**
@function workspaceLocale
@async

@description
Returns a clone of the locale for the first locale key in the params.locale. The first key is removed from a params.locale array, the remaining keys are nested locales.

The locale is looked up in the workspace.locale, the workspace.locales, or the workspace templates. A locale object provided as locale key is cloned.

@param {workspace} workspace The cached workspace.
@param {Object} params
@property {string|array|object} [params.locale] Locale key, array of locale keys, or locale object.
@returns {Promise<Object|Error>} JSON Locale or the getTemplate error.
*/
async function workspaceLocale(workspace, params) {
  if (typeof params.locale === 'string') {
    params.locale = params.locale.split(',');
  }

  let localeKey = Array.isArray(params.locale)
    ? params.locale.shift()
    : params.locale;

  localeKey ??= 'locale';

  if (typeof localeKey === 'object') {
    return structuredClone(localeKey);
  }

  let locale;

  if (localeKey === 'locale') {
    locale = structuredClone(workspace.locale);
  } else if (Object.hasOwn(workspace.locales, localeKey)) {
    locale = structuredClone(workspace.locales[localeKey]);
  } else {
    locale = await getTemplate(localeKey);
  }

  locale.key ??= localeKey;

  return locale;
}

/**
@function mergeParentLocale
@async

@description
Merges a child locale into a parent locale, composing their properties and updating the nested locale structure.

@param {Object} locale The locale object to compose.
@param {Object} [parentLocale] Optional parent locale to merge into.
*/
function mergeParentLocale(locale, parentLocale) {
  if (!parentLocale) {
    return locale;
  }

  const parentClone = structuredClone(parentLocale);

  // Only locales of a nested locales should be used for further nesting.
  delete parentClone.locales;
  delete parentClone.parentRoles;

  parentClone.keys ??= [parentClone.key];
  parentClone.keys.push(locale.key);
  parentClone.name ??= parentClone.key;

  // Compose the nested locale name.
  locale.name = `${parentClone.name}/${locale.name}`;

  if (locale.role) {
    locale.parentRoles.push(locale.role);
  }

  locale = merge(parentClone, locale);

  return locale;
}

async function localeLayers(locale, params) {
  if (!params.layers) {
    return;
  }

  if (!locale.layers) {
    return;
  }

  const layers = [];

  for (const layerKey of Object.keys(locale.layers)) {
    const layer = await getLayer({ ...params, layer: layerKey }, locale);

    if (layer instanceof Error) {
      continue;
    }

    layers.push(layer);
  }

  // Locale layers should be returned as an array.
  locale.layers = layers;
}