SOURCE CODE: Uize.Services.LocAdapter

VIEW REFERENCE

/*______________
|       ______  |   U I Z E    J A V A S C R I P T    F R A M E W O R K
|     /      /  |   ---------------------------------------------------
|    /    O /   |    MODULE : Uize.Services.LocAdapter Class
|   /    / /    |
|  /    / /  /| |    ONLINE : http://uize.com
| /____/ /__/_| | COPYRIGHT : (c)2013-2014 UIZE
|          /___ |   LICENSE : Available under MIT License or GNU General Public License
|_______________|             http://uize.com/license.html
*/

/* Module Meta Data
  type: Class
  importance: 2
  codeCompleteness: 80
  docCompleteness: 2
*/

/*?
  Introduction
    The =Uize.Services.LocAdapter= module defines a base class for adapters for the =Uize.Services.Loc= service.

    *DEVELOPERS:* `Chris van Rensburg`
*/

Uize.module ({
  name:'Uize.Services.LocAdapter',
  required:[
    'Uize.Services.FileSystem',
    'Uize.Json',
    'Uize.Data.Flatten',
    'Uize.Data.NameValueRecords',
    'Uize.Data.Csv',
    'Uize.Loc.Xliff',
    'Uize.Data.Diff',
    'Uize.Loc.Pseudo',
    'Uize.Str.Split',
    'Uize.Templates.Text.Tables.Breakdown',
    'Uize.Templates.Text.Tables.YinYangBreakdown',
    'Uize.Templates.Text.Tables.Histogram'
  ],
  superclass:'Uize.Service.Adapter',
  builder:function (_superclass) {
    'use strict';

    var
      /*** Variables for Scruncher Optimization ***/
        _undefined,
        _split = Uize.Str.Split.split,
        _breakdownTable = Uize.Templates.Text.Tables.Breakdown.process,

      /*** General Variables ***/
        _fileSystem = Uize.Services.FileSystem.singleton (),
        _sacredEmptyArray = [],
        _pathJsonSerializationOptions = {
          quoteChar:'"',
          indentChars:'',
          linebreakChars:''
        }
    ;

    /*** Utility Functions ***/
      function _twoGroupBreakdownTable (_title,_groupATitle,_groupACount,_groupBTitle,_groupBCount) {
        return _breakdownTable ({
          title:_title,
          countByCategory:Uize.pairUp (
            'All',_groupACount + _groupBCount,
            _groupATitle,_groupACount,
            _groupBTitle,_groupBCount
          )
        });
      }

      function _serializeStringPath (_path) {
        return Uize.Json.to (_path,_pathJsonSerializationOptions);
      }

    /*** Private Instance Methods ***/
      function _calculateStringsInfoForLanguage (m,_language,_languageResources,_subFolder) {
        var
          _project = m.project,
          _stringsInfo = [],
          _infoFilePath = m._workingFolderPath + _subFolder + 'strings-info/' + _language
        ;
        Uize.forEach (
          _languageResources,
          function (_resourceFileStrings,_resourceFileSubPath) {
            var
              _resourceFileIsBrandSpecific = m.isBrandResourceFile (_resourceFileSubPath),
              _resourceFileBrand = _resourceFileIsBrandSpecific
                ? m.getResourceFileBrand (_resourceFileSubPath)
                : ''
            ;
            _processStrings (
              _resourceFileStrings,
              function (_value,_path) {
                var
                  _isTranslatable = m.isTranslatableString ({
                    key:_path [_path.length - 1],
                    value:_value
                  }),
                  _stringMetrics = _getStringMetrics (m,_value),
                  _isBrandSpecific = _resourceFileIsBrandSpecific || m.isBrandResourceString (_path,_value)
                ;

                /*** check for weak tokens ***/
                  for (
                    var
                      _tokens = _stringMetrics.tokens,
                      _tokenNo = _tokens.length,
                      _hasWeakTokens = false
                    ;
                    !_hasWeakTokens && --_tokenNo >= 0;
                  ) {
                    if (m.isTokenWeak (_tokens [_tokenNo]))
                      _hasWeakTokens = true
                    ;
                  }

                _stringsInfo.push ({
                  path:[_resourceFileSubPath].concat (_path),
                  value:_value,
                  metrics:_stringMetrics,
                  isBrandSpecific:_isBrandSpecific,
                  brand:_isBrandSpecific
                    ? _resourceFileBrand || m.getStringBrand (_path,_value)
                    : '',
                  hasHtml:m.stringHasHtml (_path,_value),
                  isLong:_isTranslatable && m.isStringLong (_stringMetrics),
                  isKeyValid:m.isStringKeyValid (_path),
                  hasWeakTokens:_hasWeakTokens,
                  isTranslatable:_isTranslatable
                });
                return _value;
              }
            );
          }
        );

        /*** write the JSON file ***/
          _fileSystem.writeFile ({
            path:_infoFilePath + '.json',
            contents:Uize.Json.to (_stringsInfo)
          });

        /*** generate and write a flat CSV file version ***/
          _fileSystem.writeFile ({
            path:_infoFilePath + '.csv',
            contents:Uize.Data.Csv.to (
              Uize.map (
                _stringsInfo,
                function (_stringInfo) {
                  var
                    _path = _stringInfo.path,
                    _stringMetrics = _stringInfo.metrics
                  ;
                  return [
                    _path [_path.length - 1],
                    _stringInfo.value,
                    _path [0],
                    _serializeStringPath (_path),
                    _stringInfo.isBrandSpecific,
                    _stringInfo.brand,
                    _stringInfo.hasHtml,
                    _stringInfo.isLong,
                    _stringInfo.isKeyValid,
                    _stringInfo.hasWeakTokens,
                    _stringInfo.isTranslatable,
                    _stringMetrics.words,
                    _stringMetrics.chars,
                    _stringMetrics.tokens.join (',')
                  ];
                }
              ),
              {
                hasHeader:true,
                columns:[
                  'Key',
                  'Value',
                  'File',
                  'Path',
                  'Brand-specific',
                  'Brand',
                  'HTML',
                  'Long',
                  'Valid Key',
                  'Waak Tokens',
                  'Translatable',
                  'Word Count',
                  'Char Count',
                  'Tokens'
                ]
              }
            )
          });

        return _stringsInfo;
      }

      function _calculateMetricsForLanguage (m,_language,_languageResources,_subFolder) {
        var
          _project = m.project,
          _totalResourceFiles = 0,
          _totalBrandSpecificResourceFiles = 0,
          _totalResourceFilesPerBrand = {},
          _totalResourceStrings = 0,
          _totalBrandSpecificResourceStrings = 0,
          _totalResourceStringPerBrand = {},
          _totalWordCount = 0,
          _totalBrandSpecificWordCount = 0,
          _totalWordCountPerBrand = {},
          _totalCharCount = 0,
          _totalBrandSpecificCharCount = 0,
          _totalCharCountPerBrand = {},
          _totalTokens = 0,
          _totalTokenizedResourceStrings = 0,
          _totalHtmlResourceStrings = 0,
          _totalLongResourceStrings = 0,
          _totalInvalidKeyResourceStrings = 0,
          _totalWeakTokenResourceStrings = 0,
          _totalNonTranslatableResourceStrings = 0,
          _totalDupedResourceStrings = 0,
          _valuesLookup = {},
          _dupedResourceStringsDetails = {},
          _tokenUsage = {},
          _tokenHistogram = {},
          _wordCountHistogram = {},
          _charCountHistogram = {},
          _stringsInfo = _calculateStringsInfoForLanguage (m,_language,_languageResources,_subFolder)
        ;
        Uize.forEach (
          _languageResources,
          function (_resourceFileStrings,_resourceFileSubPath) {
            _totalResourceFiles++;
            if (m.isBrandResourceFile (_resourceFileSubPath)) {
              _totalBrandSpecificResourceFiles++;
              var _resourceFileBrand = m.getResourceFileBrand (_resourceFileSubPath);
              if (_resourceFileBrand)
                _totalResourceFilesPerBrand [_resourceFileBrand] =
                  (_totalResourceFilesPerBrand [_resourceFileBrand] || 0) + 1
              ;
            }
          }
        );
        Uize.forEach (
          _stringsInfo,
          function (_stringInfo) {
            var
              _path = _stringInfo.path,
              _value = _stringInfo.value,
              _stringFullPath = _serializeStringPath (_path)
            ;

            /*** update information on duplicates ***/
              if (_valuesLookup [_value]) {
                _totalDupedResourceStrings++;
                (
                  _dupedResourceStringsDetails [_value] ||
                  (_dupedResourceStringsDetails [_value] = [_valuesLookup [_value]])
                ).push (_stringFullPath);
              } else {
                _valuesLookup [_value] = _stringFullPath;
              }

            /*** get metrics for string ***/
              var
                _stringMetrics = _stringInfo.metrics,
                _words = _stringMetrics.words,
                _chars = _stringMetrics.chars,
                _stringTokens = _stringMetrics.tokens,
                _stringTokensLength = _stringTokens.length
              ;

              _stringInfo.hasHtml && _totalHtmlResourceStrings++;
              _stringInfo.isLong && _totalLongResourceStrings++;
              _stringInfo.isKeyValid || _totalInvalidKeyResourceStrings++;
              _stringInfo.hasWeakTokens && _totalWeakTokenResourceStrings++;
              _stringInfo.isTranslatable || _totalNonTranslatableResourceStrings++;

              /*** update general metrics ***/
                _totalResourceStrings++;
                _totalWordCount += _words;
                _totalCharCount += _chars;
                if (_stringInfo.isBrandSpecific) {
                  _totalBrandSpecificResourceStrings++;
                  _totalBrandSpecificWordCount += _words;
                  _totalBrandSpecificCharCount += _chars;

                  var _stringBrand = _stringInfo.brand;
                  if (_stringBrand) {
                    _totalResourceStringPerBrand [_stringBrand] =
                      (_totalResourceStringPerBrand [_stringBrand] || 0) + 1
                    ;
                    _totalWordCountPerBrand [_stringBrand] =
                      (_totalWordCountPerBrand [_stringBrand] || 0) + _words
                    ;
                    _totalCharCountPerBrand [_stringBrand] =
                      (_totalCharCountPerBrand [_stringBrand] || 0) + _chars
                    ;
                  }
                }
                _wordCountHistogram [_words] = (_wordCountHistogram [_words] || 0) + 1;
                _charCountHistogram [_chars] = (_charCountHistogram [_chars] || 0) + 1;

              /*** update metrics on tokenized strings and token usage ***/
                _tokenHistogram [_stringTokensLength] = (_tokenHistogram [_stringTokensLength] || 0) + 1;
                if (_stringTokensLength) {
                  Uize.forEach (
                    _stringTokens,
                    function (_tokenName) {
                      (_tokenUsage [_tokenName] || (_tokenUsage [_tokenName] = [])).push (
                        _stringFullPath
                      );
                    }
                  );
                  _totalTokens += _stringTokensLength;
                  _totalTokenizedResourceStrings++;
                }
          }
        );

        var _metrics = {
          resourceFiles:{
            all:_totalResourceFiles,
            brandSpecific:_totalBrandSpecificResourceFiles,
            perBrand:_totalResourceFilesPerBrand
          },
          resourceStrings:{
            all:_totalResourceStrings,
            brandSpecific:_totalBrandSpecificResourceStrings,
            tokenized:_totalTokenizedResourceStrings,
            html:_totalHtmlResourceStrings,
            long:_totalLongResourceStrings,
            invalidKey:_totalInvalidKeyResourceStrings,
            weakTokens:_totalWeakTokenResourceStrings,
            nonTranslatable:_totalNonTranslatableResourceStrings,
            duped:_totalDupedResourceStrings,
            perBrand:_totalResourceStringPerBrand
          },
          wordCount:{
            all:_totalWordCount,
            brandSpecific:_totalBrandSpecificWordCount,
            perBrand:_totalWordCountPerBrand
          },
          charCount:{
            all:_totalCharCount,
            brandSpecific:_totalBrandSpecificCharCount,
            perBrand:_totalCharCountPerBrand
          },
          tokens:_totalTokens,
          dupedResourceStringsDetails:_dupedResourceStringsDetails,
          tokenUsage:_tokenUsage,
          tokenHistogram:_tokenHistogram,
          wordCountHistogram:_wordCountHistogram,
          charCountHistogram:_charCountHistogram
        };
        _fileSystem.writeFile ({
          path:m._workingFolderPath + _subFolder + 'metrics/' + _language + '.json',
          contents:Uize.Json.to (_metrics)
        });

        return _metrics;
      }

      function _pseudoLocalizeResources (m,_primaryLanguageResources) {
        var
          _pseudoLocalizedResources = {},
          _pseudoLocalizeOptions = Uize.copy (m.project.pseudoLocalization,{wordSplitter:m.wordSplitter})
        ;
        Uize.forEach (
          _primaryLanguageResources,
          function (_resourceFileStrings,_resourceFileSubPath) {
            _pseudoLocalizedResources [_resourceFileSubPath] =
              Uize.Data.Diff.diff (
                _primaryLanguageResources [_resourceFileSubPath],
                {},
                function (_string) {
                  if (m.isTranslatableString (_string))
                    _string.value = Uize.Loc.Pseudo.pseudoLocalize (_string.value,_pseudoLocalizeOptions)
                  ;
                  return _string;
                }
              )
            ;
          }
        );
        return _pseudoLocalizedResources;
      }

      function _languageResourcesFilePath (m,_language) {
        return m._workingFolderPath + _language + '.json';
      }

      function _readLanguageResourcesFile (m,_language) {
        var _path = _languageResourcesFilePath (m,_language);
        return (
          _fileSystem.fileExists ({path:_path})
            ? Uize.Json.from (_fileSystem.readFile ({path:_path}))
            : _undefined
        );
      }

      function _writeLanguageResourcesFile (m,_language,_languageResources) {
        _fileSystem.writeFile ({
          path:_languageResourcesFilePath (m,_language),
          contents:Uize.Json.to (_languageResources)
        });
      }

      function _forEachTranslatableLanguage (m,_iterationHandler) {
        var
          _project = m.project,
          _primaryLanguage = _project.primaryLanguage,
          _pseudoLocale = _project.pseudoLocale
        ;
        Uize.forEach (
          _project.languages,
          function (_language) {
            _language != _primaryLanguage && _language != _pseudoLocale && _iterationHandler (_language);
          }
        );
      }

      function _getStringMetrics (m,_sourceStr) {
        var
          _chars = 0,
          _tokens = [],
          _tokenRegExp = m.tokenRegExp
        ;
        if (_tokenRegExp) {
          var
            _match,
            _tokenName,
            _tokenAdded = {}
          ;
          _tokenRegExp.index = 0;
          while (_match = _tokenRegExp.exec (_sourceStr)) {
            if (!(_tokenName = _match [1])) {
              for (var _matchSegmentNo = _match.length; !_tokenName && --_matchSegmentNo >= 0;)
                _tokenName = _match [_matchSegmentNo]
              ;
            }
            if (!_tokenAdded [_tokenName]) {
              _tokens.push (_tokenName);
              _tokenAdded [_tokenName] = 1;
            }
          }
        }
        for (
          var
            _stringSegmentNo = -2,
            _stringSegments = _split (_sourceStr,m.wordSplitter),
            _stringSegmentsLength = _stringSegments.length
          ;
          (_stringSegmentNo += 2) < _stringSegmentsLength;
        )
          _chars += _stringSegments [_stringSegmentNo].length
        ;
        return {
          words:(_stringSegmentsLength + 1) / 2,
          chars:_chars,
          tokens:_tokens
        };
      }

      function _processStrings (_strings,_stringProcessor) {
        function _processSection (_section,_path) {
          for (var _key in _section) {
            var _value = _section [_key];
            if (Uize.isObject (_value)) {
              _processSection (_value,_path.concat (_key));
            } else if (typeof _value == 'string') {
              _section [_key] = _stringProcessor (_section [_key],_path.concat (_key));
            }
          }
        }
        _processSection (_strings,[]);
      }

    return _superclass.subclass ({
      instanceMethods:{
        distributeResources:function (_resources,_language) {
          // NOTE: this method can be useful for implementation of the extract method
          var
            m = this,
            _rootFolderPath = m.project.rootFolderPath
          ;
          Uize.forEach (
            _resources,
            function (_resourceFileStrings,_resourceFileSubPath) {
              var _resourceFileFullPath =
                _rootFolderPath + '/' + m.getLanguageResourcePath (_resourceFileSubPath,_language)
              ;
              _fileSystem.writeFile ({
                path:_resourceFileFullPath,
                contents:m.serializeResourceFile (_resourceFileStrings,_language)
              });
            }
          );
        },

        prepareToExecuteMethod:function (_totalSteps) {
          this._methodTotalSteps = _totalSteps;
          this._methodCompletedSteps = 0;
        },

        stepCompleted:function (_message) {
          this._log (_message,++this._methodCompletedSteps / this._methodTotalSteps);
        },

        methodExecutionComplete:function (_summary) {
          this._log (_summary,'summary');
        },

        gatherResources:function () {
          var
            m = this,
            _resources = {},
            _rootFolderPath = m.project.rootFolderPath,
            _resourceFiles = _fileSystem.getFiles ({
              path:_rootFolderPath,
              pathMatcher:function (_filePath) {return m.isResourceFile (_filePath)},
              recursive:true
            })
          ;
          Uize.forEach (
            _resourceFiles,
            function (_filePath) {
              try {
                _resources [_filePath] = m.parseResourceFile (
                  _fileSystem.readFile ({path:_rootFolderPath + '/' + _filePath})
                );
              } catch (_error) {
                console.log (
                  'ERROR: problem parsing file ' + _filePath + '\n' +
                  _error
                );
              }
            }
          );
          return _resources;
        },

        getLanguageResourcePath:function (_enResourcePath,_language) {
          // this method should be implemented by subclasses
        },

        isBrandResourceFile:function (_filePath) {
          // this method should be implemented by subclasses
          return false;
        },

        isBrandResourceString:function (_resourceStringPath,_resourceStringText) {
          // this method should be implemented by subclasses
          return false;
        },

        getResourceFileBrand:function (_filePath) {
          // this method should be implemented by subclasses
          return '';
        },

        getStringBrand:function (_resourceStringPath,_resourceStringText) {
          // this method should be implemented by subclasses
          return '';
        },

        stringHasHtml:function (_path,_value) {
          // this method can be overridden by subclasses
          return /<[^<]+>/.test (_value); // NOTE: this is not the most robust test, so probably RegExpComposition should be used
        },

        isStringLong:function (_stringMetrics) {
          // this method can be overridden by subclasses
          return _stringMetrics.words > 50 || _stringMetrics.chars > 500;
        },

        isStringKeyValid:function (_path) {
          // this method can be overridden by subclasses
          return true;
        },

        isTokenWeak:function (_tokenName) {
          // this method can be overridden by subclasses
          return _tokenName.length < 3 || /^\d+$/.test (_tokenName);
        },

        isTranslatableString:function (_stringInfo) {
          // this method should be implemented by subclasses
          /* NOTE:
            the _stringInfo argument is an object of the form...

            .................
            {
              key:keySTR,
              value:valueSTR
            }
            .................
          */
          return true;
        },

        isResourceFile:function (_filePath) {
          // this method should be implemented by subclasses
        },

        parseResourceFile:function (_resourceFileText) {
          // this method should be implemented by subclasses
        },

        serializeResourceFile:function (_strings) {
          // this method should be implemented by subclasses
        },

        getReferencingCodeFiles:function () {
          return [];
          // this method should be implemented by subclasses
        },

        getReferencesFromCodeFile:function (_filePath) {
          return {};
          // this method should be implemented by subclasses
        },

        'import':function (_params,_callback) {
          var
            m = this,
            _project = m.project,
            _primaryLanguage = _project.primaryLanguage,
            _languages = _project.languages
          ;
          m.prepareToExecuteMethod ((_languages.length - !_project.importPrimary) * 2);
          Uize.forEach (
            _languages,
            function (_language) {
              if (_language != _primaryLanguage || _project.importPrimary) {
                var _resources = _readLanguageResourcesFile (m,_language);
                m.stepCompleted (_language + ': read language resources file');
                _resources && m.distributeResources (_resources,_language);
                m.stepCompleted (_language + ': distributed strings to individual resource files');
              }
            }
          );
          _callback ();
        },

        'export':function (_params,_callback) {
          var
            m = this,
            _project = m.project,
            _rootFolderPath = _project.rootFolderPath,
            _primaryLanguageResources = m.gatherResources (),
            _primaryLanguage = _project.primaryLanguage,
            _primaryLanguageResourcesLast = _readLanguageResourcesFile (m,_primaryLanguage) || {},
            _primaryLanguageResourcesDiff = Uize.Data.Diff.diff (
              _primaryLanguageResourcesLast,
              _primaryLanguageResources
            ),
            _resoucesByLanguage = Uize.pairUp (_primaryLanguage,_primaryLanguageResources),
            _totalLanguages = _project.languages.length,
            _totalTranslatableLanguages = _totalLanguages - 2
          ;

          m.prepareToExecuteMethod (
            _totalTranslatableLanguages * Uize.totalKeys (_primaryLanguageResources) +
              // total number of resource files to gather, across all translatable languages
            _totalLanguages
              // number of language resources files to write
          );

          /*** gather resources for all translatable languages ***/
            _forEachTranslatableLanguage (
              m,
              function (_language) {
                var _languageResources = _resoucesByLanguage [_language] = {};
                Uize.forEach (
                  _primaryLanguageResources,
                  function (_resourceFileStrings,_resourceFileSubPath) {
                    var
                      _resourceFilePath = m.getLanguageResourcePath (_resourceFileSubPath,_language),
                      _resourceFileFullPath = _rootFolderPath + '/' + _resourceFilePath
                    ;
                    _languageResources [_resourceFileSubPath] = Uize.Data.Diff.diff (
                      _fileSystem.fileExists ({path:_resourceFileFullPath})
                        ? m.parseResourceFile (_fileSystem.readFile ({path:_resourceFileFullPath}))
                        : {}
                      ,
                      _primaryLanguageResourcesDiff [_resourceFileSubPath],
                      function (_gatheredProperty,_propertyDiff) {
                        return (
                          !_propertyDiff || _propertyDiff.value == 'removed'
                            ? _undefined
                            : {
                              value:_propertyDiff.value == 'modified'
                                ? ''
                                : _gatheredProperty ? _gatheredProperty.value : ''
                            }
                        );
                      }
                    );
                    m.stepCompleted ('Gathered resources from file: ' + _resourceFilePath);
                  }
                );
              }
            );

          /*** generate resources for pseudo-locale ***/
            _resoucesByLanguage [_project.pseudoLocale] = _pseudoLocalizeResources (m,_primaryLanguageResources);

          Uize.forEach (
            _resoucesByLanguage,
            function (_languageResources,_language) {
              _writeLanguageResourcesFile (m,_language,_languageResources);
              m.stepCompleted ('Created resources file for language: ' + _language);
            }
          );
          _callback ();
        },

        exportJobs:function (_params,_callback) {
          var
            m = this,
            _project = m.project,
            _primaryLanguage = _project.primaryLanguage,
            _primaryLanguageResources = _readLanguageResourcesFile (m,_primaryLanguage),
            _totalTranslatableLanguages = _project.languages.length - 2
          ;
          m.prepareToExecuteMethod (_totalTranslatableLanguages * 3);

          _forEachTranslatableLanguage (
            m,
            function (_language) {
              /*** determine strings that need translation ***/
                var
                  _translationJobStrings = Uize.Data.Diff.diff (
                    _readLanguageResourcesFile (m,_language) || {},
                    _primaryLanguageResources,
                    function (_languageString,_primaryLanguageString) {
                      return (
                        !_languageString.value && m.isTranslatableString (_primaryLanguageString)
                          ? _primaryLanguageString
                          : _undefined
                      );
                    }
                  ),
                  _jobsPath = m._workingFolderPath + 'jobs/'
                ;
                m.stepCompleted (_language + ': determined strings that need translation');

              /*** calculate metrics for translation job ***/
                _calculateMetricsForLanguage (m,_language,_translationJobStrings,'jobs/');
                m.stepCompleted (_language + ': calculated translation job metrics');

              /*** write translation job file ***/
                var
                  _translationJobFileFormat = _project.translationJobFileFormat || 'csv',
                  _translationJobFilePath = _jobsPath + _language + '.' + _translationJobFileFormat
                ;
                _fileSystem.writeFile ({
                  path:_translationJobFilePath,
                  contents:_translationJobFileFormat == 'xliff'
                    ? Uize.Loc.Xliff.to ({
                      sourceLanguage:_primaryLanguage,
                      targetLanguage:_language,
                      strings:_translationJobStrings
                    })
                    : Uize.Data.Csv.to (
                      Uize.Data.NameValueRecords.fromHash (
                        Uize.Data.Flatten.flatten (
                          _translationJobStrings,
                          function (_path) {return Uize.Json.to (_path,'mini')}
                        ),
                        0,
                        1
                      )
                    )
                });

                m.stepCompleted (_language + ': created translation job file');
            }
          );
          _callback ();
        },

        importJobs:function (_params,_callback) {
          var
            m = this,
            _project = m.project,
            _totalTranslatableLanguages = _project.languages.length - 2,
            _jobsPath = m._workingFolderPath + 'jobs/'
          ;
          m.prepareToExecuteMethod (_totalTranslatableLanguages * 2);

          _forEachTranslatableLanguage (
            m,
            function (_language) {
              /*** determine strings that have been translated ***/
                var
                  _translationJobFileFormat = _project.translationJobFileFormat || 'csv',
                  _translationJobFilePath = _jobsPath + _language + '.' + _translationJobFileFormat,
                  _translationJobFile = _fileSystem.fileExists ({path:_translationJobFilePath})
                    ? _fileSystem.readFile ({path:_translationJobFilePath})
                    : '',
                  _translatedStrings = _translationJobFile
                    ? Uize.Data.Diff.diff (
                      _translationJobFileFormat == 'xliff'
                        ? Uize.Loc.Xliff.from (_translationJobFile)
                        : Uize.Data.Flatten.unflatten (
                          Uize.Data.NameValueRecords.toHash (Uize.Data.Csv.from (_translationJobFile),0,1),
                          Uize.Json.from
                        )
                      ,
                      {},
                      function (_string) {return _string.value ? _string : _undefined}
                    )
                    : {}
                ;
                m.stepCompleted (_language + ': determined strings that have been translated');

              /*** update language resources file ***/
                if (!Uize.isEmpty (_translatedStrings))
                  _writeLanguageResourcesFile (
                    m,
                    _language,
                    Uize.mergeInto (_readLanguageResourcesFile (m,_language),_translatedStrings)
                  )
                ;
                m.stepCompleted (_language + ': updated language resources file');
            }
          );
          _callback ();
        },

        extract:function (_params,_callback) {
          _callback ();
        },

        metrics:function (_params,_callback) {
          var
            m = this,
            _primaryLanguage = m.project.primaryLanguage
          ;
          m.prepareToExecuteMethod (2);

          /*** gather resources for primary language ***/
            var _primaryLanguageResources = m.gatherResources ();
            m.stepCompleted ('gathered resources for primary language');

          /*** calculate metrics for primary language ***/
            var _metrics = _calculateMetricsForLanguage (m,_primaryLanguage,_primaryLanguageResources,'');
            m.stepCompleted ('calculated metrics for primary language');

          /*** produce summary ***/
            /*** compile data for duplicates histogram ***/
              var _dupesHistogram = {};
              Uize.forEach (
                _metrics.dupedResourceStringsDetails,
                function (_resourceStringDupes) {
                  var _dupeCount = _resourceStringDupes.length - 1;
                  _dupesHistogram [_dupeCount] = (_dupesHistogram [_dupeCount] || 0) + 1;
                }
              );

            function _brandSpecificBreakdownTable (_title,_qualityMetrics) {
              var _countByCategory = Uize.pairUp (
                'All',_qualityMetrics.all,
                'Non Brand-specific',_qualityMetrics.all - _qualityMetrics.brandSpecific,
                'Brand-specific',_qualityMetrics.brandSpecific
              );
              Uize.forEach (
                _qualityMetrics.perBrand,
                function (_count,_brandId) {
                  _countByCategory ['Brand: ' + _brandId] = _count;
                }
              );
              return _breakdownTable ({
                title:_title,
                countByCategory:_countByCategory
              });
            }

            m.methodExecutionComplete (
              _brandSpecificBreakdownTable ('Resource Files',_metrics.resourceFiles) + '\n' +
              _brandSpecificBreakdownTable ('Resource Strings',_metrics.resourceStrings) + '\n' +
              _brandSpecificBreakdownTable ('Word Count',_metrics.wordCount) + '\n' +
              _brandSpecificBreakdownTable ('Character Count',_metrics.charCount) + '\n' +
              Uize.Templates.Text.Tables.YinYangBreakdown.process ({
                title:'Resource Strings',
                countByCategory:{
                  'All,None':_metrics.resourceStrings.all,
                  'Brand-specific,Brand-neutral':_metrics.resourceStrings.brandSpecific,
                  'Tokenized,Non-tokenized':_metrics.resourceStrings.tokenized,
                  'HTML,Non-HTML':_metrics.resourceStrings.html,
                  'Long,Normal':_metrics.resourceStrings.long,
                  'Invalid Keys,Valid Keys':_metrics.resourceStrings.invalidKey,
                  'Some Weak Tokens,Only Strong Tokens':_metrics.resourceStrings.weakTokens,
                  'Non-translatable,Translatable':_metrics.resourceStrings.nonTranslatable
                }
              }) + '\n' +
              Uize.Templates.Text.Tables.Histogram.process ({
                title:'Histogram of Resource String Duplicates',
                columnTitles:{
                  count:'Duplication Count',
                  occurrences:'Occurrences',
                  total:'Total Duplicates'
                },
                occurrencesByValue:_dupesHistogram
              }) + '\n' +
              Uize.Templates.Text.Tables.Histogram.process ({
                title:'Histogram of Resource String Tokenization',
                columnTitles:{
                  count:'Tokens in String',
                  occurrences:'Strings',
                  total:'Total Tokens'
                },
                occurrencesByValue:_metrics.tokenHistogram
              })
            );

          _callback ();
        },

        pseudoLocalize:function (_params,_callback) {
          var m = this;
          m.prepareToExecuteMethod (3);

          /*** gather resources for primary language ***/
            var _primaryLanguageResources = m.gatherResources ();
            m.stepCompleted ('gathered resources for primary language');

          /**( pseudo-localize resources for primary language ***/
            var _pseudoLocalizedResources = _pseudoLocalizeResources (m,_primaryLanguageResources);
            m.stepCompleted ('pseudo-localized resources for primary language');

          /*** distributed pseudo-localized resources to individual resource files ***/
            m.distributeResources (_pseudoLocalizedResources,m.project.primaryLanguage);
            m.stepCompleted ('distributed pseudo-localized resources to individual resource files');

          _callback ();
        },

        usage:function (_params,_callback) {
          var
            m = this,
            _allReferencesLookup = {},
            _referencingFiles = m.getReferencingCodeFiles ()
          ;

          m.prepareToExecuteMethod (_referencingFiles.length + 4);

          /*** build lookup of string references ***/
            Uize.forEach (
              _referencingFiles,
              function (_filePath) {
                Uize.forEach (
                  m.getReferencesFromCodeFile (_filePath),
                  function (_stringReferences,_stringId) {
                    Uize.push (
                      _allReferencesLookup [_stringId] || (_allReferencesLookup [_stringId] = []),
                      _stringReferences
                    );
                  }
                );
                m.stepCompleted ('scanned for resource string references in file: ' + _filePath);
              }
            );

          /*** create index of resource string references by code file ***/
            var _stringsReferencesByCodeFile = {};
            Uize.forEach (
              _allReferencesLookup,
              function (_stringReferences,_stringId) {
                Uize.forEach (
                  _stringReferences,
                  function (_stringReference) {
                    var _filePath = _stringReference.filePath;
                    (
                      _stringsReferencesByCodeFile [_filePath] ||
                      (_stringsReferencesByCodeFile [_filePath] = [])
                    ).push (_stringId);
                  }
                );
              }
            );
            m.stepCompleted ('created index of resource string references by code file');

          /*** gather resources for primary language ***/
            var _primaryLanguageResources = m.gatherResources ();
            m.stepCompleted ('gathered resources for primary language');

          /*** analyze resource string usage ***/
            var
              _stringIdLookup = {},
              _unreferenced = [],
              _references = {},
              _multiReferenced = {},
              _referencesHistogram = {},
              _trueValue = {}
            ;
            Uize.Data.Flatten.flatten (
              _primaryLanguageResources,
              function (_path) {
                var
                  _stringId = _path.slice (1).join ('.'),
                  _stringReferences = _allReferencesLookup [_stringId],
                  _stringReferenceCount = _stringReferences ? _stringReferences.length : 0
                ;
                _stringIdLookup [_stringId] = _trueValue;
                if (_stringReferenceCount) {
                  _references [_stringId] = _stringReferences;
                  if (_stringReferenceCount > 1)
                    _multiReferenced [_stringId] = _stringReferenceCount
                  ;
                } else {
                  _unreferenced.push (_stringId);
                }
                _referencesHistogram [_stringReferenceCount] =
                  (_referencesHistogram [_stringReferenceCount] || 0) + 1
                ;
              }
            );
            m.stepCompleted ('analyzed resource usage');

          /*** references to missing resource strings ***/
            var _missingStrings = {};
            Uize.forEach (
              _allReferencesLookup,
              function (_stringReferences,_stringId) {
                if (_stringIdLookup [_stringId] != _trueValue)
                  _missingStrings [_stringId] = _stringReferences
                ;
              }
            );

          /*** write report file ***/
            var _usageReportFilePath = m.workingFolderPath + 'metrics/usage-report.json';
            _fileSystem.writeFile ({
              path:_usageReportFilePath,
              contents:Uize.Json.to ({
                unreferenced:_unreferenced,
                multiReferenced:_multiReferenced,
                references:_references,
                referencesByCodeFile:_stringsReferencesByCodeFile,
                referencesHistogram:_referencesHistogram,
                missingStrings:_missingStrings
              })
            });
            m.stepCompleted ('created usage report file: ' + _usageReportFilePath);

          /*** produce summary ***/
            var
              _referencesValues = Uize.values (_references),
              _referencesValuesLength = _referencesValues.length,
              _unreferencedLength = _unreferenced.length
            ;
            m.methodExecutionComplete (
              _twoGroupBreakdownTable (
                'Resource Strings',
                'Referenced',_referencesValuesLength,
                'Unreferenced',_unreferencedLength
              ) + '\n' +
              Uize.Templates.Text.Tables.Histogram.process ({
                title:'Histogram of String References',
                columnTitles:{
                  count:'References',
                  occurrences:'Strings',
                  total:'Total References'
                },
                occurrencesByValue:_referencesHistogram
              })
            );

          _callback ();
        },

        init:function (_params,_callback) {
          var m = this;
          m.project = _params.project;
          m._workingFolderPath = m.workingFolderPath = _params.workingFolder + '/' + m.project.name + '/';
          m._log = _params.log || Uize.nop;
          _callback ();
        }
      },

      instanceProperties:{
        wordSplitter:null,
        tokenRegExp:null
      }
    });
  }
});