lingui / js-lingui

🌍 📖 A readable, automated, and optimized (3 kb) internationalization for JavaScript
https://lingui.dev
MIT License
4.6k stars 382 forks source link

[question] How to use key and defaults with i18n.t function? #128

Closed vonovak closed 6 years ago

vonovak commented 6 years ago

hi! I'm wondering - in https://github.com/lingui/js-lingui/issues/15 you mention how to use key with the i18n.t function:

// key i18n.tcomponent.title

I'd like to ask - how do I provide a default message for the key? Thanks!

I did notice there is the _ function in https://github.com/lingui/js-lingui/blob/master/packages/lingui-i18n/src/i18n.js#L158 but I haven't seen it documented so not sure if I should use it.

A related question (perhaps more important than the first): I have a set of terms that keep popping up in many places across the app. I would like to have the ability to refer to those terms by a key. Is there a way to have one place where all these keys (and their values) are defined, and then refer those terms from the i18n.t function and from the <Trans> component?

Thank you!

tricoder42 commented 6 years ago

Hello @vonovak, yeah, this is one missing piece in the puzzle. Right now there's no way to set default message using i18n.t`component.title` syntax.

i18n._ is low-level API. It's not documented, but definitely stable and won't go anyway. All tagged template literals (i18n.t, i18n.plural and so on) are transformed to low-level i18n._ call:

type MessageParams = {
    values?: Object
    defaults?: string
}
i18n._(message: string, params: MessageParams?)

The point is, that tagged template literals are transformed into this call, where message is in ICU MessageFormat. If you want to use custom keys, message is the key and params.defaults is MessageFormat:

// Auto-generated key
i18n._('Hello {world}', { values: { world } })

// Custom key
i18n._('component.title', { values: { world }, defaults: 'Hello {world}' })

So, you can use i18n._ directly, but then you have to write message by yourself. That should be enough as a temporary workaround.

Next

I need to come up with API which supports custom keys even in template literals.

Option 1: Add wrapper function which accepts tagged template literals

i18n.id('component.title', i18n.t`Hello ${world}`)

Option 2: Polymorphic tags

// Auto-generated key
i18n.t`Hello ${world}`

// Custom key
i18n.t('component.title')`Hello ${world}`

// Custom keys for other i18n methods (plural, select, selectOrdinal, date, number)
i18n.plural('component.plural', {
  value: 42,
  one: 'Book',
  other: 'Books' 
})

What do you think? Which option seems to be more readable/logical to you?

Also, I need to publish reference documentation for lingui-i18n package!

vonovak commented 6 years ago

Thanks @tricoder42 I don't have a strong opinion on the two options you gave - option one seems to be slightly more readable at the first sight, option two is more compact and makes it easier to add the custom key, plus it's not creating a new api (/ uses the existing one). I probably like nr. 2 better.

I'm just wondering: say I have a product whose name is "Great Product"

so the plan is: I do this once (or use the Trans component in a similar fashion):

i18n.t('product.name')`Great Product`

and then in other places I use

i18n.t('product.name') // prints "Great Product"

?

Then when I have a dozen terms like this, it would be nice to have one place where the defaults are configured (eg. in some JSON file). There could be a config option for this - so js-lingui automatically goes looking for the defaults into that file. Just an idea.

tricoder42 commented 6 years ago

Yes, this is actually supported right now, but only in combination with Trans tag:

<a title={i18n.t`msg.title`})
    <Trans id="msg.title">Hello World</Trans>
</a>

The only problem would be if you define different defaults for the same key - this would raise an error.

The API maybe needs a bit of polishing. i18n.t(`id`) returns a function (template tag), so type-checkers will be confused if you use const productName = i18n.t('product.name').

Also, the situation is more complicated if message uses params:

i18n.t('key')`Hello ${world}`

// in different scope, where `world` isn't available
i18n.t('key') // missing params?

I think if you want to have your messages as DRY as possible and define defaults in one place only, you should use i18n.t where you define default and i18n._ everywhere else:

i18n.t('product')`Product Name`
i18n.t('key')`Hello ${world}`

i18n._('product')
i18n._('key', { values: { world: "Fred" })

Now the return types are consistent and it works in all cases.

Then when I have a dozen terms like this, it would be nice to have one place where the defaults are configured (eg. in some JSON file). There could be a config option for this - so js-lingui automatically goes looking for the defaults into that file. Just an idea.

How would you create this JSON file and kept it up-to-date? Seems very error-prone to write it manually. Keys should be extracted automatically, but that's already job of lingui extract command. You could simply extract messages and fill in translation field in your fallbackLocale catalog.

Other possible solutions: You can create file default.js with bunch of i18n.t calls:

// defaults.js
i18n.t('name')`Product Name`
i18n.t('variable')`Hello ${world}` // we need to define `world` variable in this scope

and then use i18n._ in the code, but again: What about messages with variables? We need to define dummy value for each variable in our messages. This seems to be more consistent:

// defaults.js
i18n._('name', { defaults: 'Product Name' })
i18n._('variable', { defaults: 'Hello {world}' })

but you have to write MessageFormat manually, which is something I'm trying to avoid in this project.

We could probably just add this functionality to existing i18nMark function:

i18nMark('name', 'Product Name')
i18nMark('variable', 'Hello {world}')

but it doesn't solve the problem with manually writing messages.

What do you think?

vonovak commented 6 years ago

Thanks for the comments. I use react (native), so I have the option of using the Trans component as well as the i18n.t function, I just wanted to get a better picture of the options.

I'll probably have a file with some very common translations, eg:

export const commonTranslations = {
  done: i18n.t`Done`,
  edit: i18n.t`Edit`,
  cancel: i18n.t`Cancel`,
  yes: i18n.t`Yes`,
  no: i18n.t`No`,
  ok: i18n.t`OK`,
};

Since I work with React Native, I sometimes can use the trans component and other times I cannot - sometimes I need to be able to get a plain string translated.

I will leave the api decisions to you, this is the first time I'm working with i18n and react, so I don't yet have the necessary amount of experience.

akkie commented 6 years ago

@tricoder42 Is there currently a method which allows to use variables with the low-level API?

const message = i18n._(
  'It\'s only allowed to download max {max} documents at the same time',
  { values: { max: 50} },
);

This doesn't work

tricoder42 commented 6 years ago

@akkie Yes, that's exactly how it's supposed to work (see this test case).

This doesn't work

What's the content of message variable? {max} placeholder isn't replaced with 50? {max} placeholder is missing completely in final message? Message isn't translated at all?

akkie commented 6 years ago

@tricoder42 Ahh, sorry!

This is the message:

It's only allowed to download max {max} documents at the same time

The placeholder will not be replaced.

Same with this example:

const name = "Fred";
console.log(i18n.t`Hello ${name}`); 
#> Hello {name}
tricoder42 commented 6 years ago

No need to apologize! This library has a huge documentation dept, I'm gonna work on it during upcoming holidays (BTW, there're old tutorial for lingui-i18n).

Do you use lingui-i18n only or in combination with lingui-react? If solo, could you please try this minimum example:

import { i18n } from 'lingui-i18n'

// Required for development only
i18n.development(require('lingui-i18n/dev'))

// load messages
i18n.load({
  en: {
    messages: {
      "It's only allowed to download max {max} documents at the same time": "It's only allowed to download max {max} documents at the same time",
    }
  }
})

// set active language
i18n.use('en')

I think the important part is i18n.development(require('lingui-i18n/dev')) when you're running in development environment.

I'll try to look at it this afternoon, but it seems that either development package isn't loaded (in dev env) or compiled message catalog isn't loaded (in production env)

akkie commented 6 years ago

I use it in combination with lingui-react and the message is contained in my catalog. I use the code within a Redux Saga which is completely decoupled from the React related code. I store the language and the catalog in my Redux store. So I'm able to fetch this data in my Saga and set it with the i18n.load and i18n.use functions, as you've it suggested. This works now with my English messages which I use as keys. But it doesn't work for German messages.

const language = 'de';
i18n.load({ [language]: { messages: { Documents: 'Dokumente' } } });
i18n.use(language);

i18n.t`Documents`
#> "Documents" instead of "Dokumente"

Will print always the English message.

tricoder42 commented 6 years ago

Use .activate(language) instead of .use(language). use is for local language switching and creates a copy of i18n object, activate changes global language:

i18n.use('de')._('Documents') // Dokumente
i18n._('Documents') // Documents

i18n.activate('de')
i18n._('Documents') // Dokumente

Do you use babel-preset-lingui-react? If not, you need to load babel-plugin-lingui-transform-js explicitly.

Also, js plugin is looking for i18n.t calls, so these examples won't work:

// Bad
const { t } = i18n
t`Documents`

// Also bad
this.props.i18n.t`Documents`

// Good
i18n.t`Documents`

Does it solve your usecase?

I'm working on lingui-i18n reference guide and tutorial, should be ready in next few weeks. Sorry for confusion!

akkie commented 6 years ago

@tricoder42 activate works as expected. Thanks for the help and the great library :+1:

tricoder42 commented 6 years ago

Fixed in @lingui/core@2.0.0. See release docs

vsnig commented 6 years ago

How should we handle the case when id has not been found in catalog? I mean not just rendering it but also doing logic like if (translated === 'NOT_FOUND') {...}

If I do const translated = i18n._("bad_id", {defaults: "NOT_FOUND"}), translated will equal to "bad_id" instead of "NOT_FOUND"

And const translated = <Trans id={"bad_id"}>NOT_FOUND</Trans> also won't work because in this case translated will equal to an object instead of translated string. (But if you put it in render it will correctly output "NOT_FOUND")

mschipperheyn commented 6 years ago

I note that this idea of the message key with defaults is not part of the reference documentation on i18n.t