Skip to content

Instantly share code, notes, and snippets.

@rmpel
Last active June 13, 2024 15:07
Show Gist options
  • Save rmpel/d9989da79956fb0eafe81e49331b84b6 to your computer and use it in GitHub Desktop.
Save rmpel/d9989da79956fb0eafe81e49331b84b6 to your computer and use it in GitHub Desktop.
Translate post-type-archive-slug when different from post-type-slug. WPML allows translating custom post-type slug, but not the custom post-type-archive slug
<?php
/**
* Translate post-type-archive-slug when different from post-type-slug.
*
* You can have your archive slug set to, for example /books and the singles on /book/title by setting
* $args['rewrite'] => [ 'slug' => 'book', ... ];
* $args['has_archive'] => 'books';
* when registering your post_type
*
* WPML supports translating the single-slug, but not the archive-slug.
*
* Following code allows that. It's a 3-part solution, all 3 parts are needed.
*
* After implementation:
*
* 1. save permalinks (will register the strings with WPML)
* 2. use WPML string translation to translate the strings, found in text-domain "post-type-archive-slug"
* You may need to set the correct source-language for the text-domain using the WPML String Translation tools.
* 3. save permalinks (will update rewrite rules for tanslations)
*
* If you don't want to use WPML String Translation, change the text-domain post-type-archive-slug to that of your theme
* or plugin and register the strings by putting a line like this:
* $not_used = __('archive-slug', 'text-domain');
* in your theme or plugin and scan the files with your favorite POMO-editor.
*
* For more information:
* @see https://developer.wordpress.org/apis/handbook/internationalization/localization/#translating-themes-and-plugins
*
*
* Part 1: filter the link generation. This works on the page.
*/
add_filter( 'post_type_archive_link', function ( $permalink, $post_type ) {
$post_type_object = get_post_type_object( $post_type );
// dont do anything if
// - not a valid post_type
// - rewrite not enabled
// - slug not defined
// - archive-slug not defined
// - archive-slug not different from post-slug
if ( ! $post_type_object || ! is_array( $post_type_object->rewrite ) || ! isset( $post_type_object->rewrite['slug'] ) || ! is_a( $post_type_object, WP_Post_Type::class ) || ! is_string( $post_type_object->has_archive ) || $post_type_object->rewrite['slug'] == $post_type_object->has_archive ) {
return $permalink;
}
// use a cached version written after permalink resave, so we don't have to do grunt work every time.
$match = get_option( '__wpml_post_type_archive_slug_match', [] );
$match = $match && isset( $match[ $post_type ] ) ? $match[ $post_type ] : false;
if ( ! $match ) {
// sorry, need to save permalinks first.
// silently fail.
return $permalink;
}
/**
* @action wpml_register_single_string
* @var string $context the text-domain
* This should be $text_domain, not $context
* @var string $name a description of the string.
* because this is the actual Context for the string
* @var string $value the text to translate
* @var bool $allow_empty_value , default = false
* @var string $source_lang_code , default to system-default-language.
*/
do_action( 'wpml_register_single_string', 'post-type-archive-slug', '', $post_type_object->has_archive );
$permalink = preg_replace( '@/' . implode('|', $match) . '/@', '/' . __( $post_type_object->has_archive, 'post-type-archive-slug' ) . '/', $permalink );
/**
* Why? sometimes url's come with dual slashes after the website domain.
* replace all // with / except when it is ://
*/
$permalink = preg_replace('@([^:]/)/@', '\1', $permalink);
return $permalink;
}, PHP_INT_MAX, 2 );
// above works for "current language" but fails for the language switcher. hence the following hook
/**
* Part 2: filter the link generation. This works in the language switch.
* Warning: this is done multiple times during a page generation, but only once for the actual language-switcher
* This is detected by the code by checking the post_Type being defined. ... ugly, but it works.
*/
add_filter( 'icl_ls_languages', function ( $languages ) {
$patch = get_option( '__wpml_post_type_archive_slug_match', [] );
if ( is_admin() || ! $patch || ! is_array( $patch ) ) {
return $languages;
}
foreach ( $patch as $post_type => $_patches ) {
$post_type_object = get_post_type_object( $post_type );
if ($post_type_object && is_a($post_type_object, 'WP_Post_Type')) {
foreach ( $languages as $language_code => &$language ) {
$language['url'] = preg_replace( '@/(' . implode('|', $_patches) . ')/@', '/' . ($_patches[$language_code] ?: $post_type_object->has_archive) . '/', $language['url'] );
/**
* Why? sometimes url's come with dual slashes after the website domain.
* replace all // with / except when it is ://
*/
$language['url'] = preg_replace('@([^:]/)/@', '\1', $language['url']);
}
}
}
return $languages;
} );
/**
* Part 3: make the url's work.
* This step also caches the list of post-types and their possible slugs
* Effort is mate to only do work when there is actually an archive-slug that is different from the post-type-slug and rewrite is enabled.
*/
add_filter( 'rewrite_rules_array', function ( $rules ) {
global $sitepress;
$current_language = $sitepress->get_current_language();
$post_types = get_post_types();
$post_types = array_filter( $post_types, function ( $post_type ) {
$post_type_object = get_post_type_object( $post_type );
return ! ( ! $post_type_object || ! is_array( $post_type_object->rewrite ) || ! isset( $post_type_object->rewrite['slug'] ) || ! is_a( $post_type_object, WP_Post_Type::class ) || ! is_string( $post_type_object->has_archive ) || $post_type_object->rewrite['slug'] == $post_type_object->has_archive );
} );
if ( ! $post_types ) {
return $rules;
}
$languages = apply_filters( 'wpml_active_languages', array(), 'skip_missing=0' );
$match = array_map( function ( $post_type ) use ( $languages, $sitepress ) {
$post_type_object = get_post_type_object( $post_type );
return $post_type_object->has_archive;
}, $post_types );
$patch = array_map( function ( $post_type ) use ( $languages, $sitepress ) {
$post_type_object = get_post_type_object( $post_type );
$strings = [];
foreach ( $languages as $language_code => $language ) {
$sitepress->switch_lang( $language_code );
$strings[ $language_code ] = __( $post_type_object->has_archive, 'post-type-archive-slug' );
}
return $strings;
}, $post_types );
$sitepress->switch_lang( $current_language );
update_option( '__wpml_post_type_archive_slug_match', $patch );
$patch = array_map(function($strings){
return implode('|', $strings);
}, $patch);
$match = '@^(' . implode( '|', $match ) . ')/@';
$patch = '(?:' . implode( '|', $patch ) . ')';
$keys = array_keys( $rules );
$values = array_values( $rules );
foreach ( $keys as &$key ) {
$key = preg_replace( $match, $patch, $key );
}
$rules = array_combine( $keys, $values );
return $rules;
} );

With WPML you can localize the slug of your CPT single, but not your CPT archive, if you give it a different content. This gist will fix that.

With WPML you can localize the "page for posts", but this will not localize the post-URL-base. You can fix this with https://gist.github.com/rmpel/5672b2b943a4a015fe1777c0a31fac59 .

While this will fix all URLs on the front-end, you will still need to fix the Sitemap links, in case you use Yoast SEO (Basic or Premium), or SEO by Rank Math. You van do this with https://gist.github.com/rmpel/b00997f2504c39c718028dfa7e79b87c .

@binzorellino
Copy link

Thank you very much for sharing this code!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment