diff --git a/src/editor/components/select-voice/class-assets.php b/src/editor/components/select-voice/class-assets.php
index 9e92c54e..95a40c64 100644
--- a/src/editor/components/select-voice/class-assets.php
+++ b/src/editor/components/select-voice/class-assets.php
@@ -59,6 +59,8 @@ public static function admin_enqueue_scripts( $hook ): void {
'nonce' => wp_create_nonce( 'wp_rest' ),
'root' => esc_url_raw( rest_url() ),
'projectId' => (string) get_option( 'beyondwords_project_id', '' ),
+ // Lets the script rebuild the Accent dropdown when a language name is picked.
+ 'languages' => \BeyondWords\Editor\Components\SelectVoice::languages_for_script(),
'selectVoice' => __( 'Select a voice', 'speechkit' ),
'selectModel' => __( 'Select a model', 'speechkit' ),
'standardModel' => __( 'Legacy', 'speechkit' ),
diff --git a/src/editor/components/select-voice/class-select-voice.php b/src/editor/components/select-voice/class-select-voice.php
index c8262734..c1a9e94d 100644
--- a/src/editor/components/select-voice/class-select-voice.php
+++ b/src/editor/components/select-voice/class-select-voice.php
@@ -87,6 +87,9 @@ public static function element( $post ) {
$languages = self::get_languages();
$voices = self::get_voices_for_language( $language_code );
+ $native_filter = self::default_native_filter( $voices, $language_code, $voice_id );
+ $filtered_voices = self::filter_voices_by_native( $voices, $language_code, $native_filter, $voice_id );
+
// "Customize" is opt-in: a post is customised once it has an explicit
// language or voice; when off we store nothing so project defaults apply.
$customize = '' !== (string) $language_code || '' !== (string) $voice_id;
@@ -98,9 +101,11 @@ public static function element( $post ) {
?>
@@ -213,42 +218,153 @@ private static function get_voices_for_language( $language_code ): array {
}
/**
- * Render the language select dropdown.
+ * Whether a language row carries the fields the pickers need.
*
- * @since 6.0.0
- * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods.
+ * @since 7.0.0
+ *
+ * @param mixed $language A language record.
+ *
+ * @return bool
+ */
+ private static function is_valid_language( $language ): bool {
+ return is_array( $language )
+ && ! empty( $language['code'] )
+ && ! empty( $language['name'] )
+ && ! empty( $language['accent'] );
+ }
+
+ /**
+ * The distinct language names across the language rows, in API order.
+ *
+ * @since 7.0.0
+ *
+ * @param array $languages The languages array.
+ *
+ * @return string[] The language names.
+ */
+ public static function language_names( array $languages ): array {
+ $names = [];
+
+ foreach ( $languages as $language ) {
+ if ( ! self::is_valid_language( $language ) ) {
+ continue;
+ }
+ if ( ! in_array( $language['name'], $names, true ) ) {
+ $names[] = $language['name'];
+ }
+ }
+
+ return $names;
+ }
+
+ /**
+ * The language rows (accents) for a language name, in API order.
+ *
+ * @since 7.0.0
+ *
+ * @param array $languages The languages array.
+ * @param string $name The language name.
+ *
+ * @return array The matching language rows.
+ */
+ public static function accents_for_name( array $languages, string $name ): array {
+ if ( '' === $name ) {
+ return [];
+ }
+
+ return array_values(
+ array_filter(
+ $languages,
+ static function ( $language ) use ( $name ) {
+ return self::is_valid_language( $language ) && $language['name'] === $name;
+ }
+ )
+ );
+ }
+
+ /**
+ * Find a language row by its code.
+ *
+ * @since 7.0.0
+ *
+ * @param array $languages The languages array.
+ * @param string|false $code The language code.
+ *
+ * @return array|null The matching language row, or null.
+ */
+ public static function find_language_by_code( array $languages, $code ): ?array {
+ foreach ( $languages as $language ) {
+ if ( self::is_valid_language( $language ) && strval( $language['code'] ) === strval( $code ) ) {
+ return $language;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Slim language rows for the classic-editor script, so it can rebuild the
+ * Accent dropdown client-side without another API round-trip.
+ *
+ * @since 7.0.0
+ *
+ * @return array The slim language rows.
+ */
+ public static function languages_for_script(): array {
+ $rows = [];
+
+ foreach ( self::get_languages() as $language ) {
+ if ( ! self::is_valid_language( $language ) ) {
+ continue;
+ }
+
+ $rows[] = [
+ 'code' => strval( $language['code'] ),
+ 'name' => strval( $language['name'] ),
+ 'accent' => strval( $language['accent'] ),
+ 'defaultVoiceId' => strval( $language['default_voices']['body']['id'] ?? '' ),
+ ];
+ }
+
+ return $rows;
+ }
+
+ /**
+ * Render the Language select: one entry per language NAME (e.g. "English").
+ * Carries no `name` attribute, so it is not submitted — the Accent select
+ * holds the `beyondwords_language_code` field.
+ *
+ * @since 6.0.0 As render_language_select, combining name + accent.
+ * @since 7.0.0 Split into Language (name) + Accent selects.
*
* @param array $languages The languages array.
* @param string|false $selected_lang_code The selected language code.
*/
- private static function render_language_select( array $languages, $selected_lang_code ): void {
+ private static function render_language_name_select( array $languages, $selected_lang_code ): void {
+ $selected_language = self::find_language_by_code( $languages, $selected_lang_code );
+ $selected_name = $selected_language['name'] ?? '';
?>
-
-