(JS Poland ver.) Accessibility, better DX, and performance get a lot of attention as it improves better UX significantly. Plus, it gives satisfaction to devs by seeing the significant improvements. But how about internationalization? A fun fact: Over 70% of the users in the world access non-English content. In this talk, I'll show you more surprising facts about internationalization and what are scalable approaches. You'll see examples with libraries for frameworks with a few different logic to implement different internationalization layouts.
16. 5.07 bn. users in the world use the internet
Source: https://www.statista.com/statistics/262946/share-of-the-most-common-languages-on-the-internet/
24. Asia leads more than a half of global
internet users
Source: https://www.statista.com/statistics/262946/share-of-the-most-common-languages-on-the-internet/
45. @arisa_dev
@storyblok
i18next.server.js
import Backend from "i18next-fs-backend";
import { resolve } from "node:path";
import { RemixI18Next } from "remix-i18next";
import i18n from "~/i18n"; // i18next config file
import languageCookie from "~/cookie";
let i18next = new RemixI18Next({
detection: {
supportedLanguages: i18n.supportedLngs,
fallbackLanguage: i18n.fallbackLng,
cookie: languageCookie,
},
// This is the config for i18next & when translating messages server-side only
i18next: {
...i18n, // Iterate arr/strings from i18next config
backend: {
// Translation file paths
loadPath: resolve(“./public/locales/{{lng}}/{{ns}}.json"),
},
},
// Backend to load the translation
backend: Backend,
});
export default i18next;
46. @arisa_dev
@storyblok
i18next.server.js
import Backend from "i18next-fs-backend";
import { resolve } from "node:path";
import { RemixI18Next } from "remix-i18next";
import i18n from "~/i18n"; // i18next config file
import languageCookie from "~/cookie";
let i18next = new RemixI18Next({
detection: {
supportedLanguages: i18n.supportedLngs,
fallbackLanguage: i18n.fallbackLng,
cookie: languageCookie,
},
// This is the config for i18next & when translating messages server-side only
i18next: {
...i18n, // Iterate arr/strings from i18next config
backend: {
// Translation file paths
loadPath: resolve(“./public/locales/{{lng}}/{{ns}}.json"),
},
},
// Backend to load the translation
backend: Backend,
});
export default i18next;
47. @arisa_dev
@storyblok
i18next.server.js
import Backend from "i18next-fs-backend";
import { resolve } from "node:path";
import { RemixI18Next } from "remix-i18next";
import i18n from "~/i18n"; // i18next config file
import languageCookie from "~/cookie";
let i18next = new RemixI18Next({
detection: {
supportedLanguages: i18n.supportedLngs,
fallbackLanguage: i18n.fallbackLng,
cookie: languageCookie,
},
// This is the config for i18next & when translating messages server-side only
i18next: {
...i18n, // Iterate arr/strings from i18next config
backend: {
// Translation file paths
loadPath: resolve(“./public/locales/{{lng}}/{{ns}}.json"),
},
},
// Backend to load the translation
backend: Backend,
});
export default i18next;
48. @arisa_dev
@storyblok
i18next.server.js
import Backend from "i18next-fs-backend";
import { resolve } from "node:path";
import { RemixI18Next } from "remix-i18next";
import i18n from "~/i18n"; // i18next config file
import languageCookie from "~/cookie";
let i18next = new RemixI18Next({
detection: {
supportedLanguages: i18n.supportedLngs,
fallbackLanguage: i18n.fallbackLng,
cookie: languageCookie,
},
// This is the config for i18next & when translating messages server-side only
i18next: {
...i18n, // Iterate arr/strings from i18next config
backend: {
// Translation file paths
loadPath: resolve(“./public/locales/{{lng}}/{{ns}}.json"),
},
},
// Backend to load the translation
backend: Backend,
});
export default i18next;
49. @arisa_dev
@storyblok
i18next.server.js
import Backend from "i18next-fs-backend";
import { resolve } from "node:path";
import { RemixI18Next } from "remix-i18next";
import i18n from "~/i18n"; // i18next config file
import languageCookie from "~/cookie";
let i18next = new RemixI18Next({
detection: {
supportedLanguages: i18n.supportedLngs,
fallbackLanguage: i18n.fallbackLng,
cookie: languageCookie,
},
// This is the config for i18next & when translating messages server-side only
i18next: {
...i18n, // Iterate arr/strings from i18next config
backend: {
// Translation file paths
loadPath: resolve(“./public/locales/{{lng}}/{{ns}}.json"),
},
},
// Backend to load the translation
backend: Backend,
});
export default i18next;
50. Create Client-
side & Server-
side config files
→
@arisa_dev
entry.client.jsx
→ entry.server.jsx
51. @arisa_dev
@storyblok
entry.client.jsx
import { RemixBrowser } from "@remix-run/react";
import { hydrateRoot } from "react-dom/client";
import i18next from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";
import { I18nextProvider, initReactI18next } from "react-i18next";
import { getInitialNamespaces } from "remix-i18next";
import i18n from "./i18n";
i18next
// … i18next init, client-side lang detector, backend & namespace configs, etc
.then(() => {
// After i18next init, hydrate the app
// Wait to ensure translations are loaded before the hydration → WHY? Next slide.
// Wrap RemixBrowser in I18nextProvider → WHY? Next slide.
hydrateRoot(
document,
<I18nextProvider i18n={i18next}>
<RemixBrowser /> // It’s used by React to hydrate html ← received from server
</I18nextProvider>
);
});
52. @arisa_dev
@storyblok
entry.client.jsx
import { RemixBrowser } from "@remix-run/react";
import { hydrateRoot } from "react-dom/client";
import i18next from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";
import { I18nextProvider, initReactI18next } from "react-i18next";
import { getInitialNamespaces } from "remix-i18next";
import i18n from "./i18n";
i18next
// … i18next init, client-side lang detector, backend & namespace configs, etc
.then(() => {
// After i18next init, hydrate the app
// Wait to ensure translations are loaded before the hydration → WHY? Next slide.
// Wrap RemixBrowser in I18nextProvider → WHY? Next slide.
hydrateRoot(
document,
<I18nextProvider i18n={i18next}>
<RemixBrowser /> // It’s used by React to hydrate html ← received from server
</I18nextProvider>
);
});
53. @arisa_dev
@storyblok
entry.client.jsx
import { RemixBrowser } from "@remix-run/react";
import { hydrateRoot } from "react-dom/client";
import i18next from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";
import { I18nextProvider, initReactI18next } from "react-i18next";
import { getInitialNamespaces } from "remix-i18next";
import i18n from "./i18n";
i18next
// … i18next init, client-side lang detector, backend & namespace configs, etc
.then(() => {
// After i18next init, hydrate the app
// Wait to ensure translations are loaded before the hydration → WHY? Next slide.
// Wrap RemixBrowser in I18nextProvider → WHY? Next slide.
hydrateRoot(
document,
<I18nextProvider i18n={i18next}>
<RemixBrowser /> // It’s used by React to hydrate html ← received from server
</I18nextProvider>
);
});
62. @arisa_dev
@storyblok
app/root.jsx
// …
import { useChangeLanguage } from "remix-i18next";
import { useTranslation } from "react-i18next";
import i18next from "~/i18next.server";
export let loader = async ({ request }) => { // loader is Backend API
let locale = await i18next.getLocale(request);
return json({ locale });
};
export let handle = {
i18n: "common",
};
export default function App() {
let { locale } = useLoaderData(); // Get the locale from the loader func
let { i18n } = useTranslation();
// useChangeLanguage updates the i18n instance lang to the current locale from loader
// Locale will be updated & i18next loads the correct translation files
useChangeLanguage(locale);
return (
<html lang={locale} dir={i18n.dir()}>
// <head /><body /> etc…
</html>
);
}
63. @arisa_dev
@storyblok
app/root.jsx
// …
import { useChangeLanguage } from "remix-i18next";
import { useTranslation } from "react-i18next";
import i18next from "~/i18next.server";
export let loader = async ({ request }) => { // loader is Backend API
let locale = await i18next.getLocale(request);
return json({ locale });
};
export let handle = {
i18n: "common",
};
export default function App() {
let { locale } = useLoaderData(); // Get the locale from the loader func
let { i18n } = useTranslation();
// useChangeLanguage updates the i18n instance lang to the current locale from loader
// Locale will be updated & i18next loads the correct translation files
useChangeLanguage(locale);
return (
<html lang={locale} dir={i18n.dir()}>
// <head /><body /> etc…
</html>
);
}
64. @arisa_dev
@storyblok
app/root.jsx
// …
import { useChangeLanguage } from "remix-i18next";
import { useTranslation } from "react-i18next";
import i18next from "~/i18next.server";
export let loader = async ({ request }) => { // loader is Backend API
let locale = await i18next.getLocale(request);
return json({ locale });
};
export let handle = {
i18n: "common",
};
export default function App() {
let { locale } = useLoaderData(); // Get the locale from the loader func
let { i18n } = useTranslation();
// useChangeLanguage updates the i18n instance lang to the current locale from loader
// Locale will be updated & i18next loads the correct translation files
useChangeLanguage(locale);
return (
<html lang={locale} dir={i18n.dir()}>
// <head /><body /> etc…
</html>
);
}
65. @arisa_dev
@storyblok
app/root.jsx
// …
import { useChangeLanguage } from "remix-i18next";
import { useTranslation } from "react-i18next";
import i18next from "~/i18next.server";
export let loader = async ({ request }) => { // loader is Backend API
let locale = await i18next.getLocale(request);
return json({ locale });
};
export let handle = {
i18n: "common",
};
export default function App() {
let { locale } = useLoaderData(); // Get the locale from the loader func
let { i18n } = useTranslation();
// useChangeLanguage updates the i18n instance lang to the current locale from loader
// Locale will be updated & i18next loads the correct translation files
useChangeLanguage(locale);
return (
<html lang={locale} dir={i18n.dir()}>
// <head /><body /> etc…
</html>
);
}
66. @arisa_dev
@storyblok
app/root.jsx
// …
import { useChangeLanguage } from "remix-i18next";
import { useTranslation } from "react-i18next";
import i18next from "~/i18next.server";
export let loader = async ({ request }) => { // loader is Backend API
let locale = await i18next.getLocale(request);
return json({ locale });
};
export let handle = {
i18n: "common",
};
export default function App() {
let { locale } = useLoaderData(); // Get the locale from the loader func
let { i18n } = useTranslation();
// useChangeLanguage updates the i18n instance lang to the current locale from loader
// Locale will be updated & i18next loads the correct translation files
useChangeLanguage(locale);
return (
<html lang={locale} dir={i18n.dir()}>
// <head /><body /> etc…
</html>
);
}
67. @arisa_dev
@storyblok
any route
import { useTranslation } from
'react-i18next';
export default function Component() {
let { t } = useTranslation();
return <h1>{t("greeting")}</h1>;
}
68. @arisa_dev
@storyblok
any route
import { useTranslation } from
'react-i18next';
export default function Component() {
let { t } = useTranslation();
return <h1>{t("greeting")}</h1>;
}
85. Summary
● More than a half of the users in the
world access localized content
● Know more approaches to find
better DX for your case
● i18n is related to performance, UI
&UX
@arisa_dev