@use-voltra/ios-client 2.3.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/build/commonjs/dynamic-widget/api.js +28 -1
  2. package/build/commonjs/dynamic-widget/api.js.map +1 -1
  3. package/build/commonjs/index.js +7 -12
  4. package/build/commonjs/index.js.map +1 -1
  5. package/build/commonjs/native/NativeVoltra.js.map +1 -1
  6. package/build/commonjs/utils/useUpdateOnHMR.js +6 -14
  7. package/build/commonjs/utils/useUpdateOnHMR.js.map +1 -1
  8. package/build/module/dynamic-widget/api.js +26 -0
  9. package/build/module/dynamic-widget/api.js.map +1 -1
  10. package/build/module/index.js +1 -2
  11. package/build/module/index.js.map +1 -1
  12. package/build/module/native/NativeVoltra.js.map +1 -1
  13. package/build/module/utils/useUpdateOnHMR.js +1 -13
  14. package/build/module/utils/useUpdateOnHMR.js.map +1 -1
  15. package/build/typescript/commonjs/dynamic-widget/api.d.ts +12 -0
  16. package/build/typescript/commonjs/dynamic-widget/api.d.ts.map +1 -1
  17. package/build/typescript/commonjs/index.d.ts +1 -2
  18. package/build/typescript/commonjs/index.d.ts.map +1 -1
  19. package/build/typescript/commonjs/native/NativeVoltra.d.ts +2 -0
  20. package/build/typescript/commonjs/native/NativeVoltra.d.ts.map +1 -1
  21. package/build/typescript/commonjs/utils/useUpdateOnHMR.d.ts +1 -1
  22. package/build/typescript/commonjs/utils/useUpdateOnHMR.d.ts.map +1 -1
  23. package/build/typescript/module/dynamic-widget/api.d.ts +12 -0
  24. package/build/typescript/module/dynamic-widget/api.d.ts.map +1 -1
  25. package/build/typescript/module/index.d.ts +1 -2
  26. package/build/typescript/module/index.d.ts.map +1 -1
  27. package/build/typescript/module/native/NativeVoltra.d.ts +2 -0
  28. package/build/typescript/module/native/NativeVoltra.d.ts.map +1 -1
  29. package/build/typescript/module/utils/useUpdateOnHMR.d.ts +1 -1
  30. package/build/typescript/module/utils/useUpdateOnHMR.d.ts.map +1 -1
  31. package/expo-plugin/build/cjs/ios-widget/clientRenderedPrerender.js +4 -0
  32. package/expo-plugin/build/cjs/ios-widget/clientRenderedPrerender.js.map +1 -1
  33. package/expo-plugin/build/cjs/ios-widget/dynamic-live-activity/swift.js +9 -1
  34. package/expo-plugin/build/cjs/ios-widget/dynamic-live-activity/swift.js.map +1 -1
  35. package/expo-plugin/build/cjs/ios-widget/files/index.js +8 -1
  36. package/expo-plugin/build/cjs/ios-widget/files/index.js.map +1 -1
  37. package/expo-plugin/build/cjs/ios-widget/files/infoPlist.js +16 -4
  38. package/expo-plugin/build/cjs/ios-widget/files/infoPlist.js.map +1 -1
  39. package/expo-plugin/build/cjs/ios-widget/files/localization.js +167 -0
  40. package/expo-plugin/build/cjs/ios-widget/files/localization.js.map +1 -0
  41. package/expo-plugin/build/cjs/ios-widget/files/swift.js +68 -7
  42. package/expo-plugin/build/cjs/ios-widget/files/swift.js.map +1 -1
  43. package/expo-plugin/build/cjs/ios-widget/xcode/groups.js +4 -1
  44. package/expo-plugin/build/cjs/ios-widget/xcode/groups.js.map +1 -1
  45. package/expo-plugin/build/cjs/utils/fileDiscovery.js +7 -4
  46. package/expo-plugin/build/cjs/utils/fileDiscovery.js.map +1 -1
  47. package/expo-plugin/build/cjs/validation.js +1 -0
  48. package/expo-plugin/build/cjs/validation.js.map +1 -1
  49. package/expo-plugin/build/esm/ios-widget/clientRenderedPrerender.js +4 -0
  50. package/expo-plugin/build/esm/ios-widget/clientRenderedPrerender.js.map +1 -1
  51. package/expo-plugin/build/esm/ios-widget/dynamic-live-activity/swift.js +9 -1
  52. package/expo-plugin/build/esm/ios-widget/dynamic-live-activity/swift.js.map +1 -1
  53. package/expo-plugin/build/esm/ios-widget/files/index.js +8 -1
  54. package/expo-plugin/build/esm/ios-widget/files/index.js.map +1 -1
  55. package/expo-plugin/build/esm/ios-widget/files/infoPlist.js +16 -4
  56. package/expo-plugin/build/esm/ios-widget/files/infoPlist.js.map +1 -1
  57. package/expo-plugin/build/esm/ios-widget/files/localization.js +128 -0
  58. package/expo-plugin/build/esm/ios-widget/files/localization.js.map +1 -0
  59. package/expo-plugin/build/esm/ios-widget/files/swift.js +69 -8
  60. package/expo-plugin/build/esm/ios-widget/files/swift.js.map +1 -1
  61. package/expo-plugin/build/esm/ios-widget/xcode/groups.js +5 -2
  62. package/expo-plugin/build/esm/ios-widget/xcode/groups.js.map +1 -1
  63. package/expo-plugin/build/esm/utils/fileDiscovery.js +6 -3
  64. package/expo-plugin/build/esm/utils/fileDiscovery.js.map +1 -1
  65. package/expo-plugin/build/esm/validation.js +2 -1
  66. package/expo-plugin/build/esm/validation.js.map +1 -1
  67. package/expo-plugin/build/types/ios-widget/clientRenderedPrerender.d.ts.map +1 -1
  68. package/expo-plugin/build/types/ios-widget/dynamic-live-activity/swift.d.ts.map +1 -1
  69. package/expo-plugin/build/types/ios-widget/files/index.d.ts +1 -0
  70. package/expo-plugin/build/types/ios-widget/files/index.d.ts.map +1 -1
  71. package/expo-plugin/build/types/ios-widget/files/infoPlist.d.ts +3 -1
  72. package/expo-plugin/build/types/ios-widget/files/infoPlist.d.ts.map +1 -1
  73. package/expo-plugin/build/types/ios-widget/files/localization.d.ts +46 -0
  74. package/expo-plugin/build/types/ios-widget/files/localization.d.ts.map +1 -0
  75. package/expo-plugin/build/types/ios-widget/files/swift.d.ts.map +1 -1
  76. package/expo-plugin/build/types/ios-widget/xcode/groups.d.ts.map +1 -1
  77. package/expo-plugin/build/types/types.d.ts +13 -4
  78. package/expo-plugin/build/types/types.d.ts.map +1 -1
  79. package/expo-plugin/build/types/utils/fileDiscovery.d.ts +2 -0
  80. package/expo-plugin/build/types/utils/fileDiscovery.d.ts.map +1 -1
  81. package/expo-plugin/build/types/validation.d.ts.map +1 -1
  82. package/expo-plugin/src/ios-widget/clientRenderedPrerender.ts +4 -0
  83. package/expo-plugin/src/ios-widget/dynamic-live-activity/swift.ts +9 -1
  84. package/expo-plugin/src/ios-widget/files/index.ts +14 -1
  85. package/expo-plugin/src/ios-widget/files/infoPlist.ts +23 -4
  86. package/expo-plugin/src/ios-widget/files/localization.ts +176 -0
  87. package/expo-plugin/src/ios-widget/files/swift.ts +88 -10
  88. package/expo-plugin/src/ios-widget/xcode/groups.ts +5 -2
  89. package/expo-plugin/src/types.ts +13 -3
  90. package/expo-plugin/src/utils/fileDiscovery.ts +7 -3
  91. package/expo-plugin/src/validation.ts +2 -0
  92. package/ios/Package.swift +8 -1
  93. package/ios/VoltraWidget.podspec +1 -1
  94. package/ios/app/NativeVoltra.mm +15 -2
  95. package/ios/app/VoltraModule.swift +11 -0
  96. package/ios/app/VoltraModuleImpl.swift +14 -0
  97. package/ios/app/VoltraViewComponentView.mm +2 -2
  98. package/ios/app/VoltraViewRoot.swift +6 -12
  99. package/ios/shared/ShortNames.swift +1 -0
  100. package/ios/shared/VoltraConstants.swift +4 -0
  101. package/ios/shared/VoltraElement.swift +43 -2
  102. package/ios/shared/VoltraLocaleEnvironment.swift +185 -0
  103. package/ios/shared/VoltraModifierDescriptor.swift +33 -0
  104. package/ios/shared/VoltraWidgetDefaults.swift +24 -0
  105. package/ios/shared/dynamic-live-activity/VoltraDynamicLiveActivityRenderer.swift +50 -28
  106. package/ios/target/VoltraClientWidgetRuntime.swift +12 -1
  107. package/ios/target/VoltraHomeWidget.swift +10 -15
  108. package/ios/target/VoltraWidget.swift +26 -18
  109. package/ios/ui/Extensions/VoltraNode+View.swift +7 -0
  110. package/ios/ui/Helpers/VoltraDeepLinkResolver+URL.swift +37 -0
  111. package/ios/ui/Helpers/VoltraDeepLinkResolver.swift +1 -32
  112. package/ios/ui/Modifiers/BuiltInModifiers.swift +613 -0
  113. package/ios/ui/Modifiers/VoltraElement+nativeModifiers.swift +53 -0
  114. package/ios/ui/Modifiers/VoltraModifierRegistry.swift +181 -0
  115. package/ios/ui/Modifiers/VoltraRootDefaults.swift +100 -0
  116. package/ios/ui/Style/TextStyle.swift +4 -2
  117. package/ios/ui/Style/View+applyStyle.swift +1 -1
  118. package/ios/ui/Views/VoltraText.swift +4 -8
  119. package/package.json +4 -4
  120. package/src/dynamic-widget/api.ts +28 -0
  121. package/src/index.ts +6 -2
  122. package/src/native/NativeVoltra.ts +2 -0
  123. package/src/utils/useUpdateOnHMR.ts +1 -16
  124. package/build/commonjs/utils/helpers.js +0 -25
  125. package/build/commonjs/utils/helpers.js.map +0 -1
  126. package/build/module/utils/helpers.js +0 -4
  127. package/build/module/utils/helpers.js.map +0 -1
  128. package/build/typescript/commonjs/utils/helpers.d.ts +0 -2
  129. package/build/typescript/commonjs/utils/helpers.d.ts.map +0 -1
  130. package/build/typescript/module/utils/helpers.d.ts +0 -2
  131. package/build/typescript/module/utils/helpers.d.ts.map +0 -1
  132. package/src/utils/helpers.ts +0 -1
@@ -0,0 +1,46 @@
1
+ import type { IOSWidgetConfig } from '../../types';
2
+ /**
3
+ * The parts of the Expo config that say which languages the app supports: the `locales` map
4
+ * (`{ "pl": "./languages/pl.json" }`) and an explicit `ios.infoPlist.CFBundleLocalizations`.
5
+ */
6
+ export interface AppLocalizationSource {
7
+ locales?: Record<string, unknown> | null;
8
+ ios?: {
9
+ infoPlist?: Record<string, unknown> | null;
10
+ } | null;
11
+ }
12
+ /**
13
+ * The app's development language: `ios.infoPlist.CFBundleDevelopmentRegion` when it is a literal
14
+ * locale tag, otherwise `en` (Expo's default, and what `$(DEVELOPMENT_LANGUAGE)` is in practice).
15
+ */
16
+ export declare function resolveDevelopmentRegion(config: AppLocalizationSource | undefined): string;
17
+ /**
18
+ * The languages the widget extension declares (ADR 0009 §4).
19
+ *
20
+ * iOS resolves `Locale.current` and SwiftUI's `\.locale` in the extension against the extension
21
+ * bundle's own localisations, so an extension that declares none reports `en_PL` to a Polish user
22
+ * of a Polish app. Mirroring the app's languages makes the extension resolve the way the app does.
23
+ *
24
+ * Sources: the Expo `locales` keys, `ios.infoPlist.CFBundleLocalizations`, and every locale used
25
+ * by a widget's locale maps. The app's development language (see `resolveDevelopmentRegion`) is
26
+ * added whenever anything is declared: it is the language iOS falls back to when none of the user's
27
+ * languages match, so its `.lproj` must exist and carry every string.
28
+ */
29
+ export declare function resolveExtensionLocalizations(config: AppLocalizationSource | undefined, widgets: IOSWidgetConfig[] | undefined): string[];
30
+ /**
31
+ * The Edit Widget sheet strings for one locale: every locale-mapped title, resolved for `locale`,
32
+ * then the development language, then the usual fallback (English, `__default`, first), so every
33
+ * key resolves in every `.lproj` and none shows verbatim. Plain-string titles are compiled into the
34
+ * Swift source as literals and need no entry.
35
+ */
36
+ export declare function collectSheetStringsForLocale(widgets: IOSWidgetConfig[] | undefined, locale: string, developmentRegion?: string): Record<string, string>;
37
+ /**
38
+ * Writes `<locale>.lproj/Localizable.strings` for every declared language. The file carries the
39
+ * Edit Widget sheet strings (ADR 0009 §6) and, even when empty, makes the `.lproj` folder a real
40
+ * resource of the extension target so iOS sees the language as supported.
41
+ *
42
+ * The extension is generated and owned by Voltra, so every `Localizable.strings` in it is ours to
43
+ * rewrite; stale ones from a removed language are deleted.
44
+ */
45
+ export declare function syncExtensionLocalizableStrings(targetPath: string, locales: string[], widgets: IOSWidgetConfig[] | undefined, developmentRegion?: string): void;
46
+ //# sourceMappingURL=localization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"localization.d.ts","sourceRoot":"","sources":["../../../../src/ios-widget/files/localization.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAIlD;;;GAGG;AACH,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACxC,GAAG,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAA;CAC5D;AAeD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,qBAAqB,GAAG,SAAS,GAAG,MAAM,CAG1F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,qBAAqB,GAAG,SAAS,EACzC,OAAO,EAAE,eAAe,EAAE,GAAG,SAAS,GACrC,MAAM,EAAE,CAqCV;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,eAAe,EAAE,GAAG,SAAS,EACtC,MAAM,EAAE,MAAM,EACd,iBAAiB,SAAO,GACvB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWxB;AASD;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAC7C,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,EAAE,eAAe,EAAE,GAAG,SAAS,EACtC,iBAAiB,SAAO,GACvB,IAAI,CA8BN"}
@@ -1 +1 @@
1
- {"version":3,"file":"swift.d.ts","sourceRoot":"","sources":["../../../../src/ios-widget/files/swift.ts"],"names":[],"mappings":"AAIA,OAAO,EAKL,KAAK,uBAAuB,EAE7B,MAAM,yBAAyB,CAAA;AAGhC,OAAO,KAAK,EAAE,4BAA4B,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAEhF,OAAO,EAA+B,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAEvF,OAAO,EACL,kCAAkC,EAClC,qCAAqC,EAEtC,MAAM,gCAAgC,CAAA;AAIvC,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,eAAe,EAAE,CAAA;IAC3B,cAAc,CAAC,EAAE,4BAA4B,EAAE,CAAA;CAChD;AAQD;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0D1F;AAuXD;;GAEG;AACH,iBAAS,yBAAyB,CAChC,OAAO,EAAE,iBAAiB,EAAE,EAC5B,cAAc,GAAE,4BAA4B,EAAO,GAClD,MAAM,CAoDR;AAiCD;;GAEG;AACH,iBAAS,0BAA0B,CAAC,iBAAiB,EAAE,uBAAuB,GAAG,MAAM,CA8CtF;AAiDD,eAAO,MAAM,QAAQ;;;;;CAKpB,CAAA"}
1
+ {"version":3,"file":"swift.d.ts","sourceRoot":"","sources":["../../../../src/ios-widget/files/swift.ts"],"names":[],"mappings":"AAIA,OAAO,EAOL,KAAK,uBAAuB,EAE7B,MAAM,yBAAyB,CAAA;AAGhC,OAAO,KAAK,EAAsB,4BAA4B,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAEpG,OAAO,EAA+B,KAAK,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAEvF,OAAO,EACL,kCAAkC,EAClC,qCAAqC,EAEtC,MAAM,gCAAgC,CAAA;AAIvC,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,eAAe,EAAE,CAAA;IAC3B,cAAc,CAAC,EAAE,4BAA4B,EAAE,CAAA;CAChD;AAQD;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,yBAAyB,GAAG,OAAO,CAAC,IAAI,CAAC,CA0D1F;AAmcD;;GAEG;AACH,iBAAS,yBAAyB,CAChC,OAAO,EAAE,iBAAiB,EAAE,EAC5B,cAAc,GAAE,4BAA4B,EAAO,GAClD,MAAM,CAoDR;AAiCD;;GAEG;AACH,iBAAS,0BAA0B,CAAC,iBAAiB,EAAE,uBAAuB,GAAG,MAAM,CA8CtF;AAiDD,eAAO,MAAM,QAAQ;;;;;CAKpB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"groups.d.ts","sourceRoot":"","sources":["../../../../src/ios-widget/xcode/groups.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAEnD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAK1D,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,uBAAuB,CAAA;CACrC;AA6DD;;GAEG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAwB5F"}
1
+ {"version":3,"file":"groups.d.ts","sourceRoot":"","sources":["../../../../src/ios-widget/xcode/groups.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAEnD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AAK1D,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,uBAAuB,CAAA;CACrC;AA6DD;;GAEG;AACH,wBAAgB,cAAc,CAAC,YAAY,EAAE,YAAY,EAAE,OAAO,EAAE,kBAAkB,GAAG,IAAI,CA2B5F"}
@@ -1,5 +1,5 @@
1
1
  import type { ConfigPlugin } from '@expo/config-plugins';
2
- import type { DynamicLiveActivityEntryConfig, DynamicWidgetEntryConfig, WidgetInitialStatePath, WidgetLabel } from '@use-voltra/expo-plugin';
2
+ import type { DynamicLiveActivityEntryConfig, DynamicWidgetEntryConfig, WidgetConfigurationOption, WidgetInitialStatePath, WidgetLabel } from '@use-voltra/expo-plugin';
3
3
  /**
4
4
  * Supported iOS Home Screen widget size families.
5
5
  */
@@ -10,10 +10,19 @@ export type IOSWidgetFamily = 'systemSmall' | 'systemMedium' | 'systemLarge' | '
10
10
  export interface AppIntentParameter {
11
11
  /** Swift property name + the key under `env.configuration`. */
12
12
  name: string;
13
- /** Label shown in the widget configuration sheet. */
14
- title: string;
15
- /** Default value used before the user configures the widget (the "from code" default). */
13
+ /**
14
+ * Label shown in the widget configuration sheet: a plain string, or a locale map
15
+ * (`{ "en": "Units", "pl": "Jednostki" }`) written to the extension's `Localizable.strings`.
16
+ */
17
+ title: WidgetLabel;
18
+ /** Default value used before the user configures the widget (the "from code" default). Must be
19
+ * one of `options[].value` when `options` is set. */
16
20
  default?: string;
21
+ /**
22
+ * Fixed values the user picks from. The sheet shows a picker with each option's (localisable)
23
+ * `title` instead of a free-text field; `env.configuration` receives the option's `value`.
24
+ */
25
+ options?: WidgetConfigurationOption[];
17
26
  }
18
27
  /**
19
28
  * AppIntent configuration for a user-configurable widget (iOS 17+).
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
@@ -1,6 +1,8 @@
1
1
  import type { IOSWidgetExtensionFiles } from '../types';
2
2
  /** Table name matches basename without extension; co-located per locale in *.lproj */
3
3
  export declare const VOLTRA_WIDGET_STRINGS_BASENAME = "VoltraWidgets.strings";
4
+ /** Default table: Edit Widget sheet strings, and the file that makes each `.lproj` a real resource. */
5
+ export declare const VOLTRA_LOCALIZABLE_STRINGS_BASENAME = "Localizable.strings";
4
6
  /**
5
7
  * Scans the widget extension target directory and returns categorized file lists.
6
8
  *
@@ -1 +1 @@
1
- {"version":3,"file":"fileDiscovery.d.ts","sourceRoot":"","sources":["../../../src/utils/fileDiscovery.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAA;AAGvD,sFAAsF;AACtF,eAAO,MAAM,8BAA8B,0BAA0B,CAAA;AAErE;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,uBAAuB,CA4E1G"}
1
+ {"version":3,"file":"fileDiscovery.d.ts","sourceRoot":"","sources":["../../../src/utils/fileDiscovery.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAA;AAGvD,sFAAsF;AACtF,eAAO,MAAM,8BAA8B,0BAA0B,CAAA;AAErE,uGAAuG;AACvG,eAAO,MAAM,mCAAmC,wBAAwB,CAAA;AAExE;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,uBAAuB,CA6E1G"}
@@ -1 +1 @@
1
- {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/validation.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,oBAAoB,EAAE,4BAA4B,EAAE,eAAe,EAAmB,MAAM,SAAS,CAAA;AAanH,wBAAgB,oCAAoC,CAClD,YAAY,EAAE,4BAA4B,EAC1C,WAAW,CAAC,EAAE,MAAM,GACnB,IAAI,CAaN;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,eAAe,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CA6B3F;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,oBAAoB,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAwEpG"}
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/validation.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,oBAAoB,EAAE,4BAA4B,EAAE,eAAe,EAAmB,MAAM,SAAS,CAAA;AAanH,wBAAgB,oCAAoC,CAClD,YAAY,EAAE,4BAA4B,EAC1C,WAAW,CAAC,EAAE,MAAM,GACnB,IAAI,CAaN;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,eAAe,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CA8B3F;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,oBAAoB,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAwEpG"}
@@ -42,6 +42,10 @@ function buildPlaceholderEnv(voltraVersion: string): Record<string, unknown> {
42
42
  widgetFamily: 'systemMedium',
43
43
  colorScheme: 'light',
44
44
  locale: 'en-US',
45
+ preferredLanguages: ['en-US'],
46
+ layoutDirection: 'ltr',
47
+ hourCycle: 'h12',
48
+ timeZone: 'UTC',
45
49
  widgetRenderingMode: 'fullColor',
46
50
  showsWidgetContainerBackground: true,
47
51
  configuration: undefined,
@@ -26,7 +26,15 @@ export function generateDynamicLiveActivityTypesSwift(liveActivities: IOSDynamic
26
26
  import ActivityKit
27
27
  import Foundation
28
28
 
29
+ // This file compiles in both the app target (pod Voltra, module
30
+ // VoltraRuntime) and the widget extension target (pod VoltraWidget,
31
+ // module VoltraWidgetRuntime). The two pods must keep distinct module
32
+ // names so their build products never collide.
33
+ #if canImport(VoltraWidgetRuntime)
34
+ import VoltraWidgetRuntime
35
+ #else
29
36
  import VoltraRuntime
37
+ #endif
30
38
 
31
39
  @objc(VoltraGeneratedDynamicLiveActivityRegistration)
32
40
  public final class VoltraGeneratedDynamicLiveActivityRegistration: NSObject {
@@ -97,7 +105,7 @@ export function generateDynamicLiveActivitiesSwift(liveActivities: IOSDynamicLiv
97
105
  import ActivityKit
98
106
  import SwiftUI
99
107
  import WidgetKit
100
- import VoltraRuntime
108
+ import VoltraWidgetRuntime
101
109
 
102
110
  `
103
111
  return [header.trim(), configurations.trim()].filter(Boolean).join('\n\n')
@@ -6,6 +6,11 @@ import type { IOSDynamicLiveActivityConfig, IOSWidgetConfig } from '../../types'
6
6
  import { generateAssets } from './assets'
7
7
  import { generateEntitlements } from './entitlements'
8
8
  import { generateInfoPlist } from './infoPlist'
9
+ import {
10
+ resolveDevelopmentRegion,
11
+ resolveExtensionLocalizations,
12
+ syncExtensionLocalizableStrings,
13
+ } from './localization'
9
14
  import { generateIOSDynamicLiveActivitiesManifest, generateIOSDynamicWidgetsManifest } from './manifest'
10
15
  import { generateSwiftFiles } from './swift'
11
16
 
@@ -29,6 +34,7 @@ export interface GenerateWidgetExtensionFilesProps {
29
34
  * - VoltraWidgetBundle.swift (widget bundle definition)
30
35
  * - VoltraWidgetInitialStates.swift (pre-rendered widget states)
31
36
  * - {targetName}.entitlements (entitlements file)
37
+ * - <locale>.lproj/Localizable.strings (declared languages and Edit Widget sheet strings)
32
38
  *
33
39
  * This should run before configureXcodeProject so the files exist when Xcode project is configured.
34
40
  */
@@ -51,8 +57,11 @@ export const generateWidgetExtensionFiles: ConfigPlugin<GenerateWidgetExtensionF
51
57
  fs.mkdirSync(targetPath, { recursive: true })
52
58
  }
53
59
 
60
+ // Languages the extension declares, mirrored from the app (ADR 0009 §4)
61
+ const localizations = resolveExtensionLocalizations(config, widgets)
62
+
54
63
  // Generate Info.plist
55
- generateInfoPlist(targetPath, targetName, version, buildNumber, voltraVersion)
64
+ generateInfoPlist(targetPath, targetName, version, buildNumber, voltraVersion, localizations)
56
65
 
57
66
  // Generate Assets.xcassets and copy user images
58
67
  generateAssets({ targetPath })
@@ -65,6 +74,10 @@ export const generateWidgetExtensionFiles: ConfigPlugin<GenerateWidgetExtensionF
65
74
  liveActivities,
66
75
  })
67
76
 
77
+ // <locale>.lproj/Localizable.strings: Edit Widget sheet strings, and one real resource per
78
+ // declared language. Runs after the gallery strings, which share the .lproj folders.
79
+ syncExtensionLocalizableStrings(targetPath, localizations, widgets, resolveDevelopmentRegion(config))
80
+
68
81
  // Write the iOS-owned Dynamic Widgets manifest for Metro to consume later.
69
82
  generateIOSDynamicWidgetsManifest({
70
83
  projectRoot,
@@ -3,6 +3,10 @@ import * as path from 'path'
3
3
 
4
4
  import { logger } from '@use-voltra/expo-plugin'
5
5
 
6
+ function escapeXml(value: string): string {
7
+ return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
8
+ }
9
+
6
10
  /**
7
11
  * Generates the Info.plist content for a WidgetKit extension.
8
12
  *
@@ -13,8 +17,17 @@ function generateInfoPlistContent(
13
17
  targetName: string,
14
18
  version: string,
15
19
  buildNumber: string,
16
- voltraVersion: string
20
+ voltraVersion: string,
21
+ localizations: string[]
17
22
  ): string {
23
+ // CFBundleLocalizations mirrors the app's languages into the extension (ADR 0009 §4), so
24
+ // Locale.current and SwiftUI's \.locale there resolve the way they do in the app.
25
+ const localizationsEntry =
26
+ localizations.length > 0
27
+ ? `\t<key>CFBundleLocalizations</key>\n\t<array>\n${localizations
28
+ .map((locale) => `\t\t<string>${escapeXml(locale)}</string>\n`)
29
+ .join('')}\t</array>\n`
30
+ : ''
18
31
  return `<?xml version="1.0" encoding="UTF-8"?>
19
32
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
20
33
  <plist version="1.0">
@@ -27,7 +40,7 @@ function generateInfoPlistContent(
27
40
  <string>$(EXECUTABLE_NAME)</string>
28
41
  <key>CFBundleIdentifier</key>
29
42
  <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
30
- <key>CFBundleInfoDictionaryVersion</key>
43
+ ${localizationsEntry} <key>CFBundleInfoDictionaryVersion</key>
31
44
  <string>6.0</string>
32
45
  <key>CFBundleName</key>
33
46
  <string>$(PRODUCT_NAME)</string>
@@ -57,15 +70,21 @@ function generateInfoPlistContent(
57
70
  * @param version - The app version (CFBundleShortVersionString)
58
71
  * @param buildNumber - The build number (CFBundleVersion)
59
72
  * @param voltraVersion - The installed @use-voltra/ios-client package version
73
+ * @param localizations - Languages the extension declares (`CFBundleLocalizations`); see
74
+ * `resolveExtensionLocalizations`
60
75
  */
61
76
  export function generateInfoPlist(
62
77
  targetPath: string,
63
78
  targetName: string,
64
79
  version: string,
65
80
  buildNumber: string,
66
- voltraVersion: string
81
+ voltraVersion: string,
82
+ localizations: string[] = []
67
83
  ): void {
68
84
  const infoPlistPath = path.join(targetPath, 'Info.plist')
69
- fs.writeFileSync(infoPlistPath, generateInfoPlistContent(targetName, version, buildNumber, voltraVersion))
85
+ fs.writeFileSync(
86
+ infoPlistPath,
87
+ generateInfoPlistContent(targetName, version, buildNumber, voltraVersion, localizations)
88
+ )
70
89
  logger.info('Generated Info.plist')
71
90
  }
@@ -0,0 +1,176 @@
1
+ import * as fs from 'fs'
2
+ import * as path from 'path'
3
+
4
+ import {
5
+ collectLabelLocaleKeys,
6
+ collectWidgetConfigurationStrings,
7
+ isWidgetLocalizedMap,
8
+ normalizeLocaleTag,
9
+ pickLocalizedValue,
10
+ widgetLabelEnglish,
11
+ type WidgetLabel,
12
+ } from '@use-voltra/expo-plugin'
13
+
14
+ import type { IOSWidgetConfig } from '../../types'
15
+ import { VOLTRA_LOCALIZABLE_STRINGS_BASENAME } from '../../utils/fileDiscovery'
16
+ import { escapeForSwiftStringLiteral } from './swift-utils'
17
+
18
+ /**
19
+ * The parts of the Expo config that say which languages the app supports: the `locales` map
20
+ * (`{ "pl": "./languages/pl.json" }`) and an explicit `ios.infoPlist.CFBundleLocalizations`.
21
+ */
22
+ export interface AppLocalizationSource {
23
+ locales?: Record<string, unknown> | null
24
+ ios?: { infoPlist?: Record<string, unknown> | null } | null
25
+ }
26
+
27
+ function widgetLabels(widget: IOSWidgetConfig): Array<WidgetLabel | undefined> {
28
+ return [
29
+ widget.displayName,
30
+ widget.description,
31
+ ...(widget.appIntent?.parameters ?? []).flatMap((parameter) => [
32
+ parameter.title,
33
+ ...(parameter.options ?? []).map((option) => option.title),
34
+ ]),
35
+ ]
36
+ }
37
+
38
+ const LOCALE_TAG_PATTERN = /^[a-zA-Z][a-zA-Z0-9]*([_-][a-zA-Z0-9]+)*$/
39
+
40
+ /**
41
+ * The app's development language: `ios.infoPlist.CFBundleDevelopmentRegion` when it is a literal
42
+ * locale tag, otherwise `en` (Expo's default, and what `$(DEVELOPMENT_LANGUAGE)` is in practice).
43
+ */
44
+ export function resolveDevelopmentRegion(config: AppLocalizationSource | undefined): string {
45
+ const region = config?.ios?.infoPlist?.CFBundleDevelopmentRegion
46
+ return typeof region === 'string' && LOCALE_TAG_PATTERN.test(region.trim()) ? region.trim() : 'en'
47
+ }
48
+
49
+ /**
50
+ * The languages the widget extension declares (ADR 0009 §4).
51
+ *
52
+ * iOS resolves `Locale.current` and SwiftUI's `\.locale` in the extension against the extension
53
+ * bundle's own localisations, so an extension that declares none reports `en_PL` to a Polish user
54
+ * of a Polish app. Mirroring the app's languages makes the extension resolve the way the app does.
55
+ *
56
+ * Sources: the Expo `locales` keys, `ios.infoPlist.CFBundleLocalizations`, and every locale used
57
+ * by a widget's locale maps. The app's development language (see `resolveDevelopmentRegion`) is
58
+ * added whenever anything is declared: it is the language iOS falls back to when none of the user's
59
+ * languages match, so its `.lproj` must exist and carry every string.
60
+ */
61
+ export function resolveExtensionLocalizations(
62
+ config: AppLocalizationSource | undefined,
63
+ widgets: IOSWidgetConfig[] | undefined
64
+ ): string[] {
65
+ const candidates: string[] = []
66
+
67
+ for (const locale of Object.keys(config?.locales ?? {})) {
68
+ candidates.push(locale)
69
+ }
70
+
71
+ const infoPlistLocalizations = config?.ios?.infoPlist?.CFBundleLocalizations
72
+ if (Array.isArray(infoPlistLocalizations)) {
73
+ for (const locale of infoPlistLocalizations) {
74
+ if (typeof locale === 'string') {
75
+ candidates.push(locale)
76
+ }
77
+ }
78
+ }
79
+
80
+ candidates.push(...collectLabelLocaleKeys((widgets ?? []).flatMap(widgetLabels)))
81
+
82
+ const byNormalized = new Map<string, string>()
83
+ for (const candidate of candidates) {
84
+ const trimmed = candidate.trim()
85
+ if (!trimmed || trimmed === '__default' || !LOCALE_TAG_PATTERN.test(trimmed)) {
86
+ continue
87
+ }
88
+ const normalized = normalizeLocaleTag(trimmed)
89
+ if (!byNormalized.has(normalized)) {
90
+ byNormalized.set(normalized, trimmed)
91
+ }
92
+ }
93
+
94
+ const developmentRegion = resolveDevelopmentRegion(config)
95
+ const developmentLanguage = normalizeLocaleTag(developmentRegion).split('-')[0]
96
+ if (byNormalized.size > 0 && ![...byNormalized.keys()].some((key) => key.split('-')[0] === developmentLanguage)) {
97
+ byNormalized.set(normalizeLocaleTag(developmentRegion), developmentRegion)
98
+ }
99
+
100
+ return [...byNormalized.values()].sort((a, b) => a.localeCompare(b))
101
+ }
102
+
103
+ /**
104
+ * The Edit Widget sheet strings for one locale: every locale-mapped title, resolved for `locale`,
105
+ * then the development language, then the usual fallback (English, `__default`, first), so every
106
+ * key resolves in every `.lproj` and none shows verbatim. Plain-string titles are compiled into the
107
+ * Swift source as literals and need no entry.
108
+ */
109
+ export function collectSheetStringsForLocale(
110
+ widgets: IOSWidgetConfig[] | undefined,
111
+ locale: string,
112
+ developmentRegion = 'en'
113
+ ): Record<string, string> {
114
+ const entries: Record<string, string> = {}
115
+ for (const widget of widgets ?? []) {
116
+ for (const { key, label } of collectWidgetConfigurationStrings(widget)) {
117
+ if (!isWidgetLocalizedMap(label)) {
118
+ continue
119
+ }
120
+ entries[key] = pickLocalizedValue(label, [locale, developmentRegion]) ?? widgetLabelEnglish(label)
121
+ }
122
+ }
123
+ return entries
124
+ }
125
+
126
+ function formatLocalizableStrings(entries: Record<string, string>): string {
127
+ const lines = Object.keys(entries)
128
+ .sort()
129
+ .map((key) => `"${escapeForSwiftStringLiteral(key)}" = "${escapeForSwiftStringLiteral(entries[key]!)}";`)
130
+ return `/* Voltra widget extension strings (auto-generated) */\n${lines.join('\n')}${lines.length > 0 ? '\n' : ''}`
131
+ }
132
+
133
+ /**
134
+ * Writes `<locale>.lproj/Localizable.strings` for every declared language. The file carries the
135
+ * Edit Widget sheet strings (ADR 0009 §6) and, even when empty, makes the `.lproj` folder a real
136
+ * resource of the extension target so iOS sees the language as supported.
137
+ *
138
+ * The extension is generated and owned by Voltra, so every `Localizable.strings` in it is ours to
139
+ * rewrite; stale ones from a removed language are deleted.
140
+ */
141
+ export function syncExtensionLocalizableStrings(
142
+ targetPath: string,
143
+ locales: string[],
144
+ widgets: IOSWidgetConfig[] | undefined,
145
+ developmentRegion = 'en'
146
+ ): void {
147
+ if (fs.existsSync(targetPath)) {
148
+ for (const entry of fs.readdirSync(targetPath)) {
149
+ if (!entry.endsWith('.lproj')) {
150
+ continue
151
+ }
152
+ const dirPath = path.join(targetPath, entry)
153
+ const stringsPath = path.join(dirPath, VOLTRA_LOCALIZABLE_STRINGS_BASENAME)
154
+ if (fs.existsSync(stringsPath)) {
155
+ fs.unlinkSync(stringsPath)
156
+ }
157
+ try {
158
+ if (fs.readdirSync(dirPath).length === 0) {
159
+ fs.rmdirSync(dirPath)
160
+ }
161
+ } catch {
162
+ /* ignore */
163
+ }
164
+ }
165
+ }
166
+
167
+ for (const locale of locales) {
168
+ const lproj = path.join(targetPath, `${locale}.lproj`)
169
+ fs.mkdirSync(lproj, { recursive: true })
170
+ fs.writeFileSync(
171
+ path.join(lproj, VOLTRA_LOCALIZABLE_STRINGS_BASENAME),
172
+ formatLocalizableStrings(collectSheetStringsForLocale(widgets, locale, developmentRegion)),
173
+ 'utf8'
174
+ )
175
+ }
176
+ }
@@ -7,12 +7,14 @@ import {
7
7
  logger,
8
8
  prerenderWidgetState,
9
9
  widgetLabelEnglish,
10
+ widgetParameterOptionKey,
11
+ widgetParameterTitleKey,
10
12
  type PrerenderedWidgetStates,
11
13
  type WidgetLabel,
12
14
  } from '@use-voltra/expo-plugin'
13
15
 
14
16
  import { DEFAULT_WIDGET_FAMILIES, WIDGET_FAMILY_MAP, widgetKind } from '../../constants'
15
- import type { IOSDynamicLiveActivityConfig, IOSWidgetConfig } from '../../types'
17
+ import type { AppIntentParameter, IOSDynamicLiveActivityConfig, IOSWidgetConfig } from '../../types'
16
18
  import { VOLTRA_WIDGET_STRINGS_BASENAME } from '../../utils/fileDiscovery'
17
19
  import { detectClientRenderedWidgets, type DetectedIOSWidget } from '../clientRendered'
18
20
  import { prerenderClientRenderedWidgets } from '../clientRenderedPrerender'
@@ -372,6 +374,67 @@ function generateWidgetStruct(widget: DetectedIOSWidget): string {
372
374
  `
373
375
  }
374
376
 
377
+ /**
378
+ * Generates a Dynamic Widget backed by AppIntentConfiguration (iOS 17+): a
379
+ * WidgetConfigurationIntent (params + code defaults), an AppIntentTimelineProvider that loads the
380
+ * bundle via the shared client runtime, and an AppIntentConfiguration widget. The configured
381
+ * params are passed into the render as env.configuration; the native "Edit Widget" sheet edits them.
382
+ */
383
+ /**
384
+ * A string literal for a title on the Edit Widget sheet (ADR 0009 §6). A plain string is compiled in
385
+ * as-is. A locale map becomes a bare string-literal key into the extension's default `Localizable`
386
+ * table, which `syncExtensionLocalizableStrings` fills for every declared language: the one form
387
+ * the App Intents metadata processor is documented to accept without question.
388
+ */
389
+ function sheetStringLiteral(label: WidgetLabel, key: string): string {
390
+ return `"${escapeForSwiftStringLiteral(isWidgetLocalizedMap(label) ? key : label)}"`
391
+ }
392
+
393
+ function parameterOptionEnumName(widgetId: string, parameterName: string): string {
394
+ return `VoltraWidget_${widgetId}_${parameterName}_Option`
395
+ }
396
+
397
+ /**
398
+ * Static `options` become an `AppEnum`, so the sheet shows a picker of localised labels instead of
399
+ * a free-text field. Cases are positional (`option0`…) because option values are free-form; the raw
400
+ * value is the option's `value`, which is what `env.configuration` receives.
401
+ */
402
+ function generateParameterOptionEnum(widgetId: string, parameter: AppIntentParameter): string {
403
+ const options = parameter.options ?? []
404
+ const enumName = parameterOptionEnumName(widgetId, parameter.name)
405
+ const cases = options
406
+ .map((option, index) => ` case option${index} = "${escapeForSwiftStringLiteral(option.value)}"`)
407
+ .join('\n')
408
+ const representations = options
409
+ .map(
410
+ (option, index) =>
411
+ ` .option${index}: ${sheetStringLiteral(
412
+ option.title,
413
+ widgetParameterOptionKey(widgetId, parameter.name, option.value)
414
+ )},`
415
+ )
416
+ .join('\n')
417
+ const typeTitle = sheetStringLiteral(parameter.title, widgetParameterTitleKey(widgetId, parameter.name))
418
+
419
+ return [
420
+ '@available(iOS 17.0, *)',
421
+ `enum ${enumName}: String, AppEnum {`,
422
+ cases,
423
+ '',
424
+ ` static var typeDisplayRepresentation: TypeDisplayRepresentation = ${typeTitle}`,
425
+ ` static var caseDisplayRepresentations: [${enumName}: DisplayRepresentation] = [`,
426
+ representations,
427
+ ' ]',
428
+ '}',
429
+ ].join('\n')
430
+ }
431
+
432
+ /** Index of the option `parameter.default` names, or the first option. */
433
+ function defaultOptionIndex(parameter: AppIntentParameter): number {
434
+ const index = (parameter.options ?? []).findIndex((option) => option.value === parameter.default)
435
+ return index >= 0 ? index : 0
436
+ }
437
+
375
438
  /**
376
439
  * Generates a Dynamic Widget backed by AppIntentConfiguration (iOS 17+): a
377
440
  * WidgetConfigurationIntent (params + code defaults), an AppIntentTimelineProvider that loads the
@@ -388,20 +451,35 @@ function generateClientAppIntentWidgetCode(widget: DetectedIOSWidget): string {
388
451
  const displayNameExpr = iosWidgetGalleryLabelSwiftExpr(widget.id, 'displayName', widget.displayName)
389
452
  const descriptionExpr = iosWidgetGalleryLabelSwiftExpr(widget.id, 'description', widget.description)
390
453
 
391
- const swiftDefault = (p: { default?: string }) => `"${escapeForSwiftStringLiteral(p.default ?? '')}"`
454
+ const hasOptions = (p: AppIntentParameter) => (p.options?.length ?? 0) > 0
455
+ // The raw default, as env.configuration sees it before the user configures anything.
456
+ const swiftDefault = (p: AppIntentParameter) =>
457
+ hasOptions(p)
458
+ ? `"${escapeForSwiftStringLiteral(p.options![defaultOptionIndex(p)]!.value)}"`
459
+ : `"${escapeForSwiftStringLiteral(p.default ?? '')}"`
460
+ const swiftType = (p: AppIntentParameter) => (hasOptions(p) ? parameterOptionEnumName(widget.id, p.name) : 'String')
461
+ const parameterDefault = (p: AppIntentParameter) =>
462
+ hasOptions(p) ? `.option${defaultOptionIndex(p)}` : swiftDefault(p)
463
+ const configuredValue = (p: AppIntentParameter) =>
464
+ hasOptions(p) ? `configuration.${p.name}.rawValue` : `configuration.${p.name}`
392
465
  const dictLiteral = (entries: string[]) => (entries.length > 0 ? `[${entries.join(', ')}]` : '[:]')
393
466
 
467
+ const optionEnums = params
468
+ .filter(hasOptions)
469
+ .map((p) => generateParameterOptionEnum(widget.id, p))
470
+ .join('\n\n')
394
471
  const paramDecls = params
395
472
  .map(
396
473
  (p) =>
397
- ` @Parameter(title: "${escapeForSwiftStringLiteral(p.title)}", default: ${swiftDefault(p)})\n var ${
398
- p.name
399
- }: String`
474
+ ` @Parameter(title: ${sheetStringLiteral(
475
+ p.title,
476
+ widgetParameterTitleKey(widget.id, p.name)
477
+ )}, default: ${parameterDefault(p)})\n var ${p.name}: ${swiftType(p)}`
400
478
  )
401
479
  .join('\n\n')
402
- const initParams = params.map((p) => `${p.name}: String`).join(', ')
480
+ const initParams = params.map((p) => `${p.name}: ${swiftType(p)}`).join(', ')
403
481
  const initBody = params.map((p) => ` self.${p.name} = ${p.name}`).join('\n')
404
- const configuredDict = dictLiteral(params.map((p) => `"${p.name}": configuration.${p.name}`))
482
+ const configuredDict = dictLiteral(params.map((p) => `"${p.name}": ${configuredValue(p)}`))
405
483
  const defaultDict = dictLiteral(params.map((p) => `"${p.name}": ${swiftDefault(p)}`))
406
484
  // A server-driven Dynamic Widget fetches on every timeline request and schedules the next one
407
485
  // from its resolved interval; a plain one has nothing to ask again for, so its policy is .never.
@@ -418,7 +496,7 @@ function generateClientAppIntentWidgetCode(widget: DetectedIOSWidget): string {
418
496
 
419
497
  return dedent`
420
498
  // MARK: - Client-rendered AppIntent widget: ${widget.id}
421
-
499
+ ${optionEnums ? `\n${optionEnums}\n` : ''}
422
500
  @available(iOS 17.0, *)
423
501
  struct ${intentName}: WidgetConfigurationIntent {
424
502
  static var title: LocalizedStringResource = "${intentTitle}"
@@ -518,7 +596,7 @@ function generateWidgetBundleSwift(
518
596
 
519
597
  ${foundationImport}${appIntentsImport}import SwiftUI
520
598
  import WidgetKit
521
- import VoltraRuntime
599
+ import VoltraWidgetRuntime
522
600
 
523
601
  @main
524
602
  struct VoltraWidgetBundle: WidgetBundle {
@@ -552,7 +630,7 @@ function generateDefaultWidgetBundleSwift(): string {
552
630
 
553
631
  import SwiftUI
554
632
  import WidgetKit
555
- import VoltraRuntime // Import Voltra widgets
633
+ import VoltraWidgetRuntime // Import Voltra widgets
556
634
 
557
635
  @main
558
636
  struct VoltraWidgetBundle: WidgetBundle {
@@ -1,7 +1,7 @@
1
1
  import { XcodeProject } from '@expo/config-plugins'
2
2
 
3
3
  import type { IOSWidgetExtensionFiles } from '../../types'
4
- import { ensureWidgetFileReference } from './fileReferences'
4
+ import { ensureWidgetFileReference, normalizeRef } from './fileReferences'
5
5
 
6
6
  const pbxFile = require('xcode/lib/pbxFile')
7
7
 
@@ -89,8 +89,11 @@ export function ensurePbxGroup(xcodeProject: XcodeProject, options: AddPbxGroupO
89
89
  for (const filePath of allFiles) {
90
90
  const file = new pbxFile(filePath)
91
91
  const fileRef = ensureWidgetFileReference(xcodeProject, filePath, targetName)
92
+ // Localized files share a basename across `.lproj` folders (`pl.lproj/Localizable.strings`,
93
+ // `de.lproj/Localizable.strings`), so for them only the file reference itself identifies the child.
94
+ const isLocalized = filePath.includes('.lproj/')
92
95
  const alreadyInGroup = existingGroup.children.some(
93
- (child: any) => child.value === fileRef || child.comment === file.basename
96
+ (child: any) => normalizeRef(child.value) === fileRef || (!isLocalized && child.comment === file.basename)
94
97
  )
95
98
  if (!alreadyInGroup) {
96
99
  existingGroup.children.push({ value: fileRef, comment: file.basename })
@@ -3,6 +3,7 @@ import type { ConfigPlugin } from '@expo/config-plugins'
3
3
  import type {
4
4
  DynamicLiveActivityEntryConfig,
5
5
  DynamicWidgetEntryConfig,
6
+ WidgetConfigurationOption,
6
7
  WidgetInitialStatePath,
7
8
  WidgetLabel,
8
9
  } from '@use-voltra/expo-plugin'
@@ -25,10 +26,19 @@ export type IOSWidgetFamily =
25
26
  export interface AppIntentParameter {
26
27
  /** Swift property name + the key under `env.configuration`. */
27
28
  name: string
28
- /** Label shown in the widget configuration sheet. */
29
- title: string
30
- /** Default value used before the user configures the widget (the "from code" default). */
29
+ /**
30
+ * Label shown in the widget configuration sheet: a plain string, or a locale map
31
+ * (`{ "en": "Units", "pl": "Jednostki" }`) written to the extension's `Localizable.strings`.
32
+ */
33
+ title: WidgetLabel
34
+ /** Default value used before the user configures the widget (the "from code" default). Must be
35
+ * one of `options[].value` when `options` is set. */
31
36
  default?: string
37
+ /**
38
+ * Fixed values the user picks from. The sheet shows a picker with each option's (localisable)
39
+ * `title` instead of a free-text field; `env.configuration` receives the option's `value`.
40
+ */
41
+ options?: WidgetConfigurationOption[]
32
42
  }
33
43
 
34
44
  /**