package org.olat.core.util.i18n.ui;
/**
* OLAT - Online Learning and Training<br>
* http://www.olat.org
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Copyright (c) frentix GmbH<br>
* http://www.frentix.com<br>
* <p>
*/
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang.StringEscapeUtils;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.Component;
import org.olat.core.gui.components.ComponentRenderer;
import org.olat.core.gui.components.delegating.DelegatingComponent;
import org.olat.core.gui.components.panel.Panel;
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.Event;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.gui.control.controller.BasicController;
import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController;
import org.olat.core.gui.render.RenderResult;
import org.olat.core.gui.render.Renderer;
import org.olat.core.gui.render.RenderingState;
import org.olat.core.gui.render.StringOutput;
import org.olat.core.gui.render.URLBuilder;
import org.olat.core.gui.render.intercept.InterceptHandler;
import org.olat.core.gui.render.intercept.InterceptHandlerInstance;
import org.olat.core.gui.translator.Translator;
import org.olat.core.util.StringHelper;
import org.olat.core.util.i18n.I18nItem;
import org.olat.core.util.i18n.I18nManager;
import org.olat.core.util.i18n.I18nModule;
import org.olat.core.util.prefs.Preferences;
/**
* Description:<br>
* This class acts both as the render intercepter and as the inline translation
* tool dispatcher. For each detected translated GUI element it will add a hover
* event which triggers an edit link.
* <p>
* When the server is configured as translation server, the inline translation
* tool will start in language translation mode. Otherwhise it will start in
* language customizing mode (overlay edit)
*
* <P>
* Initial Date: 16.09.2008 <br>
*
* @author gnaegi
*/
public class InlineTranslationInterceptHandlerController extends BasicController implements InterceptHandlerInstance, InterceptHandler {
private static final String SPAN_TRANSLATION_I18NITEM_OPEN = "<span class=\"b_translation_i18nitem\">";
private static final String SPAN_CLOSE = "</span>";
private static final String BODY_TAG = "<body";
private static final String ARG_BUNDLE = "bundle";
private static final String ARG_KEY = "key";
private static final String ARG_IDENT = "id";
private URLBuilder inlineTranslationURLBuilder;
private DelegatingComponent delegatingComponent;
private TranslationToolI18nItemEditCrumbController i18nItemEditCtr;
private CloseableModalController cmc;
private Panel mainP;
// patterns to detect localized strings with identifyers
private static final String decoratedTranslatedPattern = "(" + I18nManager.IDENT_PREFIX + "(.*?)" + I18nManager.IDENT_START_POSTFIX
+ ").*?(" + I18nManager.IDENT_PREFIX + "\\2" + I18nManager.IDENT_END_POSTFIX + ")";
private static final Pattern patternLink = Pattern.compile("<a[^>]*?>(?:<span[^>]*?>)*?[^<>]*?" + decoratedTranslatedPattern
+ "[^<>]*?(?:</span>*?>)*?</a>");
private static final Pattern patternInput = Pattern.compile("<input[^>]*?" + decoratedTranslatedPattern + ".*?>");
private static final Pattern patAttribute = Pattern.compile("<[^>]*?" + decoratedTranslatedPattern + "[^>]*?>");
/**
* Constructor
*
* @param ureq
* @param control
*/
InlineTranslationInterceptHandlerController(UserRequest ureq, WindowControl control) {
super(ureq, control);
// the deleagating component is ony used to provide the
// inlineTranslationURLBuilder to be able to create the translation tool
// links
delegatingComponent = new DelegatingComponent("delegatingComponent", new ComponentRenderer() {
public void render(Renderer renderer, StringOutput sb, Component source, URLBuilder ubu, Translator translator,
RenderResult renderResult, String[] args) {
// save urlbuilder for later use (valid only for one
// request scope thus
// transient, normally you may not save the url builder
// for later usage)
inlineTranslationURLBuilder = ubu;
}
public void renderHeaderIncludes(Renderer renderer, StringOutput sb, Component source, URLBuilder ubu, Translator translator,
RenderingState rstate) {
// void
}
public void renderBodyOnLoadJSFunctionCall(Renderer renderer, StringOutput sb, Component source, RenderingState rstate) {
// trigger js method that adds hover events - in some conditions method is not available (in iframes)
sb.append("if (Object.isFunction(b_attach_i18n_inline_editing)) {b_attach_i18n_inline_editing();}");
}
});
delegatingComponent.addListener(this);
delegatingComponent.setDomReplaceable(false);
mainP = putInitialPanel(delegatingComponent);
mainP.setDomReplaceable(false);
}
/**
* @see org.olat.core.gui.render.intercept.InterceptHandler#createInterceptHandlerInstance()
*/
public InterceptHandlerInstance createInterceptHandlerInstance() {
return this;
}
public ComponentRenderer createInterceptComponentRenderer(final ComponentRenderer originalRenderer) {
return new ComponentRenderer() {
public void render(Renderer renderer, StringOutput sb, Component source, URLBuilder ubu, Translator translator,
RenderResult renderResult, String[] args) {
// ------------- show translator keys
// we must let the original renderer do its work so that the
// collecting translator is callbacked.
// we save the result in a new var since it is too early to
// append it
// to the 'stream' right now.
StringOutput sbOrig = new StringOutput();
try {
originalRenderer.render(renderer, sbOrig, source, ubu, translator, renderResult, args);
} catch (Exception e) {
String emsg = "exception while rendering component '" + source.getComponentName() + "' (" + source.getClass().getName() + ") "
+ source.getListenerInfo() + "<br />Message of exception: " + e.getMessage();
sbOrig.append("<span style=\"color:red\">Exception</span><br /><pre>" + emsg + "</pre>");
}
String rendered = sbOrig.toString();
String renderedWithHTMLMarkup = InlineTranslationInterceptHandlerController.replaceLocalizationMarkupWithHTML(rendered,
inlineTranslationURLBuilder, getTranslator());
sb.append(renderedWithHTMLMarkup);
}
/**
* @see org.olat.core.gui.components.ComponentRenderer#renderHeaderIncludes(org.olat.core.gui.render.Renderer,
* org.olat.core.gui.render.StringOutput,
* org.olat.core.gui.components.Component,
* org.olat.core.gui.render.URLBuilder,
* org.olat.core.gui.translator.Translator,
* org.olat.core.gui.render.RenderingState)
*/
public void renderHeaderIncludes(Renderer renderer, StringOutput sb, Component source, URLBuilder ubu, Translator translator,
RenderingState rstate) {
originalRenderer.renderHeaderIncludes(renderer, sb, source, ubu, translator, rstate);
}
/**
* @see org.olat.core.gui.components.ComponentRenderer#renderBodyOnLoadJSFunctionCall(org.olat.core.gui.render.Renderer,
* org.olat.core.gui.render.StringOutput,
* org.olat.core.gui.components.Component,
* org.olat.core.gui.render.RenderingState)
*/
public void renderBodyOnLoadJSFunctionCall(Renderer renderer, StringOutput sb, Component source, RenderingState rstate) {
originalRenderer.renderBodyOnLoadJSFunctionCall(renderer, sb, source, rstate);
}
};
}
/**
* @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest,
* org.olat.core.gui.components.Component,
* org.olat.core.gui.control.Event)
*/
protected void event(UserRequest ureq, Component source, Event event) {
if (source == delegatingComponent) {
String bundle = ureq.getParameter(ARG_BUNDLE);
String key = ureq.getParameter(ARG_KEY);
// The argument ARG_IDENT is not used for dispatching right now
if (isLogDebugEnabled()) {
logDebug("Got event to launch inline translation tool for bundle::" + bundle + " and key::" + key, null);
}
if (StringHelper.containsNonWhitespace(bundle) && StringHelper.containsNonWhitespace(key)) {
// Get userconfigured reference locale
Preferences guiPrefs = ureq.getUserSession().getGuiPreferences();
List<String> referenceLangs = I18nModule.getTransToolReferenceLanguages();
String referencePrefs = (String) guiPrefs.get(I18nModule.class, I18nModule.GUI_PREFS_PREFERRED_REFERENCE_LANG, referenceLangs
.get(0));
I18nManager i18nMgr = I18nManager.getInstance();
Locale referenceLocale = i18nMgr.getLocaleOrNull(referencePrefs);
// Set target local to current user language
Locale targetLocale = i18nMgr.getLocaleOrNull(ureq.getLocale().toString());
if (I18nModule.isOverlayEnabled() && !I18nModule.isTransToolEnabled()) {
// use overlay locale when in customizing mode
targetLocale = I18nModule.getOverlayLocales().get(targetLocale);
}
List<I18nItem> i18nItems = i18nMgr.findExistingAndMissingI18nItems(referenceLocale, targetLocale, bundle, false);
i18nMgr.sortI18nItems(i18nItems, true, true); // sort with priority
// Initialize inline translation controller
if (i18nItemEditCtr != null) removeAsListenerAndDispose(i18nItemEditCtr);
// Disable inline translation markup while inline translation tool is
// running -
// must be done before instantiating the translation controller
i18nMgr.setMarkLocalizedStringsEnabled(ureq.getUserSession(), false);
i18nItemEditCtr = new TranslationToolI18nItemEditCrumbController(ureq, getWindowControl(), i18nItems, referenceLocale, !I18nModule.isTransToolEnabled());
listenTo(i18nItemEditCtr);
// set current key from the package as current translation item
for (I18nItem item : i18nItems) {
if (item.getKey().equals(key)) {
i18nItemEditCtr.initialzeI18nitemAsCurrentItem(ureq, item);
break;
}
}
// Open in modal window
if (cmc != null) removeAsListenerAndDispose(cmc);
cmc = new CloseableModalController(getWindowControl(), "close", i18nItemEditCtr.getInitialComponent());
listenTo(cmc);
cmc.activate();
} else {
logError("Can not launch inline translation tool, bundle or key empty! bundle::" + bundle + " key::" + key, null);
}
}
}
/**
* @see org.olat.core.gui.control.DefaultController#event(org.olat.core.gui.UserRequest,
* org.olat.core.gui.control.Controller, org.olat.core.gui.control.Event)
*/
protected void event(UserRequest ureq, Controller source, @SuppressWarnings("unused") Event event) {
if (source == cmc) {
// user closed dialog, go back to inline translation mode
I18nManager.getInstance().setMarkLocalizedStringsEnabled(ureq.getUserSession(), true);
}
}
/**
* @see org.olat.core.gui.control.DefaultController#doDispose()
*/
protected void doDispose() {
// controllers autodisposed by basic controller
inlineTranslationURLBuilder = null;
delegatingComponent = null;
i18nItemEditCtr = null;
cmc = null;
}
/**
* Helper method to replace the translations that are wrapped with some
* identifyer markup by the translator with HTML markup to allow inline
* editing.
* <p>
* This method is public and static to be testable with jUnit.
*
* @param stringWithMarkup The text that contains translated elements that are
* wrapped with some identifyers
* @param inlineTranslationURLBuilder URI builder used to create the inline
* translation links
* @param inlineTrans
* @return
*/
public static String replaceLocalizationMarkupWithHTML(String stringWithMarkup, URLBuilder inlineTranslationURLBuilder,
Translator inlineTrans) {
while (stringWithMarkup.indexOf(I18nManager.IDENT_PREFIX) != -1) {
// calculate positions of next localization identifyer
int startSPos = stringWithMarkup.indexOf(I18nManager.IDENT_PREFIX);
int startPostfixPos = stringWithMarkup.indexOf(I18nManager.IDENT_START_POSTFIX);
String combinedKey = stringWithMarkup.substring(startSPos + I18nManager.IDENT_PREFIX.length(), startPostfixPos);
int startEPos = startPostfixPos + I18nManager.IDENT_START_POSTFIX.length();
String endIdent = I18nManager.IDENT_PREFIX + combinedKey + I18nManager.IDENT_END_POSTFIX;
int endSPos = stringWithMarkup.indexOf(endIdent);
int endEPos = endSPos + endIdent.length();
// build link for this identifyer
StringOutput link = new StringOutput();
buildInlineTranslationLink(combinedKey, link, inlineTrans, inlineTranslationURLBuilder);
// Case 1: translated within a 'a' tag. The tag can contain an optional
// span tag
// before and after translated link some other content could be
// No support for i18n text that does contain HTML markup
Matcher m = patternLink.matcher(stringWithMarkup);
boolean foundPos = m.find();
int wrapperOpen = 0;
int wrapperClose = 0;
if (foundPos) {
wrapperOpen = m.start(0);
wrapperClose = m.end(0);
// check if found position does belong to start position
if (wrapperOpen > startSPos) {
foundPos = false;
} else {
// check if link is visible, skip other links
int skipPos = stringWithMarkup.indexOf("b_skip", wrapperOpen);
if (skipPos > -1 && skipPos < wrapperClose) {
stringWithMarkup = replaceItemWithoutHTMLMarkup(stringWithMarkup, startSPos, startEPos, endSPos, endEPos);
continue;
}
// found a valid link pattern, replace it
stringWithMarkup = replaceItemWithHTMLMarkupSurrounded(stringWithMarkup, link, startSPos, startEPos, endSPos, endEPos,
wrapperOpen, wrapperClose);
continue;
}
}
// Case 2: translated within an 'input' tag
if (!foundPos) {
m = patternInput.matcher(stringWithMarkup);
foundPos = m.find();
if (foundPos) {
wrapperOpen = m.start(0);
wrapperClose = m.end(0);
// check if found position does belong to start position
if (wrapperOpen > startSPos) foundPos = false;
else {
// ignore within a checkbox
int checkboxPos = stringWithMarkup.indexOf("checkbox", wrapperOpen);
if (checkboxPos != -1 && checkboxPos < startSPos) {
stringWithMarkup = replaceItemWithoutHTMLMarkup(stringWithMarkup, startSPos, startEPos, endSPos, endEPos);
continue;
}
// ignore within a radio button
int radioPos = stringWithMarkup.indexOf("radio", wrapperOpen);
if (radioPos != -1 && radioPos < startSPos) {
stringWithMarkup = replaceItemWithoutHTMLMarkup(stringWithMarkup, startSPos, startEPos, endSPos, endEPos);
continue;
}
// found a valid input pattern, replace it
stringWithMarkup = replaceItemWithHTMLMarkupSurrounded(stringWithMarkup, link, startSPos, startEPos, endSPos, endEPos,
wrapperOpen, wrapperClose);
continue;
}
}
}
// Case 3: translated within a tag attribute of an element - don't offer
// inline translation
m = patAttribute.matcher(stringWithMarkup);
foundPos = m.find();
if (foundPos) {
wrapperOpen = m.start(0);
wrapperClose = m.end(0);
// check if found position does belong to start position
if (wrapperOpen > startSPos) foundPos = false;
else {
// found a patter in within an attribute, skip this one
stringWithMarkup = replaceItemWithoutHTMLMarkup(stringWithMarkup, startSPos, startEPos, endSPos, endEPos);
continue;
}
}
// Case 4: i18n element in html head - don't offer inline translation
if (startSPos < stringWithMarkup.indexOf(BODY_TAG)) {
// found a pattern in the HTML head, skip this one
stringWithMarkup = replaceItemWithoutHTMLMarkup(stringWithMarkup, startSPos, startEPos, endSPos, endEPos);
continue;
}
// Case 4: default case: normal translation, surround with inline
// translation link
StringBuffer tmp = new StringBuffer();
tmp.append(stringWithMarkup.substring(0, startSPos));
tmp.append(SPAN_TRANSLATION_I18NITEM_OPEN);
tmp.append(link);
tmp.append(stringWithMarkup.substring(startEPos, endSPos));
tmp.append(SPAN_CLOSE);
tmp.append(stringWithMarkup.substring(endEPos));
stringWithMarkup = tmp.toString();
}
return stringWithMarkup;
}
/**
* Internal helper to add the html markup surrounding the parent element
*
* @param stringWithMarkup
* @param link
* @param startSPos
* @param startEPos
* @param endSPos
* @param endEPos
* @param wrapperOpen
* @param wrapperClose
* @return
*/
private static String replaceItemWithHTMLMarkupSurrounded(String stringWithMarkup, StringOutput link, int startSPos, int startEPos,
int endSPos, int endEPos, int wrapperOpen, int wrapperClose) {
StringBuffer tmp = new StringBuffer();
tmp.append(stringWithMarkup.substring(0, wrapperOpen));
tmp.append(SPAN_TRANSLATION_I18NITEM_OPEN);
tmp.append(link);
tmp.append(stringWithMarkup.substring(wrapperOpen, startSPos));
tmp.append(stringWithMarkup.substring(startEPos, endSPos));
tmp.append(stringWithMarkup.substring(endEPos, wrapperClose));
tmp.append(SPAN_CLOSE);
tmp.append(stringWithMarkup.substring(wrapperClose));
return tmp.toString();
}
/**
* Internal helper to remove the localization identifyers from the code
* without adding html markup
*
* @param stringWithMarkup
* @param startSPos
* @param startEPos
* @param endSPos
* @param endEPos
* @return
*/
private static String replaceItemWithoutHTMLMarkup(String stringWithMarkup, int startSPos, int startEPos, int endSPos, int endEPos) {
StringBuffer tmp = new StringBuffer();
tmp.append(stringWithMarkup.substring(0, startSPos));
tmp.append(stringWithMarkup.substring(startEPos, endSPos));
tmp.append(stringWithMarkup.substring(endEPos));
return tmp.toString();
}
/**
* Helper method to build the inline translation link.
* <p>
* Public and static so that it can be used by the jUnit testcase
*
* @param combinedKeyWithID e.g. bundle.name:key.name:ramuniqueid
* @param link
* @param inlineTrans
* @param inlineTranslationURLBuilder
*/
public static void buildInlineTranslationLink(String combinedKeyWithID, StringOutput link, Translator inlineTrans,
URLBuilder inlineTranslationURLBuilder) {
link.append("<a class=\"b_translation_i18nitem_launcher\" style=\"display:none\" href=\"");
inlineTranslationURLBuilder.buildURI(link, new String[] { ARG_BUNDLE, ARG_KEY, ARG_IDENT }, combinedKeyWithID.split(":"));
String combinedKey = combinedKeyWithID ;//.substring(0, combinedKeyWithID.lastIndexOf(":"));
link.append("\" title=\"");
if (I18nModule.isTransToolEnabled()) {
link.append(StringEscapeUtils.escapeHtml(inlineTrans.translate("inline.translate", new String[] { combinedKey })));
} else {
link.append(StringEscapeUtils.escapeHtml(inlineTrans.translate("inline.customize.translate", new String[] { combinedKey })));
}
link.append("\"></a>");
}
}