docs: function (formatter) {
      this.formatter = formatter;
  
      /**
       * Convert 
       *
       * @param filePath input ods file path
       */
      this.convert = function (filePath) {
          log('Message resource converter');
          log('Execute convert: ' + filePath);
  
          var excelApp = new ActiveXObject('Excel.Application');
          excelApp.Visible = true;
          log('Start');
          var book = excelApp.Workbooks.Open(filePath);
          var sheet = book.Worksheets(1);
          this.extractResources(sheet);
          log("End");
          book.Close(false);
          excelApp.Quit();
          excelApp = null;
      };
  
      this.data = [];
      this.fillData = function (sheet, lastCol, lastRow) {
        WScript.StdOut.Write('                        \r');
        this.data.push(undefined);
        for (var r = 1; r <= lastRow; r++) {
          var row = [];
          row.push(undefined);
          for (var c = 1; c <= lastCol; c++) {
            row.push(sheet.Cells(r, c).Value);
          }
          this.data.push(row);
          WScript.StdOut.Write('\rLoading... ' + r + ' / ' + lastRow);
        }
        WScript.StdOut.WriteLine();
      };
      this.value = function (row, col) {
          if (row >= this.data.length) throw Error('Array index out of range error(row): ' + row + ' in ' + this.data.length);
          if (col >= this.data[row].length) throw Error('Array index out of range error(col): ' + col + ' in ' + this.data[row].length);
          return this.data[row][col];
      };
      this.getLocaleColIndex = function(locale) {
          for (var col = 1; col < 100; col++) {
              if (this.value(3, col).toLowerCase() === locale.toLowerCase()) {
                  return col;
              }
          }
          throw new Error('ref locale "' + locale + '" not found');
      };
      this.getRealValue = function (row, col) {
          var v = this.value(row, col);
          if (v === '^') {
              return this.getRealValue(row, col - 1);
          } else if (v === undefined) {
              return '';
          } else if (v.length > 1 && v.charAt(0) === '^') {
              var m = v.match(/^\^([a-z]+[\-_][a-zA-Z]+)$/);
              if (m) {
                  var refLocaleCol = this.getLocaleColIndex(m[1]);
                  return this.getRealValue(row, refLocaleCol);
              }
          }
          return v;
      };
      this.extractResources = function (sheet) {
          var locales = [];
          var lastLocaleCol = 1;
          var startLocaleCol = 4;
          for (var col = startLocaleCol, cellValue; cellValue = sheet.Cells(3, col).Value; col++) {
              lastLocaleCol = col;
          }
          var lastKeyRow = this.getLastKeyRow(sheet, 5);
          this.fillData(sheet, lastLocaleCol, lastKeyRow);
          console.log('Writing files...');
          var locales = [];
          for (var col = startLocaleCol; col <= lastLocaleCol; col++) {
              var localeKey = this.value(3, col);
              var lines = [];
              for (var row = 4; row <= lastKeyRow; row++) {
                  var key = this.value(row, 3);
                  var v = this.getRealValue(row, col);
                  if (key) {
                      if (key.substr(0, 1) === '#') {
                          lines.push(formatter.formatComment(key));
                      } else {
                          if (row === lastKeyRow) {
                              lines.push(formatter.formatLastLine(key, v));
                          } else {
                              lines.push(formatter.formatLine(key, v));
                          }
                      }
                  } else {
                      lines.push(''); // empty line
                  }
              }
              locales.push(localeKey);
              this.writeSingleResourceFile(localeKey, formatter.normalizeLines(lines));
          }
          console.log('Wrote ' + locales.length + ' files in messages/ directory.');
      };
      this.writeSingleResourceFile = function (localeKey, lines) {
          var folderName = formatter.folderName;
          if (!fso.FolderExists(folderName)) {
              fso.CreateFolder(folderName);
          }
          // const
          var adTypeBinary = 1; // Binary
          var adTypeText = 2; // Text
          var adWriteChar = 0; // no EOL
          var adSaveCreateOverWrite = 2; // create or over write
  
          var filename = formatter.getFilePath(folderName, localeKey);
          var sw = new ActiveXObject('ADODB.Stream');
          sw.Type = adTypeText;
          sw.charset = 'utf-8';
          sw.Open();
          var headerText = formatter.formatHeader(localeKey);
          sw.WriteText(headerText, adWriteChar);
          for (var i = 0; i < lines.length; i++) {
              sw.WriteText(lines[i], adWriteChar);
          }
          var footerText = formatter.formatFooter(localeKey);
          sw.WriteText(footerText, adWriteChar);
          // Change to binary mode
          sw.Position = 0;
          sw.Type = adTypeBinary;
          // UTF-8 BOM
          if (outputBOM) {
              // Keep UTF-8 BOM
              sw.Position = 0;
          } else {
              // Skip UTF-8 BOM
              sw.Position = 3;
          }
          var binBlock = sw.Read();
          sw.Close();
  
          // Write binBlock as binary
          var os = new ActiveXObject('ADODB.Stream');
          os.Type = adTypeBinary;
          os.Open();
          os.Write(binBlock);
          os.SaveToFile(filename, adSaveCreateOverWrite);
          if (formatter.requireDefaultFile && formatter.isDefaultLocale(localeKey)) {
              var filenameOriginal = formatter.getDefaultFilePath(folderName);
              os.SaveToFile(filenameOriginal, adSaveCreateOverWrite);
          }
          os.Close();
      };
  
      // True if there are 
      this.getLastKeyRow = function (sheet, startRow) {
          var isBlank10rows = function (row) {
              for (var i = 0; i < 10; i++) {
                  if (sheet.Cells(row + i, 3).Value) {
                      return false;
                  }
              }
              return true;
          };
          for (var j = startRow; j < 5000; j++) { 
              if (isBlank10rows(j)) {
                  return j - 1;
              }
          }
          return startRow;
      };
      return this;
  }