# General Translation Integrations: Локализация Shopify Hydrogen с React Router
URL: https://generaltranslation.com/ru/docs/integrations/shopify/guides/localizing-react-router.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: Как добавить gt-react в витрину Hydrogen на React Router и синхронизировать с ним языковой контекст Shopify.

В витрине на React Router `gt-react` переводит тексты в вашем коде, а Hydrogen обменивается данными с Shopify. Вся специфичная для Shopify работа сводится к тому, чтобы передать в контекст запроса Hydrogen ту же локаль, что и у страницы.

*Примечание: это руководство актуально для `@shopify/hydrogen` 2026.10 на React Router 8 либо на React Router 7 с `future.v8_middleware: true` в `react-router.config.ts`.*

<Callout type="info">
  **Изменено в Hydrogen 2026.10:** `createHydrogenContext` и шаблон, предназначенный только для React Router, заменены на не зависящий от фреймворка `createShopifyRequestContext`. Он принимает те же параметры `i18n` — язык и страну, а также необязательный `pathPrefix`. Порядок обновления описан в навыке `migrate-to-hydrogen-2026-10`, который поставляется вместе с Hydrogen.
</Callout>

## Перед началом работы [#before-start]

* Настройте Hydrogen в своём приложении на React Router с помощью `npx @shopify/hydrogen setup`. Корневое промежуточное ПО создаёт контекст запроса Shopify и клиент Storefront, а также выполняет `handleShopifyRoutes`.
* Пройдите [Быстрый старт для React Router](/docs/react/react-router-quickstart). Корневой загрузчик возвращает локаль и снимок переводов, а корневой `Layout` рендерит [`GTProvider`](/docs/react/reference/components/gt-provider) и `<html lang>`. В этом руководстве компонент [`<LocaleSelector>`](/docs/react/reference/components/locale-selector) из «Быстрого старта» заменяется переключателем, который меняет URL.

## Разрешение локали из URL [#request-locale]

Определяйте локаль по пути запроса, чтобы у каждой локали был собственный URL. Используйте один и тот же модуль как для `gt-react`, так и для Hydrogen:

```ts title="app/lib/i18n.ts"
import gtConfig from '../../gt.config.json';

// "/fr/products/x" -> "fr"; для путей без префикса используется локаль по умолчанию.
export function getRequestLocale(request: Request): string {
  const [, segment] = new URL(request.url).pathname.split('/');
  return gtConfig.locales.includes(segment) ? segment : gtConfig.defaultLocale;
}

// "/products/x" -> "/fr/products/x"; для локали по умолчанию путь остаётся без префикса.
export function localizePath(pathname: string, locale: string): string {
  if (locale === gtConfig.defaultLocale) return pathname;
  return pathname === '/' ? `/${locale}` : `/${locale}${pathname}`;
}

// "/fr/products/x" -> "/products/x"; пути без префикса возвращаются без изменений.
export function stripLocale(pathname: string): string {
  const [, segment, ...rest] = pathname.split('/');
  return gtConfig.locales.includes(segment) ? `/${rest.join('/')}` : pathname;
}
```

В корневом загрузчике вызывайте `getRequestLocale(request)` вместо `parseLocale(request)` и передавайте результат в [`getTranslationsSnapshot`](/docs/react/reference/functions/get-translations-snapshot).

*Примечание: `parseLocale` сначала считывает cookie-файл локали, а затем заголовок `Accept-Language`. Если использовать только эту функцию, все языки отдаются по одному и тому же URL, и поисковые системы индексируют лишь один язык.*

Чтобы отдавать страницы по путям с префиксом, вложите их в маршрут компоновки с необязательным параметром `:locale?`. Универсальный маршрут Hydrogen (catch-all) оставьте последним, чтобы пути без совпадений по-прежнему доходили до его перенаправлений:

```ts title="app/routes.ts"
import {type RouteConfig, index, route} from '@react-router/dev/routes';

export default [
  // Страницы рендерятся без префикса или с префиксом целевой локали, например /fr.
  route(':locale?', 'routes/locale.tsx', [
    index('routes/home.tsx'),
    route('products/:handle', 'routes/product.tsx'),
  ]),
  route('*', 'routes/catchall.tsx'),
] satisfies RouteConfig;
```

В маршруте компоновки возвращайте 404, если сегмент не является одной из настроенных целевых локалей, чтобы для `/en/...` и неизвестных префиксов не выполнялся рендеринг дубликата страницы:

```tsx title="app/routes/locale.tsx"
import {Outlet} from 'react-router';
import gtConfig from '../../gt.config.json';

// Префиксом могут быть только целевые локали; локаль по умолчанию используется без префикса.
export async function loader({params}: {params: {locale?: string}}) {
  if (params.locale && !gtConfig.locales.includes(params.locale)) {
    throw new Response(null, {status: 404});
  }
  return null;
}

export default function LocaleLayout() {
  return <Outlet />;
}
```

При маршрутизации на основе файлов назовите файл компоновки `($locale).tsx` и вложите в него файлы страниц.

Собственные обработчики Hydrogen, такие как `/api/cart`, `/api/predictive-search`, `/checkout` и маршруты Customer Account, отвечают в корневом промежуточном ПО ещё до рендеринга страниц, поэтому префикс к ним не добавляется.

### Сохраняйте язык страницы в обработчиках Hydrogen [#handler-locale]

У запросов к `/api/cart` и `/api/predictive-search` нет префикса локали, поэтому для них `getRequestLocale` разрешает локаль по умолчанию. В итоге на французской странице корзина и результаты поиска будут показывать текст каталога на английском.

Сохраняйте локаль страницы в cookie-файле и считывайте её только для этих запросов к `/api` без префикса. Для страниц единственным источником локали по-прежнему остаётся URL. Замените `getRequestLocale` в `app/lib/i18n.ts`:

```ts title="app/lib/i18n.ts"
export const LOCALE_COOKIE_NAME = 'shop_locale';

const allLocales = [gtConfig.defaultLocale, ...gtConfig.locales];

export function readCookie(request: Request, name: string): string | undefined {
  const cookie = request.headers.get('Cookie')?.split(/;\s*/).find((part) => part.startsWith(`${name}=`));
  return cookie?.slice(name.length + 1);
}

/** Страницы берут локаль из префикса URL; обработчики `/api` в Hydrogen — из локали вызвавшей их страницы. */
export function getRequestLocale(request: Request): string {
  const {pathname} = new URL(request.url);
  const [, segment] = pathname.split('/');
  if (gtConfig.locales.includes(segment)) return segment;
  if (!pathname.startsWith('/api/')) return gtConfig.defaultLocale;
  const remembered = readCookie(request, LOCALE_COOKIE_NAME);
  // Доверяем только настроенной локали; в остальных случаях используется локаль по умолчанию.
  return remembered && allLocales.includes(remembered) ? remembered : gtConfig.defaultLocale;
}

/** Загрузка страниц и клиентские переходы, но не вызовы `/api` и не спекулятивная предварительная загрузка. */
export function isPageNavigation(request: Request): boolean {
  const purpose = `${request.headers.get('Purpose') ?? ''} ${request.headers.get('Sec-Purpose') ?? ''}`;
  return (
    request.method === 'GET' &&
    !new URL(request.url).pathname.startsWith('/api/') &&
    !purpose.includes('prefetch')
  );
}
```

Корневое промежуточное ПО из следующего раздела устанавливает cookie-файл, если локаль при переходе на страницу отличается от сохранённой. При предварительной загрузке (prefetch) cookie-файл не устанавливается, поэтому предзагруженная ссылка на другую языковую версию не может сменить язык корзины, пока покупатель по ней не перейдёт.

## Передача языка в Shopify [#storefront-language]

Сопоставьте каждую локаль с соответствующим значением Shopify `LanguageCode` и передайте его в `createShopifyRequestContext` в корневом промежуточном ПО. После этого клиент Storefront будет подставлять `$language` и `$country` в каждый запрос, где объявлена директива `@inContext(country: $country, language: $language)`.

```ts title="app/root.tsx"
import {
  createShopifyRequestContext,
  createStorefrontClient,
  handleShopifyRedirects,
  handleShopifyRoutes,
} from '@shopify/hydrogen';
import gtConfig from '../gt.config.json';
import {getRequestLocale, isPageNavigation, LOCALE_COOKIE_NAME, readCookie} from '~/lib/i18n';
import type {Route} from './+types/root';

// По одной записи для каждой локали в gt.config.json.
const SHOPIFY_LANGUAGES = {en: 'EN', fr: 'FR', ja: 'JA'} as const;

export const middleware: Route.MiddlewareFunction[] = [
  async ({context, request}, next) => {
    const locale = getRequestLocale(request) as keyof typeof SHOPIFY_LANGUAGES;
    const requestContext = createShopifyRequestContext({
      request,
      // Язык определяется страницей, а страна остаётся той, на рынке которой вы продаёте.
      // pathPrefix сохраняет локаль в URL при перенаправлениях Hydrogen для вариантов и стандартных маршрутов.
      i18n: {
        language: SHOPIFY_LANGUAGES[locale],
        country: 'US',
        pathPrefix: locale === gtConfig.defaultLocale ? '' : `/${locale}`,
      },
    });
    const storefrontClient = createStorefrontClient({type: 'public', requestContext, config});

    const shopifyRoute = handleShopifyRoutes({request, requestContext, storefrontClient, routeTemplates, handlers});
    if (shopifyRoute) return shopifyRoute;

    context.set(storefrontClientContext, storefrontClient);

    const response = await next();
    if (response.status === 404) {
      const redirect = await handleShopifyRedirects({request, routeTemplates, storefrontClient});
      if (redirect) return redirect;
    }
    // Запоминаем локаль страницы для обработчиков Hydrogen `/api`. Cookie-файл нужно установить до
    // applyResponseHeaders: этот метод делает любой ответ, устанавливающий cookie-файл, приватным и некэшируемым.
    if (isPageNavigation(request) && readCookie(request, LOCALE_COOKIE_NAME) !== locale) {
      response.headers.append('Set-Cookie', `${LOCALE_COOKIE_NAME}=${locale}; Path=/; SameSite=Lax`);
    }
    storefrontClient.requestContext.applyResponseHeaders(response.headers);
    return response;
  },
];
```

`config`, `routeTemplates`, `handlers` и `storefrontClientContext` берутся из вашей текущей настройки Hydrogen; новыми здесь являются только `i18n` и cookie-файл локали. Ответ страницы, который устанавливает cookie-файл, не кешируется, поэтому это происходит только при первом просмотре страницы покупателем и при смене языка.

В этом руководстве для всех языков используется рынок США, поэтому при переключении на французский меняются тексты, но не страна покупателя и не валюта корзины. Если рынков несколько, дайте покупателям выбирать страну отдельно или свяжите каждую локаль с рынком и переносите корзину при смене языка. (См. [Продажи на нескольких рынках](/docs/integrations/shopify/guides/selling-in-markets)).

## Форматирование цен [#prices]

Форматируйте сумму из Shopify с кодом валюты, который возвращает Shopify. [`<Currency>`](/docs/react/reference/components/currency) форматирует значение в соответствии с активной локалью, но не конвертирует сумму.

```tsx title="app/components/LocalizedMoney.tsx"
import {Currency} from 'gt-react';

/** Форматирует цену Shopify для активной локали без конвертации валюты. */
export function LocalizedMoney({data}: {data: {amount?: string; currencyCode?: string}}) {
  // В оптимистичных ответах корзины поля стоимости могут временно отсутствовать.
  if (data.amount == null || !data.currencyCode) return null;
  return <Currency currency={data.currencyCode}>{data.amount}</Currency>;
}
```

## Ссылки в пределах локали [#links]

Пути маршрутов в коде оставляйте без префикса, а префикс текущей локали добавляйте при рендеринге ссылки. Получите активную локаль с помощью [`useLocale`](/docs/react/reference/hooks/use-locale) и передайте `localizePath(to, locale)` в компонент `Link` из React Router.

Чтобы переключить язык, измените префикс в текущем URL. После этого корневой загрузчик прочитает новую локаль, а содержимое корзины сохранится. Не используйте здесь [`<LocaleSelector>`](/docs/react/reference/components/locale-selector): он сохраняет локаль в cookie-файле и перезагружает тот же URL, который по-прежнему разрешается в старую локаль.

```tsx title="app/components/LanguageSwitcher.tsx"
import {useLocale, useLocales} from 'gt-react';
import {useLocation, useNavigate} from 'react-router';
import {localizePath, stripLocale} from '~/lib/i18n';

/** Переключает язык, меняя префикс URL; корневой загрузчик считывает новую локаль из URL. */
export function LanguageSwitcher() {
  const locale = useLocale();
  const locales = useLocales();
  const {pathname, search} = useLocation();
  const navigate = useNavigate();
  return (
    <select
      aria-label="Language"
      value={locale}
      onChange={(event) => navigate(localizePath(stripLocale(pathname), event.target.value) + search)}
    >
      {locales.map((option) => (
        // Каждый язык называется на нём самом, например «français» или «日本語».
        <option key={option} value={option} lang={option}>
          {new Intl.DisplayNames([option], {type: 'language'}).of(option)}
        </option>
      ))}
    </select>
  );
}
```

Для ссылок на маршруты Customer Account, например `/account/login`, используйте обычный `<a>` или `<form>`, а не `Link`. Их обработчики выполняют перенаправление на Shopify, которое client-side навигация обработать не может.

## Создать переводы [#generate]

Оберните текст в [`<T>`](/docs/react/reference/components/t), используйте [`useGT`](/docs/react/reference/hooks/use-gt) для строк вроде значений `aria-label` и запустите [`npx gt translate`](/docs/cli/reference/commands/translate) перед `react-router build`. Переводы, которые ваш загрузчик включает в бандл, доступны уже при первом серверном рендеринге, поэтому они попадают и в кэшированные ответы.

Чтобы по мере изменений в витрине текст по-прежнему оставался обёрнутым и переведённым, подключите репозиторий к [Locadex](/docs/platform/locadex/quickstart). Locadex будет открывать пул-реквесты с новыми переводами для проверки.

## Next steps

- /docs/integrations/shopify/guides/localizing-nextjs
- /docs/integrations/shopify/guides/translating-products
- /docs/integrations/shopify/guides/selling-in-markets

## Sitemap

See the full [sitemap](https://generaltranslation.com/sitemap.md) for all pages.
