Add menu to search for missing text keys
diff --git a/org.eclipse.scout.sdk.s2e.nls/plugin.xml b/org.eclipse.scout.sdk.s2e.nls/plugin.xml
index a57e6a4..9527783 100644
--- a/org.eclipse.scout.sdk.s2e.nls/plugin.xml
+++ b/org.eclipse.scout.sdk.s2e.nls/plugin.xml
@@ -52,4 +52,30 @@
          </partition>
       </javaCompletionProposalComputer>
    </extension>
+   <extension point="org.eclipse.ui.menus">
+      <menuContribution locationURI="menu:org.eclipse.scout.sdk.s2e.ui.menu?after=org.eclipse.scout.sdk.s2e.ui.menu.missingClassIdSelected">
+         <separator
+               name="org.eclipse.scout.sdk.s2e.nls.menu.separator01"
+               visible="true">
+         </separator>
+         <command
+               commandId="org.eclipse.scout.sdk.s2e.nls.commands.searchMissingTextKeys"
+               id="org.eclipse.scout.sdk.s2e.nls.menu.searchMissingTextKeys"
+               style="push">
+         </command>
+      </menuContribution>
+   </extension>
+   <extension point="org.eclipse.ui.commands">
+      <command
+            name="Search missing text keys..."
+            id="org.eclipse.scout.sdk.s2e.nls.commands.searchMissingTextKeys"
+            categoryId="org.eclipse.scout.sdk.s2e.ui.commands.category">
+      </command>
+   </extension>
+   <extension point="org.eclipse.ui.handlers">
+      <handler
+            commandId="org.eclipse.scout.sdk.s2e.nls.commands.searchMissingTextKeys"
+            class="org.eclipse.scout.sdk.s2e.nls.internal.ui.handler.NlsFindMissingKeysHandler">
+      </handler>
+    </extension>
 </plugin>
diff --git a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/model/NlsWorkspace.java b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/model/NlsWorkspace.java
index bce9577..33da8a2 100644
--- a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/model/NlsWorkspace.java
+++ b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/model/NlsWorkspace.java
@@ -10,12 +10,15 @@
  ******************************************************************************/
 package org.eclipse.scout.sdk.s2e.nls.internal.model;
 
+import java.util.ArrayList;
+import java.util.Collection;
+
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IConfigurationElement;
 import org.eclipse.core.runtime.IExtension;
 import org.eclipse.core.runtime.IExtensionPoint;
-import org.eclipse.core.runtime.IExtensionRegistry;
 import org.eclipse.core.runtime.Platform;
+import org.eclipse.scout.sdk.core.util.SdkLog;
 import org.eclipse.scout.sdk.s2e.nls.NlsCore;
 import org.eclipse.scout.sdk.s2e.nls.model.INlsProjectProvider;
 import org.eclipse.scout.sdk.s2e.nls.model.INlsWorkspace;
@@ -26,24 +29,50 @@
  */
 public final class NlsWorkspace implements INlsWorkspace {
 
-  @Override
-  public INlsProject getNlsProject(final Object[] args) throws CoreException {
-    IExtensionRegistry reg = Platform.getExtensionRegistry();
-    IExtensionPoint xp = reg.getExtensionPoint(NlsCore.PLUGIN_ID, "nlsProvider");
-    IExtension[] extensions = xp.getExtensions();
-    for (IExtension extension : extensions) {
-      IConfigurationElement[] elements = extension.getConfigurationElements();
-      for (IConfigurationElement element : elements) {
-        if ("provider".equals(element.getName())) {
-          INlsProjectProvider p = (INlsProjectProvider) element.createExecutableExtension("class");
-          if (p != null) {
-            INlsProject proj = p.getProject(args);
-            if (proj != null) {
-              return proj;
+  private volatile Collection<INlsProjectProvider> m_providers;
+
+  private Collection<INlsProjectProvider> getProviders() {
+    Collection<INlsProjectProvider> providers = m_providers;
+    if (providers != null) {
+      return providers;
+    }
+
+    synchronized (this) {
+      providers = m_providers;
+      if (providers != null) {
+        return providers;
+      }
+
+      providers = new ArrayList<>(2);
+      final IExtensionPoint xp = Platform.getExtensionRegistry().getExtensionPoint(NlsCore.PLUGIN_ID, "nlsProvider");
+      for (final IExtension extension : xp.getExtensions()) {
+        final IConfigurationElement[] elements = extension.getConfigurationElements();
+        for (final IConfigurationElement element : elements) {
+          if ("provider".equals(element.getName())) {
+            try {
+              final INlsProjectProvider p = (INlsProjectProvider) element.createExecutableExtension("class");
+              if (p != null) {
+                providers.add(p);
+              }
+            }
+            catch (final CoreException e) {
+              SdkLog.error("Unable to create extension '{}'.", element.getNamespaceIdentifier(), e);
             }
           }
         }
       }
+      m_providers = providers;
+      return providers;
+    }
+  }
+
+  @Override
+  public INlsProject getNlsProject(final Object... args) {
+    for (final INlsProjectProvider p : getProviders()) {
+      final INlsProject proj = p.getProject(args);
+      if (proj != null) {
+        return proj;
+      }
     }
     return null;
   }
diff --git a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/search/JavaProjectsWalker.java b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/search/JavaProjectsWalker.java
new file mode 100644
index 0000000..1654011
--- /dev/null
+++ b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/search/JavaProjectsWalker.java
@@ -0,0 +1,282 @@
+/*******************************************************************************
+ * Copyright (c) 2017 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.scout.sdk.s2e.nls.internal.search;
+
+import static java.util.Collections.unmodifiableCollection;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Optional;
+import java.util.function.BiPredicate;
+import java.util.function.Consumer;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.scout.sdk.core.util.SdkException;
+
+/**
+ * <h3>{@link JavaProjectsWalker}</h3>
+ *
+ * @since 7.0.100
+ */
+public class JavaProjectsWalker {
+
+  private final String m_taskName;
+  private final Collection<String> m_fileExtensions;
+
+  private boolean m_skipOutputLocation;
+  private boolean m_skipHiddenPaths;
+  private BiPredicate<Path, BasicFileAttributes> m_fileFilter;
+
+  public JavaProjectsWalker(final String taskName) {
+    m_taskName = Validate.notNull(taskName);
+    m_fileExtensions = new ArrayList<>();
+    m_skipOutputLocation = true;
+    m_skipHiddenPaths = true;
+  }
+
+  public void walk(final Consumer<WorkspaceFile> visitor, final IProgressMonitor monitor) throws CoreException {
+    final IJavaProject[] javaProjects = JavaCore.create(ResourcesPlugin.getWorkspace().getRoot()).getJavaProjects();
+    monitor.beginTask(taskName(), javaProjects.length);
+    for (final IJavaProject root : javaProjects) {
+      final IProject p = root.getProject();
+      final Path outputLocation;
+      if (isSkipOutputLocation()) {
+        outputLocation = new File(p.getLocation().toOSString(), root.getOutputLocation().removeFirstSegments(1).toOSString()).toPath();
+      }
+      else {
+        outputLocation = null;
+      }
+
+      searchInFolder(visitor, p.getLocation().toFile().toPath(), Charset.forName(p.getDefaultCharset()), outputLocation, monitor);
+
+      if (monitor.isCanceled()) {
+        return;
+      }
+      monitor.worked(1);
+    }
+  }
+
+  protected void searchInFolder(final Consumer<WorkspaceFile> visitor, final Path folder, final Charset charset, final Path outputFolder, final IProgressMonitor monitor) {
+    try {
+      Files.walkFileTree(folder,
+          new SimpleFileVisitor<Path>() {
+            @Override
+            public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs) {
+              if (monitor.isCanceled()) {
+                return FileVisitResult.TERMINATE;
+              }
+              if (dir.equals(outputFolder)) {
+                return FileVisitResult.SKIP_SUBTREE;
+              }
+              if (!hiddenFilterAndCustomFilterAccepted(dir, attrs)) {
+                return FileVisitResult.SKIP_SUBTREE;
+              }
+              return FileVisitResult.CONTINUE;
+            }
+
+            @Override
+            public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) {
+              if (monitor.isCanceled()) {
+                return FileVisitResult.TERMINATE;
+              }
+              if (allFiltersAccepted(file, attrs)) {
+                visitor.accept(new WorkspaceFile(file, charset));
+              }
+              return FileVisitResult.CONTINUE;
+            }
+          });
+    }
+    catch (final IOException e) {
+      throw new SdkException(e);
+    }
+  }
+
+  protected boolean hiddenFilterAndCustomFilterAccepted(final Path file, final BasicFileAttributes attrs) {
+    if (isSkipHiddenPaths() && isHidden(file)) {
+      return false;
+    }
+
+    return fileFilter()
+        .map(filter -> filter.test(file, attrs))
+        .orElse(Boolean.TRUE)
+        .booleanValue();
+  }
+
+  protected boolean allFiltersAccepted(final Path file, final BasicFileAttributes attrs) {
+    if (!acceptFileExtension(file)) {
+      return false;
+    }
+    return hiddenFilterAndCustomFilterAccepted(file, attrs);
+  }
+
+  protected boolean acceptFileExtension(final Path file) {
+    final Path path = file.getFileName();
+    if (path == null) {
+      return false;
+    }
+    final String fileName = path.toString().toLowerCase();
+    for (final String extension : extensionsAccepted()) {
+      if (fileName.endsWith(extension)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  protected static boolean isHidden(final Path path) {
+    final Path fileName = path.getFileName();
+    return fileName != null && fileName.toString().startsWith(".");
+  }
+
+  public String taskName() {
+    return m_taskName;
+  }
+
+  public boolean isSkipOutputLocation() {
+    return m_skipOutputLocation;
+  }
+
+  public JavaProjectsWalker withSkipOutputLocation(final boolean skipOutputLocation) {
+    m_skipOutputLocation = skipOutputLocation;
+    return this;
+  }
+
+  public boolean isSkipHiddenPaths() {
+    return m_skipHiddenPaths;
+  }
+
+  public JavaProjectsWalker withSkipHiddenPaths(final boolean skipHiddenPaths) {
+    m_skipHiddenPaths = skipHiddenPaths;
+    return this;
+  }
+
+  public Optional<BiPredicate<Path, BasicFileAttributes>> fileFilter() {
+    return Optional.ofNullable(m_fileFilter);
+  }
+
+  public JavaProjectsWalker withFilter(final BiPredicate<Path, BasicFileAttributes> fileFilter) {
+    m_fileFilter = fileFilter;
+    return this;
+  }
+
+  public Collection<String> extensionsAccepted() {
+    return unmodifiableCollection(m_fileExtensions);
+  }
+
+  public JavaProjectsWalker withExtensionsAccepted(final String... extensions) {
+    final Collection<String> l = extensions == null ? null : Arrays.asList(extensions);
+    return withExtensionsAccepted(l);
+  }
+
+  public JavaProjectsWalker withExtensionsAccepted(final Collection<String> extensions) {
+    m_fileExtensions.clear();
+    if (extensions != null && !extensions.isEmpty()) {
+      for (final String e : extensions) {
+        if (StringUtils.isNotBlank(e)) {
+          m_fileExtensions.add(e);
+        }
+      }
+    }
+    return this;
+  }
+
+  public static class WorkspaceFile {
+    private final Path m_file;
+    private final Charset m_charset;
+    private char[] m_content; // loaded on request
+    private Optional<IFile> m_workspaceFile; // loaded on request
+
+    protected WorkspaceFile(final Path file, final Charset charset) {
+      m_file = Validate.notNull(file);
+      m_charset = Validate.notNull(charset);
+    }
+
+    public Charset charset() {
+      return m_charset;
+    }
+
+    public Path path() {
+      return m_file;
+    }
+
+    public Optional<IFile> inWorkspace() {
+      if (m_workspaceFile == null) {
+        m_workspaceFile = Optional.ofNullable(resolveInWorkspace(path()));
+      }
+      return m_workspaceFile;
+    }
+
+    protected static IFile resolveInWorkspace(final Path file) {
+      final IFile[] workspaceFiles = ResourcesPlugin.getWorkspace().getRoot().findFilesForLocationURI(file.toUri());
+      if (workspaceFiles.length < 1) {
+        return null;
+      }
+      final IFile workspaceFile = workspaceFiles[0];
+      if (!workspaceFile.exists()) {
+        return null;
+      }
+      return workspaceFile;
+    }
+
+    public char[] content() {
+      if (m_content == null) {
+        try {
+          m_content = charset().decode(ByteBuffer.wrap(Files.readAllBytes(path()))).array();
+        }
+        catch (final IOException e) {
+          throw new SdkException("Unable to read content of file '" + path() + "'.", e);
+        }
+      }
+      return m_content;
+    }
+
+    @Override
+    public String toString() {
+      return WorkspaceFile.class.getSimpleName() + ": " + path();
+    }
+
+    @Override
+    public int hashCode() {
+      return m_file.hashCode();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (obj == null || getClass() != obj.getClass()) {
+        return false;
+      }
+
+      final WorkspaceFile other = (WorkspaceFile) obj;
+      return m_file.equals(other.m_file);
+    }
+  }
+}
diff --git a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/search/NlsKeySearchQuery.java b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/search/NlsFindKeyQuery.java
similarity index 94%
rename from org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/search/NlsKeySearchQuery.java
rename to org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/search/NlsFindKeyQuery.java
index b228d65..f90b603 100644
--- a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/search/NlsKeySearchQuery.java
+++ b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/search/NlsFindKeyQuery.java
@@ -23,12 +23,12 @@
 /**
  * <h4>NlsKeySearchQuery</h4>
  */
-public class NlsKeySearchQuery extends FileSearchQuery {
+public class NlsFindKeyQuery extends FileSearchQuery {
 
   private final String m_nlsKey;
   private final INlsProject m_project;
 
-  public NlsKeySearchQuery(INlsProject project, String nlsKey) {
+  public NlsFindKeyQuery(INlsProject project, String nlsKey) {
     super("", false, false, null);
     m_project = project;
     m_nlsKey = nlsKey;
@@ -46,7 +46,6 @@
 
   @Override
   public FileSearchResult getSearchResult() {
-
     return (FileSearchResult) super.getSearchResult();
   }
 
diff --git a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/search/NlsFindKeysJob.java b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/search/NlsFindKeysJob.java
index f68a61b..d2d6718 100644
--- a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/search/NlsFindKeysJob.java
+++ b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/search/NlsFindKeysJob.java
@@ -10,31 +10,24 @@
  ******************************************************************************/
 package org.eclipse.scout.sdk.s2e.nls.internal.search;
 
-import java.io.IOException;
-import java.nio.file.FileVisitResult;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
+import static java.util.Collections.singletonList;
+
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 
-import org.eclipse.core.resources.IFile;
-import org.eclipse.core.resources.IProject;
-import org.eclipse.core.resources.ResourcesPlugin;
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IProgressMonitor;
-import org.eclipse.jdt.core.IJavaProject;
-import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.compiler.CharOperation;
 import org.eclipse.jdt.internal.compiler.util.SuffixConstants;
 import org.eclipse.scout.sdk.s2e.job.AbstractJob;
+import org.eclipse.scout.sdk.s2e.nls.internal.search.JavaProjectsWalker.WorkspaceFile;
 import org.eclipse.scout.sdk.s2e.nls.model.INlsEntry;
 import org.eclipse.scout.sdk.s2e.nls.project.INlsProject;
-import org.eclipse.scout.sdk.s2e.util.ScoutStatus;
 import org.eclipse.search.ui.text.Match;
 
 /**
@@ -42,28 +35,28 @@
  */
 public class NlsFindKeysJob extends AbstractJob {
 
-  private final List<String> m_searchKeys;
+  private final List<char[]> m_searchKeys;
   private final Map<String, List<Match>> m_matches;
 
-  public NlsFindKeysJob(String nlsKey, String jobTitle) {
-    this(Collections.singletonList(nlsKey), jobTitle);
+  public NlsFindKeysJob(final String nlsKey, final String jobTitle) {
+    this(singletonList(nlsKey), jobTitle);
   }
 
-  public NlsFindKeysJob(INlsProject project, String jobTitle) {
+  public NlsFindKeysJob(final INlsProject project, final String jobTitle) {
     this(getLocalKeys(project), jobTitle);
   }
 
-  protected NlsFindKeysJob(List<String> searchKeys, String jobTitle) {
+  protected NlsFindKeysJob(final Collection<String> searchKeys, final String jobTitle) {
     super(jobTitle);
     m_searchKeys = new ArrayList<>(searchKeys.size() * 2);
-    for (String key : searchKeys) {
-      m_searchKeys.add("\"" + key + "\"");
-      m_searchKeys.add('\'' + key + '\''); // e.g. for search in .js files
+    for (final String key : searchKeys) {
+      m_searchKeys.add(('"' + key + '"').toCharArray());
+      m_searchKeys.add(('\'' + key + '\'').toCharArray()); // e.g. for search in .js files
     }
     m_matches = new HashMap<>();
   }
 
-  protected static List<String> getLocalKeys(INlsProject project) {
+  private static List<String> getLocalKeys(final INlsProject project) {
     return project.getAllEntries().stream()
         .filter(entry -> entry.getType() == INlsEntry.TYPE_LOCAL)
         .map(INlsEntry::getKey)
@@ -71,114 +64,36 @@
   }
 
   @Override
-  public void execute(IProgressMonitor monitor) throws CoreException {
+  public void execute(final IProgressMonitor monitor) throws CoreException {
     m_matches.clear();
-    IJavaProject[] javaProjects = JavaCore.create(ResourcesPlugin.getWorkspace().getRoot()).getJavaProjects();
-    monitor.beginTask("Searching for NLS keys", javaProjects.length);
-    for (IJavaProject root : javaProjects) {
-      monitor.setTaskName("Searching in '" + root.getElementName() + "'.");
-
-      IProject p = root.getProject();
-      Path outputLocation = new java.io.File(p.getLocation().toOSString(), root.getOutputLocation().removeFirstSegments(1).toOSString()).toPath();
-      searchInFolder(p.getLocation().toFile().toPath(), p.getDefaultCharset(), outputLocation, monitor);
-
-      if (monitor.isCanceled()) {
-        return;
-      }
-      monitor.worked(1);
-    }
+    new JavaProjectsWalker(getName())
+        .withExtensionsAccepted(SuffixConstants.EXTENSION_java, "js", "html", "less", "json", "xml", "sql", "css", "svg", "txt", "jsp")
+        .walk(this::searchInFile, monitor);
   }
 
-  protected void searchInFolder(Path folder, final String charset, final Path outputFolder, final IProgressMonitor monitor) throws CoreException {
-    try {
-      Files.walkFileTree(folder,
-          new SimpleFileVisitor<Path>() {
-            @Override
-            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
-              if (monitor.isCanceled()) {
-                return FileVisitResult.TERMINATE;
-              }
-              if (dir.equals(outputFolder)) {
-                return FileVisitResult.SKIP_SUBTREE;
-              }
-              Path fileName = dir.getFileName();
-              boolean isHiddenDir = fileName != null && fileName.toString().startsWith(".");
-              if (isHiddenDir) {
-                return FileVisitResult.SKIP_SUBTREE;
-              }
-              return FileVisitResult.CONTINUE;
-            }
-
-            @Override
-            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
-              if (isInterestingFile(file, attrs)) {
-                searchInFile(file, charset, monitor);
-              }
-              if (monitor.isCanceled()) {
-                return FileVisitResult.TERMINATE;
-              }
-              return FileVisitResult.CONTINUE;
-            }
-          });
-    }
-    catch (IOException e) {
-      throw new CoreException(new ScoutStatus(e));
-    }
-  }
-
-  protected boolean isInterestingFile(Path file, BasicFileAttributes attrs) {
-    if (!attrs.isRegularFile()) {
-      return false;
-    }
-    Path path = file.getFileName();
-    if (path == null) {
-      return false;
-    }
-    String fileName = path.toString().toLowerCase();
-    String[] interestingFileExtensions = {SuffixConstants.SUFFIX_STRING_java, ".js", ".html", ".less", ".json", ".xml", ".sql", ".css", ".svg", ".txt", ".jsp"};
-    for (String extension : interestingFileExtensions) {
-      if (fileName.endsWith(extension)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  protected void searchInFile(Path file, String charset, IProgressMonitor monitor) throws IOException {
-    IFile[] workspaceFiles = ResourcesPlugin.getWorkspace().getRoot().findFilesForLocationURI(file.toUri());
-    if (workspaceFiles.length < 1) {
-      return;
-    }
-    IFile workspaceFile = workspaceFiles[0];
-    if (!workspaceFile.exists()) {
-      return;
-    }
-
-    String content = new String(Files.readAllBytes(file), charset);
-    for (String search : m_searchKeys) {
+  protected void searchInFile(final WorkspaceFile file) {
+    for (final char[] search : m_searchKeys) {
       int pos = 0;
-      int index = -1;
-      while ((index = content.indexOf(search, pos)) >= 0) {
-        if (monitor.isCanceled()) {
-          return;
+      int index;
+      while ((index = CharOperation.indexOf(search, file.content(), true, pos)) >= 0) {
+        if (file.inWorkspace().isPresent()) {
+          final Match match = new Match(file.inWorkspace().get(), index, search.length);
+          final String key = String.valueOf(search, 1, search.length - 2); // remove starting and ending quotes
+          acceptNlsKeyMatch(key, match);
         }
-
-        Match match = new Match(workspaceFile, index, search.length());
-        String key = search.substring(1, search.length() - 1); // remove starting and ending quotes
-        acceptNlsKeyMatch(key, match);
-        pos = index + search.length();
+        pos = index + search.length;
       }
     }
   }
 
-  protected void acceptNlsKeyMatch(String nlsKey, Match match) {
+  protected void acceptNlsKeyMatch(final String nlsKey, final Match match) {
     m_matches
         .computeIfAbsent(nlsKey, key -> new ArrayList<>())
         .add(match);
   }
 
-  public List<Match> getMatches(String nlsKey) {
-    List<Match> list = m_matches.get(nlsKey);
+  public List<Match> getMatches(final String nlsKey) {
+    final List<Match> list = m_matches.get(nlsKey);
     if (list == null) {
       return Collections.emptyList();
     }
diff --git a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/search/NlsFindMissingKeysJob.java b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/search/NlsFindMissingKeysJob.java
new file mode 100644
index 0000000..c97345a
--- /dev/null
+++ b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/search/NlsFindMissingKeysJob.java
@@ -0,0 +1,174 @@
+/*******************************************************************************
+ * Copyright (c) 2017 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.scout.sdk.s2e.nls.internal.search;
+
+import static java.util.Collections.emptySet;
+import static java.util.Collections.unmodifiableList;
+
+import java.nio.CharBuffer;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.MatchResult;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.Signature;
+import org.eclipse.jdt.internal.compiler.util.SuffixConstants;
+import org.eclipse.scout.sdk.core.s.IScoutRuntimeTypes;
+import org.eclipse.scout.sdk.core.util.SdkException;
+import org.eclipse.scout.sdk.core.util.SdkLog;
+import org.eclipse.scout.sdk.s2e.job.AbstractJob;
+import org.eclipse.scout.sdk.s2e.nls.NlsCore;
+import org.eclipse.scout.sdk.s2e.nls.internal.search.JavaProjectsWalker.WorkspaceFile;
+import org.eclipse.scout.sdk.s2e.nls.internal.ui.formatter.InputValidator;
+import org.eclipse.scout.sdk.s2e.nls.project.INlsProject;
+import org.eclipse.scout.sdk.s2e.util.S2eUtils;
+import org.eclipse.search.ui.text.Match;
+
+/**
+ * <h3>{@link NlsFindMissingKeysJob}</h3>
+ * <p>
+ * Searches for NLS keys that are used in the code but do not exist.
+ *
+ * @since 7.0.100
+ */
+public class NlsFindMissingKeysJob extends AbstractJob {
+
+  private final Map<String, Collection<Pattern>> m_patternsByFileType;
+  private final Map<IJavaProject, Set<String>> m_existingKeys;
+  private final List<Match> m_matches;
+  private final List<Match> m_errorMatches;
+  private final String m_textClassName;
+
+  public NlsFindMissingKeysJob() {
+    super("Search for text keys that are used but do not exist.");
+    m_existingKeys = new HashMap<>();
+    m_matches = new ArrayList<>();
+    m_errorMatches = new ArrayList<>();
+    m_patternsByFileType = new HashMap<>();
+
+    final String nlsKeyPattern = InputValidator.REGEX_NLS_KEY_NAME.pattern();
+    final Pattern jsonTextKeyPat = Pattern.compile("\\$\\{textKey:(" + nlsKeyPattern + ')');
+    final Pattern jsTextKeyPat = Pattern.compile("session\\.text\\(('?)(" + nlsKeyPattern + ")('?)");
+    m_patternsByFileType.put(SuffixConstants.EXTENSION_java, Collections.singletonList(Pattern.compile("TEXTS\\.get\\((\"?)(" + nlsKeyPattern + ")(\"?)")));
+    m_patternsByFileType.put("json", Collections.singletonList(jsonTextKeyPat));
+    m_patternsByFileType.put("js", Arrays.asList(jsTextKeyPat, jsonTextKeyPat));
+    m_patternsByFileType.put("html", Arrays.asList(Pattern.compile("\\<scout:message key=\"(" + nlsKeyPattern + ")\"\\s*/?\\>"), jsTextKeyPat, jsonTextKeyPat));
+
+    m_textClassName = Signature.getSimpleName(IScoutRuntimeTypes.TEXTS) + SuffixConstants.SUFFIX_STRING_java;
+  }
+
+  private static Set<String> keysVisibleFrom(final IJavaProject jp) {
+    if (!S2eUtils.exists(jp)) {
+      return emptySet();
+    }
+
+    final INlsProject nlsProject = NlsCore.getNlsWorkspace().getNlsProject(jp);
+    if (nlsProject == null) {
+      return emptySet();
+    }
+    return nlsProject.getAllKeys();
+  }
+
+  @Override
+  protected void execute(final IProgressMonitor monitor) throws CoreException {
+    m_matches.clear();
+    m_errorMatches.clear();
+    new JavaProjectsWalker(getName())
+        .withExtensionsAccepted(m_patternsByFileType.keySet())
+        .withFilter(this::isInterestingPath)
+        .walk(this::searchInFile, monitor);
+  }
+
+  protected boolean isInterestingPath(final Path p, final BasicFileAttributes attrs) {
+    if (attrs.isDirectory()) {
+      // skip tests && archetype resources
+      return !p.endsWith("src/test") && !p.endsWith("archetype-resources") && !p.endsWith("generated-resources");
+    }
+    return !p.endsWith(m_textClassName);
+  }
+
+  protected void searchInFile(final WorkspaceFile file) {
+    if (!file.inWorkspace().isPresent()) {
+      SdkLog.warning("File '{}' could not be found in the current Eclipse Workspace.", file.path());
+      return;
+    }
+
+    final String fileName = file.path().getFileName().toString().toLowerCase();
+    final int lastDotPos = fileName.lastIndexOf('.');
+    final String extension = fileName.substring(lastDotPos + 1);
+    final Collection<Pattern> patterns = m_patternsByFileType.get(extension);
+    if (patterns == null || patterns.isEmpty()) {
+      throw new SdkException("Unexpected: no pattern for file: " + file.path());
+    }
+
+    for (final Pattern p : patterns) {
+      final Matcher matcher = p.matcher(CharBuffer.wrap(file.content()));
+      while (matcher.find()) {
+        final int keyGroup;
+        if (matcher.groupCount() > 1) {
+          // pattern with optional literal delimiter: '"' for java, ''' for js
+          // check if the literal delimiter is present. if not present: not possible to detect the real key -> add to error list.
+          keyGroup = 2;
+          final boolean noLiteral = StringUtils.isEmpty(matcher.group(1)) || StringUtils.isEmpty(matcher.group(3));
+          if (noLiteral) {
+            registerMatch(file, matcher, m_errorMatches, keyGroup);
+            continue;
+          }
+        }
+        else {
+          // pattern without literal delimiter
+          keyGroup = 1;
+        }
+
+        final String key = matcher.group(keyGroup);
+        if (!keyExists(file.inWorkspace().get().getProject(), key)) {
+          registerMatch(file, matcher, m_matches, keyGroup);
+        }
+      }
+    }
+  }
+
+  protected boolean keyExists(final IProject context, final String key) {
+    return m_existingKeys
+        .computeIfAbsent(JavaCore.create(context), NlsFindMissingKeysJob::keysVisibleFrom)
+        .contains(key);
+  }
+
+  protected static Match registerMatch(final WorkspaceFile file, final MatchResult matcher, final Collection<Match> targetList, final int keyGroup) {
+    final int index = matcher.start(keyGroup);
+    final Match match = new Match(file.inWorkspace().get(), index, matcher.end(keyGroup) - index);
+    targetList.add(match);
+    return match;
+  }
+
+  public List<Match> matches() {
+    return unmodifiableList(m_matches);
+  }
+
+  public List<Match> errors() {
+    return unmodifiableList(m_errorMatches);
+  }
+}
diff --git a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/search/NlsFindMissingKeysQuery.java b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/search/NlsFindMissingKeysQuery.java
new file mode 100644
index 0000000..a0ba13e
--- /dev/null
+++ b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/search/NlsFindMissingKeysQuery.java
@@ -0,0 +1,65 @@
+/*******************************************************************************
+ * Copyright (c) 2017 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.scout.sdk.s2e.nls.internal.search;
+
+import java.util.List;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.search.internal.ui.text.FileSearchQuery;
+import org.eclipse.search.internal.ui.text.FileSearchResult;
+import org.eclipse.search.ui.text.Match;
+
+/**
+ * <h3>{@link NlsFindMissingKeysQuery}</h3>
+ *
+ * @since 7.0.100
+ */
+public class NlsFindMissingKeysQuery extends FileSearchQuery {
+
+  public NlsFindMissingKeysQuery() {
+    super("", false, false, null);
+  }
+
+  @Override
+  public String getResultLabel(final int matches) {
+    return "References to missing text keys (" + matches + ").";
+  }
+
+  @Override
+  public String getLabel() {
+    return "Find references to missing text keys...";
+  }
+
+  @Override
+  public FileSearchResult getSearchResult() {
+    return (FileSearchResult) super.getSearchResult();
+  }
+
+  @Override
+  public IStatus run(final IProgressMonitor monitor) {
+    final NlsFindMissingKeysJob nlsFindMissingKeysJob = new NlsFindMissingKeysJob();
+    final IStatus result = nlsFindMissingKeysJob.run(monitor);
+    if (result != null && result.getSeverity() == IStatus.CANCEL) {
+      return result;
+    }
+
+    final List<Match> missingKeys = nlsFindMissingKeysJob.matches();
+    final List<Match> unableToDetect = nlsFindMissingKeysJob.errors();
+    final FileSearchResult searchResult = getSearchResult();
+    searchResult.removeAll();
+    searchResult.addMatches(missingKeys.toArray(new Match[missingKeys.size()]));
+    searchResult.addMatches(unableToDetect.toArray(new Match[unableToDetect.size()]));
+
+    return Status.OK_STATUS;
+  }
+}
diff --git a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/serviceproject/ServiceNlsProjectProvider.java b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/serviceproject/ServiceNlsProjectProvider.java
index 0cd4790..fcd3350 100644
--- a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/serviceproject/ServiceNlsProjectProvider.java
+++ b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/serviceproject/ServiceNlsProjectProvider.java
@@ -237,6 +237,10 @@
           // text service file
           return getProjectByTextServiceFile((IFile) args[0]);
         }
+        else if (args[0] instanceof IJavaProject) {
+          // java project
+          return getProjectByJavaProject((IJavaProject) args[0]);
+        }
       }
       else if (args.length == 2 && args[0] instanceof IType) {
         if (args[1] instanceof IJavaProject || args[1] == null) {
@@ -263,14 +267,28 @@
     }
   }
 
+  private static INlsProject getProjectByJavaProject(final IJavaProject jp) {
+    if (!S2eUtils.exists(jp)) {
+      return null;
+    }
+    try {
+      final Set<IType> txtProviderServices = getRegisteredTextProviderTypes(jp);
+      return textProviderTypesToNlsProject(txtProviderServices);
+    }
+    catch (final CoreException e) {
+      SdkLog.warning("Could not load text provider services for java project: {}", jp.getElementName(), e);
+    }
+    return null;
+  }
+
   private static INlsProject getProjectByTextServiceFile(IFile f) {
     try {
       IType type = getITypeForFile(f);
       if (type != null) {
-        return NlsCore.getNlsWorkspace().getNlsProject(new Object[]{type});
+        return NlsCore.getNlsWorkspace().getNlsProject(type);
       }
     }
-    catch (CoreException e) {
+    catch (JavaModelException e) {
       SdkLog.warning("Could not load text provider services for file: {}", f.getFullPath().toString(), e);
     }
     return null;
diff --git a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/simpleproject/SimpleNlsProject.java b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/simpleproject/SimpleNlsProject.java
index b1c57d0..0700531 100644
--- a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/simpleproject/SimpleNlsProject.java
+++ b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/simpleproject/SimpleNlsProject.java
@@ -176,12 +176,7 @@
   private void setSuperType(IType superType) {
     // find parent
     if (superType != null) {
-      try {
-        setParent(NlsCore.getNlsWorkspace().getNlsProject(new Object[]{superType}));
-      }
-      catch (CoreException e) {
-        SdkLog.error("parent of NLS project could not be found. Looked for type '{}'.", superType.getFullyQualifiedName(), e);
-      }
+      setParent(NlsCore.getNlsWorkspace().getNlsProject(superType));
     }
   }
 
diff --git a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/simpleproject/SimpleNlsProjectProvider.java b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/simpleproject/SimpleNlsProjectProvider.java
index fc9da41..c75a5ae 100644
--- a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/simpleproject/SimpleNlsProjectProvider.java
+++ b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/simpleproject/SimpleNlsProjectProvider.java
@@ -14,10 +14,10 @@
 import java.util.Map;
 
 import org.eclipse.core.resources.IFile;
-import org.eclipse.core.runtime.CoreException;
 import org.eclipse.jdt.core.IJavaProject;
 import org.eclipse.jdt.core.IType;
 import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
 import org.eclipse.scout.sdk.core.util.SdkLog;
 import org.eclipse.scout.sdk.s2e.nls.NlsCore;
 import org.eclipse.scout.sdk.s2e.nls.model.INlsProjectProvider;
@@ -84,7 +84,7 @@
               }
             }
           }
-          catch (CoreException e) {
+          catch (JavaModelException e) {
             SdkLog.warning("Could not load NlsFile: {}", f.getFullPath().toString(), e);
           }
         }
diff --git a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/ui/action/FindReferencesAction.java b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/ui/action/FindReferencesAction.java
index f289a14..2f62bd4 100644
--- a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/ui/action/FindReferencesAction.java
+++ b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/ui/action/FindReferencesAction.java
@@ -14,7 +14,7 @@
 import org.eclipse.jface.resource.ImageDescriptor;
 import org.eclipse.scout.sdk.s2e.nls.INlsIcons;
 import org.eclipse.scout.sdk.s2e.nls.NlsCore;
-import org.eclipse.scout.sdk.s2e.nls.internal.search.NlsKeySearchQuery;
+import org.eclipse.scout.sdk.s2e.nls.internal.search.NlsFindKeyQuery;
 import org.eclipse.scout.sdk.s2e.nls.project.INlsProject;
 import org.eclipse.search.ui.NewSearchUI;
 
@@ -34,7 +34,7 @@
 
   @Override
   public void run() {
-    NewSearchUI.runQueryInBackground(new NlsKeySearchQuery(getProject(), getKey()));
+    NewSearchUI.runQueryInBackground(new NlsFindKeyQuery(getProject(), getKey()));
   }
 
   @Override
diff --git a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/ui/editor/NlsEditor.java b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/ui/editor/NlsEditor.java
index 89cfd64..93d6696 100644
--- a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/ui/editor/NlsEditor.java
+++ b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/ui/editor/NlsEditor.java
@@ -71,27 +71,22 @@
   private void createTablePage() {
     INlsProject nlsProjects = null;
     IEditorInput input = getEditorInput();
-    try {
-      if (input instanceof FileEditorInput) {
-        nlsProjects = NlsCore.getNlsWorkspace().getNlsProject(new Object[]{((FileEditorInput) input).getFile()});
-      }
-      else if (input instanceof NlsTypeEditorInput) {
-        nlsProjects = NlsCore.getNlsWorkspace().getNlsProject(new Object[]{((NlsTypeEditorInput) input).getType()});
-      }
+    if (input instanceof FileEditorInput) {
+      nlsProjects = NlsCore.getNlsWorkspace().getNlsProject(new Object[]{((FileEditorInput) input).getFile()});
+    }
+    else if (input instanceof NlsTypeEditorInput) {
+      nlsProjects = NlsCore.getNlsWorkspace().getNlsProject(new Object[]{((NlsTypeEditorInput) input).getType()});
+    }
 
-      if (nlsProjects != null) {
-        setPartName(nlsProjects.getName());
-      }
-      else {
-        setPartName("Translations");
-      }
-      NlsTablePage page = new NlsTablePage(getContainer(), nlsProjects);
-      m_tablePageIndex = addPage(page);
-      setPageText(m_tablePageIndex, "Translations");
+    if (nlsProjects != null) {
+      setPartName(nlsProjects.getName());
     }
-    catch (CoreException e) {
-      SdkLog.error("could not load file: {}", input.getName(), e);
+    else {
+      setPartName("Translations");
     }
+    NlsTablePage page = new NlsTablePage(getContainer(), nlsProjects);
+    m_tablePageIndex = addPage(page);
+    setPageText(m_tablePageIndex, "Translations");
   }
 
   @Override
diff --git a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/ui/formatter/InputValidator.java b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/ui/formatter/InputValidator.java
index 8574226..db7983e 100644
--- a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/ui/formatter/InputValidator.java
+++ b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/ui/formatter/InputValidator.java
@@ -22,7 +22,7 @@
 import org.eclipse.swt.SWT;
 
 public final class InputValidator {
-  private static final Pattern REGEX_NLS_KEY_NAME = Pattern.compile("\\b[A-Za-z0-9][a-zA-Z0-9_.\\-]{0,200}\\b");
+  public static final Pattern REGEX_NLS_KEY_NAME = Pattern.compile("[A-Za-z][a-zA-Z0-9_.\\-]{0,200}");
 
   private InputValidator() {
   }
diff --git a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/ui/handler/NlsFindMissingKeysHandler.java b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/ui/handler/NlsFindMissingKeysHandler.java
new file mode 100644
index 0000000..24cf144
--- /dev/null
+++ b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/internal/ui/handler/NlsFindMissingKeysHandler.java
@@ -0,0 +1,29 @@
+/*******************************************************************************
+ * Copyright (c) 2017 BSI Business Systems Integration AG.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     BSI Business Systems Integration AG - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.scout.sdk.s2e.nls.internal.ui.handler;
+
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.scout.sdk.s2e.nls.internal.search.NlsFindMissingKeysQuery;
+import org.eclipse.search.ui.NewSearchUI;
+
+/**
+ * <h3>{@link NlsFindMissingKeysHandler}</h3>
+ *
+ * @since 7.0.100
+ */
+public class NlsFindMissingKeysHandler extends AbstractHandler {
+  @Override
+  public Object execute(final ExecutionEvent event) {
+    NewSearchUI.runQueryInBackground(new NlsFindMissingKeysQuery());
+    return null;
+  }
+}
diff --git a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/model/INlsWorkspace.java b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/model/INlsWorkspace.java
index 7ca238e..10dc9f9 100644
--- a/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/model/INlsWorkspace.java
+++ b/org.eclipse.scout.sdk.s2e.nls/src/main/java/org/eclipse/scout/sdk/s2e/nls/model/INlsWorkspace.java
@@ -10,7 +10,6 @@
  ******************************************************************************/
 package org.eclipse.scout.sdk.s2e.nls.model;
 
-import org.eclipse.core.runtime.CoreException;
 import org.eclipse.scout.sdk.s2e.nls.project.INlsProject;
 
 /**
@@ -25,7 +24,6 @@
    * @param args
    *          The arguments to be used when trying to find a INlsProject.
    * @return the NlsProject hierarchy for the given parameters or null.
-   * @throws CoreException
    */
-  INlsProject getNlsProject(Object[] args) throws CoreException;
+  INlsProject getNlsProject(Object... args);
 }