private void signalCopiedTranslation(
      HTextFlowTarget target, ContentState previousState, Long wordCount) {
    /*
     * Using a direct method call instead of an event because it's easier to
     * read. Since these events are being called synchronously (as opposed
     * to an 'after Transaction' events), there is no big performance gain
     * and makes the code easier to read and navigate.
     */
    // TODO how was this not causing duplicate events?  Is this bypassing TranslationServiceImpl?
    // FIXME other observers may not be notified
    HDocument document = target.getTextFlow().getDocument();

    DocumentLocaleKey key = new DocumentLocaleKey(document.getId(), target.getLocaleId());

    Map<ContentState, Long> contentStates = Maps.newHashMap();
    DocStatsEvent.updateContentStateDeltas(
        contentStates, target.getState(), previousState, wordCount);

    DocStatsEvent docEvent =
        new DocStatsEvent(
            key, document.getProjectIteration().getId(), contentStates, target.getId());

    versionStateCacheImpl.docStatsUpdated(docEvent);
  }
  @Override
  @Async
  public Future<Void> startMergeTranslations(
      String sourceProjectSlug,
      String sourceVersionSlug,
      String targetProjectSlug,
      String targetVersionSlug,
      boolean useNewerTranslation,
      MergeTranslationsTaskHandle handle) {

    HProjectIteration sourceVersion =
        projectIterationDAO.getBySlug(sourceProjectSlug, sourceVersionSlug);

    if (sourceVersion == null) {
      log.error("Cannot find source version of {}:{}", sourceProjectSlug, sourceVersionSlug);
      return AsyncTaskResult.taskResult();
    }

    HProjectIteration targetVersion =
        projectIterationDAO.getBySlug(targetProjectSlug, targetVersionSlug);

    if (targetVersion == null) {
      log.error("Cannot find target version of {}:{}", targetProjectSlug, targetVersionSlug);
      return AsyncTaskResult.taskResult();
    }

    if (isVersionsEmpty(sourceVersion, targetVersion)) {
      return AsyncTaskResult.taskResult();
    }

    if (getSupportedLocales(targetProjectSlug, targetVersionSlug).isEmpty()) {
      log.error(
          "No locales enabled in target version of {} [{}]", targetProjectSlug, targetVersionSlug);
      return AsyncTaskResult.taskResult();
    }

    Optional<MergeTranslationsTaskHandle> taskHandleOpt = Optional.fromNullable(handle);

    if (taskHandleOpt.isPresent()) {
      prepareMergeTranslationsHandle(sourceVersion, targetVersion, taskHandleOpt.get());
    }

    Stopwatch overallStopwatch = Stopwatch.createStarted();
    log.info(
        "merge translations start: from {} to {}",
        sourceProjectSlug + ":" + sourceVersionSlug,
        targetProjectSlug + ":" + targetVersionSlug);

    int startCount = 0;
    int totalCount = getTotalMatchCount(sourceVersion.getId(), targetVersion.getId());

    List<HLocale> supportedLocales =
        getSupportedLocales(targetVersion.getProject().getSlug(), targetVersion.getSlug());

    while (startCount < totalCount) {
      int processedCount =
          mergeTranslationBatch(
              sourceVersion,
              targetVersion,
              supportedLocales,
              useNewerTranslation,
              startCount,
              TEXTFLOWS_PER_BATCH);
      if (taskHandleOpt.isPresent()) {
        taskHandleOpt.get().increaseProgress(processedCount);
      }

      startCount += TEXTFLOWS_PER_BATCH;
      textFlowDAO.clear();
    }
    versionStateCacheImpl.clearVersionStatsCache(targetVersion.getId());
    log.info(
        "merge translation end: from {} to {}, {}",
        sourceProjectSlug + ":" + sourceVersionSlug,
        targetProjectSlug + ":" + targetVersionSlug,
        overallStopwatch);

    return AsyncTaskResult.taskResult();
  }
 private void clearStatsCacheForUpdatedDocument(HDocument document) {
   versionStateCacheImpl.clearVersionStatsCache(document.getProjectIteration().getId());
   translationStateCacheImpl.clearDocumentStatistics(document.getId());
 }