killbill-aplcache

Changes

invoice/src/main/java/org/killbill/billing/invoice/template/translator/InvoiceStrings.java 63(+0 -63)

Details

diff --git a/api/src/main/java/org/killbill/billing/invoice/api/formatters/InvoiceFormatterFactory.java b/api/src/main/java/org/killbill/billing/invoice/api/formatters/InvoiceFormatterFactory.java
index 3a2cdff..7601ae7 100644
--- a/api/src/main/java/org/killbill/billing/invoice/api/formatters/InvoiceFormatterFactory.java
+++ b/api/src/main/java/org/killbill/billing/invoice/api/formatters/InvoiceFormatterFactory.java
@@ -18,10 +18,13 @@ package org.killbill.billing.invoice.api.formatters;
 
 import java.util.Locale;
 
+import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.currency.api.CurrencyConversionApi;
 import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.util.template.translation.TranslatorConfig;
 
 public interface InvoiceFormatterFactory {
-    public InvoiceFormatter createInvoiceFormatter(TranslatorConfig config, Invoice invoice, Locale locale, CurrencyConversionApi currencyConversionApi);
+
+    public InvoiceFormatter createInvoiceFormatter(TranslatorConfig config, Invoice invoice, Locale locale, CurrencyConversionApi currencyConversionApi,
+                                                   ResourceBundleFactory bundleFactory, InternalTenantContext context);
 }
diff --git a/api/src/main/java/org/killbill/billing/invoice/api/formatters/ResourceBundleFactory.java b/api/src/main/java/org/killbill/billing/invoice/api/formatters/ResourceBundleFactory.java
new file mode 100644
index 0000000..2c6a86e
--- /dev/null
+++ b/api/src/main/java/org/killbill/billing/invoice/api/formatters/ResourceBundleFactory.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2014 Groupon, Inc
+ * Copyright 2014 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.api.formatters;
+
+import java.util.Locale;
+import java.util.ResourceBundle;
+
+import org.killbill.billing.callcontext.InternalTenantContext;
+
+public interface ResourceBundleFactory {
+
+    public enum ResourceBundleType {
+        INVOICE_TRANSLATION,
+        CATALOG_TRANSLATION
+    }
+
+    public ResourceBundle createBundle(Locale locale, String bundlePath, ResourceBundleType type, InternalTenantContext tenantContext);
+}
diff --git a/api/src/main/java/org/killbill/billing/tenant/api/TenantInternalApi.java b/api/src/main/java/org/killbill/billing/tenant/api/TenantInternalApi.java
index c1ff94a..b5adc7c 100644
--- a/api/src/main/java/org/killbill/billing/tenant/api/TenantInternalApi.java
+++ b/api/src/main/java/org/killbill/billing/tenant/api/TenantInternalApi.java
@@ -18,11 +18,21 @@
 package org.killbill.billing.tenant.api;
 
 import java.util.List;
+import java.util.Locale;
 
 import org.killbill.billing.callcontext.InternalTenantContext;
 
 public interface TenantInternalApi {
+
     public List<String> getTenantCatalogs(InternalTenantContext tenantContext);
 
     public String getTenantOverdueConfig(InternalTenantContext tenantContext);
+
+    public String getInvoiceTemplate(Locale locale, InternalTenantContext tenantContext);
+
+    public String getManualPayInvoiceTemplate(Locale locale, InternalTenantContext tenantContext);
+
+    public String getInvoiceTranslation(Locale locale, InternalTenantContext tenantContext);
+
+    public String getCatalogTranslation(Locale locale, InternalTenantContext tenantContext);
 }
diff --git a/api/src/main/java/org/killbill/billing/util/template/translation/Translator.java b/api/src/main/java/org/killbill/billing/util/template/translation/Translator.java
index 015cc26..70ce8a4 100644
--- a/api/src/main/java/org/killbill/billing/util/template/translation/Translator.java
+++ b/api/src/main/java/org/killbill/billing/util/template/translation/Translator.java
@@ -19,5 +19,5 @@ package org.killbill.billing.util.template.translation;
 import java.util.Locale;
 
 public interface Translator {
-    public String getTranslation(Locale locale, String originalText);
+    public String getTranslation(String originalText);
 }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
index a8b7364..2118220 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
@@ -359,7 +359,7 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
             }
         }
 
-        HtmlInvoice htmlInvoice = generator.generateInvoice(account, invoice, manualPay);
+        HtmlInvoice htmlInvoice = generator.generateInvoice(account, invoice, manualPay, internalContext);
         return htmlInvoice.getBody();
     }
 
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/glue/DefaultInvoiceModule.java b/invoice/src/main/java/org/killbill/billing/invoice/glue/DefaultInvoiceModule.java
index 268c9dc..1b56ec7 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/glue/DefaultInvoiceModule.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/glue/DefaultInvoiceModule.java
@@ -44,6 +44,8 @@ import org.killbill.billing.invoice.notification.NextBillingDateNotifier;
 import org.killbill.billing.invoice.notification.NextBillingDatePoster;
 import org.killbill.billing.invoice.notification.NullInvoiceNotifier;
 import org.killbill.billing.invoice.plugin.api.InvoicePluginApi;
+import org.killbill.billing.invoice.template.bundles.DefaultResourceBundleFactory;
+import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory;
 import org.killbill.billing.osgi.api.OSGIServiceRegistration;
 import org.killbill.billing.platform.api.KillbillConfigSource;
 import org.killbill.billing.util.config.InvoiceConfig;
@@ -89,6 +91,10 @@ public class DefaultInvoiceModule extends KillBillModule implements InvoiceModul
         bind(InvoiceService.class).to(DefaultInvoiceService.class).asEagerSingleton();
     }
 
+    protected void installResourceBundleFactory() {
+        bind(ResourceBundleFactory.class).to(DefaultResourceBundleFactory.class).asEagerSingleton();
+    }
+
     @Override
     public void installInvoiceMigrationApi() {
         bind(InvoiceMigrationApi.class).to(DefaultInvoiceMigrationApi.class).asEagerSingleton();
@@ -142,5 +148,6 @@ public class DefaultInvoiceModule extends KillBillModule implements InvoiceModul
         installInvoiceInternalApi();
         installInvoicePaymentApi();
         installInvoiceMigrationApi();
+        installResourceBundleFactory();
     }
 }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/notification/EmailInvoiceNotifier.java b/invoice/src/main/java/org/killbill/billing/invoice/notification/EmailInvoiceNotifier.java
index ae7f044..b96220b 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/notification/EmailInvoiceNotifier.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/notification/EmailInvoiceNotifier.java
@@ -88,7 +88,7 @@ public class EmailInvoiceNotifier implements InvoiceNotifier {
 
         final HtmlInvoice htmlInvoice;
         try {
-            htmlInvoice = generator.generateInvoice(account, invoice, manualPay);
+            htmlInvoice = generator.generateInvoice(account, invoice, manualPay, internalTenantContext);
         } catch (IOException e) {
             throw new InvoiceApiException(e, ErrorCode.EMAIL_SENDING_FAILED);
         }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/bundles/DefaultResourceBundleFactory.java b/invoice/src/main/java/org/killbill/billing/invoice/template/bundles/DefaultResourceBundleFactory.java
new file mode 100644
index 0000000..3a6fc9f
--- /dev/null
+++ b/invoice/src/main/java/org/killbill/billing/invoice/template/bundles/DefaultResourceBundleFactory.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2014 Groupon, Inc
+ * Copyright 2014 The Billing Project, LLC
+ *
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.killbill.billing.invoice.template.bundles;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
+import java.util.Locale;
+import java.util.MissingResourceException;
+import java.util.PropertyResourceBundle;
+import java.util.ResourceBundle;
+
+import javax.inject.Inject;
+
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory;
+import org.killbill.billing.tenant.api.TenantInternalApi;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.xmlloader.UriAccessor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.base.Charsets;
+
+public class DefaultResourceBundleFactory implements ResourceBundleFactory {
+
+    private static final Logger logger = LoggerFactory.getLogger(DefaultResourceBundleFactory.class);
+
+    private final TenantInternalApi tenantApi;
+
+    @Inject
+    public DefaultResourceBundleFactory(final TenantInternalApi tenantApi) {
+        this.tenantApi = tenantApi;
+    }
+
+    @Override
+    public ResourceBundle createBundle(final Locale locale, final String bundlePath, final ResourceBundleType type, final InternalTenantContext tenantContext) {
+        if (tenantContext.getTenantRecordId() == InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID) {
+            return getGlobalBundle(locale, bundlePath);
+        }
+        final String bundle = getTenantBundleForType(locale, type, tenantContext);
+        if (bundle != null) {
+            try {
+                return new PropertyResourceBundle(new ByteArrayInputStream(bundle.getBytes(Charsets.UTF_8)));
+            } catch (IOException e) {
+                logger.warn("Failed to de-serialize the property bundle for tenant {} and locale {}", tenantContext.getTenantRecordId(), locale);
+                // Fall through...
+            }
+        }
+        return getGlobalBundle(locale, bundlePath);
+    }
+
+    private String getTenantBundleForType(final Locale locale, final ResourceBundleType type, final InternalTenantContext tenantContext) {
+        switch (type) {
+            case CATALOG_TRANSLATION:
+                return tenantApi.getCatalogTranslation(locale, tenantContext);
+
+            case INVOICE_TRANSLATION:
+                return tenantApi.getInvoiceTranslation(locale, tenantContext);
+
+            default:
+                logger.warn("Unexpected bundle type {} ", type);
+                return null;
+        }
+    }
+
+    private ResourceBundle getGlobalBundle(final Locale locale, final String bundlePath) {
+        try {
+            // Try to load the bundle from the classpath first
+            return ResourceBundle.getBundle(bundlePath, locale);
+        } catch (MissingResourceException ignored) {
+        }
+        // Try to load it from a properties file
+        final String propertiesFileNameWithCountry = bundlePath + "_" + locale.getLanguage() + "_" + locale.getCountry() + ".properties";
+        ResourceBundle bundle = getBundleFromPropertiesFile(propertiesFileNameWithCountry);
+        if (bundle != null) {
+            return bundle;
+        } else {
+            final String propertiesFileName = bundlePath + "_" + locale.getLanguage() + ".properties";
+            bundle = getBundleFromPropertiesFile(propertiesFileName);
+        }
+
+        return bundle;
+    }
+
+    private ResourceBundle getBundleFromPropertiesFile(final String propertiesFileName) {
+        try {
+            final InputStream inputStream = UriAccessor.accessUri(propertiesFileName);
+            if (inputStream == null) {
+                return null;
+            } else {
+                return new PropertyResourceBundle(inputStream);
+            }
+        } catch (IllegalArgumentException iae) {
+            return null;
+        } catch (MissingResourceException mrex) {
+            return null;
+        } catch (URISyntaxException e) {
+            return null;
+        } catch (IOException e) {
+            return null;
+        }
+    }
+}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatter.java b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatter.java
index b741c56..8ae278a 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatter.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatter.java
@@ -29,6 +29,9 @@ import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
 import org.joda.time.format.DateTimeFormat;
 import org.joda.time.format.DateTimeFormatter;
+import org.killbill.billing.callcontext.InternalTenantContext;
+import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory;
+import org.killbill.billing.tenant.api.TenantInternalApi;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -65,13 +68,17 @@ public class DefaultInvoiceFormatter implements InvoiceFormatter {
     private final DateTimeFormatter dateFormatter;
     private final Locale locale;
     private final CurrencyConversionApi currencyConversionApi;
+    private final InternalTenantContext context;
+    private final ResourceBundleFactory bundleFactory;
 
-    public DefaultInvoiceFormatter(final TranslatorConfig config, final Invoice invoice, final Locale locale, final CurrencyConversionApi currencyConversionApi) {
+    public DefaultInvoiceFormatter(final TranslatorConfig config, final Invoice invoice, final Locale locale, final CurrencyConversionApi currencyConversionApi, final ResourceBundleFactory bundleFactory, final InternalTenantContext context) {
         this.config = config;
         this.invoice = invoice;
-        dateFormatter = DateTimeFormat.mediumDate().withLocale(locale);
+        this.dateFormatter = DateTimeFormat.mediumDate().withLocale(locale);
         this.locale = locale;
         this.currencyConversionApi = currencyConversionApi;
+        this.bundleFactory = bundleFactory;
+        this.context = context;
     }
 
     @Override
@@ -109,7 +116,7 @@ public class DefaultInvoiceFormatter implements InvoiceFormatter {
 
         final List<InvoiceItem> formatters = new ArrayList<InvoiceItem>();
         for (final InvoiceItem item : invoiceItems) {
-            formatters.add(new DefaultInvoiceItemFormatter(config, item, dateFormatter, locale));
+            formatters.add(new DefaultInvoiceItemFormatter(config, item, dateFormatter, locale, context, bundleFactory));
         }
         return formatters;
     }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatterFactory.java b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatterFactory.java
index b1212aa..890fb30 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatterFactory.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatterFactory.java
@@ -18,16 +18,20 @@ package org.killbill.billing.invoice.template.formatters;
 
 import java.util.Locale;
 
+import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.currency.api.CurrencyConversionApi;
 import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.invoice.api.formatters.InvoiceFormatter;
 import org.killbill.billing.invoice.api.formatters.InvoiceFormatterFactory;
+import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory;
+import org.killbill.billing.tenant.api.TenantInternalApi;
 import org.killbill.billing.util.template.translation.TranslatorConfig;
 
 public class DefaultInvoiceFormatterFactory implements InvoiceFormatterFactory {
 
     @Override
-    public InvoiceFormatter createInvoiceFormatter(final TranslatorConfig config, final Invoice invoice, final Locale locale, CurrencyConversionApi currencyConversionApi) {
-        return new DefaultInvoiceFormatter(config, invoice, locale, currencyConversionApi);
+    public InvoiceFormatter createInvoiceFormatter(final TranslatorConfig config, final Invoice invoice, final Locale locale, CurrencyConversionApi currencyConversionApi,
+                                                   final ResourceBundleFactory bundleFactory, final InternalTenantContext context) {
+        return new DefaultInvoiceFormatter(config, invoice, locale, currencyConversionApi, bundleFactory, context);
     }
 }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java
index ef0cca6..51ddcc6 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java
@@ -19,19 +19,25 @@ package org.killbill.billing.invoice.template.formatters;
 import java.math.BigDecimal;
 import java.text.NumberFormat;
 import java.util.Locale;
+import java.util.ResourceBundle;
 import java.util.UUID;
 
 import org.joda.time.DateTime;
 import org.joda.time.LocalDate;
 import org.joda.time.format.DateTimeFormatter;
-
+import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.invoice.api.InvoiceItem;
 import org.killbill.billing.invoice.api.InvoiceItemType;
 import org.killbill.billing.invoice.api.formatters.InvoiceItemFormatter;
+import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory;
+import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory.ResourceBundleType;
+import org.killbill.billing.util.LocaleUtils;
 import org.killbill.billing.util.template.translation.DefaultCatalogTranslator;
 import org.killbill.billing.util.template.translation.Translator;
 import org.killbill.billing.util.template.translation.TranslatorConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.google.common.base.Objects;
 import com.google.common.base.Strings;
@@ -49,12 +55,18 @@ public class DefaultInvoiceItemFormatter implements InvoiceItemFormatter {
     private final DateTimeFormatter dateFormatter;
     private final Locale locale;
 
-    public DefaultInvoiceItemFormatter(final TranslatorConfig config, final InvoiceItem item, final DateTimeFormatter dateFormatter, final Locale locale) {
+    public DefaultInvoiceItemFormatter(final TranslatorConfig config,
+                                       final InvoiceItem item,
+                                       final DateTimeFormatter dateFormatter,
+                                       final Locale locale,
+                                       final InternalTenantContext context,
+                                       final ResourceBundleFactory bundleFactory) {
         this.item = item;
         this.dateFormatter = dateFormatter;
         this.locale = locale;
-
-        this.translator = new DefaultCatalogTranslator(config);
+        final ResourceBundle bundle = bundleFactory.createBundle(locale, config.getCatalogBundlePath(), ResourceBundleType.CATALOG_TRANSLATION, context);
+        final ResourceBundle defaultBundle = bundleFactory.createBundle(LocaleUtils.toLocale(config.getDefaultLocale()), config.getCatalogBundlePath(), ResourceBundleType.CATALOG_TRANSLATION, context);
+        this.translator = new DefaultCatalogTranslator(bundle, defaultBundle);
     }
 
     @Override
@@ -126,17 +138,17 @@ public class DefaultInvoiceItemFormatter implements InvoiceItemFormatter {
 
     @Override
     public String getPlanName() {
-        return Strings.nullToEmpty(translator.getTranslation(locale, item.getPlanName()));
+        return Strings.nullToEmpty(translator.getTranslation(item.getPlanName()));
     }
 
     @Override
     public String getPhaseName() {
-        return Strings.nullToEmpty(translator.getTranslation(locale, item.getPhaseName()));
+        return Strings.nullToEmpty(translator.getTranslation(item.getPhaseName()));
     }
 
     @Override
     public String getUsageName() {
-        return Strings.nullToEmpty(translator.getTranslation(locale, item.getUsageName()));
+        return Strings.nullToEmpty(translator.getTranslation(item.getUsageName()));
     }
 
     @Override
@@ -168,4 +180,5 @@ public class DefaultInvoiceItemFormatter implements InvoiceItemFormatter {
     public boolean matches(final Object other) {
         throw new UnsupportedOperationException();
     }
+
 }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/HtmlInvoiceGenerator.java b/invoice/src/main/java/org/killbill/billing/invoice/template/HtmlInvoiceGenerator.java
index e04a8f1..3e0e0bc 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/template/HtmlInvoiceGenerator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/template/HtmlInvoiceGenerator.java
@@ -17,21 +17,31 @@
 package org.killbill.billing.invoice.template;
 
 import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
+import java.util.ResourceBundle;
 
 import javax.annotation.Nullable;
 
 import org.killbill.billing.account.api.Account;
+import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.currency.api.CurrencyConversionApi;
 import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.invoice.api.formatters.InvoiceFormatter;
 import org.killbill.billing.invoice.api.formatters.InvoiceFormatterFactory;
+import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory;
+import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory.ResourceBundleType;
 import org.killbill.billing.invoice.template.translator.DefaultInvoiceTranslator;
+import org.killbill.billing.tenant.api.TenantInternalApi;
 import org.killbill.billing.util.LocaleUtils;
+import org.killbill.billing.util.callcontext.InternalCallContextFactory;
 import org.killbill.billing.util.email.templates.TemplateEngine;
+import org.killbill.billing.util.io.IOUtils;
 import org.killbill.billing.util.template.translation.TranslatorConfig;
+import org.killbill.xmlloader.UriAccessor;
 
 import com.google.common.base.Strings;
 import com.google.inject.Inject;
@@ -39,46 +49,75 @@ import com.google.inject.Inject;
 public class HtmlInvoiceGenerator {
 
     private final InvoiceFormatterFactory factory;
-    private final TemplateEngine templateEngine;
     private final TranslatorConfig config;
     private final CurrencyConversionApi currencyConversionApi;
+    private final TemplateEngine templateEngine;
+    private final TenantInternalApi tenantApi;
+    private final ResourceBundleFactory bundleFactory;
 
     @Inject
-    public HtmlInvoiceGenerator(final InvoiceFormatterFactory factory, final TemplateEngine templateEngine,
-                                final TranslatorConfig config, final CurrencyConversionApi currencyConversionApi) {
+    public HtmlInvoiceGenerator(final InvoiceFormatterFactory factory,
+                                final TemplateEngine templateEngine,
+                                final TranslatorConfig config,
+                                final CurrencyConversionApi currencyConversionApi,
+                                final ResourceBundleFactory bundleFactory,
+                                final TenantInternalApi tenantInternalApi) {
         this.factory = factory;
-        this.templateEngine = templateEngine;
         this.config = config;
         this.currencyConversionApi = currencyConversionApi;
+        this.templateEngine = templateEngine;
+        this.bundleFactory = bundleFactory;
+        this.tenantApi = tenantInternalApi;
     }
 
-    public HtmlInvoice generateInvoice(final Account account, @Nullable final Invoice invoice, final boolean manualPay) throws IOException {
+    public HtmlInvoice generateInvoice(final Account account, @Nullable final Invoice invoice, final boolean manualPay, final InternalTenantContext context) throws IOException {
         // Don't do anything if the invoice is null
         if (invoice == null) {
             return null;
         }
 
-        HtmlInvoice invoiceData = new HtmlInvoice();
-        final Map<String, Object> data = new HashMap<String, Object>();
-        final DefaultInvoiceTranslator invoiceTranslator = new DefaultInvoiceTranslator(config);
         final String accountLocale = Strings.emptyToNull(account.getLocale());
-        // If no Locale is defined, use the default JVM one
         final Locale locale = accountLocale == null ? Locale.getDefault() : LocaleUtils.toLocale(accountLocale);
-        invoiceTranslator.setLocale(locale);
+
+        final HtmlInvoice invoiceData = new HtmlInvoice();
+        final Map<String, Object> data = new HashMap<String, Object>();
+
+        final ResourceBundle invoiceBundle = accountLocale != null ?
+                                             bundleFactory.createBundle(LocaleUtils.toLocale(accountLocale), config.getInvoiceTemplateBundlePath(), ResourceBundleType.INVOICE_TRANSLATION, context) : null;
+        final ResourceBundle defaultInvoiceBundle = bundleFactory.createBundle(Locale.getDefault(), config.getInvoiceTemplateBundlePath(), ResourceBundleType.INVOICE_TRANSLATION, context);
+        final DefaultInvoiceTranslator invoiceTranslator = new DefaultInvoiceTranslator(invoiceBundle, defaultInvoiceBundle);
+
         data.put("text", invoiceTranslator);
         data.put("account", account);
 
-        final InvoiceFormatter formattedInvoice = factory.createInvoiceFormatter(config, invoice, locale, currencyConversionApi);
+        final InvoiceFormatter formattedInvoice = factory.createInvoiceFormatter(config, invoice, locale, currencyConversionApi, bundleFactory, context);
         data.put("invoice", formattedInvoice);
 
         invoiceData.setSubject(invoiceTranslator.getInvoiceEmailSubject());
+        final String templateText = getTemplateText(locale, manualPay, context);
+        invoiceData.setBody(templateEngine.executeTemplateText(templateText, data));
+        return invoiceData;
+    }
+
+    private String getTemplateText(final Locale locale, final boolean manualPay, final InternalTenantContext context) throws IOException {
 
-        if (manualPay) {
-            invoiceData.setBody(templateEngine.executeTemplate(config.getManualPayTemplateName(), data));
-        } else {
-            invoiceData.setBody(templateEngine.executeTemplate(config.getTemplateName(), data));
+        if (context.getTenantRecordId() == InternalCallContextFactory.INTERNAL_TENANT_RECORD_ID) {
+            return getDefaultTemplate(manualPay ? config.getManualPayTemplateName() : config.getTemplateName());
         }
+        final String template = manualPay ?
+                                tenantApi.getManualPayInvoiceTemplate(locale, context) :
+                                tenantApi.getInvoiceTemplate(locale, context);
+        return template == null ?
+               getDefaultTemplate(manualPay ? config.getManualPayTemplateName() : config.getTemplateName()) :
+               template;
+    }
 
-        return invoiceData;
+    private String getDefaultTemplate(final String templateName) throws IOException {
+        try {
+            final InputStream templateStream = UriAccessor.accessUri(templateName);
+            return IOUtils.toString(templateStream);
+        } catch (URISyntaxException e) {
+            throw new IOException(e);
+        }
     }
 }
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/translator/DefaultInvoiceTranslator.java b/invoice/src/main/java/org/killbill/billing/invoice/template/translator/DefaultInvoiceTranslator.java
index 4bb5ef1..451665d 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/template/translator/DefaultInvoiceTranslator.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/template/translator/DefaultInvoiceTranslator.java
@@ -16,139 +16,99 @@
 
 package org.killbill.billing.invoice.template.translator;
 
-import java.util.Locale;
+import java.util.ResourceBundle;
 
 import org.killbill.billing.util.template.translation.DefaultTranslatorBase;
 import org.killbill.billing.util.template.translation.TranslatorConfig;
 
-import com.google.inject.Inject;
+public class DefaultInvoiceTranslator extends DefaultTranslatorBase {
 
-public class DefaultInvoiceTranslator extends DefaultTranslatorBase implements InvoiceStrings {
-
-    private Locale locale;
-
-    @Inject
-    public DefaultInvoiceTranslator(final TranslatorConfig config) {
-        super(config);
-    }
-
-    public void setLocale(final Locale locale) {
-        this.locale = locale;
-    }
-
-    @Override
-    protected String getBundlePath() {
-        return config.getInvoiceTemplateBundlePath();
-    }
-
-    @Override
-    protected String getTranslationType() {
-        return "invoice";
+    public DefaultInvoiceTranslator(final ResourceBundle bundle, final ResourceBundle defaultBundle) {
+        super(bundle, defaultBundle);
     }
 
-    @Override
     public String getInvoiceEmailSubject() {
-        String subject = getTranslation(locale, "invoiceEmailSubject");
+        String subject = getTranslation("invoiceEmailSubject");
         return (!"invoiceEmailSubject".equals(subject)) ? subject : null;
     }
 
-    @Override
     public String getInvoiceTitle() {
-        return getTranslation(locale, "invoiceTitle");
+        return getTranslation("invoiceTitle");
     }
 
-    @Override
     public String getInvoiceDate() {
-        return getTranslation(locale, "invoiceDate");
+        return getTranslation("invoiceDate");
     }
 
-    @Override
     public String getInvoiceNumber() {
-        return getTranslation(locale, "invoiceNumber");
+        return getTranslation("invoiceNumber");
     }
 
-    @Override
     public String getAccountOwnerName() {
-        return getTranslation(locale, "accountOwnerName");
+        return getTranslation("accountOwnerName");
     }
 
-    @Override
     public String getAccountOwnerEmail() {
-        return getTranslation(locale, "accountOwnerEmail");
+        return getTranslation("accountOwnerEmail");
     }
 
-    @Override
     public String getAccountOwnerPhone() {
-        return getTranslation(locale, "accountOwnerPhone");
+        return getTranslation("accountOwnerPhone");
     }
 
-    @Override
     public String getCompanyName() {
-        return getTranslation(locale, "companyName");
+        return getTranslation("companyName");
     }
 
-    @Override
     public String getCompanyAddress() {
-        return getTranslation(locale, "companyAddress");
+        return getTranslation("companyAddress");
     }
 
-    @Override
     public String getCompanyCityProvincePostalCode() {
-        return getTranslation(locale, "companyCityProvincePostalCode");
+        return getTranslation("companyCityProvincePostalCode");
     }
 
-    @Override
     public String getCompanyCountry() {
-        return getTranslation(locale, "companyCountry");
+        return getTranslation("companyCountry");
     }
 
-    @Override
     public String getCompanyUrl() {
-        return getTranslation(locale, "companyUrl");
+        return getTranslation("companyUrl");
     }
 
-    @Override
     public String getInvoiceItemBundleName() {
-        return getTranslation(locale, "invoiceItemBundleName");
+        return getTranslation("invoiceItemBundleName");
     }
 
-    @Override
     public String getInvoiceItemDescription() {
-        return getTranslation(locale, "invoiceItemDescription");
+        return getTranslation("invoiceItemDescription");
     }
 
-    @Override
     public String getInvoiceItemServicePeriod() {
-        return getTranslation(locale, "invoiceItemServicePeriod");
+        return getTranslation("invoiceItemServicePeriod");
     }
 
-    @Override
     public String getInvoiceItemAmount() {
-        return getTranslation(locale, "invoiceItemAmount");
+        return getTranslation("invoiceItemAmount");
     }
 
-    @Override
     public String getInvoiceAmount() {
-        return getTranslation(locale, "invoiceAmount");
+        return getTranslation("invoiceAmount");
     }
 
-    @Override
     public String getInvoiceAmountPaid() {
-        return getTranslation(locale, "invoiceAmountPaid");
+        return getTranslation("invoiceAmountPaid");
     }
 
-    @Override
     public String getInvoiceBalance() {
-        return getTranslation(locale, "invoiceBalance");
+        return getTranslation("invoiceBalance");
     }
 
-    @Override
     public String getProcessedPaymentCurrency() {
-        return getTranslation(locale, "processedPaymentCurrency");
+        return getTranslation("processedPaymentCurrency");
     }
 
-    @Override
     public String getProcessedPaymentRate() {
-        return getTranslation(locale, "processedPaymentRate");
+        return getTranslation("processedPaymentRate");
     }
 }
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java b/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java
index aa62f70..3db8940 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/InvoiceTestSuiteNoDB.java
@@ -25,6 +25,7 @@ import org.killbill.billing.invoice.api.InvoiceInternalApi;
 import org.killbill.billing.invoice.api.InvoiceMigrationApi;
 import org.killbill.billing.invoice.api.InvoicePaymentApi;
 import org.killbill.billing.invoice.api.InvoiceUserApi;
+import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory;
 import org.killbill.billing.invoice.dao.InvoiceDao;
 import org.killbill.billing.invoice.generator.InvoiceGenerator;
 import org.killbill.billing.invoice.glue.TestInvoiceModuleNoDB;
@@ -91,6 +92,8 @@ public abstract class InvoiceTestSuiteNoDB extends GuicyKillbillTestSuiteNoDB {
     protected CurrencyConversionApi currencyConversionApi;
     @Inject
     protected UsageUserApi usageUserApi;
+    @Inject
+    protected ResourceBundleFactory resourceBundleFactory;
 
     @Override
     protected KillbillConfigSource getConfigSource() {
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java b/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java
index 6000893..5e18596 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java
@@ -23,6 +23,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.ResourceBundle;
 import java.util.UUID;
 
 import org.joda.time.LocalDate;
@@ -34,6 +35,7 @@ import org.killbill.billing.invoice.api.InvoiceItemType;
 import org.killbill.billing.invoice.api.InvoicePaymentType;
 import org.killbill.billing.invoice.api.formatters.InvoiceFormatter;
 import org.killbill.billing.invoice.api.formatters.InvoiceFormatterFactory;
+import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory.ResourceBundleType;
 import org.killbill.billing.invoice.model.CreditAdjInvoiceItem;
 import org.killbill.billing.invoice.model.CreditBalanceAdjInvoiceItem;
 import org.killbill.billing.invoice.model.DefaultInvoice;
@@ -87,7 +89,7 @@ public class TestDefaultInvoiceFormatter extends InvoiceTestSuiteNoDB {
         Assert.assertEquals(invoice.getCreditedAmount().doubleValue(), 0.00);
 
         // Verify the merge
-        final InvoiceFormatter formatter = new DefaultInvoiceFormatter(config, invoice, Locale.US, null);
+        final InvoiceFormatter formatter = new DefaultInvoiceFormatter(config, invoice, Locale.US, null, resourceBundleFactory, internalCallContext);
         final List<InvoiceItem> invoiceItems = formatter.getInvoiceItems();
         Assert.assertEquals(invoiceItems.size(), 1);
         Assert.assertEquals(invoiceItems.get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
@@ -141,7 +143,7 @@ public class TestDefaultInvoiceFormatter extends InvoiceTestSuiteNoDB {
         Assert.assertEquals(invoice.getRefundedAmount().doubleValue(), -1.00);
 
         // Verify the merge
-        final InvoiceFormatter formatter = new DefaultInvoiceFormatter(config, invoice, Locale.US, null);
+        final InvoiceFormatter formatter = new DefaultInvoiceFormatter(config, invoice, Locale.US, null, resourceBundleFactory, internalCallContext);
         final List<InvoiceItem> invoiceItems = formatter.getInvoiceItems();
         Assert.assertEquals(invoiceItems.size(), 4);
         Assert.assertEquals(invoiceItems.get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
@@ -320,41 +322,14 @@ public class TestDefaultInvoiceFormatter extends InvoiceTestSuiteNoDB {
 
         final Map<String, Object> data = new HashMap<String, Object>();
 
-        final DefaultInvoiceTranslator translator = new DefaultInvoiceTranslator(new TranslatorConfig() {
-            @Override
-            public String getDefaultLocale() {
-                return "en_US";
-            }
+        final String bundlePath = "org/killbill/billing/util/template/translation/InvoiceTranslation";
+        final ResourceBundle bundle = resourceBundleFactory.createBundle(Locale.US, bundlePath, ResourceBundleType.INVOICE_TRANSLATION, internalCallContext);
 
-            @Override
-            public String getCatalogBundlePath() {
-                return null;
-            }
+        final DefaultInvoiceTranslator translator = new DefaultInvoiceTranslator(bundle, null);
 
-            @Override
-            public String getInvoiceTemplateBundlePath() {
-                return "org/killbill/billing/util/template/translation/InvoiceTranslation";
-            }
-
-            @Override
-            public String getTemplateName() {
-                return null;
-            }
-
-            @Override
-            public String getManualPayTemplateName() {
-                return null;
-            }
-
-            @Override
-            public Class<? extends InvoiceFormatterFactory> getInvoiceFormatterFactoryClass() {
-                return null;
-            }
-        });
-        translator.setLocale(Locale.US);
         data.put("text", translator);
 
-        data.put("invoice", new DefaultInvoiceFormatter(config, invoice, Locale.US, currencyConversionApi));
+        data.put("invoice", new DefaultInvoiceFormatter(config, invoice, Locale.US, currencyConversionApi, resourceBundleFactory, internalCallContext));
 
         final String formattedText = templateEngine.executeTemplateText(template, data);
 
@@ -363,7 +338,7 @@ public class TestDefaultInvoiceFormatter extends InvoiceTestSuiteNoDB {
 
     private void checkOutput(final Invoice invoice, final String template, final String expected, final Locale locale) {
         final Map<String, Object> data = new HashMap<String, Object>();
-        data.put("invoice", new DefaultInvoiceFormatter(config, invoice, locale, null));
+        data.put("invoice", new DefaultInvoiceFormatter(config, invoice, locale, null, resourceBundleFactory, internalCallContext));
 
         final String formattedText = templateEngine.executeTemplateText(template, data);
         Assert.assertEquals(formattedText, expected);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceItemFormatter.java b/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceItemFormatter.java
index ca988e3..fbac29d 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceItemFormatter.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceItemFormatter.java
@@ -111,7 +111,7 @@ public class TestDefaultInvoiceItemFormatter extends InvoiceTestSuiteNoDB {
 
     private void checkOutput(final InvoiceItem invoiceItem, final String template, final String expected, final Locale locale) {
         final Map<String, Object> data = new HashMap<String, Object>();
-        data.put("invoiceItem", new DefaultInvoiceItemFormatter(config, invoiceItem,  DateTimeFormat.mediumDate().withLocale(locale), locale));
+        data.put("invoiceItem", new DefaultInvoiceItemFormatter(config, invoiceItem,  DateTimeFormat.mediumDate().withLocale(locale), locale, internalCallContext, resourceBundleFactory));
 
         final String formattedText = templateEngine.executeTemplateText(template, data);
         Assert.assertEquals(formattedText, expected);
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/TestHtmlInvoiceGenerator.java b/invoice/src/test/java/org/killbill/billing/invoice/TestHtmlInvoiceGenerator.java
index 53f096f..70b1cc1 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/TestHtmlInvoiceGenerator.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/TestHtmlInvoiceGenerator.java
@@ -54,12 +54,12 @@ public class TestHtmlInvoiceGenerator extends InvoiceTestSuiteNoDB {
         final TranslatorConfig config = new ConfigurationObjectFactory(skifeConfigSource).build(TranslatorConfig.class);
         final TemplateEngine templateEngine = new MustacheTemplateEngine();
         final InvoiceFormatterFactory factory = new DefaultInvoiceFormatterFactory();
-        g = new HtmlInvoiceGenerator(factory, templateEngine, config, null);
+        g = new HtmlInvoiceGenerator(factory, templateEngine, config, null, resourceBundleFactory, null);
     }
 
     @Test(groups = "fast")
     public void testGenerateInvoice() throws Exception {
-        final HtmlInvoice output = g.generateInvoice(createAccount(), createInvoice(), false);
+        final HtmlInvoice output = g.generateInvoice(createAccount(), createInvoice(), false, internalCallContext);
         Assert.assertNotNull(output);
         Assert.assertNotNull(output.getBody());
         Assert.assertEquals(output.getSubject(), "Your invoice");
@@ -68,7 +68,7 @@ public class TestHtmlInvoiceGenerator extends InvoiceTestSuiteNoDB {
     @Test(groups = "fast")
     public void testGenerateEmptyInvoice() throws Exception {
         final Invoice invoice = Mockito.mock(Invoice.class);
-        final HtmlInvoice output = g.generateInvoice(createAccount(), invoice, false);
+        final HtmlInvoice output = g.generateInvoice(createAccount(), invoice, false, internalCallContext);
         Assert.assertNotNull(output);
         Assert.assertNotNull(output.getBody());
         Assert.assertEquals(output.getSubject(), "Your invoice");
@@ -76,7 +76,7 @@ public class TestHtmlInvoiceGenerator extends InvoiceTestSuiteNoDB {
 
     @Test(groups = "fast")
     public void testGenerateNullInvoice() throws Exception {
-        final HtmlInvoice output = g.generateInvoice(createAccount(), null, false);
+        final HtmlInvoice output = g.generateInvoice(createAccount(), null, false, internalCallContext);
         Assert.assertNull(output);
     }
 
diff --git a/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueEmailGenerator.java b/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueEmailGenerator.java
index 8cb44b2..d7fd94e 100644
--- a/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueEmailGenerator.java
+++ b/overdue/src/main/java/org/killbill/billing/overdue/applicator/OverdueEmailGenerator.java
@@ -16,7 +16,9 @@
 
 package org.killbill.billing.overdue.applicator;
 
+import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -25,7 +27,9 @@ import org.killbill.billing.overdue.api.OverdueState;
 import org.killbill.billing.overdue.applicator.formatters.OverdueEmailFormatterFactory;
 import org.killbill.billing.overdue.config.api.BillingState;
 import org.killbill.billing.util.email.templates.TemplateEngine;
+import org.killbill.billing.util.io.IOUtils;
 
+import com.google.common.io.CharSource;
 import com.google.inject.Inject;
 
 public class OverdueEmailGenerator {
@@ -50,6 +54,7 @@ public class OverdueEmailGenerator {
         data.put("nextOverdueState", nextOverdueState);
 
         // TODO single template for all languages for now
-        return templateEngine.executeTemplate(nextOverdueState.getEmailNotification().getTemplateName(), data);
+        final InputStream input = new FileInputStream(nextOverdueState.getEmailNotification().getTemplateName());
+        return templateEngine.executeTemplateText(IOUtils.toString(input), data);
     }
 }
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCatalog.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCatalog.java
index 2010e09..f4204ac 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCatalog.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestCatalog.java
@@ -29,8 +29,19 @@ import org.killbill.billing.client.model.Product;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import com.google.common.io.Resources;
+
 public class TestCatalog extends TestJaxrsBase {
 
+    @Test(groups = "slow", description = "Upload and retrieve a per tenant catalog")
+    public void testMultiTenantCatalog() throws Exception {
+        final String catalogPath = Resources.getResource("SpyCarBasic.xml").getPath();
+        killBillClient.uploadXMLCatalog(catalogPath, createdBy, reason, comment);
+
+        final String catalog = killBillClient.getXMLCatalog();
+        Assert.assertNotNull(catalog);
+    }
+
     @Test(groups = "slow", description = "Can retrieve a simplified version of the catalog")
     public void testCatalogSimple() throws Exception {
         final Set<String> allBasePlans = new HashSet<String>();
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java
index d9d9898..6685955 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestOverdue.java
@@ -31,11 +31,22 @@ import org.testng.Assert;
 import org.testng.annotations.Test;
 
 import com.google.common.collect.Ordering;
+import com.google.common.io.Resources;
 
 import static org.testng.Assert.assertEquals;
 
 public class TestOverdue extends TestJaxrsBase {
 
+    @Test(groups = "slow", description = "Upload and retrieve a per tenant overdue config")
+    public void testMultiTenantOverdueConfig() throws Exception {
+        final String overdueConfigPath = Resources.getResource("overdue.xml").getPath();
+        killBillClient.uploadXMLOverdueConfig(overdueConfigPath, createdBy, reason, comment);
+
+        final String overdueConfig = killBillClient.getXMLOverdueConfig();
+        Assert.assertNotNull(overdueConfig);
+    }
+
+
     @Test(groups = "slow", description = "Can retrieve the account overdue status")
     public void testOverdueStatus() throws Exception {
         // Create an account without a payment method
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantInternalApi.java b/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantInternalApi.java
index fa9454b..bc46459 100644
--- a/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantInternalApi.java
+++ b/tenant/src/main/java/org/killbill/billing/tenant/api/DefaultTenantInternalApi.java
@@ -18,6 +18,7 @@
 package org.killbill.billing.tenant.api;
 
 import java.util.List;
+import java.util.Locale;
 
 import javax.inject.Inject;
 import javax.inject.Named;
@@ -44,6 +45,49 @@ public class DefaultTenantInternalApi implements TenantInternalApi {
     @Override
     public String getTenantOverdueConfig(final InternalTenantContext tenantContext) {
         final List<String> values = tenantDao.getTenantValueForKey(TenantKey.OVERDUE_CONFIG.toString(), tenantContext);
-        return values.isEmpty() ? null : values.get(0);
+        return getUniqueValue(values, "overdue config", tenantContext);
+    }
+
+    @Override
+    public String getInvoiceTemplate(final Locale locale, final InternalTenantContext tenantContext) {
+        final List<String> values = tenantDao.getTenantValueForKey(getKeyFromLocale(TenantKey.INVOICE_TEMPLATE_.toString(), locale), tenantContext);
+        return getUniqueValue(values, "invoice template", tenantContext);
+    }
+
+    @Override
+    public String getManualPayInvoiceTemplate(final Locale locale, final InternalTenantContext tenantContext) {
+        final List<String> values = tenantDao.getTenantValueForKey(getKeyFromLocale(TenantKey.INVOICE_MP_TEMPLATE_.toString(), locale), tenantContext);
+        return getUniqueValue(values, "manual pay invoice template", tenantContext);
+    }
+
+    @Override
+    public String getInvoiceTranslation(final Locale locale, final InternalTenantContext tenantContext) {
+        final List<String> values = tenantDao.getTenantValueForKey(getKeyFromLocale(TenantKey.INVOICE_TRANSLATION_.toString(), locale), tenantContext);
+        return getUniqueValue(values, "invoice translation", tenantContext);
+    }
+
+    @Override
+    public String getCatalogTranslation(final Locale locale, final InternalTenantContext tenantContext) {
+        final List<String> values = tenantDao.getTenantValueForKey(getKeyFromLocale(TenantKey.CATALOG_TRANSLATION_.toString(), locale), tenantContext);
+        return getUniqueValue(values, "catalog translation", tenantContext);
+    }
+
+    private String getUniqueValue(final List<String> values, final String msg, final InternalTenantContext tenantContext) {
+        if (values.isEmpty()) {
+            return null;
+        }
+        if (values.size() > 1) {
+            throw new IllegalStateException(String.format("Unexpected number of values %d for %s and tenant %d",
+                                                          values.size(), msg, tenantContext.getTenantRecordId()));
+        }
+        return values.get(0);
+    }
+
+    private String getKeyFromLocale(final String prefix, final Locale locale) {
+        final StringBuilder tmp = new StringBuilder(prefix);
+        tmp.append(locale.getLanguage())
+           .append("_")
+           .append(locale.getCountry());
+        return tmp.toString();
     }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/email/templates/MustacheTemplateEngine.java b/util/src/main/java/org/killbill/billing/util/email/templates/MustacheTemplateEngine.java
index 10960b3..b914220 100644
--- a/util/src/main/java/org/killbill/billing/util/email/templates/MustacheTemplateEngine.java
+++ b/util/src/main/java/org/killbill/billing/util/email/templates/MustacheTemplateEngine.java
@@ -16,39 +16,16 @@
 
 package org.killbill.billing.util.email.templates;
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URISyntaxException;
 import java.util.Map;
-import org.killbill.billing.util.io.IOUtils;
-import org.killbill.xmlloader.UriAccessor;
 
-import com.google.common.annotations.VisibleForTesting;
 import com.samskivert.mustache.Mustache;
 import com.samskivert.mustache.Template;
 
 public class MustacheTemplateEngine implements TemplateEngine {
 
     @Override
-    public String executeTemplate(final String templateName, final Map<String, Object> data) throws IOException {
-        final String templateText = getTemplateText(templateName);
-        return executeTemplateText(templateText, data);
-    }
-
-    @VisibleForTesting
     public String executeTemplateText(final String templateText, final Map<String, Object> data) {
         final Template template = Mustache.compiler().compile(templateText);
         return template.execute(data);
     }
-
-    private String getTemplateText(final String templateName) throws IOException {
-        final InputStream templateStream;
-        try {
-            templateStream = UriAccessor.accessUri(templateName);
-        } catch (URISyntaxException e) {
-            throw new IOException(e);
-        }
-
-        return IOUtils.toString(templateStream);
-    }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/email/templates/TemplateEngine.java b/util/src/main/java/org/killbill/billing/util/email/templates/TemplateEngine.java
index 2854b39..b662127 100644
--- a/util/src/main/java/org/killbill/billing/util/email/templates/TemplateEngine.java
+++ b/util/src/main/java/org/killbill/billing/util/email/templates/TemplateEngine.java
@@ -16,9 +16,10 @@
 
 package org.killbill.billing.util.email.templates;
 
-import java.io.IOException;
 import java.util.Map;
 
 public interface TemplateEngine {
-    public String executeTemplate(String templateName, Map<String, Object> data) throws IOException;
+
+    public String executeTemplateText(final String templateText, final Map<String, Object> data);
+
 }
diff --git a/util/src/main/java/org/killbill/billing/util/template/translation/DefaultCatalogTranslator.java b/util/src/main/java/org/killbill/billing/util/template/translation/DefaultCatalogTranslator.java
index 0343da3..4193b91 100644
--- a/util/src/main/java/org/killbill/billing/util/template/translation/DefaultCatalogTranslator.java
+++ b/util/src/main/java/org/killbill/billing/util/template/translation/DefaultCatalogTranslator.java
@@ -16,21 +16,12 @@
 
 package org.killbill.billing.util.template.translation;
 
-import com.google.inject.Inject;
+import java.util.ResourceBundle;
 
 public class DefaultCatalogTranslator extends DefaultTranslatorBase {
-    @Inject
-    public DefaultCatalogTranslator(final TranslatorConfig config) {
-        super(config);
-    }
-
-    @Override
-    protected String getBundlePath() {
-        return config.getCatalogBundlePath();
-    }
 
-    @Override
-    protected String getTranslationType() {
-        return "catalog";
+    public DefaultCatalogTranslator(final ResourceBundle bundle,
+                                    final ResourceBundle defaultBundle) {
+        super(bundle, defaultBundle);
     }
 }
diff --git a/util/src/main/java/org/killbill/billing/util/template/translation/DefaultTranslatorBase.java b/util/src/main/java/org/killbill/billing/util/template/translation/DefaultTranslatorBase.java
index 4516415..dfe8e51 100644
--- a/util/src/main/java/org/killbill/billing/util/template/translation/DefaultTranslatorBase.java
+++ b/util/src/main/java/org/killbill/billing/util/template/translation/DefaultTranslatorBase.java
@@ -16,104 +16,39 @@
 
 package org.killbill.billing.util.template.translation;
 
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URISyntaxException;
-import java.util.Locale;
-import java.util.MissingResourceException;
-import java.util.PropertyResourceBundle;
 import java.util.ResourceBundle;
 
-import org.killbill.xmlloader.UriAccessor;
+import javax.annotation.Nullable;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import org.killbill.billing.util.LocaleUtils;
-
-import com.google.inject.Inject;
-
 public abstract class DefaultTranslatorBase implements Translator {
 
-    protected final TranslatorConfig config;
     protected final Logger log = LoggerFactory.getLogger(DefaultTranslatorBase.class);
 
-    @Inject
-    public DefaultTranslatorBase(final TranslatorConfig config) {
-        this.config = config;
-    }
-
-    protected abstract String getBundlePath();
+    private final ResourceBundle bundle;
+    private final ResourceBundle defaultBundle;
 
-    /*
-     * string used for exception handling
-     */
-    protected abstract String getTranslationType();
+    public DefaultTranslatorBase(@Nullable final ResourceBundle bundle,
+                                 @Nullable final ResourceBundle defaultBundle) {
+        this.bundle = bundle;
+        this.defaultBundle = defaultBundle;
+    }
 
     @Override
-    public String getTranslation(final Locale locale, final String originalText) {
-        final String bundlePath = getBundlePath();
-        ResourceBundle bundle = getBundle(locale, bundlePath);
-
+    public String getTranslation(final String originalText) {
+        if (originalText == null) {
+            return null;
+        }
         if ((bundle != null) && (bundle.containsKey(originalText))) {
             return bundle.getString(originalText);
         } else {
-            if (config.getDefaultLocale() == null) {
-                log.debug("No default locale configured, returning original text");
-                return originalText;
-            }
-
-            final Locale defaultLocale = LocaleUtils.toLocale(config.getDefaultLocale());
-            try {
-                bundle = getBundle(defaultLocale, bundlePath);
-
-                if ((bundle != null) && (bundle.containsKey(originalText))) {
-                    return bundle.getString(originalText);
-                } else {
-                    return originalText;
-                }
-            } catch (MissingResourceException mrex) {
-                log.warn("Missing translation bundle for locale {}", defaultLocale);
-                return originalText;
-            }
-        }
-    }
-
-    private ResourceBundle getBundle(final Locale locale, final String bundlePath) {
-        try {
-            // Try to load the bundle from the classpath first
-            return ResourceBundle.getBundle(bundlePath, locale);
-        } catch (MissingResourceException ignored) {
-        }
-
-        // Try to load it from a properties file
-        final String propertiesFileNameWithCountry = bundlePath + "_" + locale.getLanguage() + "_" + locale.getCountry() + ".properties";
-        ResourceBundle bundle = getBundleFromPropertiesFile(propertiesFileNameWithCountry);
-        if (bundle != null) {
-            return bundle;
-        } else {
-            final String propertiesFileName = bundlePath + "_" + locale.getLanguage() + ".properties";
-            bundle = getBundleFromPropertiesFile(propertiesFileName);
-        }
-
-        return bundle;
-    }
-
-    private ResourceBundle getBundleFromPropertiesFile(final String propertiesFileName) {
-        try {
-            final InputStream inputStream = UriAccessor.accessUri(propertiesFileName);
-            if (inputStream == null) {
-                return null;
+            if ((defaultBundle != null) && (defaultBundle.containsKey(originalText))) {
+                return defaultBundle.getString(originalText);
             } else {
-                return new PropertyResourceBundle(inputStream);
+                return originalText;
             }
-        } catch (IllegalArgumentException iae) {
-            return null;
-        } catch (MissingResourceException mrex) {
-            return null;
-        } catch (URISyntaxException e) {
-            return null;
-        } catch (IOException e) {
-            return null;
         }
     }
 }
diff --git a/util/src/test/java/org/killbill/billing/mock/MockInvoiceFormatterFactory.java b/util/src/test/java/org/killbill/billing/mock/MockInvoiceFormatterFactory.java
index 7efaaf2..44fd9f0 100644
--- a/util/src/test/java/org/killbill/billing/mock/MockInvoiceFormatterFactory.java
+++ b/util/src/test/java/org/killbill/billing/mock/MockInvoiceFormatterFactory.java
@@ -18,15 +18,18 @@ package org.killbill.billing.mock;
 
 import java.util.Locale;
 
+import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.currency.api.CurrencyConversionApi;
 import org.killbill.billing.invoice.api.Invoice;
 import org.killbill.billing.invoice.api.formatters.InvoiceFormatter;
 import org.killbill.billing.invoice.api.formatters.InvoiceFormatterFactory;
+import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory;
 import org.killbill.billing.util.template.translation.TranslatorConfig;
 
 public class MockInvoiceFormatterFactory implements InvoiceFormatterFactory {
+
     @Override
-    public InvoiceFormatter createInvoiceFormatter(final TranslatorConfig config, final Invoice invoice, final Locale locale, CurrencyConversionApi currencyConversionApi) {
+    public InvoiceFormatter createInvoiceFormatter(final TranslatorConfig config, final Invoice invoice, final Locale locale, final CurrencyConversionApi currencyConversionApi, final ResourceBundleFactory bundleFactory, final InternalTenantContext context) {
         return null;
     }
 }
diff --git a/util/src/test/java/org/killbill/billing/util/email/DefaultCatalogTranslationTest.java b/util/src/test/java/org/killbill/billing/util/email/DefaultCatalogTranslationTest.java
index f281f3d..6af5d3e 100644
--- a/util/src/test/java/org/killbill/billing/util/email/DefaultCatalogTranslationTest.java
+++ b/util/src/test/java/org/killbill/billing/util/email/DefaultCatalogTranslationTest.java
@@ -14,18 +14,24 @@ package org.killbill.billing.util.email;/*
  * under the License.
  */
 
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URISyntaxException;
 import java.util.Locale;
 import java.util.Map;
+import java.util.PropertyResourceBundle;
+import java.util.ResourceBundle;
 
-import org.skife.config.ConfigSource;
-import org.skife.config.ConfigurationObjectFactory;
-import org.testng.annotations.BeforeClass;
-import org.testng.annotations.Test;
-
+import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory;
 import org.killbill.billing.util.UtilTestSuiteNoDB;
 import org.killbill.billing.util.template.translation.DefaultCatalogTranslator;
 import org.killbill.billing.util.template.translation.Translator;
 import org.killbill.billing.util.template.translation.TranslatorConfig;
+import org.killbill.xmlloader.UriAccessor;
+import org.skife.config.ConfigSource;
+import org.skife.config.ConfigurationObjectFactory;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
 
 import com.google.common.collect.ImmutableMap;
 
@@ -33,70 +39,61 @@ import static org.testng.Assert.assertEquals;
 
 public class DefaultCatalogTranslationTest extends UtilTestSuiteNoDB {
 
-    private Translator translation;
-
     @Override
     @BeforeClass(groups = "fast")
     public void beforeClass() throws Exception {
         super.beforeClass();
-        final ConfigSource configSource = new ConfigSource() {
-            private final Map<String, String> properties = ImmutableMap.<String, String>of("org.killbill.template.invoiceFormatterFactoryClass",
-                                                                                           "org.killbill.billing.mock.MockInvoiceFormatterFactory");
-
-            @Override
-            public String getString(final String propertyName) {
-                return properties.get(propertyName);
-            }
-        };
-
-        final TranslatorConfig config = new ConfigurationObjectFactory(configSource).build(TranslatorConfig.class);
-        translation = new DefaultCatalogTranslator(config);
+    }
+
+    private ResourceBundle getBundle(final Locale locale) throws IOException, URISyntaxException {
+        final String propertiesFileNameWithCountry = "org/killbill/billing/util/template/translation/CatalogTranslation" + "_" + locale.getLanguage() + "_" + locale.getCountry() + ".properties";
+        final InputStream inputStream = UriAccessor.accessUri(propertiesFileNameWithCountry);
+        if (inputStream == null) {
+            return null;
+        } else {
+            return new PropertyResourceBundle(inputStream);
+        }
     }
 
     @Test(groups = "fast")
-    public void testInitialization() {
+    public void testBundle_us() throws IOException, URISyntaxException {
         final String shotgunMonthly = "shotgun-monthly";
         final String shotgunAnnual = "shotgun-annual";
         final String badText = "Bad text";
 
-        assertEquals(translation.getTranslation(Locale.US, shotgunMonthly), "Monthly shotgun plan");
-        assertEquals(translation.getTranslation(Locale.US, shotgunAnnual), "Annual shotgun plan");
-        assertEquals(translation.getTranslation(Locale.US, badText), badText);
-
-        assertEquals(translation.getTranslation(Locale.CANADA_FRENCH, shotgunMonthly), "Fusil de chasse mensuel");
-        assertEquals(translation.getTranslation(Locale.CANADA_FRENCH, shotgunAnnual), "Fusil de chasse annuel");
-        assertEquals(translation.getTranslation(Locale.CANADA_FRENCH, badText), badText);
+        final ResourceBundle bundle_en_US = getBundle(Locale.US);
+        final DefaultCatalogTranslator translation = new DefaultCatalogTranslator(bundle_en_US, null);
 
-        assertEquals(translation.getTranslation(Locale.CHINA, shotgunMonthly), "Monthly shotgun plan");
-        assertEquals(translation.getTranslation(Locale.CHINA, shotgunAnnual), "Annual shotgun plan");
-        assertEquals(translation.getTranslation(Locale.CHINA, badText), badText);
+        assertEquals(translation.getTranslation(shotgunMonthly), "Monthly shotgun plan");
+        assertEquals(translation.getTranslation(shotgunAnnual), "Annual shotgun plan");
+        assertEquals(translation.getTranslation(badText), badText);
     }
 
     @Test(groups = "fast")
-    public void testExistingTranslation() {
-        // If the translation exists, return the translation
-        final String originalText = "shotgun-monthly";
-        assertEquals(translation.getTranslation(Locale.US, originalText), "Monthly shotgun plan");
-    }
+    public void testBundle_ca_fr() throws IOException, URISyntaxException {
+        final String shotgunMonthly = "shotgun-monthly";
+        final String shotgunAnnual = "shotgun-annual";
+        final String badText = "Bad text";
 
-    @Test(groups = "fast")
-    public void testMissingTranslation() {
-        // If the translation is missing from the file, return the original text
-        final String originalText = "missing translation";
-        assertEquals(translation.getTranslation(Locale.US, originalText), originalText);
-    }
+        final ResourceBundle bundle_ca_fr = getBundle(Locale.CANADA_FRENCH);
+        final DefaultCatalogTranslator translation = new DefaultCatalogTranslator(bundle_ca_fr, null);
 
-    @Test(groups = "fast")
-    public void testMissingTranslationFileWithEnglishText() {
-        // If the translation file doesn't exist, return the "English" translation
-        final String originalText = "shotgun-monthly";
-        assertEquals(translation.getTranslation(Locale.CHINA, originalText), "Monthly shotgun plan");
+        assertEquals(translation.getTranslation(shotgunMonthly), "Fusil de chasse mensuel");
+        assertEquals(translation.getTranslation(shotgunAnnual), "Fusil de chasse annuel");
+        assertEquals(translation.getTranslation(badText), badText);
     }
 
     @Test(groups = "fast")
-    public void testMissingFileAndText() {
-        // If the file is missing, and the "English" translation is missing, return the original text
-        final String originalText = "missing translation";
-        assertEquals(translation.getTranslation(Locale.CHINA, originalText), originalText);
+    public void testBundle_ch() throws IOException, URISyntaxException {
+        final String shotgunMonthly = "shotgun-monthly";
+        final String shotgunAnnual = "shotgun-annual";
+        final String badText = "Bad text";
+
+        final DefaultCatalogTranslator translation = new DefaultCatalogTranslator(null, null);
+
+        assertEquals(translation.getTranslation(shotgunMonthly), shotgunMonthly);
+        assertEquals(translation.getTranslation(shotgunAnnual), shotgunAnnual);
+        assertEquals(translation.getTranslation(badText), badText);
     }
+
 }
diff --git a/util/src/test/java/org/killbill/billing/util/template/translation/TestDefaultTranslatorBase.java b/util/src/test/java/org/killbill/billing/util/template/translation/TestDefaultTranslatorBase.java
index eff1e43..7ac4cd6 100644
--- a/util/src/test/java/org/killbill/billing/util/template/translation/TestDefaultTranslatorBase.java
+++ b/util/src/test/java/org/killbill/billing/util/template/translation/TestDefaultTranslatorBase.java
@@ -16,7 +16,7 @@
 
 package org.killbill.billing.util.template.translation;
 
-import java.util.Locale;
+import java.util.ResourceBundle;
 import java.util.UUID;
 
 import org.mockito.Mockito;
@@ -29,25 +29,16 @@ public class TestDefaultTranslatorBase extends UtilTestSuiteNoDB {
 
     private final class TestTranslatorBase extends DefaultTranslatorBase {
 
-        public TestTranslatorBase(final TranslatorConfig config) {
-            super(config);
+        public TestTranslatorBase(final TranslatorConfig config, final ResourceBundle bundle) {
+            super(bundle, bundle);
         }
 
-        @Override
-        protected String getBundlePath() {
-            return UUID.randomUUID().toString();
-        }
-
-        @Override
-        protected String getTranslationType() {
-            return UUID.randomUUID().toString();
-        }
     }
 
     @Test(groups = "fast")
     public void testResourceDoesNotExist() throws Exception {
-        final TestTranslatorBase translator = new TestTranslatorBase(Mockito.mock(TranslatorConfig.class));
+        final TestTranslatorBase translator = new TestTranslatorBase(Mockito.mock(TranslatorConfig.class), Mockito.mock(ResourceBundle.class));
         final String originalText = UUID.randomUUID().toString();
-        Assert.assertEquals(translator.getTranslation(Locale.FRANCE, originalText), originalText);
+        Assert.assertEquals(translator.getTranslation(originalText), originalText);
     }
 }