Свой язык
Языка нет в наших трёх — его приносит хозяин страницы: подписи переводятся, непереведённое идёт запасной цепочкой, а не пропадает.

В рамке выше работает тот самый проект, чей код лежит ниже, — собранный, но не переписанный: те же файлы, только пропущенные через сборку. Считает в нём не ядро, а дублёр транспорта, поэтому числа в ячейках ничего не значат: смотреть нужно на код.
Страница, которую открывает браузер.
<!doctype html><!-- Свой язык — НАСТОЯЩИЙ МАЛЕНЬКИЙ ПРОЕКТ.
Не «страница витрины», а тот проект, который встраивающий копирует к себе целиком. Галерея примеров ЗАПУСКАЕТ эти файлы и ПОКАЗЫВАЕТ их же.--><html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>Свой язык — @f1</title> </head> <body> <div data-example="custom-locale" class="flex h-screen w-screen flex-col bg-base-100 text-base-content"> <header class="border-b border-base-300 px-4 py-2"> <div class="text-sm font-semibold">Свой язык</div> <div class="text-xs opacity-70"> Украинского у нас нет — его приносит ХОЗЯИН СТРАНИЦЫ вызовом <code>registerLocale({ code: "uk", … })</code>. Переведена горстка подписей; остальные показываются запасным языком (<code>fallback: "ru"</code>), а не пустотой. Переключите язык — и посмотрите на полосу подписей ниже: их отдаёт <code>label()</code>, та же дорога, которой говорит вся поверхность. </div> <div class="mt-1 flex items-center gap-2 text-xs" data-locale-picker></div> </header> <div class="flex flex-wrap gap-2 border-b border-base-300 px-4 py-2 text-xs" data-locale-strip></div> <div id="app" class="flex min-h-0 flex-1 flex-col"></div> <footer class="border-t border-base-300 px-4 py-2 font-mono text-xs" data-locale-coverage> покрытие считается… </footer> </div>
<script type="module" src="./src/main.ts"></script> </body></html>Точка входа: здесь лист создаётся и сюда же приходят события.
import { createSheetsCore } from "@f1/preset-sheets-core";import { applyLocaleAttributes, label, subscribeLocale, availableLocales, labelCoverage, localeChain, localeName, registerLocale, setLocale, surfaceLabelKeys,} from "@f1/i18n";import { transport } from "./book.js";import { hostAsks } from "./host.js";import { OUR_RUSSIAN_TWEAKED, UKRAINIAN } from "./dictionary.js";import "./styles.css";
/** * СВОЙ ЯЗЫК — И ПРАВИЛО ЗА НИМ. * * Языков у нас три (ru, en, ar), а у встраивающего их может быть сколько * угодно. Поэтому язык — точка расширения: словарь объявляет ХОЗЯИН СТРАНИЦЫ, * а не мы, и наш собственный словарь при этом не правится ни на строку — его * привозит генератор из продукта и переписывает целиком при каждой пересборке. * * Три вещи, которые показывает этот пример: * 1. язык, которого у нас нет, доезжает до подписей на ленте; * 2. непереведённое идёт ОБЪЯВЛЕННОЙ цепочкой (чужой словарь → наш язык → * английский) и никогда не становится пустой строкой; * 3. отдельную подпись можно переопределить, не переводя словарь целиком, — * тем же вызовом, но с кодом нашего языка. *//* ОБЪЯВЛЕНИЕ ИДЁТ ПЕРВЫМ, и это не стиль: `resolveLocale` внутри `hostAsks` сужает строку из адреса к языку, на котором поверхность УМЕЕТ говорить. Объяви словарь после — и `?locale=uk` молча стал бы запасным языком. */registerLocale(UKRAINIAN);registerLocale(OUR_RUSSIAN_TWEAKED);
const { locale } = hostAsks();
const core = createSheetsCore({ mount: document.getElementById("app")!, transport, locale,});
/** * ПОЛОСА ПОДПИСЕЙ — то же `label()`, которым говорит вся поверхность. * * Без неё пример пришлось бы принимать на веру: лист рисует числа, а перевод * виден на подписях органов. Здесь взяты подписи, часть которых переведена, а * часть нет, — и вторые обязаны остаться СЛОВОМ. */const SHOWN = ["Главная", "Вставка", "Полужирный", "Курсив", "Подчёркивание", "Объединить и поместить в центре"];
function showLabels(): void { const strip = document.querySelector("[data-locale-strip]")!; strip.textContent = ""; for (const key of SHOWN) { const chip = document.createElement("span"); chip.className = "badge badge-sm"; chip.dataset.label = key; chip.textContent = label(key); strip.append(chip); }}subscribeLocale(showLabels);
/** * ПОКРЫТИЕ — ТОТ ЖЕ СЧЁТЧИК, ЧТО У НАС, а не свой в примере: `labelCoverage` * отвечает переводчику на вопрос «сколько осталось» и называет недостающее * поимённо. Второй счётчик разошёлся бы с первым молча. */function showCoverage(code: string): void { const { total, translated, missing } = labelCoverage(surfaceLabelKeys(), code); const box = document.querySelector("[data-locale-coverage]")!; box.textContent = `${localeName(code)} (${code}): переведено ${translated} из ${total}; ` + `цепочка ${localeChain(code).join(" → ")}; ` + `не переведено, например: ${missing.slice(0, 3).join(", ")}`;}
/* Переключатель языков — из состава, а не из списка руками: `availableLocales` уже знает и наши три, и всё, что объявил хозяин. */const picker = document.querySelector("[data-locale-picker]")!;for (const code of availableLocales()) { const button = document.createElement("button"); button.className = "btn btn-xs"; button.textContent = localeName(code); button.dataset.locale = code; button.addEventListener("click", () => { setLocale(code); applyLocaleAttributes(document.documentElement, code); showCoverage(code); }); picker.append(button);}showLabels();showCoverage(locale);
import.meta.hot?.dispose(() => core.destroy());Книга примера: данные, с которыми лист открывается.
import { createMockTransport } from "@f1/contract";
/** * КНИГА-ЗАГЛУШКА — ВМЕСТО НАСТОЯЩЕГО ЯДРА. * * Транспорт контракта — единственное место, где лист разговаривает с ядром * (инвариант №1: считает ядро, оболочка только показывает). В продукте здесь * стоит `wasm` или мост хоста, и меняется РОВНО эта строка — всё остальное в * проекте остаётся как есть. */export const transport = createMockTransport({ cellAt: (row, col) => (col === 0 ? `строка ${row + 1}` : `${(row + 1) * (col + 1)}`),});Сторона хозяина страницы: что приложение отвечает листу и что от него получает.
import { applyLocaleAttributes, resolveLocale, setLocale, type Locale } from "@f1/i18n";
/** * ЧТО ХОЗЯИН СТРАНИЦЫ ДАЁТ ЛИСТУ — тема и язык. * * У встраивающего это уже есть своё: тему он знает из настроек, язык — из * профиля пользователя. Здесь и то и другое читается из адреса * (`?theme=dark&locale=ar`) ровно затем, чтобы на любое состояние можно было * дать ссылку и чтобы его проверял обход: настройка, доступная только щелчком, * недоступна ни ссылке, ни автоматической проверке. * * ПРИЗНАКИ СТАВЯТСЯ НА КОРЕНЬ ДОКУМЕНТА — потому что это делает ХОЗЯИН, а не * лист. Лист читает тему оттуда, где стоит (`:host-context`), а язык получает * СВОЙСТВОМ сборки: два листа на одной странице вправе говорить на разном. * * НАПРАВЛЕНИЕ ПИСЬМА — ИНТЕРФЕЙСА, А НЕ ДОКУМЕНТА. `dir="rtl"` зеркалит хром * страницы; «Лист справа налево» из Excel — свойство КНИГИ, и сетка от * арабского языка интерфейса не переворачивается. Это не пропуск, а решение * (`locale.ts`). */export function hostAsks(): { readonly theme: "light" | "dark"; readonly locale: Locale } { const params = new URLSearchParams(location.search); const theme = params.get("theme") === "dark" ? "dark" : "light"; const locale = resolveLocale(params.get("locale") ?? undefined, "ru");
document.documentElement.setAttribute("data-theme", theme); /* Язык — один на процесс: словарь модульный, и ставит его хозяин страницы. */ setLocale(locale); applyLocaleAttributes(document.documentElement, locale);
return { theme, locale };}Оформление страницы вокруг листа.
/* ЛИСТ СТИЛЕЙ НАБОРА КОМПОНЕНТОВ — ЕДИНСТВЕННАЯ СТРОКА, КОТОРУЮ НЕЛЬЗЯ ЗАБЫТЬ. Собственное оформление лист приносит в теневые корни сам, но утилиты раскладки, которыми он держит свои органы, компилируются ЭТИМ листом. Без него страница не просто выглядит иначе — вкладка ЗАВИСАЕТ (бесконечный цикл замера у сетки без размеров). У потребителя вне репозитория это `@fastoffices/formula-kit` из его node_modules. */@import "@fastoffices/formula-kit"; /* см. примечание выше */html,body { height: 100%; margin: 0;}Из каких пакетов собран пример.
{ "name": "f1-example-custom-locale", "private": true, "type": "module", "description": "Свой язык: словарь приносит хозяин страницы, непереведённое идёт запасной цепочкой.", "scripts": { "dev": "vite", "build": "vite build" }, "dependencies": { "@f1/preset-sheets-core": "*", "@f1/contract": "*", "@f1/i18n": "*" }, "devDependencies": { "vite": "^7.0.0" }}Файл примера.
import type { LocalePack } from "@f1/i18n";
/** * СЛОВАРЬ ВСТРАИВАЮЩЕГО — ОБЫЧНЫЕ ДАННЫЕ, КОТОРЫЕ МОЖНО ПРИВЕЗТИ ОТКУДА УГОДНО. * * Здесь он записан прямо в файле, чтобы пример читался целиком; в настоящем * проекте это `await fetch("/i18n/uk.json")` или пакет переводчика. Формы это * не меняет: `registerLocale` принимает объект. * * ЧТО ПЕРЕВОДИТСЯ: * · `messages` — ключевые строки хрома (`MessageKey`), написанные нами; * · `labels` — подписи поверхности, у которых ключ — РУССКАЯ строка. Так * устроен наш словарь: перенесённая из продукта подпись не имеет второго * идентификатора, и русский текст и есть её имя. Полный список ключей * отдаёт `surfaceLabelKeys()`, а сколько уже переведено — `labelCoverage()`. * * Переведено НАРОЧНО НЕ ВСЁ: пример показывает не только удачу, но и запасную * цепочку — `fallback: "ru"` — и то, что непереведённое остаётся СЛОВОМ. */export const UKRAINIAN: LocalePack = { code: "uk", name: "Українська", /* Запасной язык: чего нет здесь, берётся из русского, а не пропадает. Не объявишь — последним звеном встанет английский. */ fallback: "ru", messages: { "home.action.cancel": "Скасувати", "home.action.create": "Створити", "home.page.main": "Головна", "home.page.new": "Створити", }, labels: { Главная: "Головна", Вставка: "Вставлення", Данные: "Дані", Рецензирование: "Рецензування", Вид: "Вигляд", Полужирный: "Жирний", Курсив: "Курсив", "Шрифт": "Шрифт", "Выравнивание": "Вирівнювання", "Число": "Число", },};
/** * ПОПРАВКА К НАШЕМУ ЯЗЫКУ — ТОТ ЖЕ ПРИЁМ, НО ДЛЯ ОДНОЙ ПОДПИСИ. * * Встраивающему не всегда нужен новый язык: чаще он хочет назвать одно место * по-своему («Полужирный» → «Жирный шрифт»). Это тот же `registerLocale`, но с * кодом НАШЕГО языка: слой ложится поверх нашего словаря, ничего в нём не * стирая, и снимается возвратом `registerLocale`. */export const OUR_RUSSIAN_TWEAKED: LocalePack = { code: "ru", labels: { Полужирный: "Жирный шрифт" },};- Остальные примеры — Примеры F1.
- Правила, на которые ссылается код, — пять правил.
- Из чего собирается лист — Архитектура.
- Что можно вызвать сегодня — справочник Compute Contract.