This post is part of a series of post about AppText, a Content Management System for applications. Earlier posts:

In the previous post we translated labels and messages in a JavaScript example application with i18next. AppText covers this with the built-in ‘Translation’ content type that simply has a single field: ‘text’.

Sometimes however, applications have the need for more sophisticated content, for example, in help or information pages. These pages contain multiple paragraphs, require formatting and perhaps contain links to other pages. For these scenarios, just having a singe field ‘text’ simply won’t cut it anymore.

In AppText, you can define your own content types that can contain as many fields as you wish. Displaying the content for these custom content types requires a different approach than just using a localization library such as i18next. This post shows how to leverage  the GraphQL API of AppText to display content for custom content types (and more!).

Also check out the source code of the example application at  https://github.com/martijnboland/apptext/tree/main/examples/javascriptreactexample.

Custom content type – an example

Our example JavaScript application contains an Intro component that displays an intro page with a title and content with markup. In AppText, we created a custom ‘Page’ content type with 2 fields: title (short text) and content (long text):

apptext-page-content-type_thumb2

(Note: the short text field can contain max. 500 characters and no formatting where the long text field can contain an unlimited amount of characters and allows Markdown formatting. Besides short text and long text, AppText also has number and date(time) fields)

With the ‘Page’ content type, we then created a ‘pages’ collection where we can edit the content for the Intro page. Note that the ‘Page content’ field allows markdown (in AppText all long text fields can contain Markdown):

apptext-edit-intro_thumb2

We can retrieve content of collections with a custom content type such as our pages collection via the REST API or the GraphQL API. In essence, we’re now using AppText as a Headless CMS.

Retrieving content with GraphQL

Before showing how to read and display content from AppText with GraphQL, first a brief introduction about the GraphQL API itself.

graphql-pages

The GraphQL schema in AppText is partly dynamic. The top-level fields are the collections (pages in the example above). Per collection, you can query collection-specific fields (incl. content type) but also traverse into the items field, which represent content items. This is where you query the actual content. A content item has some fixed fields like contentKey and version but also has the fields as defined in the content type. In the example above, content and title are those dynamic fields.

Our JavaScript example app uses the GraphQL API to retrieve to content for the intro page with the help of the urql library. For this, we have created a graphQLClient to connect to our AppText GraphQL API:

import { createClient } from 'urql';

import { appTextApiBaseUrl, appTextAppId, appTextApiKey } from './config';

const client = createClient({
  url: `${appTextApiBaseUrl}/${appTextAppId}/graphql/public`,
  fetchOptions: () => {
    return {
      headers: { 'X-Api-Key': appTextApiKey },
    };
  },
});

export default client;

The url property of the client options object points to the AppText GraphQL api. Every AppText App has its own GraphQL url. For example, the GraphQL url of the official JavaScript demo is https://demo.apptext.io/jsexample/graphql/public. The X-Api-Key HTTP header is required and defined in the AppText admin UI in the App properties.

This client is added to our example application in the top-level component (App.tsx) via the urql Provider component.

import React, { Suspense, useRef, useState } from 'react';
import { Provider as GraphQLProvider } from 'urql';
import graphQLClient from './localization/graphQLClient';
// other imports

function App() {

  // Snip App init code

  return (
    <Suspense fallback="loading">
      <GraphQLProvider value={graphQLClient}>
        <div className="app">
           ...
        </div>
      </GraphQLProvider>
    </Suspense>
  );
}

With the GraphQL client in place, we can now query the GraphQL API for content. For convenience, the example application has a useAppTextPage hook that encapsulates the GraphQL querying of the pages collection:

import { useQuery } from 'urql';

const AppTextPageQuery = `
  query ($language: String!, $contentKey: String) {
    pages {
      items(contentKeyStartsWith:$contentKey, first:1) {
        contentKey
        title(language:$language)
        content(language:$language)
      }
    }
  }
`;

export function useAppTextPage(contentKey: string, language: string) {
  const [{ data, fetching, error }] = useQuery({
    query: AppTextPageQuery,
    variables: { language: language, contentKey: contentKey }
  });

  const page = data && data.pages && data.pages.items.length > 0
    ? data.pages.items[0]
    : null;

  if (error) {
    console.error(error);
  }
  return { page, fetching, error };
}

Note that the GraphQL query, AppTextPageQuery, receives two variables, $language and $contentKey, to allow filtering the content items for a single page and then only fetching content for the specified language.

Display localized content

In our example application, we have a component, Intro.tsx that displays simple translations via i18next, but also custom content via the useAppTextPage hook:

import React from 'react';
import { useTranslation } from 'react-i18next';
import ReactMarkdown from 'react-markdown';
import Loader from './loader/Loader';
import { useAppTextPage } from './localization/useAppTextPage';

interface IntroProps {
  onCreateNote(): void
}

const Intro: React.FC<IntroProps> = ({ onCreateNote }) => {
  const { t, i18n } = useTranslation(['labels','messages']);
  const { page, fetching, error } = useAppTextPage('intro', i18n.language);

  return (
    <section className="intro">
      {page
        ? 
        <React.Fragment>
          <h2>{page.title}</h2>
          <ReactMarkdown source={page.content} />
          <button onClick={onCreateNote}>{t('labels:Create note')}</button>
        </React.Fragment>
        : fetching
          ? <Loader />
          : <p>{t('messages:Page not found', { contentKey: 'intro' })}</p>
      }
      {error &&
        <p>{error.message}</p>
      }
    </section>
  )
}

export default Intro;

The custom content is retrieved with the useAppTextPage hook:

const { page, fetching, error } = useAppTextPage(‘intro’, i18n.language);

This will get the content for the page where the contentKey starts with ‘intro’ and for the currently selected language, resulting in a page object with title and content properties that is rendered in the component:

apptext-intro

The page.content is rendered with the ReactMarkdown component to safely convert the Markdown into HTML.

More GraphQL: languages

Besides the collections, the AppText GraphQL API also has two other useful top-level fields: languages and defaultLanguage. These are properties of the AppText App object.

In our JavaScript example, we use this in our LanguageSelector component to display the list of available languages. Check the LanguagesQuery:

import React from 'react';
import { useTranslation } from 'react-i18next';
import { useQuery } from 'urql';
import Loader from '../loader/Loader';
import { currentLanguageStorageKey } from './config';

const LanguagesQuery = `
  query {
    languages
    defaultLanguage
  }
`;

const LanguageSelector: React.FunctionComponent = () => {
  const { i18n, t } = useTranslation();

  const [{ data, fetching, error }] = useQuery({
    query: LanguagesQuery,
  });

  const currentLanguage = i18n.language;

  const changeLanguage = (e: React.ChangeEvent<HTMLSelectElement>) => {
    const newLanguage = e.target.value;
    i18n.changeLanguage(newLanguage)
      .then(() => { 
        localStorage.setItem(currentLanguageStorageKey, newLanguage);
      });
  }

  return fetching
    ?
      <Loader />
    :
      error
      ?
        <span>{error.message}</span>
      :
        <form className="form-inline">
          <label htmlFor="language" style={{marginRight: '0.5em'}}>{t('Language')}</label>
          <select value={currentLanguage} onChange={changeLanguage}>
            {data.languages.map((lang:string) => <option key={lang}>{lang}</option>)}
          </select>
        </form>
};

export default LanguageSelector;

You can see that the LanguageSelector component also uses i18next (via the useTranslation hook), not only to for the label translation, but also to set the current language for the application.

Try it yourself

All code in this and the previous post is on GitHub. You can try it by cloning the AppText repository. The JavaScript/React example is in the /examples/javascriptreactexample folder and we have a demo Docker container prepared as AppText backend for the example app.

Dynamic localization of JavaScript apps with AppText part 2: GraphQL
Tagged on:                 

Leave a Reply

Your email address will not be published. Required fields are marked *