Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save senlin/1adf65ae297c32142e765628fce194ad to your computer and use it in GitHub Desktop.
Save senlin/1adf65ae297c32142e765628fce194ad 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.
*
* Make sure that all this is done in WP backend when on the default (primary) language, see [this comment](https://gist.github.com/senlin/1adf65ae297c32142e765628fce194ad?permalink_comment_id=5351512#gistcomment-5351512)
*
* 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 .

@bluantinoo
Copy link

This is brilliant @senlin
thanks for sharing!

you should mention in the comments that when saving permalinks user language (the admin language in user profile) should be the primary language of the website. Otherwise the translation will be applied even to the main language archive slug.

@senlin
Copy link
Author

senlin commented Dec 20, 2024

Thanks for your input @bluantinoo
I have added a line to the top part; I guess you found out the hard way?

@bluantinoo
Copy link

I had to carefully read your code, it does not always happen ;)

@bluantinoo
Copy link

I noticed that the second language archive URL is working on the primary language too.
Now in primary language I have 2 URLs with the same archive. that's weird...

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