Skip to main content
Refine provides a flexible internationalization (i18n) system that works with any i18n library. It handles translations for built-in components and makes it easy to add multi-language support to your application.

Understanding i18n Provider

An i18n provider is an object with methods for translation and locale management:
import { I18nProvider } from "@refinedev/core";

const i18nProvider: I18nProvider = {
  translate: (key: string, options?: any, defaultMessage?: string) => string,
  changeLocale: (locale: string, options?: any) => Promise<any>,
  getLocale: () => string,
};

Quick Setup

1
Choose an i18n Library
2
Popular choices:
3
  • react-i18next (recommended) - Feature-rich, widely used
  • react-intl - Format.js solution
  • Custom - Build your own
  • 4
    Install Dependencies
    5
    npm install react-i18next i18next i18next-browser-languagedetector i18next-http-backend
    
    6
    Create i18n Configuration
    7
    import i18n from "i18next";
    import { initReactI18next } from "react-i18next";
    import Backend from "i18next-http-backend";
    import detector from "i18next-browser-languagedetector";
    
    i18n
      .use(Backend)
      .use(detector)
      .use(initReactI18next)
      .init({
        supportedLngs: ["en", "es", "fr", "de"],
        backend: {
          loadPath: "/locales/{{lng}}/{{ns}}.json",
        },
        defaultNS: "common",
        fallbackLng: ["en"],
      });
    
    export default i18n;
    
    8
    Create i18n Provider
    9
    import { I18nProvider } from "@refinedev/core";
    import { useTranslation } from "react-i18next";
    
    export const i18nProvider: I18nProvider = {
      translate: (key, options, defaultMessage) => {
        const { t } = useTranslation();
        return t(key, options, defaultMessage);
      },
      changeLocale: (locale: string) => {
        return i18n.changeLanguage(locale);
      },
      getLocale: () => {
        return i18n.language;
      },
    };
    
    10
    Configure Refine
    11
    import { Refine } from "@refinedev/core";
    import { i18nProvider } from "./i18nProvider";
    import "./i18n"; // Initialize i18next
    
    const App = () => (
      <Refine
        i18nProvider={i18nProvider}
        // ... other props
      >
        {/* Your app */}
      </Refine>
    );
    
    12
    Create Translation Files
    13
    Create translation files in your public directory:
    14
    {
      "pages": {
        "login": {
          "title": "Sign in to your account",
          "signin": "Sign in",
          "signup": "Sign up",
          "email": "Email",
          "password": "Password"
        }
      },
      "posts": {
        "posts": "Posts",
        "titles": {
          "list": "Posts",
          "create": "Create Post",
          "edit": "Edit Post",
          "show": "Post Details"
        },
        "fields": {
          "id": "ID",
          "title": "Title",
          "content": "Content",
          "status": "Status",
          "createdAt": "Created At"
        }
      },
      "buttons": {
        "create": "Create",
        "save": "Save",
        "edit": "Edit",
        "delete": "Delete",
        "cancel": "Cancel",
        "refresh": "Refresh"
      }
    }
    
    15
    {
      "pages": {
        "login": {
          "title": "Inicia sesión en tu cuenta",
          "signin": "Iniciar sesión",
          "signup": "Registrarse",
          "email": "Correo electrónico",
          "password": "Contraseña"
        }
      },
      "posts": {
        "posts": "Publicaciones",
        "titles": {
          "list": "Publicaciones",
          "create": "Crear Publicación",
          "edit": "Editar Publicación",
          "show": "Detalles de Publicación"
        },
        "fields": {
          "id": "ID",
          "title": "Título",
          "content": "Contenido",
          "status": "Estado",
          "createdAt": "Creado el"
        }
      },
      "buttons": {
        "create": "Crear",
        "save": "Guardar",
        "edit": "Editar",
        "delete": "Eliminar",
        "cancel": "Cancelar",
        "refresh": "Actualizar"
      }
    }
    

    Using Translations

    useTranslation Hook

    import { useTranslation } from "@refinedev/core";
    
    const PostList = () => {
      const { translate } = useTranslation();
    
      return (
        <div>
          <h1>{translate("posts.titles.list")}</h1>
          <button>{translate("buttons.create")}</button>
        </div>
      );
    };
    

    With Default Values

    const PostList = () => {
      const { translate } = useTranslation();
    
      return (
        <h1>
          {translate("posts.titles.list", "Posts")}
        </h1>
      );
    };
    

    With Interpolation

    // Translation file
    {
      "messages": {
        "welcome": "Welcome, {{name}}!",
        "itemCount": "You have {{count}} items"
      }
    }
    
    // Component
    const Welcome = ({ name, count }: { name: string; count: number }) => {
      const { translate } = useTranslation();
    
      return (
        <div>
          <p>{translate("messages.welcome", { name })}</p>
          <p>{translate("messages.itemCount", { count })}</p>
        </div>
      );
    };
    

    Pluralization

    // Translation file
    {
      "items": {
        "count_one": "{{count}} item",
        "count_other": "{{count}} items"
      }
    }
    
    // Component
    const ItemCount = ({ count }: { count: number }) => {
      const { translate } = useTranslation();
    
      return (
        <p>{translate("items.count", { count })}</p>
      );
    };
    

    Changing Locales

    useSetLocale Hook

    import { useSetLocale, useGetLocale } from "@refinedev/core";
    
    const LanguageSwitcher = () => {
      const changeLanguage = useSetLocale();
      const currentLocale = useGetLocale();
    
      return (
        <select
          value={currentLocale()}
          onChange={(e) => changeLanguage(e.target.value)}
        >
          <option value="en">English</option>
          <option value="es">Español</option>
          <option value="fr">Français</option>
          <option value="de">Deutsch</option>
        </select>
      );
    };
    

    With Flags

    const LanguageSwitcher = () => {
      const changeLanguage = useSetLocale();
      const locale = useGetLocale();
    
      const languages = [
        { code: "en", name: "English", flag: "🇬🇧" },
        { code: "es", name: "Español", flag: "🇪🇸" },
        { code: "fr", name: "Français", flag: "🇫🇷" },
        { code: "de", name: "Deutsch", flag: "🇩🇪" },
      ];
    
      return (
        <div>
          {languages.map((lang) => (
            <button
              key={lang.code}
              onClick={() => changeLanguage(lang.code)}
              className={locale() === lang.code ? "active" : ""}
            >
              <span>{lang.flag}</span>
              <span>{lang.name}</span>
            </button>
          ))}
        </div>
      );
    };
    

    Translating Refine Components

    Refine’s built-in components automatically use translations if available.

    Resource Names

    // Translation file
    {
      "posts": {
        "posts": "Posts",
        "titles": {
          "list": "Post List",
          "create": "Create New Post",
          "edit": "Edit Post",
          "show": "Post Details"
        }
      }
    }
    
    // Refine config
    <Refine
      resources={[
        {
          name: "posts",
          list: "/posts",
          create: "/posts/create",
          // Refine automatically looks for "posts.posts" for display name
          // and "posts.titles.list", "posts.titles.create", etc.
        },
      ]}
    />
    

    Button Labels

    // Translation file
    {
      "buttons": {
        "create": "Create",
        "save": "Save",
        "edit": "Edit",
        "delete": "Delete",
        "cancel": "Cancel",
        "refresh": "Refresh",
        "show": "Show",
        "undo": "Undo",
        "import": "Import",
        "export": "Export"
      }
    }
    
    // Buttons automatically use these translations
    import { CreateButton, EditButton, DeleteButton } from "@refinedev/antd";
    
    <CreateButton /> // Shows translated "Create"
    <EditButton />   // Shows translated "Edit"
    

    Field Labels

    // Translation file
    {
      "posts": {
        "fields": {
          "id": "ID",
          "title": "Title",
          "content": "Content",
          "status": "Status",
          "category": "Category",
          "createdAt": "Created At",
          "updatedAt": "Updated At"
        }
      }
    }
    
    // Use in components
    const PostList = () => {
      const { translate } = useTranslation();
    
      return (
        <table>
          <thead>
            <tr>
              <th>{translate("posts.fields.id")}</th>
              <th>{translate("posts.fields.title")}</th>
              <th>{translate("posts.fields.status")}</th>
            </tr>
          </thead>
        </table>
      );
    };
    

    Notification Messages

    // Translation file
    {
      "notifications": {
        "createSuccess": "Successfully created",
        "createError": "Error creating (status code: {{statusCode}})",
        "editSuccess": "Successfully updated",
        "editError": "Error updating (status code: {{statusCode}})",
        "deleteSuccess": "Successfully deleted",
        "deleteError": "Error deleting (status code: {{statusCode}})"
      }
    }
    
    // Customize per resource
    {
      "posts": {
        "notifications": {
          "createSuccess": "Post created successfully",
          "editSuccess": "Post updated successfully"
        }
      }
    }
    

    Advanced Patterns

    Lazy Loading Translations

    import i18n from "i18next";
    import Backend from "i18next-http-backend";
    
    i18n.use(Backend).init({
      backend: {
        loadPath: "/locales/{{lng}}/{{ns}}.json",
        // Load only when needed
        lazy: true,
      },
    });
    

    Namespace Organization

    // public/locales/en/common.json - Global translations
    {
      "buttons": { ... },
      "messages": { ... }
    }
    
    // public/locales/en/posts.json - Post-specific
    {
      "titles": { ... },
      "fields": { ... }
    }
    
    // public/locales/en/users.json - User-specific
    {
      "titles": { ... },
      "fields": { ... }
    }
    
    // Use specific namespace
    const { translate } = useTranslation();
    translate("titles.list", { ns: "posts" });
    

    Date and Number Formatting

    import { useTranslation } from "@refinedev/core";
    
    const PostList = () => {
      const { getLocale } = useTranslation();
      const locale = getLocale();
    
      const formatDate = (date: Date) => {
        return new Intl.DateTimeFormat(locale, {
          year: "numeric",
          month: "long",
          day: "numeric",
        }).format(date);
      };
    
      const formatCurrency = (amount: number) => {
        return new Intl.NumberFormat(locale, {
          style: "currency",
          currency: "USD",
        }).format(amount);
      };
    
      return (
        <div>
          <p>{formatDate(new Date())}</p>
          <p>{formatCurrency(1234.56)}</p>
        </div>
      );
    };
    

    Right-to-Left (RTL) Support

    import { useEffect } from "react";
    import { useGetLocale } from "@refinedev/core";
    
    const App = () => {
      const getLocale = useGetLocale();
      const locale = getLocale();
    
      const rtlLanguages = ["ar", "he", "fa", "ur"];
      const isRTL = rtlLanguages.includes(locale);
    
      useEffect(() => {
        document.dir = isRTL ? "rtl" : "ltr";
        document.documentElement.lang = locale;
      }, [locale, isRTL]);
    
      return (
        <div className={isRTL ? "rtl" : "ltr"}>
          {/* Your app */}
        </div>
      );
    };
    

    Translation Keys from API

    // Server returns translation keys
    interface Post {
      id: string;
      titleKey: string; // e.g., "posts.myPost.title"
      statusKey: string; // e.g., "statuses.published"
    }
    
    const PostCard = ({ post }: { post: Post }) => {
      const { translate } = useTranslation();
    
      return (
        <div>
          <h2>{translate(post.titleKey)}</h2>
          <span>{translate(post.statusKey)}</span>
        </div>
      );
    };
    

    Persist Language Choice

    import { useSetLocale, useGetLocale } from "@refinedev/core";
    import { useEffect } from "react";
    
    const usePersistedLocale = () => {
      const changeLanguage = useSetLocale();
      const getLocale = useGetLocale();
    
      useEffect(() => {
        // Load saved preference
        const saved = localStorage.getItem("locale");
        if (saved) {
          changeLanguage(saved);
        }
      }, []);
    
      const setLocale = (locale: string) => {
        changeLanguage(locale);
        localStorage.setItem("locale", locale);
      };
    
      return { setLocale, locale: getLocale() };
    };
    

    Translation Management

    Tools for managing translations:
    1. Lokalise - Translation management platform
    2. Crowdin - Collaborative translation
    3. i18n-tasks - Find missing translations
    4. Translation.io - Rails-style translation management

    Complete Translation Reference

    Here’s a complete example with all Refine keys:
    public/locales/en/common.json
    {
      "pages": {
        "login": {
          "title": "Sign in to your account",
          "signin": "Sign in",
          "signup": "Sign up",
          "divider": "or",
          "fields": {
            "email": "Email",
            "password": "Password"
          },
          "errors": {
            "validEmail": "Invalid email address"
          },
          "buttons": {
            "submit": "Login",
            "forgotPassword": "Forgot password?",
            "noAccount": "Don’t have an account?",
            "rememberMe": "Remember me"
          }
        }
      },
      "buttons": {
        "add": "Add",
        "create": "Create",
        "save": "Save",
        "edit": "Edit",
        "delete": "Delete",
        "cancel": "Cancel",
        "confirm": "Confirm",
        "filter": "Filter",
        "clear": "Clear",
        "refresh": "Refresh",
        "show": "Show",
        "undo": "Undo",
        "import": "Import",
        "export": "Export",
        "clone": "Clone",
        "accept": "Accept",
        "reject": "Reject",
        "acceptAll": "Accept All",
        "rejectAll": "Reject All"
      },
      "loading": "Loading",
      "tags": {
        "clone": "Clone"
      },
      "table": {
        "actions": "Actions"
      },
      "search": {
        "placeholder": "Search...",
        "more": "more"
      },
      "notifications": {
        "success": "Successful",
        "error": "Error (status code: {{statusCode}})",
        "undoable": "You have {{seconds}} seconds to undo",
        "createSuccess": "Successfully created {{resource}}",
        "createError": "Error when creating {{resource}} (status code: {{statusCode}})",
        "deleteSuccess": "Successfully deleted {{resource}}",
        "deleteError": "Error when deleting {{resource}} (status code: {{statusCode}})",
        "editSuccess": "Successfully edited {{resource}}",
        "editError": "Error when editing {{resource}} (status code: {{statusCode}})",
        "importProgress": "Importing: {{processed}}/{{total}}"
      },
      "warnWhenUnsavedChanges": "Are you sure you want to leave? You have unsaved changes."
    }
    

    Testing Translations

    Unit Tests

    import { render, screen } from "@testing-library/react";
    import { I18nProvider } from "@refinedev/core";
    
    const mockI18nProvider: I18nProvider = {
      translate: (key) => key,
      changeLocale: async () => {},
      getLocale: () => "en",
    };
    
    test("renders translated text", () => {
      render(
        <I18nProvider value={mockI18nProvider}>
          <MyComponent />
        </I18nProvider>
      );
      
      expect(screen.getByText("posts.titles.list")).toBeInTheDocument();
    });
    

    E2E Tests

    import { test, expect } from "@playwright/test";
    
    test("switches language", async ({ page }) => {
      await page.goto("/posts");
      
      // Check English
      await expect(page.locator("h1")).toHaveText("Posts");
      
      // Switch to Spanish
      await page.selectOption("select[name='language']", "es");
      
      // Check Spanish
      await expect(page.locator("h1")).toHaveText("Publicaciones");
    });
    

    Best Practices

    1. Organize by feature - Use namespaces for different sections
    2. Keep keys semantic - Use descriptive keys, not sentences
    3. Provide context - Add comments for translators
    4. Use interpolation - Don’t concatenate strings
    5. Handle plurals properly - Use pluralization features
    6. Test all languages - Ensure UI works with long translations
    7. Extract hard-coded strings - Use linters to find untranslated text
    8. Version translations - Track changes to translation files

    Troubleshooting

    Translations Not Loading

    1. Check file paths match configuration
    2. Verify JSON files are valid
    3. Check network tab for 404 errors
    4. Ensure i18n is initialized before rendering

    Missing Translations

    1. Use fallback values
    2. Enable debug mode to see missing keys
    3. Use tools to find untranslated strings
    i18n.init({
      debug: true, // Log missing translations
      saveMissing: true, // Save missing keys
      missingKeyHandler: (lng, ns, key) => {
        console.warn(`Missing translation: ${key}`);
      },
    });
    

    Next Steps

    Build docs developers (and LLMs) love