Пакети / Мови
@tykonpm/multilang
v0.4.029 мов з українським джерелом, React-провайдер, адмінка й черга перекладу сайту.
Документ описує код @tykonpm/multilang у main, який може випереджати npm-реліз. Перевірте version і exports установленої версії. Українська (uk) — джерело. Сторінки, таблиці, права та ШІ налаштовує сайт.
1. Модулі й можливості
MultilangProvider показує переклади; TranslationsAdmin редагує плоский список фраз для однієї мови й одразу зберігає ШІ-результати. TranslateAllPanel поєднує матрицю джерела × мови, запуск, чергу та TranslationReview: відкрийте job у черзі для перевірки. Окремі компоненти також доступні.
Рядки матриці — зареєстровані сторінки, меню чи CMS; стовпці — мови. Клітинка запускає джерело × мову, заголовок рядка — джерело всіма мовами, стовпця — весь реєстр однією мовою, «Перекласти все» — весь реєстр. API підтримує окремі keys через scope item; стандартна матриця не має списку ключів, тому для них сайт додає власний вибір із TranslationLauncher.
Пакет не сканує CMS автоматично: coverage стосується тільки зареєстрованих TranslationSource. Він не гарантує, що перекладено весь код сайта.
2. Локальний приклад без мережі й секретів
Це mock для перевірки контракту, не production-перекладач. Запускайте TypeScript-збирачем.
import {
createTranslator, createMemoryJobStore, applyWithConflictCheck,
type TranslationProvider, type TranslationSource,
} from '@tykonpm/multilang/translate-all';
const originals = [{ key: 'home.title', text: 'Ділова рада', label: 'Заголовок' }];
const translated = new Map<string, Map<string, { text: string; sourceHash?: string | null }>>();
function dictionary(lang: string) {
let rows = translated.get(lang);
if (!rows) { rows = new Map(); translated.set(lang, rows); }
return rows;
}
const home: TranslationSource = {
id: 'home', label: 'Головна',
items(lang) {
const rows = dictionary(lang);
return originals.map(item => {
const current = rows.get(item.key);
return { ...item, current: current?.text ?? '', currentHash: current?.sourceHash ?? null };
});
},
apply(lang, rows) { return applyWithConflictCheck(dictionary(lang), rows); },
};
const provider: TranslationProvider = {
id: 'mock',
async translate(req) { return { texts: req.texts.map(text => `${req.to}: ${text}`) }; },
};
const api = createTranslator({ sources: [home], provider, jobs: createMemoryJobStore(), autoApply: false });
const request = { scope: 'site' as const, langs: ['en'], mode: 'missing' as const };
console.log(await api.plan(request));
await api.enqueue(request);
await api.runAll();
for (const job of await api.jobs()) await api.apply(job.id);
console.log(await api.coverage(['en']));Memory store і словник живуть в одному процесі. Для production замініть їх на стійке сховище та захищені серверні виклики.
3. TranslationSource і конфлікти
interface TranslationSource {
id: string;
label: string;
items(lang: string): Promise<TranslatableItem[]> | TranslatableItem[];
apply(lang: string, rows: ApplyRow[]): Promise<ApplyResult> | ApplyResult;
}
// TranslatableItem: key, text (uk), current?, currentHash?, label?
// ApplyRow: key, text (переклад), sourceHash, previous
// ApplyResult: { applied: string[], conflicts: string[] }Використовуйте стабільний key (pageId + field), не мінливий український рядок. Зберігайте sourceHash українського text, щоб currentHash визначав stale. Без hash старі переклади не стають stale автоматично. Проста адмінка використовує український рядок як ключ: його перейменування без міграції дасть missing, а не зв’яже старий запис.
apply повинен атомарно перевіряти current translation === previous перед записом text/sourceHash. Поверніть конфлікти без перезапису. applyWithConflictCheck — помічник для Map, не транзакція БД. Додатково звірте hash актуального українського джерела з row.sourceHash: previous не захищає від зміни оригіналу після enqueue. Записуйте тільки дозволені поля.
4. Провайдер-незалежний ШІ
interface TranslationProvider {
id: string;
pricePerMTokens?: number;
pauseMs?: number;
translate(req: ProviderRequest): Promise<ProviderResult>;
}
// req: from, to, texts[], glossary[], context?
// result: texts[] у тому самому порядку, tokens? (використання)Власний адаптер викликає підтримуваний серверний SDK/gateway сайта. Ключі лише на сервері. ProviderError(message, status, retryAfterMs?) передає статус для retry. Не кладіть secrets у props, localStorage або browser bundle.
Необов’язкові експорти: openAICompatibleProvider({ url, apiKey, model, timeoutMs?, headers?, pauseMs?, pricePerMTokens? }), groqProvider, geminiProvider, aiGatewayProvider (model обов’язковий; застаріла назва lovableProvider), fallbackProvider([providers]). Перевірте доступність endpoint/model у своєму акаунті; для платформ із керованим AI реалізуйте TranslationProvider через їхній підтримуваний SDK. fallbackProvider перебирає сервіси при будь-якій помилці; він не переносить ціну й pauseMs в комбінований об’єкт автоматично.
// Викликається тільки всередині серверного handler із перевіреною конфігурацією.
import { openAICompatibleProvider } from '@tykonpm/multilang/translate-all';
function makeProvider(config: { url: string; apiKey: string; model: string }) {
if (!config.apiKey) throw new Error('Не налаштовано ключ перекладу');
return openAICompatibleProvider({ ...config, timeoutMs: 60000, pauseMs: 1000 });
}Глосарій: { term: 'Rotary' } — не перекладати; { term: 'Ділова рада', translations: { en: 'Business Council' } } — фіксований переклад у prompt. Це вимога до ШІ, а не гарантія правильності. Маскування захищає URL/email/теги/плейсхолдери й незмінні терміни; unmask перевіряє маркери. Воно не є HTML sanitizer і не підтримує довільні формати документів автоматично.
5. JobStore і Supabase
JobStore: list(), get(id), save(job), remove(id). createMemoryJobStore — тести; createLocalJobStore — браузерні демо після hydration; createSupabaseJobStore(client, { table?, limit? }) — стійке серверне сховище. Supabase SDK надає сайт, пакет від нього не залежить.
Default limit для Supabase list — 500 jobs. Для великої черги змініть limit або реалізуйте повну пагінацію: неповний list також псує денний облік. localStorage не використовувати для secrets/приватного контенту/спільної production-черги.
translationJobsSql({ canManage, table? }) повертає SQL, але не виконує його. Значення задає розробник, не користувач. Перевірте наявну схему, grants, рольову функцію та RLS перед міграцією; policy при повторному застосуванні може вже існувати. Пакет не створює admin-роль.
createSupabaseStore з /supabase очікує ui_translations(lang, source, text, source_hash, origin, updated_at) з unique(lang, source). updated_by потрібен лише за opts.userId. SUPABASE_SCHEMA_SQL додає тільки source_hash до існуючої таблиці, не створює повну схему. ui_translations і translation_jobs — різні таблиці. Для іншої схеми напишіть власний TranslationStore, не ламайте існуючі дані.
6. API, режими й виконання
await api.plan({ scope: 'site', langs: ['en', 'pl'], mode: 'missing' });
await api.enqueue({ scope: 'source', sourceIds: ['home'], langs: ['en'], autoApply: false });
await api.enqueue({ scope: 'item', sourceIds: ['home'], keys: ['home.title'], langs: ['pl'] });
await api.processNext();
// jobId взяти з enqueue/jobs після перевірки доступу:
await api.action(jobId, 'pause'); // також resume, cancel, remove, retry
await api.apply(jobId, ['home.title']); // без keys — весь успішний jobscope site — всі джерела; source — вибрані; item — keys у вибраних джерелах. uk виключається з цільових мов. mode missing включає відсутні й застарілі; окремого stale-only немає. all повторно перекладає готові записи.
autoApply false зберігає результати в job до підтвердження; true застосовує успішні пункти через source.apply. Review «Застосувати все» стосується одного job; затвердження цілого джерела/черги сайт робить явним обходом jobs з підтвердженням.
Defaults: batchSize 40, maxChars 6000, leaseMs 5 хвилин, retry 3 спроби з backoff 1000–30000 ms. Наддовгий один пункт не дробиться сам. Retry: ProviderError 408/429/5xx та загальні винятки; неретрайні ProviderError не повторюються. OpenAI-compatible timeout — 60 секунд.
plan — dry-run із jobs/items/chars/estimated tokens і cost лише за pricePerMTokens. Це не рахунок провайдера. maxJobTokens перевіряється в plan, не забезпечується enqueue/processNext; сервер повинен відхиляти overLimit/завеликий пакет. Денний ліміт перевіряється перед запуском і останній пакет може його перевищити. Видалення jobs або неповний list змінює облік. Для жорсткого бюджету потрібен окремий надійний ledger.
enqueue лише записує jobs. processNext — один пакет; runAll — послідовний цикл. useTranslationQueue/TranslationJobs працюють, поки адмін-сторінка відкрита. Закриття вкладки не створює background worker; його додає сайт. Стійкий JobStore зберігає стан між відкриттями.
Немає атомарного distributed claim, глобального дедупу чи кешу ШІ за hash. Один активний процесор або зовнішній lock. pause/cancel не переривають поточний AI-запит; конкурентне завершення може перезаписати статус. Lease може повторно виконати покинутий running job: at-least-once, не exactly-once. Не обіцяйте миттєвого скасування чи жорстких квот.
7. Адмінка й дизайн
import { TranslateAllPanel } from '@tykonpm/multilang/translate-all/react';
import type { TranslateAllApi } from '@tykonpm/multilang/translate-all';
export function SiteTranslationAdmin({ api }: { api: TranslateAllApi }) {
return <TranslateAllPanel api={api} languages={['en', 'pl', 'de']}
classNames={{ root: 'translation-admin', table: 'translation-table', button: 'translation-button' }}
onError={message => console.error(message)} />;
}api — browser proxy до захищеного сервера, не translator із ключем. CSS-класи визначає сайт. classNames: root/toolbar/button/primary/table/cell/badge/input/diff; labels змінюють написи. Panel вбудовує Review, доступний через відкриття job у черзі.
Окремі екрани: TranslationMatrix, TranslationLauncher, TranslationJobs, TranslationReview; hook useTranslationQueue. Review має пошук, статуси, порівняння й apply одного пункту/всіх успішних пунктів job. Для item scope сайт додає власний вибір ключів.
Екрани використовують власні HTML-контроли; classNames не робить їх автоматично компонентами вашої дизайн-системи. Збережіть затверджений UI або побудуйте власну адмінку на TranslateAllApi. Додайте показ мережевих помилок, loading/empty і підтвердження all/autoApply. На телефоні прокрутка має бути всередині широкої таблиці, не всієї сторінки.
8. Захищений серверний proxy
Реалізуйте TranslateAllApi через серверні виклики: coverage, plan, enqueue, jobs, processNext, action, apply. КОЖЕН handler перевіряє session, роль, доступ до source/job; валідує langs/scope/keys/режим/розміри. createdBy беріть із session, не клієнтського поля.
Serverless: translator у handler зі стійким JobStore та узгодженим lock, не singleton у різних інстансах. Не повертайте credentials чи чужий контент. SQL/RLS не замінює перевірки серверного privileged client. Використовуйте серверні механізми платформи й повертайте зрозумілі помилки без secrets.
9. Перевірка інтеграції
1. Чисте встановлення реальної npm-версії, exports і tarball з README/TRANSLATE-ALL/AI-GUIDE. 2. Mock: один key × мова → plan → enqueue → processNext → review → apply → read back → coverage 100%. 3. Змінити uk text зі стабільним key: stale=1, missing-mode включає пункт. Старі записи без hash перевірити окремо. 4. Редагування перекладу після enqueue → conflict без перезапису; окремо перевірити зміну українського джерела. 5. autoApply false не пише до підтвердження; true пише через адаптер. Перевірити item/source/site, missing/all. 6. Reload зберігає jobs; один процесор продовжує. 429/5xx, timeout, порожня відповідь, пошкоджені маркери показані оператору. 7. Signed-out та звичайний користувач не мають доступу до керування — перевірити сервер і RLS, не тільки меню. 8. Один малий реальний AI-пакет із дозволеним бюджетом і читанням результату в UI. Mock не замінює цей тест.
10. Супровід
Оновлюйте README, цей документ і AI-GUIDE.md разом з API та changeset. /multilang читає ті самі файли напряму, /use — той самий platform guide. npm README оновлюється лише після релізу GitHub Actions, не від коміту.