diff --git a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java index 24e7969d9f..a08c7f0405 100644 --- a/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java +++ b/Ports/JavaSE/src/com/codename1/impl/javase/JavaSEPort.java @@ -14337,8 +14337,18 @@ private void disableAutoLocalizationBundle() { autoLocalizationBundle = null; } - private File findDefaultLocalizationBundleFile() { - File localizationDir = findLocalizationDirectory(); + File findDefaultLocalizationBundleFile() { + return findDefaultLocalizationBundleFile(getCWD()); + } + + // Returns null when no real bundle file exists. Previously this method + // returned a non-existent `Bundle.properties` path as a fallback, which + // combined with `findLocalizationDirectory`'s old mkdirs() to create empty + // ghost bundles in modules the developer never asked to localize. Now the + // auto-bundle activates only when the developer ships at least one + // bundle file, matching the project-level opt-in semantics. + static File findDefaultLocalizationBundleFile(File projectDir) { + File localizationDir = findLocalizationDirectory(projectDir); if (localizationDir == null) { return null; } @@ -14372,10 +14382,10 @@ private File findDefaultLocalizationBundleFile() { java.util.Collections.sort(bundles); return bundles.get(0); } - return preferred; + return null; } - private void collectLocalizationBundles(File dir, java.util.List out) { + private static void collectLocalizationBundles(File dir, java.util.List out) { File[] files = dir.listFiles(); if (files == null) { return; @@ -14389,25 +14399,49 @@ private void collectLocalizationBundles(File dir, java.util.List out) { } } - private File findLocalizationDirectory() { - File projectDir = getCWD(); - File[] candidates = new File[]{ - new File(projectDir, "src" + File.separator + "main" + File.separator + "l10n"), - new File(projectDir, "l10n"), - new File(projectDir, "src" + File.separator + "l10n") - }; + File findLocalizationDirectory() { + return findLocalizationDirectory(getCWD()); + } + + // Resolves the project's localization bundle directory for the auto-bundle. + // + // The simulator forks `cn1:run` from the `javase/` module, so cwd is `javase/` + // -- but the developer's bundles live in the sibling `common/` module under + // `common/src/main/l10n`. Issue #4850: previous versions of this method + // looked only at cwd, missed the real bundle, and then auto-created a + // throwaway `javase/src/main/l10n/Bundle.properties` via mkdirs(). The + // throwaway file accumulated wormhole-poisoned `@im=@im` entries from older + // CN1 versions; even after users cleaned the *real* bundle in `common/`, + // every CN1CSSCLI subprocess respawn loaded the ghost bundle from `javase/`, + // crashed inside `parseTextFieldInputMode`, and CSSWatcher restarted it in + // an infinite respawn loop. + // + // New rules: + // 1. Check the current module first (cwd/src/main/{l10n,i18n}). + // 2. Then check the sibling `common/` module (matches CN1 maven layout + // and CSSWatcher.addLocalizationCandidates). + // 3. Never auto-create the directory. Project-level opt-in: if the + // developer hasn't set up localization, the auto-bundle is a no-op. + static File findLocalizationDirectory(File projectDir) { + if (projectDir == null) { + return null; + } + java.util.List candidates = new java.util.ArrayList(); + candidates.add(new File(projectDir, "src" + File.separator + "main" + File.separator + "l10n")); + candidates.add(new File(projectDir, "src" + File.separator + "main" + File.separator + "i18n")); + candidates.add(new File(projectDir, "l10n")); + candidates.add(new File(projectDir, "src" + File.separator + "l10n")); + File parent = projectDir.getParentFile(); + if (parent != null) { + File commonModule = new File(parent, "common"); + candidates.add(new File(commonModule, "src" + File.separator + "main" + File.separator + "l10n")); + candidates.add(new File(commonModule, "src" + File.separator + "main" + File.separator + "i18n")); + } for (File dir : candidates) { - if (dir.exists() && dir.isDirectory()) { + if (dir != null && dir.exists() && dir.isDirectory()) { return dir; } } - File fallback = candidates[0]; - if (!fallback.exists()) { - fallback.mkdirs(); - } - if (fallback.exists() && fallback.isDirectory()) { - return fallback; - } return null; } diff --git a/tests/core/test/com/codename1/impl/javase/AutoLocalizationBundleTest.java b/tests/core/test/com/codename1/impl/javase/AutoLocalizationBundleTest.java index e7f888c056..0ad48132be 100644 --- a/tests/core/test/com/codename1/impl/javase/AutoLocalizationBundleTest.java +++ b/tests/core/test/com/codename1/impl/javase/AutoLocalizationBundleTest.java @@ -7,6 +7,7 @@ import java.io.FileOutputStream; import java.io.OutputStream; import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; import java.util.Properties; @@ -82,6 +83,9 @@ public boolean runTest() throws Exception { verifySetBundleSmokeOnFreshProject(ctor, tempDir); verifySetBundleHealsLegacyWormholeFile(ctor, tempDir); + verifyFindLocalizationDirectoryDoesNotAutoCreate(tempDir); + verifyFindLocalizationDirectoryWalksToCommonSibling(tempDir); + verifyFindDefaultBundleReturnsNullWhenNoBundleFile(tempDir); return true; } finally { @@ -186,6 +190,85 @@ private void verifySetBundleHealsLegacyWormholeFile(Constructor ctor, File te } } + /// Issue #4850 root cause: `findLocalizationDirectory` used to call + /// `mkdirs()` on `/src/main/l10n` whenever the directory was missing, + /// then `findDefaultLocalizationBundleFile` returned a non-existent + /// `Bundle.properties` path as a fallback. The CSS subprocess inherits cwd + /// = `javase/`, so this auto-created a ghost bundle in the wrong module + /// that older CN1 versions then poisoned with `@im=@im`. After this fix, + /// no l10n dir on disk = no auto-bundle (project-level opt-in). + private void verifyFindLocalizationDirectoryDoesNotAutoCreate(File tempDir) throws Exception { + File freshModule = new File(tempDir, "no-l10n-module"); + if (!freshModule.mkdirs()) { + throw new RuntimeException("Failed to create test module dir " + freshModule); + } + + Method findLocDir = Class.forName("com.codename1.impl.javase.JavaSEPort") + .getDeclaredMethod("findLocalizationDirectory", File.class); + findLocDir.setAccessible(true); + + Object result = findLocDir.invoke(null, freshModule); + assertNull(result, "findLocalizationDirectory must return null when no l10n dir exists"); + + File ghostDir = new File(freshModule, "src" + File.separator + "main" + File.separator + "l10n"); + assertBool(!ghostDir.exists(), "findLocalizationDirectory must not auto-create src/main/l10n"); + } + + /// Issue #4850: the simulator forks `cn1:run` from `javase/` while the + /// developer's bundles live in the sibling `common/` module. The new + /// `findLocalizationDirectory` walks up to find `../common/src/main/l10n` + /// when the current module has no l10n dir of its own, mirroring + /// `CSSWatcher.addLocalizationCandidates`. + private void verifyFindLocalizationDirectoryWalksToCommonSibling(File tempDir) throws Exception { + File rootProject = new File(tempDir, "multi-module-project"); + File javaseModule = new File(rootProject, "javase"); + File commonL10n = new File(rootProject, "common" + File.separator + "src" + File.separator + "main" + File.separator + "l10n"); + if (!javaseModule.mkdirs() || !commonL10n.mkdirs()) { + throw new RuntimeException("Failed to create multi-module project layout under " + rootProject); + } + + Method findLocDir = Class.forName("com.codename1.impl.javase.JavaSEPort") + .getDeclaredMethod("findLocalizationDirectory", File.class); + findLocDir.setAccessible(true); + + File result = (File) findLocDir.invoke(null, javaseModule); + assertNotNull(result, "findLocalizationDirectory must locate sibling common/src/main/l10n"); + assertEqual(commonL10n.getCanonicalPath(), result.getCanonicalPath(), + "findLocalizationDirectory should resolve to the common module's l10n dir when cwd is javase"); + + // Local module wins when both exist. + File javaseL10n = new File(javaseModule, "src" + File.separator + "main" + File.separator + "l10n"); + if (!javaseL10n.mkdirs()) { + throw new RuntimeException("Failed to create local l10n dir " + javaseL10n); + } + File preferLocal = (File) findLocDir.invoke(null, javaseModule); + assertEqual(javaseL10n.getCanonicalPath(), preferLocal.getCanonicalPath(), + "Local module's l10n dir should take precedence over common"); + } + + /// `findDefaultLocalizationBundleFile` previously returned a non-existent + /// `Bundle.properties` path when the dir was empty; that triggered + /// `AutoLocalizationBundle.persist()` to create the empty file even when + /// the project shipped no bundles. Now it returns null and + /// `enableAutoLocalizationBundle` no-ops. + private void verifyFindDefaultBundleReturnsNullWhenNoBundleFile(File tempDir) throws Exception { + File emptyL10nModule = new File(tempDir, "empty-l10n-module"); + File emptyL10n = new File(emptyL10nModule, "src" + File.separator + "main" + File.separator + "l10n"); + if (!emptyL10n.mkdirs()) { + throw new RuntimeException("Failed to create empty l10n dir " + emptyL10n); + } + + Method findDefaultBundle = Class.forName("com.codename1.impl.javase.JavaSEPort") + .getDeclaredMethod("findDefaultLocalizationBundleFile", File.class); + findDefaultBundle.setAccessible(true); + + Object result = findDefaultBundle.invoke(null, emptyL10nModule); + assertNull(result, "findDefaultLocalizationBundleFile must return null when the l10n dir has no .properties files"); + + File preferred = new File(emptyL10n, "Bundle.properties"); + assertBool(!preferred.exists(), "findDefaultLocalizationBundleFile must not create Bundle.properties"); + } + private Properties load(File file) throws Exception { Properties props = new Properties(); if (file.exists()) {