ilib-tools-common 1.2.0 → 1.4.0

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.
@@ -0,0 +1,178 @@
1
+ /*
2
+ * TranslationUnit.js - model a translation unit in a translation file
3
+ *
4
+ * Copyright © 2023 JEDLSoft
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ *
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+ import log4js from "@log4js-node/log4js-api";
20
+
21
+ import TranslationVariant from './TranslationVariant.js';
22
+ import { hashKey } from './utils.js';
23
+
24
+ const logger = log4js.getLogger("tools-common.TranslationUnit");
25
+
26
+ /**
27
+ * @class Represent a translation unit. A translation unit is
28
+ * a segment in the source language, along with one or
29
+ * more variants, which are translations to various
30
+ * target languages. A translation unit may contain more
31
+ * than one translation for a particular locale, as there
32
+ * are sometimes more than one translation for a particular
33
+ * phrase in the source language, depending on the context.
34
+ */
35
+ class TranslationUnit {
36
+ /**
37
+ * Create a new translation unit.
38
+ *
39
+ * The options may be undefined, which represents
40
+ * a new, clean TranslationUnit instance. The options object may also
41
+ * be an object with the following properties:
42
+ *
43
+ * <ul>
44
+ * <li><i>source</i> - source text for this unit (required)
45
+ * <li><i>sourceLocale</i> - the source locale spec for this unit (required)
46
+ * <li><i>target</i> - target text for this unit (optional)
47
+ * <li><i>targetLocale</i> - the target locale spec for this unit (optional)
48
+ * <li><i>key</i> - the unique resource key for this translation unit (required)
49
+ * <li><i>file</i> - path to the original source code file that contains the
50
+ * source text of this translation unit (required)
51
+ * <li><i>project</i> - the project that this string/unit is part of
52
+ * <li><i>resType</i> - type of this resource (string, array, plural) (optional)
53
+ * <li><i>state</i> - the state of the current unit (optional)
54
+ * <li><i>comment</i> - the translator's comment for this unit (optional)
55
+ * <li><i>datatype</i> - the source of the data of this unit (optional)
56
+ * <li><i>flavor</i> - the flavor that this string comes from(optional)
57
+ * </ul>
58
+ *
59
+ * If the required properties are not given, the constructor throws an exception.<p>
60
+ *
61
+ * For newly extracted strings, there is no target text yet. There must be a target
62
+ * locale for the translators to use when creating new target text, however. This
63
+ * means that there may be multiple translation units in a file with the same
64
+ * source locale and no target text, but different target locales.
65
+ *
66
+ * @param {Object} options options for this unit
67
+ */
68
+ constructor(options) {
69
+ // this.locale = options.locale; -> sourceLocale
70
+ // this.string = options.string; -> source
71
+ // this.datatype = options.datatype; -> datatype
72
+
73
+ this.variants = [];
74
+ this.variantHash = {};
75
+ this.properties = {};
76
+
77
+ if (options) {
78
+ const requiredFields = ["source", "sourceLocale"];
79
+ const missing = requiredFields.filter(p => {
80
+ if (typeof(options[p]) !== "undefined") {
81
+ this[p] = options[p];
82
+ return false;
83
+ }
84
+ return true;
85
+ });
86
+ // logger.trace("options is " + JSON.stringify(options));
87
+ if (missing.length) {
88
+ throw new Error("Missing required parameters in the TranslationUnit constructor: " + missing.join(", "));
89
+ }
90
+
91
+ const otherFields = ["key", "file", "project", "target", "targetLocale", "resType", "state", "comment", "datatype", "flavor"];
92
+ for (var p of otherFields) {
93
+ this[p] = options[p];
94
+ }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Clone the current unit and return the clone.
100
+ * @returns {TranslationUnit} a clone of the current unit.
101
+ */
102
+ clone() {
103
+ return new TranslationUnit(this);
104
+ }
105
+
106
+ /**
107
+ * Return a unique hash key for this translation unit. The
108
+ * hash key is calculated from the source string and locale
109
+ * and does not depend on the properties or variants in
110
+ * the unit.
111
+ *
112
+ * @returns {string} the unique hash key
113
+ */
114
+ hashKey() {
115
+ return [hashKey(this.source), this.sourceLocale, this.datatype].join("_");
116
+ }
117
+
118
+ /**
119
+ * Return the list of variants for this translation unit.
120
+ * @returns {Array.<TranslationVariant>} the variants for
121
+ * this translation unit
122
+ */
123
+ getVariants() {
124
+ return this.variants;
125
+ }
126
+
127
+ /**
128
+ * Add a single variant to this translation unit. This variant
129
+ * is only added if it is unique in this translation unit. That is,
130
+ * No other variant exists in this unit with the same locale and
131
+ * string.
132
+ *
133
+ * @param {TranslationVariant} variant the variant to add
134
+ */
135
+ addVariant(variant) {
136
+ var key = variant.hashKey();
137
+ if (!this.variantHash[key]) {
138
+ this.variants.push(variant);
139
+ this.variantHash[key] = variant;
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Add an array of variants to this translation unit. This only
145
+ * adds a variant if it is unique. That is, the unit is not
146
+ * added if the locale and string are the same as an existing
147
+ * variant.
148
+ *
149
+ * @param {Array.<TranslationVariant>} variants the array of variants to add
150
+ */
151
+ addVariants(variants) {
152
+ variants.forEach(variant => {
153
+ this.addVariant(variant);
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Return the list of properties and their values for this translation unit.
159
+ * @returns {Object} an object mapping properties to values
160
+ */
161
+ getProperties() {
162
+ return this.properties;
163
+ }
164
+
165
+ /**
166
+ * Add a property to this translation unit.
167
+ * @param {Object} properties an object that maps properties to values
168
+ */
169
+ addProperties(properties) {
170
+ for (let p in properties) {
171
+ if (properties[p]) {
172
+ this.properties[p] = properties[p];
173
+ }
174
+ }
175
+ }
176
+ }
177
+
178
+ export default TranslationUnit;
@@ -0,0 +1,51 @@
1
+ /*
2
+ * TranslationVariant.js - model a translation variant in a translation file
3
+ *
4
+ * Copyright © 2023 JEDLSoft
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ *
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+
20
+ import { hashKey } from './utils.js';
21
+
22
+ /**
23
+ * @class A class that represents a translation unit variant.
24
+ */
25
+ class TranslationVariant {
26
+ /**
27
+ * Options may contain the following properties:
28
+ * - locale: locale of the target string
29
+ * - string: the translation for this locale
30
+ *
31
+ * @param {Object} options
32
+ */
33
+ constructor(options) {
34
+ if (options) {
35
+ this.locale = options.locale;
36
+ this.string = options.string;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Return a unique hash key for this translation unit variant. The
42
+ * hash key is calculated from the source string and locale.
43
+ *
44
+ * @returns {string} the unique hash key
45
+ */
46
+ hashKey() {
47
+ return [hashKey(this.string), this.locale].join("_");
48
+ }
49
+ }
50
+
51
+ export default TranslationVariant;
package/src/index.js CHANGED
@@ -23,7 +23,17 @@ import ResourceArray from './ResourceArray.js';
23
23
  import ResourcePlural from './ResourcePlural.js';
24
24
  import TranslationSet from './TranslationSet.js';
25
25
  import ResourceXliff from './ResourceXliff.js';
26
- import { formatPath, getLocaleFromPath } from './utils.js';
26
+ import TranslationUnit from './TranslationUnit.js';
27
+ import TranslationVariant from './TranslationVariant.js';
28
+ import {
29
+ formatPath,
30
+ getLocaleFromPath,
31
+ cleanString,
32
+ isEmpty,
33
+ makeDirs,
34
+ containsActualText,
35
+ objectMap
36
+ } from './utils.js';
27
37
 
28
38
  export {
29
39
  Resource,
@@ -31,7 +41,14 @@ export {
31
41
  ResourceArray,
32
42
  ResourcePlural,
33
43
  TranslationSet,
44
+ TranslationUnit,
45
+ TranslationVariant,
34
46
  ResourceXliff,
35
47
  formatPath,
36
- getLocaleFromPath
48
+ getLocaleFromPath,
49
+ cleanString,
50
+ isEmpty,
51
+ makeDirs,
52
+ containsActualText,
53
+ objectMap
37
54
  };
package/src/utils.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * utils.js - utility functions to support the other code
3
3
  *
4
- * Copyright © 2022 JEDLSoft
4
+ * Copyright © 2022-2023 JEDLSoft
5
5
  *
6
6
  * Licensed under the Apache License, Version 2.0 (the "License");
7
7
  * you may not use this file except in compliance with the License.
@@ -17,9 +17,12 @@
17
17
  * limitations under the License.
18
18
  */
19
19
 
20
+ import fs from 'node:fs';
20
21
  import path from 'node:path';
21
22
  import Locale from 'ilib-locale';
22
23
 
24
+ import { isAlnum, isIdeo } from 'ilib-ctype';
25
+
23
26
  /**
24
27
  * Clean a string for matching against other strings by removing
25
28
  * differences that are inconsequential for translation.
@@ -312,3 +315,90 @@ export function getLocaleFromPath(template, pathname) {
312
315
 
313
316
  return "";
314
317
  };
318
+
319
+ export function makeDirs(path) {
320
+ const parts = path.split(/[\\\/]/);
321
+
322
+ for (let i = 1; i <= parts.length; i++) {
323
+ const p = parts.slice(0, i).join("/");
324
+ if (p && p.length > 0 && !fs.existsSync(p)) {
325
+ fs.mkdirSync(p);
326
+ }
327
+ }
328
+ };
329
+
330
+ /**
331
+ * Return true if the string still contains some text after removing all HTML tags and entities.
332
+ * @param {string} str the string to check
333
+ * @returns {boolean} true if there is text left over, and false otherwise
334
+ */
335
+ export function containsActualText(str) {
336
+ // remove the html and entities first
337
+ const cleaned = str.replace(/<("(\\"|[^"])*"|'(\\'|[^'])*'|[^>])*>/g, "").replace(/&[a-zA-Z]+;/g, "");
338
+
339
+ for (let i = 0; i < cleaned.length; i++) {
340
+ const c = cleaned.charAt(i);
341
+ if (isAlnum(c) || isIdeo(c)) return true;
342
+ }
343
+ return false;
344
+ };
345
+
346
+ function isPrimitive(type) {
347
+ return ["boolean", "number", "integer", "string"].indexOf(type) > -1;
348
+ }
349
+
350
+ /**
351
+ * Recursively visit every node in an object and call the visitor on any
352
+ * primitive values.
353
+ * @param {*} object any object, arrary, or primitive
354
+ * @param {Function(*)} visitor function to call on any primitive
355
+ * @returns {*} the same type as the original object, but with every
356
+ * primitive processed by the visitor function
357
+ */
358
+ export function objectMap(object, visitor) {
359
+ if (!object) return object;
360
+ if (isPrimitive(typeof(object))) {
361
+ return visitor(object);
362
+ } else if (Array.isArray(object)) {
363
+ return object.map(item => {
364
+ return objectMap(item, visitor);
365
+ });
366
+ } else {
367
+ const ret = {};
368
+ for (let prop in object) {
369
+ if (object.hasOwnProperty(prop)) {
370
+ ret[prop] = objectMap(object[prop], visitor);
371
+ }
372
+ }
373
+ return ret;
374
+ }
375
+ };
376
+
377
+ /**
378
+ * Return a standard hash of the given source string.
379
+ *
380
+ * @param {String} source the source string as extracted from the
381
+ * source code, unmodified
382
+ * @returns {String} the hash key
383
+ */
384
+ export function hashKey(source) {
385
+ if (!source) return undefined;
386
+ let hash = 0;
387
+ // these two numbers together = 46 bits so it won't blow out the precision of an integer in javascript
388
+ const modulus = 1073741789; // largest prime number that fits in 30 bits
389
+ const multiple = 65521; // largest prime that fits in 16 bits, co-prime with the modulus
390
+
391
+ // logger.trace("hash starts off at " + hash);
392
+
393
+ for (let i = 0; i < source.length; i++) {
394
+ // logger.trace("hash " + hash + " char " + source.charCodeAt(i) + "=" + source.charAt(i));
395
+ hash += source.charCodeAt(i);
396
+ hash *= multiple;
397
+ hash %= modulus;
398
+ }
399
+ const value = "r" + hash;
400
+
401
+ // System.out.println("String '" + source + "' hashes to " + value);
402
+
403
+ return value;
404
+ };