Skip to content

Commit c5e9a52

Browse files
Dev master8 sep (#1703)
* @releng updating af-core (#1699) * @releng updating af-core * Fixing test case * Fixing test cases * Fixing the test * Fixing Accordion panel cannot collapse when a Text component is the first item. (#1702) * FORMS-21693 Localization not working with fragment (#1701) * Setting fragment i18n * FORMS-21693 Localization not working with fragment * FORMS-21693 Localization not working with fragment * Adding more test * FORMS-21693 Localization not working with fragment --------- Co-authored-by: sakshi-arora1 <[email protected]>
2 parents b4db6c5 + dee3688 commit c5e9a52

File tree

22 files changed

+310
-69
lines changed

22 files changed

+310
-69
lines changed

bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v1/form/FragmentImpl.java

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717

1818
import java.util.ArrayList;
1919
import java.util.List;
20+
import java.util.Locale;
2021
import java.util.Map;
22+
import java.util.ResourceBundle;
23+
import java.util.UUID;
2124

25+
import javax.annotation.Nonnull;
2226
import javax.annotation.PostConstruct;
2327

2428
import org.apache.commons.lang3.StringUtils;
@@ -27,6 +31,7 @@
2731
import org.apache.sling.api.resource.ResourceResolver;
2832
import org.apache.sling.api.resource.ValueMap;
2933
import org.apache.sling.api.wrappers.SlingHttpServletRequestWrapper;
34+
import org.apache.sling.i18n.ResourceBundleProvider;
3035
import org.apache.sling.models.annotations.Exporter;
3136
import org.apache.sling.models.annotations.Model;
3237
import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;
@@ -36,16 +41,20 @@
3641
import org.jetbrains.annotations.NotNull;
3742
import org.jetbrains.annotations.Nullable;
3843

44+
import com.adobe.aemds.guide.utils.GuideUtils;
45+
import com.adobe.aemds.guide.utils.TranslationUtils;
3946
import com.adobe.cq.export.json.ComponentExporter;
4047
import com.adobe.cq.export.json.ExporterConstants;
4148
import com.adobe.cq.export.json.SlingModelFilter;
4249
import com.adobe.cq.forms.core.components.internal.form.FormConstants;
4350
import com.adobe.cq.forms.core.components.internal.form.ReservedProperties;
4451
import com.adobe.cq.forms.core.components.models.form.FormClientLibManager;
52+
import com.adobe.cq.forms.core.components.models.form.FormComponent;
4553
import com.adobe.cq.forms.core.components.models.form.FormContainer;
4654
import com.adobe.cq.forms.core.components.models.form.Fragment;
4755
import com.adobe.cq.forms.core.components.util.ComponentUtils;
4856
import com.adobe.cq.forms.core.components.views.Views;
57+
import com.day.cq.i18n.I18n;
4958
import com.fasterxml.jackson.annotation.JsonIgnore;
5059
import com.fasterxml.jackson.annotation.JsonView;
5160

@@ -67,6 +76,11 @@ public class FragmentImpl extends PanelImpl implements Fragment {
6776
@OSGiService
6877
private ModelFactory modelFactory;
6978

79+
@OSGiService(
80+
filter = "(service.pid=org.apache.sling.i18n.impl.JcrResourceBundleProvider)",
81+
injectionStrategy = InjectionStrategy.OPTIONAL)
82+
private ResourceBundleProvider resourceBundleProvider;
83+
7084
@ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL, name = ReservedProperties.PN_FRAGMENT_PATH)
7185
private String fragmentPath;
7286

@@ -127,7 +141,52 @@ public Object getAttribute(String attrName) {
127141
}
128142
};
129143
}
130-
return getChildrenModels(wrappedSlingHttpServletRequest, modelClass, filteredChildrenResources);
144+
Map<String, T> models = getChildrenModels(wrappedSlingHttpServletRequest, modelClass, filteredChildrenResources);
145+
146+
// Set i18n for fragment children since they are processed with request != null
147+
// Use fragment container-specific i18n to ensure correct resource bundle path
148+
if (i18n != null) {
149+
String tempLang = request != null ? GuideUtils.getAcceptLang(request) : lang;
150+
I18n fragmentI18n = getFragmentContainerI18n(tempLang);
151+
for (T model : models.values()) {
152+
if (model instanceof FormComponent) {
153+
((FormComponent) model).setI18n(fragmentI18n);
154+
((FormComponent) model).setLang(tempLang);
155+
}
156+
}
157+
}
158+
159+
return models;
160+
}
161+
162+
/**
163+
* Creates a new I18n object for fragment children using the fragment container resource path
164+
* instead of the parent form's resource path. This ensures that fragment children use the
165+
* correct resource bundle path for translations.
166+
*
167+
* @return a new I18n object configured for the fragment container resource
168+
*/
169+
private @Nonnull I18n getFragmentContainerI18n(@Nonnull String localeLang) {
170+
// Get the locale from the lang setter
171+
ResourceBundle resourceBundle = null;
172+
if (localeLang != null) {
173+
Locale desiredLocale = new Locale(localeLang);
174+
// Get the resource resolver from the fragment container
175+
ResourceResolver resourceResolver = fragmentContainer.getResourceResolver();
176+
// Get the dictionary path for the fragment container instead of the parent form
177+
String baseName = TranslationUtils.getDictionaryPath(resourceResolver, fragmentContainer.getPath());
178+
Resource baseResource = resourceResolver.getResource(baseName);
179+
if (resourceBundleProvider != null) {
180+
if (GuideUtils.isDesiredLocaleDictPresent(baseResource, desiredLocale)) {
181+
// Use the fragment container's resource bundle if available
182+
resourceBundle = resourceBundleProvider.getResourceBundle(baseName, desiredLocale);
183+
} else {
184+
// Fallback to a random UUID-based resource bundle if fragment-specific translations are not available
185+
resourceBundle = resourceBundleProvider.getResourceBundle("/" + UUID.randomUUID(), desiredLocale);
186+
}
187+
}
188+
}
189+
return new I18n(resourceBundle);
131190
}
132191

133192
@Override

bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/internal/models/v2/form/FormContainerImpl.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import com.day.cq.wcm.api.Page;
6666
import com.day.cq.wcm.api.PageManager;
6767
import com.fasterxml.jackson.annotation.JsonIgnore;
68+
import com.fasterxml.jackson.annotation.JsonProperty;
6869

6970
@Model(
7071
adaptables = { SlingHttpServletRequest.class, Resource.class },
@@ -331,9 +332,11 @@ public String getDataUrl() {
331332
}
332333

333334
@Override
335+
@JsonProperty("lang")
334336
public String getLang() {
335-
// todo: uncomment once forms sdk is released
336-
if (request != null) {
337+
if (lang != null) {
338+
return lang;
339+
} else if (request != null) {
337340
return GuideUtils.getAcceptLang(request);
338341
} else {
339342
return FormContainer.super.getLang();

bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/Base.java

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -145,16 +145,4 @@ default String getTooltip() {
145145
default boolean isTooltipVisible() {
146146
return false;
147147
}
148-
149-
/**
150-
* Returns the language to use for formatting the field.
151-
*
152-
* @return returns the language to use for formatting the field.
153-
* @since com.adobe.cq.forms.core.components.models.form 5.3.1
154-
*/
155-
@JsonInclude(JsonInclude.Include.NON_NULL)
156-
@Nullable
157-
default String getLang() {
158-
return DEFAULT_LANGUAGE;
159-
}
160148
}

bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/FormComponent.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.day.cq.i18n.I18n;
3030
import com.fasterxml.jackson.annotation.JsonIgnore;
3131
import com.fasterxml.jackson.annotation.JsonInclude;
32+
import com.fasterxml.jackson.annotation.JsonProperty;
3233
import com.fasterxml.jackson.annotation.JsonView;
3334

3435
@ProviderType
@@ -156,4 +157,37 @@ default void setI18n(@Nonnull I18n i18n) {
156157
// empty body
157158
}
158159

160+
/**
161+
* Sets the language for the form component
162+
*
163+
* @param lang the language code
164+
* @since com.adobe.cq.forms.core.components.models.form 5.12.2
165+
*/
166+
default void setLang(@Nullable String lang) {
167+
// empty body
168+
}
169+
170+
/**
171+
* Returns the language to use for formatting the field.
172+
*
173+
* @return returns the language to use for formatting the field.
174+
* @since com.adobe.cq.forms.core.components.models.form 5.3.1
175+
*/
176+
@Nullable
177+
default String getLang() {
178+
return Base.DEFAULT_LANGUAGE;
179+
}
180+
181+
/**
182+
* Returns the language if it is present as a property in JCR
183+
*
184+
* @return the language code if present in JCR, null otherwise
185+
* @since com.adobe.cq.forms.core.components.models.form 5.12.2
186+
*/
187+
@JsonProperty("lang")
188+
@Nullable
189+
default String getLangIfPresent() {
190+
return null;
191+
}
192+
159193
}

bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/models/form/package-info.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
* </p>
3636
*/
3737

38-
@Version("5.12.1")
38+
@Version("5.12.2")
3939
package com.adobe.cq.forms.core.components.models.form;
4040

4141
import org.osgi.annotation.versioning.Version;

bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/util/AbstractBaseImpl.java

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,6 @@ public abstract class AbstractBaseImpl extends AbstractFormComponentImpl impleme
114114
@ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL, name = ReservedProperties.PN_MAX_ITEMS)
115115
protected Integer maxItems;
116116

117-
@ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL, name = ReservedProperties.PN_LANG)
118-
protected String lang;
119-
120117
@Nullable
121118
@ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL, name = ReservedProperties.PN_VIEWTYPE)
122119
protected String viewType;
@@ -159,11 +156,6 @@ public boolean isTooltipVisible() {
159156
return tooltipVisible;
160157
}
161158

162-
@Override
163-
public String getLang() {
164-
return lang;
165-
}
166-
167159
@JsonIgnore
168160
public @NotNull Map<String, Object> getCustomLayoutProperties() {
169161
Map<String, Object> customLayoutProperties = super.getCustomLayoutProperties();

bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/util/AbstractComponentImpl.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ public abstract class AbstractComponentImpl implements Component {
9494
private String id;
9595

9696
protected I18n i18n = null;
97+
protected String lang = null;
9798

9899
protected static final String REQ_ATTR_RESOURCE_CALLER_PATH = "resourceCallerPath";
99100

bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/util/AbstractContainerImpl.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ protected <T> Map<String, T> getChildrenModels(@Nullable SlingHttpServletRequest
158158
model = child.adaptTo(modelClass);
159159
if (model instanceof FormComponent && i18n != null) {
160160
((FormComponent) model).setI18n(i18n);
161+
((FormComponent) model).setLang(lang);
161162
}
162163
} catch (Exception e) {
163164
// Log the exception as info, since there can be site component inside form, but we don't care about they being adapted

bundles/af-core/src/main/java/com/adobe/cq/forms/core/components/util/AbstractFormComponentImpl.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ public class AbstractFormComponentImpl extends AbstractComponentImpl implements
102102
@JsonInclude(JsonInclude.Include.NON_NULL)
103103
protected Boolean visible;
104104

105+
@ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL, name = ReservedProperties.PN_LANG)
106+
@Nullable
107+
protected String langJcr;
108+
105109
@ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL, name = ReservedProperties.PN_UNBOUND_FORM_ELEMENT)
106110
@Nullable
107111
protected Boolean unboundFormElement;
@@ -171,6 +175,21 @@ public void setI18n(@Nonnull I18n i18n) {
171175
this.i18n = i18n;
172176
}
173177

178+
public void setLang(@Nullable String lang) {
179+
this.lang = lang;
180+
}
181+
182+
@Override
183+
@Nullable
184+
public String getLang() {
185+
return langJcr != null ? langJcr : lang;
186+
}
187+
188+
@JsonProperty("lang")
189+
public String getLangIfPresent() {
190+
return langJcr;
191+
}
192+
174193
public BaseConstraint.Type getType() {
175194
return null;
176195
}

bundles/af-core/src/test/java/com/adobe/cq/forms/core/components/internal/models/v1/form/FragmentImplTest.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,20 @@
1616

1717
package com.adobe.cq.forms.core.components.internal.models.v1.form;
1818

19+
import java.lang.reflect.Field;
20+
import java.lang.reflect.Method;
1921
import java.util.HashSet;
2022
import java.util.List;
23+
import java.util.Locale;
2124
import java.util.Map;
25+
import java.util.ResourceBundle;
2226
import java.util.Set;
2327
import java.util.stream.Collectors;
2428
import java.util.stream.StreamSupport;
2529

2630
import org.apache.sling.api.resource.Resource;
31+
import org.apache.sling.api.resource.ResourceResolver;
32+
import org.apache.sling.i18n.ResourceBundleProvider;
2733
import org.junit.jupiter.api.Assertions;
2834
import org.junit.jupiter.api.BeforeEach;
2935
import org.junit.jupiter.api.Test;
@@ -40,6 +46,7 @@
4046
import com.adobe.cq.forms.core.components.models.form.TextInput;
4147
import com.adobe.cq.forms.core.components.views.Views;
4248
import com.adobe.cq.forms.core.context.FormsCoreComponentTestContext;
49+
import com.day.cq.i18n.I18n;
4350
import com.day.cq.wcm.api.NameConstants;
4451
import com.day.cq.wcm.msm.api.MSMNameConstants;
4552
import io.wcm.testing.mock.aem.junit5.AemContext;
@@ -201,4 +208,69 @@ void testNoFieldType() {
201208
Fragment fragment = Utils.getComponentUnderTest(PATH_FRAGMENT_WITHOUT_FIELDTYPE, Fragment.class, context);
202209
assertEquals(FieldType.PANEL.getValue(), fragment.getFieldType());
203210
}
211+
212+
@Test
213+
void testGetFragmentContainerI18n() throws Exception {
214+
Fragment fragment = Utils.getComponentUnderTest(PATH_FRAGMENT, Fragment.class, context);
215+
FragmentImpl fragmentImpl = (FragmentImpl) fragment;
216+
217+
// Use reflection to access the private method
218+
Method getFragmentContainerI18nMethod = FragmentImpl.class.getDeclaredMethod("getFragmentContainerI18n", String.class);
219+
getFragmentContainerI18nMethod.setAccessible(true);
220+
221+
// Create mocks for the dependencies
222+
Resource mockFragmentContainer = Mockito.mock(Resource.class);
223+
ResourceResolver mockResourceResolver = Mockito.mock(ResourceResolver.class);
224+
Resource mockBaseResource = Mockito.mock(Resource.class);
225+
ResourceBundle mockResourceBundle = Mockito.mock(ResourceBundle.class);
226+
ResourceBundleProvider mockResourceBundleProvider = Mockito.mock(ResourceBundleProvider.class);
227+
228+
// Use reflection to set the private fragmentContainer field
229+
Field fragmentContainerField = FragmentImpl.class.getDeclaredField("fragmentContainer");
230+
fragmentContainerField.setAccessible(true);
231+
fragmentContainerField.set(fragmentImpl, mockFragmentContainer);
232+
233+
// Use reflection to set the private resourceBundleProvider field
234+
Field resourceBundleProviderField = FragmentImpl.class.getDeclaredField("resourceBundleProvider");
235+
resourceBundleProviderField.setAccessible(true);
236+
resourceBundleProviderField.set(fragmentImpl, mockResourceBundleProvider);
237+
238+
// Setup mock behaviors
239+
Mockito.when(mockFragmentContainer.getResourceResolver()).thenReturn(mockResourceResolver);
240+
Mockito.when(mockFragmentContainer.getPath()).thenReturn("/content/fragment");
241+
Mockito.when(mockResourceResolver.getResource(Mockito.anyString())).thenReturn(mockBaseResource);
242+
Mockito.when(mockResourceBundleProvider.getResourceBundle(Mockito.anyString(), Mockito.any(Locale.class)))
243+
.thenReturn(mockResourceBundle);
244+
245+
// Test case 1: When localeLang is null - should return I18n with null resource bundle
246+
I18n result1 = (I18n) getFragmentContainerI18nMethod.invoke(fragmentImpl, (String) null);
247+
Assertions.assertNotNull(result1, "getFragmentContainerI18n should return a non-null I18n object even when localeLang is null");
248+
249+
// Test case 2: When localeLang is set and resourceBundleProvider is available - should return I18n with resource bundle
250+
I18n result2 = (I18n) getFragmentContainerI18nMethod.invoke(fragmentImpl, "en");
251+
Assertions.assertNotNull(result2, "getFragmentContainerI18n should return a non-null I18n object when localeLang is set");
252+
253+
// Verify that the method called the expected dependencies
254+
Mockito.verify(mockFragmentContainer, Mockito.atLeastOnce()).getResourceResolver();
255+
Mockito.verify(mockFragmentContainer, Mockito.atLeastOnce()).getPath();
256+
Mockito.verify(mockResourceResolver, Mockito.atLeastOnce()).getResource(Mockito.anyString());
257+
Mockito.verify(mockResourceBundleProvider, Mockito.atLeastOnce())
258+
.getResourceBundle(Mockito.anyString(), Mockito.any(Locale.class));
259+
260+
// Test case 3: When resourceBundleProvider is null - should still return I18n object
261+
resourceBundleProviderField.set(fragmentImpl, null);
262+
I18n result3 = (I18n) getFragmentContainerI18nMethod.invoke(fragmentImpl, "fr");
263+
Assertions.assertNotNull(result3,
264+
"getFragmentContainerI18n should return a non-null I18n object even when resourceBundleProvider is null");
265+
266+
// Test case 4: When baseResource is null - should still work
267+
Mockito.when(mockResourceResolver.getResource(Mockito.anyString())).thenReturn(null);
268+
resourceBundleProviderField.set(fragmentImpl, mockResourceBundleProvider);
269+
I18n result4 = (I18n) getFragmentContainerI18nMethod.invoke(fragmentImpl, "de");
270+
Assertions.assertNotNull(result4, "getFragmentContainerI18n should handle null baseResource");
271+
272+
// Test case 5: Test with empty string localeLang
273+
I18n result5 = (I18n) getFragmentContainerI18nMethod.invoke(fragmentImpl, "");
274+
Assertions.assertNotNull(result5, "getFragmentContainerI18n should handle empty string localeLang");
275+
}
204276
}

0 commit comments

Comments
 (0)