Skip to content

Commit 0a57383

Browse files
authored
Announce scrolling state for VoiceOver (#1644)
Announce scrollable list state after scroll event when VoiceOver is on Fixes https://youtrack.jetbrains.com/issue/CMP-702/iOS-a11y-refine-scrolling-behavior ## Testing With VoiceOver enabled, the following list statuses should be announced after three-finger scrolling events - First page (when at the top of the scrollable list) - Next page (when scrolling forwards) - Previous page (when scrolling backwards) - Last page (when reaching the end of the list) ## Release Notes ### Features - iOS Support state announcements for scrollable lists in VoiceOver
1 parent cd3ea9c commit 0a57383

File tree

48 files changed

+1689
-136
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+1689
-136
lines changed

compose/ui/ui/build.gradle

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ if(AndroidXComposePlugin.isMultiplatformEnabled(project)) {
354354
}
355355
}
356356

357-
// This task updates the translations of the localizable strings in this module.
357+
// This task updates the translations of the localizable strings for the desktopMain target.
358358
// It obtains them from Android's base repository.
359359
tasks.register("updateTranslations", UpdateTranslationsTask.class) {
360360
group = "localization"
@@ -386,6 +386,93 @@ tasks.register("updateTranslations", UpdateTranslationsTask.class) {
386386
]
387387
}
388388

389+
// This task updates the translations of the localizable strings for the uikitMain target.
390+
// It obtains them from compose multiplatform repository.
391+
// See also `scripts/convertCrowdinToStringsXml.sh`.
392+
tasks.register("updateTranslationsIos", UpdateTranslationsTask.class) {
393+
group = "localization"
394+
gitRepo = "https://github.com/JetBrains/compose-multiplatform-core"
395+
repoResDirectories = ["compose/ui/ui/src/uikitMain/res"]
396+
targetDirectory = project.file("src/uikitMain/kotlin/androidx/compose/ui/platform/l10n")
397+
targetPackageName = "androidx.compose.ui.platform.l10n"
398+
kotlinStringsPackageName = "androidx.compose.ui.platform"
399+
stringByResourceName = [
400+
"first_page": "FirstPage",
401+
"last_page": "LastPage",
402+
"next_page": "NextPage",
403+
"previous_page": "PreviousPage"
404+
]
405+
406+
// Currently, strings are used in accessibility features, which limits the language list to the
407+
// languages supported in accessibility on iOS: https://support.apple.com/en-us/111748.
408+
locales = [
409+
"ar", // Arabic
410+
"eu", // Basque
411+
"bn", // Bengali (India)
412+
// "bh_IN", // Bhojpuri (India)
413+
"bg", // Bulgarian
414+
"zh_HK", // Cantonese (Hong Kong)
415+
"ca", // Catalan
416+
"hr", // Croatian
417+
"cs", // Czech
418+
"da", // Danish
419+
"nl_BE", // Dutch (Belgium)
420+
"nl", // Dutch (Netherlands)
421+
"en_AU", // English (Australia)
422+
"en_IN", // English (India)
423+
"en_IE", // English (Ireland)
424+
"en_GB", // English (Scotland)
425+
"en_ZA", // English (South Africa)
426+
"en_GB", // English (UK)
427+
"en", // English (US)
428+
"fa", // Farsi
429+
"fi", // Finnish
430+
"fr_BE", // French (Belgium)
431+
"fr_CA", // French (Canada)
432+
"fr", // French (France)
433+
"gl", // Galician
434+
"de", // German
435+
"el", // Greek
436+
"iw", // Hebrew
437+
"hi", // Hindi
438+
"hu", // Hungarian
439+
"in", // Indonesian
440+
"it", // Italian
441+
"ja", // Japanese
442+
"kn", // Kannada
443+
"ko", // Korean
444+
"ms", // Malay
445+
"zh_CN", // Chinese (China mainland)
446+
// "zh_CN", // Chinese (Liaoning, China mainland)
447+
// "zh_CN", // Chinese (Shaanxi, China mainland)
448+
// "zh_CN", // Chinese (Sichuan, China mainland)
449+
"zh_TW", // Chinese (Taiwan)
450+
"mr", // Marathi
451+
"nb", // Norwegian
452+
"pl", // Polish
453+
"pt_BR", // Portuguese (Brazil)
454+
"pt", // Portuguese (Portugal)
455+
"ro", // Romanian
456+
"ru", // Russian
457+
// "zh_CN", // Shanghainese (China mainland)
458+
"sk", // Slovak
459+
"sl", // Slovenian
460+
"es_AR", // Spanish (Argentina)
461+
"es_CL", // Spanish (Chile)
462+
"es_CO", // Spanish (Colombia)
463+
"es_MX", // Spanish (Mexico)
464+
"es", // Spanish (Spain)
465+
"sv", // Swedish
466+
"th", // Thai
467+
"tr", // Turkish
468+
"ta", // Tamil
469+
"te", // Telugu
470+
"uk", // Ukrainian
471+
"ca_ES", // Valencian
472+
"vi", // Vietnamese
473+
]
474+
}
475+
389476
androidx {
390477
name = "Compose UI primitives"
391478
type = LibraryType.PUBLISHED_LIBRARY
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2024 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package androidx.compose.ui.platform
18+
19+
import androidx.compose.ui.text.intl.Locale
20+
21+
internal class TranslationsCache<Key>(private val getTranslations: (String) -> Map<Key, String>?) {
22+
internal fun getString(key: Key): String {
23+
val locale = Locale.current
24+
val tag = localeTag(language = locale.language, region = locale.region)
25+
val translation = translationByLocaleTag.getOrPut(tag) {
26+
findTranslation(locale)
27+
}
28+
return translation.get(key = key) ?: error("Missing translation for $key")
29+
}
30+
31+
/**
32+
* Translations we've already loaded, mapped by the locale tag (see [localeTag]).
33+
*/
34+
private val translationByLocaleTag = mutableMapOf<String, Map<Key, String>>()
35+
36+
/**
37+
* Returns the tag for the given locale.
38+
*
39+
* Note that this is our internal format; this isn't the same as [Locale.toLanguageTag].
40+
*/
41+
private fun localeTag(language: String, region: String) = when {
42+
language == "" -> ""
43+
region == "" -> language
44+
else -> "${language}_$region"
45+
}
46+
47+
/**
48+
* Returns a sequence of locale tags to use as keys to look up the translation for the given locale.
49+
*
50+
* Note that we don't need to check children (e.g. use `fr_FR` if `fr` is missing) because the
51+
* translations should never have a missing parent.
52+
*/
53+
private fun localeTagChain(locale: Locale) = sequence {
54+
if (locale.region != "") {
55+
yield(localeTag(language = locale.language, region = locale.region))
56+
}
57+
if (locale.language != "") {
58+
yield(localeTag(language = locale.language, region = ""))
59+
}
60+
yield(localeTag("", ""))
61+
}
62+
63+
/**
64+
* Finds a translation map for the given locale.
65+
*/
66+
private fun findTranslation(locale: Locale): Map<Key, String> {
67+
// We don't need to merge translations because each one should contain all the strings.
68+
return localeTagChain(locale).firstNotNullOf { getTranslations(it) }
69+
}
70+
}

compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/platform/Strings.kt

Lines changed: 5 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@
1616

1717
package androidx.compose.ui.platform
1818

19-
import androidx.compose.runtime.Composable
2019
import androidx.compose.runtime.Immutable
21-
import androidx.compose.runtime.ReadOnlyComposable
22-
import androidx.compose.ui.text.intl.Locale
2320
import androidx.compose.ui.platform.l10n.en
2421
import androidx.compose.ui.platform.l10n.translationFor
2522

@@ -32,68 +29,16 @@ internal value class Strings private constructor(@Suppress("unused") private val
3229
val Paste = Strings(2)
3330
val SelectAll = Strings(3)
3431
// When adding values here, make sure to also add them in ui/build.gradle,
35-
// updateTranslations task (stringByResourceName parameter), and re-run the task
32+
// updateTranslationsDesktop task (stringByResourceName parameter), and re-run the task
3633
}
3734
}
3835

39-
@Composable
40-
@ReadOnlyComposable
41-
internal fun getString(string: Strings): String {
42-
val locale = Locale.current
43-
val tag = localeTag(language = locale.language, region = locale.region)
44-
val translation = translationByLocaleTag.getOrPut(tag) {
45-
findTranslation(locale)
46-
}
47-
return translation[string] ?: error("Missing translation for $string")
48-
}
49-
50-
/**
51-
* A single translation; should contain all the [Strings].
52-
*/
53-
internal typealias Translation = Map<Strings, String>
54-
55-
/**
56-
* Translations we've already loaded, mapped by the locale tag (see [localeTag]).
57-
*/
58-
private val translationByLocaleTag = mutableMapOf<String, Translation>()
36+
private val cache = TranslationsCache(::translationFor)
5937

60-
/**
61-
* Returns the tag for the given locale.
62-
*
63-
* Note that this is our internal format; this isn't the same as [Locale.toLanguageTag].
64-
*/
65-
private fun localeTag(language: String, region: String) = when {
66-
language == "" -> ""
67-
region == "" -> language
68-
else -> "${language}_$region"
69-
}
70-
71-
/**
72-
* Returns a sequence of locale tags to use as keys to look up the translation for the given locale.
73-
*
74-
* Note that we don't need to check children (e.g. use `fr_FR` if `fr` is missing) because the
75-
* translations should never have a missing parent.
76-
*/
77-
private fun localeTagChain(locale: Locale) = sequence {
78-
if (locale.region != "") {
79-
yield(localeTag(language = locale.language, region = locale.region))
80-
}
81-
if (locale.language != "") {
82-
yield(localeTag(language = locale.language, region = ""))
83-
}
84-
yield(localeTag("", ""))
85-
}
86-
87-
/**
88-
* Finds a [Translation] for the given locale.
89-
*/
90-
private fun findTranslation(locale: Locale): Map<Strings, String> {
91-
// We don't need to merge translations because each one should contain all the strings.
92-
return localeTagChain(locale).firstNotNullOf { translationFor(it) }
93-
}
38+
internal fun getString(string: Strings): String = cache.getString(string)
9439

9540
/**
96-
* This object is only needed to provide a namespace for the [Translation] provider functions
41+
* This object is only needed to provide a namespace for the translation map provider functions
9742
* (e.g. [Translations.en]), to avoid polluting the global namespace.
9843
*/
99-
internal object Translations
44+
internal object Translations

0 commit comments

Comments
 (0)