jeanfortheweb/translata

View on GitHub
packages/core/src/middlewares/placeholders.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { LocaleOptions } from './locale';
import { Middleware } from '..';

/**
 * Defines the signature of a value callback.
 *
 * @see withPlaceholders
 */
export interface ValueCallback<Context> {
  (values: Context): string;
}

/**
 * Defines a map of values for `withPlaceholders`
 *
 * @see withPlaceholders
 */
export type ValueMap<Context> = {
  [key: string]: string | ValueCallback<Context>;
};

/**
 * Translator options used by `withPlaceholders`
 *
 * @see withPlaceholders
 */
export interface PlaceholderOptions<
  Values extends ValueMap<Context> = {},
  Context = any
> extends LocaleOptions {
  values?: Values;
  context?: Context;
}

/**
 * Enables placeholders for translation strings. Placeholders are defined as: `{{placeholdername}}`.
 * To replace these placeholders, pass the `values` option to the translator function.
 *
 * @example
 * ```
 * const _ = createTranslator(
 *    withTranslations('en', {
 *      'example.greeting': 'Hello, {{name}}.'
 *    }),
 *    withPlaceholders()
 * );
 *
 * _('example.greeting', { values: { name: 'John Doe' }});
 * ```
 *
 * @example
 * ```
 * const _ = createTranslator(
 *    withTranslations('en', {
 *      'example.greeting': 'Hello, {{name}}. Today is {{date}}.'
 *    }),
 *    withPlaceholders({
 *      date: (context) => context.toDateString()
 *    })
 * );
 *
 * _('example.greeting', {
 *    context: new Date(),
 *    values: {
 *      name: 'John Doe',
 *    }
 * });
 * ```
 */
export function withPlaceholders(): Middleware<PlaceholderOptions>;

/**
 * Enables placeholders for translation strings including default placeholder values.
 *
 * Placeholders are defined as: `{{placeholdername}}`. To replace these placeholders,
 * pass the `values` option to the translator function.
 *
 * @example
 * ```
 * const _ = createTranslator(
 *    withTranslations('en', {
 *      'example.greeting': 'Hello, {{name}}.'
 *    }),
 *    withPlaceholders()
 * );
 *
 * _('example.greeting', { values: { name: 'John Doe' }});
 * ```
 *
 * @example
 * ```
 * const _ = createTranslator(
 *    withTranslations('en', {
 *      'example.greeting': 'Hello, {{name}}. Today is {{date}}.'
 *    }),
 *    withPlaceholders({
 *      date: (context) => context.toDateString()
 *    })
 * );
 *
 * _('example.greeting', {
 *    context: new Date(),
 *    values: {
 *      name: 'John Doe',
 *    }
 * });
 * ```
 */
export function withPlaceholders<Context>(
  values: ValueMap<Context>,
): Middleware<PlaceholderOptions<typeof values, Context>>;

export function withPlaceholders(values = {}): Middleware<PlaceholderOptions> {
  return next => (id, options) => {
    const translated = next(id, options);
    const merged = {
      ...values,
      ...options.values,
    };

    if (translated !== undefined) {
      return Object.entries(merged).reduce(
        (replaced, [name, value]) =>
          replaced.replace(
            `\{\{${name}\}\}`,
            typeof value === 'function' ? value(options.context) : value,
          ),
        translated,
      );
    }

    return translated;
  };
}

/**
 * Translator options used by `withPluralizer`.
 *
 * @see withPluralizer
 */
export interface PluralizerOptions {
  /**
   * Count for `withPluralizer`.
   */
  count?: number;
}

/**
 * Enables pluralizable translation strings.
 * The translations string has to be separated up to three times by using `||`.
 * The first section is used when the count is zero, the second one when the count is one and the third
 * is used when count is greater than one. It's not required to define all three sections.
 *
 * To determine the string to use, pass the `count` option in the translator options.
 * The `count` value is also passed to the `values` options of `withPlaceholders`, which allows placeholder
 * replacements for the count inside the sections.
 *
 * @example
 * ```
 * const _  = createTranslator(
 *    withTranslations('en', {
 *      'pluralized.cats': 'no cat || one cat || many cats'
 *    }),
 *    withPluralizer()
 * );
 *
 * _('pluralized.cats', { count: 1 });
 * ```
 * @example
 * ```
 * const _  = createTranslator(
 *    withTranslations('en', {
 *      'pluralized.cats': 'no cat || one cat || {{count}} cats'
 *    }),
 *    withPlaceholders(),
 *    withPluralizer()
 * );
 *
 * _('pluralized.cats', { count: 1 });
 * ```
 */
export function withPluralizer(): Middleware<
  PluralizerOptions & PlaceholderOptions
> {
  return next => (id, options) => {
    const translated = next(id, {
      ...options,
      values: {
        ...options.values,
        count: options.count,
      },
    });

    if (options.count !== undefined && translated !== undefined) {
      const strings = translated
        .split(/\s+\|\|\s+/)
        .map(partial => partial.trim());

      const [none] = strings;
      const [, one = none] = strings;
      const [, , many = one] = strings;

      if (options.count === 0) {
        return none;
      }

      if (options.count === 1) {
        return one;
      }

      return many;
    }

    return translated;
  };
}