Widget Extension
En esta página
Architecture #
WidgetKit extensions run in a separate process and have no direct access to your Expo JS bundle. BetterI18n bridges the gap using iOS App Groups — a shared storage area that both your app and its extensions can read and write.
┌─────────────────────────┐ ┌──────────────────────────┐
│ Expo JS (React) │ │ WidgetKit Extension │
│ │ │ (Swift / SwiftUI) │
│ changeLanguage("tr") │ │ │
│ saveLocaleToWidget() │──────▶ │ BetterI18n(config: │
│ │ App │ I18nConfig( │
│ BetterI18n CDN fetch │ Group │ appGroupIdentifier │
│ → saves to AppGroup │──────▶ │ )) │
└─────────────────────────┘ User └──────────────────────────┘
DefaultsThe Expo side writes the locale preference and cached translations to UserDefaults(suiteName: appGroup). The Swift widget reads from the same location — no network call needed at render time.
App Group Entitlement #
Add an App Group to your app.config.js (or app.json). The group identifier must start with group. and match what you'll use in Swift.
export default {
expo: {
// ...
ios: {
bundleIdentifier: 'com.mycompany.myapp',
entitlements: {
'com.apple.security.application-groups': ['group.com.mycompany.myapp'],
},
},
},
};Add the Widget Target with @bacons/apple-targets #
Install the plugin and create a widget target config:
npx expo install @bacons/apple-targetsmodule.exports = () => ({
type: 'widget',
name: 'MyWidget',
bundleIdentifier: 'com.mycompany.myapp.widget',
deploymentTarget: '16.0',
entitlements: {
'com.apple.security.application-groups': ['group.com.mycompany.myapp'],
},
});Register it in app.config.js:
plugins: [
['@bacons/apple-targets', { targets: ['./targets/widget'] }],
],Add the BetterI18n Swift Package #
In Xcode, open your workspace and go to File → Add Package Dependencies:
- URL:
https://github.com/better-i18n/swift - Dependency Rule: Up to Next Major Version, from
0.1.0
Add both products:
- BetterI18n → your widget extension target
- BetterI18nUI → your main app target (for
I18nProvider)
Bridge the Locale from JS #
Install the shared preferences library:
npx expo install react-native-shared-group-preferencesCreate a helper that writes the selected locale to the App Group:
import SharedGroupPreferences from 'react-native-shared-group-preferences';
import { Platform } from 'react-native';
const APP_GROUP = 'group.com.mycompany.myapp';
const PROJECT = 'my-org/my-app';
export async function saveLocaleToWidget(locale: string) {
if (Platform.OS !== 'ios') return;
await SharedGroupPreferences.setItem(
`@better-i18n:locale:${PROJECT}`,
locale,
APP_GROUP,
);
}Call this function whenever the user changes the language:
import { useLocale } from '@better-i18n/expo';
import { saveLocaleToWidget } from '@/lib/utils/save-locale-to-widget';
const { changeLanguage } = useLocale();
async function handleLanguageChange(locale: string) {
await changeLanguage(locale);
await saveLocaleToWidget(locale);
}Read Translations in the Widget #
In your widget Swift target, use BetterI18n with appGroupIdentifier. The storage-only API skips the network and reads cached translations instantly — ideal for widget timelines.
import WidgetKit
import SwiftUI
import BetterI18n
struct Provider: TimelineProvider {
let i18n = BetterI18n(config: I18nConfig(
projectId: "my-org/my-app",
defaultLocale: "en",
appGroupIdentifier: "group.com.mycompany.myapp"
))
func getTimeline(
in context: Context,
completion: @escaping (Timeline<SimpleEntry>) -> Void
) {
Task {
let locale = await i18n.detectLocaleFromStorageOnly()
let translator = await i18n.getTranslatorFromStorageOnly(locale: locale)
let entry = SimpleEntry(
date: .now,
title: translator?("widget.title") ?? "My App",
subtitle: translator?("widget.subtitle") ?? ""
)
completion(Timeline(entries: [entry], policy: .atEnd))
}
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let title: String
let subtitle: String
}
struct MyWidgetView: View {
let entry: SimpleEntry
var body: some View {
VStack(alignment: .leading) {
Text(entry.title).font(.headline)
Text(entry.subtitle).font(.caption)
}
}
}
@main
struct MyWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "com.mycompany.myapp.widget", provider: Provider()) { entry in
MyWidgetView(entry: entry)
}
.configurationDisplayName("My App Widget")
.supportedFamilies([.systemSmall, .systemMedium])
}
}Test Language Switching #
Build and run the app via EAS Build or a connected device (simulators support widgets). Change the language in your app settings — the widget should reflect the new locale on the next timeline refresh.
To force a refresh during development:
WidgetCenter.shared.reloadAllTimelines()Storage Key Schema #
The JS SDK and Swift SDK use the same UserDefaults keys. If you write custom bridge code, match these exactly:
@better-i18n:locale:{project} ← selected locale (string)
@better-i18n:messages:{project}:{locale} ← cached translations (JSON string)- iOS Native Docs — Use BetterI18n in a standalone SwiftUI app without Expo
- Expo Setup — Get started with BetterI18n in your Expo project
- Offline Caching — How the JS SDK caches translations for offline use
Better I18N