Profile picture of Travis Spencer Travis Spencer — Software Engineer


Localizing API Responses

Last updated:

As we continue working at Curity on our new hypermedia authentication API, one thing that's come up is how to localize the resources that are returned from it. This articles summarizes what we've come up with. I take very little credit for any of this design. (Pedro and the others get the props for that.) I mostly did research to find out if there was an existing RFC or common design pattern. I was really surprised to find that there wasn't, so hopefully this will help shed some light on this area of API design.

The problem: We want our API's hypermedia responses to be localized. This is relatively easy using content negotiation. The client can indicate in the request what language it would like using the Accept-Language request header. Then, the server can pick what language to use to encode the message strings, considering the client's request. If the server doesn't understand any of the languages requested by the client, it can use its configured default. This is how we've done in our Web app (which is also a hypermedia API) for years, and it works fine.

When considering more capable API clients besides a browser though, we wanted to expand on this to satisfy not only the existing requirement but a couple additional ones:

  1. Client wants the server to localize all messages (existing implementation that's entirely server-drive)
  2. Client wants the server to localize most messages (new)
  3. Client wants to localize all messages (new)

To satisfy the existing requirement and these couple of new ones, we came up with this approach: The hypermedia API will consider the client's preference for how and where localization should be done using a combination of the Accept-Language request header and the Prefer header. We defined a preference called haapi.messages that can have one of three values:

The client would like the server to localize the messages (default)
The client wants the server to localize some messages; to allow the client to localize the others, the server should also return the message keys (i.e., the IDs for certain messages) that represent human-readable strings.
The client wants to localize all messages, so it would like the server to return messages keys only

The server will consider this preference, and indicate to the client what it selected by including the Preference-Applied response header.

Besides the use of the Prefer header, we also considered the Expect header, but this imposed requirements on all intermediaries to understand the expectation or fail. We also considered using Accept-Features header, but that was not as commonly used as Prefer. We also looked into adding parameters on the media type included in the Accept request header and the Content-Type response header; these would indicate if the client wanted message keys or not (e.g., something like Accept: application/vnd.curity.auth+json; haapi.messages=keys-only). The problem with this was that existing media types that we're using don't allow parameters, and this could interfere with content negotiation.

With our chosen solution, the client can indicate how localization should be done. When it wants the server to perform any kind of localization, the client continues to indicate the languages the user understands using the Accept-Language request header. The server indicates which of these was selected in the Content-Language response header. When combined with the Content-Type and Preference-Applied response headers, it's very clear to the client how to interpret the response. As described below, there will be one of three ways to do so.

Entirely Server-driven Localization

The first case is the easy one. In this, the client lets the server perform all localization. The following example request indicates to the server that the end user prefers US English by way of the Accept-Language header. The request looks like this:

GET / HTTP/1.1 Host: Accept-Language: en-US Accept: application/vnd.curity.auth+json

The response to this will look like this:

HTTP/1.1 404 Not Found Content-Type: application/problem+json Content-Language: en { "type": "", "title": "Resource not found", "extra-info": "The source was not found on the server. Check the URL and try again." }

Here, the server indicates that the language that was selected is English (not necessarily US English) using the Content-Language response header. The hypermedia representation of the resource complies with the Problem Details RFC; as stated there, the title is a localizable, human readable short description of the problem. This localization, in this case, was done on the server. Because that's what the client wanted, no Prefer header was sent (though it could have been if messages-only was used).

Partial Client-side Localization

Now, let's look at how the client could localize parts of the response after receiving it. In this case, the client must indicate that it prefers to use this tactic, so the server's default behavior isn't performed. The client also includes the user's desired language in the Accept-Language for those messages that the server will localize. So, the request will look like this:

GET / HTTP/1.1 Host: Accept-Language: en-US Accept: application/vnd.curity.auth+json Prefer: haapi.messages=messages-and-keys

The response is similar to the one before, but now includes the Preference-Applied response header and message keys for each string that the server localized, allowing the client to easily lookup any of those:

HTTP/1.1 404 Not Found Content-Type: application/problem+json Content-Language: en Preference-Applied: haapi.messages=messages-and-keys { "type": "", "title": "Resource not found", "titleKey": "not-found.title" "extra-info": "The source was not found on the server. Check the URL and try again." "extra-infoKey": "not-found.extra-info" }

At this point, the client would know from the Preference-Applied response header that the content includes not only server-translated messages, but also keys. These keys are unique server-wide, and allow the client to look them up in a translation table, client side. It could do this considering the user's preferences or perhaps its own translation that it thinks is better in some cases. To do this, it just looks for a property in each JSON object(s) included in the response for names that end in "Key". The associated value is the message key (i.e., message ID). The corresponding, server-localized message will be in the property with the same name but without this "Key" suffix.

Entirely Client-driven Localization

There can be cases where the client wants to localize the entire response, client-side. This can happen when the server doesn't support a language that the client provides and the publisher of the client doesn't control the server. To see how this would work, observe the following request:

GET / HTTP/1.1 Host: Accept: application/vnd.curity.auth+json Prefer: haapi.messages=keys-only

Here, the client doesn't bother sending the Accept-Language; it may, but it's superfluous. It also indicates that it will do all localization by including keys-only as the preferred value of the haapi.messages token. The server responds to this request like this:

HTTP/1.1 404 Not Found Content-Type: application/problem+json Preference-Applied: haapi.messages=keys-only { "type": "", "titleKey": "not-found.title" "extra-infoKey": "not-found.extra-info" }

The response does not have a Content-Language response header. The server tells the client that it got it's way by including haapi.messages=keys-only in the Preference-Applied header. The response JSON document does not include any localized message, but only the keys. The client can then look these up and localize them all itself.

This is what we've come up with. The plan is to ship this in version 5.4.0 of the Curity Identity Server (the next release). If you have any feedback, please let me know. We're talking about this on Twitter, so feel free to jump in with your opinions.