PARQORE Native Translate#
Overview#
On-device text translation for NativePHP Mobile — no cloud API, no developer-supplied credentials. Translate one string or many, with optional automatic source-language detection, on-device model management, and honest platform-difference reporting throughout.
- iOS: Apple's
Translationframework (TranslationSession,LanguageAvailability) +NaturalLanguage(NLLanguageRecognizer) for detection - Android: Google ML Kit Translate (
Translator,RemoteModelManager) + ML Kit Language ID for detection
This is not an app-localization / locale-preference package. It doesn't
touch your app's UI language or Locale. It translates one string of
user-generated text into another language, on request. No translation
history is stored anywhere by this plugin.
What this deliberately does NOT claim#
- No fixed language count. Apple's and Google's supported-language
lists change by OS version, region, and installed system capabilities.
supportedLanguages()andqueryModel()check at runtime; nothing here hardcodes a number. (For the record, at verification time: iOS reported 21 tags, Android reported 59 — a real, confirmed platform difference, not something to paper over.) - No Simulator support. Apple's Translation framework does not run in the iOS Simulator — device only, like several of this vendor's other plugins.
- No claimed platform parity. iOS has no app-facing model-deletion API
(Apple only allows deletion through Settings/the Translate app) —
deleteModel()returnssupported: falsethere, never a fake success. Android's ML KitTaskAPI has no public cancel —cancel()reportssupported: trueon iOS (a real, documented API) and does its honest best on Android (interrupting the tracked job's thread). On both platforms, cancelling a model download stops the plugin waiting and reportsCANCELLED; the system may still finish downloading the model in the background — see "Whatcancel()does". - No offline-before-install claim. Translation is offline only once a
language pair's model is actually installed. The first use of any pair
needs a network connection to download it; with no network, a needed
download fails straight away with
NETWORK_UNAVAILABLE— see "Failure codes". - No numeric download progress. Neither platform's API reports a percentage. Events are lifecycle states only: Started, Completed, Failed.
- No silent auto-download.
translate()andtranslateBatch()never download a model just because they were called — they fail closed with an actionable "model required" result unless you explicitly passautoDownload: true. - Never assumes English. Omitting
sourceLanguagetriggers on-device detection, not an English default. If detection isn't confident, you getLANGUAGE_UNDETERMINED(or, fromdetectLanguage()directly,undetermined: truewith no language code) — never a guess presented as fact.
Installation#
The repository is private. Add it to your app's composer.json as a VCS
repository:
"repositories": [ { "type": "vcs", "url": "https://github.com/prosenthal828/nativephp-native-translate" }]
composer require partek/native-translatephp artisan native:plugin:register partek/native-translate
Rebuild the native projects afterwards (php artisan native:run android or
ios). There are no permissions to declare on either platform.
Usage#
PHP#
use PARTek\NativeTranslate\Facades\NativeTranslate; // Translate an already-installed pair. Fails closed (TranslationFailed,// code MODEL_REQUIRED) if the pair isn't installed and autoDownload isn't set.$jobId = NativeTranslate::translate('Hello there', targetLanguage: 'es', sourceLanguage: 'en'); // Omit sourceLanguage to auto-detect it first.$jobId = NativeTranslate::translate('Hello there', targetLanguage: 'es'); // Or explicitly allow a download as part of the call:$jobId = NativeTranslate::translate('Hello there', 'es', 'en', autoDownload: true); // Multiple strings, one pair, one job. sourceLanguage IS required here --// batch does not auto-detect per-item.$jobId = NativeTranslate::translateBatch(['Good morning.', 'Thank you.'], sourceLanguage: 'en', targetLanguage: 'es'); // Download a pair ahead of time instead.$jobId = NativeTranslate::prepareModel(sourceLanguage: 'en', targetLanguage: 'es'); // Synchronous status check.$status = NativeTranslate::queryModel('en', 'es');$status->status->isInstalled(); // bool$status->status->requiresDownload(); // bool$status->status->isUnsupportedLanguagePair(); // bool // Every language tag the platform could support -- a static capability// list, not a per-pair readiness check.$languages = NativeTranslate::supportedLanguages();$languages->supports('es'); // bool // Detect a string's language. Treat as uncertain -- see $result->undetermined.$result = NativeTranslate::detectLanguage('Bonjour');$result->languageCode; // ?string, e.g. 'fr'$result->confidence; // ?float$result->undetermined; // bool // Delete a downloaded pair's models. Real on Android; iOS always reports// supported: false (no app-facing deletion API exists there).$result = NativeTranslate::deleteModel('en', 'es');$result->supported; // bool$result->deleted; // bool // Cancel an in-flight job by the jobId translate()/translateBatch()/// prepareModel() returned.$result = NativeTranslate::cancel($jobId);$result->supported; // bool -- real on both platforms, honestly reported$result->cancelled; // bool -- whether a matching in-flight job was found
translate(), translateBatch(), and prepareModel() are asynchronous —
they return a jobId string immediately, not a result. Listen for the
real result as a Laravel event on your NativeComponent:
use Native\Mobile\Attributes\On;use PARTek\NativeTranslate\Events\TranslationCompleted;use PARTek\NativeTranslate\Events\TranslationFailed;use PARTek\NativeTranslate\Events\BatchTranslationCompleted;use PARTek\NativeTranslate\Events\BatchTranslationFailed; #[On(TranslationCompleted::class)]public function translated(string $jobId, string $translatedText, string $sourceLanguage, string $targetLanguage): void{ // ...} #[On(TranslationFailed::class)]public function translationFailed(string $jobId, string $code, string $message): void{ // code is e.g. 'MODEL_REQUIRED', 'NETWORK_UNAVAILABLE', // 'MODEL_DOWNLOAD_FAILED', 'LANGUAGE_UNDETERMINED', 'CANCELLED' -- // see "Failure codes"} #[On(BatchTranslationCompleted::class)]public function batchTranslated(string $jobId, array $translatedTexts, string $sourceLanguage, string $targetLanguage): void{ // $translatedTexts is in the same order as the texts you passed in}
queryModel(), supportedLanguages(), detectLanguage(), deleteModel(),
and cancel() are all synchronous and return a typed result directly — no
event.
JavaScript#
import { translate, translateBatch, prepareModel, queryModel, supportedLanguages, detectLanguage, deleteModel, cancel,} from 'partek-native-translate'; const { jobId } = await translate({ text: 'Hello there', targetLanguage: 'es', sourceLanguage: 'en' });const { jobId } = await translate({ text: 'Hello there', targetLanguage: 'es' }); // auto-detectconst { jobId } = await translateBatch({ texts: ['Good morning.'], sourceLanguage: 'en', targetLanguage: 'es' });const { jobId } = await prepareModel({ sourceLanguage: 'en', targetLanguage: 'es' });const { status } = await queryModel({ sourceLanguage: 'en', targetLanguage: 'es' });const { languages } = await supportedLanguages();const { languageCode, confidence, undetermined } = await detectLanguage({ text: 'Bonjour' });const { supported, deleted } = await deleteModel({ sourceLanguage: 'en', targetLanguage: 'es' });const { supported, cancelled } = await cancel({ jobId });
Add a Vite alias the same way partek/native-share documents (NativePHP
doesn't alias plugin JavaScript automatically):
resolve: { alias: { 'partek-native-translate': fileURLToPath( new URL('./vendor/partek/native-translate/resources/js/index.js', import.meta.url) ), },}
Events#
| Event | Payload | Fired by |
|---|---|---|
ModelDownloadStarted |
jobId, languageTag |
prepareModel() |
ModelDownloadCompleted |
jobId, languageTag |
prepareModel() |
ModelDownloadFailed |
jobId, languageTag, code, message |
prepareModel() |
TranslationCompleted |
jobId, translatedText, sourceLanguage, targetLanguage |
translate() |
TranslationFailed |
jobId, code, message |
translate() |
BatchTranslationCompleted |
jobId, translatedTexts, sourceLanguage, targetLanguage |
translateBatch() |
BatchTranslationFailed |
jobId, code, message |
translateBatch() |
All payload keys are camelCase, matching Laravel event listener parameter
names exactly (#[On(...)] binds by literal name — no case conversion).
Failure codes#
TranslationFailed, BatchTranslationFailed and ModelDownloadFailed
carry a code to branch on and a message for logs (branch on the code;
the message wording may change).
| Code | Sent with | Platform | When |
|---|---|---|---|
MODEL_REQUIRED |
TranslationFailed, BatchTranslationFailed |
both | The pair isn't installed and autoDownload wasn't set. On iOS, any failed first attempt without autoDownload reports this. |
NETWORK_UNAVAILABLE |
all three | both | A model download is needed (prepareModel(), or autoDownload: true on a pair that isn't installed) and the device has no usable network. Sent straight away rather than waiting on a download that can't start. Android checks for an active network with validated internet; iOS checks NWPathMonitor. |
MODEL_DOWNLOAD_FAILED |
all three | both | A needed download started but failed or didn't finish in time — for example, the connection dropped mid-download. Android: any download error, or 120 s passing per language model. iOS: translate() / translateBatch() report every download failure except NETWORK_UNAVAILABLE this way (including the 45 s timeout); prepareModel() uses it for errors without a more specific code below. |
CANCELLED |
all three | both | cancel() stopped the job. It doesn't mean the model is absent — see "What cancel() does". |
LANGUAGE_UNDETERMINED |
TranslationFailed |
both | sourceLanguage was omitted and detection wasn't confident. |
UNSUPPORTED_LANGUAGE_PAIR |
all three | Android | ML Kit has no model for one of the tags. On iOS, queryModel() reports unsupportedLanguagePair instead. |
TRANSLATION_FAILED |
TranslationFailed, BatchTranslationFailed |
Android | Any other ML Kit error. |
NO_PRESENTER |
ModelDownloadFailed |
iOS | No on-screen view controller to host the download (for example, the app isn't in the foreground). |
PREPARE_TIMED_OUT |
ModelDownloadFailed |
iOS | prepareTranslation() didn't respond within 45 s. |
VERIFICATION_TIMED_OUT |
ModelDownloadFailed |
iOS | The download finished but the system didn't report the pair installed within about 6 s. |
Offline downloads#
With no network, prepareModel() — and translate() / translateBatch()
with autoDownload: true on a pair that isn't installed — fail at once
with NETWORK_UNAVAILABLE. An installed pair still translates offline,
and prepareModel() on an installed pair still succeeds offline.
The check runs once, before the download starts. If the connection drops
after that, the plugin doesn't notice early: the job ends with
MODEL_DOWNLOAD_FAILED (Android, after 120 s per model) or
MODEL_DOWNLOAD_FAILED / PREPARE_TIMED_OUT (iOS, after 45 s). Android's
ML Kit waits for a network rather than failing, and may still finish that
download later on its own.
What cancel() does#
cancel() stops the plugin waiting and reports CANCELLED. Neither
platform lets an app abort a model download that has already started:
ML Kit's RemoteModelManager has no cancel, and on iOS prepareModel()
can only stop waiting for the system's download. So the system may still
finish downloading the model in the background, and a later queryModel()
can report installed. Use queryModel() for the real state rather than
treating CANCELLED as "not installed". On iOS 26+, cancelling
translate() / translateBatch() also calls TranslationSession.cancel().
Seen on the Galaxy A54 (Android 16): a prepareModel('en', 'es') cancelled
mid-download returned cancelled: true and fired ModelDownloadFailed /
CANCELLED, and queryModel() reported installed 27 s later. en/es had
taken about 5 s to download earlier, so that download may have finished
before the cancel landed; either way, ML Kit can't stop one.
The four status concepts#
queryModel() returns a QueryModelResult whose status is a ModelStatus
enum, not raw booleans, specifically to keep these four ideas from drifting
apart:
- supported by platform —
$result->status->isSupportedByPlatform() - model installed —
$result->status->isInstalled() - model requires download —
$result->status->requiresDownload() - unsupported language pair —
$result->status->isUnsupportedLanguagePair()
Methods#
translate(string $text, string $targetLanguage, ?string $sourceLanguage = null, bool $autoDownload = false): string#
Translate one string. Returns $jobId. Fires TranslationCompleted / TranslationFailed.
| Parameter | Type | Default | Description |
|---|---|---|---|
$text |
string | required | Text to translate |
$targetLanguage |
string | required | BCP-47 language tag, e.g. 'es' |
$sourceLanguage |
?string | null |
Omit to auto-detect first. TranslationFailed carries code LANGUAGE_UNDETERMINED if detection isn't confident |
$autoDownload |
bool | false |
If false and the pair isn't installed, fails closed with code MODEL_REQUIRED instead of downloading silently. If true and the download can't happen, fails with NETWORK_UNAVAILABLE or MODEL_DOWNLOAD_FAILED (see "Failure codes") |
JS: translate({ text, targetLanguage, sourceLanguage?, autoDownload? }), resolves to { jobId }.
translateBatch(array $texts, string $sourceLanguage, string $targetLanguage, bool $autoDownload = false): string#
Translate multiple strings between one explicit pair in a single job. Returns $jobId. Fires BatchTranslationCompleted (translatedTexts in the same order as $texts) / BatchTranslationFailed.
| Parameter | Type | Default | Description |
|---|---|---|---|
$texts |
array<int, string> | required | Strings to translate |
$sourceLanguage |
string | required | No auto-detect here — each string could genuinely be a different language |
$targetLanguage |
string | required | BCP-47 language tag |
$autoDownload |
bool | false |
Same fail-closed behavior as translate() |
JS: translateBatch({ texts, sourceLanguage, targetLanguage, autoDownload? }), resolves to { jobId }.
prepareModel(string $sourceLanguage, string $targetLanguage): string#
Download the on-device model for a language pair, if not already installed. Returns $jobId. Fires ModelDownloadStarted, then ModelDownloadCompleted or ModelDownloadFailed. No numeric progress — neither platform's API reports one.
JS: prepareModel({ sourceLanguage, targetLanguage }), resolves to { jobId }.
queryModel(string $sourceLanguage, string $targetLanguage): QueryModelResult#
Synchronous status check for a language pair — see "The four status concepts" above.
JS: queryModel({ sourceLanguage, targetLanguage }).
supportedLanguages(): SupportedLanguagesResult#
Synchronous. Every language tag the platform's Translate API could support — a static capability list, not a per-pair readiness check (use queryModel() for that). Never hardcode a count from this.
JS: supportedLanguages().
detectLanguage(string $text): DetectedLanguageResult#
Synchronous. Detects $text's language. Treat as uncertain: $languageCode is null and $undetermined is true when the platform isn't confident — never assume English.
JS: detectLanguage({ text }).
deleteModel(string $sourceLanguage, string $targetLanguage): ModelDeletionResult#
Synchronous. Deletes a downloaded pair's models. Real on Android; iOS always reports $supported: false (no app-facing deletion API exists there).
JS: deleteModel({ sourceLanguage, targetLanguage }).
cancel(string $jobId): CancellationResult#
Synchronous. Cancels an in-flight translate()/translateBatch()/prepareModel() job by the $jobId those methods returned. Real cancellation of a translation on iOS 26+ (TranslationSession.cancel()); Android interrupts the tracked job's thread as the closest honest equivalent (ML Kit's own Task API has no public cancel). Check the result's $supported/$cancelled to know which happened. Cancel stops waiting and reports CANCELLED; the system may still finish downloading the model in the background, so check queryModel() afterwards — see "What cancel() does".
JS: cancel({ jobId }).
Platform notes#
iOS#
TranslateusesTranslationSession(installedSource:target:)— the plain, non-UI initializer — when a source language is given or detected. OmittingsourceLanguagerunsNLLanguageRecognizer.dominantLanguage(for:)first (NaturalLanguageframework, notTranslation— the latter has no standalone detection API outside a SwiftUI flow).PrepareModel(andTranslate/TranslateBatch'sautoDownloadfallback) download via a transparent, non-interactiveUIHostingControllerbriefly presented on the topmost view controller, hosting a SwiftUI view whose.translationTaskcallssession.prepareTranslation()— the plain initializer cannot trigger a first-time download on its own. SeeNativeTranslatePrepareView.swiftanddownloadAndVerifyPairinNativeTranslateFunctions.swift.QueryModelandSupportedLanguagesbridgeLanguageAvailability's async API with a semaphore, since neither needs a mounted view.DetectLanguageusesNLLanguageRecognizer.languageHypothesesfor a confidence score alongside the detected language.Cancelcalls the realTranslationSession.cancel()for an in-flightTranslate/TranslateBatchjob. For aPrepareModeljob (no directly held session — seedownloadAndVerifyPair), it resumes a tracked continuation viawithTaskCancellationHandler, backed by a 45s hard timeout in case the underlyingprepareTranslation()call itself never resolves (see "Verification status" — this was a real, confirmed bug on real hardware, not a defensive guess).DeleteModelalways reportssupported: false— no such API exists on iOS.- Before presenting the download host, the download path checks
NWPathMonitorwhenLanguageAvailabilitysays the pair still needs a download, and fails withNETWORK_UNAVAILABLEif there's no network path.Translate/TranslateBatchpass that code through; other download failures there still arrive asMODEL_DOWNLOAD_FAILED. - Min iOS 18.0 (the Translation framework's
.translationTasksession). Device only, not the Simulator. On iOS 26+,Translate/TranslateBatchuseTranslationSession(installedSource:target:)directly andCancelcallssession.cancel(). On iOS 18.0-25.x, where that initializer andcancel()don't exist, they run inside the same hidden SwiftUI host asPrepareModel(.translationTask), fail withMODEL_REQUIREDwhen the pair isn't installed, andCancelends the job without a session cancel. Both paths are behind#availablechecks, so the plugin compiles at NativePHP's iOS 18.2 deployment target. Device-verified on iOS 26 only: we have no iOS 18-25 device, so the fallback is type-checked, not device-tested.
Android#
- All functions use ML Kit Translate (
Translator,RemoteModelManager) and ML Kit Language ID (LanguageIdentification).Translate,TranslateBatch, andPrepareModelrun on a backgroundThread, matchingpartek/media-pipeline's convention, and report results via events rather than blocking the bridge call.QueryModel,SupportedLanguages,DetectLanguage,DeleteModel, andCancelare synchronous. Cancelinterrupts the tracked job'sThread(NativeTranslateJobs.kt) — ML Kit's ownTaskAPI has no public cancel method, so this is the closest honest equivalent. It ends the job withCANCELLED, butRemoteModelManagercan't abort a download it has started, so the model may still finish installing afterwards.- Before a needed model download,
Translate/TranslateBatch(withautoDownload) andPrepareModelcheck for an active network with validated internet and fail withNETWORK_UNAVAILABLEif there isn't one. ML Kit's download otherwise waits for a network instead of failing. This usesACCESS_NETWORK_STATE, which ML Kit Translate's own manifest already declares; the plugin adds no permission. DeleteModelusesRemoteModelManager.deleteDownloadedModel()— real, and confirmed working on real hardware.- Min SDK 23 (Android 6.0).
Verification status#
Confirmed working end-to-end on real hardware, both platforms — Galaxy
A54 (Android) and an iPad running iOS 26.6.2 ("Onyx"). Every bridge
function, the full async event round-trip (native dispatch → Laravel
#[On(...)] listener, correct jobId correlation), and real translated
output were verified via native:tail / a pulled laravel.log, not
assumed from source review.
Five real bugs were found and fixed during this verification, not before it. Worth reading in full before touching this plugin's native code again:
- iOS:
prepareTranslation()completing does not mean the model is immediately usable. Atranslate()call fired right afterModelDownloadCompletedfailed repeatedly withMODEL_REQUIRED/ "Unable to Translate," andQueryModelkept reportingrequiresDownloadin the same window — an eventual-consistency gap betweenprepareTranslation()finishing and the system'sLanguageAvailabilityregistry catching up (matches forum reports of "Offline models not available" despite.installedstatus). Fixed by pollingLanguageAvailabilityfor up to ~6s before reportingModelDownloadCompleted. - iOS:
Translatesilently ignoredautoDownloadentirely. The parameter was accepted but never read. Fixed: it now falls through to the verified download path before retrying the translation once. TranslationSession.init(installedSource:target:)needs iOS 26.0, not the 18.0 an initial documentation read suggested (since 1.0.1 it is behind#available, with an iOS 18-25 fallback; see Platform notes), andBridgeResponse.success()needed a labeleddata:argument on iOS — both caught by a real Xcode build, not assumed from docs.- Android:
texts(theTranslateBatcharray parameter) arrived as anorg.json.JSONArray, not a KotlinList<*>.parameters["texts"] as? List<*>silently returned null, which read as "texts is required" even though a real array was sent — this is the plugin family's first array-typed bridge parameter, so there was no existing precedent to check against. Fixed with a helper (NativeTranslateSupport.stringList) that handles both shapes. - iOS: cancelling a
PrepareModeljob for a language pair genuinely never downloaded before could hang forever, with no event ever firing — not even a failure. Two compounding causes, both found on real hardware: (a) plainTask.cancel()doesn't interrupt awithCheckedThrowingContinuationwaiting on a UIKit callback, and a naivewithTaskCancellationHandlerfix still raced —onCancelcan fire before the continuation is even set (the job starts out already cancelled), silently dropping the resume signal into nothing. Fixed with aSingleResumeguard that stashes a pending result and applies it retroactively once the continuation is set. (b) Even with that fixed,prepareTranslation()itself was observed to never resolve at all for a truly first-time pair, cancelled or not — plausibly blocked on a system-level download-permission prompt this plugin's invisible, non-interactive hosting view has no way to reach (unconfirmed without inspecting the device screen at the exact moment, but consistent with every symptom observed). Fixed with a 45s hard timeout — a detachedTasksharing the sameSingleResumeguard as the cancellation handler — racing the continuation, so the job always reaches a terminal event either way. Directly confirmed, not just reasoned about: a genuinely freshde → ittranslateBatch(autoDownload: true)call fired and resolved exactly 45s later withBatchTranslationFailed/MODEL_DOWNLOAD_FAILED— proving the timeout itself actually fires on real hardware. (An earlier version of this fix raced the timeout viawithThrowingTaskGroupinstead; that compiled and looked correct but showed unexplained extra latency in practice — replaced with the simpler detached-timer approach after that direct observation, not before it.) This hang class is iOS-specific. The same freshde → ittranslateBatch(autoDownload: true)scenario was run against a real, never-downloaded pair on Android (Galaxy A54) and did not hang or need a timeout at all — ML Kit'sRemoteModelManagerauto-download runs synchronously inline with nothing to get stuck behind a system dialog for. The genuinely cold call resolvedBatchTranslationCompletedin ~9s (a real network download, confirmed by three immediate repeat calls against the now-cached model resolving in ~1s each instead). A real, confirmed platform asymmetry, not an assumption.
Retested clean after all fixes, including a genuinely fresh (never
downloaded before) fr → de pair on Android and iOS, and a fresh
de → it pair for translateBatch's autoDownload path on both platforms:
Cancel ends an in-flight download job with an event on Android (confirmed
via ModelDownloadStarted → ModelDownloadFailed/CANCELLED, not just a
returned flag), and the same on iOS after the fixes above. That shows the
job stopped waiting, not that the download stopped: see "What cancel()
does" for a later A54 run where the model was installed afterwards. The happy path
(translate/batch/prepare/query/detect/supported-languages on an
already-installed pair) was re-verified after every fix with no
regressions.
Testing#
Test helpers#
On a NativePHP core with the macroable test harness (nativephp/mobile
4.0.0 or later), this plugin adds translation helpers to
Native::fakeBridge() and Native::test(), so your app's tests can drive a
screen that translates without a device:
use App\NativeComponents\TranslateScreen;use Native\Mobile\Testing\Native; Native::fakeBridge()->withNativeTranslate(); Native::test(TranslateScreen::class) ->tap('Translate') ->assertTranslationRequested('Good morning', 'es') ->completeTranslation('Buenos días') ->assertSee('Buenos días');
| Helper | Registered on | What it does |
|---|---|---|
withNativeTranslate(array $languages = ['en', 'es']) |
FakeBridge |
Answers the bridge as a working device does: supportedLanguages() lists $languages, queryModel() reports every pair installed, translate(), translateBatch() and prepareModel() are accepted, and cancel() and deleteModel() succeed (Android's answer; iOS can't delete models). |
withTranslationModel(string $languageTag, ModelStatus|string $status = ModelStatus::Installed) |
FakeBridge |
Sets queryModel()'s status for pairs involving $languageTag: 'es' matches either side of a pair, 'en->es' matches that pair only. The latest matching call wins. |
withDetectedLanguage(string $languageTag, float $confidence = 0.9) |
FakeBridge |
Makes detectLanguage() return $languageTag with $confidence. Without it, detection is undetermined. |
assertTranslationRequested(?string $text = null, ?string $target = null) |
FakeBridge |
Asserts translate() reached the bridge: any call, or exactly $text and/or $target. |
assertBatchTranslationRequested(?array $texts = null) |
FakeBridge |
Asserts translateBatch() reached the bridge: any call, or exactly $texts in order. |
assertTranslationModelPrepared(?string $languageTag = null) |
FakeBridge |
Asserts prepareModel() reached the bridge: any pair, a pair involving 'es', or exactly 'en->es'. |
completeTranslation(string $translatedText, string $sourceLanguage = 'en', string $targetLanguage = 'es', ?string $jobId = null) |
TestableComponent |
Delivers TranslationCompleted. $jobId defaults to the last translate() call's. |
completeBatchTranslation(array $translatedTexts, string $sourceLanguage = 'en', string $targetLanguage = 'es', ?string $jobId = null) |
TestableComponent |
Delivers BatchTranslationCompleted. $jobId defaults to the last translateBatch() call's. |
failTranslation(string $code = 'TRANSLATION_FAILED', string $message = '', ?string $jobId = null) |
TestableComponent |
Delivers TranslationFailed, with the same default. Pass a native code such as MODEL_REQUIRED or LANGUAGE_UNDETERMINED. |
failBatchTranslation(string $code = 'TRANSLATION_FAILED', string $message = '', ?string $jobId = null) |
TestableComponent |
Delivers BatchTranslationFailed, with the translateBatch() default. |
downloadTranslationModel(string $languageTag, ?string $jobId = null) |
TestableComponent |
Delivers ModelDownloadStarted, then ModelDownloadCompleted, for the last prepareModel() call matching $languageTag. |
failTranslationModelDownload(string $languageTag, string $code = 'MODEL_DOWNLOAD_FAILED', string $message = '', ?string $jobId = null) |
TestableComponent |
Delivers ModelDownloadFailed for the same call. |
FakeBridge helpers also chain off the harness:
Native::test(...)->assertTranslationRequested() returns the harness.
- Call
withNativeTranslate()beforeNative::test()when the screen queries the platform inmount(). Without it the fake bridge answersnull, soqueryModel()reportsunsupportedLanguagePairandsupportedLanguages()is empty. - The model download events carry the matched
prepareModel()call's pair tagsource->target(for exampleen->es), as both platforms send it, whichever form of$languageTagyou pass. With$jobIdand no matching call (for example underNativeTranslate::fake()),$languageTagis sent as given, so pass the pair tag. - The event helpers fail with a clear message when the screen never made
the matching call and you did not pass
$jobId. NativeTranslate::fake()(below) swaps out the facade, so calls never reach the bridge and theFakeBridgehelpers see nothing. The event helpers still work when you pass$jobId.- The helpers are registered only while the app runs unit tests
(
APP_ENV=testing). Production boots are untouched.
Facade fake#
Use NativeTranslate::fake() in application tests to record calls without
crossing the native bridge. Synchronous query methods return useful defaults
and can be configured for the scenario under test:
use PARTek\NativeTranslate\Data\ModelStatus;use PARTek\NativeTranslate\Facades\NativeTranslate; NativeTranslate::fake() ->withModelStatus(ModelStatus::RequiresDownload) ->withSupportedLanguages(['en', 'es']); NativeTranslate::translate('Hello', 'es', 'en'); NativeTranslate::assertTranslated( fn (array $parameters) => $parameters['text'] === 'Hello');
The fake also provides assertBatchTranslated(), assertModelPrepared(),
assertModelQueried(), assertLanguageDetected(), assertModelDeleted(),
assertCancelled(), and assertNothingCalled(). Configure synchronous
results with withDetectedLanguage(), withDeletionResult(), and
withCancellationResult().
This package's suite#
Run the plugin's own suite (contract-level only — see "Verification status" above for the real device-testing that actually proves behavior):
vendor/bin/phpunit
NativeTranslateMacrosTest boots the NativePHP core with Testbench and
drives the test helpers through the real facade and a small fixture screen.
Notes#
- No permissions on either platform.
- Register the plugin and rebuild, or the bridge functions never get compiled in.
- Not an app-localization/locale-preference package — see [developernauts' nativephp-mobile-locales] for that, if it exists; they solve a different problem.
License#
Proprietary commercial software. See LICENSE.