Skip to content

@sonenta/feedback

Let your own end users rate (5★) and suggest translations from inside your shipped app. One package, five entry points, /react, /native, /vue, /svelte, /core, same wire, same server-minted session, same moderation back office. React & React Native plug into the @sonenta/*-i18n provider you already run (no second context, no host re-render); Vue & Svelte are idiomatic standalone adapters. Available as a Pro+ paid add-on.

One package. Import the entry for your framework: @sonenta/feedback/react, /native (RN/Expo), /vue, /svelte, or /core for anything else. vue / svelte are optional peer deps, only the matching entry needs them.

Fenêtre de terminal
npm i @sonenta/react-i18next @sonenta/feedback
// optional peer deps only for the matching entry: vue · svelte

Add feedbackPlugin() to your existing @sonenta/react-i18next (>= 0.7.0) provider's plugins slot, no new provider, no second context. The provider calls the plugin's setup() once and reuses its own apiBase / projectId / defaultLocale. The panel mounts as an isolated sibling leaf with a private open/close store, so opening it never re-renders your host tree. Trigger from your own CTA via the controller delivered through controllerRef (or the onReady callback).

// src/main.tsx: plugin of the i18n provider you already run
import { SonentaProvider } from "@sonenta/react-i18next";
import { feedbackPlugin } from "@sonenta/feedback/react";
import { useRef } from "react";
const feedback = useRef(null);
<SonentaProvider
projectUuid="proj_xxx"
token={import.meta.env.VITE_SONENTA_TOKEN}
plugins={[ feedbackPlugin({ controllerRef: feedback }) ]}
>
<App />
</SonentaProvider>
// own CTA, does NOT re-render the host tree
<button onClick={() => feedback.current?.open()}>Rate translations</button>
All feedbackPlugin() / createFeedback() options
OptionTypeDefault
controllerRefRef<Controller>-
onReady(c) => void-
keysstring[]auto-discovered
flushDebounceMsnumber1500
maxBatchnumber20
defaultButtonbooleanfalse

Identical pattern from the /native entry: add feedbackPlugin() to the same @sonenta/react-i18next provider's plugins slot in your Expo app and trigger via the controller. No extra native modules; token storage uses the platform secure store.

// App.tsx (Expo / React Native): same plugins slot
import { SonentaProvider } from "@sonenta/react-i18next";
import { feedbackPlugin } from "@sonenta/feedback/native";
<SonentaProvider
projectUuid="proj_xxx"
token={process.env.EXPO_PUBLIC_SONENTA_TOKEN}
plugins={[ feedbackPlugin({ onReady: (c) => (ctrl = c) }) ]}
>{/* … */}</SonentaProvider>
// wire ctrl.open() to your own button / FAB

@sonenta/feedback/vue is a standalone adapter, explicit config, no i18n provider to inherit from. createFeedback(config) returns { client, isOpen, controller, FeedbackPanel }. Mount <FeedbackPanel /> once near your root (it Teleports to body), and call controller.open() from your own CTA. Same isolated open-state, it never re-renders your app.

// main.ts: standalone adapter, explicit config
import { createFeedback } from "@sonenta/feedback/vue";
export const { controller, FeedbackPanel } = createFeedback({
apiBase: "https://api.sonenta.com",
projectId: "proj_xxx", language: "fr",
});
// App.vue: mount once near root (Teleports to body)
<FeedbackPanel />
<button @click="controller.open()">Rate translations</button>

@sonenta/feedback/svelte is headless and idiomatic: createFeedback(config) returns Svelte stores, isOpen (Writable), strings (Writable), plus open(), close(), loadStrings(), rate(), suggest(). You render your own panel from the stores; the SDK owns transport, consent and the server session.

// feedback.ts: headless idiomatic stores
import { createFeedback } from "@sonenta/feedback/svelte";
export const fb = createFeedback({
apiBase: "https://api.sonenta.com",
projectId: "proj_xxx", language: "fr",
});
// component: render your own panel from the stores
{#if $fb.isOpen}{#each $fb.strings as s}…{/each}{/if}
<button on:click={fb.open}>Rate translations</button>

@sonenta/feedback/core exposes the frozen FeedbackClient all adapters are built on: acceptTos(), loadStrings(), rate(), suggest(), debounced batched transport, rotating JWT. Use it directly for any framework without a first-party adapter.

// any framework: the frozen client all adapters wrap
import { FeedbackClient } from "@sonenta/feedback/core";
const client = new FeedbackClient({
apiBase: "https://api.sonenta.com",
projectId: "proj_xxx", language: "fr",
});
await client.acceptTos(); // server mints the session
await client.loadStrings(); client.rate(/* … */); client.suggest(/* … */);

The panel auto-scopes to the keys actually rendered on the current view, via the global key registry the @sonenta/*-i18n SDK produces, no configuration. Pass an explicit keys array only as a fallback (e.g. strings not sourced from @sonenta/*-i18n); never pass your whole catalogue, that would surface every string in the app, not the ones the user is looking at. The registry is mount-tracked and ref-counted: persistent always-on-screen strings (a header, an eyebrow) stay registered while their component is mounted, there is no per-view reset. (reset() exists only as an escape hatch for non-React routing edge cases; the SDK never calls it automatically.)

// the panel auto-scopes to keys RENDERED on the current
// view, via the global key registry the @sonenta/*-i18n
// SDK produces, no config needed:
feedbackPlugin({ controllerRef: feedback }); // auto-scoped
// explicit keys = FALLBACK only (e.g. strings not from
// @sonenta/*-i18n). NEVER pass your whole catalogue.
feedbackPlugin({ keys: ["common:checkout.cta"] });

A screen that renders several namespaces can scope the panel to just the one the customer cares about, pass an optional namespace (string | string[]) on the trigger/config (feedbackPlugin() for React/Native, createFeedback() for Vue/Svelte, resolveKeys() / filterByNamespace() for /core). It composes after rendered-scoping, shown = rendered ∩ namespace. Unset, "", or [] means no filter (identical to before). It never falls back to the whole project.

// §0d: OPTIONAL namespace filter (customer feature).
// Composes AFTER rendered-scoping: shown = rendered ∩ namespace.
feedbackPlugin({ controllerRef: feedback, namespace: "quiz" });
// Vue / Svelte, same option on createFeedback:
createFeedback({ apiBase, projectId, language, namespace: ["quiz"] });
// /core, resolveKeys / filterByNamespace:
resolveKeys(explicit, "quiz"); // or filterByNamespace(keys, "quiz")
// unset / "" / [] ⇒ no filter.

The session / grouping key is minted server-side at consent. The client never sends or self-generates it, there is no groupingKey config and no request field. On acceptTos() the backend returns it (bound into the scoped JWT); every adapter exposes it read-only as client.sessionId. A returning endUserId keeps its stable server value; a new end user gets a fresh sess_….

// session/grouping key is MINTED SERVER-SIDE at consent
await client.acceptTos(); // POST /v1/feedback/tos
client.sessionId; // read-only, e.g. "sess_018f…"
// NO groupingKey config, NO request field:
// the client never sends or self-generates the session.

Before the first write, the end user accepts the versioned Sonenta end-user ToS. The SDK then acquires a short-lived JWT scoped to feedback:write only, cryptographically separate from your customer auth, and rotates it transparently. End users are anonymous (opaque id, no PII). You ship nothing security-side.

  • Zero host re-render. React/Native run as an isolated sibling leaf of the i18n provider; Vue/Svelte keep open-state in their own store. Adding or opening feedback never re-renders your app.
  • Consent + server session handled. ToS acceptance, server-minted session, JWT acquisition, rotating refresh, and a one-shot transparent retry on 401, all inside the SDK. It never throws into your render path.
  • Debounced, batched transport. Ratings and suggestions are queued and flushed on a debounce (default 1.5s), at a max batch, or on close. Best-effort: a failed batch is re-queued once, then swallowed.
  • Moderation before publish. Nothing an end user submits goes live automatically. Suggestions land as pending in your dashboard's moderation queue, you approve, reject, or apply through the normal audited edit path. Ratings roll up into a realtime per-key / per-language dashboard.