Skip to content

Localization (l10n) and internationalization (i18n)

Because HortiView will be an internationally widely used application, there is a need for a proper implementation of translations throughout the application. That means we need to support multiple languages, currencies, and regions across the globe.

For module development, this means we need to implement some kind of translation logic that translates every text users can see while using the application.

In general, most internationalization/localization solutions use some kind of translation-to-identifier logic, where each string or text is represented by a unique identifier. This identifier is used to translate the text using a translation file that contains the translation for the currently selected language.

There are many frameworks that already solve this exact issue. One of them is i18next, an i18n-library for JavaScript applications. It simplifies the implementation of localization by managing translations and other locale-specific functionalities. Here's how it works:

recommended but not required

The usage of i18next is not required, but as we use it in the platform itself and the modules, it can be seen as a best-practice implementation of internationalization.

  • Translation Files: i18next uses JSON or YAML files to store translations. Each file corresponds to a specific language (e.g., en.json for English, de.json for German, or es.json for Spanish).
  • Language Detection: The library can automatically detect the user's preferred language based on browser settings, cookies, or URL parameters, if configured correctly.
  • Interpolation: i18next supports placeholders in translations, allowing dynamic content to be inserted (e.g., "Hello, {{name}}!").
  • Middleware Integration: i18next integrates seamlessly with frameworks like React, Angular, and Vue.js, as well as server-side environments like Node.js.

The HortiView platform already uses this library to translate everything. The user's language preference is stored in the user profile and will be used to initialize the translation. Users can change their language at any time via the settings menu:

Change Language

Implementation in modules

Requirements

This implementation requires setting up i18next in the module as well.

TranslationProvider.ts
import { BaseProps, useBreadcrumbTranslation } from "@hortiview/modulebase";
import i18next from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";
import { ReactNode, useEffect } from "react";
import {
I18nextProvider,
initReactI18next,
useTranslation,
} from "react-i18next";

//Language files
import en from "../../locales/en-US.json";
import es from "../../locales/es-MX.json";
import tr from "../../locales/tr-TR.json";

// different instances have to be created since we initialize i18n for each of the modules and the main platform
const i18n = i18next.createInstance();

i18n
// i18next-http-backend
// loads translations from your server
// https://github.com/i18next/i18next-http-backend
.use(Backend)
// detect user language
// learn more: https://github.com/i18next/i18next-browser-languageDetector
.use(LanguageDetector)
// pass the i18n instance to react-i18next.
.use(initReactI18next)
// init i18next
// for all options read: https://www.i18next.com/overview/configuration-options
.init({
    lng: "en",
    fallbackLng: ["en", "es", "tr", "de"],
    resources: {
    en: {
        translation: en,
    },
    es: {
        translation: es,
    },
    tr: {
        translation: tr,
    }
    },
    debug: true,
    returnObjects: true,
    interpolation: {
    escapeValue: false, // not needed for react as it escapes by default
    },
    returnNull: false,
});

export default i18n;

It is important to create a new instance for your module. Otherwise, the implementation could interfere with the platform's initialization of i18next.

For easy synchronization, we pass the user's currently chosen language locale to the modules. In base modules (and the template), we use this language locale to trigger a language switch/initialization of the module's i18n with the help of a hook implementation:

TranslationProvider.tsx
export const TranslationProvider = ({
  children,
  currentLanguage,
}: {
  children: ReactNode;
  currentLanguage: BaseProps["currentLanguage"];
}) => {
  const translateKeyInHortiview = useBreadcrumbTranslation();
  const { t } = useTranslation();
  //react on language change (from platform) and change the language in the i18n instance
  useEffect(() => {
    i18n.changeLanguage(currentLanguage);
  }, [currentLanguage, i18n]);

The effect hook listens to changes in the passed language variables and keeps them synchronized with the HortiView platform.

Provide translations to the platform | Breadcrumb

required for breadcrumb

This step is important if you use the platform's breadcrumb.

If your module uses some kind of navigation or routing, you will probably want to use the platform's breadcrumb. This provides a module with the function to navigate between pages/routes using a breadcrumb. The breadcrumb is automatically calculated based on the routes of a module.

Breadcrumb

Each breadcrumb part corresponds to a segment of the URL and is automatically translated using a translation string for the currently selected language. This translation string needs to be provided by you via the addBreadcrumbTranslation function (see Base Methods). In the base module, this logic is implemented by default for every route:

TranslationProvider.tsx
  //add the translations for static route-names to the breadcrumb
  //foreach key in routes-object of a translation-json
  useEffect(() => {
    const staticRouteNames = en.routes;
    if (staticRouteNames)
      Object.keys(staticRouteNames).forEach((routeKey) => {
        translateKeyInHortiview(routeKey, t(`routes.${routeKey}`));
      });
  }, [t, translateKeyInHortiview]);

  return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
};

Lines 73-75 iterate over every route that is passed to the ModuleBase via the route configuration (see RouteConfig.tsx). It uses the configured translationKey of the passed RouteConfig.tsx and adds the current translation (based on the currently selected language) for all routes that have a translationKey configured.

This requires a proper translation provided in the translation files, which are normally located in a path like this:

  • locales/en-US.json
  • locales/es-MX.json
  • [...other supported languages...]

If we take a look on the base-templates translation, we can see, that we have a translation for the testpage:

en-US.json
{
    "template": {
        "testname": "Test in english",
        "routingtest": "Routing test to Testpage",
        "farmTitle": "First Farm for the organization",
        "testpageTitle": "Testpage",
        "testpageText": "This is a testpage",
        "testpageLink": "Go to Home"
    },
    "routes": {
        "local": "ModuleName",
        "debug": "Debugging",
        "testpage": "Testpage"
    }
}

A translation for the module name and the base route is automatically provided by the platform with the ID and the ModuleName.

Translations for dynamic keys

If you want to include translations for dynamic route elements, like IDs, you need to call the addBreadcrumbTranslation function in your dynamic route component, depending on the ID itself. This ensures that if the ID changes, you still add the translation for the new/changed ID:

Example Route

modules/[id of the module]/season/[Dynamic RouteKey - ID of a season]

Example RouteConfig

RouteConfig.tsx
export const ROUTES = [
{
    path: "*",
    element: <NotFound />,
},
{
    path: "",
    element: <Home />,
},
{
    path: "testpage",
    element: <Testpage />,
},
{
    path: "example",
    element: <Example />,
}
];

The route-defining path-value will be used for the automatic translation, so you need to provide a key for this in the route-configuration.

Implementation of translations

The [id of the module] will be automatically translated by HortiView with your module name.

The [Dynamic RouteKey - ID of a season] can be translated by calling the addBreadcrumbTranslation function each time the value of :id changes inside the Season component. The implementation could look like this:

Season.tsx
export const Season = () => {
    const { t } = useTranslation();
    const { id } = useParams();
    const { data: season, isLoading } = useSeasons({
        filter: `id eq ${id}`
    });
    const addBreadcrumbTranslation = useBaseProps().addBreadcrumbTranslation;
    const { pathname } = useLocation();

    const seasonElement: SeasonElement = useMemo(() => {
        if (!season) return;
        addBreadcrumbTranslation({ key: season.id, value: season.cropSeasonName });

        return {
            id: season.id,
            title: season.cropSeasonName,
        };
    }, [season, addBreadcrumbTranslation, t]);

    if (isLoading) return <LoadingSpinner text={t('common.loading')} />;
    return (
        <>
            <h1>Translation Example with Seasons</h1>
            <h2>{seasonElement.title}</h2>
        </>
    );
};

We get the addBreadcrumbTranslation function via the useBaseProps-Hook from the ModuleBase-Dependency and simply add the translated (name of the season) for the identifier of the season (ID, that is used in the route) in a key-value pair. Keep in mind, that this method will be passed to every module (as property, so you don't need the template for this)