@polyglot-hub/sdk
v0.1.1
Published
Клиент polyglot-hub.io: серверная часть с service key и кешем + клиент без секретов — переводы, plural- и gender-формы
Downloads
583
Readme
@polyglot-hub/sdk
SDK polyglot-hub.io: переводы приложения с подстановкой переменных, plural- и gender-формами. Без зависимостей, ESM и CommonJS, Node ≥ 18 и браузеры.
Два входа:
@polyglot-hub/sdk/server— для бэкенда. ДержитserviceKey, сам ходит в polyglot-hub, кеширует ответы и отдаёт готовый HTTP-обработчик для вашего API.@polyglot-hub/sdk— для фронтенда (и SSR). Секретов не знает: берёт переводы с вашего бэкенда.
serviceKey— секрет. Держите его только в переменных окружения бэкенда; в клиентский бандл он попадать не должен. Поэтому клиент ходит не в polyglot-hub, а в ваш бэкенд — back-to-back.
браузер ──GET /api/i18n/ru──▶ ваш бэкенд (sdk/server) ──service_key──▶ api.polyglot-hub.ioУстановка
npm i @polyglot-hub/sdkПодключение
Бэкенд на Node (Express, Fastify, Hono, node:http…) + любой фронтенд.
Для Next.js и NestJS удобнее готовые обёртки — см. ниже.
Ключи приложения. В polyglot-hub.io откройте приложение:
appId— в адресе (/apps/<appId>/…) и на вкладке «Подключение»,serviceKey— в «Настройках». Положите их в окружение бэкенда:POLYGLOT_APP_ID=… POLYGLOT_SERVICE_KEY=… # секрет: не коммитить и не отдавать в браузерБэкенд — обработчик переводов на вашем API:
import express from 'express'; import {createPolyglotServer} from '@polyglot-hub/sdk/server'; const polyglotServer = createPolyglotServer({ appId: process.env.POLYGLOT_APP_ID!, serviceKey: process.env.POLYGLOT_SERVICE_KEY!, }); const app = express(); app.use('/api/i18n', polyglotServer.nodeHandler); // GET /api/i18n/locales и GET /api/i18n/:localeФронтенд — клиент знает только адрес обработчика:
import {createPolyglot} from '@polyglot-hub/sdk'; export const polyglot = createPolyglot({endpoint: '/api/i18n'}); // другой домен — полный URL await polyglot.init(); document.title = polyglot.getLang('app.title');В React — подписка на состояние клиента, язык и список языков берутся из него:
import {useSyncExternalStore} from 'react'; export const LangSwitcher = () => { const {locale, locales} = useSyncExternalStore(polyglot.subscribe, polyglot.getState); return ( <select value={locale} onChange={(e) => polyglot.setLocale(e.target.value)}> {locales.map(({iso, title}) => <option key={iso} value={iso}>{title}</option>)} </select> ); };Пока
init()не завершился,getLangвозвращает имена ключей — покажите загрузку или дождитесьinit()до первого рендера.Проверка:
curl http://localhost:3000/api/i18n/locales # [{"iso":"ru","title":"Русский"}, …] curl http://localhost:3000/api/i18n/ru # {"cart.title":"Корзина", …}
Бэкенд
import {createPolyglotServer} from '@polyglot-hub/sdk/server';
export const polyglotServer = createPolyglotServer({
appId: process.env.POLYGLOT_APP_ID!,
serviceKey: process.env.POLYGLOT_SERVICE_KEY!,
ttl: 60_000, // кеш ответов polyglot-hub в памяти, мс; 0 — без кеша
});Обработчик отвечает на GET …/<locale> (переводы) и GET …/locales (языки приложения — [{iso, title}]),
остальное — 400/405.
Подключение к серверу:
// Express, Connect, node:http — app.use с префиксом; не GET уходит дальше по цепочке
app.use('/api/i18n', polyglotServer.nodeHandler);
// Fastify
await app.register(polyglotServer.fastifyPlugin, {prefix: '/api/i18n'});
// Web Request → Response: Hono, Bun, Deno, Cloudflare Workers
app.get('/api/i18n/:locale', (c) => polyglotServer.handler(c.req.raw));
// Любой другой фреймворк: resolve отдаёт status, headers и body без привязки к фреймворку
const {status, headers, body} = await polyglotServer.resolve('GET', locale);Для Next.js и NestJS есть готовые обёртки — @polyglot-hub/next-sdk
и @polyglot-hub/nest-sdk.
Тексты на самом бэкенде (письма, пуши, ответы API):
const t = await polyglotServer.getTranslator('ru');
t.getPluralLang('cart.items', 5); // «5 товаров»Если polyglot-hub недоступен, сервер отдаёт последний удачный ответ из кеша. clearCache() сбрасывает кеш.
Фронтенд
import {createPolyglot} from '@polyglot-hub/sdk';
const polyglot = createPolyglot({endpoint: '/api/i18n'});
await polyglot.init(); // запомненный язык, иначе язык браузера среди языков приложения, иначе первый
polyglot.locale; // 'ru'
polyglot.locales; // [{iso: 'ru', title: 'Русский'}, …] — для переключателя
await polyglot.setLocale('en'); // загрузит переводы и запомнит выбор
polyglot.getLang('cart.title'); // «Корзина»
polyglot.getLang('greeting', {name: 'Аня'}); // «Привет, Аня!» — {{name}} в переводе
polyglot.getPluralLang('cart.items', 5); // «5 товаров» — {{count}} в переводе
polyglot.getGenderLang('profile.updated', 'female'); // форма female, иначе нейтральнаяКлюча нет в переводах — возвращается само имя ключа.
Всё состояние — текущий язык, языки приложения, загруженные переводы — живёт в клиенте.
Подписка на изменения — subscribe, снимок — getState(); в React:
const {locale, locales} = useSyncExternalStore(polyglot.subscribe, polyglot.getState);Где запоминается язык
По умолчанию — localStorage браузера. Своё хранилище (например, cookie, которую читает ваш бэкенд)
или false, чтобы не запоминать:
const polyglot = createPolyglot({
endpoint: '/api/i18n',
storage: {
get: () => readCookie('lang'),
set: (locale) => writeCookie('lang', locale),
},
});SSR, письма и прочее на сервере
Клиент можно создать поверх серверной части — без HTTP (или просто polyglotServer.getTranslator(locale)):
const polyglot = createPolyglot({
loadTranslations: polyglotServer.getTranslations,
loadLocales: polyglotServer.getLocales,
});Переводы (и языки), полученные на сервере, передайте в браузер и положите через hydrate — без повторного запроса:
polyglot.hydrate('ru', translationsFromServer, localesFromServer);API клиента
| Метод | Что делает |
|-------|------------|
| createPolyglot({endpoint, fetch?, storage?}) | Клиент поверх вашего бэкенда: GET <endpoint>/<locale>, GET <endpoint>/locales. |
| createPolyglot({loadTranslations, loadLocales?, storage?}) | Клиент со своими функциями загрузки. |
| storage | {get, set} — где запоминать выбранный язык; по умолчанию localStorage в браузере, false — нигде. |
| init(iso?) | Первая загрузка: языки приложения и переводы. Язык — переданный, иначе запомненный в storage (если он включён в приложении), иначе язык браузера (navigator.languages), иначе первый. Возвращает выбранную локаль. |
| setLocale(iso) | Загружает переводы локали (если ещё нет), делает её текущей и запоминает в storage. |
| load(iso) | Только загружает в кеш. Параллельные вызовы делают один запрос. |
| hydrate(iso, translations, locales?) | Кладёт готовые переводы (и языки) и делает локаль текущей, без запроса. |
| locale | Текущая локаль ('' до первой загрузки). |
| locales | Языки приложения [{iso, title}] в порядке из настроек ([] до загрузки). |
| getState() | Снимок {locale, locales} — новый объект при каждом изменении. |
| getLocales() | Загружает языки приложения (один раз) и возвращает их. |
| getLang(key, props?) | Текст ключа, {{name}} заменяются значениями из props. |
| getPluralLang(key, count, props?) | Форма по правилам языка (Intl.PluralRules): key_one, key_few, key_many…; нет формы — key. {{count}} — число в записи языка (1,5 в ru). |
| getGenderLang(key, gender, props?) | key_male / key_female; род не задан или форма пустая — нейтральная key. |
| subscribe(listener) | Вызывает listener(state) при смене локали или языков, возвращает функцию отписки. |
API сервера
| Метод | Что делает |
|-------|------------|
| createPolyglotServer({appId, serviceKey, host?, ttl?, fetch?}) | host по умолчанию https://api.polyglot-hub.io, ttl — 60 000 мс. В браузере бросает ошибку. |
| getTranslations(locale) | Переводы локали из polyglot-hub (с кешем). |
| getLocales() | Включённые языки приложения [{iso, title}]. |
| getTranslator(locale) | Переводчик с загруженной локалью (getLang, getPluralLang, getGenderLang). |
| nodeHandler(req, res, next?) | Обработчик для Express / Connect / node:http. |
| fastifyPlugin | Плагин Fastify, регистрировать с prefix. |
| handler(request) | Обработчик Web Request → Response. |
| resolve(method, segment) | Ответ {status, headers, body} для своего адаптера. Ошибки polyglot-hub — 502 без деталей. |
| clearCache() | Сбросить кеш. |
Ошибки приходят как PolyglotError с полем status. Утилиты plural-форм тоже экспортируются:
pluralCategory(iso, count), pluralCategoriesFor(iso), formatCount(iso, count).
Формат переводов
Ключи — плоский объект в формате i18next, его можно отдать и в i18next напрямую:
plural — name_one, name_few, name_many, name_other (+ name = форма other),
gender — name_male, name_female (+ name = нейтральная форма).
