This is an automated email from the ASF dual-hosted git repository. joerghoh pushed a commit to branch SLING-13393 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-i18n.git
commit 298a7aa0e3f251b7c6d01613afbaec883165bc01 Author: Joerg Hoh <[email protected]> AuthorDate: Wed Oct 14 17:41:13 2026 +0200 Refresh i18n parents before dependent bundles Co-authored-by: Copilot <[email protected]> --- 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
