intor 2.1.0 → 2.2.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.
@@ -86,8 +86,8 @@ var normalizePathname = (rawPathname, options = {}) => {
86
86
  const length = rawPathname.length;
87
87
  let start = 0;
88
88
  let end = length - 1;
89
- while (start <= end && rawPathname.charCodeAt(start) <= 32) start++;
90
- while (end >= start && rawPathname.charCodeAt(end) <= 32) end--;
89
+ while (start <= end && (rawPathname.codePointAt(start) ?? 0) <= 32) start++;
90
+ while (end >= start && (rawPathname.codePointAt(end) ?? 0) <= 32) end--;
91
91
  if (start > end) return "/";
92
92
  let result = "";
93
93
  let hasSlash = false;
@@ -98,11 +98,7 @@ var normalizePathname = (rawPathname, options = {}) => {
98
98
  hasSlash = true;
99
99
  }
100
100
  } else {
101
- if (hasSlash || result === "") {
102
- result += "/" + char;
103
- } else {
104
- result += char;
105
- }
101
+ result += hasSlash || result === "" ? "/" + char : char;
106
102
  hasSlash = false;
107
103
  }
108
104
  }
@@ -119,7 +115,7 @@ var resolveRoutingOptions = (routing = {}) => {
119
115
  ...routing,
120
116
  firstVisit: {
121
117
  ...DEFAULT_ROUTING_OPTIONS.firstVisit,
122
- ...routing.firstVisit || {}
118
+ ...routing.firstVisit
123
119
  },
124
120
  basePath: normalizePathname(routing?.basePath || "")
125
121
  };
@@ -1,5 +1,5 @@
1
1
  import { Level, NormalizerConfig, FormatterConfig, LoggerPreset } from 'logry/edge';
2
- import { Locale, LocaleNamespaceMessages, FallbackLocalesMap } from 'intor-translator';
2
+ import { Locale, LocaleMessages, FallbackLocalesMap } from 'intor-translator';
3
3
 
4
4
  type CookieRawOptions = {
5
5
  /** Completely disable cookie usage (no read, no write, no lookup by name) - default: false */
@@ -109,7 +109,7 @@ type WithLoader = {
109
109
  };
110
110
  type IntorRawConfig = (WithLoader | WithoutLoader) & {
111
111
  readonly id?: string;
112
- readonly messages?: LocaleNamespaceMessages;
112
+ readonly messages?: LocaleMessages;
113
113
  readonly defaultLocale: Locale;
114
114
  readonly fallbackLocales?: FallbackLocalesMap;
115
115
  readonly translator?: TranslatorOptions;
@@ -120,7 +120,7 @@ type IntorRawConfig = (WithLoader | WithoutLoader) & {
120
120
  };
121
121
  type IntorResolvedConfig = (WithLoader | WithoutLoader) & {
122
122
  readonly id: string;
123
- readonly messages?: LocaleNamespaceMessages;
123
+ readonly messages?: LocaleMessages;
124
124
  readonly defaultLocale: Locale;
125
125
  readonly fallbackLocales: FallbackLocalesMap;
126
126
  readonly translator?: TranslatorOptions;
@@ -1,5 +1,5 @@
1
1
  import { Level, NormalizerConfig, FormatterConfig, LoggerPreset } from 'logry/edge';
2
- import { Locale, LocaleNamespaceMessages, FallbackLocalesMap } from 'intor-translator';
2
+ import { Locale, LocaleMessages, FallbackLocalesMap } from 'intor-translator';
3
3
 
4
4
  type CookieRawOptions = {
5
5
  /** Completely disable cookie usage (no read, no write, no lookup by name) - default: false */
@@ -109,7 +109,7 @@ type WithLoader = {
109
109
  };
110
110
  type IntorRawConfig = (WithLoader | WithoutLoader) & {
111
111
  readonly id?: string;
112
- readonly messages?: LocaleNamespaceMessages;
112
+ readonly messages?: LocaleMessages;
113
113
  readonly defaultLocale: Locale;
114
114
  readonly fallbackLocales?: FallbackLocalesMap;
115
115
  readonly translator?: TranslatorOptions;
@@ -120,7 +120,7 @@ type IntorRawConfig = (WithLoader | WithoutLoader) & {
120
120
  };
121
121
  type IntorResolvedConfig = (WithLoader | WithoutLoader) & {
122
122
  readonly id: string;
123
- readonly messages?: LocaleNamespaceMessages;
123
+ readonly messages?: LocaleMessages;
124
124
  readonly defaultLocale: Locale;
125
125
  readonly fallbackLocales: FallbackLocalesMap;
126
126
  readonly translator?: TranslatorOptions;
@@ -84,8 +84,8 @@ var normalizePathname = (rawPathname, options = {}) => {
84
84
  const length = rawPathname.length;
85
85
  let start = 0;
86
86
  let end = length - 1;
87
- while (start <= end && rawPathname.charCodeAt(start) <= 32) start++;
88
- while (end >= start && rawPathname.charCodeAt(end) <= 32) end--;
87
+ while (start <= end && (rawPathname.codePointAt(start) ?? 0) <= 32) start++;
88
+ while (end >= start && (rawPathname.codePointAt(end) ?? 0) <= 32) end--;
89
89
  if (start > end) return "/";
90
90
  let result = "";
91
91
  let hasSlash = false;
@@ -96,11 +96,7 @@ var normalizePathname = (rawPathname, options = {}) => {
96
96
  hasSlash = true;
97
97
  }
98
98
  } else {
99
- if (hasSlash || result === "") {
100
- result += "/" + char;
101
- } else {
102
- result += char;
103
- }
99
+ result += hasSlash || result === "" ? "/" + char : char;
104
100
  hasSlash = false;
105
101
  }
106
102
  }
@@ -117,7 +113,7 @@ var resolveRoutingOptions = (routing = {}) => {
117
113
  ...routing,
118
114
  firstVisit: {
119
115
  ...DEFAULT_ROUTING_OPTIONS.firstVisit,
120
- ...routing.firstVisit || {}
116
+ ...routing.firstVisit
121
117
  },
122
118
  basePath: normalizePathname(routing?.basePath || "")
123
119
  };
package/dist/index.cjs CHANGED
@@ -31,33 +31,6 @@ var DEFAULT_CACHE_OPTIONS = {
31
31
  // 1 hour
32
32
  };
33
33
 
34
- // src/shared/error/intor-error.ts
35
- var IntorError = class extends Error {
36
- constructor({ message, code, id }) {
37
- const fullMessage = id ? `[${id}] ${message}` : message;
38
- super(fullMessage);
39
- this.name = "IntorError";
40
- this.id = id;
41
- this.code = code;
42
- Object.setPrototypeOf(this, new.target.prototype);
43
- }
44
- };
45
-
46
- // src/modules/messages/load-local-messages/utils/read-message-record-file.ts
47
- var readMessageRecordFile = async (filePath, loggerOptions) => {
48
- const fileName = path__default.default.basename(filePath, ".json");
49
- const content = await fs__default.default.readFile(filePath, "utf-8");
50
- const parsed = JSON.parse(content);
51
- if (typeof parsed !== "object" || parsed === null) {
52
- throw new IntorError({
53
- id: loggerOptions.id,
54
- code: "INTOR_INVALID_MESSAGE_FORMAT" /* INVALID_MESSAGE_FORMAT */,
55
- message: "Invalid message format"
56
- });
57
- }
58
- return { fileName, content: parsed };
59
- };
60
-
61
34
  // src/shared/logger/global-logger-pool.ts
62
35
  function getGlobalLoggerPool() {
63
36
  if (!globalThis.__INTOR_LOGGER_POOL__) {
@@ -91,13 +64,25 @@ function getLogger({
91
64
  });
92
65
  pool.set(id, logger);
93
66
  if (pool.size > 1e3) {
94
- const keys = Array.from(pool.keys());
67
+ const keys = [...pool.keys()];
95
68
  for (const key of keys.slice(0, 200)) pool.delete(key);
96
69
  }
97
70
  }
98
71
  return logger;
99
72
  }
100
73
 
74
+ // src/shared/error/intor-error.ts
75
+ var IntorError = class extends Error {
76
+ constructor({ message, code, id }) {
77
+ const fullMessage = id ? `[${id}] ${message}` : message;
78
+ super(fullMessage);
79
+ this.name = "IntorError";
80
+ this.id = id;
81
+ this.code = code;
82
+ Object.setPrototypeOf(this, new.target.prototype);
83
+ }
84
+ };
85
+
101
86
  // src/modules/messages/load-local-messages/load-namespace-group/parse-message-file.ts
102
87
  var MAX_PATH_LENGTH = 260;
103
88
  var parseMessageFile = async (filePath, loggerOptions) => {
@@ -118,9 +103,17 @@ var parseMessageFile = async (filePath, loggerOptions) => {
118
103
  return null;
119
104
  }
120
105
  try {
121
- const { content } = await readMessageRecordFile(trimmedPath, loggerOptions);
122
- logger.trace(`Message file loaded.`, { filePath: trimmedPath });
123
- return content;
106
+ const content = await fs__default.default.readFile(trimmedPath, "utf8");
107
+ const parsed = JSON.parse(content);
108
+ if (typeof parsed !== "object" || parsed === null) {
109
+ throw new IntorError({
110
+ id: loggerOptions.id,
111
+ code: "INTOR_INVALID_MESSAGE_FORMAT" /* INVALID_MESSAGE_FORMAT */,
112
+ message: "Invalid message format"
113
+ });
114
+ }
115
+ logger.trace("Message file loaded.", { filePath: trimmedPath });
116
+ return parsed;
124
117
  } catch (error) {
125
118
  logger.warn("Failed to parse message file.", {
126
119
  filePath: trimmedPath,
@@ -214,7 +207,7 @@ var addToNamespaceGroup = ({
214
207
  const filePathsSet = new Set(group.filePaths);
215
208
  if (!filePathsSet.has(filePath)) {
216
209
  filePathsSet.add(filePath);
217
- group.filePaths = Array.from(filePathsSet);
210
+ group.filePaths = [...filePathsSet];
218
211
  namespaceGroups.set(nsKey, group);
219
212
  }
220
213
  };
@@ -224,14 +217,15 @@ var traverseDirectory = async ({
224
217
  options,
225
218
  currentDirPath,
226
219
  namespaceGroups,
227
- namespacePathSegments
220
+ namespacePathSegments,
221
+ readdir = fs__default.default.readdir
228
222
  }) => {
229
223
  const { limit } = options;
230
224
  const loggerOptions = options.logger || { id: "default" };
231
225
  const baseLogger = getLogger({ ...loggerOptions });
232
226
  const logger = baseLogger.child({ scope: "traverse-directory" });
233
227
  try {
234
- const dirents = await fs__default.default.readdir(currentDirPath, { withFileTypes: true });
228
+ const dirents = await readdir(currentDirPath, { withFileTypes: true });
235
229
  const dirPromises = dirents.map(
236
230
  (dirent) => limit(async () => {
237
231
  const filePath = path__default.default.join(currentDirPath, dirent.name);
@@ -247,7 +241,8 @@ var traverseDirectory = async ({
247
241
  namespaceGroups,
248
242
  currentDirPath: filePath,
249
243
  namespacePathSegments: [...namespacePathSegments, dirent.name],
250
- options
244
+ options,
245
+ readdir
251
246
  });
252
247
  }
253
248
  }).catch((error) => {
@@ -322,7 +317,7 @@ var loadSingleLocale = async ({
322
317
  namespaceGroups: [...namespaceGroups.entries()].map(([ns, val]) => ({
323
318
  namespace: ns,
324
319
  isAtRoot: val.isAtRoot,
325
- fileCounts: val.filePaths.length
320
+ fileCount: val.filePaths.length
326
321
  }))
327
322
  });
328
323
  const namespaceGroupTasks = [...namespaceGroups.entries()].filter(
@@ -353,8 +348,8 @@ var loadLocaleWithFallback = async ({
353
348
  }) => {
354
349
  const baseLogger = getLogger({ ...loggerOptions });
355
350
  const logger = baseLogger.child({ scope: "load-locale-with-fallback" });
356
- const localesToTry = [targetLocale, ...fallbackLocales];
357
- for (const locale of localesToTry) {
351
+ const candidateLocales = [targetLocale, ...fallbackLocales];
352
+ for (const locale of candidateLocales) {
358
353
  try {
359
354
  const validNamespaces = await loadSingleLocale({
360
355
  basePath,
@@ -373,7 +368,7 @@ var loadLocaleWithFallback = async ({
373
368
  }
374
369
  }
375
370
  logger.warn("All fallback locales failed.", {
376
- attemptedLocales: localesToTry
371
+ attemptedLocales: candidateLocales
377
372
  });
378
373
  return;
379
374
  };
@@ -390,7 +385,7 @@ function clearMessagesPool() {
390
385
 
391
386
  // src/shared/utils/merge-messages.ts
392
387
  var mergeMessages = (staticMessages = {}, loadedMessages = {}) => {
393
- const result = Object.keys(staticMessages).length ? { ...staticMessages } : {};
388
+ const result = Object.keys(staticMessages).length > 0 ? { ...staticMessages } : {};
394
389
  for (const locale in loadedMessages) {
395
390
  const loaded = loadedMessages[locale];
396
391
  if (!result[locale]) {
@@ -409,7 +404,7 @@ var mergeMessages = (staticMessages = {}, loadedMessages = {}) => {
409
404
  var CACHE_KEY_DELIMITER = "|";
410
405
  var sanitize = (k) => k.replaceAll(/[\u200B-\u200D\uFEFF]/g, "").replaceAll(/[\r\n]/g, "").trim();
411
406
  var normalizeCacheKey = (key, delimiter = CACHE_KEY_DELIMITER) => {
412
- if (!key) return null;
407
+ if (key === null || key === void 0) return null;
413
408
  if (Array.isArray(key)) {
414
409
  if (key.length === 0) return null;
415
410
  const normalized = key.map((k) => {
@@ -433,56 +428,44 @@ var resolveNamespaces = ({
433
428
  pathname
434
429
  }) => {
435
430
  const { loader } = config;
436
- const {
437
- routeNamespaces = {},
438
- namespaces: fallbackNamespaces
439
- } = loader;
440
- const { unprefixedPathname } = extractPathname({ config, pathname });
441
- const standardizedPathname = standardizePathname({
442
- config,
443
- pathname: unprefixedPathname
444
- });
431
+ const { routeNamespaces = {}, namespaces } = loader || {};
432
+ const standardizedPathname = standardizePathname({ config, pathname });
445
433
  const placeholderRemovedPathname = standardizedPathname.replace(
446
434
  `/${PREFIX_PLACEHOLDER}`,
447
435
  ""
448
436
  );
449
- const defaultNamespaces = routeNamespaces.default ?? [];
450
- const exactMatchNamespaces = routeNamespaces[standardizedPathname] ?? routeNamespaces[placeholderRemovedPathname];
451
- if (exactMatchNamespaces) {
452
- return [...defaultNamespaces, ...exactMatchNamespaces];
453
- }
454
- let bestMatch = "";
455
- let bestNamespaces;
437
+ const collected = [
438
+ ...routeNamespaces.default || [],
439
+ // default
440
+ ...namespaces || [],
441
+ // default
442
+ ...routeNamespaces[standardizedPathname] || [],
443
+ // exact match
444
+ ...routeNamespaces[placeholderRemovedPathname] || []
445
+ // exact match
446
+ ];
456
447
  const prefixPatterns = Object.keys(routeNamespaces).filter(
457
448
  (pattern) => pattern.endsWith("/*")
458
449
  );
459
450
  for (const pattern of prefixPatterns) {
460
451
  const basePath = pattern.replace(/\/\*$/, "");
461
- if (standardizedPathname.startsWith(basePath)) {
462
- if (basePath.length > bestMatch.length) {
463
- bestMatch = basePath;
464
- bestNamespaces = routeNamespaces[pattern];
465
- }
452
+ if (standardizedPathname.startsWith(basePath) || placeholderRemovedPathname.startsWith(basePath)) {
453
+ collected.push(...routeNamespaces[pattern] || []);
466
454
  }
467
455
  }
468
- const matchedNamespaces = bestNamespaces ?? routeNamespaces["/*"] ?? fallbackNamespaces ?? [];
469
- if (matchedNamespaces.length > 0) {
470
- return [...defaultNamespaces, ...matchedNamespaces];
471
- } else {
472
- return [...defaultNamespaces];
473
- }
456
+ return [...new Set(collected)];
474
457
  };
475
458
 
476
459
  // src/shared/utils/locale/normalize-locale.ts
460
+ var toCanonical = (input) => {
461
+ try {
462
+ return Intl.getCanonicalLocales(input)[0]?.toLowerCase();
463
+ } catch {
464
+ return;
465
+ }
466
+ };
477
467
  var normalizeLocale = (locale = "", supportedLocales = []) => {
478
468
  if (!locale || supportedLocales.length === 0) return;
479
- const toCanonical = (input) => {
480
- try {
481
- return Intl.getCanonicalLocales(input)[0]?.toLowerCase();
482
- } catch {
483
- return;
484
- }
485
- };
486
469
  const canonicalLocale = toCanonical(locale);
487
470
  if (!canonicalLocale) return;
488
471
  const supportedCanonicalMap = /* @__PURE__ */ new Map();
@@ -513,12 +496,12 @@ var resolvePreferredLocale = (acceptLanguageHeader, supportedLocales) => {
513
496
  const supportedLocalesSet = new Set(supportedLocales);
514
497
  const preferred = acceptLanguageHeader.split(",").map((part) => {
515
498
  const [lang, qValue] = part.split(";");
516
- const q = qValue ? parseFloat(qValue.split("=")[1]) : 1;
517
- if (isNaN(q)) {
499
+ const q = qValue ? Number.parseFloat(qValue.split("=")[1]) : 1;
500
+ if (Number.isNaN(q)) {
518
501
  return { lang: lang.trim(), q: 0 };
519
502
  }
520
503
  return { lang: lang.trim(), q };
521
- }).sort((a, b) => b.q - a.q).find(({ lang }) => supportedLocalesSet.has(lang))?.lang;
504
+ }).toSorted((a, b) => b.q - a.q).find(({ lang }) => supportedLocalesSet.has(lang))?.lang;
522
505
  return preferred;
523
506
  };
524
507
 
@@ -527,8 +510,8 @@ var normalizePathname = (rawPathname, options = {}) => {
527
510
  const length = rawPathname.length;
528
511
  let start = 0;
529
512
  let end = length - 1;
530
- while (start <= end && rawPathname.charCodeAt(start) <= 32) start++;
531
- while (end >= start && rawPathname.charCodeAt(end) <= 32) end--;
513
+ while (start <= end && (rawPathname.codePointAt(start) ?? 0) <= 32) start++;
514
+ while (end >= start && (rawPathname.codePointAt(end) ?? 0) <= 32) end--;
532
515
  if (start > end) return "/";
533
516
  let result = "";
534
517
  let hasSlash = false;
@@ -539,11 +522,7 @@ var normalizePathname = (rawPathname, options = {}) => {
539
522
  hasSlash = true;
540
523
  }
541
524
  } else {
542
- if (hasSlash || result === "") {
543
- result += "/" + char;
544
- } else {
545
- result += char;
546
- }
525
+ result += hasSlash || result === "" ? "/" + char : char;
547
526
  hasSlash = false;
548
527
  }
549
528
  }
@@ -567,8 +546,8 @@ var extractPathname = ({
567
546
  } else if (basePath && normalizedPathname === basePath) {
568
547
  prefixedPathname = "/";
569
548
  }
570
- const pathParts = prefixedPathname.split("/").filter(Boolean);
571
- const maybeLocale = pathParts[0] || "";
549
+ const pathPart = prefixedPathname.split("/").find(Boolean);
550
+ const maybeLocale = pathPart || "";
572
551
  const isLocalePrefixed = config.supportedLocales?.includes(maybeLocale);
573
552
  let unprefixedPathname = prefixedPathname;
574
553
  if (prefix === "all") {
@@ -601,7 +580,7 @@ var standardizePathname = ({
601
580
  PREFIX_PLACEHOLDER,
602
581
  normalizePathname(pathname)
603
582
  ];
604
- const standardizedPathname = parts.join("/").replace(/\/{2,}/g, "/");
583
+ const standardizedPathname = parts.join("/").replaceAll(/\/{2,}/g, "/");
605
584
  return normalizePathname(standardizedPathname);
606
585
  };
607
586
 
@@ -640,8 +619,8 @@ var loadLocalMessages = async ({
640
619
  loggerOptions.id,
641
620
  resolvedBasePath,
642
621
  locale,
643
- [...fallbackLocales ?? []].sort().join(","),
644
- [...namespaces ?? []].sort().join(",")
622
+ (fallbackLocales ?? []).toSorted().join(","),
623
+ (namespaces ?? []).toSorted().join(",")
645
624
  ]);
646
625
  if (cache.enabled && key) {
647
626
  const cached = await pool?.get(key);
@@ -786,8 +765,8 @@ var loadApiMessages = async ({
786
765
  loggerOptions.id,
787
766
  basePath,
788
767
  locale,
789
- [...fallbackLocales ?? []].sort().join(","),
790
- [...namespaces ?? []].sort().join(",")
768
+ (fallbackLocales ?? []).toSorted().join(","),
769
+ (namespaces ?? []).toSorted().join(",")
791
770
  ]);
792
771
  if (cache.enabled && key) {
793
772
  const cached = await pool?.get(key);
@@ -849,7 +828,7 @@ var loadMessages = async ({
849
828
  );
850
829
  return;
851
830
  }
852
- const { loader } = config;
831
+ const { id, loader, cache } = config;
853
832
  const fallbackLocales = config.fallbackLocales[locale] || [];
854
833
  const namespaces = resolveNamespaces({ config, pathname });
855
834
  logger.debug("Namespaces ready for loading.", {
@@ -864,8 +843,8 @@ var loadMessages = async ({
864
843
  locale,
865
844
  fallbackLocales,
866
845
  namespaces,
867
- cache: config.cache,
868
- logger: { id: config.id }
846
+ cache,
847
+ logger: { id }
869
848
  });
870
849
  } else if (loader.type === "api") {
871
850
  loadedMessages = await loadApiMessages({
@@ -873,7 +852,7 @@ var loadMessages = async ({
873
852
  locale,
874
853
  fallbackLocales,
875
854
  namespaces,
876
- logger: { id: config.id }
855
+ logger: { id }
877
856
  });
878
857
  }
879
858
  if (!loadedMessages || Object.keys(loadedMessages).length === 0) {
@@ -889,15 +868,10 @@ var intor = async (config, i18nContext) => {
889
868
  logger.info("Start Intor initialization.");
890
869
  const { messages, loader } = config;
891
870
  const isI18nContextFunction = typeof i18nContext === "function";
892
- let context;
893
- if (isI18nContextFunction) {
894
- context = await i18nContext(config);
895
- } else {
896
- context = {
897
- locale: i18nContext?.locale || config.defaultLocale,
898
- pathname: i18nContext?.pathname || ""
899
- };
900
- }
871
+ const context = isI18nContextFunction ? await i18nContext(config) : {
872
+ locale: i18nContext?.locale || config.defaultLocale,
873
+ pathname: i18nContext?.pathname || ""
874
+ };
901
875
  const { locale, pathname } = context;
902
876
  const source = isI18nContextFunction ? "[function]" : "[static object]";
903
877
  logger.debug(`Context resolved via ${source}.`, context);
@@ -922,29 +896,23 @@ var intor = async (config, i18nContext) => {
922
896
  };
923
897
  };
924
898
  async function getTranslator(opts) {
925
- const { config, locale, pathname = "", preKey } = opts;
899
+ const { config, locale, pathname = "", preKey, handlers } = opts;
926
900
  const messages = await loadMessages({ config, locale, pathname });
927
901
  const translator = new intorTranslator.Translator({
928
902
  locale,
929
903
  messages,
930
904
  fallbackLocales: config.fallbackLocales,
931
905
  loadingMessage: config.translator?.loadingMessage,
932
- placeholder: config.translator?.placeholder
906
+ placeholder: config.translator?.placeholder,
907
+ handlers
933
908
  });
934
- const props = {
935
- messages,
936
- locale
909
+ const props = { messages, locale };
910
+ const scoped = translator.scoped(preKey);
911
+ return {
912
+ ...props,
913
+ hasKey: preKey ? scoped.hasKey : translator.hasKey,
914
+ t: preKey ? scoped.t : translator.t
937
915
  };
938
- if (preKey) {
939
- const scoped = translator.scoped(preKey);
940
- return { ...props, ...scoped };
941
- } else {
942
- return {
943
- ...props,
944
- t: translator.t,
945
- hasKey: translator.hasKey
946
- };
947
- }
948
916
  }
949
917
 
950
918
  Object.defineProperty(exports, "Translator", {