Started switching to create_translation_table.
[zzz-pokedex.git] / pokedex / db / tables.py
index 8ea9586..0c2f5d3 100644 (file)
@@ -15,45 +15,77 @@ Columns have a info dictionary with these keys:
   - identifier: A fan-made identifier in the [-_a-z0-9]* format. Not intended
     for translation.
   - latex: A formula in LaTeX syntax.
   - identifier: A fan-made identifier in the [-_a-z0-9]* format. Not intended
     for translation.
   - latex: A formula in LaTeX syntax.
+
+A localizable text column is visible as two properties:
+The plural-name property (e.g. Pokemon.names) is a language-to-name dictionary:
+  bulbasaur.names['en'] == "Bulbasaur" and bulbasaur.names['de'] == "Bisasam".
+  You can use Pokemon.names['en'] to filter a query.
+The singular-name property returns the name in the default language, English.
+  For example bulbasaur.name == "Bulbasaur"
+  Setting pokedex.db.tables.default_lang changes the default language.
 """
 # XXX: Check if "gametext" is set correctly everywhere
 
 """
 # XXX: Check if "gametext" is set correctly everywhere
 
-from sqlalchemy import Column, ForeignKey, MetaData, PrimaryKeyConstraint, Table
-from sqlalchemy.ext.declarative import declarative_base, declared_attr
+import collections
+from functools import partial
+
+from sqlalchemy import Column, ForeignKey, MetaData, PrimaryKeyConstraint, Table, UniqueConstraint
+from sqlalchemy.ext.declarative import (
+        declarative_base, declared_attr, DeclarativeMeta,
+    )
 from sqlalchemy.ext.associationproxy import association_proxy
 from sqlalchemy.ext.associationproxy import association_proxy
-from sqlalchemy.orm import backref, eagerload_all, relation, class_mapper
-from sqlalchemy.orm.session import Session
-from sqlalchemy.orm.collections import attribute_mapped_collection
+from sqlalchemy.orm import (
+        backref, compile_mappers, eagerload_all, relation, class_mapper, synonym, mapper,
+    )
+from sqlalchemy.orm.session import Session, object_session
+from sqlalchemy.orm.interfaces import AttributeExtension
+from sqlalchemy.orm.collections import attribute_mapped_collection, MappedCollection, collection, collection_adapter
+from sqlalchemy.ext.associationproxy import _AssociationDict, association_proxy
 from sqlalchemy.sql import and_
 from sqlalchemy.sql import and_
+from sqlalchemy.sql.expression import ColumnOperators, bindparam
+from sqlalchemy.schema import ColumnDefault
 from sqlalchemy.types import *
 from inspect import isclass
 
 from sqlalchemy.types import *
 from inspect import isclass
 
-from pokedex.db import markdown
+from pokedex.db import markdown, multilang
 
 
-metadata = MetaData()
-TableBase = declarative_base(metadata=metadata)
+# A list of all table classes will live in table_classes
+table_classes = []
 
 
-### Helper classes
-class Named(object):
-    """Mixin for objects that have names"""
+class TableMetaclass(DeclarativeMeta):
+    def __init__(cls, name, bases, attrs):
+        super(TableMetaclass, cls).__init__(name, bases, attrs)
+        if hasattr(cls, '__tablename__'):
+            table_classes.append(cls)
+
+class TableSuperclass(object):
+    """Superclass for declarative tables, to give them some generic niceties
+    like stringification.
+    """
     def __unicode__(self):
     def __unicode__(self):
+        """Be as useful as possible.  Show the primary key, and an identifier
+        if we've got one.
+        """
+        typename = u'.'.join((__name__, type(self).__name__))
+
+        pk_constraint = self.__table__.primary_key
+        if not pk_constraint:
+            return u"<%s object at %x>" % (typename, id(self))
+
+        pk = u', '.join(unicode(getattr(self, column.name))
+            for column in pk_constraint.columns)
         try:
         try:
-            return '<%s: %s>' % (type(self).__name__, self.identifier)
+            return u"<%s object (%s): %s>" % (typename, pk, self.identifier)
         except AttributeError:
         except AttributeError:
-            return '<%s>' % type(self).__name__
+            return u"<%s object (%s)>" % (typename, pk)
 
     def __str__(self):
 
     def __str__(self):
-        return unicode(self).encode('utf-8')
-
-    def __repr__(self):
-        return str(self)
+        return unicode(self).encode('utf8')
 
 
-class OfficiallyNamed(Named):
-    """Mixin for stuff with official names"""
-
-class UnofficiallyNamed(Named):
-    """Mixin for stuff with unofficial names"""
+metadata = MetaData()
+TableBase = declarative_base(metadata=metadata, cls=TableSuperclass, metaclass=TableMetaclass)
 
 
+### Helper classes
 class LanguageSpecific(object):
     """Mixin for prose and text tables"""
     @declared_attr
 class LanguageSpecific(object):
     """Mixin for prose and text tables"""
     @declared_attr
@@ -63,10 +95,13 @@ class LanguageSpecific(object):
 
 class LanguageSpecificColumn(object):
     """A column that will not appear in the table it's defined in, but in a related one"""
 
 class LanguageSpecificColumn(object):
     """A column that will not appear in the table it's defined in, but in a related one"""
+    _ordering = [1]
     def __init__(self, *args, **kwargs):
         self.args = args
         self.plural = kwargs.pop('plural')
         self.kwargs = kwargs
     def __init__(self, *args, **kwargs):
         self.args = args
         self.plural = kwargs.pop('plural')
         self.kwargs = kwargs
+        self.order = self._ordering[0]
+        self._ordering[0] += 1
 
     def makeSAColumn(self):
         return Column(*self.args, **self.kwargs)
 
     def makeSAColumn(self):
         return Column(*self.args, **self.kwargs)
@@ -78,9 +113,33 @@ class TextColumn(LanguageSpecificColumn):
     """A column that will appear in the corresponding _text table"""
 
 
     """A column that will appear in the corresponding _text table"""
 
 
+### Need Language first, to create the partial() below
+
+class Language(TableBase):
+    u"""A language the Pokémon games have been transleted into
+    """
+    __tablename__ = 'languages'
+    __singlename__ = 'language'
+    id = Column(Integer, primary_key=True, nullable=False,
+        info=dict(description="A numeric ID"))
+    iso639 = Column(Unicode(2), nullable=False,
+        info=dict(description="The two-letter code of the country where this language is spoken. Note that it is not unique.", format='identifier'))
+    iso3166 = Column(Unicode(2), nullable=False,
+        info=dict(description="The two-letter code of the language. Note that it is not unique.", format='identifier'))
+    identifier = Column(Unicode(16), nullable=False,
+        info=dict(description="An identifier", format='identifier'))
+    official = Column(Boolean, nullable=False, index=True,
+        info=dict(description=u"True iff games are produced in the language."))
+    order = Column(Integer, nullable=True,
+        info=dict(description=u"Order for sorting in foreign name lists."))
+    name = TextColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
+
+create_translation_table = partial(multilang.create_translation_table, language_class=Language)
+
 ### The actual tables
 
 ### The actual tables
 
-class Ability(TableBase, OfficiallyNamed):
+class Ability(TableBase):
     u"""An ability a Pokémon can have, such as Static or Pressure.
     """
     __tablename__ = 'abilities'
     u"""An ability a Pokémon can have, such as Static or Pressure.
     """
     __tablename__ = 'abilities'
@@ -91,10 +150,17 @@ class Ability(TableBase, OfficiallyNamed):
         info=dict(description="An identifier", format='identifier'))
     generation_id = Column(Integer, ForeignKey('generations.id'), nullable=False,
         info=dict(description="The ID of the generation this ability was introduced in", detail=True))
         info=dict(description="An identifier", format='identifier'))
     generation_id = Column(Integer, ForeignKey('generations.id'), nullable=False,
         info=dict(description="The ID of the generation this ability was introduced in", detail=True))
-    effect = ProseColumn(markdown.MarkdownColumn(5120), plural='effects', nullable=False,
-        info=dict(description="A detailed description of this ability's effect", format='markdown'))
-    short_effect = ProseColumn(markdown.MarkdownColumn(255), plural='short_effects', nullable=False,
-        info=dict(description="A short summary of this ability's effect", format='markdown'))
+
+create_translation_table('ability_texts', Ability, 'names',
+    name = Column(Unicode(24), nullable=False, index=True,
+        info=dict(description="The name", format='plaintext', official=True)),
+)
+create_translation_table('ability_prose', Ability, 'prose',
+    effect = Column(markdown.MarkdownColumn(5120), nullable=False,
+        info=dict(description="A detailed description of this ability's effect", format='markdown')),
+    short_effect = Column(markdown.MarkdownColumn(255), nullable=False,
+        info=dict(description="A short summary of this ability's effect", format='markdown')),
+)
 
 class AbilityChangelog(TableBase):
     """History of changes to abilities across main game versions."""
 
 class AbilityChangelog(TableBase):
     """History of changes to abilities across main game versions."""
@@ -147,7 +213,7 @@ class Berry(TableBase):
     smoothness = Column(Integer, nullable=False,
         info=dict(description="The smoothness of this Berry, used in making Pokéblocks or Poffins"))
 
     smoothness = Column(Integer, nullable=False,
         info=dict(description="The smoothness of this Berry, used in making Pokéblocks or Poffins"))
 
-class BerryFirmness(TableBase, OfficiallyNamed):
+class BerryFirmness(TableBase):
     u"""A Berry firmness, such as "hard" or "very soft".
     """
     __tablename__ = 'berry_firmness'
     u"""A Berry firmness, such as "hard" or "very soft".
     """
     __tablename__ = 'berry_firmness'
@@ -156,6 +222,8 @@ class BerryFirmness(TableBase, OfficiallyNamed):
         info=dict(description="A unique ID for this firmness"))
     identifier = Column(Unicode(10), nullable=False,
         info=dict(description="An identifier", format='identifier'))
         info=dict(description="A unique ID for this firmness"))
     identifier = Column(Unicode(10), nullable=False,
         info=dict(description="An identifier", format='identifier'))
+    name = TextColumn(Unicode(10), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 class BerryFlavor(TableBase):
     u"""A Berry flavor level.
 
 class BerryFlavor(TableBase):
     u"""A Berry flavor level.
@@ -193,7 +261,7 @@ class ContestEffect(TableBase):
     effect = ProseColumn(Unicode(255), plural='effects', nullable=False,
         info=dict(description="A detailed description of the effect", format='plaintext'))
 
     effect = ProseColumn(Unicode(255), plural='effects', nullable=False,
         info=dict(description="A detailed description of the effect", format='plaintext'))
 
-class ContestType(TableBase, OfficiallyNamed):
+class ContestType(TableBase):
     u"""A Contest type, such as "cool" or "smart", and their associated Berry flavors and Pokéblock colors.
     """
     __tablename__ = 'contest_types'
     u"""A Contest type, such as "cool" or "smart", and their associated Berry flavors and Pokéblock colors.
     """
     __tablename__ = 'contest_types'
@@ -206,8 +274,10 @@ class ContestType(TableBase, OfficiallyNamed):
         info=dict(description="The name of the corresponding Berry flavor", official=True, format='plaintext'))
     color = TextColumn(Unicode(6), nullable=False, plural='colors',
         info=dict(description=u"The name of the corresponding Pokéblock color", official=True, format='plaintext'))
         info=dict(description="The name of the corresponding Berry flavor", official=True, format='plaintext'))
     color = TextColumn(Unicode(6), nullable=False, plural='colors',
         info=dict(description=u"The name of the corresponding Pokéblock color", official=True, format='plaintext'))
+    name = TextColumn(Unicode(6), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 
-class EggGroup(TableBase, UnofficiallyNamed):
+class EggGroup(TableBase):
     u"""An Egg group. Usually, two Pokémon can breed if they share an Egg Group.
 
     (exceptions are the Ditto and No Eggs groups)
     u"""An Egg group. Usually, two Pokémon can breed if they share an Egg Group.
 
     (exceptions are the Ditto and No Eggs groups)
@@ -218,6 +288,8 @@ class EggGroup(TableBase, UnofficiallyNamed):
         info=dict(description="A unique ID for this group"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description=u"An identifier.", format='identifier'))
         info=dict(description="A unique ID for this group"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description=u"An identifier.", format='identifier'))
+    name = ProseColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 class Encounter(TableBase):
     u"""Encounters with wild Pokémon.
 
 class Encounter(TableBase):
     u"""Encounters with wild Pokémon.
@@ -259,7 +331,7 @@ class Encounter(TableBase):
     max_level = Column(Integer, nullable=False, autoincrement=False,
         info=dict(description=u"The maxmum level of the encountered Pokémon"))
 
     max_level = Column(Integer, nullable=False, autoincrement=False,
         info=dict(description=u"The maxmum level of the encountered Pokémon"))
 
-class EncounterCondition(TableBase, UnofficiallyNamed):
+class EncounterCondition(TableBase):
     u"""A conditions in the game world that affects Pokémon encounters, such as time of day.
     """
 
     u"""A conditions in the game world that affects Pokémon encounters, such as time of day.
     """
 
@@ -269,8 +341,10 @@ class EncounterCondition(TableBase, UnofficiallyNamed):
         info=dict(description="A unique ID for this condition"))
     identifier = Column(Unicode(64), nullable=False,
         info=dict(description="An identifier", format='identifier'))
         info=dict(description="A unique ID for this condition"))
     identifier = Column(Unicode(64), nullable=False,
         info=dict(description="An identifier", format='identifier'))
+    name = ProseColumn(Unicode(64), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 
-class EncounterConditionValue(TableBase, UnofficiallyNamed):
+class EncounterConditionValue(TableBase):
     u"""A possible state for a condition; for example, the state of 'swarm' could be 'swarm' or 'no swarm'.
     """
 
     u"""A possible state for a condition; for example, the state of 'swarm' could be 'swarm' or 'no swarm'.
     """
 
@@ -284,6 +358,8 @@ class EncounterConditionValue(TableBase, UnofficiallyNamed):
         info=dict(description="An identifier", format='identifier'))
     is_default = Column(Boolean, nullable=False,
         info=dict(description='Set if this value is the default state for the condition'))
         info=dict(description="An identifier", format='identifier'))
     is_default = Column(Boolean, nullable=False,
         info=dict(description='Set if this value is the default state for the condition'))
+    name = ProseColumn(Unicode(64), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 class EncounterConditionValueMap(TableBase):
     u"""Maps encounters to the specific conditions under which they occur.
 
 class EncounterConditionValueMap(TableBase):
     u"""Maps encounters to the specific conditions under which they occur.
@@ -294,7 +370,7 @@ class EncounterConditionValueMap(TableBase):
     encounter_condition_value_id = Column(Integer, ForeignKey('encounter_condition_values.id'), primary_key=True, nullable=False, autoincrement=False,
         info=dict(description="The ID of the encounter condition value"))
 
     encounter_condition_value_id = Column(Integer, ForeignKey('encounter_condition_values.id'), primary_key=True, nullable=False, autoincrement=False,
         info=dict(description="The ID of the encounter condition value"))
 
-class EncounterTerrain(TableBase, UnofficiallyNamed):
+class EncounterTerrain(TableBase):
     u"""A way the player can enter a wild encounter, e.g., surfing, fishing, or walking through tall grass.
     """
 
     u"""A way the player can enter a wild encounter, e.g., surfing, fishing, or walking through tall grass.
     """
 
@@ -304,6 +380,8 @@ class EncounterTerrain(TableBase, UnofficiallyNamed):
         info=dict(description="A unique ID for the terrain"))
     identifier = Column(Unicode(64), nullable=False,
         info=dict(description="An identifier", format='identifier'))
         info=dict(description="A unique ID for the terrain"))
     identifier = Column(Unicode(64), nullable=False,
         info=dict(description="An identifier", format='identifier'))
+    name = ProseColumn(Unicode(64), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 class EncounterSlot(TableBase):
     u"""An abstract "slot" within a terrain, associated with both some set of conditions and a rarity.
 
 class EncounterSlot(TableBase):
     u"""An abstract "slot" within a terrain, associated with both some set of conditions and a rarity.
@@ -335,7 +413,7 @@ class EvolutionChain(TableBase):
     baby_trigger_item_id = Column(Integer, ForeignKey('items.id'), nullable=True,
         info=dict(description="Item that a parent must hold while breeding to produce a baby"))
 
     baby_trigger_item_id = Column(Integer, ForeignKey('items.id'), nullable=True,
         info=dict(description="Item that a parent must hold while breeding to produce a baby"))
 
-class EvolutionTrigger(TableBase, UnofficiallyNamed):
+class EvolutionTrigger(TableBase):
     u"""An evolution type, such as "level" or "trade".
     """
     __tablename__ = 'evolution_triggers'
     u"""An evolution type, such as "level" or "trade".
     """
     __tablename__ = 'evolution_triggers'
@@ -344,6 +422,8 @@ class EvolutionTrigger(TableBase, UnofficiallyNamed):
         info=dict(description="A numeric ID"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description="An identifier", format='identifier'))
         info=dict(description="A numeric ID"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description="An identifier", format='identifier'))
+    name = ProseColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 class Experience(TableBase):
     u"""EXP needed for a certain level with a certain growth rate
 
 class Experience(TableBase):
     u"""EXP needed for a certain level with a certain growth rate
@@ -356,7 +436,7 @@ class Experience(TableBase):
     experience = Column(Integer, nullable=False,
         info=dict(description="The number of EXP points needed to get to that level"))
 
     experience = Column(Integer, nullable=False,
         info=dict(description="The number of EXP points needed to get to that level"))
 
-class Generation(TableBase, OfficiallyNamed):
+class Generation(TableBase):
     u"""A Generation of the Pokémon franchise
     """
     __tablename__ = 'generations'
     u"""A Generation of the Pokémon franchise
     """
     __tablename__ = 'generations'
@@ -369,8 +449,10 @@ class Generation(TableBase, OfficiallyNamed):
         info=dict(description=u"ID of the Pokédex this generation's main games use by default"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description=u'An identifier', format='identifier'))
         info=dict(description=u"ID of the Pokédex this generation's main games use by default"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description=u'An identifier', format='identifier'))
+    name = TextColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 
-class GrowthRate(TableBase, UnofficiallyNamed):
+class GrowthRate(TableBase):
     u"""Growth rate of a Pokémon, i.e. the EXP → level function.
     """
     __tablename__ = 'growth_rates'
     u"""Growth rate of a Pokémon, i.e. the EXP → level function.
     """
     __tablename__ = 'growth_rates'
@@ -381,8 +463,10 @@ class GrowthRate(TableBase, UnofficiallyNamed):
         info=dict(description="An identifier", format='identifier'))
     formula = Column(Unicode(500), nullable=False,
         info=dict(description="The formula", format='latex'))
         info=dict(description="An identifier", format='identifier'))
     formula = Column(Unicode(500), nullable=False,
         info=dict(description="The formula", format='latex'))
+    name = ProseColumn(Unicode(20), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 
-class Item(TableBase, OfficiallyNamed):
+class Item(TableBase):
     u"""An Item from the games, like "Poké Ball" or "Bicycle".
     """
     __tablename__ = 'items'
     u"""An Item from the games, like "Poké Ball" or "Bicycle".
     """
     __tablename__ = 'items'
@@ -403,6 +487,8 @@ class Item(TableBase, OfficiallyNamed):
         info=dict(description="A short summary of the effect", format='plaintext'))
     effect = ProseColumn(markdown.MarkdownColumn(5120), plural='effects', nullable=False,
         info=dict(description=u"Detailed description of the item's effect.", format='markdown'))
         info=dict(description="A short summary of the effect", format='plaintext'))
     effect = ProseColumn(markdown.MarkdownColumn(5120), plural='effects', nullable=False,
         info=dict(description=u"Detailed description of the item's effect.", format='markdown'))
+    name = TextColumn(Unicode(20), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
     @property
     def appears_underground(self):
 
     @property
     def appears_underground(self):
@@ -410,7 +496,7 @@ class Item(TableBase, OfficiallyNamed):
         """
         return any(flag.identifier == u'underground' for flag in self.flags)
 
         """
         return any(flag.identifier == u'underground' for flag in self.flags)
 
-class ItemCategory(TableBase, UnofficiallyNamed):
+class ItemCategory(TableBase):
     u"""An item category
     """
     # XXX: This is fanon, right?
     u"""An item category
     """
     # XXX: This is fanon, right?
@@ -422,8 +508,10 @@ class ItemCategory(TableBase, UnofficiallyNamed):
         info=dict(description="ID of the pocket these items go to"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description="An identifier", format='identifier'))
         info=dict(description="ID of the pocket these items go to"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description="An identifier", format='identifier'))
+    name = ProseColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 
-class ItemFlag(TableBase, UnofficiallyNamed):
+class ItemFlag(TableBase):
     u"""An item attribute such as "consumable" or "holdable".
     """
     __tablename__ = 'item_flags'
     u"""An item attribute such as "consumable" or "holdable".
     """
     __tablename__ = 'item_flags'
@@ -434,6 +522,8 @@ class ItemFlag(TableBase, UnofficiallyNamed):
         info=dict(description="Identifier of the flag", format='identifier'))
     description = ProseColumn(Unicode(64), plural='descriptions', nullable=False,
         info=dict(description="Short description of the flag", format='plaintext'))
         info=dict(description="Identifier of the flag", format='identifier'))
     description = ProseColumn(Unicode(64), plural='descriptions', nullable=False,
         info=dict(description="Short description of the flag", format='plaintext'))
+    name = ProseColumn(Unicode(24), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 class ItemFlagMap(TableBase):
     u"""Maps an item flag to its item.
 
 class ItemFlagMap(TableBase):
     u"""Maps an item flag to its item.
@@ -477,7 +567,7 @@ class ItemInternalID(TableBase):
     internal_id = Column(Integer, nullable=False,
         info=dict(description="Internal ID of the item in the generation"))
 
     internal_id = Column(Integer, nullable=False,
         info=dict(description="Internal ID of the item in the generation"))
 
-class ItemPocket(TableBase, OfficiallyNamed):
+class ItemPocket(TableBase):
     u"""A pocket that categorizes items
     """
     __tablename__ = 'item_pockets'
     u"""A pocket that categorizes items
     """
     __tablename__ = 'item_pockets'
@@ -486,44 +576,10 @@ class ItemPocket(TableBase, OfficiallyNamed):
         info=dict(description="A numeric ID"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description="An identifier of this pocket", format='identifier'))
         info=dict(description="A numeric ID"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description="An identifier of this pocket", format='identifier'))
+    name = TextColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 
-class Language(TableBase, OfficiallyNamed):
-    u"""A language the Pokémon games have been transleted into
-    """
-    __tablename__ = 'languages'
-    __singlename__ = 'language'
-    id = Column(Integer, primary_key=True, nullable=False,
-        info=dict(description="A numeric ID"))
-    iso639 = Column(Unicode(2), nullable=False,
-        info=dict(description="The two-letter code of the country where this language is spoken. Note that it is not unique.", format='identifier'))
-    iso3166 = Column(Unicode(2), nullable=False,
-        info=dict(description="The two-letter code of the language. Note that it is not unique.", format='identifier'))
-    identifier = Column(Unicode(16), nullable=False,
-        info=dict(description="An identifier", format='identifier'))
-    official = Column(Boolean, nullable=False, index=True,
-        info=dict(description=u"True iff games are produced in the language."))
-    order = Column(Integer, nullable=True,
-        info=dict(description=u"Order for sorting in foreign name lists."))
-
-    # Languages compare equal to its identifier, so a dictionary of
-    # translations, with a Language as the key, can be indexed by the identifier
-    def __eq__(self, other):
-        try:
-            return (
-                    self is other or
-                    self.identifier == other or
-                    self.identifier == other.identifier
-                )
-        except AttributeError:
-            return NotImplemented
-
-    def __ne__(self, other):
-        return not (self == other)
-
-    def __hash__(self):
-        return hash(self.identifier)
-
-class Location(TableBase, OfficiallyNamed):
+class Location(TableBase):
     u"""A place in the Pokémon world
     """
     __tablename__ = 'locations'
     u"""A place in the Pokémon world
     """
     __tablename__ = 'locations'
@@ -534,8 +590,10 @@ class Location(TableBase, OfficiallyNamed):
         info=dict(description="ID of the region this location is in"))
     identifier = Column(Unicode(64), nullable=False,
         info=dict(description="An identifier", format='identifier'))
         info=dict(description="ID of the region this location is in"))
     identifier = Column(Unicode(64), nullable=False,
         info=dict(description="An identifier", format='identifier'))
+    name = TextColumn(Unicode(64), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 
-class LocationArea(TableBase, UnofficiallyNamed):
+class LocationArea(TableBase):
     u"""A sub-area of a location
     """
     __tablename__ = 'location_areas'
     u"""A sub-area of a location
     """
     __tablename__ = 'location_areas'
@@ -548,6 +606,8 @@ class LocationArea(TableBase, UnofficiallyNamed):
         info=dict(description="ID the games ude for this area"))
     identifier = Column(Unicode(64), nullable=True,
         info=dict(description="An identifier", format='identifier'))
         info=dict(description="ID the games ude for this area"))
     identifier = Column(Unicode(64), nullable=True,
         info=dict(description="An identifier", format='identifier'))
+    name = ProseColumn(Unicode(64), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 class LocationAreaEncounterRate(TableBase):
     # XXX: What's this exactly? Someone add the docstring & revise the descriptions
 
 class LocationAreaEncounterRate(TableBase):
     # XXX: What's this exactly? Someone add the docstring & revise the descriptions
@@ -591,7 +651,7 @@ class Machine(TableBase):
         """
         return self.machine_number >= 100
 
         """
         return self.machine_number >= 100
 
-class MoveBattleStyle(TableBase, UnofficiallyNamed):
+class MoveBattleStyle(TableBase):
     u"""A battle style of a move"""  # XXX: Explain better
     __tablename__ = 'move_battle_styles'
     __singlename__ = 'move_battle_style'
     u"""A battle style of a move"""  # XXX: Explain better
     __tablename__ = 'move_battle_styles'
     __singlename__ = 'move_battle_style'
@@ -599,8 +659,10 @@ class MoveBattleStyle(TableBase, UnofficiallyNamed):
         info=dict(description="A numeric ID"))
     identifier = Column(Unicode(8), nullable=False,
         info=dict(description="An identifier", format='identifier'))
         info=dict(description="A numeric ID"))
     identifier = Column(Unicode(8), nullable=False,
         info=dict(description="An identifier", format='identifier'))
+    name = ProseColumn(Unicode(8), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 
-class MoveEffectCategory(TableBase, UnofficiallyNamed):
+class MoveEffectCategory(TableBase):
     u"""Category of a move effect
     """
     __tablename__ = 'move_effect_categories'
     u"""Category of a move effect
     """
     __tablename__ = 'move_effect_categories'
@@ -611,6 +673,8 @@ class MoveEffectCategory(TableBase, UnofficiallyNamed):
         info=dict(description="An identifier", format='identifier'))
     can_affect_user = Column(Boolean, nullable=False,
         info=dict(description="Set if the user can be affected"))
         info=dict(description="An identifier", format='identifier'))
     can_affect_user = Column(Boolean, nullable=False,
         info=dict(description="Set if the user can be affected"))
+    name = ProseColumn(Unicode(64), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 class MoveEffectCategoryMap(TableBase):
     u"""Maps a move effect category to a move effect
 
 class MoveEffectCategoryMap(TableBase):
     u"""Maps a move effect category to a move effect
@@ -623,7 +687,7 @@ class MoveEffectCategoryMap(TableBase):
     affects_user = Column(Boolean, primary_key=True, nullable=False,
         info=dict(description="Set if the user is affected"))
 
     affects_user = Column(Boolean, primary_key=True, nullable=False,
         info=dict(description="Set if the user is affected"))
 
-class MoveDamageClass(TableBase, UnofficiallyNamed):
+class MoveDamageClass(TableBase):
     u"""Any of the damage classes moves can have, i.e. physical, special, or non-damaging.
     """
     __tablename__ = 'move_damage_classes'
     u"""Any of the damage classes moves can have, i.e. physical, special, or non-damaging.
     """
     __tablename__ = 'move_damage_classes'
@@ -634,6 +698,8 @@ class MoveDamageClass(TableBase, UnofficiallyNamed):
         info=dict(description="An identifier", format='identifier'))
     description = ProseColumn(Unicode(64), plural='descriptions', nullable=False,
         info=dict(description="A description of the class", format='plaintext'))
         info=dict(description="An identifier", format='identifier'))
     description = ProseColumn(Unicode(64), plural='descriptions', nullable=False,
         info=dict(description="A description of the class", format='plaintext'))
+    name = ProseColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 class MoveEffect(TableBase):
     u"""An effect of a move
 
 class MoveEffect(TableBase):
     u"""An effect of a move
@@ -653,13 +719,18 @@ class MoveEffectChangelog(TableBase):
     __singlename__ = 'move_effect_changelog'
     id = Column(Integer, primary_key=True, nullable=False,
         info=dict(description="A numeric ID"))
     __singlename__ = 'move_effect_changelog'
     id = Column(Integer, primary_key=True, nullable=False,
         info=dict(description="A numeric ID"))
-    effect_id = Column(Integer, ForeignKey('move_effects.id'), primary_key=True, nullable=False,
+    effect_id = Column(Integer, ForeignKey('move_effects.id'), nullable=False,
         info=dict(description="The ID of the effect that changed"))
         info=dict(description="The ID of the effect that changed"))
-    changed_in_version_group_id = Column(Integer, ForeignKey('version_groups.id'), primary_key=True, nullable=False,
+    changed_in_version_group_id = Column(Integer, ForeignKey('version_groups.id'), nullable=False,
         info=dict(description="The ID of the version group in which the effect changed"))
     effect = ProseColumn(markdown.MarkdownColumn(512), plural='effects', nullable=False,
         info=dict(description="A description of the old behavior", format='markdown'))
 
         info=dict(description="The ID of the version group in which the effect changed"))
     effect = ProseColumn(markdown.MarkdownColumn(512), plural='effects', nullable=False,
         info=dict(description="A description of the old behavior", format='markdown'))
 
+    __table_args__ = (
+        UniqueConstraint(effect_id, changed_in_version_group_id),
+        {},
+    )
+
 class MoveFlag(TableBase):
     u"""Maps a move flag to a move
     """
 class MoveFlag(TableBase):
     u"""Maps a move flag to a move
     """
@@ -671,7 +742,7 @@ class MoveFlag(TableBase):
     move_flag_type_id = Column(Integer, ForeignKey('move_flag_types.id'), primary_key=True, nullable=False, autoincrement=False,
         info=dict(description="ID of the flag"))
 
     move_flag_type_id = Column(Integer, ForeignKey('move_flag_types.id'), primary_key=True, nullable=False, autoincrement=False,
         info=dict(description="ID of the flag"))
 
-class MoveFlagType(TableBase, UnofficiallyNamed):
+class MoveFlagType(TableBase):
     u"""A Move attribute such as "snatchable" or "contact".
     """
     __tablename__ = 'move_flag_types'
     u"""A Move attribute such as "snatchable" or "contact".
     """
     __tablename__ = 'move_flag_types'
@@ -682,6 +753,8 @@ class MoveFlagType(TableBase, UnofficiallyNamed):
         info=dict(description="A short identifier for the flag", format='identifier'))
     description = ProseColumn(markdown.MarkdownColumn(128), plural='descriptions', nullable=False,
         info=dict(description="A short description of the flag", format='markdown'))
         info=dict(description="A short identifier for the flag", format='identifier'))
     description = ProseColumn(markdown.MarkdownColumn(128), plural='descriptions', nullable=False,
         info=dict(description="A short description of the flag", format='markdown'))
+    name = ProseColumn(Unicode(32), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 class MoveFlavorText(TableBase, LanguageSpecific):
     u"""In-game description of a move
 
 class MoveFlavorText(TableBase, LanguageSpecific):
     u"""In-game description of a move
@@ -697,44 +770,66 @@ class MoveFlavorText(TableBase, LanguageSpecific):
 class MoveMeta(TableBase):
     u"""Metadata for move effects, sorta-kinda ripped straight from the game"""
     __tablename__ = 'move_meta'
 class MoveMeta(TableBase):
     u"""Metadata for move effects, sorta-kinda ripped straight from the game"""
     __tablename__ = 'move_meta'
-    move_id = Column(Integer, ForeignKey('moves.id'), primary_key=True, nullable=False, autoincrement=False)
-    meta_category_id = Column(Integer, ForeignKey('move_meta_categories.id'), nullable=False)
-    meta_ailment_id = Column(Integer, ForeignKey('move_meta_ailments.id'), nullable=False)
-    min_hits = Column(Integer, nullable=True, index=True)
-    max_hits = Column(Integer, nullable=True, index=True)
-    min_turns = Column(Integer, nullable=True, index=True)
-    max_turns = Column(Integer, nullable=True, index=True)
-    recoil = Column(Integer, nullable=False, index=True)
-    healing = Column(Integer, nullable=False, index=True)
-    crit_rate = Column(Integer, nullable=False, index=True)
-    ailment_chance = Column(Integer, nullable=False, index=True)
-    flinch_chance = Column(Integer, nullable=False, index=True)
-    stat_chance = Column(Integer, nullable=False, index=True)
-
-class MoveMetaAilment(TableBase, OfficiallyNamed):
+    move_id = Column(Integer, ForeignKey('moves.id'), primary_key=True, nullable=False, autoincrement=False,
+        info=dict(description="A numeric ID"))
+    meta_category_id = Column(Integer, ForeignKey('move_meta_categories.id'), nullable=False,
+        info=dict(description="ID of the move category"))
+    meta_ailment_id = Column(Integer, ForeignKey('move_meta_ailments.id'), nullable=False,
+        info=dict(description="ID of the caused ailment"))
+    min_hits = Column(Integer, nullable=True, index=True,
+        info=dict(description="Minimum number of hits per use"))
+    max_hits = Column(Integer, nullable=True, index=True,
+        info=dict(description="Maximum number of hits per use"))
+    min_turns = Column(Integer, nullable=True, index=True,
+        info=dict(description="Minimum number of turns the user is forced to use the move"))
+    max_turns = Column(Integer, nullable=True, index=True,
+        info=dict(description="Maximum number of turns the user is forced to use the move"))
+    recoil = Column(Integer, nullable=False, index=True,
+        info=dict(description="Recoil damage, in percent of damage done"))
+    healing = Column(Integer, nullable=False, index=True,
+        info=dict(description="Healing, in percent of user's max HP"))
+    crit_rate = Column(Integer, nullable=False, index=True,
+        info=dict(description="Critical hit rate bonus"))
+    ailment_chance = Column(Integer, nullable=False, index=True,
+        info=dict(description="Chance to cause an ailment, in percent"))
+    flinch_chance = Column(Integer, nullable=False, index=True,
+        info=dict(description="Chance to cause flinching, in percent"))
+    stat_chance = Column(Integer, nullable=False, index=True,
+        info=dict(description="Chance to cause a stat change, in percent"))
+
+class MoveMetaAilment(TableBase):
     u"""Common status ailments moves can inflict on a single Pokémon, including
     major ailments like paralysis and minor ailments like trapping.
     """
     __tablename__ = 'move_meta_ailments'
     __singlename__ = 'move_meta_ailment'
     u"""Common status ailments moves can inflict on a single Pokémon, including
     major ailments like paralysis and minor ailments like trapping.
     """
     __tablename__ = 'move_meta_ailments'
     __singlename__ = 'move_meta_ailment'
-    id = Column(Integer, primary_key=True, nullable=False)
-    identifier = Column(Unicode(24), nullable=False)
+    id = Column(Integer, primary_key=True, nullable=False,
+        info=dict(description="A numeric ID"))
+    identifier = Column(Unicode(24), nullable=False,
+        info=dict(description="An identifier", format='identifier'))
+    name = TextColumn(Unicode(24), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 class MoveMetaCategory(TableBase):
     u"""Very general categories that loosely group move effects."""
     __tablename__ = 'move_meta_categories'
     __singlename__ = 'move_meta_category'
 
 class MoveMetaCategory(TableBase):
     u"""Very general categories that loosely group move effects."""
     __tablename__ = 'move_meta_categories'
     __singlename__ = 'move_meta_category'
-    id = Column(Integer, primary_key=True, nullable=False)
-    description = ProseColumn(Unicode(64), plural='descriptions', nullable=False)
+    id = Column(Integer, primary_key=True, nullable=False,
+        info=dict(description="A numeric ID"))
+    description = ProseColumn(Unicode(64), plural='descriptions', nullable=False,
+        info=dict(description="A description of the category"))
 
 class MoveMetaStatChange(TableBase):
     u"""Stat changes moves (may) make."""
     __tablename__ = 'move_meta_stat_changes'
 
 class MoveMetaStatChange(TableBase):
     u"""Stat changes moves (may) make."""
     __tablename__ = 'move_meta_stat_changes'
-    move_id = Column(Integer, ForeignKey('moves.id'), primary_key=True, nullable=False, autoincrement=False)
-    stat_id = Column(Integer, ForeignKey('stats.id'), primary_key=True, nullable=False, autoincrement=False)
-    change = Column(Integer, nullable=False, index=True)
+    move_id = Column(Integer, ForeignKey('moves.id'), primary_key=True, nullable=False, autoincrement=False,
+        info=dict(description="ID of the move"))
+    stat_id = Column(Integer, ForeignKey('stats.id'), primary_key=True, nullable=False, autoincrement=False,
+        info=dict(description="ID of the stat"))
+    change = Column(Integer, nullable=False, index=True,
+        info=dict(description="Amount of increase/decrease, in stages"))
 
 
-class MoveTarget(TableBase, UnofficiallyNamed):
+class MoveTarget(TableBase):
     u"""Targetting or "range" of a move, e.g. "Affects all opponents" or "Affects user".
     """
     __tablename__ = 'move_targets'
     u"""Targetting or "range" of a move, e.g. "Affects all opponents" or "Affects user".
     """
     __tablename__ = 'move_targets'
@@ -745,8 +840,10 @@ class MoveTarget(TableBase, UnofficiallyNamed):
         info=dict(description="An identifier", format='identifier'))
     description = ProseColumn(Unicode(128), plural='descriptions', nullable=False,
         info=dict(description="A description", format='plaintext'))
         info=dict(description="An identifier", format='identifier'))
     description = ProseColumn(Unicode(128), plural='descriptions', nullable=False,
         info=dict(description="A description", format='plaintext'))
+    name = ProseColumn(Unicode(32), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 
-class Move(TableBase, OfficiallyNamed):
+class Move(TableBase):
     u"""A Move: technique or attack a Pokémon can learn to use
     """
     __tablename__ = 'moves'
     u"""A Move: technique or attack a Pokémon can learn to use
     """
     __tablename__ = 'moves'
@@ -782,6 +879,12 @@ class Move(TableBase, OfficiallyNamed):
     super_contest_effect_id = Column(Integer, ForeignKey('super_contest_effects.id'), nullable=True,
         info=dict(description="ID of the move's Super Contest effect"))
 
     super_contest_effect_id = Column(Integer, ForeignKey('super_contest_effects.id'), nullable=True,
         info=dict(description="ID of the move's Super Contest effect"))
 
+create_translation_table('move_texts', Move, 'names',
+    name = Column(Unicode(24), nullable=False, index=True,
+        info=dict(description="The name", format='plaintext', official=True))
+)
+
+
 class MoveChangelog(TableBase):
     """History of changes to moves across main game versions."""
     __tablename__ = 'move_changelog'
 class MoveChangelog(TableBase):
     """History of changes to moves across main game versions."""
     __tablename__ = 'move_changelog'
@@ -803,7 +906,7 @@ class MoveChangelog(TableBase):
     effect_chance = Column(Integer, nullable=True,
         info=dict(description="Prior effect chance, or NULL if unchanged"))
 
     effect_chance = Column(Integer, nullable=True,
         info=dict(description="Prior effect chance, or NULL if unchanged"))
 
-class Nature(TableBase, OfficiallyNamed):
+class Nature(TableBase):
     u"""A nature a Pokémon can have, such as Calm or Brave
     """
     __tablename__ = 'natures'
     u"""A nature a Pokémon can have, such as Calm or Brave
     """
     __tablename__ = 'natures'
@@ -820,6 +923,8 @@ class Nature(TableBase, OfficiallyNamed):
         info=dict(description=u"ID of the Berry flavor the Pokémon hates (if likes_flavor_id is the same, the effects cancel out)"))
     likes_flavor_id = Column(Integer, ForeignKey('contest_types.id'), nullable=False,
         info=dict(description=u"ID of the Berry flavor the Pokémon likes (if hates_flavor_id is the same, the effects cancel out)"))
         info=dict(description=u"ID of the Berry flavor the Pokémon hates (if likes_flavor_id is the same, the effects cancel out)"))
     likes_flavor_id = Column(Integer, ForeignKey('contest_types.id'), nullable=False,
         info=dict(description=u"ID of the Berry flavor the Pokémon likes (if hates_flavor_id is the same, the effects cancel out)"))
+    name = TextColumn(Unicode(8), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
     @property
     def is_neutral(self):
 
     @property
     def is_neutral(self):
@@ -855,7 +960,7 @@ class NaturePokeathlonStat(TableBase):
     max_change = Column(Integer, nullable=False,
         info=dict(description="Maximum change"))
 
     max_change = Column(Integer, nullable=False,
         info=dict(description="Maximum change"))
 
-class PokeathlonStat(TableBase, OfficiallyNamed):
+class PokeathlonStat(TableBase):
     u"""A Pokéathlon stat, such as "Stamina" or "Jump".
     """
     __tablename__ = 'pokeathlon_stats'
     u"""A Pokéathlon stat, such as "Stamina" or "Jump".
     """
     __tablename__ = 'pokeathlon_stats'
@@ -864,8 +969,10 @@ class PokeathlonStat(TableBase, OfficiallyNamed):
         info=dict(description="A numeric ID"))
     identifier = Column(Unicode(8), nullable=False,
         info=dict(description="An identifier", format='identifier'))
         info=dict(description="A numeric ID"))
     identifier = Column(Unicode(8), nullable=False,
         info=dict(description="An identifier", format='identifier'))
+    name = TextColumn(Unicode(8), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 
-class Pokedex(TableBase, UnofficiallyNamed):
+class Pokedex(TableBase):
     u"""A collection of Pokémon species ordered in a particular way
     """
     __tablename__ = 'pokedexes'
     u"""A collection of Pokémon species ordered in a particular way
     """
     __tablename__ = 'pokedexes'
@@ -878,8 +985,10 @@ class Pokedex(TableBase, UnofficiallyNamed):
         info=dict(description=u"An identifier", format='identifier'))
     description = ProseColumn(Unicode(512), plural='descriptions', nullable=False,
         info=dict(description=u"A longer description of the Pokédex", format='plaintext'))
         info=dict(description=u"An identifier", format='identifier'))
     description = ProseColumn(Unicode(512), plural='descriptions', nullable=False,
         info=dict(description=u"A longer description of the Pokédex", format='plaintext'))
+    name = ProseColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 
-class Pokemon(TableBase, OfficiallyNamed):
+class Pokemon(TableBase):
     u"""A species of Pokémon.  The core to this whole mess.
     """
     __tablename__ = 'pokemon'
     u"""A species of Pokémon.  The core to this whole mess.
     """
     __tablename__ = 'pokemon'
@@ -896,9 +1005,6 @@ class Pokemon(TableBase, OfficiallyNamed):
         info=dict(description=u"The height of the Pokémon, in decimeters (tenths of a meter)"))
     weight = Column(Integer, nullable=False,
         info=dict(description=u"The weight of the Pokémon, in tenths of a kilogram (decigrams)"))
         info=dict(description=u"The height of the Pokémon, in decimeters (tenths of a meter)"))
     weight = Column(Integer, nullable=False,
         info=dict(description=u"The weight of the Pokémon, in tenths of a kilogram (decigrams)"))
-    species = TextColumn(Unicode(16), nullable=False, plural='species_names',
-        info=dict(description=u'The short flavor text, such as "Seed" or "Lizard"; usually affixed with the word "Pokémon"',
-        official=True, format='plaintext'))
     color_id = Column(Integer, ForeignKey('pokemon_colors.id'), nullable=False,
         info=dict(description=u"ID of this Pokémon's Pokédex color, as used for a gimmick search function in the games."))
     pokemon_shape_id = Column(Integer, ForeignKey('pokemon_shapes.id'), nullable=True,
     color_id = Column(Integer, ForeignKey('pokemon_colors.id'), nullable=False,
         info=dict(description=u"ID of this Pokémon's Pokédex color, as used for a gimmick search function in the games."))
     pokemon_shape_id = Column(Integer, ForeignKey('pokemon_shapes.id'), nullable=True,
@@ -1004,6 +1110,14 @@ class Pokemon(TableBase, OfficiallyNamed):
         else:
             return None
 
         else:
             return None
 
+create_translation_table('pokemon_texts', Pokemon, 'names',
+    name = Column(Unicode(20), nullable=False, index=True,
+        info=dict(description="The name", format='plaintext', official=True)),
+    species = Column(Unicode(16), nullable=False,
+        info=dict(description=u'The short flavor text, such as "Seed" or "Lizard"; usually affixed with the word "Pokémon"',
+        official=True, format='plaintext')),
+)
+
 class PokemonAbility(TableBase):
     u"""Maps an ability to a Pokémon that can have it
     """
 class PokemonAbility(TableBase):
     u"""Maps an ability to a Pokémon that can have it
     """
@@ -1020,7 +1134,7 @@ class PokemonAbility(TableBase):
     slot = Column(Integer, primary_key=True, nullable=False, autoincrement=False,
         info=dict(description=u"The ability slot, i.e. 1 or 2 for gen. IV"))
 
     slot = Column(Integer, primary_key=True, nullable=False, autoincrement=False,
         info=dict(description=u"The ability slot, i.e. 1 or 2 for gen. IV"))
 
-class PokemonColor(TableBase, OfficiallyNamed):
+class PokemonColor(TableBase):
     u"""The "Pokédex color" of a Pokémon species. Usually based on the Pokémon's color.
     """
     __tablename__ = 'pokemon_colors'
     u"""The "Pokédex color" of a Pokémon species. Usually based on the Pokémon's color.
     """
     __tablename__ = 'pokemon_colors'
@@ -1029,6 +1143,8 @@ class PokemonColor(TableBase, OfficiallyNamed):
         info=dict(description=u"ID of the Pokémon"))
     identifier = Column(Unicode(6), nullable=False,
         info=dict(description=u"An identifier", format='identifier'))
         info=dict(description=u"ID of the Pokémon"))
     identifier = Column(Unicode(6), nullable=False,
         info=dict(description=u"An identifier", format='identifier'))
+    name = TextColumn(Unicode(6), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 class PokemonDexNumber(TableBase):
     u"""The number of a Pokémon in a particular Pokédex (e.g. Jigglypuff is #138 in Hoenn's 'dex)
 
 class PokemonDexNumber(TableBase):
     u"""The number of a Pokémon in a particular Pokédex (e.g. Jigglypuff is #138 in Hoenn's 'dex)
@@ -1099,7 +1215,7 @@ class PokemonFlavorText(TableBase, LanguageSpecific):
     flavor_text = Column(Unicode(255), nullable=False,
         info=dict(description=u"ID of the version that has this flavor text", official=True, format='gametext'))
 
     flavor_text = Column(Unicode(255), nullable=False,
         info=dict(description=u"ID of the version that has this flavor text", official=True, format='gametext'))
 
-class PokemonForm(TableBase, OfficiallyNamed):
+class PokemonForm(TableBase):
     u"""An individual form of a Pokémon.
 
     Pokémon that do not have separate forms are still given a single row to
     u"""An individual form of a Pokémon.
 
     Pokémon that do not have separate forms are still given a single row to
@@ -1121,6 +1237,8 @@ class PokemonForm(TableBase, OfficiallyNamed):
         info=dict(description=u'Set for exactly one form used as the default for each species.'))
     order = Column(Integer, nullable=False, autoincrement=False,
         info=dict(description=u'The order in which forms should be sorted.  Multiple forms may have equal order, in which case they should fall back on sorting by name.'))
         info=dict(description=u'Set for exactly one form used as the default for each species.'))
     order = Column(Integer, nullable=False, autoincrement=False,
         info=dict(description=u'The order in which forms should be sorted.  Multiple forms may have equal order, in which case they should fall back on sorting by name.'))
+    name = TextColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
     @property
     def pokemon(self):
 
     @property
     def pokemon(self):
@@ -1179,7 +1297,7 @@ class PokemonFormPokeathlonStat(TableBase):
     maximum_stat = Column(Integer, nullable=False, autoincrement=False,
         info=dict(description=u'The maximum value for this stat for this Pokémon form.'))
 
     maximum_stat = Column(Integer, nullable=False, autoincrement=False,
         info=dict(description=u'The maximum value for this stat for this Pokémon form.'))
 
-class PokemonHabitat(TableBase, OfficiallyNamed):
+class PokemonHabitat(TableBase):
     u"""The habitat of a Pokémon, as given in the FireRed/LeafGreen version Pokédex
     """
     __tablename__ = 'pokemon_habitats'
     u"""The habitat of a Pokémon, as given in the FireRed/LeafGreen version Pokédex
     """
     __tablename__ = 'pokemon_habitats'
@@ -1188,6 +1306,8 @@ class PokemonHabitat(TableBase, OfficiallyNamed):
         info=dict(description=u"A numeric ID"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description=u"An identifier", format='identifier'))
         info=dict(description=u"A numeric ID"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description=u"An identifier", format='identifier'))
+    name = TextColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 class PokemonInternalID(TableBase):
     u"""The number of a Pokémon a game uses internally
 
 class PokemonInternalID(TableBase):
     u"""The number of a Pokémon a game uses internally
@@ -1235,7 +1355,7 @@ class PokemonMove(TableBase):
         {},
     )
 
         {},
     )
 
-class PokemonMoveMethod(TableBase, UnofficiallyNamed):
+class PokemonMoveMethod(TableBase):
     u"""A method a move can be learned by, such as "Level up" or "Tutor".
     """
     __tablename__ = 'pokemon_move_methods'
     u"""A method a move can be learned by, such as "Level up" or "Tutor".
     """
     __tablename__ = 'pokemon_move_methods'
@@ -1246,8 +1366,10 @@ class PokemonMoveMethod(TableBase, UnofficiallyNamed):
         info=dict(description=u"An identifier", format='identifier'))
     description = ProseColumn(Unicode(255), plural='descriptions', nullable=False,
         info=dict(description=u"A detailed description of how the method works", format='plaintext'))
         info=dict(description=u"An identifier", format='identifier'))
     description = ProseColumn(Unicode(255), plural='descriptions', nullable=False,
         info=dict(description=u"A detailed description of how the method works", format='plaintext'))
+    name = ProseColumn(Unicode(64), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 
-class PokemonShape(TableBase, UnofficiallyNamed):
+class PokemonShape(TableBase):
     u"""The shape of a Pokémon's body, as used in generation IV Pokédexes.
     """
     __tablename__ = 'pokemon_shapes'
     u"""The shape of a Pokémon's body, as used in generation IV Pokédexes.
     """
     __tablename__ = 'pokemon_shapes'
@@ -1258,6 +1380,8 @@ class PokemonShape(TableBase, UnofficiallyNamed):
         info=dict(description=u"An identifier", format='identifier'))
     awesome_name = ProseColumn(Unicode(16), plural='awesome_names', nullable=False,
         info=dict(description=u"A splendiferous name of the body shape", format='plaintext'))
         info=dict(description=u"An identifier", format='identifier'))
     awesome_name = ProseColumn(Unicode(16), plural='awesome_names', nullable=False,
         info=dict(description=u"A splendiferous name of the body shape", format='plaintext'))
+    name = ProseColumn(Unicode(24), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=False))
 
 class PokemonStat(TableBase):
     u"""A stat value of a Pokémon
 
 class PokemonStat(TableBase):
     u"""A stat value of a Pokémon
@@ -1283,7 +1407,7 @@ class PokemonType(TableBase):
     slot = Column(Integer, primary_key=True, nullable=False, autoincrement=False,
         info=dict(description=u"The type's slot, 1 or 2, used to sort types if there are two of them"))
 
     slot = Column(Integer, primary_key=True, nullable=False, autoincrement=False,
         info=dict(description=u"The type's slot, 1 or 2, used to sort types if there are two of them"))
 
-class Region(TableBase, OfficiallyNamed):
+class Region(TableBase):
     u"""Major areas of the world: Kanto, Johto, etc.
     """
     __tablename__ = 'regions'
     u"""Major areas of the world: Kanto, Johto, etc.
     """
     __tablename__ = 'regions'
@@ -1292,8 +1416,10 @@ class Region(TableBase, OfficiallyNamed):
         info=dict(description=u"A numeric ID"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description=u"An identifier", format='identifier'))
         info=dict(description=u"A numeric ID"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description=u"An identifier", format='identifier'))
+    name = TextColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 
-class Stat(TableBase, OfficiallyNamed):
+class Stat(TableBase):
     u"""A Stat, such as Attack or Speed
     """
     __tablename__ = 'stats'
     u"""A Stat, such as Attack or Speed
     """
     __tablename__ = 'stats'
@@ -1304,6 +1430,8 @@ class Stat(TableBase, OfficiallyNamed):
         info=dict(description=u"For offensive and defensive stats, the damage this stat relates to; otherwise None (the NULL value)"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description=u"An identifier", format='identifier'))
         info=dict(description=u"For offensive and defensive stats, the damage this stat relates to; otherwise None (the NULL value)"))
     identifier = Column(Unicode(16), nullable=False,
         info=dict(description=u"An identifier", format='identifier'))
+    name = TextColumn(Unicode(16), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 class StatHint(TableBase):
     u"""Flavor text for genes that appears in a Pokémon's summary.  Sometimes
 
 class StatHint(TableBase):
     u"""Flavor text for genes that appears in a Pokémon's summary.  Sometimes
@@ -1311,11 +1439,14 @@ class StatHint(TableBase):
     """
     __tablename__ = 'stat_hints'
     __singlename__ = 'stat_hint'
     """
     __tablename__ = 'stat_hints'
     __singlename__ = 'stat_hint'
-    id = Column(Integer, primary_key=True, nullable=False)
-    stat_id = Column(Integer, ForeignKey('stats.id'), nullable=False)
-    gene_mod_5 = Column(Integer, nullable=False, index=True)
-    text = TextColumn(Unicode(24), plural='texts', nullable=False, index=True, unique=True,
-        info=dict(description=u"The English text displayed", official=True, format='plaintext'))
+    id = Column(Integer, primary_key=True, nullable=False,
+        info=dict(description=u"A numeric ID"))
+    stat_id = Column(Integer, ForeignKey('stats.id'), nullable=False,
+        info=dict(description=u"ID of the highest stat"))
+    gene_mod_5 = Column(Integer, nullable=False, index=True,
+        info=dict(description=u"Value of the highest stat modulo 5"))
+    message = TextColumn(Unicode(24), plural='messages', nullable=False, index=True, unique=True,
+        info=dict(description=u"The text displayed", official=True, format='plaintext'))
 
 class SuperContestCombo(TableBase):
     u"""Combo of two moves in a Super Contest.
 
 class SuperContestCombo(TableBase):
     u"""Combo of two moves in a Super Contest.
@@ -1350,18 +1481,20 @@ class TypeEfficacy(TableBase):
     damage_factor = Column(Integer, nullable=False,
         info=dict(description=u"The multiplier, as a percentage of damage inflicted."))
 
     damage_factor = Column(Integer, nullable=False,
         info=dict(description=u"The multiplier, as a percentage of damage inflicted."))
 
-class Type(TableBase, OfficiallyNamed):
+class Type(TableBase):
     u"""Any of the elemental types Pokémon and moves can have."""
     __tablename__ = 'types'
     __singlename__ = 'type'
     id = Column(Integer, primary_key=True, nullable=False,
         info=dict(description=u"A unique ID for this type."))
     u"""Any of the elemental types Pokémon and moves can have."""
     __tablename__ = 'types'
     __singlename__ = 'type'
     id = Column(Integer, primary_key=True, nullable=False,
         info=dict(description=u"A unique ID for this type."))
-    identifier = Column(Unicode(8), nullable=False,
+    identifier = Column(Unicode(12), nullable=False,
         info=dict(description=u"An identifier", format='identifier'))
     generation_id = Column(Integer, ForeignKey('generations.id'), nullable=False,
         info=dict(description=u"The ID of the generation this type first appeared in."))
     damage_class_id = Column(Integer, ForeignKey('move_damage_classes.id'), nullable=True,
         info=dict(description=u"The ID of the damage class this type's moves had before Generation IV, null if not applicable (e.g. ???)."))
         info=dict(description=u"An identifier", format='identifier'))
     generation_id = Column(Integer, ForeignKey('generations.id'), nullable=False,
         info=dict(description=u"The ID of the generation this type first appeared in."))
     damage_class_id = Column(Integer, ForeignKey('move_damage_classes.id'), nullable=True,
         info=dict(description=u"The ID of the damage class this type's moves had before Generation IV, null if not applicable (e.g. ???)."))
+    name = TextColumn(Unicode(12), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 class VersionGroup(TableBase):
     u"""A group of versions, containing either two paired versions (such as Red
 
 class VersionGroup(TableBase):
     u"""A group of versions, containing either two paired versions (such as Red
@@ -1383,7 +1516,7 @@ class VersionGroupRegion(TableBase):
     region_id = Column(Integer, ForeignKey('regions.id'), primary_key=True, nullable=False,
         info=dict(description=u"The ID of the region."))
 
     region_id = Column(Integer, ForeignKey('regions.id'), primary_key=True, nullable=False,
         info=dict(description=u"The ID of the region."))
 
-class Version(TableBase, OfficiallyNamed):
+class Version(TableBase):
     u"""An individual main-series Pokémon game."""
     __tablename__ = 'versions'
     __singlename__ = 'version'
     u"""An individual main-series Pokémon game."""
     __tablename__ = 'versions'
     __singlename__ = 'version'
@@ -1393,6 +1526,8 @@ class Version(TableBase, OfficiallyNamed):
         info=dict(description=u"The ID of the version group this game belongs to."))
     identifier = Column(Unicode(32), nullable=False,
         info=dict(description=u'And identifier', format='identifier'))
         info=dict(description=u"The ID of the version group this game belongs to."))
     identifier = Column(Unicode(32), nullable=False,
         info=dict(description=u'And identifier', format='identifier'))
+    name = TextColumn(Unicode(32), nullable=False, index=True, plural='names',
+        info=dict(description="The name", format='plaintext', official=True))
 
 
 ### Relations down here, to avoid ordering problems
 
 
 ### Relations down here, to avoid ordering problems
@@ -1405,7 +1540,7 @@ Ability.generation = relation(Generation, backref='abilities')
 Ability.all_pokemon = relation(Pokemon,
     secondary=PokemonAbility.__table__,
     order_by=Pokemon.order,
 Ability.all_pokemon = relation(Pokemon,
     secondary=PokemonAbility.__table__,
     order_by=Pokemon.order,
-    back_populates='all_abilities',
+    #back_populates='all_abilities',
 )
 Ability.pokemon = relation(Pokemon,
     secondary=PokemonAbility.__table__,
 )
 Ability.pokemon = relation(Pokemon,
     secondary=PokemonAbility.__table__,
@@ -1414,7 +1549,7 @@ Ability.pokemon = relation(Pokemon,
         PokemonAbility.is_dream == False
     ),
     order_by=Pokemon.order,
         PokemonAbility.is_dream == False
     ),
     order_by=Pokemon.order,
-    back_populates='abilities',
+    #back_populates='abilities',
 )
 Ability.dream_pokemon = relation(Pokemon,
     secondary=PokemonAbility.__table__,
 )
 Ability.dream_pokemon = relation(Pokemon,
     secondary=PokemonAbility.__table__,
@@ -1423,7 +1558,7 @@ Ability.dream_pokemon = relation(Pokemon,
         PokemonAbility.is_dream == True
     ),
     order_by=Pokemon.order,
         PokemonAbility.is_dream == True
     ),
     order_by=Pokemon.order,
-    back_populates='dream_ability',
+    #back_populates='dream_ability',
 )
 
 AbilityChangelog.changed_in = relation(VersionGroup, backref='ability_changelog')
 )
 
 AbilityChangelog.changed_in = relation(VersionGroup, backref='ability_changelog')
@@ -1459,7 +1594,7 @@ EncounterSlot.version_group = relation(VersionGroup)
 
 EvolutionChain.growth_rate = relation(GrowthRate, backref='evolution_chains')
 EvolutionChain.baby_trigger_item = relation(Item, backref='evolution_chains')
 
 EvolutionChain.growth_rate = relation(GrowthRate, backref='evolution_chains')
 EvolutionChain.baby_trigger_item = relation(Item, backref='evolution_chains')
-EvolutionChain.pokemon = relation(Pokemon, order_by=Pokemon.order, back_populates='evolution_chain')
+EvolutionChain.pokemon = relation(Pokemon, order_by=Pokemon.order)#, back_populates='evolution_chain')
 
 Experience.growth_rate = relation(GrowthRate, backref='experience_table')
 
 
 Experience.growth_rate = relation(GrowthRate, backref='experience_table')
 
@@ -1519,8 +1654,7 @@ Move.super_contest_effect = relation(SuperContestEffect, backref='moves')
 Move.super_contest_combo_next = association_proxy('super_contest_combo_first', 'second')
 Move.super_contest_combo_prev = association_proxy('super_contest_combo_second', 'first')
 Move.target = relation(MoveTarget, backref='moves')
 Move.super_contest_combo_next = association_proxy('super_contest_combo_first', 'second')
 Move.super_contest_combo_prev = association_proxy('super_contest_combo_second', 'first')
 Move.target = relation(MoveTarget, backref='moves')
-Move.type = relation(Type, back_populates='moves')
-
+Move.type = relation(Type)#, back_populates='moves')
 
 MoveChangelog.changed_in = relation(VersionGroup, backref='move_changelog')
 MoveChangelog.move_effect = relation(MoveEffect, backref='move_changelog')
 
 MoveChangelog.changed_in = relation(VersionGroup, backref='move_changelog')
 MoveChangelog.move_effect = relation(MoveEffect, backref='move_changelog')
@@ -1563,7 +1697,7 @@ NatureBattleStylePreference.battle_style = relation(MoveBattleStyle, backref='na
 NaturePokeathlonStat.pokeathlon_stat = relation(PokeathlonStat, backref='nature_effects')
 
 Pokedex.region = relation(Region, backref='pokedexes')
 NaturePokeathlonStat.pokeathlon_stat = relation(PokeathlonStat, backref='nature_effects')
 
 Pokedex.region = relation(Region, backref='pokedexes')
-Pokedex.version_groups = relation(VersionGroup, order_by=VersionGroup.id, back_populates='pokedex')
+Pokedex.version_groups = relation(VersionGroup, order_by=VersionGroup.id)#, back_populates='pokedex')
 
 Pokemon.all_abilities = relation(Ability,
     secondary=PokemonAbility.__table__,
 
 Pokemon.all_abilities = relation(Ability,
     secondary=PokemonAbility.__table__,
@@ -1591,7 +1725,7 @@ Pokemon.dex_numbers = relation(PokemonDexNumber, order_by=PokemonDexNumber.poked
 Pokemon.egg_groups = relation(EggGroup, secondary=PokemonEggGroup.__table__,
                                         order_by=PokemonEggGroup.egg_group_id,
                                         backref=backref('pokemon', order_by=Pokemon.order))
 Pokemon.egg_groups = relation(EggGroup, secondary=PokemonEggGroup.__table__,
                                         order_by=PokemonEggGroup.egg_group_id,
                                         backref=backref('pokemon', order_by=Pokemon.order))
-Pokemon.evolution_chain = relation(EvolutionChain, back_populates='pokemon')
+Pokemon.evolution_chain = relation(EvolutionChain)#, back_populates='pokemon')
 Pokemon.child_pokemon = relation(Pokemon,
     primaryjoin=Pokemon.id==PokemonEvolution.from_pokemon_id,
     secondary=PokemonEvolution.__table__,
 Pokemon.child_pokemon = relation(Pokemon,
     primaryjoin=Pokemon.id==PokemonEvolution.from_pokemon_id,
     secondary=PokemonEvolution.__table__,
@@ -1613,7 +1747,7 @@ Pokemon.shape = relation(PokemonShape, backref='pokemon')
 Pokemon.stats = relation(PokemonStat, backref='pokemon', order_by=PokemonStat.stat_id.asc())
 Pokemon.types = relation(Type, secondary=PokemonType.__table__,
                                order_by=PokemonType.slot.asc(),
 Pokemon.stats = relation(PokemonStat, backref='pokemon', order_by=PokemonStat.stat_id.asc())
 Pokemon.types = relation(Type, secondary=PokemonType.__table__,
                                order_by=PokemonType.slot.asc(),
-                               back_populates='pokemon')
+                               )#back_populates='pokemon')
 
 PokemonDexNumber.pokedex = relation(Pokedex)
 
 
 PokemonDexNumber.pokedex = relation(Pokedex)
 
@@ -1711,7 +1845,7 @@ Type.generation = relation(Generation, backref='types')
 Type.damage_class = relation(MoveDamageClass, backref='types')
 Type.pokemon = relation(Pokemon, secondary=PokemonType.__table__,
                                  order_by=Pokemon.order,
 Type.damage_class = relation(MoveDamageClass, backref='types')
 Type.pokemon = relation(Pokemon, secondary=PokemonType.__table__,
                                  order_by=Pokemon.order,
-                                 back_populates='types')
+                                 )#back_populates='types')
 Type.moves = relation(Move, back_populates='type', order_by=Move.id)
 
 Version.version_group = relation(VersionGroup, back_populates='versions')
 Type.moves = relation(Move, back_populates='type', order_by=Move.id)
 
 Version.version_group = relation(VersionGroup, back_populates='versions')
@@ -1723,120 +1857,138 @@ VersionGroup.version_group_regions = relation(VersionGroupRegion, backref='versi
 VersionGroup.regions = association_proxy('version_group_regions', 'region')
 VersionGroup.pokedex = relation(Pokedex, back_populates='version_groups')
 
 VersionGroup.regions = association_proxy('version_group_regions', 'region')
 VersionGroup.pokedex = relation(Pokedex, back_populates='version_groups')
 
-### Convenience function
-def all_tables():
-    u"""Yields all tables in the pokédex"""
-    for table in set(t for t in globals().values() if isclass(t)):
-        if issubclass(table, TableBase) and table is not TableBase:
-            yield table
-
-
-### Add name tables
-for table in all_tables():
-    if issubclass(table, OfficiallyNamed):
-        cls = TextColumn
-        info=dict(description="The name", format='plaintext', official=True)
-    elif issubclass(table, UnofficiallyNamed):
-        cls = ProseColumn
-        info=dict(description="The name", format='plaintext', official=False)
-    else:
-        continue
-    table.name = cls(Unicode(class_mapper(table).c.identifier.type.length),
-        plural='names', nullable=False, info=info)
 
 ### Add text/prose tables
 
 
 ### Add text/prose tables
 
-def makeTextTable(object_table, name_plural, name_singular, columns, lazy):
+default_lang = u'en'
+
+def makeTextTable(foreign_table_class, table_suffix_plural, table_suffix_singular, columns, lazy, Language=Language):
     # With "Language", we'd have two language_id. So, rename one to 'lang'
     # With "Language", we'd have two language_id. So, rename one to 'lang'
-    safe_name = object_table.__singlename__
-    if safe_name == 'language':
-        safe_name = 'lang'
-
-    fields = {
-            safe_name + '_id': Column(Integer,
-                    ForeignKey(object_table.id), primary_key=True, nullable=False,
-                    info=dict(description="ID of the object this table represents")
+    foreign_key_name = foreign_table_class.__singlename__
+    if foreign_key_name == 'language':
+        foreign_key_name = 'lang'
+
+    table_name = foreign_table_class.__singlename__ + '_' + table_suffix_plural
+
+    class TranslatedStringsTable(object):
+        __tablename__ = table_name
+        _attrname = table_suffix_plural
+        _language_identifier = association_proxy('language', 'identifier')
+
+    for column_name, column_name_plural, column in columns:
+        column.name = column_name
+        if not column.nullable:
+            # A Python side default value, so that the strings can be set
+            # one by one without the DB complaining about missing values
+            column.default = ColumnDefault(u'')
+
+    table = Table(table_name, foreign_table_class.__table__.metadata,
+            Column(foreign_key_name + '_id', Integer, ForeignKey(foreign_table_class.id),
+                    primary_key=True, nullable=False),
+            Column('language_id', Integer, ForeignKey(Language.id),
+                    primary_key=True, index=True, nullable=False),
+            *(column for name, plural, column in columns)
+        )
+
+    mapper(TranslatedStringsTable, table,
+        properties={
+            "object_id": synonym(foreign_key_name + '_id'),
+            "language": relation(Language,
+                primaryjoin=table.c.language_id == Language.id,
+            ),
+            foreign_key_name: relation(foreign_table_class,
+                primaryjoin=(foreign_table_class.id == table.c[foreign_key_name + "_id"]),
+                backref=backref(table_suffix_plural,
+                    collection_class=attribute_mapped_collection('_language_identifier'),
+                    lazy=lazy,
                 ),
                 ),
-            '__tablename__': table.__singlename__ + '_' + name_plural,
-            '__singlename__': table.__singlename__ + '_' + name_singular,
-            'is_%s_table' % name_singular: True,
-        }
+            ),
+        },
+    )
 
 
-    fields.update((name, col) for name, plural, col in columns)
+    # The relation to the object
+    TranslatedStringsTable.object = getattr(TranslatedStringsTable, foreign_key_name)
 
 
-    name = table.__name__ + name_singular.capitalize()
+    # Link the tables themselves, so we can get them if needed
+    TranslatedStringsTable.foreign_table_class = foreign_table_class
+    setattr(foreign_table_class, table_suffix_singular + '_table', TranslatedStringsTable)
 
 
-    # There are some dynamic things that can only be set at class
-    # creation time because of declarative metaclass magic.
-    # So create class dynamically.
-    Strings = type(name, (TableBase, LanguageSpecific), fields)
+    for column_name, column_name_plural, column in columns:
+        # Provide a property with all the names, and an English accessor
+        # for backwards compatibility
+        def text_string_creator(language_code, string):
+            row = TranslatedStringsTable()
+            row._language_identifier = language_code
+            setattr(row, column_name, string)
+            return row
 
 
-    # Alias the described thing to 'object', to make meta stuff easier
-    Strings.object_id = getattr(Strings, safe_name + '_id')
+        setattr(foreign_table_class, column_name_plural,
+            association_proxy(table_suffix_plural, column_name, creator=text_string_creator))
+        setattr(foreign_table_class, column_name, DefaultLangProperty(column_name_plural))
 
 
-    # The relation to the object
-    setattr(table, name_plural, relation(
-            Strings,
-            primaryjoin=(table.id == Strings.object_id),
-            backref=safe_name,
-            collection_class=attribute_mapped_collection('language'),
-            lazy=lazy,
-        ))
-
-    Strings.object = getattr(Strings, safe_name)
-
-    Strings.object_table = table
-    setattr(table, name_singular + '_table', Strings)
-
-    Strings.language = relation(
-            Language,
-            primaryjoin=Strings.language_id == Language.id,
-        )
+        if column_name == 'name':
+            foreign_table_class.name_table = TranslatedStringsTable
 
 
-    for colname, pluralname, column in columns:
-        # Provide a relation with all the names, and an English accessor
-        # for backwards compatibility
-        def scope(colname, pluralname, column):
-            def get_string(self):
-                return dict(
-                        (l, getattr(t, colname))
-                        for l, t in getattr(self, name_plural).items()
-                    )
+    compile_mappers()
+    return TranslatedStringsTable
 
 
-            def get_english_string(self):
-                try:
-                    return get_string(self)['en']
-                except KeyError:
-                    raise AttributeError(colname)
+class DefaultLangProperty(object):
+    def __init__(self, column_name):
+        self.column_name = column_name
 
 
-            setattr(table, pluralname, property(get_string))
-            setattr(table, colname, property(get_english_string))
-        scope(colname, pluralname, column)
+    def __get__(self, instance, cls):
+        if instance:
+            return getattr(instance, self.column_name)[default_lang]
+        else:
+            # TODO I think this is kind of broken
+            return getattr(cls, self.column_name)[default_lang]
 
 
-        if colname == 'name':
-            table.name_table = Strings
+    def __set__(self, instance, value):
+        getattr(instance, self.colname)[default_lang] = value
 
 
-    return Strings
+    def __delete__(self, instance):
+        del getattr(instance, self.colname)[default_lang]
 
 
-for table in all_tables():
-    text_columns = []
-    prose_columns = []
+for table in list(table_classes):
+    # Find all the language-specific columns, keeping them in the order they
+    # were defined
+    all_columns = []
     for colname in dir(table):
         column = getattr(table, colname)
     for colname in dir(table):
         column = getattr(table, colname)
+        if isinstance(column, LanguageSpecificColumn):
+            all_columns.append((colname, column))
+            delattr(table, colname)
+    all_columns.sort(key=lambda pair: pair[1].order)
+
+    # Break them into text and prose columns
+    text_columns = []
+    prose_columns = []
+    for colname, column in all_columns:
+        spec = colname, column.plural, column.makeSAColumn()
         if isinstance(column, TextColumn):
         if isinstance(column, TextColumn):
-            text_columns.append((colname, column.plural, column.makeSAColumn()))
+            text_columns.append(spec)
         elif isinstance(column, ProseColumn):
         elif isinstance(column, ProseColumn):
-            prose_columns.append((colname, column.plural, column.makeSAColumn()))
+            prose_columns.append(spec)
+
+    if (text_columns or prose_columns) and issubclass(table, LanguageSpecific):
+        raise AssertionError("Language-specific table %s shouldn't have explicit language-specific columns" % table)
+
     if text_columns:
         string_table = makeTextTable(table, 'texts', 'text', text_columns, lazy=False)
     if text_columns:
         string_table = makeTextTable(table, 'texts', 'text', text_columns, lazy=False)
-        globals()[string_table.__name__] = string_table
     if prose_columns:
     if prose_columns:
-        string_table = makeTextTable(table, 'prose', 'prose', prose_columns, lazy=True)
-        globals()[string_table.__name__] = string_table
-    if (text_columns or prose_columns) and issubclass(table, LanguageSpecific):
-        raise AssertionError("Language-specific table %s shouldn't have explicit language-specific columns" % table)
+        string_table = makeTextTable(table, 'prose', 'prose', prose_columns, lazy='select')
 
 ### Add language relations
 
 ### Add language relations
-for table in all_tables():
+for table in list(table_classes):
     if issubclass(table, LanguageSpecific):
         table.language = relation(Language, primaryjoin=table.language_id == Language.id)
     if issubclass(table, LanguageSpecific):
         table.language = relation(Language, primaryjoin=table.language_id == Language.id)
+
+Move.effect = DefaultLangProperty('effects')
+Move.effects = markdown.MoveEffectsProperty('effect')
+Move.short_effect = DefaultLangProperty('short_effects')
+Move.short_effects = markdown.MoveEffectsProperty('short_effect')
+
+MoveChangelog.effect = DefaultLangProperty('effects')
+MoveChangelog.effects = markdown.MoveEffectsProperty('effect')
+MoveChangelog.short_effect = DefaultLangProperty('short_effects')
+MoveChangelog.short_effects = markdown.MoveEffectsProperty('short_effect')