Как поддерживать языки с письмом справа налево (RTL) в TanStack Start v1

Зеркальные макеты для арабского и иврита

Проблема

Большинство веб-макетов предполагают, что текст читается слева направо. Меню навигации закреплены слева, боковые панели тоже слева, а контент читается слева направо. Для таких языков, как арабский и иврит, где чтение идет справа налево, это создает дезориентирующий опыт: визуальный поток противоречит направлению чтения. Пользователи видят навигацию не с той стороны, иконки расположены неудобно, а макеты кажутся перевернутыми. Без правильной поддержки RTL интерфейс становится сложным для навигации и понимания для носителей этих языков.

Решение

Динамически задавайте направление текста документа в зависимости от текущей локали, чтобы браузер автоматически зеркалил макет для RTL-языков. Используйте логические CSS-свойства вместо физических направлений, чтобы отступы, позиционирование и выравнивание автоматически подстраивались под направление текста без дополнительного кода. Такой подход делает макеты независимыми от направления: то, что находится в "начале" контента на английском, окажется в "начале" (справа) на арабском, а браузер сам выполнит преобразование.

Шаги

1. Определите направление текста по локали

Создайте вспомогательную функцию, которая возвращает направление текста для заданной локали, используя встроенный API интернационализации браузера.

export function getTextDirection(locale: string): "ltr" | "rtl" {
  try {
    const localeObj = new Intl.Locale(locale);
    if (
      "getTextInfo" in localeObj &&
      typeof localeObj.getTextInfo === "function"
    ) {
      return localeObj.getTextInfo().direction;
    }
  } catch (e) {
    console.warn(`Could not determine direction for locale: ${locale}`);
  }

  const rtlLocales = ["ar", "he", "fa", "ur"];
  const lang = locale.split("-")[0];
  return rtlLocales.includes(lang) ? "rtl" : "ltr";
}

Эта функция использует Intl.Locale.getTextInfo(), если он доступен, и в противном случае обращается к списку известных RTL-языков. Она возвращает либо 'ltr', либо 'rtl' в зависимости от локали.

2. Установите атрибут dir на элементе html

В вашем корневом маршруте получите текущую локаль и примените соответствующее направление к элементу <html> документа.

import {
  createRootRoute,
  Outlet,
  Scripts,
  HeadContent,
} from "@tanstack/react-router";
import { useIntl } from "react-intl";
import { getTextDirection } from "~/utils/text-direction";

export const Route = createRootRoute({
  component: RootComponent,
});

function RootComponent() {
  return (
    <RootDocument>
      <Outlet />
    </RootDocument>
  );
}

function RootDocument({ children }: { children: React.ReactNode }) {
  const intl = useIntl();
  const dir = getTextDirection(intl.locale);

  return (
    <html lang={intl.locale} dir={dir}>
      <head>
        <HeadContent />
      </head>
      <body>
        {children}
        <Scripts />
      </body>
    </html>
  );
}

Атрибут dir на элементе <html> сообщает браузеру, что нужно инвертировать макет для RTL-языков. Flexbox, grid и встроенное содержимое автоматически зеркалируются.

3. Замените физические CSS-свойства на логические

Обновите ваши стили, чтобы использовать логические свойства, которые реагируют на направление текста, а не фиксированные физические направления.

.sidebar {
  padding-inline-start: 1rem;
  margin-inline-end: 2rem;
  border-inline-start: 1px solid #ccc;
}

.icon {
  margin-inline-end: 0.5rem;
}

.card {
  inset-inline-start: 0;
  text-align: start;
}

Логические свойства, такие как padding-inline-start, соответствуют padding-left в LTR и padding-right в RTL. Браузер применяет нужное физическое свойство на основе атрибута dir, поэтому ваши стили работают в обоих направлениях без дублирования.

4. Используйте значения выравнивания, не зависящие от направления

Замените физические ключевые слова выравнивания на логические в вашем CSS и встроенных стилях.

export function Header() {
  return (
    <header
      style={{
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
      }}
    >
      <nav style={{ display: "flex", gap: "1rem" }}>
        <a href="/">Home</a>
        <a href="/about">About</a>
      </nav>
      <div style={{ textAlign: "end" }}>
        <button>Menu</button>
      </div>
    </header>
  );
}

Использование textAlign: 'end' вместо 'right' гарантирует, что текст будет выравниваться по концу направления чтения. Свойства Flexbox, такие как justifyContent и alignItems, автоматически учитывают направление, заданное атрибутом dir.