facebook / relay

Relay is a JavaScript framework for building data-driven React applications.
https://relay.dev
MIT License
18.41k stars 1.83k forks source link

Localizing query results #1411

Closed Naoto-Ida closed 8 years ago

Naoto-Ida commented 8 years ago

(*This has been asked on SO over a week ago, but thought it be better to go to the source). What is the recommended way of localizing results, and how would I pass a locale?

We have a multilingual system, which means that the results are localized. So we have an argument called lang which is passed down to the field resolvers. Queries are written as such:

query {
  viewer(lang: ja) {
    books { <- Type is `Book`
      id
      title <- This would be localized to Japanese.
    }
  }
}

Which would return:

{
  "data": {
    "viewer": {
      "books": [
        {
          "id": "SOMETHINGSOMETHING123",
          "name": "ハリー・ポッター" // "Harry Potter" in Japanese.
        }
      ]
    }
  }
}

This is possible due to the resolver for translatable fields looking like this (note that I am using dataloader):

...
resolve: (parent, args, ast, { rootValue }) => {
  const { language } = rootValue
  const { BookLoader } = rootValue.loaders
  return BookLoader.TranslationsLoader(language).load(parent.id).then(translations => translations[parent.id].title || parent.title)
}

If a translation exists, it will use that instead of the original value. This was working fine until we introduced Relay into our project.

We want to utilize Relay's node, and have made it partially work, i.e.:

query {
  node(id: "SOMETHINGSOMETHING123") {
    ...b
  }
}
fragment b on Book {
  id
  name
}

But, since using node can't take arguments like viewer can, there is no way to pass the language.

How do you guys at Facebook deal with language in general, regardless of using Relay with GraphQL?

josephsavona commented 8 years ago

投稿してくれてありがとうございます!

This is a great question. I believe that the way we handle this is implicitly. As in, both the authenticated user and their language preference are part of the GraphQL executor context, as opposed to being passed explicitly as arguments to any field.

Cc @dschafer - any suggestions here?

dschafer commented 8 years ago

As in, both the authenticated user and their language preference are part of the GraphQL executor context, as opposed to being passed explicitly as arguments to any field.

Yep, that's exactly what we do, and we haven't run into too many issues.

nodkz commented 8 years ago

First of all let fix args names in your resolver(obj, args, context, info):

resolve: (parent, args, context, { rootValue }) => {
  const { language } = rootValue
  const { BookLoader } = rootValue.loaders
  return BookLoader.TranslationsLoader(language).load(parent.id).then(translations => translations[parent.id].title || parent.title)
}

DO NOT use rootValue for session data (request). rootValue is same for all requests, but context not. So use context for storing language value per request.

To solve your problem write Viewer.resolve in such manner:

resolve: (obj, args, context, info) => {
  context.language = args.lang || 'en';
  return {}; // or may return somedata due your bussiness logic
} 

Viewer resolver should write language to context.

So after that all underlying resolvers should get lang via context:

resolve: (parent, args, context, { rootValue }) => {
  const { language } = context;
  const { BookLoader } = rootValue.loaders
  return BookLoader.TranslationsLoader(language).load(parent.id).then(translations => translations[parent.id].title || parent.title)
}
Naoto-Ida commented 8 years ago

Thank you guys for the input (and the Japanese too!).

I think the first thing I need to confirm is my understanding of viewer. In Facebook's case, viewer is a representation of the user, correct? And how would you go about passing "who" the user is? And if theres a generic list of articles, but targeted articles based on who the viewer is, how would one go about doing that?

// General list of articles
{
  articles {
    id
    title
  }
}

{
  viewer { // knows its me
    articles { // articles based on my 'liked' artist on our services
      id
      title
    }
  }
}

Sorry for the mountain of questions. My team is very excited about the possibilities, but am worried about our implementation of things being uncommon, so we want to get it right. Would love to get 1:1 with one of your for about 30 min. if any of you are gracious enough.

What do you do in cases where they aren't logged in? Our user base is mainly Japanese, but have a lot of English speakers as well, so I want to avoid defaulting to Japanese as much as possible, i.e. modifying nodkz's example:

resolve: (obj, args, context, info) => {
  context.language = args.lang || 'ja';
  return {}; // or may return somedata due your bussiness logic
} 

Btw, I am using it for our music news app, which gets its data from our brand new GraphQL API.

eugene1g commented 8 years ago

In Facebook's case, viewer is a representation of the user, correct?

For FB, I think that's correct. But I've seen quite a number of Relay implementations that use this field for dual purposes: 'a currently authenticated user' and 'a gateway to GraphQL information' (ala File Explorer). So viewer.stockPrices() can exist as a field, even if it has nothing to do with the 'current user'. For us, we don't use this naming convention at all, and replace it with a business-level name (in music, an equivalent could be a field called catalogue).

And how would you go about passing "who" the user is?

Our frontend injects the Authorization header with every Relay query (using react-relay-network-layer). For all requests, the auth token is extracted from this header, converted to the domain-level ACL object/rules, and attached to the root-level context for GraphQL to consume. Therefore, the 'current user' information is available across all fields, including viewer, node, and any others. Then each field is free to respond with generic public information, or user-specific information for those who are logged in. Similar to auth, another strategy for you could be to use the express route handler to check the request header for Accept-Language (or a custom parameter passed by the frontend), and then set context.language to the appropriate value for GraphQL to use (and it will be available in the node field as well). Then the lang argument can be an optional override for any specific field (if required).

Naoto-Ida commented 8 years ago

@eugene1g Thanks for sharing your implementation ideas! It's very interesting to hear how other people have implemented it. I've never thought of using Accept-Language... will give it a go.

rturk commented 8 years ago

@Naoto-Ida. Just like @eugene1g described above in our App we parse select Headers and make them available the GraphQL context.

Naoto-Ida commented 8 years ago

@rturk Thank you! We went with the discussed implementations and our service is running better than ever. Hopefully soon we'll be able to apply for getting on the list of apps using Relay. Thank you guys.

josephsavona commented 8 years ago

@Naoto-Ida no need to apply, just send us a PR to add your company's name to the USERS.md file.

Naoto-Ida commented 8 years ago

@josephsavona Great! Thank you so much.