Skip to content

Commit

Permalink
Qute message bundles - search for localized files in all app archives
Browse files Browse the repository at this point in the history
- resolves quarkusio#24090
  • Loading branch information
mkouba committed Mar 8, 2022
1 parent bf28520 commit eb74feb
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 25 deletions.
6 changes: 3 additions & 3 deletions docs/src/main/asciidoc/qute-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2067,9 +2067,9 @@ However, the `io.quarkus.qute.i18n.MessageBundle#locale()` can be used to specif
Additionally, there are two ways to define a localized bundle:

1. Create an interface that extends the default interface that is annotated with `@Localized`
2. Create an UTF-8 encoded file located in `src/main/resources/messages`; e.g. `msg_de.properties`.
2. Create an UTF-8 encoded file located in the `src/main/resources/messages` directory of an application archive; e.g. `msg_de.properties`.

TIP: A localized interface is the preferred solution mainly due to the possibility of easy refactoring.
TIP: While a localized interface enables easy refactoring an external file might be more convenient in many situations.

.Localized Interface Example
[source,java]
Expand All @@ -2088,7 +2088,7 @@ public interface GermanAppMessages extends AppMessages {
<1> The value is the locale tag string (IETF).
<2> The value is the localized template.

Message bundle files must be encoded in UTF-8.
Message bundle files must be encoded in _UTF-8_.
The file name consists of the relevant bundle name (e.g. `msg`) and underscore followed by the locate tag (IETF).
The file format is very simple: each line represents either a key/value pair with the equals sign used as a separator or a comment (line starts with `#`).
Blank lines are ignored.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import io.quarkus.arc.processor.Annotations;
import io.quarkus.arc.processor.BeanInfo;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.deployment.ApplicationArchive;
import io.quarkus.deployment.GeneratedClassGizmoAdaptor;
import io.quarkus.deployment.IsNormal;
import io.quarkus.deployment.annotations.BuildProducer;
Expand Down Expand Up @@ -123,7 +124,7 @@ List<MessageBundleBuildItem> processBundles(BeanArchiveIndexBuildItem beanArchiv
Map<String, ClassInfo> found = new HashMap<>();
List<MessageBundleBuildItem> bundles = new ArrayList<>();
List<DotName> localizedInterfaces = new ArrayList<>();
Set<Path> messageFiles = findMessageFiles(applicationArchivesBuildItem, watchedFiles);
List<Path> messageFiles = findMessageFiles(applicationArchivesBuildItem, watchedFiles);

// First collect all interfaces annotated with @MessageBundle
for (AnnotationInstance bundleAnnotation : index.getAnnotations(Names.BUNDLE)) {
Expand Down Expand Up @@ -1054,32 +1055,66 @@ private String getDefaultLocale(AnnotationInstance bundleAnnotation, LocalesBuil
return defaultLocale;
}

private Set<Path> findMessageFiles(ApplicationArchivesBuildItem applicationArchivesBuildItem,
private List<Path> findMessageFiles(ApplicationArchivesBuildItem applicationArchivesBuildItem,
BuildProducer<HotDeploymentWatchedFileBuildItem> watchedFiles) throws IOException {
return applicationArchivesBuildItem.getRootArchive().apply(tree -> {
final Path messagesPath = tree.getPath(MESSAGES);
if (messagesPath == null) {
return Collections.emptySet();
}
Set<Path> messageFiles = new HashSet<>();
try (Stream<Path> files = Files.list(messagesPath)) {
Iterator<Path> iter = files.iterator();
while (iter.hasNext()) {
Path filePath = iter.next();
if (Files.isRegularFile(filePath)) {
messageFiles.add(filePath);
String messageFilePath = messagesPath.relativize(filePath).toString();
if (File.separatorChar != '/') {
messageFilePath = messageFilePath.replace(File.separatorChar, '/');

Map<String, List<Path>> messageFileNameToPath = new HashMap<>();

for (ApplicationArchive archive : applicationArchivesBuildItem.getAllApplicationArchives()) {
archive.accept(tree -> {
final Path messagesPath = tree.getPath(MESSAGES);
if (messagesPath == null) {
return;
}
try (Stream<Path> files = Files.list(messagesPath)) {
Iterator<Path> iter = files.iterator();
while (iter.hasNext()) {
Path filePath = iter.next();
if (Files.isRegularFile(filePath)) {
String messageFileName = messagesPath.relativize(filePath).toString();
if (File.separatorChar != '/') {
messageFileName = messageFileName.replace(File.separatorChar, '/');
}
List<Path> paths = messageFileNameToPath.get(messageFileName);
if (paths == null) {
paths = new ArrayList<>();
messageFileNameToPath.put(messageFileName, paths);
}
paths.add(filePath);
}
watchedFiles.produce(new HotDeploymentWatchedFileBuildItem(MESSAGES + "/" + messageFilePath));
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
});
}

if (messageFileNameToPath.isEmpty()) {
return Collections.emptyList();
}

// Check duplicates
List<Entry<String, List<Path>>> duplicates = messageFileNameToPath.entrySet().stream()
.filter(e -> e.getValue().size() > 1).collect(Collectors.toList());
if (!duplicates.isEmpty()) {
StringBuilder builder = new StringBuilder("Duplicate localized files found:");
for (Entry<String, List<Path>> e : duplicates) {
builder.append("\n\t- ")
.append(e.getKey())
.append(": ")
.append(e.getValue());
}
return messageFiles;
});
throw new IllegalStateException(builder.toString());
}

// Hot deployment
for (String messageFileName : messageFileNameToPath.keySet()) {
watchedFiles.produce(new HotDeploymentWatchedFileBuildItem(MESSAGES + "/" + messageFileName));
}

List<Path> messageFiles = new ArrayList<>();
messageFileNameToPath.values().forEach(messageFiles::addAll);
return messageFiles;
}

private static class AppClassPredicate implements Predicate<String> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package io.quarkus.qute.deployment.i18n;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

public class LocalizedFileDuplicateFoundTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(root -> root.addAsResource(new StringAsset("hello=Ahoj!"), "messages/messages_cs.properties"))
.withAdditionalDependency(
d -> d.addAsResource(new StringAsset("hello=Cau!"), "messages/messages_cs.properties"))
.assertException(t -> {
Throwable e = t;
IllegalStateException ise = null;
while (e != null) {
if (e instanceof IllegalStateException) {
ise = (IllegalStateException) e;
break;
}
e = e.getCause();
}
assertNotNull(ise);
assertTrue(ise.getMessage().contains("Duplicate localized files found:"), ise.getMessage());
assertTrue(ise.getMessage().contains("messages_cs.properties"), ise.getMessage());
});

@Test
public void testValidation() {
fail();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.quarkus.qute.deployment.i18n;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.qute.i18n.Localized;
import io.quarkus.test.QuarkusUnitTest;

public class LocalizedFileOutsideRootTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(root -> root.addClass(BravoMessages.class))
.overrideConfigKey("quarkus.default-locale", "en")
.withAdditionalDependency(
d -> d.addAsResource(new StringAsset("hello=Ahoj!"), "messages/msg_cs.properties"));

@Localized("cs")
BravoMessages messages;

@Test
public void testLocalizedFile() {
assertEquals("Ahoj!", messages.hello());
}

}

0 comments on commit eb74feb

Please sign in to comment.