Index: src/wp-includes/class-wp-site-query.php
===================================================================
--- src/wp-includes/class-wp-site-query.php	(revision 40321)
+++ src/wp-includes/class-wp-site-query.php	(working copy)
@@ -99,6 +99,7 @@
 	 * Sets up the site query, based on the query vars passed.
 	 *
 	 * @since 4.6.0
+	 * @since 4.8.0 Introduced the 'lang_id', 'lang__in', and 'lang__not_in' parameters.
 	 * @access public
 	 *
 	 * @param string|array $query {
@@ -138,6 +139,9 @@
 	 *     @type int          $mature            Limit results to mature sites. Accepts '1' or '0'. Default empty.
 	 *     @type int          $spam              Limit results to spam sites. Accepts '1' or '0'. Default empty.
 	 *     @type int          $deleted           Limit results to deleted sites. Accepts '1' or '0'. Default empty.
+	 *     @type int          $lang_id           Limit results to a language ID. Default empty.
+	 *     @type array        $lang__in          Array of language IDs to include affiliated sites for. Default empty.
+	 *     @type array        $lang__not_in      Array of language IDs to exclude affiliated sites for. Default empty.
 	 *     @type string       $search            Search term(s) to retrieve matching sites for. Default empty.
 	 *     @type array        $search_columns    Array of column names to be searched. Accepts 'domain' and 'path'.
 	 *                                           Default empty array.
@@ -169,6 +173,9 @@
 			'mature'            => null,
 			'spam'              => null,
 			'deleted'           => null,
+			'lang_id'           => null,
+			'lang__in'          => '',
+			'lang__not_in'      => '',
 			'search'            => '',
 			'search_columns'    => array(),
 			'count'             => false,
@@ -471,6 +478,21 @@
 			$this->sql_clauses['where']['public'] = $wpdb->prepare( "public = %d ", $public );
 		}
 
+		if ( is_numeric( $this->query_vars['lang_id'] ) ) {
+			$lang_id = absint( $this->query_vars['lang_id'] );
+			$this->sql_clauses['where']['lang_id'] = $wpdb->prepare( "lang_id = %d ", $lang_id );
+		}
+
+		// Parse site language IDs for an IN clause.
+		if ( ! empty( $this->query_vars['lang__in'] ) ) {
+			$this->sql_clauses['where']['lang__in'] = 'lang_id IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['lang__in'] ) ) . ' )';
+		}
+
+		// Parse site language IDs for a NOT IN clause.
+		if ( ! empty( $this->query_vars['lang__not_in'] ) ) {
+			$this->sql_clauses['where']['lang__not_in'] = 'lang_id NOT IN ( ' . implode( ',', wp_parse_id_list( $this->query_vars['lang__not_in'] ) ) . ' )';
+		}
+
 		// Falsey search strings are ignored.
 		if ( strlen( $this->query_vars['search'] ) ) {
 			$search_columns = array();
Index: src/wp-includes/ms-blogs.php
===================================================================
--- src/wp-includes/ms-blogs.php	(revision 40321)
+++ src/wp-includes/ms-blogs.php	(working copy)
@@ -574,6 +574,7 @@
  * Retrieves a list of sites matching requested arguments.
  *
  * @since 4.6.0
+ * @since 4.8.0 Introduced the 'lang_id', 'lang__in', and 'lang__not_in' parameters.
  *
  * @see WP_Site_Query::parse_query()
  *
@@ -614,6 +615,9 @@
  *     @type int          $mature            Limit results to mature sites. Accepts '1' or '0'. Default empty.
  *     @type int          $spam              Limit results to spam sites. Accepts '1' or '0'. Default empty.
  *     @type int          $deleted           Limit results to deleted sites. Accepts '1' or '0'. Default empty.
+ *     @type int          $lang_id           Limit results to a language ID. Default empty.
+ *     @type array        $lang__in          Array of language IDs to include affiliated sites for. Default empty.
+ *     @type array        $lang__not_in      Array of language IDs to exclude affiliated sites for. Default empty.
  *     @type string       $search            Search term(s) to retrieve matching sites for. Default empty.
  *     @type array        $search_columns    Array of column names to be searched. Accepts 'domain' and 'path'.
  *                                           Default empty array.
Index: tests/phpunit/tests/multisite/siteQuery.php
===================================================================
--- tests/phpunit/tests/multisite/siteQuery.php	(revision 40321)
+++ tests/phpunit/tests/multisite/siteQuery.php	(working copy)
@@ -47,7 +47,7 @@
 			'www.w.org/'                  => array( 'domain' => 'www.w.org',          'path' => '/' ),
 			'www.w.org/foo/'              => array( 'domain' => 'www.w.org',          'path' => '/foo/' ),
 			'www.w.org/foo/bar/'          => array( 'domain' => 'www.w.org',          'path' => '/foo/bar/' ),
-			'www.w.org/make/'             => array( 'domain' => 'www.w.org',          'path' => '/make/' ),
+			'www.w.org/make/'             => array( 'domain' => 'www.w.org',          'path' => '/make/', 'meta' => array( 'public' => 1, 'lang_id' => 1 ) ),
 		);
 
 		foreach ( self::$site_ids as &$id ) {
@@ -431,6 +431,92 @@
 		$this->assertEqualSets( array_values( self::$site_ids ), $found );
 	}
 
+	public function test_wp_site_query_by_lang_id_with_zero() {
+		$q = new WP_Site_Query();
+		$found = $q->query( array(
+			'fields'       => 'ids',
+			// Exclude main site since we don't have control over it here.
+			'site__not_in' => array( 1 ),
+			'lang_id'      => 0,
+		) );
+
+		$this->assertEqualSets( array_diff( array_values( self::$site_ids ), array( self::$site_ids['www.w.org/make/'] ) ), $found );
+	}
+
+	public function test_wp_site_query_by_lang_id() {
+		$q = new WP_Site_Query();
+		$found = $q->query( array(
+			'fields'       => 'ids',
+			'lang_id'      => 1,
+		) );
+
+		$expected = array(
+			self::$site_ids['www.w.org/make/'],
+		);
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_site_query_by_lang_id_with_no_results() {
+		$q = new WP_Site_Query();
+		$found = $q->query( array(
+			'fields'       => 'ids',
+			'lang_id'      => 2,
+		) );
+
+		$this->assertEmpty( $found );
+	}
+
+	public function test_wp_site_query_by_lang__in() {
+		$q = new WP_Site_Query();
+		$found = $q->query( array(
+			'fields' => 'ids',
+			'lang__in' => array( 1 ),
+		) );
+
+		$expected = array(
+			self::$site_ids['www.w.org/make/'],
+		);
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_site_query_by_lang__in_with_multiple_ids() {
+		$q = new WP_Site_Query();
+		$found = $q->query( array(
+			'fields' => 'ids',
+			// Exclude main site since we don't have control over it here.
+			'site__not_in' => array( 1 ),
+			'lang__in' => array( 0, 1 ),
+		) );
+
+		$this->assertEqualSets( array_values( self::$site_ids ), $found );
+	}
+
+	public function test_wp_site_query_by_lang__not_in() {
+		$q = new WP_Site_Query();
+		$found = $q->query( array(
+			'fields' => 'ids',
+			'lang__not_in' => array( 0 ),
+		) );
+
+		$expected = array(
+			self::$site_ids['www.w.org/make/'],
+		);
+
+		$this->assertEqualSets( $expected, $found );
+	}
+
+	public function test_wp_site_query_by_lang__not_in_with_multiple_ids() {
+		$q = new WP_Site_Query();
+		$found = $q->query( array(
+			'fields' => 'ids',
+			'lang__not_in' => array( 0, 1 ),
+		) );
+
+		$this->assertEmpty( $found );
+	}
+
 	public function test_wp_site_query_by_search_with_text_in_domain() {
 		$q = new WP_Site_Query();
 		$found = $q->query( array(
