@sonenta/feedback
Embed a Sonenta End-User Translation Evaluation widget in your app.
Your end users accept a short ToS, then rate (5-star) and suggest
alternatives for the translations on screen. You moderate everything from
the Sonenta dashboard — nothing auto-publishes.
@sonenta/feedback/react — React (web)
@sonenta/feedback/native — React Native / Expo
@sonenta/feedback/core — framework-agnostic client (advanced)
MIT. Add-on available from the Pro plan upward.
Install
npm i @sonenta/feedback
react (and react-native for the native entry) are peer deps.
Architecture
@sonenta/feedback is a plugin of your existing @sonenta/*-i18n
provider — not a second context/provider. You add it to the i18n
provider's plugins slot; it reuses that provider's apiBase /
projectUuid / locale (no re-config) and never re-renders your app when
the panel opens. The sessionId is minted server-side — you don't supply
a cohort/session id.
React
import { SonentaProvider } from "@sonenta/react-i18next";
import { feedbackPlugin } from "@sonenta/feedback/react";
const feedbackCtl = { current: null };
<SonentaProvider
token="vrb_live_…"
projectUuid="<project-uuid>"
defaultLocale="fr"
plugins={[
feedbackPlugin({
tosVersion: "2026-05-18",
keys: [{ namespace: "common", key: "home.title" }], // or omit → i18n registry
controllerRef: feedbackCtl, // imperative handle
}),
]}
>
<App />
</SonentaProvider>;
// trigger from your own CTA — no hook, no extra provider:
<button onClick={() => feedbackCtl.current?.open()}>Rate translations</button>;
feedbackPlugin also accepts onReady(controller), and optional
apiBase / projectId / language overrides (defaults come from the
i18n provider config).
React Native / Expo
import { feedbackPlugin } from "@sonenta/feedback/native";
Key discovery
If @sonenta/*-i18n is on the page it exposes a tiny key registry; the widget
reads the on-screen keys from it automatically (scope: "current-view", the
default).
When our capture is wrong, you can always correct it — per open:
await controller.open({
keys: [{ namespace: "common", key: "home.title" }],
});
Precedence: open({ keys }) → the static keys plugin option → whatever the
current-view capture found.
1.3.1 — this README used to promise that fallback and the API did not allow
it. controller.open() took no argument, and the only keys was the
STATIC plugin option: the same list on every screen, which is the opposite of a
per-view fallback. So on the night our capture broke, the workaround this page
told you to use was unreachable. open({ keys }) makes it true. If you read
the old sentence and could not find the door, it was not there — that is on us.
Behaviour
Ratings/suggestions are debounced + batched and sent best-effort — the
widget never blocks or breaks your app. See CONTRACT.md
for the exact wire shape.