SDK · Оценка конечным пользователем
Превью@sonenta/feedback
Позвольте вашим собственным конечным пользователям оценивать (5★) и предлагать переводы прямо из вашего продакшен-приложения. Один пакет, пять точек входа — /react, /native, /vue, /svelte, /core — одна и та же сеть, одна и та же сессия, выпущенная на сервере, один и тот же бэк-офис модерации. React и React Native подключаются к провайдеру @sonenta/*-i18n, который вы уже используете (никакого второго контекста, никаких ре-рендеров хоста); Vue и Svelte — самостоятельные идиоматичные адаптеры. Доступно как платное дополнение начиная с Pro.
Пакет @sonenta/feedback поставляется с дополнением «Оценка переводов конечным пользователем» в момент запуска Sonenta V1. Его ещё нет в npm — сетевой контракт зафиксирован (v3); привязки к фреймворкам стабилизируются и ещё могут измениться до запуска.
1. Установка (при запуске)
Один пакет. Импортируйте точку входа вашего фреймворка: @sonenta/feedback/react, /native (RN/Expo), /vue, /svelte или /core для всего остального. vue / svelte — это опциональные peer-зависимости; они нужны только соответствующей точке входа. Публикуется вместе с дополнением при запуске V1.
terminal 1// ships with the End-user evaluation add-on at the V1 launch2npm i @sonenta/react-i18next @sonenta/feedback3// feedback peers with @sonenta/react-i18next 2.x4// optional peer deps only for the matching entry: vue · svelte 2. React (web) — плагин i18n
Добавьте feedbackPlugin() в слот plugins вашего существующего провайдера @sonenta/react-i18next (>= 0.7.0) — никакого нового провайдера, никакого второго контекста. Провайдер вызывает setup() плагина один раз и переиспользует свои apiBase / projectId / defaultLocale. Панель монтируется как изолированный соседний лист с приватным хранилищем открытия/закрытия: её открытие никогда не вызывает ре-рендер вашего хост-дерева. Запускайте из своего собственного CTA через контроллер, предоставленный через controllerRef (или коллбэк onReady).
main.tsx 1// src/main.tsx — plugin of the i18n provider you already run2import { SonentaProvider } from "@sonenta/react-i18next";3import { feedbackPlugin } from "@sonenta/feedback/react";4import { useRef } from "react"; 6const feedback = useRef(null); 8<SonentaProvider9 projectUuid="proj_xxx"10 token={import.meta.env.VITE_SONENTA_TOKEN}11 plugins={[ feedbackPlugin({ controllerRef: feedback }) ]}12>13 <App />14</SonentaProvider> 16// own CTA — does NOT re-render the host tree17<button onClick={() => feedback.current?.open()}>Rate translations</button> Все опции feedbackPlugin() / createFeedback()
| Опция | Тип | По умолчанию |
|---|---|---|
| controllerRef | Ref<Controller> | — |
| onReady | (c) => void | — |
| keys | string[] | auto-discovered |
| flushDebounceMs | number | 1500 |
| maxBatch | number | 20 |
| defaultButton | boolean | false |
3. React Native / Expo
Та же схема из точки входа /native: добавьте feedbackPlugin() в слот plugins того же провайдера @sonenta/react-i18next в вашем приложении Expo и запускайте через контроллер. Никаких дополнительных нативных модулей; хранение токена использует защищённое хранилище платформы.
App.tsx 1// App.tsx (Expo / React Native) — same plugins slot2import { SonentaProvider } from "@sonenta/react-i18next";3import { feedbackPlugin } from "@sonenta/feedback/native"; 5<SonentaProvider6 projectUuid="proj_xxx"7 token={process.env.EXPO_PUBLIC_SONENTA_TOKEN}8 plugins={[ feedbackPlugin({ onReady: (c) => (ctrl = c) }) ]}9>{/* … */}</SonentaProvider> 11// wire ctrl.open() to your own button / FAB 4. Vue
@sonenta/feedback/vue — самостоятельный адаптер: явная конфигурация, нет провайдера i18n, от которого нужно наследоваться. createFeedback(config) возвращает { client, isOpen, controller, FeedbackPanel }. Смонтируйте <FeedbackPanel /> один раз рядом с корнем (он использует Teleport в body) и вызывайте controller.open() из вашего собственного CTA. То же изолированное состояние открытия — оно никогда не вызывает ре-рендер вашего приложения.
App.vue 1// main.ts — standalone adapter, explicit config2import { createFeedback } from "@sonenta/feedback/vue"; 4export const { controller, FeedbackPanel } = createFeedback({5 apiBase: "https://api.sonenta.com",6 projectId: "proj_xxx", language: "fr",7}); 9// App.vue — mount once near root (Teleports to body)10<FeedbackPanel />11<button @click="controller.open()">Rate translations</button> 5. Svelte
@sonenta/feedback/svelte — headless и идиоматичный: createFeedback(config) возвращает store-ы Svelte — isOpen (Writable), strings (Writable) — плюс open(), close(), loadStrings(), rate(), suggest(). Вы рисуете собственную панель из store-ов; SDK берёт на себя транспорт, согласие и серверную сессию.
+page.svelte 1// feedback.ts — headless idiomatic stores2import { createFeedback } from "@sonenta/feedback/svelte"; 4export const fb = createFeedback({5 apiBase: "https://api.sonenta.com",6 projectId: "proj_xxx", language: "fr",7}); 9// component — render your own panel from the stores10{#if $fb.isOpen}{#each $fb.strings as s}…{/each}{/if}11<button on:click={fb.open}>Rate translations</button> 6. Всё остальное — /core
@sonenta/feedback/core предоставляет зафиксированный FeedbackClient, на котором построены все адаптеры: acceptTos(), loadStrings(), rate(), suggest(), транспорт с дебаунсом и батчингом, ротируемый JWT. Используйте его напрямую для любого фреймворка без first-party адаптера.
feedback.ts 1// any framework — the frozen client all adapters wrap2import { FeedbackClient } from "@sonenta/feedback/core"; 4const client = new FeedbackClient({5 apiBase: "https://api.sonenta.com",6 projectId: "proj_xxx", language: "fr",7});8await client.acceptTos(); // server mints the session9await client.loadStrings(); client.rate(/* … */); client.suggest(/* … */); 7. Привязка к отображаемым ключам (автоматически)
Панель автоматически ограничивается ключами, фактически отображёнными в текущем представлении, через глобальный реестр ключей, который формирует SDK @sonenta/*-i18n — без настройки. Передавайте явный массив keys только как запасной вариант (например, строки не из @sonenta/*-i18n); никогда не передавайте весь ваш каталог — это раскрыло бы все строки приложения, а не те, на которые смотрит пользователь. Реестр отслеживается по монтированию и считается по ссылкам: постоянно присутствующие на экране строки (заголовок, eyebrow) остаются зарегистрированными, пока их компонент смонтирован — никакого сброса на каждое представление. (reset() существует только как аварийный выход для краевых случаев маршрутизации вне React; SDK никогда не вызывает его автоматически.)
scoping.ts 1// the panel auto-scopes to keys RENDERED on the current2// view, via the global key registry the @sonenta/*-i18n3// SDK produces — no config needed:4feedbackPlugin({ controllerRef: feedback }); // auto-scoped 6// explicit keys = FALLBACK only (e.g. strings not from7// @sonenta/*-i18n). NEVER pass your whole catalogue.8feedbackPlugin({ keys: ["common:checkout.cta"] }); 8. Фильтр по namespace (опционально)
Экран, который рендерит несколько namespace, может ограничить панель именно тем, который интересует клиента — передайте опциональный namespace (string | string[]) в триггер/конфигурацию (feedbackPlugin() для React/Native, createFeedback() для Vue/Svelte, resolveKeys() / filterByNamespace() для /core). Он применяется после привязки к отображаемым ключам — показано = отображённое ∩ namespace. Не задано, "" или [] = без фильтра (как раньше). Он никогда не откатывается на весь проект.
namespace.ts 1// §0d — OPTIONAL namespace filter (customer feature).2// Composes AFTER rendered-scoping: shown = rendered ∩ namespace.3feedbackPlugin({ controllerRef: feedback, namespace: "quiz" }); 5// Vue / Svelte — same option on createFeedback:6createFeedback({ apiBase, projectId, language, namespace: ["quiz"] }); 8// /core — resolveKeys / filterByNamespace:9resolveKeys(explicit, "quiz"); // or filterByNamespace(keys, "quiz") 11// unset / "" / [] ⇒ no filter (identical to v5). 9. Сессия, выпущенная на сервере (все фреймворки)
Ключ сессии / группировки выпускается на стороне сервера при согласии. Клиент никогда не отправляет и не генерирует его сам — нет конфигурации groupingKey и нет поля запроса. При acceptTos() бэкенд возвращает его (привязанным к ограниченному JWT); каждый адаптер предоставляет его только для чтения через client.sessionId. Возвращающийся endUserId сохраняет своё стабильное серверное значение; новый пользователь получает свежий sess_….
consent.ts 1// session/grouping key is MINTED SERVER-SIDE at consent2await client.acceptTos(); // POST /v1/feedback/tos3client.sessionId; // read-only, e.g. "sess_018f…" 5// NO groupingKey config, NO request field —6// the client never sends or self-generates the session. 10. Согласие и безопасность (автоматически)
Перед первой записью конечный пользователь принимает версионированные пользовательские условия Sonenta. Затем SDK получает короткоживущий JWT, ограниченный скоупом feedback:write — криптографически отделённый от вашей клиентской авторизации — и обновляет его прозрачно. Конечные пользователи анонимны (непрозрачный id, без PII). Со стороны безопасности вам ничего писать не нужно.
Что вы получаете бесплатно
- Ноль ре-рендеров хоста. React/Native работают как изолированный соседний лист провайдера i18n; Vue/Svelte держат состояние открытия в собственном store. Добавление или открытие обратной связи никогда не вызывает ре-рендер вашего приложения.
- Согласие + серверная сессия обрабатываются. Принятие условий, выпущенная на сервере сессия, получение JWT, ротирующее обновление и единичный прозрачный повтор при 401 — всё внутри SDK. Он никогда не выбрасывает исключение в ваш путь рендеринга.
- Транспорт с дебаунсом и батчингом. Оценки и предложения ставятся в очередь и отправляются по дебаунсу (по умолчанию 1,5 с), при достижении максимального батча или при закрытии. Best-effort: неудавшийся батч повторно ставится в очередь один раз, затем поглощается.
- Модерация перед публикацией. Ничто, отправленное конечным пользователем, не публикуется автоматически. Предложения попадают в статусе
pendingв очередь модерации вашего дашборда — вы одобряете, отклоняете или применяете их обычным аудируемым путём редактирования. Оценки агрегируются в реальном времени по ключу / по языку.