Skip to content

Commit 439b906

Browse files
committed
Add designer CI workflow and localization tests
1 parent 183e059 commit 439b906

File tree

7 files changed

+474
-3
lines changed

7 files changed

+474
-3
lines changed

.github/workflows/ant.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
name: Java CI
22

3-
on:
3+
on:
44
push:
55
branches:
66
- master
7+
paths-ignore:
8+
- 'CodenameOneDesigner/**'
79

810
jobs:
911
build-linux-jdk8:

.github/workflows/designer.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
name: Codename One Designer CI
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
paths:
8+
- 'CodenameOneDesigner/**'
9+
- '.github/workflows/designer.yml'
10+
pull_request:
11+
branches:
12+
- master
13+
paths:
14+
- 'CodenameOneDesigner/**'
15+
- '.github/workflows/designer.yml'
16+
17+
jobs:
18+
build-designer:
19+
runs-on: ubuntu-latest
20+
21+
steps:
22+
- name: Check out repository
23+
uses: actions/checkout@v4
24+
25+
- name: Set up JDK 8
26+
uses: actions/setup-java@v1
27+
with:
28+
java-version: 1.8
29+
java-package: jdk
30+
31+
- name: Install build dependencies
32+
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends xvfb unzip
33+
34+
- name: Fetch cn1 binaries
35+
run: |
36+
wget https://github.com/codenameone/cn1-binaries/archive/refs/heads/master.zip
37+
unzip master.zip -d ..
38+
mv ../cn1-binaries-master ../cn1-binaries
39+
40+
- name: Build core dependencies
41+
run: |
42+
xvfb-run -a ant -noinput -buildfile Ports/JavaSE/build.xml jar
43+
xvfb-run -a ant -noinput -buildfile Ports/JavaSEWithSVGSupport/build.xml jar
44+
xvfb-run -a ant -noinput -buildfile CodenameOne/build.xml jar
45+
46+
- name: Run designer CSS localization tests
47+
run: xvfb-run -a ant -noinput -buildfile CodenameOneDesigner/build.xml test-css-localization

.github/workflows/pr.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99
- 'docs/**'
1010
- '**/*.md'
1111
- '.github/workflows/developer-guide-docs.yml'
12+
- 'CodenameOneDesigner/**'
1213
push:
1314
branches:
1415
- master
@@ -17,6 +18,7 @@ on:
1718
- 'docs/**'
1819
- '**/*.md'
1920
- '.github/workflows/developer-guide-docs.yml'
21+
- 'CodenameOneDesigner/**'
2022

2123
permissions:
2224
contents: write

CodenameOneDesigner/build.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,19 @@
163163
<target name="mac_os">
164164
<copy file="dist/designer.jar" todir="Designer.app/Contents/Resources/Java" />
165165
</target>
166+
<target name="test-css-localization" depends="compile">
167+
<mkdir dir="${build.test.classes.dir}"/>
168+
<javac srcdir="${test.src.dir}" destdir="${build.test.classes.dir}"
169+
encoding="${source.encoding}" source="${javac.source}" target="${javac.target}"
170+
includeantruntime="false">
171+
<classpath>
172+
<path path="${javac.test.classpath}"/>
173+
</classpath>
174+
</javac>
175+
<java classname="com.codename1.designer.css.CSSLocalizationTest" fork="true" failonerror="true">
176+
<classpath>
177+
<path path="${run.test.classpath}"/>
178+
</classpath>
179+
</java>
180+
</target>
166181
</project>

CodenameOneDesigner/src/com/codename1/designer/css/CN1CSSCLI.java

Lines changed: 212 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import java.io.InputStream;
5353
import java.io.PrintWriter;
5454
import java.io.RandomAccessFile;
55+
import java.io.UncheckedIOException;
5556
import java.math.BigInteger;
5657
import java.net.MalformedURLException;
5758
import java.net.Socket;
@@ -73,6 +74,7 @@
7374
import java.util.Arrays;
7475
import java.util.HashMap;
7576
import java.util.HashSet;
77+
import java.util.LinkedHashMap;
7678
import java.util.List;
7779
import java.util.Map;
7880
import java.util.Properties;
@@ -136,6 +138,7 @@ private static void relaunch() throws Exception {
136138
public static boolean mergeMode;
137139
public static boolean watchmode;
138140
private static Thread watchThread;
141+
private static File localizationDir;
139142

140143

141144

@@ -609,6 +612,7 @@ public static void main(String[] args) throws Exception {
609612
System.out.println(" -i, -input Input CSS file path. Multiple files separated by commas.");
610613
System.out.println(" -o, -output Output res file path.");
611614
System.out.println(" -m, -merge Path to merge file, used in case there are multipl input files.");
615+
System.out.println(" -l, -localization Directory containing Java resource bundle .properties files to include.");
612616
System.out.println(" -w, -watch Run in watch mode.");
613617
System.out.println(" Watches input files for changes and automatically recompiles.");
614618
System.out.println("\nSystem Properties:");
@@ -621,6 +625,15 @@ public static void main(String[] args) throws Exception {
621625

622626
}
623627
statelessMode = getArgByName(args, "i", "input") != null;
628+
String localizationPath = getArgByName(args, "l", "localization");
629+
if (localizationPath != null) {
630+
if ("true".equals(localizationPath)) {
631+
throw new IllegalArgumentException("Localization path is required when using -l or -localization");
632+
}
633+
localizationDir = new File(localizationPath);
634+
} else {
635+
localizationDir = null;
636+
}
624637
String inputPath;
625638
String outputPath;
626639
String mergedFile;
@@ -919,8 +932,13 @@ public void call(Component c) {
919932
theme.loadSelectorCacheStatus(cacheFile);
920933
}
921934

935+
Map<String, Map<String, Map<String, String>>> localizationBundles = loadLocalizationBundles(localizationDir);
936+
922937
theme.createImageBorders(webViewProvider);
923938
theme.updateResources();
939+
if (!localizationBundles.isEmpty()) {
940+
theme.applyLocalizationBundles(localizationBundles);
941+
}
924942
theme.save(outputFile);
925943

926944
theme.saveSelectorChecksums(cacheFile);
@@ -1022,6 +1040,199 @@ private static void saveChecksums(File baseDir, Map<String,String> map) throws I
10221040
out.println(key+":"+map.get(key));
10231041
}
10241042
}
1025-
1043+
10261044
}
1045+
1046+
private static Map<String, Map<String, Map<String, String>>> loadLocalizationBundles(File localizationDirectory) throws IOException {
1047+
Map<String, Map<String, Map<String, String>>> bundles = new LinkedHashMap<>();
1048+
if (localizationDirectory == null) {
1049+
return bundles;
1050+
}
1051+
if (!localizationDirectory.exists()) {
1052+
throw new IOException("Localization directory does not exist: " + localizationDirectory.getAbsolutePath());
1053+
}
1054+
if (!localizationDirectory.isDirectory()) {
1055+
throw new IOException("Localization path is not a directory: " + localizationDirectory.getAbsolutePath());
1056+
}
1057+
Path root = localizationDirectory.toPath();
1058+
try (java.util.stream.Stream<Path> stream = Files.walk(root)) {
1059+
stream.filter(Files::isRegularFile)
1060+
.filter(p -> p.getFileName().toString().endsWith(".properties"))
1061+
.forEach(p -> {
1062+
try {
1063+
Path relPath = root.relativize(p);
1064+
String rel = relPath.toString().replace(File.separatorChar, '/');
1065+
if (rel.isEmpty() || !rel.endsWith(".properties")) {
1066+
return;
1067+
}
1068+
String withoutExt = rel.substring(0, rel.length() - ".properties".length());
1069+
int lastSlash = withoutExt.lastIndexOf('/');
1070+
String packagePath = lastSlash >= 0 ? withoutExt.substring(0, lastSlash) : "";
1071+
String fileNamePart = lastSlash >= 0 ? withoutExt.substring(lastSlash + 1) : withoutExt;
1072+
if (fileNamePart.isEmpty()) {
1073+
return;
1074+
}
1075+
String[] tokens = fileNamePart.split("_");
1076+
String baseNamePart = fileNamePart;
1077+
String locale = "";
1078+
if (tokens.length > 1) {
1079+
for (int start = 1; start < tokens.length; start++) {
1080+
String localeCandidate = joinTokens(tokens, start, tokens.length);
1081+
if (isValidLocale(localeCandidate)) {
1082+
baseNamePart = joinTokens(tokens, 0, start);
1083+
locale = normalizeLocale(localeCandidate);
1084+
break;
1085+
}
1086+
}
1087+
}
1088+
String baseName;
1089+
if (!packagePath.isEmpty()) {
1090+
baseName = packagePath.replace('/', '.');
1091+
if (!baseNamePart.isEmpty()) {
1092+
baseName = baseName + "." + baseNamePart;
1093+
}
1094+
} else {
1095+
baseName = baseNamePart;
1096+
}
1097+
if (baseName == null || baseName.isEmpty()) {
1098+
return;
1099+
}
1100+
Properties props = new Properties();
1101+
try (InputStream is = Files.newInputStream(p)) {
1102+
props.load(is);
1103+
}
1104+
Map<String, Map<String, String>> baseBundles = bundles.computeIfAbsent(baseName, k -> new LinkedHashMap<>());
1105+
Map<String, String> translations = new LinkedHashMap<>();
1106+
for (Map.Entry<Object, Object> entry : props.entrySet()) {
1107+
translations.put(entry.getKey().toString(), entry.getValue().toString());
1108+
}
1109+
baseBundles.put(locale, translations);
1110+
} catch (IOException ex) {
1111+
throw new UncheckedIOException(ex);
1112+
}
1113+
});
1114+
} catch (UncheckedIOException ex) {
1115+
throw ex.getCause();
1116+
}
1117+
return bundles;
1118+
}
1119+
1120+
private static String joinTokens(String[] parts, int start, int end) {
1121+
StringBuilder sb = new StringBuilder();
1122+
for (int i = start; i < end; i++) {
1123+
if (i > start) {
1124+
sb.append('_');
1125+
}
1126+
sb.append(parts[i]);
1127+
}
1128+
return sb.toString();
1129+
}
1130+
1131+
private static boolean isValidLocale(String localeCandidate) {
1132+
if (localeCandidate == null || localeCandidate.isEmpty()) {
1133+
return false;
1134+
}
1135+
String[] parts = localeCandidate.split("_");
1136+
if (parts.length == 0 || !isValidLanguage(parts[0])) {
1137+
return false;
1138+
}
1139+
int index = 1;
1140+
if (index < parts.length && isValidScript(parts[index])) {
1141+
index++;
1142+
}
1143+
if (index < parts.length && isValidCountry(parts[index])) {
1144+
index++;
1145+
}
1146+
while (index < parts.length) {
1147+
if (!isValidVariant(parts[index])) {
1148+
return false;
1149+
}
1150+
index++;
1151+
}
1152+
return true;
1153+
}
1154+
1155+
private static boolean isValidLanguage(String token) {
1156+
if (token.length() < 2 || token.length() > 8) {
1157+
return false;
1158+
}
1159+
for (int i = 0; i < token.length(); i++) {
1160+
char c = token.charAt(i);
1161+
if (!Character.isLetter(c) || !Character.isLowerCase(c)) {
1162+
return false;
1163+
}
1164+
}
1165+
return true;
1166+
}
1167+
1168+
private static boolean isValidScript(String token) {
1169+
if (token.length() != 4) {
1170+
return false;
1171+
}
1172+
for (int i = 0; i < token.length(); i++) {
1173+
if (!Character.isLetter(token.charAt(i))) {
1174+
return false;
1175+
}
1176+
}
1177+
return true;
1178+
}
1179+
1180+
private static boolean isValidCountry(String token) {
1181+
if (token.length() == 2) {
1182+
for (int i = 0; i < token.length(); i++) {
1183+
if (!Character.isLetter(token.charAt(i))) {
1184+
return false;
1185+
}
1186+
}
1187+
return true;
1188+
}
1189+
if (token.length() == 3) {
1190+
for (int i = 0; i < token.length(); i++) {
1191+
if (!Character.isDigit(token.charAt(i))) {
1192+
return false;
1193+
}
1194+
}
1195+
return true;
1196+
}
1197+
return false;
1198+
}
1199+
1200+
private static boolean isValidVariant(String token) {
1201+
if (token.isEmpty()) {
1202+
return false;
1203+
}
1204+
for (int i = 0; i < token.length(); i++) {
1205+
char c = token.charAt(i);
1206+
if (!(Character.isLetterOrDigit(c) || c == '_')) {
1207+
return false;
1208+
}
1209+
}
1210+
return true;
1211+
}
1212+
1213+
private static String normalizeLocale(String locale) {
1214+
if (locale == null || locale.isEmpty()) {
1215+
return "";
1216+
}
1217+
String[] parts = locale.split("_");
1218+
if (parts.length == 0) {
1219+
return "";
1220+
}
1221+
StringBuilder sb = new StringBuilder(parts[0].toLowerCase());
1222+
int index = 1;
1223+
if (index < parts.length && isValidScript(parts[index])) {
1224+
String token = parts[index];
1225+
sb.append('_').append(Character.toUpperCase(token.charAt(0))).append(token.substring(1).toLowerCase());
1226+
index++;
1227+
}
1228+
if (index < parts.length && isValidCountry(parts[index])) {
1229+
sb.append('_').append(parts[index].toUpperCase());
1230+
index++;
1231+
}
1232+
while (index < parts.length) {
1233+
sb.append('_').append(parts[index]);
1234+
index++;
1235+
}
1236+
return sb.toString();
1237+
}
10271238
}

CodenameOneDesigner/src/com/codename1/designer/css/CSSTheme.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1601,7 +1601,38 @@ private static String str(LexicalUnit lu, String defaultVal) {
16011601
}
16021602
return num+unitText;
16031603
}
1604-
1604+
1605+
public void applyLocalizationBundles(Map<String, Map<String, Map<String, String>>> bundles) {
1606+
if (bundles == null || bundles.isEmpty()) {
1607+
return;
1608+
}
1609+
if (res == null) {
1610+
res = new EditableResourcesForCSS(resourceFile);
1611+
}
1612+
for (Map.Entry<String, Map<String, Map<String, String>>> entry : bundles.entrySet()) {
1613+
String bundleName = entry.getKey();
1614+
Map<String, Map<String, String>> locales = entry.getValue();
1615+
if (bundleName == null || bundleName.isEmpty() || locales == null || locales.isEmpty()) {
1616+
continue;
1617+
}
1618+
res.setL10N(bundleName, new Hashtable());
1619+
for (Map.Entry<String, Map<String, String>> localeEntry : locales.entrySet()) {
1620+
String locale = localeEntry.getKey();
1621+
if (locale == null) {
1622+
locale = "";
1623+
}
1624+
res.addLocale(bundleName, locale);
1625+
Map<String, String> translations = localeEntry.getValue();
1626+
if (translations == null) {
1627+
continue;
1628+
}
1629+
for (Map.Entry<String, String> translation : translations.entrySet()) {
1630+
res.setLocaleProperty(bundleName, locale, translation.getKey(), translation.getValue());
1631+
}
1632+
}
1633+
}
1634+
}
1635+
16051636
public Map<String, CacheStatus> calculateSelectorCacheStatus(File cachedFile) throws IOException {
16061637
try {
16071638
Map<String,String> current = calculateSelectorChecksums();

0 commit comments

Comments
 (0)