next-intl icon indicating copy to clipboard operation
next-intl copied to clipboard

Enable usage in backend code

Open stefnnn opened this issue 3 years ago • 8 comments

Is your feature request related to a problem? Please describe. I would like to use next-intl in backend code, specifically I'd like to send an email based on the users language. There is no react rendering and hence no context with the translations available. If I understand correctly, then #40 still involved react rendering (although to a pdf).

Describe the solution you'd like Ideally I'd like to setup some intl context statically in the backend and then in an api route load messages based on the current users language and have a translation function available.

Describe alternatives you've considered Probably using https://formatjs.io/docs/intl could work, but I'm afraid there's quite a bit to rebuild, since e.g. the message lookup functionality is implemented in the useTranslations hook.

Additional context Thanks for working on this library :-)

stefnnn avatar Aug 04 '21 06:08 stefnnn

That's an interesting request. I think this could be doable, I just had a look through the code base to get an overview of the required parameters for all the functions. Some parameters are passed directly to the functions generated by the two hooks while others are read from context and are then put in the hook closure.

In detail these are:

useTranslations

  • t(key, values, formats) with these variables from the closure:
    • messages
    • Static namespace
    • onError
    • getMessageFallback
    • locale
    • global formats
    • timeZone
    • (caching implemented with React APIs)

useIntl

  • formatDateTime(value, formatOrOptions) with these variables from the closure:

    • timeZone
    • locale
    • onError
    • global formats
  • formatNumber(value, formatOrOptions) with these variables from the closure:

    • locale
    • global formats
    • onError
  • formatRelativeTime(date, now) with these variables from the closure:

    • global now
    • locale
    • onError

So to enable this, I guess we could offer a factory function which receives the variables that need to be available in the closure and that generates the necessary functions in return.

E.g.:

const t = createTranslator({namespace: 'foo', locale: 'en', messages: {'bar': 'Bar'}});
t('bar');

const intl = createIntl({locale: 'en', timeZone: 'Europe/Vienna'});
intl.formatNumber(1000);

Would something like this work for your use case?

Ideally we'd reuse these factory functions in the existing hooks then to avoid code duplication. The translation function also has some caching that is implemented with useRef – that should continue to work. Also there shouldn't be any regressions in terms of bundle size or performance, so this is something that needs to be investigated during the implementation. The factory functions should be split the same ways as the currently existing hooks, so tree shaking can continue to work.

If this works, the new lower level APIs could even be provided as a standalone library, potentially for other frameworks to use.

amannn avatar Aug 04 '21 12:08 amannn

Thanks @amannn for the swift reply 😀. This looks pretty much like what I would need, except that I think createTranslator would also need messages, right? This sounds like quite a refactoring, so there might be simpler way to use translations with the same json format in the backend. Bundle size and performance would be of lesser concern in the backend. Maybe there's a more straightforward path using formatjs?

stefnnn avatar Aug 04 '21 12:08 stefnnn

I think createTranslator would also need messages, right?

True, good point! I've added it above.

Yep, it's indeed a bit of a refactoring. I'd definitely be open to a contribution here if you're interested, or if you need it for a commercial project and have a strong need, you can hire me to take care of the refactoring (I think it would be around 4h of work) 😉 .

Maybe there's a more straightforward path using formatjs?

You can of course also use functions from formatjs directly, but beware that there might be some tricky parts (see e.g. https://github.com/amannn/next-intl/pull/45).

amannn avatar Aug 04 '21 12:08 amannn

Right now I neither have the time to contribute nor the resources to fund you. I will look for lightweight workarounds, as currently this is really only used in one spot of our codebase. If there's more interest here in this issue or our own needs grow, I may be able to contribute at a later time. Thanks again for your time to analyze!

stefnnn avatar Aug 04 '21 17:08 stefnnn

Ok, sure – sounds good! Let's leave this issue open for now then.

amannn avatar Aug 05 '21 05:08 amannn

David Brands from Marvia reached out to me to discuss the feature. He brought up the point that it's questionable how t.rich would be supported for usage in a non-React Node.js context.

I think the options are:

  1. Not support it at all.
  2. Return a ReactNode and have the consumer call renderToString on the result.
  3. Call renderToString internally.
  4. Use a separate entry point like import {createTranslator} from 'next-intl/server'; where a separate API is used for t.rich, that works with strings. E.g. t.rich('richText', {b: (children) => '<b>${children}</b>'}) (the returned string would use backticks in real code).

(2) and (3) are both not so ideal in my opinion, as (2) is a bit cumbersome to handle and (3) would require an import from the server bundle of React (need to be careful with tree shaking, or use a different entry point). I was in fact wondering if the base implementation resulting from this could be useful to other frameworks as well and therefore avoiding a React dependency for this part could be quite helpful. (2) would actually be the default handling, but it could lead to errors where React components are accidentally stringified, so I'd consider throwing an error here.

So I think either (1) or (4) might be the way to go here.

amannn avatar Oct 04 '22 12:10 amannn

Thank you @amannn for the detailed comment.

As we do have immediate need for this feature, option 4 would be our preference. We currently do not necessarily need to render ReactNode's but instead, as shown in your example, regular HTML tags such as <b>. So that would be fine for us.

I was in fact wondering if the base implementation resulting from this could be useful to other frameworks as well and therefore avoiding a React dependency for this part could be quite helpful.

I also figured that this would likely turn out to be more of a "framework agnostic" approach, but I think that would actually be great.

dbrxnds avatar Oct 04 '22 13:10 dbrxnds

I think that's the right way to go, yes. After all providing the rich text formatters is really bound to the view layer and as we're leaving the React space here, the string interpolation might be the right choice to keep it simple.

amannn avatar Oct 05 '22 15:10 amannn