Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,16 @@ Update 23/02/2024
noExternal: ['remix-i18next'],
},
```

Update 1/03/2024

- Following Remix vite 18next example by Sergio Xalambri
https://github.com/sergiodxa/remix-vite-i18next/tree/main, i refactored the
example.
- Updated to latest version of `remix-i18next`, we no longer need to modify the
`vite.config.ts` file.
- Exit `i18next-fs-backend` adn `i18next-http-backend`, we are now transforming
the json files to ts files that are managed by vite.
- This way, we can have HMR and a really nice developer UX.
- Locales are now located in `/app/locales`, `i18n` configuration is moved to
`/app/config` and load the translations.
18 changes: 18 additions & 0 deletions app/config/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import en from '#/app/locales/en'
import fr from '#/app/locales/fr'

// This is the list of languages your application supports
export const supportedLngs = ['fr', 'en']

// This is the language you want to use in case
// if the user language is not in the supportedLngs
export const fallbackLng = 'en'

// The default namespace of i18next is "translation", but you can customize it
// here
export const defaultNS = 'common'

export const resources = {
en: { common: en },
fr: { common: fr },
}
18 changes: 2 additions & 16 deletions app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { RemixBrowser } from '@remix-run/react'
import i18next from 'i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import HTTPBackend from 'i18next-http-backend'
import { StrictMode, startTransition } from 'react'
import { hydrateRoot } from 'react-dom/client'
import { I18nextProvider, initReactI18next } from 'react-i18next'
import { getInitialNamespaces } from 'remix-i18next'
import { i18n } from './utils/i18n.ts'
import { getInitialNamespaces } from 'remix-i18next/client'
import * as i18n from '#app/config/i18n'

if (ENV.MODE === 'production' && ENV.SENTRY_DSN) {
import('./utils/monitoring.client.tsx').then(({ init }) => init())
Expand All @@ -16,23 +15,10 @@ async function hydrate() {
await i18next
.use(initReactI18next) // Tell i18next to use the react-i18next plugin
.use(LanguageDetector) // Setup a client-side language detector
.use(HTTPBackend) // Setup your backend
.init({
...i18n, // spread the configuration
// This function detects the namespaces your routes rendered while SSR use
ns: getInitialNamespaces(),
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
// you can also use a query parameter in production env to force reloading the translations
// queryStringParams: { v: "version" },
customHeaders: {
'cache-control':
ENV.MODE === 'development'
? 'no-cache, no-store, must-revalidate'
: undefined,
},
},

detection: {
// Here only enable htmlTag detection, we'll detect the language only
// server-side with remix-i18next, by using the `<html lang>` attribute
Expand Down
26 changes: 9 additions & 17 deletions app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { resolve } from 'path'
import { PassThrough } from 'stream'
import {
createReadableStreamFromReadable,
Expand All @@ -9,13 +8,12 @@ import {
import { RemixServer } from '@remix-run/react'
import * as Sentry from '@sentry/remix'
import { createInstance } from 'i18next'
import FSBackend from 'i18next-fs-backend'
import { isbot } from 'isbot'
import { renderToPipeableStream } from 'react-dom/server'
import { I18nextProvider, initReactI18next } from 'react-i18next'
import * as i18n from '#app/config/i18n'
import { getEnv, init } from './utils/env.server.ts'
import { i18n } from './utils/i18n.ts'
import { i18next } from './utils/i18next.server.ts'
import { i18next as i18nServer } from './utils/i18next.server.ts'
import { getInstanceInfo } from './utils/litefs.server.ts'
import { NonceProvider } from './utils/nonce-provider.ts'
import { makeTimings } from './utils/timing.server.ts'
Expand Down Expand Up @@ -53,21 +51,15 @@ export default async function handleRequest(...args: DocRequestArgs) {
// completely unique instance and not share any state
const i18nInstance = createInstance()
// Then we could detect locale from the request
let lng = await i18next.getLocale(request)
let lng = await i18nServer.getLocale(request)
// And here we detect what namespaces the routes about to render want to use
let ns = i18next.getRouteNamespaces(remixContext)
let ns = i18nServer.getRouteNamespaces(remixContext)

await i18nInstance
.use(initReactI18next)
.use(FSBackend)
.init({
...i18n,
lng,
ns,
backend: {
loadPath: resolve('./public/locales/{{lng}}/{{ns}}.json'),
},
})
await i18nInstance.use(initReactI18next).init({
...i18n,
lng,
ns,
})

const nonce = String(loadContext.cspNonce) ?? undefined
return new Promise(async (resolve, reject) => {
Expand Down
15 changes: 15 additions & 0 deletions app/locales/en.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default {
'auth.invalidUsernameOrPassword': 'Invalid username or password',
'marketing.title.start': 'Check the',
'marketing.title.middle': 'Getting Started',
'marketing.title.end':
'guide file for how to get your project off the ground!',
'root.login': 'Log in',
'root.language': 'Switch language',
'root.french': 'French',
'root.english': 'English',
'root.profile': 'Profile',
'root.logout': 'Logout',
'root.notes': 'Notes',
search: 'Search',
}
15 changes: 15 additions & 0 deletions app/locales/fr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default {
'auth.invalidUsernameOrPassword':
"Nom d'utilisateur ou mot de passe incorrect",
'marketing.title.start': 'Consulter le fichier',
'marketing.title.middle': 'Getting Started',
'marketing.title.end': 'pour savoir comment lancer votre projet !',
'root.login': 'Se connecter',
'root.language': 'Changer de langue',
'root.french': 'Français',
'root.english': 'Anglais',
'root.profile': 'Profil',
'root.logout': 'Se déconnecter',
'root.notes': 'Notes',
search: 'Rechercher',
}
7 changes: 3 additions & 4 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ import {
import { withSentry } from '@sentry/remix'
import { useRef } from 'react'
import { useTranslation } from 'react-i18next'
// import { useChangeLanguage } from 'remix-i18next'
import { useChangeLanguage } from 'remix-i18next/react'
import { HoneypotProvider } from 'remix-utils/honeypot/react'
import { z } from 'zod'
import * as i18n from '#app/config/i18n.ts'
import { GeneralErrorBoundary } from './components/error-boundary.tsx'
import { EpicProgress } from './components/progress-bar.tsx'
import { SearchBar } from './components/search-bar.tsx'
Expand All @@ -53,7 +54,6 @@ import { ClientHintCheck, getHints, useHints } from './utils/client-hints.tsx'
import { prisma } from './utils/db.server.ts'
import { getEnv } from './utils/env.server.ts'
import { honeypot } from './utils/honeypot.server.ts'
import { i18n, useChangeLanguage } from './utils/i18n.ts'
import { i18next } from './utils/i18next.server.ts'
import { combineHeaders, getDomainUrl, getUserImgSrc } from './utils/misc.tsx'
import { useNonce } from './utils/nonce-provider.ts'
Expand Down Expand Up @@ -167,7 +167,7 @@ export const handle = {
// will need to load. This key can be a single string or an array of strings.
// TIP: In most cases, you should set this to your defaultNS from your i18n config
// or if you did not set one, set it to the i18next default namespace "translation"
i18n: 'common',
i18n: ['common'],
}

export const headers: HeadersFunction = ({ loaderHeaders }) => {
Expand Down Expand Up @@ -492,7 +492,6 @@ function LanguageDropDown() {
export function ErrorBoundary() {
// the nonce doesn't rely on the loader so we can access that
const nonce = useNonce()
// const locale = useLocale()

// NOTE: you cannot use useLoaderData in an ErrorBoundary because the loader
// likely failed to run so we have to do the best we can.
Expand Down
26 changes: 0 additions & 26 deletions app/utils/i18n.ts

This file was deleted.

15 changes: 4 additions & 11 deletions app/utils/i18next.server.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { resolve } from 'node:path'
import { createCookie } from '@remix-run/node'
import Backend from 'i18next-fs-backend'
import { RemixI18Next } from 'remix-i18next'
import { i18n } from './i18n.ts' // your i18n configuration file
import { RemixI18Next } from 'remix-i18next/server'
import * as i18n from '#/app/config/i18n'

export const i18nCookie = createCookie('en_lang', {
sameSite: 'lax',
path: '/',
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
})

export const i18next = new RemixI18Next({
Expand All @@ -19,12 +19,5 @@ export const i18next = new RemixI18Next({
// when translating messages server-side only
i18next: {
...i18n,
backend: {
loadPath: resolve('./public/locales/{{lng}}/{{ns}}.json'),
},
},
// The i18next plugins you want RemixI18next to use for `i18n.getFixedT` inside loaders and actions.
// E.g. The Backend plugin for loading translations from the file system
// Tip: You could pass `resources` to the `i18next` configuration and avoid a backend here
plugins: [Backend],
})
Loading