Adding an ApolloLink

So, I’m working on implementing internationalization on a :rocket: project soon to be online.

I’m using RW’s i18n implementation and I’d like to be able to pass its language value to all of my graphql queries.
I understand ApolloLink enables a middleware functionality here so I gave it a shot:

# web/src/App.js
# [...]

<RedwoodApolloProvider graphQLClientConfig={{
          link: (rwLinks) => {
            const translationLink = new ApolloLink((operation, forward) => {
              operation.setContext(({headers = {}}) => ({
                headers: {...headers, 'expected-language': i18n.language},
              }))

              return forward(operation)
            })

            return ApolloLink.from([translationLink, ...rwLinks])
          }
        }}>
          <Routes/>
        </RedwoodApolloProvider>

The code is executed but on the server side this is nowhere to be found.
I’m using a directive to handle the heavy translation work and I would expect its context variable to reflect my modification and display expected-language:

// api/src/directives/translate/translate.ts
import {createTransformerDirective, TransformerDirectiveFunc,} from '@redwoodjs/graphql-server';

export const schema = gql`
  """
  Use @translate to transform the resolved value to return a modified result.
  """
  directive @translate(language: String) on FIELD_DEFINITION
`

const transform: TransformerDirectiveFunc = ({context, directiveArgs, resolvedValue}) => {
  console.log(context)
  // [...]
}

const translate = createTransformerDirective(schema, transform)

export default translate

console.log(context) is impervious to my any attempts.

What am I missing? Should I do it a different way?

1 Like

I have the same need. This is how I solve it right now

export const beforeQuery = ({ categoryId }) => {
  // This will be executed in the context of a component, so it's fine to
  // use hooks here
  // eslint-disable-next-line react-hooks/rules-of-hooks
  const settings = useSettingsContext()

  return {
    variables: {
      categoryId,
      lang: settings.state.language,
    },
  }
}

It’s not pretty, but it works.

Thanks for your reply @Tobbe , this is what I have indeed today but as you say it’s not pretty.
That’s for the new website for redwood, so I’m sort of desperately looking for some neat graphql usage, but I have no experience with links and haven’t managed to get them working in my researches:-/.

@simoncrypta mentioned that this could be handled as authentication in redwood, which would be another interesting thing to try but that would require framework modification to the best of my assumptions.

1 Like

Yes, the idea is we could plug the i18n context as a header for GraphQL request like it already does for Auth (see FetchConfigProvider)

So on the web side is look like this :

import i18n from './i18n'
[...]
<RedwoodApolloProvider i18n={i18n}>
    <Routes />
</RedwoodApolloProvider>

And on the API side… I don’t know yet :sweat_smile: but we got the data and directives is a good start I guess !

One question we need to answer is if we want this to be handled behind the scenes for all gql requests. Or if it should only be automatic for Cells.

It would be pretty easy to add something to the createCell HOC that would inject a i18nLanguage variable. This would

  • Keep the RW magic contained to Cells
  • Make it easy to override the language by implementing beforeQuery

But it does mean additional work is required whenever the user does any GQL stuff outside of Cells.

1 Like

But it does mean additional work is required whenever the user does any GQL stuff outside of Cells.

Which shouldn’t happen much if Rw delivers, as one would mostly use cells… Wouldn’t they?

Correct.

But I know @Chris uses useQuery a bit when working with https://react-table.tanstack.com
And I’ve used useLazyQuery a few times.

This is probably something we should discuss in the Core Team. But everyone so busy with v1 prep it’s going to be difficult to get any attention on it.

@simoncrypta What do you think? What’s the next step here?

Also, useMutation might need the language, depending on what data it returns.

For example:

const ANSWER_GAME_MUTATION = gql`
  mutation AnswerGame($input: AnswerGameInput!) {
    play: answerGame(input: $input) {
      id
      correctness
      answeredMovieId
      answeredMovie {
        id
        title
        overview
        releasedOn
        photoPath
      }
      correctMovie {
        id
        title
        overview
        releasedOn
        photoPath
        overview
      }
      possibleMovies {
        movie {
          id
          title
          releasedOn
          photoPath
        }
      }
    }
  }
`

Maybe I wanted all the overviews to be returned in the user’s language

1 Like

Also, useMutation might need the language, depending on what data it returns.

Definitely! I think we should always have something like CurrentLanguage and maybe CurrentLocalisation accessible from the API side to be able to write the right logic for any Query or Mutation in GraphQL.

@simoncrypta What do you think? What’s the next step here?

Since at the end the focus is really about the DX, I think it is worth to give a try by adding to the tutorial an unofficial bonus step with i18n and the DX we want. After that, if we have a consensus, I think it is worth to discuss if it should be part of the V1 release or not. Are you with that ? I can start something today.

1 Like

@noire.munich

I got a better understanding of what you’re trying to do with ApolloLink and Directive, and I think I finally know what’s the problem.

ApolloLink like you said works, but the response should be somewhere in the function handler inside event.headers['expected-language'] and I don’t realty know how you can get that for Directive.

However, I don’t think we need something like that for Redwood since we will never translate anything from the API.

Compare to some other framework, our API don’t serve HTML with template data, and like @danny had said to me before, every translation of string should be done in the front-end. When we got generic message from the API to translate, the best thing to do is to create the translation string with a code like on the example for failure with errorCode : Docs - Cells : RedwoodJS Docs

So at the end, we only need to specify language to query or mutate for something in the database, and in this case, I think that query parameter is the way to go.

I will try to write a continuation of the tutorial with i18n as soon I can to show every use case possible with translation and localization.

Really? Danny said that? I thought I was the only one who held that belief :sweat_smile:

But even I have had to change my mind a little bit. I have a project where I’m doing machine translation. And I think that’s best done on the API side. It’s too inefficient (and expensive) to send the same string off to Google Translate over and over again for every single user on the web side. So I do the translations once on the API side, and then save the translations to my DB so I don’t have to do them again. And another use case is if you’re doing something like a CMS where your users can provide content in multiple languages. In that case you don’t want the translations in your en.json, fr.json, etc, files. You’d want them in your database.

So my new take on this is more nuanced. I still believe you should prefer to do the translations on the web side whenever possible. But sometimes you have to do translation related things on the backend too. And when you do, you should prefer to limit it to just picking the right translation from a DB table. But I do realize there are probably exceptions to that rule too.

1 Like

In that case you don’t want the translations in your en.json , fr.json , etc, files. You’d want them in your database.

In this situation, using beforeQuery like you do right now is probably the best way !

Hum!
Finally caught up with this thread :).

I’m conflicted now :D.
I also firmly believe that any non editable/dynamic translation should be handled on the frontend side: ui text + api errors are the obvious case.
However I do not know a clean way to handle editable translation, content translation from the frontend, as @Tobbe describes it. Either it comes from the DB, or we’d allow to edit files from a backoffice interface, which I find rather unreliable.

So… I can move forward with the beforeQuery solution if we agree on it being a sound way to do it. Which leaves me with the question: what about the idea of passing localization & language as context? I thought it was pretty neat and that would make an obvious case of it.

React Context, or some other context?

Some other context, like a context binding web & api.

Update, there’s still a lot to do here and there for the launch, and this can be resolved with beforeQuery and a directive - I kept both for now.
Still opened to improvements though :+1:

1 Like