class FalseFriendRuleHandler extends XMLRuleHandler {

  // Definitions of values in XML files:
  private static final String TRANSLATION = "translation";

  private final ResourceBundle englishMessages;
  private final ResourceBundle messages;
  private final MessageFormat formatter;
  private final Language textLanguage;
  private final Language motherTongue;
  private final Map<String, List<String>> suggestionMap = new HashMap<>();  // rule ID -> list of translations
  private final List<String> suggestions = new ArrayList<>();
  private final List<StringBuilder> translations = new ArrayList<>();
  private final String falseFriendHint;

  private boolean defaultOff;
  private Language language;
  private Language translationLanguage;
  private Language currentTranslationLanguage;
  private StringBuilder translation = new StringBuilder();
  private boolean inTranslation;

  FalseFriendRuleHandler(Language textLanguage, Language motherTongue, String falseFriendHint) {
    englishMessages = ResourceBundleTools.getMessageBundle(Languages.getLanguageForShortCode("en-US"));
    messages = ResourceBundleTools.getMessageBundle(motherTongue);
    formatter = new MessageFormat("");
    formatter.setLocale(motherTongue.getLocale());
    this.textLanguage = textLanguage;
    this.motherTongue = motherTongue;
    this.falseFriendHint = falseFriendHint;
  }

  public Map<String, List<String>> getSuggestionMap() {
    return suggestionMap;
  }

  // ===========================================================
  // SAX DocumentHandler methods
  // ===========================================================

  @Override
  public void startElement(String namespaceURI, String lName,
      String qName, Attributes attrs) throws SAXException {
    if (qName.equals(RULE)) {
      translations.clear();
      id = attrs.getValue("id");
      if (!(inRuleGroup && defaultOff)) {
        defaultOff = "off".equals(attrs.getValue("default"));
      }
      if (inRuleGroup && id == null) {
        id = ruleGroupId;
      }
      correctExamples = new ArrayList<>();
      incorrectExamples = new ArrayList<>();
    } else if (qName.equals(PATTERN)) {
      inPattern = true;
      String languageStr = attrs.getValue("lang");
      if (Languages.isLanguageSupported(languageStr)) {
        language = Languages.getLanguageForShortCode(languageStr);
      }
    } else if (qName.equals(TOKEN)) {
      setToken(attrs);
    } else if (qName.equals(TRANSLATION)) {
      inTranslation = true;
      String languageStr = attrs.getValue("lang");
      if (Languages.isLanguageSupported(languageStr)) {
        Language tmpLang = Languages.getLanguageForShortCode(languageStr);
        currentTranslationLanguage = tmpLang;
        if (tmpLang.equalsConsiderVariantsIfSpecified(motherTongue)) {
          translationLanguage = tmpLang;
        }
      }
    } else if (qName.equals(EXAMPLE)) {
      correctExample = new StringBuilder();
      incorrectExample = new StringBuilder();
      if (attrs.getValue(TYPE).equals("incorrect")) {
        inIncorrectExample = true;
      } else if (attrs.getValue(TYPE).equals("correct")) {
        inCorrectExample = true;
      } else if (attrs.getValue(TYPE).equals("triggers_error")) {
        throw new RuntimeException("'triggers_error' is not supported for false friend XML");
      }
    } else if (qName.equals(MESSAGE)) {
      inMessage = true;
      message = new StringBuilder();
    } else if (qName.equals(RULEGROUP)) {
      ruleGroupId = attrs.getValue("id");
      inRuleGroup = true;
      defaultOff = "off".equals(attrs.getValue(DEFAULT));
    }
  }

  @Override
  public void endElement(String namespaceURI, String sName,
      String qName) throws SAXException {
    switch (qName) {
      case RULE:
        if (language.equalsConsiderVariantsIfSpecified(textLanguage) && translationLanguage != null
                && translationLanguage.equalsConsiderVariantsIfSpecified(motherTongue) && language != motherTongue
                && !translations.isEmpty()) {
          formatter.applyPattern(falseFriendHint);
          String tokensAsString = StringUtils.join(patternTokens, " ").replace('|', '/');
          Object[] messageArguments = {tokensAsString,
                  englishMessages.getString(textLanguage.getShortCode()),
                  formatTranslations(translations),
                  englishMessages.getString(motherTongue.getShortCode())};
          String description = formatter.format(messageArguments);
          PatternRule rule = new FalseFriendPatternRule(id, language, patternTokens,
                  messages.getString("false_friend_desc") + " "
                          + tokensAsString, description, messages.getString("false_friend"));
          rule.setCorrectExamples(correctExamples);
          rule.setIncorrectExamples(incorrectExamples);
          rule.setCategory(Categories.FALSE_FRIENDS.getCategory(messages));
          if (defaultOff) {
            rule.setDefaultOff();
          }
          rules.add(rule);
        }
        if (patternTokens != null) {
          patternTokens.clear();
        }
        break;
      case TOKEN:
        finalizeTokens(language.getUnifierConfiguration());
        break;
      case PATTERN:
        inPattern = false;
        break;
      case TRANSLATION:
        if (currentTranslationLanguage != null && currentTranslationLanguage.equalsConsiderVariantsIfSpecified(motherTongue)) {
          // currentTranslationLanguage can be null if the language is not supported
          translations.add(translation);
        }
        if (currentTranslationLanguage != null && currentTranslationLanguage.equalsConsiderVariantsIfSpecified(textLanguage)
                && language.equalsConsiderVariantsIfSpecified(motherTongue) && !suggestions.contains(translation.toString())) {
          suggestions.add(translation.toString());
        }
        translation = new StringBuilder();
        inTranslation = false;
        currentTranslationLanguage = null;
        break;
      case EXAMPLE:
        if (inCorrectExample) {
          correctExamples.add(new CorrectExample(correctExample.toString()));
        } else if (inIncorrectExample) {
          incorrectExamples.add(new IncorrectExample(incorrectExample.toString()));
        }
        inCorrectExample = false;
        inIncorrectExample = false;
        correctExample = new StringBuilder();
        incorrectExample = new StringBuilder();
        break;
      case MESSAGE:
        inMessage = false;
        break;
      case RULEGROUP:
        if (!suggestions.isEmpty()) {
          List<String> l = new ArrayList<>(suggestions);
          suggestionMap.put(id, l);
          suggestions.clear();
        }
        inRuleGroup = false;
        break;
    }
  }

  private String formatTranslations(List<StringBuilder> translations) {
    return translations.stream().map(o -> "\"" + o + "\"").collect(Collectors.joining(", "));
  }

  @Override
  public void characters(char[] buf, int offset, int len) {<FILL_FUNCTION_BODY>}

}

    String s = new String(buf, offset, len);
    if (inToken && inPattern) {
      elements.append(s);
    } else if (inCorrectExample) {
      correctExample.append(s);
    } else if (inIncorrectExample) {
      incorrectExample.append(s);
    } else if (inTranslation) {
      translation.append(s);
    }
  
<methods>public void <init>() ,public void error(org.xml.sax.SAXParseException) throws org.xml.sax.SAXException,public List<org.languagetool.rules.patterns.AbstractPatternRule> getRules() ,public void setDocumentLocator(org.xml.sax.Locator) ,public void warning(org.xml.sax.SAXParseException) throws org.xml.sax.SAXException<variables>protected static final java.lang.String AND,protected static final java.lang.String ANTIPATTERN,protected static final java.lang.String CASE_SENSITIVE,protected static final java.lang.String CHUNKTAG,protected static final java.lang.String CHUNKTAG_REGEXP,protected static final java.lang.String DEFAULT,protected static final java.lang.String DISTANCETOKENS,protected static final java.lang.String EXAMPLE,protected static final java.lang.String EXCEPTION,protected static final java.lang.String FALSE,protected static final java.lang.String FEATURE,protected static final java.lang.String GOAL_SPECIFIC,public static final java.lang.String ID,protected static final java.lang.String IGNORE,protected static final java.lang.String INFLECTED,protected static final java.lang.String MARK,protected static final java.lang.String MARKER,protected static final java.lang.String MATCH,protected static final java.lang.String MAX,protected static final java.lang.String MESSAGE,protected static final java.lang.String MIN,protected static final java.lang.String MINPREVMATCHES,public static final java.lang.String NAME,protected static final java.lang.String NEGATE,protected static final java.lang.String NEGATE_POS,protected static final java.lang.String NO,protected static final java.lang.String OFF,protected static final java.lang.String ON,protected static final java.lang.String OR,protected static final java.lang.String PATTERN,protected static final java.lang.String PHRASES,protected static final java.lang.String POSTAG,protected static final java.lang.String POSTAG_REGEXP,protected static final java.lang.String PREMIUM,protected static final java.lang.String PRIO,protected static final java.lang.String REGEXP,protected static final java.lang.String RULE,protected static final java.lang.String RULEGROUP,protected static final java.lang.String RULES,protected static final java.lang.String SCOPE,protected static final java.lang.String SKIP,protected static final java.lang.String SPACEBEFORE,protected static final java.lang.String SUGGESTION,protected static final java.lang.String TABNAME,protected static final java.lang.String TEMP_OFF,protected static final java.lang.String TOKEN,protected static final java.lang.String TRUE,protected static final java.lang.String TYPE,protected static final java.lang.String UNIFICATION,protected static final java.lang.String UNIFY,protected static final java.lang.String UNIFY_IGNORE,protected static final java.lang.String YES,protected int andGroupCounter,protected java.lang.StringBuilder antiPatternExample,protected java.lang.StringBuilder antiPatternForRuleGroupExample,protected List<org.languagetool.rules.CorrectExample> antipatternExamples,protected List<org.languagetool.rules.CorrectExample> antipatternForRuleGroupsExamples,protected boolean caseSensitive,protected List<java.lang.String> categoryTags,protected List<java.lang.String> categoryToneTags,protected org.languagetool.chunking.ChunkTag chunkTag,protected java.lang.StringBuilder correctExample,protected List<org.languagetool.rules.CorrectExample> correctExamples,protected java.lang.StringBuilder elements,protected int endPositionCorrection,protected Map<java.lang.String,List<java.lang.String>> equivalenceFeatures,protected java.lang.StringBuilder errorTriggerExample,protected List<org.languagetool.rules.ErrorTriggeringExample> errorTriggeringExamples,protected java.lang.StringBuilder exampleCorrection,protected java.lang.Boolean exceptionLevelCaseSensitive,protected boolean exceptionLevelCaseSet,protected boolean exceptionPosNegation,protected boolean exceptionPosRegExp,protected java.lang.String exceptionPosToken,protected boolean exceptionSet,protected boolean exceptionSpaceBefore,protected boolean exceptionSpaceBeforeSet,protected boolean exceptionStringInflected,protected boolean exceptionStringNegation,protected boolean exceptionStringRegExp,protected boolean exceptionValidNext,protected boolean exceptionValidPrev,protected java.lang.StringBuilder exceptions,protected java.lang.String id,protected boolean inAndGroup,protected boolean inAntiPatternExample,protected boolean inAntiPatternForRuleGroupExample,protected boolean inCorrectExample,protected boolean inErrorTriggerExample,protected boolean inException,protected boolean inIncorrectExample,protected boolean inMarker,protected boolean inMatch,protected boolean inMessage,protected boolean inOrGroup,protected boolean inPattern,protected boolean inPhrases,protected boolean inRegex,protected boolean inRuleGroup,protected boolean inShortMessage,protected boolean inShortMessageForRuleGroup,protected boolean inSuggestion,protected boolean inToken,protected boolean inUnification,protected boolean inUnificationDef,protected boolean inUnificationNeutral,protected boolean inUrl,protected boolean inUrlForRuleGroup,protected java.lang.StringBuilder incorrectExample,protected List<org.languagetool.rules.IncorrectExample> incorrectExamples,private final Map<Triple<java.lang.String,java.lang.Boolean,java.lang.Boolean>,org.languagetool.rules.patterns.StringMatcher> internedMatchers,private final Map<Triple<java.lang.String,java.lang.Boolean,java.lang.Boolean>,org.languagetool.rules.patterns.PatternToken.PosToken> internedPos,private final Map<java.lang.String,java.lang.String> internedStrings,protected boolean isGoalSpecific,protected java.lang.String isGoalSpecificCategoryAttribute,protected java.lang.String isGoalSpecificRuleGroupAttribute,protected boolean isPremiumRule,protected org.languagetool.Language language,protected boolean lastPhrase,protected java.lang.StringBuilder match,protected int maxOccurrence,protected java.lang.StringBuilder message,protected java.util.ResourceBundle messages,protected int minOccurrence,protected int orGroupCounter,protected org.xml.sax.Locator pLocator,protected org.languagetool.rules.patterns.PatternToken patternToken,protected List<org.languagetool.rules.patterns.PatternToken> patternTokens,protected java.lang.String phraseId,protected java.lang.String phraseIdRef,protected Map<java.lang.String,List<List<org.languagetool.rules.patterns.PatternToken>>> phraseMap,protected List<ArrayList<org.languagetool.rules.patterns.PatternToken>> phrasePatternTokens,protected boolean posNegation,protected boolean posRegExp,protected java.lang.String posToken,protected java.lang.String premiumCategoryAttribute,protected java.lang.String premiumFileAttribute,protected java.lang.String premiumRuleGroupAttribute,protected int prioCategoryAttribute,protected int prioRuleAttribute,protected int prioRuleGroupAttribute,protected boolean regExpression,protected java.lang.StringBuilder regex,protected boolean regexCaseSensitive,protected org.languagetool.rules.patterns.XMLRuleHandler.RegexpMode regexMode,protected int regexpMark,protected java.lang.String ruleGroupId,protected List<java.lang.String> ruleGroupTags,protected List<java.lang.String> ruleGroupToneTags,protected List<java.lang.String> ruleTags,protected List<java.lang.String> ruleToneTags,protected List<org.languagetool.rules.patterns.AbstractPatternRule> rules,protected java.lang.StringBuilder shortMessage,protected java.lang.StringBuilder shortMessageForRuleGroup,protected int skipPos,protected int startPositionCorrection,protected List<org.languagetool.rules.patterns.Match> suggestionMatches,protected List<org.languagetool.rules.patterns.Match> suggestionMatchesOutMsg,protected java.lang.StringBuilder suggestionsOutMsg,protected int tokenCounter,protected boolean tokenInflected,protected boolean tokenLevelCaseSensitive,protected boolean tokenLevelCaseSet,protected boolean tokenNegated,protected org.languagetool.rules.patterns.Match tokenReference,protected boolean tokenSpaceBefore,protected boolean tokenSpaceBeforeSet,protected java.lang.String uFeature,protected java.lang.String uType,protected List<java.lang.String> uTypeList,protected boolean uniNegation,protected java.lang.StringBuilder url,protected java.lang.StringBuilder urlForRuleGroup,protected int xmlLineNumber,protected int xmlLineNumberAntiPattern