@scoutello/i18n-magic 0.54.0 → 0.56.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/src/lib/utils.ts CHANGED
@@ -3,6 +3,7 @@ import { Parser } from "i18next-scanner"
3
3
  import { minimatch } from "minimatch"
4
4
  import fs from "node:fs"
5
5
  import path from "node:path"
6
+ import { performance } from "node:perf_hooks"
6
7
  import type OpenAI from "openai"
7
8
  import prompts from "prompts"
8
9
  import { languages } from "./languges.js"
@@ -527,7 +528,9 @@ export const findExistingTranslation = async (
527
528
  for (const namespace of namespaces) {
528
529
  try {
529
530
  const existingKeys = await loadLocalesFile(loadPath, locale, namespace)
530
- if (existingKeys[key]) {
531
+ // Use explicit existence check instead of truthy check
532
+ // to handle empty string values correctly
533
+ if (Object.prototype.hasOwnProperty.call(existingKeys, key)) {
531
534
  return existingKeys[key]
532
535
  }
533
536
  } catch (error) {
@@ -567,7 +570,12 @@ export const findExistingTranslations = async (
567
570
  for (const key of keys) {
568
571
  let found = false
569
572
  for (const namespace of namespaces) {
570
- if (namespaceKeys[namespace]?.[key]) {
573
+ // Use explicit existence check instead of truthy check
574
+ // to handle empty string values correctly
575
+ if (
576
+ namespaceKeys[namespace] &&
577
+ Object.prototype.hasOwnProperty.call(namespaceKeys[namespace], key)
578
+ ) {
571
579
  results[key] = namespaceKeys[namespace][key]
572
580
  found = true
573
581
  break
@@ -692,29 +700,49 @@ export class TranslationError extends Error {
692
700
  * This function adds to the specified language locale, and will also translate
693
701
  * and save to other locales if OpenAI is configured.
694
702
  */
695
- export const addTranslationKey = async ({
696
- key,
697
- value,
698
- language = "en",
703
+ /**
704
+ * Add multiple translation keys in batch. This is optimized for performance:
705
+ * - Single codebase scan for all keys
706
+ * - Batched file I/O operations
707
+ * - Batched translations per locale
708
+ */
709
+ export const addTranslationKeys = async ({
710
+ keys,
699
711
  config,
700
712
  }: {
701
- key: string
702
- value: string
703
- language?: string
713
+ keys: Array<{ key: string; value: string; language?: string }>
704
714
  config: Configuration
705
715
  }) => {
716
+ const startTime = performance.now()
706
717
  const { loadPath, savePath, defaultNamespace, namespaces, globPatterns, defaultLocale, openai, context, model } = config
707
718
 
708
- // Try to find which namespaces this key is used in by scanning the codebase
709
- const affectedNamespaces: string[] = []
710
-
719
+ if (keys.length === 0) {
720
+ return { results: [], performance: { totalTime: 0, scanTime: 0, translationTime: 0, fileIOTime: 0 } }
721
+ }
722
+
723
+ const log = console.log
724
+ log(`🚀 Batch adding ${keys.length} translation key(s)...`)
725
+
726
+ // Step 1: Single codebase scan for all keys (most expensive operation)
727
+ const scanStartTime = performance.now()
728
+ let keysWithNamespaces: Array<{ key: string; namespaces: string[]; file: string }> = []
711
729
  try {
712
- // Scan the codebase to find where this key is already being used
713
- const keysWithNamespaces = await getKeysWithNamespaces({
730
+ keysWithNamespaces = await getKeysWithNamespaces({
714
731
  globPatterns,
715
732
  defaultNamespace,
716
733
  })
734
+ } catch (error) {
735
+ console.error(`Warning: Failed to scan codebase for key usage: ${error}`)
736
+ }
737
+ const scanTime = performance.now() - scanStartTime
738
+ log(`⏱️ Codebase scan completed in ${scanTime.toFixed(2)}ms`)
717
739
 
740
+ // Step 2: Determine namespaces for each key
741
+ const keyToNamespaces = new Map<string, Set<string>>()
742
+
743
+ for (const { key, language = "en" } of keys) {
744
+ const affectedNamespaces: string[] = []
745
+
718
746
  // Find entries for this specific key
719
747
  const keyEntries = keysWithNamespaces.filter(
720
748
  (entry) => entry.key === key || entry.key === `${defaultNamespace}:${key}`
@@ -730,112 +758,232 @@ export const addTranslationKey = async ({
730
758
 
731
759
  if (foundNamespaces.size > 0) {
732
760
  affectedNamespaces.push(...Array.from(foundNamespaces))
733
- console.log(
734
- `🔍 Found key "${key}" in use across ${affectedNamespaces.length} namespace(s): ${affectedNamespaces.join(", ")}`
735
- )
761
+ } else {
762
+ // If the key is not found in the codebase, use the default namespace
763
+ affectedNamespaces.push(defaultNamespace)
736
764
  }
737
- } catch (error) {
738
- // If scanning fails, continue with fallback logic
739
- console.error(`Warning: Failed to scan codebase for key usage: ${error}`)
740
- }
741
765
 
742
- if (affectedNamespaces.length === 0) {
743
- // If the key is not found in the codebase, use the default namespace
744
- affectedNamespaces.push(defaultNamespace)
745
- console.log(
746
- `📝 Key "${key}" not found in codebase. Adding to default namespace "${defaultNamespace}".`
747
- )
766
+ keyToNamespaces.set(key, new Set(affectedNamespaces))
748
767
  }
749
768
 
750
- // Use the specified language as the locale for adding keys
751
- const locale = language
752
-
753
- // Use console.error for logging when called from MCP server (console.log is suppressed)
754
- const log = console.log
755
-
756
- // Track which locales were successfully saved
757
- const savedLocales = new Set<string>([locale])
758
-
759
- for (const targetNamespace of affectedNamespaces) {
760
- log(`➕ Adding translation key "${key}" to namespace "${targetNamespace}" (${locale})`)
761
-
762
- // Load existing keys for the specified locale
763
- let existingKeys: Record<string, string>
764
- try {
765
- existingKeys = await loadLocalesFile(loadPath, locale, targetNamespace)
766
- } catch (error) {
767
- // If file doesn't exist, start with empty object
768
- log(`📄 Creating new namespace file for ${locale}/${targetNamespace}`)
769
- existingKeys = {}
769
+ // Step 3: Group keys by namespace and locale
770
+ const namespaceLocaleToKeys = new Map<string, Array<{ key: string; value: string; language: string }>>()
771
+
772
+ for (const { key, value, language = "en" } of keys) {
773
+ const namespaces = keyToNamespaces.get(key) || new Set([defaultNamespace])
774
+ for (const namespace of namespaces) {
775
+ const mapKey = `${namespace}:${language}`
776
+ if (!namespaceLocaleToKeys.has(mapKey)) {
777
+ namespaceLocaleToKeys.set(mapKey, [])
778
+ }
779
+ namespaceLocaleToKeys.get(mapKey)!.push({ key, value, language })
770
780
  }
781
+ }
771
782
 
772
- // Check if key already exists
773
- if (existingKeys[key]) {
774
- log(`⚠️ Key "${key}" already exists in ${locale}/${targetNamespace} with value: "${existingKeys[key]}"`)
775
- log(` Updating to new value: "${value}"`)
783
+ // Step 4: Collect all unique namespaces that will be affected
784
+ const affectedNamespaces = new Set<string>()
785
+ for (const namespaces of keyToNamespaces.values()) {
786
+ for (const ns of namespaces) {
787
+ affectedNamespaces.add(ns)
776
788
  }
789
+ }
777
790
 
778
- // Add or update the key
779
- existingKeys[key] = value
791
+ // Step 5: Batch load ALL locale files for affected namespaces (not just input language)
792
+ // This ensures we preserve existing keys in all locales
793
+ const fileIOStartTime = performance.now()
794
+ const localeFiles = new Map<string, Record<string, string>>()
795
+ const loadPromises: Promise<void>[] = []
796
+
797
+ // Load files for all locales × all affected namespaces
798
+ for (const namespace of affectedNamespaces) {
799
+ for (const locale of config.locales) {
800
+ const fileKey = `${locale}:${namespace}`
801
+
802
+ if (!localeFiles.has(fileKey)) {
803
+ loadPromises.push(
804
+ loadLocalesFile(loadPath, locale, namespace)
805
+ .then((keys) => {
806
+ localeFiles.set(fileKey, keys)
807
+ })
808
+ .catch(() => {
809
+ localeFiles.set(fileKey, {})
810
+ })
811
+ )
812
+ }
813
+ }
814
+ }
815
+
816
+ await Promise.all(loadPromises)
817
+ const fileIOTime = performance.now() - fileIOStartTime
818
+ log(`⏱️ File I/O (load) completed in ${fileIOTime.toFixed(2)}ms`)
819
+
820
+ // Step 6: Add new keys to the input language locale files
821
+ for (const [namespaceLocale, keyValues] of namespaceLocaleToKeys) {
822
+ const [namespace, locale] = namespaceLocale.split(":")
823
+ const fileKey = `${locale}:${namespace}`
824
+ const existingKeys = localeFiles.get(fileKey) || {}
825
+
826
+ for (const { key, value } of keyValues) {
827
+ existingKeys[key] = value
828
+ }
829
+
830
+ localeFiles.set(fileKey, existingKeys)
831
+ }
780
832
 
781
- // Save the updated keys using the writeLocalesFile function
782
- await writeLocalesFile(savePath, locale, targetNamespace, existingKeys)
783
- log(`✅ Successfully saved key to ${locale}/${targetNamespace}`)
833
+ // Step 7: Batch translate if OpenAI is configured
834
+ const translationStartTime = performance.now()
835
+ const translationCache = new Map<string, Record<string, string>>()
836
+
837
+ if (openai) {
838
+ // Group keys by input language
839
+ const keysByLanguage = new Map<string, Array<{ key: string; value: string; namespaces: Set<string> }>>()
840
+
841
+ for (const { key, value, language = "en" } of keys) {
842
+ if (!keysByLanguage.has(language)) {
843
+ keysByLanguage.set(language, [])
844
+ }
845
+ keysByLanguage.get(language)!.push({
846
+ key,
847
+ value,
848
+ namespaces: keyToNamespaces.get(key) || new Set([defaultNamespace]),
849
+ })
850
+ }
784
851
 
785
- // If OpenAI is configured, translate to other locales automatically
786
- if (openai) {
787
- const otherLocales = config.locales.filter(l => l !== locale)
852
+ // Translate each language group to all other locales
853
+ const translationPromises: Promise<void>[] = []
854
+
855
+ for (const [inputLanguage, keyValues] of keysByLanguage) {
856
+ const otherLocales = config.locales.filter(l => l !== inputLanguage)
788
857
 
789
- if (otherLocales.length > 0) {
790
- log(`🌐 Translating key "${key}" to ${otherLocales.length} other locale(s)...`)
858
+ for (const targetLocale of otherLocales) {
859
+ const keysToTranslate = Object.fromEntries(
860
+ keyValues.map(({ key, value }) => [key, value])
861
+ )
791
862
 
792
- try {
793
- // Translate to all other locales in parallel
794
- await Promise.all(
795
- otherLocales.map(async (targetLocale) => {
796
- try {
797
- const translatedValue = await translateKey({
798
- inputLanguage: locale,
799
- outputLanguage: targetLocale,
800
- context: context || "",
801
- object: { [key]: value },
802
- openai,
803
- model,
804
- })
805
-
806
- // Load existing keys for the target locale
807
- let targetLocaleKeys: Record<string, string>
808
- try {
809
- targetLocaleKeys = await loadLocalesFile(loadPath, targetLocale, targetNamespace)
810
- } catch (error) {
811
- log(`📄 Creating new namespace file for ${targetLocale}/${targetNamespace}`)
812
- targetLocaleKeys = {}
813
- }
814
-
815
- // Add the translated key
816
- targetLocaleKeys[key] = translatedValue[key]
817
-
818
- // Save the updated keys
819
- await writeLocalesFile(savePath, targetLocale, targetNamespace, targetLocaleKeys)
820
- savedLocales.add(targetLocale)
821
- log(`✅ Successfully translated and saved key to ${targetLocale}/${targetNamespace}`)
822
- } catch (error) {
823
- log(`⚠️ Failed to translate key to ${targetLocale}: ${error instanceof Error ? error.message : "Unknown error"}`)
824
- }
863
+ translationPromises.push(
864
+ translateKey({
865
+ inputLanguage,
866
+ outputLanguage: targetLocale,
867
+ context: context || "",
868
+ object: keysToTranslate,
869
+ openai,
870
+ model,
871
+ })
872
+ .then((translated) => {
873
+ translationCache.set(`${inputLanguage}:${targetLocale}`, translated)
825
874
  })
826
- )
827
- } catch (error) {
828
- log(`⚠️ Translation error: ${error instanceof Error ? error.message : "Unknown error"}`)
829
- log(` You can run 'i18n-magic sync' to translate this key later`)
875
+ .catch((error) => {
876
+ log(`⚠️ Failed to translate ${keyValues.length} key(s) from ${inputLanguage} to ${targetLocale}: ${error instanceof Error ? error.message : "Unknown error"}`)
877
+ })
878
+ )
879
+ }
880
+ }
881
+
882
+ await Promise.all(translationPromises)
883
+
884
+ // Add translated keys to locale files (merge with existing keys)
885
+ for (const [inputLanguage, keyValues] of keysByLanguage) {
886
+ const otherLocales = config.locales.filter(l => l !== inputLanguage)
887
+
888
+ for (const targetLocale of otherLocales) {
889
+ const translated = translationCache.get(`${inputLanguage}:${targetLocale}`)
890
+ if (!translated) continue
891
+
892
+ for (const { key, namespaces } of keyValues) {
893
+ if (translated[key]) {
894
+ for (const namespace of namespaces) {
895
+ const fileKey = `${targetLocale}:${namespace}`
896
+ // File should already be loaded from step 5, but ensure it exists
897
+ if (!localeFiles.has(fileKey)) {
898
+ localeFiles.set(fileKey, {})
899
+ }
900
+ // Merge translated key with existing keys (don't overwrite the whole file)
901
+ localeFiles.get(fileKey)![key] = translated[key]
902
+ }
903
+ }
830
904
  }
831
905
  }
832
906
  }
833
907
  }
908
+
909
+ const translationTime = performance.now() - translationStartTime
910
+ if (openai && translationTime > 0) {
911
+ log(`⏱️ Translation completed in ${translationTime.toFixed(2)}ms`)
912
+ }
913
+
914
+ // Step 8: Batch write all files
915
+ const writeStartTime = performance.now()
916
+ const writePromises: Promise<void>[] = []
917
+
918
+ for (const [fileKey, keys] of localeFiles) {
919
+ const [locale, namespace] = fileKey.split(":")
920
+ writePromises.push(
921
+ writeLocalesFile(savePath, locale, namespace, keys)
922
+ )
923
+ }
924
+
925
+ await Promise.all(writePromises)
926
+ const writeTime = performance.now() - writeStartTime
927
+ log(`⏱️ File I/O (write) completed in ${writeTime.toFixed(2)}ms`)
928
+
929
+ const totalTime = performance.now() - startTime
930
+ log(`✅ Batch operation completed in ${totalTime.toFixed(2)}ms (${(totalTime / keys.length).toFixed(2)}ms per key)`)
931
+
932
+ // Build results
933
+ const results = keys.map(({ key, value, language = "en" }) => {
934
+ const namespaces = Array.from(keyToNamespaces.get(key) || new Set([defaultNamespace]))
935
+ const savedLocales = new Set<string>([language])
936
+
937
+ if (openai) {
938
+ config.locales.forEach(locale => {
939
+ if (locale !== language) {
940
+ const translated = translationCache.get(`${language}:${locale}`)
941
+ if (translated?.[key]) {
942
+ savedLocales.add(locale)
943
+ }
944
+ }
945
+ })
946
+ }
947
+
948
+ return {
949
+ key,
950
+ value,
951
+ namespace: namespaces.join(", "),
952
+ locale: Array.from(savedLocales).sort().join(", "),
953
+ }
954
+ })
834
955
 
835
956
  return {
836
- key,
837
- value,
838
- namespace: affectedNamespaces.join(", "),
839
- locale: Array.from(savedLocales).sort().join(", "),
957
+ results,
958
+ performance: {
959
+ totalTime,
960
+ scanTime,
961
+ translationTime,
962
+ fileIOTime: fileIOTime + writeTime,
963
+ },
840
964
  }
841
965
  }
966
+
967
+ export const addTranslationKey = async ({
968
+ key,
969
+ value,
970
+ language = "en",
971
+ config,
972
+ }: {
973
+ key: string
974
+ value: string
975
+ language?: string
976
+ config: Configuration
977
+ }) => {
978
+ const startTime = performance.now()
979
+ const result = await addTranslationKeys({
980
+ keys: [{ key, value, language }],
981
+ config,
982
+ })
983
+ const totalTime = performance.now() - startTime
984
+
985
+ const log = console.log
986
+ log(`⏱️ Single key operation completed in ${totalTime.toFixed(2)}ms`)
987
+
988
+ return result.results[0]
989
+ }
package/src/mcp-server.ts CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  ListToolsRequestSchema,
6
6
  } from "@modelcontextprotocol/sdk/types.js"
7
7
  import { z } from "zod"
8
- import { addTranslationKey, getMissingKeys, loadConfig, loadLocalesFile } from "./lib/utils.js"
8
+ import { addTranslationKey, addTranslationKeys, getMissingKeys, loadConfig, loadLocalesFile } from "./lib/utils.js"
9
9
  import type { Configuration } from "./lib/types.js"
10
10
  import path from "path"
11
11
  import fs from "fs"
@@ -71,6 +71,15 @@ const AddTranslationKeySchema = z.object({
71
71
  language: z.string().optional().describe("The language code of the provided value (e.g., \"en\", \"de\", \"fr\"). Defaults to \"en\" (English) if not specified."),
72
72
  })
73
73
 
74
+ // Zod schema for the add_translation_keys (batch) tool parameters
75
+ const AddTranslationKeysSchema = z.object({
76
+ keys: z.array(z.object({
77
+ key: z.string().describe("The translation key to add (e.g., \"welcomeMessage\")"),
78
+ value: z.string().describe("The text value for this translation key"),
79
+ language: z.string().optional().describe("The language code of the provided value (e.g., \"en\", \"de\", \"fr\"). Defaults to \"en\" (English) if not specified."),
80
+ })).describe("Array of translation keys to add in batch. Use this for adding multiple keys at once for better performance."),
81
+ })
82
+
74
83
  // Zod schema for the list_untranslated_keys tool parameters
75
84
  const ListUntranslatedKeysSchema = z.object({
76
85
  namespace: z
@@ -184,7 +193,7 @@ class I18nMagicServer {
184
193
  tools: [
185
194
  {
186
195
  name: "add_translation_key",
187
- description: "Add a new translation key with a text value. You can optionally specify the language of the value you're providing (defaults to English).",
196
+ description: "Add a new translation key with a text value. You can optionally specify the language of the value you're providing (defaults to English). For adding multiple keys at once, use add_translation_keys instead for better performance.",
188
197
  inputSchema: {
189
198
  type: "object",
190
199
  properties: {
@@ -207,6 +216,38 @@ class I18nMagicServer {
207
216
  required: ["key", "value"],
208
217
  },
209
218
  },
219
+ {
220
+ name: "add_translation_keys",
221
+ description: "Add multiple translation keys in batch. This is optimized for performance - when adding 2 or more keys, prefer this over multiple add_translation_key calls. It performs a single codebase scan, batches file I/O operations, and batches translations for much better performance.",
222
+ inputSchema: {
223
+ type: "object",
224
+ properties: {
225
+ keys: {
226
+ type: "array",
227
+ description: "Array of translation keys to add in batch",
228
+ items: {
229
+ type: "object",
230
+ properties: {
231
+ key: {
232
+ type: "string",
233
+ description: "The translation key to add (e.g., \"welcomeMessage\")",
234
+ },
235
+ value: {
236
+ type: "string",
237
+ description: "The text value for this translation key",
238
+ },
239
+ language: {
240
+ type: "string",
241
+ description: "The language code of the provided value (e.g., \"en\" for English, \"de\" for German, \"fr\" for French). Defaults to \"en\" if not specified.",
242
+ },
243
+ },
244
+ required: ["key", "value"],
245
+ },
246
+ },
247
+ },
248
+ required: ["keys"],
249
+ },
250
+ },
210
251
  {
211
252
  name: "list_untranslated_keys",
212
253
  description:
@@ -399,6 +440,131 @@ class I18nMagicServer {
399
440
  }
400
441
  }
401
442
 
443
+ if (request.params.name === "add_translation_keys") {
444
+ try {
445
+ // Validate parameters
446
+ const params = AddTranslationKeysSchema.parse(request.params.arguments)
447
+
448
+ if (!params.keys || params.keys.length === 0) {
449
+ return {
450
+ content: [
451
+ {
452
+ type: "text",
453
+ text: JSON.stringify(
454
+ {
455
+ success: false,
456
+ error: "No keys provided. The 'keys' array must contain at least one key-value pair.",
457
+ },
458
+ null,
459
+ 2,
460
+ ),
461
+ },
462
+ ],
463
+ isError: true,
464
+ }
465
+ }
466
+
467
+ // Ensure config is loaded
468
+ const config = await this.ensureConfig()
469
+
470
+ // Capture console.log output for diagnostics
471
+ const originalConsoleLog = console.log
472
+ const logMessages: string[] = []
473
+ console.log = (...args: any[]) => {
474
+ const message = args.map(arg =>
475
+ typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
476
+ ).join(' ')
477
+ logMessages.push(message)
478
+ // Also log to stderr for debugging
479
+ console.error(`[i18n-magic] ${message}`)
480
+ }
481
+
482
+ let result
483
+ try {
484
+ // Add the translation keys in batch
485
+ result = await addTranslationKeys({
486
+ keys: params.keys.map(k => ({
487
+ key: k.key,
488
+ value: k.value,
489
+ language: k.language || "en",
490
+ })),
491
+ config,
492
+ })
493
+ } finally {
494
+ // Restore console.log
495
+ console.log = originalConsoleLog
496
+ }
497
+
498
+ return {
499
+ content: [
500
+ {
501
+ type: "text",
502
+ text: JSON.stringify(
503
+ {
504
+ success: true,
505
+ message: `Successfully added ${result.results.length} translation key(s) in batch`,
506
+ results: result.results,
507
+ performance: result.performance,
508
+ summary: {
509
+ totalKeys: result.results.length,
510
+ totalTime: `${result.performance.totalTime.toFixed(2)}ms`,
511
+ averageTimePerKey: `${(result.performance.totalTime / result.results.length).toFixed(2)}ms`,
512
+ scanTime: `${result.performance.scanTime.toFixed(2)}ms`,
513
+ translationTime: `${result.performance.translationTime.toFixed(2)}ms`,
514
+ fileIOTime: `${result.performance.fileIOTime.toFixed(2)}ms`,
515
+ },
516
+ diagnostics: logMessages.join('\n'),
517
+ },
518
+ null,
519
+ 2,
520
+ ),
521
+ },
522
+ ],
523
+ }
524
+ } catch (error) {
525
+ const errorMessage =
526
+ error instanceof Error ? error.message : "Unknown error occurred"
527
+
528
+ // Get more detailed error information
529
+ let errorDetails = errorMessage
530
+ if (error instanceof Error) {
531
+ // Check if there's a cause
532
+ const cause = (error as any).cause
533
+ if (cause instanceof Error) {
534
+ errorDetails = `${errorMessage}\nCause: ${cause.message}\nStack: ${cause.stack}`
535
+ } else if (cause) {
536
+ errorDetails = `${errorMessage}\nCause: ${JSON.stringify(cause)}`
537
+ }
538
+ // Include stack trace
539
+ if (error.stack) {
540
+ errorDetails = `${errorDetails}\nStack: ${error.stack}`
541
+ }
542
+ }
543
+
544
+ // Log detailed error to stderr for debugging
545
+ console.error(`[i18n-magic MCP] Error adding translation keys in batch:`)
546
+ console.error(errorDetails)
547
+
548
+ return {
549
+ content: [
550
+ {
551
+ type: "text",
552
+ text: JSON.stringify(
553
+ {
554
+ success: false,
555
+ error: errorMessage,
556
+ details: errorDetails,
557
+ },
558
+ null,
559
+ 2,
560
+ ),
561
+ },
562
+ ],
563
+ isError: true,
564
+ }
565
+ }
566
+ }
567
+
402
568
  if (request.params.name === "list_untranslated_keys") {
403
569
  try {
404
570
  // Validate parameters