WordPress + Regionalization

Regionalized merch, regionalized promos, regionalized blog posts, and the list goes on and on

Regionalized merch, regionalized promos, regionalized blog posts, and the list goes on and on

One of the coolest things about WordPress is that it is built from the ground up with translation tools. Many blogs want to vibe in a language other than English, and many blogs want to get their international game on and present content to people in several languages.

But what if you want to detect a user’s geographic location and display content based on their country code (“US” for USA, “FR” for France, et al) or your own regional site code (“EU” for European Union countries, for example)? This is not built-in but can be built by you if you know what you are doing. Let’s learn!

Btdubs, eMusic makes extensive use of regionalization. We have 4 regional sites: US, UK, EU, CA. We regionalize everything, so almost every database query on our site has to be subject to some filtering. We also have to intersect WP with our regionalized catalog data and in various places link them together magically.

Custom Taxonomy: Region

Creating new taxonomies in WP requires code, but adding terms to that taxonomy can be done by anyone in the admin. Let’s get the code out of the way:

/**
 * Shortcut function for assigning Labels to a custom taxonomy
 *
 * @param string $term
 * @param string $plural If not specified, "s" is added to the end of $term
 * @return array Labels for use by the custom taxonomy
 *
 */
function emusic_tax_inflection( $str = '', $plural = '' ) {
    $p = strlen( $plural ) ? $plural : $str . 's';

    return array(
        'name'              => _x( $p, 'taxonomy general name' ),
        'singular_name'     => _x( $str, 'taxonomy singular name' ),
        'search_items'      => __( 'Search ' . $p ),
        'all_items'         => __( 'All ' . $p ),
        'parent_item'       => __( 'Parent ' . $str ),
        'parent_item_colon' => __( 'Parent ' . $str . ':' ),
        'edit_item'         => __( 'Edit ' . $str ),
        'update_item'       => __( 'Update ' . $str ),
        'add_new_item'      => __( 'Add New ' . $str ),
        'new_item_name'     => __( 'New ' . $str . ' Name' ),
        'menu_name'         => __( $p ),
    );
}

$post_types = get_post_types( array( 'exclude_from_search' => false, '_builtin' => false ) );

$defaults = array(
    'hierarchical'      => true,
    'public'            => true,
    'show_ui'           => true,
    '_builtin'          => true,
    'show_in_nav_menus' => false,
    'query_var'         => true,
    'rewrite'           => false
);

register_taxonomy( 'region', $post_types, wp_parse_args( array(
    'labels' => emusic_tax_inflection( 'Region' )
), $defaults ) );

Because we load a bunch of custom taxonomies, this code helps us stay modular. But yes, this is the amount of code required if you only have one taxonomy!

All you *really* need to know is: we made a hierarchical taxonomy. It’s called region. We are now going to use it EVERYWHERE.

Boom

So now that we have a taxonomy for region, we want to be able to assign region(s) to posts.

Sorting

We also want to view the region in the posts list table. We can add custom columns to the posts table and make the region column sortable. We need some more code for that:

class CustomClassNamedWhatever {

.......

/**
 * Filters for Admin
 *
 */
function admin() {
    add_filter( 'manage_posts_columns', array( $this, 'manage_columns' ) );
    add_action( 'manage_posts_custom_column', array( $this, 'manage_custom_column' ), 10, 2 );
    add_filter( 'posts_clauses', array( $this, 'clauses' ), 10, 2 );

	// this is an internal method that gives me an array of relevant post types
    foreach ( $this->get_post_types( false ) as $t ) {
        add_filter( "manage_edit-{$t}_sortable_columns", array( $this, 'sortables' ) );
    }
}

/**
 * Register sortable columns
 *
 * @param array $columns
 * @return array
 */
function sortables( $columns ) {
    $post_type_obj = get_post_type();

    if ( is_object_in_taxonomy( $post_type_obj, 'region' ) )
	$columns['region'] = 'region';

    return $columns;
}

/**
 * Add custom column headers
 *
 * @param array $defaults
 * @return array
 */
function manage_columns( $columns ) {
    $columns['region'] = __( 'Region' );
    return $columns;
}

/**
 * Output terms for post in tax
 *
 * @param int $id
 * @param string $tax
 */
function _list_terms( $id, $tax ) {
    $terms = wp_get_object_terms( $id, $tax, array( 'fields' => 'names' ) );
    if ( ! empty( $terms ) )
        echo join( ', ', $terms );
}
/**
 * Output custom HTML for custom column row
 *
 * @param string $column
 * @param int $id
 */
function manage_custom_column( $column, $post_id ) {
    if ( 'region' === $column )
	$this->_list_terms( $id, $column );
}

/**
 * Filter SQL with this monstrosity for sorting
 *
 * @global hyperdb $wpdb
 * @param array $clauses
 * @param WP_Query $wp_query
 * @return array
 *
 * TODO: fix this
 */
function clauses( $clauses, $wp_query ) {
    global $wpdb;

    if ( isset( $wp_query->query['orderby'] ) &&
        'region' === $wp_query->query['orderby'] ) {
        $tax = $wp_query->query['orderby'];

        $clauses['join'] .= <<term_relationships} ON {$wpdb->posts}.ID={$wpdb->term_relationships}.object_id
LEFT OUTER JOIN {$wpdb->term_taxonomy} USING (term_taxonomy_id)
LEFT OUTER JOIN {$wpdb->terms} USING (term_id)
SQL;

        $clauses['where'] .= " AND (taxonomy = '{$tax}' OR taxonomy IS NULL)";
        $clauses['groupby'] = "object_id";
        $clauses['orderby']  = "GROUP_CONCAT({$wpdb->terms}.name ORDER BY name ASC) ";
        $clauses['orderby'] .= ( 'ASC' == strtoupper( $wp_query->get('order') ) ) ? 'ASC' : 'DESC';
    }

    return $clauses;
}

........

}

After all of that, we can sort our posts by region:

Sorting

If you’re new to WordPress, you may have just barfed. Just know that we added relevant code to make the Region column sortable. Meanwhile, we still haven’t done anything to make our site regionalized. If we let WordPress just do its thing, you would still get content from every region, not to the one you are specifically targeting.

Geolocation

We have very specific values we check to regionalize users. If you’re anonymous, we get an X-Akamai-Edgescape HTTP header that can parsed, and it contains a value for “country_code.” Based on that country code, we can assign you to a region. Anybody with a debug console can probably view this header in their eMusic requests. If you’re logged-in or “cookied” – we pin you to your original country code.

At the end of the day, we want to be able to set some constants:

/**
 * Register default constants
 *
 */
if ( ! defined( 'THE_COUNTRY' ) )
    define( 'THE_COUNTRY', get_country_code() );

if ( ! defined( 'THE_REGION' ) )
    define( 'THE_REGION', get_region() );

if ( ! defined( 'CURRENCY_SYMBOL' ) ) {
    global $currency_symbol_map;
    if ( isset( $currency_symbol_map[THE_REGION] ) ) {
	define( 'CURRENCY_SYMBOL', $currency_symbol_map[THE_REGION] );
    } else {
	define( 'CURRENCY_SYMBOL', $currency_symbol_map['US'] );
    }
}

get_country_code() and get_region() are very specific to eMusic, so I won’t bore you with them, but they both return a 2-character-uppercase value for country or region, “US” and “US” for example. Now, anywhere in our code where we need to refer to region or country, THE_REGION and THE_COUNTRY will do it.

Linking Region to Taxonomy

Ok cool, we have a taxonomy, and we have some vague representation of region / country defined as constants. We still don’t have regionalized content. One way we could link region to taxonomy terms would be to make a map and refer to it when necessary:

$regions_map = array(
    'US' => 3,
    'UK' => 6,
    'CA' => 9,
    'EU' => 11,
);

This method sucks. Why? Are those IDs term_ids or term_taxonomy_ids? What if they change? Also, grabbing a PHP global every time you need to translate region code to term_id is weird. How else can we get the terms belonging to the region taxonomy? Let’s try this database call which is cached in memory after the first time it is retrieved:

get_terms( 'region' );

That’s great, but what do I do with it? Because your Region codes are also the names of your region terms, you could dynamically create your map like this:

$map = array();
foreach ( $terms as $term )
    $map[$term->name] = $term->term_taxonomy_id;

Next question: where do I set this, and how do I account for crazy stuff like switch_to_blog()? Try this:

/**
 * Automatically resets regions_tax_map on switch_to_blog()
 * Because we switch_to_blog() in sunrise.php, this function is
 * called when plugins_loaded to create initial values
 *
 * @param int $blog_id
 *
 */
function set_regions_tax_map( $blog_id = 0 ) {
     if ( ! isset( get_emusic()->regions_tax_maps ) )
	get_emusic()->regions_tax_maps = array();

     if ( ! taxonomy_exists( 'region' ) )
	return;

     if ( empty( $blog_id ) )
	$blog_id = get_current_blog_id();

     if ( isset( get_emusic()->regions_tax_maps[$blog_id] ) ) {
	get_emusic()->regions_tax_map = get_emusic()->regions_tax_maps[$blog_id];
	return;
    }

    $terms = get_terms( 'region' );

    $map = array();
    foreach ( $terms as $term )
	$map[$term->name] = $term->term_taxonomy_id;

    get_emusic()->regions_tax_maps[$blog_id] = $map;
    get_emusic()->regions_tax_map = get_emusic()->regions_tax_maps[$blog_id];
}

/**
 * The initial setting of $emusic->regions_tax_map on load
 *
 */
add_action( 'init', 'set_regions_tax_map', 20 );
/**
 * Resets context on switch to blog
 *
 */
add_action( 'switch_blog', 'set_regions_tax_map' );

STILL, we don’t have regionalized content, we just have a standard way of mapping region code to term_taxonomy_ids. So why do I care about the IDs? Because we have to use them to alter our default WP queries so that posts only intersect the region of the current user.

Tax Query

Tax Query is an advanced way of altering WP_Query. If you load a page of WP posts, you ran WP_Query. A developer can make their own instances of WP_Query or alter the global one. We need to alter the global one and we don’t want to ever use query_posts to do that. The first thing we need to do is retrieve our map of Term Name => Term Taxonomy ID:

/**
 * Gets a map of region name => term_taxonomy_id
 *
 * @return array
 */
function get_current_regions_tax_map() {
    return get_emusic()->regions_tax_map;
}

Now that we have that, we want to use it in the tax query that we are going to inject into WP_Query. One step at a time, here’s the tax_query portion, encapsulated in a function that always returns the proper context:

/**
 * Encapsulates common code to create a regionalized tax_query
 *
 * e.g. $tax_query = array( get_region_tax_query() );
 *
 * @param int $region
 * @param int $all
 * @return array
 */
function get_region_tax_query( $region = '', $all = true ) {
	$regions_tax_map = get_current_regions_tax_map();
	if ( empty( $regions_tax_map ) )
		return;

	$terms = array();
	$terms[] = $regions_tax_map[ empty( $region ) ? THE_REGION : strtoupper( $region ) ];
	if ( true === $all )
		$terms[] = $regions_tax_map['ALL'];

	$tax_query = array(
		'operator'		=> 'IN',
		'taxonomy'		=> 'region',
		'field'			=> 'term_taxonomy_id',
		'terms'			=> $terms,
		'include_children'	=> false
	);

	return $tax_query;
}

Being able to query by term_taxonomy_id is important, because it is the primary key for the term / taxonomy relationship. If I had passed term_ids, they would need to be translated into term_taxonomy_ids by WP_Tax_Query before WP_Query could complete its logic. I helped nurse this new functionality along in WordPress core (3.5): https://core.trac.wordpress.org/ticket/21228

Ok cool, we have the tax_query portion, but we haven’t applied it to the global WP_Query yet, let’s do that next.

“pre_get_posts”

Here is what you may use in your theme or plugin to apply regionalization to WP_Query. The best place to hook in is “pre_get_posts.” In this hook, we can directly alter the query:

class MyAwesomeExampleThemeClass {

.....

protected function regionalize() {
    add_filter( 'pre_get_posts', array( $this, 'pre_posts' ) );
}

/**
 *
 * @param WP_Query $query
 * @return WP_Query
 */
function pre_posts( $query ) {
    if ( $query->is_main_query() && ! is_admin() && ( is_archive() || is_front_page() || is_search() ) ) {
        if ( ! $query->is_post_type_archive() )
            $query->set( 'post_type', $this->post_types );

        $query->set( 'tax_query', array( get_region_tax_query() ) );

        $query = apply_filters( 'emusic_pre_get_posts', $query );
    }
    return $query;
}

....

}

The above will regionalize posts in search results and on archive pages. If you want to alter a query inline, now all you have to do is something like this:

$results = new WP_Query( array(
    'tax_query'	=> array( get_region_tax_query() ),
    'orderby' => 'comment_count',
    'order' => 'DESC',
    'posts_per_page'=> $limit
) );

In Action

View eMusic in multiple regions:

From the US
From France in the EU
From Canada (CA)
From Great Britain, mate (UK)

Listen to This: “Trilogy” by The Weeknd

So, it turns out there’s a genre named “PBR&B.” I had never heard of it til last night, but I think it is my new favorite type of music. I was telling some friends last week about how much I love the Frank Ocean record, and they responded with “you’d love The Weeknd.” Luckily, last week, Trilogy was released – remastered versions of The Weeknd’s 3 brilliant mixtapes from 2011. This being 2012, do we all include it in our best of lists, or is it honorable mention with a footnote?

It’s a lot of music to listen to, but it captures a vibe I can’t get enough of. It’s like Portishead,  Porcelain Raft, and Frank Ocean having a conversation about sex while on drugs, quietly, in Canada (? that’s where Abel Tesfaye is from). And he was also a co-writer of Drake’s Take Care.

The review on eMusic by Barry Walters

“The Morning”

“High for This”

“Wicked Games”

Listen To This: “Channel Orange,” by Frank Ocean

There is no SoundCloud embed for this one, so I’m gonna post some vids. This is one of the more acclaimed debuts of the year, I just finally got around to listening to it.

“Thinkin Bout You”

Official Video for “Pyramids” – featuring John Mayer at the end playing a guitar solo that is not on the record:

Go download the whole record, which is awesome.

The Walkmen: Tiny Desk Concert

The Walkmen’s wiry, weary, seething rock ‘n’ roll has mellowed in recent years, as vein-bulging hollers have given way to a richer, more gently paced and layered sound. But it’s still not exactly the stuff of campfire crooning: We’d spent the days leading up to this Tiny Desk Concert in the NPR Music offices wondering how on earth The Walkmen would downsize these songs behind Bob Boilen’s desk. For one thing, Hamilton Leithauser sings with such intensity that it’d be hard to dial down the instrumentation too much — you’d never hear it.
Leithauser may wield an acoustic guitar in these three songs, but this is no awkward attempt to shoehorn booming rock anthems into meek arrangements that don’t suit them. From the opening notes of “Heaven,” the magnificently stirring title track of The Walkmen’s recent seventh album, it became clear that these guys were making the Tiny Desk accommodate their sound rather than the other way around. Leithauser’s voice soared far down the hallways at various points in all three of these songs from Heaven — “Remember, rememmmmmmmber, all we fight for!” “It’s been soooooooooooooo long!” “Love is luuuuuuuuuuuuuck!” — lending them grit and grace, not to mention hair-raising intensity that feels a little jarring coming from a bunch of guys in crisp button-up shirts.

Set List:
• “Heaven”
• “We Can’t Be Beat”
• “Love Is Luck”

Credits:
Producer and Editor: Bob Boilen; Videographers: Becky Lettenberger, Nick Michael and Bob Boilen; Audio Engineer: Kevin Wait; photo by Blake Lipthratt/NPR

via YouTube

Bon Iver, Woods HD (Live from Radio City Music Hall)

Bon Iver play a track from the ‘Blood Bank’ EP during their September 21 webcast on The Bowery Presents Live.

http://boniver.org/

Subscribe to The Bowery Presents Live: Subscribe: http://tbp.im/xHmiTH
http://www.bowerypresents.com/

The Bowery Presents Live features groundbreaking, must-see live music and the latest in emerging music culture. The Bowery Presents Live offers live multicamera concert films and intimate performances from industry leaders and the next big things in music.

A Show Cobra Production
http://www.showcobra.com/

Executive Producer: Jason Ross
Supervising Producer: Kate Susman
Director: Matt Boyd
Live Director: Dom Whitworth
Director of Production: Bryan Walters
Line Producer: Karen Scarminach
Editorial Director: Peter Gaston
Tech Manager: Tony Pietzrak
Broadcast Engineer: Jacob Feinberg
Consulting Producer: Dan Huiting

Bon Iver, Calgary / Beth/Rest HD (Live from Radio City Music Hall)

Bon Iver play two tracks from their self-titled album during their September 21 webcast on The Bowery Presents Live.

http://boniver.org/

Subscribe to The Bowery Presents Live: Subscribe: http://tbp.im/xHmiTH
http://www.bowerypresents.com/

The Bowery Presents Live features groundbreaking, must-see live music and the latest in emerging music culture. The Bowery Presents Live offers live multicamera concert films and intimate performances from industry leaders and the next big things in music.

A Show Cobra Production
http://www.showcobra.com/

Executive Producer: Jason Ross
Supervising Producer: Kate Susman
Director: Matt Boyd
Live Director: Dom Whitworth
Director of Production: Bryan Walters
Line Producer: Karen Scarminach
Editorial Director: Peter Gaston
Tech Manager: Tony Pietzrak
Broadcast Engineer: Jacob Feinberg
Consulting Producer: Dan Huiting

Best Albums of (75% of) 2012 (So Far)

Updated version of: http://scotty-t.com/2012/06/22/best-albums-of-2012-so-far/

Top 10

fun., Some Nights
Porcelain Raft, Strange Weekend
Dirty Projectors, Swing Lo Magellan
Beach House, Bloom
Metric, Synthetica
The Walkmen, Heaven
Bruce Springsteen, Wrecking Ball
Matthew Dear, Beams
Sharon Van Etten, Tramp
Father John Misty, Fear Fun
Escort, Escort

The Rest (Bottom 10)

Grizzly Bear, Shields
Strand of Oaks, Dark Shores
Metric, Synthetica
Daughn Gibson, All Hell
White Rabbits, Milk Famous
Silversun Pickups, Neck of the Woods
of Montreal, Paralytic Stalks
Japandroids, Celebration Rock
Craig Finn, Clear Heart Full Eyes
Heartless Bastards, Arrow
Bear in Heaven, I Love You, It’s Cool
Escort, Escort
Sharon Van Etten, Tramp
Beach House, Bloom