aurelia/aurelia

View on GitHub
packages/__tests__/src/i18n/i18n.spec.ts

Summary

Maintainability
F
1 wk
Test Coverage
import { I18N, I18nInitOptions, Signals } from '@aurelia/i18n';
import { EventAggregator } from '@aurelia/kernel';
import { nowrap } from '@aurelia/runtime';
import { assert, MockSignaler, createFixture } from '@aurelia/testing';
import { ISignaler } from '@aurelia/runtime-html';
import i18next, { PostProcessorModule } from 'i18next';
import { Spy } from '../Spy.js';
import { createI18NContainer } from './util.js';

const translation = {
  simple: {
    text: 'simple text',
    attr: 'simple attribute'
  }
};

describe('i18n/i18n.spec.ts', function () {
  async function $createFixture(options: I18nInitOptions = {}) {
    const i18nextSpy = new Spy();
    const eaSpy: Spy = new Spy();
    const container = createI18NContainer({
      ea: eaSpy.getMock(new EventAggregator()),
      i18nextWrapper: { i18next: i18nextSpy.getMock(i18next) },
      initOptions: options,
    });
    const sut = container.get(I18N);
    await sut.initPromise;
    await sut.setLocale('en');
    return { i18nextSpy, sut, eaSpy, mockSignaler: container.get<MockSignaler>(ISignaler) };
  }

  it('initializes i18next with default options on instantiation', async function () {
    const { i18nextSpy } = await $createFixture();

    i18nextSpy.methodCalledOnceWith('init', [{
      lng: 'en',
      fallbackLng: ['en'],
      debug: false,
      plugins: [],
      rtEpsilon: 0.01,
      skipTranslationOnMissingKey: false,
    }]);
  });

  it('respects user-defined config options', async function () {
    const customization = { lng: 'de', rtEpsilon: 0.001 };
    const { i18nextSpy } = await $createFixture(customization);

    i18nextSpy.methodCalledOnceWith('init', [{
      lng: customization.lng,
      fallbackLng: ['en'],
      debug: false,
      plugins: [],
      rtEpsilon: customization.rtEpsilon,
      skipTranslationOnMissingKey: false,
    }]);
  });

  it('registers external plugins provided by user-defined options', async function () {
    const customization = {
      plugins: [
        {
          type: 'postProcessor',
          name: 'custom1',
          process: function (value: string, _key: string, _options: any, _translator: any) { return value; }
        },
        {
          type: 'postProcessor',
          name: 'custom2',
          process: function (value: string, _key: string, _options: any, _translator: any) { return value; }
        }
      ] as PostProcessorModule[]
    };
    const { i18nextSpy } = await $createFixture(customization);

    i18nextSpy.methodCalledNthTimeWith('use', 1, [customization.plugins[0]]);
    i18nextSpy.methodCalledNthTimeWith('use', 2, [customization.plugins[1]]);
  });

  [
    { input: 'simple.text', output: [{ attributes: [], value: translation.simple.text, key: 'simple.text' }] },
    { input: '[foo]simple.text', output: [{ attributes: ['foo'], value: translation.simple.text, key: 'simple.text' }] },
    { input: '[foo,bar]simple.text', output: [{ attributes: ['foo', 'bar'], value: translation.simple.text, key: 'simple.text' }] },
    { input: '[foo,bar]simple.text;[baz]simple.attr', output: [{ attributes: ['foo', 'bar'], value: translation.simple.text, key: 'simple.text' }, { attributes: ['baz'], value: translation.simple.attr, key: 'simple.attr' }] },
    { input: 'non.existent.key', output: [{ attributes: [], value: 'non.existent.key', key: 'non.existent.key' }] },
    { input: 'non.existent.key', skipTranslationOnMissingKey: true, output: [], key: 'non.existent.key' },
    { input: '[foo,bar]non.existent.key;[baz]simple.attr', skipTranslationOnMissingKey: true, output: [{ attributes: ['baz'], value: translation.simple.attr, key: 'simple.attr' }] },
  ].forEach(({ input, skipTranslationOnMissingKey, output }) =>
    it(`'evaluate' resolves key expression ${input} to ${JSON.stringify(output)}`, async function () {
      const customization = {
        resources: {
          en: { translation }
        },
        skipTranslationOnMissingKey
      };
      const { sut } = await $createFixture(customization);

      const result = sut.evaluate(input);
      assert.deepEqual(result, output);
    }));

  it('getLocale returns the active language of i18next', async function () {
    const { sut } = await $createFixture();
    assert.equal(sut.getLocale(), 'en');
  });

  it('setLocale changes the active language of i18next', async function () {
    const { sut, eaSpy, mockSignaler } = await $createFixture();
    eaSpy.clearCallRecords();
    mockSignaler.calls.splice(0);

    await sut.setLocale('de');

    eaSpy.methodCalledOnceWith('publish', [Signals.I18N_EA_CHANNEL, { newLocale: 'de', oldLocale: 'en' }]);
    const dispatchCall = mockSignaler.calls.find((call) => call[0] === 'dispatchSignal');
    assert.notEqual(dispatchCall, undefined);
    const [, args] = dispatchCall;
    assert.deepEqual(args, Signals.I18N_SIGNAL);
    assert.equal(sut.getLocale(), 'de');
  });

  describe('createNumberFormat', function () {
    it('returns Intl.NumberFormat with the active locale', async function () {
      const { sut } = await $createFixture();

      const nf = sut.createNumberFormat();
      assert.instanceOf(nf, Intl.NumberFormat);
      assert.equal(nf.resolvedOptions().locale, 'en');
    });
    it('returns Intl.NumberFormat with the given locale', async function () {
      const { sut } = await $createFixture();

      const nf = sut.createNumberFormat(undefined, 'de');
      assert.instanceOf(nf, Intl.NumberFormat);
      assert.equal(nf.resolvedOptions().locale, 'de');
    });
    it('returns Intl.NumberFormat with the given NumberFormatOptions', async function () {
      const { sut } = await $createFixture();

      const nf = sut.createNumberFormat({ currency: 'EUR', style: 'currency' });
      assert.instanceOf(nf, Intl.NumberFormat);
      const options = nf.resolvedOptions();
      assert.equal(options.currency, 'EUR');
      assert.equal(options.style, 'currency');
    });
  });

  describe('nf', function () {
    it('formats a given number as per default formatting options', async function () {
      const { sut } = await $createFixture();

      assert.equal(sut.nf(123456789.12), '123,456,789.12');
    });

    it('formats a given number as per given formatting options', async function () {
      const { sut } = await $createFixture();

      assert.equal(sut.nf(123456789.12, { style: 'currency', currency: 'EUR' }), '€123,456,789.12');
    });

    it('formats a given number as per given locale', async function () {
      const { sut } = await $createFixture();

      assert.equal(sut.nf(123456789.12, undefined, 'de'), '123.456.789,12');
    });

    it('formats a given number as per given locale and formating options', async function () {
      const { sut } = await $createFixture();

      assert.equal(sut.nf(123456789.12, { style: 'currency', currency: 'EUR' }, 'de'), '123.456.789,12\u00A0€');
    });
  });

  describe('createDateTimeFormat', function () {
    it('returns Intl.DateTimeFormat with the active locale', async function () {
      const { sut } = await $createFixture();

      const nf = sut.createDateTimeFormat();
      assert.instanceOf(nf, Intl.DateTimeFormat);
      assert.equal(nf.resolvedOptions().locale, 'en');
    });
    it('returns Intl.DateTimeFormat with the given locale', async function () {
      const { sut } = await $createFixture();

      const nf = sut.createDateTimeFormat(undefined, 'de');
      assert.instanceOf(nf, Intl.DateTimeFormat);
      assert.equal(nf.resolvedOptions().locale, 'de');
    });
    it('returns Intl.DateTimeFormat with the given DateTimeFormatOptions', async function () {
      const { sut } = await $createFixture();

      const nf = sut.createDateTimeFormat({ month: 'short', timeZoneName: 'long' });
      assert.instanceOf(nf, Intl.DateTimeFormat);
      const options = nf.resolvedOptions();
      assert.equal(options.month, 'short');
      assert.equal(options.timeZoneName, 'long');
    });
  });

  describe('df', function () {
    it('formats a given date as per default formatting options', async function () {
      const { sut } = await $createFixture();

      assert.equal(sut.df(new Date(2020, 1, 10)), '2/10/2020');
    });

    it('formats a given date as per given formatting options', async function () {
      const { sut } = await $createFixture();

      assert.equal(sut.df(new Date(2020, 1, 10), { month: '2-digit', day: 'numeric', year: 'numeric' }), '02/10/2020');
    });

    it('formats a given date as per given locale', async function () {
      const { sut } = await $createFixture();

      assert.equal(sut.df(new Date(2020, 1, 10), undefined, 'de'), '10.2.2020');
    });

    it('formats a given date as per given locale and formating options', async function () {
      const { sut } = await $createFixture();

      assert.equal(sut.df(new Date(2020, 1, 10), { month: '2-digit', day: 'numeric', year: 'numeric' }, 'de'), '10.02.2020');
    });

    it('formats a given number considering it as UNIX timestamp', async function () {
      const { sut } = await $createFixture();

      assert.equal(sut.df(0), new Date(0).toLocaleDateString('en'));
    });
  });

  describe('rt', function () {

    for (const multiplier of [1, -1]) {
      for (const value of [1, 5]) {
        it(`works for time difference in seconds - ${multiplier > 0 ? 'future' : 'past'} - ${value > 1 ? 'plural' : 'singular'}`, async function () {
          const { sut } = await $createFixture();
          const input = new Date();
          input.setSeconds(input.getSeconds() + multiplier * value);
          assert.equal(
            sut.rt(input),
            value > 1
              ? multiplier > 0 ? 'in 5 seconds' : '5 seconds ago'
              : multiplier > 0 ? 'in 1 second' : '1 second ago'
          );
        });

        it(`works for time difference in minutes - ${multiplier > 0 ? 'future' : 'past'} - ${value > 1 ? 'plural' : 'singular'}`, async function () {
          const { sut } = await $createFixture();
          const input = new Date();
          input.setMinutes(input.getMinutes() + multiplier * value);
          assert.equal(
            sut.rt(input),
            value > 1
              ? multiplier > 0 ? 'in 5 minutes' : '5 minutes ago'
              : multiplier > 0 ? 'in 1 minute' : '1 minute ago'
          );
        });

        it(`works for time difference in hours - ${multiplier > 0 ? 'future' : 'past'} - ${value > 1 ? 'plural' : 'singular'}`, async function () {
          const { sut } = await $createFixture();
          const input = new Date();
          input.setHours(input.getHours() + multiplier * value);
          assert.equal(
            sut.rt(input),
            value > 1
              ? multiplier > 0 ? 'in 5 hours' : '5 hours ago'
              : multiplier > 0 ? 'in 1 hour' : '1 hour ago'
          );
        });

        it(`works for time difference in days - ${multiplier > 0 ? 'future' : 'past'} - ${value > 1 ? 'plural' : 'singular'}`, async function () {
          const { sut } = await $createFixture();
          const input = new Date();
          input.setDate(input.getDate() + multiplier * value);
          assert.equal(
            sut.rt(input),
            value > 1
              ? multiplier > 0 ? 'in 5 days' : '5 days ago'
              : multiplier > 0 ? 'in 1 day' : '1 day ago'
          );
        });

        it(`works for time difference in months - ${multiplier > 0 ? 'future' : 'past'} - ${value > 1 ? 'plural' : 'singular'}`, async function () {
          const { sut } = await $createFixture();
          const input = new Date();
          // month time span for rt is of 30 days, therefore for February, forcing this to be January. We play fair for other months :)
          if (input.getMonth() === 1 && multiplier > 0 && value === 1) {
            input.setMonth(0);
            input.setDate(31);
            sut['now'] = () => new Date(input.getFullYear(), 0, 1).getTime();
          } else if (input.getMonth() === 2 && multiplier < 0 && value === 1) {
            input.setMonth(0);
            input.setDate(1);
            input.setHours(0);
            input.setMinutes(0);
            sut['now'] = () => new Date(input.getFullYear(), 0, 31, 23, 59).getTime();
          } else {
            input.setMonth(input.getMonth() + multiplier * value);
          }
          assert.equal(
            sut.rt(input),
            value > 1
              ? multiplier > 0 ? 'in 5 months' : '5 months ago'
              : multiplier > 0 ? 'in 1 month' : '1 month ago'
          );
        });

        it(`works for time difference in years - ${multiplier > 0 ? 'future' : 'past'} - ${value > 1 ? 'plural' : 'singular'}`, async function () {
          const { sut } = await $createFixture();
          const input = new Date();
          input.setFullYear(input.getFullYear() + multiplier * value);
          assert.equal(
            sut.rt(input),
            value > 1
              ? multiplier > 0 ? 'in 5 years' : '5 years ago'
              : multiplier > 0 ? 'in 1 year' : '1 year ago'
          );
        });
      }
    }

    for (const multiplier of [1, -1]) {
      for (const value of [7, 14]) {
        it(`works for time difference in weeks - ${multiplier > 0 ? 'future' : 'past'} - ${value > 7 ? 'plural' : 'singular'}`, async function () {
          const { sut } = await $createFixture();
          const input = new Date();
          input.setDate(input.getDate() + multiplier * value);
          assert.equal(
            sut.rt(input),
            value > 7
              ? multiplier > 0 ? 'in 2 weeks' : '2 weeks ago'
              : multiplier > 0 ? 'in 1 week' : '1 week ago'
          );
        });
      }
    }

    it(`respects given locale`, async function () {
      const { sut } = await $createFixture();
      const input = new Date();
      input.setSeconds(input.getSeconds() - 5);
      assert.equal(sut.rt(input, undefined, 'de'), 'vor 5 Sekunden');
    });

    it(`respects given options`, async function () {
      const { sut } = await $createFixture();
      const input = new Date();
      input.setSeconds(input.getSeconds() - 5);
      assert.equal(sut.rt(input, { style: 'short' }, 'de'), 'vor 5 Sek.');
    });
  });

  describe('uf', function () {
    const cases = [
      { input: '123,456,789.12' },
      { input: '123.456.789,12', locale: 'de' },
      { input: '$ 123,456,789.12' },
      { input: '123,456,789.12 foo bar' },
      { input: '- 123,456,789.12' },
    ];
    for (const { input, locale } of cases) {
      it(`returns 123456789.12 given ${input}${locale ? ` - ${locale}` : ''}`, async function () {
        const { sut } = await $createFixture();
        assert.equal(
          sut.uf(input, locale),
          input.startsWith('-') ? -123456789.12 : 123456789.12);
      });
    }
  });

  it('does not track i18n property gh - 1614 https://github.com/aurelia/aurelia/issues/1614', async function () {
    let count = 0;
    class MyEl {
      @nowrap
      i = i18next.createInstance({
        fallbackLng: 'en',
        defaultNS: 'namespace1',
        resources: {
          en: {
            namespace1: {
              key: 'k',
              key2: 'k2'
            },
          },
        }
      });
      constructor() {
        void this.i.init();
      }

      tr(key: string) {
        // this is where the main issue is
        // because inside i18next.t call
        // there's a write during a read call
        // so it causes a lot of upgrades for previously done bindings
        // without nowrap, it will be greater than 3
        if (count++ > 3) {
          throw new Error(`count++: ${count}`);
        }
        return this.i.t(key);
      }

      get c1() {
        return this.tr('key2');
      }

      get data() {
        return [
          this.c1,
          this.tr('key2'),
          this.tr('key'),
        ];
      }
    }

    const { assertText } = await createFixture
      .html('${tr(`key`)} ${data}')
      .component(MyEl)
      .build()
      .started;

    assertText('k k2,k2,k');
  });
});