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

Hi Mikael,

sorry, for responding late.
Your mail was very interesting to me as I dealt with exactly the same 
issues,
I just hadn't the time to respond.
See my comments below.

Greets, Thomas

On 06/19/2015 02:50 PM, Mikael Labrut wrote:
> Hi,
>
> My name is Mikaël Labrut, I am actually working on an API using Hydra 
> using DunglasApiBundle.
> This is great but, but one of my question have no "normalized" answer 
> : i18n.
>
> Here is a copy of this topic 
> (https://github.com/dunglas/DunglasApiBundle/issues/127), we want to 
> have a lot of opinion about this topic before implementing a solution.
> Feel free to say what do you think about that and what is the best way ??
>
> Problem :
> Imagine you have a "Country" (http://schema.org/Country) resource in 
> your API.
> How to manage i18N on it ?
>
> 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)
I don't see a description for this variant.
> - One URL = One localized resource (like wikipedia, better for 
> indexing or caching)
Certainly the best for wikipedia as the volume of textual content for an 
article is quite
high and translations are mostly not just 1:1 translations.
> - 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
>
Drawback is that you don't want to send ~300 translations for any
localized field to the client as it would kill the performance.
We actually have exactly this case with a Countries resource
and data from http://www.geonames.org/.

> ### 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.
We do exactly this to override the client
specified locale preference(Accept-Lanuage header).
And to be able to specify multiple locales as
colon separated list.

>
> ### 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.
We don't do this. Usually you have some none localized data
which you can send back anyways. If the localized properties
cannot be found in the desired locale, then we send them back for a
fall back locale.

>
> ## 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
I would not do that, as mentioned above, I would return
the localization you successfully negotiated with the client,
a fall back otherwise.

>
> ### 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",
>       "name": "Angleterre"
>     }
>   ]
> }
> ```
> => Will return resource with requested locale
See above.
>
> ### 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": []
> }
> ```
>
> ## 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
Same how we do it.
>
> DELETE http://api.example.com/countries/1?locale=fr-FR
> => Will delete country localization fr-FR only !
>
In case of a delete this is ignored because in our case the IRI does not 
address
a localized property or multiple of them, it addresses the whole resource
as in the example before.

> ## 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
Yes
>
> OR
>
> POST http://api.example.com/countries
> ```json
> {
>     "@context": {
>         "@language": "fr-FR"
>     },
>     "name": "Angleterre"
> }
> ```
> => Will create country with one localized value
Also possible but then you would need to apply this for all
properties which can be language tagged.
>
> ## 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
Would replace the _whole resource_, not only the property sent.
So this would also delete existing translations.
We allow in-place updates of single translations of single properties via
the HTTP PATCH method.
>
> PUT http://api.example.com/countries/1
> ```json
> {
>     "name": {
>         "fr" : "Angleterre"
>     }
> }
> ```
> => Will delete all localization for name and just have fr localization
Yes.
>
> ## 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

We do this similarly -- with a query param (locales)
without a value.

>
> ## 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's more-or-less the "Wikipedia approach" with the
difference that you have the locale in a query param.
It's perfectly valid with linked data principles.
I don't see how it prevents you from using schema.org.
You can still provide a custom context using schema.org terms.

> 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"
>
> It's not JSON-LD / Hydra compliant because :
> * you change structure of object ( localization / localizations 
> attribute is dynamical ... ), you cannot use schema.org 
> <http://schema..org> vocabulary because of that
>
> What do you think about this ?
>
> Best regards,
>
> -- 
> Mikaël Labrut - Team B2B
> Github : MLKiiwy

Received on Thursday, 20 August 2015 12:10:18 UTC