ai-l10n-core 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.
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./logger"), exports);
18
+ __exportStar(require("./consoleLogger"), exports);
19
+ __exportStar(require("./constants"), exports);
20
+ __exportStar(require("./translationService"), exports);
21
+ __exportStar(require("./languageUtils"), exports);
@@ -0,0 +1,27 @@
1
+ /**
2
+ * BCP 47 language code regex with optional script and region subtags.
3
+ * Matches patterns like: en, en-US, en_US, zh-Hans, zh-Hans-CN, zh_Hans_CN
4
+ */
5
+ export declare const LANGUAGE_CODE_REGEX: RegExp;
6
+ /**
7
+ * Normalizes language codes to a consistent BCP 47 format with optional script and region subtags
8
+ * Examples:
9
+ * - "en" -> "en"
10
+ * - "en-us", "en_US" -> "en-US"
11
+ * - "zh_hans", "zh-Hans" -> "zh-Hans"
12
+ * - "zh_HANS_CN", "zh-Hans-CN" -> "zh-Hans-CN"
13
+ * @param code Language code to normalize
14
+ * @returns Normalized language code in the format: language[-Script][-REGION]
15
+ */
16
+ export declare function normalizeLanguageCode(code: string): string;
17
+ /**
18
+ * Validates whether the given string is a valid BCP 47 language code
19
+ */
20
+ export declare function validateLanguageCode(code: string): boolean;
21
+ /**
22
+ * Extracts language code from file name, handling custom prefixes and suffixes for different file types
23
+ * ARB files: app_en_US.arb -> en_US, my_app_fr.arb -> fr
24
+ * JSON files: en-US.json -> en-US
25
+ * Shopify theme: en.default.schema.json -> en, es-ES.schema.json -> es-ES
26
+ */
27
+ export declare function extractLanguageCode(fileName: string): string | null;
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LANGUAGE_CODE_REGEX = void 0;
4
+ exports.normalizeLanguageCode = normalizeLanguageCode;
5
+ exports.validateLanguageCode = validateLanguageCode;
6
+ exports.extractLanguageCode = extractLanguageCode;
7
+ /**
8
+ * BCP 47 language code regex with optional script and region subtags.
9
+ * Matches patterns like: en, en-US, en_US, zh-Hans, zh-Hans-CN, zh_Hans_CN
10
+ */
11
+ exports.LANGUAGE_CODE_REGEX = /^(?<language>[a-z]{2,3})([-|_](?<script>[A-Z][a-z]{3}))?([-|_](?<region>[A-Z]{2,3}|[0-9]{3}))?$/i;
12
+ /**
13
+ * Normalizes language codes to a consistent BCP 47 format with optional script and region subtags
14
+ * Examples:
15
+ * - "en" -> "en"
16
+ * - "en-us", "en_US" -> "en-US"
17
+ * - "zh_hans", "zh-Hans" -> "zh-Hans"
18
+ * - "zh_HANS_CN", "zh-Hans-CN" -> "zh-Hans-CN"
19
+ * @param code Language code to normalize
20
+ * @returns Normalized language code in the format: language[-Script][-REGION]
21
+ */
22
+ function normalizeLanguageCode(code) {
23
+ const match = code.match(exports.LANGUAGE_CODE_REGEX);
24
+ if (!match?.groups) {
25
+ return code; // Return as-is if it doesn't match the pattern
26
+ }
27
+ const { language, script, region } = match.groups;
28
+ let normalized = language.toLowerCase();
29
+ if (script) {
30
+ // Script codes: first letter uppercase, rest lowercase
31
+ normalized +=
32
+ "-" + script.charAt(0).toUpperCase() + script.slice(1).toLowerCase();
33
+ }
34
+ if (region) {
35
+ // Region codes: all uppercase
36
+ normalized += "-" + region.toUpperCase();
37
+ }
38
+ return normalized;
39
+ }
40
+ /**
41
+ * Validates whether the given string is a valid BCP 47 language code
42
+ */
43
+ function validateLanguageCode(code) {
44
+ return !!code && exports.LANGUAGE_CODE_REGEX.test(code);
45
+ }
46
+ /**
47
+ * Extracts language code from file name, handling custom prefixes and suffixes for different file types
48
+ * ARB files: app_en_US.arb -> en_US, my_app_fr.arb -> fr
49
+ * JSON files: en-US.json -> en-US
50
+ * Shopify theme: en.default.schema.json -> en, es-ES.schema.json -> es-ES
51
+ */
52
+ function extractLanguageCode(fileName) {
53
+ const isArbFile = fileName.endsWith(".arb");
54
+ const nameWithoutExt = fileName.replace(/\.[^.]+$/, ""); // Remove extension
55
+ if (isArbFile) {
56
+ // ARB files use underscores instead of hyphens
57
+ const arbLanguageCodeRegex = /^(?<language>[a-z]{2,3})(_(?<script>[A-Z][a-z]{3}))?(_(?<region>[A-Z]{2,3}|[0-9]{3}))?$/;
58
+ // For ARB files, try to extract language code after potential prefix
59
+ // Pattern: [prefix_]language[_script][_region]
60
+ const parts = nameWithoutExt.split("_");
61
+ // Try combinations from right to left to find valid language code
62
+ // For example, for "my_app_en_US.arb", parts = ["my", "app", "en", "US"]
63
+ // Check "en_US", then "app_en_US", then "my_app_en_US" until we find a match
64
+ for (let i = 0; i < parts.length; i++) {
65
+ const potentialCode = parts.slice(i).join("_");
66
+ if (arbLanguageCodeRegex.test(potentialCode)) {
67
+ return potentialCode;
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ // Handle Shopify theme pattern: en.default.schema, es-ES.schema
73
+ // Remove .schema first, then .default before extracting language code
74
+ const cleanedFileName = nameWithoutExt
75
+ .replace(/\.schema$/, "")
76
+ .replace(/\.default$/, "");
77
+ // For JSON files, the entire filename should be the language code
78
+ return exports.LANGUAGE_CODE_REGEX.test(cleanedFileName) ? cleanedFileName : null;
79
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Logger interface for dependency injection
3
+ */
4
+ export interface ILogger {
5
+ logInfo(message: string): void;
6
+ logWarning(message: string, error?: unknown): void;
7
+ logError(message: string, error?: unknown): void;
8
+ showAndLogError(message: string, error?: unknown, context?: string, linkBtnText?: string, url?: string): void;
9
+ }
package/dist/logger.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const assert = __importStar(require("assert"));
37
+ const sinon = __importStar(require("sinon"));
38
+ const consoleLogger_1 = require("../consoleLogger");
39
+ suite("ConsoleLogger Test Suite", () => {
40
+ let logger;
41
+ let consoleErrorStub;
42
+ let consoleLogStub;
43
+ let consoleWarnStub;
44
+ setup(() => {
45
+ logger = new consoleLogger_1.ConsoleLogger();
46
+ consoleErrorStub = sinon.stub(console, "error");
47
+ consoleLogStub = sinon.stub(console, "log");
48
+ consoleWarnStub = sinon.stub(console, "warn");
49
+ });
50
+ teardown(() => {
51
+ sinon.restore();
52
+ });
53
+ suite("showAndLogError", () => {
54
+ test("logs error message with emoji", () => {
55
+ logger.showAndLogError("Test error message");
56
+ assert.ok(consoleErrorStub.calledOnce);
57
+ assert.ok(consoleErrorStub.firstCall.args[0].includes("❌"));
58
+ assert.ok(consoleErrorStub.firstCall.args[0].includes("Test error message"));
59
+ });
60
+ test("logs context when provided", () => {
61
+ logger.showAndLogError("Error", undefined, "Test context");
62
+ assert.ok(consoleErrorStub.calledTwice);
63
+ assert.ok(consoleErrorStub.secondCall.args[0].includes("Context:"));
64
+ assert.ok(consoleErrorStub.secondCall.args[0].includes("Test context"));
65
+ });
66
+ test("logs Error object with message and stack", () => {
67
+ const error = new Error("Test error");
68
+ logger.showAndLogError("Error occurred", error);
69
+ // Should log: main message, error message, and stack
70
+ assert.ok(consoleErrorStub.callCount >= 2);
71
+ const calls = consoleErrorStub.getCalls();
72
+ const errorMessageCall = calls.find((call) => call.args[0].includes("Error: Test error"));
73
+ assert.ok(errorMessageCall, "Should log error message");
74
+ const stackCall = calls.find((call) => typeof call.args[0] === "string" && call.args[0].includes("at "));
75
+ assert.ok(stackCall, "Should log stack trace");
76
+ });
77
+ test("logs non-Error objects as strings", () => {
78
+ const error = { message: "Custom error" };
79
+ logger.showAndLogError("Error occurred", error);
80
+ const calls = consoleErrorStub.getCalls();
81
+ const errorCall = calls.find((call) => call.args[0].includes("[object Object]"));
82
+ assert.ok(errorCall, "Should convert error to string");
83
+ });
84
+ test("logs link when both linkBtnText and url provided", () => {
85
+ logger.showAndLogError("Error", undefined, undefined, "Visit docs", "https://example.com");
86
+ const calls = consoleErrorStub.getCalls();
87
+ const linkCall = calls.find((call) => call.args[0].includes("Visit docs") &&
88
+ call.args[0].includes("https://example.com"));
89
+ assert.ok(linkCall, "Should log link with button text");
90
+ });
91
+ test("does not log link when only linkBtnText provided", () => {
92
+ logger.showAndLogError("Error", undefined, undefined, "Visit docs");
93
+ // Should only log the main error message, not the link
94
+ assert.strictEqual(consoleErrorStub.callCount, 1);
95
+ });
96
+ test("does not log link when only url provided", () => {
97
+ logger.showAndLogError("Error", undefined, undefined, undefined, "https://example.com");
98
+ // Should only log the main error message, not the link
99
+ assert.strictEqual(consoleErrorStub.callCount, 1);
100
+ });
101
+ test("handles all parameters together", () => {
102
+ const error = new Error("Full test error");
103
+ logger.showAndLogError("Complete error", error, "Full context", "Help link", "https://help.com");
104
+ // Should log: message, context, error message, stack, link
105
+ assert.ok(consoleErrorStub.callCount >= 4);
106
+ const allCalls = consoleErrorStub.getCalls().map((call) => call.args[0]);
107
+ const allOutput = allCalls.join(" ");
108
+ assert.ok(allOutput.includes("❌ Complete error"));
109
+ assert.ok(allOutput.includes("Context: Full context"));
110
+ assert.ok(allOutput.includes("Error: Full test error"));
111
+ assert.ok(allOutput.includes("Help link: https://help.com"));
112
+ });
113
+ test("handles Error without stack trace", () => {
114
+ const error = new Error("No stack error");
115
+ delete error.stack;
116
+ logger.showAndLogError("Error", error);
117
+ // Should still log the error message
118
+ const calls = consoleErrorStub.getCalls();
119
+ const errorCall = calls.find((call) => call.args[0].includes("Error: No stack error"));
120
+ assert.ok(errorCall, "Should log error message even without stack");
121
+ });
122
+ test("handles null error", () => {
123
+ logger.showAndLogError("Error", null);
124
+ // Should only log main message, null is falsy so no error details
125
+ assert.strictEqual(consoleErrorStub.callCount, 1);
126
+ });
127
+ test("handles undefined error", () => {
128
+ logger.showAndLogError("Error", undefined);
129
+ // Should only log main message
130
+ assert.strictEqual(consoleErrorStub.callCount, 1);
131
+ });
132
+ test("handles number as error", () => {
133
+ logger.showAndLogError("Error", 404);
134
+ const calls = consoleErrorStub.getCalls();
135
+ const errorCall = calls.find((call) => call.args[0].includes("Error: 404"));
136
+ assert.ok(errorCall, "Should convert number to string");
137
+ });
138
+ test("handles string as error", () => {
139
+ logger.showAndLogError("Error", "String error message");
140
+ const calls = consoleErrorStub.getCalls();
141
+ const errorCall = calls.find((call) => call.args[0].includes("Error: String error message"));
142
+ assert.ok(errorCall, "Should log string error");
143
+ });
144
+ });
145
+ suite("logInfo", () => {
146
+ test("logs info message", () => {
147
+ logger.logInfo("Information message");
148
+ assert.ok(consoleLogStub.calledOnce);
149
+ assert.strictEqual(consoleLogStub.firstCall.args[0], "Information message");
150
+ });
151
+ test("logs empty string", () => {
152
+ logger.logInfo("");
153
+ assert.ok(consoleLogStub.calledOnce);
154
+ assert.strictEqual(consoleLogStub.firstCall.args[0], "");
155
+ });
156
+ test("logs multiline message", () => {
157
+ const multiline = "Line 1\nLine 2\nLine 3";
158
+ logger.logInfo(multiline);
159
+ assert.ok(consoleLogStub.calledOnce);
160
+ assert.strictEqual(consoleLogStub.firstCall.args[0], multiline);
161
+ });
162
+ test("logs message with special characters", () => {
163
+ const message = "Message with 🎉 emoji and special chars: @#$%";
164
+ logger.logInfo(message);
165
+ assert.ok(consoleLogStub.calledOnce);
166
+ assert.strictEqual(consoleLogStub.firstCall.args[0], message);
167
+ });
168
+ });
169
+ suite("logWarning", () => {
170
+ test("logs warning with emoji", () => {
171
+ logger.logWarning("Warning message");
172
+ assert.ok(consoleWarnStub.calledOnce);
173
+ assert.ok(consoleWarnStub.firstCall.args[0].includes("⚠️ "));
174
+ assert.ok(consoleWarnStub.firstCall.args[0].includes("Warning message"));
175
+ });
176
+ test("logs empty warning", () => {
177
+ logger.logWarning("");
178
+ assert.ok(consoleWarnStub.calledOnce);
179
+ assert.strictEqual(consoleWarnStub.firstCall.args[0], "⚠️ ");
180
+ });
181
+ test("logs multiline warning", () => {
182
+ const multiline = "Warning line 1\nWarning line 2";
183
+ logger.logWarning(multiline);
184
+ assert.ok(consoleWarnStub.calledOnce);
185
+ assert.ok(consoleWarnStub.firstCall.args[0].includes(multiline));
186
+ });
187
+ test("preserves warning message format", () => {
188
+ logger.logWarning("Detailed warning with info");
189
+ const output = consoleWarnStub.firstCall.args[0];
190
+ assert.strictEqual(output, "⚠️ Detailed warning with info");
191
+ });
192
+ });
193
+ suite("Logger Interface Compliance", () => {
194
+ test("implements all required ILogger methods", () => {
195
+ assert.strictEqual(typeof logger.showAndLogError, "function");
196
+ assert.strictEqual(typeof logger.logInfo, "function");
197
+ assert.strictEqual(typeof logger.logWarning, "function");
198
+ });
199
+ test("showAndLogError returns void", () => {
200
+ const result = logger.showAndLogError("Test");
201
+ assert.strictEqual(result, undefined);
202
+ });
203
+ test("logInfo returns void", () => {
204
+ const result = logger.logInfo("Test");
205
+ assert.strictEqual(result, undefined);
206
+ });
207
+ test("logWarning returns void", () => {
208
+ const result = logger.logWarning("Test");
209
+ assert.strictEqual(result, undefined);
210
+ });
211
+ });
212
+ suite("Edge Cases", () => {
213
+ test("handles very long messages", () => {
214
+ const longMessage = "A".repeat(10000);
215
+ logger.logInfo(longMessage);
216
+ assert.ok(consoleLogStub.calledOnce);
217
+ assert.strictEqual(consoleLogStub.firstCall.args[0].length, 10000);
218
+ });
219
+ test("handles messages with null bytes", () => {
220
+ const messageWithNull = "Message\x00with\x00nulls";
221
+ logger.logInfo(messageWithNull);
222
+ assert.ok(consoleLogStub.calledOnce);
223
+ });
224
+ test("handles Unicode characters", () => {
225
+ const unicode = "Hello 世界 🌍 Привет";
226
+ logger.logInfo(unicode);
227
+ assert.ok(consoleLogStub.calledOnce);
228
+ assert.strictEqual(consoleLogStub.firstCall.args[0], unicode);
229
+ });
230
+ test("does not throw on multiple calls", () => {
231
+ assert.doesNotThrow(() => {
232
+ logger.logInfo("Message 1");
233
+ logger.logWarning("Warning 1");
234
+ logger.showAndLogError("Error 1");
235
+ logger.logInfo("Message 2");
236
+ });
237
+ assert.strictEqual(consoleLogStub.callCount, 2);
238
+ assert.strictEqual(consoleWarnStub.callCount, 1);
239
+ assert.ok(consoleErrorStub.callCount >= 1);
240
+ });
241
+ });
242
+ });
@@ -0,0 +1 @@
1
+ export {};