diff --git a/.divinerconfig b/.divinerconfig
index 6590534..4ea4cf6 100644
--- a/.divinerconfig
+++ b/.divinerconfig
@@ -1,33 +1,34 @@
 {
   "name" : "libphutil",
   "src_link" :
     "https://secure.phabricator.com/diffusion/PHU/browse/master/%f$%l",
   "groups" : {
     "overview" : "Overview",
     "contrib" : "Contributing to libphutil",
     "working" : "Working with libphutil",
     "util" : "Core Utilities",
     "library" : "Phutil Module System",
     "utf8" : "UTF-8",
+    "internationalization" : "Internationalization",
     "filesystem" : "Filesystem",
     "exec" : "Command Execution",
     "futures" : "Futures",
     "channel" : "Channels (I/O Wrappers)",
     "aws" : "Amazon Web Services",
     "error" : "Error Handling",
     "markup" : "Markup",
     "console" : "Console Utilities",
     "aast" : "Abstract Abstract Syntax Tree",
     "xhpast" : "XHPAST (PHP/XHP Parser)",
     "conduit" : "Conduit (Service API)",
     "event" : "Events",
     "daemon" : "Daemons",
     "parser" : "Other Parsers",
     "testcase" : "Test Cases"
   },
   "engines" : [
     ["DivinerArticleEngine", {}],
     ["DivinerXHPEngine", {}]
   ]
 }
 
diff --git a/src/internationalization/PhutilPerson.php b/src/internationalization/PhutilPerson.php
index 2e5d95e..116fc7a 100644
--- a/src/internationalization/PhutilPerson.php
+++ b/src/internationalization/PhutilPerson.php
@@ -1,27 +1,30 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
+/**
+ * @group internationalization
+ */
 interface PhutilPerson {
   const SEX_MALE = 'm';
   const SEX_FEMALE = 'f';
   const SEX_UNKNOWN = '';
 
   function getSex();
   function __toString();
 
 }
diff --git a/src/internationalization/PhutilTranslator.php b/src/internationalization/PhutilTranslator.php
index 0ebfe56..7fe649d 100644
--- a/src/internationalization/PhutilTranslator.php
+++ b/src/internationalization/PhutilTranslator.php
@@ -1,119 +1,122 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
+/**
+ * @group internationalization
+ */
 final class PhutilTranslator {
   static private $instance;
 
   private $language = 'en';
   private $translations = array();
 
   public static function getInstance() {
     if (self::$instance === null) {
       self::$instance = new PhutilTranslator();
     }
     return self::$instance;
   }
 
   public static function setInstance(PhutilTranslator $instance) {
     self::$instance = $instance;
   }
 
   public function setLanguage($language) {
     $this->language = $language;
     return $this;
   }
 
   /**
    * Add translations which will be later used by @{method:translate}.
    * The parameter is an array of strings (for simple translations) or arrays
    * (for translastions with variants). The number of items in the array is
    * language specific. It is `array($singular, $plural)` for English.
    *
    *   array(
    *     'color' => 'colour',
    *     '%d beer(s)' => array('%d beer', '%d beers'),
    *   );
    *
    * The arrays can be nested for strings with more variant parts:
    *
    *   array(
    *     '%d char(s) on %d row(s)' => array(
    *       array('%d char on %d row', '%d char on %d rows'),
    *       array('%d chars on %d row', '%d chars on %d rows'),
    *     ),
    *   );
    *
    * The translation should have the same placeholders as originals. Swapping
    * parameter order is possible:
    *
    *   array(
    *     '%s owns %s.' => '%2$s is owned by %1$s.',
    *   );
    *
    * @param array Identifier in key, translation in value.
    * @return PhutilTranslator Provides fluent interface.
    */
   public function addTranslations(array $translations) {
     $this->translations = array_merge($this->translations, $translations);
     return $this;
   }
 
   public function translate($text /*, ... */) {
     $translation = idx($this->translations, $text, $text);
     $args = func_get_args();
     while (is_array($translation)) {
       $translation = $this->chooseVariant($translation, next($args));
     }
     array_shift($args);
     return vsprintf($translation, $args);
   }
 
   private function chooseVariant(array $translations, $variant) {
     switch ($this->language) {
 
       case 'en':
         list($singular, $plural) = $translations;
         if ($variant == 1) {
           return $singular;
         }
         return $plural;
 
       case 'cs':
         if ($variant instanceof PhutilPerson) {
           list($male, $female) = $translations;
           if ($variant->getSex() == PhutilPerson::SEX_FEMALE) {
             return $female;
           }
           return $male;
         }
 
         list($singular, $paucal, $plural) = $translations;
         if ($variant == 1) {
           return $singular;
         }
         if ($variant >= 2 && $variant <= 4) {
           return $paucal;
         }
         return $plural;
 
       default:
         throw new Exception("Unknown language '{$this->language}'.");
     }
   }
 
 }
diff --git a/src/internationalization/__tests__/PhutilPersonTest.php b/src/internationalization/__tests__/PhutilPersonTest.php
index 98095f9..61c7d5d 100644
--- a/src/internationalization/__tests__/PhutilPersonTest.php
+++ b/src/internationalization/__tests__/PhutilPersonTest.php
@@ -1,35 +1,38 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
+/**
+ * @group testcase
+ */
 final class PhutilPersonTest implements PhutilPerson {
   private $sex = PhutilPerson::SEX_UNKNOWN;
 
   public function getSex() {
     return $this->sex;
   }
 
   public function setSex($value) {
     $this->sex = $value;
     return $this;
   }
 
   public function __toString() {
     return 'Test ('.$this->sex.')';
   }
 
 }
diff --git a/src/internationalization/__tests__/PhutilTranslatorTestCase.php b/src/internationalization/__tests__/PhutilTranslatorTestCase.php
index 3841dbe..7f96244 100644
--- a/src/internationalization/__tests__/PhutilTranslatorTestCase.php
+++ b/src/internationalization/__tests__/PhutilTranslatorTestCase.php
@@ -1,105 +1,108 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
+/**
+ * @group testcase
+ */
 final class PhutilTranslatorTestCase extends ArcanistPhutilTestCase {
 
   public function testEnglish() {
     $translator = new PhutilTranslator();
     $translator->addTranslations(
       array(
         '%d line(s)' => array('%d line', '%d lines'),
         '%d char(s) on %d row(s)' => array(
           array('%d char on %d row', '%d char on %d rows'),
           array('%d chars on %d row', '%d chars on %d rows'),
         ),
       ));
 
     $this->assertEqual('line', $translator->translate('line'));
     $this->assertEqual('param', $translator->translate('%s', 'param'));
 
     $this->assertEqual('0 lines', $translator->translate('%d line(s)', 0));
     $this->assertEqual('1 line', $translator->translate('%d line(s)', 1));
     $this->assertEqual('2 lines', $translator->translate('%d line(s)', 2));
 
     $this->assertEqual(
       '1 char on 1 row',
       $translator->translate('%d char(s) on %d row(s)', 1, 1));
     $this->assertEqual(
       '5 chars on 2 rows',
       $translator->translate('%d char(s) on %d row(s)', 5, 2));
 
     $this->assertEqual('1 beer(s)', $translator->translate('%d beer(s)', 1));
   }
 
   public function testCzech() {
     $translator = new PhutilTranslator();
     $translator->setLanguage('cs');
     $translator->addTranslations(
       array(
         '%d beer(s)' => array('%d pivo', '%d piva', '%d piv'),
       ));
 
     $this->assertEqual('0 piv', $translator->translate('%d beer(s)', 0));
     $this->assertEqual('1 pivo', $translator->translate('%d beer(s)', 1));
     $this->assertEqual('2 piva', $translator->translate('%d beer(s)', 2));
     $this->assertEqual('5 piv', $translator->translate('%d beer(s)', 5));
 
     $this->assertEqual('1 line(s)', $translator->translate('%d line(s)', 1));
   }
 
   public function testPerson() {
     $translator = new PhutilTranslator();
     $translator->setLanguage('cs');
     $translator->addTranslations(
       array(
         '%s wrote.' => array('%s napsal.', '%s napsala.'),
       ));
 
     $person = new PhutilPersonTest();
     $this->assertEqual(
       'Test () napsal.',
       $translator->translate('%s wrote.', $person));
 
     $person->setSex(PhutilPerson::SEX_MALE);
     $this->assertEqual(
       'Test (m) napsal.',
       $translator->translate('%s wrote.', $person));
 
     $person->setSex(PhutilPerson::SEX_FEMALE);
     $this->assertEqual(
       'Test (f) napsala.',
       $translator->translate('%s wrote.', $person));
   }
 
   public function testSetInstance() {
     $original = PhutilTranslator::getInstance();
     $this->assertEqual('color', pht('color'));
 
     $british = new PhutilTranslator();
     $british->addTranslations(
       array(
         'color' => 'colour',
       ));
     PhutilTranslator::setInstance($british);
     $this->assertEqual('colour', pht('color'));
 
     PhutilTranslator::setInstance($original);
     $this->assertEqual('color', pht('color'));
   }
 
 }
diff --git a/src/internationalization/pht.php b/src/internationalization/pht.php
index 5f02348..f04f577 100644
--- a/src/internationalization/pht.php
+++ b/src/internationalization/pht.php
@@ -1,34 +1,36 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 /**
  * Translate a string. It uses a translator set by
  * `PhutilTranslator::setInstance()` or translations specified by
  * `PhutilTranslator::getInstance()->addTranslations()` and language rules set
  * by `PhutilTranslator::getInstance()->setLanguage()`.
  *
  * @param string Translation identifier with sprintf() placeholders.
  * @param mixed Value to select the variant from (e.g. singular or plural).
  * @param ... Next values referenced from $text.
  * @return string Translated string with substituted values.
+ *
+ * @group internationalization
  */
 function pht($text, $variant = null /*, ... */) {
   $args = func_get_args();
   $translator = PhutilTranslator::getInstance();
   return call_user_func_array(array($translator, 'translate'), $args);
 }