Initial check-in of contribution of Stefan Muecke
diff --git a/org.eclipse.babel.editor/.classpath b/org.eclipse.babel.editor/.classpath
index ce73933..5f03640 100644
--- a/org.eclipse.babel.editor/.classpath
+++ b/org.eclipse.babel.editor/.classpath
@@ -3,5 +3,6 @@
 	<classpathentry kind="src" path="src"/>
 	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/J2SE-1.4"/>
 	<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
+	<classpathentry kind="src" path="tests"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/org.eclipse.babel.editor/META-INF/MANIFEST.MF b/org.eclipse.babel.editor/META-INF/MANIFEST.MF
index 9f3b0aa..522a82c 100644
--- a/org.eclipse.babel.editor/META-INF/MANIFEST.MF
+++ b/org.eclipse.babel.editor/META-INF/MANIFEST.MF
@@ -1,19 +1,23 @@
 Manifest-Version: 1.0
 Bundle-ManifestVersion: 2
-Bundle-Name: Messages Editor Plug-in
+Bundle-Name: %plugin.name
 Bundle-SymbolicName: org.eclipse.babel.editor;singleton:=true
-Bundle-Version: 1.0.0
+Bundle-Version: 1.0.1
 Bundle-Activator: org.eclipse.babel.editor.plugin.MessagesEditorPlugin
 Require-Bundle: org.eclipse.ui,
  org.eclipse.core.runtime,
  org.eclipse.jface.text,
- org.eclipse.core.resources,
  org.eclipse.ui.editors,
  org.eclipse.ui.ide,
  org.eclipse.ui.workbench.texteditor,
  org.eclipse.ui.views,
+ org.eclipse.ui.forms;bundle-version="3.2.0",
+ org.eclipse.core.resources;bundle-version="3.2.0",
+ org.eclipse.jdt.core;bundle-version="3.2.0",
+ org.eclipse.pde.core;bundle-version="3.2.0",
+ org.junit;resolution:=optional,
  org.eclipse.babel.core
-Eclipse-LazyStart: true
-Bundle-Vendor: Eclipse
-Bundle-RequiredExecutionEnvironment: J2SE-1.4
+Bundle-ActivationPolicy: lazy
+Bundle-Vendor: %plugin.provider
+Bundle-RequiredExecutionEnvironment: J2SE-1.5
 Bundle-Localization: plugin
diff --git a/org.eclipse.babel.editor/icons/elcl16/clear_co.gif b/org.eclipse.babel.editor/icons/elcl16/clear_co.gif
new file mode 100644
index 0000000..af30a42
--- /dev/null
+++ b/org.eclipse.babel.editor/icons/elcl16/clear_co.gif
Binary files differ
diff --git a/org.eclipse.babel.editor/icons/elcl16/conf_columns.gif b/org.eclipse.babel.editor/icons/elcl16/conf_columns.gif
new file mode 100644
index 0000000..5ef0ed7
--- /dev/null
+++ b/org.eclipse.babel.editor/icons/elcl16/conf_columns.gif
Binary files differ
diff --git a/org.eclipse.babel.editor/icons/elcl16/export.gif b/org.eclipse.babel.editor/icons/elcl16/export.gif
new file mode 100644
index 0000000..3465699
--- /dev/null
+++ b/org.eclipse.babel.editor/icons/elcl16/export.gif
Binary files differ
diff --git a/org.eclipse.babel.editor/icons/elcl16/filter_obj.gif b/org.eclipse.babel.editor/icons/elcl16/filter_obj.gif
new file mode 100644
index 0000000..ef51bd5
--- /dev/null
+++ b/org.eclipse.babel.editor/icons/elcl16/filter_obj.gif
Binary files differ
diff --git a/org.eclipse.babel.editor/icons/elcl16/refresh.gif b/org.eclipse.babel.editor/icons/elcl16/refresh.gif
new file mode 100644
index 0000000..3ca04d0
--- /dev/null
+++ b/org.eclipse.babel.editor/icons/elcl16/refresh.gif
Binary files differ
diff --git a/org.eclipse.babel.editor/icons/obj16/nls_editor.gif b/org.eclipse.babel.editor/icons/obj16/nls_editor.gif
new file mode 100644
index 0000000..e6913d0
--- /dev/null
+++ b/org.eclipse.babel.editor/icons/obj16/nls_editor.gif
Binary files differ
diff --git a/org.eclipse.babel.editor/plugin.properties b/org.eclipse.babel.editor/plugin.properties
index 161b7d5..980946e 100644
--- a/org.eclipse.babel.editor/plugin.properties
+++ b/org.eclipse.babel.editor/plugin.properties
@@ -1,7 +1,7 @@
 editor.title = Messages Editor
 
 plugin.name     = Messages Editor Plug-in
-plugin.provider = Eclipse
+plugin.provider = Eclipse.org
 
 prefs.formatting  = Formatting
 prefs.performance = Reports
@@ -16,3 +16,7 @@
 
 removeNatureAction.label = Disable &localization properties validator
 removeNatureAction.tooltip = Remove the localization validator builder
+
+localizationEditorName = Localization Editor
+command_openLocalizationEditor_name = Open Localization Editor
+command_openLocalizationEditor_mnemonic = L
\ No newline at end of file
diff --git a/org.eclipse.babel.editor/plugin.xml b/org.eclipse.babel.editor/plugin.xml
index a94f711..013f9a3 100644
--- a/org.eclipse.babel.editor/plugin.xml
+++ b/org.eclipse.babel.editor/plugin.xml
@@ -161,4 +161,44 @@
        </description>
     </wizard>
  </extension>
+
+
+
+   <extension
+         point="org.eclipse.ui.elementFactories">
+      <factory
+            class="org.eclipse.pde.nls.internal.ui.editor.LocalizationEditorInputFactory"
+            id="org.eclipse.pde.nls.ui.LocalizationEditorInputFactory"/>
+   </extension>
+
+   <extension
+         point="org.eclipse.ui.editors">
+      <editor
+            class="org.eclipse.pde.nls.internal.ui.editor.LocalizationEditor"
+            icon="icons/obj16/nls_editor.gif"
+            id="org.eclipse.pde.nls.ui.LocalizationEditor"
+            name="%localizationEditorName"/>
+   </extension>
+
+   <extension
+         point="org.eclipse.ui.commands">
+      <command
+            defaultHandler="org.eclipse.pde.nls.internal.ui.OpenLocalizationEditorHandler"
+            id="org.eclipse.pde.nls.ui.OpenLocalizationEditor"
+            name="%command_openLocalizationEditor_name">
+      </command>
+   </extension>
+   
+   <extension
+         point="org.eclipse.ui.menus">
+      <menuContribution
+            locationURI="menu:edit?after=additions">
+         <command
+               commandId="org.eclipse.pde.nls.ui.OpenLocalizationEditor"
+               mnemonic="%command_openLocalizationEditor_mnemonic"
+               style="push">
+         </command>
+      </menuContribution>
+   </extension>
+
 </plugin>
diff --git a/org.eclipse.babel.editor/src/org/eclipse/babel/editor/MessagesEditor.java b/org.eclipse.babel.editor/src/org/eclipse/babel/editor/MessagesEditor.java
index 7f3866d..67571ed 100644
--- a/org.eclipse.babel.editor/src/org/eclipse/babel/editor/MessagesEditor.java
+++ b/org.eclipse.babel.editor/src/org/eclipse/babel/editor/MessagesEditor.java
@@ -38,7 +38,6 @@
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.jface.dialogs.ErrorDialog;
 import org.eclipse.jface.util.IPropertyChangeListener;
-import org.eclipse.jface.util.PropertyChangeEvent;
 import org.eclipse.swt.SWT;
 import org.eclipse.ui.IEditorInput;
 import org.eclipse.ui.IEditorPart;
diff --git a/org.eclipse.babel.editor/src/org/eclipse/babel/editor/builder/Builder.java b/org.eclipse.babel.editor/src/org/eclipse/babel/editor/builder/Builder.java
index b054e94..b7ce85c 100644
--- a/org.eclipse.babel.editor/src/org/eclipse/babel/editor/builder/Builder.java
+++ b/org.eclipse.babel.editor/src/org/eclipse/babel/editor/builder/Builder.java
@@ -17,10 +17,8 @@
 import java.util.Map;
 import java.util.Set;
 
-//import org.apache.lucene.index.CorruptIndexException;
 import org.eclipse.babel.core.message.MessagesBundle;
 import org.eclipse.babel.core.message.MessagesBundleGroup;
-//import org.eclipse.babel.editor.builder.indexer.Indexer;
 import org.eclipse.babel.editor.bundle.MessagesBundleGroupFactory;
 import org.eclipse.babel.editor.plugin.MessagesEditorPlugin;
 import org.eclipse.babel.editor.resource.validator.FileMarkerStrategy;
diff --git a/org.eclipse.babel.editor/src/org/eclipse/babel/editor/i18n/I18NPage.java b/org.eclipse.babel.editor/src/org/eclipse/babel/editor/i18n/I18NPage.java
index 803cae7..e87b6a7 100644
--- a/org.eclipse.babel.editor/src/org/eclipse/babel/editor/i18n/I18NPage.java
+++ b/org.eclipse.babel.editor/src/org/eclipse/babel/editor/i18n/I18NPage.java
@@ -20,7 +20,6 @@
 import org.eclipse.babel.editor.MessagesEditor;
 import org.eclipse.babel.editor.MessagesEditorChangeAdapter;
 import org.eclipse.babel.editor.util.UIUtils;
-import org.eclipse.jface.action.IToolBarManager;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.SashForm;
 import org.eclipse.swt.custom.ScrolledComposite;
diff --git a/org.eclipse.babel.editor/src/org/eclipse/babel/editor/i18n/SideNavTextBoxComposite.java b/org.eclipse.babel.editor/src/org/eclipse/babel/editor/i18n/SideNavTextBoxComposite.java
index 9d1a946..f97ace2 100644
--- a/org.eclipse.babel.editor/src/org/eclipse/babel/editor/i18n/SideNavTextBoxComposite.java
+++ b/org.eclipse.babel.editor/src/org/eclipse/babel/editor/i18n/SideNavTextBoxComposite.java
@@ -10,6 +10,11 @@
  ******************************************************************************/
 package org.eclipse.babel.editor.i18n;
 
+import org.eclipse.babel.core.message.tree.KeyTreeNode;
+import org.eclipse.babel.core.message.tree.visitor.NodePathRegexVisitor;
+import org.eclipse.babel.editor.MessagesEditor;
+import org.eclipse.babel.editor.MessagesEditorChangeAdapter;
+import org.eclipse.babel.editor.plugin.MessagesEditorPlugin;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.events.KeyAdapter;
 import org.eclipse.swt.events.KeyEvent;
@@ -23,13 +28,6 @@
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Text;
 
-
-import org.eclipse.babel.core.message.tree.KeyTreeNode;
-import org.eclipse.babel.core.message.tree.visitor.NodePathRegexVisitor;
-import org.eclipse.babel.editor.MessagesEditor;
-import org.eclipse.babel.editor.MessagesEditorChangeAdapter;
-import org.eclipse.babel.editor.plugin.MessagesEditorPlugin;
-
 /**
  * Tree for displaying and navigating through resource bundle keys.
  * @author Pascal Essiembre
diff --git a/org.eclipse.babel.editor/src/org/eclipse/babel/editor/i18n/actions/ShowDuplicateAction.java b/org.eclipse.babel.editor/src/org/eclipse/babel/editor/i18n/actions/ShowDuplicateAction.java
index 5bdbaf8..a9586d7 100644
--- a/org.eclipse.babel.editor/src/org/eclipse/babel/editor/i18n/actions/ShowDuplicateAction.java
+++ b/org.eclipse.babel.editor/src/org/eclipse/babel/editor/i18n/actions/ShowDuplicateAction.java
@@ -12,8 +12,6 @@
 
 import java.util.Locale;
 
-import org.eclipse.babel.core.message.MessagesBundleGroup;
-import org.eclipse.babel.core.message.checks.DuplicateValueCheck;
 import org.eclipse.babel.editor.util.UIUtils;
 import org.eclipse.jface.action.Action;
 import org.eclipse.jface.dialogs.MessageDialog;
diff --git a/org.eclipse.babel.editor/src/org/eclipse/babel/editor/i18n/actions/ShowSimilarAction.java b/org.eclipse.babel.editor/src/org/eclipse/babel/editor/i18n/actions/ShowSimilarAction.java
index 0ebfd03..c4910be 100644
--- a/org.eclipse.babel.editor/src/org/eclipse/babel/editor/i18n/actions/ShowSimilarAction.java
+++ b/org.eclipse.babel.editor/src/org/eclipse/babel/editor/i18n/actions/ShowSimilarAction.java
@@ -12,7 +12,6 @@
 
 import java.util.Locale;
 
-import org.eclipse.babel.core.message.MessagesBundleGroup;
 import org.eclipse.babel.editor.util.UIUtils;
 import org.eclipse.jface.action.Action;
 import org.eclipse.jface.dialogs.MessageDialog;
diff --git a/org.eclipse.babel.editor/src/org/eclipse/babel/editor/plugin/MessagesEditorPlugin.java b/org.eclipse.babel.editor/src/org/eclipse/babel/editor/plugin/MessagesEditorPlugin.java
index dce8283..b5b451d 100644
--- a/org.eclipse.babel.editor/src/org/eclipse/babel/editor/plugin/MessagesEditorPlugin.java
+++ b/org.eclipse.babel.editor/src/org/eclipse/babel/editor/plugin/MessagesEditorPlugin.java
@@ -30,7 +30,12 @@
 import org.eclipse.core.resources.IResourceChangeListener;
 import org.eclipse.core.resources.ResourcesPlugin;
 import org.eclipse.core.runtime.FileLocator;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.pde.nls.internal.ui.model.ResourceBundleModel;
 import org.eclipse.ui.plugin.AbstractUIPlugin;
 import org.osgi.framework.BundleContext;
 
@@ -64,6 +69,8 @@
 	//private Map<String,Set<SimpleResourceChangeListners>> resourceChangeSubscribers;
 	private Map resourceChangeSubscribers;
 	
+	private ResourceBundleModel model;
+
 	/**
 	 * The constructor
 	 */
@@ -228,4 +235,65 @@
 	protected ResourceBundle getResourceBundle() {
 		return resourceBundle;
 	}
+	
+	// Stefan's activator methods:
+	
+	/**
+	 * Returns an image descriptor for the given icon filename.
+	 * 
+	 * @param filename the icon filename relative to the icons path
+	 * @return the image descriptor
+	 */
+	public static ImageDescriptor getImageDescriptor(String filename) {
+		String iconPath = "icons/"; //$NON-NLS-1$
+		return imageDescriptorFromPlugin(PLUGIN_ID, iconPath + filename);
+	}
+
+	public static ResourceBundleModel getModel(IProgressMonitor monitor) {
+		if (plugin.model == null) {
+			plugin.model = new ResourceBundleModel(monitor);
+		}
+		return plugin.model;
+	}
+
+	public static void disposeModel() {
+		if (plugin != null) {
+			plugin.model = null;
+		}
+	}
+
+	// Logging
+
+	/**
+	 * Adds the given exception to the log.
+	 * 
+	 * @param e the exception to log
+	 * @return the logged status
+	 */
+	public static IStatus log(Throwable e) {
+		return log(new Status(IStatus.ERROR, PLUGIN_ID, 0, "Internal error.", e));
+	}
+
+	/**
+	 * Adds the given exception to the log.
+	 * 
+	 * @param exception the exception to log
+	 * @return the logged status
+	 */
+	public static IStatus log(String message, Throwable exception) {
+		return log(new Status(IStatus.ERROR, PLUGIN_ID, -1, message, exception));
+	}
+
+	/**
+	 * Adds the given <code>IStatus</code> to the log.
+	 * 
+	 * @param status the status to log
+	 * @return the logged status
+	 */
+	public static IStatus log(IStatus status) {
+		getDefault().getLog().log(status);
+		return status;
+	}
+
+	
 }
diff --git a/org.eclipse.babel.editor/src/org/eclipse/babel/editor/resource/EclipsePropertiesEditorResource.java b/org.eclipse.babel.editor/src/org/eclipse/babel/editor/resource/EclipsePropertiesEditorResource.java
index eef42e7..928dec2 100644
--- a/org.eclipse.babel.editor/src/org/eclipse/babel/editor/resource/EclipsePropertiesEditorResource.java
+++ b/org.eclipse.babel.editor/src/org/eclipse/babel/editor/resource/EclipsePropertiesEditorResource.java
@@ -12,14 +12,10 @@
 
 import java.util.Locale;
 
-import org.eclipse.babel.core.message.AbstractIFileChangeListener;
 import org.eclipse.babel.core.message.resource.AbstractPropertiesResource;
 import org.eclipse.babel.core.message.resource.ser.PropertiesDeserializer;
 import org.eclipse.babel.core.message.resource.ser.PropertiesSerializer;
 import org.eclipse.core.resources.IResource;
-import org.eclipse.core.resources.IResourceChangeEvent;
-import org.eclipse.core.resources.IResourceChangeListener;
-import org.eclipse.core.resources.ResourcesPlugin;
 import org.eclipse.jface.text.DocumentEvent;
 import org.eclipse.jface.text.IDocument;
 import org.eclipse.jface.text.IDocumentListener;
diff --git a/org.eclipse.babel.editor/src/org/eclipse/babel/editor/resource/validator/FileMarkerStrategy.java b/org.eclipse.babel.editor/src/org/eclipse/babel/editor/resource/validator/FileMarkerStrategy.java
index 98f3258..317c8a7 100644
--- a/org.eclipse.babel.editor/src/org/eclipse/babel/editor/resource/validator/FileMarkerStrategy.java
+++ b/org.eclipse.babel.editor/src/org/eclipse/babel/editor/resource/validator/FileMarkerStrategy.java
@@ -10,16 +10,14 @@
  ******************************************************************************/
 package org.eclipse.babel.editor.resource.validator;
 
-import org.eclipse.core.resources.IMarker;
-import org.eclipse.core.resources.IResource;
-import org.eclipse.core.runtime.CoreException;
-
-
 import org.eclipse.babel.core.message.checks.DuplicateValueCheck;
 import org.eclipse.babel.core.message.checks.MissingValueCheck;
 import org.eclipse.babel.core.util.BabelUtils;
 import org.eclipse.babel.editor.plugin.MessagesEditorPlugin;
 import org.eclipse.babel.editor.preferences.MsgEditorPreferences;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
 
 /**
  * @author Pascal Essiembre
diff --git a/org.eclipse.babel.editor/src/org/eclipse/babel/editor/tree/KeyTreeLabelProvider.java b/org.eclipse.babel.editor/src/org/eclipse/babel/editor/tree/KeyTreeLabelProvider.java
index 8d1d787..88f31c8 100644
--- a/org.eclipse.babel.editor/src/org/eclipse/babel/editor/tree/KeyTreeLabelProvider.java
+++ b/org.eclipse.babel.editor/src/org/eclipse/babel/editor/tree/KeyTreeLabelProvider.java
@@ -11,25 +11,18 @@
 package org.eclipse.babel.editor.tree;
 
 import java.util.Collection;
-import java.util.Iterator;
 
 import org.eclipse.babel.core.message.MessagesBundleGroup;
 import org.eclipse.babel.core.message.tree.IKeyTreeModel;
 import org.eclipse.babel.core.message.tree.KeyTreeNode;
 import org.eclipse.babel.editor.MessagesEditor;
 import org.eclipse.babel.editor.MessagesEditorMarkers;
-import org.eclipse.babel.editor.plugin.MessagesEditorPlugin;
-import org.eclipse.babel.editor.resource.validator.ValidationFailureEvent;
 import org.eclipse.babel.editor.util.OverlayImageIcon;
 import org.eclipse.babel.editor.util.UIUtils;
 import org.eclipse.jface.resource.ImageRegistry;
 import org.eclipse.jface.viewers.IColorProvider;
-import org.eclipse.jface.viewers.IDecorationContext;
 import org.eclipse.jface.viewers.IFontProvider;
-import org.eclipse.jface.viewers.ILabelDecorator;
 import org.eclipse.jface.viewers.ILabelProvider;
-import org.eclipse.jface.viewers.ILabelProviderListener;
-import org.eclipse.jface.viewers.LabelDecorator;
 import org.eclipse.jface.viewers.LabelProvider;
 import org.eclipse.swt.graphics.Color;
 import org.eclipse.swt.graphics.Font;
diff --git a/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/OpenLocalizationEditorHandler.java b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/OpenLocalizationEditorHandler.java
new file mode 100644
index 0000000..683b5b4
--- /dev/null
+++ b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/OpenLocalizationEditorHandler.java
@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.nls.internal.ui;
+
+import org.eclipse.core.commands.AbstractHandler;
+import org.eclipse.core.commands.ExecutionEvent;
+import org.eclipse.core.commands.ExecutionException;
+import org.eclipse.pde.nls.internal.ui.editor.LocalizationEditor;
+import org.eclipse.pde.nls.internal.ui.editor.LocalizationEditorInput;
+import org.eclipse.ui.IWorkbenchPage;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.PlatformUI;
+
+public class OpenLocalizationEditorHandler extends AbstractHandler {
+
+	public OpenLocalizationEditorHandler() {
+	}
+
+	/*
+	 * @see org.eclipse.core.commands.IHandler#execute(org.eclipse.core.commands.ExecutionEvent)
+	 */
+	public Object execute(ExecutionEvent event) throws ExecutionException {
+		try {
+			IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+			IWorkbenchPage page = window.getActivePage();
+			page.openEditor(new LocalizationEditorInput(), LocalizationEditor.ID);
+		} catch (PartInitException e) {
+			throw new RuntimeException(e);
+		}
+		return null;
+	}
+
+}
diff --git a/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/dialogs/ConfigureColumnsDialog.java b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/dialogs/ConfigureColumnsDialog.java
new file mode 100644
index 0000000..6318c6a
--- /dev/null
+++ b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/dialogs/ConfigureColumnsDialog.java
@@ -0,0 +1,197 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.nls.internal.ui.dialogs;
+
+import java.util.ArrayList;
+
+import org.eclipse.babel.editor.plugin.MessagesEditorPlugin;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.pde.nls.internal.ui.parser.LocaleUtil;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.swt.widgets.ToolItem;
+
+public class ConfigureColumnsDialog extends Dialog {
+
+	private class ColumnField {
+		Text text;
+		ToolItem clearButton;
+	}
+
+	private ArrayList<ColumnField> fields = new ArrayList<ColumnField>();
+
+	private ArrayList<String> result = new ArrayList<String>();
+	private String[] initialValues;
+	private Color errorColor;
+
+	private Image clearImage;
+
+	public ConfigureColumnsDialog(Shell parentShell, String[] initialValues) {
+		super(parentShell);
+		setShellStyle(getShellStyle() | SWT.RESIZE);
+		this.initialValues = initialValues;
+	}
+
+	/*
+	 * @see org.eclipse.jface.window.Window#open()
+	 */
+	@Override
+	public int open() {
+		return super.open();
+	}
+
+	/*
+	 * @see org.eclipse.jface.window.Window#configureShell(org.eclipse.swt.widgets.Shell)
+	 */
+	@Override
+	protected void configureShell(Shell newShell) {
+		super.configureShell(newShell);
+		newShell.setText("Configure Columns");
+	}
+
+	/*
+	 * @see org.eclipse.jface.dialogs.Dialog#createDialogArea(org.eclipse.swt.widgets.Composite)
+	 */
+	@Override
+	protected Control createDialogArea(Composite parent) {
+		Composite composite = (Composite) super.createDialogArea(parent);
+		GridLayout gridLayout = (GridLayout) composite.getLayout();
+		gridLayout.numColumns = 3;
+
+		Label label = new Label(composite, SWT.NONE);
+		label.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, false, false));
+		label.setText("Enter \"key\", \"default\" or locale (e.g. \"de\" or \"zh_TW\"):");
+		label.setLayoutData(GridDataFactory.fillDefaults().hint(300, SWT.DEFAULT).span(3, 1).create());
+
+		fields.add(createLanguageField(composite, "Column &1:"));
+		fields.add(createLanguageField(composite, "Column &2:"));
+		fields.add(createLanguageField(composite, "Column &3:"));
+		fields.add(createLanguageField(composite, "Column &4:"));
+
+		if (initialValues != null) {
+			for (int i = 0; i < 4 && i < initialValues.length; i++) {
+				fields.get(i).text.setText(initialValues[i]);
+			}
+		}
+		
+		ModifyListener modifyListener = new ModifyListener() {
+			public void modifyText(ModifyEvent e) {
+				validate();
+			}
+		};
+		for (ColumnField field : fields) {
+			field.text.addModifyListener(modifyListener);
+		}
+		errorColor = new Color(Display.getCurrent(), 0xff, 0x7f, 0x7f);
+
+		return composite;
+	}
+	
+	/*
+	 * @see org.eclipse.jface.dialogs.Dialog#createContents(org.eclipse.swt.widgets.Composite)
+	 */
+	@Override
+	protected Control createContents(Composite parent) {
+		Control contents = super.createContents(parent);
+		validate();
+		return contents;
+	}
+
+	private ColumnField createLanguageField(Composite parent, String labelText) {
+		Label label = new Label(parent, SWT.NONE);
+		label.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, false, false));
+		label.setText(labelText);
+
+		Text text = new Text(parent, SWT.SINGLE | SWT.LEAD | SWT.BORDER);
+		text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+
+		if (clearImage == null)
+			clearImage = MessagesEditorPlugin.getImageDescriptor("elcl16/clear_co.gif").createImage(); //$NON-NLS-1$
+		
+		ToolBar toolbar = new ToolBar(parent, SWT.FLAT);
+		ToolItem item = new ToolItem(toolbar, SWT.PUSH);
+		item.setImage(clearImage);
+		item.setToolTipText("Clear");
+		item.addSelectionListener(new SelectionAdapter() {
+			@Override
+			public void widgetSelected(SelectionEvent e) {
+				for (ColumnField field : fields) {
+					if (field.clearButton == e.widget) {
+						field.text.setText(""); //$NON-NLS-1$
+					}
+				}
+			}
+		});
+		
+		ColumnField field = new ColumnField();
+		field.text = text;
+		field.clearButton = item;
+		return field;
+	}
+
+	/*
+	 * @see org.eclipse.jface.dialogs.Dialog#okPressed()
+	 */
+	@Override
+	protected void okPressed() {
+		for (ColumnField field : fields) {
+			String text = field.text.getText().trim();
+			if (text.length() > 0) {
+				result.add(text);
+			}
+		}
+		super.okPressed();
+		errorColor.dispose();
+		clearImage.dispose();
+	}
+
+	public String[] getResult() {
+		return result.toArray(new String[result.size()]);
+	}
+	
+	protected void validate() {
+		boolean isValid = true;
+		for (ColumnField field : fields) {
+			String text = field.text.getText();
+			if (text.equals("") || text.equals("key") || text.equals("default")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+				field.text.setBackground(null);
+			} else {
+				try {
+					LocaleUtil.parseLocale(text);
+					field.text.setBackground(null);
+				} catch (IllegalArgumentException e) {
+					field.text.setBackground(errorColor);
+					isValid = false;
+				}
+			}
+		}
+		Button okButton = getButton(IDialogConstants.OK_ID);
+		okButton.setEnabled(isValid);
+	}
+
+}
diff --git a/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/dialogs/EditMultiLineEntryDialog.java b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/dialogs/EditMultiLineEntryDialog.java
new file mode 100644
index 0000000..62947de
--- /dev/null
+++ b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/dialogs/EditMultiLineEntryDialog.java
@@ -0,0 +1,73 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.nls.internal.ui.dialogs;
+
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+public class EditMultiLineEntryDialog extends Dialog {
+
+	private Text textWidget;
+	private String text;
+	private boolean readOnly;
+	
+	protected EditMultiLineEntryDialog(Shell parentShell, String initialInput, boolean readOnly) {
+		super(parentShell);
+		this.readOnly = readOnly;
+		setShellStyle(getShellStyle() | SWT.RESIZE);
+		text = initialInput;
+	}
+	
+	/*
+	 * @see org.eclipse.jface.window.Window#configureShell(org.eclipse.swt.widgets.Shell)
+	 */
+	@Override
+	protected void configureShell(Shell newShell) {
+		super.configureShell(newShell);
+		newShell.setText("Edit Resource Bundle Entry");
+	}
+
+	public String getValue() {
+		return text;
+	}
+	
+	/*
+	 * @see org.eclipse.jface.dialogs.Dialog#createDialogArea(org.eclipse.swt.widgets.Composite)
+	 */
+	@Override
+	protected Control createDialogArea(Composite parent) {
+		Composite composite = (Composite) super.createDialogArea(parent);
+		
+		int readOnly = this.readOnly ? SWT.READ_ONLY : 0;
+		Text text = new Text(composite, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL | SWT.BORDER | readOnly);
+		text.setLayoutData(GridDataFactory.fillDefaults().grab(true, true).hint(350, 150).create());
+		text.setText(text == null ? "" : this.text);
+		
+		textWidget = text;
+		
+		return composite;
+	}
+	
+	/*
+	 * @see org.eclipse.jface.dialogs.Dialog#okPressed()
+	 */
+	@Override
+	protected void okPressed() {
+		text = textWidget.getText();
+		super.okPressed();
+	}
+
+}
diff --git a/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/dialogs/EditResourceBundleEntriesDialog.java b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/dialogs/EditResourceBundleEntriesDialog.java
new file mode 100644
index 0000000..cec1e05
--- /dev/null
+++ b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/dialogs/EditResourceBundleEntriesDialog.java
@@ -0,0 +1,391 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.nls.internal.ui.dialogs;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Locale;
+
+import org.eclipse.babel.editor.plugin.MessagesEditorPlugin;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.dialogs.IDialogSettings;
+import org.eclipse.jface.layout.GridDataFactory;
+import org.eclipse.jface.window.Window;
+import org.eclipse.pde.nls.internal.ui.model.ResourceBundle;
+import org.eclipse.pde.nls.internal.ui.model.ResourceBundleKey;
+import org.eclipse.pde.nls.internal.ui.parser.RawBundle;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+public class EditResourceBundleEntriesDialog extends Dialog {
+
+	private class LocaleField {
+		ResourceBundle bundle;
+		Label label;
+		Text text;
+		Locale locale;
+		String oldValue;
+		boolean isReadOnly;
+		Button button;
+	}
+
+	private ResourceBundleKey resourceBundleKey;
+	protected ArrayList<LocaleField> fields = new ArrayList<LocaleField>();
+	private final Locale[] locales;
+	private Color errorColor;
+
+	/**
+	 * @param locales the locales to edit 
+	 */
+	public EditResourceBundleEntriesDialog(Shell parentShell, Locale[] locales) {
+		super(parentShell);
+		this.locales = locales;
+		setShellStyle(getShellStyle() | SWT.RESIZE);
+	}
+
+	public void setResourceBundleKey(ResourceBundleKey resourceBundleKey) {
+		this.resourceBundleKey = resourceBundleKey;
+	}
+
+	/*
+	 * @see org.eclipse.jface.window.Window#open()
+	 */
+	@Override
+	public int open() {
+		if (resourceBundleKey == null)
+			throw new RuntimeException("Resource bundle key not set.");
+		return super.open();
+	}
+
+	/*
+	 * @see org.eclipse.jface.window.Window#configureShell(org.eclipse.swt.widgets.Shell)
+	 */
+	@Override
+	protected void configureShell(Shell newShell) {
+		super.configureShell(newShell);
+		newShell.setText("Edit Resource Bundle Entries");
+	}
+
+	/*
+	 * @see org.eclipse.jface.dialogs.Dialog#createDialogArea(org.eclipse.swt.widgets.Composite)
+	 */
+	@Override
+	protected Control createDialogArea(Composite parent) {
+		Composite composite = (Composite) super.createDialogArea(parent);
+		GridLayout gridLayout = (GridLayout) composite.getLayout();
+		gridLayout.numColumns = 3;
+
+		Label keyLabel = new Label(composite, SWT.NONE);
+		keyLabel.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, false, false));
+		keyLabel.setText("&Key:");
+
+		int style = SWT.SINGLE | SWT.LEAD | SWT.BORDER | SWT.READ_ONLY;
+		Text keyText = new Text(composite, style);
+		keyText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+		keyText.setText(resourceBundleKey.getName());
+
+		new Label(composite, SWT.NONE); // spacer
+
+		for (Locale locale : locales) {
+			if (locale.getLanguage().equals("")) { //$NON-NLS-1$
+				fields.add(createLocaleField(composite, locale, "&Default Bundle:"));
+			} else {
+				fields.add(createLocaleField(composite, locale, "&" + locale.getDisplayName() + ":"));
+			}
+		}
+
+		// Set focus on first editable field
+		if (fields.size() > 0) {
+			for (int i = 0; i < fields.size(); i++) {
+				if (fields.get(i).text.getEditable()) {
+					fields.get(i).text.setFocus();
+					break;
+				}
+			}
+		}
+
+		Label label = new Label(composite, SWT.NONE);
+		label.setLayoutData(GridDataFactory.fillDefaults().span(3, 1).create());
+		label.setText("Note: The following escape sequences are allowed: \\r, \\n, \\t, \\\\");
+
+		ModifyListener modifyListener = new ModifyListener() {
+			public void modifyText(ModifyEvent e) {
+				validate();
+			}
+		};
+		for (LocaleField field : fields) {
+			field.text.addModifyListener(modifyListener);
+		}
+		errorColor = new Color(Display.getCurrent(), 0xff, 0x7f, 0x7f);
+		
+		return composite;
+	}
+
+	private LocaleField createLocaleField(Composite parent, Locale locale, String localeLabel) {
+		ResourceBundle bundle = resourceBundleKey.getFamily().getBundle(locale);
+
+		Label label = new Label(parent, SWT.NONE);
+		label.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, false, false));
+		label.setText(localeLabel);
+
+		boolean readOnly = bundle == null || bundle.isReadOnly();
+		int style = SWT.SINGLE | SWT.LEAD | SWT.BORDER | (readOnly ? SWT.READ_ONLY : 0);
+		Text text = new Text(parent, style);
+		text.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
+
+		String value = null;
+		if (bundle != null) {
+			try {
+				value = bundle.getString(resourceBundleKey.getName());
+			} catch (CoreException e) {
+				MessagesEditorPlugin.log(e);
+			}
+			if (value == null) {
+				if (readOnly) {
+					value = "(Key does not exist)";
+				} else {
+					value = ""; // TODO Indicate that the entry is missing: perhaps red background
+				}
+			}
+			text.setText(escape(value));
+		} else {
+			text.setText("(Resource bundle not found)");
+		}
+
+		Button button = new Button(parent, SWT.PUSH);
+		button.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, false, false));
+		button.setText("..."); //$NON-NLS-1$
+		button.addSelectionListener(new SelectionAdapter() {
+			@Override
+			public void widgetSelected(SelectionEvent e) {
+				for (LocaleField field : fields) {
+					if (e.widget == field.button) {
+						EditMultiLineEntryDialog dialog = new EditMultiLineEntryDialog(
+							getShell(),
+							unescape(field.text.getText()),
+							field.isReadOnly);
+						if (dialog.open() == Window.OK) {
+							field.text.setText(escape(dialog.getValue()));
+						}
+					}
+				}
+			}
+		});
+
+		LocaleField field = new LocaleField();
+		field.bundle = bundle;
+		field.label = label;
+		field.text = text;
+		field.locale = locale;
+		field.oldValue = value;
+		field.isReadOnly = readOnly;
+		field.button = button;
+		return field;
+	}
+
+	/*
+	 * @see org.eclipse.jface.dialogs.Dialog#okPressed()
+	 */
+	@Override
+	protected void okPressed() {
+		for (LocaleField field : fields) {
+			if (field.isReadOnly)
+				continue;
+			String key = resourceBundleKey.getName();
+			String value = unescape(field.text.getText());
+			boolean hasChanged = (field.oldValue == null && !value.equals("")) //$NON-NLS-1$
+					|| (field.oldValue != null && !field.oldValue.equals(value));
+			if (hasChanged) {
+				try {
+					Object resource = field.bundle.getUnderlyingResource();
+					InputStream inputStream;
+					if (resource instanceof IFile) {
+						IFile file = (IFile) resource;
+						inputStream = file.getContents();
+						RawBundle rawBundle;
+						try {
+							rawBundle = RawBundle.createFrom(inputStream);
+							rawBundle.put(key, value);
+						} catch (Exception e) {
+							openError("Value could not be saved: " + value, e);
+							return;
+						}
+						StringWriter stringWriter = new StringWriter();
+						rawBundle.writeTo(stringWriter);
+						byte[] bytes = stringWriter.toString().getBytes("ISO-8859-1"); //$NON-NLS-1$
+						ByteArrayInputStream newContents = new ByteArrayInputStream(bytes);
+						file.setContents(newContents, false, false, new NullProgressMonitor());
+					} else {
+						// Unexpected type of resource
+						throw new RuntimeException("Not yet implemented."); //$NON-NLS-1$
+					}
+					field.bundle.put(key, value);
+				} catch (Exception e) {
+					openError("Value could not be saved: " + value, e);
+					return;
+				}
+			}
+		}
+		super.okPressed();
+		errorColor.dispose();
+	}
+
+	/*
+	 * @see org.eclipse.jface.dialogs.Dialog#getDialogBoundsStrategy()
+	 */
+	@Override
+	protected int getDialogBoundsStrategy() {
+		return DIALOG_PERSISTLOCATION | DIALOG_PERSISTSIZE;
+	}
+
+	/*
+	 * @see org.eclipse.jface.dialogs.Dialog#getDialogBoundsSettings()
+	 */
+	@Override
+	protected IDialogSettings getDialogBoundsSettings() {
+		IDialogSettings settings = MessagesEditorPlugin.getDefault().getDialogSettings();
+		String sectionName = "EditResourceBundleEntriesDialog"; //$NON-NLS-1$
+		IDialogSettings section = settings.getSection(sectionName);
+		if (section == null)
+			section = settings.addNewSection(sectionName);
+		return section;
+	}
+	
+	/*
+	 * @see org.eclipse.jface.dialogs.Dialog#getInitialSize()
+	 */
+	@Override
+	protected Point getInitialSize() {
+		Point initialSize = super.getInitialSize();
+		// Make sure that all locales are visible
+		Point size = getShell().computeSize(SWT.DEFAULT, SWT.DEFAULT, true);
+		if (initialSize.y < size.y)
+			initialSize.y = size.y;
+		return initialSize;
+	}
+
+	protected void validate() {
+		boolean isValid = true;
+		for (LocaleField field : fields) {
+			try {
+				unescape(field.text.getText());
+				field.text.setBackground(null);
+			} catch (IllegalArgumentException e) {
+				field.text.setBackground(errorColor);
+				isValid = false;
+			}
+		}
+		Button okButton = getButton(IDialogConstants.OK_ID);
+		okButton.setEnabled(isValid);
+	}
+	
+	private void openError(String message, Exception e) {
+		IStatus status;
+		if (e instanceof CoreException) {
+			CoreException coreException = (CoreException) e;
+			status = coreException.getStatus();
+		} else {
+			status = new Status(IStatus.ERROR, "<dummy>", e.getMessage(), e); //$NON-NLS-1$
+		}
+		e.printStackTrace();
+		ErrorDialog.openError(getParentShell(), "Error", message, status);
+	}
+
+	/**
+	 * Escapes line separators, tabulators and double backslashes. 
+	 * 
+	 * @param str
+	 * @return the escaped string
+	 */
+	public static String escape(String str) {
+		StringBuilder builder = new StringBuilder(str.length() + 10);
+		for (int i = 0; i < str.length(); i++) {
+			char c = str.charAt(i);
+			switch (c) {
+				case '\r' :
+					builder.append("\\r"); //$NON-NLS-1$
+					break;
+				case '\n' :
+					builder.append("\\n"); //$NON-NLS-1$
+					break;
+				case '\t' :
+					builder.append("\\t"); //$NON-NLS-1$
+					break;
+				case '\\' :
+					builder.append("\\\\"); //$NON-NLS-1$
+					break;
+				default :
+					builder.append(c);
+					break;
+			}
+		}
+		return builder.toString();
+	}
+
+	/**
+	 * Unescapes line separators, tabulators and double backslashes. 
+	 * 
+	 * @param str
+	 * @return the unescaped string
+	 * @throws IllegalArgumentException when an invalid or unexpected escape is encountered
+	 */
+	public static String unescape(String str) {
+		StringBuilder builder = new StringBuilder(str.length() + 10);
+		for (int i = 0; i < str.length(); i++) {
+			char c = str.charAt(i);
+			if (c == '\\') {
+				switch (str.charAt(i + 1)) {
+					case 'r' :
+						builder.append('\r');
+						break;
+					case 'n' :
+						builder.append('\n');
+						break;
+					case 't' :
+						builder.append('\t');
+						break;
+					case '\\' :
+						builder.append('\\');
+						break;
+					default :
+						throw new IllegalArgumentException("Invalid escape sequence.");
+				}
+				i++;
+			} else {
+				builder.append(c);
+			}
+		}
+		return builder.toString();
+	}
+}
diff --git a/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/dialogs/FilterOptions.java b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/dialogs/FilterOptions.java
new file mode 100644
index 0000000..7b6ff5b
--- /dev/null
+++ b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/dialogs/FilterOptions.java
@@ -0,0 +1,19 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.nls.internal.ui.dialogs;
+
+public class FilterOptions {
+
+	public boolean filterPlugins; 
+	public String[] pluginPatterns; 
+	public boolean keysWithMissingEntriesOnly; 
+
+}
diff --git a/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/dialogs/FilterOptionsDialog.java b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/dialogs/FilterOptionsDialog.java
new file mode 100644
index 0000000..ae4e038
--- /dev/null
+++ b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/dialogs/FilterOptionsDialog.java
@@ -0,0 +1,112 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.nls.internal.ui.dialogs;
+
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Text;
+
+public class FilterOptionsDialog extends Dialog {
+
+	private static final String SEPARATOR = ","; //$NON-NLS-1$
+
+	private Button enablePluginPatterns;
+	private Button keysWithMissingEntriesOnly;
+	private Text pluginPatterns;
+	
+	private FilterOptions initialOptions;
+	private FilterOptions result;
+
+	public FilterOptionsDialog(Shell shell) {
+		super(shell);
+	}
+
+	public void setInitialFilterOptions(FilterOptions initialfilterOptions) {
+		this.initialOptions = initialfilterOptions;
+	}
+
+	/*
+	 * @see org.eclipse.ui.dialogs.SelectionDialog#configureShell(org.eclipse.swt.widgets.Shell)
+	 */
+	protected void configureShell(Shell shell) {
+		super.configureShell(shell);
+		shell.setText("Filters");
+	}
+
+	/*
+	 * @see org.eclipse.jface.dialogs.Dialog#createDialogArea(org.eclipse.swt.widgets.Composite)
+	 */
+	protected Control createDialogArea(Composite parent) {
+		Composite composite = (Composite) super.createDialogArea(parent);
+
+		// Checkbox
+		enablePluginPatterns = new Button(composite, SWT.CHECK);
+		enablePluginPatterns.setFocus();
+		enablePluginPatterns.setText("Filter by plug-in id patterns (separated by comma):");
+		enablePluginPatterns.addSelectionListener(new SelectionAdapter() {
+			public void widgetSelected(SelectionEvent e) {
+				pluginPatterns.setEnabled(enablePluginPatterns.getSelection());
+			}
+		});
+		enablePluginPatterns.setSelection(initialOptions.filterPlugins);
+
+		// Pattern	field
+		pluginPatterns = new Text(composite, SWT.SINGLE | SWT.BORDER);
+		GridData data = new GridData(GridData.HORIZONTAL_ALIGN_FILL | GridData.GRAB_HORIZONTAL);
+		data.widthHint = convertWidthInCharsToPixels(59);
+		pluginPatterns.setLayoutData(data);
+		pluginPatterns.setEnabled(initialOptions.filterPlugins);
+		if (initialOptions.pluginPatterns != null) {
+			StringBuilder builder = new StringBuilder();
+			String[] patterns = initialOptions.pluginPatterns;
+			for (String pattern : patterns) {
+				if (builder.length() > 0) {
+					builder.append(SEPARATOR);
+					builder.append(" ");
+				}
+				builder.append(pattern);
+			}
+			pluginPatterns.setText(builder.toString());
+		}
+		
+		keysWithMissingEntriesOnly = new Button(composite, SWT.CHECK);
+		keysWithMissingEntriesOnly.setText("Keys with missing entries only");
+		keysWithMissingEntriesOnly.setSelection(initialOptions.keysWithMissingEntriesOnly);
+
+		applyDialogFont(parent);
+		return parent;
+	}
+
+	protected void okPressed() {
+		String patterns = pluginPatterns.getText();
+		result = new FilterOptions();
+		result.filterPlugins = enablePluginPatterns.getSelection();
+		String[] split = patterns.split(SEPARATOR);
+		for (int i = 0; i < split.length; i++) {
+			split[i] = split[i].trim();
+		}
+		result.pluginPatterns = split;
+		result.keysWithMissingEntriesOnly = keysWithMissingEntriesOnly.getSelection();
+		super.okPressed();
+	}
+	
+	public FilterOptions getResult() {
+		return result;
+	}
+	
+}
diff --git a/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/editor/LocalizationEditor.java b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/editor/LocalizationEditor.java
new file mode 100644
index 0000000..e0f5ef6
--- /dev/null
+++ b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/editor/LocalizationEditor.java
@@ -0,0 +1,1088 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.nls.internal.ui.editor;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+
+import org.eclipse.babel.editor.plugin.MessagesEditorPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.ISchedulingRule;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.IContributionItem;
+import org.eclipse.jface.action.IMenuListener;
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.action.IToolBarManager;
+import org.eclipse.jface.action.MenuManager;
+import org.eclipse.jface.action.Separator;
+import org.eclipse.jface.action.ToolBarManager;
+import org.eclipse.jface.dialogs.ErrorDialog;
+import org.eclipse.jface.dialogs.IDialogSettings;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.layout.TableColumnLayout;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.ColumnWeightData;
+import org.eclipse.jface.viewers.DoubleClickEvent;
+import org.eclipse.jface.viewers.IDoubleClickListener;
+import org.eclipse.jface.viewers.ILazyContentProvider;
+import org.eclipse.jface.viewers.IStructuredSelection;
+import org.eclipse.jface.viewers.StructuredSelection;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.Viewer;
+import org.eclipse.jface.window.Window;
+import org.eclipse.pde.nls.internal.ui.dialogs.ConfigureColumnsDialog;
+import org.eclipse.pde.nls.internal.ui.dialogs.EditResourceBundleEntriesDialog;
+import org.eclipse.pde.nls.internal.ui.dialogs.FilterOptions;
+import org.eclipse.pde.nls.internal.ui.dialogs.FilterOptionsDialog;
+import org.eclipse.pde.nls.internal.ui.model.ResourceBundle;
+import org.eclipse.pde.nls.internal.ui.model.ResourceBundleFamily;
+import org.eclipse.pde.nls.internal.ui.model.ResourceBundleKey;
+import org.eclipse.pde.nls.internal.ui.model.ResourceBundleKeyList;
+import org.eclipse.pde.nls.internal.ui.model.ResourceBundleModel;
+import org.eclipse.pde.nls.internal.ui.parser.LocaleUtil;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.BusyIndicator;
+import org.eclipse.swt.events.KeyAdapter;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Menu;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Table;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.swt.widgets.ToolBar;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.IPropertyListener;
+import org.eclipse.ui.IWorkbenchWindow;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.actions.ContributionItemFactory;
+import org.eclipse.ui.forms.widgets.ExpandableComposite;
+import org.eclipse.ui.forms.widgets.Form;
+import org.eclipse.ui.forms.widgets.FormToolkit;
+import org.eclipse.ui.forms.widgets.Section;
+import org.eclipse.ui.internal.misc.StringMatcher;
+import org.eclipse.ui.part.EditorPart;
+import org.eclipse.ui.part.IShowInSource;
+import org.eclipse.ui.part.ShowInContext;
+
+// TODO Fix restriction and remove warning 
+@SuppressWarnings("restriction")
+public class LocalizationEditor extends EditorPart {
+
+	private final class LocalizationLabelProvider extends ColumnLabelProvider {
+
+		private final Object columnConfig;
+
+		public LocalizationLabelProvider(Object columnConfig) {
+			this.columnConfig = columnConfig;
+		}
+
+		public String getText(Object element) {
+			ResourceBundleKey key = (ResourceBundleKey) element;
+			if (columnConfig == KEY)
+				return key.getName();
+			Locale locale = (Locale) columnConfig;
+			String value;
+			try {
+				value = key.getValue(locale);
+			} catch (CoreException e) {
+				value = null;
+				MessagesEditorPlugin.log(e);
+			}
+			if (value == null)
+				value = "";
+			return value;
+		}
+	}
+
+	private class EditEntryAction extends Action {
+		public EditEntryAction() {
+			super("&Edit", IAction.AS_PUSH_BUTTON);
+		}
+
+		/*
+		 * @see org.eclipse.jface.action.Action#run()
+		 */
+		public void run() {
+			ResourceBundleKey key = getSelectedEntry();
+			if (key == null) {
+				return;
+			}
+			Shell shell = Display.getCurrent().getActiveShell();
+			Locale[] locales = getLocales();
+			EditResourceBundleEntriesDialog dialog = new EditResourceBundleEntriesDialog(shell, locales);
+			dialog.setResourceBundleKey(key);
+			if (dialog.open() == Window.OK) {
+				updateLabels();
+			}
+		}
+	}
+
+	private class ConfigureColumnsAction extends Action {
+		public ConfigureColumnsAction() {
+			super(null, IAction.AS_PUSH_BUTTON); //$NON-NLS-1$
+			setImageDescriptor(MessagesEditorPlugin.getImageDescriptor("elcl16/conf_columns.gif"));
+			setToolTipText("Configure Columns");
+		}
+
+		/*
+		 * @see org.eclipse.jface.action.Action#run()
+		 */
+		public void run() {
+			Shell shell = Display.getCurrent().getActiveShell();
+			String[] values = new String[columnConfigs.length];
+			for (int i = 0; i < columnConfigs.length; i++) {
+				String config = columnConfigs[i].toString();
+				if (config.equals("")) //$NON-NLS-1$
+					config = "default"; //$NON-NLS-1$
+				values[i] = config;
+			}
+			ConfigureColumnsDialog dialog = new ConfigureColumnsDialog(shell, values);
+			if (dialog.open() == Window.OK) {
+				String[] result = dialog.getResult();
+				Object[] newConfigs = new Object[result.length];
+				for (int i = 0; i < newConfigs.length; i++) {
+					if (result[i].equals("key")) { //$NON-NLS-1$
+						newConfigs[i] = KEY;
+					} else if (result[i].equals("default")) { //$NON-NLS-1$
+						newConfigs[i] = new Locale("");
+					} else {
+						newConfigs[i] = LocaleUtil.parseLocale(result[i]);
+					}
+				}
+				setColumns(newConfigs);
+			}
+		}
+	}
+
+	private class EditFilterOptionsAction extends Action {
+		public EditFilterOptionsAction() {
+			super(null, IAction.AS_PUSH_BUTTON); //$NON-NLS-1$
+			setImageDescriptor(MessagesEditorPlugin.getImageDescriptor("elcl16/filter_obj.gif"));
+			setToolTipText("Edit Filter Options");
+		}
+
+		/*
+		 * @see org.eclipse.jface.action.Action#run()
+		 */
+		public void run() {
+			Shell shell = Display.getCurrent().getActiveShell();
+			FilterOptionsDialog dialog = new FilterOptionsDialog(shell);
+			dialog.setInitialFilterOptions(filterOptions);
+			if (dialog.open() == Window.OK) {
+				filterOptions = dialog.getResult();
+				refresh();
+				updateFilterLabel();
+			}
+		}
+	}
+
+	private class RefreshAction extends Action {
+		public RefreshAction() {
+			super(null, IAction.AS_PUSH_BUTTON); //$NON-NLS-1$
+			setImageDescriptor(MessagesEditorPlugin.getImageDescriptor("elcl16/refresh.gif"));
+			setToolTipText("Refresh");
+		}
+
+		/*
+		 * @see org.eclipse.jface.action.Action#run()
+		 */
+		public void run() {
+			MessagesEditorPlugin.disposeModel();
+			entryList = new ResourceBundleKeyList(new ResourceBundleKey[0]);
+			tableViewer.getTable().setItemCount(0);
+			updateLabels();
+			refresh();
+		}
+	}
+
+	private class BundleStringComparator implements Comparator<ResourceBundleKey> {
+		private final Locale locale;
+		public BundleStringComparator(Locale locale) {
+			this.locale = locale;
+		}
+		public int compare(ResourceBundleKey o1, ResourceBundleKey o2) {
+			String value1 = null;
+			String value2 = null;
+			try {
+				value1 = o1.getValue(locale);
+			} catch (CoreException e) {
+				MessagesEditorPlugin.log(e);
+			}
+			try {
+				value2 = o2.getValue(locale);
+			} catch (CoreException e) {
+				MessagesEditorPlugin.log(e);
+			}
+			if (value1 == null)
+				value1 = ""; //$NON-NLS-1$
+			if (value2 == null)
+				value2 = ""; //$NON-NLS-1$
+			return value1.compareToIgnoreCase(value2);
+		}
+	}
+
+	private class ExportAction extends Action {
+		public ExportAction() {
+			super(null, IAction.AS_PUSH_BUTTON); //$NON-NLS-1$
+			setImageDescriptor(MessagesEditorPlugin.getImageDescriptor("elcl16/export.gif"));
+			setToolTipText("Export Current View to CSV or HTML File");
+		}
+
+		/*
+		 * @see org.eclipse.jface.action.Action#run()
+		 */
+		public void run() {
+			Shell shell = Display.getCurrent().getActiveShell();
+			FileDialog dialog = new FileDialog(shell);
+			dialog.setText("Export File");
+			dialog.setFilterExtensions(new String[] {"*.*", "*.htm; *.html", "*.txt; *.csv"});
+			dialog.setFilterNames(new String[] {"All Files (*.*)", "HTML File (*.htm; *.html)",
+					"Tabulator Separated File (*.txt; *.csv)"});
+			final String filename = dialog.open();
+			if (filename != null) {
+				BusyIndicator.showWhile(Display.getCurrent(), new Runnable() {
+					public void run() {
+						File file = new File(filename);
+						try {
+							BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
+								new FileOutputStream(file),
+								"UTF8")); //$NON-NLS-1$
+							boolean isHtml = filename.endsWith(".htm") || filename.endsWith(".html"); //$NON-NLS-1$ //$NON-NLS-2$
+							if (isHtml) {
+								writer.write("" //$NON-NLS-1$
+										+ "<html>\r\n" //$NON-NLS-1$
+										+ "<head>\r\n" //$NON-NLS-1$
+										+ "<meta http-equiv=Content-Type content=\"text/html; charset=UTF-8\">\r\n" //$NON-NLS-1$
+										+ "<style>\r\n" //$NON-NLS-1$
+										+ "table {width:100%;}\r\n" //$NON-NLS-1$
+										+ "td.sep {height:10px;background:#C0C0C0;}\r\n" //$NON-NLS-1$
+										+ "</style>\r\n" //$NON-NLS-1$
+										+ "</head>\r\n" //$NON-NLS-1$
+										+ "<body>\r\n" //$NON-NLS-1$
+										+ "<table width=\"100%\" border=\"1\">\r\n"); //$NON-NLS-1$
+							}
+
+							int size = entryList.getSize();
+							Object[] configs = LocalizationEditor.this.columnConfigs;
+							int valueCount = 0;
+							int missingCount = 0;
+							for (int i = 0; i < size; i++) {
+								ResourceBundleKey key = entryList.getKey(i);
+								if (isHtml) {
+									writer.write("<table border=\"1\">\r\n"); //$NON-NLS-1$
+								}
+								for (int j = 0; j < configs.length; j++) {
+									if (isHtml) {
+										writer.write("<tr><td>"); //$NON-NLS-1$
+									}
+									Object config = configs[j];
+									if (!isHtml && j > 0)
+										writer.write("\t"); //$NON-NLS-1$
+									if (config == KEY) {
+										writer.write(key.getName());
+									} else {
+										Locale locale = (Locale) config;
+										String value;
+										try {
+											value = key.getValue(locale);
+										} catch (CoreException e) {
+											value = null;
+											MessagesEditorPlugin.log(e);
+										}
+										if (value == null) {
+											value = ""; //$NON-NLS-1$
+											missingCount++;
+										} else {
+											valueCount++;
+										}
+										writer.write(EditResourceBundleEntriesDialog.escape(value));
+									}
+									if (isHtml) {
+										writer.write("</td></tr>\r\n"); //$NON-NLS-1$
+									}
+								}
+								if (isHtml) {
+									writer.write("<tr><td class=\"sep\">&nbsp;</td></tr>\r\n"); //$NON-NLS-1$
+									writer.write("</table>\r\n"); //$NON-NLS-1$
+								} else {
+									writer.write("\r\n"); //$NON-NLS-1$
+								}
+							}
+							if (isHtml) {
+								writer.write("</body>\r\n" + "</html>\r\n"); //$NON-NLS-1$ //$NON-NLS-2$
+							}
+							writer.close();
+							Shell shell = Display.getCurrent().getActiveShell();
+							MessageDialog.openInformation(
+									shell,
+									"Finished",
+									"File written successfully.\n\nNumber of entries written: "
+											+ entryList.getSize()
+											+ "\nNumber of translations: "
+											+ valueCount
+											+ " ("
+											+ missingCount
+											+ " missing)");
+						} catch (IOException e) {
+							Shell shell = Display.getCurrent().getActiveShell();
+							ErrorDialog.openError(shell, "Error", "Error saving file.", new Status(
+								IStatus.ERROR,
+								MessagesEditorPlugin.PLUGIN_ID,
+								e.getMessage(),
+								e));
+						}
+					}
+				});
+			}
+		}
+	}
+
+	public static final String ID = "org.eclipse.pde.nls.ui.LocalizationEditor"; //$NON-NLS-1$
+
+	protected static final Object KEY = "key"; // used to indicate the key column
+
+	private static final String PREF_SECTION_NAME = "org.eclipse.pde.nls.ui.LocalizationEditor"; //$NON-NLS-1$
+	private static final String PREF_SORT_ORDER = "sortOrder"; //$NON-NLS-1$
+	private static final String PREF_COLUMNS = "columns"; //$NON-NLS-1$
+	private static final String PREF_FILTER_OPTIONS_FILTER_PLUGINS = "filterOptions_filterPlugins"; //$NON-NLS-1$
+	private static final String PREF_FILTER_OPTIONS_PLUGIN_PATTERNS = "filterOptions_pluginPatterns"; //$NON-NLS-1$
+	private static final String PREF_FILTER_OPTIONS_MISSING_ONLY = "filterOptions_missingOnly"; //$NON-NLS-1$
+
+	// Actions
+	private EditFilterOptionsAction editFiltersAction;
+	private ConfigureColumnsAction selectLanguagesAction;
+	private RefreshAction refreshAction;
+	private ExportAction exportAction;
+
+	// Form
+	protected FormToolkit toolkit = new FormToolkit(Display.getCurrent());
+	private Form form;
+	private Image formImage;
+
+	// Query
+	protected Composite queryComposite;
+	protected Text queryText;
+
+	// Results 
+	private Section resultsSection;
+	private Composite tableComposite;
+	protected TableViewer tableViewer;
+	protected Table table;
+	protected ArrayList<TableColumn> columns = new ArrayList<TableColumn>();
+
+	// Data and configuration
+	protected LocalizationEditorInput input;
+	protected ResourceBundleKeyList entryList;
+	protected FilterOptions filterOptions;
+
+	/**
+	 * Column configuration. Values may be either <code>KEY</code> or a {@link Locale}.
+	 */
+	protected Object[] columnConfigs;
+	/**
+	 * Either <code>KEY</code> or a {@link Locale}.
+	 */
+	protected Object sortOrder;
+
+	private String lastQuery = "";
+	protected Job searchJob;
+
+	private ISchedulingRule mutexRule = new ISchedulingRule() {
+		public boolean contains(ISchedulingRule rule) {
+			return rule == this;
+		}
+		public boolean isConflicting(ISchedulingRule rule) {
+			return rule == this;
+		}
+	};
+
+	private Label filteredLabel;
+
+	public LocalizationEditor() {
+	}
+
+	public ResourceBundleKey getSelectedEntry() {
+		IStructuredSelection selection = (IStructuredSelection) tableViewer.getSelection();
+		if (selection.size() == 1) {
+			return (ResourceBundleKey) selection.getFirstElement();
+		}
+		return null;
+	}
+
+	public String getQueryText() {
+		return queryText.getText();
+	}
+
+	@Override
+	public void createPartControl(Composite parent) {
+		GridData gd;
+		GridLayout gridLayout;
+
+		form = toolkit.createForm(parent);
+		form.setSeparatorVisible(true);
+		form.setText("Localization");
+
+		form.setImage(formImage = MessagesEditorPlugin.getImageDescriptor("obj16/nls_editor.gif").createImage()); //$NON-NLS-1$
+		toolkit.adapt(form);
+		toolkit.paintBordersFor(form);
+		final Composite body = form.getBody();
+		gridLayout = new GridLayout();
+		gridLayout.numColumns = 1;
+		body.setLayout(gridLayout);
+		toolkit.paintBordersFor(body);
+		toolkit.decorateFormHeading(form);
+
+		queryComposite = toolkit.createComposite(body);
+		gd = new GridData(SWT.FILL, SWT.CENTER, true, false);
+		queryComposite.setLayoutData(gd);
+		gridLayout = new GridLayout(5, false);
+		gridLayout.marginHeight = 0;
+		queryComposite.setLayout(gridLayout);
+		toolkit.paintBordersFor(queryComposite);
+
+		// Form toolbar
+		editFiltersAction = new EditFilterOptionsAction();
+		selectLanguagesAction = new ConfigureColumnsAction();
+		refreshAction = new RefreshAction();
+		exportAction = new ExportAction();
+		IToolBarManager toolBarManager = form.getToolBarManager();
+		toolBarManager.add(refreshAction);
+		toolBarManager.add(editFiltersAction);
+		toolBarManager.add(selectLanguagesAction);
+		toolBarManager.add(exportAction);
+		form.updateToolBar();
+
+		toolkit.createLabel(queryComposite, "Search:");
+
+		// Query text
+		queryText = toolkit.createText(queryComposite, "", SWT.WRAP | SWT.SINGLE); //$NON-NLS-1$
+		queryText.addKeyListener(new KeyAdapter() {
+			public void keyPressed(KeyEvent e) {
+				if (e.keyCode == SWT.ARROW_DOWN) {
+					table.setFocus();
+				} else if (e.keyCode == SWT.ESC) {
+					queryText.setText(""); //$NON-NLS-1$
+				}
+			}
+		});
+		queryText.addModifyListener(new ModifyListener() {
+			public void modifyText(ModifyEvent e) {
+				executeQuery();
+
+				Object[] listeners = getListeners();
+				for (int i = 0; i < listeners.length; i++) {
+					IPropertyListener listener = (IPropertyListener) listeners[i];
+					listener.propertyChanged(this, PROP_TITLE);
+				}
+			}
+		});
+		gd = new GridData(SWT.FILL, SWT.CENTER, true, false);
+		queryText.setLayoutData(gd);
+		toolkit.adapt(queryText, true, true);
+
+		ToolBarManager toolBarManager2 = new ToolBarManager(SWT.FLAT);
+		toolBarManager2.createControl(queryComposite);
+		ToolBar control = toolBarManager2.getControl();
+		toolkit.adapt(control);
+
+		// Results section
+		resultsSection = toolkit.createSection(body, ExpandableComposite.TITLE_BAR
+				| ExpandableComposite.LEFT_TEXT_CLIENT_ALIGNMENT);
+		resultsSection.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1));
+		resultsSection.setText("Localization Strings");
+		toolkit.adapt(resultsSection);
+
+		final Composite resultsComposite = toolkit.createComposite(resultsSection, SWT.NONE);
+		toolkit.adapt(resultsComposite);
+		final GridLayout gridLayout2 = new GridLayout();
+		gridLayout2.marginTop = 1;
+		gridLayout2.marginWidth = 1;
+		gridLayout2.marginHeight = 1;
+		gridLayout2.horizontalSpacing = 0;
+		resultsComposite.setLayout(gridLayout2);
+
+		filteredLabel = new Label(resultsSection, SWT.NONE);
+		filteredLabel.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, false, false));
+		filteredLabel.setForeground(Display.getCurrent().getSystemColor(SWT.COLOR_RED));
+		filteredLabel.setText(""); //$NON-NLS-1$
+
+		toolkit.paintBordersFor(resultsComposite);
+		resultsSection.setClient(resultsComposite);
+		resultsSection.setTextClient(filteredLabel);
+
+		tableComposite = toolkit.createComposite(resultsComposite, SWT.NONE);
+		tableComposite.setData(FormToolkit.KEY_DRAW_BORDER, FormToolkit.TREE_BORDER);
+		toolkit.adapt(tableComposite);
+		tableComposite.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
+
+		// Table
+		createTableViewer();
+
+		registerContextMenu();
+
+		// Set default configuration
+		filterOptions = new FilterOptions();
+		filterOptions.filterPlugins = false;
+		filterOptions.pluginPatterns = new String[0];
+		filterOptions.keysWithMissingEntriesOnly = false;
+		sortOrder = KEY;
+		columnConfigs = new Object[] {KEY, new Locale(""), new Locale("de")}; //$NON-NLS-1$ //$NON-NLS-2$
+
+		// Load configuration
+		try {
+			loadSettings();
+		} catch (Exception e) {
+			// Ignore
+		}
+
+		updateColumns();
+		updateFilterLabel();
+		table.setSortDirection(SWT.UP);
+	}
+
+	protected void updateFilterLabel() {
+		if (filterOptions.filterPlugins || filterOptions.keysWithMissingEntriesOnly) {
+			filteredLabel.setText("(filtered)");
+		} else {
+			filteredLabel.setText(""); //$NON-NLS-1$
+		}
+		filteredLabel.getParent().layout(true);
+	}
+
+	private void loadSettings() {
+		// TODO Move this to the preferences?
+		IDialogSettings dialogSettings = MessagesEditorPlugin.getDefault().getDialogSettings();
+		IDialogSettings section = dialogSettings.getSection(PREF_SECTION_NAME);
+		if (section == null)
+			return;
+
+		// Sort order
+		String sortOrderString = section.get(PREF_SORT_ORDER);
+		if (sortOrderString != null) {
+			if (sortOrderString.equals(KEY)) {
+				sortOrder = KEY;
+			} else {
+				try {
+					sortOrder = LocaleUtil.parseLocale(sortOrderString);
+				} catch (IllegalArgumentException e) {
+					// Should never happen
+				}
+			}
+		}
+
+		// Columns
+		String columns = section.get(PREF_COLUMNS);
+		if (columns != null) {
+			String[] cols = columns.substring(1, columns.length() - 1).split(","); //$NON-NLS-1$
+			columnConfigs = new Object[cols.length];
+			for (int i = 0; i < cols.length; i++) {
+				String value = cols[i].trim();
+				if (value.equals(KEY)) {
+					columnConfigs[i] = KEY;
+				} else if (value.equals("default")) { //$NON-NLS-1$
+					columnConfigs[i] = new Locale(""); //$NON-NLS-1$
+				} else {
+					try {
+						columnConfigs[i] = LocaleUtil.parseLocale(value);
+					} catch (IllegalArgumentException e) {
+						columnConfigs[i] = null;
+					}
+				}
+			}
+		}
+
+		// Filter options
+		String filterOptions = section.get(PREF_FILTER_OPTIONS_FILTER_PLUGINS);
+		this.filterOptions.filterPlugins = "true".equals(filterOptions); //$NON-NLS-1$
+		String patterns = section.get(PREF_FILTER_OPTIONS_PLUGIN_PATTERNS);
+		if (patterns != null) {
+			String[] split = patterns.substring(1, patterns.length() - 1).split(","); //$NON-NLS-1$
+			for (int i = 0; i < split.length; i++) {
+				split[i] = split[i].trim();
+			}
+			this.filterOptions.pluginPatterns = split;
+		}
+		this.filterOptions.keysWithMissingEntriesOnly = "true".equals(section.get(PREF_FILTER_OPTIONS_MISSING_ONLY)); //$NON-NLS-1$
+
+		// TODO Save column widths 
+	}
+
+	private void saveSettings() {
+		IDialogSettings dialogSettings = MessagesEditorPlugin.getDefault().getDialogSettings();
+		IDialogSettings section = dialogSettings.getSection(PREF_SECTION_NAME);
+		if (section == null) {
+			section = dialogSettings.addNewSection(PREF_SECTION_NAME);
+		}
+		// Sort order
+		section.put(PREF_SORT_ORDER, sortOrder.toString());
+
+		// Columns
+		section.put(PREF_COLUMNS, Arrays.toString(columnConfigs));
+
+		// Filter options
+		section.put(PREF_FILTER_OPTIONS_FILTER_PLUGINS, filterOptions.filterPlugins);
+		section.put(PREF_FILTER_OPTIONS_PLUGIN_PATTERNS, Arrays.toString(filterOptions.pluginPatterns));
+		section.put(PREF_FILTER_OPTIONS_MISSING_ONLY, filterOptions.keysWithMissingEntriesOnly);
+	}
+
+	private void createTableViewer() {
+		table = new Table(tableComposite, SWT.VIRTUAL | SWT.FULL_SELECTION | SWT.MULTI);
+		tableViewer = new TableViewer(table);
+		table.setHeaderVisible(true);
+		toolkit.adapt(table);
+		toolkit.paintBordersFor(table);
+		toolkit.adapt(table, true, true);
+
+		tableViewer.setContentProvider(new ILazyContentProvider() {
+			public void updateElement(int index) {
+				tableViewer.replace(entryList.getKey(index), index);
+			}
+			public void dispose() {
+			}
+			public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
+			}
+		});
+		tableViewer.addDoubleClickListener(new IDoubleClickListener() {
+			public void doubleClick(DoubleClickEvent event) {
+				new EditEntryAction().run();
+			}
+		});
+	}
+
+	private void registerContextMenu() {
+		MenuManager menuManager = new MenuManager("#PopupMenu"); //$NON-NLS-1$
+		menuManager.setRemoveAllWhenShown(true);
+		menuManager.addMenuListener(new IMenuListener() {
+			public void menuAboutToShow(IMenuManager menu) {
+				fillContextMenu(menu);
+			}
+		});
+		Menu contextMenu = menuManager.createContextMenu(table);
+		table.setMenu(contextMenu);
+		getSite().registerContextMenu(menuManager, getSite().getSelectionProvider());
+	}
+
+	protected void fillContextMenu(IMenuManager menu) {
+		int selectionCount = table.getSelectionCount();
+		if (selectionCount == 1) {
+			menu.add(new EditEntryAction());
+			menu.add(new Separator());
+		}
+		MenuManager showInSubMenu = new MenuManager("&Show In");
+		IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
+		IContributionItem item = ContributionItemFactory.VIEWS_SHOW_IN.create(window);
+		showInSubMenu.add(item);
+		menu.add(showInSubMenu);
+	}
+
+	@Override
+	public void setFocus() {
+		queryText.setFocus();
+	}
+
+	@Override
+	public void doSave(IProgressMonitor monitor) {
+	}
+
+	@Override
+	public void doSaveAs() {
+	}
+
+	@Override
+	public void init(IEditorSite site, IEditorInput input) throws PartInitException {
+		setSite(site);
+		setInput(input);
+		this.input = (LocalizationEditorInput) input;
+	}
+
+	@Override
+	public boolean isDirty() {
+		return false;
+	}
+
+	@Override
+	public boolean isSaveAsAllowed() {
+		return false;
+	}
+
+	/*
+	 * @see org.eclipse.ui.part.WorkbenchPart#dispose()
+	 */
+	@Override
+	public void dispose() {
+		saveSettings();
+		if (formImage != null) {
+			formImage.dispose();
+			formImage = null;
+		}
+		MessagesEditorPlugin.disposeModel();
+	}
+
+	protected void executeQuery() {
+		String pattern = queryText.getText();
+		lastQuery = pattern;
+		executeQuery(pattern);
+	}
+
+	protected void executeQuery(final String pattern) {
+		if (searchJob != null) {
+			searchJob.cancel();
+		}
+		searchJob = new Job("Localization Editor Search...") {
+
+			@Override
+			protected IStatus run(IProgressMonitor monitor) {
+				Display.getDefault().syncExec(new Runnable() {
+					public void run() {
+						form.setBusy(true);
+					}
+				});
+
+				String keyPattern = pattern;
+				if (!pattern.endsWith("*")) { //$NON-NLS-1$
+					keyPattern = pattern.concat("*"); //$NON-NLS-1$
+				}
+				String strPattern = keyPattern;
+				if (strPattern.length() > 0 && !strPattern.startsWith("*")) { //$NON-NLS-1$
+					strPattern = "*".concat(strPattern); //$NON-NLS-1$
+				}
+
+				ResourceBundleModel model = MessagesEditorPlugin.getModel(new NullProgressMonitor());
+				Locale[] locales = getLocales();
+
+				// Collect keys
+				ResourceBundleKey[] keys;
+				if (!filterOptions.filterPlugins
+						|| filterOptions.pluginPatterns == null
+						|| filterOptions.pluginPatterns.length == 0) {
+
+					// Ensure the bundles are loaded
+					for (Locale locale : locales) {
+						try {
+							model.loadBundles(locale);
+						} catch (CoreException e) {
+							MessagesEditorPlugin.log(e);
+						}
+					}
+
+					try {
+						keys = model.getAllKeys();
+					} catch (CoreException e) {
+						MessagesEditorPlugin.log(e);
+						keys = new ResourceBundleKey[0];
+					}
+				} else {
+					String[] patterns = filterOptions.pluginPatterns;
+					StringMatcher[] matchers = new StringMatcher[patterns.length];
+					for (int i = 0; i < matchers.length; i++) {
+						matchers[i] = new StringMatcher(patterns[i], true, false);
+					}
+
+					int size = 0;
+					ResourceBundleFamily[] allFamilies = model.getFamilies();
+					ArrayList<ResourceBundleFamily> families = new ArrayList<ResourceBundleFamily>();
+					for (int i = 0; i < allFamilies.length; i++) {
+						ResourceBundleFamily family = allFamilies[i];
+						String pluginId = family.getPluginId();
+						for (StringMatcher matcher : matchers) {
+							if (matcher.match(pluginId)) {
+								families.add(family);
+								break;
+							}
+						}
+
+					}
+					for (ResourceBundleFamily family : families) {
+						size += family.getKeyCount();
+					}
+
+					ArrayList<ResourceBundleKey> filteredKeys = new ArrayList<ResourceBundleKey>(size);
+					for (ResourceBundleFamily family : families) {
+						// Ensure the bundles are loaded
+						for (Locale locale : locales) {
+							try {
+								ResourceBundle bundle = family.getBundle(locale);
+								if (bundle != null)
+									bundle.load();
+							} catch (CoreException e) {
+								MessagesEditorPlugin.log(e);
+							}
+						}
+
+						ResourceBundleKey[] familyKeys = family.getKeys();
+						for (ResourceBundleKey key : familyKeys) {
+							filteredKeys.add(key);
+						}
+					}
+					keys = filteredKeys.toArray(new ResourceBundleKey[filteredKeys.size()]);
+				}
+
+				// Filter keys
+				ArrayList<ResourceBundleKey> filtered = new ArrayList<ResourceBundleKey>();
+
+				StringMatcher keyMatcher = new StringMatcher(keyPattern, true, false);
+				StringMatcher strMatcher = new StringMatcher(strPattern, true, false);
+				for (ResourceBundleKey key : keys) {
+					if (monitor.isCanceled())
+						return Status.OK_STATUS;
+
+					// Missing entries
+					if (filterOptions.keysWithMissingEntriesOnly) {
+						boolean hasMissingEntry = false;
+						// Check all columns for missing values
+						for (Object config : columnConfigs) {
+							if (config == KEY)
+								continue;
+							Locale locale = (Locale) config;
+							String value = null;
+							try {
+								value = key.getValue(locale);
+							} catch (CoreException e) {
+								MessagesEditorPlugin.log(e);
+							}
+							if (value == null || value.length() == 0) {
+								hasMissingEntry = true;
+								break;
+							}
+						}
+						if (!hasMissingEntry)
+							continue;
+					}
+
+					// Match key
+					if (keyMatcher.match(key.getName())) {
+						filtered.add(key);
+						continue;
+					}
+
+					// Match entries
+					for (Object config : columnConfigs) {
+						if (config == KEY)
+							continue;
+						Locale locale = (Locale) config;
+						String value = null;
+						try {
+							value = key.getValue(locale);
+						} catch (CoreException e) {
+							MessagesEditorPlugin.log(e);
+						}
+						if (strMatcher.match(value)) {
+							filtered.add(key);
+							break;
+						}
+					}
+				}
+
+				ResourceBundleKey[] array = filtered.toArray(new ResourceBundleKey[filtered.size()]);
+				if (sortOrder == KEY) {
+					Arrays.sort(array, new Comparator<ResourceBundleKey>() {
+						public int compare(ResourceBundleKey o1, ResourceBundleKey o2) {
+							return o1.getName().compareToIgnoreCase(o2.getName());
+						}
+					});
+				} else {
+					Locale locale = (Locale) sortOrder;
+					Arrays.sort(array, new BundleStringComparator(locale));
+				}
+				entryList = new ResourceBundleKeyList(array);
+
+				if (monitor.isCanceled())
+					return Status.OK_STATUS;
+
+				final ResourceBundleKeyList entryList2 = entryList;
+				Display.getDefault().syncExec(new Runnable() {
+					public void run() {
+						form.setBusy(false);
+						if (entryList2 != null) {
+							entryList = entryList2;
+						}
+						setSearchResult(entryList);
+					}
+				});
+				return Status.OK_STATUS;
+			}
+		};
+		searchJob.setSystem(true);
+		searchJob.setRule(mutexRule);
+		searchJob.schedule();
+	}
+
+	protected void updateTableLayout() {
+		table.getParent().layout(true, true);
+	}
+
+	protected void setSearchResult(ResourceBundleKeyList entryList) {
+		table.removeAll();
+		if (entryList != null) {
+			table.setItemCount(entryList.getSize());
+		} else {
+			table.setItemCount(0);
+		}
+		updateTableLayout();
+	}
+
+	public void refresh() {
+		executeQuery(lastQuery);
+	}
+
+	public void updateLabels() {
+		table.redraw();
+		table.update();
+	}
+
+	/**
+	 * @param columnConfigs an array containing <code>KEY</code> and {@link Locale} values 
+	 */
+	public void setColumns(Object[] columnConfigs) {
+		this.columnConfigs = columnConfigs;
+		updateColumns();
+	}
+
+	public void updateColumns() {
+		for (TableColumn column : columns) {
+			column.dispose();
+		}
+		columns.clear();
+
+		TableColumnLayout tableColumnLayout = new TableColumnLayout();
+		tableComposite.setLayout(tableColumnLayout);
+
+		HashSet<Locale> localesToUnload = new HashSet<Locale>(4);
+		Locale[] currentLocales = getLocales();
+		for (Locale locale : currentLocales) {
+			localesToUnload.add(locale);
+		}
+
+		// Create columns
+		for (Object config : columnConfigs) {
+			if (config == null)
+				continue;
+
+			final TableViewerColumn viewerColumn = new TableViewerColumn(tableViewer, SWT.NONE);
+			TableColumn column = viewerColumn.getColumn();
+			if (config == KEY) {
+				column.setText("Key");
+			} else {
+				Locale locale = (Locale) config;
+				if (locale.getLanguage().equals("")) { //$NON-NLS-1$
+					column.setText("Default Bundle");
+				} else {
+					String displayName = locale.getDisplayName();
+					if (displayName.equals("")) //$NON-NLS-1$
+						displayName = locale.toString();
+					column.setText(displayName);
+					localesToUnload.remove(locale);
+				}
+			}
+
+			viewerColumn.setLabelProvider(new LocalizationLabelProvider(config));
+			tableColumnLayout.setColumnData(column, new ColumnWeightData(33));
+			columns.add(column);
+			column.addSelectionListener(new SelectionAdapter() {
+				@Override
+				public void widgetSelected(SelectionEvent e) {
+					int size = columns.size();
+					for (int i = 0; i < size; i++) {
+						TableColumn column = columns.get(i);
+						if (column == e.widget) {
+							Object config = columnConfigs[i];
+							sortOrder = config;
+							table.setSortColumn(column);
+							table.setSortDirection(SWT.UP);
+							refresh();
+							break;
+						}
+					}
+				}
+			});
+		}
+
+		// Update sort order
+		List<Object> configs = Arrays.asList(columnConfigs);
+		if (!configs.contains(sortOrder)) {
+			sortOrder = KEY; // fall back to default sort order
+		}
+		int index = configs.indexOf(sortOrder);
+		if (index != -1)
+			table.setSortColumn(columns.get(index));
+
+		refresh();
+	}
+
+	/*
+	 * @see org.eclipse.ui.part.WorkbenchPart#getAdapter(java.lang.Class)
+	 */
+	@SuppressWarnings("unchecked")
+	@Override
+	public Object getAdapter(Class adapter) {
+		if (IShowInSource.class == adapter) {
+			return new IShowInSource() {
+				public ShowInContext getShowInContext() {
+					ResourceBundleKey entry = getSelectedEntry();
+					if (entry == null)
+						return null;
+					ResourceBundle bundle = entry.getParent().getBundle(new Locale(""));
+					if (bundle == null)
+						return null;
+					Object resource = bundle.getUnderlyingResource();
+					return new ShowInContext(resource, new StructuredSelection(resource));
+				}
+			};
+		}
+		return super.getAdapter(adapter);
+	}
+
+	/**
+	 * Returns the currently displayed locales.
+	 * 
+	 * @return the currently displayed locales
+	 */
+	public Locale[] getLocales() {
+		ArrayList<Locale> locales = new ArrayList<Locale>(columnConfigs.length);
+		for (Object config : columnConfigs) {
+			if (config instanceof Locale) {
+				Locale locale = (Locale) config;
+				locales.add(locale);
+			}
+		}
+		return locales.toArray(new Locale[locales.size()]);
+	}
+
+}
diff --git a/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/editor/LocalizationEditorInput.java b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/editor/LocalizationEditorInput.java
new file mode 100644
index 0000000..2ad8c7e
--- /dev/null
+++ b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/editor/LocalizationEditorInput.java
@@ -0,0 +1,79 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.nls.internal.ui.editor;
+
+import org.eclipse.jface.resource.ImageDescriptor;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IMemento;
+import org.eclipse.ui.IPersistableElement;
+
+public class LocalizationEditorInput implements IEditorInput, IPersistableElement {
+
+	public LocalizationEditorInput() {
+	}
+
+	/*
+	 * @see org.eclipse.ui.IEditorInput#exists()
+	 */
+	public boolean exists() {
+		return true;
+	}
+
+	/*
+	 * @see org.eclipse.ui.IEditorInput#getImageDescriptor()
+	 */
+	public ImageDescriptor getImageDescriptor() {
+		return ImageDescriptor.getMissingImageDescriptor();
+	}
+
+	/*
+	 * @see org.eclipse.ui.IEditorInput#getName()
+	 */
+	public String getName() {
+		return "Localization Editor";
+	}
+
+	/*
+	 * @see org.eclipse.ui.IEditorInput#getPersistable()
+	 */
+	public IPersistableElement getPersistable() {
+		return this;
+	}
+
+	/*
+	 * @see org.eclipse.ui.IEditorInput#getToolTipText()
+	 */
+	public String getToolTipText() {
+		return getName();
+	}
+
+	/*
+	 * @see org.eclipse.core.runtime.IAdaptable#getAdapter(java.lang.Class)
+	 */
+	@SuppressWarnings("unchecked")
+	public Object getAdapter(Class adapter) {
+		return null;
+	}
+
+	/*
+	 * @see org.eclipse.ui.IPersistableElement#getFactoryId()
+	 */
+	public String getFactoryId() {
+		return LocalizationEditorInputFactory.FACTORY_ID;
+	}
+	
+	/*
+	 * @see org.eclipse.ui.IPersistable#saveState(org.eclipse.ui.IMemento)
+	 */
+	public void saveState(IMemento memento) {
+	}
+	
+}
diff --git a/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/editor/LocalizationEditorInputFactory.java b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/editor/LocalizationEditorInputFactory.java
new file mode 100644
index 0000000..698c218
--- /dev/null
+++ b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/editor/LocalizationEditorInputFactory.java
@@ -0,0 +1,31 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.nls.internal.ui.editor;
+
+import org.eclipse.core.runtime.IAdaptable;
+import org.eclipse.ui.IElementFactory;
+import org.eclipse.ui.IMemento;
+
+public class LocalizationEditorInputFactory implements IElementFactory {
+
+	public static final String FACTORY_ID = "org.eclipse.pde.nls.ui.LocalizationEditorInputFactory"; //$NON-NLS-1$
+
+	public LocalizationEditorInputFactory() {
+	}
+
+	/*
+	 * @see org.eclipse.ui.IElementFactory#createElement(org.eclipse.ui.IMemento)
+	 */
+	public IAdaptable createElement(IMemento memento) {
+		return new LocalizationEditorInput();
+	}
+
+}
diff --git a/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/model/ResourceBundle.java b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/model/ResourceBundle.java
new file mode 100644
index 0000000..63f8719
--- /dev/null
+++ b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/model/ResourceBundle.java
@@ -0,0 +1,171 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.nls.internal.ui.model;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Properties;
+import java.util.Set;
+import java.util.Map.Entry;
+
+import org.eclipse.babel.editor.plugin.MessagesEditorPlugin;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.core.IJarEntryResource;
+
+/**
+ * A <code>ResourceBundle</code> represents a single <code>.properties</code> file.
+ * <p>
+ * <code>ResourceBundle</code> implements lazy loading. A bundle will be loaded
+ * automatically when its entries are accessed. It can through the parent model by
+ * calling {@link ResourceBundleModel#unloadBundles(Locale)} with the proper locale.
+ * </p>
+ */
+public class ResourceBundle extends ResourceBundleElement {
+
+	private static boolean debug = false;
+	
+	/**
+	 * The bundle's locale.
+	 */
+	private Locale locale;
+	/**
+	 * The underlying resource. Either an {@link IFile} or an {@link IJarEntryResource}.
+	 */
+	private Object resource;
+
+	private HashMap<String, String> entries;
+
+	public ResourceBundle(ResourceBundleFamily parent, Object resource, Locale locale) {
+		super(parent);
+		this.resource = resource;
+		this.locale = locale;
+		if (locale == null)
+			throw new IllegalArgumentException("Locale may not be null.");
+	}
+
+	/**
+	 * Returns the family to which this bundle belongs.
+	 * 
+	 * @return the family to which this bundle belongs
+	 */
+	public ResourceBundleFamily getFamily() {
+		return (ResourceBundleFamily) super.getParent();
+	}
+
+	/**
+	 * Returns the locale.
+	 * 
+	 * @return the locale
+	 */
+	public Locale getLocale() {
+		return locale;
+	}
+
+	public String getString(String key) throws CoreException {
+		load();
+		return entries.get(key);
+	}
+
+	/**
+	 * Returns the underlying resource. This may be an {@link IFile}
+	 * or an {@link IJarEntryResource}.
+	 * 
+	 * @return the underlying resource (an {@link IFile} or an {@link IJarEntryResource})
+	 */
+	public Object getUnderlyingResource() {
+		return resource;
+	}
+
+	protected boolean isLoaded() {
+		return entries != null;
+	}
+
+	public void load() throws CoreException {
+		if (isLoaded())
+			return;
+		entries = new HashMap<String, String>();
+
+		if (resource instanceof IFile) {
+			if (debug) {
+				System.out.println("Loading " + resource + "...");
+			}
+			IFile file = (IFile) resource;
+			InputStream inputStream = file.getContents();
+			Properties properties = new Properties();
+			try {
+				properties.load(inputStream);
+				putAll(properties);
+			} catch (IOException e) {
+				MessagesEditorPlugin.log("Error reading property file.", e);
+			}
+		} else if (resource instanceof IJarEntryResource) {
+			IJarEntryResource jarEntryResource = (IJarEntryResource) resource;
+			InputStream inputStream = jarEntryResource.getContents();
+			Properties properties = new Properties();
+			try {
+				properties.load(inputStream);
+				putAll(properties);
+			} catch (IOException e) {
+				MessagesEditorPlugin.log("Error reading property file.", e);
+			}
+		} else {
+			MessagesEditorPlugin.log("Unknown resource type.", new RuntimeException());
+		}
+	}
+
+	protected void unload() {
+		entries = null;
+	}
+
+	public boolean isReadOnly() {
+		if (resource instanceof IJarEntryResource)
+			return true;
+		if (resource instanceof IFile) {
+			IFile file = (IFile) resource;
+			return file.isReadOnly() || file.isLinked();
+		}
+		return false;
+	}
+
+	protected void putAll(Properties properties) throws CoreException {
+		Set<Entry<Object, Object>> entrySet = properties.entrySet();
+		Iterator<Entry<Object, Object>> iter = entrySet.iterator();
+		ResourceBundleFamily family = getFamily();
+		while (iter.hasNext()) {
+			Entry<Object, Object> next = iter.next();
+			Object key = next.getKey();
+			Object value = next.getValue();
+			if (key instanceof String && value instanceof String) {
+				String stringKey = (String) key;
+				entries.put(stringKey, (String) value);
+				family.addKey(stringKey);
+			}
+		}
+	}
+
+	public void put(String key, String value) throws CoreException {
+		load();
+		ResourceBundleFamily family = getFamily();
+		entries.put(key, value);
+		family.addKey(key);
+	}
+
+	public String[] getKeys() throws CoreException {
+		load();
+		Set<String> keySet = entries.keySet();
+		return keySet.toArray(new String[keySet.size()]);
+	}
+
+}
diff --git a/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/model/ResourceBundleElement.java b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/model/ResourceBundleElement.java
new file mode 100644
index 0000000..4025bda
--- /dev/null
+++ b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/model/ResourceBundleElement.java
@@ -0,0 +1,25 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.nls.internal.ui.model;
+
+public abstract class ResourceBundleElement {
+
+	private final ResourceBundleElement parent;
+
+	public ResourceBundleElement(ResourceBundleElement parent) {
+		this.parent = parent;
+	}
+	
+	public ResourceBundleElement getParent() {
+		return parent;
+	}
+
+}
diff --git a/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/model/ResourceBundleFamily.java b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/model/ResourceBundleFamily.java
new file mode 100644
index 0000000..dedb937
--- /dev/null
+++ b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/model/ResourceBundleFamily.java
@@ -0,0 +1,132 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.nls.internal.ui.model;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Locale;
+
+import org.eclipse.core.runtime.Assert;
+import org.eclipse.core.runtime.CoreException;
+
+/**
+ * A <code>ResourceBundleGroup</code> represents a group of resource bundles
+ * that belong together. Member resource bundles may reside in the same project as the
+ * default resource bundle, or in case of a plugin project, in a separate fragment
+ * project.
+ */
+public class ResourceBundleFamily extends ResourceBundleElement {
+
+	/**
+	 * The project name of the default bundle.
+	 */
+	private String projectName;
+	/**
+	 * The plugin id of the default bundle, or <code>null</code> if not a plugin or fragment project.
+	 */
+	private String pluginId;
+	/**
+	 * The package name or path.
+	 */
+	private String packageName;
+	/**
+	 * The base name that all family members have in common.
+	 */
+	private String baseName;
+	/**
+	 * The members that belong to this resource bundle family (excluding the default bundle).
+	 */
+	private ArrayList<ResourceBundle> members = new ArrayList<ResourceBundle>();
+	/**
+	 * A collection of known keys.
+	 */
+	private HashMap<String, ResourceBundleKey> keys = new HashMap<String, ResourceBundleKey>();
+
+	public ResourceBundleFamily(ResourceBundleModel parent, String projectName, String pluginId,
+			String packageName, String baseName) {
+		super(parent);
+		this.projectName = projectName;
+		this.pluginId = pluginId;
+		this.packageName = packageName;
+		this.baseName = baseName;
+	}
+
+	public String getProjectName() {
+		return projectName;
+	}
+
+	public String getPluginId() {
+		return pluginId;
+	}
+
+	public String getPackageName() {
+		return packageName;
+	}
+
+	public String getBaseName() {
+		return baseName;
+	}
+
+	public ResourceBundle[] getBundles() {
+		return members.toArray(new ResourceBundle[members.size()]);
+	}
+
+	public ResourceBundle getBundle(Locale locale) {
+		for (ResourceBundle bundle : members) {
+			if (bundle.getLocale().equals(locale)) {
+				return bundle;
+			}
+		}
+		return null;
+	}
+
+	/*
+	 * @see java.lang.Object#hashCode()
+	 */
+	@Override
+	public int hashCode() {
+		if (pluginId != null) {
+			return baseName.hashCode() ^ pluginId.hashCode();
+		} else {
+			return baseName.hashCode() ^ projectName.hashCode();
+		}
+	}
+
+	protected void addBundle(ResourceBundle bundle) throws CoreException {
+		Assert.isTrue(bundle.getParent() == this);
+		members.add(bundle);
+	}
+
+	protected void addKey(String key) {
+		if (keys.get(key) == null) {
+			keys.put(key, new ResourceBundleKey(this, key));
+		}
+	}
+
+	public ResourceBundleKey[] getKeys() {
+		Collection<ResourceBundleKey> values = keys.values();
+		return values.toArray(new ResourceBundleKey[values.size()]);
+	}
+
+	public int getKeyCount() {
+		return keys.size();
+	}
+
+	/*
+	 * @see java.lang.Object#toString()
+	 */
+	@Override
+	public String toString() {
+		return "projectName=" + projectName + ", packageName=" + packageName + ", baseName=" + baseName;
+	}
+
+}
diff --git a/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/model/ResourceBundleKey.java b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/model/ResourceBundleKey.java
new file mode 100644
index 0000000..04cff98
--- /dev/null
+++ b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/model/ResourceBundleKey.java
@@ -0,0 +1,64 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.nls.internal.ui.model;
+
+import java.util.Locale;
+
+import org.eclipse.core.runtime.CoreException;
+
+/**
+ * A <code>ResourceBundleKey</code> represents a key used in one or more bundles of
+ * a {@link ResourceBundleFamily}.
+ */
+public class ResourceBundleKey extends ResourceBundleElement {
+
+	private String key;
+
+	public ResourceBundleKey(ResourceBundleFamily parent, String key) {
+		super(parent);
+		this.key = key;
+	}
+	
+	/*
+	 * @see org.eclipse.nls.ui.model.ResourceBundleElement#getParent()
+	 */
+	@Override
+	public ResourceBundleFamily getParent() {
+		return (ResourceBundleFamily) super.getParent();
+	}
+
+	public ResourceBundleFamily getFamily() {
+		return getParent();
+	}
+
+	public String getName() {
+		return key;
+	}
+
+	public String getValue(Locale locale) throws CoreException {
+		ResourceBundle bundle = getFamily().getBundle(locale);
+		if (bundle == null)
+			return null;
+		return bundle.getString(key);
+	}
+
+	public boolean hasValue(Locale locale) throws CoreException {
+		return getValue(locale) != null;
+	}
+
+	/*
+	 * @see java.lang.Object#toString()
+	 */
+	public String toString() {
+		return "ResourceBundleKey {" + key + "}";
+	}
+
+}
diff --git a/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/model/ResourceBundleKeyList.java b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/model/ResourceBundleKeyList.java
new file mode 100644
index 0000000..8683638
--- /dev/null
+++ b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/model/ResourceBundleKeyList.java
@@ -0,0 +1,29 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.nls.internal.ui.model;
+
+public class ResourceBundleKeyList {
+
+	private final ResourceBundleKey[] keys;
+
+	public ResourceBundleKeyList(ResourceBundleKey[] keys) {
+		this.keys = keys;
+	}
+
+	public ResourceBundleKey getKey(int index) {
+		return keys[index];
+	}
+
+	public int getSize() {
+		return keys.length;
+	}
+
+}
diff --git a/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/model/ResourceBundleModel.java b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/model/ResourceBundleModel.java
new file mode 100644
index 0000000..500ce6a
--- /dev/null
+++ b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/model/ResourceBundleModel.java
@@ -0,0 +1,520 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.nls.internal.ui.model;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Locale;
+
+import org.eclipse.babel.editor.plugin.MessagesEditorPlugin;
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspace;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJarEntryResource;
+import org.eclipse.jdt.core.IJavaElement;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.pde.core.plugin.IFragmentModel;
+import org.eclipse.pde.core.plugin.IPluginModelBase;
+import org.eclipse.pde.core.plugin.PluginRegistry;
+
+/**
+ * A <code>ResourceBundleModel</code> is the host for all {@link ResourceBundleFamily} elements. 
+ */
+public class ResourceBundleModel extends ResourceBundleElement {
+
+	private static final String PROPERTIES_SUFFIX = ".properties"; //$NON-NLS-1$
+
+	private static final String JAVA_NATURE = "org.eclipse.jdt.core.javanature"; //$NON-NLS-1$
+
+	private ArrayList<ResourceBundleFamily> bundleFamilies = new ArrayList<ResourceBundleFamily>();
+
+	/**
+	 * The locales for which all bundles have been loaded.
+	 */
+	// TODO Perhaps we should add reference counting to prevent unexpected unloading of bundles 
+	private HashSet<Locale> loadedLocales = new HashSet<Locale>();
+
+	public ResourceBundleModel(IProgressMonitor monitor) {
+		super(null);
+		try {
+			populateFromWorkspace(monitor);
+		} catch (CoreException e) {
+			MessagesEditorPlugin.log(e);
+		}
+	}
+
+	/**
+	 * Returns all resource bundle families contained in this model.
+	 * 
+	 * @return all resource bundle families contained in this model
+	 */
+	public ResourceBundleFamily[] getFamilies() {
+		return bundleFamilies.toArray(new ResourceBundleFamily[bundleFamilies.size()]);
+	}
+
+	public ResourceBundleFamily[] getFamiliesForPluginId(String pluginId) {
+		ArrayList<ResourceBundleFamily> found = new ArrayList<ResourceBundleFamily>();
+		for (ResourceBundleFamily family : bundleFamilies) {
+			if (family.getPluginId().equals(pluginId)) {
+				found.add(family);
+			}
+		}
+		return found.toArray(new ResourceBundleFamily[found.size()]);
+	}
+
+	public ResourceBundleFamily[] getFamiliesForProjectName(String projectName) {
+		ArrayList<ResourceBundleFamily> found = new ArrayList<ResourceBundleFamily>();
+		for (ResourceBundleFamily family : bundleFamilies) {
+			if (family.getProjectName().equals(projectName)) {
+				found.add(family);
+			}
+		}
+		return found.toArray(new ResourceBundleFamily[found.size()]);
+	}
+
+	public ResourceBundleFamily[] getFamiliesForProject(IProject project) {
+		return getFamiliesForProjectName(project.getName());
+	}
+
+	/**
+	 * Returns an array of all currently known bundle keys. This always includes
+	 * the keys from the default bundles and may include some additional keys
+	 * from bundles that have been loaded sometime and that contain keys not found in
+	 * a bundle's default bundle. When a bundle is unloaded, these additional keys
+	 * will not be removed from the model.
+	 * 
+	 * @return the array of bundles keys
+	 * @throws CoreException 
+	 */
+	public ResourceBundleKey[] getAllKeys() throws CoreException {
+		Locale root = new Locale("", "", "");
+		
+		// Ensure default bundle is loaded and count keys  
+		int size = 0;
+		for (ResourceBundleFamily family : bundleFamilies) {
+			ResourceBundle bundle = family.getBundle(root);
+			if (bundle != null)
+				bundle.load();
+			size += family.getKeyCount();
+		}
+
+		ArrayList<ResourceBundleKey> allKeys = new ArrayList<ResourceBundleKey>(size);
+		for (ResourceBundleFamily family : bundleFamilies) {
+			ResourceBundleKey[] keys = family.getKeys();
+			for (ResourceBundleKey key : keys) {
+				allKeys.add(key);
+			}
+		}
+
+		return allKeys.toArray(new ResourceBundleKey[allKeys.size()]);
+	}
+
+	/**
+	 * Loads all the bundles for the given locale into memory.
+	 * 
+	 * @param locale the locale of the bundles to load
+	 * @throws CoreException 
+	 */
+	public void loadBundles(Locale locale) throws CoreException {
+		ResourceBundleFamily[] families = getFamilies();
+		for (ResourceBundleFamily family : families) {
+			ResourceBundle bundle = family.getBundle(locale);
+			if (bundle != null)
+				bundle.load();
+		}
+		loadedLocales.add(locale);
+	}
+
+	/**
+	 * Unloads all the bundles for the given locale from this model. The default
+	 * bundle cannot be unloaded. Such a request will be ignored.
+	 * 
+	 * @param locale the locale of the bundles to unload
+	 */
+	public void unloadBundles(Locale locale) {
+		if ("".equals(locale.getLanguage()))
+			return; // never unload the default bundles
+
+		ResourceBundleFamily[] families = getFamilies();
+		for (ResourceBundleFamily family : families) {
+			ResourceBundle bundle = family.getBundle(locale);
+			if (bundle != null)
+				bundle.unload();
+		}
+		loadedLocales.remove(locale);
+	}
+
+	private void populateFromWorkspace(IProgressMonitor monitor) throws CoreException {
+		IWorkspace workspace = ResourcesPlugin.getWorkspace();
+		IWorkspaceRoot root = workspace.getRoot();
+		IProject[] projects = root.getProjects();
+		for (IProject project : projects) {
+			try {
+				if (!project.isOpen())
+					continue;
+
+				IJavaProject javaProject = (IJavaProject) project.getNature(JAVA_NATURE);
+
+				// Plugin and fragment projects
+				IPluginModelBase pluginModel = PluginRegistry.findModel(project);
+				String pluginId = null;
+				if (pluginModel != null) {
+					// Get plugin id
+					pluginId = pluginModel.getBundleDescription().getName(); // OSGi bundle name
+					if (pluginId == null) {
+						pluginId = pluginModel.getPluginBase().getId(); // non-OSGi plug-in id
+					}
+					boolean isFragment = pluginModel instanceof IFragmentModel;
+					if (isFragment) {
+						IFragmentModel fragmentModel = (IFragmentModel) pluginModel;
+						pluginId = fragmentModel.getFragment().getPluginId();
+					}
+
+					// Look for additional 'nl' resources
+					IFolder nl = project.getFolder("nl"); //$NON-NLS-1$
+					if (isFragment && nl.exists()) {
+						IResource[] members = nl.members();
+						for (IResource member : members) {
+							if (member instanceof IFolder) {
+								IFolder langFolder = (IFolder) member;
+								String language = langFolder.getName();
+
+								// Collect property files
+								IFile[] propertyFiles = collectPropertyFiles(langFolder);
+								for (IFile file : propertyFiles) {
+									// Compute path name
+									IPath path = file.getProjectRelativePath();
+									String country = ""; //$NON-NLS-1$
+									String packageName = null;
+									int segmentCount = path.segmentCount();
+									if (segmentCount > 1) {
+										StringBuilder builder = new StringBuilder();
+
+										// Segment 0: 'nl'
+										// Segment 1: language code
+										// Segment 2: (country code)
+										int begin = 2;
+										if (segmentCount > 2 && isCountry(path.segment(2))) {
+											begin = 3;
+											country = path.segment(2);
+										}
+
+										for (int i = begin; i < segmentCount - 1; i++) {
+											if (i > begin)
+												builder.append('.');
+											builder.append(path.segment(i));
+										}
+										packageName = builder.toString();
+									}
+
+									String baseName = getBaseName(file.getName());
+
+									ResourceBundleFamily family = getOrCreateFamily(
+											project.getName(),
+											pluginId,
+											packageName,
+											baseName);
+									addBundle(family, getLocale(language, country), file);
+								}
+							}
+						}
+					}
+
+					// Collect property files
+					if (isFragment || javaProject == null) {
+						IFile[] propertyFiles = collectPropertyFiles(project);
+						for (IFile file : propertyFiles) {
+							IPath path = file.getProjectRelativePath();
+							int segmentCount = path.segmentCount();
+
+							if (segmentCount > 0 && path.segment(0).equals("nl")) //$NON-NLS-1$
+								continue; // 'nl' resource have been processed above
+
+							// Guess package name
+							String packageName = null;
+							if (segmentCount > 1) {
+								StringBuilder builder = new StringBuilder();
+								for (int i = 0; i < segmentCount - 1; i++) {
+									if (i > 0)
+										builder.append('.');
+									builder.append(path.segment(i));
+								}
+								packageName = builder.toString();
+							}
+
+							String baseName = getBaseName(file.getName());
+							String language = getLanguage(file.getName());
+							String country = getCountry(file.getName());
+
+							ResourceBundleFamily family = getOrCreateFamily(
+									project.getName(),
+									pluginId,
+									packageName,
+									baseName);
+							addBundle(family, getLocale(language, country), file);
+						}
+					}
+
+				}
+
+				// Look for resource bundles in Java packages (output folders, e.g. 'bin', will be ignored)
+				if (javaProject != null) {
+					IClasspathEntry[] classpathEntries = javaProject.getResolvedClasspath(true);
+					for (IClasspathEntry entry : classpathEntries) {
+						if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
+							IPath path = entry.getPath();
+							IFolder folder = workspace.getRoot().getFolder(path);
+							IFile[] propertyFiles = collectPropertyFiles(folder);
+
+							for (IFile file : propertyFiles) {
+								String name = file.getName();
+								String baseName = getBaseName(name);
+								String language = getLanguage(name);
+								String country = getCountry(name);
+								IPackageFragment pf = javaProject.findPackageFragment(file.getParent()
+										.getFullPath());
+								String packageName = pf.getElementName();
+
+								ResourceBundleFamily family = getOrCreateFamily(
+										project.getName(),
+										pluginId,
+										packageName,
+										baseName);
+
+								addBundle(family, getLocale(language, country), file);
+							}
+						} else if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) {
+							IPackageFragmentRoot[] findPackageFragmentRoots = javaProject.findPackageFragmentRoots(entry);
+							for (IPackageFragmentRoot packageFragmentRoot : findPackageFragmentRoots) {
+								IJavaElement[] children = packageFragmentRoot.getChildren();
+								for (IJavaElement child : children) {
+									IPackageFragment pf = (IPackageFragment) child;
+									Object[] nonJavaResources = pf.getNonJavaResources();
+
+									for (Object resource : nonJavaResources) {
+										if (resource instanceof IJarEntryResource) {
+											IJarEntryResource jarEntryResource = (IJarEntryResource) resource;
+											String name = jarEntryResource.getName();
+											if (name.endsWith(PROPERTIES_SUFFIX)) {
+												String baseName = getBaseName(name);
+												String language = getLanguage(name);
+												String country = getCountry(name);
+												String packageName = pf.getElementName();
+
+												ResourceBundleFamily family = getOrCreateFamily(
+														project.getName(),
+														pluginId,
+														packageName,
+														baseName);
+
+												addBundle(
+														family,
+														getLocale(language, country),
+														jarEntryResource);
+											}
+										}
+									}
+								}
+							}
+						}
+					}
+
+					// Collect non-Java resources 
+					Object[] nonJavaResources = javaProject.getNonJavaResources();
+					ArrayList<IFile> files = new ArrayList<IFile>();
+					for (Object resource : nonJavaResources) {
+						if (resource instanceof IContainer) {
+							IContainer container = (IContainer) resource;
+							collectPropertyFiles(container, files);
+						} else if (resource instanceof IFile) {
+							IFile file = (IFile) resource;
+							String name = file.getName();
+							if (isIgnoredFilename(name))
+								continue;
+							if (name.endsWith(PROPERTIES_SUFFIX)) {
+								files.add(file);
+							}
+						}
+					}
+					for (IFile file : files) {
+
+						// Convert path to package name format
+						IPath path = file.getProjectRelativePath();
+						String packageName = null;
+						int segmentCount = path.segmentCount();
+						if (segmentCount > 1) {
+							StringBuilder builder = new StringBuilder();
+							for (int i = 0; i < segmentCount - 1; i++) {
+								if (i > 0)
+									builder.append('.');
+								builder.append(path.segment(i));
+							}
+							packageName = builder.toString();
+						}
+
+						String baseName = getBaseName(file.getName());
+						String language = getLanguage(file.getName());
+						String country = getCountry(file.getName());
+
+						ResourceBundleFamily family = getOrCreateFamily(
+								project.getName(),
+								pluginId,
+								packageName,
+								baseName);
+						addBundle(family, getLocale(language, country), file);
+					}
+
+				}
+			} catch (Exception e) {
+				MessagesEditorPlugin.log(e);
+			}
+		}
+	}
+
+	private IFile[] collectPropertyFiles(IContainer container) throws CoreException {
+		ArrayList<IFile> files = new ArrayList<IFile>();
+		collectPropertyFiles(container, files);
+		return files.toArray(new IFile[files.size()]);
+	}
+
+	private void collectPropertyFiles(IContainer container, ArrayList<IFile> files) throws CoreException {
+		IResource[] members = container.members();
+		for (IResource resource : members) {
+			if (!resource.exists())
+				continue;
+			if (resource instanceof IContainer) {
+				IContainer childContainer = (IContainer) resource;
+				collectPropertyFiles(childContainer, files);
+			} else if (resource instanceof IFile) {
+				IFile file = (IFile) resource;
+				String name = file.getName();
+				if (file.getProjectRelativePath().segmentCount() == 0 && isIgnoredFilename(name))
+					continue;
+				if (name.endsWith(PROPERTIES_SUFFIX)) {
+					files.add(file);
+				}
+			}
+		}
+	}
+
+	private boolean isCountry(String name) {
+		if (name == null || name.length() != 2)
+			return false;
+		char c1 = name.charAt(0);
+		char c2 = name.charAt(1);
+		return 'A' <= c1 && c1 <= 'Z' && 'A' <= c2 && c2 <= 'Z';
+	}
+
+	private Locale getLocale(String language, String country) {
+		if (language == null)
+			language = ""; //$NON-NLS-1$
+		if (country == null)
+			country = ""; //$NON-NLS-1$
+		return new Locale(language, country);
+	}
+
+	private void addBundle(ResourceBundleFamily family, Locale locale, Object resource) throws CoreException {
+		ResourceBundle bundle = new ResourceBundle(family, resource, locale);
+		if ("".equals(locale.getLanguage()))
+			bundle.load();
+		family.addBundle(bundle);
+	}
+
+	private String getBaseName(String filename) {
+		if (!filename.endsWith(PROPERTIES_SUFFIX))
+			throw new IllegalArgumentException();
+		String name = filename.substring(0, filename.length() - 11);
+		int len = name.length();
+		if (len > 3 && name.charAt(len - 3) == '_') {
+			if (len > 6 && name.charAt(len - 6) == '_') {
+				return name.substring(0, len - 6);
+			} else {
+				return name.substring(0, len - 3);
+			}
+		}
+		return name;
+	}
+
+	private String getLanguage(String filename) {
+		if (!filename.endsWith(PROPERTIES_SUFFIX))
+			throw new IllegalArgumentException();
+		String name = filename.substring(0, filename.length() - 11);
+		int len = name.length();
+		if (len > 3 && name.charAt(len - 3) == '_') {
+			if (len > 6 && name.charAt(len - 6) == '_') {
+				return name.substring(len - 5, len - 3);
+			} else {
+				return name.substring(len - 2);
+			}
+		}
+		return ""; //$NON-NLS-1$
+	}
+
+	private String getCountry(String filename) {
+		if (!filename.endsWith(PROPERTIES_SUFFIX))
+			throw new IllegalArgumentException();
+		String name = filename.substring(0, filename.length() - 11);
+		int len = name.length();
+		if (len > 3 && name.charAt(len - 3) == '_') {
+			if (len > 6 && name.charAt(len - 6) == '_') {
+				return name.substring(len - 2);
+			} else {
+				return ""; //$NON-NLS-1$
+			}
+		}
+		return ""; //$NON-NLS-1$
+	}
+
+	private ResourceBundleFamily getOrCreateFamily(String projectName, String pluginId, String packageName,
+			String baseName) {
+
+		// Ignore project name
+		if (pluginId != null)
+			projectName = null;
+
+		for (ResourceBundleFamily family : bundleFamilies) {
+			if (areEqual(family.getProjectName(), projectName)
+					&& areEqual(family.getPluginId(), pluginId)
+					&& areEqual(family.getPackageName(), packageName)
+					&& areEqual(family.getBaseName(), baseName)) {
+				return family;
+			}
+		}
+		ResourceBundleFamily family = new ResourceBundleFamily(
+			this,
+			projectName,
+			pluginId,
+			packageName,
+			baseName);
+		bundleFamilies.add(family);
+		return family;
+	}
+
+	private boolean isIgnoredFilename(String filename) {
+		return filename.equals("build.properties") || filename.equals("logging.properties"); //$NON-NLS-1$ //$NON-NLS-2$
+	}
+
+	private boolean areEqual(String str1, String str2) {
+		return str1 == null && str2 == null || str1 != null && str1.equals(str2);
+	}
+
+}
diff --git a/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/parser/CharArraySource.java b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/parser/CharArraySource.java
new file mode 100644
index 0000000..e111b69
--- /dev/null
+++ b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/parser/CharArraySource.java
@@ -0,0 +1,262 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.nls.internal.ui.parser;
+
+import java.io.IOException;
+import java.io.Reader;
+
+/**
+ * A scanner source that is backed by a character array.
+ */
+public class CharArraySource implements IScannerSource {
+
+	private char[] cbuf;
+
+	/** The end position of this source. */
+	private int end;
+
+	private int[] lineEnds = new int[2048];
+
+	/**
+	 * The current position at which the next character will be read.
+	 * The value <code>Integer.MAX_VALUE</code> indicates, that the end
+	 * of the source has been reached (the EOF character has been returned).
+	 */
+	int currentPosition = 0;
+
+	/** The number of the current line. (Line numbers are one-based.) */
+	int currentLineNumber = 1;
+
+	protected CharArraySource() {
+	}
+
+	/**
+	 * Constructs a scanner source from a char array.
+	 */
+	public CharArraySource(char[] cbuf) {
+		this.cbuf = cbuf;
+		this.end = cbuf.length;
+	}
+
+	/**
+	 * Resets this source on the given array.
+	 * 
+	 * @param cbuf the array to read from
+	 * @param begin where to begin reading
+	 * @param end where to end reading
+	 */
+	protected void reset(char[] cbuf, int begin, int end) {
+		if (cbuf == null) {
+			this.cbuf = null;
+			this.end = -1;
+			currentPosition = -1;
+			currentLineNumber = -1;
+			lineEnds = null;
+		} else {
+			this.cbuf = cbuf;
+			this.end = end;
+			currentPosition = begin;
+			currentLineNumber = 1;
+			lineEnds = new int[2];
+		}
+	}
+
+	/*
+	 * @see scanner.IScannerSource#charAt(int)
+	 */
+	public int charAt(int index) {
+		if (index < end) {
+			return cbuf[index];
+		} else {
+			return -1;
+		}
+	}
+
+	/*
+	 * @see scanner.IScannerSource#currentChar()
+	 */
+	public int lookahead() {
+		if (currentPosition < end) {
+			return cbuf[currentPosition];
+		} else {
+			return -1;
+		}
+	}
+
+	/*
+	 * @see scanner.IScannerSource#lookahead(int)
+	 */
+	public int lookahead(int n) {
+		int pos = currentPosition + n - 1;
+		if (pos < end) {
+			return cbuf[pos];
+		} else {
+			return -1;
+		}
+	}
+
+	/*
+	 * @see core.IScannerSource#readChar()
+	 */
+	public int readChar() {
+		if (currentPosition < end) {
+			return cbuf[currentPosition++];
+		} else {
+			currentPosition++;
+			return -1;
+		}
+	}
+
+	/*
+	 * @see core.IScannerSource#readChar(int)
+	 */
+	public int readChar(int expected) {
+		int c = readChar();
+		if (c == expected) {
+			return c;
+		} else {
+			String message = "Expected char '"
+					+ (char) expected
+					+ "' (0x"
+					+ hexDigit((expected >> 4) & 0xf)
+					+ hexDigit(expected & 0xf)
+					+ ") but got '" + (char) c + "' (0x" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+					+ hexDigit((c >> 4) & 0xf)
+					+ hexDigit(c & 0xf)
+					+ ")"; //$NON-NLS-1$
+			throw new LexicalErrorException(this, message);
+		}
+	}
+
+	/*
+	 * @see scanner.IScannerSource#unreadChar()
+	 */
+	public void unreadChar() {
+		currentPosition--;
+	}
+
+	/*
+	 * @see core.IScannerSource#hasMoreChars()
+	 */
+	public boolean hasMoreChars() {
+		return currentPosition < end;
+	}
+
+	/*
+	 * @see scanner.IScannerSource#getPosition()
+	 */
+	public int getPosition() {
+		if (currentPosition < end)
+			return currentPosition;
+		else
+			return end;
+	}
+
+	/*
+	 * @see core.IScannerSource#isAtLineBegin()
+	 */
+	public boolean isAtLineBegin() {
+		return currentPosition == lineEnds[currentLineNumber - 1];
+	}
+
+	/*
+	 * @see scanner.IScannerSource#getCurrentLineNumber()
+	 */
+	public int getCurrentLineNumber() {
+		return currentLineNumber;
+	}
+
+	/*
+	 * @see scanner.IScannerSource#getCurrentColumnNumber()
+	 */
+	public int getCurrentColumnNumber() {
+		return currentPosition - lineEnds[currentLineNumber - 1] + 1;
+	}
+
+	/*
+	 * @see scanner.IScannerSource#getLineEnds()
+	 */
+	public int[] getLineEnds() {
+		return lineEnds;
+	}
+	/*
+	 * @see scanner.IScannerSource#pushLineSeparator()
+	 */
+	public void pushLineSeparator() {
+		if (currentLineNumber >= lineEnds.length) {
+			int[] newLineEnds = new int[lineEnds.length * 2];
+			System.arraycopy(lineEnds, 0, newLineEnds, 0, lineEnds.length);
+			lineEnds = newLineEnds;
+		}
+		lineEnds[currentLineNumber++] = currentPosition;
+	}
+
+	/*
+	 * @see scanner.IScannerSource#length()
+	 */
+	public int length() {
+		return cbuf.length;
+	}
+
+	/**
+	 * Returns a string that contains the characters of the source specified
+	 * by the range <code>beginIndex</code> and the current position as the
+	 * end index.
+	 * 
+	 * @param beginIndex
+	 * @return the String
+	 */
+	public String toString(int beginIndex) {
+		return toString(beginIndex, currentPosition);
+	}
+
+	/*
+	 * @see scanner.IScannerSource#toString(int, int)
+	 */
+	public String toString(int beginIndex, int endIndex) {
+		return new String(cbuf, beginIndex, endIndex - beginIndex);
+	}
+
+	/**
+	 * Returns the original character array that backs the scanner source.
+	 * All subsequent changes to the returned array will affect the scanner
+	 * source.
+	 * 
+	 * @return the array
+	 */
+	public char[] getArray() {
+		return cbuf;
+	}
+
+	/*
+	 * @see java.lang.Object#toString()
+	 */
+	public String toString() {
+		return "Line=" + getCurrentLineNumber() + ", Column=" + getCurrentColumnNumber(); //$NON-NLS-1$ //$NON-NLS-2$
+	}
+
+	private static char hexDigit(int digit) {
+		return "0123456789abcdef".charAt(digit); //$NON-NLS-1$
+	}
+
+	public static CharArraySource createFrom(Reader reader) throws IOException {
+		StringBuffer buffer = new StringBuffer();
+		int BUF_SIZE = 4096;
+		char[] array = new char[BUF_SIZE];
+		for (int read = 0; (read = reader.read(array, 0, BUF_SIZE)) > 0;) {
+			buffer.append(array, 0, read);
+		}
+		char[] result = new char[buffer.length()];
+		buffer.getChars(0, buffer.length(), result, 0);
+		return new CharArraySource(result);
+	}
+
+}
\ No newline at end of file
diff --git a/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/parser/IScannerSource.java b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/parser/IScannerSource.java
new file mode 100644
index 0000000..6ae6dce
--- /dev/null
+++ b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/parser/IScannerSource.java
@@ -0,0 +1,139 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.nls.internal.ui.parser;
+
+public interface IScannerSource {
+
+	/**
+	 * Returns the character at the specified position.
+	 * 
+	 * @param position the index of the character to be returned
+	 * @return the character at the specified position (0-based)
+	 */
+	public int charAt(int position);
+
+	/**
+	 * Returns the character that will be returned by the next call
+	 * to <code>readChar()</code>. Calling this method is equal to the
+	 * following calls:
+	 * <pre>
+	 *     lookahead(1)
+	 *     charAt(getPosition())
+	 * </pre>
+	 * 
+	 * @return the character at the current position
+	 */
+	public int lookahead();
+
+	/**
+	 * Returns the character at the given lookahead position. Calling this
+	 * method is equal to the following call:
+	 * <pre>
+	 *     charAt(currentPosition + n - 1)
+	 * </pre>
+	 * 
+	 * @param n the number of characters to look ahead
+	 * 
+	 * @return the character
+	 */
+	public int lookahead(int n);
+
+	/**
+	 * Reads a single character.
+	 * 
+	 * @return the character read, or -1 if the end of the source
+	 *     has been reached 
+	 */
+	public int readChar();
+	
+	/**
+	 * Reads a single character.
+	 * 
+	 * @param expected the expected character; if the character read does not
+	 *     match this character, a <code>LexcialErrorException</code> will be thrown
+	 * @return the character read, or -1 if the end of the source
+	 *     has been reached 
+	 */
+	public int readChar(int expected);
+	
+	/**
+	 * Unreads a single character. The current position will be decreased
+	 * by 1. If -1 has been read multiple times, it will be unread multiple
+	 * times.
+	 */
+	public void unreadChar();
+
+	/**
+	 * Retruns the current position of the source.
+	 * 
+	 * @return the position (0-based)
+	 */
+	public int getPosition();
+
+	/**
+	 * Returns <code>true</code> if the current position is at the beginning of a line.
+	 * 
+	 * @return <code>true</code> if the current position is at the beginning of a line
+	 */
+	public boolean isAtLineBegin();
+
+	/**
+	 * Returns the current line number. 
+	 * 
+	 * @return the current line number (1-based)
+	 */
+	public int getCurrentLineNumber();
+	
+	/**
+	 * Returns the current column number. 
+	 * 
+	 * @return the current column number (1-based)
+	 */
+	public int getCurrentColumnNumber(); 
+
+	/**
+	 * Records the next line end position. This method has to be called
+	 * just after the line separator has been read.
+	 * 
+	 * @see IScannerSource#getLineEnds()
+	 */
+	public void pushLineSeparator();
+
+	/**
+	 * Returns an array of the line end positions recorded so far. Each value points
+	 * to first character following the line end (and is thus an exclusive index
+	 * to the line end). By definition the value <code>lineEnds[0]</code> is 0.
+	 * <code>lineEnds[1]</code> contains the line end position of the first line.
+	 * 
+	 * @return an array containing the line end positions
+	 * 
+	 * @see IScannerSource#pushLineSeparator()
+	 */
+	public int[] getLineEnds();
+
+	/**
+	 * Returns <code>true</code> if more characters are available.
+	 * 
+	 * @return <code>true</code> if more characters are available
+	 */
+	public boolean hasMoreChars();
+
+	/**
+	 * Returns a String that contains the characters in the specified range
+	 * of the source.
+	 * 
+	 * @param beginIndex the beginning index, inclusive
+	 * @param endIndex the ending index, exclusive
+	 * @return the newly created <code>String</code>
+	 */
+	public String toString(int beginIndex, int endIndex);
+
+}
diff --git a/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/parser/LexicalErrorException.java b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/parser/LexicalErrorException.java
new file mode 100644
index 0000000..4b5439a
--- /dev/null
+++ b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/parser/LexicalErrorException.java
@@ -0,0 +1,80 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.nls.internal.ui.parser;
+
+/**
+ * Exception thrown by a scanner when encountering lexical errors.
+ */
+public class LexicalErrorException extends RuntimeException {
+
+	private int lineNumber;
+	private int columnNumber;
+
+	/**
+	 * Creates a <code>LexicalErrorException</code> without a detailed message. 
+	 * 
+	 * @param source the scanner source the error occured on  
+	 */
+	public LexicalErrorException(IScannerSource source) {
+		this(source.getCurrentLineNumber(), source.getCurrentColumnNumber(), null);
+	}
+
+	/**
+	 * @param source the scanner source the error occured on  
+	 * @param message the error message 
+	 */
+	public LexicalErrorException(IScannerSource source, String message) {
+		this(source.getCurrentLineNumber(), source.getCurrentColumnNumber(), message);
+	}
+
+	/**
+	 * @param line the number of the line where the error occured  
+	 * @param column the numer of the column where the error occured  
+	 * @param message the error message 
+	 */
+	public LexicalErrorException(int line, int column, String message) {
+		super("Lexical error (" + line + ", " + column + (message == null ? ")" : "): " + message)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
+		this.lineNumber = line;
+		this.columnNumber = column;
+	}
+
+	/**
+	 * Returns the line number where the error occured.
+	 * 
+	 * @return the line number where the error occured
+	 */
+	public int getLineNumber() {
+		return lineNumber;
+	}
+
+	/**
+	 * Returns the column number where the error occured.
+	 * 
+	 * @return the column number where the error occured
+	 */
+	public int getColumnNumber() {
+		return columnNumber;
+	}
+
+	public static LexicalErrorException unexpectedCharacter(IScannerSource source, int c) {
+		return new LexicalErrorException(source, "Unexpected character: '" //$NON-NLS-1$
+			+ (char) c
+			+ "' (0x" //$NON-NLS-1$
+			+ hexDigit((c >> 4) & 0xf)
+			+ hexDigit(c & 0xf)
+			+ ")"); //$NON-NLS-1$
+	}
+
+	private static char hexDigit(int digit) {
+		return "0123456789abcdef".charAt(digit); //$NON-NLS-1$
+	}
+
+}
diff --git a/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/parser/LocaleUtil.java b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/parser/LocaleUtil.java
new file mode 100644
index 0000000..d5ecba3
--- /dev/null
+++ b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/parser/LocaleUtil.java
@@ -0,0 +1,56 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.nls.internal.ui.parser;
+
+import java.util.Locale;
+import java.util.StringTokenizer;
+
+public class LocaleUtil {
+
+	private LocaleUtil() {
+	}
+
+	public static Locale parseLocale(String name) throws IllegalArgumentException {
+		String language = ""; //$NON-NLS-1$
+		String country = ""; //$NON-NLS-1$
+		String variant = ""; //$NON-NLS-1$
+
+		StringTokenizer tokenizer = new StringTokenizer(name, "_"); //$NON-NLS-1$
+		if (tokenizer.hasMoreTokens())
+			language = tokenizer.nextToken();
+		if (tokenizer.hasMoreTokens())
+			country = tokenizer.nextToken();
+		if (tokenizer.hasMoreTokens())
+			variant = tokenizer.nextToken();
+
+		if (!language.equals("") && language.length() != 2) //$NON-NLS-1$
+			throw new IllegalArgumentException();
+		if (!country.equals("") && country.length() != 2) //$NON-NLS-1$
+			throw new IllegalArgumentException();
+
+		if (!language.equals("")) { //$NON-NLS-1$
+			char l1 = language.charAt(0);
+			char l2 = language.charAt(1);
+			if (!('a' <= l1 && l1 <= 'z' && 'a' <= l2 && l2 <= 'z'))
+				throw new IllegalArgumentException();
+		}
+
+		if (!country.equals("")) { //$NON-NLS-1$
+			char c1 = country.charAt(0);
+			char c2 = country.charAt(1);
+			if (!('A' <= c1 && c1 <= 'Z' && 'A' <= c2 && c2 <= 'Z'))
+				throw new IllegalArgumentException();
+		}
+
+		return new Locale(language, country, variant);
+	}
+
+}
diff --git a/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/parser/RawBundle.java b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/parser/RawBundle.java
new file mode 100644
index 0000000..7958772
--- /dev/null
+++ b/org.eclipse.babel.editor/src/org/eclipse/pde/nls/internal/ui/parser/RawBundle.java
@@ -0,0 +1,322 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.pde.nls.internal.ui.parser;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.ArrayList;
+
+/**
+ * A class used to manipulate resource bundle files.
+ */
+public class RawBundle {
+
+	public static abstract class RawLine {
+		String rawData;
+		public RawLine(String rawData) {
+			this.rawData = rawData;
+		}
+		public String getRawData() {
+			return rawData;
+		}
+	}
+	public static class CommentLine extends RawLine {
+		public CommentLine(String line) {
+			super(line);
+		}
+	}
+	public static class EmptyLine extends RawLine {
+		public EmptyLine(String line) {
+			super(line);
+		}
+	}
+	public static class EntryLine extends RawLine {
+		String key;
+		public EntryLine(String key, String lineData) {
+			super(lineData);
+			this.key = key;
+		}
+	}
+
+	/**
+	 * The logical lines of the resource bundle.
+	 */
+	private ArrayList<RawLine> lines = new ArrayList<RawLine>();
+
+	public RawBundle() {
+	}
+
+	public EntryLine getEntryLine(String key) {
+		for (RawLine line : lines) {
+			if (line instanceof EntryLine) {
+				EntryLine entryLine = (EntryLine) line;
+				if (entryLine.key.equals(key))
+					return entryLine;
+			}
+		}
+		return null;
+	}
+
+	public void put(String key, String value) {
+
+		// Find insertion position
+		int size = lines.size();
+		int pos = -1;
+		for (int i = 0; i < size; i++) {
+			RawLine line = lines.get(i);
+			if (line instanceof EntryLine) {
+				EntryLine entryLine = (EntryLine) line;
+				int compare = key.compareToIgnoreCase(entryLine.key);
+				if (compare < 0) {
+					if (pos == -1) {
+						pos = i; // possible insertion position
+					}
+				} else if (compare > 0) {
+					continue;
+				} else if (key.equals(entryLine.key)) {
+					entryLine.rawData = key + "=" + escape(value) + "\r\n";
+					return;
+				} else {
+					pos = i; // possible insertion position
+				}
+			}
+		}
+		if (pos == -1)
+			pos = lines.size();
+
+		// Append new entry
+		lines.add(pos, new EntryLine(key, key + "=" + escape(value) + "\r\n"));
+	}
+
+	private String escape(String str) {
+		StringBuilder builder = new StringBuilder();
+		int len = str.length();
+		for (int i = 0; i < len; i++) {
+			char c = str.charAt(i);
+			switch (c) {
+				case ' ' :
+					if (i == 0) {
+						builder.append("\\ ");
+					} else {
+						builder.append(c);
+					}
+					break;
+				case '\t' :
+					builder.append("\\t");
+					break;
+				case '=' :
+				case ':' :
+				case '#' :
+				case '!' :
+				case '\\' :
+					builder.append('\\').append(c);
+					break;
+				default :
+					if (31 <= c && c <= 255) {
+						builder.append(c);
+					} else {
+						builder.append("\\u");
+						builder.append(hexDigit((c >> 12) & 0x0f));
+						builder.append(hexDigit((c >> 8) & 0x0f));
+						builder.append(hexDigit((c >> 4) & 0x0f));
+						builder.append(hexDigit(c & 0x0f));
+					}
+					break;
+			}
+		}
+		return builder.toString();
+	}
+
+	private static char hexDigit(int digit) {
+		return "0123456789ABCDEF".charAt(digit); //$NON-NLS-1$
+	}
+
+	public void writeTo(OutputStream out) throws IOException {
+		OutputStreamWriter writer = new OutputStreamWriter(out, "ISO-8859-1");
+		writeTo(writer);
+	}
+
+	public void writeTo(Writer writer) throws IOException {
+		for (RawLine line : lines) {
+			writer.write(line.rawData);
+		}
+	}
+
+	public static RawBundle createFrom(InputStream in) throws IOException {
+		IScannerSource source = CharArraySource.createFrom(new InputStreamReader(in, "ISO-8859-1"));
+		return RawBundle.createFrom(source);
+	}
+
+	public static RawBundle createFrom(Reader reader) throws IOException {
+		IScannerSource source = CharArraySource.createFrom(reader);
+		return RawBundle.createFrom(source);
+	}
+
+	public static RawBundle createFrom(IScannerSource source) throws IOException {
+		RawBundle rawBundle = new RawBundle();
+		StringBuilder builder = new StringBuilder();
+
+		while (source.hasMoreChars()) {
+			int begin = source.getPosition();
+			skipAllOf(" \t\u000c", source);
+
+			// Comment line
+			if (source.lookahead() == '#' || source.lookahead() == '!') {
+				skipToOneOf("\r\n", false, source);
+				consumeLineSeparator(source);
+				int end = source.getPosition();
+				String line = source.toString(begin, end);
+				rawBundle.lines.add(new CommentLine(line));
+				continue;
+			}
+
+			// Empty line
+			if (isAtLineEnd(source)) {
+				consumeLineSeparator(source);
+				int end = source.getPosition();
+				String line = source.toString(begin, end);
+				rawBundle.lines.add(new EmptyLine(line));
+				continue;
+			}
+
+			// Entry line
+			{
+				// Key
+				builder.setLength(0);
+				loop : while (source.hasMoreChars()) {
+					char c = (char) source.readChar();
+					switch (c) {
+						case ' ' :
+						case '\t' :
+						case '\u000c' :
+						case '=' :
+						case '\r' :
+						case '\n' :
+							break loop;
+						case '\\' :
+							source.unreadChar();
+							builder.append(readEscapedChar(source));
+							break;
+						default :
+							builder.append(c);
+							break;
+					}
+				}
+				String key = builder.toString();
+
+				// Value
+				int end = 0;
+				loop : while (source.hasMoreChars()) {
+					char c = (char) source.readChar();
+					switch (c) {
+						case '\r' :
+						case '\n' :
+							consumeLineSeparator(source);
+							end = source.getPosition();
+							break loop;
+						case '\\' :
+							if (isAtLineEnd(source)) {
+								consumeLineSeparator(source);
+							} else {
+								source.unreadChar();
+								readEscapedChar(source);
+							}
+							break;
+						default :
+							break;
+					}
+				}
+				if (end == 0)
+					end = source.getPosition();
+
+				String lineData = source.toString(begin, end);
+				EntryLine entryLine = new EntryLine(key, lineData);
+				rawBundle.lines.add(entryLine);
+			}
+		}
+
+		return rawBundle;
+	}
+
+	private static char readEscapedChar(IScannerSource source) {
+		source.readChar('\\');
+		char c = (char) source.readChar();
+		switch (c) {
+			case ' ' :
+			case '=' :
+			case ':' :
+			case '#' :
+			case '!' :
+			case '\\' :
+				return c;
+			case 't' :
+				return '\t';
+			case 'n' :
+				return '\n';
+			case 'u' :
+				int d1 = Character.digit(source.readChar(), 16);
+				int d2 = Character.digit(source.readChar(), 16);
+				int d3 = Character.digit(source.readChar(), 16);
+				int d4 = Character.digit(source.readChar(), 16);
+				if (d1 == -1 || d2 == -1 || d3 == -1 || d4 == -1)
+					throw new LexicalErrorException(source, "Illegal escape sequence");
+				return (char) (d1 << 12 | d2 << 8 | d3 << 4 | d4);
+			default :
+				throw new LexicalErrorException(source, "Unknown escape sequence");
+		}
+	}
+
+	private static boolean isAtLineEnd(IScannerSource source) {
+		return source.lookahead() == '\r' || source.lookahead() == '\n';
+	}
+
+	private static void consumeLineSeparator(IScannerSource source) {
+		if (source.lookahead() == '\n') {
+			source.readChar();
+			source.pushLineSeparator();
+		} else if (source.lookahead() == '\r') {
+			source.readChar();
+			if (source.lookahead() == '\n') {
+				source.readChar();
+			}
+			source.pushLineSeparator();
+		}
+	}
+	
+	private static void skipToOneOf(String delimiters, boolean readDelimiter, IScannerSource source) {
+		loop : while (source.hasMoreChars()) {
+			int c = source.readChar();
+			if (delimiters.indexOf(c) != -1) {
+				if (!readDelimiter) {
+					source.unreadChar();
+				}
+				break loop;
+			}
+			if (c == '\r') {
+				source.readChar('\n');
+				source.pushLineSeparator();
+			}
+		}
+	}
+
+	private static void skipAllOf(String string, IScannerSource source) {
+		while (source.hasMoreChars() && string.indexOf(source.lookahead()) != -1) {
+			source.readChar();
+		}
+	}
+	
+
+}
diff --git a/org.eclipse.babel.editor/tests/org/eclipse/nls/ui/tests/PropertiesTest.java b/org.eclipse.babel.editor/tests/org/eclipse/nls/ui/tests/PropertiesTest.java
new file mode 100644
index 0000000..8e33143
--- /dev/null
+++ b/org.eclipse.babel.editor/tests/org/eclipse/nls/ui/tests/PropertiesTest.java
@@ -0,0 +1,65 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.nls.ui.tests;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Properties;
+
+import junit.framework.TestCase;
+
+public class PropertiesTest extends TestCase {
+
+	private Properties properties;
+
+	public void test() throws IOException {
+		String input = "   # a comment line\r\n"
+				+ "key1=value1\r\n"
+				+ "key2=a value \\\r\n"
+				+ "     on two lines\r\n"
+				+ "key3=a value \\\r\n"
+				+ "     "
+				+ "     with an empty line in between\r\n";
+		properties = readProperties(input);
+		assertValue("value1", "key1");
+		assertValue("a value on two lines", "key2");
+		assertValue("a value with an empty line in between", "key3");
+	}
+
+	public void testKeysWithWhitespace() throws IOException {
+		String input = ""
+				+ "key1\t=key with tab\r\n"
+				+ "key\\ 2 =key with escaped space\r\n"
+				+ "key 3 =key with space\r\n";
+		properties = readProperties(input);
+		assertValue("key with tab", "key1");
+		assertValue("key with escaped space", "key 2");
+		assertValue("3 =key with space", "key");
+	}
+
+	public void testKeysWithMissingValue() throws IOException {
+		String input = "" + "keyWithoutValue\r\n" + "key=value\r\n";
+		properties = readProperties(input);
+		assertValue("", "keyWithoutValue");
+		assertValue("value", "key");
+	}
+
+	private void assertValue(String expected, String key) {
+		assertEquals(expected, properties.get(key));
+	}
+
+	private Properties readProperties(String input) throws IOException {
+		Properties properties = new Properties();
+		properties.load(new StringReader(input));
+		return properties;
+	}
+
+}
diff --git a/org.eclipse.babel.editor/tests/org/eclipse/nls/ui/tests/RawBundleTest.java b/org.eclipse.babel.editor/tests/org/eclipse/nls/ui/tests/RawBundleTest.java
new file mode 100644
index 0000000..579ee4d
--- /dev/null
+++ b/org.eclipse.babel.editor/tests/org/eclipse/nls/ui/tests/RawBundleTest.java
@@ -0,0 +1,84 @@
+/*******************************************************************************
+ * Copyright (c) 2008 Stefan Mücke and others.
+ * 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:
+ *     Stefan Mücke - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.nls.ui.tests;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import junit.framework.TestCase;
+
+import org.eclipse.pde.nls.internal.ui.parser.RawBundle;
+import org.eclipse.pde.nls.internal.ui.parser.RawBundle.EntryLine;
+
+public class RawBundleTest extends TestCase {
+
+	private RawBundle rawBundle;
+
+	public void test() throws IOException {
+		String input = "   # a comment line\r\n"
+				+ "key1=value1\r\n"
+				+ "key2=a value \\\r\n"
+				+ "     on two lines\r\n"
+				+ "key3=a value \\\r\n"
+				+ "     "
+				+ "     with an empty line in between\r\n";
+		rawBundle = readRawBundle(input);
+		assertRawData("key1=value1\r\n", "key1");
+		assertRawData("key2=a value \\\r\n" + "     on two lines\r\n", "key2");
+		assertRawData("key3=a value \\\r\n" + "     " + "     with an empty line in between\r\n", "key3");
+	}
+
+	public void testKeysWithWhitespace() throws IOException {
+		String input = ""
+				+ "key1\t=key with tab\r\n"
+				+ "key\\ 2 =key with escaped space\r\n"
+				+ "key 3 =key with space\r\n";
+		rawBundle = readRawBundle(input);
+		assertRawData("key1\t=key with tab\r\n", "key1");
+		assertRawData("key\\ 2 =key with escaped space\r\n", "key 2");
+		assertRawData("key 3 =key with space\r\n", "key");
+	}
+
+	public void testKeysWithMissingValue() throws IOException {
+		String input = "keyWithoutValue\r\n" + "key=value\r\n";
+		rawBundle = readRawBundle(input);
+		assertRawData("keyWithoutValue\r\n", "keyWithoutValue");
+		assertRawData("key=value\r\n", "key");
+	}
+
+	public void testPut() throws IOException {
+		String input = "" + "key1=value1\r\n" + "key3=value3\r\n";
+		rawBundle = readRawBundle(input);
+		rawBundle.put("key2", "value2");
+		rawBundle.put("key4", "value4");
+		rawBundle.put("key0", "value0\\\t");
+		StringWriter stringWriter = new StringWriter();
+		rawBundle.writeTo(stringWriter);
+		assertEquals(""
+				+ "key0=value0\\\\\\t\r\n"
+				+ "key1=value1\r\n"
+				+ "key2=value2\r\n"
+				+ "key3=value3\r\n"
+				+ "key4=value4\r\n", stringWriter.toString());
+
+	}
+	
+	private void assertRawData(String expected, String key) {
+		EntryLine entryLine = rawBundle.getEntryLine(key);
+		assertEquals(expected, entryLine.getRawData());
+	}
+
+	private RawBundle readRawBundle(String input) throws IOException {
+		return RawBundle.createFrom(new StringReader(input));
+	}
+
+}