ttag-org / ttag

:orange_book: simple approach for javascript localization
https://ttag.js.org/
MIT License
338 stars 41 forks source link

Suitable for Node/express? #190

Closed OrKoN closed 4 years ago

OrKoN commented 4 years ago

ttag looks like a great module for managing translations. I wonder if it's suitable for Node/express environment where the locale is determined on per request basis?

From this example, https://ttag.js.org/docs/typescript.html#runtime-load it looks like it's only possible to define the locale globally?

import { ngettext, msgid, t, addLocale, useLocale } from 'ttag';

const locale = process.env.LOCALE; // uk

if (locale) {
    const translationObj = require(`./${locale}.po.json`); // will load uk.po.json
    addLocale(locale, translationObj); // adding locale to ttag
    useLocale(locale); // make uk locale active
}
OrKoN commented 4 years ago

Would be creating a context per locale a canonical way to use it with Node.js? Maybe I have overlooked the documentation?

import { c } from 'ttag';

c('en').t`...`

P.S. it looks like you cannot change the locale on the context

OrKoN commented 4 years ago

I guess I could wrap the lib functions with my own and call useLocale globally before translating a key relying on the sync execution of js, but it does not sound nice. Do you have any plans to provide an API to create instances of ttag?

P.S. will string extraction work if I create a wrapper P.P.S. wrapper won't work, throws an error that variable cannot be used as a key.

AlexMost commented 4 years ago

Hi @OrKoN! Glad that you liked our lib! Unfortunately, for now, the only working solution is to use useLocale https://ttag.js.org/docs/library-api.html#uselocale per request, somewhere in your request handler. We have several SSR servers that are working in that way and seems like it works ok. I will be glad to work on a better backend API. Can you describe a little bit more about what API will be good for you? Or maybe some examples from existing solutions? Maybe some other suggestions @MrOrz @vharitonsky ?

OrKoN commented 4 years ago

@AlexMost I envision smth like in this PR https://github.com/ttag-org/ttag/pull/191 So that I can create as many instances I want:

import { TTag } from 'ttag';

const instance = TTag(); 
instance.addLocale('en');
console.log(instance.t`translate`);

I am not sure if this can be parsed by the ttag-cli though. Let me know.

Without such API, I'd need to call useLocale before every call to t. If you have some examples how you use it on the server, I'd be helpful.

AlexMost commented 4 years ago

Thanks for your effort, and sorry for the late reply! You don't need to call useLocale before every call, you can call it once per backend render. Here is an example of ttag usage on a backend, you can retrieve locale from cookie and call it once per render https://github.com/ttag-org/isomorph-example/blob/master/server/app.js#L17 (a little bit old version of ttag, but the main idea is the same). As for your PR, unfortunately, current implementation of babel-plugin-ttag will not handle that, is there are some cases that can not be implemented with the approach in my example repo?

OrKoN commented 4 years ago

@AlexMost the approach in your example only works because there is no async io. My use case is like this (and I don't render HTML, it's a JSON API )

app.get('/', async (req, res) => {
    const locale = req.cookies.locale || 'en';
    useLocale(locale);
     try {
       const data = await db.get(....);
       return res.json(data):
     } catch (err) {
        return res.json({ err:  t`my translated error message` });
    }
});

So in this case, useLocale might be called with different values while the requests are ongoing. For example, req 1 sets it to En and makes a query to the db, req 2 sets the locale to De and starts the db query, then the req1 errors and uses the locale De to render the error. The only way to avoid it is to synchronously call useLocale before every use of the t helper.

AlexMost commented 4 years ago

Ah, I've got your point. For now, we can configure babel-plugin-ttag to discover t globally (without import), so we can use destructuring after that and use t for the specific ttag instance. I guess addLocale in your case can be set globally? What do you think about this API?

import { Ttag, addLocale } from 'ttag'

addLocale('en', enLocaleData);

app.get('/', async (req, res) => {
    const locale = req.cookies.locale || 'en';
    const { t } = new Ttag(locale);

     try {
       const data = await db.get(....);
       return res.json(data):
     } catch (err) {
        return res.json({ err:  t`my translated error message` });
    }
});

So, the key idea is to have a t that is bound to the specific locale.

OrKoN commented 4 years ago

For now, we can configure babel-plugin-ttag to discover t globally (without import), so we can use destructuring after that and use t for the specific ttag instance

This sounds complicated but if that works reliably it'd be awesome. For the API, it'd be great to allow all the same functions to be available on an instance as well as globally. It'd be perfectly fine for me to do it like this:

import { Ttag } from 'ttag'

app.use((req, res, next) => {
    const locale = req.cookies.locale || 'en';
    req.state.ttag = new Ttag();
    req.state.ttag.addLocale(locale, data[locale]);
    next();
})
app.get('/', async (req, res) => {
    const { t } = req.state.ttag;
     try {
       const data = await db.get(....);
       return res.json(data):
     } catch (err) {
        return res.json({ err:  t`my translated error message` });
    }
});

but const { t } = new Ttag(locale); would also work.

AlexMost commented 4 years ago

Ok, that sounds reasonable. I will try to check that today and release the new version with an example repo.

MrOrz commented 4 years ago

Hi @OrKoN! Glad that you liked our lib! Unfortunately, for now, the only working solution is to use useLocale https://ttag.js.org/docs/library-api.html#uselocale per request, somewhere in your request handler. We have several SSR servers that are working in that way and seems like it works ok. I will be glad to work on a better backend API. Can you describe a little bit more about what API will be good for you? Or maybe some examples from existing solutions? Maybe some other suggestions @MrOrz @vharitonsky ?

We run one SSR server for each locale, and use nginx to redirect browsers in different languages to different locales.

Nginx config: https://github.com/cofacts/rumors-deploy/blob/master/volumes/nginx/sites-enabled/rumors-site#L42

For us we only have 2 locales to support, so it is OK to run 2 copy of SSR website (built with different locales) on the same time. Not sure if it suits @OrKoN 's case.

matpen commented 4 years ago

I think that @OrKoN makes a good point. I independently discovered this problem, and came to this issue. Since the issue is still open, I thought had not been addressed yet, and started to look for an alternative library.

Luckily I decided to have a look at the code, because it turns out that the problem is indeed solved at least on version 1.7.22:

// Require the module
const { TTag } = require('ttag');

// Setup a demo locale (adapted from the quickstart tutorial)
const ukLocale = {
  'headers': {
    'plural-forms': 'nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);'
  },
  'translations': {
    '': {
      'Current time is: ${time}': {
        'msgid': 'Current time is: ${time}','msgstr': ['Поточний час:']
      },
    }
  }
}

// Setup two instances of TTag, configured with different locales
const ttag1 = new TTag();
const ttag2 = new TTag();
const { t : t1 } = ttag1;
const { t : t2 } = ttag2;
ttag2.addLocale('uk', ukLocale);
ttag1.useLocale('en');
ttag2.useLocale('uk');

// We can now use the locales at the same time
const time = 123;
console.log(t1`Current time is: ${time}`);
console.log(t2`Current time is: ${time}`);

So my suggestions here would be to

Thanks again to the devs for working on this!

OrKoN commented 4 years ago

@matpen I believe it works but the ttag-cli is probably not supporting this style. I.e., you won't be able to automatically generate translation files if you use custom variable names like t1 an t2. If it works with ttag-cli too, it's awesome and then the issue is solved.

matpen commented 4 years ago

Oh no worries: there is absolutely no need to rename the variables like I did above. That was only for the sake of the example, demonstrating that you can use two different instances (configured in different ways) in the same context (same function).

However, in normal cases you will only be dealing with one variable per context, for example like you showed in your code: const { t } = req.state.ttag;. This means that you can keep the "usual" approach and the usual function and tag names, because they will not conflict with anything else.

For completeness, it shall be mentioned that you can make the extraction work by using the --discover flag (docs here): e.g. ttag extract --discover="t1,t2" myfile.js.

AlexMost commented 4 years ago

@matpen thanks for help, we definitely should have a doc for that. I have created issue for the doc - https://github.com/ttag-org/ttag/issues/195. And closing this ticket.