ilib-tools-common 1.21.2 → 1.21.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/utils.js CHANGED
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.cleanString=cleanString;exports.containsActualText=containsActualText;exports.formatLocaleParams=formatLocaleParams;exports.formatPath=formatPath;exports.getLanguagePluralCategories=getLanguagePluralCategories;exports.getLocaleFromPath=getLocaleFromPath;exports.hashKey=hashKey;exports.ignoreTags=void 0;exports.isEmpty=isEmpty;exports.localizableAttributes=void 0;exports.makeDirs=makeDirs;exports.nonBreakingTags=void 0;exports.objectMap=objectMap;exports.parsePath=parsePath;exports.selfClosingTags=void 0;var _fs=_interopRequireDefault(require("fs"));var _path=_interopRequireDefault(require("path"));var _ilibLocale=_interopRequireDefault(require("ilib-locale"));var _ilibCtype=require("ilib-ctype");var _pluralCategories=_interopRequireDefault(require("./pluralCategories.js"));function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _typeof(o){"@babel/helpers - typeof";return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(o){return typeof o}:function(o){return o&&"function"==typeof Symbol&&o.constructor===Symbol&&o!==Symbol.prototype?"symbol":typeof o},_typeof(o)}/*
1
+ "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.cleanString=cleanString;exports.containsActualText=containsActualText;exports.formatLocaleParams=formatLocaleParams;exports.formatPath=formatPath;exports.getLanguagePluralCategories=getLanguagePluralCategories;exports.getLocaleFromPath=getLocaleFromPath;exports.hashKey=hashKey;exports.ignoreTags=void 0;exports.isEmpty=isEmpty;exports.localizableAttributes=void 0;exports.makeDirs=makeDirs;exports.nonBreakingTags=void 0;exports.objectMap=objectMap;exports.parsePath=parsePath;exports.selfClosingTags=void 0;var _fs=_interopRequireDefault(require("fs"));var _path=_interopRequireDefault(require("path"));var _ilibLocale=_interopRequireDefault(require("ilib-locale"));var _log4jsApi=_interopRequireDefault(require("@log4js-node/log4js-api"));var _ilibCtype=require("ilib-ctype");var _pluralCategories=_interopRequireDefault(require("./pluralCategories.js"));function _interopRequireDefault(e){return e&&e.__esModule?e:{"default":e}}function _typeof(o){"@babel/helpers - typeof";return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(o){return typeof o}:function(o){return o&&"function"==typeof Symbol&&o.constructor===Symbol&&o!==Symbol.prototype?"symbol":typeof o},_typeof(o)}/*
2
2
  * utils.js - utility functions to support the other code
3
3
  *
4
4
  * Copyright © 2022-2023, 2025-2026 JEDLSoft
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * See the License for the specific language governing permissions and
17
17
  * limitations under the License.
18
- *//**
18
+ */var logger=_log4jsApi["default"].getLogger("tools-common.utils");/**
19
19
  * Clean a string for matching against other strings by removing
20
20
  * differences that are inconsequential for translation.
21
21
  *
@@ -106,7 +106,7 @@ output+="["+keyword+"]";break}}}return output}/**
106
106
  * @returns {string} the formatted file path
107
107
  */function formatPath(template,parameters){var pathname=parameters.sourcepath||"";var locale=parameters.locale||"en";var resourceDir=parameters.resourceDir||".";// First, handle locale-related substitutions without path normalization
108
108
  var output=formatLocaleParams(template,locale);// Now handle path-specific keywords
109
- var base;var lastDot;output=output.replace(/\[dir\]/g,_path["default"].dirname(pathname));output=output.replace(/\[filename\]/g,_path["default"].basename(pathname));output=output.replace(/\[resourceDir\]/g,resourceDir);if(output.includes("[extension]")){base=_path["default"].basename(pathname);output=output.replace(/\[extension\]/g,base.indexOf(".")>-1?base.substring(base.lastIndexOf(".")+1):"")}if(output.includes("[basename]")){base=_path["default"].basename(pathname);lastDot=base.lastIndexOf(".");output=output.replace(/\[basename\]/g,lastDot>-1?base.substring(0,lastDot):base)}return _path["default"].normalize(output)};var matchExprs={"dir":{regex:"(.*)",brackets:1,groups:{dir:1}},"basename":{regex:"(.*?)",brackets:1,groups:{basename:1}},"extension":{regex:"(.*)",brackets:1,groups:{extension:1}},"locale":{regex:"(([a-z][a-z][a-z]?)(-([A-Z][a-z][a-z][a-z]))?(-([A-Z][A-Z]|[0-9][0-9][0-9]))?)",brackets:6,groups:{locale:1,language:2,script:4,region:6}},"language":{regex:"([a-z][a-z][a-z]?)",brackets:1,groups:{language:1}},"script":{regex:"([A-Z][a-z][a-z][a-z])",brackets:1,groups:{script:1}},"region":{regex:"([A-Z][A-Z]|[0-9][0-9][0-9])",brackets:1,groups:{region:1}},"localeDir":{regex:"(([a-z][a-z][a-z]?)(/([A-Z][a-z][a-z][a-z]))?(/([A-Z][A-Z]|[0-9][0-9][0-9]))?)",brackets:6,groups:{localeDir:1,language:2,script:4,region:6}},"localeUnder":{regex:"(([a-z][a-z][a-z]?)(_([A-Z][a-z][a-z][a-z]))?(_([A-Z][A-Z]|[0-9][0-9][0-9]))?)",brackets:6,groups:{localeUnder:1,language:2,script:4,region:6}},"localeLower":{regex:"(([a-z][a-z][a-z]?)(-([a-z][a-z][a-z][a-z]))?(-([a-z][a-z]|[0-9][0-9][0-9]))?)",brackets:6,groups:{localeUnder:1,language:2,script:4,region:6}}};/**
109
+ var base;var lastDot;output=output.replace(/\[dir\]/g,_path["default"].dirname(pathname));output=output.replace(/\[filename\]/g,_path["default"].basename(pathname));output=output.replace(/\[resourceDir\]/g,resourceDir);if(output.includes("[extension]")){base=_path["default"].basename(pathname);output=output.replace(/\[extension\]/g,base.indexOf(".")>-1?base.substring(base.lastIndexOf(".")+1):"")}if(output.includes("[basename]")){base=_path["default"].basename(pathname);lastDot=base.lastIndexOf(".");output=output.replace(/\[basename\]/g,lastDot>-1?base.substring(0,lastDot):base)}return _path["default"].normalize(output)};var matchExprs={"dir":{regex:"(.*)",brackets:1,groups:{dir:1}},"basename":{regex:"(.*?)",brackets:1,groups:{basename:1}},"resourceDir":{regex:"(.*?)",brackets:1,groups:{resourceDir:1}},"extension":{regex:"(.*)",brackets:1,groups:{extension:1}},"locale":{regex:"(([a-z][a-z][a-z]?)(-([A-Z][a-z][a-z][a-z]))?(-([A-Z][A-Z]|[0-9][0-9][0-9]))?)",brackets:6,groups:{locale:1,language:2,script:4,region:6}},"language":{regex:"([a-z][a-z][a-z]?)",brackets:1,groups:{language:1}},"script":{regex:"([A-Z][a-z][a-z][a-z])",brackets:1,groups:{script:1}},"region":{regex:"([A-Z][A-Z]|[0-9][0-9][0-9])",brackets:1,groups:{region:1}},"localeDir":{regex:"(([a-z][a-z][a-z]?)(/([A-Z][a-z][a-z][a-z]))?(/([A-Z][A-Z]|[0-9][0-9][0-9]))?)",brackets:6,groups:{localeDir:1,language:2,script:4,region:6}},"localeUnder":{regex:"(([a-z][a-z][a-z]?)(_([A-Z][a-z][a-z][a-z]))?(_([A-Z][A-Z]|[0-9][0-9][0-9]))?)",brackets:6,groups:{localeUnder:1,language:2,script:4,region:6}},"localeLower":{regex:"(([a-z][a-z][a-z]?)(-([a-z][a-z][a-z][a-z]))?(-([a-z][a-z]|[0-9][0-9][0-9]))?)",brackets:6,groups:{localeUnder:1,language:2,script:4,region:6}}};/**
110
110
  * Parse a path according to the given template, and return the parts.
111
111
  * The parts can be any of the fields mentioned in the {@link formatPath}
112
112
  * documentation. If any field is not parsed, the result is an empty object
@@ -115,7 +115,18 @@ var base;var lastDot;output=output.replace(/\[dir\]/g,_path["default"].dirname(p
115
115
  * @param {String} pathname the path name to match against the template
116
116
  * @returns {Object} an object mapping the fields to their values in the
117
117
  * the pathname
118
- */function parsePath(template,pathname){var regex="";var matchGroups={};var totalBrackets=0;var base;if(!template){template=defaultMappings["**/*.json"].template}for(var i=0;i<template.length;i++){if(template[i]!=="["){regex+=template[i]}else{var start=++i;while(i<template.length&&template[i]!=="]"){i++}var keyword=template.substring(start,i);switch(keyword){case"filename":regex+=_path["default"].basename(pathname);break;default:if(!matchExprs[keyword]){logger.warning("Warning: template contains unknown substitution parameter "+keyword);return""}regex+=matchExprs[keyword].regex;for(var prop in matchExprs[keyword].groups){matchGroups[prop]=totalBrackets+matchExprs[keyword].groups[prop]}totalBrackets+=matchExprs[keyword].brackets;break}}}var re=new RegExp(regex,"u");var match;if((match=re.exec(pathname))!==null){var groups={};var found=false;for(var groupName in matchGroups){if(match[matchGroups[groupName]]){groups[groupName]=match[matchGroups[groupName]];found=true}}return groups}return{}}/**
118
+ */function parsePath(template,pathname){var regex="";var matchGroups={};var totalBrackets=0;var base;if(!template){template=defaultMappings["**/*.json"].template}for(var i=0;i<template.length;i++){if(template[i]!=="["){regex+=template[i]}else{var start=++i;while(i<template.length&&template[i]!=="]"){i++}var keyword=template.substring(start,i);switch(keyword){case"filename":// escape special characters in the filename so they don't have their special meaning in the regex
119
+ regex+=_path["default"].basename(pathname||"").replace(/[.*+?^${}()|[\]\\]/g,"\\$&");break;default:if(!matchExprs[keyword]){logger.warning("Warning: template contains unknown substitution parameter "+keyword);return""}// [dir]/ is optional when followed by / and another token. Always capture dir so callers
120
+ // (e.g. PropertiesParser) can build paths. Use (?:()|(.*?)/) so we require the slash when
121
+ // dir is non-empty - this prevents "test.properties" from matching as locale "est" with dir "test/testfiles/t".
122
+ // For "./de.po" the empty alternative matches; for "test/testfiles/de-DE.properties" we capture dir.
123
+ // Do NOT make optional for [basename], [extension], [resourceDir] - that would break path parsing.
124
+ var optionalDirSlash=false;if(keyword==="dir"&&i+1<template.length&&template[i+1]==="/"&&i+2<template.length&&template[i+2]==="["){var endBracket=template.indexOf("]",i+2);var nextKeyword=endBracket>-1?template.substring(i+3,endBracket):"";var optionalDirTokens=["filename","locale","language","script","region","localeDir","localeUnder","localeLower"];optionalDirSlash=optionalDirTokens.includes(nextKeyword)}if(keyword==="dir"&&optionalDirSlash){// (?:()|(.*?)/) - empty dir or (.*?)/ (require slash when dir is non-empty)
125
+ regex+="(?:()|(.*?)/)";matchGroups.dir=totalBrackets+2;// second group has dir when present
126
+ totalBrackets+=2;i++}else{regex+=matchExprs[keyword].regex;for(var prop in matchExprs[keyword].groups){matchGroups[prop]=totalBrackets+matchExprs[keyword].groups[prop]}totalBrackets+=matchExprs[keyword].brackets}break}}}// Anchor at start. If path starts with "./", allow it so template matches the rest (e.g. "./ja/foo.mdx" matches "[language]/[dir]/[filename]").
127
+ var prefix=pathname&&pathname.startsWith("./")?"^\\.\\/":"^";var re=new RegExp(prefix+regex,"u");var match=re.exec(pathname||"");if(match!==null){var groups={};var found=false;for(var groupName in matchGroups){var idx=matchGroups[groupName];var value=idx<match.length?match[idx]:undefined;// Capture empty only for dir (so "[dir]/[filename]" with "foo.mdx" gives dir "."); optional locale parts stay omitted.
128
+ if(value!==undefined&&(value||groupName==="dir"&&value==="")){groups[groupName]=value===undefined?"":value;found=true}}if(groups.dir===""||matchGroups.dir&&groups.dir===undefined){groups.dir="."}// When path started with "./", prefix consumed it; put it back into dir so dir is the full path.
129
+ if(pathname&&pathname.startsWith("./")&&groups.dir!==undefined&&groups.dir!=="."){groups.dir="./"+groups.dir}return groups}return{}}/**
119
130
  * Return a locale encoded in the path using template to parse that path.
120
131
  * See {#formatPath} for the full description of the syntax of the template.
121
132
  * @param {String} template template for the output file
package/lib/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","names":["_fs","_interopRequireDefault","require","_path","_ilibLocale","_ilibCtype","_pluralCategories","e","__esModule","_typeof","o","Symbol","iterator","constructor","prototype","cleanString","str","undefined","toLowerCase","replace","trim","isEmpty","obj","prop","formatLocaleParams","template","locale","l","Locale","localeSpec","getSpec","output","i","length","start","keyword","substring","getLanguage","getScript","getRegion","formatPath","parameters","pathname","sourcepath","resourceDir","base","lastDot","path","dirname","basename","includes","indexOf","lastIndexOf","normalize","matchExprs","regex","brackets","groups","dir","extension","language","script","region","localeDir","localeUnder","parsePath","matchGroups","totalBrackets","defaultMappings","logger","warning","re","RegExp","match","exec","found","groupName","getLocaleFromPath","charAt","toUpperCase","slice","makeDirs","parts","split","p","join","fs","existsSync","mkdirSync","containsActualText","cleaned","c","isAlnum","isIdeo","isPrimitive","type","objectMap","object","visitor","Array","isArray","map","item","ret","hasOwnProperty","hashKey","source","hash","modulus","multiple","charCodeAt","value","nonBreakingTags","exports","selfClosingTags","ignoreTags","localizableAttributes","defaultPluralCategories","getLanguagePluralCategories","_locale$getLanguage","_pluralCategories$lan","lang","pluralCategories"],"sources":["../src/utils.js"],"sourcesContent":["/*\n * utils.js - utility functions to support the other code\n *\n * Copyright © 2022-2023, 2025-2026 JEDLSoft\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport fs from 'fs';\nimport path from 'path';\nimport Locale from 'ilib-locale';\n\nimport { isAlnum, isIdeo } from 'ilib-ctype';\n\nimport pluralCategories from './pluralCategories.js';\n\n/**\n * Clean a string for matching against other strings by removing\n * differences that are inconsequential for translation.\n *\n * @param {String} str string to clean\n * @returns {String} the cleaned string\n */\nexport function cleanString(str) {\n if (typeof(str) !== 'string') {\n return undefined;\n }\n return str.toLowerCase().\n replace(/\\\\n/g, \" \").\n replace(/\\\\t/g, \" \").\n replace(/\\\\/g, \"\").\n replace(/\\s+/g, \" \").\n trim().\n replace(/&apos;/g, \"'\").\n replace(/&quot;/g, '\"').\n replace(/&lt;/g, \"<\").\n replace(/&gt;/g, \">\").\n replace(/&amp;/g, \"&\").\n replace(/’/g, \"'\");\n};\n\n/**\n * Is an empty object or not\n * @param {Object} obj object to test\n * @returns {Boolean} true if there are no properties, false otherwise\n */\nexport function isEmpty(obj) {\n let prop = undefined;\n\n if (!obj) {\n return true;\n }\n\n for (prop in obj) {\n if (prop && obj[prop]) {\n return false;\n }\n }\n return true;\n};\n\n/**\n * Format a string template with locale-related parameters.\n *\n * This function substitutes locale-related placeholders in a template string\n * without treating the string as a file path (no path normalization).\n * This is useful for formatting headers, footers, or other strings that\n * contain locale placeholders but should not be treated as file paths.\n *\n * This function recognizes and replaces the following strings in\n * templates:\n * - [locale] the full BCP-47 locale specification for the target locale\n * eg. \"zh-Hans-CN\" -> \"zh-Hans-CN\"\n * - [language] the language portion of the full locale\n * eg. \"zh-Hans-CN\" -> \"zh\"\n * - [script] the script portion of the full locale\n * eg. \"zh-Hans-CN\" -> \"Hans\"\n * - [region] the region portion of the full locale\n * eg. \"zh-Hans-CN\" -> \"CN\"\n * - [localeDir] the full locale where each portion of the locale\n * is a directory in this order: [langage], [script], [region].\n * eg, \"zh-Hans-CN\" -> \"zh/Hans/CN\", but \"en\" -> \"en\".\n * - [localeUnder] the full BCP-47 locale specification, but using\n * underscores to separate the locale parts instead of dashes.\n * eg. \"zh-Hans-CN\" -> \"zh_Hans_CN\"\n * - [localeLower] the full BCP-47 locale specification, but makes\n * all locale parts lowercased.\n * eg. \"zh-Hans-CN\" -> \"zh-hans-cn\"\n *\n * Unknown keywords are preserved in the output unchanged.\n *\n * @param {string} template the template string to format\n * @param {string|Object} locale the locale specifier, either as a string\n * or as a Locale object\n * @returns {string} the formatted string with locale placeholders replaced\n */\nexport function formatLocaleParams(template, locale) {\n if (!template) return \"\";\n const l = typeof locale === 'string' ? new Locale(locale || \"en\") : locale;\n const localeSpec = l.getSpec();\n let output = \"\";\n\n for (let i = 0; i < template.length; i++) {\n if (template[i] !== '[') {\n output += template[i];\n } else {\n let start = ++i;\n while (i < template.length && template[i] !== ']') {\n i++;\n }\n const keyword = template.substring(start, i);\n switch (keyword) {\n case 'locale':\n output += localeSpec;\n break;\n case 'language':\n output += l.getLanguage() || \"\";\n break;\n case 'script':\n output += l.getScript() || \"\";\n break;\n case 'region':\n output += l.getRegion() || \"\";\n break;\n case 'localeDir':\n output += localeSpec.replace(/-/g, '/');\n break;\n case 'localeUnder':\n output += localeSpec.replace(/-/g, '_');\n break;\n case 'localeLower':\n output += localeSpec.toLowerCase();\n break;\n default:\n // unknown keyword, preserve it unchanged\n output += '[' + keyword + ']';\n break;\n }\n }\n }\n return output;\n}\n\n/**\n * Format a file path using a path template and parameters.\n *\n * This function is used to generate an output file path for a given\n * source file path and a locale specifier.\n * The template replaces strings in square brackets with special values,\n * and keeps any characters intact that are not in square brackets.\n * This function recognizes and replaces the following strings in\n * templates:\n * - [dir] the original directory where the source file\n * came from. This is given as a directory that is relative\n * to the root of the project. eg. \"foo/bar/strings.json\" -> \"foo/bar\"\n * - [filename] the file name of the source file.\n * eg. \"foo/bar/strings.json\" -> \"strings.json\"\n * - [basename] the basename of the source file without any extension\n * eg. \"foo/bar/strings.json\" -> \"strings\"\n * - [extension] the extension part of the file name of the source file.\n * etc. \"foo/bar/strings.json\" -> \"json\"\n * - [locale] the full BCP-47 locale specification for the target locale\n * eg. \"zh-Hans-CN\" -> \"zh-Hans-CN\"\n * - [language] the language portion of the full locale\n * eg. \"zh-Hans-CN\" -> \"zh\"\n * - [script] the script portion of the full locale\n * eg. \"zh-Hans-CN\" -> \"Hans\"\n * - [region] the region portion of the full locale\n * eg. \"zh-Hans-CN\" -> \"CN\"\n * - [localeDir] the full locale where each portion of the locale\n * is a directory in this order: [langage], [script], [region].\n * eg, \"zh-Hans-CN\" -> \"zh/Hans/CN\", but \"en\" -> \"en\".\n * - [localeUnder] the full BCP-47 locale specification, but using\n * underscores to separate the locale parts instead of dashes.\n * eg. \"zh-Hans-CN\" -> \"zh_Hans_CN\"\n * - [localeLower] the full BCP-47 locale specification, but makes\n * all locale parts lowercased.\n * eg. \"zh-Hans-CN\" -> \"zh-hans-cn\"\n *\n * @param {string} template the path template string\n * @param {Object} parameters an object containing:\n * @param {string} parameters.sourcepath the path to the source file, relative to the\n * root of the project\n * @param {string} parameters.locale the locale for the output file path\n * @param {string} parameters.resourceDir optional resource directory to substitute\n * for [resourceDir] in the template\n * @returns {string} the formatted file path\n */\nexport function formatPath(template, parameters) {\n const pathname = parameters.sourcepath || \"\";\n const locale = parameters.locale || \"en\";\n const resourceDir = parameters.resourceDir || \".\";\n\n // First, handle locale-related substitutions without path normalization\n let output = formatLocaleParams(template, locale);\n\n // Now handle path-specific keywords\n let base;\n let lastDot;\n\n output = output.replace(/\\[dir\\]/g, path.dirname(pathname));\n output = output.replace(/\\[filename\\]/g, path.basename(pathname));\n output = output.replace(/\\[resourceDir\\]/g, resourceDir);\n\n if (output.includes('[extension]')) {\n base = path.basename(pathname);\n output = output.replace(/\\[extension\\]/g, base.indexOf('.') > -1 ? base.substring(base.lastIndexOf('.')+1) : \"\");\n }\n\n if (output.includes('[basename]')) {\n base = path.basename(pathname);\n lastDot = base.lastIndexOf('.');\n output = output.replace(/\\[basename\\]/g, lastDot > -1 ? base.substring(0, lastDot) : base);\n }\n\n return path.normalize(output);\n};\n\nconst matchExprs = {\n \"dir\": {\n regex: \"(.*)\",\n brackets: 1,\n groups: {\n dir: 1\n }\n },\n \"basename\": {\n regex: \"(.*?)\",\n brackets: 1,\n groups: {\n basename: 1\n }\n },\n \"extension\": {\n regex: \"(.*)\",\n brackets: 1,\n groups: {\n extension: 1\n }\n },\n \"locale\": {\n regex: \"(([a-z][a-z][a-z]?)(-([A-Z][a-z][a-z][a-z]))?(-([A-Z][A-Z]|[0-9][0-9][0-9]))?)\",\n brackets: 6,\n groups: {\n locale: 1,\n language: 2,\n script: 4,\n region: 6\n }\n },\n \"language\": {\n regex: \"([a-z][a-z][a-z]?)\",\n brackets: 1,\n groups: {\n language: 1\n }\n },\n \"script\": {\n regex: \"([A-Z][a-z][a-z][a-z])\",\n brackets: 1,\n groups: {\n script: 1\n }\n },\n \"region\": {\n regex: \"([A-Z][A-Z]|[0-9][0-9][0-9])\",\n brackets: 1,\n groups: {\n region: 1\n }\n },\n \"localeDir\": {\n regex: \"(([a-z][a-z][a-z]?)(/([A-Z][a-z][a-z][a-z]))?(/([A-Z][A-Z]|[0-9][0-9][0-9]))?)\",\n brackets: 6,\n groups: {\n localeDir: 1,\n language: 2,\n script: 4,\n region: 6\n }\n },\n \"localeUnder\": {\n regex: \"(([a-z][a-z][a-z]?)(_([A-Z][a-z][a-z][a-z]))?(_([A-Z][A-Z]|[0-9][0-9][0-9]))?)\",\n brackets: 6,\n groups: {\n localeUnder: 1,\n language: 2,\n script: 4,\n region: 6\n }\n },\n \"localeLower\": {\n regex: \"(([a-z][a-z][a-z]?)(-([a-z][a-z][a-z][a-z]))?(-([a-z][a-z]|[0-9][0-9][0-9]))?)\",\n brackets: 6,\n groups: {\n localeUnder: 1,\n language: 2,\n script: 4,\n region: 6\n }\n }\n};\n\n/**\n * Parse a path according to the given template, and return the parts.\n * The parts can be any of the fields mentioned in the {@link formatPath}\n * documentation. If any field is not parsed, the result is an empty object\n *\n * @param {String} template the ilib template for matching against the path\n * @param {String} pathname the path name to match against the template\n * @returns {Object} an object mapping the fields to their values in the\n * the pathname\n */\nexport function parsePath(template, pathname) {\n let regex = \"\";\n let matchGroups = {};\n let totalBrackets = 0;\n let base;\n\n if (!template) {\n template = defaultMappings[\"**/*.json\"].template;\n }\n\n for (let i = 0; i < template.length; i++) {\n if ( template[i] !== '[' ) {\n regex += template[i];\n } else {\n let start = ++i;\n while (i < template.length && template[i] !== ']') {\n i++;\n }\n const keyword = template.substring(start, i);\n switch (keyword) {\n case 'filename':\n regex += path.basename(pathname);\n break;\n default:\n if (!matchExprs[keyword]) {\n logger.warning(\"Warning: template contains unknown substitution parameter \" + keyword);\n return \"\";\n }\n regex += matchExprs[keyword].regex;\n for (let prop in matchExprs[keyword].groups) {\n matchGroups[prop] = totalBrackets + matchExprs[keyword].groups[prop];\n }\n totalBrackets += matchExprs[keyword].brackets;\n break;\n }\n }\n }\n\n const re = new RegExp(regex, \"u\");\n let match;\n\n if ((match = re.exec(pathname)) !== null) {\n let groups = {};\n let found = false;\n for (let groupName in matchGroups) {\n if (match[matchGroups[groupName]]) {\n groups[groupName] = match[matchGroups[groupName]];\n found = true;\n }\n }\n return groups;\n }\n\n return {};\n}\n\n/**\n * Return a locale encoded in the path using template to parse that path.\n * See {#formatPath} for the full description of the syntax of the template.\n * @param {String} template template for the output file\n * @param {String} pathname path to the source file\n * @returns {String} the locale within the path, or undefined if no locale found\n */\nexport function getLocaleFromPath(template, pathname) {\n const groups = parsePath(template, pathname);\n\n if (groups.locale || groups.language || groups.script || groups.region ) {\n // TODO: Remove script transformation once similar change is implemented in iLib/Locale class.\n if (groups.script && groups.script.length) {\n groups.script = groups.script.charAt(0).toUpperCase() + groups.script.slice(1).toLowerCase();\n }\n\n const l = groups.locale ?\n new Locale(groups.locale) :\n new Locale(groups.language, groups.region, undefined, groups.script);\n\n return l.getSpec();\n }\n\n return \"\";\n};\n\nexport function makeDirs(path) {\n const parts = path.split(/[\\\\\\/]/);\n\n for (let i = 1; i <= parts.length; i++) {\n const p = parts.slice(0, i).join(\"/\");\n if (p && p.length > 0 && !fs.existsSync(p)) {\n fs.mkdirSync(p);\n }\n }\n};\n\n/**\n * Return true if the string still contains some text after removing all HTML tags and entities.\n * @param {string} str the string to check\n * @returns {boolean} true if there is text left over, and false otherwise\n */\nexport function containsActualText(str) {\n // remove the html and entities first\n const cleaned = str.replace(/<(\"(\\\\\"|[^\"])*\"|'(\\\\'|[^'])*'|[^>])*>/g, \"\").replace(/&[a-zA-Z]+;/g, \"\");\n\n for (let i = 0; i < cleaned.length; i++) {\n const c = cleaned.charAt(i);\n if (isAlnum(c) || isIdeo(c)) return true;\n }\n return false;\n};\n\nfunction isPrimitive(type) {\n return [\"boolean\", \"number\", \"integer\", \"string\"].indexOf(type) > -1;\n}\n\n/**\n * Recursively visit every node in an object and call the visitor on any\n * primitive values.\n * @param {*} object any object, arrary, or primitive\n * @param {Function(*)} visitor function to call on any primitive\n * @returns {*} the same type as the original object, but with every\n * primitive processed by the visitor function\n */\nexport function objectMap(object, visitor) {\n if (!object) return object;\n if (isPrimitive(typeof(object))) {\n return visitor(object);\n } else if (Array.isArray(object)) {\n return object.map(item => {\n return objectMap(item, visitor);\n });\n } else {\n const ret = {};\n for (let prop in object) {\n if (object.hasOwnProperty(prop)) {\n ret[prop] = objectMap(object[prop], visitor);\n }\n }\n return ret;\n }\n};\n\n/**\n * Return a standard hash of the given source string.\n *\n * @param {String} source the source string as extracted from the\n * source code, unmodified\n * @returns {String} the hash key\n */\nexport function hashKey(source) {\n if (!source) return undefined;\n let hash = 0;\n // these two numbers together = 46 bits so it won't blow out the precision of an integer in javascript\n const modulus = 1073741789; // largest prime number that fits in 30 bits\n const multiple = 65521; // largest prime that fits in 16 bits, co-prime with the modulus\n\n // logger.trace(\"hash starts off at \" + hash);\n\n for (let i = 0; i < source.length; i++) {\n // logger.trace(\"hash \" + hash + \" char \" + source.charCodeAt(i) + \"=\" + source.charAt(i));\n hash += source.charCodeAt(i);\n hash *= multiple;\n hash %= modulus;\n }\n const value = \"r\" + hash;\n\n // System.out.println(\"String '\" + source + \"' hashes to \" + value);\n\n return value;\n};\n\n/**\n * A hash containing a list of HTML tags that do not\n * cause a break in a resource string. These tags should\n * be included in the middle of the string.\n */\nexport const nonBreakingTags = {\n \"a\": true,\n \"abbr\": true,\n \"b\": true,\n \"bdi\": true,\n \"bdo\": true,\n \"br\": true,\n \"dfn\": true,\n \"del\": true,\n \"em\": true,\n \"i\": true,\n \"ins\": true,\n \"mark\": true,\n \"ruby\": true,\n \"rt\": true,\n \"span\": true,\n \"strong\": true,\n \"sub\": true,\n \"sup\": true,\n \"time\": true,\n \"u\": true,\n \"var\": true,\n \"wbr\": true\n};\n\n/**\n * A hash containing a list of HTML tags that are\n * typically self-closing. That is, in HTML4 and earlier,\n * the close tag was not needed for these.\n */\nexport const selfClosingTags = {\n \"area\": true,\n \"base\": true,\n \"bdi\": true,\n \"bdo\": true,\n \"br\": true,\n \"embed\": true,\n \"hr\": true,\n \"img\": true,\n \"input\": true,\n \"link\": true,\n \"option\": true,\n \"param\": true,\n \"source\": true,\n \"track\": true\n};\n\n/**\n * A hash containing a list of HTML tags where\n * the text content inside of those tags should be\n * ignored for localization purposes. Instead,\n * those contents should just be copied to the\n * localized file unmodified.\n */\nexport const ignoreTags = {\n \"code\": true,\n \"output\": true,\n \"samp\": true,\n \"script\": true,\n \"style\": true\n};\n\n/**\n * List of html5 tags and their attributes that contain localizable strings.\n * The \"*\" indicates it applies to the given attributes on every tag.\n * Also added ARIA attributes to localize for accessibility. For more details,\n * see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/\n */\nexport const localizableAttributes = {\n \"area\": {\"alt\":true},\n \"img\": {\"alt\":true},\n \"input\": {\n \"alt\": true,\n \"placeholder\": true\n },\n \"optgroup\": {\"label\":true},\n \"option\": {\"label\":true},\n \"textarea\": {\"placeholder\":true},\n \"track\": {\"label\":true},\n \"*\": {\n \"title\": true,\n \"aria-braillelabel\": true,\n \"aria-brailleroledescription\": true,\n \"aria-description\": true,\n \"aria-label\": true,\n \"aria-placeholder\": true,\n \"aria-roledescription\": true,\n \"aria-rowindextext\": true,\n \"aria-valuetext\": true\n }\n};\n\n// this is for English and many other languages\nconst defaultPluralCategories = [\n \"one\",\n \"other\"\n];\n\n/**\n * Return the plural categories for the given language.\n * If there is no entry for the given\n * language, then the default English plural categories are returned.\n *\n * @param {string|undefined} language the ISO 639 language code to get the plural\n * categories for\n * @returns {Array} an array of strings containing the names of the plural categories\n */\nexport function getLanguagePluralCategories(language) {\n // make sure to get the language only if someone accidentally sends in a full locale\n const locale = new Locale(language);\n const lang = locale.getLanguage() ?? \"en\";\n return pluralCategories[lang] ?? defaultPluralCategories;\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"utils.js","names":["_fs","_interopRequireDefault","require","_path","_ilibLocale","_log4jsApi","_ilibCtype","_pluralCategories","e","__esModule","_typeof","o","Symbol","iterator","constructor","prototype","logger","log4js","getLogger","cleanString","str","undefined","toLowerCase","replace","trim","isEmpty","obj","prop","formatLocaleParams","template","locale","l","Locale","localeSpec","getSpec","output","i","length","start","keyword","substring","getLanguage","getScript","getRegion","formatPath","parameters","pathname","sourcepath","resourceDir","base","lastDot","path","dirname","basename","includes","indexOf","lastIndexOf","normalize","matchExprs","regex","brackets","groups","dir","extension","language","script","region","localeDir","localeUnder","parsePath","matchGroups","totalBrackets","defaultMappings","warning","optionalDirSlash","endBracket","nextKeyword","optionalDirTokens","prefix","startsWith","re","RegExp","match","exec","found","groupName","idx","value","getLocaleFromPath","charAt","toUpperCase","slice","makeDirs","parts","split","p","join","fs","existsSync","mkdirSync","containsActualText","cleaned","c","isAlnum","isIdeo","isPrimitive","type","objectMap","object","visitor","Array","isArray","map","item","ret","hasOwnProperty","hashKey","source","hash","modulus","multiple","charCodeAt","nonBreakingTags","exports","selfClosingTags","ignoreTags","localizableAttributes","defaultPluralCategories","getLanguagePluralCategories","_locale$getLanguage","_pluralCategories$lan","lang","pluralCategories"],"sources":["../src/utils.js"],"sourcesContent":["/*\n * utils.js - utility functions to support the other code\n *\n * Copyright © 2022-2023, 2025-2026 JEDLSoft\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n *\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport fs from 'fs';\nimport path from 'path';\nimport Locale from 'ilib-locale';\nimport log4js from '@log4js-node/log4js-api';\n\nimport { isAlnum, isIdeo } from 'ilib-ctype';\n\nimport pluralCategories from './pluralCategories.js';\n\nconst logger = log4js.getLogger('tools-common.utils');\n\n/**\n * Clean a string for matching against other strings by removing\n * differences that are inconsequential for translation.\n *\n * @param {String} str string to clean\n * @returns {String} the cleaned string\n */\nexport function cleanString(str) {\n if (typeof(str) !== 'string') {\n return undefined;\n }\n return str.toLowerCase().\n replace(/\\\\n/g, \" \").\n replace(/\\\\t/g, \" \").\n replace(/\\\\/g, \"\").\n replace(/\\s+/g, \" \").\n trim().\n replace(/&apos;/g, \"'\").\n replace(/&quot;/g, '\"').\n replace(/&lt;/g, \"<\").\n replace(/&gt;/g, \">\").\n replace(/&amp;/g, \"&\").\n replace(/’/g, \"'\");\n};\n\n/**\n * Is an empty object or not\n * @param {Object} obj object to test\n * @returns {Boolean} true if there are no properties, false otherwise\n */\nexport function isEmpty(obj) {\n let prop = undefined;\n\n if (!obj) {\n return true;\n }\n\n for (prop in obj) {\n if (prop && obj[prop]) {\n return false;\n }\n }\n return true;\n};\n\n/**\n * Format a string template with locale-related parameters.\n *\n * This function substitutes locale-related placeholders in a template string\n * without treating the string as a file path (no path normalization).\n * This is useful for formatting headers, footers, or other strings that\n * contain locale placeholders but should not be treated as file paths.\n *\n * This function recognizes and replaces the following strings in\n * templates:\n * - [locale] the full BCP-47 locale specification for the target locale\n * eg. \"zh-Hans-CN\" -> \"zh-Hans-CN\"\n * - [language] the language portion of the full locale\n * eg. \"zh-Hans-CN\" -> \"zh\"\n * - [script] the script portion of the full locale\n * eg. \"zh-Hans-CN\" -> \"Hans\"\n * - [region] the region portion of the full locale\n * eg. \"zh-Hans-CN\" -> \"CN\"\n * - [localeDir] the full locale where each portion of the locale\n * is a directory in this order: [langage], [script], [region].\n * eg, \"zh-Hans-CN\" -> \"zh/Hans/CN\", but \"en\" -> \"en\".\n * - [localeUnder] the full BCP-47 locale specification, but using\n * underscores to separate the locale parts instead of dashes.\n * eg. \"zh-Hans-CN\" -> \"zh_Hans_CN\"\n * - [localeLower] the full BCP-47 locale specification, but makes\n * all locale parts lowercased.\n * eg. \"zh-Hans-CN\" -> \"zh-hans-cn\"\n *\n * Unknown keywords are preserved in the output unchanged.\n *\n * @param {string} template the template string to format\n * @param {string|Object} locale the locale specifier, either as a string\n * or as a Locale object\n * @returns {string} the formatted string with locale placeholders replaced\n */\nexport function formatLocaleParams(template, locale) {\n if (!template) return \"\";\n const l = typeof locale === 'string' ? new Locale(locale || \"en\") : locale;\n const localeSpec = l.getSpec();\n let output = \"\";\n\n for (let i = 0; i < template.length; i++) {\n if (template[i] !== '[') {\n output += template[i];\n } else {\n let start = ++i;\n while (i < template.length && template[i] !== ']') {\n i++;\n }\n const keyword = template.substring(start, i);\n switch (keyword) {\n case 'locale':\n output += localeSpec;\n break;\n case 'language':\n output += l.getLanguage() || \"\";\n break;\n case 'script':\n output += l.getScript() || \"\";\n break;\n case 'region':\n output += l.getRegion() || \"\";\n break;\n case 'localeDir':\n output += localeSpec.replace(/-/g, '/');\n break;\n case 'localeUnder':\n output += localeSpec.replace(/-/g, '_');\n break;\n case 'localeLower':\n output += localeSpec.toLowerCase();\n break;\n default:\n // unknown keyword, preserve it unchanged\n output += '[' + keyword + ']';\n break;\n }\n }\n }\n return output;\n}\n\n/**\n * Format a file path using a path template and parameters.\n *\n * This function is used to generate an output file path for a given\n * source file path and a locale specifier.\n * The template replaces strings in square brackets with special values,\n * and keeps any characters intact that are not in square brackets.\n * This function recognizes and replaces the following strings in\n * templates:\n * - [dir] the original directory where the source file\n * came from. This is given as a directory that is relative\n * to the root of the project. eg. \"foo/bar/strings.json\" -> \"foo/bar\"\n * - [filename] the file name of the source file.\n * eg. \"foo/bar/strings.json\" -> \"strings.json\"\n * - [basename] the basename of the source file without any extension\n * eg. \"foo/bar/strings.json\" -> \"strings\"\n * - [extension] the extension part of the file name of the source file.\n * etc. \"foo/bar/strings.json\" -> \"json\"\n * - [locale] the full BCP-47 locale specification for the target locale\n * eg. \"zh-Hans-CN\" -> \"zh-Hans-CN\"\n * - [language] the language portion of the full locale\n * eg. \"zh-Hans-CN\" -> \"zh\"\n * - [script] the script portion of the full locale\n * eg. \"zh-Hans-CN\" -> \"Hans\"\n * - [region] the region portion of the full locale\n * eg. \"zh-Hans-CN\" -> \"CN\"\n * - [localeDir] the full locale where each portion of the locale\n * is a directory in this order: [langage], [script], [region].\n * eg, \"zh-Hans-CN\" -> \"zh/Hans/CN\", but \"en\" -> \"en\".\n * - [localeUnder] the full BCP-47 locale specification, but using\n * underscores to separate the locale parts instead of dashes.\n * eg. \"zh-Hans-CN\" -> \"zh_Hans_CN\"\n * - [localeLower] the full BCP-47 locale specification, but makes\n * all locale parts lowercased.\n * eg. \"zh-Hans-CN\" -> \"zh-hans-cn\"\n *\n * @param {string} template the path template string\n * @param {Object} parameters an object containing:\n * @param {string} parameters.sourcepath the path to the source file, relative to the\n * root of the project\n * @param {string} parameters.locale the locale for the output file path\n * @param {string} parameters.resourceDir optional resource directory to substitute\n * for [resourceDir] in the template\n * @returns {string} the formatted file path\n */\nexport function formatPath(template, parameters) {\n const pathname = parameters.sourcepath || \"\";\n const locale = parameters.locale || \"en\";\n const resourceDir = parameters.resourceDir || \".\";\n\n // First, handle locale-related substitutions without path normalization\n let output = formatLocaleParams(template, locale);\n\n // Now handle path-specific keywords\n let base;\n let lastDot;\n\n output = output.replace(/\\[dir\\]/g, path.dirname(pathname));\n output = output.replace(/\\[filename\\]/g, path.basename(pathname));\n output = output.replace(/\\[resourceDir\\]/g, resourceDir);\n\n if (output.includes('[extension]')) {\n base = path.basename(pathname);\n output = output.replace(/\\[extension\\]/g, base.indexOf('.') > -1 ? base.substring(base.lastIndexOf('.')+1) : \"\");\n }\n\n if (output.includes('[basename]')) {\n base = path.basename(pathname);\n lastDot = base.lastIndexOf('.');\n output = output.replace(/\\[basename\\]/g, lastDot > -1 ? base.substring(0, lastDot) : base);\n }\n\n return path.normalize(output);\n};\n\nconst matchExprs = {\n \"dir\": {\n regex: \"(.*)\",\n brackets: 1,\n groups: {\n dir: 1\n }\n },\n \"basename\": {\n regex: \"(.*?)\",\n brackets: 1,\n groups: {\n basename: 1\n }\n },\n \"resourceDir\": {\n regex: \"(.*?)\",\n brackets: 1,\n groups: {\n resourceDir: 1\n }\n },\n \"extension\": {\n regex: \"(.*)\",\n brackets: 1,\n groups: {\n extension: 1\n }\n },\n \"locale\": {\n regex: \"(([a-z][a-z][a-z]?)(-([A-Z][a-z][a-z][a-z]))?(-([A-Z][A-Z]|[0-9][0-9][0-9]))?)\",\n brackets: 6,\n groups: {\n locale: 1,\n language: 2,\n script: 4,\n region: 6\n }\n },\n \"language\": {\n regex: \"([a-z][a-z][a-z]?)\",\n brackets: 1,\n groups: {\n language: 1\n }\n },\n \"script\": {\n regex: \"([A-Z][a-z][a-z][a-z])\",\n brackets: 1,\n groups: {\n script: 1\n }\n },\n \"region\": {\n regex: \"([A-Z][A-Z]|[0-9][0-9][0-9])\",\n brackets: 1,\n groups: {\n region: 1\n }\n },\n \"localeDir\": {\n regex: \"(([a-z][a-z][a-z]?)(/([A-Z][a-z][a-z][a-z]))?(/([A-Z][A-Z]|[0-9][0-9][0-9]))?)\",\n brackets: 6,\n groups: {\n localeDir: 1,\n language: 2,\n script: 4,\n region: 6\n }\n },\n \"localeUnder\": {\n regex: \"(([a-z][a-z][a-z]?)(_([A-Z][a-z][a-z][a-z]))?(_([A-Z][A-Z]|[0-9][0-9][0-9]))?)\",\n brackets: 6,\n groups: {\n localeUnder: 1,\n language: 2,\n script: 4,\n region: 6\n }\n },\n \"localeLower\": {\n regex: \"(([a-z][a-z][a-z]?)(-([a-z][a-z][a-z][a-z]))?(-([a-z][a-z]|[0-9][0-9][0-9]))?)\",\n brackets: 6,\n groups: {\n localeUnder: 1,\n language: 2,\n script: 4,\n region: 6\n }\n }\n};\n\n/**\n * Parse a path according to the given template, and return the parts.\n * The parts can be any of the fields mentioned in the {@link formatPath}\n * documentation. If any field is not parsed, the result is an empty object\n *\n * @param {String} template the ilib template for matching against the path\n * @param {String} pathname the path name to match against the template\n * @returns {Object} an object mapping the fields to their values in the\n * the pathname\n */\nexport function parsePath(template, pathname) {\n let regex = \"\";\n let matchGroups = {};\n let totalBrackets = 0;\n let base;\n\n if (!template) {\n template = defaultMappings[\"**/*.json\"].template;\n }\n\n for (let i = 0; i < template.length; i++) {\n if ( template[i] !== '[' ) {\n regex += template[i];\n } else {\n let start = ++i;\n while (i < template.length && template[i] !== ']') {\n i++;\n }\n const keyword = template.substring(start, i);\n switch (keyword) {\n case 'filename':\n // escape special characters in the filename so they don't have their special meaning in the regex\n regex += path.basename(pathname || \"\").replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n break;\n default:\n if (!matchExprs[keyword]) {\n logger.warning(\"Warning: template contains unknown substitution parameter \" + keyword);\n return \"\";\n }\n // [dir]/ is optional when followed by / and another token. Always capture dir so callers\n // (e.g. PropertiesParser) can build paths. Use (?:()|(.*?)/) so we require the slash when\n // dir is non-empty - this prevents \"test.properties\" from matching as locale \"est\" with dir \"test/testfiles/t\".\n // For \"./de.po\" the empty alternative matches; for \"test/testfiles/de-DE.properties\" we capture dir.\n // Do NOT make optional for [basename], [extension], [resourceDir] - that would break path parsing.\n let optionalDirSlash = false;\n if (keyword === \"dir\" && i + 1 < template.length && template[i + 1] === \"/\" && i + 2 < template.length && template[i + 2] === \"[\") {\n const endBracket = template.indexOf(\"]\", i + 2);\n const nextKeyword = endBracket > -1 ? template.substring(i + 3, endBracket) : \"\";\n const optionalDirTokens = [\"filename\", \"locale\", \"language\", \"script\", \"region\", \"localeDir\", \"localeUnder\", \"localeLower\"];\n optionalDirSlash = optionalDirTokens.includes(nextKeyword);\n }\n if (keyword === \"dir\" && optionalDirSlash) {\n // (?:()|(.*?)/) - empty dir or (.*?)/ (require slash when dir is non-empty)\n regex += \"(?:()|(.*?)/)\";\n matchGroups.dir = totalBrackets + 2; // second group has dir when present\n totalBrackets += 2;\n i++;\n } else {\n regex += matchExprs[keyword].regex;\n for (let prop in matchExprs[keyword].groups) {\n matchGroups[prop] = totalBrackets + matchExprs[keyword].groups[prop];\n }\n totalBrackets += matchExprs[keyword].brackets;\n }\n break;\n }\n }\n }\n\n // Anchor at start. If path starts with \"./\", allow it so template matches the rest (e.g. \"./ja/foo.mdx\" matches \"[language]/[dir]/[filename]\").\n const prefix = (pathname && pathname.startsWith(\"./\")) ? \"^\\\\.\\\\/\" : \"^\";\n const re = new RegExp(prefix + regex, \"u\");\n const match = re.exec(pathname || \"\");\n if (match !== null) {\n let groups = {};\n let found = false;\n for (let groupName in matchGroups) {\n const idx = matchGroups[groupName];\n const value = idx < match.length ? match[idx] : undefined;\n // Capture empty only for dir (so \"[dir]/[filename]\" with \"foo.mdx\" gives dir \".\"); optional locale parts stay omitted.\n if (value !== undefined && (value || (groupName === \"dir\" && value === \"\"))) {\n groups[groupName] = value === undefined ? \"\" : value;\n found = true;\n }\n }\n if (groups.dir === \"\" || (matchGroups.dir && groups.dir === undefined)) {\n groups.dir = \".\";\n }\n // When path started with \"./\", prefix consumed it; put it back into dir so dir is the full path.\n if (pathname && pathname.startsWith(\"./\") && groups.dir !== undefined && groups.dir !== \".\") {\n groups.dir = \"./\" + groups.dir;\n }\n return groups;\n }\n\n return {};\n}\n\n/**\n * Return a locale encoded in the path using template to parse that path.\n * See {#formatPath} for the full description of the syntax of the template.\n * @param {String} template template for the output file\n * @param {String} pathname path to the source file\n * @returns {String} the locale within the path, or undefined if no locale found\n */\nexport function getLocaleFromPath(template, pathname) {\n const groups = parsePath(template, pathname);\n\n if (groups.locale || groups.language || groups.script || groups.region ) {\n // TODO: Remove script transformation once similar change is implemented in iLib/Locale class.\n if (groups.script && groups.script.length) {\n groups.script = groups.script.charAt(0).toUpperCase() + groups.script.slice(1).toLowerCase();\n }\n\n const l = groups.locale ?\n new Locale(groups.locale) :\n new Locale(groups.language, groups.region, undefined, groups.script);\n\n return l.getSpec();\n }\n\n return \"\";\n};\n\nexport function makeDirs(path) {\n const parts = path.split(/[\\\\\\/]/);\n\n for (let i = 1; i <= parts.length; i++) {\n const p = parts.slice(0, i).join(\"/\");\n if (p && p.length > 0 && !fs.existsSync(p)) {\n fs.mkdirSync(p);\n }\n }\n};\n\n/**\n * Return true if the string still contains some text after removing all HTML tags and entities.\n * @param {string} str the string to check\n * @returns {boolean} true if there is text left over, and false otherwise\n */\nexport function containsActualText(str) {\n // remove the html and entities first\n const cleaned = str.replace(/<(\"(\\\\\"|[^\"])*\"|'(\\\\'|[^'])*'|[^>])*>/g, \"\").replace(/&[a-zA-Z]+;/g, \"\");\n\n for (let i = 0; i < cleaned.length; i++) {\n const c = cleaned.charAt(i);\n if (isAlnum(c) || isIdeo(c)) return true;\n }\n return false;\n};\n\nfunction isPrimitive(type) {\n return [\"boolean\", \"number\", \"integer\", \"string\"].indexOf(type) > -1;\n}\n\n/**\n * Recursively visit every node in an object and call the visitor on any\n * primitive values.\n * @param {*} object any object, arrary, or primitive\n * @param {Function(*)} visitor function to call on any primitive\n * @returns {*} the same type as the original object, but with every\n * primitive processed by the visitor function\n */\nexport function objectMap(object, visitor) {\n if (!object) return object;\n if (isPrimitive(typeof(object))) {\n return visitor(object);\n } else if (Array.isArray(object)) {\n return object.map(item => {\n return objectMap(item, visitor);\n });\n } else {\n const ret = {};\n for (let prop in object) {\n if (object.hasOwnProperty(prop)) {\n ret[prop] = objectMap(object[prop], visitor);\n }\n }\n return ret;\n }\n};\n\n/**\n * Return a standard hash of the given source string.\n *\n * @param {String} source the source string as extracted from the\n * source code, unmodified\n * @returns {String} the hash key\n */\nexport function hashKey(source) {\n if (!source) return undefined;\n let hash = 0;\n // these two numbers together = 46 bits so it won't blow out the precision of an integer in javascript\n const modulus = 1073741789; // largest prime number that fits in 30 bits\n const multiple = 65521; // largest prime that fits in 16 bits, co-prime with the modulus\n\n // logger.trace(\"hash starts off at \" + hash);\n\n for (let i = 0; i < source.length; i++) {\n // logger.trace(\"hash \" + hash + \" char \" + source.charCodeAt(i) + \"=\" + source.charAt(i));\n hash += source.charCodeAt(i);\n hash *= multiple;\n hash %= modulus;\n }\n const value = \"r\" + hash;\n\n // System.out.println(\"String '\" + source + \"' hashes to \" + value);\n\n return value;\n};\n\n/**\n * A hash containing a list of HTML tags that do not\n * cause a break in a resource string. These tags should\n * be included in the middle of the string.\n */\nexport const nonBreakingTags = {\n \"a\": true,\n \"abbr\": true,\n \"b\": true,\n \"bdi\": true,\n \"bdo\": true,\n \"br\": true,\n \"dfn\": true,\n \"del\": true,\n \"em\": true,\n \"i\": true,\n \"ins\": true,\n \"mark\": true,\n \"ruby\": true,\n \"rt\": true,\n \"span\": true,\n \"strong\": true,\n \"sub\": true,\n \"sup\": true,\n \"time\": true,\n \"u\": true,\n \"var\": true,\n \"wbr\": true\n};\n\n/**\n * A hash containing a list of HTML tags that are\n * typically self-closing. That is, in HTML4 and earlier,\n * the close tag was not needed for these.\n */\nexport const selfClosingTags = {\n \"area\": true,\n \"base\": true,\n \"bdi\": true,\n \"bdo\": true,\n \"br\": true,\n \"embed\": true,\n \"hr\": true,\n \"img\": true,\n \"input\": true,\n \"link\": true,\n \"option\": true,\n \"param\": true,\n \"source\": true,\n \"track\": true\n};\n\n/**\n * A hash containing a list of HTML tags where\n * the text content inside of those tags should be\n * ignored for localization purposes. Instead,\n * those contents should just be copied to the\n * localized file unmodified.\n */\nexport const ignoreTags = {\n \"code\": true,\n \"output\": true,\n \"samp\": true,\n \"script\": true,\n \"style\": true\n};\n\n/**\n * List of html5 tags and their attributes that contain localizable strings.\n * The \"*\" indicates it applies to the given attributes on every tag.\n * Also added ARIA attributes to localize for accessibility. For more details,\n * see https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/\n */\nexport const localizableAttributes = {\n \"area\": {\"alt\":true},\n \"img\": {\"alt\":true},\n \"input\": {\n \"alt\": true,\n \"placeholder\": true\n },\n \"optgroup\": {\"label\":true},\n \"option\": {\"label\":true},\n \"textarea\": {\"placeholder\":true},\n \"track\": {\"label\":true},\n \"*\": {\n \"title\": true,\n \"aria-braillelabel\": true,\n \"aria-brailleroledescription\": true,\n \"aria-description\": true,\n \"aria-label\": true,\n \"aria-placeholder\": true,\n \"aria-roledescription\": true,\n \"aria-rowindextext\": true,\n \"aria-valuetext\": true\n }\n};\n\n// this is for English and many other languages\nconst defaultPluralCategories = [\n \"one\",\n \"other\"\n];\n\n/**\n * Return the plural categories for the given language.\n * If there is no entry for the given\n * language, then the default English plural categories are returned.\n *\n * @param {string|undefined} language the ISO 639 language code to get the plural\n * categories for\n * @returns {Array} an array of strings containing the names of the plural categories\n */\nexport function getLanguagePluralCategories(language) {\n // make sure to get the language only if someone accidentally sends in a full locale\n const locale = new Locale(language);\n const lang = locale.getLanguage() ?? \"en\";\n return pluralCategories[lang] ?? defaultPluralCategories;\n}\n"],"mappings":"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","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ilib-tools-common",
3
- "version": "1.21.2",
3
+ "version": "1.21.3",
4
4
  "main": "./lib/index.js",
5
5
  "module": "./src/index.js",
6
6
  "exports": {
@@ -81,11 +81,11 @@
81
81
  "ilib-xliff-webos": "^1.0.11",
82
82
  "intl-messageformat": "^10.7.11",
83
83
  "micromatch": "^4.0.8",
84
- "ilib-ctype": "^1.3.0",
85
84
  "ilib-istring": "^1.1.1",
85
+ "ilib-ctype": "^1.3.0",
86
+ "ilib-common": "^1.1.6",
86
87
  "ilib-locale": "^1.4.0",
87
- "ilib-xliff": "^1.4.1",
88
- "ilib-common": "^1.1.6"
88
+ "ilib-xliff": "^1.4.1"
89
89
  },
90
90
  "scripts": {
91
91
  "build": "pnpm build:data ; pnpm build:prod",
package/src/utils.js CHANGED
@@ -20,11 +20,14 @@
20
20
  import fs from 'fs';
21
21
  import path from 'path';
22
22
  import Locale from 'ilib-locale';
23
+ import log4js from '@log4js-node/log4js-api';
23
24
 
24
25
  import { isAlnum, isIdeo } from 'ilib-ctype';
25
26
 
26
27
  import pluralCategories from './pluralCategories.js';
27
28
 
29
+ const logger = log4js.getLogger('tools-common.utils');
30
+
28
31
  /**
29
32
  * Clean a string for matching against other strings by removing
30
33
  * differences that are inconsequential for translation.
@@ -242,6 +245,13 @@ const matchExprs = {
242
245
  basename: 1
243
246
  }
244
247
  },
248
+ "resourceDir": {
249
+ regex: "(.*?)",
250
+ brackets: 1,
251
+ groups: {
252
+ resourceDir: 1
253
+ }
254
+ },
245
255
  "extension": {
246
256
  regex: "(.*)",
247
257
  brackets: 1,
@@ -343,35 +353,67 @@ export function parsePath(template, pathname) {
343
353
  const keyword = template.substring(start, i);
344
354
  switch (keyword) {
345
355
  case 'filename':
346
- regex += path.basename(pathname);
356
+ // escape special characters in the filename so they don't have their special meaning in the regex
357
+ regex += path.basename(pathname || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
347
358
  break;
348
359
  default:
349
360
  if (!matchExprs[keyword]) {
350
361
  logger.warning("Warning: template contains unknown substitution parameter " + keyword);
351
362
  return "";
352
363
  }
353
- regex += matchExprs[keyword].regex;
354
- for (let prop in matchExprs[keyword].groups) {
355
- matchGroups[prop] = totalBrackets + matchExprs[keyword].groups[prop];
364
+ // [dir]/ is optional when followed by / and another token. Always capture dir so callers
365
+ // (e.g. PropertiesParser) can build paths. Use (?:()|(.*?)/) so we require the slash when
366
+ // dir is non-empty - this prevents "test.properties" from matching as locale "est" with dir "test/testfiles/t".
367
+ // For "./de.po" the empty alternative matches; for "test/testfiles/de-DE.properties" we capture dir.
368
+ // Do NOT make optional for [basename], [extension], [resourceDir] - that would break path parsing.
369
+ let optionalDirSlash = false;
370
+ if (keyword === "dir" && i + 1 < template.length && template[i + 1] === "/" && i + 2 < template.length && template[i + 2] === "[") {
371
+ const endBracket = template.indexOf("]", i + 2);
372
+ const nextKeyword = endBracket > -1 ? template.substring(i + 3, endBracket) : "";
373
+ const optionalDirTokens = ["filename", "locale", "language", "script", "region", "localeDir", "localeUnder", "localeLower"];
374
+ optionalDirSlash = optionalDirTokens.includes(nextKeyword);
375
+ }
376
+ if (keyword === "dir" && optionalDirSlash) {
377
+ // (?:()|(.*?)/) - empty dir or (.*?)/ (require slash when dir is non-empty)
378
+ regex += "(?:()|(.*?)/)";
379
+ matchGroups.dir = totalBrackets + 2; // second group has dir when present
380
+ totalBrackets += 2;
381
+ i++;
382
+ } else {
383
+ regex += matchExprs[keyword].regex;
384
+ for (let prop in matchExprs[keyword].groups) {
385
+ matchGroups[prop] = totalBrackets + matchExprs[keyword].groups[prop];
386
+ }
387
+ totalBrackets += matchExprs[keyword].brackets;
356
388
  }
357
- totalBrackets += matchExprs[keyword].brackets;
358
389
  break;
359
390
  }
360
391
  }
361
392
  }
362
393
 
363
- const re = new RegExp(regex, "u");
364
- let match;
365
-
366
- if ((match = re.exec(pathname)) !== null) {
394
+ // Anchor at start. If path starts with "./", allow it so template matches the rest (e.g. "./ja/foo.mdx" matches "[language]/[dir]/[filename]").
395
+ const prefix = (pathname && pathname.startsWith("./")) ? "^\\.\\/" : "^";
396
+ const re = new RegExp(prefix + regex, "u");
397
+ const match = re.exec(pathname || "");
398
+ if (match !== null) {
367
399
  let groups = {};
368
400
  let found = false;
369
401
  for (let groupName in matchGroups) {
370
- if (match[matchGroups[groupName]]) {
371
- groups[groupName] = match[matchGroups[groupName]];
402
+ const idx = matchGroups[groupName];
403
+ const value = idx < match.length ? match[idx] : undefined;
404
+ // Capture empty only for dir (so "[dir]/[filename]" with "foo.mdx" gives dir "."); optional locale parts stay omitted.
405
+ if (value !== undefined && (value || (groupName === "dir" && value === ""))) {
406
+ groups[groupName] = value === undefined ? "" : value;
372
407
  found = true;
373
408
  }
374
409
  }
410
+ if (groups.dir === "" || (matchGroups.dir && groups.dir === undefined)) {
411
+ groups.dir = ".";
412
+ }
413
+ // When path started with "./", prefix consumed it; put it back into dir so dir is the full path.
414
+ if (pathname && pathname.startsWith("./") && groups.dir !== undefined && groups.dir !== ".") {
415
+ groups.dir = "./" + groups.dir;
416
+ }
375
417
  return groups;
376
418
  }
377
419