Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
15 changes: 15 additions & 0 deletions ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
265BA9BA2F7BDB1E00287691 /* SyncWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 265BA9A92F7BDB1D00287691 /* SyncWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
C0DE1B5001AB12CD34EF0004 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = C0DE1B5001AB12CD34EF0003 /* InfoPlist.strings */; };
42257B77B623F2ABAD2A1912 /* DynamicIslandManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A4D4F9C0A149C751B796E19 /* DynamicIslandManager.swift */; };
5736417E0A4E184E8572693A /* WalletPathResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5736417D0A4E184E8572693A /* WalletPathResolver.swift */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
Expand Down Expand Up @@ -106,6 +107,8 @@
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
C0DE1B5001AB12CD34EF0001 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
C0DE1B5001AB12CD34EF0002 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/InfoPlist.strings; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
BGSYNC000ED2DC5600515810 /* BackgroundSyncManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundSyncManager.swift; sourceTree = "<group>"; };
BIOUNLK00ED2DC5600515810 /* BiometricUnlockHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BiometricUnlockHandler.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -248,6 +251,7 @@
8A0F5C502F8B001200A1C001 /* AppIcon.icon */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
C0DE1B5001AB12CD34EF0003 /* InfoPlist.strings */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
Expand Down Expand Up @@ -363,6 +367,7 @@
hasScannedForEncodings = 0;
knownRegions = (
en,
ko,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
Expand Down Expand Up @@ -398,6 +403,7 @@
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
C0DE1B5001AB12CD34EF0004 /* InfoPlist.strings in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
8A0F5C512F8B001200A1C001 /* AppIcon.icon in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
Expand Down Expand Up @@ -552,6 +558,15 @@
/* End PBXTargetDependency section */

/* Begin PBXVariantGroup section */
C0DE1B5001AB12CD34EF0003 /* InfoPlist.strings */ = {
isa = PBXVariantGroup;
children = (
C0DE1B5001AB12CD34EF0001 /* en */,
C0DE1B5001AB12CD34EF0002 /* ko */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
};
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
Expand Down
5 changes: 5 additions & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>ko</string>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add Korean InfoPlist strings for native prompts

Adding ko here advertises a Korean native localization, but the camera and Face ID permission prompts still come from the English NSCameraUsageDescription / NSFaceIDUsageDescription values because there is no ko.lproj/InfoPlist.strings under ios/Runner (and the same search shows only Base.lproj under macOS). In the QR-scan or biometric flows, Korean users will see English OS dialogs while the Flutter UI is Korean; add localized InfoPlist strings before declaring Korean support.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed — ios/Runner/en.lproj/InfoPlist.strings, ios/Runner/ko.lproj/InfoPlist.strings, and the macOS equivalents are in this branch (wired into both Runner.xcodeproj variant groups, ko added to knownRegions). Verified the built app bundles contain ko.lproj/InfoPlist.strings on both platforms.

</array>
Comment on lines +25 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor the language kill switch for native localizations

When a build is launched with VIZOR_LANGUAGE_ENABLED=false, the Dart app is pinned to English, but this bundle still advertises Korean as a supported native localization (and the same pattern is added on macOS). On a Korean-system device, iOS will still use the new ko.lproj/InfoPlist.strings for camera/Face ID permission prompts, so the rollout kill switch no longer restores the pre-i18n English-only behavior and users can see mixed-language system prompts while the app UI is forced to English.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Intended scope: the kill switch is a compile-time Dart define and governs the Flutter layer (app strings, Language UI, locale resolution) — it exists to roll back app-string regressions. The InfoPlist strings are static bundle resources rendered by the OS in the device language, which is standard iOS/macOS behavior for any app that ships localizations; they can't observe a dart-define without custom build-phase scripting, which seems disproportionate for two static permission sentences with no regression surface. Documented this scope in the PR description. If the team prefers, a follow-up could gate the lproj resources per build flavor, but I'd argue an OS prompt in the device language alongside an English-pinned app is the platform-correct state, not a regression.

<key>CFBundleDisplayName</key>
<string>Vizor</string>
<key>CFBundleExecutable</key>
Expand Down
2 changes: 2 additions & 0 deletions ios/Runner/en.lproj/InfoPlist.strings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"NSCameraUsageDescription" = "Vizor uses the camera to scan Keystone QR codes.";
"NSFaceIDUsageDescription" = "Vizor uses Face ID to unlock your wallet and confirm sensitive actions.";
2 changes: 2 additions & 0 deletions ios/Runner/ko.lproj/InfoPlist.strings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"NSCameraUsageDescription" = "Vizor는 Keystone QR 코드를 스캔하기 위해 카메라를 사용합니다.";
"NSFaceIDUsageDescription" = "Vizor는 지갑 잠금 해제와 민감한 작업 확인을 위해 Face ID를 사용합니다.";
4 changes: 4 additions & 0 deletions l10n.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
nullable-getter: false
89 changes: 57 additions & 32 deletions lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:go_router/go_router.dart';
import 'package:desktop_window_bootstrap/desktop_window_bootstrap.dart';
import 'package:url_launcher/url_launcher.dart';

import 'l10n/app_localizations.dart';
import 'src/app_bootstrap.dart';
import 'src/core/config/swap_feature_config.dart';
import 'src/core/layout/app_layout.dart';
Expand Down Expand Up @@ -72,6 +73,7 @@ import 'src/features/voting/screens/voting_review_screen.dart';
import 'src/features/voting/screens/voting_software_account_guard.dart';
import 'src/features/voting/screens/voting_status_screen.dart';
import 'src/features/voting/screens/voting_submission_confirmation_screen.dart';
import 'src/providers/locale_provider.dart';
import 'src/providers/theme_mode_provider.dart';
import 'src/providers/app_security_provider.dart';
import 'src/providers/linux_update_provider.dart';
Expand Down Expand Up @@ -378,16 +380,18 @@ List<RouteBase> appAuthRoutes(
),
GoRoute(
path: '/terms',
builder: (_, state) => kAppFormFactor == AppFormFactor.mobile
? const MobileLegalScreen(title: 'Terms of Use')
builder: (context, state) => kAppFormFactor == AppFormFactor.mobile
? MobileLegalScreen(title: AppLocalizations.of(context).legalTermsOfUse)
: TermsScreen(
forceFullPane: state.uri.queryParameters['from'] == 'onboarding',
),
),
GoRoute(
path: '/privacy',
builder: (_, state) => kAppFormFactor == AppFormFactor.mobile
? const MobileLegalScreen(title: 'Privacy Policy')
builder: (context, state) => kAppFormFactor == AppFormFactor.mobile
? MobileLegalScreen(
title: AppLocalizations.of(context).aboutPrivacyPolicy,
)
: PrivacyPolicyScreen(
forceFullPane: state.uri.queryParameters['from'] == 'onboarding',
),
Expand Down Expand Up @@ -862,6 +866,7 @@ class ZcashWalletApp extends ConsumerWidget {
final appRouter = ref.watch(_routerProvider);
final router = appRouter.router;
final themeMode = ref.watch(themeModeProvider);
final locale = ref.watch(localeProvider);

return MaterialApp.router(
title: 'Vizor',
Expand All @@ -874,6 +879,14 @@ class ZcashWalletApp extends ConsumerWidget {
routerDelegate: router.routerDelegate,
backButtonDispatcher: appRouter.backButtonDispatcher,
onNavigationNotification: appRouter.onNavigationNotification,
// No stored preference (null) follows the OS locale, clamped to
// supportedLocales with an English fallback — the whole app is
// translated, so Korean-system users get Korean out of the box
// (Keplr parity). A saved Language choice always wins. The
// kLanguageFeatureEnabled kill switch pins English outright.
locale: kLanguageFeatureEnabled ? locale : kEnglishLocale,
supportedLocales: AppLocalizations.supportedLocales,
localizationsDelegates: AppLocalizations.localizationsDelegates,
builder: (context, child) {
return AppThemeHost(
themeMode: themeMode,
Expand Down Expand Up @@ -1110,7 +1123,8 @@ class _WindowsUpdatePrompt extends StatelessWidget {
Widget build(BuildContext context) {
final colors = context.colors;
final isDark = AppTheme.of(context) == AppThemeData.dark;
final action = _primaryAction();
final l10n = AppLocalizations.of(context);
final action = _primaryAction(l10n);

return DefaultTextStyle.merge(
style: const TextStyle(decoration: TextDecoration.none),
Expand Down Expand Up @@ -1149,7 +1163,7 @@ class _WindowsUpdatePrompt extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_title(),
_title(l10n),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppTypography.labelLarge.copyWith(
Expand All @@ -1159,7 +1173,7 @@ class _WindowsUpdatePrompt extends StatelessWidget {
),
const SizedBox(height: 2),
Text(
_message(),
_message(l10n),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: AppTypography.bodySmall.copyWith(
Expand All @@ -1186,7 +1200,7 @@ class _WindowsUpdatePrompt extends StatelessWidget {
onPressed: onLater,
variant: AppButtonVariant.ghost,
size: AppButtonSize.small,
child: const Text('Later'),
child: Text(l10n.updateActionLater),
),
const SizedBox(width: AppSpacing.xxs),
],
Expand All @@ -1206,24 +1220,26 @@ class _WindowsUpdatePrompt extends StatelessWidget {
);
}

String _title() {
String _title(AppLocalizations l10n) {
return switch (state.status) {
WindowsUpdateStatus.available =>
'Update ${state.availableVersion} available',
WindowsUpdateStatus.downloading => 'Downloading update',
WindowsUpdateStatus.ready => 'Update ready',
WindowsUpdateStatus.applying => 'Restarting Vizor',
_ => 'Update available',
WindowsUpdateStatus.available => l10n.updateTitleAvailableVersion(
state.availableVersion,
),
WindowsUpdateStatus.downloading => l10n.updateTitleDownloading,
WindowsUpdateStatus.ready => l10n.updateTitleReady,
WindowsUpdateStatus.applying => l10n.updateTitleApplying,
_ => l10n.updateTitleAvailable,
};
}

String _message() {
String _message(AppLocalizations l10n) {
return switch (state.status) {
WindowsUpdateStatus.available => 'Download now or keep working.',
WindowsUpdateStatus.downloading =>
'${state.downloadProgress}% downloaded.',
WindowsUpdateStatus.ready => 'Restart when you are ready.',
WindowsUpdateStatus.applying => 'Applying after Vizor closes.',
WindowsUpdateStatus.available => l10n.updateBodyAvailable,
WindowsUpdateStatus.downloading => l10n.updateBodyDownloading(
state.downloadProgress,
),
WindowsUpdateStatus.ready => l10n.updateBodyReady,
WindowsUpdateStatus.applying => l10n.updateBodyApplying,
_ => '',
};
}
Expand All @@ -1233,23 +1249,23 @@ class _WindowsUpdatePrompt extends StatelessWidget {
state.status == WindowsUpdateStatus.ready;
}

_WindowsUpdatePromptAction _primaryAction() {
_WindowsUpdatePromptAction _primaryAction(AppLocalizations l10n) {
return switch (state.status) {
WindowsUpdateStatus.available => _WindowsUpdatePromptAction(
label: 'Download',
label: l10n.updateActionDownload,
onPressed: onDownload,
),
WindowsUpdateStatus.ready => _WindowsUpdatePromptAction(
label: 'Restart',
label: l10n.updateActionRestart,
onPressed: onRestart,
),
WindowsUpdateStatus.downloading => const _WindowsUpdatePromptAction(
label: 'Downloading',
WindowsUpdateStatus.downloading => _WindowsUpdatePromptAction(
label: l10n.updateActionDownloading,
),
WindowsUpdateStatus.applying => const _WindowsUpdatePromptAction(
label: 'Restarting',
WindowsUpdateStatus.applying => _WindowsUpdatePromptAction(
label: l10n.updateActionRestarting,
),
_ => const _WindowsUpdatePromptAction(label: 'Update'),
_ => _WindowsUpdatePromptAction(label: l10n.updateActionUpdate),
};
}
}
Expand Down Expand Up @@ -1338,10 +1354,14 @@ class _LinuxUpdateNoticeListener extends ConsumerWidget {
messenger.hideCurrentSnackBar();
messenger.showSnackBar(
SnackBar(
content: Text('Vizor ${update.assetVersion} is available.'),
content: Text(
AppLocalizations.of(
context,
).updateLinuxAvailable(update.assetVersion),
),
duration: const Duration(seconds: 8),
action: SnackBarAction(
label: 'View Release',
label: AppLocalizations.of(context).updateViewRelease,
onPressed: () => unawaited(_openLinuxUpdateRelease(update)),
),
),
Expand Down Expand Up @@ -1401,7 +1421,12 @@ class _RpcEndpointFailoverToastBridge extends ConsumerWidget {
if (!context.mounted) return;
showNetworkFallbackToast(
context,
next.message,
switch (next.kind) {
RpcEndpointFailoverEventKind.switchedToFallback =>
AppLocalizations.of(context).endpointFailoverSwitched,
RpcEndpointFailoverEventKind.switchedToPrimary =>
AppLocalizations.of(context).endpointFailoverRecovered,
},
duration: const Duration(seconds: 4),
);
});
Expand Down
Loading