RE: How to manage i18n resource with API / JSON-LD / Hydra ?

Hi Mikaël,

Welcome on our mailing list.

On Friday, June 19, 2015 2:50 PM, Mikael Labrut wrote:
> My name is Mikaël Labrut, I am actually working on an API using Hydra
> using DunglasApiBundle.

Cool! If you can share some more details, please do so.


[...]
> Problem : 
> Imagine you have a "Country" (http://schema.org/Country) resource in your API.
> How to manage i18N on it ?

So we are talking about concepts which have localized descriptions. This is (sometimes) an important distinction as, let's say, a translated blog post is really a different resource.


> Here is a detailed solution, i have design based on experience, and the JSON-LD spec.
> Tell me if you think this is a good idea ?
> If you think it's not ... tell me why ?
> 
> ## How to get a specific locale on a resource ? (=GET)
> 
> ### My solution
> 
> Add a GET specific parameter "locale"
> Why ?
> - Simple (this is the way used in FB graph api for example)
> - One URL = One localized resource (like wikipedia, better for indexing or caching)

Let's make this clearer. There are n URLs, one for each localization. In the case of things like countries this is tricky and probably not the best design. Assume you addresses for Italian and English customers. Do you really want to link different resources just because they speak different languages?

  { "@id": "/italian-addres", "country": "/francia" }

vs. 

  { "@id": "/english-addres", "country": "/france" }
 
I deliberately didn't use query parameters here.. it simply doesn't matter whether it is "just" a query parameter or some other part of the URL that differs. You would need to reconcile those to resource with something like "sameAs" to tell a machine that they are really identifying the same concept.


> - use of @language specified in JSON-LD spec
> - use @container:@language
> 
> ### Sample 1 : No locale specified
> 
> GET http://api.example.com/countries/1
> ```json
> {
>   "@context": {
>     "@base" : "http://schema.org",
>     "name" : {
>       "@container": "@language"
>     }
>   },
>   "@type": "Country",
>   "@id": "/countries/1",
>   "name": {
>     "fr-FR" : "Angleterre",
>     "en" : "England"
>   }
> }
> ```
> => Will return resource with all locale data

>From a linked data perspective this would be preferable I'd say. You can still only return a partial view on the resource with content negotiation (Accept-Language)


> ### Sample 2 : Use specific localization
> 
> GET http://api.example.com/countries/1?locale=fr-FR
> ```json
> {
>   "@context": "http://schema.org",
>   "@type": "Country",
>   "@id": "/countries/1",
>   "@language": "fr-FR",
>   "name": "Angleterre"
> }
> ```
> => Will return resource with requested locale
> 
> Note we use locale (on request + response) in W3C format (IETF's BCP
> 47), see http://www.w3.org/International/articles/language-tags/
> It's the format used also with JSON-LD.

AFAICT this is what you labeled as "My solution" in the beginning of this mail. Right?


> ### Sample 3 : Error localization don't exist
> 
> GET http://api.example.com/countries/1?locale=es-CA
> ```json
> {
>   "@context": "/contexts/LocalizationError",
>   "@type": "LocalizationError",
>   "hydra:title": "An error occurred",
>   "hydra:description": "no localization found for locale es-CA"
> }
> ```
> => Will return code "404 not found", because es-CA localization don't
> exist in my api.

Sounds good if you decide to go down that route.


> ## How to get a specific locale on a list of resource ? (=GET)
> 
> ### My solution : 
> same as for one resource
> 
> ### Sample 1 : No locale specified
> 
> GET http://api.example.com/countries
> ```json
> {
>   "@context": {
>     "@base" : "http://schema.org",
>     "name" : {
>       "@container": "@language"
>     }
>   },
>   "@id": "/countries",
>   "@type": "hydra:PagedCollection",
>   "hydra:totalItems": 1,
>   "hydra:itemsPerPage": 3,
>   "hydra:firstPage": "/countries",
>   "hydra:lastPage": "/countries",
>   "hydra:member": [
>     {
>       "@type": "Country",
>       "@id": "/countries/1",
>       "name": {
>         "fr-FR" : "Angleterre",
>         "en" : "England"
>       }
>     }
>   ]
> }
> ```
> => Will return resources with all available localization
> 
> ### Sample 2 : Use specific localization
> 
> GET http://api.example.com/countries?locale=fr-FR
> ```json
> {
>   "@context": "http://schema.org",
>   "@id": "/countries",
>   "@type": "hydra:PagedCollection",
>   "@language": "fr-FR",
>   "hydra:totalItems": 1,
>   "hydra:itemsPerPage": 3,
>   "hydra:firstPage": "/countries",
>   "hydra:lastPage": "/countries",
>   "hydra:member": [
>     {
>       "@type": "Country",
>       "@id": "/countries/1",

To be consistent with your earlier description, you would need to turn this into /countries/1?locale=fr-FR I guess. But it also work without it... you just wouldn't be able to discover /countries/1?locale=fr-FR. In that case, howerver, you should make the language explicit (@language in either the value or a default value in the context).

>       "name": "Angleterre"
>     }
>   ]
> }
> ```
> => Will return resource with requested locale
> 
> ### Sample 3 : Error localization don't exist
> 
> GET http://api.example.com/countries?locale=es-CA
> ```json
> {
>   "@context": "http://schema.org",
>   "@id": "/countries",
>   "@type": "hydra:PagedCollection",
>   "@language": "es-CA",
>   "hydra:totalItems": 0,
>   "hydra:itemsPerPage": 3,
>   "hydra:firstPage": "/countries",
>   "hydra:lastPage": "/countries",
>   "hydra:member": []
> }
> ```

This suggest you really should add the locale parameter to the URLs in the previous examples. Otherwise this doesn't make much sense. You could still return the list of countries without localized information.


> ## How to delete a localized resource ? (=DELETE)
> 
> ### My solution : 
> same as usual but the localization are also deleted completely
> 
> DELETE http://api.example.com/countries/1
> => Will delete country and all associed localization
>
> DELETE http://api.example.com/countries/1?locale=fr-FR
> => Will delete country localization fr-FR only !
>
> ## How to create a localized resource ? (POST)
> 
> ### My solution : 
> create with a locale container or indicate the language in context
> 
> POST http://api.example.com/countries
> ```json
> {
>     "name": {
>         "fr-FR" : "Angleterre",
>         "en": "England"
>     }
> }
> ```
> => Will create country with two localized value
> 
> OR
> 
> POST http://api.example.com/countries
> ```json
> {
>     "@context": {
>         "@language": "fr-FR"
>     },
>     "name": "Angleterre"
> }
> ```
>
> => Will create country with one localized value

SGTM



> ## How to add a new locale to a localized resource ? (PUT)
> 
> ### My solution : 
> same as POST, you must specified @language or put all localized data
> 
> PUT http://api.example.com/countries/1
> ```json
> {
>     "@context" : {
>         "@language": "it"
>     },
>     "name": "Inghilterra"
> }
> ```
> => Will add or replace locale "it" name for the resource /countries/1

Strictly speaking, this should also delete all other localizations. PUT *replaces* the resource. So either PUT it to countries/1?locale=it, PUT the complete representation or POST it.


> PUT http://api.example.com/countries/1
> ```json
> {
>     "name": {
>         "fr" : "Angleterre"
>     }
> }
> ```
> => Will delete all localization for name and just have fr localization

Correct. You probably have a typo in the explanation above then.


> ## How to list all available locale for a resource ?
> 
> ### My solution : 
> add a specific endpoint to api like :
> 
> GET http://api.example.com/countries/1/locales
> ```json
> {
>   "@context": "http://schema.org",
>   "@id": "/http://api.example.com/countries/1/locales",
>   "@type": "hydra:PagedCollection",
>   "hydra:totalItems": 2,
>   "hydra:itemsPerPage": 3,
>   "hydra:firstPage": "/countries",
>   "hydra:lastPage": "/countries",
>   "hydra:member": [
>     {
>       "@type": "Locale",
>       "@id": "/locale/fr"
>     },
>     {
>       "@type": "Locale",
>       "@id": "/locale/en"
>     }
>   ]
> }
> ```
> => Will return all available locale for the resource

Why? Why is a GET on countries/1 without query parameter or conneg header not enough?


> ## Another way : separate localized content / resource 
> 
> I study another possibility to provide the i18n support on a resource.
> To add a collection of translation when needed.
> 
> For example : 
> 
> GET http://api.example.com/countries/1?locale=fr-FR
> ```json
> {
>   "@context": "http://schema.org",
>   "@type": "Country",
>   "@id": "/countries/1",
>   "@language": "fr-FR",
>   "nonLocalizedAttribute": 10,
>   "localization": {
>     "@type": "CountryLocalization",
>     "@id": "/country_localization/1/fr-FR",
>     "name": "Angleterre"
>   }
> }
> ```
>
> GET http://api.example.com/countries/1
> ```json
> {
>   "@context": "http://schema.org",
>   "@type": "Country",
>   "@id": "/countries/1",
>   "nonLocalizedAttribute": 10,
>   "localizations":{ 
>      "en": {
>          "@type": "CountryLocalization",
>          "@id": "/country_localization/1/fr-FR",
>          "name": "Angleterre"
>       },
>      "fr-FR": {
>          "@type": "CountryLocalization",
>          "@id": "/country_localization/1/fr-FR",
>          "name": "England"
>       }
> }
> }
> ```
> 
> This way he simpler for many reasons, but I think it's not very
> JSON-LD / Hydra compliant.

It is fully compliant but not very idiomatic. That is, very few people would model it this way I guess.


> It's simpler because : 
> * you cut the "localized" part of the resource into separate resource,
> wich can be managed like other resources. 
> * "Like the DB storage"

You shouldn't leak your implementation details.


> It's not JSON-LD / Hydra compliant because : 
> * you change structure of object ( localization / localizations
> attribute is dynamical ... ), you cannot use schema.org vocabulary
> because of that
> 
> What do you think about this ?

Sounds all very sensible but I have no idea of what problem you actually want to solve. What the constraints and what the requirements are. It is thus difficult to make concrete suggestions. For instance, I wonder why you completely ignored the Accept-Language header!?


Cheers,
Markus


--
Markus Lanthaler
@markuslanthaler

Received on Sunday, 21 June 2015 14:33:59 UTC