Internationalization (i18n)
Every user-facing string the SDK can show is overridable — there is no
bundled locale prop. By design you supply the translated strings from your
own i18n catalogue (i18next, FormatJS, etc.); the SDK never ships translations
it can't keep in sync with your wording. There are exactly two surfaces, and
together they cover 100% of what a user reads.
1. guidanceCopy — everything the SDK renders on screen
A single Partial<GuidanceCopy> prop. Pass the keys you want to translate;
omitted keys fall back to the English default. This covers the rotate prompt,
the pan hint, the live "too fast" cue, the lateral-drift popups, the crop-editor
buttons, the capture-status banner, and the crop-editor warning banners:
Each default below is the exact, complete source string — translate it
verbatim (and keep the {included} / {requested} / {percent} placeholders
in warnLowFrameUtilization). You can also import { DEFAULT_GUIDANCE_COPY }
to seed your catalogue programmatically.
| Key | Where it appears | English default (translate verbatim) |
|---|---|---|
rotateToLandscape | rotate prompt | Rotate to landscape |
rotateToPortrait | rotate prompt | Rotate to portrait |
panHint | pan how-to overlay | Pan slowly top to bottom |
tooFast | speed-cue pill | Moving too fast — slow down |
lateralStopTitle | lateral-drift popup (stitched) | Keep the pan straight |
lateralStopBody | lateral-drift popup (stitched) | You moved sideways. Pan in one direction only — we stitched what you captured. |
lateralStopDismiss | lateral-drift popup button | Got it |
lateralWrongDirectionTitle | lateral-drift popup (too few frames) | Follow the arrow |
lateralWrongDirectionBody | lateral-drift popup (too few frames) | You moved the phone the wrong way. Pan slowly in the direction the arrow shows, in one straight line. |
cropConfirm | crop-editor button | Crop |
cropReset | crop-editor button | Reset |
cropUseOriginal | crop-editor button | Use original |
cropRetake | crop-editor button | Retake |
previewConfirm | preview accept button (showPreview) | Confirm |
statusRecording | status banner | Hold steady — pan slowly |
statusStitching | status banner | Stitching panorama… |
warnLowFrameUtilization | crop warning (template) | Only {included} of {requested} captured frames ({percent}%) could be used — the panorama may be incomplete. Pan more slowly and steadily next time. |
warnLateralDriftFinalize | crop warning | Capture stopped early because the phone drifted sideways — only the part captured before the drift was stitched. |
warnHighPanSpeed | crop warning | The capture was taken faster than the recommended pace — the result may not be the best. Pan more slowly next time. |
warnLowFrameUtilization is interpolated at runtime — your translation must
keep the {included}, {requested} and {percent} placeholders (an unknown
placeholder is left verbatim rather than throwing). Overriding a warn* key
re-words both the on-screen banner and the message carried on
onCapture(...).warnings[]. The matching machine-readable code (e.g.
HIGH_PAN_SPEED) is always present regardless of wording, so you can also
branch on the code instead of the string.
2. userFacingStitchError(code, overrides?) — the host-rendered error alert
The recoverable-stitch-error copy is rendered by you (in onError), so it's
localised at the call site: pass an overrides map (keyed by the codes in the
exported RECOVERABLE_STITCH_CODES) and any match wins over the bundled English;
omitted codes keep the default. See Capture result & errors
for the full list of error codes.
import {
Camera,
userFacingStitchError,
RECOVERABLE_STITCH_CODES,
DEFAULT_GUIDANCE_COPY,
type GuidanceCopy,
type UserFacingStitchErrorOverrides,
} from 'react-native-image-stitcher';
import { Alert } from 'react-native';
import { useTranslation } from 'react-i18next';
function CaptureScreen() {
const { t } = useTranslation();
// (1) SDK-rendered copy — translate the keys you care about.
const guidanceCopy: Partial<GuidanceCopy> = {
rotateToLandscape: t('pano.rotateToLandscape'),
statusRecording: t('pano.statusRecording'),
// keep the placeholders in the template translation:
warnLowFrameUtilization: t('pano.warnLowFrames'), // "{included}/{requested} ({percent}%) …"
// …any subset; the rest stay English via DEFAULT_GUIDANCE_COPY
};
// (2) Host-rendered error alerts — translate by code.
const errorCopy: UserFacingStitchErrorOverrides = Object.fromEntries(
RECOVERABLE_STITCH_CODES.map((code) => [
code,
{ title: t(`pano.err.${code}.title`), message: t(`pano.err.${code}.msg`) },
]),
);
return (
<Camera
guidanceCopy={guidanceCopy}
onError={(err) => {
const friendly = userFacingStitchError(err.code, errorCopy);
if (friendly) Alert.alert(friendly.title, friendly.message);
else reportGenericError(err);
}}
/>
);
}
Seeding your translation catalogue
DEFAULT_GUIDANCE_COPY and DEFAULT_CAPTURE_WARNING_COPY are exported so you
can seed your translation catalogue from the source strings, and
RECOVERABLE_STITCH_GUIDANCE exposes the built-in error copy for the same
reason. Every one of these is the single source of truth the SDK falls back to,
so diffing against them on upgrade tells you exactly which strings changed.
Worked example: Spanish, with a dynamic language setting
You don't have to use a JSON file — <Camera guidanceCopy> takes a plain
Partial<GuidanceCopy> object, so the strings can come from anywhere (i18next,
FormatJS, an inline object). A JSON file per language is the simplest
hand-authored option, so that's what this example uses. English is the SDK's
built-in default, so you only ship files for the other languages.
1. The file you bring — i18n/es.json
Two sections: guidance (the on-screen GuidanceCopy keys from the table above)
and errors (the host-rendered alert copy, keyed by RECOVERABLE_STITCH_CODES).
The strings here are illustrative — have a translator refine them.
{
"guidance": {
"rotateToLandscape": "Gira el teléfono a horizontal",
"rotateToPortrait": "Gira el teléfono a vertical",
"panHint": "Desplázate despacio de arriba a abajo",
"tooFast": "Demasiado rápido: ve más despacio",
"lateralStopTitle": "Mantén el barrido recto",
"lateralStopBody": "Te moviste hacia un lado. Desplázate en una sola dirección: hemos unido lo que capturaste.",
"lateralStopDismiss": "Entendido",
"lateralWrongDirectionTitle": "Sigue la flecha",
"lateralWrongDirectionBody": "Moviste el teléfono en la dirección equivocada. Desplázate despacio en la dirección que indica la flecha, en línea recta.",
"cropConfirm": "Recortar",
"cropReset": "Restablecer",
"cropUseOriginal": "Usar original",
"cropRetake": "Repetir",
"previewConfirm": "Confirmar",
"statusRecording": "Mantén firme: desplázate despacio",
"statusStitching": "Uniendo panorama…",
"warnLowFrameUtilization": "Solo se pudieron usar {included} de {requested} fotogramas ({percent}%): el panorama puede quedar incompleto. Desplázate más despacio y de forma constante la próxima vez.",
"warnLateralDriftFinalize": "La captura se detuvo antes de tiempo porque el teléfono se desvió hacia un lado: solo se unió la parte capturada antes de la desviación.",
"warnHighPanSpeed": "La captura se realizó más rápido que el ritmo recomendado: el resultado puede no ser óptimo. Desplázate más despacio la próxima vez."
},
"errors": {
"STITCH_NEED_MORE_IMGS": {
"title": "Desplázate más despacio",
"message": "No hubo suficiente superposición entre los fotogramas para unirlos: cada fotograma debe superponerse con el anterior."
},
"STITCH_CAMERA_PARAMS_FAIL": {
"title": "Desplázate más despacio",
"message": "La vista cambió demasiado entre fotogramas para alinearlos: normalmente porque el teléfono se desplazó por el espacio en lugar de solo girar. La lente ultra gran angular (0.5x) es especialmente sensible a esto; prueba con 1x para escenas amplias."
},
"STITCH_HOMOGRAPHY_FAIL": {
"title": "Desplázate más despacio",
"message": "No se pudieron alinear los fotogramas: mantén el teléfono nivelado y estable para que cada fotograma se superponga con el anterior."
},
"STITCH_LOW_QUALITY": {
"title": "No salió bien",
"message": "El panorama no se unió en una sola imagen limpia. Inténtalo de nuevo, desplazándote despacio y de forma constante en una dirección para que cada fotograma se superponga con el anterior."
},
"STITCH_OOM": {
"title": "Prueba un barrido más corto",
"message": "Este panorama necesita más memoria de la que el dispositivo puede dedicar para terminar: un barrido más corto y estrecho (o 1x para escenas amplias) sí cabrá."
}
}
}
Importing a .json file needs "resolveJsonModule": true in your
tsconfig.json. Keys you omit fall back to the bundled English — partial files
are fine.
2. Register your catalogues
// i18n/catalogues.ts
import es from './es.json';
// English needs no file — it's the SDK's bundled default. Map it to
// `undefined` so the built-in English shows through unchanged.
export const CATALOGUES = { en: undefined, es } as const;
export type Lang = keyof typeof CATALOGUES;
3. Use it — and switch languages at runtime
<Camera> re-resolves guidanceCopy reactively (it's a useMemo on the prop),
so selecting a different catalogue updates every on-screen string with no
remount. The error alert is rendered by you in onError, so it picks up the
current language at the moment an error fires. Both surfaces therefore follow
your app's language setting live:
import { Camera, userFacingStitchError } from 'react-native-image-stitcher';
import { Alert } from 'react-native';
import { CATALOGUES, type Lang } from './i18n/catalogues';
// `language` comes from your app's settings (context / redux / MMKV / …);
// when the user changes it, this component re-renders with the new value.
function CaptureScreen({ language }: { language: Lang }) {
const catalogue = CATALOGUES[language]; // swapping language → new object
return (
<Camera
// On-screen copy. A new `guidanceCopy` reference re-resolves the
// merge and re-renders in the new language — no remount needed.
guidanceCopy={catalogue?.guidance}
onError={(err) => {
// Resolved at call time, so it's always in the current language.
const friendly = userFacingStitchError(err.code, catalogue?.errors);
if (friendly) {
Alert.alert(friendly.title, friendly.message);
} else {
// Codes with no single recoverable action return null —
// fall back to your app's generic error UI.
showGenericError(err);
}
}}
/>
);
}
The reactive update keys off the guidanceCopy reference
(useMemo([guidanceCopy])). Selecting CATALOGUES[language] hands <Camera> a
new object whenever the language changes — that's what triggers the re-render.
Don't mutate one shared object in place; swap the whole catalogue.