diff --git a/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy b/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy index b518e4bc..5dd70440 100644 --- a/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy +++ b/grails-app/taglib/grails/plugin/formfields/FormFieldsTagLib.groovy @@ -18,6 +18,7 @@ package grails.plugin.formfields import grails.core.GrailsApplication import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j import groovy.xml.MarkupBuilder import org.apache.commons.lang.StringUtils import org.grails.buffer.FastStringWriter @@ -44,6 +45,7 @@ import java.text.NumberFormat import static FormFieldsTemplateService.toPropertyNameFormat +@Slf4j class FormFieldsTagLib { static final namespace = 'f' @@ -615,6 +617,9 @@ class FormFieldsTagLib { def message = keysInPreferenceOrder.findResult { key -> message(code: key, default: null) ?: null } + if(log.traceEnabled && !message) { + log.trace("i18n missing translation for one of ${keysInPreferenceOrder}") + } message ?: defaultMessage } diff --git a/src/docs/asciidoc/customizingFieldRendering.adoc b/src/docs/asciidoc/customizingFieldRendering.adoc index 0feddca5..3a465041 100644 --- a/src/docs/asciidoc/customizingFieldRendering.adoc +++ b/src/docs/asciidoc/customizingFieldRendering.adoc @@ -279,7 +279,8 @@ NOTE: If the `bean` attribute was not supplied to `f:field` then `bean`, `type`, If the `label` attribute is not supplied to the `f:field` tag then the label string passed to the field template is resolved by convention. The plugin uses the following order of preference for the label: -* An i18n message using the key '_beanClass_._path_`.label`'. For example when using `` the plugin will try the i18n key `person.address.city.label`. If the property path contains any index it is removed so `` would use the key `author.books.title.label`. +* An i18n message using the key '_beanClass_._path_.label'. For example when using `` the plugin will try the i18n key `author.book.title.label`. If the property path contains any index it is removed so `` would use the key `author.books.title.label`. +* For classes using the same bean class as properties, it is possible to get a key without the class name prefixed. If the configuration value `grails.plugin.fields.i18n.addPathFromRoot` is set to `true` (default: `false`). _Example_: a class `Publisher` has two `Address` properties `authorAddress` and `printAddress`. With `addPathFromRoot=true` they will share the key `address.city.label`. The same goes if `Author` and `Publisher` had a `Book book`, the key would be `book.title.label`, and if they both had a `List books` the key would be `books.title.label` * An i18n message using the key '_objectType_._propertyName_`.label`'. For example when using `` the plugin will try the i18n key `address.city.label`. * The natural property name. For example when using `` the plugin will use the label `"Date Of Birth"`. diff --git a/src/main/groovy/grails/plugin/formfields/BeanPropertyAccessorFactory.groovy b/src/main/groovy/grails/plugin/formfields/BeanPropertyAccessorFactory.groovy index a13ad163..ac0415e5 100644 --- a/src/main/groovy/grails/plugin/formfields/BeanPropertyAccessorFactory.groovy +++ b/src/main/groovy/grails/plugin/formfields/BeanPropertyAccessorFactory.groovy @@ -74,14 +74,18 @@ class BeanPropertyAccessorFactory implements GrailsApplicationAware { DomainProperty domainProperty = resolvePropertyFromPathComponents(beanWrapper, pathElements, params) if (domainProperty != null) { - new DelegatingBeanPropertyAccessorImpl(bean, params.value, params.propertyType as Class, pathFromRoot, domainProperty) + new DelegatingBeanPropertyAccessorImpl(bean, params.value, params.propertyType as Class, pathFromRoot, domainProperty, addPathFromRoot) } else { new BeanPropertyAccessorImpl(params) } } - private DomainProperty resolvePropertyFromPathComponents(BeanWrapper beanWrapper, List pathElements, Map params) { + private boolean getAddPathFromRoot() { + grailsApplication.config.getProperty('grails.plugin.fields.i18n.addPathFromRoot', Boolean) + } + + private DomainProperty resolvePropertyFromPathComponents(BeanWrapper beanWrapper, List pathElements, Map params) { String propertyName = pathElements.remove(0) PersistentEntity beanClass = resolveDomainClass(beanWrapper.wrappedClass) Class propertyType = resolvePropertyType(beanWrapper, beanClass, propertyName) diff --git a/src/main/groovy/grails/plugin/formfields/BeanPropertyAccessorImpl.groovy b/src/main/groovy/grails/plugin/formfields/BeanPropertyAccessorImpl.groovy index 64938b16..003fc3e3 100644 --- a/src/main/groovy/grails/plugin/formfields/BeanPropertyAccessorImpl.groovy +++ b/src/main/groovy/grails/plugin/formfields/BeanPropertyAccessorImpl.groovy @@ -69,6 +69,10 @@ class BeanPropertyAccessorImpl implements BeanPropertyAccessor { grailsApplication.config.getProperty("grails.databinding.$paramName", Boolean, defaultParamValue) } + private boolean getAddPathFromRoot() { + grailsApplication.config.getProperty('grails.plugin.fields.i18n.addPathFromRoot', Boolean, false) + } + List getBeanSuperclasses() { getSuperclassesAndInterfaces(beanType) } @@ -78,10 +82,15 @@ class BeanPropertyAccessorImpl implements BeanPropertyAccessor { } List getLabelKeys() { - [ - "${GrailsNameUtils.getPropertyName(rootBeanType.simpleName)}.${pathFromRoot}.label".replaceAll(/\[(.+)\]/, ''), - "${GrailsNameUtils.getPropertyName(beanType.simpleName)}.${propertyName}.label" - ].unique() as List + List labelKeys = [] + + labelKeys << "${GrailsNameUtils.getPropertyName(rootBeanType.simpleName)}.${pathFromRoot}.label".replaceAll(/\[(.+)\]/, '') + if(addPathFromRoot) { + labelKeys << "${pathFromRoot}.label".replaceAll(/\[(.+)\]/, '') + } + labelKeys << "${GrailsNameUtils.getPropertyName(beanType.simpleName)}.${propertyName}.label".toString() + + return labelKeys.unique() as List } String getDefaultLabel() { diff --git a/src/main/groovy/grails/plugin/formfields/DelegatingBeanPropertyAccessorImpl.groovy b/src/main/groovy/grails/plugin/formfields/DelegatingBeanPropertyAccessorImpl.groovy index e941158d..409a38e1 100644 --- a/src/main/groovy/grails/plugin/formfields/DelegatingBeanPropertyAccessorImpl.groovy +++ b/src/main/groovy/grails/plugin/formfields/DelegatingBeanPropertyAccessorImpl.groovy @@ -30,8 +30,9 @@ class DelegatingBeanPropertyAccessorImpl implements BeanPropertyAccessor { final Class beanType final String propertyName final Class propertyType + final boolean addPathFromRoot - DelegatingBeanPropertyAccessorImpl(Object rootBean, Object value, Class propertyType, String pathFromRoot, DomainProperty domainProperty) { + DelegatingBeanPropertyAccessorImpl(Object rootBean, Object value, Class propertyType, String pathFromRoot, DomainProperty domainProperty, boolean addPathFromRoot) { this.rootBean = rootBean this.value = value this.pathFromRoot = pathFromRoot @@ -39,6 +40,7 @@ class DelegatingBeanPropertyAccessorImpl implements BeanPropertyAccessor { this.propertyType = propertyType this.propertyName = domainProperty.name this.beanType = domainProperty.beanType + this.addPathFromRoot = addPathFromRoot } @Override @@ -97,6 +99,9 @@ class DelegatingBeanPropertyAccessorImpl implements BeanPropertyAccessor { List labelKeys = [] if (rootBean) { labelKeys.add("${GrailsNameUtils.getPropertyName(rootBeanType.simpleName)}.${pathFromRoot}.label".replaceAll(/\[(.+)\]/, '')) + if (addPathFromRoot) { + labelKeys.add("${pathFromRoot}.label".replaceAll(/\[(.+)\]/, '')) + } } labelKeys.addAll(domainProperty.labelKeys) labelKeys.unique() as List diff --git a/src/test/groovy/grails/plugin/formfields/BuildsAccessorFactory.groovy b/src/test/groovy/grails/plugin/formfields/BuildsAccessorFactory.groovy index d5fb99f4..efb43045 100644 --- a/src/test/groovy/grails/plugin/formfields/BuildsAccessorFactory.groovy +++ b/src/test/groovy/grails/plugin/formfields/BuildsAccessorFactory.groovy @@ -21,6 +21,7 @@ abstract class BuildsAccessorFactory extends Specification implements GrailsWebU proxyHandler = new DefaultProxyHandler() grailsDomainClassMappingContext = ref("grailsDomainClassMappingContext") fieldsDomainPropertyFactory = dpf + grailsApplication = ref('grailsApplication') } } } diff --git a/src/test/groovy/grails/plugin/formfields/DomainClassPropertyAccessorSpec.groovy b/src/test/groovy/grails/plugin/formfields/DomainClassPropertyAccessorSpec.groovy index e19cc7cf..2c3b1b67 100644 --- a/src/test/groovy/grails/plugin/formfields/DomainClassPropertyAccessorSpec.groovy +++ b/src/test/groovy/grails/plugin/formfields/DomainClassPropertyAccessorSpec.groovy @@ -158,7 +158,7 @@ class DomainClassPropertyAccessorSpec extends BuildsAccessorFactory { propertyAccessor.domainProperty == null } - @Issue('https://github.com/grails-fields-plugin/grails-fields/issues/37') + @Issue('https://github.com/gpc/fields/issues/37') void "resolves constraints of the '#property' property when the intervening path is null"() { given: def book = new Book() @@ -214,7 +214,7 @@ class DomainClassPropertyAccessorSpec extends BuildsAccessorFactory { propertyAccessor.constraints.inList == ["USA", "UK", "Canada"] } - @Issue('https://github.com/grails-fields-plugin/grails-fields/issues/38') + @Issue('https://github.com/gpc/fields/issues/38') void "label keys for '#property' are '#labels'"() { given: def bean = beanType.list().find { it.class == beanType } @@ -232,6 +232,25 @@ class DomainClassPropertyAccessorSpec extends BuildsAccessorFactory { Author | 'books[0].title' | ['author.books.title.label', 'book.title.label'] } + @Issue('https://github.com/gpc/fields/issues/340') + void "label keys for '#property' are '#labels' when addPathFromRoot == true"() { + given: + config.setAt('grails.plugin.fields.i18n.addPathFromRoot', true) + def bean = beanType.list().find { it.class == beanType} + def propertyAccessor = factory.accessorFor(bean, property) + + expect: + propertyAccessor.labelKeys == labels + + where: + beanType | property | labels + Person | 'name' | ['person.name.label', 'name.label'] + Person | 'dateOfBirth' | ['person.dateOfBirth.label', 'dateOfBirth.label'] + Person | 'address' | ['person.address.label', 'address.label'] + Person | 'address.city' | ['person.address.city.label', 'address.city.label'] + Author | 'books[0].title' | ['author.books.title.label', 'books.title.label', 'book.title.label'] + } + void "default label for '#property' is '#label'"() { given: def bean = beanType.list().first() @@ -292,7 +311,7 @@ class DomainClassPropertyAccessorSpec extends BuildsAccessorFactory { propertyAccessor.invalid } - @Issue('https://github.com/grails-fields-plugin/grails-fields/issues/160') + @Issue('https://github.com/gpc/fields/issues/160') void "resolves transient property"() { given: def propertyAccessor = factory.accessorFor(person, "transientText") @@ -309,7 +328,7 @@ class DomainClassPropertyAccessorSpec extends BuildsAccessorFactory { propertyAccessor.constraints.nullable } - @Issue('https://github.com/grails-fields-plugin/grails-fields/issues/160') + @Issue('https://github.com/gpc/fields/issues/160') void "resolves id property that has no constraints"() { given: def propertyAccessor = factory.accessorFor(person, "id")