← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~abentley/launchpad/translation-splitting into lp:launchpad

 

Aaron Bentley has proposed merging lp:~abentley/launchpad/translation-splitting into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~abentley/launchpad/translation-splitting/+merge/50949

= Summary =
Allow un-merging translations for a ProductSeries and SourcePackage.

When ProductSeries and SourcePackages are linked via a Packaging, their translations are shared.  However, if the Packaging is destroyed, they need to stop sharing.  This branch provides a mechanism for doing the splitting. Running a Job when the Packaging is destroyed is left for a follow-on branch.

== Proposed fix ==
Implement a TranslationSplitter.

== Pre-implementation notes ==
Discussed with henninge

== Implementation details ==
The TranslationSplitter
- Finds the shared POTMsgSets
- Creates a new POTMsgSet (via the new clone method) for the SourcePackage's template.
- Moves diverged TranslationMessages associated with the SourcePackage to the new POTMsgSet.
- Copies shared TranslationMessages (via the new clone method) to the new POTMsgSet.

== Tests ==
bin/test -v translationsplitter

== Demo and Q/A ==
None.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/translations/utilities/translationsplitter.py
  lib/lp/translations/interfaces/translationmessage.py
  lib/lp/translations/interfaces/potmsgset.py
  lib/lp/translations/interfaces/translationtemplateitem.py
  lib/lp/translations/tests/test_translationmessage.py
  lib/lp/testing/factory.py
  lib/lp/translations/tests/test_potmsgset.py
  lib/lp/translations/configure.zcml
  lib/lp/translations/tests/test_translationsplitter.py
  lib/lp/translations/model/potmsgset.py
  lib/lp/translations/model/potemplate.py
  lib/lp/translations/model/translationmessage.py
-- 
https://code.launchpad.net/~abentley/launchpad/translation-splitting/+merge/50949
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/translation-splitting into lp:launchpad.
=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2011-02-19 13:26:27 +0000
+++ lib/lp/testing/factory.py	2011-02-23 16:37:30 +0000
@@ -2720,15 +2720,27 @@
         return potemplate.newPOFile(language_code,
                                     create_sharing=create_sharing)
 
-    def makePOTMsgSet(self, potemplate, singular=None, plural=None,
-                      context=None, sequence=None):
+    def makePOTMsgSet(self, potemplate=None, singular=None, plural=None,
+                      context=None, sequence=None, commenttext=None,
+                      filereferences=None, sourcecomment=None,
+                      flagscomment=None):
         """Make a new `POTMsgSet` in the given template."""
+        if potemplate is None:
+            potemplate = self.makePOTemplate()
         if singular is None and plural is None:
             singular = self.getUniqueString()
         if sequence is None:
             sequence = self.getUniqueInteger()
         potmsgset = potemplate.createMessageSetFromText(
             singular, plural, context, sequence)
+        if commenttext is not None:
+            potmsgset.commenttext = commenttext
+        if filereferences is not None:
+            potmsgset.filereferences = filereferences
+        if sourcecomment is not None:
+            potmsgset.sourcecomment = sourcecomment
+        if flagscomment is not None:
+            potmsgset.flagscomment = flagscomment
         removeSecurityProxy(potmsgset).sync()
         return potmsgset
 
@@ -2791,7 +2803,8 @@
                                       translations=None, diverged=False,
                                       current_other=False,
                                       date_created=None, date_reviewed=None,
-                                      language=None, side=None):
+                                      language=None, side=None,
+                                      potemplate=None):
         """Create a `TranslationMessage` and make it current.
 
         By default the message will only be current on the side (Ubuntu
@@ -2819,6 +2832,8 @@
         :param date_reviewed: Force a specific review date instead of 'now'.
         :param language: `Language` to use for the POFile
         :param side: The `TranslationSide` this translation should be for.
+        :param potemplate: If provided, the POTemplate to use when creating
+            the POFile.
         """
         assert not (diverged and current_other), (
             "A diverged message can't be current on the other side.")
@@ -2827,9 +2842,14 @@
         assert None in (side, pofile), (
             'Cannot specify both side and pofile.')
         if pofile is None:
-            pofile = self.makePOFile(language=language, side=side)
+            pofile = self.makePOFile(
+                language=language, side=side, potemplate=potemplate)
+        else:
+            assert potemplate is None, (
+                'Cannot specify both pofile and potemplate')
+        potemplate = pofile.potemplate
         if potmsgset is None:
-            potmsgset = self.makePOTMsgSet(pofile.potemplate)
+            potmsgset = self.makePOTMsgSet(potemplate)
         if translator is None:
             translator = self.makePerson()
         if reviewer is None:

=== modified file 'lib/lp/translations/configure.zcml'
--- lib/lp/translations/configure.zcml	2010-12-23 22:12:49 +0000
+++ lib/lp/translations/configure.zcml	2011-02-23 16:37:30 +0000
@@ -383,6 +383,7 @@
         class="lp.translations.model.translationtemplateitem.TranslationTemplateItem">
         <allow
             interface="lp.translations.interfaces.translationtemplateitem.ITranslationTemplateItem"/>
+        <require permission="launchpad.Admin" set_attributes="potmsgset"/>
     </class>
 
     <!-- ProductSeriesLanguage -->

=== modified file 'lib/lp/translations/interfaces/potmsgset.py'
--- lib/lp/translations/interfaces/potmsgset.py	2011-02-17 14:16:17 +0000
+++ lib/lp/translations/interfaces/potmsgset.py	2011-02-23 16:37:30 +0000
@@ -128,6 +128,9 @@
             queries that search for credits messages.
             """))
 
+    def clone():
+        """Return a new copy of this POTMsgSet."""
+
     def getCurrentTranslationMessageOrDummy(pofile):
         """Return the current `TranslationMessage`, or a dummy.
 

=== modified file 'lib/lp/translations/interfaces/translationmessage.py'
--- lib/lp/translations/interfaces/translationmessage.py	2011-02-05 15:24:50 +0000
+++ lib/lp/translations/interfaces/translationmessage.py	2011-02-23 16:37:30 +0000
@@ -123,7 +123,7 @@
 
     potmsgset = Object(
         title=_("The template message that this translation is for"),
-        readonly=True, required=True, schema=IPOTMsgSet)
+        readonly=False, required=True, schema=IPOTMsgSet)
 
     date_created = Datetime(
         title=_("The date we saw this translation first"),
@@ -260,6 +260,13 @@
         It must not be referenced by any other object.
         """
 
+    def clone(potmsgset):
+        """Create a copy of this message associated with a different MsgSet.
+
+        potemplate of the clone is always None.  Aside from this, all values
+        should be the same.
+        """
+
     def approve(pofile, reviewer, share_with_other_side=False,
                 lock_timestamp=None):
         """Approve this suggestion, making it a current translation."""

=== modified file 'lib/lp/translations/interfaces/translationtemplateitem.py'
--- lib/lp/translations/interfaces/translationtemplateitem.py	2010-08-20 20:31:18 +0000
+++ lib/lp/translations/interfaces/translationtemplateitem.py	2011-02-23 16:37:30 +0000
@@ -37,4 +37,4 @@
 
     potmsgset = Object(
         title=_("The template message that this translation is for"),
-        readonly=True, required=True, schema=IPOTMsgSet)
+        readonly=False, required=True, schema=IPOTMsgSet)

=== modified file 'lib/lp/translations/model/potemplate.py'
--- lib/lp/translations/model/potemplate.py	2011-02-22 20:43:35 +0000
+++ lib/lp/translations/model/potemplate.py	2011-02-23 16:37:30 +0000
@@ -853,7 +853,8 @@
 
         return potmsgset
 
-    def getOrCreatePOMsgID(self, text):
+    @staticmethod
+    def getOrCreatePOMsgID(text):
         """Creates or returns existing POMsgID for given `text`."""
         try:
             msgid = POMsgID.byMsgid(text)

=== modified file 'lib/lp/translations/model/potmsgset.py'
--- lib/lp/translations/model/potmsgset.py	2011-02-17 14:18:27 +0000
+++ lib/lp/translations/model/potmsgset.py	2011-02-23 16:37:30 +0000
@@ -148,6 +148,17 @@
 
     credits_message_ids = credits_message_info.keys()
 
+    def clone(self):
+        return POTMsgSet(
+            context=self.context,
+            msgid_singular=self.msgid_singular,
+            msgid_plural=self.msgid_plural,
+            commenttext=self.commenttext,
+            filereferences=self.filereferences,
+            sourcecomment=self.sourcecomment,
+            flagscomment=self.flagscomment,
+        )
+
     def _conflictsExistingSourceFileFormats(self, source_file_format=None):
         """Return whether `source_file_format` conflicts with existing ones
         for this `POTMsgSet`.
@@ -1165,6 +1176,7 @@
         if translation_template_item is not None:
             # Update the sequence for the translation template item.
             translation_template_item.sequence = sequence
+            return translation_template_item
         elif sequence >= 0:
             # Introduce this new entry into the TranslationTemplateItem for
             # later usage.
@@ -1179,7 +1191,7 @@
                     "Attempt to add a POTMsgSet into a POTemplate which "
                     "has a conflicting value for uses_english_msgids.")
 
-            TranslationTemplateItem(
+            return TranslationTemplateItem(
                 potemplate=potemplate,
                 sequence=sequence,
                 potmsgset=self)
@@ -1187,7 +1199,7 @@
             # There is no entry for this potmsgset in TranslationTemplateItem
             # table, neither we need to create one, given that the sequence is
             # less than zero.
-            pass
+            return None
 
     def getSequence(self, potemplate):
         """See `IPOTMsgSet`."""

=== modified file 'lib/lp/translations/model/translationmessage.py'
--- lib/lp/translations/model/translationmessage.py	2011-02-04 23:51:50 +0000
+++ lib/lp/translations/model/translationmessage.py	2011-02-23 16:37:30 +0000
@@ -485,6 +485,21 @@
 
         return twins.order_by(TranslationMessage.id).first()
 
+    def clone(self, potmsgset):
+        clone = TranslationMessage(
+            potmsgset=potmsgset, submitter=self.submitter, origin=self.origin,
+            language=self.language, date_created=self.date_created,
+            reviewer=self.reviewer, date_reviewed=self.date_reviewed,
+            msgstr0=self.msgstr0, msgstr1=self.msgstr1,
+            msgstr2=self.msgstr2, msgstr3=self.msgstr3,
+            msgstr4=self.msgstr4, msgstr5=self.msgstr5,
+            comment=self.comment, validation_status=self.validation_status,
+            is_current_ubuntu=self.is_current_ubuntu,
+            is_current_upstream=self.is_current_upstream,
+            was_obsolete_in_last_import=self.was_obsolete_in_last_import,
+            )
+        return clone
+
 
 class TranslationMessageSet:
     """See `ITranslationMessageSet`."""

=== modified file 'lib/lp/translations/tests/test_potmsgset.py'
--- lib/lp/translations/tests/test_potmsgset.py	2011-02-18 16:47:52 +0000
+++ lib/lp/translations/tests/test_potmsgset.py	2011-02-23 16:37:30 +0000
@@ -1880,3 +1880,33 @@
         found = potmsgset.findTranslationMessage(
             pofile, translations=translations, prefer_shared=True)
         self.assertEqual(shared, found)
+
+
+class TestClone(TestCaseWithFactory):
+    """Test the clone() method."""
+
+    layer = ZopelessDatabaseLayer
+
+    def test_clone(self):
+        """Cloning a POTMsgSet should produce a near-identical copy."""
+        msgset = self.factory.makePOTMsgSet(
+            context=self.factory.getUniqueString('context'),
+            plural=self.factory.getUniqueString('plural'),
+            singular=self.factory.getUniqueString('singular'),
+            commenttext=self.factory.getUniqueString('comment'),
+            filereferences=self.factory.getUniqueString('filereferences'),
+            sourcecomment=self.factory.getUniqueString('sourcecomment'),
+            flagscomment=self.factory.getUniqueString('flagscomment'),
+        )
+        new_msgset = msgset.clone()
+        naked_msgset = removeSecurityProxy(msgset)
+        naked_new_msgset = removeSecurityProxy(new_msgset)
+        self.assertNotEqual(msgset.id, new_msgset.id)
+        self.assertEqual(msgset.context, new_msgset.context)
+        self.assertEqual(msgset.msgid_singular, new_msgset.msgid_singular)
+        self.assertEqual(msgset.msgid_plural, new_msgset.msgid_plural)
+        self.assertEqual(
+            msgset.commenttext, new_msgset.commenttext)
+        self.assertEqual(msgset.filereferences, new_msgset.filereferences)
+        self.assertEqual(msgset.sourcecomment, new_msgset.sourcecomment)
+        self.assertEqual(msgset.flagscomment, new_msgset.flagscomment)

=== modified file 'lib/lp/translations/tests/test_translationmessage.py'
--- lib/lp/translations/tests/test_translationmessage.py	2011-02-17 21:41:31 +0000
+++ lib/lp/translations/tests/test_translationmessage.py	2011-02-23 16:37:30 +0000
@@ -786,6 +786,38 @@
         tm.potmsgset.setSequence(pofile.potemplate, 0)
         self.assertEquals(None, tm.getOnePOFile())
 
+    def test_clone(self):
+        """Cloning a translation should produce a near-identical copy."""
+        translations = [self.factory.getUniqueString() for x in range(6)]
+        tm = self.factory.makeCurrentTranslationMessage(
+            date_created=self.factory.getUniqueDate(),
+            translations=translations, current_other=True)
+        tm.comment = self.factory.getUniqueString()
+        tm.was_obsolete_in_last_import = True
+        potmsgset = self.factory.makePOTMsgSet()
+        clone = tm.clone(potmsgset)
+        self.assertNotEqual(tm.id, clone.id)
+        self.assertIs(None, clone.potemplate)
+        self.assertEqual(potmsgset, clone.potmsgset)
+        self.assertEqual(tm.submitter, clone.submitter)
+        self.assertEqual(tm.language, clone.language)
+        self.assertEqual(tm.origin, clone.origin)
+        self.assertEqual(tm.date_created, clone.date_created)
+        self.assertEqual(tm.reviewer, clone.reviewer)
+        self.assertEqual(tm.date_reviewed, clone.date_reviewed)
+        self.assertEqual(tm.msgstr0, clone.msgstr0)
+        self.assertEqual(tm.msgstr1, clone.msgstr1)
+        self.assertEqual(tm.msgstr2, clone.msgstr2)
+        self.assertEqual(tm.msgstr3, clone.msgstr3)
+        self.assertEqual(tm.msgstr4, clone.msgstr4)
+        self.assertEqual(tm.msgstr5, clone.msgstr5)
+        self.assertEqual(tm.comment, clone.comment)
+        self.assertEqual(tm.validation_status, clone.validation_status)
+        self.assertEqual(tm.is_current_ubuntu, clone.is_current_ubuntu)
+        self.assertEqual(tm.is_current_upstream, clone.is_current_upstream)
+        self.assertEqual(
+            tm.was_obsolete_in_last_import, clone.was_obsolete_in_last_import)
+
 
 class TestTranslationMessageFindIdenticalMessage(TestCaseWithFactory):
     """Tests for `TranslationMessage.findIdenticalMessage`."""

=== added file 'lib/lp/translations/tests/test_translationsplitter.py'
--- lib/lp/translations/tests/test_translationsplitter.py	1970-01-01 00:00:00 +0000
+++ lib/lp/translations/tests/test_translationsplitter.py	2011-02-23 16:37:30 +0000
@@ -0,0 +1,145 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+
+from zope.security.proxy import removeSecurityProxy
+
+from canonical.testing.layers import ZopelessDatabaseLayer
+from lp.testing import TestCaseWithFactory
+from lp.translations.interfaces.side import (
+    TranslationSide,
+    )
+from lp.translations.utilities.translationsplitter import (
+    TranslationSplitter,
+    )
+
+
+class TestTranslationSplitter(TestCaseWithFactory):
+
+    layer = ZopelessDatabaseLayer
+
+    def useInTemplate(self, potmsgset, potemplate):
+        return potmsgset.setSequence(
+            potemplate, self.factory.getUniqueInteger())
+
+    def test_findShared_requires_both(self):
+        """Results are only included when both sides have the POTMsgSet."""
+        upstream_template = self.factory.makePOTemplate(
+            side=TranslationSide.UPSTREAM)
+        productseries = upstream_template.productseries
+        ubuntu_template = self.factory.makePOTemplate(
+            side=TranslationSide.UBUNTU)
+        package = ubuntu_template.sourcepackage
+        potmsgset = self.factory.makePOTMsgSet(upstream_template, sequence=1)
+        splitter = TranslationSplitter(productseries, package)
+        self.assertContentEqual([], splitter.findShared())
+        (upstream_item,) = potmsgset.getAllTranslationTemplateItems()
+        ubuntu_item = self.useInTemplate(potmsgset, ubuntu_template)
+        self.assertContentEqual(
+            [(upstream_item, ubuntu_item)], splitter.findShared())
+        removeSecurityProxy(upstream_item).destroySelf()
+        self.assertContentEqual([], splitter.findShared())
+
+    def makeTranslationSplitter(self):
+        return TranslationSplitter(
+            self.factory.makeProductSeries(),
+            self.factory.makeSourcePackage())
+
+    def makeSharedPOTMsgSet(self, splitter):
+        upstream_template = self.factory.makePOTemplate(
+            productseries=splitter.productseries)
+        potmsgset = self.factory.makePOTMsgSet(
+            upstream_template, sequence=self.factory.getUniqueInteger())
+        (upstream_item,) = potmsgset.getAllTranslationTemplateItems()
+        ubuntu_template = self.factory.makePOTemplate(
+            sourcepackage=splitter.sourcepackage)
+        ubuntu_item = self.useInTemplate(potmsgset, ubuntu_template)
+        return upstream_item, ubuntu_item
+
+    def test_findSharedGroupsPOTMsgSet(self):
+        """POTMsgSets are correctly grouped."""
+        splitter = self.makeTranslationSplitter()
+        self.makeSharedPOTMsgSet(splitter)
+        self.makeSharedPOTMsgSet(splitter)
+        for num, (upstream, ubuntu) in enumerate(splitter.findShared()):
+            self.assertEqual(upstream.potmsgset, ubuntu.potmsgset)
+        self.assertEqual(1, num)
+
+    def test_splitPOTMsgSet(self):
+        """Splitting a POTMsgSet clones it and updates TemplateItem."""
+        splitter = self.makeTranslationSplitter()
+        upstream_item, ubuntu_item = self.makeSharedPOTMsgSet(splitter)
+        ubuntu_template = ubuntu_item.potemplate
+        ubuntu_sequence = ubuntu_item.sequence
+        new_potmsgset = splitter.splitPOTMsgSet(ubuntu_item)
+        self.assertEqual(new_potmsgset, ubuntu_item.potmsgset)
+
+    def test_migrateTranslations_diverged_upstream(self):
+        """Diverged upstream translation stays put."""
+        splitter = self.makeTranslationSplitter()
+        upstream_item, ubuntu_item = self.makeSharedPOTMsgSet(splitter)
+        upstream_message = self.factory.makeCurrentTranslationMessage(
+            potmsgset=upstream_item.potmsgset,
+            potemplate=upstream_item.potemplate, diverged=True)
+        splitter.splitPOTMsgSet(ubuntu_item)
+        upstream_translation = splitter.migrateTranslations(
+            upstream_item.potmsgset, ubuntu_item)
+        self.assertEqual(
+            upstream_message,
+            upstream_item.potmsgset.getAllTranslationMessages().one())
+        self.assertIs(
+            None, ubuntu_item.potmsgset.getAllTranslationMessages().one())
+
+    def test_migrateTranslations_diverged_ubuntu(self):
+        """Diverged ubuntu translation moves."""
+        splitter = self.makeTranslationSplitter()
+        upstream_item, ubuntu_item = self.makeSharedPOTMsgSet(splitter)
+        ubuntu_message = self.factory.makeCurrentTranslationMessage(
+            potmsgset=ubuntu_item.potmsgset,
+            potemplate=ubuntu_item.potemplate, diverged=True)
+        splitter.splitPOTMsgSet(ubuntu_item)
+        upstream_translation = splitter.migrateTranslations(
+            upstream_item.potmsgset, ubuntu_item)
+        self.assertEqual(
+            ubuntu_message,
+            ubuntu_item.potmsgset.getAllTranslationMessages().one())
+        self.assertIs(
+            None,
+            upstream_item.potmsgset.getAllTranslationMessages().one())
+
+    def test_migrateTranslations_shared(self):
+        """Shared translation is copied."""
+        splitter = self.makeTranslationSplitter()
+        upstream_item, ubuntu_item = self.makeSharedPOTMsgSet(splitter)
+        self.factory.makeCurrentTranslationMessage(
+            potmsgset=upstream_item.potmsgset)
+        splitter.splitPOTMsgSet(ubuntu_item)
+        splitter.migrateTranslations(upstream_item.potmsgset, ubuntu_item)
+        (upstream_translation,) = (
+            upstream_item.potmsgset.getAllTranslationMessages())
+        (ubuntu_translation,) = (
+            ubuntu_item.potmsgset.getAllTranslationMessages())
+        self.assertEqual(
+            ubuntu_translation.translations,
+            upstream_translation.translations)
+
+    def test_split_translations(self):
+        """Split translations splits POTMsgSet and TranslationMessage."""
+        splitter = self.makeTranslationSplitter()
+        upstream_item, ubuntu_item = self.makeSharedPOTMsgSet(splitter)
+        upstream_message = self.factory.makeCurrentTranslationMessage(
+            potmsgset=upstream_item.potmsgset,
+            potemplate=upstream_item.potemplate)
+        splitter.split()
+        self.assertNotEqual(
+            list(upstream_item.potemplate), list(ubuntu_item.potemplate))
+        self.assertNotEqual(
+            list(upstream_item.potmsgset.getAllTranslationMessages()),
+            list(ubuntu_item.potmsgset.getAllTranslationMessages()),
+            )
+        self.assertEqual(
+            upstream_item.potmsgset.getAllTranslationMessages().count(),
+            ubuntu_item.potmsgset.getAllTranslationMessages().count(),
+        )

=== added file 'lib/lp/translations/utilities/translationsplitter.py'
--- lib/lp/translations/utilities/translationsplitter.py	1970-01-01 00:00:00 +0000
+++ lib/lp/translations/utilities/translationsplitter.py	2011-02-23 16:37:30 +0000
@@ -0,0 +1,86 @@
+# Copyright 2011 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+
+from storm.locals import ClassAlias, Store
+
+from lp.translations.model.potemplate import POTemplate
+from lp.translations.model.translationtemplateitem import (
+    TranslationTemplateItem,
+    )
+
+
+class TranslationSplitter:
+    """Split translations for a productseries, sourcepackage pair.
+
+    If a productseries and sourcepackage were linked in error, and then
+    unlinked, they may still share some translations.  This class breaks those
+    associations.
+    """
+
+    def __init__(self, productseries, sourcepackage):
+        """Constructor.
+
+        :param productseries: The `ProductSeries` to split from.
+        :param sourcepackage: The `SourcePackage` to split from.
+        """
+        self.productseries = productseries
+        self.sourcepackage = sourcepackage
+
+    def findShared(self):
+        """Provide tuples of upstream, ubuntu for each shared POTMsgSet."""
+        store = Store.of(self.productseries)
+        UpstreamItem = ClassAlias(TranslationTemplateItem, 'UpstreamItem')
+        UpstreamTemplate = ClassAlias(POTemplate, 'UpstreamTemplate')
+        UbuntuItem = ClassAlias(TranslationTemplateItem, 'UbuntuItem')
+        UbuntuTemplate = ClassAlias(POTemplate, 'UbuntuTemplate')
+        return store.find(
+            (UpstreamItem, UbuntuItem),
+            UpstreamItem.potmsgsetID == UbuntuItem.potmsgsetID,
+            UbuntuItem.potemplateID == UbuntuTemplate.id,
+            UbuntuTemplate.sourcepackagenameID ==
+                self.sourcepackage.sourcepackagename.id,
+            UbuntuTemplate.distroseriesID ==
+                self.sourcepackage.distroseries.id,
+            UpstreamItem.potemplateID == UpstreamTemplate.id,
+            UpstreamTemplate.productseriesID == self.productseries.id,
+        )
+
+    @staticmethod
+    def splitPOTMsgSet(ubuntu_item):
+        """Split the POTMsgSet for TranslationTemplateItem.
+
+        The specified `TranslationTemplateItem` will have a new `POTMsgSet`
+        that is a clone of the old one.  All other TranslationTemplateItems
+        will continue to use the old POTMsgSet.
+
+        :param ubuntu_item: The `TranslationTemplateItem` to use.
+        """
+        new_potmsgset = ubuntu_item.potmsgset.clone()
+        ubuntu_item.potmsgset = new_potmsgset
+        return new_potmsgset
+
+    @staticmethod
+    def migrateTranslations(upstream_msgset, ubuntu_item):
+        """Migrate the translations between potemplates.
+
+        :param upstream_msgset: The `POTMsgSet` to copy or move translations
+            from.
+        :param ubuntu_item: The target `TranslationTemplateItem`.
+            ubuntu_item.potmsgset is the msgset to attach translations to and
+            ubuntu_item.potemplate is used to determine whether to move a
+            diverged translation.
+        """
+        for message in upstream_msgset.getAllTranslationMessages():
+            if message.potemplate == ubuntu_item.potemplate:
+                message.potmsgset = ubuntu_item.potmsgset
+            elif not message.is_diverged:
+                message.clone(ubuntu_item.potmsgset)
+
+    def split(self):
+        """Split the translations for the ProductSeries and SourcePackage."""
+        for upstream_item, ubuntu_item in self.findShared():
+            self.splitPOTMsgSet(ubuntu_item)
+            self.migrateTranslations(upstream_item.potmsgset, ubuntu_item)