You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by jo...@apache.org on 2026/10/14 20:32:31 UTC
(sling-org-apache-sling-i18n) 01/02: SLING-13392: Refresh i18n parents before dependent bundles
This is an automated email from the ASF dual-hosted git repository.
joerghoh pushed a commit to branch SLING-13392
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-i18n.git
commit 3e2e590f51e972fddf67381edebc2de3f877aa22
Author: Joerg Hoh <jo...@apache.org>
AuthorDate: Wed Oct 14 17:41:13 2026 +0200
SLING-13392: Refresh i18n parents before dependent bundles
---
README.md | 8 ++
.../sling/i18n/impl/JcrResourceBundleProvider.java | 8 +-
.../ConcurrentJcrResourceBundleLoadingTest.java | 96 ++++++++++++++++++++++
.../i18n/it/EagerResourceBundleProviderIT.java | 38 +++++++++
.../sling/i18n/it/ResourceBundleProviderIT.java | 14 ++++
5 files changed, 160 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index e6e2d53..64dc559 100644
--- a/README.md
+++ b/README.md
@@ -8,3 +8,11 @@ This module is part of the [Apache Sling](https://sling.apache.org) project.
Support for creating Java I18N ResourceBundles from repository
resources. Further details in <https://sling.apache.org/documentation/bundles/internationalization-support-i18n.html>.
+
+## Dictionary updates
+
+When `preload.bundles` is enabled, an individual bundle refresh reloads parents
+before their dependent locales so that refreshed bundles use current fallback
+translations. With preloading disabled, affected bundles are invalidated and
+loaded on demand. Existing bundle objects remain snapshots; callers should
+request a bundle again to obtain refreshed translations.
diff --git a/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundleProvider.java b/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundleProvider.java
index 15a57e9..97047ab 100644
--- a/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundleProvider.java
+++ b/src/main/java/org/apache/sling/i18n/impl/JcrResourceBundleProvider.java
@@ -407,14 +407,14 @@ public class JcrResourceBundleProvider
}
}
}
- for (JcrResourceBundle dependentBundle : dependentBundles) {
- reloadBundle(new Key(dependentBundle.getBaseName(), dependentBundle.getLocale()));
- }
-
if (this.preloadBundles && !resourceBundleRegistry.isClosed()) {
// reload the bundle from the repository (will also fill cache and register as a service)
getResourceBundleInternal(null, key.baseName, key.locale, true);
}
+
+ for (JcrResourceBundle dependentBundle : dependentBundles) {
+ reloadBundle(new Key(dependentBundle.getBaseName(), dependentBundle.getLocale()));
+ }
}
// ---------- SCR Integration ----------------------------------------------
diff --git a/src/test/java/org/apache/sling/i18n/impl/ConcurrentJcrResourceBundleLoadingTest.java b/src/test/java/org/apache/sling/i18n/impl/ConcurrentJcrResourceBundleLoadingTest.java
index 7f3d506..34bd61d 100644
--- a/src/test/java/org/apache/sling/i18n/impl/ConcurrentJcrResourceBundleLoadingTest.java
+++ b/src/test/java/org/apache/sling/i18n/impl/ConcurrentJcrResourceBundleLoadingTest.java
@@ -35,6 +35,10 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
+import org.apache.sling.api.resource.ModifiableValueMap;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.commons.scheduler.ScheduleOptions;
import org.apache.sling.commons.scheduler.Scheduler;
import org.apache.sling.i18n.impl.JcrResourceBundleProvider.Key;
@@ -46,6 +50,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.mockito.Mockito;
+import org.osgi.framework.Constants;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -54,6 +59,7 @@ import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
/**
* Test case to verify that each bundle is only loaded once, even
@@ -74,9 +80,18 @@ public class ConcurrentJcrResourceBundleLoadingTest {
public Boolean preload = Boolean.FALSE;
private JcrResourceBundleProvider provider;
+ private final List<Resource> dictionaryRoots = new ArrayList<>();
@Before
public void setup() throws Exception {
+ ResourceResolver resolver = Mockito.spy(context.resourceResolver());
+ Mockito.doNothing().when(resolver).close();
+ Mockito.doAnswer(query -> dictionaryRoots.iterator())
+ .when(resolver)
+ .findResources(eq(JcrResourceBundle.QUERY_LANGUAGE_ROOTS), eq("xpath"));
+ ResourceResolverFactory resolverFactory = Mockito.mock(ResourceResolverFactory.class);
+ Mockito.when(resolverFactory.getServiceResourceResolver(null)).thenReturn(resolver);
+ context.registerService(ResourceResolverFactory.class, resolverFactory, Constants.SERVICE_RANKING, 1000);
// mock other required services
Scheduler mockScheduler = context.registerService(Scheduler.class, Mockito.mock(Scheduler.class));
// mock this call to avoid a NPE during activation
@@ -153,12 +168,92 @@ public class ConcurrentJcrResourceBundleLoadingTest {
assertSame(english, provider.getResourceBundle(Locale.ENGLISH));
ResourceBundle german2 = provider.getResourceBundle(Locale.GERMAN);
assertNotSame(german2, german);
+ assertSame(english, ((JcrResourceBundle) german2).getParent());
assertSame(english, provider.getResourceBundle(Locale.ENGLISH));
assertSame(german2, provider.getResourceBundle(Locale.GERMAN));
assertSame(english, provider.getResourceBundle(Locale.ENGLISH));
assertSame(german2, provider.getResourceBundle(Locale.GERMAN));
}
+ @Test
+ public void reloadedFallbacksUseCurrentParents() {
+ Resource englishDictionary = createDictionary("en", "en");
+ Resource fallback =
+ context.create().resource(englishDictionary.getPath() + "/fallback", "sling:message", "old English");
+ Resource germanDictionary = createDictionary("de", "de");
+ context.create().resource(germanDictionary.getPath() + "/override", "sling:message", "German override");
+ Resource countryDictionary = createDictionary("de_DE", "de_DE");
+ context.create().resource(countryDictionary.getPath() + "/override", "sling:message", "Country override");
+ Resource namedDictionary = createDictionary("named-en", "en", "named");
+ context.create().resource(namedDictionary.getPath() + "/named", "sling:message", "Named English");
+
+ ResourceBundle english = provider.getResourceBundle(Locale.ENGLISH);
+ ResourceBundle german = provider.getResourceBundle(Locale.GERMAN);
+ ResourceBundle country = provider.getResourceBundle(Locale.GERMANY);
+ ResourceBundle french = provider.getResourceBundle(Locale.FRENCH);
+ ResourceBundle named = provider.getResourceBundle("named", Locale.GERMANY);
+ assertEquals("old English", country.getString("fallback"));
+ assertEquals("old English", french.getString("fallback"));
+
+ fallback.adaptTo(ModifiableValueMap.class).put("sling:message", "new English");
+ provider.reloadBundle(new Key(null, Locale.ENGLISH));
+
+ ResourceBundle english2 = provider.getResourceBundle(Locale.ENGLISH);
+ ResourceBundle german2 = provider.getResourceBundle(Locale.GERMAN);
+ ResourceBundle country2 = provider.getResourceBundle(Locale.GERMANY);
+ ResourceBundle french2 = provider.getResourceBundle(Locale.FRENCH);
+ assertNotSame(english, english2);
+ assertNotSame(german, german2);
+ assertNotSame(country, country2);
+ assertNotSame(french, french2);
+ assertSame(english2, ((JcrResourceBundle) german2).getParent());
+ assertSame(german2, ((JcrResourceBundle) country2).getParent());
+ assertSame(english2, ((JcrResourceBundle) french2).getParent());
+ assertEquals("new English", country2.getString("fallback"));
+ assertEquals("new English", french2.getString("fallback"));
+ assertEquals("German override", german2.getString("override"));
+ assertEquals("Country override", country2.getString("override"));
+ assertSame(named, provider.getResourceBundle("named", Locale.GERMANY));
+ assertEquals("Named English", named.getString("named"));
+ }
+
+ @Test
+ public void namedFallbackRefreshDoesNotReplaceOtherBasenames() {
+ Resource dictionary = createDictionary("named-en", "en", "named");
+ Resource message =
+ context.create().resource(dictionary.getPath() + "/fallback", "sling:message", "old named English");
+ ResourceBundle english = provider.getResourceBundle(Locale.ENGLISH);
+ ResourceBundle german = provider.getResourceBundle(Locale.GERMAN);
+ ResourceBundle named = provider.getResourceBundle("named", Locale.GERMANY);
+ assertEquals("old named English", named.getString("fallback"));
+
+ message.adaptTo(ModifiableValueMap.class).put("sling:message", "new named English");
+ provider.reloadBundle(new Key("named", Locale.ENGLISH));
+
+ ResourceBundle named2 = provider.getResourceBundle("named", Locale.GERMANY);
+ JcrResourceBundle namedGerman = (JcrResourceBundle) provider.getResourceBundle("named", Locale.GERMAN);
+ assertNotSame(named, named2);
+ assertSame(namedGerman, ((JcrResourceBundle) named2).getParent());
+ assertSame(provider.getResourceBundle("named", Locale.ENGLISH), namedGerman.getParent());
+ assertEquals("new named English", named2.getString("fallback"));
+ assertSame(english, provider.getResourceBundle(Locale.ENGLISH));
+ assertSame(german, provider.getResourceBundle(Locale.GERMAN));
+ }
+
+ private Resource createDictionary(String name, String language, String... baseNames) {
+ Resource dictionary = context.create()
+ .resource(
+ "/apps/i18n/" + name,
+ "jcr:mixinTypes",
+ new String[] {"mix:language"},
+ "jcr:language",
+ language,
+ "sling:basename",
+ baseNames);
+ dictionaryRoots.add(dictionary);
+ return dictionary;
+ }
+
@Test
public void newBundleUsedAsParentAfterReload() throws Exception {
ResourceBundle english = provider.getResourceBundle(Locale.ENGLISH);
@@ -170,6 +265,7 @@ public class ConcurrentJcrResourceBundleLoadingTest {
assertNotSame(english2, english);
ResourceBundle german2 = provider.getResourceBundle(Locale.GERMAN);
assertNotSame(german2, german);
+ assertSame(english2, ((JcrResourceBundle) german2).getParent());
assertSame(english2, provider.getResourceBundle(Locale.ENGLISH));
assertSame(german2, provider.getResourceBundle(Locale.GERMAN));
assertSame(english2, provider.getResourceBundle(Locale.ENGLISH));
diff --git a/src/test/java/org/apache/sling/i18n/it/EagerResourceBundleProviderIT.java b/src/test/java/org/apache/sling/i18n/it/EagerResourceBundleProviderIT.java
new file mode 100644
index 0000000..df51deb
--- /dev/null
+++ b/src/test/java/org/apache/sling/i18n/it/EagerResourceBundleProviderIT.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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.apache.sling.i18n.it;
+
+import org.ops4j.pax.exam.Configuration;
+import org.ops4j.pax.exam.Option;
+import org.ops4j.pax.exam.OptionUtils;
+
+import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration;
+
+public class EagerResourceBundleProviderIT extends ResourceBundleProviderIT {
+
+ @Override
+ @Configuration
+ public Option[] configuration() {
+ return OptionUtils.combine(
+ super.configuration(),
+ newConfiguration("org.apache.sling.i18n.impl.JcrResourceBundleProvider")
+ .put("preload.bundles", true)
+ .asOption());
+ }
+}
diff --git a/src/test/java/org/apache/sling/i18n/it/ResourceBundleProviderIT.java b/src/test/java/org/apache/sling/i18n/it/ResourceBundleProviderIT.java
index ed2419b..d5ae71c 100644
--- a/src/test/java/org/apache/sling/i18n/it/ResourceBundleProviderIT.java
+++ b/src/test/java/org/apache/sling/i18n/it/ResourceBundleProviderIT.java
@@ -175,6 +175,20 @@ public class ResourceBundleProviderIT extends I18nTestSupport {
assertMessage(MSG_KEY1, Locale.ENGLISH, BASENAME, "overwritten");
}
+ @Test
+ public void testFallbackChangesForUntypedMessages() throws RepositoryException {
+ // Message loading also supports entries without the sling:MessageEntry type.
+ Node message = enRoot.addNode("fallback", "nt:unstructured");
+ message.setProperty("sling:key", MSG_KEY2);
+ message.setProperty("sling:message", "old English fallback");
+ session.save();
+ assertMessages(MSG_KEY2, "old English fallback", "old English fallback", "old English fallback");
+
+ message.setProperty("sling:message", "new English fallback");
+ session.save();
+ assertMessages(MSG_KEY2, "new English fallback", "new English fallback", "new English fallback");
+ }
+
@Test
public void testChangesDetection() throws RepositoryException {
// set a key which is only available in the en dictionary