voltra 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.
- package/build/cjs/config/normalize.js +41 -4
- package/build/cjs/config/normalize.js.map +1 -1
- package/build/cjs/platforms/android/generated.js +55 -3
- package/build/cjs/platforms/android/generated.js.map +1 -1
- package/build/cjs/platforms/ios/generated.js +188 -3
- package/build/cjs/platforms/ios/generated.js.map +1 -1
- package/build/esm/config/normalize.js +41 -4
- package/build/esm/config/normalize.js.map +1 -1
- package/build/esm/platforms/android/generated.js +54 -2
- package/build/esm/platforms/android/generated.js.map +1 -1
- package/build/esm/platforms/ios/generated.js +188 -3
- package/build/esm/platforms/ios/generated.js.map +1 -1
- package/build/types/config/normalize.d.ts.map +1 -1
- package/build/types/config/types.d.ts +16 -5
- package/build/types/config/types.d.ts.map +1 -1
- package/build/types/platforms/android/generated.d.ts +8 -1
- package/build/types/platforms/android/generated.d.ts.map +1 -1
- package/build/types/platforms/ios/generated.d.ts +16 -0
- package/build/types/platforms/ios/generated.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/config/normalize.ts +57 -4
- package/src/config/types.ts +17 -5
- package/src/platforms/android/generated.node.test.ts +46 -0
- package/src/platforms/android/generated.ts +67 -2
- package/src/platforms/ios/generated.node.test.ts +90 -0
- package/src/platforms/ios/generated.ts +262 -5
|
@@ -59,3 +59,93 @@ describe('generateWidgetBundleSwift', () => {
|
|
|
59
59
|
assert.ok(!swift.includes('Voltra_Widget_streak'))
|
|
60
60
|
})
|
|
61
61
|
})
|
|
62
|
+
|
|
63
|
+
describe('localisation (ADR 0009)', () => {
|
|
64
|
+
const configurable: DetectedIOSWidget = {
|
|
65
|
+
id: 'weather',
|
|
66
|
+
entry: './widgets/weather.tsx',
|
|
67
|
+
displayName: { en: 'Weather', pl: 'Pogoda' },
|
|
68
|
+
description: 'Forecast',
|
|
69
|
+
supportedFamilies: ['systemSmall'],
|
|
70
|
+
clientRendered: true,
|
|
71
|
+
clientSourcePath: '/tmp/weather.tsx',
|
|
72
|
+
appIntent: {
|
|
73
|
+
parameters: [
|
|
74
|
+
{
|
|
75
|
+
name: 'units',
|
|
76
|
+
title: { en: 'Units', pl: 'Jednostki' },
|
|
77
|
+
default: 'imperial',
|
|
78
|
+
options: [
|
|
79
|
+
{ value: 'metric', title: { en: 'Metric', pl: 'Metryczne' } },
|
|
80
|
+
{ value: 'imperial', title: 'Imperial' },
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
{ name: 'city', title: 'City', default: 'Warsaw' },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
test('declares the app and widget languages, plus en', () => {
|
|
89
|
+
assert.deepEqual(__test__.resolveExtensionLocalizations(['de'], [configurable]), ['de', 'en', 'pl'])
|
|
90
|
+
assert.deepEqual(__test__.resolveExtensionLocalizations([], []), [])
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('uses a non-English development language for the declared set and the string fallback', async () => {
|
|
94
|
+
assert.deepEqual(__test__.resolveExtensionLocalizations(['fr'], [], 'de'), ['de', 'fr'])
|
|
95
|
+
|
|
96
|
+
const fs = await import('node:fs')
|
|
97
|
+
const os = await import('node:os')
|
|
98
|
+
const path = await import('node:path')
|
|
99
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'voltra-cli-devregion-'))
|
|
100
|
+
try {
|
|
101
|
+
await __test__.generateLocalizableStrings(root, path.join(root, 'Ext'), ['fr'], [configurable], 'pl')
|
|
102
|
+
const french = fs.readFileSync(path.join(root, 'Ext', 'fr.lproj', 'Localizable.strings'), 'utf8')
|
|
103
|
+
assert.ok(french.includes('"voltra_widget_weather_param_units_title" = "Jednostki";'))
|
|
104
|
+
} finally {
|
|
105
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('keys locale-mapped sheet titles and emits an AppEnum for static options', () => {
|
|
110
|
+
const swift = __test__.generateWidgetBundleSwift([configurable])
|
|
111
|
+
|
|
112
|
+
assert.ok(swift.includes('static var title: LocalizedStringResource = "Configure Weather"'))
|
|
113
|
+
assert.ok(swift.includes('enum VoltraWidget_weather_units_Option: String, AppEnum {'))
|
|
114
|
+
assert.ok(swift.includes('.option0: "voltra_widget_weather_param_units_option_metric",'))
|
|
115
|
+
assert.ok(swift.includes('.option1: "Imperial",'))
|
|
116
|
+
assert.ok(
|
|
117
|
+
swift.includes(
|
|
118
|
+
'@Parameter(title: "voltra_widget_weather_param_units_title", default: .option1)\n var units: VoltraWidget_weather_units_Option'
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
assert.ok(swift.includes('@Parameter(title: "City", default: "Warsaw")\n var city: String'))
|
|
122
|
+
assert.ok(swift.includes('"units": configuration.units.rawValue'))
|
|
123
|
+
assert.ok(swift.includes('configuration: ["units": "imperial", "city": "Warsaw"]'))
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('writes Localizable.strings with fallback for every declared language', async () => {
|
|
127
|
+
const fs = await import('node:fs')
|
|
128
|
+
const os = await import('node:os')
|
|
129
|
+
const path = await import('node:path')
|
|
130
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'voltra-cli-l10n-'))
|
|
131
|
+
try {
|
|
132
|
+
const result = await __test__.generateLocalizableStrings(
|
|
133
|
+
root,
|
|
134
|
+
path.join(root, 'Ext'),
|
|
135
|
+
['en', 'fr', 'pl'],
|
|
136
|
+
[configurable]
|
|
137
|
+
)
|
|
138
|
+
assert.deepEqual(result.files.sort(), [
|
|
139
|
+
'Ext/en.lproj/Localizable.strings',
|
|
140
|
+
'Ext/fr.lproj/Localizable.strings',
|
|
141
|
+
'Ext/pl.lproj/Localizable.strings',
|
|
142
|
+
])
|
|
143
|
+
const polish = fs.readFileSync(path.join(root, 'Ext', 'pl.lproj', 'Localizable.strings'), 'utf8')
|
|
144
|
+
assert.ok(polish.includes('"voltra_widget_weather_param_units_title" = "Jednostki";'))
|
|
145
|
+
const french = fs.readFileSync(path.join(root, 'Ext', 'fr.lproj', 'Localizable.strings'), 'utf8')
|
|
146
|
+
assert.ok(french.includes('"voltra_widget_weather_param_units_title" = "Units";'))
|
|
147
|
+
} finally {
|
|
148
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
})
|
|
@@ -29,6 +29,7 @@ const FONT_EXTENSIONS = new Set(['.ttf', '.otf', '.woff', '.woff2'])
|
|
|
29
29
|
const MAX_IMAGE_SIZE_BYTES = 4096
|
|
30
30
|
const DEFAULT_INITIAL_STATE_LOCALE = '__default'
|
|
31
31
|
const VOLTRA_WIDGET_STRINGS_BASENAME = 'VoltraWidgets.strings'
|
|
32
|
+
const VOLTRA_LOCALIZABLE_STRINGS_BASENAME = 'Localizable.strings'
|
|
32
33
|
const IOS_DYNAMIC_WIDGET_MANIFEST_PATH = path.join('.voltra', 'manifest.ios.json')
|
|
33
34
|
|
|
34
35
|
const IOS_WIDGET_FAMILY_MAP: Record<IOSWidgetFamily, string> = {
|
|
@@ -123,6 +124,10 @@ interface MainAppMetadata {
|
|
|
123
124
|
shortVersionString: string
|
|
124
125
|
buildNumber: string
|
|
125
126
|
urlTypes?: Array<{ CFBundleURLSchemes: string[] }>
|
|
127
|
+
/** The app's own `CFBundleLocalizations`, mirrored into the extension (ADR 0009 §4). */
|
|
128
|
+
localizations: string[]
|
|
129
|
+
/** The app's `CFBundleDevelopmentRegion` when it is a literal tag, otherwise `en`. */
|
|
130
|
+
developmentRegion: string
|
|
126
131
|
}
|
|
127
132
|
|
|
128
133
|
type WidgetVariants = Record<string, unknown>
|
|
@@ -226,6 +231,15 @@ export async function generateIOSFiles(options: GenerateIOSFilesOptions): Promis
|
|
|
226
231
|
const localizedStringResults = await generateLocalizedWidgetStrings(projectRoot, targetPath, detectedWidgets)
|
|
227
232
|
mergeResult(localizedStringResults, changes, warnings, generatedFiles)
|
|
228
233
|
|
|
234
|
+
const localizableStringResults = await generateLocalizableStrings(
|
|
235
|
+
projectRoot,
|
|
236
|
+
targetPath,
|
|
237
|
+
resolveExtensionLocalizations(mainAppMetadata.localizations, detectedWidgets, mainAppMetadata.developmentRegion),
|
|
238
|
+
detectedWidgets,
|
|
239
|
+
mainAppMetadata.developmentRegion
|
|
240
|
+
)
|
|
241
|
+
mergeResult(localizableStringResults, changes, warnings, generatedFiles)
|
|
242
|
+
|
|
229
243
|
return {
|
|
230
244
|
changes,
|
|
231
245
|
files: [...generatedFiles].sort(),
|
|
@@ -245,6 +259,11 @@ async function generateInfoPlistFile(
|
|
|
245
259
|
voltraVersion: string
|
|
246
260
|
): Promise<GeneratedFileResult> {
|
|
247
261
|
const plistPath = path.join(targetPath, 'Info.plist')
|
|
262
|
+
const extensionLocalizations = resolveExtensionLocalizations(
|
|
263
|
+
mainAppMetadata.localizations,
|
|
264
|
+
widgets,
|
|
265
|
+
mainAppMetadata.developmentRegion
|
|
266
|
+
)
|
|
248
267
|
const fontNames = ios.fonts.map((fontPath) => path.basename(fontPath)).sort()
|
|
249
268
|
const serverWidgets = widgets.filter((widget) => widget.serverUpdate)
|
|
250
269
|
const hasClientRenderedWidget = widgets.some((widget) => widget.clientRendered)
|
|
@@ -267,6 +286,9 @@ async function generateInfoPlistFile(
|
|
|
267
286
|
CFBundleDisplayName: targetName,
|
|
268
287
|
CFBundleExecutable: '$(EXECUTABLE_NAME)',
|
|
269
288
|
CFBundleIdentifier: '$(PRODUCT_BUNDLE_IDENTIFIER)',
|
|
289
|
+
// Mirrors the app's languages so Locale.current and \.locale in the extension resolve like
|
|
290
|
+
// they do in the app (ADR 0009 §4).
|
|
291
|
+
CFBundleLocalizations: extensionLocalizations.length > 0 ? extensionLocalizations : undefined,
|
|
270
292
|
CFBundleInfoDictionaryVersion: '6.0',
|
|
271
293
|
CFBundleName: '$(PRODUCT_NAME)',
|
|
272
294
|
CFBundlePackageType: '$(PRODUCT_BUNDLE_PACKAGE_TYPE)',
|
|
@@ -445,6 +467,149 @@ async function generateLocalizedWidgetStrings(
|
|
|
445
467
|
}
|
|
446
468
|
}
|
|
447
469
|
|
|
470
|
+
const LOCALE_TAG_PATTERN = /^[a-zA-Z][a-zA-Z0-9]*([_-][a-zA-Z0-9]+)*$/
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* The languages the widget extension declares (ADR 0009 §4): the app's `CFBundleLocalizations` and
|
|
474
|
+
* every locale a widget's locale maps use, plus the app's development language, which iOS falls back
|
|
475
|
+
* to when none of the user's languages match. Mirrors `resolveExtensionLocalizations` in the Expo
|
|
476
|
+
* plugin.
|
|
477
|
+
*/
|
|
478
|
+
function resolveExtensionLocalizations(
|
|
479
|
+
appLocalizations: string[],
|
|
480
|
+
widgets: NormalizedIOSWidgetConfig[],
|
|
481
|
+
developmentRegion = 'en'
|
|
482
|
+
): string[] {
|
|
483
|
+
const candidates = [...appLocalizations]
|
|
484
|
+
for (const widget of widgets) {
|
|
485
|
+
for (const label of widgetLabels(widget)) {
|
|
486
|
+
if (label !== undefined && isWidgetLocalizedMap(label)) {
|
|
487
|
+
candidates.push(...Object.keys(label).filter((locale) => label[locale]?.trim()))
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const byNormalized = new Map<string, string>()
|
|
493
|
+
for (const candidate of candidates) {
|
|
494
|
+
const trimmed = candidate.trim()
|
|
495
|
+
if (!trimmed || trimmed === DEFAULT_INITIAL_STATE_LOCALE || !LOCALE_TAG_PATTERN.test(trimmed)) {
|
|
496
|
+
continue
|
|
497
|
+
}
|
|
498
|
+
const normalized = normalizeLocaleTag(trimmed)
|
|
499
|
+
if (!byNormalized.has(normalized)) {
|
|
500
|
+
byNormalized.set(normalized, trimmed)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const developmentLanguage = normalizeLocaleTag(developmentRegion).split('-')[0]
|
|
505
|
+
if (byNormalized.size > 0 && ![...byNormalized.keys()].some((key) => key.split('-')[0] === developmentLanguage)) {
|
|
506
|
+
byNormalized.set(normalizeLocaleTag(developmentRegion), developmentRegion)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return [...byNormalized.values()].sort((a, b) => a.localeCompare(b))
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function widgetLabels(widget: NormalizedIOSWidgetConfig): Array<WidgetLabel | undefined> {
|
|
513
|
+
return [
|
|
514
|
+
widget.displayName,
|
|
515
|
+
widget.description,
|
|
516
|
+
...collectWidgetConfigurationStrings(widget).map(({ label }) => label),
|
|
517
|
+
]
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* The Edit Widget sheet strings (ADR 0009 §6), keyed the same way on both platforms. Mirrors
|
|
522
|
+
* `collectWidgetConfigurationStrings` in `@use-voltra/expo-plugin`.
|
|
523
|
+
*/
|
|
524
|
+
function collectWidgetConfigurationStrings(
|
|
525
|
+
widget: NormalizedIOSWidgetConfig
|
|
526
|
+
): Array<{ key: string; label: WidgetLabel }> {
|
|
527
|
+
const strings: Array<{ key: string; label: WidgetLabel }> = []
|
|
528
|
+
for (const parameter of widget.appIntent?.parameters ?? []) {
|
|
529
|
+
strings.push({ key: widgetParameterTitleKey(widget.id, parameter.name), label: parameter.title })
|
|
530
|
+
for (const option of parameter.options ?? []) {
|
|
531
|
+
strings.push({ key: widgetParameterOptionKey(widget.id, parameter.name, option.value), label: option.title })
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return strings
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function widgetParameterTitleKey(widgetId: string, parameterName: string): string {
|
|
538
|
+
return `voltra_widget_${widgetId}_param_${parameterName}_title`
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function widgetParameterOptionKey(widgetId: string, parameterName: string, optionValue: string): string {
|
|
542
|
+
return `voltra_widget_${widgetId}_param_${parameterName}_option_${optionValue}`
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* `<locale>.lproj/Localizable.strings` for every declared language: the locale-mapped sheet strings,
|
|
547
|
+
* resolved with development-language and English fallback so a key never shows verbatim, and — even
|
|
548
|
+
* when empty —
|
|
549
|
+
* the resource that makes the `.lproj` folder part of the extension.
|
|
550
|
+
*/
|
|
551
|
+
async function generateLocalizableStrings(
|
|
552
|
+
projectRoot: string,
|
|
553
|
+
targetPath: string,
|
|
554
|
+
locales: string[],
|
|
555
|
+
widgets: NormalizedIOSWidgetConfig[],
|
|
556
|
+
developmentRegion = 'en'
|
|
557
|
+
): Promise<GenerateIOSFilesResult> {
|
|
558
|
+
const changes: ReportedChange[] = []
|
|
559
|
+
const generatedFiles = new Set<string>()
|
|
560
|
+
|
|
561
|
+
for (const locale of locales) {
|
|
562
|
+
const entries: Record<string, string> = {}
|
|
563
|
+
for (const widget of widgets) {
|
|
564
|
+
for (const { key, label } of collectWidgetConfigurationStrings(widget)) {
|
|
565
|
+
if (isWidgetLocalizedMap(label)) {
|
|
566
|
+
entries[key] = pickLocalizedLabel(label, [locale, developmentRegion])
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
const keys = Object.keys(entries).sort()
|
|
571
|
+
const content = `/* Voltra widget extension strings (auto-generated) */\n${keys
|
|
572
|
+
.map((key) => `${JSON.stringify(key)} = ${JSON.stringify(entries[key])};\n`)
|
|
573
|
+
.join('')}`
|
|
574
|
+
const result = await writeGeneratedTextFile(
|
|
575
|
+
projectRoot,
|
|
576
|
+
path.join(targetPath, `${locale}.lproj`, VOLTRA_LOCALIZABLE_STRINGS_BASENAME),
|
|
577
|
+
content
|
|
578
|
+
)
|
|
579
|
+
mergeSingleResult(result, changes, generatedFiles)
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return {
|
|
583
|
+
changes,
|
|
584
|
+
files: [...generatedFiles],
|
|
585
|
+
warnings: [],
|
|
586
|
+
targetName: '',
|
|
587
|
+
targetPath,
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function normalizeLocaleTag(tag: string): string {
|
|
592
|
+
return tag.trim().toLowerCase().replace(/_/g, '-')
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** For each wanted locale an exact tag, then its language; then English: the initial-state order. */
|
|
596
|
+
function pickLocalizedLabel(label: Record<string, string>, locales: string[]): string {
|
|
597
|
+
const entries = Object.entries(label).filter(([, value]) => value.trim())
|
|
598
|
+
for (const locale of locales) {
|
|
599
|
+
const wanted = normalizeLocaleTag(locale)
|
|
600
|
+
const exact = entries.find(([key]) => normalizeLocaleTag(key) === wanted)
|
|
601
|
+
if (exact) {
|
|
602
|
+
return exact[1]
|
|
603
|
+
}
|
|
604
|
+
const language = wanted.split('-')[0]
|
|
605
|
+
const sameLanguage = entries.find(([key]) => normalizeLocaleTag(key).split('-')[0] === language)
|
|
606
|
+
if (sameLanguage) {
|
|
607
|
+
return sameLanguage[1]
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return widgetLabelEnglish(label)
|
|
611
|
+
}
|
|
612
|
+
|
|
448
613
|
/**
|
|
449
614
|
* The widget extension has a single Info.plist, so its version has to come from one of the app's.
|
|
450
615
|
* An app whose build configurations disagree on the version would ship an extension that matches
|
|
@@ -489,11 +654,23 @@ async function readMainAppMetadata(infoPlistPath: string): Promise<MainAppMetada
|
|
|
489
654
|
const shortVersionString = readPlistString(dict, 'CFBundleShortVersionString') ?? '1.0.0'
|
|
490
655
|
const buildNumber = readPlistString(dict, 'CFBundleVersion') ?? '1'
|
|
491
656
|
const urlTypes = readUrlTypes(dict)
|
|
657
|
+
const localizationsValue = dict.CFBundleLocalizations
|
|
658
|
+
const localizations = Array.isArray(localizationsValue)
|
|
659
|
+
? localizationsValue.filter((value): value is string => typeof value === 'string')
|
|
660
|
+
: []
|
|
661
|
+
|
|
662
|
+
// RN CLI templates set `$(DEVELOPMENT_LANGUAGE)`, which only the build resolves; `en` is what that
|
|
663
|
+
// is in practice.
|
|
664
|
+
const developmentRegionValue = readPlistString(dict, 'CFBundleDevelopmentRegion')?.trim()
|
|
665
|
+
const developmentRegion =
|
|
666
|
+
developmentRegionValue && LOCALE_TAG_PATTERN.test(developmentRegionValue) ? developmentRegionValue : 'en'
|
|
492
667
|
|
|
493
668
|
return {
|
|
494
669
|
shortVersionString,
|
|
495
670
|
buildNumber,
|
|
496
671
|
urlTypes: urlTypes.length > 0 ? urlTypes : undefined,
|
|
672
|
+
localizations,
|
|
673
|
+
developmentRegion,
|
|
497
674
|
}
|
|
498
675
|
}
|
|
499
676
|
|
|
@@ -718,20 +895,34 @@ function generateClientAppIntentWidgetCode(
|
|
|
718
895
|
const intentTitle = `Configure ${widgetLabelEnglish(widget.displayName)}`
|
|
719
896
|
const displayNameExpr = createSwiftLabelExpression(widget.id, 'displayName', widget.displayName)
|
|
720
897
|
const descriptionExpr = createSwiftLabelExpression(widget.id, 'description', widget.description)
|
|
898
|
+
const optionEnums = widget.appIntent.parameters
|
|
899
|
+
.filter(hasParameterOptions)
|
|
900
|
+
.map((parameter) => generateParameterOptionEnum(widget.id, parameter))
|
|
721
901
|
const parameterDeclarations = widget.appIntent.parameters
|
|
722
902
|
.map(
|
|
723
903
|
(parameter) =>
|
|
724
|
-
` @Parameter(title: ${
|
|
725
|
-
parameter.
|
|
726
|
-
|
|
904
|
+
` @Parameter(title: ${sheetStringLiteral(
|
|
905
|
+
parameter.title,
|
|
906
|
+
widgetParameterTitleKey(widget.id, parameter.name)
|
|
907
|
+
)}, default: ${parameterDefaultExpression(parameter)})\n var ${parameter.name}: ${parameterSwiftType(
|
|
908
|
+
widget.id,
|
|
909
|
+
parameter
|
|
910
|
+
)}`
|
|
727
911
|
)
|
|
728
912
|
.join('\n\n')
|
|
729
|
-
const initializerParameters = widget.appIntent.parameters
|
|
913
|
+
const initializerParameters = widget.appIntent.parameters
|
|
914
|
+
.map((parameter) => `${parameter.name}: ${parameterSwiftType(widget.id, parameter)}`)
|
|
915
|
+
.join(', ')
|
|
730
916
|
const initializerAssignments = widget.appIntent.parameters
|
|
731
917
|
.map((parameter) => ` self.${parameter.name} = ${parameter.name}`)
|
|
732
918
|
.join('\n')
|
|
733
919
|
const configuredDictionary = createSwiftDictionaryLiteral(
|
|
734
|
-
widget.appIntent.parameters.map(
|
|
920
|
+
widget.appIntent.parameters.map(
|
|
921
|
+
(parameter) =>
|
|
922
|
+
`${JSON.stringify(parameter.name)}: configuration.${parameter.name}${
|
|
923
|
+
hasParameterOptions(parameter) ? '.rawValue' : ''
|
|
924
|
+
}`
|
|
925
|
+
)
|
|
735
926
|
)
|
|
736
927
|
const defaultDictionary = createSwiftDictionaryLiteral(
|
|
737
928
|
widget.appIntent.parameters.map((parameter) => `${JSON.stringify(parameter.name)}: ${swiftDefaultValue(parameter)}`)
|
|
@@ -754,6 +945,7 @@ function generateClientAppIntentWidgetCode(
|
|
|
754
945
|
]
|
|
755
946
|
|
|
756
947
|
return [
|
|
948
|
+
...optionEnums.flatMap((optionEnum) => [optionEnum, '']),
|
|
757
949
|
`@available(iOS 17.0, *)`,
|
|
758
950
|
`struct ${intentName}: WidgetConfigurationIntent {`,
|
|
759
951
|
` static var title: LocalizedStringResource = ${JSON.stringify(intentTitle)}`,
|
|
@@ -828,10 +1020,69 @@ function createSwiftLabelExpression(
|
|
|
828
1020
|
)}), table: ${JSON.stringify('VoltraWidgets')}))`
|
|
829
1021
|
}
|
|
830
1022
|
|
|
1023
|
+
/** The raw default, as env.configuration sees it before the user configures anything. */
|
|
831
1024
|
function swiftDefaultValue(parameter: IOSWidgetAppIntentParameter): string {
|
|
1025
|
+
if (hasParameterOptions(parameter)) {
|
|
1026
|
+
return JSON.stringify(parameter.options![defaultOptionIndex(parameter)]!.value)
|
|
1027
|
+
}
|
|
832
1028
|
return JSON.stringify(parameter.default ?? '')
|
|
833
1029
|
}
|
|
834
1030
|
|
|
1031
|
+
function hasParameterOptions(parameter: IOSWidgetAppIntentParameter): boolean {
|
|
1032
|
+
return (parameter.options?.length ?? 0) > 0
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function defaultOptionIndex(parameter: IOSWidgetAppIntentParameter): number {
|
|
1036
|
+
const index = (parameter.options ?? []).findIndex((option) => option.value === parameter.default)
|
|
1037
|
+
return index >= 0 ? index : 0
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function parameterOptionEnumName(widgetId: string, parameterName: string): string {
|
|
1041
|
+
return `VoltraWidget_${widgetId}_${parameterName}_Option`
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
function parameterSwiftType(widgetId: string, parameter: IOSWidgetAppIntentParameter): string {
|
|
1045
|
+
return hasParameterOptions(parameter) ? parameterOptionEnumName(widgetId, parameter.name) : 'String'
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function parameterDefaultExpression(parameter: IOSWidgetAppIntentParameter): string {
|
|
1049
|
+
return hasParameterOptions(parameter) ? `.option${defaultOptionIndex(parameter)}` : swiftDefaultValue(parameter)
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* A title on the Edit Widget sheet (ADR 0009 §6): a plain string is compiled in as-is; a locale map
|
|
1054
|
+
* becomes a bare string-literal key into the extension's `Localizable` table.
|
|
1055
|
+
*/
|
|
1056
|
+
function sheetStringLiteral(label: WidgetLabel, key: string): string {
|
|
1057
|
+
return JSON.stringify(isWidgetLocalizedMap(label) ? key : label)
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/** Static options become an AppEnum so the sheet shows a picker of (localised) labels. */
|
|
1061
|
+
function generateParameterOptionEnum(widgetId: string, parameter: IOSWidgetAppIntentParameter): string {
|
|
1062
|
+
const enumName = parameterOptionEnumName(widgetId, parameter.name)
|
|
1063
|
+
const options = parameter.options ?? []
|
|
1064
|
+
return [
|
|
1065
|
+
'@available(iOS 17.0, *)',
|
|
1066
|
+
`enum ${enumName}: String, AppEnum {`,
|
|
1067
|
+
...options.map((option, index) => ` case option${index} = ${JSON.stringify(option.value)}`),
|
|
1068
|
+
'',
|
|
1069
|
+
` static var typeDisplayRepresentation: TypeDisplayRepresentation = ${sheetStringLiteral(
|
|
1070
|
+
parameter.title,
|
|
1071
|
+
widgetParameterTitleKey(widgetId, parameter.name)
|
|
1072
|
+
)}`,
|
|
1073
|
+
` static var caseDisplayRepresentations: [${enumName}: DisplayRepresentation] = [`,
|
|
1074
|
+
...options.map(
|
|
1075
|
+
(option, index) =>
|
|
1076
|
+
` .option${index}: ${sheetStringLiteral(
|
|
1077
|
+
option.title,
|
|
1078
|
+
widgetParameterOptionKey(widgetId, parameter.name, option.value)
|
|
1079
|
+
)},`
|
|
1080
|
+
),
|
|
1081
|
+
' ]',
|
|
1082
|
+
'}',
|
|
1083
|
+
].join('\n')
|
|
1084
|
+
}
|
|
1085
|
+
|
|
835
1086
|
function createSwiftDictionaryLiteral(entries: string[]): string {
|
|
836
1087
|
return entries.length > 0 ? `[${entries.join(', ')}]` : '[:]'
|
|
837
1088
|
}
|
|
@@ -975,6 +1226,10 @@ async function prerenderClientRenderedWidgets(
|
|
|
975
1226
|
widgetFamily: 'systemMedium',
|
|
976
1227
|
colorScheme: 'light',
|
|
977
1228
|
locale: 'en-US',
|
|
1229
|
+
preferredLanguages: ['en-US'],
|
|
1230
|
+
layoutDirection: 'ltr',
|
|
1231
|
+
hourCycle: 'h12',
|
|
1232
|
+
timeZone: 'UTC',
|
|
978
1233
|
widgetRenderingMode: 'fullColor',
|
|
979
1234
|
showsWidgetContainerBackground: true,
|
|
980
1235
|
configuration: undefined,
|
|
@@ -1328,5 +1583,7 @@ function mergeSingleResult(result: GeneratedFileResult, changes: ReportedChange[
|
|
|
1328
1583
|
}
|
|
1329
1584
|
|
|
1330
1585
|
export const __test__ = {
|
|
1586
|
+
resolveExtensionLocalizations,
|
|
1587
|
+
generateLocalizableStrings,
|
|
1331
1588
|
generateWidgetBundleSwift,
|
|
1332
1589
|
}
|