🎯 Learning Path:
Welcome to the REL-ID Internationalization codelab! This tutorial builds upon your existing MFA implementation to add comprehensive multi-language support with dynamic language switching using REL-ID SDK's language management APIs.
In this codelab, you'll enhance your existing MFA application with:
initialize() with initOptionssetSDKLanguage() API without app restart after sdk initializationstrings.xml and iOS .strings files for error code mappingRDNALanguageDirection enumBy completing this codelab, you'll master:
initialize() with initOptions.internationalizationOptions and setSDKLanguage() APIonSetLanguageResponse callbacks for language updatesBefore starting this codelab, ensure you have:
The code to get started can be found in a GitHub repository.
You can clone the repository using the following command:
git clone https://github.com/uniken-public/codelab-flutter.git
Navigate to the relid-internationalization folder in the repository you cloned earlier
This codelab extends your MFA application with three core internationalization components:
onInitialized callbackinitOptions, setSDKLanguage() API and event handlers (onInitialized, onSetLanguageResponse)Before implementing internationalization, let's understand the two critical phases of language management with REL-ID SDK.
REL-ID SDK manages language in two distinct phases:
PHASE 1: SDK INITIALIZATION
↓
App starts → Load default languages → Get user's saved language preference →
Extract short language code (e.g., 'en-US' → 'en') →
Call initialize() with initOptions.internationalizationOptions.localeCode = 'en' →
SDK initializes with language preference →
If error occurs: SDK internally reads app's localization files (strings.xml or .strings) →
SDK returns LOCALIZED error message (not error code) →
App displays error message directly to user
If success: Fire onInitialized event with:
- data.additionalInfo.supportedLanguage (array of available languages)
- data.additionalInfo.selectedLanguage (initialized language code)
→ App calls updateFromSDK() to sync LanguageProvider
↓
↓
PHASE 2: RUNTIME LANGUAGE SWITCHING (After initialization)
↓
User selects language from UI → Call setSDKLanguage('hi-IN', direction) →
SDK processes request → Fire onSetLanguageResponse event →
Response includes supportedLanguages and localeCode →
Check statusCode and longErrorCode for success →
Update LanguageProvider with new languages if successful →
No app restart needed - UI updates dynamically
Phase | API Method | Parameters | Response | Purpose | Documentation |
Initialization |
|
| Localized error message (if error) or success response | Set initial language preference with fallback to English | |
Runtime |
| locale: ‘en-US', ‘hi-IN'; direction: 0/1 | Sync response with error code | Request language change | |
Runtime |
| N/A | Complete language data + supported languages | Callback with language update result |
During initialization, if an error occurs, the SDK automatically reads your app's localization files and returns the localized error message:
Initialize Called with initOptions.internationalizationOptions.localeCode = 'hi' (Hindi)
↓
SDK initializes and encounters error
↓
SDK internally reads: android/app/src/main/res/values-hi/strings_rel_id.xml
OR: ios/SharedLocalization/hi.lproj/RELID.strings
↓
SDK finds localized message: "SDK प्रारंभीकरण विफल"
↓
SDK Returns: { error: { errorString: "SDK प्रारंभीकरण विफल" } }
↓
App displays error message directly to user
No manual error code mapping needed - SDK handles reading localization files internally!
On successful initialization, the SDK returns the list of supported languages via the onInitialized event:
// onInitialized callback returns RDNAInitialized
void handleInitialized(RDNAInitialized data) {
// Access supported languages from additionalInfo
final supportedLanguages = data.additionalInfo?.supportedLanguage; // List<RDNASupportedLanguage>
final selectedLanguage = data.additionalInfo?.selectedLanguage; // String (e.g., 'hi-IN')
// Supported languages structure:
// [
// RDNASupportedLanguage(lang: 'en-US', displayText: 'English', direction: 'LTR'),
// RDNASupportedLanguage(lang: 'hi-IN', displayText: 'Hindi', direction: 'LTR'),
// RDNASupportedLanguage(lang: 'es-ES', displayText: 'Spanish', direction: 'LTR')
// ]
// Selected language structure:
// 'hi-IN' (Full locale code that was initialized with)
// Update LanguageProvider with SDK's languages
ref.read(languageProvider.notifier).updateFromSDK(supportedLanguages, selectedLanguage);
}
Key Points:
data.additionalInfo.supportedLanguage - Array of all languages SDK supportsdata.additionalInfo.selectedLanguage - Currently selected language codeupdateFromSDK() to update LanguageProvider after initializationThe RDNALanguageDirection enum provides language direction options:
enum RDNALanguageDirection {
RDNA_LOCALE_LTR, // 0: Left-to-Right (English, Spanish, Hindi)
RDNA_LOCALE_RTL, // 1: Right-to-Left (Arabic, Hebrew)
RDNA_LOCALE_TBRL, // 2: Top-Bottom Right-Left (vertical)
RDNA_LOCALE_TBLR, // 3: Top-Bottom Left-Right (vertical)
RDNA_LOCALE_UNSUPPORTED // 4
}
Example languages:
Language | Locale Code | Direction | Native Name |
English | en-US | LTR (0) | English |
Hindi | hi-IN | LTR (0) | हिन्दी |
Spanish | es-ES | LTR (0) | Español |
Arabic | ar-SA | RTL (1) | العربية |
After calling setSDKLanguage(), the SDK fires onSetLanguageResponse with this structure:
class RDNASetLanguageResponse {
final String? localeCode; // 'es-ES'
final String? localeName; // 'Spanish'
final String? languageDirection; // 'LTR' or 'RTL'
final List<RDNASupportedLanguage>? supportedLanguages; // All available languages
final Map<String, dynamic>? appMessages; // Localized messages
final RDNAStatus? status; // Status(statusCode: 100, statusMessage: 'Success')
final RDNAError? error; // Error(longErrorCode: 0, errorString: '')
}
Language screens use proper event handler cleanup:
@override
void initState() {
super.initState();
// Setup handler on screen mount
final eventManager = rdnaService.getEventManager();
eventManager.setSetLanguageResponseHandler(_handleSetLanguageResponse);
}
@override
void dispose() {
// Cleanup when screen disposes
final eventManager = rdnaService.getEventManager();
eventManager.setSetLanguageResponseHandler(null);
super.dispose();
}
Initialize error codes need to be mapped to localized strings. Let's set up native localization files for Android and iOS.
During SDK initialization, if an error occurs, the SDK automatically reads your app's localization files:
Scenario: User starts app with Spanish preference, but network is down
1. App calls initialize() with initOptions.internationalizationOptions.localeCode = 'es'
2. SDK tries to initialize but fails due to network error
3. SDK internally reads: android/app/src/main/res/values-es/strings_rel_id.xml
OR: ios/SharedLocalization/es.lproj/RELID.strings
4. SDK finds the localized error message: "Error de conexión de red"
5. SDK returns: { error: { errorString: "Error de conexión de red" } }
6. App displays the error message to user in Spanish
Key Point: The SDK handles all localization internally. Your app just displays the error message it receives from the SDK.
Create localized string files in android/app/src/main/res/:
Step 1: Create default English strings
Create file: android/app/src/main/res/values/strings_rel_id.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- REL-ID SDK Error Codes -->
<string name="RDNA_ERR_50001">Network connection error</string>
<string name="RDNA_ERR_50002">Invalid server configuration</string>
<string name="RDNA_ERR_50003">SDK initialization timeout</string>
<string name="RDNA_ERR_UNKNOWN">An unexpected error occurred</string>
</resources>
Step 2: Create Spanish localization
Create file: android/app/src/main/res/values-es/strings_rel_id.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- REL-ID SDK Error Codes - Spanish -->
<string name="RDNA_ERR_50001">Error de conexión de red</string>
<string name="RDNA_ERR_50002">Configuración de servidor inválida</string>
<string name="RDNA_ERR_50003">Tiempo de espera de inicialización del SDK</string>
<string name="RDNA_ERR_UNKNOWN">Ocurrió un error inesperado</string>
</resources>
Step 3: Create Hindi localization
Create file: android/app/src/main/res/values-hi/strings_rel_id.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- REL-ID SDK Error Codes - Hindi -->
<string name="RDNA_ERR_50001">नेटवर्क कनेक्शन त्रुटि</string>
<string name="RDNA_ERR_50002">अमान्य सर्वर कॉन्फ़िगरेशन</string>
<string name="RDNA_ERR_50003">SDK प्रारंभीकरण समय समाप्त</string>
<string name="RDNA_ERR_UNKNOWN">एक अप्रत्याशित त्रुटि हुई</string>
</resources>
Create localized string files in ios/SharedLocalization/:
Step 1: Create default English strings
Create file: ios/SharedLocalization/en.lproj/RELID.strings
/* REL-ID SDK Error Codes */
"RDNA_ERR_50001" = "Network connection error";
"RDNA_ERR_50002" = "Invalid server configuration";
"RDNA_ERR_50003" = "SDK initialization timeout";
"RDNA_ERR_UNKNOWN" = "An unexpected error occurred";
Step 2: Create Spanish localization
Create file: ios/SharedLocalization/es.lproj/RELID.strings
/* REL-ID SDK Error Codes - Spanish */
"RDNA_ERR_50001" = "Error de conexión de red";
"RDNA_ERR_50002" = "Configuración de servidor inválida";
"RDNA_ERR_50003" = "Tiempo de espera de inicialización del SDK";
"RDNA_ERR_UNKNOWN" = "Ocurrió un error inesperado";
Step 3: Create Hindi localization
Create file: ios/SharedLocalization/hi.lproj/RELID.strings
/* REL-ID SDK Error Codes - Hindi */
"RDNA_ERR_50001" = "नेटवर्क कनेक्शन त्रुटि";
"RDNA_ERR_50002" = "अमान्य सर्वर कॉन्फ़िगरेशन";
"RDNA_ERR_50003" = "SDK प्रारंभीकरण समय समाप्त";
"RDNA_ERR_UNKNOWN" = "एक अप्रत्याशित त्रुटि हुई";
Step 4: Link to Xcode Project
In Xcode:
ios/SharedLocalization folderThis links the same physical files to both projects, enabling shared localization updates.
Let's implement the language management APIs in your service layer following REL-ID SDK patterns.
Add this method to
lib/uniken/services/rdna_service.dart
:
// lib/uniken/services/rdna_service.dart
import 'package:rdna_client/rdna_struct.dart';
/// Changes the SDK language dynamically after initialization
///
/// This method allows changing the SDK's language preference after initialization has completed.
/// The SDK will update all internal messages and supported language configurations accordingly.
/// After successful API call, the SDK triggers an onSetLanguageResponse event with updated language data.
/// Uses sync response pattern similar to other API methods.
///
/// ## Parameters
/// - [localeCode]: The language locale code to set (e.g., 'en-US', 'hi-IN', 'ar-SA')
/// - [languageDirection]: Language text direction (RDNA_LOCALE_LTR = 0, RDNA_LOCALE_RTL = 1)
///
/// ## Returns
/// RDNASyncResponse containing sync response (may have error or success)
///
/// ## Events Triggered
/// - `onSetLanguageResponse`: Triggered with updated language configuration after successful API call
///
/// ## Response Validation Logic
/// 1. Check error.longErrorCode: 0 = success, > 0 = error
/// 2. An onSetLanguageResponse event will be triggered with updated language configuration
/// 3. Async events will be handled by event listeners
///
/// ## Example
/// ```dart
/// // Change to Arabic (RTL language)
/// final response = await rdnaService.setSDKLanguage(
/// 'ar-SA',
/// RDNALanguageDirection.RDNA_LOCALE_RTL,
/// );
/// if (response.error?.longErrorCode == 0) {
/// print('SetSDKLanguage sync success, waiting for onSetLanguageResponse event');
/// }
/// ```
Future<RDNASyncResponse> setSDKLanguage(
String localeCode,
RDNALanguageDirection languageDirection,
) async {
print('RdnaService - Setting SDK language:');
print(' Locale Code: $localeCode');
print(' Language Direction: $languageDirection');
final response = await _rdnaClient.setSDKLanguage(localeCode, languageDirection);
print('RdnaService - SetSDKLanguage sync callback received');
print('RdnaService - Sync response:');
print(' Long Error Code: ${response.error?.longErrorCode}');
print(' Short Error Code: ${response.error?.shortErrorCode}');
return response;
}
Add to
lib/uniken/services/rdna_event_manager.dart
:
// Import at top
// Language Management Callbacks
typedef RDNASetLanguageResponseCallback = void Function(RDNASetLanguageResponse);
// In RdnaEventManager class
class RdnaEventManager {
// Language Management Handlers
RDNASetLanguageResponseCallback? _setLanguageResponseHandler;
// In _registerEventListeners method, add listener
void _registerEventListeners() {
// ... existing listeners ...
// Language Management Event Listeners
_listeners.add(
_rdnaClient.on(RdnaClient.onSetLanguageResponse, _onSetLanguageResponse),
);
}
// Add event handler method
/// Handles set language response events containing updated language configuration
void _onSetLanguageResponse(dynamic setLanguageData) {
print('RdnaEventManager - Set language response event received');
final languageResponse = setLanguageData as RDNASetLanguageResponse;
print('RdnaEventManager - Set language response data:');
print(' Locale Code: ${languageResponse.localeCode}');
print(' Locale Name: ${languageResponse.localeName}');
print(' Language Direction: ${languageResponse.languageDirection}');
print(' Supported Languages: ${languageResponse.supportedLanguages?.length ?? 0}');
print(' Status Code: ${languageResponse.status?.statusCode}');
print(' Error Code: ${languageResponse.error?.longErrorCode}');
if (_setLanguageResponseHandler != null) {
_setLanguageResponseHandler!(languageResponse);
}
}
// Add public setter for handler
/// Sets the handler for set language response events
void setSetLanguageResponseHandler(RDNASetLanguageResponseCallback? callback) {
_setLanguageResponseHandler = callback;
}
// In cleanup method, add
void cleanup() {
// ... existing cleanup ...
_setLanguageResponseHandler = null;
}
}
After successful initialization, the SDK provides supported languages via the onInitialized callback. Let's integrate this with LanguageProvider to sync UI with SDK's available languages.
Add to
lib/uniken/providers/sdk_event_provider.dart
:
import '../../tutorial/providers/language_provider.dart';
class _SDKEventProviderWidgetState extends ConsumerState<SDKEventProviderWidget> {
/// Event handler for successful initialization
///
/// Automatically navigates to TutorialSuccess screen with session data,
/// just like React Native's NavigationService.navigate call.
///
/// Also updates language context with SDK's supported languages and selected language
void _handleInitialized(RDNAInitialized data) {
print('SDKEventProvider - Successfully initialized');
print(' Session ID: ${data.session?.sessionId}');
print(' Status Code: ${data.status?.statusCode}');
print(' Session Type: ${data.session?.sessionType}');
// Update language context with SDK's supported languages and selected language
if (data.additionalInfo?.supportedLanguage != null &&
data.additionalInfo!.supportedLanguage!.isNotEmpty) {
print('SDKEventProvider - Updating language context with SDK languages:');
print(' Supported Count: ${data.additionalInfo!.supportedLanguage!.length}');
print(' Selected Language: ${data.additionalInfo!.selectedLanguage}');
ref.read(languageProvider.notifier).updateFromSDK(
data.additionalInfo!.supportedLanguage!,
data.additionalInfo!.selectedLanguage ?? 'en',
);
} else {
print('SDKEventProvider - No supported languages in SDK response, keeping defaults');
}
// Update state
ref.read(initializedDataProvider.notifier).state = data;
// Navigate to success screen
appRouter.goNamed('tutorialSuccessScreen', extra: data);
}
/// Event handler for set language response event
///
/// Called when setSDKLanguage API is invoked and SDK responds with updated language configuration.
/// Updates language provider with the new language configuration from SDK.
void _handleSetLanguageResponse(RDNASetLanguageResponse data) {
print('SDKEventProvider - Set language response event received:');
print(' Locale Code: ${data.localeCode}');
print(' Locale Name: ${data.localeName}');
print(' Language Direction: ${data.languageDirection}');
print(' Supported Languages Count: ${data.supportedLanguages?.length ?? 0}');
print(' Status Code: ${data.status?.statusCode}');
print(' Error Code: ${data.error?.longErrorCode}');
// Check if language change failed
if (data.error?.longErrorCode != 0) {
print('SDKEventProvider - Language change failed: ${data.error?.errorString}');
return;
}
// Success - update language context with SDK's updated language configuration
if (data.supportedLanguages != null && data.supportedLanguages!.isNotEmpty) {
print('SDKEventProvider - Updating language context with new SDK languages');
ref.read(languageProvider.notifier).updateFromSDK(
data.supportedLanguages!,
data.localeCode ?? 'en',
);
print('SDKEventProvider - Language context updated successfully');
} else {
print('SDKEventProvider - Warning: No supported languages in set language response');
}
}
@override
void initState() {
super.initState();
_setupSDKEventHandlers();
}
void _setupSDKEventHandlers() {
final rdnaService = RdnaService.getInstance();
final eventManager = rdnaService.getEventManager();
eventManager.setInitializedHandler(_handleInitialized);
eventManager.setSetLanguageResponseHandler(_handleSetLanguageResponse);
// ... other handlers ...
}
}
Supported Languages Data Structure from onInitialized:
// From: data.additionalInfo.supportedLanguage (List)
[
RDNASupportedLanguage(
lang: 'en-US', // Full locale code
displayText: 'English', // Display name
direction: 'LTR' // Text direction: 'LTR' or 'RTL'
),
RDNASupportedLanguage(
lang: 'hi-IN',
displayText: 'Hindi',
direction: 'LTR'
),
RDNASupportedLanguage(
lang: 'es-ES',
displayText: 'Spanish',
direction: 'LTR'
)
]
// From: data.additionalInfo.selectedLanguage (String)
'hi-IN' // Currently selected language code (what was initialized with)
updateFromSDK() is called twice:onInitialized - sync SDK's supported languages with apponSetLanguageResponse (from runtime language change) - update with new language selectioninitOptionsonInitialized eventonSetLanguageResponse eventLet's create the language state management and UI components for language selection.
Create
lib/tutorial/types/language.dart
:
/// Customer Language Interface
///
/// Optimized for customer UI display and navigation.
/// Separate from SDK's RDNASupportedLanguage to provide better UX control.
class Language {
final String lang; // 'en-US', 'hi-IN', 'es-ES'
final String displayText; // 'English', 'Hindi', 'Spanish'
final String nativeName; // 'English', 'हिन्दी', 'Español'
final int direction; // 0 = LTR, 1 = RTL
final bool isRTL; // Boolean flag for UI decisions
Language({
required this.lang,
required this.displayText,
required this.nativeName,
required this.direction,
required this.isRTL,
});
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Language && other.lang == lang;
}
@override
int get hashCode => lang.hashCode;
}
Create
lib/tutorial/utils/language_config.dart
:
import 'package:rdna_client/rdna_struct.dart';
import '../types/language.dart';
/// Default Hardcoded Languages
final List<Language> defaultSupportedLanguages = [
Language(
lang: 'en-US',
displayText: 'English',
nativeName: 'English',
direction: 0,
isRTL: false,
),
Language(
lang: 'hi-IN',
displayText: 'Hindi',
nativeName: 'हिन्दी',
direction: 0,
isRTL: false,
),
Language(
lang: 'ar-SA',
displayText: 'Arabic',
nativeName: 'العربية',
direction: 1,
isRTL: true,
),
Language(
lang: 'es-ES',
displayText: 'Spanish',
nativeName: 'Español',
direction: 0,
isRTL: false,
),
Language(
lang: 'fr-FR',
displayText: 'French',
nativeName: 'Français',
direction: 0,
isRTL: false,
),
];
final Language defaultLanguage = defaultSupportedLanguages[0];
/// Native Name Lookup Table
final Map<String, String> nativeNameLookup = {
'en': 'English',
'hi': 'हिन्दी',
'ar': 'العربية',
'es': 'Español',
'fr': 'Français',
};
/// Convert SDK's RDNASupportedLanguage to Customer's Language interface
Language convertSDKLanguageToCustomer(RDNASupportedLanguage sdkLang) {
final directionNum = sdkLang.direction == 'RTL' ? 1 : 0;
final isRTL = sdkLang.direction == 'RTL';
final langCode = sdkLang.lang ?? 'en';
final displayText = sdkLang.displayText ?? 'Unknown';
final baseCode = langCode.split('-')[0];
final nativeName = nativeNameLookup[baseCode] ?? displayText;
return Language(
lang: langCode,
displayText: displayText,
nativeName: nativeName,
direction: directionNum,
isRTL: isRTL,
);
}
/// Extract short language code for SDK initOptions
String getShortLanguageCode(String fullLocale) {
return fullLocale.split('-')[0];
}
/// Convert direction integer to RDNALanguageDirection enum
RDNALanguageDirection getLanguageDirectionEnum(int direction) {
return direction == 1
? RDNALanguageDirection.RDNA_LOCALE_RTL
: RDNALanguageDirection.RDNA_LOCALE_LTR;
}
Create
lib/tutorial/utils/language_storage.dart
:
import 'package:shared_preferences/shared_preferences.dart';
const String _languageKey = 'tutorial_app_language';
class LanguageStorage {
/// Save selected language code to persistent storage
static Future<void> save(String languageCode) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_languageKey, languageCode);
print('Language saved to storage: $languageCode');
} catch (error) {
print('Failed to save language: $error');
rethrow;
}
}
/// Load previously saved language code
static Future<String?> load() async {
try {
final prefs = await SharedPreferences.getInstance();
final code = prefs.getString(_languageKey);
print('Language loaded from storage: $code');
return code;
} catch (error) {
print('Failed to load language: $error');
return null;
}
}
}
Create
lib/tutorial/providers/language_provider.dart
:
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:rdna_client/rdna_struct.dart';
import '../types/language.dart';
import '../utils/language_config.dart';
import '../utils/language_storage.dart';
/// Language Provider State
class LanguageState {
final Language currentLanguage;
final List<Language> supportedLanguages;
final bool isLoading;
LanguageState({
required this.currentLanguage,
required this.supportedLanguages,
required this.isLoading,
});
LanguageState copyWith({
Language? currentLanguage,
List<Language>? supportedLanguages,
bool? isLoading,
}) {
return LanguageState(
currentLanguage: currentLanguage ?? this.currentLanguage,
supportedLanguages: supportedLanguages ?? this.supportedLanguages,
isLoading: isLoading ?? this.isLoading,
);
}
}
/// Language Provider Notifier
class LanguageNotifier extends StateNotifier<LanguageState> {
LanguageNotifier()
: super(LanguageState(
currentLanguage: defaultLanguage,
supportedLanguages: defaultSupportedLanguages,
isLoading: true,
)) {
loadPersistedLanguage();
}
/// Load persisted language on initialization
Future<void> loadPersistedLanguage() async {
try {
final savedCode = await LanguageStorage.load();
if (savedCode != null) {
final language = state.supportedLanguages.firstWhere(
(l) => l.lang == savedCode,
orElse: () => defaultLanguage,
);
state = state.copyWith(
currentLanguage: language,
isLoading: false,
);
print('LanguageProvider - Loaded persisted language: ${language.displayText}');
} else {
state = state.copyWith(isLoading: false);
print('LanguageProvider - No persisted language, using default');
}
} catch (error) {
print('LanguageProvider - Error loading language: $error');
state = state.copyWith(isLoading: false);
}
}
/// Change language and persist preference
Future<void> changeLanguage(Language language) async {
try {
await LanguageStorage.save(language.lang);
state = state.copyWith(currentLanguage: language);
print('LanguageProvider - Language changed to: ${language.displayText}');
} catch (error) {
print('LanguageProvider - Error changing language: $error');
rethrow;
}
}
/// Update supported languages from SDK response
void updateFromSDK(List<RDNASupportedLanguage> sdkLanguages, String sdkSelectedLanguage) {
try {
print('LanguageProvider - Updating from SDK:');
print(' SDK Languages Count: ${sdkLanguages.length}');
print(' SDK Selected Language: $sdkSelectedLanguage');
final convertedLanguages = sdkLanguages.map(convertSDKLanguageToCustomer).toList();
final sdkCurrentLanguage = convertedLanguages.firstWhere(
(l) => l.lang == sdkSelectedLanguage,
orElse: () => convertedLanguages.first,
);
state = state.copyWith(
supportedLanguages: convertedLanguages,
currentLanguage: sdkCurrentLanguage,
);
LanguageStorage.save(sdkCurrentLanguage.lang).catchError((error) {
print('LanguageProvider - Failed to persist SDK language: $error');
});
} catch (error) {
print('LanguageProvider - Error updating from SDK: $error');
}
}
}
/// Language Provider
final languageProvider = StateNotifierProvider<LanguageNotifier, LanguageState>((ref) {
return LanguageNotifier();
});
Create
lib/tutorial/screens/components/language_selector.dart
:
import 'package:flutter/material.dart';
import '../../types/language.dart';
/// Language Selector Modal
class LanguageSelector extends StatelessWidget {
final Language currentLanguage;
final List<Language> supportedLanguages;
final Function(Language) onSelectLanguage;
const LanguageSelector({
super.key,
required this.currentLanguage,
required this.supportedLanguages,
required this.onSelectLanguage,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Header
Container(
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(color: Color(0xFFE0E0E0), width: 1),
),
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Select Language',
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Color(0xFF333333),
),
),
SizedBox(height: 4),
Text(
'Choose your preferred language',
style: TextStyle(fontSize: 14, color: Color(0xFF666666)),
),
],
),
),
// Language List
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: supportedLanguages.length,
itemBuilder: (context, index) {
final language = supportedLanguages[index];
final isSelected = currentLanguage.lang == language.lang;
return InkWell(
onTap: () => onSelectLanguage(language),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isSelected ? const Color(0xFFF5F9FF) : Colors.white,
border: Border(
left: isSelected
? const BorderSide(color: Color(0xFF007AFF), width: 4)
: BorderSide.none,
bottom: const BorderSide(color: Color(0xFFF0F0F0), width: 1),
),
),
child: Row(
children: [
// Language Info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
language.nativeName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Color(0xFF333333),
),
),
const SizedBox(height: 2),
Text(
language.displayText,
style: const TextStyle(
fontSize: 14,
color: Color(0xFF666666),
),
),
],
),
),
// Metadata (RTL Badge + Checkmark)
Row(
children: [
if (language.isRTL) ...[
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: const Color(0xFFFFF3E0),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'RTL',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Color(0xFFF57C00),
),
),
),
const SizedBox(width: 8),
],
if (isSelected)
Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
color: Color(0xFF007AFF),
shape: BoxShape.circle,
),
child: const Center(
child: Text(
'✓',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
],
),
),
);
},
),
),
// Close Button
Padding(
padding: const EdgeInsets.fromLTRB(20, 10, 20, 20),
child: SizedBox(
width: double.infinity,
child: TextButton(
onPressed: () => Navigator.of(context).pop(),
style: TextButton.styleFrom(
backgroundColor: const Color(0xFFF0F0F0),
padding: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text(
'Cancel',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF333333),
),
),
),
),
),
],
),
);
}
}
/// Show Language Selector Modal
Future<void> showLanguageSelector(
BuildContext context, {
required Language currentLanguage,
required List<Language> supportedLanguages,
required Function(Language) onSelectLanguage,
}) async {
return showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
useRootNavigator: true,
builder: (context) => Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
child: LanguageSelector(
currentLanguage: currentLanguage,
supportedLanguages: supportedLanguages,
onSelectLanguage: onSelectLanguage,
),
),
);
}
Add to
lib/tutorial/screens/components/drawer_content.dart
:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:rdna_client/rdna_struct.dart';
import '../../../uniken/services/rdna_service.dart';
import '../../providers/language_provider.dart';
import '../../utils/language_config.dart';
import 'language_selector.dart';
class DrawerContent extends ConsumerStatefulWidget {
final RDNAUserLoggedIn? sessionData;
final String? currentRoute;
const DrawerContent({
super.key,
this.sessionData,
this.currentRoute,
});
@override
ConsumerState<DrawerContent> createState() => _DrawerContentState();
}
class _DrawerContentState extends ConsumerState<DrawerContent> {
bool _isChangingLanguage = false;
/// Handle change language menu item tap
Future<void> _handleChangeLanguage() async {
final languageState = ref.read(languageProvider);
// Capture ScaffoldMessenger BEFORE showing modal
final scaffoldMessenger = ScaffoldMessenger.of(context);
final rootNavigator = Navigator.of(context, rootNavigator: true);
// Show language selector modal
showLanguageSelector(
context,
currentLanguage: languageState.currentLanguage,
supportedLanguages: languageState.supportedLanguages,
onSelectLanguage: (language) async {
// Close modal
rootNavigator.pop();
// If selecting the same language, do nothing
if (language.lang == languageState.currentLanguage.lang) {
return;
}
// Show loading state
if (mounted) {
setState(() => _isChangingLanguage = true);
}
try {
final rdnaService = RdnaService.getInstance();
// Call setSDKLanguage API
final response = await rdnaService.setSDKLanguage(
language.lang,
getLanguageDirectionEnum(language.direction),
);
if (mounted) {
setState(() => _isChangingLanguage = false);
}
if (response.error?.longErrorCode == 0) {
// Success - async event will update languageProvider
scaffoldMessenger.showSnackBar(
SnackBar(
content: Text('Language changed to ${language.nativeName}'),
backgroundColor: const Color(0xFF10B981),
duration: const Duration(seconds: 2),
),
);
} else {
// Error
if (mounted) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Language Change Error'),
content: Text(response.error?.errorString ?? 'Language change failed'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
),
);
}
}
} catch (error) {
if (mounted) {
setState(() => _isChangingLanguage = false);
}
}
},
);
}
@override
Widget build(BuildContext context) {
final languageState = ref.watch(languageProvider);
return Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
// ... other drawer items ...
// Language Change Menu Item
ListTile(
leading: const Icon(Icons.language),
title: const Text('Change Language'),
subtitle: Text(languageState.currentLanguage.nativeName),
trailing: _isChangingLanguage
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: null,
onTap: _isChangingLanguage ? null : _handleChangeLanguage,
),
// ... other drawer items ...
],
),
);
}
}
The following image showcases the screen from the sample application:
|
|
|
Let's verify that your internationalization implementation works correctly.
Follow these steps to test your i18n implementation:
Phase 1: SDK Initialization (App Startup)
initOptions.internationalizationOptions.localeCode = 'es')values-es/strings_rel_id.xmlPhase 2: Runtime Language Switching (Post-Login)
setSDKLanguage() APIonSetLanguageResponse event fires after API callLocalization Files Setup (Android)
android/app/src/main/res/values-hi/strings_rel_id.xml existsHindi message here flutter run -d androidLocalization Files Setup (iOS)
ios/SharedLocalization/hi.lproj/RELID.strings existsflutter run -d iosinitOptions.internationalizationOptions.localeCode = 'en' (short code)'en-US' (full code)dispose() for proper cleanupdirection fieldRDNALanguageDirection🎉 Congratulations! You've successfully implemented comprehensive internationalization support with REL-ID SDK!
In this codelab, you've learned and implemented:
✅ Two-Phase Language Lifecycle
initOptionssetSDKLanguage() APIonSetLanguageResponseonInitialized callback✅ Native Platform Localization
strings.xml configuration for error codes.strings file setup for localization✅ Language State Management
LanguageProvider✅ User Interface Components
✅ Production-Ready Error Handling
Your internationalization implementation is now production-ready! Consider these enhancements:
defaultSupportedLanguagesnativeNameLookup table in language_config.dartHappy coding with internationalization! 🌍
This codelab was created to help you master multi-language support with REL-ID SDK. The patterns you've learned here are production-tested and used by enterprise applications worldwide.