Search

    Localized Data

    This guide extends Localization/i18n of static content, like labels or messages, to serving localized versions of actual application data.

    Localized data means maintaining different translations of textual data and automatically fetching the translations matching the users’ preferred language with per-row fallback to default languages, if the required translations aren’t available. Language codes are in ISO 639-1 format.

    Find a working sample at https://github.com/sap-samples/cloud-cap-samples/tree/master/bookshop.

    Content

    Declaring Localized Data

    Use the localized modifier to mark entity elements that require translated texts.

    entity Books {
      key ID       : UUID;
          title    : localized String;
          descr    : localized String;
          price    : Decimal;
          currency : Currency;
    }
    

    Find this source also in cap/samples.

    Restriction If you want to use the localized modifier, the entity’s keys must not be associations.

    localized in entity sub elements isn’t supported at the moment and ignored. This includes localized in structured elements and structured types.

    Behind the Scenes

    The cds compiler automatically unfolds the previous definition as follows, applying the basic mechanisms of Managed Compositions, and Scoped Names

    First, a separate Books.texts entity is added to hold translated texts:

    entity Books.texts {
      key locale : String(5);
      key ID : UUID; //= source's primary key
       title : String;
       descr : String;
    }
    

    Note: The above shows the situation with CDS compiler v2. Former versions of the compiler generated an entity Books_texts.

    Second, the source entity is extended with associations to Books.texts:

    extend entity Books with {
      texts : Composition of many Books.texts on texts.ID=ID;
      localized : Association to Books.texts on localized.ID=ID
        and localized.locale = $user.locale;
    }
    

    The association texts points to all translated texts for the given entity, whereas the association localized points to the translated texts and is additionally narrowed to the request’s locale.

    Third, views are generated in SQL DDL to easily read localized texts with fallback, which are the equivalent:

    entity localized.Books as SELECT from Books {*,
      coalesce (localized.title, title) as title,
      coalesce (localized.descr, descr) as descr
    };
    

    Note: In contrast to former versions, with CDS compiler v2 we dont add such entities to CSN anymore but only on generated SQL DDL output.

    Resolving localized texts via views

    As mentioned the CDS compiler is already creating views that resolve the translated texts internally. Once a CDS runtime detects a request with a user locale it uses those views instead of the table of the involved entity.

    Note, that SQLite doesn’t support locale like SAP HANA does. For SQLite, additional views are generated for different languages. Currently those views are generated for the locales ‘de’ and ‘fr’ and the default locale is handled as ‘en’.

    "i18n": { "for_sqlite": ["en", ...] }
    

    In package.json put this snippet in the cds block, but don’t do so for .cdsrc.json.

    For testing with SQLite: Make sure that the Books table contains the English texts and that the other languages go into the Books.texts table.

    Resolving localized texts at runtime

    Although the approach with the localized views is very convenient it’s pretty limited on SQLite and shows suboptimal performance with large data sets on SAP HANA. Thus, a CDS runtime can use the association localized to generate SQL statements that resolve the localized texts in a way that are optimized for the underlying database. This is, however, only possible if association localized of your entity is present and accessible by the given CQL statement.

    When CQL queries select entities directly there is no issue as the association localized is automatically accessible in an entity with localized elements. If a CQL query selects from a view it is important that the views’ projection preserves the association localized. This is preferably achieved by excluding elements that must not be exposed by the view:

    entity OpenBookView as SELECT from Books {*}
      excluding {price, currency};
    

    Otherwise it is recommended to include the association localized in the projection:

    entity ClosedBookView as SELECT from Books {ID, title, descr, localized};
    

    Both view definitions preserve the association localized in the view, allowing to optimize query execution or for broader language support on SQLite, H2 and PostgreSQL.

    Base Entities Stay Intact

    In contrast to similar strategies, all texts aren’t externalized but the original texts are kept in the source entity. This saves one join when reading localized texts with fallback to the original ones.

    Pseudo var $user.locale

    As shown in the second step, the pseudo variable $user.locale is used to refer to the user’s preferred locale and join matching translations from .texts tables. This pseudo variable allows expressing such queries in a database-independent way, which is realized in the service runtimes as follows:

    Determining $user.locale from Inbound Requests

    The user’s preferred locale is determined from request parameters, user settings, or the accept-language header of inbound requests as explained in the Localization guide.

    Programmatic Access to $user.locale

    The resulting normalized locale is available programmatically, in your event handlers.

    • Node.js: req.user.locale
    • Java: context.getParameterInfo().getLocale()

    Propagating $user.locale to Databases

    Finally, the normalized locale is propagated to underlying databases using session variables, that is, $user.locale translates to session_context('locale') in native SQL of SAP HANA and most databases.

    Not all databases support session variables. For example, for SQLite we currently would just create stand-in views for selected languages. With that, the APIs are kept stable but have restricted feature support.

    Reading Localized Data

    Given the asserted unfolding and user locales propagated to the database, you can read localized data as follows:

    In Agnostic Code

    Read original texts, that is, the ones in the originally created data entry:

    SELECT ID, title, descr from Books
    

    For End Users

    Reading texts for end users uses the localized association, which requires prior propagation of $user.locale to the underlying database.

    Read localized texts in the user’s preferred language:

    SELECT ID, localized.title, localized.descr from Books
    

    For Translation UIs

    Translation UIs would read and write texts in all languages, independent from the current user’s preferred one. They use the to-many texts association, which is independent from $user.locale.

    Read texts in different translations:

    SELECT ID, texts[locale='fr'].title, texts[locale='fr'].descr from Books
    

    Read texts in all translations:

    SELECT ID, texts.locale, texts.title, texts.descr from Books
    

    Serving Localized Data

    The generic handlers of the service runtimes automatically serve read requests from localized views. Users see all texts in their preferred language or the fallback language.

    See also Enabling Draft for Localized Data.

    For example, given this service definition:

    using { Books } from './books';
    service CatalogService {
      entity BooksList as projection on Books { ID, title, price };
      entity BooksDetails as projection on Books;
    }
    

    localized. Helper Views

    For each exposed entity in a service definition, and all intermediate views, a corresponding localized. entity is created. It has the same query clauses and all annotations, except for the from clause being redirected to the underlying entity’s localized. counterpart.

    using { localized.Books } from './books_localized';
    
    entity localized.CatalogService.BooksList as
    SELECT from localized.Books { ID, title, price };
    
    entity localized.CatalogService.BooksDetails as
    SELECT from localized.Books;
    

    Note: In contrast to former versions, with CDS compiler v2 we dont add such entities to CSN anymore but only on generated SQL DDL output. Note that these localized. entities also aren’t exposed through OData.

    Read Operations

    The generic handlers in the service framework will automatically redirect all incoming read requests to the localized_ helper views in the SQL database, unless in SAP Fiori Draft mode.

    In the Node.js runtime, the @cds.localized:false annotation can be used to explicitly switch off the automatic redirection to the localized views. All incoming requests to an entity annotated with @cds.localized:false will directly access the base entity.

    using { Books } from './books';
    service CatalogService {
      @cds.localized:false //> direct access to base entity; all fields are non-localized defaults
      entity BooksDetails as projection on Books;
    }
    

    Write Operations

    Since the corresponding text table is linked through composition, you can use deep inserts or upserts to fill in language-specific texts.

    POST <your_service_url>/Entity
    Content-Type: application/json
    
    {
      "name": "Some name",
      "description": "Some description",
      "texts": [ {"name": "Ein Name", "description": "Eine Beschreibung", "locale": "de"} ]
    }
    

    If you wish to add a language-specific text to an existing entity, perform a POST request to the text table of the entity through navigation.

    POST <your_service_url>/Entity(<entity_key>)/texts
    Content-Type: application/json
    
    {
      {"name": "Ein Name", "description": "Eine Beschreibung", "locale": "de"}
    }
    

    Update Operations

    To update the language-specific texts of an entity along with the default fallback text, you can perform a deep update as a PUT or PATCH request to the entity through navigation.

    PUT/PATCH <your_service_url>/Entity(<entity_key>)
    Content-Type: application/json
    
    {
      "name": "Some new name",
      "description": "Some new description",
      "texts": [ {"name": "Ein neuer Name", "description": "Eine neue Beschreibung", "locale": "de"} ]
    }
    

    To update a single language-specific text field, perform a PUT or a PATCH request to the entity’s text field via navigation.

    PUT/PATCH <your_service_url>/Entity(<entity_key>)/texts(ID=<entity_key>,locale='<locale>')/<field_name>
    Content-Type: application/json
    
    {
      {"name": "Ein neuer Name"} ]
    }
    

    Delete Operations

    To delete a locale’s language-specific texts of an entity, perform a DELETE request to the entity’s texts table through navigation. Specify the entity’s key and the locale you wish to delete.

    DELETE <your_service_url>/Entity(<entity_key>)/texts(ID=<entity_key>,locale='<locale>')
    

    Nested Localized Data

    The definition of books has an element currency, which is effectively an association to code list entity sap.common.Currencies, which in turn has localized texts. Find the respective definitions in the reference docs for @sap/cds/common, in the section on Common Code Lists.

    Upon unfolding, all associations to other entities with localized texts are automatically redirected as follows:

    entity localized.Currencies as SELECT from Currencies AS c {* /*...*/};
    entity localized.Books as SELECT from Books AS p mixin {
      // association is redirected to localized.Currencies
      country : Association to localized.Currencies on country = p.country;
    } into {* /*...*/};
    

    Given that, nested localized data can be easily read with independent fallback logic:

    SELECT from localized.Books {
      ID, title, descr,
      currency.name as currency
    } where title like '%pen%' or currency.name like '%land%'
    

    In the result sets for this query, values for title, descr as well as currency name are localized.

    Show/Hide Beta Features