react-native-ensys-camera-x 1.0.7

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.
Files changed (506) hide show
  1. package/CustomCamera.podspec +20 -0
  2. package/LICENSE +20 -0
  3. package/README.md +833 -0
  4. package/android/build.gradle +135 -0
  5. package/android/consumer-rules.pro +47 -0
  6. package/android/src/main/AndroidManifest.xml +72 -0
  7. package/android/src/main/java/android/print/PdfPrint.java +51 -0
  8. package/android/src/main/java/com/customcamera/AnimatedImageViewManager.kt +199 -0
  9. package/android/src/main/java/com/customcamera/AudioPlayerView.kt +322 -0
  10. package/android/src/main/java/com/customcamera/AudioRecorder.kt +506 -0
  11. package/android/src/main/java/com/customcamera/BarcodeScannerActivity.kt +361 -0
  12. package/android/src/main/java/com/customcamera/BlurBackgroundView.kt +199 -0
  13. package/android/src/main/java/com/customcamera/CacheCleanupManager.kt +386 -0
  14. package/android/src/main/java/com/customcamera/CameraAspectRatioMode.kt +77 -0
  15. package/android/src/main/java/com/customcamera/CameraAsset.kt +211 -0
  16. package/android/src/main/java/com/customcamera/CameraTheme.kt +479 -0
  17. package/android/src/main/java/com/customcamera/CompressionCacheStore.kt +504 -0
  18. package/android/src/main/java/com/customcamera/CompressionFileNameUtils.kt +7 -0
  19. package/android/src/main/java/com/customcamera/CompressionProfile.kt +195 -0
  20. package/android/src/main/java/com/customcamera/CustomCameraActivity.kt +7291 -0
  21. package/android/src/main/java/com/customcamera/CustomCameraFileProvider.kt +9 -0
  22. package/android/src/main/java/com/customcamera/CustomCameraModule.kt +2479 -0
  23. package/android/src/main/java/com/customcamera/CustomCameraPackage.kt +53 -0
  24. package/android/src/main/java/com/customcamera/DisplayUtils.kt +9 -0
  25. package/android/src/main/java/com/customcamera/DocumentPreviewView.kt +846 -0
  26. package/android/src/main/java/com/customcamera/DrawingView.kt +214 -0
  27. package/android/src/main/java/com/customcamera/EmojiPickerData.kt +228 -0
  28. package/android/src/main/java/com/customcamera/FilterStripAdapter.kt +158 -0
  29. package/android/src/main/java/com/customcamera/GifSupport.kt +215 -0
  30. package/android/src/main/java/com/customcamera/GlobalAudioPlayer.kt +320 -0
  31. package/android/src/main/java/com/customcamera/ImageExporter.kt +1394 -0
  32. package/android/src/main/java/com/customcamera/ImageFormatSupport.kt +163 -0
  33. package/android/src/main/java/com/customcamera/Localization.kt +206 -0
  34. package/android/src/main/java/com/customcamera/MediaAdapters.kt +758 -0
  35. package/android/src/main/java/com/customcamera/MediaCacheManager.kt +782 -0
  36. package/android/src/main/java/com/customcamera/MediaExportFormats.kt +201 -0
  37. package/android/src/main/java/com/customcamera/MediaFileIo.kt +310 -0
  38. package/android/src/main/java/com/customcamera/MediaMetadataStore.kt +494 -0
  39. package/android/src/main/java/com/customcamera/MediaPreviewEditor.kt +14865 -0
  40. package/android/src/main/java/com/customcamera/MediaPublisher.kt +423 -0
  41. package/android/src/main/java/com/customcamera/MediaStorageWorkflow.kt +722 -0
  42. package/android/src/main/java/com/customcamera/MediaStoreRepository.kt +378 -0
  43. package/android/src/main/java/com/customcamera/MediaThumbnailView.kt +286 -0
  44. package/android/src/main/java/com/customcamera/MediaUtils.kt +37 -0
  45. package/android/src/main/java/com/customcamera/NativeVideoViewManager.kt +845 -0
  46. package/android/src/main/java/com/customcamera/OfficeDocumentConverter.kt +2791 -0
  47. package/android/src/main/java/com/customcamera/PendingCaptureCrops.kt +88 -0
  48. package/android/src/main/java/com/customcamera/PowerPointPreview.kt +4077 -0
  49. package/android/src/main/java/com/customcamera/RecentPickerStore.kt +74 -0
  50. package/android/src/main/java/com/customcamera/SecureFileEraser.kt +411 -0
  51. package/android/src/main/java/com/customcamera/SecureKeyModule.kt +88 -0
  52. package/android/src/main/java/com/customcamera/SecureMediaDeleter.kt +493 -0
  53. package/android/src/main/java/com/customcamera/StickerPickerData.kt +107 -0
  54. package/android/src/main/java/com/customcamera/SvgDrawable.kt +175 -0
  55. package/android/src/main/java/com/customcamera/SvgIconViewManager.kt +77 -0
  56. package/android/src/main/java/com/customcamera/TextEntryController.kt +553 -0
  57. package/android/src/main/java/com/customcamera/ThumbnailLoader.kt +455 -0
  58. package/android/src/main/java/com/customcamera/UploadService.kt +909 -0
  59. package/android/src/main/java/com/customcamera/VideoEditUtils.kt +210 -0
  60. package/android/src/main/java/com/customcamera/VideoRotationTranscoder.kt +1992 -0
  61. package/android/src/main/java/com/customcamera/gif/AnimatedGifEncoder.kt +473 -0
  62. package/android/src/main/java/com/customcamera/gif/GifDecoder.kt +690 -0
  63. package/android/src/main/java/com/customcamera/gif/GifFramesDrawable.kt +160 -0
  64. package/android/src/main/java/com/customcamera/gif/GifHttp.kt +112 -0
  65. package/android/src/main/java/com/customcamera/gif/GifImageLoader.kt +186 -0
  66. package/android/src/main/java/com/customcamera/gif/GifModels.kt +74 -0
  67. package/android/src/main/java/com/customcamera/gif/GifProvider.kt +49 -0
  68. package/android/src/main/java/com/customcamera/gif/GifProviders.kt +27 -0
  69. package/android/src/main/java/com/customcamera/gif/GifRepository.kt +124 -0
  70. package/android/src/main/java/com/customcamera/gif/GifSafety.kt +33 -0
  71. package/android/src/main/java/com/customcamera/gif/GiphyGifProvider.kt +146 -0
  72. package/android/src/main/java/com/customcamera/gif/LZWEncoder.kt +258 -0
  73. package/android/src/main/java/com/customcamera/gif/NeuQuant.kt +435 -0
  74. package/android/src/main/java/com/customcamera/gif/TenorGifProvider.kt +147 -0
  75. package/android/src/main/res/drawable/bg_brush_selected.xml +5 -0
  76. package/android/src/main/res/drawable/bg_color_swatch.xml +5 -0
  77. package/android/src/main/res/drawable/bg_editor_toolbar.xml +6 -0
  78. package/android/src/main/res/drawable/bg_icon_circle.xml +5 -0
  79. package/android/src/main/res/drawable/bg_icon_circle_light.xml +5 -0
  80. package/android/src/main/res/drawable/bg_mode_tab_active.xml +6 -0
  81. package/android/src/main/res/drawable/bg_pill.xml +6 -0
  82. package/android/src/main/res/drawable/bg_recording_dot.xml +6 -0
  83. package/android/src/main/res/drawable/bg_selection_badge.xml +8 -0
  84. package/android/src/main/res/drawable/bg_send_button.xml +5 -0
  85. package/android/src/main/res/drawable/bg_shutter_inner.xml +5 -0
  86. package/android/src/main/res/drawable/bg_shutter_recording.xml +6 -0
  87. package/android/src/main/res/drawable/bg_shutter_ring.xml +8 -0
  88. package/android/src/main/res/drawable/bg_shutter_video_idle.xml +5 -0
  89. package/android/src/main/res/drawable/bg_thumbnail.xml +9 -0
  90. package/android/src/main/res/drawable/ic_aspect_ratio.xml +17 -0
  91. package/android/src/main/res/drawable/ic_audio.xml +10 -0
  92. package/android/src/main/res/drawable/ic_back.xml +10 -0
  93. package/android/src/main/res/drawable/ic_brush_medium.xml +12 -0
  94. package/android/src/main/res/drawable/ic_brush_thick.xml +12 -0
  95. package/android/src/main/res/drawable/ic_brush_thin.xml +12 -0
  96. package/android/src/main/res/drawable/ic_check.xml +17 -0
  97. package/android/src/main/res/drawable/ic_chevron_down.xml +10 -0
  98. package/android/src/main/res/drawable/ic_chevron_up.xml +13 -0
  99. package/android/src/main/res/drawable/ic_clear_drawing.xml +10 -0
  100. package/android/src/main/res/drawable/ic_close.xml +10 -0
  101. package/android/src/main/res/drawable/ic_cover.xml +9 -0
  102. package/android/src/main/res/drawable/ic_crop.xml +10 -0
  103. package/android/src/main/res/drawable/ic_delete.xml +9 -0
  104. package/android/src/main/res/drawable/ic_document.xml +10 -0
  105. package/android/src/main/res/drawable/ic_done.xml +10 -0
  106. package/android/src/main/res/drawable/ic_download.xml +10 -0
  107. package/android/src/main/res/drawable/ic_emoji.xml +10 -0
  108. package/android/src/main/res/drawable/ic_filter.xml +10 -0
  109. package/android/src/main/res/drawable/ic_flash_auto.xml +7 -0
  110. package/android/src/main/res/drawable/ic_flash_off.xml +23 -0
  111. package/android/src/main/res/drawable/ic_flash_on.xml +16 -0
  112. package/android/src/main/res/drawable/ic_flip.xml +15 -0
  113. package/android/src/main/res/drawable/ic_gallery.xml +10 -0
  114. package/android/src/main/res/drawable/ic_gif.xml +24 -0
  115. package/android/src/main/res/drawable/ic_lock.xml +19 -0
  116. package/android/src/main/res/drawable/ic_lock_open.xml +22 -0
  117. package/android/src/main/res/drawable/ic_marker.xml +10 -0
  118. package/android/src/main/res/drawable/ic_more_vert.xml +18 -0
  119. package/android/src/main/res/drawable/ic_overlay_close.xml +19 -0
  120. package/android/src/main/res/drawable/ic_overlay_resize.xml +18 -0
  121. package/android/src/main/res/drawable/ic_pause.xml +10 -0
  122. package/android/src/main/res/drawable/ic_photo_mode.xml +10 -0
  123. package/android/src/main/res/drawable/ic_play.xml +10 -0
  124. package/android/src/main/res/drawable/ic_redo.xml +10 -0
  125. package/android/src/main/res/drawable/ic_rotate.xml +9 -0
  126. package/android/src/main/res/drawable/ic_send.xml +10 -0
  127. package/android/src/main/res/drawable/ic_share.xml +10 -0
  128. package/android/src/main/res/drawable/ic_stat_upload.xml +10 -0
  129. package/android/src/main/res/drawable/ic_sticker.xml +24 -0
  130. package/android/src/main/res/drawable/ic_text.xml +10 -0
  131. package/android/src/main/res/drawable/ic_text_align_center.xml +11 -0
  132. package/android/src/main/res/drawable/ic_text_align_left.xml +11 -0
  133. package/android/src/main/res/drawable/ic_text_align_right.xml +11 -0
  134. package/android/src/main/res/drawable/ic_text_bg.xml +11 -0
  135. package/android/src/main/res/drawable/ic_trim.xml +23 -0
  136. package/android/src/main/res/drawable/ic_undo.xml +10 -0
  137. package/android/src/main/res/drawable/ic_video_mode.xml +10 -0
  138. package/android/src/main/res/drawable/ic_video_note.xml +13 -0
  139. package/android/src/main/res/drawable/ic_volume_off.xml +9 -0
  140. package/android/src/main/res/drawable/ic_volume_on.xml +9 -0
  141. package/android/src/main/res/drawable/sticker_heart.xml +9 -0
  142. package/android/src/main/res/drawable/sticker_smile.xml +9 -0
  143. package/android/src/main/res/drawable/sticker_star.xml +9 -0
  144. package/android/src/main/res/drawable/sticker_thumbsup.xml +9 -0
  145. package/android/src/main/res/values/colors.xml +34 -0
  146. package/android/src/main/res/values/ids.xml +15 -0
  147. package/android/src/main/res/values/strings.xml +15 -0
  148. package/android/src/main/res/values/styles.xml +20 -0
  149. package/android/src/main/res/xml/file_paths.xml +34 -0
  150. package/ios/CustomCamera.h +5 -0
  151. package/ios/CustomCamera.mm +214 -0
  152. package/lib/module/AudioPlayerContext.js +233 -0
  153. package/lib/module/AudioPlayerContext.js.map +1 -0
  154. package/lib/module/CameraPreview.js +304 -0
  155. package/lib/module/CameraPreview.js.map +1 -0
  156. package/lib/module/NativeAnimatedImageView.js +41 -0
  157. package/lib/module/NativeAnimatedImageView.js.map +1 -0
  158. package/lib/module/NativeAudioPlayerView.js +33 -0
  159. package/lib/module/NativeAudioPlayerView.js.map +1 -0
  160. package/lib/module/NativeBlurView.js +35 -0
  161. package/lib/module/NativeBlurView.js.map +1 -0
  162. package/lib/module/NativeCustomCamera.js +128 -0
  163. package/lib/module/NativeCustomCamera.js.map +1 -0
  164. package/lib/module/NativeCustomCamera.web.js +143 -0
  165. package/lib/module/NativeCustomCamera.web.js.map +1 -0
  166. package/lib/module/NativeCustomVideoView.js +5 -0
  167. package/lib/module/NativeCustomVideoView.js.map +1 -0
  168. package/lib/module/NativeDocumentPreviewView.js +156 -0
  169. package/lib/module/NativeDocumentPreviewView.js.map +1 -0
  170. package/lib/module/NativeMediaThumbnailView.js +110 -0
  171. package/lib/module/NativeMediaThumbnailView.js.map +1 -0
  172. package/lib/module/NativeSvgIcon.js +32 -0
  173. package/lib/module/NativeSvgIcon.js.map +1 -0
  174. package/lib/module/ResponsivePreviewContainer.js +171 -0
  175. package/lib/module/ResponsivePreviewContainer.js.map +1 -0
  176. package/lib/module/accessibility.js +61 -0
  177. package/lib/module/accessibility.js.map +1 -0
  178. package/lib/module/config.js +352 -0
  179. package/lib/module/config.js.map +1 -0
  180. package/lib/module/cpuOptimization.js +296 -0
  181. package/lib/module/cpuOptimization.js.map +1 -0
  182. package/lib/module/db/DatabaseManager.js +191 -0
  183. package/lib/module/db/DatabaseManager.js.map +1 -0
  184. package/lib/module/db/MediaMapper.js +284 -0
  185. package/lib/module/db/MediaMapper.js.map +1 -0
  186. package/lib/module/db/MediaRepository.js +242 -0
  187. package/lib/module/db/MediaRepository.js.map +1 -0
  188. package/lib/module/db/index.js +41 -0
  189. package/lib/module/db/index.js.map +1 -0
  190. package/lib/module/db/migrations.js +180 -0
  191. package/lib/module/db/migrations.js.map +1 -0
  192. package/lib/module/db/react-native-sqlcipher.d.js +2 -0
  193. package/lib/module/db/react-native-sqlcipher.d.js.map +1 -0
  194. package/lib/module/db/secureKey.js +87 -0
  195. package/lib/module/db/secureKey.js.map +1 -0
  196. package/lib/module/db/types.js +57 -0
  197. package/lib/module/db/types.js.map +1 -0
  198. package/lib/module/db/useMediaDatabase.js +123 -0
  199. package/lib/module/db/useMediaDatabase.js.map +1 -0
  200. package/lib/module/defaultIcons.js +2259 -0
  201. package/lib/module/defaultIcons.js.map +1 -0
  202. package/lib/module/deviceCapabilities.js +408 -0
  203. package/lib/module/deviceCapabilities.js.map +1 -0
  204. package/lib/module/errors.js +267 -0
  205. package/lib/module/errors.js.map +1 -0
  206. package/lib/module/iconConfig.js +796 -0
  207. package/lib/module/iconConfig.js.map +1 -0
  208. package/lib/module/icons.js +514 -0
  209. package/lib/module/icons.js.map +1 -0
  210. package/lib/module/index.js +251 -0
  211. package/lib/module/index.js.map +1 -0
  212. package/lib/module/jest-globals.d.js +2 -0
  213. package/lib/module/jest-globals.d.js.map +1 -0
  214. package/lib/module/library/MediaLibrary.js +107 -0
  215. package/lib/module/library/MediaLibrary.js.map +1 -0
  216. package/lib/module/library/MediaLibraryScreen.js +2597 -0
  217. package/lib/module/library/MediaLibraryScreen.js.map +1 -0
  218. package/lib/module/library/components/AudioPreview.js +74 -0
  219. package/lib/module/library/components/AudioPreview.js.map +1 -0
  220. package/lib/module/library/components/DocumentPreview.js +331 -0
  221. package/lib/module/library/components/DocumentPreview.js.map +1 -0
  222. package/lib/module/library/components/ExpandableFab.js +343 -0
  223. package/lib/module/library/components/ExpandableFab.js.map +1 -0
  224. package/lib/module/library/components/FileTiles.js +251 -0
  225. package/lib/module/library/components/FileTiles.js.map +1 -0
  226. package/lib/module/library/components/ImagePreview.js +70 -0
  227. package/lib/module/library/components/ImagePreview.js.map +1 -0
  228. package/lib/module/library/components/MediaMetadataHeader.js +175 -0
  229. package/lib/module/library/components/MediaMetadataHeader.js.map +1 -0
  230. package/lib/module/library/components/RecordAudioScreen.js +816 -0
  231. package/lib/module/library/components/RecordAudioScreen.js.map +1 -0
  232. package/lib/module/library/components/VideoPreview.js +453 -0
  233. package/lib/module/library/components/VideoPreview.js.map +1 -0
  234. package/lib/module/library/components/ViewerCounter.js +55 -0
  235. package/lib/module/library/components/ViewerCounter.js.map +1 -0
  236. package/lib/module/library/compressionEstimate.js +507 -0
  237. package/lib/module/library/compressionEstimate.js.map +1 -0
  238. package/lib/module/library/fileMetadata.js +196 -0
  239. package/lib/module/library/fileMetadata.js.map +1 -0
  240. package/lib/module/library/index.js +42 -0
  241. package/lib/module/library/index.js.map +1 -0
  242. package/lib/module/library/libraryIconRegistry.js +69 -0
  243. package/lib/module/library/libraryIconRegistry.js.map +1 -0
  244. package/lib/module/library/libraryIcons.js +858 -0
  245. package/lib/module/library/libraryIcons.js.map +1 -0
  246. package/lib/module/library/localizedFormat.js +181 -0
  247. package/lib/module/library/localizedFormat.js.map +1 -0
  248. package/lib/module/library/palette.js +225 -0
  249. package/lib/module/library/palette.js.map +1 -0
  250. package/lib/module/library/responsive.js +53 -0
  251. package/lib/module/library/responsive.js.map +1 -0
  252. package/lib/module/locales.js +416 -0
  253. package/lib/module/locales.js.map +1 -0
  254. package/lib/module/localization.js +1064 -0
  255. package/lib/module/localization.js.map +1 -0
  256. package/lib/module/logger.js +20 -0
  257. package/lib/module/logger.js.map +1 -0
  258. package/lib/module/lowMemoryCache.js +206 -0
  259. package/lib/module/lowMemoryCache.js.map +1 -0
  260. package/lib/module/mediaHistory.js +33 -0
  261. package/lib/module/mediaHistory.js.map +1 -0
  262. package/lib/module/mediaStorageService.js +402 -0
  263. package/lib/module/mediaStorageService.js.map +1 -0
  264. package/lib/module/memory.js +79 -0
  265. package/lib/module/memory.js.map +1 -0
  266. package/lib/module/nativeActions.js +307 -0
  267. package/lib/module/nativeActions.js.map +1 -0
  268. package/lib/module/nativeComponentSupport.js +61 -0
  269. package/lib/module/nativeComponentSupport.js.map +1 -0
  270. package/lib/module/networkPerformance.js +24 -0
  271. package/lib/module/networkPerformance.js.map +1 -0
  272. package/lib/module/package.json +1 -0
  273. package/lib/module/performance.js +104 -0
  274. package/lib/module/performance.js.map +1 -0
  275. package/lib/module/performanceMonitor.js +155 -0
  276. package/lib/module/performanceMonitor.js.map +1 -0
  277. package/lib/module/permissions.js +370 -0
  278. package/lib/module/permissions.js.map +1 -0
  279. package/lib/module/rendering.js +40 -0
  280. package/lib/module/rendering.js.map +1 -0
  281. package/lib/module/security.js +18 -0
  282. package/lib/module/security.js.map +1 -0
  283. package/lib/module/state.js +266 -0
  284. package/lib/module/state.js.map +1 -0
  285. package/lib/module/storage.js +400 -0
  286. package/lib/module/storage.js.map +1 -0
  287. package/lib/module/theme.js +390 -0
  288. package/lib/module/theme.js.map +1 -0
  289. package/lib/module/types.js +4 -0
  290. package/lib/module/types.js.map +1 -0
  291. package/lib/module/utils.js +115 -0
  292. package/lib/module/utils.js.map +1 -0
  293. package/lib/module/validation.js +288 -0
  294. package/lib/module/validation.js.map +1 -0
  295. package/lib/typescript/package.json +1 -0
  296. package/lib/typescript/src/AudioPlayerContext.d.ts +27 -0
  297. package/lib/typescript/src/AudioPlayerContext.d.ts.map +1 -0
  298. package/lib/typescript/src/CameraPreview.d.ts +13 -0
  299. package/lib/typescript/src/CameraPreview.d.ts.map +1 -0
  300. package/lib/typescript/src/NativeAnimatedImageView.d.ts +33 -0
  301. package/lib/typescript/src/NativeAnimatedImageView.d.ts.map +1 -0
  302. package/lib/typescript/src/NativeAudioPlayerView.d.ts +12 -0
  303. package/lib/typescript/src/NativeAudioPlayerView.d.ts.map +1 -0
  304. package/lib/typescript/src/NativeBlurView.d.ts +12 -0
  305. package/lib/typescript/src/NativeBlurView.d.ts.map +1 -0
  306. package/lib/typescript/src/NativeCustomCamera.d.ts +167 -0
  307. package/lib/typescript/src/NativeCustomCamera.d.ts.map +1 -0
  308. package/lib/typescript/src/NativeCustomCamera.web.d.ts +47 -0
  309. package/lib/typescript/src/NativeCustomCamera.web.d.ts.map +1 -0
  310. package/lib/typescript/src/NativeCustomVideoView.d.ts +30 -0
  311. package/lib/typescript/src/NativeCustomVideoView.d.ts.map +1 -0
  312. package/lib/typescript/src/NativeDocumentPreviewView.d.ts +48 -0
  313. package/lib/typescript/src/NativeDocumentPreviewView.d.ts.map +1 -0
  314. package/lib/typescript/src/NativeMediaThumbnailView.d.ts +54 -0
  315. package/lib/typescript/src/NativeMediaThumbnailView.d.ts.map +1 -0
  316. package/lib/typescript/src/NativeSvgIcon.d.ts +25 -0
  317. package/lib/typescript/src/NativeSvgIcon.d.ts.map +1 -0
  318. package/lib/typescript/src/ResponsivePreviewContainer.d.ts +92 -0
  319. package/lib/typescript/src/ResponsivePreviewContainer.d.ts.map +1 -0
  320. package/lib/typescript/src/accessibility.d.ts +20 -0
  321. package/lib/typescript/src/accessibility.d.ts.map +1 -0
  322. package/lib/typescript/src/config.d.ts +145 -0
  323. package/lib/typescript/src/config.d.ts.map +1 -0
  324. package/lib/typescript/src/cpuOptimization.d.ts +63 -0
  325. package/lib/typescript/src/cpuOptimization.d.ts.map +1 -0
  326. package/lib/typescript/src/db/DatabaseManager.d.ts +48 -0
  327. package/lib/typescript/src/db/DatabaseManager.d.ts.map +1 -0
  328. package/lib/typescript/src/db/MediaMapper.d.ts +77 -0
  329. package/lib/typescript/src/db/MediaMapper.d.ts.map +1 -0
  330. package/lib/typescript/src/db/MediaRepository.d.ts +66 -0
  331. package/lib/typescript/src/db/MediaRepository.d.ts.map +1 -0
  332. package/lib/typescript/src/db/index.d.ts +23 -0
  333. package/lib/typescript/src/db/index.d.ts.map +1 -0
  334. package/lib/typescript/src/db/migrations.d.ts +31 -0
  335. package/lib/typescript/src/db/migrations.d.ts.map +1 -0
  336. package/lib/typescript/src/db/secureKey.d.ts +10 -0
  337. package/lib/typescript/src/db/secureKey.d.ts.map +1 -0
  338. package/lib/typescript/src/db/types.d.ts +115 -0
  339. package/lib/typescript/src/db/types.d.ts.map +1 -0
  340. package/lib/typescript/src/db/useMediaDatabase.d.ts +41 -0
  341. package/lib/typescript/src/db/useMediaDatabase.d.ts.map +1 -0
  342. package/lib/typescript/src/defaultIcons.d.ts +139 -0
  343. package/lib/typescript/src/defaultIcons.d.ts.map +1 -0
  344. package/lib/typescript/src/deviceCapabilities.d.ts +92 -0
  345. package/lib/typescript/src/deviceCapabilities.d.ts.map +1 -0
  346. package/lib/typescript/src/errors.d.ts +95 -0
  347. package/lib/typescript/src/errors.d.ts.map +1 -0
  348. package/lib/typescript/src/iconConfig.d.ts +176 -0
  349. package/lib/typescript/src/iconConfig.d.ts.map +1 -0
  350. package/lib/typescript/src/icons.d.ts +217 -0
  351. package/lib/typescript/src/icons.d.ts.map +1 -0
  352. package/lib/typescript/src/index.d.ts +104 -0
  353. package/lib/typescript/src/index.d.ts.map +1 -0
  354. package/lib/typescript/src/library/MediaLibrary.d.ts +57 -0
  355. package/lib/typescript/src/library/MediaLibrary.d.ts.map +1 -0
  356. package/lib/typescript/src/library/MediaLibraryScreen.d.ts +51 -0
  357. package/lib/typescript/src/library/MediaLibraryScreen.d.ts.map +1 -0
  358. package/lib/typescript/src/library/components/AudioPreview.d.ts +6 -0
  359. package/lib/typescript/src/library/components/AudioPreview.d.ts.map +1 -0
  360. package/lib/typescript/src/library/components/DocumentPreview.d.ts +10 -0
  361. package/lib/typescript/src/library/components/DocumentPreview.d.ts.map +1 -0
  362. package/lib/typescript/src/library/components/ExpandableFab.d.ts +39 -0
  363. package/lib/typescript/src/library/components/ExpandableFab.d.ts.map +1 -0
  364. package/lib/typescript/src/library/components/FileTiles.d.ts +8 -0
  365. package/lib/typescript/src/library/components/FileTiles.d.ts.map +1 -0
  366. package/lib/typescript/src/library/components/ImagePreview.d.ts +5 -0
  367. package/lib/typescript/src/library/components/ImagePreview.d.ts.map +1 -0
  368. package/lib/typescript/src/library/components/MediaMetadataHeader.d.ts +16 -0
  369. package/lib/typescript/src/library/components/MediaMetadataHeader.d.ts.map +1 -0
  370. package/lib/typescript/src/library/components/RecordAudioScreen.d.ts +8 -0
  371. package/lib/typescript/src/library/components/RecordAudioScreen.d.ts.map +1 -0
  372. package/lib/typescript/src/library/components/VideoPreview.d.ts +20 -0
  373. package/lib/typescript/src/library/components/VideoPreview.d.ts.map +1 -0
  374. package/lib/typescript/src/library/components/ViewerCounter.d.ts +13 -0
  375. package/lib/typescript/src/library/components/ViewerCounter.d.ts.map +1 -0
  376. package/lib/typescript/src/library/compressionEstimate.d.ts +313 -0
  377. package/lib/typescript/src/library/compressionEstimate.d.ts.map +1 -0
  378. package/lib/typescript/src/library/fileMetadata.d.ts +33 -0
  379. package/lib/typescript/src/library/fileMetadata.d.ts.map +1 -0
  380. package/lib/typescript/src/library/index.d.ts +40 -0
  381. package/lib/typescript/src/library/index.d.ts.map +1 -0
  382. package/lib/typescript/src/library/libraryIconRegistry.d.ts +35 -0
  383. package/lib/typescript/src/library/libraryIconRegistry.d.ts.map +1 -0
  384. package/lib/typescript/src/library/libraryIcons.d.ts +85 -0
  385. package/lib/typescript/src/library/libraryIcons.d.ts.map +1 -0
  386. package/lib/typescript/src/library/localizedFormat.d.ts +49 -0
  387. package/lib/typescript/src/library/localizedFormat.d.ts.map +1 -0
  388. package/lib/typescript/src/library/palette.d.ts +104 -0
  389. package/lib/typescript/src/library/palette.d.ts.map +1 -0
  390. package/lib/typescript/src/library/responsive.d.ts +15 -0
  391. package/lib/typescript/src/library/responsive.d.ts.map +1 -0
  392. package/lib/typescript/src/locales.d.ts +103 -0
  393. package/lib/typescript/src/locales.d.ts.map +1 -0
  394. package/lib/typescript/src/localization.d.ts +1051 -0
  395. package/lib/typescript/src/localization.d.ts.map +1 -0
  396. package/lib/typescript/src/logger.d.ts +4 -0
  397. package/lib/typescript/src/logger.d.ts.map +1 -0
  398. package/lib/typescript/src/lowMemoryCache.d.ts +64 -0
  399. package/lib/typescript/src/lowMemoryCache.d.ts.map +1 -0
  400. package/lib/typescript/src/mediaHistory.d.ts +3 -0
  401. package/lib/typescript/src/mediaHistory.d.ts.map +1 -0
  402. package/lib/typescript/src/mediaStorageService.d.ts +155 -0
  403. package/lib/typescript/src/mediaStorageService.d.ts.map +1 -0
  404. package/lib/typescript/src/memory.d.ts +21 -0
  405. package/lib/typescript/src/memory.d.ts.map +1 -0
  406. package/lib/typescript/src/nativeActions.d.ts +174 -0
  407. package/lib/typescript/src/nativeActions.d.ts.map +1 -0
  408. package/lib/typescript/src/nativeComponentSupport.d.ts +30 -0
  409. package/lib/typescript/src/nativeComponentSupport.d.ts.map +1 -0
  410. package/lib/typescript/src/networkPerformance.d.ts +12 -0
  411. package/lib/typescript/src/networkPerformance.d.ts.map +1 -0
  412. package/lib/typescript/src/performance.d.ts +32 -0
  413. package/lib/typescript/src/performance.d.ts.map +1 -0
  414. package/lib/typescript/src/performanceMonitor.d.ts +45 -0
  415. package/lib/typescript/src/performanceMonitor.d.ts.map +1 -0
  416. package/lib/typescript/src/permissions.d.ts +132 -0
  417. package/lib/typescript/src/permissions.d.ts.map +1 -0
  418. package/lib/typescript/src/rendering.d.ts +9 -0
  419. package/lib/typescript/src/rendering.d.ts.map +1 -0
  420. package/lib/typescript/src/security.d.ts +6 -0
  421. package/lib/typescript/src/security.d.ts.map +1 -0
  422. package/lib/typescript/src/state.d.ts +102 -0
  423. package/lib/typescript/src/state.d.ts.map +1 -0
  424. package/lib/typescript/src/storage.d.ts +117 -0
  425. package/lib/typescript/src/storage.d.ts.map +1 -0
  426. package/lib/typescript/src/theme.d.ts +15 -0
  427. package/lib/typescript/src/theme.d.ts.map +1 -0
  428. package/lib/typescript/src/types.d.ts +802 -0
  429. package/lib/typescript/src/types.d.ts.map +1 -0
  430. package/lib/typescript/src/utils.d.ts +50 -0
  431. package/lib/typescript/src/utils.d.ts.map +1 -0
  432. package/lib/typescript/src/validation.d.ts +52 -0
  433. package/lib/typescript/src/validation.d.ts.map +1 -0
  434. package/package.json +204 -0
  435. package/react-native.config.js +17 -0
  436. package/src/AudioPlayerContext.tsx +220 -0
  437. package/src/CameraPreview.tsx +352 -0
  438. package/src/NativeAnimatedImageView.tsx +71 -0
  439. package/src/NativeAudioPlayerView.tsx +39 -0
  440. package/src/NativeBlurView.tsx +40 -0
  441. package/src/NativeCustomCamera.ts +305 -0
  442. package/src/NativeCustomCamera.web.ts +192 -0
  443. package/src/NativeCustomVideoView.ts +30 -0
  444. package/src/NativeDocumentPreviewView.tsx +196 -0
  445. package/src/NativeMediaThumbnailView.tsx +167 -0
  446. package/src/NativeSvgIcon.tsx +46 -0
  447. package/src/ResponsivePreviewContainer.tsx +227 -0
  448. package/src/accessibility.ts +80 -0
  449. package/src/config.ts +406 -0
  450. package/src/cpuOptimization.ts +365 -0
  451. package/src/db/DatabaseManager.ts +212 -0
  452. package/src/db/MediaMapper.ts +377 -0
  453. package/src/db/MediaRepository.ts +313 -0
  454. package/src/db/index.ts +73 -0
  455. package/src/db/migrations.ts +222 -0
  456. package/src/db/react-native-sqlcipher.d.ts +76 -0
  457. package/src/db/secureKey.ts +100 -0
  458. package/src/db/types.ts +123 -0
  459. package/src/db/useMediaDatabase.ts +191 -0
  460. package/src/defaultIcons.tsx +2390 -0
  461. package/src/deviceCapabilities.ts +438 -0
  462. package/src/errors.ts +350 -0
  463. package/src/iconConfig.tsx +962 -0
  464. package/src/icons.tsx +788 -0
  465. package/src/index.tsx +763 -0
  466. package/src/jest-globals.d.ts +9 -0
  467. package/src/library/MediaLibrary.tsx +141 -0
  468. package/src/library/MediaLibraryScreen.tsx +3003 -0
  469. package/src/library/components/AudioPreview.tsx +73 -0
  470. package/src/library/components/DocumentPreview.tsx +361 -0
  471. package/src/library/components/ExpandableFab.tsx +342 -0
  472. package/src/library/components/FileTiles.tsx +252 -0
  473. package/src/library/components/ImagePreview.tsx +72 -0
  474. package/src/library/components/MediaMetadataHeader.tsx +208 -0
  475. package/src/library/components/RecordAudioScreen.tsx +845 -0
  476. package/src/library/components/VideoPreview.tsx +478 -0
  477. package/src/library/components/ViewerCounter.tsx +71 -0
  478. package/src/library/compressionEstimate.ts +662 -0
  479. package/src/library/fileMetadata.ts +212 -0
  480. package/src/library/index.ts +66 -0
  481. package/src/library/libraryIconRegistry.tsx +86 -0
  482. package/src/library/libraryIcons.tsx +928 -0
  483. package/src/library/localizedFormat.ts +207 -0
  484. package/src/library/palette.ts +281 -0
  485. package/src/library/responsive.ts +49 -0
  486. package/src/locales.ts +381 -0
  487. package/src/localization.ts +1947 -0
  488. package/src/logger.ts +20 -0
  489. package/src/lowMemoryCache.ts +218 -0
  490. package/src/mediaHistory.ts +42 -0
  491. package/src/mediaStorageService.ts +546 -0
  492. package/src/memory.ts +102 -0
  493. package/src/nativeActions.ts +422 -0
  494. package/src/nativeComponentSupport.ts +60 -0
  495. package/src/networkPerformance.ts +30 -0
  496. package/src/performance.ts +137 -0
  497. package/src/performanceMonitor.ts +219 -0
  498. package/src/permissions.ts +403 -0
  499. package/src/rendering.ts +47 -0
  500. package/src/security.ts +19 -0
  501. package/src/state.ts +333 -0
  502. package/src/storage.ts +453 -0
  503. package/src/theme.tsx +467 -0
  504. package/src/types.ts +946 -0
  505. package/src/utils.ts +177 -0
  506. package/src/validation.ts +372 -0
@@ -0,0 +1,1947 @@
1
+ import React, {
2
+ createContext,
3
+ useContext,
4
+ useEffect,
5
+ useMemo,
6
+ useState,
7
+ type ReactNode,
8
+ } from 'react';
9
+ import { I18nManager, NativeModules, StyleSheet } from 'react-native';
10
+ import type { TextStyle, ViewStyle } from 'react-native';
11
+ import {
12
+ DEFAULT_LOCALE,
13
+ isRtlLocale,
14
+ normalizeLocale,
15
+ resolveLocaleTranslations,
16
+ } from './locales';
17
+
18
+ export interface CameraTranslations {
19
+ // Common / General
20
+ all: string;
21
+ photos: string;
22
+ videos: string;
23
+ documents: string;
24
+ cancel: string;
25
+ done: string;
26
+ ok: string;
27
+ use: string;
28
+ trim: string;
29
+ selectCover: string;
30
+ add: string;
31
+ addMore: string;
32
+ /** The "Add more" button on the editor's media strip, `+` prefix included. */
33
+ addMorePlus: string;
34
+ /** Position of the visible item in a multi-item preview, e.g. `2/5`. */
35
+ mediaCounter: string;
36
+ /** The selected trim range under a video, e.g. `0:03 – 0:12`. */
37
+ trimRangeLabel: string;
38
+ /** A zero timestamp, shown before a clip's duration is known. */
39
+ durationZero: string;
40
+ /**
41
+ * File-size units. `{0}` is the number, already formatted with the active
42
+ * language's digits and decimal separator — translate only the unit itself.
43
+ */
44
+ fileSizeBytes: string;
45
+ fileSizeKb: string;
46
+ fileSizeMb: string;
47
+ fileSizeGb: string;
48
+ /**
49
+ * Date pattern for the gallery's day headers, in Android `SimpleDateFormat`
50
+ * syntax. Month and day names come from the active language automatically;
51
+ * override this only to change field order, e.g. `dd MMM yyyy`.
52
+ */
53
+ dateFormatPattern: string;
54
+ openSettings: string;
55
+ close: string;
56
+ back: string;
57
+ send: string;
58
+ save: string;
59
+ delete: string;
60
+ deleteFile: string;
61
+ clear: string;
62
+ clearFilter: string;
63
+ /** Filter-picker "None" label while a filter is active; `{0}` = filter name. */
64
+ filterClearSuffix: string;
65
+ audio: string;
66
+ document: string;
67
+ openingCamera: string;
68
+ initializingCamera: string;
69
+ gallery: string;
70
+ selectedCount: string;
71
+ openGallery: string;
72
+ openWith: string;
73
+ libraryEmptyTitle: string;
74
+ libraryEmptyMessage: string;
75
+ /** `ACTION_SEND` chooser title on Android (the native share-sheet header). */
76
+ share: string;
77
+
78
+ // CustomCameraActivity / Camera Setup
79
+ photoMode: string;
80
+ videoMode: string;
81
+ recording: string;
82
+ // Hold-to-record gesture
83
+ shutterCapture: string;
84
+ shutterHoldToRecord: string;
85
+ holdForVideo: string;
86
+ slideUpToLock: string;
87
+ slideToCancel: string;
88
+ cancelRecording: string;
89
+ releaseToCancel: string;
90
+ recordingLocked: string;
91
+ flashOn: string;
92
+ flashOff: string;
93
+ switchCamera: string;
94
+ toggleFlashlight: string;
95
+ pointCameraAtBarcode: string;
96
+ closeScanner: string;
97
+ permissionRequiredTitle: string;
98
+ permissionRequiredMessage: string;
99
+ /**
100
+ * Permissions are requested just-in-time, one feature at a time: the camera on launch, the
101
+ * gallery on the first gallery open, the microphone on the first video recording. Each of
102
+ * those has its own "permanently denied" copy so the message names only what is missing.
103
+ */
104
+ permissionCameraRequiredMessage: string;
105
+ permissionMediaRequiredTitle: string;
106
+ permissionMediaRequiredMessage: string;
107
+ /**
108
+ * Native pre-permission rationale shown BEFORE the camera's system permission dialog.
109
+ * Its whole purpose is to steer the user to the persistent "While using the app" grant
110
+ * rather than the one-time "Only this time" option — the latter is revoked on process
111
+ * death / reboot and forces a re-prompt on the next cold start.
112
+ */
113
+ permissionRationaleCameraPersistentMessage: string;
114
+ /** Positive button on the native pre-permission rationale ("Continue"). */
115
+ permissionContinue: string;
116
+ /**
117
+ * Default rationale texts for the JS-side `PermissionsAndroid` flow
118
+ * (`requestCameraPermission` / `requestStoragePermission` /
119
+ * `requestMicrophonePermission`). The native in-camera permission dialogs use
120
+ * the `permission*Required*` keys above; these cover hosts that request
121
+ * permission ahead of time through the exported helpers.
122
+ */
123
+ permissionRationaleCameraTitle: string;
124
+ permissionRationaleCameraMessage: string;
125
+ permissionRationaleStorageTitle: string;
126
+ permissionRationaleStorageMessage: string;
127
+ permissionRationaleMicrophoneTitle: string;
128
+ permissionRationaleMicrophoneMessage: string;
129
+ /** Positive button on the JS permission rationale. */
130
+ permissionGrant: string;
131
+ /** Negative button on the JS permission rationale. */
132
+ permissionDeny: string;
133
+ cameraUnavailable: string;
134
+ cameraNotFound: string;
135
+ cameraInitializationFailed: string;
136
+ cameraPermissionDenied: string;
137
+ cameraPermissionRequired: string;
138
+ tapToScan: string;
139
+
140
+ // MediaPreviewEditor & Feedback
141
+ preparingVideo: string;
142
+ selectionTooShort: string;
143
+ trimming: string;
144
+ trimFailed: string;
145
+ cropping: string;
146
+ cropFailed: string;
147
+ savingCover: string;
148
+ couldNotSetCover: string;
149
+ coverUpdated: string;
150
+ saving: string;
151
+ savedToGallery: string;
152
+ failedToSaveMedia: string;
153
+ sending: string;
154
+ failedToSendMedia: string; // expects {0} (error message)
155
+ couldNotSaveEditsSendingOriginal: string;
156
+ sendFailed: string;
157
+ sendFailedWithMessage: string; // expects {0}
158
+ addTextTitle: string;
159
+ addTextButton: string;
160
+ typeTextHint: string;
161
+ sizeLabel: string; // expects {0} (size in bytes formatted)
162
+ drawOnImage: string;
163
+ preparing: string;
164
+ couldNotPrepareEditsForCropping: string;
165
+ useTwoFingersToResizeOrRotate: string;
166
+ operationTimedOut: string;
167
+ audioMuted: string;
168
+ audioOn: string;
169
+ compressVideo: string;
170
+ originalVideo: string;
171
+ /** The overflow menu's compression row on a still preview, the counterpart of `compressVideo`. */
172
+ compressImage: string;
173
+ /** The still menu's first row: send the image at its original size. */
174
+ originalImage: string;
175
+ /**
176
+ * The overflow menu's quality-strategy row, and its accessibility label. Governs what BOTH
177
+ * Compress and Save As produce; its applied value is one of the three `profile*` names below.
178
+ */
179
+ compressionProfile: string;
180
+ /**
181
+ * The default quality strategy: the balance of size, quality, speed and RAM chosen for the
182
+ * device it runs on.
183
+ */
184
+ profileRecommended: string;
185
+ /**
186
+ * Result message when no re-encode of a still came out smaller than the original — there is
187
+ * nothing left to compress, so the original is kept. Not an error.
188
+ */
189
+ compressionAlreadyMinimal: string;
190
+ /**
191
+ * A compression dropdown row plus the size it is expected to produce; `{0}` = the row's
192
+ * label, `{1}` = the formatted size. The tilde marks it as an estimate.
193
+ */
194
+ compressEstimatedSize: string;
195
+ /**
196
+ * The "Original video" / "Original image" row plus its real file size; `{0}` = label,
197
+ * `{1}` = formatted size.
198
+ */
199
+ compressOriginalSize: string;
200
+ /**
201
+ * Two formatted sizes as one span, for a compression row whose output is predicted as a range
202
+ * rather than a single figure; `{0}` = the low end, `{1}` = the high end.
203
+ *
204
+ * A clip is re-encoded at a TARGET bit rate, which the encoder spends around rather than hits
205
+ * exactly, so its predicted size is honestly a band ("5.8 – 7.1 MB"). The low end drops the unit
206
+ * when both ends share one, so translations should keep `{0}` unit-less. Wrapped by
207
+ * `compressOriginalSize`, since the band already reads as an approximation and does not also take
208
+ * the tilde.
209
+ */
210
+ compressSizeRange: string;
211
+ /**
212
+ * The message shown when a compression finishes; `{0}` = the formatted output size, `{1}` = whole
213
+ * percent smaller than the original.
214
+ *
215
+ * A STILL quotes its two REAL files (the copy just written and the source it came from): a still
216
+ * is a byte-target search that lands on the size the menu quoted, so the measured figure is right.
217
+ * A CLIP is quoted PROPORTIONALLY instead — the original with the chosen percent removed, and the
218
+ * chosen percent as the saving — because a video transcode copies the audio + container through
219
+ * untouched, so its produced file stays well above the picked level at 70–99% and reads as the
220
+ * wrong size; the menu quotes that same proportion, so the two agree and "N% smaller" always means
221
+ * "N% of the original removed". Neither comes from the tilde-marked estimate ladder. A translation
222
+ * must keep both placeholders.
223
+ */
224
+ compressedToSize: string;
225
+ /**
226
+ * The same message without a saving; `{0}` = the compressed file's formatted size. Used when the
227
+ * original's size cannot be measured, and when the copy did not actually come out smaller —
228
+ * quoting "0% smaller" in either case would state something untrue.
229
+ */
230
+ compressedToSizeOnly: string;
231
+ /** Shown when a compression could not produce a usable file. */
232
+ compressionFailed: string;
233
+ /**
234
+ * The compression progress overlay: a single centred line counting the ESTIMATED time
235
+ * remaining down to one second; `{0}` = whole seconds left.
236
+ */
237
+ compressingSecondsLeft: string;
238
+ /**
239
+ * The line that replaces the countdown when the encode outlives its estimate, shown until it
240
+ * finishes rather than letting the count run past zero. Shared by compressions and "Export By"
241
+ * conversions, whose tails are equally unmeasurable.
242
+ */
243
+ finalizingCompression: string;
244
+ /**
245
+ * The compression dropdown row and the applied-value badge, labelled as a QUALITY level;
246
+ * `{0}` = the percentage of the original quality kept (5..99, lower = stronger compression).
247
+ */
248
+ compressActiveLabel: string;
249
+ /**
250
+ * Deprecated: the compression rows now read as a quality level via `compressActiveLabel`. Kept
251
+ * for backward compatibility with hosts that still translate it; no longer shown by the plugin.
252
+ */
253
+ compressSmallerByPercent: string;
254
+ /**
255
+ * Accessibility suffix for the currently selected row of a menu/dropdown;
256
+ * `{0}` = the row's label.
257
+ */
258
+ selectedOption: string;
259
+
260
+ // "Save As" — the preview header's format-conversion menu (images and videos)
261
+ /**
262
+ * The preview header's "⋮" overflow button, which holds both output settings (compression and
263
+ * format conversion). Used as its accessibility label while neither is applied — once one is,
264
+ * the button announces the applied value via `compressActiveDesc` / `exportActiveDesc` instead.
265
+ */
266
+ moreOptions: string;
267
+ /** The overflow menu's row for format conversion, and its label while none is applied. */
268
+ exportBy: string;
269
+ /** Overflow-button accessibility label once a format is applied; `{0}` = format name. */
270
+ exportActiveDesc: string;
271
+ /** The menu's first row: send the media in the format it already is. */
272
+ originalFormat: string;
273
+ /**
274
+ * Retained for translation bundles that already carry it. The conversion no longer shows a
275
+ * static label: it counts the remaining time down with `convertingSecondsLeft`, exactly as a
276
+ * compression does.
277
+ */
278
+ exportingMedia: string;
279
+ /**
280
+ * The "Export By" progress overlay — the same single centred countdown line the compression
281
+ * overlay shows, phrased for a format conversion; `{0}` = whole seconds left. When the encode
282
+ * outlives its estimate this hands over to `finalizingCompression`, which both jobs share.
283
+ */
284
+ convertingSecondsLeft: string;
285
+ exportFailed: string;
286
+ /** Result toast; `{0}` = format name. */
287
+ exportedTo: string;
288
+ /** Result toast with size; `{0}` = format name, `{1}` = formatted file size. */
289
+ exportedToFormat: string;
290
+ /**
291
+ * Result toast when the target container cannot carry the source's audio codec
292
+ * (WebM takes only Vorbis/Opus); `{0}` and `{1}` are both the format name.
293
+ */
294
+ exportedAudioDropped: string;
295
+ /** Confirmation body shown before exporting an animated GIF, which flattens it. */
296
+ convertGifExportMessage: string;
297
+ /** Confirmation body shown before compressing an animated GIF, which flattens it. */
298
+ convertGifCompressMessage: string;
299
+ /** Confirmation title shown before flattening an animated GIF into a still. */
300
+ convertGifTitle: string;
301
+ /** Negative button on that confirmation: keep the animated GIF as-is. */
302
+ keepAnimation: string;
303
+ /** Positive button on that confirmation: flatten the GIF to a still image. */
304
+ convertToImage: string;
305
+
306
+ clearAppliedFilter: string;
307
+ toggleAudio: string;
308
+ trimVideo: string;
309
+ rotate90Degrees: string;
310
+ /** Content description of the Crop screen's aspect-ratio button (next to Rotate). */
311
+ cropAspectRatio: string;
312
+ /**
313
+ * Crop aspect-ratio option that locks nothing, letting the crop box take any shape.
314
+ * The other options are the image's own shape (`aspectRatioOriginal`) and plain numeric
315
+ * ratios ("1:1", "4:3", …), which are the same in every language and so are not keys.
316
+ */
317
+ cropAspectFree: string;
318
+ noStickersAvailable: string;
319
+ noEmojisAvailable: string;
320
+ filterClearLabel: string; // expects {0}
321
+
322
+ // Document preview
323
+ emptyFile: string;
324
+ previewTruncated: string;
325
+ unsupportedPreviewKind: string; // expects {0}
326
+ fileNotPreviewableText: string;
327
+ couldNotOpenPdf: string; // expects {0}
328
+ cannotOpenDocumentStream: string;
329
+ couldNotReadFile: string; // expects {0}
330
+ webViewUnavailable: string; // expects {0}
331
+ notDecodableImage: string;
332
+ couldNotDecodeImage: string; // expects {0}
333
+ documentPreviewUnavailable: string;
334
+
335
+ // Editor Filter Labels
336
+ filterNone: string;
337
+ filterAuto: string;
338
+ filterGray: string;
339
+ filterNegative: string;
340
+ filterBright: string;
341
+ filterContrast: string;
342
+ filterSharpen: string;
343
+ filterFilm: string;
344
+ filterTint: string;
345
+ filterVignette: string;
346
+ filterSoft: string;
347
+ filterDark: string;
348
+ filterLight: string;
349
+ filterDesaturate: string;
350
+ filterEnhance: string;
351
+ filterCinematic: string;
352
+ filterBW: string;
353
+ filterSepia: string;
354
+ filterVintage: string;
355
+ filterCool: string;
356
+ filterWarm: string;
357
+ filterSaturate: string;
358
+ filterFade: string;
359
+ filterVivid: string;
360
+ filterNormal75: string;
361
+
362
+ // Editor Sticker / Emoji Categories
363
+ categoryClassic: string;
364
+ categoryFaces: string;
365
+ categoryLove: string;
366
+ categoryCelebration: string;
367
+ categoryAnimals: string;
368
+ categoryFood: string;
369
+ categoryVibes: string;
370
+ categorySmileys: string;
371
+ categoryPeople: string;
372
+ categoryAnimalsAndNature: string;
373
+ categoryFoodAndDrink: string;
374
+ categoryActivities: string;
375
+ categoryTravelAndPlaces: string;
376
+ categoryObjects: string;
377
+ categorySymbols: string;
378
+ categoryFlags: string;
379
+ categoryRecent: string;
380
+
381
+ // Upload Service
382
+ uploadProgressTitle: string;
383
+ uploadProgressDescription: string;
384
+ uploadStatusTitle: string;
385
+ uploadStatusDescription: string;
386
+ uploadInterruptedTitle: string;
387
+ uploadInterruptedMessage: string; // expects {0}, {1}
388
+ uploadRetrying: string; // expects {0}, {1}, {2}, {3}
389
+ uploadCompletedTitle: string;
390
+ uploadCompletedMessage: string; // expects {0}
391
+ uploadFailedTitle: string;
392
+ uploadFailedMessage: string; // expects {0}, {1}, {2}, {3}
393
+ uploadingFileProgress: string; // expects {0}, {1}, {2}
394
+ preparingFilesProgress: string; // expects {0}
395
+ uploadingMediaTitle: string;
396
+
397
+ // Validation
398
+ validationSelectionLimitNonNegativeInteger: string;
399
+ validationInitialCameraInvalid: string;
400
+ validationMediaTypesInvalid: string;
401
+ validationHoldToRecordDelayInvalid: string;
402
+ validationMinRecordDurationInvalid: string;
403
+ validationUrisMustBeArray: string;
404
+ validationUrisCannotBeEmpty: string;
405
+ validationTooManyFiles: string; // expects {0}
406
+ validationInvalidUriAtIndex: string; // expects {0}, {1}
407
+ validationAlbumNameRequired: string;
408
+ validationAlbumNameTooLong: string; // expects {0}
409
+ validationAlbumNameInvalidCharacters: string;
410
+ validationCaptionMustBeString: string;
411
+ validationCaptionTooLong: string; // expects {0}
412
+ validationDelayMustBeNumber: string;
413
+ validationDelayCannotBeNegative: string;
414
+ validationDelayTooLong: string; // expects {0}
415
+ validationStartTimeInvalid: string;
416
+ validationEndTimeInvalid: string;
417
+ validationStartBeforeEnd: string;
418
+ validationTrimDurationTooLong: string; // expects {0}
419
+ validationUploadUrlRequired: string;
420
+ validationUploadUrlInvalid: string;
421
+ validationFileTooLarge: string; // expects {0}, {1}, {2}
422
+ validationAlbumIdRequired: string;
423
+ validationAlbumIdTooLong: string; // expects {0}
424
+ validationDegreesMustBeNumber: string;
425
+ validationDegreesInvalid: string;
426
+ validationWidthPositive: string;
427
+ validationHeightPositive: string;
428
+ validationXNumber: string;
429
+ validationYNumber: string;
430
+ validationFilterTypeInvalid: string; // expects {0}, {1}
431
+ validationFlashModeInvalid: string; // expects {0}, {1}
432
+ validationFaceEffectInvalid: string; // expects {0}, {1}
433
+ validationThemeInvalid: string; // expects {0}, {1}
434
+
435
+ // Errors / State (used in React Native side & native side)
436
+ errorCameraPermission: string;
437
+ errorPleaseEnableCameraPermission: string;
438
+ errorMicrophonePermission: string;
439
+ /** Shown when the mic is declined at record time — the clip is recorded silently, not blocked. */
440
+ errorMicrophoneDeniedSilentVideo: string;
441
+ errorStoragePermission: string;
442
+ /** Shown when the gallery permission is declined; the camera itself stays usable. */
443
+ errorGalleryPermission: string;
444
+ errorInvalidUri: string;
445
+ errorFileNotFound: string;
446
+ errorFileTooLarge: string;
447
+ errorOperationCancelled: string;
448
+ errorCameraUnavailable: string;
449
+ errorInsufficientStorage: string;
450
+ /** Refusal shown when a recording is started with too little free space to be worth beginning. */
451
+ errorStorageFullVideo: string;
452
+ /** Shown when a recording ends early because the volume filled up. What was recorded is kept. */
453
+ noticeStorageFullStopped: string;
454
+ /** Title of the blocking dialog raised when low storage ends a recording in flight. */
455
+ storageAlmostFullTitle: string;
456
+ /** Body of that dialog. The clip is already finalized by the time it is shown. */
457
+ storageAlmostFullMessage: string;
458
+ /** Title of the blocking dialog raised when low MEMORY ends a recording in flight. */
459
+ memoryCriticalTitle: string;
460
+ /** Body of that dialog. The clip is already finalized by the time it is shown. */
461
+ memoryCriticalMessage: string;
462
+ /** Sole button on that dialog; advances to the editor with the recorded clip. */
463
+ next: string;
464
+ /** Shown when the encoder failed and the clip could not be saved at all. */
465
+ errorRecordingFailed: string;
466
+ errorNetworkUnavailable: string;
467
+ errorUploadFailed: string;
468
+ /** Shown when a photo/video picked from the gallery can't be opened in the editor. */
469
+ errorMediaLoadFailed: string;
470
+ errorUnknown: string;
471
+ statusReady: string;
472
+ statusProcessingWithProgress: string; // expects {0}
473
+ statusProcessing: string;
474
+ statusSuccess: string;
475
+ statusUnknown: string;
476
+
477
+ // ===== Multilingual coverage additions =====
478
+ /** Camera flash "auto" label (fixes pre-existing default without interface member). */
479
+ flashAuto: string;
480
+ // Generic fallbacks / nouns
481
+ unknown: string;
482
+ untitled: string;
483
+ preview: string;
484
+ media: string;
485
+ image: string;
486
+ video: string;
487
+ // Library item actions & confirmations
488
+ deleteConfirmTitle: string;
489
+ deleteConfirmMessage: string;
490
+ notSecurelyErasedTitle: string;
491
+ notSecurelyErasedMessage: string;
492
+ deleteMedia: string;
493
+ shareMedia: string;
494
+ // Editor tools / sheets
495
+ toolFilter: string;
496
+ toolCrop: string;
497
+ toolText: string;
498
+ toolEmoji: string;
499
+ toolGrid: string;
500
+ emoji: string;
501
+ stickers: string;
502
+ // Editor GIF picker
503
+ toolGif: string;
504
+ gifPickerTitle: string;
505
+ gifSearchPlaceholder: string;
506
+ gifTrending: string;
507
+ gifCategories: string;
508
+ noGifsFound: string;
509
+ gifLoadError: string;
510
+ gifOfflineError: string;
511
+ gifNotConfigured: string;
512
+ gifRateLimited: string;
513
+ gifLoading: string;
514
+ deleteGif: string;
515
+ categoryGestures: string;
516
+ playVideo: string;
517
+ pauseVideo: string;
518
+ deleteSticker: string;
519
+ closeEditor: string;
520
+ download: string;
521
+ selectedVideo: string;
522
+ selectedPhoto: string;
523
+ removeSelectedItem: string;
524
+ addMoreMedia: string;
525
+ compressingPleaseWait: string;
526
+ compressingEllipsis: string;
527
+ selectFilterTitle: string;
528
+ filterAccessibilityLabel: string; // expects {0}
529
+ closeFilters: string;
530
+ // Audio recording (demo record-audio screen + FAB)
531
+ recordAudio: string;
532
+ browseAudio: string;
533
+ camera: string;
534
+ closeActionsMenu: string;
535
+ openActionsMenu: string;
536
+ micPermanentlyDenied: string;
537
+ micPermissionDenied: string;
538
+ micPermissionRequestFailed: string;
539
+ failedToStartRecording: string;
540
+ micPermissionNotGranted: string;
541
+ recordingTooShort: string;
542
+ failedToSaveRecording: string;
543
+ requestingPermission: string;
544
+ allowMicAccess: string;
545
+ recordingError: string;
546
+ tryAgain: string;
547
+ pausedBadge: string;
548
+ paused: string;
549
+ tapToStartRecording: string;
550
+ discard: string;
551
+ pause: string;
552
+ resume: string;
553
+ savingRecording: string;
554
+ micRationaleMessage: string;
555
+ // Document preview (demo)
556
+ unknownSize: string;
557
+ noAppToOpenFile: string;
558
+ couldNotOpenFile: string;
559
+ pageCountSingular: string; // expects {0}
560
+ pageCountPlural: string; // expects {0}
561
+ fileCouldNotBePreviewed: string;
562
+ noInAppPreview: string;
563
+ openNamed: string; // expects {0}
564
+ open: string;
565
+ opening: string;
566
+ compressing: string;
567
+ // Kotlin: capture / upload / office / powerpoint
568
+ errorProcessPhoto: string;
569
+ errorCapturePhoto: string;
570
+ uploadErrorCouldNotOpenStream: string;
571
+ uploadErrorCouldNotOpenFile: string; // expects {0}
572
+ uploadErrorServerCode: string; // expects {0}
573
+ uploadErrorNetwork: string; // expects {0}
574
+ powerPointPresentationTitle: string;
575
+ couldNotOpenPresentation: string; // expects {0}
576
+ couldNotReadPowerPoint: string; // expects {0}
577
+ couldNotOpenDocument: string; // expects {0}
578
+ couldNotConvertDocument: string; // expects {0}
579
+ officeBinaryPreviewNote: string;
580
+ unableToExtractBinaryText: string;
581
+ openInOfficeAppNote: string;
582
+ previewTruncatedRowsCols: string; // expects {0},{1}
583
+ previewTruncatedRows: string; // expects {0}
584
+ previewTruncatedChapters: string; // expects {0}
585
+ previewTruncatedNotice: string;
586
+ powerPointNoSlides: string;
587
+ unknownDate: string;
588
+ aspectRatioOriginal: string;
589
+ aspectRatioFull: string;
590
+ aspectRatioOriginalShort: string;
591
+ gifBadge: string;
592
+ textFontSample: string;
593
+ slideNumberLabel: string; // expects {0}
594
+ docTypePowerpoint: string;
595
+ docTypeWord: string;
596
+ docTypeExcel: string;
597
+ docTypeOffice: string;
598
+ // Native (Kotlin) strings previously wired to keys absent from this map — now host-overridable
599
+ compressActiveDesc: string; // expects {0} = quality percent kept (5..99)
600
+ convertGifMessage: string;
601
+ convertGifCropMessage: string;
602
+ couldNotDecodeImageGeneric: string;
603
+ memoryWarningMessage: string;
604
+ savingEdits: string;
605
+ textCancel: string;
606
+ textFont: string;
607
+ textAlignment: string;
608
+ textBackground: string;
609
+ textDone: string;
610
+ // GIF picker / overlay (native MediaPreviewEditor). gifNotConfigured, gifRateLimited
611
+ // and gifLoadError are declared once in the "Editor GIF picker" section above.
612
+ gifSearchHint: string;
613
+ addingGif: string;
614
+ gifLoadFailed: string;
615
+ gifOffline: string;
616
+ gifTimeout: string;
617
+ gifServerError: string;
618
+ gifNoResults: string;
619
+ // Media library shell — details sheet, metadata rows and formatting units
620
+ mediaDetails: string;
621
+ photoDetails: string;
622
+ videoDetails: string;
623
+ metaFileName: string;
624
+ metaFileType: string;
625
+ metaFileSize: string;
626
+ metaDuration: string;
627
+ metaResolution: string;
628
+ metaDate: string;
629
+ metaPath: string;
630
+ pixelsUnit: string;
631
+ viewDetails: string;
632
+ fileTypeGeneric: string;
633
+ audioTypeGeneric: string;
634
+ // Full-screen viewer video controls
635
+ play: string;
636
+ toggleVideoControls: string;
637
+ seekBackward: string; // expects {0} = seconds
638
+ seekForward: string; // expects {0} = seconds
639
+ }
640
+
641
+ export type CameraTranslationKey = keyof CameraTranslations;
642
+
643
+ export type CameraTranslationFunction = (
644
+ key: CameraTranslationKey,
645
+ ...args: Array<string | number | boolean | null | undefined>
646
+ ) => string | undefined | null;
647
+
648
+ export type CameraTranslationMap = Partial<
649
+ Record<CameraTranslationKey, string | undefined | null>
650
+ >;
651
+
652
+ /**
653
+ * Translations for many languages at once, keyed by language identifier.
654
+ *
655
+ * Keys are BCP-47 tags in whatever spelling the host already uses — `fr`,
656
+ * `pt-BR`, `zh_CN`, `zh-Hant` all work, and lookup is case- and
657
+ * separator-insensitive. Register the whole set once and switch languages with
658
+ * {@link setCameraLanguage}.
659
+ *
660
+ * ```ts
661
+ * const bundles: CameraTranslationBundles = {
662
+ * fr: { send: 'Envoyer', cancel: 'Annuler' },
663
+ * ar: { send: 'إرسال', cancel: 'إلغاء' },
664
+ * };
665
+ * ```
666
+ */
667
+ export type CameraTranslationBundles = Record<string, CameraTranslationMap>;
668
+
669
+ /**
670
+ * Whatever a host's `subscribe` happens to hand back. Functions, RN's
671
+ * `EmitterSubscription` (`remove`), and Rx-style subscriptions (`unsubscribe`)
672
+ * are all accepted so the provider can usually be a one-line adapter over an
673
+ * i18n library the host already has; returning nothing is fine too.
674
+ */
675
+ export type CameraLanguageSubscription =
676
+ | (() => void)
677
+ | { remove: () => void }
678
+ | { unsubscribe: () => void }
679
+ | null
680
+ | undefined
681
+ | void;
682
+
683
+ /**
684
+ * A live view of the host app's active language. Bind one once at startup and
685
+ * every plugin surface — React Native screens *and* the native Android camera —
686
+ * follows the host from then on, with no further calls on language change.
687
+ *
688
+ * The host stays the single source of truth: the plugin only reads this, never
689
+ * the device locale, and keeps no language state of its own beyond the last
690
+ * value read here.
691
+ *
692
+ * ```ts
693
+ * configureCameraPlugin({
694
+ * language: {
695
+ * get: () => i18n.language,
696
+ * subscribe: (onChange) => {
697
+ * i18n.on('languageChanged', onChange);
698
+ * return () => i18n.off('languageChanged', onChange);
699
+ * },
700
+ * },
701
+ * translations: myBundles,
702
+ * });
703
+ * ```
704
+ */
705
+ export interface CameraLanguageProvider {
706
+ /**
707
+ * The host's language right now, as a BCP-47 tag. Called on bind and on every
708
+ * change notification, so it must be cheap and must not throw for a missing
709
+ * language — return `null`/`''` instead, which means "shipped English".
710
+ */
711
+ get(): string | null | undefined;
712
+ /**
713
+ * Registers a change listener and returns a way to remove it. The callback may
714
+ * be handed the new tag (i18next's `languageChanged` does) or called bare, in
715
+ * which case the plugin re-reads {@link CameraLanguageProvider.get}.
716
+ *
717
+ * Return the unsubscriber so a re-bind or teardown can release the listener;
718
+ * the plugin only ever holds one subscription per provider.
719
+ */
720
+ subscribe(
721
+ callback: (locale?: string | null) => void
722
+ ): CameraLanguageSubscription;
723
+ }
724
+
725
+ export interface CameraLocalizationConfig {
726
+ /**
727
+ * The host app's currently active language, as a BCP-47 tag (`'fr'`,
728
+ * `'pt-BR'`, `'zh-Hans'`, `'ar'`). This is the only thing the plugin needs in
729
+ * order to pick a bundle and decide text direction — there is no language
730
+ * selector and no language list inside the plugin.
731
+ *
732
+ * A bound {@link CameraLocalizationConfig.languageProvider} outranks this: the
733
+ * provider is re-read after the config is applied, so the two cannot drift.
734
+ */
735
+ locale?: string;
736
+ /**
737
+ * A live view of the host's language, for one-time automatic synchronization.
738
+ * Configure it once and the plugin follows every later host language change on
739
+ * its own — no repeated {@link setCameraLanguage} calls.
740
+ *
741
+ * Binding is by object identity, so passing the same provider again re-reads it
742
+ * without stacking a second listener, and replacing it releases the old
743
+ * subscription. Pass `null` to detach and go back to manual switching.
744
+ *
745
+ * Unlike every other field here, a provider is *not* cleared by an omitted key
746
+ * in {@link setCameraLocalization} — a binding made once at startup survives a
747
+ * later config replace.
748
+ */
749
+ languageProvider?: CameraLanguageProvider | null;
750
+ /**
751
+ * The host's own translate function. Called with a plugin translation key; return
752
+ * a string to use it, or a falsy value to fall through to `bundles`, then to
753
+ * English. Errors thrown here are swallowed and treated as "not translated".
754
+ */
755
+ translate?: CameraTranslationFunction;
756
+ /** Overrides for the active language only. Highest priority of all sources. */
757
+ translations?: CameraTranslationMap;
758
+ /**
759
+ * Translations for every language the host supports, keyed by locale. Register
760
+ * once at startup; from then on a bound
761
+ * {@link CameraLocalizationConfig.languageProvider} picks the right bundle
762
+ * automatically, or call {@link setCameraLanguage} on each change manually.
763
+ */
764
+ bundles?: CameraTranslationBundles;
765
+ /** Language used when the active one has no translation for a key. Defaults to `'en'`. */
766
+ fallbackLocale?: string;
767
+ /**
768
+ * Force text direction. Normally omit this: direction is derived from `locale`
769
+ * (Arabic, Persian, Hebrew, Urdu, … are right-to-left), and falls back to the
770
+ * app-level `I18nManager.isRTL` when no locale is set.
771
+ */
772
+ isRtl?: boolean;
773
+ }
774
+
775
+ export interface CameraLocalizationProviderProps
776
+ extends Omit<CameraLocalizationConfig, 'locale'> {
777
+ children: ReactNode;
778
+ /**
779
+ * The host app's active language identifier. Changing it re-renders every
780
+ * plugin surface and re-pushes translations to the native camera — no restart.
781
+ *
782
+ * A non-string value is treated purely as a cache-busting token, which keeps
783
+ * hosts working that pass a revision number here. Omit it entirely when a
784
+ * `languageProvider` is bound: the provider is then the authority and this
785
+ * component needs no re-render to follow a language change.
786
+ */
787
+ locale?: string | number;
788
+ /**
789
+ * Optional host-controlled invalidation token for non-locale i18n changes.
790
+ */
791
+ version?: string | number;
792
+ }
793
+
794
+ export const defaultTranslations: CameraTranslations = {
795
+ all: 'All',
796
+ photos: 'Photos',
797
+ videos: 'Videos',
798
+ documents: 'Documents',
799
+ cancel: 'Cancel',
800
+ done: 'Done',
801
+ ok: 'OK',
802
+ use: 'Use',
803
+ trim: 'Trim',
804
+ selectCover: 'Select cover',
805
+ add: 'Add',
806
+ addMore: 'Add more',
807
+ addMorePlus: '+ Add more',
808
+ mediaCounter: '{0} / {1}',
809
+ trimRangeLabel: '{0} – {1}',
810
+ durationZero: '0:00',
811
+ fileSizeBytes: '{0} B',
812
+ fileSizeKb: '{0} KB',
813
+ fileSizeMb: '{0} MB',
814
+ fileSizeGb: '{0} GB',
815
+ dateFormatPattern: 'MMM dd, yyyy',
816
+ openSettings: 'Open Settings',
817
+ close: 'Close',
818
+ back: 'Back',
819
+ send: 'Send',
820
+ save: 'Save',
821
+ delete: 'Delete',
822
+ deleteFile: 'Delete file',
823
+ clear: 'Clear',
824
+ clearFilter: 'Clear applied filter',
825
+ filterClearSuffix: '{0} (Clear)',
826
+ audio: 'Audio',
827
+ document: 'Document',
828
+ openingCamera: 'Opening camera...',
829
+ initializingCamera: 'Initializing camera...',
830
+ gallery: 'Gallery',
831
+ selectedCount: '{0} selected',
832
+ openGallery: 'Open gallery',
833
+ openWith: 'Open with',
834
+ libraryEmptyTitle: 'Nothing here yet',
835
+ libraryEmptyMessage: 'Tap the + button to capture photos and videos with the camera',
836
+ share: 'Share',
837
+
838
+ photoMode: 'Photo',
839
+ videoMode: 'Video',
840
+ recording: 'Recording',
841
+ shutterCapture: 'Capture',
842
+ shutterHoldToRecord: 'Capture. Hold to record video',
843
+ holdForVideo: 'Hold for video, tap for photo',
844
+ slideUpToLock: 'Slide up to lock',
845
+ slideToCancel: 'Slide to cancel',
846
+ cancelRecording: 'Cancel',
847
+ releaseToCancel: 'Release to cancel',
848
+ recordingLocked: 'Recording locked · Tap to stop',
849
+ flashOn: 'Flash on',
850
+ flashOff: 'Flash off',
851
+ flashAuto: 'Flash auto',
852
+ switchCamera: 'Switch camera',
853
+ toggleFlashlight: 'Toggle flashlight',
854
+ pointCameraAtBarcode: 'Point the camera at a QR code or barcode',
855
+ closeScanner: 'Close scanner',
856
+ permissionRequiredTitle: 'Permission required',
857
+ permissionRequiredMessage:
858
+ 'Camera and media access are needed to use the camera. Please enable the permissions for this app in Settings.',
859
+ permissionCameraRequiredMessage:
860
+ 'Camera access is needed to use the camera. Please enable it for this app in Settings.',
861
+ permissionMediaRequiredTitle: 'Gallery access required',
862
+ permissionMediaRequiredMessage:
863
+ 'Photo and video access is needed to browse your gallery. Please enable it for this app in Settings.',
864
+ permissionRationaleCameraPersistentMessage:
865
+ 'This app needs camera access to take photos and record video. When the system asks, choose "While using the app" so you are not asked again each time you reopen the app.',
866
+ permissionContinue: 'Continue',
867
+ permissionRationaleCameraTitle: 'Camera Permission Required',
868
+ permissionRationaleCameraMessage:
869
+ 'This app needs camera access to take photos and record videos.',
870
+ permissionRationaleStorageTitle: 'Storage Permission Required',
871
+ permissionRationaleStorageMessage:
872
+ 'This app needs storage access to save photos and videos to your gallery.',
873
+ permissionRationaleMicrophoneTitle: 'Microphone Permission Required',
874
+ permissionRationaleMicrophoneMessage:
875
+ 'This app needs microphone access to record audio with videos.',
876
+ permissionGrant: 'Grant',
877
+ permissionDeny: 'Deny',
878
+ cameraUnavailable: 'Camera not available on this device',
879
+ cameraNotFound: 'No camera found. Check device settings.',
880
+ cameraInitializationFailed: 'Failed to initialize camera. Please try again.',
881
+ cameraPermissionDenied: 'Camera permission denied',
882
+ cameraPermissionRequired: 'Camera permission is required to use this feature',
883
+ tapToScan: 'Tap to scan',
884
+
885
+ preparingVideo: 'Preparing video...',
886
+ selectionTooShort: 'Selection too short',
887
+ trimming: 'Trimming...',
888
+ trimFailed: 'Trim failed',
889
+ cropping: 'Cropping...',
890
+ cropFailed: 'Crop failed',
891
+ savingCover: 'Saving cover...',
892
+ couldNotSetCover: 'Could not set cover',
893
+ coverUpdated: 'Cover updated',
894
+ saving: 'Saving...',
895
+ savedToGallery: 'Saved to gallery',
896
+ failedToSaveMedia: 'Failed to save media',
897
+ sending: 'Sending...',
898
+ failedToSendMedia: 'Failed to send media',
899
+ couldNotSaveEditsSendingOriginal:
900
+ 'Could not save edits, sending original',
901
+ sendFailed: 'Send failed',
902
+ sendFailedWithMessage: 'Send failed: {0}',
903
+ addTextTitle: 'Add text',
904
+ addTextButton: 'Add',
905
+ typeTextHint: 'Type text',
906
+ sizeLabel: 'Size: {0}',
907
+ drawOnImage: 'Draw on the image',
908
+ preparing: 'Preparing...',
909
+ couldNotPrepareEditsForCropping: 'Could not prepare edits for cropping',
910
+ useTwoFingersToResizeOrRotate: 'Use two fingers to resize or rotate',
911
+ operationTimedOut: 'Operation timed out',
912
+ audioMuted: 'Audio muted',
913
+ audioOn: 'Audio on',
914
+ compressVideo: 'Compress Video',
915
+ originalVideo: 'Original video',
916
+ compressImage: 'Compress Image',
917
+ originalImage: 'Original image',
918
+ compressionProfile: 'Compression',
919
+ profileRecommended: 'Recommended',
920
+ compressionAlreadyMinimal:
921
+ 'Already as small as it gets — keeping the original',
922
+ compressEstimatedSize: '{0} (~{1})',
923
+ compressOriginalSize: '{0} ({1})',
924
+ compressSizeRange: '{0} – {1}',
925
+ compressedToSize: 'Compressed to {0} ({1}% smaller)',
926
+ compressedToSizeOnly: 'Compressed to {0}',
927
+ compressionFailed: 'Compression failed',
928
+ compressingSecondsLeft: 'Compressing ({0}s left)',
929
+ finalizingCompression: 'Finalizing...',
930
+ compressActiveLabel: '{0}% quality',
931
+ compressSmallerByPercent: 'Smaller by {0}%',
932
+ selectedOption: '{0}, selected',
933
+
934
+ moreOptions: 'More options',
935
+ exportBy: 'Save As',
936
+ exportActiveDesc: 'Save format: {0}',
937
+ originalFormat: 'Original',
938
+ exportingMedia: 'Saving…',
939
+ convertingSecondsLeft: 'Exporting ({0}s left)',
940
+ exportFailed: 'Export failed',
941
+ exportedTo: 'Exported as {0}',
942
+ exportedToFormat: 'Exported as {0} ({1})',
943
+ exportedAudioDropped:
944
+ 'Exported as {0} — {1} cannot carry this audio track, so audio was removed',
945
+ convertGifExportMessage:
946
+ 'Exporting this GIF to another format will remove its animation and save it as a still image.',
947
+ convertGifCompressMessage:
948
+ 'Compressing this GIF will remove its animation and save it as a still image.',
949
+ convertGifTitle: 'Convert GIF to image?',
950
+ keepAnimation: 'Keep animation',
951
+ convertToImage: 'Convert',
952
+
953
+ clearAppliedFilter: 'Clear applied filter',
954
+ toggleAudio: 'Toggle audio',
955
+ trimVideo: 'Trim video',
956
+ rotate90Degrees: 'Rotate 90 degrees',
957
+ cropAspectRatio: 'Aspect ratio',
958
+ cropAspectFree: 'Free',
959
+ noStickersAvailable: 'No stickers available',
960
+ noEmojisAvailable: 'No emojis available',
961
+ filterClearLabel: '{0} (Clear)',
962
+
963
+ emptyFile: '(empty file)',
964
+ previewTruncated: 'preview truncated',
965
+ unsupportedPreviewKind: 'Unsupported preview kind: {0}',
966
+ fileNotPreviewableText: 'File is not previewable text',
967
+ couldNotOpenPdf: 'Could not open PDF: {0}',
968
+ cannotOpenDocumentStream: 'Cannot open document stream',
969
+ couldNotReadFile: 'Could not read file: {0}',
970
+ webViewUnavailable: 'WebView unavailable: {0}',
971
+ notDecodableImage: 'Not a decodable image',
972
+ couldNotDecodeImage: 'Could not decode image: {0}',
973
+ documentPreviewUnavailable: 'Document preview unavailable',
974
+
975
+ filterNone: 'None',
976
+ filterAuto: 'Auto',
977
+ filterGray: 'Gray',
978
+ filterNegative: 'Negative',
979
+ filterBright: 'Bright',
980
+ filterContrast: 'Contrast',
981
+ filterSharpen: 'Sharpen',
982
+ filterFilm: 'Film',
983
+ filterTint: 'Tint',
984
+ filterVignette: 'Vignette',
985
+ filterSoft: 'Soft',
986
+ filterDark: 'Dark',
987
+ filterLight: 'Light',
988
+ filterDesaturate: 'Desaturate',
989
+ filterEnhance: 'Enhance',
990
+ filterCinematic: 'Cinematic',
991
+ filterBW: 'B&W',
992
+ filterSepia: 'Sepia',
993
+ filterVintage: 'Vintage',
994
+ filterCool: 'Cool',
995
+ filterWarm: 'Warm',
996
+ filterSaturate: 'Saturate',
997
+ filterFade: 'Fade',
998
+ filterVivid: 'Vivid',
999
+ filterNormal75: 'Normal (75%)',
1000
+
1001
+ categoryClassic: 'Classic',
1002
+ categoryFaces: 'Faces',
1003
+ categoryLove: 'Love',
1004
+ categoryCelebration: 'Celebration',
1005
+ categoryAnimals: 'Animals',
1006
+ categoryFood: 'Food',
1007
+ categoryVibes: 'Vibes',
1008
+ categorySmileys: 'Smileys',
1009
+ categoryPeople: 'People',
1010
+ categoryAnimalsAndNature: 'Animals & Nature',
1011
+ categoryFoodAndDrink: 'Food & Drink',
1012
+ categoryActivities: 'Activities',
1013
+ categoryTravelAndPlaces: 'Travel & Places',
1014
+ categoryObjects: 'Objects',
1015
+ categorySymbols: 'Symbols',
1016
+ categoryFlags: 'Flags',
1017
+ categoryRecent: 'Recent',
1018
+
1019
+ uploadProgressTitle: 'Upload Progress',
1020
+ uploadProgressDescription: 'Displays progress of background uploads',
1021
+ uploadStatusTitle: 'Upload Status',
1022
+ uploadStatusDescription:
1023
+ 'Notifies when a background upload completes or fails',
1024
+ uploadInterruptedTitle: 'Upload interrupted',
1025
+ uploadInterruptedMessage:
1026
+ 'A previous upload stopped before completing (file {0} of {1}).',
1027
+ uploadRetrying: 'Retrying file {0} of {1} (attempt {2} of {3})...',
1028
+ uploadCompletedTitle: 'Upload completed',
1029
+ uploadCompletedMessage: 'Successfully uploaded {0} file(s).',
1030
+ uploadFailedTitle: 'Upload failed',
1031
+ uploadFailedMessage: 'Uploaded {0} of {1} file(s) - {2} failed. {3}',
1032
+ uploadingFileProgress: 'Uploading file {0} of {1} ({2}%)',
1033
+ preparingFilesProgress: 'Preparing files... ({0}%)',
1034
+ uploadingMediaTitle: 'Uploading media',
1035
+
1036
+ validationSelectionLimitNonNegativeInteger:
1037
+ 'Selection limit must be a non-negative integer',
1038
+ validationInitialCameraInvalid:
1039
+ 'Initial camera must be either "back" or "front"',
1040
+ validationMediaTypesInvalid:
1041
+ 'Media type must be one of: "all", "photo", "video"',
1042
+ validationHoldToRecordDelayInvalid:
1043
+ 'Hold-to-record delay must be a non-negative number of milliseconds (0 = system long-press timeout)',
1044
+ validationMinRecordDurationInvalid:
1045
+ 'Minimum recording duration must be a non-negative number of milliseconds',
1046
+ validationUrisMustBeArray: 'Media URIs must be an array',
1047
+ validationUrisCannotBeEmpty: 'Media URIs cannot be empty',
1048
+ validationTooManyFiles: 'Cannot process more than {0} files at once',
1049
+ validationInvalidUriAtIndex: 'Invalid URI at index {0}: {1}',
1050
+ validationAlbumNameRequired: 'Album name must be a non-empty string',
1051
+ validationAlbumNameTooLong: 'Album name cannot exceed {0} characters',
1052
+ validationAlbumNameInvalidCharacters:
1053
+ 'Album name contains invalid characters: / : * ? " < > |',
1054
+ validationCaptionMustBeString: 'Caption must be a string',
1055
+ validationCaptionTooLong: 'Caption cannot exceed {0} characters',
1056
+ validationDelayMustBeNumber: 'Delay must be a valid number',
1057
+ validationDelayCannotBeNegative: 'Delay cannot be negative',
1058
+ validationDelayTooLong: 'Delay cannot exceed {0} seconds',
1059
+ validationStartTimeInvalid: 'Start time must be a non-negative number',
1060
+ validationEndTimeInvalid: 'End time must be a non-negative number',
1061
+ validationStartBeforeEnd: 'Start time must be less than end time',
1062
+ validationTrimDurationTooLong:
1063
+ 'Trim duration cannot exceed {0} seconds',
1064
+ validationUploadUrlRequired: 'Upload URL must be a non-empty string',
1065
+ validationUploadUrlInvalid: 'Upload URL must be a valid URL',
1066
+ validationFileTooLarge:
1067
+ '{0} is too large ({1}MB). Maximum allowed size is {2}MB',
1068
+ validationAlbumIdRequired: 'Album ID must be a non-empty string',
1069
+ validationAlbumIdTooLong: 'Album ID cannot exceed {0} characters',
1070
+ validationDegreesMustBeNumber: 'Degrees must be a valid number',
1071
+ validationDegreesInvalid: 'Degrees must be one of: 0, 90, 180, 270',
1072
+ validationWidthPositive: 'Width must be a positive number',
1073
+ validationHeightPositive: 'Height must be a positive number',
1074
+ validationXNumber: 'X coordinate must be a number',
1075
+ validationYNumber: 'Y coordinate must be a number',
1076
+ validationFilterTypeInvalid: 'Invalid filter type: {0}. Valid options: {1}',
1077
+ validationFlashModeInvalid: 'Invalid flash mode: {0}. Valid options: {1}',
1078
+ validationFaceEffectInvalid: 'Invalid face effect: {0}. Valid options: {1}',
1079
+ validationThemeInvalid: 'Invalid camera theme: {0}. Valid options: {1}',
1080
+
1081
+ errorCameraPermission: 'Camera permission is required to use this feature',
1082
+ errorPleaseEnableCameraPermission:
1083
+ 'Please enable camera permission in settings',
1084
+ errorMicrophonePermission:
1085
+ 'Microphone permission is required for video recording',
1086
+ errorMicrophoneDeniedSilentVideo:
1087
+ 'Microphone access denied — this video will be recorded without sound',
1088
+ errorStoragePermission: 'Storage permission is required to save media',
1089
+ errorGalleryPermission: 'Gallery permission is required to browse your media',
1090
+ errorInvalidUri: 'Invalid media file',
1091
+ errorFileNotFound: 'Media file not found',
1092
+ errorFileTooLarge: 'File is too large to process',
1093
+ errorOperationCancelled: 'Operation was cancelled',
1094
+ errorCameraUnavailable: 'Camera is not available on this device',
1095
+ errorInsufficientStorage: 'Not enough storage space available',
1096
+ errorStorageFullVideo: 'Not enough storage space to record a video.',
1097
+ noticeStorageFullStopped: 'Storage is full — recording stopped.',
1098
+ storageAlmostFullTitle: 'Storage almost full',
1099
+ storageAlmostFullMessage:
1100
+ 'Storage space is almost full. The current recording will be finalized to prevent data loss.',
1101
+ memoryCriticalTitle: 'Low memory',
1102
+ memoryCriticalMessage:
1103
+ 'Your device is running critically low on memory. The current recording will be finalized to prevent crashes or data loss.',
1104
+ next: 'Next',
1105
+ errorRecordingFailed: 'Recording could not be saved. Please try again.',
1106
+ errorNetworkUnavailable: 'Network connection is required',
1107
+ errorUploadFailed: 'Failed to upload media',
1108
+ errorMediaLoadFailed: 'Unable to open the selected media',
1109
+ errorUnknown: 'An error occurred',
1110
+
1111
+ statusReady: 'Ready',
1112
+ statusProcessingWithProgress: 'Processing... {0}%',
1113
+ statusProcessing: 'Processing...',
1114
+ statusSuccess: 'Completed successfully',
1115
+ statusUnknown: 'Unknown status',
1116
+
1117
+ // ===== Multilingual coverage additions =====
1118
+ unknown: 'Unknown',
1119
+ untitled: 'Untitled',
1120
+ preview: 'Preview',
1121
+ media: 'Media',
1122
+ image: 'Image',
1123
+ video: 'Video',
1124
+ deleteConfirmTitle: 'Delete this item?',
1125
+ deleteConfirmMessage:
1126
+ 'Are you sure you want to delete this item? This action cannot be undone.',
1127
+ notSecurelyErasedTitle: 'Removed, but not securely erased',
1128
+ notSecurelyErasedMessage:
1129
+ 'This item was removed from your library, but its file data could not be fully overwritten. It may still be recoverable from device storage.',
1130
+ deleteMedia: 'Delete media',
1131
+ shareMedia: 'Share media',
1132
+ toolFilter: 'Filter',
1133
+ toolCrop: 'Crop',
1134
+ toolText: 'Text',
1135
+ toolEmoji: 'Emoji',
1136
+ toolGrid: 'Grid',
1137
+ emoji: 'Emoji',
1138
+ stickers: 'Stickers',
1139
+ toolGif: 'GIF',
1140
+ gifPickerTitle: 'Add GIF',
1141
+ gifSearchPlaceholder: 'Search GIFs',
1142
+ gifTrending: 'Trending',
1143
+ gifCategories: 'Categories',
1144
+ noGifsFound: 'No GIFs found',
1145
+ gifLoadError: "Couldn't load GIFs",
1146
+ gifOfflineError: 'No internet connection',
1147
+ gifNotConfigured: 'GIF picker is not set up',
1148
+ gifRateLimited: 'Too many requests. Try again shortly.',
1149
+ gifLoading: 'Loading GIFs…',
1150
+ deleteGif: 'Delete GIF',
1151
+ categoryGestures: 'Gestures',
1152
+ playVideo: 'Play video',
1153
+ pauseVideo: 'Pause video',
1154
+ deleteSticker: 'Delete sticker',
1155
+ closeEditor: 'Close editor',
1156
+ download: 'Download',
1157
+ selectedVideo: 'Selected video',
1158
+ selectedPhoto: 'Selected photo',
1159
+ removeSelectedItem: 'Remove selected item',
1160
+ addMoreMedia: 'Add more media',
1161
+ compressingPleaseWait: 'Compressing, please wait',
1162
+ compressingEllipsis: 'Compressing…',
1163
+ selectFilterTitle: 'Select Filter',
1164
+ filterAccessibilityLabel: '{0} filter',
1165
+ closeFilters: 'Close filters',
1166
+ recordAudio: 'Record Audio',
1167
+ browseAudio: 'Browse Audio',
1168
+ camera: 'Camera',
1169
+ closeActionsMenu: 'Close actions menu',
1170
+ openActionsMenu: 'Open actions menu',
1171
+ micPermanentlyDenied:
1172
+ 'Microphone permission permanently denied. Please enable it in Settings.',
1173
+ micPermissionDenied: 'Microphone permission denied.',
1174
+ micPermissionRequestFailed: 'Failed to request microphone permission.',
1175
+ failedToStartRecording: 'Failed to start recording',
1176
+ micPermissionNotGranted: 'Microphone permission not granted.',
1177
+ recordingTooShort: 'Recording too short. Hold for at least 0.5 seconds.',
1178
+ failedToSaveRecording: 'Failed to save recording',
1179
+ requestingPermission: 'Requesting Permission',
1180
+ allowMicAccess: 'Please allow microphone access to record audio.',
1181
+ recordingError: 'Recording Error',
1182
+ tryAgain: 'Try Again',
1183
+ pausedBadge: 'PAUSED',
1184
+ paused: 'Paused',
1185
+ tapToStartRecording: 'Tap the button below to start recording',
1186
+ discard: 'Discard',
1187
+ pause: 'Pause',
1188
+ resume: 'Resume',
1189
+ savingRecording: 'Saving recording…',
1190
+ micRationaleMessage: 'This app needs microphone access to record audio.',
1191
+ unknownSize: 'Unknown size',
1192
+ noAppToOpenFile: 'No app available to open this file.',
1193
+ couldNotOpenFile: 'Could not open this file.',
1194
+ pageCountSingular: '{0} page',
1195
+ pageCountPlural: '{0} pages',
1196
+ fileCouldNotBePreviewed: 'This file could not be previewed.',
1197
+ noInAppPreview: 'No in-app preview for this format.',
1198
+ openNamed: 'Open {0}',
1199
+ open: 'Open',
1200
+ opening: 'Opening…',
1201
+ compressing: 'Compressing...',
1202
+ errorProcessPhoto: 'Failed to process captured photo.',
1203
+ errorCapturePhoto: 'Failed to capture photo. Please try again.',
1204
+ uploadErrorCouldNotOpenStream: 'Could not open the file to upload',
1205
+ uploadErrorCouldNotOpenFile: 'Could not open file: {0}',
1206
+ uploadErrorServerCode: 'Server returned error code: {0}',
1207
+ uploadErrorNetwork: 'Network error: {0}',
1208
+ powerPointPresentationTitle: 'PowerPoint presentation',
1209
+ couldNotOpenPresentation: 'Could not open presentation: {0}',
1210
+ couldNotReadPowerPoint: 'Could not read PowerPoint presentation: {0}',
1211
+ couldNotOpenDocument: 'Could not open document: {0}',
1212
+ couldNotConvertDocument: 'Could not convert document: {0}',
1213
+ officeBinaryPreviewNote:
1214
+ 'Text preview shown below — open in PowerPoint, Google Slides, or WPS Office for full formatting and images.',
1215
+ unableToExtractBinaryText:
1216
+ 'Unable to extract text from this binary file. Open in PowerPoint for the best experience.',
1217
+ openInOfficeAppNote:
1218
+ 'Open this file in PowerPoint, Google Slides, or WPS Office for the best viewing experience.',
1219
+ previewTruncatedRowsCols:
1220
+ 'Preview truncated — showing first {0} rows / {1} columns',
1221
+ previewTruncatedRows: 'Preview truncated — showing first {0} rows',
1222
+ previewTruncatedChapters: 'Preview truncated — first {0} chapters',
1223
+ previewTruncatedNotice: 'Preview truncated',
1224
+ powerPointNoSlides: 'No slides',
1225
+ unknownDate: 'Unknown date',
1226
+ aspectRatioOriginal: 'Original',
1227
+ aspectRatioFull: 'Full',
1228
+ aspectRatioOriginalShort: 'Original',
1229
+ gifBadge: 'GIF',
1230
+ textFontSample: 'Aa',
1231
+ slideNumberLabel: 'SLIDE {0}',
1232
+ docTypePowerpoint: 'PowerPoint Presentation',
1233
+ docTypeWord: 'Word Document',
1234
+ docTypeExcel: 'Excel Spreadsheet',
1235
+ docTypeOffice: 'Office Document',
1236
+ compressActiveDesc: 'Quality: {0}%',
1237
+ convertGifMessage:
1238
+ 'Editing this GIF will remove its animation and save it as a still image.',
1239
+ convertGifCropMessage:
1240
+ 'Cropping this GIF will remove its animation and save it as a still image.',
1241
+ couldNotDecodeImageGeneric: 'Could not decode image',
1242
+ memoryWarningMessage:
1243
+ 'Low memory detected. Background operations are being reduced to keep recording stable.',
1244
+ savingEdits: 'Saving edits...',
1245
+ textCancel: 'Cancel text',
1246
+ textFont: 'Change font',
1247
+ textAlignment: 'Change text alignment',
1248
+ textBackground: 'Change text background',
1249
+ textDone: 'Apply text',
1250
+ // GIF picker / overlay (native MediaPreviewEditor). gifNotConfigured, gifRateLimited
1251
+ // and gifLoadError already have defaults in the "Editor GIF picker" section above.
1252
+ gifSearchHint: 'Search GIFs',
1253
+ addingGif: 'Adding GIF…',
1254
+ gifLoadFailed: "Couldn't load that GIF",
1255
+ gifOffline: 'No internet connection',
1256
+ gifTimeout: 'The request timed out',
1257
+ gifServerError: 'The GIF service is unavailable',
1258
+ gifNoResults: 'No GIFs found',
1259
+ // Media library shell — details sheet, metadata rows and formatting units
1260
+ mediaDetails: 'Media details',
1261
+ photoDetails: 'Photo details',
1262
+ videoDetails: 'Video details',
1263
+ metaFileName: 'File name',
1264
+ metaFileType: 'File type',
1265
+ metaFileSize: 'File size',
1266
+ metaDuration: 'Duration',
1267
+ metaResolution: 'Resolution',
1268
+ metaDate: 'Date',
1269
+ metaPath: 'Path',
1270
+ pixelsUnit: 'px',
1271
+ viewDetails: 'View details',
1272
+ fileTypeGeneric: 'FILE',
1273
+ audioTypeGeneric: 'AUD',
1274
+ // Full-screen viewer video controls
1275
+ play: 'Play',
1276
+ toggleVideoControls: 'Toggle video controls',
1277
+ seekBackward: 'Rewind {0} seconds',
1278
+ seekForward: 'Forward {0} seconds',
1279
+ };
1280
+
1281
+ export const cameraLocalizationKeys = Object.freeze(
1282
+ Object.keys(defaultTranslations) as CameraTranslationKey[]
1283
+ );
1284
+
1285
+ function normalizeTranslations(
1286
+ translations?: CameraTranslationMap
1287
+ ): Partial<CameraTranslations> {
1288
+ if (!translations) return {};
1289
+
1290
+ return cameraLocalizationKeys.reduce<Partial<CameraTranslations>>(
1291
+ (acc, key) => {
1292
+ const value = translations[key];
1293
+ if (typeof value === 'string' && value.length > 0) {
1294
+ acc[key] = value;
1295
+ }
1296
+ return acc;
1297
+ },
1298
+ {}
1299
+ );
1300
+ }
1301
+
1302
+ function formatMessage(
1303
+ message: string,
1304
+ args: Array<string | number | boolean | null | undefined>
1305
+ ): string {
1306
+ return args.reduce<string>(
1307
+ (text, arg, index) =>
1308
+ text.split(`{${index}}`).join(arg == null ? '' : String(arg)),
1309
+ message
1310
+ );
1311
+ }
1312
+
1313
+ function resolveIsRtl(isRtl?: boolean, locale?: string): boolean {
1314
+ // Precedence: an explicit override, then the direction implied by the active
1315
+ // language, then whatever direction the host app itself is running in.
1316
+ if (typeof isRtl === 'boolean') return isRtl;
1317
+ if (locale && normalizeLocale(locale).length > 0) return isRtlLocale(locale);
1318
+ return I18nManager.isRTL;
1319
+ }
1320
+
1321
+ /**
1322
+ * Normalizes whatever a host's `subscribe` returned into a plain unsubscriber.
1323
+ *
1324
+ * Returning `undefined` for an unrecognized shape is deliberate: the binding
1325
+ * still works, it simply cannot be released — better than calling an arbitrary
1326
+ * method on a host object and guessing wrong.
1327
+ */
1328
+ function toLanguageUnsubscribe(
1329
+ subscription: CameraLanguageSubscription
1330
+ ): (() => void) | undefined {
1331
+ if (typeof subscription === 'function') return subscription;
1332
+ if (typeof subscription !== 'object' || subscription === null) return undefined;
1333
+
1334
+ const remove = (subscription as { remove?: unknown }).remove;
1335
+ if (typeof remove === 'function') {
1336
+ return () => (remove as () => void).call(subscription);
1337
+ }
1338
+
1339
+ const unsubscribe = (subscription as { unsubscribe?: unknown }).unsubscribe;
1340
+ if (typeof unsubscribe === 'function') {
1341
+ return () => (unsubscribe as () => void).call(subscription);
1342
+ }
1343
+
1344
+ return undefined;
1345
+ }
1346
+
1347
+ /**
1348
+ * True when a value looks like a {@link CameraLanguageProvider} rather than a
1349
+ * language tag. Lets a single `language` option accept either one.
1350
+ */
1351
+ export function isCameraLanguageProvider(
1352
+ value: unknown
1353
+ ): value is CameraLanguageProvider {
1354
+ if (typeof value !== 'object' || value === null) return false;
1355
+ const candidate = value as Partial<CameraLanguageProvider>;
1356
+ return (
1357
+ typeof candidate.get === 'function' && typeof candidate.subscribe === 'function'
1358
+ );
1359
+ }
1360
+
1361
+ class LocalizationManagerImpl {
1362
+ private translateFn?: CameraTranslationFunction;
1363
+ private hostTranslations?: CameraTranslationMap;
1364
+ private bundles: CameraTranslationBundles = {};
1365
+ private fallbackLocale: string = DEFAULT_LOCALE;
1366
+ private locale = '';
1367
+ private explicitIsRtl?: boolean;
1368
+ private translations: CameraTranslations = { ...defaultTranslations };
1369
+ private isRtl = resolveIsRtl();
1370
+ private listeners = new Set<() => void>();
1371
+ /**
1372
+ * The host's language authority, when one is bound. Held by identity so a
1373
+ * re-bind of the same object is recognized as a no-op.
1374
+ */
1375
+ private languageProvider?: CameraLanguageProvider;
1376
+ /** Releases {@link languageProvider}'s subscription. One at a time, always. */
1377
+ private releaseProviderSubscription?: () => void;
1378
+
1379
+ /**
1380
+ * Binds the host app's language provider — the plugin's language authority for
1381
+ * as long as it stays bound.
1382
+ *
1383
+ * Leak- and duplicate-safe by construction: the previous subscription is always
1384
+ * released before a new one is taken, and re-binding the *same* provider object
1385
+ * only re-reads it, so a startup path that runs more than once (Fast Refresh, a
1386
+ * remount, a second `configureCameraPlugin`) cannot stack listeners.
1387
+ *
1388
+ * @param options.resolve `false` for callers that are about to
1389
+ * {@link LocalizationManagerImpl.resolve} anyway, keeping a config change to a
1390
+ * single re-render and a single native push.
1391
+ */
1392
+ public bindLanguageProvider(
1393
+ provider?: CameraLanguageProvider | null,
1394
+ options: { resolve?: boolean } = {}
1395
+ ): void {
1396
+ const shouldResolve = options.resolve !== false;
1397
+
1398
+ if (provider && provider === this.languageProvider) {
1399
+ // Already bound: adopt whatever it reads now, in case the host changed
1400
+ // language through a path that does not notify.
1401
+ if (this.adoptProviderLocale() && shouldResolve) this.resolve();
1402
+ return;
1403
+ }
1404
+
1405
+ this.releaseLanguageProvider();
1406
+ if (!provider) return;
1407
+
1408
+ this.languageProvider = provider;
1409
+
1410
+ // Adopt the CURRENT language before subscribing, so a provider bound at
1411
+ // startup takes effect without waiting for a first change event, and so a
1412
+ // provider that notifies synchronously on subscribe finds state in place.
1413
+ const changed = this.adoptProviderLocale();
1414
+
1415
+ let subscription: CameraLanguageSubscription;
1416
+ try {
1417
+ subscription = provider.subscribe((next) => {
1418
+ this.handleProviderLanguageChange(next);
1419
+ });
1420
+ } catch {
1421
+ // A provider that cannot be subscribed to still drives the language it
1422
+ // reported above; it just will not push later changes.
1423
+ subscription = undefined;
1424
+ }
1425
+ this.releaseProviderSubscription = toLanguageUnsubscribe(subscription);
1426
+
1427
+ if (changed && shouldResolve) this.resolve();
1428
+ }
1429
+
1430
+ /** Detaches the bound provider, if any. The active language is left as-is. */
1431
+ public unbindLanguageProvider(): void {
1432
+ this.releaseLanguageProvider();
1433
+ }
1434
+
1435
+ /**
1436
+ * Re-reads the bound provider and republishes if its language moved. The manual
1437
+ * escape hatch for a host whose provider cannot notify.
1438
+ */
1439
+ public syncLanguageFromProvider(): void {
1440
+ if (!this.languageProvider) return;
1441
+ if (this.adoptProviderLocale()) this.resolve();
1442
+ }
1443
+
1444
+ /** The bound provider, for hosts that need to check whether auto-sync is on. */
1445
+ public getLanguageProvider(): CameraLanguageProvider | undefined {
1446
+ return this.languageProvider;
1447
+ }
1448
+
1449
+ private releaseLanguageProvider(): void {
1450
+ const release = this.releaseProviderSubscription;
1451
+ this.releaseProviderSubscription = undefined;
1452
+ this.languageProvider = undefined;
1453
+ if (!release) return;
1454
+ try {
1455
+ release();
1456
+ } catch {
1457
+ // Teardown is host-owned; a throwing unsubscriber must not break re-binding.
1458
+ }
1459
+ }
1460
+
1461
+ private readProviderLocale(): string | null | undefined {
1462
+ try {
1463
+ return this.languageProvider?.get();
1464
+ } catch {
1465
+ // A throwing `get()` must not silently drop the plugin back to English.
1466
+ return this.locale;
1467
+ }
1468
+ }
1469
+
1470
+ /**
1471
+ * Pulls the provider's language into `locale`. Returns whether it moved, which
1472
+ * is what lets callers skip a redundant re-render and native push.
1473
+ */
1474
+ private adoptProviderLocale(): boolean {
1475
+ const next = normalizeLocale(this.readProviderLocale());
1476
+ if (next === this.locale) return false;
1477
+ this.locale = next;
1478
+ return true;
1479
+ }
1480
+
1481
+ private handleProviderLanguageChange(next?: string | null): void {
1482
+ // The tag may arrive with the notification (i18next passes `lng`) or not at
1483
+ // all, in which case `get()` is the authority.
1484
+ const reported = typeof next === 'string' ? next : this.readProviderLocale();
1485
+ const normalized = normalizeLocale(reported);
1486
+ if (normalized === this.locale) return;
1487
+ this.locale = normalized;
1488
+ this.resolve();
1489
+ }
1490
+
1491
+ /**
1492
+ * Re-asserts a bound provider's language over any `locale` a config carried, so
1493
+ * the host stays the single source of truth even when the two disagree.
1494
+ */
1495
+ private assertLanguageAuthority(): void {
1496
+ if (this.languageProvider) this.adoptProviderLocale();
1497
+ }
1498
+
1499
+ /**
1500
+ * Replaces the whole localization configuration. Anything omitted is cleared,
1501
+ * which is what makes `setCameraLocalization({})` a reset.
1502
+ *
1503
+ * A bound language provider is the one exception: it is only touched when the
1504
+ * config actually carries a `languageProvider` key. Otherwise a screen that
1505
+ * pushes its own bundles would silently switch off auto-sync that was set up at
1506
+ * startup. Pass `languageProvider: null` to detach on purpose.
1507
+ */
1508
+ public setLocalization(config: CameraLocalizationConfig = {}) {
1509
+ this.translateFn = config.translate;
1510
+ this.hostTranslations = config.translations;
1511
+ this.bundles = config.bundles ? { ...config.bundles } : {};
1512
+ this.fallbackLocale = normalizeLocale(config.fallbackLocale) || DEFAULT_LOCALE;
1513
+ this.locale = normalizeLocale(config.locale);
1514
+ this.explicitIsRtl = config.isRtl;
1515
+
1516
+ if ('languageProvider' in config) {
1517
+ this.bindLanguageProvider(config.languageProvider, { resolve: false });
1518
+ }
1519
+ this.assertLanguageAuthority();
1520
+
1521
+ this.resolve();
1522
+ }
1523
+
1524
+ /**
1525
+ * Merges a partial configuration into the current one. Use this to add bundles
1526
+ * or change the language without re-sending everything else.
1527
+ */
1528
+ public updateLocalization(config: CameraLocalizationConfig = {}) {
1529
+ if ('translate' in config) this.translateFn = config.translate;
1530
+ if ('translations' in config) this.hostTranslations = config.translations;
1531
+ if (config.bundles) this.bundles = { ...this.bundles, ...config.bundles };
1532
+ if (config.fallbackLocale !== undefined) {
1533
+ this.fallbackLocale = normalizeLocale(config.fallbackLocale) || DEFAULT_LOCALE;
1534
+ }
1535
+ if (config.locale !== undefined) this.locale = normalizeLocale(config.locale);
1536
+ if ('isRtl' in config) this.explicitIsRtl = config.isRtl;
1537
+
1538
+ if ('languageProvider' in config) {
1539
+ this.bindLanguageProvider(config.languageProvider, { resolve: false });
1540
+ }
1541
+ this.assertLanguageAuthority();
1542
+
1543
+ this.resolve();
1544
+ }
1545
+
1546
+ /**
1547
+ * Switches the active language, keeping registered bundles and callbacks.
1548
+ *
1549
+ * Remains available as a manual fallback while a provider is bound; the next
1550
+ * change the provider reports simply wins, since the host owns the language.
1551
+ */
1552
+ public setLanguage(locale?: string | null): void {
1553
+ this.locale = normalizeLocale(locale);
1554
+ this.resolve();
1555
+ }
1556
+
1557
+ /**
1558
+ * Rebuilds the resolved translation table and text direction, pushes both to
1559
+ * the native camera, and notifies every subscribed component.
1560
+ *
1561
+ * Priority, lowest to highest: shipped English defaults, locale bundles along
1562
+ * the fallback chain, the host's translate callback, then explicit overrides.
1563
+ */
1564
+ private resolve(): void {
1565
+ this.isRtl = resolveIsRtl(this.explicitIsRtl, this.locale);
1566
+
1567
+ const bundleTranslations = normalizeTranslations(
1568
+ resolveLocaleTranslations<CameraTranslationMap>(
1569
+ this.locale,
1570
+ this.bundles,
1571
+ this.fallbackLocale
1572
+ )
1573
+ );
1574
+
1575
+ const generatedTranslations: Partial<CameraTranslations> = {};
1576
+ if (this.translateFn) {
1577
+ cameraLocalizationKeys.forEach((key) => {
1578
+ try {
1579
+ const translated = this.translateFn?.(key);
1580
+ if (typeof translated === 'string' && translated.length > 0) {
1581
+ generatedTranslations[key] = translated;
1582
+ }
1583
+ } catch {
1584
+ // Translation callbacks are host-owned; missing keys fall back below.
1585
+ }
1586
+ });
1587
+ }
1588
+
1589
+ this.translations = {
1590
+ ...defaultTranslations,
1591
+ ...bundleTranslations,
1592
+ ...generatedTranslations,
1593
+ ...normalizeTranslations(this.hostTranslations),
1594
+ };
1595
+
1596
+ try {
1597
+ NativeModules.CustomCamera?.setLocalization?.(
1598
+ this.translations,
1599
+ this.isRtl,
1600
+ this.locale
1601
+ );
1602
+ } catch {
1603
+ // Native localization is best-effort for test, web, and non-Android contexts.
1604
+ }
1605
+
1606
+ this.notifyListeners();
1607
+ }
1608
+
1609
+ public subscribe(listener: () => void): () => void {
1610
+ this.listeners.add(listener);
1611
+ return () => {
1612
+ this.listeners.delete(listener);
1613
+ };
1614
+ }
1615
+
1616
+ public getString(
1617
+ key: CameraTranslationKey,
1618
+ ...args: Array<string | number | boolean | null | undefined>
1619
+ ): string {
1620
+ let text: string | undefined | null;
1621
+
1622
+ if (this.translateFn) {
1623
+ try {
1624
+ text = this.translateFn(key, ...args);
1625
+ } catch {
1626
+ text = undefined;
1627
+ }
1628
+ }
1629
+
1630
+ const message =
1631
+ typeof text === 'string' && text.length > 0
1632
+ ? text
1633
+ : this.translations[key] || defaultTranslations[key] || String(key);
1634
+
1635
+ return formatMessage(message, args);
1636
+ }
1637
+
1638
+ public getIsRtl(): boolean {
1639
+ return this.isRtl;
1640
+ }
1641
+
1642
+ /** The active language, or `''` when the host has not set one. */
1643
+ public getLocale(): string {
1644
+ return this.locale;
1645
+ }
1646
+
1647
+ /** The language used to fill gaps in the active one. `'en'` unless changed. */
1648
+ public getFallbackLocale(): string {
1649
+ return this.fallbackLocale;
1650
+ }
1651
+
1652
+ /** Every locale the host has registered a bundle for. */
1653
+ public getAvailableLocales(): string[] {
1654
+ return Object.keys(this.bundles);
1655
+ }
1656
+
1657
+ public getTextAlign(): 'left' | 'right' {
1658
+ return this.isRtl ? 'right' : 'left';
1659
+ }
1660
+
1661
+ public getFlexDirection(): 'row' | 'row-reverse' {
1662
+ return this.isRtl ? 'row-reverse' : 'row';
1663
+ }
1664
+
1665
+ public getTranslations(): CameraTranslations {
1666
+ return this.translations;
1667
+ }
1668
+
1669
+ private notifyListeners(): void {
1670
+ this.listeners.forEach((listener) => listener());
1671
+ }
1672
+ }
1673
+
1674
+ export const LocalizationManager = new LocalizationManagerImpl();
1675
+
1676
+ /**
1677
+ * Hands the host app's language and translations to the plugin. This is the
1678
+ * single integration point — call it once at startup, and either bind a
1679
+ * `languageProvider` so later switches are picked up automatically or call
1680
+ * {@link setCameraLanguage} on each change.
1681
+ *
1682
+ * ```ts
1683
+ * setCameraLocalization({
1684
+ * bundles: myBundles,
1685
+ * // Bound once; every later host language change follows automatically.
1686
+ * languageProvider: {
1687
+ * get: () => i18n.language,
1688
+ * subscribe: (onChange) => {
1689
+ * i18n.on('languageChanged', onChange);
1690
+ * return () => i18n.off('languageChanged', onChange);
1691
+ * },
1692
+ * },
1693
+ * translate: (key) => i18n.t(`camera.${key}`),
1694
+ * });
1695
+ * ```
1696
+ *
1697
+ * Every field is optional and omitted fields are cleared, so `{}` restores the
1698
+ * shipped English strings — with one deliberate exception: a language provider
1699
+ * bound by {@link setCameraLanguageProvider} is kept unless the config explicitly
1700
+ * carries `languageProvider`, so automatic synchronization set up once at startup
1701
+ * cannot be switched off by a later config push.
1702
+ */
1703
+ export function setCameraLocalization(
1704
+ config: CameraLocalizationConfig = {}
1705
+ ): void {
1706
+ LocalizationManager.setLocalization(config);
1707
+ }
1708
+
1709
+ /**
1710
+ * Merges into the current configuration instead of replacing it. Handy for
1711
+ * registering more bundles later, or changing one field on its own.
1712
+ */
1713
+ export function updateCameraLocalization(
1714
+ config: CameraLocalizationConfig = {}
1715
+ ): void {
1716
+ LocalizationManager.updateLocalization(config);
1717
+ }
1718
+
1719
+ /**
1720
+ * Turns on one-time automatic language synchronization: bind the host app's
1721
+ * language once and every plugin surface follows it from then on.
1722
+ *
1723
+ * On each host language change the plugin re-resolves its translation table,
1724
+ * re-renders every React Native screen and component, and pushes the new
1725
+ * translations, locale, and RTL direction across the bridge to the native Android
1726
+ * camera and editor — no further calls needed.
1727
+ *
1728
+ * Safe to call more than once: the same provider object re-binds without stacking
1729
+ * a listener, a different one releases the previous subscription first, and `null`
1730
+ * detaches (after which {@link setCameraLanguage} is the way to switch).
1731
+ *
1732
+ * ```ts
1733
+ * // i18next
1734
+ * setCameraLanguageProvider({
1735
+ * get: () => i18n.language,
1736
+ * subscribe: (onChange) => {
1737
+ * i18n.on('languageChanged', onChange);
1738
+ * return () => i18n.off('languageChanged', onChange);
1739
+ * },
1740
+ * });
1741
+ *
1742
+ * // a plain store / context
1743
+ * setCameraLanguageProvider({
1744
+ * get: () => store.getState().language,
1745
+ * subscribe: (onChange) => store.subscribe(onChange),
1746
+ * });
1747
+ * ```
1748
+ */
1749
+ export function setCameraLanguageProvider(
1750
+ provider?: CameraLanguageProvider | null
1751
+ ): void {
1752
+ LocalizationManager.bindLanguageProvider(provider);
1753
+ }
1754
+
1755
+ /** Turns automatic synchronization back off. The active language is kept. */
1756
+ export function clearCameraLanguageProvider(): void {
1757
+ LocalizationManager.unbindLanguageProvider();
1758
+ }
1759
+
1760
+ /**
1761
+ * Re-reads the bound language provider and republishes if it moved. Only needed
1762
+ * for a host whose language can change without notifying — normally the
1763
+ * subscription covers this.
1764
+ */
1765
+ export function syncCameraLanguage(): void {
1766
+ LocalizationManager.syncLanguageFromProvider();
1767
+ }
1768
+
1769
+ /** The bound language provider, or `undefined` when auto-sync is off. */
1770
+ export function getCameraLanguageProvider(): CameraLanguageProvider | undefined {
1771
+ return LocalizationManager.getLanguageProvider();
1772
+ }
1773
+
1774
+ /**
1775
+ * Switches the plugin to the host's newly selected language — the manual
1776
+ * alternative to {@link setCameraLanguageProvider}, and still supported.
1777
+ *
1778
+ * Registered bundles and the host's translate callback are kept, so this is the
1779
+ * one call to make from a language-change handler. Text direction is re-derived
1780
+ * from the tag, so Arabic and Persian flip the plugin to RTL automatically. Open
1781
+ * plugin screens and the native camera update immediately — no restart.
1782
+ *
1783
+ * ```ts
1784
+ * i18n.on('languageChanged', setCameraLanguage);
1785
+ * ```
1786
+ */
1787
+ export function setCameraLanguage(
1788
+ locale?: string | null | CameraLanguageProvider
1789
+ ): void {
1790
+ // Accepting a provider here too means a host that reaches for the familiar
1791
+ // function gets automatic synchronization instead of a mangled language tag.
1792
+ if (isCameraLanguageProvider(locale)) {
1793
+ LocalizationManager.bindLanguageProvider(locale);
1794
+ return;
1795
+ }
1796
+ LocalizationManager.setLanguage(locale);
1797
+ }
1798
+
1799
+ /** Registers (or replaces) translations for one or more languages. */
1800
+ export function registerCameraTranslations(
1801
+ bundles: CameraTranslationBundles
1802
+ ): void {
1803
+ LocalizationManager.updateLocalization({ bundles });
1804
+ }
1805
+
1806
+ /** The plugin's active language, or `''` when the host has not set one. */
1807
+ export function getCameraLocale(): string {
1808
+ return LocalizationManager.getLocale();
1809
+ }
1810
+
1811
+ /** Whether plugin UI is currently laid out right-to-left. */
1812
+ export function getCameraIsRtl(): boolean {
1813
+ return LocalizationManager.getIsRtl();
1814
+ }
1815
+
1816
+ type CameraLocalizationContextValue = ReturnType<typeof useCameraLocalization>;
1817
+
1818
+ const CameraLocalizationContext =
1819
+ createContext<CameraLocalizationContextValue | null>(null);
1820
+
1821
+ export function CameraLocalizationProvider({
1822
+ children,
1823
+ translate,
1824
+ translations,
1825
+ bundles,
1826
+ fallbackLocale,
1827
+ isRtl,
1828
+ locale,
1829
+ languageProvider,
1830
+ version,
1831
+ }: CameraLocalizationProviderProps) {
1832
+ useEffect(() => {
1833
+ setCameraLocalization({
1834
+ // A non-string `locale` is a legacy cache-busting token, not a language tag.
1835
+ locale: typeof locale === 'string' ? locale : undefined,
1836
+ translate,
1837
+ translations,
1838
+ bundles,
1839
+ fallbackLocale,
1840
+ isRtl,
1841
+ // Spread rather than always passing the key: an absent prop must leave a
1842
+ // provider bound elsewhere (at startup) alone, while `null` still detaches.
1843
+ ...(languageProvider === undefined ? null : { languageProvider }),
1844
+ });
1845
+ }, [
1846
+ translate,
1847
+ translations,
1848
+ bundles,
1849
+ fallbackLocale,
1850
+ isRtl,
1851
+ locale,
1852
+ languageProvider,
1853
+ version,
1854
+ ]);
1855
+
1856
+ const value = useCameraLocalization();
1857
+
1858
+ return React.createElement(
1859
+ CameraLocalizationContext.Provider,
1860
+ { value },
1861
+ children
1862
+ );
1863
+ }
1864
+
1865
+ export function useCameraLocalization() {
1866
+ const [revision, setRevision] = useState(0);
1867
+
1868
+ useEffect(() => {
1869
+ return LocalizationManager.subscribe(() => {
1870
+ setRevision((tick) => tick + 1);
1871
+ });
1872
+ }, []);
1873
+
1874
+ return useMemo(() => {
1875
+ const isRtl = LocalizationManager.getIsRtl();
1876
+ return {
1877
+ t: (
1878
+ key: CameraTranslationKey,
1879
+ ...args: Array<string | number | boolean | null | undefined>
1880
+ ) => LocalizationManager.getString(key, ...args),
1881
+ /** The host's active language, or `''` when it has not set one. */
1882
+ locale: LocalizationManager.getLocale(),
1883
+ isRtl,
1884
+ direction: isRtl ? 'rtl' : 'ltr',
1885
+ textAlign: LocalizationManager.getTextAlign(),
1886
+ flexDirection: LocalizationManager.getFlexDirection(),
1887
+ rtlStyle: <T extends object>(ltr: T, rtl: T): T => (isRtl ? rtl : ltr),
1888
+ writingDirectionStyle: {
1889
+ writingDirection: isRtl ? 'rtl' : 'ltr',
1890
+ textAlign: isRtl ? 'right' : 'left',
1891
+ } as const,
1892
+ layoutDirectionStyle: {
1893
+ direction: isRtl ? 'rtl' : 'ltr',
1894
+ } as const,
1895
+ /** Row/text styles that follow the active language's direction. */
1896
+ styles: isRtl ? rtlDirectionalStyles.rtl : rtlDirectionalStyles.ltr,
1897
+ translations: LocalizationManager.getTranslations(),
1898
+ };
1899
+ }, [revision]);
1900
+ }
1901
+
1902
+ export function useCameraLocalizationContext() {
1903
+ const context = useContext(CameraLocalizationContext);
1904
+ const fallback = useCameraLocalization();
1905
+ return context ?? fallback;
1906
+ }
1907
+
1908
+ /**
1909
+ * Both directions, created up front. `StyleSheet.create` runs once at module
1910
+ * load, so the only way to stay correct across a live language switch is to
1911
+ * build both variants and choose between them at read time.
1912
+ */
1913
+ const rtlDirectionalStyles = {
1914
+ ltr: StyleSheet.create({
1915
+ row: { flexDirection: 'row' },
1916
+ text: { writingDirection: 'ltr', textAlign: 'left' },
1917
+ }),
1918
+ rtl: StyleSheet.create({
1919
+ row: { flexDirection: 'row-reverse' },
1920
+ text: { writingDirection: 'rtl', textAlign: 'right' },
1921
+ }),
1922
+ } as const;
1923
+
1924
+ /** Row/text styles for the active language. Prefer the hook inside components. */
1925
+ export function getRtlAwareStyles(): { row: ViewStyle; text: TextStyle } {
1926
+ return LocalizationManager.getIsRtl()
1927
+ ? rtlDirectionalStyles.rtl
1928
+ : rtlDirectionalStyles.ltr;
1929
+ }
1930
+
1931
+ /**
1932
+ * Direction-aware styles that re-read the active language on every access.
1933
+ *
1934
+ * Defined with getters rather than a plain object: the language can change at
1935
+ * any time, and a value captured at module load would keep an Arabic UI
1936
+ * left-to-right until the app restarted.
1937
+ */
1938
+ export const rtlAwareStyles: { readonly row: ViewStyle; readonly text: TextStyle } =
1939
+ {
1940
+ get row() {
1941
+ return getRtlAwareStyles().row;
1942
+ },
1943
+ get text() {
1944
+ return getRtlAwareStyles().text;
1945
+ },
1946
+ };
1947
+