LocaleGenerator.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. <?php
  2. namespace PhpOffice\PhpSpreadsheetInfra;
  3. use Exception;
  4. use PhpOffice\PhpSpreadsheet\Cell\Cell;
  5. use PhpOffice\PhpSpreadsheet\IOFactory;
  6. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  7. use PhpOffice\PhpSpreadsheet\Worksheet\Column;
  8. use PhpOffice\PhpSpreadsheet\Worksheet\Row;
  9. use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
  10. class LocaleGenerator
  11. {
  12. private const EXCEL_LOCALISATION_WORKSHEET = 'Excel Localisation';
  13. private const EXCEL_FUNCTIONS_WORKSHEET = 'Excel Functions';
  14. private const LOCALE_NAME_ROW = 1;
  15. private const LOCALE_LANGUAGE_NAME_ROW = 2;
  16. private const ENGLISH_LANGUAGE_NAME_ROW = 3;
  17. private const ARGUMENT_SEPARATOR_ROW = 5;
  18. private const ERROR_CODES_FIRST_ROW = 8;
  19. private const FUNCTION_NAME_LIST_FIRST_ROW = 4;
  20. private const ENGLISH_FUNCTION_CATEGORIES_COLUMN = 'A';
  21. private const ENGLISH_REFERENCE_COLUMN = 'B';
  22. private const EOL = "\n"; // not PHP_EOL
  23. /**
  24. * @var string
  25. */
  26. protected $translationSpreadsheetName;
  27. /**
  28. * @var string
  29. */
  30. protected $translationBaseFolder;
  31. protected $phpSpreadsheetFunctions;
  32. /**
  33. * @var Spreadsheet
  34. */
  35. protected $translationSpreadsheet;
  36. protected $verbose;
  37. /**
  38. * @var Worksheet
  39. */
  40. protected $localeTranslations;
  41. protected $localeLanguageMap = [];
  42. protected $errorCodeMap = [];
  43. /**
  44. * @var Worksheet
  45. */
  46. private $functionNameTranslations;
  47. protected $functionNameLanguageMap = [];
  48. protected $functionNameMap = [];
  49. public function __construct(
  50. string $translationBaseFolder,
  51. string $translationSpreadsheetName,
  52. array $phpSpreadsheetFunctions,
  53. bool $verbose = false
  54. ) {
  55. $this->translationBaseFolder = $translationBaseFolder;
  56. $this->translationSpreadsheetName = $translationSpreadsheetName;
  57. $this->phpSpreadsheetFunctions = $phpSpreadsheetFunctions;
  58. $this->verbose = $verbose;
  59. }
  60. public function generateLocales(): void
  61. {
  62. $this->openTranslationWorkbook();
  63. $this->localeTranslations = $this->getTranslationSheet(self::EXCEL_LOCALISATION_WORKSHEET);
  64. $this->localeLanguageMap = $this->mapLanguageColumns($this->localeTranslations);
  65. $this->mapErrorCodeRows();
  66. $this->functionNameTranslations = $this->getTranslationSheet(self::EXCEL_FUNCTIONS_WORKSHEET);
  67. $this->functionNameLanguageMap = $this->mapLanguageColumns($this->functionNameTranslations);
  68. $this->mapFunctionNameRows();
  69. foreach ($this->localeLanguageMap as $column => $locale) {
  70. $this->buildConfigFileForLocale($column, $locale);
  71. }
  72. foreach ($this->functionNameLanguageMap as $column => $locale) {
  73. $this->buildFunctionsFileForLocale($column, $locale);
  74. }
  75. }
  76. protected function buildConfigFileForLocale($column, $locale): void
  77. {
  78. $language = $this->localeTranslations->getCell($column . self::ENGLISH_LANGUAGE_NAME_ROW)->getValue();
  79. $localeLanguage = $this->localeTranslations->getCell($column . self::LOCALE_LANGUAGE_NAME_ROW)->getValue();
  80. $configFile = $this->openConfigFile($locale, $language, $localeLanguage);
  81. $this->writeConfigArgumentSeparator($configFile, $column);
  82. $this->writeFileSectionHeader($configFile, 'Error Codes');
  83. foreach ($this->errorCodeMap as $errorCode => $row) {
  84. $translationCell = $this->localeTranslations->getCell($column . $row);
  85. $translationValue = $translationCell->getValue();
  86. if (!empty($translationValue)) {
  87. $errorCodeTranslation = "{$errorCode} = {$translationValue}" . self::EOL;
  88. fwrite($configFile, $errorCodeTranslation);
  89. } else {
  90. $errorCodeTranslation = "{$errorCode}" . self::EOL;
  91. fwrite($configFile, $errorCodeTranslation);
  92. $this->log("No {$language} translation available for error code {$errorCode}");
  93. }
  94. }
  95. fclose($configFile);
  96. }
  97. protected function writeConfigArgumentSeparator($configFile, $column): void
  98. {
  99. $translationCell = $this->localeTranslations->getCell($column . self::ARGUMENT_SEPARATOR_ROW);
  100. $localeValue = $translationCell->getValue();
  101. if (!empty($localeValue)) {
  102. $functionTranslation = "ArgumentSeparator = {$localeValue}" . self::EOL;
  103. fwrite($configFile, $functionTranslation);
  104. } else {
  105. $this->log('No Argument Separator defined');
  106. }
  107. }
  108. protected function buildFunctionsFileForLocale($column, $locale): void
  109. {
  110. $language = $this->functionNameTranslations->getCell($column . self::ENGLISH_LANGUAGE_NAME_ROW)->getValue();
  111. $localeLanguage = $this->functionNameTranslations->getCell($column . self::LOCALE_LANGUAGE_NAME_ROW)
  112. ->getValue();
  113. $functionFile = $this->openFunctionNameFile($locale, $language, $localeLanguage);
  114. foreach ($this->functionNameMap as $functionName => $row) {
  115. $translationCell = $this->functionNameTranslations->getCell($column . $row);
  116. $translationValue = $translationCell->getValue();
  117. if ($this->isFunctionCategoryEntry($translationCell)) {
  118. $this->writeFileSectionHeader($functionFile, "{$translationValue} ({$functionName})");
  119. } elseif (!array_key_exists($functionName, $this->phpSpreadsheetFunctions) && substr($functionName, 0, 1) !== '*') {
  120. $this->log("Function {$functionName} is not defined in PhpSpreadsheet");
  121. } elseif (!empty($translationValue)) {
  122. $functionTranslation = "{$functionName} = {$translationValue}" . self::EOL;
  123. fwrite($functionFile, $functionTranslation);
  124. } else {
  125. $this->log("No {$language} translation available for function {$functionName}");
  126. }
  127. }
  128. fclose($functionFile);
  129. }
  130. protected function openConfigFile(string $locale, string $language, string $localeLanguage)
  131. {
  132. $this->log("Building locale {$locale} ($language) configuration");
  133. $localeFolder = $this->getLocaleFolder($locale);
  134. $configFileName = realpath($localeFolder . DIRECTORY_SEPARATOR . 'config');
  135. $this->log("Writing locale configuration to {$configFileName}");
  136. $configFile = fopen($configFileName, 'wb');
  137. $this->writeFileHeader($configFile, $localeLanguage, $language, 'locale settings');
  138. return $configFile;
  139. }
  140. protected function openFunctionNameFile(string $locale, string $language, string $localeLanguage)
  141. {
  142. $this->log("Building locale {$locale} ($language) function names");
  143. $localeFolder = $this->getLocaleFolder($locale);
  144. $functionFileName = realpath($localeFolder . DIRECTORY_SEPARATOR . 'functions');
  145. $this->log("Writing local function names to {$functionFileName}");
  146. $functionFile = fopen($functionFileName, 'wb');
  147. $this->writeFileHeader($functionFile, $localeLanguage, $language, 'function name translations');
  148. return $functionFile;
  149. }
  150. protected function getLocaleFolder(string $locale): string
  151. {
  152. $localeFolder = $this->translationBaseFolder .
  153. DIRECTORY_SEPARATOR .
  154. str_replace('_', DIRECTORY_SEPARATOR, $locale);
  155. if (!file_exists($localeFolder) || !is_dir($localeFolder)) {
  156. mkdir($localeFolder, 0777, true);
  157. }
  158. return $localeFolder;
  159. }
  160. protected function writeFileHeader($localeFile, string $localeLanguage, string $language, string $title): void
  161. {
  162. fwrite($localeFile, str_repeat('#', 60) . self::EOL);
  163. fwrite($localeFile, '##' . self::EOL);
  164. fwrite($localeFile, "## PhpSpreadsheet - {$title}" . self::EOL);
  165. fwrite($localeFile, '##' . self::EOL);
  166. fwrite($localeFile, "## {$localeLanguage} ({$language})" . self::EOL);
  167. fwrite($localeFile, '##' . self::EOL);
  168. fwrite($localeFile, str_repeat('#', 60) . self::EOL . self::EOL);
  169. }
  170. protected function writeFileSectionHeader($localeFile, string $header): void
  171. {
  172. fwrite($localeFile, self::EOL . '##' . self::EOL);
  173. fwrite($localeFile, "## {$header}" . self::EOL);
  174. fwrite($localeFile, '##' . self::EOL);
  175. }
  176. protected function openTranslationWorkbook(): void
  177. {
  178. $filepathName = $this->translationBaseFolder . '/' . $this->translationSpreadsheetName;
  179. $this->translationSpreadsheet = IOFactory::load($filepathName);
  180. }
  181. protected function getTranslationSheet(string $sheetName): Worksheet
  182. {
  183. $worksheet = $this->translationSpreadsheet->setActiveSheetIndexByName($sheetName);
  184. if ($worksheet === null) {
  185. throw new Exception("{$sheetName} Worksheet not found");
  186. }
  187. return $worksheet;
  188. }
  189. protected function mapLanguageColumns(Worksheet $translationWorksheet): array
  190. {
  191. $sheetName = $translationWorksheet->getTitle();
  192. $this->log("Mapping Languages for {$sheetName}:");
  193. $baseColumn = self::ENGLISH_REFERENCE_COLUMN;
  194. $languagesList = $translationWorksheet->getColumnIterator(++$baseColumn);
  195. $languageNameMap = [];
  196. foreach ($languagesList as $languageColumn) {
  197. /** @var Column $languageColumn */
  198. $cells = $languageColumn->getCellIterator(self::LOCALE_NAME_ROW, self::LOCALE_NAME_ROW);
  199. $cells->setIterateOnlyExistingCells(true);
  200. foreach ($cells as $cell) {
  201. /** @var Cell $cell */
  202. if ($this->localeCanBeSupported($translationWorksheet, $cell)) {
  203. $languageNameMap[$cell->getColumn()] = $cell->getValue();
  204. $this->log($cell->getColumn() . ' -> ' . $cell->getValue());
  205. }
  206. }
  207. }
  208. return $languageNameMap;
  209. }
  210. protected function localeCanBeSupported(Worksheet $worksheet, Cell $cell): bool
  211. {
  212. if ($worksheet->getTitle() === self::EXCEL_LOCALISATION_WORKSHEET) {
  213. // Only provide support for languages that have a function argument separator defined
  214. // in the localisation worksheet
  215. return !empty(
  216. $worksheet->getCell($cell->getColumn() . self::ARGUMENT_SEPARATOR_ROW)->getValue()
  217. );
  218. }
  219. // If we're processing other worksheets, then language support is determined by whether we included the
  220. // language in the map when we were processing the localisation worksheet (which is always processed first)
  221. return in_array($cell->getValue(), $this->localeLanguageMap, true);
  222. }
  223. protected function mapErrorCodeRows(): void
  224. {
  225. $this->log('Mapping Error Codes:');
  226. $errorList = $this->localeTranslations->getRowIterator(self::ERROR_CODES_FIRST_ROW);
  227. foreach ($errorList as $errorRow) {
  228. /** @var Row $errorList */
  229. $cells = $errorRow->getCellIterator(self::ENGLISH_REFERENCE_COLUMN, self::ENGLISH_REFERENCE_COLUMN);
  230. $cells->setIterateOnlyExistingCells(true);
  231. foreach ($cells as $cell) {
  232. /** @var Cell $cell */
  233. if ($cell->getValue() != '') {
  234. $this->log($cell->getRow() . ' -> ' . $cell->getValue());
  235. $this->errorCodeMap[$cell->getValue()] = $cell->getRow();
  236. }
  237. }
  238. }
  239. }
  240. protected function mapFunctionNameRows(): void
  241. {
  242. $this->log('Mapping Functions:');
  243. $functionList = $this->functionNameTranslations->getRowIterator(self::FUNCTION_NAME_LIST_FIRST_ROW);
  244. foreach ($functionList as $functionRow) {
  245. /** @var Row $functionRow */
  246. $cells = $functionRow->getCellIterator(self::ENGLISH_REFERENCE_COLUMN, self::ENGLISH_REFERENCE_COLUMN);
  247. $cells->setIterateOnlyExistingCells(true);
  248. foreach ($cells as $cell) {
  249. /** @var Cell $cell */
  250. if ($this->isFunctionCategoryEntry($cell)) {
  251. if (!empty($cell->getValue())) {
  252. $this->log('CATEGORY: ' . $cell->getValue());
  253. $this->functionNameMap[$cell->getValue()] = $cell->getRow();
  254. }
  255. continue;
  256. }
  257. if ($cell->getValue() != '') {
  258. if (is_bool($cell->getValue())) {
  259. $this->log($cell->getRow() . ' -> ' . ($cell->getValue() ? 'TRUE' : 'FALSE'));
  260. $this->functionNameMap[($cell->getValue() ? 'TRUE' : 'FALSE')] = $cell->getRow();
  261. } else {
  262. $this->log($cell->getRow() . ' -> ' . $cell->getValue());
  263. $this->functionNameMap[$cell->getValue()] = $cell->getRow();
  264. }
  265. }
  266. }
  267. }
  268. }
  269. private function isFunctionCategoryEntry(Cell $cell): bool
  270. {
  271. $categoryCheckCell = self::ENGLISH_FUNCTION_CATEGORIES_COLUMN . $cell->getRow();
  272. if ($this->functionNameTranslations->getCell($categoryCheckCell)->getValue() != '') {
  273. return true;
  274. }
  275. return false;
  276. }
  277. private function log(string $message): void
  278. {
  279. if ($this->verbose === false) {
  280. return;
  281. }
  282. echo $message, self::EOL;
  283. }
  284. }