Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
102 changes: 94 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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');
Expand All @@ -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 <Text>Preparing model… ({status})</Text>;
// …
}
```

## 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<boolean>` — `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<ModelStatus>` — `'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<boolean>`

### `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<DetectedEntity[]>` | Detect entities using the configured language. `options.types` selects entity types. |
| `prepare` | `() => Promise<void>` | 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?)`

Expand All @@ -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'`

Expand All @@ -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 |

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
Expand All @@ -32,9 +33,20 @@ class ReactNativeDataDetectorModule : Module() {
}
}

AsyncFunction("detect") { text: String, types: List<String>, 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<String>, language: String, promise: Promise ->
val options = EntityExtractorOptions.Builder(modelIdentifierFor(language)).build()
val extractor = EntityExtraction.getClient(options)

extractor.downloadModelIfNeeded()
Expand Down Expand Up @@ -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"
Expand Down
Loading
Loading