From 76512a2178eeb1967130930e63246f6fd9cbaa86 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 5 May 2026 04:58:10 +0300 Subject: [PATCH] Fix #4850: stop auto-creating ghost l10n bundles in wrong module The CSS subprocess (CN1CSSCLI forked by CSSWatcher) inherits cwd = `javase/` from the simulator. JavaSEPort.findLocalizationDirectory was looking only at cwd and -- when nothing was found -- mkdirs()'d a throwaway `javase/src/main/l10n/Bundle.properties`. Older CN1 versions poisoned that ghost bundle with `@im=@im` via the AutoLocalizationBundle wormhole. After users cleaned the *real* bundle in `common/src/main/l10n` per liannacasper's workaround, every CN1CSSCLI respawn still loaded the ghost bundle from `javase/`, crashed inside parseTextFieldInputMode, and CSSWatcher.start() respawned the dead subprocess in an infinite loop -- hence "fix worked, simulator doesn't crash, but `CSS> ...` stack trace keeps spamming the log" (Ngosti2000 + ThomasH99). New rules for findLocalizationDirectory: 1. Check the current module first (cwd/src/main/{l10n,i18n}). 2. Walk up to the sibling `common/` module (matches CN1 maven layout and CSSWatcher.addLocalizationCandidates which already does this). 3. Never auto-create the directory. If the developer hasn't set up localization, the auto-bundle is a no-op -- project-level opt-in. findDefaultLocalizationBundleFile no longer falls back to a non-existent `Bundle.properties` path either. Combined with rule 3 above, no l10n files on disk = enableAutoLocalizationBundle returns early at `bundleFile == null`, and ThomasH99's "the auto-bundle fills up by itself" complaint goes away for projects that didn't ask for it. Both methods get static overloads taking an explicit projectDir so unit tests can drive them without mutating user.dir. The instance methods become thin wrappers around the static versions. AutoLocalizationBundleTest gains three regression tests: - verifyFindLocalizationDirectoryDoesNotAutoCreate: no l10n dir => returns null AND src/main/l10n is not created on disk. - verifyFindLocalizationDirectoryWalksToCommonSibling: cwd=javase with sibling common/src/main/l10n => returns common's dir; once javase/src/main/l10n exists, local wins. - verifyFindDefaultBundleReturnsNullWhenNoBundleFile: empty l10n dir => returns null and Bundle.properties is not created. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../com/codename1/impl/javase/JavaSEPort.java | 72 +++++++++++----- .../javase/AutoLocalizationBundleTest.java | 83 +++++++++++++++++++ 2 files changed, 136 insertions(+), 19 deletions(-) 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()) {