diff --git a/CRM/Core/Smarty.php b/CRM/Core/Smarty.php
index a656aa3c70955b01120529e89d75e4310edd990c..6422ca2c489ac59884a160adcaaff16ae972e333 100644
--- a/CRM/Core/Smarty.php
+++ b/CRM/Core/Smarty.php
@@ -159,6 +159,7 @@ class CRM_Core_Smarty extends Smarty {
     }
 
     $this->register_function('crmURL', array('CRM_Utils_System', 'crmURL'));
+    $this->load_filter('pre', 'resetExtScope');
   }
 
   /**
diff --git a/CRM/Core/Smarty/plugins/block.crmScope.php b/CRM/Core/Smarty/plugins/block.crmScope.php
new file mode 100644
index 0000000000000000000000000000000000000000..b02efd10fed2d72f62d9348f539387f7bcc8509e
--- /dev/null
+++ b/CRM/Core/Smarty/plugins/block.crmScope.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * Smarty block function to temporarily define variables.
+ *
+ * Example:
+ *
+ * @code
+ * {tsScope x=1}
+ *   Expect {$x}==1
+ *   {tsScope x=2}
+ *     Expect {$x}==2
+ *   {/tsScope}
+ *   Expect {$x}==1
+ * {/tsScope}
+ * @endcode
+ *
+ * @param array $params   must define 'name'
+ * @param string $content    Default content
+ * @param object $smarty  the Smarty object
+ *
+ * @return string
+ */
+function smarty_block_crmScope($params, $content, &$smarty, &$repeat) {
+  // A list of variables/values to save temporarily
+  static $backupFrames = array();
+
+  if ($repeat) {
+    // open crmScope
+    $vars = $smarty->get_template_vars();
+    $backupFrame = array();
+    foreach ($params as $key => $value) {
+      $backupFrame[$key] = isset($vars[$key]) ? $vars[$key] : NULL;
+    }
+    $backupFrames[] = $backupFrame;
+    _smarty_block_crmScope_applyFrame($smarty, $params);
+  }
+  else {
+    // close crmScope
+    _smarty_block_crmScope_applyFrame($smarty, array_pop($backupFrames));
+  }
+
+  return $content;
+}
+
+function _smarty_block_crmScope_applyFrame(&$smarty, $frame) {
+  foreach ($frame as $key => $value) {
+    $smarty->assign($key, $value);
+  }
+}
\ No newline at end of file
diff --git a/CRM/Core/Smarty/plugins/block.ts.php b/CRM/Core/Smarty/plugins/block.ts.php
index c0fb5f54be462cc8fb1a6e09097b0fa6992007cf..5d16be9c46ac4f0565b7cc30d5df2e131fa3dce8 100644
--- a/CRM/Core/Smarty/plugins/block.ts.php
+++ b/CRM/Core/Smarty/plugins/block.ts.php
@@ -47,6 +47,9 @@
  * @return string  the string, translated by gettext
  */
 function smarty_block_ts($params, $text, &$smarty) {
+  if (!isset($params['domain'])) {
+    $params['domain'] = $smarty->get_template_vars('extensionKey');
+  }
   return ts($text, $params);
 }
 
diff --git a/CRM/Core/Smarty/plugins/prefilter.resetExtScope.php b/CRM/Core/Smarty/plugins/prefilter.resetExtScope.php
new file mode 100644
index 0000000000000000000000000000000000000000..01b007009c5fd84cd01a16363170b2682bf06c09
--- /dev/null
+++ b/CRM/Core/Smarty/plugins/prefilter.resetExtScope.php
@@ -0,0 +1,12 @@
+<?php
+
+/**
+ * Wrap every Smarty template in a {crmScope} tag that sets the
+ * variable "extensionKey" to blank.
+ */
+function smarty_prefilter_resetExtScope($tpl_source, &$smarty) {
+  return
+    '{crmScope extensionKey=""}'
+    . $tpl_source
+    .'{/crmScope}';
+}
\ No newline at end of file
diff --git a/tests/phpunit/CRM/Core/Smarty/plugins/CrmScopeTest.php b/tests/phpunit/CRM/Core/Smarty/plugins/CrmScopeTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..56ce3d1165bef380f9c78af38b7ed18429e56557
--- /dev/null
+++ b/tests/phpunit/CRM/Core/Smarty/plugins/CrmScopeTest.php
@@ -0,0 +1,39 @@
+<?php
+
+require_once 'CiviTest/CiviUnitTestCase.php';
+
+class CRM_Core_Smarty_plugins_CrmScopeTest extends CiviUnitTestCase {
+  function setUp() {
+    parent::setUp();
+    require_once 'CRM/Core/Smarty.php';
+
+    // Templates should normally be file names, but for unit-testing it's handy to use "string:" notation
+    require_once 'CRM/Core/Smarty/resources/String.php';
+    civicrm_smarty_register_string_resource();
+  }
+
+  function scopeCases() {
+    $cases = array();
+    $cases[] = array('', '{crmScope}{/crmScope}');
+    $cases[] = array('', '{crmScope x=1}{/crmScope}');
+    $cases[] = array('x=', 'x={$x}');
+    $cases[] = array('x=1', '{crmScope x=1}x={$x}{/crmScope}');
+    $cases[] = array('x=1', '{$x}{crmScope x=1}x={$x}{/crmScope}{$x}');
+    $cases[] = array('x=1 x=2 x=1', '{crmScope x=1}x={$x} {crmScope x=2}x={$x}{/crmScope} x={$x}{/crmScope}');
+    $cases[] = array('x=1 x=2 x=3 x=2 x=1', '{crmScope x=1}x={$x} {crmScope x=2}x={$x} {crmScope x=3}x={$x}{/crmScope} x={$x}{/crmScope} x={$x}{/crmScope}');
+    $cases[] = array('x=1,y=9', '{crmScope x=1 y=9}x={$x},y={$y}{/crmScope}');
+    $cases[] = array('x=1,y=9 x=1,y=8 x=1,y=9', '{crmScope x=1 y=9}x={$x},y={$y} {crmScope y=8}x={$x},y={$y}{/crmScope} x={$x},y={$y}{/crmScope}');
+    $cases[] = array('x=', 'x={$x}');
+    return $cases;
+  }
+
+  /**
+   * @dataProvider scopeCases
+   */
+  function testBlank($expected, $input) {
+    $smarty = CRM_Core_Smarty::singleton();
+    $actual = $smarty->fetch('string:' . $input);
+    $this->assertEquals($expected, $actual, "Process input=[$input]");
+  }
+
+}