Files
Eric FELIXINE e30ae8ed09 feat(smart-app): implement complete mobile app MVP
- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
2026-06-01 18:00:35 -04:00

239 lines
8.3 KiB
Swift

// Copyright 2021-present 650 Industries. All rights reserved.
import Foundation
import ExpoModulesCore
let LOCALE_SETTINGS_CHANGED = "onLocaleSettingsChanged"
let CALENDAR_SETTINGS_CHANGED = "onCalendarSettingsChanged"
public class LocalizationModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoLocalization")
Constants {
return Self.getCurrentLocalization()
}
AsyncFunction("getLocalizationAsync") {
return Self.getCurrentLocalization()
}
Function("getLocales") {
return Self.getLocales()
}
Function("getCalendars") {
return Self.getCalendars()
}
OnCreate {
if let forceRTL = Bundle.main.object(forInfoDictionaryKey: "ExpoLocalization_forcesRTL") as? Bool {
self.setRTLPreferences(true, forceRTL)
} else {
if let enableRTL = Bundle.main.object(forInfoDictionaryKey: "ExpoLocalization_supportsRTL") as? Bool {
self.setRTLPreferences(enableRTL, false)
}
}
}
Events(LOCALE_SETTINGS_CHANGED, CALENDAR_SETTINGS_CHANGED)
OnStartObserving {
NotificationCenter.default.addObserver(
self,
selector: #selector(LocalizationModule.localeChanged),
name: NSLocale.currentLocaleDidChangeNotification, // swiftlint:disable:this legacy_objc_type
object: nil
)
}
OnStopObserving {
NotificationCenter.default.removeObserver(
self,
name: NSLocale.currentLocaleDidChangeNotification, // swiftlint:disable:this legacy_objc_type
object: nil
)
}
}
func isRTLPreferredForCurrentLocale() -> Bool {
// swiftlint:disable:next legacy_objc_type
return NSLocale.characterDirection(forLanguage: NSLocale.preferredLanguages.first ?? "en-US") == NSLocale.LanguageDirection.rightToLeft
}
func setRTLPreferences(_ supportsRTL: Bool, _ forceRTL: Bool) {
// These keys are used by React Native here: https://github.com/facebook/react-native/blob/main/React/Modules/RCTI18nUtil.m
// We set them before React loads to ensure it gets rendered correctly the first time the app is opened.
// On iOS we need to set both forceRTL and allowRTL so apps don't have to include localization strings.
// Uses required reason API based on the following reason: CA92.1
if forceRTL {
UserDefaults.standard.set(true, forKey: "RCTI18nUtil_allowRTL")
UserDefaults.standard.set(true, forKey: "RCTI18nUtil_forceRTL")
} else {
UserDefaults.standard.set(supportsRTL, forKey: "RCTI18nUtil_allowRTL")
UserDefaults.standard.set(supportsRTL ? isRTLPreferredForCurrentLocale() : false, forKey: "RCTI18nUtil_forceRTL")
}
UserDefaults.standard.synchronize()
}
// If the application isn't manually localized for the device language then the
// native `Locale.current` will fallback on using English US
// [cite](https://stackoverflow.com/questions/48136456/locale-current-reporting-wrong-language-on-device).
// This method will attempt to return the locale that the device is using regardless of the app,
// providing better parity across platforms.
static func getLocale() -> Locale {
guard let preferredIdentifier = Locale.preferredLanguages.first else {
return Locale.current
}
return Locale(identifier: preferredIdentifier)
}
/**
Maps ios unique identifiers to [BCP 47 calendar types]
(https://github.com/unicode-org/cldr/blob/main/common/bcp47/calendar.xml)
*/
static func getUnicodeCalendarIdentifier(calendar: Calendar) -> String {
switch calendar.identifier {
case .buddhist:
return "buddhist"
case .chinese:
return "chinese"
case .coptic:
return "coptic"
case .ethiopicAmeteAlem:
return "ethioaa"
case .ethiopicAmeteMihret:
return "ethiopic"
case .gregorian:
return "gregory"
case .hebrew:
return "hebrew"
case .indian:
return "indian"
case .islamic:
return "islamic"
case .islamicCivil:
return "islamic-civil"
case .islamicTabular:
return "islamic-tbla"
case .islamicUmmAlQura:
return "islamic-umalqura"
case .japanese:
return "japanese"
case .persian:
return "persian"
case .republicOfChina:
return "roc"
case .iso8601:
return "iso8601"
}
}
static func getMeasurementSystemForLocale(_ locale: Locale) -> String {
if #available(iOS 16, tvOS 16, *) {
let measurementSystems = [
Locale.MeasurementSystem.us: "us",
Locale.MeasurementSystem.uk: "uk",
Locale.MeasurementSystem.metric: "metric"
]
return measurementSystems[locale.measurementSystem] ?? "metric"
}
return locale.usesMetricSystem ? "metric" : "us"
}
static func getLocales() -> [[String: Any?]] {
let userSettingsLocale = Locale.current
return (Locale.preferredLanguages.isEmpty ? [Locale.current.identifier] : Locale.preferredLanguages)
.map { languageTag -> [String: Any?] in
let languageLocale = Locale.init(identifier: languageTag)
if #available(iOS 16, tvOS 16, *) {
return [
"languageTag": languageTag,
"languageCode": languageLocale.language.languageCode?.identifier,
"regionCode": languageLocale.region?.identifier,
"textDirection": languageLocale.language.characterDirection == .rightToLeft ? "rtl" : "ltr",
"decimalSeparator": userSettingsLocale.decimalSeparator,
"digitGroupingSeparator": userSettingsLocale.groupingSeparator,
"measurementSystem": getMeasurementSystemForLocale(userSettingsLocale),
"currencyCode": languageLocale.currencyCode,
"currencySymbol": languageLocale.currencySymbol,
"temperatureUnit": getTemperatureUnit()
]
}
return [
"languageTag": languageTag,
"languageCode": languageLocale.languageCode,
"regionCode": languageLocale.regionCode,
"textDirection": Locale.characterDirection(forLanguage: languageTag) == .rightToLeft ? "rtl" : "ltr",
"decimalSeparator": userSettingsLocale.decimalSeparator,
"digitGroupingSeparator": userSettingsLocale.groupingSeparator,
"measurementSystem": getMeasurementSystemForLocale(userSettingsLocale),
"currencyCode": languageLocale.currencyCode,
"currencySymbol": languageLocale.currencySymbol,
"temperatureUnit": getTemperatureUnit()
]
}
}
@objc
private func localeChanged() {
// we send both events since on iOS it means both calendar and locale needs an update
sendEvent(LOCALE_SETTINGS_CHANGED)
sendEvent(CALENDAR_SETTINGS_CHANGED)
}
static func getTemperatureUnit() -> String? {
let formatter = MeasurementFormatter()
formatter.locale = Locale.current
let temperature = Measurement(value: 0, unit: UnitTemperature.celsius)
let formatted = formatter.string(from: temperature)
guard let unitCharacter = formatted.last else {
return nil
}
return unitCharacter == "F" ? "fahrenheit" : "celsius"
}
// https://stackoverflow.com/a/28183182
static func uses24HourClock() -> Bool {
let dateFormat = DateFormatter.dateFormat(fromTemplate: "j", options: 0, locale: Locale.current)!
return dateFormat.firstIndex(of: "a") == nil
}
static func getCalendars() -> [[String: Any?]] {
var calendar = Locale.current.calendar
return [
[
"calendar": getUnicodeCalendarIdentifier(calendar: calendar),
"timeZone": "\(calendar.timeZone.identifier)",
"uses24hourClock": uses24HourClock(),
"firstWeekday": calendar.firstWeekday
]
]
}
static func getCurrentLocalization() -> [String: Any?] {
let locale = getLocale()
let languageCode = locale.languageCode ?? "en"
var languageIds = Locale.preferredLanguages
if languageIds.isEmpty {
languageIds.append("en-US")
}
return [
"currency": locale.currencyCode ?? "USD",
"decimalSeparator": locale.decimalSeparator ?? ".",
"digitGroupingSeparator": locale.groupingSeparator ?? ",",
"isoCurrencyCodes": Locale.isoCurrencyCodes,
"isMetric": locale.usesMetricSystem,
"isRTL": Locale.characterDirection(forLanguage: languageCode) == .rightToLeft,
"locale": languageIds.first,
"locales": languageIds,
"region": locale.regionCode ?? "US",
"timezone": TimeZone.current.identifier
]
}
}