Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/ant.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
name: Java CI

on:
on:
push:
branches:
- master
paths-ignore:
- 'CodenameOneDesigner/**'

jobs:
build-linux-jdk8:
Expand Down
47 changes: 47 additions & 0 deletions .github/workflows/designer.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
- 'docs/**'
- '**/*.md'
- '.github/workflows/developer-guide-docs.yml'
- 'CodenameOneDesigner/**'
push:
branches:
- master
Expand All @@ -17,6 +18,7 @@ on:
- 'docs/**'
- '**/*.md'
- '.github/workflows/developer-guide-docs.yml'
- 'CodenameOneDesigner/**'

permissions:
contents: write
Expand Down
15 changes: 15 additions & 0 deletions CodenameOneDesigner/build.xml
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,19 @@
<target name="mac_os">
<copy file="dist/designer.jar" todir="Designer.app/Contents/Resources/Java" />
</target>
<target name="test-css-localization" depends="compile">
<mkdir dir="${build.test.classes.dir}"/>
<javac srcdir="${test.src.dir}" destdir="${build.test.classes.dir}"
encoding="${source.encoding}" source="${javac.source}" target="${javac.target}"
includeantruntime="false">
<classpath>
<path path="${javac.test.classpath}"/>
</classpath>
</javac>
<java classname="com.codename1.designer.css.CSSLocalizationTest" fork="true" failonerror="true">
<classpath>
<path path="${run.test.classpath}"/>
</classpath>
</java>
</target>
</project>
213 changes: 212 additions & 1 deletion CodenameOneDesigner/src/com/codename1/designer/css/CN1CSSCLI.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;



Expand Down Expand Up @@ -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:");
Expand All @@ -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;
Expand Down Expand Up @@ -919,8 +932,13 @@ public void call(Component c) {
theme.loadSelectorCacheStatus(cacheFile);
}

Map<String, Map<String, Map<String, String>>> localizationBundles = loadLocalizationBundles(localizationDir);

theme.createImageBorders(webViewProvider);
theme.updateResources();
if (!localizationBundles.isEmpty()) {
theme.applyLocalizationBundles(localizationBundles);
}
theme.save(outputFile);

theme.saveSelectorChecksums(cacheFile);
Expand Down Expand Up @@ -1022,6 +1040,199 @@ private static void saveChecksums(File baseDir, Map<String,String> map) throws I
out.println(key+":"+map.get(key));
}
}

}

private static Map<String, Map<String, Map<String, String>>> loadLocalizationBundles(File localizationDirectory) throws IOException {
Map<String, Map<String, Map<String, String>>> 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<Path> 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<String, Map<String, String>> baseBundles = bundles.computeIfAbsent(baseName, k -> new LinkedHashMap<>());
Map<String, String> translations = new LinkedHashMap<>();
for (Map.Entry<Object, Object> entry : props.entrySet()) {
translations.put(entry.getKey().toString(), entry.getValue().toString());
}
baseBundles.put(locale, translations);
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
});
} catch (UncheckedIOException ex) {
throw ex.getCause();
}
return bundles;
}

private static String joinTokens(String[] parts, int start, int end) {
StringBuilder sb = new StringBuilder();
for (int i = start; i < end; i++) {
if (i > start) {
sb.append('_');
}
sb.append(parts[i]);
}
return sb.toString();
}

private static boolean isValidLocale(String localeCandidate) {
if (localeCandidate == null || localeCandidate.isEmpty()) {
return false;
}
String[] parts = localeCandidate.split("_");
if (parts.length == 0 || !isValidLanguage(parts[0])) {
return false;
}
int index = 1;
if (index < parts.length && isValidScript(parts[index])) {
index++;
}
if (index < parts.length && isValidCountry(parts[index])) {
index++;
}
while (index < parts.length) {
if (!isValidVariant(parts[index])) {
return false;
}
index++;
}
return true;
}

private static boolean isValidLanguage(String token) {
if (token.length() < 2 || token.length() > 8) {
return false;
}
for (int i = 0; i < token.length(); i++) {
char c = token.charAt(i);
if (!Character.isLetter(c) || !Character.isLowerCase(c)) {
return false;
}
}
return true;
}

private static boolean isValidScript(String token) {
if (token.length() != 4) {
return false;
}
for (int i = 0; i < token.length(); i++) {
if (!Character.isLetter(token.charAt(i))) {
return false;
}
}
return true;
}

private static boolean isValidCountry(String token) {
if (token.length() == 2) {
for (int i = 0; i < token.length(); i++) {
if (!Character.isLetter(token.charAt(i))) {
return false;
}
}
return true;
}
if (token.length() == 3) {
for (int i = 0; i < token.length(); i++) {
if (!Character.isDigit(token.charAt(i))) {
return false;
}
}
return true;
}
return false;
}

private static boolean isValidVariant(String token) {
if (token.isEmpty()) {
return false;
}
for (int i = 0; i < token.length(); i++) {
char c = token.charAt(i);
if (!(Character.isLetterOrDigit(c) || c == '_')) {
return false;
}
}
return true;
}

private static String normalizeLocale(String locale) {
if (locale == null || locale.isEmpty()) {
return "";
}
String[] parts = locale.split("_");
if (parts.length == 0) {
return "";
}
StringBuilder sb = new StringBuilder(parts[0].toLowerCase());
int index = 1;
if (index < parts.length && isValidScript(parts[index])) {
String token = parts[index];
sb.append('_').append(Character.toUpperCase(token.charAt(0))).append(token.substring(1).toLowerCase());
index++;
}
if (index < parts.length && isValidCountry(parts[index])) {
sb.append('_').append(parts[index].toUpperCase());
index++;
}
while (index < parts.length) {
sb.append('_').append(parts[index]);
index++;
}
return sb.toString();
}
}
33 changes: 32 additions & 1 deletion CodenameOneDesigner/src/com/codename1/designer/css/CSSTheme.java
Original file line number Diff line number Diff line change
Expand Up @@ -1601,7 +1601,38 @@ private static String str(LexicalUnit lu, String defaultVal) {
}
return num+unitText;
}


public void applyLocalizationBundles(Map<String, Map<String, Map<String, String>>> bundles) {
if (bundles == null || bundles.isEmpty()) {
return;
}
if (res == null) {
res = new EditableResourcesForCSS(resourceFile);
}
for (Map.Entry<String, Map<String, Map<String, String>>> entry : bundles.entrySet()) {
String bundleName = entry.getKey();
Map<String, Map<String, String>> locales = entry.getValue();
if (bundleName == null || bundleName.isEmpty() || locales == null || locales.isEmpty()) {
continue;
}
res.setL10N(bundleName, new Hashtable());
for (Map.Entry<String, Map<String, String>> localeEntry : locales.entrySet()) {
String locale = localeEntry.getKey();
if (locale == null) {
locale = "";
}
res.addLocale(bundleName, locale);
Map<String, String> translations = localeEntry.getValue();
if (translations == null) {
continue;
}
for (Map.Entry<String, String> translation : translations.entrySet()) {
res.setLocaleProperty(bundleName, locale, translation.getKey(), translation.getValue());
}
}
}
}

public Map<String, CacheStatus> calculateSelectorCacheStatus(File cachedFile) throws IOException {
try {
Map<String,String> current = calculateSelectorChecksums();
Expand Down
Loading