diff --git a/.github/workflows/ant.yml b/.github/workflows/ant.yml
index cea30d03a7..9644107db0 100644
--- a/.github/workflows/ant.yml
+++ b/.github/workflows/ant.yml
@@ -1,9 +1,11 @@
name: Java CI
-on:
+on:
push:
branches:
- master
+ paths-ignore:
+ - 'CodenameOneDesigner/**'
jobs:
build-linux-jdk8:
diff --git a/.github/workflows/designer.yml b/.github/workflows/designer.yml
new file mode 100644
index 0000000000..68c07c6ca6
--- /dev/null
+++ b/.github/workflows/designer.yml
@@ -0,0 +1,47 @@
+name: Codename One Designer CI
+
+on:
+ push:
+ branches:
+ - master
+ paths:
+ - 'CodenameOneDesigner/**'
+ - '.github/workflows/designer.yml'
+ pull_request:
+ branches:
+ - master
+ paths:
+ - 'CodenameOneDesigner/**'
+ - '.github/workflows/designer.yml'
+
+jobs:
+ build-designer:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+
+ - name: Set up JDK 8
+ uses: actions/setup-java@v1
+ with:
+ java-version: 1.8
+ java-package: jdk
+
+ - name: Install build dependencies
+ run: sudo apt-get update && sudo apt-get install -y --no-install-recommends xvfb unzip
+
+ - name: Fetch cn1 binaries
+ run: |
+ wget https://github.com/codenameone/cn1-binaries/archive/refs/heads/master.zip
+ unzip master.zip -d ..
+ mv ../cn1-binaries-master ../cn1-binaries
+
+ - name: Build core dependencies
+ run: |
+ xvfb-run -a ant -noinput -buildfile Ports/JavaSE/build.xml jar
+ xvfb-run -a ant -noinput -buildfile Ports/JavaSEWithSVGSupport/build.xml jar
+ xvfb-run -a ant -noinput -buildfile CodenameOne/build.xml jar
+
+ - name: Run designer CSS localization tests
+ run: xvfb-run -a ant -noinput -buildfile CodenameOneDesigner/build.xml test-css-localization
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index e21307eb19..79508d44fb 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -9,6 +9,7 @@ on:
- 'docs/**'
- '**/*.md'
- '.github/workflows/developer-guide-docs.yml'
+ - 'CodenameOneDesigner/**'
push:
branches:
- master
@@ -17,6 +18,7 @@ on:
- 'docs/**'
- '**/*.md'
- '.github/workflows/developer-guide-docs.yml'
+ - 'CodenameOneDesigner/**'
permissions:
contents: write
diff --git a/CodenameOneDesigner/build.xml b/CodenameOneDesigner/build.xml
index 7b7f09fa93..c4316aedde 100644
--- a/CodenameOneDesigner/build.xml
+++ b/CodenameOneDesigner/build.xml
@@ -163,4 +163,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CodenameOneDesigner/src/com/codename1/designer/css/CN1CSSCLI.java b/CodenameOneDesigner/src/com/codename1/designer/css/CN1CSSCLI.java
index b2fc1170c2..c0c3da67cb 100644
--- a/CodenameOneDesigner/src/com/codename1/designer/css/CN1CSSCLI.java
+++ b/CodenameOneDesigner/src/com/codename1/designer/css/CN1CSSCLI.java
@@ -52,6 +52,7 @@
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.RandomAccessFile;
+import java.io.UncheckedIOException;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.net.Socket;
@@ -73,6 +74,7 @@
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
@@ -136,6 +138,7 @@ private static void relaunch() throws Exception {
public static boolean mergeMode;
public static boolean watchmode;
private static Thread watchThread;
+ private static File localizationDir;
@@ -609,6 +612,7 @@ public static void main(String[] args) throws Exception {
System.out.println(" -i, -input Input CSS file path. Multiple files separated by commas.");
System.out.println(" -o, -output Output res file path.");
System.out.println(" -m, -merge Path to merge file, used in case there are multipl input files.");
+ System.out.println(" -l, -localization Directory containing Java resource bundle .properties files to include.");
System.out.println(" -w, -watch Run in watch mode.");
System.out.println(" Watches input files for changes and automatically recompiles.");
System.out.println("\nSystem Properties:");
@@ -621,6 +625,15 @@ public static void main(String[] args) throws Exception {
}
statelessMode = getArgByName(args, "i", "input") != null;
+ String localizationPath = getArgByName(args, "l", "localization");
+ if (localizationPath != null) {
+ if ("true".equals(localizationPath)) {
+ throw new IllegalArgumentException("Localization path is required when using -l or -localization");
+ }
+ localizationDir = new File(localizationPath);
+ } else {
+ localizationDir = null;
+ }
String inputPath;
String outputPath;
String mergedFile;
@@ -919,8 +932,13 @@ public void call(Component c) {
theme.loadSelectorCacheStatus(cacheFile);
}
+ Map>> localizationBundles = loadLocalizationBundles(localizationDir);
+
theme.createImageBorders(webViewProvider);
theme.updateResources();
+ if (!localizationBundles.isEmpty()) {
+ theme.applyLocalizationBundles(localizationBundles);
+ }
theme.save(outputFile);
theme.saveSelectorChecksums(cacheFile);
@@ -1022,6 +1040,199 @@ private static void saveChecksums(File baseDir, Map map) throws I
out.println(key+":"+map.get(key));
}
}
-
+
}
+
+ private static Map>> loadLocalizationBundles(File localizationDirectory) throws IOException {
+ Map>> bundles = new LinkedHashMap<>();
+ if (localizationDirectory == null) {
+ return bundles;
+ }
+ if (!localizationDirectory.exists()) {
+ throw new IOException("Localization directory does not exist: " + localizationDirectory.getAbsolutePath());
+ }
+ if (!localizationDirectory.isDirectory()) {
+ throw new IOException("Localization path is not a directory: " + localizationDirectory.getAbsolutePath());
+ }
+ Path root = localizationDirectory.toPath();
+ try (java.util.stream.Stream stream = Files.walk(root)) {
+ stream.filter(Files::isRegularFile)
+ .filter(p -> p.getFileName().toString().endsWith(".properties"))
+ .forEach(p -> {
+ try {
+ Path relPath = root.relativize(p);
+ String rel = relPath.toString().replace(File.separatorChar, '/');
+ if (rel.isEmpty() || !rel.endsWith(".properties")) {
+ return;
+ }
+ String withoutExt = rel.substring(0, rel.length() - ".properties".length());
+ int lastSlash = withoutExt.lastIndexOf('/');
+ String packagePath = lastSlash >= 0 ? withoutExt.substring(0, lastSlash) : "";
+ String fileNamePart = lastSlash >= 0 ? withoutExt.substring(lastSlash + 1) : withoutExt;
+ if (fileNamePart.isEmpty()) {
+ return;
+ }
+ String[] tokens = fileNamePart.split("_");
+ String baseNamePart = fileNamePart;
+ String locale = "";
+ if (tokens.length > 1) {
+ for (int start = 1; start < tokens.length; start++) {
+ String localeCandidate = joinTokens(tokens, start, tokens.length);
+ if (isValidLocale(localeCandidate)) {
+ baseNamePart = joinTokens(tokens, 0, start);
+ locale = normalizeLocale(localeCandidate);
+ break;
+ }
+ }
+ }
+ String baseName;
+ if (!packagePath.isEmpty()) {
+ baseName = packagePath.replace('/', '.');
+ if (!baseNamePart.isEmpty()) {
+ baseName = baseName + "." + baseNamePart;
+ }
+ } else {
+ baseName = baseNamePart;
+ }
+ if (baseName == null || baseName.isEmpty()) {
+ return;
+ }
+ Properties props = new Properties();
+ try (InputStream is = Files.newInputStream(p)) {
+ props.load(is);
+ }
+ Map> baseBundles = bundles.computeIfAbsent(baseName, k -> new LinkedHashMap<>());
+ Map translations = new LinkedHashMap<>();
+ for (Map.Entry