Skip to content

Internationalization (i18n) Setup in SparkyFitnessFrontend ​

This document outlines the technical setup for internationalization (i18n) in the SparkyFitnessFrontend application using i18next and react-i18next.

INFO

Only the English (en) files may be edited by hand. Every other language is translated on Weblate and synced from SparkyFitnessTranslations.

1. Core Libraries ​

The following npm packages are used for i18n:

  • i18next: The core i18n library.
  • react-i18next: Integration for React applications.
  • i18next-browser-languagedetector: Detects the user's language from the browser.
  • i18next-http-backend: Loads translation files over HTTP.

These dependencies are installed in the SparkyFitnessFrontend directory.

2. Translation File Structure ​

Translation files are stored in the public/locales directory, following the format public/locales//translation.json.

  • public/locales/en/translation.json: the English source, and the only file a code contributor edits.
  • public/locales//translation.json: every other language, owned by translators and synced from SparkyFitnessTranslations via Weblate.

Each translation.json file is a simple JSON object where keys represent translation identifiers and values are the translated strings. Nested objects can be used to organize translations (e.g., "nav.diary").

Example (translation.json):

json
{
  "nav": {
    "diary": "Diary",
    "checkin": "Check-In"
  },
  "settings": {
    "profileInformation": {
      "title": "Profile Information"
    }
  }
}

3. i18next Configuration (src/i18n.ts) ​

The i18next instance is configured in SparkyFitnessFrontend/src/i18n.ts.

typescript
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import HttpApi from 'i18next-http-backend';
import { getSupportedLanguages } from './utils/languageUtils';

i18n
  .use(HttpApi)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    supportedLngs: getSupportedLanguages(), // from src/utils/languageUtils.ts
    fallbackLng: 'en', // Fallback language if a translation is missing
    detection: {
      order: ['localStorage', 'querystring', 'cookie', 'sessionStorage', 'navigator', 'htmlTag'],
      caches: ['localStorage', 'cookie'],
    },
    backend: {
      loadPath: '/locales/{{lng}}/{{ns}}.json', // Path to load translation files
    },
    react: {
      useSuspense: false, // Set to true if you want to use React.Suspense for loading translations
    },
  });

export default i18n;

Key Configuration Details:

  • supportedLngs: the languages the app offers, from getSupportedLanguages() in SparkyFitnessFrontend/src/utils/languageUtils.ts. A locale directory that is not listed there cannot be selected.
  • fallbackLng: The language to use if a translation for the current language is missing.
  • detection.order: Specifies the order in which i18next tries to detect the user's language. localStorage is prioritized to use the user's saved preference.
  • backend.loadPath: The URL pattern to fetch translation files. is replaced by the current language code, and by the namespace (defaulting to translation).
  • react.useSuspense: Set to false to avoid using React's Suspense feature for translations, simplifying initial setup.

4. Integration into React Application (src/main.tsx) ​

The i18next instance is initialized and provided to the React application in SparkyFitnessFrontend/src/main.tsx.

tsx
import { createRoot } from 'react-dom/client';
import App from './App.tsx';
import './index.css';
import './i18n'; // side-effect import: configures the shared i18next instance
import { Suspense } from 'react';
// ...

createRoot(document.getElementById('root')!).render(
  <Suspense fallback="loading">
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </Suspense>,
);

react.useSuspense is false, so i18next does not suspend while a catalog loads — it renders the fallback language until the translation arrives. The Suspense boundary above is therefore not what handles translation loading; set useSuspense to true if you want it to.

5. Language Handling Component (src/components/LanguageHandler.tsx) ​

A dedicated component, SparkyFitnessFrontend/src/components/LanguageHandler.tsx, is used to synchronize the i18next language with the user's preference stored in the PreferencesContext.

typescript
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { usePreferences } from '@/contexts/PreferencesContext';

const LanguageHandler = () => {
  const { i18n } = useTranslation();
  const { language } = usePreferences();

  useEffect(() => {
    if (language) {
      i18n.changeLanguage(language);
    }
  }, [language, i18n]);

  return null; // This component doesn't render anything
};

export default LanguageHandler;

This component ensures that when the language preference changes (e.g., via the language switcher in settings), i18next updates its active language.

6. Using Translations in Components ​

To use translations in any React component, import the useTranslation hook from react-i18next.

typescript
import { useTranslation } from 'react-i18next';

const MyComponent = () => {
  const { t } = useTranslation();

  return (
    <div>
      <h1>{t('nav.diary')}</h1>
      <p>{t('settings.profileInformation.description')}</p>
    </div>
  );
};

The t function takes a translation key (e.g., "nav.diary") and returns the corresponding translated string for the currently active language.

7. Language Switcher in Settings (src/pages/Settings/PreferenceSettings.tsx) ​

The language switcher lives in the Preferences section of the settings page. It renders one entry per supported language, so adding a language to languageUtils.ts is enough to make it appear — the list is never hardcoded here.

tsx
import { getLanguageDisplayName, getSupportedLanguages } from "@/utils/languageUtils";
// ...

<Select value={language} onValueChange={setLanguage}>
  <SelectTrigger>
    <SelectValue />
  </SelectTrigger>
  <SelectContent>
    {getSupportedLanguages().map((langCode) => (
      <SelectItem key={langCode} value={langCode}>
        {getLanguageDisplayName(langCode)}
      </SelectItem>
    ))}
  </SelectContent>
</Select>

getLanguageDisplayName returns each language's endonym — Deutsch, Español, 日本語 — so the list reads the same whatever the interface language is.

8. Adding New Languages ​

Translation files are not created by hand. A language is requested on Weblate and translated there. A maintainer then runs the Sync Translations workflow (workflow_dispatch), which copies the non-English files into public/locales/ and opens a pull request. Enabling the language is a separate edit: add the code to getSupportedLanguages() and getLanguageDisplayName() in src/utils/languageUtils.ts.

Released under the GPL-3.0 License.