SlideShare a Scribd company logo
1 of 86
Download to read offline
Dzien dobry👋
i18n was the missing piece
ARISA FUKUZAKI
Senior DevRel Engineer at Storyblok
Make your apps accessible to 70%+ of the users in the world
Arisa Fukuzaki
福﨑 有彩
Senior DevRel Engineer
GirlCode Ambassador
GDE, Web Technologies
🥑
👩💻
@arisa_dev
“Do you like to implement i18n logic?”
Talk slides
@arisa_dev
🌴 linktr.ee/arisa_dev
3 takeaways
from my talk
→
@arisa_dev
Impact of i18n
i18n fundamental logics
→
→ How Remix i18n works
Notes ⚠
→
@arisa_dev
There’re still discussions going
on about Remix & i18n features
Still some improvements
→
→ Join discussions #2877
Internationalization (i18n)
18 characters
“Do you like to implement i18n logic?”
Maybe, their i18n DX is not good? 🤔
Seems it’s not the best DX 👀
Based on the i18n DX, let’s talk about
numbers & facts 📊
5.07 Billion
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/
25.9%
English is used only 25.9% on the internet
Source: https://www.statista.com/statistics/262946/share-of-the-most-common-languages-on-the-internet/
74.1%
74.1% users access non-English content
Source: https://www.statista.com/statistics/262946/share-of-the-most-common-languages-on-the-internet/
China
China has the most internet users
worldwide
Source: https://www.statista.com/statistics/262946/share-of-the-most-common-languages-on-the-internet/
Asia
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/
Huge numbers to ignore
Knowing different approaches with
more options will solve i18n DX🪄
Let’s talk about fundamental
logic
3 ways to
Determine
languages &
regions
→
@arisa_dev
Location from IP address
Accept-Language header/
Navigator.languages
→
→ Identifier in URL
We use 2
ways - hybrid
→
@arisa_dev
Location from IP address
Accept-Language header/
Navigator.languages
→
→ Identifier in URL
3 identifier URL patterns
Differentiate by domains (Won’t
follow the same-origin policy🙅 )
@arisa_dev
hello.com
hello.es
Pattern 1
URL parameters (NOT user
friendly🙅 )
@arisa_dev
hello.com?loc=de
hello.com?loc=nl-NL
Pattern 2
Localized sub-directories 👍
@arisa_dev
hello.com/ja
hello.com/nl-NL
Pattern 3
Let’s talk about Frameworks
& libs
@arisa_dev
Some frameworks & libs use i18n
frameworks.
Let’s see how it works in
Remix
2 approaches
to choose
→
@arisa_dev
remix-i18next
→ CMS
remix-i18next is a npm package for
Remix to use i18next.
remix-i18next example
Default and a preferred language.
→
@arisa_dev
{
“greeting”: “Hello”
}
Create translation
files
public/locales/en/common.json
{
“greeting”: “こんにちは”
}
public/locales/ja/common.json
i18next config file.
→
@arisa_dev
export default {
supportedLngs: ['en', 'ja'],
fallbackLng: 'en',
// customize namespace here
defaultNS: 'common',
react: {useSuspense: false},
};
Create app/
i18n.js
app/i18n.js
i18next config file.
→
@arisa_dev
export default {
supportedLngs: ['en', 'ja'],
fallbackLng: 'en',
// customize namespace here
defaultNS: 'common',
react: {useSuspense: false},
};
Create app/
i18n.js
app/i18n.js
i18next config file.
→
@arisa_dev
export default {
supportedLngs: ['en', 'ja'],
fallbackLng: 'en',
// customize namespace here
defaultNS: 'common',
react: {useSuspense: false},
};
Create app/
i18n.js
app/i18n.js
i18next config file.
→
@arisa_dev
export default {
supportedLngs: ['en', 'ja'],
fallbackLng: 'en',
// customize namespace here
defaultNS: 'common',
react: {useSuspense: false},
};
Create app/
i18n.js
app/i18n.js
@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;
@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;
@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;
@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;
@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;
Create Client-
side & Server-
side config files
→
@arisa_dev
entry.client.jsx
→ entry.server.jsx
@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>
);
});
@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>
);
});
@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>
);
});
Why translation should be loaded
before hydration?
The app is not yet interactive
→
@arisa_dev
If translation is
NOT loaded
before hydration English 日本語 “Hello”
?
@arisa_dev
English 日本語 “こんにちは”
The app is interactive
→
If translation is
loaded before
hydration
Why wrapping RemixBrowser with
I18nextProvider?
@arisa_dev
@storyblok
i18nextProvider from react-i18next
import { createElement, useMemo } from 'react';
import { I18nContext } from './context';
export function I18nextProvider({ i18n, defaultNS, children }) {
const value = useMemo(
() => ({
i18n, // i18n config
defaultNS, // default namespace
}),
[i18n, defaultNS], // i18n & defaultNS are the same ? cache calc : re-render
);
return createElement(
// …
}
@arisa_dev
@storyblok
i18nextProvider from react-i18next
import { createElement, useMemo } from 'react';
import { I18nContext } from './context';
export function I18nextProvider({ i18n, defaultNS, children }) {
const value = useMemo(
() => ({
i18n, // i18n config
defaultNS, // default namespace
}),
[i18n, defaultNS], // i18n & defaultNS are the same ? cache calc : re-render
);
return createElement(
// …
}
@arisa_dev
@storyblok
i18nextProvider from react-i18next
import { createElement, useMemo } from 'react';
import { I18nContext } from './context';
export function I18nextProvider({ i18n, defaultNS, children }) {
const value = useMemo(
() => ({
i18n, // i18n config
defaultNS, // default namespace
}),
[i18n, defaultNS], // i18n & defaultNS are the same ? cache calc : re-render
);
return createElement(
// …
}
Let’s use configs in action
@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>
);
}
@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>
);
}
@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>
);
}
@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>
);
}
@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>
);
}
@arisa_dev
@storyblok
any route
import { useTranslation } from
'react-i18next';
export default function Component() {
let { t } = useTranslation();
return <h1>{t("greeting")}</h1>;
}
@arisa_dev
@storyblok
any route
import { useTranslation } from
'react-i18next';
export default function Component() {
let { t } = useTranslation();
return <h1>{t("greeting")}</h1>;
}
I have 3 confessions.
1.I used URL param
2. Do we (devs) need to maintain
translation files…?
3. Did we translate slugs…?
We want to
achieve…
→
@arisa_dev
Localized URL (sub-directory)
→ No translation files in code
(Headless) CMS example
Connect your
Remix app
with CMS
@arisa_dev
i.e. Remix & Storyblok 5 min tutorial: https://
www.storyblok.com/tp/headless-cms-remix
Choose
between 4
approaches →
@arisa_dev
Mix above
→ Space-level translation
→ Field-level translation
→ Folder-level translation
Choose
between 4
approaches →
@arisa_dev
Mix above
→ Space-level translation
→ Field-level translation
→ Folder-level translation
Splats
@arisa_dev
@arisa_dev
@storyblok
app/routes/$.tsx
export default function Page() {
// useLoaderData returns JSON parsed data from loader func
let story = useLoaderData();
story = useStoryblokState(story, {
resolveRelations: ["featured-posts.posts", "selected-posts.posts"]
});
return <StoryblokComponent blok={story.content} />
};
// loader is Backend API & Wired up through useLoaderData
export const loader = async ({ params, preview = false }) => {
let slug = params["*"] ?? "home";
slug = slug.endsWith("/") ? slug.slice(0, -1) : slug;
let sbParams = {
version: "draft",
resolve_relations: ["featured-posts.posts", "selected-posts.posts"],
};
// …
let { data } = await getStoryblokApi().get(`cdn/stories/${slug}`,
sbParams);
return json(data?.story, preview);
};
@arisa_dev
@storyblok
app/routes/$.tsx
export default function Page() {
// useLoaderData returns JSON parsed data from loader func
let story = useLoaderData();
story = useStoryblokState(story, {
resolveRelations: ["featured-posts.posts", "selected-posts.posts"]
});
return <StoryblokComponent blok={story.content} />
};
// loader is Backend API & Wired up through useLoaderData
export const loader = async ({ params, preview = false }) => {
let slug = params["*"] ?? "home";
slug = slug.endsWith("/") ? slug.slice(0, -1) : slug;
let sbParams = {
version: "draft",
resolve_relations: ["featured-posts.posts", "selected-posts.posts"],
};
// …
let { data } = await getStoryblokApi().get(`cdn/stories/${slug}`,
sbParams);
return json(data?.story, preview);
};
@arisa_dev
@storyblok
app/routes/$.tsx
export default function Page() {
// useLoaderData returns JSON parsed data from loader func
let story = useLoaderData();
story = useStoryblokState(story, {
resolveRelations: ["featured-posts.posts", "selected-posts.posts"]
});
return <StoryblokComponent blok={story.content} />
};
// loader is Backend API & Wired up through useLoaderData
export const loader = async ({ params, preview = false }) => {
let slug = params["*"] ?? "home";
slug = slug.endsWith("/") ? slug.slice(0, -1) : slug;
let sbParams = {
version: "draft",
resolve_relations: ["featured-posts.posts", "selected-posts.posts"],
};
// …
let { data } = await getStoryblokApi().get(`cdn/stories/${slug}`,
sbParams);
return json(data?.story, preview);
};
Example repo
@arisa_dev
i.e. Remix & Storyblok i18n repo:
https://github.com/schabibi1/remix-i18n-talk
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
Thank you - ありがとう
@arisa_dev

More Related Content

Similar to Implement i18n in Remix with remix-i18next

ZFConf 2012: Capistrano для деплоймента PHP-приложений (Роман Лапин)
ZFConf 2012: Capistrano для деплоймента PHP-приложений (Роман Лапин)ZFConf 2012: Capistrano для деплоймента PHP-приложений (Роман Лапин)
ZFConf 2012: Capistrano для деплоймента PHP-приложений (Роман Лапин)ZFConf Conference
 
Writing multi-language documentation using Sphinx
Writing multi-language documentation using SphinxWriting multi-language documentation using Sphinx
Writing multi-language documentation using SphinxMarkus Zapke-Gründemann
 
Hosting Your Own OTA Update Service
Hosting Your Own OTA Update ServiceHosting Your Own OTA Update Service
Hosting Your Own OTA Update ServiceQuinlan Jung
 
Building with Firebase
Building with FirebaseBuilding with Firebase
Building with FirebaseMike Fowler
 
How React Native, Appium and me made each other shine @ContinuousDeliveryAmst...
How React Native, Appium and me made each other shine @ContinuousDeliveryAmst...How React Native, Appium and me made each other shine @ContinuousDeliveryAmst...
How React Native, Appium and me made each other shine @ContinuousDeliveryAmst...Wim Selles
 
Ultimate Survival - React-Native edition
Ultimate Survival - React-Native editionUltimate Survival - React-Native edition
Ultimate Survival - React-Native editionRichard Radics
 
Introduction to Spring Boot.pdf
Introduction to Spring Boot.pdfIntroduction to Spring Boot.pdf
Introduction to Spring Boot.pdfShaiAlmog1
 
XebiConFr 15 - Brace yourselves Angular 2 is coming
XebiConFr 15 - Brace yourselves Angular 2 is comingXebiConFr 15 - Brace yourselves Angular 2 is coming
XebiConFr 15 - Brace yourselves Angular 2 is comingPublicis Sapient Engineering
 
Deploying configurable frontend web application containers
Deploying configurable frontend web application containersDeploying configurable frontend web application containers
Deploying configurable frontend web application containersJosé Moreira
 
You got database in my cloud!
You got database  in my cloud!You got database  in my cloud!
You got database in my cloud!Liz Frost
 
Puppet at Pinterest
Puppet at PinterestPuppet at Pinterest
Puppet at PinterestPuppet
 
DCEU 18: App-in-a-Box with Docker Application Packages
DCEU 18: App-in-a-Box with Docker Application PackagesDCEU 18: App-in-a-Box with Docker Application Packages
DCEU 18: App-in-a-Box with Docker Application PackagesDocker, Inc.
 
Mobile Open Day: React Native: Crossplatform fast dive
Mobile Open Day: React Native: Crossplatform fast diveMobile Open Day: React Native: Crossplatform fast dive
Mobile Open Day: React Native: Crossplatform fast diveepamspb
 
Deploy Serverless Apps with Python: AWS Chalice Deep Dive (DEV427-R2) - AWS r...
Deploy Serverless Apps with Python: AWS Chalice Deep Dive (DEV427-R2) - AWS r...Deploy Serverless Apps with Python: AWS Chalice Deep Dive (DEV427-R2) - AWS r...
Deploy Serverless Apps with Python: AWS Chalice Deep Dive (DEV427-R2) - AWS r...Amazon Web Services
 
2017-03-11 02 Денис Нелюбин. Docker & Ansible - лучшие друзья DevOps
2017-03-11 02 Денис Нелюбин. Docker & Ansible - лучшие друзья DevOps2017-03-11 02 Денис Нелюбин. Docker & Ansible - лучшие друзья DevOps
2017-03-11 02 Денис Нелюбин. Docker & Ansible - лучшие друзья DevOpsОмские ИТ-субботники
 
Generating efficient APK by Reducing Size and Improving Performance
Generating efficient APK by Reducing Size and Improving PerformanceGenerating efficient APK by Reducing Size and Improving Performance
Generating efficient APK by Reducing Size and Improving PerformanceParesh Mayani
 
Introduction to React Native Workshop
Introduction to React Native WorkshopIntroduction to React Native Workshop
Introduction to React Native WorkshopIgnacio Martín
 

Similar to Implement i18n in Remix with remix-i18next (20)

ZFConf 2012: Capistrano для деплоймента PHP-приложений (Роман Лапин)
ZFConf 2012: Capistrano для деплоймента PHP-приложений (Роман Лапин)ZFConf 2012: Capistrano для деплоймента PHP-приложений (Роман Лапин)
ZFConf 2012: Capistrano для деплоймента PHP-приложений (Роман Лапин)
 
Writing multi-language documentation using Sphinx
Writing multi-language documentation using SphinxWriting multi-language documentation using Sphinx
Writing multi-language documentation using Sphinx
 
Hosting Your Own OTA Update Service
Hosting Your Own OTA Update ServiceHosting Your Own OTA Update Service
Hosting Your Own OTA Update Service
 
Building with Firebase
Building with FirebaseBuilding with Firebase
Building with Firebase
 
Capistrano
CapistranoCapistrano
Capistrano
 
How React Native, Appium and me made each other shine @ContinuousDeliveryAmst...
How React Native, Appium and me made each other shine @ContinuousDeliveryAmst...How React Native, Appium and me made each other shine @ContinuousDeliveryAmst...
How React Native, Appium and me made each other shine @ContinuousDeliveryAmst...
 
Ultimate Survival - React-Native edition
Ultimate Survival - React-Native editionUltimate Survival - React-Native edition
Ultimate Survival - React-Native edition
 
Introduction to Spring Boot.pdf
Introduction to Spring Boot.pdfIntroduction to Spring Boot.pdf
Introduction to Spring Boot.pdf
 
Android workshop
Android workshopAndroid workshop
Android workshop
 
XebiConFr 15 - Brace yourselves Angular 2 is coming
XebiConFr 15 - Brace yourselves Angular 2 is comingXebiConFr 15 - Brace yourselves Angular 2 is coming
XebiConFr 15 - Brace yourselves Angular 2 is coming
 
Deploying configurable frontend web application containers
Deploying configurable frontend web application containersDeploying configurable frontend web application containers
Deploying configurable frontend web application containers
 
You got database in my cloud!
You got database  in my cloud!You got database  in my cloud!
You got database in my cloud!
 
Puppet at Pinterest
Puppet at PinterestPuppet at Pinterest
Puppet at Pinterest
 
DCEU 18: App-in-a-Box with Docker Application Packages
DCEU 18: App-in-a-Box with Docker Application PackagesDCEU 18: App-in-a-Box with Docker Application Packages
DCEU 18: App-in-a-Box with Docker Application Packages
 
第26回PHP勉強会
第26回PHP勉強会第26回PHP勉強会
第26回PHP勉強会
 
Mobile Open Day: React Native: Crossplatform fast dive
Mobile Open Day: React Native: Crossplatform fast diveMobile Open Day: React Native: Crossplatform fast dive
Mobile Open Day: React Native: Crossplatform fast dive
 
Deploy Serverless Apps with Python: AWS Chalice Deep Dive (DEV427-R2) - AWS r...
Deploy Serverless Apps with Python: AWS Chalice Deep Dive (DEV427-R2) - AWS r...Deploy Serverless Apps with Python: AWS Chalice Deep Dive (DEV427-R2) - AWS r...
Deploy Serverless Apps with Python: AWS Chalice Deep Dive (DEV427-R2) - AWS r...
 
2017-03-11 02 Денис Нелюбин. Docker & Ansible - лучшие друзья DevOps
2017-03-11 02 Денис Нелюбин. Docker & Ansible - лучшие друзья DevOps2017-03-11 02 Денис Нелюбин. Docker & Ansible - лучшие друзья DevOps
2017-03-11 02 Денис Нелюбин. Docker & Ansible - лучшие друзья DevOps
 
Generating efficient APK by Reducing Size and Improving Performance
Generating efficient APK by Reducing Size and Improving PerformanceGenerating efficient APK by Reducing Size and Improving Performance
Generating efficient APK by Reducing Size and Improving Performance
 
Introduction to React Native Workshop
Introduction to React Native WorkshopIntroduction to React Native Workshop
Introduction to React Native Workshop
 

Recently uploaded

#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024BookNet Canada
 
Swan(sea) Song – personal research during my six years at Swansea ... and bey...
Swan(sea) Song – personal research during my six years at Swansea ... and bey...Swan(sea) Song – personal research during my six years at Swansea ... and bey...
Swan(sea) Song – personal research during my six years at Swansea ... and bey...Alan Dix
 
My Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 PresentationMy Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 PresentationRidwan Fadjar
 
The Codex of Business Writing Software for Real-World Solutions 2.pptx
The Codex of Business Writing Software for Real-World Solutions 2.pptxThe Codex of Business Writing Software for Real-World Solutions 2.pptx
The Codex of Business Writing Software for Real-World Solutions 2.pptxMalak Abu Hammad
 
Scanning the Internet for External Cloud Exposures via SSL Certs
Scanning the Internet for External Cloud Exposures via SSL CertsScanning the Internet for External Cloud Exposures via SSL Certs
Scanning the Internet for External Cloud Exposures via SSL CertsRizwan Syed
 
Understanding the Laravel MVC Architecture
Understanding the Laravel MVC ArchitectureUnderstanding the Laravel MVC Architecture
Understanding the Laravel MVC ArchitecturePixlogix Infotech
 
Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...
Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...
Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...shyamraj55
 
SIEMENS: RAPUNZEL – A Tale About Knowledge Graph
SIEMENS: RAPUNZEL – A Tale About Knowledge GraphSIEMENS: RAPUNZEL – A Tale About Knowledge Graph
SIEMENS: RAPUNZEL – A Tale About Knowledge GraphNeo4j
 
Key Features Of Token Development (1).pptx
Key  Features Of Token  Development (1).pptxKey  Features Of Token  Development (1).pptx
Key Features Of Token Development (1).pptxLBM Solutions
 
Unblocking The Main Thread Solving ANRs and Frozen Frames
Unblocking The Main Thread Solving ANRs and Frozen FramesUnblocking The Main Thread Solving ANRs and Frozen Frames
Unblocking The Main Thread Solving ANRs and Frozen FramesSinan KOZAK
 
Streamlining Python Development: A Guide to a Modern Project Setup
Streamlining Python Development: A Guide to a Modern Project SetupStreamlining Python Development: A Guide to a Modern Project Setup
Streamlining Python Development: A Guide to a Modern Project SetupFlorian Wilhelm
 
Advanced Test Driven-Development @ php[tek] 2024
Advanced Test Driven-Development @ php[tek] 2024Advanced Test Driven-Development @ php[tek] 2024
Advanced Test Driven-Development @ php[tek] 2024Scott Keck-Warren
 
SQL Database Design For Developers at php[tek] 2024
SQL Database Design For Developers at php[tek] 2024SQL Database Design For Developers at php[tek] 2024
SQL Database Design For Developers at php[tek] 2024Scott Keck-Warren
 
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | DelhiFULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhisoniya singh
 
Pigging Solutions in Pet Food Manufacturing
Pigging Solutions in Pet Food ManufacturingPigging Solutions in Pet Food Manufacturing
Pigging Solutions in Pet Food ManufacturingPigging Solutions
 
AI as an Interface for Commercial Buildings
AI as an Interface for Commercial BuildingsAI as an Interface for Commercial Buildings
AI as an Interface for Commercial BuildingsMemoori
 
Are Multi-Cloud and Serverless Good or Bad?
Are Multi-Cloud and Serverless Good or Bad?Are Multi-Cloud and Serverless Good or Bad?
Are Multi-Cloud and Serverless Good or Bad?Mattias Andersson
 
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...Patryk Bandurski
 

Recently uploaded (20)

#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
 
Swan(sea) Song – personal research during my six years at Swansea ... and bey...
Swan(sea) Song – personal research during my six years at Swansea ... and bey...Swan(sea) Song – personal research during my six years at Swansea ... and bey...
Swan(sea) Song – personal research during my six years at Swansea ... and bey...
 
My Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 PresentationMy Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 Presentation
 
The Codex of Business Writing Software for Real-World Solutions 2.pptx
The Codex of Business Writing Software for Real-World Solutions 2.pptxThe Codex of Business Writing Software for Real-World Solutions 2.pptx
The Codex of Business Writing Software for Real-World Solutions 2.pptx
 
Scanning the Internet for External Cloud Exposures via SSL Certs
Scanning the Internet for External Cloud Exposures via SSL CertsScanning the Internet for External Cloud Exposures via SSL Certs
Scanning the Internet for External Cloud Exposures via SSL Certs
 
Understanding the Laravel MVC Architecture
Understanding the Laravel MVC ArchitectureUnderstanding the Laravel MVC Architecture
Understanding the Laravel MVC Architecture
 
Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...
Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...
Automating Business Process via MuleSoft Composer | Bangalore MuleSoft Meetup...
 
SIEMENS: RAPUNZEL – A Tale About Knowledge Graph
SIEMENS: RAPUNZEL – A Tale About Knowledge GraphSIEMENS: RAPUNZEL – A Tale About Knowledge Graph
SIEMENS: RAPUNZEL – A Tale About Knowledge Graph
 
Key Features Of Token Development (1).pptx
Key  Features Of Token  Development (1).pptxKey  Features Of Token  Development (1).pptx
Key Features Of Token Development (1).pptx
 
Unblocking The Main Thread Solving ANRs and Frozen Frames
Unblocking The Main Thread Solving ANRs and Frozen FramesUnblocking The Main Thread Solving ANRs and Frozen Frames
Unblocking The Main Thread Solving ANRs and Frozen Frames
 
Streamlining Python Development: A Guide to a Modern Project Setup
Streamlining Python Development: A Guide to a Modern Project SetupStreamlining Python Development: A Guide to a Modern Project Setup
Streamlining Python Development: A Guide to a Modern Project Setup
 
Advanced Test Driven-Development @ php[tek] 2024
Advanced Test Driven-Development @ php[tek] 2024Advanced Test Driven-Development @ php[tek] 2024
Advanced Test Driven-Development @ php[tek] 2024
 
SQL Database Design For Developers at php[tek] 2024
SQL Database Design For Developers at php[tek] 2024SQL Database Design For Developers at php[tek] 2024
SQL Database Design For Developers at php[tek] 2024
 
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | DelhiFULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
 
Pigging Solutions in Pet Food Manufacturing
Pigging Solutions in Pet Food ManufacturingPigging Solutions in Pet Food Manufacturing
Pigging Solutions in Pet Food Manufacturing
 
E-Vehicle_Hacking_by_Parul Sharma_null_owasp.pptx
E-Vehicle_Hacking_by_Parul Sharma_null_owasp.pptxE-Vehicle_Hacking_by_Parul Sharma_null_owasp.pptx
E-Vehicle_Hacking_by_Parul Sharma_null_owasp.pptx
 
The transition to renewables in India.pdf
The transition to renewables in India.pdfThe transition to renewables in India.pdf
The transition to renewables in India.pdf
 
AI as an Interface for Commercial Buildings
AI as an Interface for Commercial BuildingsAI as an Interface for Commercial Buildings
AI as an Interface for Commercial Buildings
 
Are Multi-Cloud and Serverless Good or Bad?
Are Multi-Cloud and Serverless Good or Bad?Are Multi-Cloud and Serverless Good or Bad?
Are Multi-Cloud and Serverless Good or Bad?
 
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
 

Implement i18n in Remix with remix-i18next

  • 2. i18n was the missing piece ARISA FUKUZAKI Senior DevRel Engineer at Storyblok Make your apps accessible to 70%+ of the users in the world
  • 3. Arisa Fukuzaki 福﨑 有彩 Senior DevRel Engineer GirlCode Ambassador GDE, Web Technologies 🥑 👩💻 @arisa_dev
  • 4. “Do you like to implement i18n logic?”
  • 6. 3 takeaways from my talk → @arisa_dev Impact of i18n i18n fundamental logics → → How Remix i18n works
  • 7. Notes ⚠ → @arisa_dev There’re still discussions going on about Remix & i18n features Still some improvements → → Join discussions #2877
  • 9. “Do you like to implement i18n logic?”
  • 10.
  • 11. Maybe, their i18n DX is not good? 🤔
  • 12.
  • 13. Seems it’s not the best DX 👀
  • 14. Based on the i18n DX, let’s talk about numbers & facts 📊
  • 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/
  • 17. 25.9%
  • 18. English is used only 25.9% on the internet Source: https://www.statista.com/statistics/262946/share-of-the-most-common-languages-on-the-internet/
  • 19. 74.1%
  • 20. 74.1% users access non-English content Source: https://www.statista.com/statistics/262946/share-of-the-most-common-languages-on-the-internet/
  • 21. China
  • 22. China has the most internet users worldwide Source: https://www.statista.com/statistics/262946/share-of-the-most-common-languages-on-the-internet/
  • 23. Asia
  • 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/
  • 25. Huge numbers to ignore
  • 26. Knowing different approaches with more options will solve i18n DX🪄
  • 27. Let’s talk about fundamental logic
  • 28. 3 ways to Determine languages & regions → @arisa_dev Location from IP address Accept-Language header/ Navigator.languages → → Identifier in URL
  • 29. We use 2 ways - hybrid → @arisa_dev Location from IP address Accept-Language header/ Navigator.languages → → Identifier in URL
  • 30. 3 identifier URL patterns
  • 31. Differentiate by domains (Won’t follow the same-origin policy🙅 ) @arisa_dev hello.com hello.es Pattern 1
  • 32. URL parameters (NOT user friendly🙅 ) @arisa_dev hello.com?loc=de hello.com?loc=nl-NL Pattern 2
  • 34. Let’s talk about Frameworks & libs
  • 35. @arisa_dev Some frameworks & libs use i18n frameworks.
  • 36. Let’s see how it works in Remix
  • 38. remix-i18next is a npm package for Remix to use i18next.
  • 40. Default and a preferred language. → @arisa_dev { “greeting”: “Hello” } Create translation files public/locales/en/common.json { “greeting”: “こんにちは” } public/locales/ja/common.json
  • 41. i18next config file. → @arisa_dev export default { supportedLngs: ['en', 'ja'], fallbackLng: 'en', // customize namespace here defaultNS: 'common', react: {useSuspense: false}, }; Create app/ i18n.js app/i18n.js
  • 42. i18next config file. → @arisa_dev export default { supportedLngs: ['en', 'ja'], fallbackLng: 'en', // customize namespace here defaultNS: 'common', react: {useSuspense: false}, }; Create app/ i18n.js app/i18n.js
  • 43. i18next config file. → @arisa_dev export default { supportedLngs: ['en', 'ja'], fallbackLng: 'en', // customize namespace here defaultNS: 'common', react: {useSuspense: false}, }; Create app/ i18n.js app/i18n.js
  • 44. i18next config file. → @arisa_dev export default { supportedLngs: ['en', 'ja'], fallbackLng: 'en', // customize namespace here defaultNS: 'common', react: {useSuspense: false}, }; Create app/ i18n.js app/i18n.js
  • 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> ); });
  • 54. Why translation should be loaded before hydration?
  • 55. The app is not yet interactive → @arisa_dev If translation is NOT loaded before hydration English 日本語 “Hello” ?
  • 56. @arisa_dev English 日本語 “こんにちは” The app is interactive → If translation is loaded before hydration
  • 57. Why wrapping RemixBrowser with I18nextProvider?
  • 58. @arisa_dev @storyblok i18nextProvider from react-i18next import { createElement, useMemo } from 'react'; import { I18nContext } from './context'; export function I18nextProvider({ i18n, defaultNS, children }) { const value = useMemo( () => ({ i18n, // i18n config defaultNS, // default namespace }), [i18n, defaultNS], // i18n & defaultNS are the same ? cache calc : re-render ); return createElement( // … }
  • 59. @arisa_dev @storyblok i18nextProvider from react-i18next import { createElement, useMemo } from 'react'; import { I18nContext } from './context'; export function I18nextProvider({ i18n, defaultNS, children }) { const value = useMemo( () => ({ i18n, // i18n config defaultNS, // default namespace }), [i18n, defaultNS], // i18n & defaultNS are the same ? cache calc : re-render ); return createElement( // … }
  • 60. @arisa_dev @storyblok i18nextProvider from react-i18next import { createElement, useMemo } from 'react'; import { I18nContext } from './context'; export function I18nextProvider({ i18n, defaultNS, children }) { const value = useMemo( () => ({ i18n, // i18n config defaultNS, // default namespace }), [i18n, defaultNS], // i18n & defaultNS are the same ? cache calc : re-render ); return createElement( // … }
  • 61. Let’s use configs in action
  • 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>; }
  • 69. I have 3 confessions.
  • 70. 1.I used URL param
  • 71. 2. Do we (devs) need to maintain translation files…?
  • 72. 3. Did we translate slugs…?
  • 73. We want to achieve… → @arisa_dev Localized URL (sub-directory) → No translation files in code
  • 75. Connect your Remix app with CMS @arisa_dev i.e. Remix & Storyblok 5 min tutorial: https:// www.storyblok.com/tp/headless-cms-remix
  • 76. Choose between 4 approaches → @arisa_dev Mix above → Space-level translation → Field-level translation → Folder-level translation
  • 77. Choose between 4 approaches → @arisa_dev Mix above → Space-level translation → Field-level translation → Folder-level translation
  • 78.
  • 79.
  • 81. @arisa_dev @storyblok app/routes/$.tsx export default function Page() { // useLoaderData returns JSON parsed data from loader func let story = useLoaderData(); story = useStoryblokState(story, { resolveRelations: ["featured-posts.posts", "selected-posts.posts"] }); return <StoryblokComponent blok={story.content} /> }; // loader is Backend API & Wired up through useLoaderData export const loader = async ({ params, preview = false }) => { let slug = params["*"] ?? "home"; slug = slug.endsWith("/") ? slug.slice(0, -1) : slug; let sbParams = { version: "draft", resolve_relations: ["featured-posts.posts", "selected-posts.posts"], }; // … let { data } = await getStoryblokApi().get(`cdn/stories/${slug}`, sbParams); return json(data?.story, preview); };
  • 82. @arisa_dev @storyblok app/routes/$.tsx export default function Page() { // useLoaderData returns JSON parsed data from loader func let story = useLoaderData(); story = useStoryblokState(story, { resolveRelations: ["featured-posts.posts", "selected-posts.posts"] }); return <StoryblokComponent blok={story.content} /> }; // loader is Backend API & Wired up through useLoaderData export const loader = async ({ params, preview = false }) => { let slug = params["*"] ?? "home"; slug = slug.endsWith("/") ? slug.slice(0, -1) : slug; let sbParams = { version: "draft", resolve_relations: ["featured-posts.posts", "selected-posts.posts"], }; // … let { data } = await getStoryblokApi().get(`cdn/stories/${slug}`, sbParams); return json(data?.story, preview); };
  • 83. @arisa_dev @storyblok app/routes/$.tsx export default function Page() { // useLoaderData returns JSON parsed data from loader func let story = useLoaderData(); story = useStoryblokState(story, { resolveRelations: ["featured-posts.posts", "selected-posts.posts"] }); return <StoryblokComponent blok={story.content} /> }; // loader is Backend API & Wired up through useLoaderData export const loader = async ({ params, preview = false }) => { let slug = params["*"] ?? "home"; slug = slug.endsWith("/") ? slug.slice(0, -1) : slug; let sbParams = { version: "draft", resolve_relations: ["featured-posts.posts", "selected-posts.posts"], }; // … let { data } = await getStoryblokApi().get(`cdn/stories/${slug}`, sbParams); return json(data?.story, preview); };
  • 84. Example repo @arisa_dev i.e. Remix & Storyblok i18n repo: https://github.com/schabibi1/remix-i18n-talk
  • 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
  • 86. Thank you - ありがとう @arisa_dev