Source

adminjs/src/utils/translate-functions.factory.ts

import { i18n as I18n, TFunction, TOptions } from 'i18next'
import startCase from 'lodash/startCase'

/**
 * @memberof TranslateFunctions
 * @alias TranslateFunction
 */
export type TranslateFunction = (
  /**
   * kwy which should be translated in a given namespace
   */
  key: string,
  /**
   * Optional resourceId or [Translate options]{@link https://www.i18next.com/overview/configuration-options}
   */
  resourceId?: string | TOptions,
  /**
   * [Translate options]{@link https://www.i18next.com/overview/configuration-options}
   */
  options?: TOptions
) => string

/**
 * Translate Functions are the helper functions which you can use to translate
 * your application.
 *
 * On the fronted they can be used with {@link useTranslation} hook. On the backend
 * they are injected to any {@link AdminJS} instance and {@link ActionContext}.
 */
export interface TranslateFunctions {
  /**
   * shortcut for I18n.translate function.
   * @see https://www.i18next.com/overview/api#t
   */
  t: TFunction;
  /**
   * I18n.translate function.
   * @see https://www.i18next.com/overview/api#t
   */
  translate: TFunction;
  /**
   * Shortcut for {@link TranslateFunctions#translateAction}
   */
  ta: TranslateFunction;
  /**
   * Translates all [actions]{@link Action}, to be more specific - their labels.
   * By default, it looks for a [translation key]{@link LocaleTranslations} in
   * `resource.{resourceId}.actions.{actionName}`, when it doesn't find
   * that, the lookup is moved to `actions.{actionName}`.
   * Finally, when that also fails, it returns startCase of the action name.
   */
  translateAction: TranslateFunction;
  /**
   * Shortcut for {@link TranslateFunctions#translateButton}
   */
  tb: TranslateFunction;
  /**
   * Translates all buttons.
   * By default, it looks for a [translation key]{@link LocaleTranslations} in
   * `resource.{resourceId}.buttons.{actionName}`, when it doesn't find
   * that, the lookup is moved to `buttons.{actionName}`.
   * Finally, when that also fails, it returns startCase of the given button name.
   */
  translateButton: TranslateFunction;
  /**
   * Shortcut for {@link TranslateFunctions#translateLabel}
   */
  tl: TranslateFunction;
  /**
   * Translates all labels. Most of all all resource names are treated as labels.
   * Also, labels are texts in the user interface which cannot be recognized
   * as any other type.
   * By default, it looks for a [translation key]{@link LocaleTranslations} in
   * `resource.{resourceId}.labels.{actionName}`, when it doesn't find
   * that, the lookup is moved to `labels.{actionName}`.
   * Finally, when that also fails, it returns startCase of the given label.
   */
  translateLabel: TranslateFunction;
  /**
   * Shortcut for {@link TranslateFunctions#translateProperty}
   */
  tp: TranslateFunction;
  /**
   * Translates all the property names.
   * By default, it looks for a [translation key]{@link LocaleTranslations} in
   * `resource.{resourceId}.properties.{propertyPath}`, when it doesn't find
   * that, the lookup is moved to `properties.{propertyPath}`. When that fails,
   * it returns startCase of the given property name.
   *
   * What is important here is that you can put nested property as well, In that
   * case you have to pass dotted path:
   *
   * ```javascript
   * {
   *   properties: {
   *      parent: 'parent property',
   *      'parent.nested': 'nested property'
   *   }
   * }
   * ```
   */
  translateProperty: TranslateFunction;
  /**
   * Shortcut for {@link TranslateFunctions#translateMessage}
   */
  tm: TranslateFunction;
  /**
   * Translates all the messages in the application.
   * By default, it looks for a [translation key]{@link LocaleTranslations} in
   * `resource.{resourceId}.messages.{messageName}`, when it doesn't find
   * that, the lookup is moved to `messages.{messageName}`.
   * Finally, when that also fails, it returns startCase of the given message name.
   */
  translateMessage: TranslateFunction;
}

export const formatName = (name: string): string => name.split('.').join('.')

const translate = (
  i18n: I18n,
  key: string,
  name: string,
  resourceId?: string | TOptions,
  options?: TOptions,
): string => {
  const realOptions: TOptions = (typeof resourceId === 'string' ? options : resourceId) || {}
  const formattedName = formatName(name)
  let keys = [`${key}.${formattedName}`]
  if (resourceId) {
    keys = [`resources.${resourceId}.${key}.${formattedName}`, ...keys]
  }
  if (i18n.exists(keys)) {
    return i18n.t(keys, realOptions)
  }
  return realOptions.defaultValue ?? startCase(name)
}

export const createFunctions = (i18n: I18n): TranslateFunctions => {
  const translateAction: TranslateFunction = (actionName, resourceId, options) => (
    translate(i18n, 'actions', actionName as string, resourceId, options)
  )

  const translateButton: TranslateFunction = (
    buttonLabel, resourceId, options,
  ) => (
    translate(i18n, 'buttons', buttonLabel, resourceId, options)
  )

  const translateLabel: TranslateFunction = (label, resourceId, options) => (
    translate(i18n, 'labels', label as string, resourceId, options)
  )

  const translateProperty: TranslateFunction = (propertyName, resourceId, options) => (
    translate(i18n, 'properties', propertyName, resourceId, options)
  )

  const translateMessage: TranslateFunction = (messageName, resourceId, options) => (
    translate(i18n, 'messages', messageName, resourceId, options)
  )

  return {
    translateAction,
    ta: translateAction,
    translateButton,
    tb: translateButton,
    translateLabel,
    tl: translateLabel,
    translateProperty,
    tp: translateProperty,
    translateMessage,
    tm: translateMessage,
    t: i18n.t,
    translate: i18n.t,
  }
}