skolmer / es2015-i18n-tag

ES2015 template literal tag for i18n and l10n (translation and internationalization)
http://i18n-tag.kolmer.net
MIT License
191 stars 13 forks source link

Dynamic keys #37

Closed arthot closed 6 years ago

arthot commented 7 years ago

Hi! I'm wondering, is it possible to use es2015-i18n-tag with dynamic key? For instance:

const translations = { KEY_FROM_TRANSLATION: 'translated string' }
i18nConfig({  locales: 'en', translations })
let key = 'KEY_FROM_TRANSLATION'
let translation = i18n`${key}`

will not work, because it will pass key variable as param of template string. So do any official way to do this exists? Because right now I'm using following workaround:

let translation = i18n([key])

but maybe it worth to consider to have official simple function fallback? Because this issue also occurs with different template engines were tagged strings are not yet supported (Vue for instance) and this limit usage scope of this great project.

skolmer commented 7 years ago

Hey there,

to be honest I was at the same point in my current project. I had some text variables from an external library I wanted to translate. I think the best would be to create a translate function like you suggested. A way to use the tag functionality by calling a function. Unfortunately I don't have a lot of time right now so I will keep this task open for someone to grab.

arthot commented 7 years ago

Most probably I will have time in the next 2 weeks to participate. The problem I see is that the most intuitive way to use tag as function is occupied by groups functionality: let translation = i18n(key) will just return new function. let translation = i18n()(key) also doesn't look good. So the only solution without braking changes is a separate function like let translation = i18n.translate(key) or let translation = i18n(group).translate(key) What do you think?

skolmer commented 7 years ago

Thanks I appreciate your help. The syntax looks good. There is also a third case: let translation = i18n(translationGroup, configGroup).translate(key)

pedroteixeira commented 7 years ago

hello, justed started using the library. thanks for the work :)

any hint on the workaround for calling i18n for a dynamic key with arguments? does that make sense? I tried i18n([key, value]) work? It does not seem to be working.

skolmer commented 7 years ago

thanks @pedroteixeira 👍 @arthot do you have a workaround for this usecase?

arthot commented 7 years ago

Yes. It's not so obvious, but workaround exists. For instance you want to construct a phrase: Hi, {0}. I'm a {1} value so direct call will be: i18n(["Hi, ", ". I'm a ", " value"], ZERO_VALUE, ONE_VALUE) So the main rule is: item count in first array must equals to variables count + 1 If you want to use formatting: Hi, {0}:c. I'm a {1}:c value i18n(["Hi, ", ":c. I'm a ", ":c value"], ZERO_VALUE, ONE_VALUE) It also possible to have only variables: {0}:c i18n(["", ":c"], ZERO_VALUE)

skolmer commented 7 years ago

thanks a lot! how about a function like i18n(group, configGroup).translate('my key {0} eg.', ['value', 'format'] | 'value', ...) to support the same formatting functionality of a i18n template string in a simple function?

pedroteixeira commented 7 years ago

to have a 'oficial' library function that accepts a string template and internally does the spliting based on some regexp and accept a array of values will be really handy :)

arthot commented 7 years ago

@skolmer , in your example i18n(group, configGroup).translate('my key {0} eg.', ['value', 'format'] | 'value', ...), why do you move formatting to the variable, instead of being part of template like my key {0}:format eg.'. I suppose we should adhere to the same style like in tagged strings.

const translations = {'your phone is {0}': 'deine Telefonnummer ist {0}', 'your email is: {0}': 'deine email ist {0}'}

let dynamic_var = api.getType();
let result = i18n.translate(`your ${dynamic_var} is {0}:format`, 1234)

But we can even implement both variants, because sometimes variables can have really different meaning like string or number

pedroteixeira commented 7 years ago

Hi, I ended writing the following wrapper function:

export function translate(i18n, template, values) {
  let tokens = template.split(/\${[^}\r\n]*}/g);

  let args = [tokens];
  if(values) {
    args = args.concat(values)
  }
  return i18n.apply(null, args);
}

Some usages:

describe('text with two arguments', () => {

  const translations = {
    "Changed label from ${0} to ${1}": "Renamed from '${0}' to '${1}'"
  };

  const A = 'A';
  const B = 'B';

  describe('translating dynamic keys', () => {
    beforeEach(() => {
      i18nConfig({
        translations: translations
      });
    });

    it('i18n expects an array for the text, and the values as rest', () => {
      expect(i18n(['Changed label from ', ' to ', ''], A, B)).toEqual(
        "Renamed from 'A' to 'B'"
      );
    });

    it('translate should work just like i18n, assuming indices for arguments', () => {
      expect(translate(i18n, 'Changed label from ${0} to ${1}', [A, B])).toEqual(
        "Renamed from 'A' to 'B'"
      );
    });
  });
});
skolmer commented 7 years ago

@arthot I used this syntax because the formatting information is part of the template string but not part of the key. If you write a template string like this:

i18n`total: ${var}:n(2)` 

the translation will look like this:

{ "total: ${0}": "Summe: ${0}" }

the function could use the same key format:

i18n.translate("total: ${0}", [var, 'n(2)']) 

It will keep the key management more consistent and you can use the same key via template string or function. You will also be able to use the same key for different formats. The translation will be decoupled from formatting which brings a lot more flexibility.

we could even skip a lot of the string parsing work if we would choose a format like:

i18n.translate("total: ${0}", { value: var, formatter: 'n', format: 2 }) 
skolmer commented 6 years ago

Is someone already working on this? Please let me know if that's the case. We need this feature in our current project too and I will start with an implementation if no one has already started working on this. Thanks!

simlrh commented 6 years ago

My current workaround:

i18nConfig({
  locales: 'en-GB',
  translations: {
    variables: {
      word: 'thing',
    },
  },
  standardFormatters: {
    string: {
      i18n: (locales, stringOptions, value) => i18n('variables')([value]),
    },
  },
});

const variable = 'word';
console.log(i18n`Translate this ${variable}:s(i18n)`);

Output Translate this thing.

Downside is you can't specify a translation group for the variable (unless you create a separate formatter for every group, or your environment allows Proxies to achieve the same thing).

Could resolve this by adding a new localizer to the module:

i /*translatable variable*/: (config, v, group) => {
  return this.i18n(group, config, [v])
},

But @skolmer you said you didn't want to do this using formatting information? IMO it's preferable to have the translation key include placeholders and have the variables as separate translation keys.

skolmer commented 6 years ago

@simlrh very creative solution 👍

I just pushed the implementation I worked on. It's not well documented yet but has 100% test coverage, so it should work fine.

Release: https://github.com/skolmer/es2015-i18n-tag/releases/tag/v1.2.0 Docs: https://github.com/skolmer/es2015-i18n-tag#translating-without-template-literals

skolmer commented 6 years ago

Update: i18n-tag-schema is now able to pick up translation keys from i18n.translate()