From 6506b457d4076ce40846833960b9231112b8d441 Mon Sep 17 00:00:00 2001 From: mugren Date: Wed, 13 Jul 2016 14:47:53 +0200 Subject: [PATCH 1/9] Loading messages with encoding specified --- .../asset/pipeline/i18n/I18nProcessor.groovy | 67 ++++++++++--------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/src/main/groovy/asset/pipeline/i18n/I18nProcessor.groovy b/src/main/groovy/asset/pipeline/i18n/I18nProcessor.groovy index b9786d4..55e9ebf 100644 --- a/src/main/groovy/asset/pipeline/i18n/I18nProcessor.groovy +++ b/src/main/groovy/asset/pipeline/i18n/I18nProcessor.groovy @@ -22,6 +22,7 @@ package asset.pipeline.i18n import asset.pipeline.AbstractProcessor import asset.pipeline.AssetCompiler import asset.pipeline.AssetFile +import grails.io.IOUtils import groovy.transform.CompileStatic import java.util.regex.Matcher import org.springframework.core.io.DefaultResourceLoader @@ -39,7 +40,7 @@ import org.springframework.core.io.ResourceLoader * * - * @author Daniel Ellermann - * @author David Estes + * @author Daniel Ellermann + * @author David Estes * @version 3.0 */ @CompileStatic @@ -64,25 +65,22 @@ class I18nProcessor extends AbstractProcessor { protected static final String PROPERTIES_SUFFIX = '.properties' protected static final String XML_SUFFIX = '.xml' - //-- Fields --------------------------------- ResourceLoader resourceLoader = new DefaultResourceLoader() - //-- Constructors --------------------------- /** * Creates a new i18n resource processor within the given asset * pre-compiler. * - * @param precompiler the given asset pre-compiler + * @param precompiler the given asset pre-compiler */ I18nProcessor(AssetCompiler precompiler) { super(precompiler) } - //-- Public methods ------------------------- @Override @@ -90,28 +88,32 @@ class I18nProcessor extends AbstractProcessor { Matcher m = assetFile.name =~ /._(\w+)\.i18n$/ StringBuilder buf = new StringBuilder('messages') if (m) buf << '_' << m.group(1) - Properties props = loadMessages(buf.toString()) + Properties props + if (assetFile.encoding != null) { + props = loadMessages(buf.toString(), assetFile.encoding) + } else { + props = loadMessages(buf.toString()) + } // At this point, inputText has been pre-processed (I18nPreprocessor). - Map messages = [: ] + Map messages = [:] inputText.toString() - .eachLine { String line -> - if (line != '') { - messages.put line, props.getProperty(line, line) - } + .eachLine { String line -> + if (line != '') { + messages.put line, props.getProperty(line, line) } + } compileJavaScript messages } - //-- Non-public methods --------------------- /** * Compiles JavaScript code from the given localized messages. * - * @param messages the given messages - * @return the compiled JavaScript code + * @param messages the given messages + * @return the compiled JavaScript code */ private String compileJavaScript(Map messages) { StringBuilder buf = new StringBuilder('''(function (win) { @@ -123,9 +125,9 @@ class I18nProcessor extends AbstractProcessor { buf << ',\n' } String value = entry.value - .replace('\\', '\\\\') - .replace('\n', '\\n') - .replace('"', '\\"') + .replace('\\', '\\\\') + .replace('\n', '\\n') + .replace('"', '\\"') buf << ' "' << entry.key << '": "' << value << '"' } buf << ''' @@ -142,15 +144,16 @@ class I18nProcessor extends AbstractProcessor { /** * Loads the message resources from the given file. * - * @param fileName the given base file name - * @return the read message resources + * @param fileName the given base file name + * @return the read message resources * @throws FileNotFoundException if no resource with the required * localized messages exists */ - private Properties loadMessages(String fileName) { + private Properties loadMessages(String fileName, String encoding = 'utf-8') { Resource res = locateResource(fileName) Properties props = new Properties() - props.load res.inputStream + String propertiesString = IOUtils.toString(res.inputStream, encoding) + props.load(new StringReader(propertiesString)) props } @@ -162,35 +165,35 @@ class I18nProcessor extends AbstractProcessor { *
  • in classpath with extension {@code .properties}
  • *
  • in classpath with extension {@code .xml}
  • *
  • in file system in folder {@code grails-app/i18n} with extension - * {@code .properties}
  • + * {@code .properties} *
  • in file system in folder {@code grails-app/i18n} with extension - * {@code .xml}
  • + * {@code .xml} * * - * @param fileName the given base file name - * @return the resource containing the messages + * @param fileName the given base file name + * @return the resource containing the messages * @throws FileNotFoundException if no resource with the required * localized messages exists */ private Resource locateResource(String fileName) { Resource resource = - resourceLoader.getResource(fileName + PROPERTIES_SUFFIX) + resourceLoader.getResource(fileName + PROPERTIES_SUFFIX) if (!resource.exists()) { resource = resourceLoader.getResource(fileName + XML_SUFFIX) } if (!resource.exists()) { resource = resourceLoader.getResource( - "file:grails-app/i18n/${fileName}${PROPERTIES_SUFFIX}" + "file:grails-app/i18n/${fileName}${PROPERTIES_SUFFIX}" ) } if (!resource.exists()) { resource = resourceLoader.getResource( - "file:grails-app/i18n/${fileName}${XML_SUFFIX}" + "file:grails-app/i18n/${fileName}${XML_SUFFIX}" ) } if (!resource.exists()) { throw new FileNotFoundException( - "Cannot find i18n messages file ${fileName}." + "Cannot find i18n messages file ${fileName}." ) } From 8a0ba60f57509c3fa663430e8740e9c9de4e97fa Mon Sep 17 00:00:00 2001 From: Dennie de Lange Date: Mon, 22 May 2017 14:03:31 +0200 Subject: [PATCH 2/9] * Changed version to 2.0.1-SNAPSHOT * Merged commit from mugren --- .gitignore | 2 ++ build.gradle | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6df258b..b45aaf3 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ /build /plugin.xml /web-app/WEB-INF +.idea +*.iml \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8e5fa3d..e6ebd12 100644 --- a/build.gradle +++ b/build.gradle @@ -30,7 +30,7 @@ buildscript { } } -version '2.0.0' +version '2.0.1-SNAPSHOT' group 'org.grails.plugins' apply plugin: 'eclipse' From 40cbde8c81a6b1b061ed0517125b8a47b17be302 Mon Sep 17 00:00:00 2001 From: Dennie de Lange Date: Tue, 23 May 2017 10:31:52 +0200 Subject: [PATCH 3/9] * Added new resolve method, .i18n content is inherited like same as resource bundles are, i.e.: messages_en.i18n inherits from messages.i18n. * Added support for loading messages bundles from plugins * Added support for regexp to select keys from the source properties file. * Removed support for @import _ (not needed anymore). * messages_nl.i18n loads messages from messages_nl.properties and from messages.properties. (They are merged before selecting keys). --- .gitignore | 2 +- README.md | 95 +++++------ build.gradle | 3 +- gradle/wrapper/gradle-wrapper.properties | 4 +- .../asset/pipeline/i18n/I18nAssetFile.groovy | 11 +- .../i18n/I18nAssetPipelineGrailsPlugin.groovy | 17 +- .../pipeline/i18n/I18nPreprocessor.groovy | 98 +++++++----- .../asset/pipeline/i18n/I18nProcessor.groovy | 151 ++++++++++++++++-- 8 files changed, 241 insertions(+), 140 deletions(-) diff --git a/.gitignore b/.gitignore index b45aaf3..84ba590 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ /plugin.xml /web-app/WEB-INF .idea -*.iml \ No newline at end of file +*.iml diff --git a/README.md b/README.md index 7bc3e99..4b4103b 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,20 @@ i18n-asset-pipeline version | required for 1.x | `asset-pipeline` version 2.0.0 or higher 2.x | Grails 3.x + +## Breaking update information 2.0.1 +* Removed @import option, replaced with default i18n messages inheritance + (i.e. all keys from messages.i18n are merged with the localized version messages_nl.properties) +* Added support for loading messages.properties from plugins, + *Note:* Changes to the messages properties files are not hot reloaded. You need + to update the i18n file for this to happen. +* Added support for custom named i18n properties files (i.e. ) + will actually search for a messages bundle shared.properties +* Added support for regular expressions for selecting key values from the message bundle. + simply add `regexp: ` in the .i18n file. + For example: `regexp: portal\..*` adds all messages starting with `portal.`. +* Added support for correctly loading the encoding of the messages bundle. + ## Installation To use this plugin you have to add the following code to your `build.gradle`: @@ -25,12 +39,12 @@ To use this plugin you have to add the following code to your `build.gradle`: ```groovy buildscript { dependencies { - classpath 'org.amcworld.plugins:i18n-asset-pipeline:2.0.0' + classpath 'org.amcworld.plugins:i18n-asset-pipeline:2.0.1' } } dependencies { - runtime 'org.grails.plugins:i18n-asset-pipeline:2.0.0' + runtime 'org.grails.plugins:i18n-asset-pipeline:2.0.1' } ``` @@ -62,19 +76,14 @@ Each i18n file must be defined according to the following rules: * Files are line based. * Lines are trimmed (i. e. leading and terminating whitespaces are removed). * Empty lines and lines starting with a hash `#` (comment lines) are ignored. -* Lines starting with `@import` *`url`* are resolved by importing file - *`url`*, processing it according to these rules, and replacing the - `@import` statement by its content. The import file may contain further - import statements, even circular ones. You may omit file extension `.i18n` - in *`url`*. +* Lines starting with `regexp:` are matched to all keys from the message file. + For example: `regexp:.*` adds all keys to the message file. * All other lines are treated as messsage codes which are translated to the required language. * Comments after import statements and message codes are not allowed. Each i18n file may contain asset-pipeline `require` statements to load other -assets such as JavaScript files. **ATTENTION!** Don't use `require` to load -other i18n files because they will not be processed correctly. Use the -`@import` declaration instead. +assets such as JavaScript files. ## Typical file structure @@ -95,54 +104,27 @@ Then, you should have the same set of files in e. g. `grails-app/assets/i18n`: * `messages_es.i18n` * `messages_fr.i18n` -Normally, you would have to declare the same set of message codes in each file. -To DRY, add a file `_messages.i18n` to `grails-app/assets/i18n` (the -leading underscore prevents the i18n file to be compiled itself): - -``` -# -# _messages.i18n -# List of message codes that should be available on client-side. -# - -# Add your messages codes here: -default.btn.cancel -default.btn.ok -contact.foo.bar - -``` - -Then, you can import this file in all other files, e. g.: - -``` -# -# messages.i18n -# Client-side i18n, English messages. -# - -@import _messages - -``` +The codes are can be added manually to each file, but the default inheritance of i18n properties +files are enforced. So the entries of `messages_en_UK.i18n` contains all entries from: -``` -# -# messages_de.i18n -# Client-side i18n, German messages. -# - -@import _messages - -``` +* `messages.i18n` +* `messages_en.i18n` +* `messages_en_UK.i18n` -``` -# -# messages_es.i18n -# Client-side i18n, Spanish messages. -# +To add all properties defined in the `messages.properties` bundle simply add `regexp:.*` +to `messages.i18n` and add empty files for each supported client side locale. + +* `messages.i18n` (contains `regexp:.*`) +* `messages_en.i18n` (empty) +* `messages_en_UK.i18n` (empty) +* `messages_nl.i18n` (empty) -@import _messages +This results in: -``` +* `messages.js` (all entries from messages.properties) +* `messages_en.js` (all entries from messages.properties merged with messages_en.properties) +* `messages_en_UK.js` (all entries from messages.properties merged with messages_en.properties merged with messages_en_UK.properties) +* `messages_nl.js` (all entries from messages.properties merged with messages_nl.properties) ## Including localized assets @@ -165,11 +147,16 @@ Examples: ``` + + + ## Author This plugin was written by [Daniel Ellermann](mailto:d.ellermann@amc-world.de) ([AMC World Technologies GmbH][amc-world]). +Updated by [Dennie de Lange](mailto:dennie@tkvw.nl). + ## License This plugin was published under the diff --git a/build.gradle b/build.gradle index e6ebd12..cfb3eed 100644 --- a/build.gradle +++ b/build.gradle @@ -65,7 +65,8 @@ dependencies { compile 'org.grails:grails-core' compile 'org.grails:grails-dependencies' compile 'org.grails:grails-web-boot' - compile 'org.grails.plugins:asset-pipeline:3.1.0' + compile 'com.bertramlabs.plugins:asset-pipeline-grails:2.13.2' + compile 'com.bertramlabs.plugins:asset-pipeline-core:2.13.2' console 'org.grails:grails-console' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3869368..cc8aa1a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Nov 27 23:09:32 CET 2015 +#Mon May 22 15:03:29 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-all.zip diff --git a/src/main/groovy/asset/pipeline/i18n/I18nAssetFile.groovy b/src/main/groovy/asset/pipeline/i18n/I18nAssetFile.groovy index 55d5776..c11ec05 100644 --- a/src/main/groovy/asset/pipeline/i18n/I18nAssetFile.groovy +++ b/src/main/groovy/asset/pipeline/i18n/I18nAssetFile.groovy @@ -60,16 +60,7 @@ class I18nAssetFile extends AbstractAssetFile { String processedStream(AssetCompiler precompiler) { def skipCache = precompiler ?: (!processors || processors.size() == 0) - String fileText - if(baseFile?.encoding || encoding) { - fileText = inputStream?.getText( - baseFile?.encoding ? baseFile.encoding : encoding - ) - } else { - fileText = inputStream?.text - } - - fileText = I18nPreprocessor.instance.preprocess(this, fileText) + String fileText = I18nPreprocessor.instance.preprocess(this) def md5 = AssetHelper.getByteDigest(fileText.bytes) if (!skipCache) { diff --git a/src/main/groovy/asset/pipeline/i18n/I18nAssetPipelineGrailsPlugin.groovy b/src/main/groovy/asset/pipeline/i18n/I18nAssetPipelineGrailsPlugin.groovy index 8d25e6b..25c677e 100644 --- a/src/main/groovy/asset/pipeline/i18n/I18nAssetPipelineGrailsPlugin.groovy +++ b/src/main/groovy/asset/pipeline/i18n/I18nAssetPipelineGrailsPlugin.groovy @@ -19,8 +19,7 @@ package asset.pipeline.i18n -import grails.plugins.* - +import grails.plugins.Plugin class I18nAssetPipelineGrailsPlugin extends Plugin { @@ -44,28 +43,24 @@ class I18nAssetPipelineGrailsPlugin extends Plugin { url: 'https://github.com/dellermann/i18n-asset-pipeline/issues' ] def scm = [url: 'https://github.com/dellermann/i18n-asset-pipeline'] - + def watchedResources = ['file:./grails-app/i18n/*.properties'] //-- Public methods ------------------------- - Closure doWithSpring() { - { -> - // TODO Implement runtime spring config (optional) - } - } + Closure doWithSpring() {{ -> + + }} void doWithDynamicMethods() { // TODO Implement registering dynamic methods to classes (optional) } void doWithApplicationContext() { - // TODO Implement post initialization spring config (optional) + } void onChange(Map event) { // TODO Implement code that is executed when any artefact that this plugin is - // watching is modified and reloaded. The event contains: event.source, - // event.application, event.manager, event.ctx, and event.plugin. } void onConfigChange(Map event) { diff --git a/src/main/groovy/asset/pipeline/i18n/I18nPreprocessor.groovy b/src/main/groovy/asset/pipeline/i18n/I18nPreprocessor.groovy index d22e889..1f70099 100644 --- a/src/main/groovy/asset/pipeline/i18n/I18nPreprocessor.groovy +++ b/src/main/groovy/asset/pipeline/i18n/I18nPreprocessor.groovy @@ -45,8 +45,8 @@ class I18nPreprocessor { protected static final Pattern REGEX_IGNORE = ~/^\s*(?:#.*)?$/ protected static final Pattern REGEX_IMPORT = ~/^\s*@import\s+(.+)$/ - - + static final Pattern REGEX_FILENAME_SPLIT = ~/(\w+?)(_\w+)?\.i18n$/ + protected static final String EXTENSION = '.i18n' //-- Constructors --------------------------- protected I18nPreprocessor() {} @@ -71,17 +71,60 @@ class I18nPreprocessor { * @param input the content of the i18n file * @return the pre-processed content */ - String preprocess(AssetFile file, String input = file?.inputStream?.text) { - if (!input) { - return '' + String preprocess(AssetFile file) { + StringBuilder sb = new StringBuilder() + + Set resultCodes = new HashSet<>() + + String filename = file.name + if (!filename.endsWith(EXTENSION)) { + filename += EXTENSION } - Set fileHistory = new HashSet<>() - fileHistory << file + Matcher matcher = REGEX_FILENAME_SPLIT.matcher(filename) + matcher.matches() + + String baseFilename = matcher.group(1) + String locales = matcher.group(2) + + if(locales){ + String[] localeParts = locales?.split('_') - doPreprocess input, fileHistory + String localeFile = baseFilename + for(int i=0;i + acc.append(item).append('\n') + }.toString() } + private void doPreprocess(AssetFile file,Set codes){ + if(file==null) return + + def fileContent + String encoding = file.baseFile?.encoding?:file.encoding + if(encoding){ + fileContent = file.inputStream.getText(encoding) + } + else{ + fileContent = file.inputStream.text + } + + doPreprocess(fileContent,codes) + } + //-- Non-public methods --------------------- @@ -95,48 +138,15 @@ class I18nPreprocessor { * circular dependencies * @return the pre-processed content */ - private String doPreprocess(String input, Set fileHistory) { - StringBuffer buf = new StringBuffer(input.length()) + private void doPreprocess(String input, Set codes) { input.eachLine { String line -> line = line.trim() if (line ==~ REGEX_IGNORE) return - - Matcher m = line =~ REGEX_IMPORT - if (m) { - line = resolveImport(m.group(1).trim(), fileHistory) - if (!line) return - line = line.trim() - } - buf << line << '\n' + + codes.add(line) } - - buf.toString() } - /** - * Loads the import file with the file name and processes its content. - * - * @param fileName the name of the import file - * @param fileHistory the history of all import files that have been - * processed already; this is needed to handle - * circular dependencies - * @return the pre-processed content of the import file - */ - private String resolveImport(String fileName, Set fileHistory) { - if (!fileName.endsWith('.i18n')) { - fileName += '.i18n' - } - - AssetFile importFile = AssetHelper.fileForUri(fileName) - if (importFile == null || importFile in fileHistory) { - return '' - } - - fileHistory << importFile - doPreprocess importFile.inputStream.text, fileHistory - } - - //-- Inner classes -------------------------- private static class InstanceHolder { diff --git a/src/main/groovy/asset/pipeline/i18n/I18nProcessor.groovy b/src/main/groovy/asset/pipeline/i18n/I18nProcessor.groovy index 55e9ebf..f6b6f4b 100644 --- a/src/main/groovy/asset/pipeline/i18n/I18nProcessor.groovy +++ b/src/main/groovy/asset/pipeline/i18n/I18nProcessor.groovy @@ -22,13 +22,30 @@ package asset.pipeline.i18n import asset.pipeline.AbstractProcessor import asset.pipeline.AssetCompiler import asset.pipeline.AssetFile +import asset.pipeline.AssetPipelineConfigHolder +import asset.pipeline.fs.AssetResolver +import asset.pipeline.fs.JarAssetResolver import grails.io.IOUtils +import grails.plugins.GrailsPlugin +import grails.plugins.Plugin +import grails.util.Environment +import grails.util.Holder +import grails.util.Holders import groovy.transform.CompileStatic +import org.grails.plugins.BinaryGrailsPlugin +import org.springframework.core.io.FileSystemResource +import org.springframework.core.io.InputStreamResource +import org.springframework.core.io.UrlResource + +import java.util.jar.JarEntry +import java.util.jar.JarFile import java.util.regex.Matcher import org.springframework.core.io.DefaultResourceLoader import org.springframework.core.io.Resource import org.springframework.core.io.ResourceLoader +import java.util.regex.Pattern + /** * The class {@code I18nProcessor} represents an asset processor which converts @@ -85,14 +102,41 @@ class I18nProcessor extends AbstractProcessor { @Override String process(String inputText, AssetFile assetFile) { - Matcher m = assetFile.name =~ /._(\w+)\.i18n$/ - StringBuilder buf = new StringBuilder('messages') - if (m) buf << '_' << m.group(1) + Matcher m = assetFile.name =~ /(\w+?)(_\w+)?\.i18n$/ + + def options = [] + + if(m){ + def baseFile = m.group(1) + if (m.group(2)){ + def locales = m.group(2).split('_') + + def sb = new StringBuffer(baseFile) + for(locale in locales){ + if(locale.empty){ + options << sb.toString() + } + else{ + sb.append('_').append(locale) + options << sb.toString() + } + } + } + else{ + options << baseFile + } + } + else{ + options << 'messages' + } + + + Properties props if (assetFile.encoding != null) { - props = loadMessages(buf.toString(), assetFile.encoding) + props = loadMessages(options, assetFile.encoding) } else { - props = loadMessages(buf.toString()) + props = loadMessages(options) } // At this point, inputText has been pre-processed (I18nPreprocessor). @@ -100,8 +144,15 @@ class I18nProcessor extends AbstractProcessor { inputText.toString() .eachLine { String line -> if (line != '') { - messages.put line, props.getProperty(line, line) - } + if(line.startsWith('regexp:')){ + def p = Pattern.compile(line.substring('regexp:'.length()).trim()) + Map matchedEntries = props.findAll{p.matcher((String)it.key).matches()} + messages.putAll((Map)(Map)matchedEntries) + } + else{ + messages.put line, props.getProperty(line, line/*defaultValue*/) + } + } } compileJavaScript messages @@ -149,13 +200,22 @@ class I18nProcessor extends AbstractProcessor { * @throws FileNotFoundException if no resource with the required * localized messages exists */ - private Properties loadMessages(String fileName, String encoding = 'utf-8') { - Resource res = locateResource(fileName) - Properties props = new Properties() - String propertiesString = IOUtils.toString(res.inputStream, encoding) - props.load(new StringReader(propertiesString)) - - props + private Properties loadMessages(List options, String encoding = 'utf-8') { + Properties messages = new Properties() + + for(option in options){ + Properties props = new Properties() + try{ + Resource res = locateResource(option) + String propertiesString = IOUtils.toString(res.inputStream, encoding) + props.load(new StringReader(propertiesString)) + messages.putAll(props) + } + catch(Exception e){ + System.out.println "Could not load file ${option}" + } + } + messages } /** @@ -192,11 +252,68 @@ class I18nProcessor extends AbstractProcessor { ) } if (!resource.exists()) { - throw new FileNotFoundException( - "Cannot find i18n messages file ${fileName}." - ) + // Nuclear approach, scan all jar files for the messages file + resource = loadFromAssetResolvers(fileName) + + if(!resource){ + throw new FileNotFoundException( + "Cannot find i18n messages file ${fileName}." + ) + } } resource } + + private static Resource loadFromAssetResolvers(String filename){ + Resource result = null + if(Environment.developmentMode){ + for(GrailsPlugin plugin in Holders.pluginManager.allPlugins){ + if(plugin instanceof BinaryGrailsPlugin){ + String projectDir = ((BinaryGrailsPlugin)plugin).projectDirectory + String i18nPropertiesPath = new File(projectDir,"grails-app/i18n").canonicalPath + + if(new File(i18nPropertiesPath,filename+PROPERTIES_SUFFIX).exists()){ + result = new FileSystemResource(new File(i18nPropertiesPath,filename+PROPERTIES_SUFFIX)) + break + } + else if(new File(i18nPropertiesPath,filename+XML_SUFFIX).exists()){ + result = new FileSystemResource(new File(i18nPropertiesPath,filename+XML_SUFFIX)) + break + } + } + else{ + def classLoader = plugin.getPluginClass().getClassLoader() + def url = classLoader.getResource(filename+PROPERTIES_SUFFIX) + if(!url){ + url = classLoader.getResource(filename+XML_SUFFIX) + } + if(url){ + result = new UrlResource(url) + break + } + } + } + } + else{ + def filenameOptions = [filename+PROPERTIES_SUFFIX,filename+XML_SUFFIX] + for(AssetResolver resolver in AssetPipelineConfigHolder.resolvers){ + if(resolver instanceof JarAssetResolver){ + JarFile jarFile = ((JarAssetResolver)resolver).baseJar; + Enumeration entries = jarFile.entries() + + while(entries.hasMoreElements()){ + JarEntry entry = entries.nextElement() + String entryName = entry.name + if(filenameOptions.contains(entryName)){ + result = new InputStreamResource(jarFile.getInputStream(entry)) + break + } + } + if(result) break + } + } + } + return result + } } From 54f5b2af1c08f978364250277b657e47c56c6000 Mon Sep 17 00:00:00 2001 From: Dennie de Lange Date: Tue, 23 May 2017 12:19:39 +0200 Subject: [PATCH 4/9] * Updated gradle * Updated grails version * Added i18n.js javascript to allow more advanced formatting on the client. * Added i18n usage to README.MD --- README.md | 10 +++++ build.gradle | 10 ++++- gradle.properties | 4 +- gradle/wrapper/gradle-wrapper.properties | 4 +- .../assets/javascripts/i18n-asset/i18n.js | 37 +++++++++++++++++++ .../asset/pipeline/i18n/I18nTagLib.groovy | 3 +- 6 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 grails-app/assets/javascripts/i18n-asset/i18n.js diff --git a/README.md b/README.md index 4b4103b..ef6ee6b 100644 --- a/README.md +++ b/README.md @@ -147,8 +147,18 @@ Examples: ``` +## i18n.js +A more advanced i18n.js is included to add more advanced formatting on the client. +It can be included with `0){ + message = this.format(message,args); + } + return message; + }, + i18n.prototype.format = function(source,params){ + if ( params === undefined ) { + return source; + } + for(var i=0;i Properties manifest = AssetPipelineConfigHolder.manifest ApplicationContext ctx = grailsApplication.mainContext - String mapping = assetProcessorService.assetMapping - + def l = attrs.remove('locale') ?: '' String locale = '' if (l instanceof Locale || l instanceof CharSequence) { From e1304dd82d60d0686b2cfe85456737080cd36e54 Mon Sep 17 00:00:00 2001 From: Dennie de Lange Date: Tue, 23 May 2017 12:21:28 +0200 Subject: [PATCH 5/9] * Changed version to 2.1.0-SNAPSHOT --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f5f3637..610ca36 100644 --- a/build.gradle +++ b/build.gradle @@ -31,7 +31,7 @@ buildscript { } } -version '2.0.1-SNAPSHOT' +version '2.1.0-SNAPSHOT' group 'org.grails.plugins' apply plugin: 'eclipse' From 7a221497a0b1493ddb2ce5c5db8fc6ea75a0e79d Mon Sep 17 00:00:00 2001 From: Dennie de Lange Date: Tue, 23 May 2017 12:27:11 +0200 Subject: [PATCH 6/9] * Fixed typos --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ef6ee6b..2a38084 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ i18n-asset-pipeline version | required for 2.x | Grails 3.x -## Breaking update information 2.0.1 +## Breaking update information 2.1.0 * Removed @import option, replaced with default i18n messages inheritance - (i.e. all keys from messages.i18n are merged with the localized version messages_nl.properties) + (i.e. all keys from messages.i18n are merged with the localized version messages_nl.i18n) * Added support for loading messages.properties from plugins, *Note:* Changes to the messages properties files are not hot reloaded. You need to update the i18n file for this to happen. From 400ddcce011ec727f9d63853a995d32c638af217 Mon Sep 17 00:00:00 2001 From: Dennie de Lange Date: Mon, 29 May 2017 11:08:20 +0200 Subject: [PATCH 7/9] Default properties files was not considered. --- README.md | 4 ++-- grails-app/taglib/asset/pipeline/i18n/I18nTagLib.groovy | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 2a38084..dff41b5 100644 --- a/README.md +++ b/README.md @@ -39,12 +39,12 @@ To use this plugin you have to add the following code to your `build.gradle`: ```groovy buildscript { dependencies { - classpath 'org.amcworld.plugins:i18n-asset-pipeline:2.0.1' + classpath 'org.amcworld.plugins:i18n-asset-pipeline:2.1.0-SNAPSHOT' } } dependencies { - runtime 'org.grails.plugins:i18n-asset-pipeline:2.0.1' + runtime 'org.grails.plugins:i18n-asset-pipeline:2.1.0-SNAPSHOT' } ``` diff --git a/grails-app/taglib/asset/pipeline/i18n/I18nTagLib.groovy b/grails-app/taglib/asset/pipeline/i18n/I18nTagLib.groovy index a08d079..00f3e7d 100644 --- a/grails-app/taglib/asset/pipeline/i18n/I18nTagLib.groovy +++ b/grails-app/taglib/asset/pipeline/i18n/I18nTagLib.groovy @@ -78,9 +78,9 @@ class I18nTagLib implements TagLibrary { String [] parts = locale.split('_') String src = null - for (int i = parts.length - 1; i >= 0 && !src; --i) { + for (int i = parts.length; i >= 0 && !src; --i) { StringBuilder buf = new StringBuilder(name) - for (int j = 0; j <= i; j++) { + for (int j = 0; j < i; j++) { buf << '_' << parts[j] } buf << '.js' @@ -115,7 +115,8 @@ class I18nTagLib implements TagLibrary { log.debug "Localized asset not found - using default asset '${name}.js'" } } - - out << asset.javascript(src: src ?: (name + '.js')) + if(src){ + out << asset.javascript(src: src ?: (name + '.js')) + } } } From 640a997aa3f3aea2d929f2f449e98f7dd2c56f3f Mon Sep 17 00:00:00 2001 From: Dennie de Lange Date: Mon, 29 May 2017 12:25:16 +0200 Subject: [PATCH 8/9] Developmentmode does not guarantee Holders.pluginManager to be available, changed to simply checking for Holders.pluginManager not null. Added sorted output for keys. --- src/main/groovy/asset/pipeline/i18n/I18nProcessor.groovy | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/groovy/asset/pipeline/i18n/I18nProcessor.groovy b/src/main/groovy/asset/pipeline/i18n/I18nProcessor.groovy index f6b6f4b..6badb4f 100644 --- a/src/main/groovy/asset/pipeline/i18n/I18nProcessor.groovy +++ b/src/main/groovy/asset/pipeline/i18n/I18nProcessor.groovy @@ -171,6 +171,7 @@ class I18nProcessor extends AbstractProcessor { var messages = { ''') int i = 0 + messages = messages.sort() for (Map.Entry entry in messages.entrySet()) { if (i++ > 0) { buf << ',\n' @@ -267,8 +268,9 @@ class I18nProcessor extends AbstractProcessor { private static Resource loadFromAssetResolvers(String filename){ Resource result = null - if(Environment.developmentMode){ - for(GrailsPlugin plugin in Holders.pluginManager.allPlugins){ + def pluginManager = Holders.pluginManager + if(pluginManager != null){ + for(GrailsPlugin plugin in pluginManager.allPlugins){ if(plugin instanceof BinaryGrailsPlugin){ String projectDir = ((BinaryGrailsPlugin)plugin).projectDirectory String i18nPropertiesPath = new File(projectDir,"grails-app/i18n").canonicalPath From bc5b7f562d180adc27451db20f6336bd7abf8a93 Mon Sep 17 00:00:00 2001 From: Dennie de Lange Date: Thu, 1 Jun 2017 15:39:33 +0200 Subject: [PATCH 9/9] Add warning if no messagebundle is loaded. --- grails-app/assets/javascripts/i18n-asset/i18n.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/grails-app/assets/javascripts/i18n-asset/i18n.js b/grails-app/assets/javascripts/i18n-asset/i18n.js index b40341e..daca186 100644 --- a/grails-app/assets/javascripts/i18n-asset/i18n.js +++ b/grails-app/assets/javascripts/i18n-asset/i18n.js @@ -1,6 +1,10 @@ ;(function(factory){ factory(window.$L); }(function(messages){ + if(!messages){ + console.log('No messagebundle loaded, always returning message code as fallback.'); + } + var emptyArray = []; var i18n = function(){} i18n.prototype.md = function(code,defaultMessage){ @@ -31,7 +35,7 @@ return '[' + code + ']'; }, i18n.prototype.getMessage = function(code){ - return messages(code) + return messages? messages(code): null; } window.$i18n = new i18n(); })); \ No newline at end of file