build(deps): bump remix-i18next from 7.5.0 to 8.0.0 (#3215)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kalle <38327916+Sendouc@users.noreply.github.com>
This commit is contained in:
dependabot[bot]
2026-07-10 17:40:24 +03:00
committed by GitHub
parent 3cef92e82f
commit 938d7efd96
17 changed files with 198 additions and 74 deletions

View File

@@ -1,21 +1,19 @@
import { PassThrough } from "node:stream";
import { createReadableStreamFromReadable } from "@react-router/node";
import * as Sentry from "@sentry/react-router";
import { createInstance } from "i18next";
import { isbot } from "isbot";
import cron from "node-cron";
import { renderToPipeableStream } from "react-dom/server";
import { I18nextProvider, initReactI18next } from "react-i18next";
import { I18nextProvider } from "react-i18next";
import {
type EntryContext,
type HandleErrorFunction,
type RouterContextProvider,
ServerRouter,
} from "react-router";
import { Config } from "~/config";
import { ServerConfig } from "~/config.server";
import { config } from "~/modules/i18n/config"; // your i18n configuration file
import { i18next } from "~/modules/i18n/i18next.server";
import { resources } from "./modules/i18n/resources.server";
import { getI18nInstance } from "~/modules/i18n/i18next.server";
import {
daily,
everyHourAt00,
@@ -34,23 +32,13 @@ async function handleRequest(
responseStatusCode: number,
responseHeaders: Headers,
reactRouterContext: EntryContext,
loadContext: RouterContextProvider,
) {
const callbackName = isbot(request.headers.get("user-agent"))
? "onAllReady"
: "onShellReady";
const instance = createInstance();
const lng = await i18next.getLocale(request);
const ns = i18next.getRouteNamespaces(reactRouterContext);
await instance
.use(initReactI18next) // Tell our instance to use react-i18next
.init({
...config, // spread the configuration
lng, // The locale we detected above
ns, // The namespaces the routes about to render wants to use
resources,
});
const instance = getI18nInstance(loadContext);
return new Promise((resolve, reject) => {
let didError = false;

View File

@@ -1,7 +1,7 @@
import type { LoaderFunctionArgs } from "react-router";
import { z } from "zod";
import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.server";
import { i18next } from "~/modules/i18n/i18next.server";
import { getFixedTForLanguage } from "~/modules/i18n/i18next.server";
import { notFoundIfFalsy, parseParams } from "~/utils/remix.server";
import { id } from "~/utils/zod";
import type { GetSendouqMatchResponse, MapListMap } from "../schema";
@@ -18,7 +18,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
const match = notFoundIfFalsy(await SQMatchRepository.findById(matchId));
const t = await i18next.getFixedT("en", ["game-misc"]);
const t = await getFixedTForLanguage("en", ["game-misc"]);
const userIdToRank = (userId: number) => {
const memento = match.memento;

View File

@@ -6,7 +6,7 @@ import * as TournamentRepository from "~/features/tournament/TournamentRepositor
import * as TournamentTeamRepository from "~/features/tournament/TournamentTeamRepository.server";
import { tournamentFromDBCached } from "~/features/tournament-bracket/core/Tournament.server";
import { resolveMapList } from "~/features/tournament-match/core/mapList.server";
import { i18next } from "~/modules/i18n/i18next.server";
import { getFixedTForLanguage } from "~/modules/i18n/i18next.server";
import { logger } from "~/utils/logger";
import { notFoundIfFalsy, parseParams } from "~/utils/remix.server";
import { id } from "~/utils/zod";
@@ -17,7 +17,7 @@ const paramsSchema = z.object({
});
export const loader = async ({ params }: LoaderFunctionArgs) => {
const t = await i18next.getFixedT("en", ["game-misc"]);
const t = await getFixedTForLanguage("en", ["game-misc"]);
const { id } = parseParams({
params,
schema: paramsSchema,

View File

@@ -5,7 +5,7 @@ import { z } from "zod";
import { db } from "~/db/sql";
import { ordinalToSp } from "~/features/mmr/mmr-utils";
import * as TournamentRepository from "~/features/tournament/TournamentRepository.server";
import { i18next } from "~/modules/i18n/i18next.server";
import { getFixedTForLanguage } from "~/modules/i18n/i18next.server";
import { nullifyingAvg } from "~/utils/arrays";
import { databaseTimestampToDate } from "~/utils/dates";
import { concatUserSubmittedImagePrefix } from "~/utils/kysely.server";
@@ -18,7 +18,7 @@ const paramsSchema = z.object({
});
export const loader = async ({ params }: LoaderFunctionArgs) => {
const t = await i18next.getFixedT("en", ["game-misc"]);
const t = await getFixedTForLanguage("en", ["game-misc"]);
const { id } = parseParams({
params,
schema: paramsSchema,

View File

@@ -6,7 +6,7 @@ import * as Seasons from "~/features/mmr/core/Seasons";
import { userSkills as _userSkills } from "~/features/mmr/tiered.server";
import { peakXpOverallSql } from "~/features/top-search/XRankPlacementRepository.server";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { i18next } from "~/modules/i18n/i18next.server";
import { getFixedTForLanguage } from "~/modules/i18n/i18next.server";
import { safeNumberParse } from "~/utils/number";
import { notFoundIfFalsy, parseParams } from "~/utils/remix.server";
import { badgeUrl } from "~/utils/urls";
@@ -17,7 +17,7 @@ const paramsSchema = z.object({
});
export const loader = async ({ params }: LoaderFunctionArgs) => {
const t = await i18next.getFixedT("en", ["weapons"]);
const t = await getFixedTForLanguage("en", ["weapons"]);
const { identifier } = parseParams({ params, schema: paramsSchema });
const user = notFoundIfFalsy(

View File

@@ -1,15 +1,15 @@
import { cachified } from "@epic-web/cachified";
import type { LoaderFunctionArgs } from "react-router";
import * as BuildRepository from "~/features/builds/BuildRepository.server";
import { i18next } from "~/modules/i18n/i18next.server";
import { getServerTFunction } from "~/modules/i18n/i18next.server";
import { weaponIdToType } from "~/modules/in-game-lists/weapon-ids";
import { cache, IN_MILLISECONDS, ttl } from "~/utils/cache.server";
import { notFoundIfNullLike } from "~/utils/remix.server";
import { weaponNameSlugToId } from "~/utils/unslugify.server";
import { popularBuilds } from "../build-stats-utils";
export const loader = async ({ params, request }: LoaderFunctionArgs) => {
const t = await i18next.getFixedT(request, ["builds", "weapons"]);
export const loader = async ({ params }: LoaderFunctionArgs) => {
const t = getServerTFunction(["builds", "weapons"]);
const slug = params.slug;
const weaponId = notFoundIfNullLike(weaponNameSlugToId(slug));

View File

@@ -1,15 +1,15 @@
import { cachified } from "@epic-web/cachified";
import type { LoaderFunctionArgs } from "react-router";
import * as BuildRepository from "~/features/builds/BuildRepository.server";
import { i18next } from "~/modules/i18n/i18next.server";
import { getServerTFunction } from "~/modules/i18n/i18next.server";
import { weaponIdToType } from "~/modules/in-game-lists/weapon-ids";
import { cache } from "~/utils/cache.server";
import { notFoundIfNullLike } from "~/utils/remix.server";
import { weaponNameSlugToId } from "~/utils/unslugify.server";
import { abilityPointCountsToAverages } from "../build-stats-utils";
export const loader = async ({ params, request }: LoaderFunctionArgs) => {
const t = await i18next.getFixedT(request, ["builds", "weapons"]);
export const loader = async ({ params }: LoaderFunctionArgs) => {
const t = getServerTFunction(["builds", "weapons"]);
const weaponId = notFoundIfNullLike(weaponNameSlugToId(params.slug));
if (weaponIdToType(weaponId) === "ALT_SKIN") {

View File

@@ -1,6 +1,6 @@
import type { LoaderFunctionArgs } from "react-router";
import { getUser } from "~/features/auth/core/user.server";
import { i18next } from "~/modules/i18n/i18next.server";
import { getFixedTForLanguage } from "~/modules/i18n/i18next.server";
import { weaponIdToType } from "~/modules/in-game-lists/weapon-ids";
import { weaponNameSlugToId } from "~/utils/unslugify.server";
import { mySlugify } from "~/utils/urls";
@@ -15,11 +15,9 @@ import {
} from "../builds-schemas";
import { filterBuilds } from "../core/filter.server";
export const loader = async ({ request, params, url }: LoaderFunctionArgs) => {
export const loader = async ({ params, url }: LoaderFunctionArgs) => {
const user = getUser();
const t = await i18next.getFixedT(request, ["weapons", "common"], {
lng: "en",
});
const t = await getFixedTForLanguage("en", ["weapons", "common"]);
const weaponId = weaponNameSlugToId(params.slug);
if (typeof weaponId !== "number" || weaponIdToType(weaponId) === "ALT_SKIN") {

View File

@@ -4,7 +4,7 @@ import { type Urgency, WebPushError } from "web-push";
import { IS_E2E_TEST_RUN } from "~/utils/e2e";
import { APP_ICON_URL } from "~/utils/urls";
import type { NotificationSubscription } from "../../../db/tables";
import { i18next } from "../../../modules/i18n/i18next.server";
import { getFixedTForLanguage } from "../../../modules/i18n/i18next.server";
import { logger } from "../../../utils/logger";
import * as NotificationRepository from "../NotificationRepository.server";
import type { Notification } from "../notifications-types";
@@ -76,7 +76,7 @@ export async function notify({
dededuplicatedUserIds,
);
if (subscriptions.length > 0) {
const t = await i18next.getFixedT("en-US", ["common"]);
const t = await getFixedTForLanguage("en-US", ["common"]);
const limit = pLimit(50);

View File

@@ -1,6 +1,6 @@
import type { LoaderFunctionArgs } from "react-router";
import { mainWeaponParams } from "~/features/build-analyzer/core/utils";
import { i18next } from "~/modules/i18n/i18next.server";
import { getFixedTForLanguage } from "~/modules/i18n/i18next.server";
import type {
MainWeaponId,
SpecialWeaponId,
@@ -35,10 +35,8 @@ const subParamsData = subWeaponParamsData as WeaponParams.AllVersionParams;
const specialParamsData =
specialWeaponParamsData as WeaponParams.AllVersionParams;
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const t = await i18next.getFixedT(request, ["weapons"], {
lng: "en",
});
export const loader = async ({ params }: LoaderFunctionArgs) => {
const t = await getFixedTForLanguage("en", ["weapons"]);
const mainWeaponId = weaponNameSlugToId(params.slug);
if (typeof mainWeaponId === "number") {

View File

@@ -3,7 +3,7 @@ import { requireUser } from "~/features/auth/core/user.server";
import * as ShowcaseTournaments from "~/features/front-page/core/ShowcaseTournaments.server";
import { clearTournamentDataCache } from "~/features/tournament-bracket/core/Tournament.server";
import { parseFormDataWithImages } from "~/form/parse.server";
import { i18next } from "~/modules/i18n/i18next.server";
import { getServerTFunction } from "~/modules/i18n/i18next.server";
import { requirePermission } from "~/modules/permissions/guards.server";
import { actionError } from "~/utils/remix.server";
import { tournamentOrganizationPage } from "~/utils/urls";
@@ -24,7 +24,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
const data = result.data;
const t = await i18next.getFixedT(request, ["org"]);
const t = getServerTFunction(["org"]);
const organization = await organizationFromParams(params);

View File

@@ -1,5 +1,20 @@
import { createCookie } from "react-router";
import { RemixI18Next } from "remix-i18next/server";
import { AsyncLocalStorage } from "node:async_hooks";
import {
createInstance,
type DefaultNamespace,
type FlatNamespace,
type i18n,
type Namespace,
type TFunction,
} from "i18next";
import { type FallbackNs, initReactI18next } from "react-i18next";
import {
createCookie,
type MiddlewareFunction,
type RouterContextProvider,
} from "react-router";
import { createI18nextMiddleware } from "remix-i18next";
import invariant from "~/utils/invariant";
import { config } from "./config";
import { resources } from "./resources.server";
@@ -11,14 +26,95 @@ export const i18nCookie = createCookie("i18n", {
maxAge: TEN_YEARS_IN_SECONDS,
});
export const i18next = new RemixI18Next({
detection: {
cookie: i18nCookie,
supportedLanguages: config.supportedLngs,
fallbackLanguage: config.fallbackLng,
},
i18next: {
...config,
resources: resources,
},
});
const [remixI18nextMiddleware, getLocaleFromContext, getInstanceFromContext] =
createI18nextMiddleware({
detection: {
cookie: i18nCookie,
supportedLanguages: config.supportedLngs,
fallbackLanguage: config.fallbackLng,
},
i18next: {
...config,
resources,
},
plugins: [initReactI18next],
});
interface I18nStore {
locale: string;
instance: i18n;
}
const i18nAsyncLocalStorage = new AsyncLocalStorage<I18nStore>();
/**
* Detects the request locale, initializes a request-scoped i18next instance and
* makes both available to server helpers ({@link getLocale},
* {@link getServerTFunction}) for the duration of the request.
*/
export const i18nMiddleware: MiddlewareFunction<Response> = (args, next) =>
remixI18nextMiddleware(args, () =>
i18nAsyncLocalStorage.run(
{
locale: getLocaleFromContext(args.context),
instance: getInstanceFromContext(args.context),
},
() => next(),
),
);
/** The locale detected for the current request. */
export function getLocale(): string {
return currentStore().locale;
}
/**
* A `TFunction` bound to the current request's locale and the given namespaces.
* Namespaces are already available in-memory so no async loading is needed.
*/
export function getServerTFunction<
N extends
| FlatNamespace
| readonly [FlatNamespace, ...FlatNamespace[]] = DefaultNamespace,
>(namespaces?: N): TFunction<FallbackNs<N>> {
const { instance, locale } = currentStore();
return instance.getFixedT(
locale,
namespaces as Namespace,
) as unknown as TFunction<FallbackNs<N>>;
}
/**
* The request-scoped i18next instance, used to provide translations while
* server-side rendering. Reads from the React Router context so it can be called
* from `entry.server` where the request context is passed explicitly.
*/
export function getI18nInstance(
context: Readonly<RouterContextProvider>,
): i18n {
return getInstanceFromContext(context);
}
/**
* A `TFunction` bound to a fixed language, independent of the current request.
* Use this when translating outside of a request (e.g. notifications) or when a
* specific language is required regardless of the user's locale.
*/
export async function getFixedTForLanguage<
N extends
| FlatNamespace
| readonly [FlatNamespace, ...FlatNamespace[]] = DefaultNamespace,
>(language: string, namespaces?: N): Promise<TFunction<FallbackNs<N>>> {
const instance = createInstance();
await instance.init({ ...config, resources, lng: language });
return instance.getFixedT(
language,
namespaces as Namespace,
) as unknown as TFunction<FallbackNs<N>>;
}
function currentStore(): I18nStore {
const store = i18nAsyncLocalStorage.getStore();
invariant(store, "i18n store not found, is i18nMiddleware registered?");
return store;
}

View File

@@ -2,7 +2,6 @@ import i18next from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import Backend from "i18next-http-backend";
import { initReactI18next } from "react-i18next";
import { getInitialNamespaces } from "remix-i18next/client";
import { config } from "./config";
export function i18nLoader() {
@@ -30,3 +29,32 @@ export function i18nLoader() {
initAsync: false,
});
}
declare global {
interface Window {
__reactRouterRouteModules: Record<string, { handle?: unknown } | undefined>;
}
}
/**
* Get the list of namespaces used by the application server-side so they can be
* set on i18next init options and preloaded before hydration.
*
* Vendored from remix-i18next (removed in v8).
*/
function getInitialNamespaces(): string[] {
return Object.values(window.__reactRouterRouteModules).flatMap((route) => {
const handle = route?.handle;
if (typeof handle !== "object" || handle === null) return [];
if (!("i18n" in handle)) return [];
const namespaces = (handle as { i18n: unknown }).i18n;
if (typeof namespaces === "string") return [namespaces];
if (
Array.isArray(namespaces) &&
namespaces.every((value) => typeof value === "string")
) {
return namespaces as string[];
}
return [];
});
}

View File

@@ -0,0 +1,15 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
/**
* Detect when the locale returned by the root route loader changes and call
* `i18n.changeLanguage` with the new locale so translations load automatically.
*
* Vendored from remix-i18next (removed in v8).
*/
export function useChangeLanguage(locale: string) {
const { i18n } = useTranslation();
React.useEffect(() => {
if (i18n.language !== locale) i18n.changeLanguage(locale);
}, [locale, i18n]);
}

View File

@@ -27,7 +27,6 @@ import {
useRevalidator,
useSearchParams,
} from "react-router";
import { useChangeLanguage } from "remix-i18next/react";
import { Config } from "~/config";
import type { CustomTheme } from "~/db/tables";
import * as NotificationRepository from "~/features/notifications/NotificationRepository.server";
@@ -56,7 +55,12 @@ import { getThemeSession } from "./features/theme/core/theme-session.server";
import { useUserIntlPreference } from "./hooks/intl/useUserIntlPreference";
import { useHydrated } from "./hooks/useHydrated";
import { DEFAULT_LANGUAGE } from "./modules/i18n/config";
import { i18nCookie, i18next } from "./modules/i18n/i18next.server";
import {
getLocale,
i18nCookie,
i18nMiddleware,
} from "./modules/i18n/i18next.server";
import { useChangeLanguage } from "./modules/i18n/useChangeLanguage";
import { isSupporter } from "./modules/permissions/utils";
import { IS_E2E_TEST_RUN } from "./utils/e2e";
import { allI18nNamespaces } from "./utils/i18n";
@@ -68,6 +72,7 @@ export const middleware: Route.MiddlewareFunction[] = [
requestContextMiddleware,
sessionIdMiddleware,
userMiddleware,
i18nMiddleware,
];
import "~/styles/vars.css";
@@ -110,7 +115,7 @@ export type LoggedInUser = NonNullable<RootLoaderData["user"]>;
export const loader = async ({ request }: LoaderFunctionArgs) => {
const user = getUser();
const locale = await i18next.getLocale(request);
const locale = getLocale();
const themeSession = await getThemeSession(request);
const sidenavSession = await getSidenavSession(request);

View File

@@ -90,7 +90,7 @@
"remeda": "2.39.0",
"remix-auth": "4.2.0",
"remix-auth-oauth2": "3.4.1",
"remix-i18next": "7.5.0",
"remix-i18next": "8.0.0",
"slugify": "1.6.9",
"swr": "2.4.2",
"web-haptics": "0.0.6",

18
pnpm-lock.yaml generated
View File

@@ -172,8 +172,8 @@ importers:
specifier: 3.4.1
version: 3.4.1(remix-auth@4.2.0)
remix-i18next:
specifier: 7.5.0
version: 7.5.0(i18next@26.3.4(typescript@6.0.3))(react-i18next@17.0.8(i18next@26.3.4(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(react-router@8.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7)
specifier: 8.0.0
version: 8.0.0(i18next@26.3.4(typescript@6.0.3))(react-router@8.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))
slugify:
specifier: 1.6.9
version: 1.6.9
@@ -4089,14 +4089,12 @@ packages:
resolution: {integrity: sha512-3LSfWEvSgG2CgbG/p4ge5hbV8tTXWNnnYIGbTr9oSSiHz9dD7wh6S0MEyo3pwh7MlKezB2WIlevGeyqUZykk7g==}
engines: {node: '>=20.0.0'}
remix-i18next@7.5.0:
resolution: {integrity: sha512-7vAwnNkAXzr/YQwFkV1FHrY+3utgGrDwn7oP/aM2gFHrwbGT0+uVtuUx3USpTrJvhzAN+2YU/mnmgyBi9EhYyw==}
engines: {node: '>=20.0.0'}
remix-i18next@8.0.0:
resolution: {integrity: sha512-+QbCBo3VP6JH2zCbPoRPhgYErY89vDnr4Sy66tlzANwyRnEIHEA2o6HlEuz0iCoDn9f8I88cknNrHGsc4n77zg==}
engines: {node: '>=22.22.0'}
peerDependencies:
i18next: ^24.0.0 || ^25.0.0 || ^26.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-i18next: ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
react-router: ^7.0.0
react-router: ^8.0.0
require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
@@ -8700,11 +8698,9 @@ snapshots:
remix-auth@4.2.0: {}
remix-i18next@7.5.0(i18next@26.3.4(typescript@6.0.3))(react-i18next@17.0.8(i18next@26.3.4(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(react-router@8.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7):
remix-i18next@8.0.0(i18next@26.3.4(typescript@6.0.3))(react-router@8.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)):
dependencies:
i18next: 26.3.4(typescript@6.0.3)
react: 19.2.7
react-i18next: 17.0.8(i18next@26.3.4(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)
react-router: 8.1.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
require-directory@2.1.1: {}