DEV Community

Karamoulas Eleftherios for Bloomreach

Posted on

Content SaaS | How to configure and use Resource Bundles

Bloomreach Content is a powerful Software-as-a-Service (SaaS) solution that allows businesses to create, manage, and deliver digital content across multiple channels. Resource bundles are an important aspect of Bloomreach Content that enable users to easily manage localised content and translations for different regions and languages.

The use case of resource bundles is the localisation of labels that are reused frequently across your site. For example, unique texts in the header and footer, button labels, common component texts etc. The question at hand is how to make the resource bundles available to the frontend application in a headless implementation.

In this blog, we will showcase two solutions with their respective configurations, coding examples, and pros and cons.

Solution 1: Developer configurable Channel property in combination with an SPA provider component

In this approach, we define a new channel property (eg. rbPath) that will contain the absolute content path to the resource bundle (it can also be a comma-separated list if multiple is necessary). The frontend will utilise the document delivery API to retrieve and store the resource bundles. Below we provide implementation examples in React and Vue of such services.

Note: the implementation examples assume a single resource bundle is configured.

Adding the channel property can be achieved either via the site development tool (see screenshots below) or via the site management API as described here.

Channel tabs
Channel properties
Add new property
Channel property values

This is an example response of the content delivery API for a resource bundle
eg.https://{client}.bloomreach.io/delivery/site/v1/channels/{channel}/documents/{path-to-rb-document}

Important: Remember to change the {client}, {channel} and {path-to-rb-document} with the right values for you. The same applies to the provided URLs in the code snippets below.

{
 "meta": {
   "product": "brx",
   "version": "1.0",
   "branch": "master"
 },
 "document": {
   "$ref": "/content/u3e27f53db63b4a76a7912a1896acd157"
 },
 "content": {
   "u3e27f53db63b4a76a7912a1896acd157": {
     "type": "document",
     "links": {
       "site": {
         "type": "unknown"
       }
     },
     "meta": {},
     "data": {
       "name": "site-labels",
       "displayName": "Site labels",
       "stateSummary": "live",
       "messages": [
         "Test"
       ],
       "keys": [
         "test"
       ],
       "id": "3e27f53d-b63b-4a76-a791-2a1896acd157",
       "state": "published",
       "valueSets": [
         {
           "name": "[default]",
           "messages": [
             "Test"
           ]
         },
         {
           "name": "en",
           "messages": [
             "Test EN"
           ]
         },
         {
           "name": "nl",
           "messages": [
             "Test NL"
           ]
         }
       ],
       "localeString": null,
       "contentType": "resourcebundle:resourcebundle"
     }
   }
 }
}
Enter fullscreen mode Exit fullscreen mode

React example code - Provider/Consumer

In React, we can have an implementation leveraging the Context provider/consumer functionality.

RBContext.tsx is our frontend component responsible for fetching and providing the resource bundle document

import React, {useEffect, useState} from "react";
import axios from "axios";

export const RBContext = React.createContext(null);

async function rbMap(page: any) {
 const locale = 'en'; //'this should be dynamic based on how your SPA handles the locale'
 const rbpath = page.model.channel.info.props.rbPath;

 const data = await axios
   .get(`https://{client}.bloomreach.io/delivery/site/v1/channels/{channel}/documents${rbpath}`)
   .then(response => response.data)
   .catch(error => console.log(error))

 const content = data?.document.$ref.split('/').reduce((value, key) => (key ? value?.[key] : data), data).data
 const keys = content.keys;
 const valueSets = content.valueSets;

 let valueSet;
 if(valueSets.some((set:any) => set.name === locale)) {
   valueSet = valueSets.find((set:any) => set.name === locale);
 } else {
   valueSet = valueSets.find((set:any) => set.name === '[default]');
 }

 const rbMap = {};
 keys.forEach((element, index) => {
    rbMap[element] = valueSet.messages[index];
 });

 return rbMap;
}

export const RBProvider = (props:any) =>{
 const [data, setData] = useState(new Map)

 useEffect(()=> {
   (async () => {
     const data = await rbMap(props.page);
     setData(data);
   })()
 },[])


 return(
   <RBContext.Provider value={data}>
     {props.children}
   </RBContext.Provider>
 )
}
Enter fullscreen mode Exit fullscreen mode

In App.tsx we should wrap our application with the RBProvider component and pass to it the page context.

<BrPage configuration={{ ...configuration, httpClient: axios as any }} mapping={mapping} page={page}>
 <BrPageContext.Consumer>
   {(contextPage) => (<>
     <RBProvider page={contextPage}>
    //Rest of App
</RBProvider>
   </>)}
 </BrPageContext.Consumer>
</BrPage>
Enter fullscreen mode Exit fullscreen mode

In any of your components where you want to retrieve and use a label

const valueSet = React.useContext(RBContext);

{valueSet?.['test']}//where test is the key
Enter fullscreen mode Exit fullscreen mode

Vue example code - Provide/Inject

RBContext.vue is our frontend component responsible for fetching and providing the resource bundle document

<template>
 <div>
   <slot />
 </div>
</template>

<script lang="ts">
import { Page } from '@bloomreach/spa-sdk';
import { Component, Prop, Vue } from 'nuxt-property-decorator';
import axios from 'axios';

@Component({
 name: 'RBContext',
 provide()  {
   return {
     rbMap: this.rbMap
   }
 },
 computed: {
   async rbMap() {
     const locale = 'en'; //'this should be dynamic based on how your SPA handles the locale'
     const rbpath = this.page.model.channel.info.props.rbPath;

     const data = await axios
       .get(`https://{client}.bloomreach.io/delivery/site/v1/channels/{channel}/documents${rbpath}`)
       .then(response => response.data)
       .catch(error => console.log(error))

     const content = data?.document.$ref.split('/').reduce((value, key) => (key ? value?.[key] : data), data).data
     const keys = content.keys;
     const valueSets = content.valueSets;

     let valueSet;
     if(valueSets.some(set => set.name === locale)) {
       valueSet = valueSets.find(set => set.name === locale);
     } else {
       valueSet = valueSets.find(set => set.name === '[default]');
     }

     const rbMap = {};
     keys.forEach((element, index) => {
        rbMap[element] = valueSet.messages[index];
     });

     return rbMap;
   }
 },
})

export default class RBContext extends Vue {
@Prop() page!: Page;
data: any;
}
</script>
Enter fullscreen mode Exit fullscreen mode

In your _.vue file the template should look something like the following example.

Important: the component r-b-context should be within the br-page component and include the rest application structure so that we can use the provide/inject functionality of Vue.

<template>
 <br-page v-if="configuration && page" :configuration="configuration" :mapping="mapping" :page="page">
   <template #default="props">
     <template v-if="props.page">
       <r-b-context :page="page">
         <br-component component="header" />
         <br-component component="main">
           <template v-slot:default="{ component, page }" />
         </br-component>
         <br-component component="footer" />
       </r-b-context>
     </template>
   </template>
 </br-page>
</template>
Enter fullscreen mode Exit fullscreen mode

In your template use the valueSet map like below

{{valueSet?.['test']}} //where 'test' is the key
In your component configuration have the inject, data and mounted defined as follows

@Component({
 name: 'HomepageBanner',
 inject: ['rbMap'],
 data() {
   return {
     valueSet: null,
   };
 },
 computed: {...},
 async mounted(): Promise<void> {
   this.valueSet = await this.rbMap;
 },
})
Enter fullscreen mode Exit fullscreen mode

Solution 2: Page layout root component with a SPA provider component

An alternative solution to the first approach is to make the resource bundle document(s) part of the page model API response directly.

For the above, all the page layouts will require a root component (via inheritance and explicitly) that contributes the configured resource bundle to the API response. As a static component, the document(s) can only be configured by site developers. The resource bundle component will have to have the rest of the page structure below it. This hierarchy is required so that the frontend functionality can work.

Base layout with rbcontext component
Two-column layout with rbcontext component
rbcontext component definition
Document parameter configuration

The frontend implementation though similar would have to change slightly. Now the RBContext component won’t read the data from the Content delivery API but instead from the component itself that is part of the page structure. The rest functionality of parsing the retrieved resource bundle document and providing it for consumption (React) or injection (Vue) will remain the same.

Implementation remarks

  • Any type of async functionality has to be done outside the rendering of components to HTML if you want to support SSR. Getting the valueSet on the server side and setting it as a value in the provider/context directly.
  • We advise building in some caching mechanism that allows you to only request the translation once per session or something like that. Usually, translations can be requested per page, but once requested they could simply be saved in memory instead of re-requesting it on every page load as a user is navigating through the app.
  • Solution 1
    + smaller payload of initial page load
    + easier maintainable configuration
    - requires extra call(s) to retrieve the configured resource bundles

  • Solution 2
    + no extra calls necessary
    - due to how translations are usually quite static the request can not be cached this way as it is always part of the page
    - the response payload will be bigger as the resource bundle component and the document(s) will always be part of the API response
    - it is a more complicated setup
    - configuring more than one resource bundles will be an extra configuration overhead

Top comments (0)