peachsim 0.1.674 → 0.1.675

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 (81) hide show
  1. package/dist-lib/agent-daemon-client.cjs +1 -1
  2. package/dist-lib/agent-events.cjs +1 -1
  3. package/dist-lib/agent-identity.cjs +1 -1
  4. package/dist-lib/agent-sessions.cjs +1 -1
  5. package/dist-lib/attached-projects.cjs +1 -1
  6. package/dist-lib/auth/shared-session.cjs +1 -1
  7. package/dist-lib/backend-origin.cjs +1 -1
  8. package/dist-lib/beta.cjs +1 -1
  9. package/dist-lib/beta.mjs +1 -1
  10. package/dist-lib/bridge-constants.cjs +1 -1
  11. package/dist-lib/bridge-contract-input.cjs +1 -1
  12. package/dist-lib/bridge-contract-input.mjs +1 -1
  13. package/dist-lib/bridge-contract.cjs +1 -1
  14. package/dist-lib/bridge-contract.mjs +1 -1
  15. package/dist-lib/capture-contract.cjs +1 -1
  16. package/dist-lib/capture-contract.mjs +1 -1
  17. package/dist-lib/cli-constants.cjs +1 -1
  18. package/dist-lib/cloud-contract.cjs +1 -1
  19. package/dist-lib/cloud-contract.mjs +1 -1
  20. package/dist-lib/cloud-test-args.cjs +1 -1
  21. package/dist-lib/cloud-test-args.mjs +1 -1
  22. package/dist-lib/cloud.cjs +1 -1
  23. package/dist-lib/cloud.mjs +1 -1
  24. package/dist-lib/config.cjs +1 -1
  25. package/dist-lib/detox/index.cjs +1 -1
  26. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  27. package/dist-lib/home-paths.cjs +1 -1
  28. package/dist-lib/host/bridge-host.cjs +1 -1
  29. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  30. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  31. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  32. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  33. package/dist-lib/host/websocket-proxy.cjs +1 -1
  34. package/dist-lib/index.cjs +1 -1
  35. package/dist-lib/jump-to-source-babel.cjs +1 -1
  36. package/dist-lib/jump-to-source-native.cjs +1 -1
  37. package/dist-lib/kotlin-host.cjs +78 -2
  38. package/dist-lib/menu.cjs +1 -1
  39. package/dist-lib/menu.mjs +1 -1
  40. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  41. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  42. package/dist-lib/metro-production-bundle.cjs +1 -1
  43. package/dist-lib/metro-production-bundle.mjs +1 -1
  44. package/dist-lib/metro.cjs +1 -1
  45. package/dist-lib/native-app-identity.cjs +1 -1
  46. package/dist-lib/native-app-identity.mjs +1 -1
  47. package/dist-lib/native-firebase-metadata.cjs +1 -1
  48. package/dist-lib/native-firebase-metadata.mjs +1 -1
  49. package/dist-lib/plist.cjs +1 -1
  50. package/dist-lib/plist.mjs +1 -1
  51. package/dist-lib/profiles.cjs +1 -1
  52. package/dist-lib/public-brand.cjs +1 -1
  53. package/dist-lib/react-native-host-modules.cjs +1 -1
  54. package/dist-lib/react-native-host-modules.mjs +1 -1
  55. package/dist-lib/render-mode.cjs +1 -1
  56. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  57. package/dist-lib/sdk.cjs +1 -1
  58. package/dist-lib/sdk.mjs +1 -1
  59. package/dist-lib/skills.cjs +1 -1
  60. package/dist-lib/swift-compile.cjs +1 -1
  61. package/dist-lib/swift-compile.mjs +1 -1
  62. package/dist-lib/swift-package.cjs +1 -1
  63. package/dist-lib/swift-package.mjs +1 -1
  64. package/dist-lib/swift-submit.cjs +1 -1
  65. package/dist-lib/swift-submit.mjs +1 -1
  66. package/dist-lib/swift.cjs +1 -1
  67. package/dist-lib/vite-swift.cjs +1 -1
  68. package/dist-lib/vite.cjs +1 -1
  69. package/kotlin/AppSources/android/content/Context.kt +4 -0
  70. package/kotlin/AppSources/android/content/res/Configuration.kt +4 -1
  71. package/kotlin/AppSources/android/content/res/Resources.kt +45 -1
  72. package/kotlin/AppSources/androidx/compose/ui/platform/AndroidCompositionLocals.kt +19 -0
  73. package/kotlin/AppSources/androidx/compose/ui/res/StringResources.kt +53 -0
  74. package/kotlin/AppSources/dev/peach/app/JavaFormat.kt +264 -0
  75. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +23 -7
  76. package/kotlin/fixtures/kotlin-app-variants/Strings.kt +49 -0
  77. package/package.json +1 -1
  78. package/src/android-strings.ts +163 -0
  79. package/src/android-xml.ts +3 -3
  80. package/src/kotlin-app.ts +4 -0
  81. package/src/kotlin-host.ts +98 -0
@@ -1,12 +1,15 @@
1
1
  package android.content.res
2
2
 
3
3
  // the device configuration an app reads through its resources: a phone's
4
- // normal ui mode type, night or not as the device's dark theme is set
4
+ // normal ui mode type, night or not as the device's dark theme is set, and
5
+ // the device locale's language tag
5
6
  class Configuration {
6
7
  var uiMode: Int = UI_MODE_TYPE_NORMAL or UI_MODE_NIGHT_NO
8
+ internal var localeTag: String = ""
7
9
 
8
10
  fun setTo(other: Configuration) {
9
11
  uiMode = other.uiMode
12
+ localeTag = other.localeTag
10
13
  }
11
14
 
12
15
  companion object {
@@ -1,6 +1,50 @@
1
1
  package android.content.res
2
2
 
3
- class Resources internal constructor(val configuration: Configuration)
3
+ import dev.peach.app.javaFormat
4
+ import dev.peach.app.peachAppArrays
5
+ import dev.peach.app.peachAppPlurals
6
+ import dev.peach.app.peachAppStrings
7
+
8
+ class Resources internal constructor(val configuration: Configuration) {
9
+ class NotFoundException(name: String) : RuntimeException(name)
10
+
11
+ fun getString(id: Int): String =
12
+ resolve(peachAppStrings[id]) ?: throw NotFoundException("String resource ID #0x${id.toString(16)}")
13
+
14
+ fun getString(id: Int, vararg formatArgs: Any?): String =
15
+ javaFormat(configuration.localeTag, getString(id), formatArgs)
16
+
17
+ // ResourcesImpl.getQuantityText: the item the locale's plural rule
18
+ // selects for the quantity, then the other item
19
+ fun getQuantityString(id: Int, quantity: Int): String {
20
+ val items = resolve(peachAppPlurals[id])
21
+ ?: throw NotFoundException("Plural resource ID #0x${id.toString(16)}")
22
+ val rule = pluralRule(configuration.localeTag, quantity)
23
+ return items[rule] ?: items["other"]
24
+ ?: throw NotFoundException("Plural resource ID #0x${id.toString(16)} quantity=$quantity item=$rule")
25
+ }
26
+
27
+ fun getQuantityString(id: Int, quantity: Int, vararg formatArgs: Any?): String =
28
+ javaFormat(configuration.localeTag, getQuantityString(id, quantity), formatArgs)
29
+
30
+ fun getStringArray(id: Int): Array<String> =
31
+ resolve(peachAppArrays[id])?.toTypedArray()
32
+ ?: throw NotFoundException("String array resource ID #0x${id.toString(16)}")
33
+
34
+ // the value the configuration's locale selects: its language and region,
35
+ // then its language, then the default values, as the resource table
36
+ // matches a locale qualifier. a plurals or array resource resolves whole
37
+ private fun <Value> resolve(values: Map<String, Value>?): Value? {
38
+ if (values == null) return null
39
+ val tag = configuration.localeTag
40
+ return values[tag] ?: values[tag.substringBefore('-')] ?: values[""]
41
+ }
42
+ }
43
+
44
+ // ICU's PluralRules.forLocale(locale).select(quantity); Intl reads the same
45
+ // CLDR plural rules
46
+ private fun pluralRule(locale: String, quantity: Int): String =
47
+ js("new Intl.PluralRules(locale || undefined).select(quantity)")
4
48
 
5
49
  // the app process's resources, set once the device configuration is known at
6
50
  // cold start; every context in the process reads the same configuration, which
@@ -0,0 +1,19 @@
1
+ package androidx.compose.ui.platform
2
+
3
+ import android.content.Context
4
+ import android.content.res.Configuration
5
+ import androidx.compose.runtime.compositionLocalOf
6
+ import androidx.compose.runtime.neverEqualPolicy
7
+ import androidx.compose.runtime.staticCompositionLocalOf
8
+
9
+ // AndroidCompositionLocals.android.kt: the configuration and context a
10
+ // ComposeView's content reads, provided where its composition starts
11
+ val LocalConfiguration = compositionLocalOf<Configuration>(neverEqualPolicy()) {
12
+ noLocalProvidedFor("LocalConfiguration")
13
+ }
14
+
15
+ val LocalContext = staticCompositionLocalOf<Context> { noLocalProvidedFor("LocalContext") }
16
+
17
+ private fun noLocalProvidedFor(name: String): Nothing {
18
+ error("CompositionLocal $name not present")
19
+ }
@@ -0,0 +1,53 @@
1
+ package androidx.compose.ui.res
2
+
3
+ import android.content.res.Resources
4
+ import androidx.compose.runtime.Composable
5
+ import androidx.compose.runtime.ReadOnlyComposable
6
+ import androidx.compose.ui.platform.LocalConfiguration
7
+ import androidx.compose.ui.platform.LocalContext
8
+
9
+ // StringResources.android.kt and Resources.android.kt: a string, plural or
10
+ // string array resource read through the composition's context, recomposed
11
+ // when its configuration changes
12
+
13
+ @Composable
14
+ @ReadOnlyComposable
15
+ fun stringResource(id: Int): String {
16
+ val resources = resources()
17
+ return resources.getString(id)
18
+ }
19
+
20
+ @Composable
21
+ @ReadOnlyComposable
22
+ fun stringResource(id: Int, vararg formatArgs: Any): String {
23
+ val resources = resources()
24
+ return resources.getString(id, *formatArgs)
25
+ }
26
+
27
+ @Composable
28
+ @ReadOnlyComposable
29
+ fun pluralStringResource(id: Int, count: Int): String {
30
+ val resources = resources()
31
+ return resources.getQuantityString(id, count)
32
+ }
33
+
34
+ @Composable
35
+ @ReadOnlyComposable
36
+ fun pluralStringResource(id: Int, count: Int, vararg formatArgs: Any): String {
37
+ val resources = resources()
38
+ return resources.getQuantityString(id, count, *formatArgs)
39
+ }
40
+
41
+ @Composable
42
+ @ReadOnlyComposable
43
+ fun stringArrayResource(id: Int): Array<String> {
44
+ val resources = resources()
45
+ return resources.getStringArray(id)
46
+ }
47
+
48
+ @Composable
49
+ @ReadOnlyComposable
50
+ internal fun resources(): Resources {
51
+ LocalConfiguration.current
52
+ return LocalContext.current.resources
53
+ }
@@ -0,0 +1,264 @@
1
+ package dev.peach.app
2
+
3
+ // java.util.Formatter as String.format(locale, format, args) runs it on the
4
+ // device, which Resources.getString(id, args) calls: the general, character,
5
+ // integral and decimal float conversions with their flags, width, precision
6
+ // and argument indexes, digits and separators in the locale's symbols. a
7
+ // conversion outside those (scientific, hex float, date) is rejected by name.
8
+
9
+ private val SPECIFIER = Regex("%(\\d+\\$)?([-#+ 0,(<]*)(\\d+)?(\\.\\d+)?([tT])?([a-zA-Z%])")
10
+
11
+ private class Symbols(val zero: Char, val decimal: String, val group: String)
12
+
13
+ // one part of the locale's number format, as ICU's decimal format symbols
14
+ // give it (Intl reads the same CLDR data)
15
+ private fun numberPart(locale: String, value: Double, type: String): String =
16
+ js("(new Intl.NumberFormat(locale || undefined, { useGrouping: true }).formatToParts(value).find((part) => part.type === type) || { value: '' }).value")
17
+
18
+ private fun symbols(locale: String) = Symbols(
19
+ zero = numberPart(locale, 0.0, "integer").single(),
20
+ decimal = numberPart(locale, 1.5, "decimal"),
21
+ group = numberPart(locale, 1234567.0, "group"),
22
+ )
23
+
24
+ private class Spec(val flags: String, val width: Int, val precision: Int, val text: String) {
25
+ fun has(flag: Char) = flag in flags
26
+ }
27
+
28
+ internal fun javaFormat(locale: String, format: String, args: Array<out Any?>): String {
29
+ val out = StringBuilder()
30
+ var ordinary = 0
31
+ var last = -1
32
+ var index = 0
33
+ while (index < format.length) {
34
+ val percent = format.indexOf('%', index)
35
+ if (percent == -1) {
36
+ out.append(format, index, format.length)
37
+ break
38
+ }
39
+ out.append(format, index, percent)
40
+ val match = SPECIFIER.matchAt(format, percent)
41
+ ?: throw IllegalArgumentException(
42
+ "Conversion = '${format.getOrNull(percent + 1) ?: '%'}'",
43
+ )
44
+ index = match.range.last + 1
45
+ val (position, flags, width, precision, time, conversion) = match.destructured
46
+ if (time.isNotEmpty())
47
+ throw UnsupportedOperationException("peach kotlin: date conversion ${match.value} is not supported")
48
+ val spec = Spec(
49
+ flags,
50
+ width.toIntOrNull() ?: -1,
51
+ precision.drop(1).toIntOrNull() ?: -1,
52
+ match.value,
53
+ )
54
+ val c = conversion.single()
55
+ if (c == '%') {
56
+ out.append(justify(spec, "%"))
57
+ continue
58
+ }
59
+ if (c == 'n') {
60
+ out.append('\n')
61
+ continue
62
+ }
63
+ val argument = when {
64
+ spec.has('<') -> last
65
+ position.isNotEmpty() -> position.dropLast(1).toInt() - 1
66
+ else -> ordinary++
67
+ }
68
+ if (argument !in args.indices) throw IllegalArgumentException("Format specifier '${match.value}'")
69
+ last = argument
70
+ out.append(convert(c, args[argument], spec, locale))
71
+ }
72
+ return out.toString()
73
+ }
74
+
75
+ private fun justify(spec: Spec, value: String): String {
76
+ if (spec.width <= value.length) return value
77
+ val pad = " ".repeat(spec.width - value.length)
78
+ return if (spec.has('-')) value + pad else pad + value
79
+ }
80
+
81
+ private fun typeName(value: Any?): String = when (value) {
82
+ is String -> "java.lang.String"
83
+ is Int -> "java.lang.Integer"
84
+ is Long -> "java.lang.Long"
85
+ is Short -> "java.lang.Short"
86
+ is Byte -> "java.lang.Byte"
87
+ is Double -> "java.lang.Double"
88
+ is Float -> "java.lang.Float"
89
+ is Boolean -> "java.lang.Boolean"
90
+ is Char -> "java.lang.Character"
91
+ else -> value?.let { it::class.simpleName } ?: "null"
92
+ }
93
+
94
+ private fun mismatch(c: Char, value: Any?): Nothing =
95
+ throw IllegalArgumentException("$c != ${typeName(value)}")
96
+
97
+ private fun convert(c: Char, value: Any?, spec: Spec, locale: String): String = when (c) {
98
+ 'b', 'B' -> {
99
+ val text = when (value) {
100
+ null -> "false"
101
+ is Boolean -> value.toString()
102
+ else -> "true"
103
+ }
104
+ justify(spec, precise(spec, if (c == 'B') text.uppercase() else text))
105
+ }
106
+ 's', 'S' -> {
107
+ val text = precise(spec, value.toString())
108
+ justify(spec, if (c == 'S') text.uppercase() else text)
109
+ }
110
+ 'c', 'C' -> {
111
+ val text = when (value) {
112
+ null -> "null"
113
+ is Char -> value.toString()
114
+ is Int -> codePoint(value)
115
+ is Byte -> codePoint(value.toInt())
116
+ is Short -> codePoint(value.toInt())
117
+ else -> mismatch(c, value)
118
+ }
119
+ justify(spec, if (c == 'C') text.uppercase() else text)
120
+ }
121
+ 'd' -> when (value) {
122
+ null -> justify(spec, "null")
123
+ is Int -> decimal(spec, value.toString(), symbols(locale))
124
+ is Long -> decimal(spec, value.toString(), symbols(locale))
125
+ is Short -> decimal(spec, value.toString(), symbols(locale))
126
+ is Byte -> decimal(spec, value.toString(), symbols(locale))
127
+ else -> mismatch(c, value)
128
+ }
129
+ 'x', 'X', 'o' -> {
130
+ val radix = if (c == 'o') 8 else 16
131
+ val digits = when (value) {
132
+ null -> null
133
+ is Int -> value.toUInt().toString(radix)
134
+ is Long -> value.toULong().toString(radix)
135
+ is Short -> (value.toInt() and 0xffff).toString(radix)
136
+ is Byte -> (value.toInt() and 0xff).toString(radix)
137
+ else -> mismatch(c, value)
138
+ }
139
+ if (digits == null) justify(spec, "null")
140
+ else {
141
+ val prefix = if (spec.has('#')) (if (radix == 16) "0x" else "0") else ""
142
+ var body = digits
143
+ if (spec.has('0')) body = body.padStart(spec.width - prefix.length, '0')
144
+ val text = prefix + body
145
+ justify(spec, if (c == 'X') text.uppercase() else text)
146
+ }
147
+ }
148
+ 'f' -> when (value) {
149
+ null -> justify(spec, "null")
150
+ is Double -> fixed(spec, value, symbols(locale))
151
+ is Float -> fixed(spec, value.toDouble(), symbols(locale))
152
+ else -> mismatch(c, value)
153
+ }
154
+ else -> throw UnsupportedOperationException("peach kotlin: conversion ${spec.text} is not supported")
155
+ }
156
+
157
+ private fun codePoint(value: Int): String =
158
+ if (value < 0x10000) value.toChar().toString()
159
+ else {
160
+ val offset = value - 0x10000
161
+ charArrayOf((0xd800 + (offset shr 10)).toChar(), (0xdc00 + (offset and 0x3ff)).toChar()).concatToString()
162
+ }
163
+
164
+ private fun precise(spec: Spec, text: String) =
165
+ if (spec.precision >= 0 && spec.precision < text.length) text.take(spec.precision) else text
166
+
167
+ // Formatter.print(long): the sign, then the localized magnitude zero padded
168
+ // to the width, then the closing parenthesis, then the justification
169
+ private fun decimal(spec: Spec, value: String, symbols: Symbols): String {
170
+ val negative = value.startsWith('-')
171
+ return signed(spec, negative, value.removePrefix("-"), symbols)
172
+ }
173
+
174
+ private fun signed(spec: Spec, negative: Boolean, magnitude: String, symbols: Symbols): String {
175
+ val out = StringBuilder()
176
+ if (negative) out.append(if (spec.has('(')) '(' else '-')
177
+ else if (spec.has('+')) out.append('+')
178
+ else if (spec.has(' ')) out.append(' ')
179
+ val width = if (spec.width != -1 && negative && spec.has('(')) spec.width - 1 else spec.width
180
+ localized(out, magnitude, spec, width, symbols)
181
+ if (negative && spec.has('(')) out.append(')')
182
+ return justify(spec, out.toString())
183
+ }
184
+
185
+ // Formatter.localizedMagnitude: each digit in the locale's digits, grouped
186
+ // by three with the ',' flag, the decimal separator in place of the point,
187
+ // and zero padding inserted after the sign
188
+ private fun localized(out: StringBuilder, value: String, spec: Spec, width: Int, symbols: Symbols) {
189
+ val begin = out.length
190
+ val dot = value.indexOf('.').let { if (it == -1) value.length else it }
191
+ var group = spec.has(',')
192
+ for (index in value.indices) {
193
+ if (index == dot) {
194
+ out.append(symbols.decimal)
195
+ group = false
196
+ continue
197
+ }
198
+ out.append(symbols.zero + (value[index] - '0'))
199
+ if (group && index != dot - 1 && (dot - index) % 3 == 1) out.append(symbols.group)
200
+ }
201
+ if (width != -1 && spec.has('0')) while (out.length < width) out.insert(begin, symbols.zero)
202
+ }
203
+
204
+ // Formatter.print(double) for %f: the shortest decimal digits of the value,
205
+ // rounded half up to the precision, as FormattedFloatingDecimal does
206
+ private fun fixed(spec: Spec, value: Double, symbols: Symbols): String {
207
+ if (value.isNaN()) return justify(spec, "NaN")
208
+ val negative = value < 0.0 || (value == 0.0 && 1.0 / value < 0.0)
209
+ if (value.isInfinite()) {
210
+ val text = if (negative) (if (spec.has('(')) "(Infinity)" else "-Infinity")
211
+ else if (spec.has('+')) "+Infinity" else if (spec.has(' ')) " Infinity" else "Infinity"
212
+ return justify(spec, text)
213
+ }
214
+ val precision = if (spec.precision == -1) 6 else spec.precision
215
+ var magnitude = roundHalfUp(shortestDigits(kotlin.math.abs(value)), precision)
216
+ if (precision == 0 && spec.has('#')) magnitude += "."
217
+ return signed(spec, negative, magnitude, symbols)
218
+ }
219
+
220
+ // the shortest round trip decimal of a finite non negative double as plain
221
+ // digits and a point, the digits Double.toString prints
222
+ private fun shortestDigits(value: Double): String {
223
+ val text = value.toString()
224
+ val exponentAt = text.indexOfFirst { it == 'e' || it == 'E' }
225
+ if (exponentAt == -1) return text
226
+ val mantissa = text.substring(0, exponentAt)
227
+ val exponent = text.substring(exponentAt + 1).toInt()
228
+ val point = mantissa.indexOf('.').let { if (it == -1) mantissa.length else it }
229
+ val digits = mantissa.replace(".", "")
230
+ val shifted = point + exponent
231
+ return when {
232
+ shifted <= 0 -> "0." + "0".repeat(-shifted) + digits
233
+ shifted >= digits.length -> digits + "0".repeat(shifted - digits.length)
234
+ else -> digits.substring(0, shifted) + "." + digits.substring(shifted)
235
+ }
236
+ }
237
+
238
+ private fun roundHalfUp(decimal: String, precision: Int): String {
239
+ val point = decimal.indexOf('.')
240
+ val whole = if (point == -1) decimal else decimal.substring(0, point)
241
+ val fraction = if (point == -1) "" else decimal.substring(point + 1)
242
+ if (fraction.length <= precision) {
243
+ val padded = fraction.padEnd(precision, '0')
244
+ return if (precision == 0) whole else "$whole.$padded"
245
+ }
246
+ val kept = (whole + fraction.substring(0, precision)).toCharArray()
247
+ if (fraction[precision] >= '5') {
248
+ var carry = true
249
+ var index = kept.size - 1
250
+ while (carry && index >= 0) {
251
+ if (kept[index] == '9') kept[index] = '0'
252
+ else {
253
+ kept[index] = kept[index] + 1
254
+ carry = false
255
+ }
256
+ index--
257
+ }
258
+ val digits = (if (carry) "1" else "") + kept.concatToString()
259
+ val wholeLength = digits.length - precision
260
+ return if (precision == 0) digits else digits.substring(0, wholeLength) + "." + digits.substring(wholeLength)
261
+ }
262
+ val digits = kept.concatToString()
263
+ return if (precision == 0) digits else digits.substring(0, whole.length) + "." + digits.substring(whole.length)
264
+ }
@@ -20,6 +20,8 @@ import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
20
20
  import androidx.compose.runtime.Composable
21
21
  import androidx.compose.runtime.CompositionLocalProvider
22
22
  import androidx.compose.ui.platform.ComposeView
23
+ import androidx.compose.ui.platform.LocalConfiguration
24
+ import androidx.compose.ui.platform.LocalContext
23
25
  import androidx.compose.ui.platform.PlatformArchitectureComponentsOwner
24
26
  import androidx.compose.ui.platform.PlatformInsets
25
27
  import androidx.compose.ui.platform.PlatformWindowInsets
@@ -27,6 +29,7 @@ import androidx.compose.runtime.getValue
27
29
  import androidx.compose.runtime.key
28
30
  import androidx.compose.runtime.mutableIntStateOf
29
31
  import androidx.compose.runtime.mutableStateOf
32
+ import androidx.compose.runtime.neverEqualPolicy
30
33
  import androidx.compose.runtime.setValue
31
34
  import androidx.compose.ui.text.font.FontFamily
32
35
  import androidx.lifecycle.Lifecycle
@@ -130,13 +133,17 @@ private class AppProcess(
130
133
  var reportedNight = false
131
134
  // the software keyboard's height above the screen's bottom edge, in pixels
132
135
  var ime by mutableIntStateOf(0)
136
+ // the configuration the content view composes with: a copy the view's
137
+ // configuration change replaces, as AndroidComposeView's observer does
138
+ val configuration = mutableStateOf(Configuration(), neverEqualPolicy())
133
139
  }
134
140
 
135
141
  private var process: AppProcess? = null
136
142
 
137
- private fun nightConfiguration(night: Boolean) = Configuration().apply {
143
+ private fun deviceConfiguration(night: Boolean, locale: String) = Configuration().apply {
138
144
  uiMode = Configuration.UI_MODE_TYPE_NORMAL or
139
145
  if (night) Configuration.UI_MODE_NIGHT_YES else Configuration.UI_MODE_NIGHT_NO
146
+ localeTag = locale
140
147
  }
141
148
 
142
149
  // a cold start in ActivityThread's order: the Application and its onCreate,
@@ -153,7 +160,7 @@ internal fun peachStartApp(
153
160
  try {
154
161
  require(backend == "graphite" || backend == "raster")
155
162
  preparePeachDevice(locale)
156
- processResources = Resources(nightConfiguration(night))
163
+ processResources = Resources(deviceConfiguration(night, locale))
157
164
  val application = spec.createApplication()
158
165
  application.onCreate()
159
166
  val app = AppProcess(
@@ -235,12 +242,20 @@ private fun launchActivity(app: AppProcess, lastNonConfigurationInstance: Any?):
235
242
  insets = DeviceInsets(window, app.insets) { app.ime },
236
243
  clockRunning = { componentActivity == null || componentActivity.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) },
237
244
  )
245
+ app.configuration.value = Configuration().apply { setTo(activity.resources.configuration) }
238
246
  mount.setContent {
239
247
  val content = @Composable { held?.let { view -> key(place) { view.Content() } }; Unit }
240
- // the content view's tree resolves the activity as its back dispatcher owner
241
- if (componentActivity != null)
242
- CompositionLocalProvider(LocalOnBackPressedDispatcherOwner provides componentActivity, content = content)
243
- else content()
248
+ // the content view's context is the activity, its configuration the
249
+ // copy above, and its tree resolves the activity as its back
250
+ // dispatcher owner
251
+ CompositionLocalProvider(
252
+ LocalContext provides activity,
253
+ LocalConfiguration provides app.configuration.value,
254
+ ) {
255
+ if (componentActivity != null)
256
+ CompositionLocalProvider(LocalOnBackPressedDispatcherOwner provides componentActivity, content = content)
257
+ else content()
258
+ }
244
259
  }
245
260
  if (app.mountId == 0) app.mountId = addMount(mount)
246
261
  else {
@@ -319,6 +334,7 @@ private fun reportConfiguration(app: AppProcess): Boolean {
319
334
  }
320
335
  app.reportedNight = isNight(configuration)
321
336
  app.activity.performConfigurationChanged(Configuration().apply { setTo(configuration) })
337
+ app.configuration.value = Configuration().apply { setTo(configuration) }
322
338
  app.mount.night = app.reportedNight
323
339
  return false
324
340
  }
@@ -341,7 +357,7 @@ internal fun peachAppNight(night: Boolean) {
341
357
  val app = runningApp()
342
358
  val configuration = processResources.configuration
343
359
  if (isNight(configuration) == night) return
344
- configuration.setTo(nightConfiguration(night))
360
+ configuration.setTo(deviceConfiguration(night, app.locale))
345
361
  app.application.onConfigurationChanged(Configuration().apply { setTo(configuration) })
346
362
  if (!app.stopped) reportConfiguration(app)
347
363
  }
@@ -0,0 +1,49 @@
1
+ package dev.contrast.kotlinapp
2
+
3
+ import androidx.compose.foundation.background
4
+ import androidx.compose.foundation.layout.Arrangement
5
+ import androidx.compose.foundation.layout.Column
6
+ import androidx.compose.foundation.layout.fillMaxSize
7
+ import androidx.compose.foundation.layout.padding
8
+ import androidx.compose.foundation.layout.safeDrawingPadding
9
+ import androidx.compose.material3.Text
10
+ import androidx.compose.runtime.Composable
11
+ import androidx.compose.ui.Modifier
12
+ import androidx.compose.ui.graphics.Color
13
+ import androidx.compose.ui.platform.LocalContext
14
+ import androidx.compose.ui.res.pluralStringResource
15
+ import androidx.compose.ui.res.stringArrayResource
16
+ import androidx.compose.ui.res.stringResource
17
+ import androidx.compose.ui.unit.dp
18
+
19
+ // string, plural and array resources read through the compose helpers and
20
+ // the context, each line showing what aapt2 compiled, the plural rule chose
21
+ // and String.format made of one rule
22
+ @Composable
23
+ fun StringsScreen() {
24
+ val context = LocalContext.current
25
+ Column(
26
+ Modifier.fillMaxSize().background(Color(0xFFE8EEF8)).safeDrawingPadding().padding(16.dp),
27
+ verticalArrangement = Arrangement.spacedBy(8.dp),
28
+ ) {
29
+ Text(stringResource(R.string.title))
30
+ Text(stringResource(R.string.greeting, "Ada", 3))
31
+ Text("[" + stringResource(R.string.spaced) + "]")
32
+ Text("[" + stringResource(R.string.quoted) + "]")
33
+ Text(context.getString(R.string.escaped))
34
+ Text(stringResource(R.string.styled))
35
+ Text(stringResource(R.string.entity))
36
+ Text(stringResource(R.string.percent))
37
+ Text(stringResource(R.string.numbers, 1234567, 1.005, 42, 255, "ab"))
38
+ Text(context.resources.getString(R.string.numbers, -7, 2.5, -3, -1, "x"))
39
+ Text(stringResource(R.string.signs, 5, -5, 1234.5678, 0.125f))
40
+ Text(
41
+ pluralStringResource(R.plurals.messages, 0, 0) + " / " +
42
+ pluralStringResource(R.plurals.messages, 1, 1) + " / " +
43
+ pluralStringResource(R.plurals.messages, 2, 2),
44
+ )
45
+ Text(pluralStringResource(R.plurals.only_other, 1) + " / " + context.resources.getQuantityString(R.plurals.messages, 1, 1))
46
+ Text(stringArrayResource(R.array.planets).joinToString("|"))
47
+ Text(context.resources.getStringArray(R.array.seasons).joinToString("|"))
48
+ }
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peachsim",
3
- "version": "0.1.674",
3
+ "version": "0.1.675",
4
4
  "description": "Vite and Metro plugins, testing drivers, and SDK exports for peach.",
5
5
  "author": "Tamagui LLC",
6
6
  "license": "SEE LICENSE IN LICENSE",