diff --git a/CHANGELOG.md b/CHANGELOG.md
index 158ff87..179df40 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,22 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
-### Fixed
+### Added
-- iOS: return UTF-16 offsets from `NSDataDetector` instead of grapheme-cluster
- distances, so `start`/`end` align with JavaScript string indices and Android's
- ML Kit char offsets. Previously mismatched on text containing emoji, accented,
- or non-BMP characters.
-- Android: read the library version from `package.json` instead of a hardcoded
- value, so `build.gradle` can no longer drift from the published version.
+- `useDataDetector` hook — tracks model readiness (`status`/`isReady`/`error`),
+ exposes `detect`, and auto-downloads the model on Android (opt out with
+ `autoPrepare: false`).
+- `getModelStatus()` and `isModelReady()` to query whether a language model is
+ downloaded. On iOS both always report ready.
+- Multi-language support on Android: `detect`, `prepareModel`, `getModelStatus`,
+ `isModelReady`, and `useDataDetector` all accept a `language` option (ISO 639-1
+ code) selecting one of 15 ML Kit models. Defaults to `'en'`. Ignored on iOS.
### Changed
+- Renamed `downloadModel()` to `prepareModel()` — clearer intent and now accepts a
+ `{ language }` option. `downloadModel()` remains as a deprecated alias (targets
+ the default `'en'` model) and will be removed in a future major version.
- Ship an explicit `files` allowlist in `package.json` and drop the stale
`.npmignore`; development config (ESLint, Prettier, EditorConfig, CONTRIBUTING)
is no longer included in the published tarball.
- README hero image now uses an absolute URL so it renders on npmjs.com.
+### Fixed
+
+- iOS: return UTF-16 offsets from `NSDataDetector` instead of grapheme-cluster
+ distances, so `start`/`end` align with JavaScript string indices and Android's
+ ML Kit char offsets. Previously mismatched on text containing emoji, accented,
+ or non-BMP characters.
+- Android: read the library version from `package.json` instead of a hardcoded
+ value, so `build.gradle` can no longer drift from the published version.
+
## [0.2.0]
- Initial public release: cross-platform text data detection using
diff --git a/README.md b/README.md
index ea1d613..217f342 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,8 @@ Cross-platform text data detection for React Native. Uses **NSDataDetector** on
- **Addresses** — Detect street addresses with parsed components (iOS)
- **Dates** — Detect dates and times with ISO 8601 output
- **Native accuracy** — Uses battle-tested platform APIs instead of regex
+- **React hook** — [`useDataDetector`](#usedatadetectoroptions) tracks model readiness and auto-downloads on Android
+- **Multiple languages** — Choose from 15 ML Kit language models on Android
- **Expo Modules API** — Built with the modern Expo native module system
## Installation
@@ -32,15 +34,17 @@ npx pod-install
### Android
-The ML Kit entity extraction model (~5.6MB) is downloaded on the user's device at runtime. You can control when this happens using [`downloadModel()`](#downloadmodel) — for example, calling it at app startup to ensure `detect()` works offline later. If you don't call it explicitly, the model will be downloaded automatically on the first `detect()` call.
+The ML Kit entity extraction model (~5.6MB per language) is downloaded on the user's device at runtime. You can control when this happens using [`prepareModel()`](#preparemodeloptions) or the [`useDataDetector`](#usedatadetectoroptions) hook (which downloads automatically on mount) — for example, to ensure `detect()` works offline later. If you don't trigger it explicitly, the model is downloaded automatically on the first `detect()` call.
## Usage
+### Functions
+
```typescript
-import { detect, downloadModel } from 'react-native-data-detector';
+import { detect, prepareModel } from 'react-native-data-detector';
// Pre-download the ML Kit model at app startup (Android only, no-op on iOS)
-await downloadModel();
+await prepareModel();
// Detect all entity types
const entities = await detect('Call me at 555-1234 or email john@example.com');
@@ -56,22 +60,94 @@ const phones = await detect('Call 555-1234 or visit https://example.com', {
// [
// { type: 'phoneNumber', text: '555-1234', start: 5, end: 13, data: { phoneNumber: '555-1234' } }
// ]
+
+// Use a specific language model (Android only, ignored on iOS)
+const fr = await detect('Appelez-moi au 01 23 45 67 89', { language: 'fr' });
+```
+
+### Hook
+
+The `useDataDetector` hook tracks model readiness and, on Android, downloads the
+model automatically on mount. On iOS the model is always ready.
+
+```tsx
+import { useDataDetector } from 'react-native-data-detector';
+
+function MyComponent() {
+ const { detect, status, isReady } = useDataDetector();
+
+ const onAnalyze = async (text: string) => {
+ const entities = await detect(text, { types: ['email', 'phoneNumber'] });
+ // …
+ };
+
+ // status: 'notDownloaded' | 'downloading' | 'ready' | 'error'
+ if (!isReady) return Preparing model… ({status});
+ // …
+}
```
## API
-### `downloadModel()`
+### `prepareModel(options?)`
-Pre-downloads the ML Kit entity extraction model on Android. On iOS, this is a no-op that resolves immediately — `NSDataDetector` is built into the OS and requires no model download.
+Pre-downloads the entity-detection model so `detect()` can run offline afterwards. On iOS, this is a no-op that resolves immediately — `NSDataDetector` is built into the OS and requires no model download.
Call this at app startup or before the first `detect()` call to ensure the model is available offline.
+**Parameters:**
+
+| Parameter | Type | Description |
+|-----------|------|-------------|
+| `options.language` | `ModelLanguage` | Which language model to prepare (Android only, default `'en'`). Ignored on iOS. |
+
**Returns:** `Promise` — `true` when the model is ready.
| Platform | Behavior |
|----------|----------|
| **iOS** | No-op, resolves `true` immediately |
-| **Android** | Downloads the ML Kit model (~5.6MB) if not already cached. Requires internet on first call. |
+| **Android** | Downloads the ML Kit model (~5.6MB) for the language if not already cached. Requires internet on first call. |
+
+### `getModelStatus(options?)`
+
+Returns the download status of the model for the given language.
+
+**Parameters:** `options.language` — `ModelLanguage` (Android only, default `'en'`).
+
+**Returns:** `Promise` — `'ready'` or `'notDownloaded'`. On iOS always resolves `'ready'`. (A pure query never returns `'downloading'` or `'error'` — those are only surfaced by the [`useDataDetector`](#usedatadetectoroptions) hook.)
+
+### `isModelReady(options?)`
+
+Convenience wrapper around `getModelStatus`. Resolves `true` when the model for the given language is available.
+
+**Parameters:** `options.language` — `ModelLanguage` (Android only, default `'en'`).
+
+**Returns:** `Promise`
+
+### `useDataDetector(options?)`
+
+React hook that tracks model availability and, on Android, downloads the language model automatically. On iOS the model is always available, so `status` settles on `'ready'`.
+
+**Parameters:**
+
+| Property | Type | Default | Description |
+|----------|------|---------|-------------|
+| `options.language` | `ModelLanguage` | `'en'` | Which language model to use (Android only). Changing it re-checks/prepares the new model. |
+| `options.autoPrepare` | `boolean` | `true` | Download the model on mount if not already available (Android). |
+
+**Returns:** `UseDataDetectorResult`
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `detect` | `(text, options?) => Promise` | Detect entities using the configured language. `options.types` selects entity types. |
+| `prepare` | `() => Promise` | Manually (re)download the configured language model. |
+| `status` | `ModelStatus` | `'notDownloaded' \| 'downloading' \| 'ready' \| 'error'`. |
+| `isReady` | `boolean` | `true` when `status === 'ready'`. |
+| `error` | `Error \| null` | The last preparation error, or `null`. |
+
+### `downloadModel()`
+
+> **Deprecated since 0.3.0** — use [`prepareModel()`](#preparemodeloptions) instead. Kept as an alias (always targets the default `'en'` model) and will be removed in a future major version.
### `detect(text, options?)`
@@ -89,6 +165,7 @@ Detects entities in the given text using native platform APIs.
| Property | Type | Default | Description |
|----------|------|---------|-------------|
| `types` | `DetectionType[]` | All types | Which entity types to detect |
+| `language` | `ModelLanguage` | `'en'` | Which language model to use (Android only). Ignored on iOS. |
**DetectionType:** `'phoneNumber' | 'link' | 'email' | 'address' | 'date'`
@@ -114,13 +191,22 @@ Detects entities in the given text using native platform APIs.
| `address` | `{ street, city, state, zip, country }` (iOS) / `{ address }` (Android) |
| `date` | `{ date }` ISO 8601 string |
+## Supported Languages
+
+The `language` option selects which **Android** ML Kit model is used. It is a no-op on iOS, where `NSDataDetector` is language-agnostic.
+
+`ModelLanguage`: `'ar'` (Arabic), `'nl'` (Dutch), `'en'` (English), `'fr'` (French), `'de'` (German), `'it'` (Italian), `'ja'` (Japanese), `'ko'` (Korean), `'pl'` (Polish), `'pt'` (Portuguese), `'ru'` (Russian), `'es'` (Spanish), `'th'` (Thai), `'tr'` (Turkish), `'zh'` (Chinese).
+
+Each language is a separate ~5.6MB on-device model, downloaded on demand.
+
## Platform Differences
| Feature | iOS | Android |
|---------|-----|---------|
| Engine | NSDataDetector | ML Kit Entity Extraction |
-| Offline | Always | After `downloadModel()` or first `detect()` call |
-| Model download | Not needed | ~5.6MB, on-device at runtime |
+| Offline | Always | After `prepareModel()` or first `detect()` call |
+| Model download | Not needed | ~5.6MB per language, on-device at runtime |
+| Language selection | Language-agnostic (option ignored) | 15 selectable language models |
| Address parsing | Structured components | Raw string |
| Date output | ISO 8601 | ISO 8601 |
diff --git a/android/src/main/java/expo/modules/datadetector/ReactNativeDataDetectorModule.kt b/android/src/main/java/expo/modules/datadetector/ReactNativeDataDetectorModule.kt
index 46cf34d..6bd2e98 100644
--- a/android/src/main/java/expo/modules/datadetector/ReactNativeDataDetectorModule.kt
+++ b/android/src/main/java/expo/modules/datadetector/ReactNativeDataDetectorModule.kt
@@ -1,8 +1,10 @@
package expo.modules.datadetector
+import com.google.mlkit.common.model.RemoteModelManager
import com.google.mlkit.nl.entityextraction.Entity
import com.google.mlkit.nl.entityextraction.EntityExtraction
import com.google.mlkit.nl.entityextraction.EntityExtractionParams
+import com.google.mlkit.nl.entityextraction.EntityExtractionRemoteModel
import com.google.mlkit.nl.entityextraction.EntityExtractorOptions
import expo.modules.kotlin.Promise
import expo.modules.kotlin.modules.Module
@@ -16,9 +18,8 @@ class ReactNativeDataDetectorModule : Module() {
override fun definition() = ModuleDefinition {
Name("ReactNativeDataDetector")
- AsyncFunction("downloadModel") { promise: Promise ->
- val options = EntityExtractorOptions.Builder(EntityExtractorOptions.ENGLISH)
- .build()
+ AsyncFunction("prepareModel") { language: String, promise: Promise ->
+ val options = EntityExtractorOptions.Builder(modelIdentifierFor(language)).build()
val extractor = EntityExtraction.getClient(options)
extractor.downloadModelIfNeeded()
@@ -32,9 +33,20 @@ class ReactNativeDataDetectorModule : Module() {
}
}
- AsyncFunction("detect") { text: String, types: List, promise: Promise ->
- val options = EntityExtractorOptions.Builder(EntityExtractorOptions.ENGLISH)
- .build()
+ AsyncFunction("getModelStatus") { language: String, promise: Promise ->
+ val model = EntityExtractionRemoteModel.Builder(modelIdentifierFor(language)).build()
+
+ RemoteModelManager.getInstance().isModelDownloaded(model)
+ .addOnSuccessListener { downloaded ->
+ promise.resolve(if (downloaded) "ready" else "notDownloaded")
+ }
+ .addOnFailureListener { e ->
+ promise.reject("MODEL_STATUS_ERROR", e.message ?: "Failed to read ML Kit model status", e)
+ }
+ }
+
+ AsyncFunction("detect") { text: String, types: List, language: String, promise: Promise ->
+ val options = EntityExtractorOptions.Builder(modelIdentifierFor(language)).build()
val extractor = EntityExtraction.getClient(options)
extractor.downloadModelIfNeeded()
@@ -81,6 +93,32 @@ class ReactNativeDataDetectorModule : Module() {
}
}
+ /**
+ * Maps an ISO 639-1 language code from JavaScript to its ML Kit
+ * [EntityExtractorOptions] model identifier. Falls back to English for
+ * unknown codes so detection still works.
+ */
+ private fun modelIdentifierFor(language: String): String {
+ return when (language) {
+ "ar" -> EntityExtractorOptions.ARABIC
+ "nl" -> EntityExtractorOptions.DUTCH
+ "en" -> EntityExtractorOptions.ENGLISH
+ "fr" -> EntityExtractorOptions.FRENCH
+ "de" -> EntityExtractorOptions.GERMAN
+ "it" -> EntityExtractorOptions.ITALIAN
+ "ja" -> EntityExtractorOptions.JAPANESE
+ "ko" -> EntityExtractorOptions.KOREAN
+ "pl" -> EntityExtractorOptions.POLISH
+ "pt" -> EntityExtractorOptions.PORTUGUESE
+ "ru" -> EntityExtractorOptions.RUSSIAN
+ "es" -> EntityExtractorOptions.SPANISH
+ "th" -> EntityExtractorOptions.THAI
+ "tr" -> EntityExtractorOptions.TURKISH
+ "zh" -> EntityExtractorOptions.CHINESE
+ else -> EntityExtractorOptions.ENGLISH
+ }
+ }
+
private fun mapEntityType(entity: Entity): String? {
return when (entity.type) {
Entity.TYPE_PHONE -> "phoneNumber"
diff --git a/example/App.tsx b/example/App.tsx
index c79bd97..d53c981 100644
--- a/example/App.tsx
+++ b/example/App.tsx
@@ -12,10 +12,10 @@ import {
View,
} from 'react-native';
import {
- detect,
- downloadModel,
+ useDataDetector,
type DetectedEntity,
type DetectionType,
+ type ModelLanguage,
} from 'react-native-data-detector';
const SAMPLE_TEXT =
@@ -41,12 +41,25 @@ const TYPE_LABELS: Record = {
date: 'Date',
};
+const LANGUAGES: ModelLanguage[] = ['en', 'fr', 'es', 'de', 'ja', 'zh'];
+
+const STATUS_LABELS: Record = {
+ notDownloaded: 'Model not downloaded',
+ downloading: 'Downloading model…',
+ ready: 'Model ready',
+ error: 'Model error',
+};
+
export default function App() {
const [text, setText] = useState(SAMPLE_TEXT);
const [selectedTypes, setSelectedTypes] = useState>(new Set(ALL_TYPES));
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
- const [modelStatus, setModelStatus] = useState(null);
+ const [detectError, setDetectError] = useState(null);
+ const [language, setLanguage] = useState('en');
+
+ // The hook tracks model readiness and auto-downloads on Android.
+ const { detect, status, isReady, prepare, error } = useDataDetector({ language });
const toggleType = (type: DetectionType) => {
setSelectedTypes((prev) => {
@@ -60,26 +73,17 @@ export default function App() {
});
};
- const handleDownloadModel = async () => {
- setModelStatus('Downloading…');
- try {
- const success = await downloadModel();
- setModelStatus(success ? 'Model ready' : 'No download needed');
- } catch (e: any) {
- setModelStatus(`Error: ${e.message}`);
- }
- };
-
const handleDetect = async () => {
if (!text.trim()) return;
setLoading(true);
+ setDetectError(null);
try {
const types = Array.from(selectedTypes);
const entities = await detect(text, types.length < ALL_TYPES.length ? { types } : undefined);
setResults(entities);
} catch (e: any) {
setResults([]);
- setModelStatus(`Detection error: ${e.message}`);
+ setDetectError(`Detection error: ${e.message}`);
} finally {
setLoading(false);
}
@@ -96,10 +100,34 @@ export default function App() {
{Platform.OS === 'android' && (
-
- Download Model (Android)
-
- {modelStatus && {modelStatus}}
+ Language Model
+
+ {LANGUAGES.map((lang) => {
+ const active = language === lang;
+ return (
+ setLanguage(lang)}
+ >
+
+ {lang.toUpperCase()}
+
+
+ );
+ })}
+
+
+ {status === 'downloading' && }
+
+ {error ? `Error: ${error.message}` : (STATUS_LABELS[status] ?? status)}
+
+
+ {status === 'error' && (
+ prepare().catch(() => {})}>
+ Retry Download
+
+ )}
)}
@@ -139,17 +167,21 @@ export default function App() {
{loading ? (
) : (
- Detect Entities
+
+ {isReady ? 'Detect Entities' : 'Preparing model…'}
+
)}
+ {detectError && {detectError}}
+
{results.length > 0 && (
@@ -273,12 +305,31 @@ const styles = StyleSheet.create({
fontSize: 15,
fontWeight: '600',
},
+ statusRow: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ gap: 8,
+ marginTop: 10,
+ },
statusText: {
fontSize: 13,
color: '#8E8E93',
- marginTop: 6,
textAlign: 'center',
},
+ errorText: {
+ fontSize: 13,
+ color: '#FF3B30',
+ textAlign: 'center',
+ marginBottom: 16,
+ },
+ langChip: {
+ borderColor: '#C7C7CC',
+ },
+ langChipActive: {
+ backgroundColor: '#007AFF',
+ borderColor: '#007AFF',
+ },
card: {
backgroundColor: '#fff',
borderRadius: 12,
diff --git a/ios/ReactNativeDataDetectorModule.swift b/ios/ReactNativeDataDetectorModule.swift
index 0ac691d..932ee13 100644
--- a/ios/ReactNativeDataDetectorModule.swift
+++ b/ios/ReactNativeDataDetectorModule.swift
@@ -5,12 +5,19 @@ public class ReactNativeDataDetectorModule: Module {
public func definition() -> ModuleDefinition {
Name("ReactNativeDataDetector")
- // No-op on iOS — NSDataDetector requires no model download
- AsyncFunction("downloadModel") { () -> Bool in
+ // No-op on iOS — NSDataDetector requires no model download.
+ // The `language` argument exists for API parity with Android and is ignored.
+ AsyncFunction("prepareModel") { (_: String) -> Bool in
return true
}
- AsyncFunction("detect") { (text: String, types: [String]) -> [[String: Any]] in
+ // NSDataDetector is always available, so the model is always ready.
+ AsyncFunction("getModelStatus") { (_: String) -> String in
+ return "ready"
+ }
+
+ // `language` is ignored — NSDataDetector is language-agnostic.
+ AsyncFunction("detect") { (text: String, types: [String], _: String) -> [[String: Any]] in
var checkingTypes: NSTextCheckingResult.CheckingType = []
for type in types {
diff --git a/package-lock.json b/package-lock.json
index a26dfa4..dd8922b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,8 +9,10 @@
"version": "0.2.0",
"license": "MIT",
"devDependencies": {
+ "@types/react": "^19.0.0",
"expo-module-scripts": "^4.0.0",
"expo-modules-core": "^2.0.0",
+ "react": "^19.0.0",
"typescript": "^5.3.0"
},
"peerDependencies": {
@@ -4931,6 +4933,16 @@
"undici-types": "~7.19.0"
}
},
+ "node_modules/@types/react": {
+ "version": "19.2.16",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.16.tgz",
+ "integrity": "sha512-esJiCAnl0kfpNdE69f3So4WJUXy95dLZydX0KwK46riIHDzHM7O9Vtf9xCHW0PXIqvgqNrswl522kA/5yx+F4w==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
"node_modules/@types/stack-utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
@@ -6595,6 +6607,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "devOptional": true,
+ "license": "MIT"
+ },
"node_modules/data-urls": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz",
@@ -13918,7 +13937,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
diff --git a/package.json b/package.json
index 2532069..e285d62 100644
--- a/package.json
+++ b/package.json
@@ -47,8 +47,10 @@
"expo-module": "expo-module"
},
"devDependencies": {
+ "@types/react": "^19.0.0",
"expo-module-scripts": "^4.0.0",
"expo-modules-core": "^2.0.0",
+ "react": "^19.0.0",
"typescript": "^5.3.0"
},
"peerDependencies": {
diff --git a/src/ReactNativeDataDetector.ts b/src/ReactNativeDataDetector.ts
index f1ebe36..bc2eb27 100644
--- a/src/ReactNativeDataDetector.ts
+++ b/src/ReactNativeDataDetector.ts
@@ -1,16 +1,51 @@
import { requireNativeModule } from 'expo-modules-core';
-import type { DetectedEntity, DetectOptions } from './ReactNativeDataDetector.types';
+import type {
+ DetectedEntity,
+ DetectOptions,
+ ModelLanguage,
+ ModelOptions,
+ ModelStatus,
+} from './ReactNativeDataDetector.types';
const NativeModule = requireNativeModule('ReactNativeDataDetector');
+const DEFAULT_LANGUAGE: ModelLanguage = 'en';
+const ALL_TYPES = ['phoneNumber', 'link', 'email', 'address', 'date'] as const;
+
/**
- * Pre-downloads the ML Kit entity extraction model on Android.
- * No-op on iOS (NSDataDetector requires no model download).
- * Call this at app startup to ensure `detect()` works offline later.
+ * Pre-downloads the entity-detection model for the given language so that
+ * `detect()` can run offline afterwards.
+ *
+ * On Android this downloads the ML Kit model for `language` (~5.6MB) if it is
+ * not already cached. On iOS this is a no-op that resolves immediately —
+ * `NSDataDetector` is built into the OS and requires no model.
+ *
+ * @returns `true` once the model is ready.
*/
-export async function downloadModel(): Promise {
- return NativeModule.downloadModel();
+export async function prepareModel(options?: ModelOptions): Promise {
+ return NativeModule.prepareModel(options?.language ?? DEFAULT_LANGUAGE);
+}
+
+/**
+ * Returns the download status of the model for the given language.
+ *
+ * On Android this reflects whether the ML Kit model is cached on the device
+ * (`'ready'` or `'notDownloaded'`). On iOS this always resolves to `'ready'`.
+ *
+ * A pure status query never returns `'downloading'` or `'error'` — those states
+ * are only surfaced by the {@link useDataDetector} hook while it drives a download.
+ */
+export async function getModelStatus(options?: ModelOptions): Promise {
+ return NativeModule.getModelStatus(options?.language ?? DEFAULT_LANGUAGE);
+}
+
+/**
+ * Convenience wrapper around {@link getModelStatus} that resolves `true` when the
+ * model for the given language is available and `detect()` can run offline.
+ */
+export async function isModelReady(options?: ModelOptions): Promise {
+ return (await getModelStatus(options)) === 'ready';
}
/**
@@ -18,6 +53,15 @@ export async function downloadModel(): Promise {
* using native platform APIs (NSDataDetector on iOS, ML Kit on Android).
*/
export async function detect(text: string, options?: DetectOptions): Promise {
- const types = options?.types ?? ['phoneNumber', 'link', 'email', 'address', 'date'];
- return NativeModule.detect(text, types);
+ const types = options?.types ?? [...ALL_TYPES];
+ return NativeModule.detect(text, types, options?.language ?? DEFAULT_LANGUAGE);
+}
+
+/**
+ * @deprecated Renamed to {@link prepareModel} in 0.3.0. This alias will be removed
+ * in a future major version. `prepareModel` accepts a `{ language }` option;
+ * `downloadModel` always targets the default (`'en'`) model.
+ */
+export async function downloadModel(): Promise {
+ return prepareModel();
}
diff --git a/src/ReactNativeDataDetector.types.ts b/src/ReactNativeDataDetector.types.ts
index ca5d724..869aea0 100644
--- a/src/ReactNativeDataDetector.types.ts
+++ b/src/ReactNativeDataDetector.types.ts
@@ -3,6 +3,53 @@
*/
export type DetectionType = 'phoneNumber' | 'link' | 'email' | 'address' | 'date';
+/**
+ * A language model supported by ML Kit Entity Extraction on Android, expressed as
+ * an ISO 639-1 code. Selects which on-device model is used for detection.
+ *
+ * Ignored on iOS — `NSDataDetector` is language-agnostic and needs no model.
+ */
+export type ModelLanguage =
+ | 'ar' // Arabic
+ | 'nl' // Dutch
+ | 'en' // English
+ | 'fr' // French
+ | 'de' // German
+ | 'it' // Italian
+ | 'ja' // Japanese
+ | 'ko' // Korean
+ | 'pl' // Polish
+ | 'pt' // Portuguese
+ | 'ru' // Russian
+ | 'es' // Spanish
+ | 'th' // Thai
+ | 'tr' // Turkish
+ | 'zh'; // Chinese
+
+/**
+ * The download state of a detection model.
+ *
+ * - `notDownloaded` — the model is not available on the device yet (Android only).
+ * - `downloading` — a download is currently in progress. Only reported by the
+ * {@link useDataDetector} hook while it drives a download; native status queries
+ * never return this.
+ * - `ready` — the model is available and `detect()` can run offline. iOS always
+ * reports `ready` since `NSDataDetector` requires no model.
+ * - `error` — the last preparation attempt failed. Only reported by the hook.
+ */
+export type ModelStatus = 'notDownloaded' | 'downloading' | 'ready' | 'error';
+
+/**
+ * Options that select which language model a model operation targets.
+ */
+export interface ModelOptions {
+ /**
+ * Which language model to target (Android only, defaults to `'en'`).
+ * Ignored on iOS.
+ */
+ language?: ModelLanguage;
+}
+
/**
* A single detected entity within the text.
*/
@@ -25,4 +72,9 @@ export interface DetectedEntity {
export interface DetectOptions {
/** Which entity types to detect. Defaults to all types. */
types?: DetectionType[];
+ /**
+ * Which language model to use (Android only, defaults to `'en'`).
+ * Ignored on iOS.
+ */
+ language?: ModelLanguage;
}
diff --git a/src/index.ts b/src/index.ts
index 59ae6cb..afe8ba1 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,2 +1,17 @@
-export { detect, downloadModel } from './ReactNativeDataDetector';
-export type { DetectedEntity, DetectionType, DetectOptions } from './ReactNativeDataDetector.types';
+export {
+ detect,
+ downloadModel,
+ getModelStatus,
+ isModelReady,
+ prepareModel,
+} from './ReactNativeDataDetector';
+export { useDataDetector } from './useDataDetector';
+export type { UseDataDetectorOptions, UseDataDetectorResult } from './useDataDetector';
+export type {
+ DetectedEntity,
+ DetectionType,
+ DetectOptions,
+ ModelLanguage,
+ ModelOptions,
+ ModelStatus,
+} from './ReactNativeDataDetector.types';
diff --git a/src/useDataDetector.ts b/src/useDataDetector.ts
new file mode 100644
index 0000000..dc73bba
--- /dev/null
+++ b/src/useDataDetector.ts
@@ -0,0 +1,122 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+
+import { detect as detectFn, getModelStatus, prepareModel } from './ReactNativeDataDetector';
+import type {
+ DetectedEntity,
+ DetectionType,
+ ModelLanguage,
+ ModelStatus,
+} from './ReactNativeDataDetector.types';
+
+const DEFAULT_LANGUAGE: ModelLanguage = 'en';
+
+export interface UseDataDetectorOptions {
+ /**
+ * Which language model to use (Android only, defaults to `'en'`). Ignored on iOS.
+ * Changing this re-checks/prepares the model for the new language.
+ */
+ language?: ModelLanguage;
+ /**
+ * When `true` (default), the hook downloads the model on mount if it is not
+ * already available (Android). Set to `false` to manage downloads yourself via
+ * the returned `prepare()`. No effect on iOS, which is always ready.
+ */
+ autoPrepare?: boolean;
+}
+
+export interface UseDataDetectorResult {
+ /**
+ * Detects entities in `text` using the hook's configured language. Safe to call
+ * before the model is ready — on Android the underlying call downloads the model
+ * on demand if needed.
+ */
+ detect: (text: string, options?: { types?: DetectionType[] }) => Promise;
+ /** Manually (re)download the model for the configured language. */
+ prepare: () => Promise;
+ /** Current model download state. */
+ status: ModelStatus;
+ /** `true` when the model is available and `detect()` can run offline. */
+ isReady: boolean;
+ /** The last preparation error, or `null`. */
+ error: Error | null;
+}
+
+/**
+ * React hook for data detection that tracks model availability and, on Android,
+ * downloads the language model automatically.
+ *
+ * On iOS the model is always available, so `status` settles on `'ready'` and
+ * `autoPrepare` has no effect.
+ *
+ * @example
+ * const { detect, status, isReady } = useDataDetector();
+ * // later: const entities = await detect(text, { types: ['email'] });
+ */
+export function useDataDetector(options?: UseDataDetectorOptions): UseDataDetectorResult {
+ const language = options?.language ?? DEFAULT_LANGUAGE;
+ const autoPrepare = options?.autoPrepare ?? true;
+
+ const [status, setStatus] = useState('notDownloaded');
+ const [error, setError] = useState(null);
+
+ // The language the hook currently cares about. Lets late-resolving promises
+ // from a previous language (after a switch or unmount) be ignored.
+ const activeLanguage = useRef(language);
+
+ const prepare = useCallback(async () => {
+ setError(null);
+ setStatus('downloading');
+ try {
+ await prepareModel({ language });
+ if (activeLanguage.current === language) setStatus('ready');
+ } catch (e) {
+ const err = e instanceof Error ? e : new Error(String(e));
+ if (activeLanguage.current === language) {
+ setError(err);
+ setStatus('error');
+ }
+ throw err;
+ }
+ }, [language]);
+
+ useEffect(() => {
+ activeLanguage.current = language;
+ let active = true;
+
+ (async () => {
+ setError(null);
+ try {
+ const current = await getModelStatus({ language });
+ if (!active) return;
+ if (current === 'ready') {
+ setStatus('ready');
+ return;
+ }
+ if (!autoPrepare) {
+ setStatus(current);
+ return;
+ }
+ setStatus('downloading');
+ await prepareModel({ language });
+ if (active) setStatus('ready');
+ } catch (e) {
+ if (active) {
+ setError(e instanceof Error ? e : new Error(String(e)));
+ setStatus('error');
+ }
+ }
+ })();
+
+ return () => {
+ active = false;
+ };
+ }, [language, autoPrepare]);
+
+ const detect = useCallback(
+ (text: string, opts?: { types?: DetectionType[] }): Promise =>
+ detectFn(text, { types: opts?.types, language }),
+ [language],
+ );
+
+ return { detect, prepare, status, isReady: status === 'ready', error };
+}