Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 11 additions & 1 deletion classes/class-mpw_migrate_posts.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ public function migrate_posts() {
foreach ($posts_grouped_by_polylang_lang_relation as $relation) {

$default_language_code = $this->get_default_language_code($relation);
if ('' === $default_language_code['wpml']) {
continue;
}

$originalPostId = $this->getOriginalPostId( $relation, $default_language_code );
if ( ! $originalPostId ) {
Expand Down Expand Up @@ -245,6 +248,9 @@ private function set_other_posts_language_details( $relation, $default_language_
}

$next_post_language_code_wpml_format = $this->polylang_data->lang_slug_to_wpml_format($next_post_language_code);
if ('' === $next_post_language_code_wpml_format) {
continue;
}

do_action('wpml_set_element_language_details', array(
'element_id' => $post_id,
Expand All @@ -262,7 +268,11 @@ private function set_other_posts_language_details( $relation, $default_language_
// Polylang's sync map is keyed by its own slugs; make_duplicate() wants a WPML code.
foreach ( $sync as $targetLang => $sourceLang ) {
if ( $targetLang !== $sourceLang ) {
$sitepress->make_duplicate( $originalPostId, $this->polylang_data->lang_slug_to_wpml_format( $targetLang ) );
$target_language_code = $this->polylang_data->lang_slug_to_wpml_format( $targetLang );

if ( '' !== $target_language_code ) {
$sitepress->make_duplicate( $originalPostId, $target_language_code );
}
}
}
}
Expand Down
146 changes: 128 additions & 18 deletions classes/class-mpw_polylang_data.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,25 @@
defined('ABSPATH') || exit;

class mpw_polylang_data {

private static $terms;


/**
* Resolved Polylang slug => WPML language code, for the life of the request.
*
* @var array
*/
private $wpml_code_cache = array();

/**
* Polylang slug => locale, for languages that resolved to a code WPML does not know.
*
* @var array
*/
private $unmapped_languages = array();

public function __construct() {

}

public function get_languages() {
Expand Down Expand Up @@ -113,6 +127,21 @@ public function get_languages_map() {
return $polylang_languages_map;
}

/**
* Translates a Polylang language slug into the WPML language code.
*
* Both Polylang slugs and WPML codes can be customised independently. The dependable bridge
* between them is the locale: Polylang keeps it in the `language` term's serialised
* description, and WPML keeps it in `icl_languages.default_locale`, with per-site overrides
* in `icl_locale_map`.
*
* WPML resolves a code to a locale in WPML_Locale::get_all_locales() by preferring the override
* and falling back to the default. This runs that same lookup backwards.
*
* @param mixed $slug
*
* @return string A WPML language code, or an empty string for unusable input.
*/
public function lang_slug_to_wpml_format($slug) {

if (!is_scalar($slug)) {
Expand All @@ -121,26 +150,107 @@ public function lang_slug_to_wpml_format($slug) {

$slug = (string) $slug;

$different = array(
'pt' => 'pt-pt',
'zh' => 'zh-hans'
);
if ('' === $slug) {
return '';
}

if (!array_key_exists($slug, $this->wpml_code_cache)) {
$this->wpml_code_cache[$slug] = $this->resolve_wpml_code($slug);
}

$languages = $this->get_languages();
return $this->wpml_code_cache[$slug];
}

foreach ($languages as $language) {
if (isset($language->slug, $language->description) && $language->slug === 'pt') {
$pt_language_details = maybe_unserialize($language->description);
if (is_array($pt_language_details) && isset($pt_language_details['locale']) && $pt_language_details['locale'] === "pt_BR") {
$different['pt'] = 'pt-br';
}
/**
* Languages whose locale could not be mapped unambiguously to a WPML language.
*
* A Polylang slug and a WPML code may happen to be equal, but that is not evidence that they
* represent the same language. Callers receive an empty code for these entries so they do not
* write an unsupported or incorrect language into WPML.
*
* @return array Polylang slug => locale (empty string when Polylang had no locale either).
*/
public function get_unmapped_languages() {
return $this->unmapped_languages;
}

/**
* @param string $slug
*
* @return string
*/
private function resolve_wpml_code($slug) {
$locale = $this->get_locale_for_slug($slug);

if ('' !== $locale) {
$code = $this->wpml_code_for_locale($locale);

if ('' !== $code) {
return $code;
}
}

$this->unmapped_languages[$slug] = $locale;

return '';
}

/**
* @param string $slug
*
* @return string The locale Polylang recorded for this language, or an empty string.
*/
private function get_locale_for_slug($slug) {
foreach ($this->get_languages() as $language) {
if (!isset($language->slug) || $language->slug !== $slug) {
continue;
}

if (!isset($language->description)) {
return '';
}

$details = maybe_unserialize($language->description);

if (is_array($details) && isset($details['locale']) && is_string($details['locale'])) {
return $details['locale'];
}

return '';
}
if (isset($different[$slug])) {
$slug = $different[$slug];

return '';
}

/**
* @param string $locale
*
* @return string The WPML code for this locale, or an empty string.
*/
private function wpml_code_for_locale($locale) {
global $wpdb;

if (!$this->wpml_tables_available()) {
return '';
}

return $slug;

$codes = $wpdb->get_col($wpdb->prepare(
"SELECT languages.code
FROM {$wpdb->prefix}icl_languages languages
LEFT JOIN {$wpdb->prefix}icl_locale_map locale_map ON locale_map.code = languages.code
WHERE COALESCE(locale_map.locale, languages.default_locale) = %s
ORDER BY languages.code",
$locale
));

return is_array($codes) && 1 === count($codes) && is_string($codes[0]) ? $codes[0] : '';
}

/**
* @return bool
*/
private function wpml_tables_available() {
return defined('ICL_SITEPRESS_VERSION');
}


Expand Down
44 changes: 31 additions & 13 deletions migrate-polylang-to-wpml.php
Original file line number Diff line number Diff line change
Expand Up @@ -435,17 +435,26 @@ private function migrate_languages() {

$pll_languages = $this->polylang_data->get_languages();

if (!empty($pll_languages) && is_array($pll_languages)) {
foreach ($pll_languages as $pll_language) {
if (isset($pll_language->slug)) {
$slug = $this->polylang_data->lang_slug_to_wpml_format($pll_language->slug);
$wpdb->update(
$wpdb->prefix . 'icl_languages',
array('active' => 1),
array('code' => $slug)
);
}
if (empty($pll_languages) || !is_array($pll_languages)) {
return;
}

foreach ($pll_languages as $pll_language) {
if (!isset($pll_language->slug)) {
continue;
}

$code = $this->polylang_data->lang_slug_to_wpml_format($pll_language->slug);

if ('' === $code) {
continue;
}

$wpdb->update(
$wpdb->prefix . 'icl_languages',
array('active' => 1),
array('code' => $code)
);
}
}

Expand Down Expand Up @@ -499,6 +508,10 @@ private function migrate_taxonomies() {
$element_type = apply_filters('wpml_element_type', $original_term->taxonomy);
$original_language_code = $this->polylang_data->lang_slug_to_wpml_format($original_slug);

if ('' === $original_language_code) {
continue;
}

do_action('wpml_set_element_language_details', array(
'element_id' => $original_term->term_taxonomy_id,
'element_type' => $element_type,
Expand All @@ -523,16 +536,17 @@ private function migrate_taxonomies() {

foreach ($relation as $translation_slug => $term_id) {
$translated_term = $this->get_term_by_term_id($term_id);
$translation_language_code = $this->polylang_data->lang_slug_to_wpml_format($translation_slug);

if (!isset($translated_term->term_taxonomy_id)) {
if (!isset($translated_term->term_taxonomy_id) || '' === $translation_language_code) {
continue;
}

do_action('wpml_set_element_language_details', array(
'element_id' => $translated_term->term_taxonomy_id,
'element_type' => $element_type,
'trid' => $trid,
'language_code' => $this->polylang_data->lang_slug_to_wpml_format($translation_slug),
'language_code' => $translation_language_code,
'source_language_code' => $original_language_code
));
}
Expand Down Expand Up @@ -707,7 +721,11 @@ private function migrate_widgets() {
if ($option && is_array($option)) {
foreach ($option as $key => $val) {
if (is_numeric($key) && is_array($val) && isset($val['pll_lang'])) {
$option[$key]['wpml_language'] = $this->polylang_data->lang_slug_to_wpml_format($val['pll_lang']);
$language_code = $this->polylang_data->lang_slug_to_wpml_format($val['pll_lang']);

if ('' !== $language_code) {
$option[$key]['wpml_language'] = $language_code;
}
}
}
update_option($widget->option_name, $option);
Expand Down
104 changes: 104 additions & 0 deletions tests/phpunit/TestLanguageMapping.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php
class TestLanguageMapping extends OTGS_TestCase {

private LanguageMappingWpdb $wpdb;

public function setUp(): void {
parent::setUp();

$this->wpdb = new LanguageMappingWpdb();
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- The subject reads the WordPress database global directly.
$GLOBALS['wpdb'] = $this->wpdb;

WP_Mock::userFunction( 'maybe_unserialize', array( 'return_arg' => 0 ) );
}

/**
* @dataProvider localeMappings
*/
public function test_it_maps_polylang_slugs_to_wpml_codes( string $slug, string $locale, string $code ): void {
$this->wpdb->effective_locales[ $code ] = $locale;

$subject = $this->subject_with_languages( array( $this->language( $slug, $locale ) ) );

$this->assertSame( $code, $subject->lang_slug_to_wpml_format( $slug ) );
}

public function localeMappings(): array {
return array(
'Traditional Chinese' => array( 'zh', 'zh_TW', 'zh-hant' ),
'Simplified Chinese' => array( 'zh', 'zh_CN', 'zh-hans' ),
'Norwegian Bokmal' => array( 'no', 'nb_NO', 'nb' ),
'custom English slug' => array( 'english', 'en_US', 'en' ),
'Portuguese' => array( 'pt', 'pt_PT', 'pt-pt' ),
);
}

public function test_it_supports_an_arbitrary_custom_wpml_code(): void {
$this->wpdb->effective_locales['customers-own-code'] = 'pt_PT';

$subject = $this->subject_with_languages( array( $this->language( 'pt', 'pt_PT' ) ) );

$this->assertSame( 'customers-own-code', $subject->lang_slug_to_wpml_format( 'pt' ) );
}

public function test_it_records_an_unknown_slug(): void {
$subject = $this->subject_with_languages( array( $this->language( 'klingon', 'tlh_AA' ) ) );

$this->assertSame( '', $subject->lang_slug_to_wpml_format( 'klingon' ) );
$this->assertSame( array( 'klingon' => 'tlh_AA' ), $subject->get_unmapped_languages() );
}

public function test_equal_codes_do_not_map_without_a_matching_locale(): void {
$this->wpdb->effective_locales['es'] = 'es_MX';

$subject = $this->subject_with_languages( array( $this->language( 'es', 'es_ES' ) ) );

$this->assertSame( '', $subject->lang_slug_to_wpml_format( 'es' ) );
$this->assertSame( array( 'es' => 'es_ES' ), $subject->get_unmapped_languages() );
}

public function test_an_ambiguous_locale_is_not_mapped_arbitrarily(): void {
$this->wpdb->effective_locales = array(
'custom-one' => 'en_US',
'custom-two' => 'en_US',
);

$subject = $this->subject_with_languages( array( $this->language( 'english', 'en_US' ) ) );

$this->assertSame( '', $subject->lang_slug_to_wpml_format( 'english' ) );
$this->assertSame( array( 'english' => 'en_US' ), $subject->get_unmapped_languages() );
}

/**
* @dataProvider unusableSlugs
*/
public function test_it_rejects_unusable_slugs( $slug ): void {
$subject = $this->subject_with_languages( array() );

$this->assertSame( '', $subject->lang_slug_to_wpml_format( $slug ) );
}

public function unusableSlugs(): array {
return array(
'empty string' => array( '' ),
'array' => array( array( 'en' ) ),
'object' => array( (object) array( 'slug' => 'en' ) ),
);
}

private function subject_with_languages( array $languages ): mpw_polylang_data {
$terms = new ReflectionProperty( 'mpw_polylang_data', 'terms' );
$terms->setAccessible( true );
$terms->setValue( null, array( 'language' => $languages ) );

return new mpw_polylang_data();
}

private function language( string $slug, string $locale ): object {
return (object) array(
'slug' => $slug,
'description' => array( 'locale' => $locale ),
);
}
}
Loading
Loading