Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
13edbc0
feat: derive and apply locale text direction
dcalhoun Jul 27, 2026
d1accfb
feat: load stylesheets matching the editor text direction
dcalhoun Jul 27, 2026
7f5a1ec
feat(demo-ios): forward Xcode's App Language to the editor
dcalhoun Jul 27, 2026
d055062
fix: Apply border styles for RTL layouts
dcalhoun Jul 27, 2026
e8a5ce1
feat(demo-android): forward the per-app language to the editor
dcalhoun Jul 27, 2026
631ced0
fix: Update asymmetric properties for RTL language layouts
dcalhoun Jul 27, 2026
1b04755
fix: Remove unused styles
dcalhoun Jul 27, 2026
6e5ab95
fix(demo-android): refresh the locale when returning from the picker
dcalhoun Jul 27, 2026
a49dfa5
docs: Reduce comment verbosity
dcalhoun Jul 27, 2026
c225998
refactor(demo-ios): move DemoAppLocale into Services
dcalhoun Jul 27, 2026
99b76ef
fix(ios): declare the editor's language to assistive technology
dcalhoun Jul 27, 2026
5778959
fix(demo-ios): stop English falling through to the next preferred lan…
dcalhoun Jul 27, 2026
f96418e
fix(ios): guard EditorAccessibilityLanguage behind canImport(UIKit)
dcalhoun Jul 28, 2026
4fd93be
fix: inject the editor styles before the existing stylesheets
dcalhoun Jul 28, 2026
a970b6b
fix: correct the toolbar scroll indicators for RTL layouts
dcalhoun Jul 28, 2026
cc384ef
refactor: derive the editor direction from a single resolution
dcalhoun Jul 28, 2026
615436b
perf: avoid resolving computed style on every toolbar scroll
dcalhoun Jul 28, 2026
16e41d1
fix(ios): apply the accessibility language once the view is in a hier…
dcalhoun Jul 28, 2026
225f6d2
fix(demo-android): guard the per-app language picker against no handler
dcalhoun Jul 28, 2026
4bff8d8
fix(demo-ios): match English by language subtag rather than prefix
dcalhoun Jul 28, 2026
9bc49b5
refactor(ios): declare the accessibility language once on the editor
dcalhoun Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:localeConfig="@xml/locales_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.GutenbergKit"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.example.gutenbergkit

import android.app.LocaleManager
import android.content.Context
import android.os.Build
import java.util.Locale

/**
* Reads the language the demo app is running in, so the editor can be told
* which translations to load.
*
* The language is chosen through the system's per-app language picker
* (Settings > Apps > GutenbergKit > Language), which offers the locales
* declared in `res/xml/locales_config.xml`. Forwarding it to
* `EditorConfiguration` lets the editor's localization — including
* right-to-left rendering — be exercised without code changes.
*
* Unlike the iOS demo app, no resolution logic lives here:
* `EditorConfiguration.Builder.setLocale(Locale)` already resolves against the
* bundled translations via the library's `LocaleResolver`.
*/
object DemoAppLocale {

/**
* The locale to hand the editor.
*
* Reads the platform's [LocaleManager] directly rather than going through
* `AppCompatDelegate.getApplicationLocales()`. That helper resolves the
* application locale by walking appcompat's registry of live activity
* delegates, and every activity in this app extends `ComponentActivity`
* rather than `AppCompatActivity`, so the registry is always empty and the
* helper reports no selection regardless of what the system holds.
*
* Falls back to the device language when no per-app language is set, or on
* Android versions predating per-app languages (API < 33).
*/
fun current(context: Context): Locale {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
return Locale.getDefault()
}

val locales = context.getSystemService(LocaleManager::class.java)
?.applicationLocales

if (locales == null || locales.isEmpty) {
return Locale.getDefault()
}
return locales[0]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ package com.example.gutenbergkit

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import org.json.JSONObject
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
Expand Down Expand Up @@ -43,17 +47,26 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.example.gutenbergkit.ui.theme.AppTheme
import java.util.Locale
import org.wordpress.gutenberg.model.EditorConfiguration
import org.wordpress.gutenberg.model.EditorDependencies
import org.wordpress.gutenberg.model.EditorDependenciesSerializer
Expand Down Expand Up @@ -203,7 +216,11 @@ fun SitePreparationScreen(
) {
val uiState by viewModel.uiState.collectAsState()

LaunchedEffect(Unit) {
// Re-read on resume so returning from the system language picker is
// noticed.
val locale = rememberLocaleOnResume()

LaunchedEffect(locale) {
viewModel.startLoading()
}

Expand Down Expand Up @@ -515,8 +532,85 @@ private fun EditorConfigurationDetailsCard(configuration: EditorConfiguration) {
KeyValueRow(key = "API Root", value = configuration.siteApiRoot)
KeyValueBooleanRow(key = "Supports Block Assets", value = configuration.plugins)
KeyValueBooleanRow(key = "Supports Theme Styles", value = configuration.themeStyles)
EditorLocaleRow(locale = configuration.locale)
}
}
}

/**
* The app's current locale, re-read every time the activity resumes.
*
* Returning from the system language picker does not reliably recreate this
* activity — the picker belongs to another task, so this one is often just
* stopped and resumed — and a plain read during composition would never see
* the new value. Observing `ON_RESUME` covers both cases.
*/
@Composable
private fun rememberLocaleOnResume(): Locale {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
var locale by remember { mutableStateOf(DemoAppLocale.current(context)) }

DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
locale = DemoAppLocale.current(context)
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
}

return locale
}

/**
* Shows the locale the editor will use, linking to the system's per-app
* language picker where one exists.
*
* The value is what the library resolved the app's language to, not the
* language itself — a locale with no bundled translations resolves to `en`,
* which is otherwise indistinguishable from the selection being ignored.
*/
@Composable
private fun EditorLocaleRow(locale: String?) {
val context = LocalContext.current
val resolved = locale ?: "en"

// The action is optional even on API 33+ — some devices ship no handler for
// it — so resolve the intent rather than inferring availability from the SDK
// level, which would throw on tap.
val settingsIntent = remember(context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
null
} else {
Intent(
Settings.ACTION_APP_LOCALE_SETTINGS,
Uri.fromParts("package", context.packageName, null)
).takeIf { it.resolveActivity(context.packageManager) != null }
}
}

if (settingsIntent == null) {
KeyValueRow(key = "Editor Locale", value = resolved)
return
}

Row(
modifier = Modifier
.fillMaxWidth()
.clickable { context.startActivity(settingsIntent) }
.padding(vertical = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
KeyValueRow(key = "Editor Locale", value = resolved)
Text(
text = "Change",
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary
)
}
}

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ class SitePreparationViewModel(
.setAuthHeader("")
.setCookies(emptyMap())
.setEnableOfflineMode(true)
.setLocale(DemoAppLocale.current(getApplication()))
.build()
}

Expand Down Expand Up @@ -278,6 +279,7 @@ class SitePreparationViewModel(
.setCookies(emptyMap())
.setEnableNetworkLogging(true)
.setEnableAssetCaching(capabilities.supportsPlugins)
.setLocale(DemoAppLocale.current(getApplication()))
.build()
}

Expand Down
33 changes: 33 additions & 0 deletions android/app/src/main/res/xml/locales_config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Languages offered by the system's per-app language picker
(Settings > Apps > GutenbergKit > Language).

This is a deliberately curated subset of the ~49 locales the editor ships
translations for, chosen so each entry exercises a distinct rendering path:

en left-to-right baseline
es/fr Latin translations, accented characters
ar right-to-left, cursive script with contextual letter shaping
he right-to-left, non-cursive — catches shaping bugs `ar` alone hides
ja CJK: Han glyph selection driven by `lang`, wide glyphs, different
line-breaking rules
pt-BR the only regional bundle here, so it exercises the library
resolver's `language-region` step rather than the bare-language
fallback

Add entries as needed; anything listed must have a matching
`src/translations/<tag>.json` or the editor silently falls back to English.

Only read on Android 13 (API 33) and above. Older versions ignore this file
and the app's language follows the device.
-->
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="en" />
<locale android:name="es" />
<locale android:name="fr" />
<locale android:name="ar" />
<locale android:name="he" />
<locale android:name="ja" />
<locale android:name="pt-BR" />
</locale-config>
4 changes: 4 additions & 0 deletions ios/Demo-iOS/Gutenberg.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
246852562EAABB7800ED1F09 /* WordPressAPI in Frameworks */ = {isa = PBXBuildFile; productRef = 0C4F59A12BEFF4980028BD96 /* WordPressAPI */; };
2468526B2EAACCA100ED1F09 /* AuthenticationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 246852682EAACCA100ED1F09 /* AuthenticationManager.swift */; };
2468526C2EAACCA100ED1F09 /* ConfigurationStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 246852692EAACCA100ED1F09 /* ConfigurationStorage.swift */; };
2FCF7A593017EC80008F5560 /* DemoAppLocale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FCF7A503017EC80008F5560 /* DemoAppLocale.swift */; };
BB0000012F11000000000001 /* GutenbergKitHTTP in Frameworks */ = {isa = PBXBuildFile; productRef = BB0000012F11000000000002 /* GutenbergKitHTTP */; };
/* End PBXBuildFile section */

Expand All @@ -36,6 +37,7 @@
0CE8E7892C339B0600B9DC67 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
246852682EAACCA100ED1F09 /* AuthenticationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationManager.swift; sourceTree = "<group>"; };
246852692EAACCA100ED1F09 /* ConfigurationStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationStorage.swift; sourceTree = "<group>"; };
2FCF7A503017EC80008F5560 /* DemoAppLocale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoAppLocale.swift; sourceTree = "<group>"; };
AA0000012F00000000000001 /* GutenbergUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GutenbergUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -124,6 +126,7 @@
isa = PBXGroup;
children = (
246852682EAACCA100ED1F09 /* AuthenticationManager.swift */,
2FCF7A503017EC80008F5560 /* DemoAppLocale.swift */,
246852692EAACCA100ED1F09 /* ConfigurationStorage.swift */,
);
path = Services;
Expand Down Expand Up @@ -272,6 +275,7 @@
0C4F59A62BEFF4980028BD96 /* ConfigurationItem.swift in Sources */,
0CE8E78E2C339B0600B9DC67 /* GutenbergApp.swift in Sources */,
2468526B2EAACCA100ED1F09 /* AuthenticationManager.swift in Sources */,
2FCF7A593017EC80008F5560 /* DemoAppLocale.swift in Sources */,
2468526C2EAACCA100ED1F09 /* ConfigurationStorage.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
Loading
Loading