react-native-lingua 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Lingua.podspec CHANGED
@@ -17,5 +17,48 @@ Pod::Spec.new do |s|
17
17
  s.private_header_files = "ios/**/*.h"
18
18
  s.vendored_frameworks = "ios/*.xcframework"
19
19
 
20
+ # Build Rust library from source during pod install
21
+ s.prepare_command = <<-CMD
22
+ set -e
23
+
24
+ # Check if Rust is installed
25
+ if ! command -v cargo &> /dev/null; then
26
+ echo "❌ Error: Rust is not installed."
27
+ echo "Please install Rust: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"
28
+ exit 1
29
+ fi
30
+
31
+ # Check if iOS targets are installed
32
+ if ! rustup target list --installed | grep -q "aarch64-apple-ios"; then
33
+ echo "❌ Error: iOS Rust targets not installed."
34
+ echo "Please run: rustup target add aarch64-apple-ios aarch64-apple-ios-sim"
35
+ exit 1
36
+ fi
37
+
38
+ # Build the xcframework
39
+ cd rust
40
+ make header
41
+
42
+ # Build for iOS targets
43
+ echo "Building Rust library for iOS..."
44
+ cargo build --release --target aarch64-apple-ios
45
+ cargo build --release --target aarch64-apple-ios-sim
46
+
47
+ # Create xcframework
48
+ mkdir -p generated/include
49
+ xcodebuild -create-xcframework \
50
+ -library target/aarch64-apple-ios/release/liblingua_native.a \
51
+ -headers generated/include \
52
+ -library target/aarch64-apple-ios-sim/release/liblingua_native.a \
53
+ -headers generated/include \
54
+ -output generated/liblingua_native.xcframework
55
+
56
+ # Copy to ios directory
57
+ cp -rf generated/*.xcframework ../ios/
58
+ cp -f generated/include/liblingua.h ../cpp/
59
+
60
+ echo "✅ Rust library built successfully"
61
+ CMD
62
+
20
63
  install_modules_dependencies(s)
21
64
  end
@@ -86,6 +86,30 @@ repositories {
86
86
  google()
87
87
  }
88
88
 
89
+ // Build Rust library before native build
90
+ task buildRustHeader(type: Exec) {
91
+ workingDir file("${projectDir}/../rust")
92
+ commandLine 'make', 'header'
93
+
94
+ // Always run to ensure header is up to date
95
+ }
96
+
97
+ task buildRust(type: Exec) {
98
+ workingDir file("${projectDir}/../rust")
99
+ commandLine 'make', 'android'
100
+
101
+ // Only run if jniLibs don't exist
102
+ onlyIf {
103
+ !file("${projectDir}/jniLibs/arm64-v8a/liblingua_native.a").exists()
104
+ }
105
+
106
+ dependsOn buildRustHeader
107
+ }
108
+
109
+ tasks.matching { it.name.startsWith('externalNativeBuild') }.configureEach {
110
+ dependsOn buildRust
111
+ }
112
+
89
113
  def kotlin_version = getExtOrDefault("kotlinVersion")
90
114
 
91
115
  dependencies {
package/cpp/lingua.cpp CHANGED
@@ -1,6 +1,6 @@
1
1
  #include "lingua.h"
2
+ #include "liblingua.h"
2
3
  #include "macros.h"
3
- #include <liblingua.h>
4
4
  #include <string>
5
5
  #include <vector>
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-lingua",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "A lingua-rs wrapper for React Native",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -16,8 +16,13 @@
16
16
  "src",
17
17
  "lib",
18
18
  "android",
19
- "ios",
19
+ "ios/*.{h,mm}",
20
20
  "cpp",
21
+ "rust/src",
22
+ "rust/Cargo.toml",
23
+ "rust/build.rs",
24
+ "rust/Makefile",
25
+ "rust/rust-toolchain.toml",
21
26
  "*.podspec",
22
27
  "react-native.config.js",
23
28
  "!ios/build",
@@ -26,10 +31,13 @@
26
31
  "!android/gradlew",
27
32
  "!android/gradlew.bat",
28
33
  "!android/local.properties",
34
+ "!cpp/liblingua.h",
29
35
  "!**/__tests__",
30
36
  "!**/__fixtures__",
31
37
  "!**/__mocks__",
32
- "!**/.*"
38
+ "!**/.*",
39
+ "README.md",
40
+ "LICENSE"
33
41
  ],
34
42
  "workspaces": [
35
43
  "example"
@@ -158,7 +166,6 @@
158
166
  "lint": "eslint \"**/*.{js,ts,tsx}\"",
159
167
  "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
160
168
  "build:rust": "cd rust && make all",
161
- "postinstall": "npm run build:rust || echo 'Warning: Rust build failed. Pre-built binaries may be needed.'",
162
169
  "release": "release-it --only-version"
163
170
  }
164
171
  }
@@ -0,0 +1,16 @@
1
+ [package]
2
+ name = "lingua_native"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [lib]
7
+ name = "lingua_native"
8
+ crate-type = ["staticlib", "cdylib"]
9
+
10
+ [dependencies]
11
+ lingua = "1.7.2"
12
+ libc = "0.2"
13
+ lazy_static = "1.4"
14
+
15
+ [build-dependencies]
16
+ cbindgen = "0.26.0"
package/rust/Makefile ADDED
@@ -0,0 +1,88 @@
1
+ ARCHS_IOS = aarch64-apple-ios aarch64-apple-ios-sim
2
+ ARCHS_ANDROID = aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android
3
+ LIB_FILE = liblingua_native.a
4
+ DYLIB = liblingua_native.so
5
+ XCFRAMEWORK = liblingua_native.xcframework
6
+
7
+ all: ios android
8
+
9
+ # Generate header only (fast, for development)
10
+ header:
11
+ mkdir -p generated/include
12
+ cargo build --lib
13
+ cp -f generated/include/liblingua.h ../cpp/
14
+ @echo "✅ Header generated and copied to cpp/"
15
+
16
+ ios: clean header ios-build package-xcframework copy-ios notify
17
+
18
+ android: header android-build copy-android notify
19
+
20
+ notify:
21
+ @echo "🟢 Build Completed!"
22
+
23
+ lint:
24
+ cargo clippy --all-features --all-targets -- --no-deps -D warnings
25
+
26
+ fix:
27
+ cargo clippy --all-targets --all-features --fix --allow-dirty --allow-staged
28
+ cargo fmt --all
29
+
30
+ clean:
31
+ rm -rf generated/$(XCFRAMEWORK)
32
+ rm -rf ../ios/$(XCFRAMEWORK)
33
+
34
+ # iOS Build
35
+
36
+ .PHONY: $(ARCHS_IOS)
37
+ $(ARCHS_IOS): %:
38
+ cargo build --target $@ --release
39
+
40
+ package-xcframework: $(ARCHS_IOS)
41
+ mkdir -p generated/include
42
+ xcodebuild -create-xcframework \
43
+ -library target/aarch64-apple-ios/release/$(LIB_FILE) \
44
+ -headers generated/include \
45
+ -library target/aarch64-apple-ios-sim/release/$(LIB_FILE) \
46
+ -headers generated/include \
47
+ -output generated/$(XCFRAMEWORK)
48
+
49
+ copy-ios:
50
+ cp -rf generated/*.xcframework ../ios
51
+ cp -f generated/include/liblingua.h ../cpp/
52
+ @echo "✅ iOS xcframework and headers copied"
53
+
54
+ ios-build:
55
+ cargo build --release --target aarch64-apple-ios
56
+ cargo build --release --target aarch64-apple-ios-sim
57
+
58
+ # Android Build
59
+
60
+ .PHONY: $(ARCHS_ANDROID)
61
+ $(ARCHS_ANDROID): %:
62
+ cargo ndk --target $@ --platform 31 build --release
63
+
64
+ android-build:
65
+ cargo ndk --target aarch64-linux-android --platform 31 build --release
66
+ cargo ndk --target armv7-linux-androideabi --platform 31 build --release
67
+ cargo ndk --target i686-linux-android --platform 31 build --release
68
+ cargo ndk --target x86_64-linux-android --platform 31 build --release
69
+
70
+ copy-android:
71
+ # Copy header
72
+ mkdir -p ../android/src/main/jni/include
73
+ cp -rf generated/include/* ../android/src/main/jni/include
74
+ # Copy per architecture generated .a files
75
+ mkdir -p ../android/jniLibs/x86
76
+ cp target/i686-linux-android/release/$(LIB_FILE) ../android/jniLibs/x86
77
+
78
+ mkdir -p ../android/jniLibs/x86_64
79
+ cp target/x86_64-linux-android/release/$(LIB_FILE) ../android/jniLibs/x86_64
80
+
81
+ mkdir -p ../android/jniLibs/armeabi-v7a
82
+ cp target/armv7-linux-androideabi/release/$(LIB_FILE) ../android/jniLibs/armeabi-v7a
83
+
84
+ mkdir -p ../android/jniLibs/arm64-v8a
85
+ cp target/aarch64-linux-android/release/$(LIB_FILE) ../android/jniLibs/arm64-v8a
86
+ @echo "✅ Android libraries copied"
87
+
88
+ .PHONY: all ios android clean notify lint fix ios-build android-build copy-ios copy-android package-xcframework header
package/rust/build.rs ADDED
@@ -0,0 +1,31 @@
1
+ extern crate cbindgen;
2
+
3
+ use std::env;
4
+
5
+ fn generate_c_headers() {
6
+ let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
7
+ let header_path = "generated/include/liblingua.h";
8
+
9
+ cbindgen::Builder::new()
10
+ .with_crate(crate_dir)
11
+ .with_language(cbindgen::Language::Cxx)
12
+ .with_include_guard("lingua_h")
13
+ .with_autogen_warning(
14
+ "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */",
15
+ )
16
+ .with_namespace("lingua")
17
+ .with_cpp_compat(true)
18
+ .generate()
19
+ .expect("Unable to generate bindings")
20
+ .write_to_file(header_path);
21
+ }
22
+
23
+ fn main() {
24
+ // Tell Cargo that if the given file changes, to rerun this build script.
25
+ println!("cargo:rerun-if-changed=src/lib.rs");
26
+
27
+ if std::env::var("CARGO_CFG_CLIPPY").is_err() {
28
+ // Only run cbindgen if not running cargo clippy
29
+ generate_c_headers();
30
+ }
31
+ }
@@ -0,0 +1,11 @@
1
+ [toolchain]
2
+ channel = "stable"
3
+ targets = [
4
+ "x86_64-apple-ios",
5
+ "aarch64-apple-ios",
6
+ "aarch64-apple-ios-sim",
7
+ "aarch64-linux-android",
8
+ "armv7-linux-androideabi",
9
+ "x86_64-linux-android",
10
+ "i686-linux-android",
11
+ ]
@@ -0,0 +1,264 @@
1
+ use lingua::{IsoCode639_1, Language, LanguageDetector, LanguageDetectorBuilder};
2
+ use std::ffi::{c_char, CStr, CString};
3
+ use std::os::raw::c_int;
4
+ use std::ptr;
5
+ use std::str::FromStr;
6
+ use std::sync::Mutex;
7
+
8
+ lazy_static::lazy_static! {
9
+ static ref GLOBAL_ERROR: Mutex<Option<String>> = Mutex::new(None);
10
+ }
11
+
12
+ // Error handling
13
+ // Helper function for internal use
14
+ unsafe fn set_error_internal(err: *mut *const c_char, error_message: &str) {
15
+ let mut global_error = GLOBAL_ERROR.lock().unwrap();
16
+ *global_error = Some(error_message.to_string());
17
+
18
+ if !err.is_null() {
19
+ // Point to the stored error string in GLOBAL_ERROR
20
+ if let Some(ref error_str) = *global_error {
21
+ *err = error_str.as_ptr() as *const c_char;
22
+ }
23
+ }
24
+ }
25
+
26
+ // Exported C function (not actually used, but needed for header generation)
27
+ #[no_mangle]
28
+ pub unsafe extern "C" fn set_error(err: *mut *const c_char, error_message: *const c_char) {
29
+ if error_message.is_null() {
30
+ return;
31
+ }
32
+ let c_str = CStr::from_ptr(error_message);
33
+ if let Ok(s) = c_str.to_str() {
34
+ set_error_internal(err, s);
35
+ }
36
+ }
37
+
38
+ #[no_mangle]
39
+ pub extern "C" fn get_error() -> *const c_char {
40
+ let global_error = GLOBAL_ERROR.lock().unwrap();
41
+ match &*global_error {
42
+ Some(err) => err.as_ptr() as *const c_char,
43
+ None => ptr::null(),
44
+ }
45
+ }
46
+
47
+ // LanguageDetector opaque pointer type
48
+ pub struct LinguaDetector {
49
+ detector: LanguageDetector,
50
+ }
51
+
52
+ /// Creates a language detector with all languages
53
+ #[no_mangle]
54
+ pub extern "C" fn lingua_detector_create_all() -> *mut LinguaDetector {
55
+ let detector = LanguageDetectorBuilder::from_all_languages().build();
56
+ Box::into_raw(Box::new(LinguaDetector { detector }))
57
+ }
58
+
59
+ /// Creates a language detector with specific languages
60
+ /// languages: comma-separated ISO 639-1 codes (e.g., "en,fr,de,es")
61
+ #[no_mangle]
62
+ pub unsafe extern "C" fn lingua_detector_create_from_languages(
63
+ languages: *const c_char,
64
+ error: *mut *const c_char,
65
+ ) -> *mut LinguaDetector {
66
+ if languages.is_null() {
67
+ set_error_internal(error, "languages parameter is null");
68
+ return ptr::null_mut();
69
+ }
70
+
71
+ let c_str = CStr::from_ptr(languages);
72
+ let lang_str = match c_str.to_str() {
73
+ Ok(s) => s,
74
+ Err(_) => {
75
+ set_error_internal(error, "Invalid UTF-8 in languages parameter");
76
+ return ptr::null_mut();
77
+ }
78
+ };
79
+
80
+ let lang_codes: Vec<&str> = lang_str.split(',').map(|s| s.trim()).collect();
81
+ let mut selected_iso_codes = Vec::new();
82
+
83
+ for code in lang_codes {
84
+ let upper_code = code.to_uppercase();
85
+ match IsoCode639_1::from_str(&upper_code) {
86
+ Ok(iso_code) => selected_iso_codes.push(iso_code),
87
+ Err(_) => {
88
+ let error_msg = format!("Invalid language code: {}", code);
89
+ set_error_internal(error, &error_msg);
90
+ return ptr::null_mut();
91
+ }
92
+ }
93
+ }
94
+
95
+ if selected_iso_codes.is_empty() {
96
+ set_error_internal(error, "No valid languages provided");
97
+ return ptr::null_mut();
98
+ }
99
+
100
+ let detector = LanguageDetectorBuilder::from_iso_codes_639_1(&selected_iso_codes).build();
101
+ Box::into_raw(Box::new(LinguaDetector { detector }))
102
+ }
103
+
104
+ /// Detects the language of the given text
105
+ /// Returns the ISO 639-1 code (e.g., "en") or null if detection failed
106
+ #[no_mangle]
107
+ pub unsafe extern "C" fn lingua_detect_language(
108
+ detector: *const LinguaDetector,
109
+ text: *const c_char,
110
+ error: *mut *const c_char,
111
+ ) -> *mut c_char {
112
+ if detector.is_null() {
113
+ set_error_internal(error, "detector is null");
114
+ return ptr::null_mut();
115
+ }
116
+
117
+ if text.is_null() {
118
+ set_error_internal(error, "text is null");
119
+ return ptr::null_mut();
120
+ }
121
+
122
+ let detector = &(*detector).detector;
123
+ let c_str = CStr::from_ptr(text);
124
+ let text_str = match c_str.to_str() {
125
+ Ok(s) => s,
126
+ Err(_) => {
127
+ set_error_internal(error, "Invalid UTF-8 in text");
128
+ return ptr::null_mut();
129
+ }
130
+ };
131
+
132
+ match detector.detect_language_of(text_str) {
133
+ Some(language) => {
134
+ let iso_code = language.iso_code_639_1().to_string().to_lowercase();
135
+ match CString::new(iso_code) {
136
+ Ok(c_string) => c_string.into_raw(),
137
+ Err(_) => {
138
+ set_error_internal(error, "Failed to create C string");
139
+ ptr::null_mut()
140
+ }
141
+ }
142
+ }
143
+ None => ptr::null_mut(),
144
+ }
145
+ }
146
+
147
+ /// Computes the confidence value for a specific language
148
+ #[no_mangle]
149
+ pub unsafe extern "C" fn lingua_compute_language_confidence(
150
+ detector: *const LinguaDetector,
151
+ text: *const c_char,
152
+ language_code: *const c_char,
153
+ error: *mut *const c_char,
154
+ ) -> f64 {
155
+ if detector.is_null() || text.is_null() || language_code.is_null() {
156
+ set_error_internal(error, "null parameter");
157
+ return 0.0;
158
+ }
159
+
160
+ let detector = &(*detector).detector;
161
+ let text_str = CStr::from_ptr(text).to_str().unwrap_or("");
162
+ let lang_str = CStr::from_ptr(language_code).to_str().unwrap_or("");
163
+
164
+ // Convert the ISO code string to a Language enum
165
+ // We need to iterate through all possible languages and match
166
+ let all_languages = Language::all();
167
+ let language = all_languages
168
+ .into_iter()
169
+ .find(|lang| {
170
+ lang.iso_code_639_1()
171
+ .to_string()
172
+ .eq_ignore_ascii_case(lang_str)
173
+ })
174
+ .unwrap_or_else(|| {
175
+ set_error_internal(error, "Invalid language code");
176
+ Language::English // Default fallback, but we return 0.0 anyway
177
+ });
178
+
179
+ if language == Language::English && !lang_str.eq_ignore_ascii_case("en") {
180
+ return 0.0; // Error was already set
181
+ }
182
+
183
+ detector.compute_language_confidence(text_str, language)
184
+ }
185
+
186
+ /// Result structure for confidence values
187
+ #[repr(C)]
188
+ pub struct ConfidenceValue {
189
+ pub language_code: *mut c_char,
190
+ pub confidence: f64,
191
+ }
192
+
193
+ /// Computes confidence values for all languages
194
+ /// Returns an array of ConfidenceValue structs and sets the count
195
+ #[no_mangle]
196
+ pub unsafe extern "C" fn lingua_compute_language_confidence_values(
197
+ detector: *const LinguaDetector,
198
+ text: *const c_char,
199
+ count: *mut c_int,
200
+ error: *mut *const c_char,
201
+ ) -> *mut ConfidenceValue {
202
+ if detector.is_null() || text.is_null() || count.is_null() {
203
+ set_error_internal(error, "null parameter");
204
+ return ptr::null_mut();
205
+ }
206
+
207
+ let detector = &(*detector).detector;
208
+ let text_str = match CStr::from_ptr(text).to_str() {
209
+ Ok(s) => s,
210
+ Err(_) => {
211
+ set_error_internal(error, "Invalid UTF-8");
212
+ return ptr::null_mut();
213
+ }
214
+ };
215
+
216
+ let confidence_values = detector.compute_language_confidence_values(text_str);
217
+ *count = confidence_values.len() as c_int;
218
+
219
+ let mut results: Vec<ConfidenceValue> = Vec::with_capacity(confidence_values.len());
220
+
221
+ for (language, confidence) in confidence_values {
222
+ let iso_code = language.iso_code_639_1().to_string().to_lowercase();
223
+ if let Ok(c_string) = CString::new(iso_code) {
224
+ results.push(ConfidenceValue {
225
+ language_code: c_string.into_raw(),
226
+ confidence,
227
+ });
228
+ }
229
+ }
230
+
231
+ let ptr = results.as_mut_ptr();
232
+ std::mem::forget(results);
233
+ ptr
234
+ }
235
+
236
+ /// Frees a string allocated by lingua
237
+ #[no_mangle]
238
+ pub unsafe extern "C" fn lingua_free_string(s: *mut c_char) {
239
+ if !s.is_null() {
240
+ let _ = CString::from_raw(s);
241
+ }
242
+ }
243
+
244
+ /// Frees the confidence values array
245
+ #[no_mangle]
246
+ pub unsafe extern "C" fn lingua_free_confidence_values(values: *mut ConfidenceValue, count: c_int) {
247
+ if !values.is_null() {
248
+ for i in 0..count {
249
+ let value = &mut *values.offset(i as isize);
250
+ if !value.language_code.is_null() {
251
+ let _ = CString::from_raw(value.language_code);
252
+ }
253
+ }
254
+ let _ = Vec::from_raw_parts(values, count as usize, count as usize);
255
+ }
256
+ }
257
+
258
+ /// Destroys the detector and frees memory
259
+ #[no_mangle]
260
+ pub unsafe extern "C" fn lingua_detector_destroy(detector: *mut LinguaDetector) {
261
+ if !detector.is_null() {
262
+ let _ = Box::from_raw(detector);
263
+ }
264
+ }