Skip to content

fix(widget): use app language instead of device language for widgets#578

Open
MaorRocky wants to merge 2 commits intobetter-rail:mainfrom
MaorRocky:fix/widget-language-from-app-language
Open

fix(widget): use app language instead of device language for widgets#578
MaorRocky wants to merge 2 commits intobetter-rail:mainfrom
MaorRocky:fix/widget-language-from-app-language

Conversation

@MaorRocky
Copy link

@MaorRocky MaorRocky commented Feb 13, 2026

Summary

  • Use app language (from i18n) instead of device language for widgets
  • Adds LocaleManager to handle language retrieval from app state
  • Translates widget strings to supported languages (Arabic, Hebrew, Russian)

Description

This PR fixes an issue where widgets were displaying in the device's system language instead of the app's selected language.

Changes Made:

  1. i18n.ts - Modified to expose a method for getting the current app language
  2. LocaleManager.swift - New file that bridges the app language to the widget extension
  3. Widget Views - Updated to use the app language for localized content
  4. Widget Strings - Added localized strings for Arabic (ar), Hebrew (he), and Russian (ru)

Translations

The translations for the widget strings were generated using AI.

Testing

  • Tested with app set to Hebrew
  • Tested with app set to Arabic
  • Tested with app set to Russian
  • Verified widget displays correct language on home screen

- Add native module method to save app language to shared UserDefaults (App Group)
- Create LocaleManager.swift to read language from shared UserDefaults
- Apply locale and layout direction to widget views based on app language
- Add localized strings for en, he, ar, ru in widget extension
- Use String(localized:) for all hardcoded strings in widget views
- Pass correct locale to Live Activity server API
@gemini-code-assist
Copy link

Summary of Changes

Hello @MaorRocky, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses an inconsistency where iOS widgets were not respecting the language chosen by the user within the application. By integrating a new locale management system and leveraging shared user defaults, widgets now correctly display content in the app's selected language, including proper handling of right-to-left layouts. This enhances the user experience by providing a consistent linguistic interface across the main app and its associated widgets.

Highlights

  • Widget Localization: Implemented a mechanism to ensure iOS widgets display content in the language selected within the app, rather than defaulting to the device's system language.
  • Locale Management: Introduced a new LocaleManager in the iOS widget extension to retrieve the app's language and determine the correct locale and layout direction (e.g., Right-to-Left for Hebrew/Arabic).
  • Native Module Integration: Added a new native module method setAppGroupUserLocale to RNBetterRail to persist the user's chosen language to shared UserDefaults, making it accessible to the widget extension.
  • Localized Strings for Widgets: Added dedicated .strings files for Arabic, English, Hebrew, and Russian within the widget extension to support localized text for widget elements like 'NEXT TRAIN', 'ARRIVAL', and 'PLATFORM'.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • app/i18n/i18n.ts
    • Imported NativeModules to enable communication with native modules.
    • Added a call to NativeModules.RNBetterRail.setAppGroupUserLocale for iOS when the user's language is updated, ensuring the widget receives the correct locale.
  • ios/BetterRail/RNBetterRail.swift
    • Implemented the @objc func setAppGroupUserLocale method to store the provided language code in UserDefaults within the app group, making it accessible to the widget extension.
  • ios/BetterRailWidget/Live Activity/Activity.swift
    • Updated the Ride initialization to use getAppLanguage() for the locale parameter instead of a hardcoded 'en' value, ensuring live activities are localized.
  • ios/BetterRailWidget/LocaleManager.swift
    • Added a new file LocaleManager.swift containing functions getAppLanguage(), getAppLocale(), isAppRTL(), and getLayoutDirection() to centralize locale retrieval and direction logic for widgets.
  • ios/BetterRailWidget/Views/AccessoryEntryView.swift
    • Modified Text components to use String(localized:) for dynamic localization of strings like 'TOMORROW' and 'NEXT TRAIN'.
    • Applied .environment(\.locale, getAppLocale()) and .environment(\.layoutDirection, getLayoutDirection()) to the view hierarchy to ensure proper localization and layout.
  • ios/BetterRailWidget/Views/BetterRailWidgetView.swift
    • Applied .environment(\.locale, getAppLocale()) and .environment(\.layoutDirection, getLayoutDirection()) to both AccessoryEntryView and WidgetEntryView to propagate locale settings.
  • ios/BetterRailWidget/Views/WidgetEntryView.swift
    • Updated multiple Text components to use String(localized:) for localization of various labels such as 'TOMORROW', 'NEXT TRAIN', 'ARRIVAL', 'PLATFORM', and 'TRAIN NO.'.
    • Introduced new localized text fields for arrival, platform, and train number details within the widget.
  • ios/BetterRailWidget/ar.lproj/Widget.strings
    • Added a new localization file for Arabic strings used in widgets, including translations for key phrases.
  • ios/BetterRailWidget/en.lproj/Widget.strings
    • Added a new localization file for English strings used in widgets, providing default translations.
  • ios/BetterRailWidget/he.lproj/Widget.strings
    • Added a new localization file for Hebrew strings used in widgets, including translations for key phrases.
  • ios/BetterRailWidget/ru.lproj/Widget.strings
    • Added a new localization file for Russian strings used in widgets, including translations for key phrases.
  • ios/RNBetterRail.m
    • Declared the setAppGroupUserLocale method in the RNBetterRail native module interface, making it callable from JavaScript.
Activity
  • MaorRocky created this pull request with a clear intent to fix widget language inconsistencies.
  • The pull request description outlines the core changes: using app language for widgets and adding a LocaleManager.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request aims to align the language of iOS widgets with the application's language setting, rather than the device's default. The approach of passing the language code to the native side and storing it in shared UserDefaults for the widget extensions to access is sound. However, the implementation has a few critical issues that need addressing. There's a potential crash due to a force-unwrap in RNBetterRail.swift. Additionally, WidgetEntryView.swift contains a significant amount of duplicated code and a new hardcoded string that isn't localized, which undermines the goal of this PR. I've left specific comments with suggestions to fix these issues.

}

@objc func setAppGroupUserLocale(_ languageCode: String) -> Void {
UserDefaults(suiteName: "group.il.co.better-rail")!.set(languageCode, forKey: "userLocale")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Force-unwrapping UserDefaults with ! can cause a crash if the app group isn't configured correctly or if the suite name is invalid. It's much safer to use optional binding (guard let) to handle this potential failure gracefully and prevent the app from crashing.

    guard let userDefaults = UserDefaults(suiteName: "group.il.co.better-rail") else {
      // It's good practice to log an error here to help with debugging.
      print("Error: Could not access UserDefaults for app group 'group.il.co.better-rail'.")
      return
    }
    userDefaults.set(languageCode, forKey: "userLocale")

Comment on lines 51 to 95
if (widgetFamily == .systemMedium ||
widgetFamily == .systemLarge) {
VStack(alignment: .leading) {
Text(String(localized: "ARRIVAL"))
.preferredFont(size: 11).fontWeight(.medium)

Text(entry.arrivalTime).font(.system(size: 22, weight: .bold))
}.foregroundColor(.gray)
.padding(.leading,
widgetFamily == .systemLarge && isMediumScreen
? 8 : 2)

if (widgetFamily == .systemLarge) {
VStack(alignment: .leading) {
Text(String(localized: "PLATFORM"))
.preferredFont(size: 11).fontWeight(.medium)

Text(String(entry.platform)).font(.system(size: 22, weight: .bold))
}
.foregroundColor(.gray)
.padding(.leading, isMediumScreen ? CGFloat(8.0) : 4.0)

if (UIScreen.main.bounds.width >= 360) {
VStack(alignment: .leading) {
Text(String(localized: isMediumScreen ? "TRAIN NO." : "TRAIN"))
.preferredFont(size: 11).fontWeight(.medium)

Text(String(entry.trainNumber)).font(.system(size: 22, weight: .bold))
}
.foregroundColor(.gray)
.padding(.leading, 8)
}

}
}
.font(.system(size: nextTrainFontSize, weight: .bold))

if (widgetFamily != .systemLarge) {
Text("Platform \(String(entry.platform))・Train \(String(entry.trainNumber))")
.font(.system(size: 11.5)).fontWeight(.medium).foregroundColor(.white).opacity(/*@START_MENU_TOKEN@*/0.8/*@END_MENU_TOKEN@*/)
}

if (widgetFamily == .systemMedium ||
widgetFamily == .systemLarge) {
VStack(alignment: .leading) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This added code block almost entirely duplicates the existing code that follows it (starting at line 93 in the file). It appears the intention was to replace the old block with this new localized version, but the old one was not removed. Please remove the duplicated, unlocalized code block to avoid future maintenance issues and potential bugs.

Comment on lines 89 to 90
Text("Platform \(String(entry.platform))・Train \(String(entry.trainNumber))")
.font(.system(size: 11.5)).fontWeight(.medium).foregroundColor(.white).opacity(/*@START_MENU_TOKEN@*/0.8/*@END_MENU_TOKEN@*/)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This Text view contains a hardcoded string that is not localized. This will result in English text being displayed for all users, which is contrary to the goal of this pull request. Please use a localized string from your .strings files.

For example, you can add the following to your .strings files:

en.lproj/Widget.strings:

"PLATFORM_TRAIN" = "Platform %1$@・Train %2$@";

And then update the code to use it.

                  Text(String(format: NSLocalizedString("PLATFORM_TRAIN", comment: "Platform and train number"), String(entry.platform), String(entry.trainNumber)))
                    .font(.system(size: 11.5)).fontWeight(.medium).foregroundColor(.white).opacity(0.8)

Comment on lines 4 to 22
let appGroupUserDefaults = UserDefaults(suiteName: "group.il.co.better-rail")

func getAppLanguage() -> String {
return appGroupUserDefaults?.string(forKey: "userLocale") ?? "en"
}

func getAppLocale() -> Locale {
let languageCode = getAppLanguage()
return Locale(identifier: languageCode)
}

func isAppRTL() -> Bool {
let languageCode = getAppLanguage()
return languageCode == "he" || languageCode == "ar"
}

func getLayoutDirection() -> LayoutDirection {
return isAppRTL() ? .rightToLeft : .leftToRight
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using global functions and a global variable for locale management can make the code harder to maintain and test. It's better to encapsulate this logic in a singleton class. This improves code structure, enhances testability, and avoids polluting the global namespace. You would then update call sites to use LocaleManager.shared.getAppLanguage() and so on.

class LocaleManager {
    static let shared = LocaleManager()

    private let appGroupUserDefaults: UserDefaults?

    private init() {
        self.appGroupUserDefaults = UserDefaults(suiteName: "group.il.co.better-rail")
    }

    func getAppLanguage() -> String {
        return appGroupUserDefaults?.string(forKey: "userLocale") ?? "en"
    }

    func getAppLocale() -> Locale {
        return Locale(identifier: getAppLanguage())
    }

    func isAppRTL() -> Bool {
        let languageCode = getAppLanguage()
        return languageCode == "he" || languageCode == "ar"
    }

    func getLayoutDirection() -> LayoutDirection {
        return isAppRTL() ? .rightToLeft : .leftToRight
    }
}

…e, localize strings, and use singleton pattern
Copy link
Member

@guytepper guytepper left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, thanks for the contribution!

I'm not keen on supporting this as it goes against the OS guidelines.

However, if you're willing I'm open to make it accessible via the widget settings.

What do you think?

Copy link
Member

@guytepper guytepper Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is that needed when we have the existing Localizable.strings files?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i might be wrong, since it been 4 yrs since I touched production Swift code, but from what I remember:
Widget extension = separate bundle = needs its own localization files
The main app's Localizable.strings is inaccessible to the widget at runtime

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a separate target, but we are sharing the localization files between targets, so I think it should work - it already does today 😅

Can you check what happens when you remove all localization changes you've done?

@MaorRocky
Copy link
Author

Hey, thanks for the contribution!

I'm not keen on supporting this as it goes against the OS guidelines.

However, if you're willing I'm open to make it accessible via the widget settings.

What do you think?

yeah sure, it sounds like a good option 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants