diff --git a/scripts/initializr/build.sh b/scripts/initializr/build.sh index 8a99f56684..b356c698ba 100755 --- a/scripts/initializr/build.sh +++ b/scripts/initializr/build.sh @@ -1,16 +1,37 @@ #!/bin/bash set -e MVNW="./mvnw" +RUN_TESTS_DEFAULT="true" + +function should_run_tests { + if [ "$INITIALIZR_RUN_TESTS" == "" ]; then + [ "$RUN_TESTS_DEFAULT" == "true" ] + return + fi + [ "$INITIALIZR_RUN_TESTS" == "true" ] +} + +function run_common_tests { + if should_run_tests; then + "$MVNW" "-pl" "common" "-am" "test" "-DskipTests=false" "-DfailIfNoTests=false" "-Dtest=GeneratorModelMatrixTest,GeneratorModelLocalizationPackagingTest" "-U" "-e" + fi +} function mac_desktop { + run_common_tests + "$MVNW" "package" "-DskipTests" "-Dcodename1.platform=javase" "-Dcodename1.buildTarget=mac-os-x-desktop" "-U" "-e" } function windows_desktop { + run_common_tests + "$MVNW" "package" "-DskipTests" "-Dcodename1.platform=javase" "-Dcodename1.buildTarget=windows-desktop" "-U" "-e" } function windows_device { + run_common_tests + "$MVNW" "package" "-DskipTests" "-Dcodename1.platform=win" "-Dcodename1.buildTarget=windows-device" "-U" "-e" } @@ -19,14 +40,20 @@ function uwp { "windows_device" } function javascript { + run_common_tests + "$MVNW" "package" "-DskipTests" "-Dcodename1.platform=javascript" "-Dcodename1.buildTarget=javascript" "-U" "-e" } function android { + run_common_tests + "$MVNW" "package" "-DskipTests" "-Dcodename1.platform=android" "-Dcodename1.buildTarget=android-device" "-U" "-e" } function xcode { + run_common_tests + "$MVNW" "package" "-DskipTests" "-Dcodename1.platform=ios" "-Dcodename1.buildTarget=ios-source" "-U" "-e" } @@ -34,18 +61,26 @@ function ios_source { "xcode" } function android_source { + run_common_tests + "$MVNW" "package" "-DskipTests" "-Dcodename1.platform=android" "-Dcodename1.buildTarget=android-source" "-U" "-e" } function ios { + run_common_tests + "$MVNW" "package" "-DskipTests" "-Dcodename1.platform=ios" "-Dcodename1.buildTarget=ios-device" "-U" "-e" } function ios_release { + run_common_tests + "$MVNW" "package" "-DskipTests" "-Dcodename1.platform=ios" "-Dcodename1.buildTarget=ios-device-release" "-U" "-e" } function jar { + run_common_tests + "$MVNW" "-Pexecutable-jar" "package" "-Dcodename1.platform=javase" "-DskipTests" "-U" "-e" } diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java index e555b41095..0b295528f7 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/model/GeneratorModel.java @@ -90,9 +90,10 @@ private void addLocalizationEntries(Map mergedEntries) throws IO if (!isBareTemplate() || !options.includeLocalizationBundles) { return; } + addLocalizationEntry(mergedEntries, "messages.properties"); copySingleTextEntryToMap( - "common/src/main/resources/messages.properties", - readResourceToString("/messages.properties"), + "common/src/main/resources/messages_en.properties", + readRequiredResourceToString("/messages.properties"), mergedEntries, ZipEntryType.COMMON ); @@ -100,15 +101,19 @@ private void addLocalizationEntries(Map mergedEntries) throws IO if (language == ProjectOptions.PreviewLanguage.ENGLISH) { continue; } - copySingleTextEntryToMap( - "common/src/main/resources/messages_" + language.bundleSuffix + ".properties", - readResourceToString("/messages_" + language.bundleSuffix + ".properties"), - mergedEntries, - ZipEntryType.COMMON - ); + addLocalizationEntry(mergedEntries, "messages_" + language.bundleSuffix + ".properties"); } } + private void addLocalizationEntry(Map mergedEntries, String fileName) throws IOException { + copySingleTextEntryToMap( + "common/src/main/resources/" + fileName, + readRequiredResourceToString("/" + fileName), + mergedEntries, + ZipEntryType.COMMON + ); + } + private void copyZipEntriesToMap(String zipResource, Map mergedEntries, ZipEntryType zipType) throws IOException { try(ZipInputStream zis = new ZipInputStream(getResourceAsStream(zipResource))) { ZipEntry entry = zis.getNextEntry(); @@ -216,7 +221,16 @@ private String injectJavaLocalizationBootstrap(String content) { String method = "\n @Override\n" + " public void init(Object context) {\n" + " String language = L10NManager.getInstance().getLanguage();\n" + + " if (language == null || language.length() == 0) {\n" + + " language = \"en\";\n" + + " }\n" + " Hashtable bundle = Resources.getGlobalResources().getL10N(\"messages\", language);\n" + + " if (bundle == null && language != null && language.indexOf('_') > 0) {\n" + + " bundle = Resources.getGlobalResources().getL10N(\"messages\", language.substring(0, language.indexOf('_')));\n" + + " }\n" + + " if (bundle == null) {\n" + + " bundle = Resources.getGlobalResources().getL10N(\"messages\", \"en\");\n" + + " }\n" + " UIManager.getInstance().setBundle(bundle);\n" + " }\n\n"; int firstBrace = content.indexOf('{'); @@ -232,8 +246,17 @@ private String injectKotlinLocalizationBootstrap(String content) { } content = StringUtil.replaceAll(content, "import com.codename1.system.Lifecycle\n", "import com.codename1.system.Lifecycle\nimport com.codename1.l10n.L10NManager\nimport com.codename1.ui.plaf.UIManager\nimport com.codename1.ui.util.Resources\nimport java.util.Hashtable\n"); String method = "\n override fun init(context: Any?) {\n" - + " val language = L10NManager.getInstance().language\n" - + " val bundle: Hashtable? = Resources.getGlobalResources().getL10N(\"messages\", language)\n" + + " var language = L10NManager.getInstance().language\n" + + " if (language == null || language.length == 0) {\n" + + " language = \"en\"\n" + + " }\n" + + " var bundle: Hashtable? = Resources.getGlobalResources().getL10N(\"messages\", language)\n" + + " if (bundle == null && language != null && language.indexOf('_') > 0) {\n" + + " bundle = Resources.getGlobalResources().getL10N(\"messages\", language.substring(0, language.indexOf('_')))\n" + + " }\n" + + " if (bundle == null) {\n" + + " bundle = Resources.getGlobalResources().getL10N(\"messages\", \"en\")\n" + + " }\n" + " UIManager.getInstance().setBundle(bundle)\n" + " }\n\n"; int firstBrace = content.indexOf('{'); @@ -462,6 +485,18 @@ private static String readResourceToString(String resourcePath) throws IOExcepti } } + static String readRequiredResourceToString(String resourcePath) throws IOException { + InputStream inputStream = getResourceAsStream(resourcePath); + if (inputStream == null) { + throw new IOException("Missing required resource " + resourcePath); + } + try { + return readToStringNoClose(inputStream); + } finally { + inputStream.close(); + } + } + private static String readToStringNoClose(InputStream is) throws IOException { return StringUtil.newString(readToBytesNoClose(is)); } diff --git a/scripts/initializr/common/src/main/java/com/codename1/initializr/ui/TemplatePreviewPanel.java b/scripts/initializr/common/src/main/java/com/codename1/initializr/ui/TemplatePreviewPanel.java index 647e1ebdc6..fb57ed77e4 100644 --- a/scripts/initializr/common/src/main/java/com/codename1/initializr/ui/TemplatePreviewPanel.java +++ b/scripts/initializr/common/src/main/java/com/codename1/initializr/ui/TemplatePreviewPanel.java @@ -3,7 +3,6 @@ import com.codename1.components.ImageViewer; import com.codename1.initializr.model.ProjectOptions; import com.codename1.initializr.model.Template; -import com.codename1.io.Log; import com.codename1.io.Properties; import com.codename1.ui.Button; import com.codename1.ui.Component; @@ -100,28 +99,18 @@ private Hashtable findBundle(ProjectOptions.PreviewLanguage lang if (language == null) { return null; } - Resources resources = Resources.getGlobalResources(); - if (resources != null) { - try { - Hashtable exact = resources.getL10N("messages", language.bundleSuffix); - if (exact != null) { - return exact; - } - int split = language.bundleSuffix.indexOf('_'); - if (split > 0) { - Hashtable languageOnly = resources.getL10N("messages", language.bundleSuffix.substring(0, split)); - if (languageOnly != null) { - return languageOnly; - } - } - } catch (RuntimeException err) { - Log.e(err); - } - } - String[] candidates = language.bundleSuffix.indexOf('_') > 0 - ? new String[]{"/messages_" + language.bundleSuffix + ".properties", "/messages_" + language.bundleSuffix.substring(0, language.bundleSuffix.indexOf('_')) + ".properties", "/messages.properties"} - : new String[]{"/messages_" + language.bundleSuffix + ".properties", "/messages.properties"}; + ? new String[]{ + "/messages_" + language.bundleSuffix + ".properties", + "/messages_" + language.bundleSuffix.substring(0, language.bundleSuffix.indexOf('_')) + ".properties", + "/messages_en.properties", + "/messages.properties" + } + : new String[]{ + "/messages_" + language.bundleSuffix + ".properties", + "/messages_en.properties", + "/messages.properties" + }; for (String path : candidates) { Hashtable loaded = loadBundleProperties(path); @@ -146,8 +135,7 @@ private Hashtable loadBundleProperties(String resourcePath) { } return out; } catch (Exception err) { - Log.e(err); - return null; + throw new RuntimeException("Failed to load localization bundle " + resourcePath, err); } } diff --git a/scripts/initializr/common/src/main/resources/messages_en.properties b/scripts/initializr/common/src/main/resources/messages_en.properties new file mode 100644 index 0000000000..4f24396858 --- /dev/null +++ b/scripts/initializr/common/src/main/resources/messages_en.properties @@ -0,0 +1,7 @@ +Hi\ World=Hi World +Hello\ World=Hello World +Hello\ Command=Hello Command +Hello\ Codename\ One=Hello Codename One +Welcome\ to\ Codename\ One=Welcome to Codename One +OK=OK +Side\ menu\ is\ not\ available\ in\ embedded\ preview\ mode.=Side menu is not available in embedded preview mode. diff --git a/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelLocalizationPackagingTest.java b/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelLocalizationPackagingTest.java new file mode 100644 index 0000000000..f19957784b --- /dev/null +++ b/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelLocalizationPackagingTest.java @@ -0,0 +1,145 @@ +package com.codename1.initializr.model; + +import com.codename1.io.Util; +import com.codename1.testing.AbstractTest; +import net.sf.zipme.ZipEntry; +import net.sf.zipme.ZipInputStream; + +import javax.tools.JavaCompiler; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; + +public class GeneratorModelLocalizationPackagingTest extends AbstractTest { + @Override + public boolean runTest() throws Exception { + byte[] zip = createProjectZip(); + Map entries = readZipEntries(zip); + + File classesRoot = Files.createTempDirectory("cn1-localization-packaging").toFile(); + try { + compileDisplayClass(classesRoot); + writeGeneratedLocalizationResources(entries, classesRoot); + assertResourcesLoadFromDisplayClass(classesRoot); + } finally { + Util.cleanup(classesRoot.getAbsolutePath()); + } + return true; + } + + private static byte[] createProjectZip() throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + GeneratorModel.create( + IDE.INTELLIJ, + Template.BAREBONES, + "PackagingProbeApp", + "com.example.packaging" + ).writeProjectZip(output); + return output.toByteArray(); + } + + private static Map readZipEntries(byte[] zipData) throws IOException { + Map entries = new HashMap(); + ByteArrayInputStream input = new ByteArrayInputStream(zipData); + ZipInputStream zis = new ZipInputStream(input); + try { + ZipEntry entry = zis.getNextEntry(); + while (entry != null) { + if (!entry.isDirectory()) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + Util.copyNoClose(zis, bos, 8192); + entries.put(entry.getName(), bos.toByteArray()); + bos.close(); + } + zis.closeEntry(); + entry = zis.getNextEntry(); + } + } finally { + zis.close(); + input.close(); + } + return entries; + } + + private static void compileDisplayClass(File classesRoot) throws IOException { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + if (compiler == null) { + throw new IOException("JDK compiler is required to run packaging test"); + } + File sourceRoot = new File(classesRoot, "src"); + File packageDir = new File(sourceRoot, "com/example/packaging"); + packageDir.mkdirs(); + + File javaFile = new File(packageDir, "DisplayClass.java"); + String source = "package com.example.packaging; public class DisplayClass {}"; + Files.write(javaFile.toPath(), source.getBytes(StandardCharsets.UTF_8)); + + StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); + try { + fileManager.setLocation(StandardLocation.CLASS_OUTPUT, java.util.Collections.singletonList(classesRoot)); + Boolean ok = compiler.getTask(null, fileManager, null, null, null, + fileManager.getJavaFileObjectsFromFiles(java.util.Collections.singletonList(javaFile))).call(); + if (!Boolean.TRUE.equals(ok)) { + throw new IOException("Failed to compile display class for packaging test"); + } + } finally { + fileManager.close(); + Util.cleanup(sourceRoot.getAbsolutePath()); + } + } + + private static void writeGeneratedLocalizationResources(Map entries, File classesRoot) throws IOException { + writeResource(entries, classesRoot, "messages.properties"); + writeResource(entries, classesRoot, "messages_en.properties"); + for (ProjectOptions.PreviewLanguage language : ProjectOptions.PreviewLanguage.values()) { + if (language == ProjectOptions.PreviewLanguage.ENGLISH) { + continue; + } + writeResource(entries, classesRoot, "messages_" + language.bundleSuffix + ".properties"); + } + } + + private static void writeResource(Map entries, File classesRoot, String resourceName) throws IOException { + byte[] data = entries.get("common/src/main/resources/" + resourceName); + if (data == null) { + throw new IOException("Generated project is missing localization resource " + resourceName); + } + try (FileOutputStream fos = new FileOutputStream(new File(classesRoot, resourceName))) { + fos.write(data); + } + } + + private void assertResourcesLoadFromDisplayClass(File classesRoot) throws Exception { + URLClassLoader loader = new URLClassLoader(new URL[]{classesRoot.toURI().toURL()}, null); + try { + Class displayClass = Class.forName("com.example.packaging.DisplayClass", true, loader); + assertNotNull(displayClass.getResourceAsStream("/messages.properties"), "Default bundle should resolve from display class"); + assertNotNull(displayClass.getResourceAsStream("/messages_en.properties"), "English bundle alias should resolve from display class"); + for (ProjectOptions.PreviewLanguage language : ProjectOptions.PreviewLanguage.values()) { + if (language == ProjectOptions.PreviewLanguage.ENGLISH) { + continue; + } + String resource = "/messages_" + language.bundleSuffix + ".properties"; + InputStream in = displayClass.getResourceAsStream(resource); + assertNotNull(in, "Bundle should resolve from display class: " + resource); + if (in != null) { + in.close(); + } + } + } finally { + loader.close(); + } + } +} diff --git a/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java b/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java index df6d831423..340f4eb8a2 100644 --- a/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java +++ b/scripts/initializr/common/src/test/java/com/codename1/initializr/model/GeneratorModelMatrixTest.java @@ -139,6 +139,8 @@ private void assertMainSourceFile(Map entries, Template template if (template == Template.BAREBONES || template == Template.KOTLIN) { assertContains(mainSource, "setBundle", "Barebones starter should install localization bundle"); assertContains(mainSource, "messages", "Barebones starter should load i18n messages properties"); + assertContains(mainSource, "getL10N(\"messages\", \"en\")", "Barebones starter should fallback to English bundle when locale-specific bundle is missing"); + assertContains(mainSource, "language == null", "Barebones starter should default language to English when locale is unavailable"); } if (template == Template.GRUB) { String grubModel = getText(entries, "common/src/main/java/" + packagePath + "/models/AccountModel.java"); @@ -155,8 +157,14 @@ private void assertMainSourceFile(Map entries, Template template private void assertLocalizationBundles(Map entries, Template template) { if (template == Template.BAREBONES || template == Template.KOTLIN) { assertNotNull(entries.get("common/src/main/resources/messages.properties"), "Barebones templates should include default localization bundle"); - assertNotNull(entries.get("common/src/main/resources/messages_ar.properties"), "Barebones templates should include Arabic localization bundle"); - assertNotNull(entries.get("common/src/main/resources/messages_he.properties"), "Barebones templates should include Hebrew localization bundle"); + assertNotNull(entries.get("common/src/main/resources/messages_en.properties"), "Barebones templates should include English localization bundle alias for JS lookup"); + for (ProjectOptions.PreviewLanguage language : ProjectOptions.PreviewLanguage.values()) { + if (language == ProjectOptions.PreviewLanguage.ENGLISH) { + continue; + } + String path = "common/src/main/resources/messages_" + language.bundleSuffix + ".properties"; + assertNotNull(entries.get(path), "Missing expected localization bundle for " + language.bundleSuffix); + } return; } assertNull(entries.get("common/src/main/resources/messages.properties"), "Non-bare templates should not receive default localization bundle");