Content SaaS | How to configure and use Resource Bundles
Tags: bloomreach, saas, resourcebundles, frontend
Published: 2023-05-22 14:40:45

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](https://documentation.bloomreach.com/content/reference/putchannelparameterinfo).

![Channel tabs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ktmrtr758lovlw46lopb.png)
![Channel properties](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v51bs4el8s6x7nc5ossw.png)
![Add new property](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xcgk96ygc3cfadkuard7.png)
![Channel property values](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4ts6vpkz7s2vtuzkassc.png)

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

<u>**Important:**</u> 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.

```json
{
 "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"
     }
   }
 }
}
```

## React example code - Provider/Consumer
In React, we can have an implementation leveraging the [Context provider/consumer](https://react.dev/learn/passing-data-deeply-with-context) functionality.

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

```javascript
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>
 )
}
```
In `App.tsx` we should wrap our application with the `RBProvider` component and pass to it the page context.

```javascript
<BrPage configuration={{ ...configuration, httpClient: axios as any }} mapping={mapping} page={page}>
 <BrPageContext.Consumer>
   {(contextPage) => (<>
     <RBProvider page={contextPage}>
	//Rest of App
</RBProvider>
   </>)}
 </BrPageContext.Consumer>
</BrPage>
```
In any of your components where you want to retrieve and use a label

```javascript
const valueSet = React.useContext(RBContext);

{valueSet?.['test']}//where test is the key
```
## Vue example code - Provide/Inject
`RBContext.vue` is our frontend component responsible for fetching and providing the resource bundle document

```javascript
<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>
```
In your `_.vue` file the template should look something like the following example.

**<u>Important:</u>** 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](https://vuejs.org/guide/components/provide-inject.html) functionality of Vue.

```javascript
<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>
```
In your template use the valueSet map like below

```javascript
{{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;
 },
})
```
## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0rd2f7m8yzsl8mr5pog8.png)
![Two-column layout with rbcontext component](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9gbacskg1lx105fakc0f.png)
![rbcontext component definition](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nm3u0kukslzeh3xc1mdv.png)
![Document parameter configuration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q6el0w1b2kbdajaq4ici.png)

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