solid-translate 1.4.1 → 1.4.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/dist/vite.js CHANGED
@@ -14617,9 +14617,40 @@ function hashContent(content) {
14617
14617
 
14618
14618
  // src/translate.ts
14619
14619
  import { z } from "zod";
14620
- async function loadGenerateObject() {
14621
- const { generateObject } = await import("ai");
14622
- return generateObject;
14620
+ async function loadGenerateText() {
14621
+ const { generateText } = await import("ai");
14622
+ return generateText;
14623
+ }
14624
+ function extractJsonObject(text) {
14625
+ const start = text.indexOf("{");
14626
+ const end = text.lastIndexOf("}");
14627
+ if (start === -1 || end <= start) {
14628
+ throw new Error("model response contained no JSON object");
14629
+ }
14630
+ const parsed = JSON.parse(text.slice(start, end + 1));
14631
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
14632
+ throw new Error("model response was not a JSON object");
14633
+ }
14634
+ return parsed;
14635
+ }
14636
+ function collectBatchTranslations(parsed, requestedKeys) {
14637
+ let dict = parsed;
14638
+ const inner = parsed["translations"];
14639
+ if (typeof inner === "object" && inner !== null && !Array.isArray(inner) && // Only unwrap when the envelope key is not itself a requested key
14640
+ !requestedKeys.includes("translations")) {
14641
+ dict = inner;
14642
+ }
14643
+ const translations = {};
14644
+ const missing = [];
14645
+ for (const key of requestedKeys) {
14646
+ const value = dict[key];
14647
+ if (typeof value === "string" && value.length > 0) {
14648
+ translations[key] = value;
14649
+ } else {
14650
+ missing.push(key);
14651
+ }
14652
+ }
14653
+ return { translations, missing };
14623
14654
  }
14624
14655
  async function translateBatch(model, entries, targetLocale, sourceLocale, systemPrompt, contexts) {
14625
14656
  const keys = Object.keys(entries);
@@ -14646,26 +14677,48 @@ async function translateBatch(model, entries, targetLocale, sourceLocale, system
14646
14677
  ].join("\n");
14647
14678
  }
14648
14679
  }
14649
- const generateObject = await loadGenerateObject();
14650
- const { object } = await generateObject({
14651
- model,
14652
- schema: z.object({
14653
- translations: z.record(z.string(), z.string())
14654
- }),
14655
- system: systemPrompt || defaultSystem,
14656
- prompt: [
14657
- `Translate each value in this JSON object from "${sourceLocale}" to "${targetLocale}".`,
14658
- `Return a JSON object with the exact same keys and the translated values.`,
14659
- contextSection,
14660
- JSON.stringify(entries, null, 2)
14661
- ].join("\n")
14662
- });
14663
- return object.translations;
14680
+ const generateText = await loadGenerateText();
14681
+ const basePrompt = [
14682
+ `Translate each value in this JSON object from "${sourceLocale}" to "${targetLocale}".`,
14683
+ `Respond with ONLY a JSON object \u2014 no prose, no code fences \u2014 containing the exact same keys and the translated values.`,
14684
+ contextSection,
14685
+ JSON.stringify(entries, null, 2)
14686
+ ].join("\n");
14687
+ const attempt = async (prompt) => {
14688
+ const { text } = await generateText({
14689
+ model,
14690
+ system: systemPrompt || defaultSystem,
14691
+ prompt
14692
+ });
14693
+ return collectBatchTranslations(extractJsonObject(text), keys);
14694
+ };
14695
+ let { translations, missing } = await attempt(basePrompt);
14696
+ if (missing.length > 0) {
14697
+ const retryEntries = {};
14698
+ for (const key of missing) retryEntries[key] = entries[key];
14699
+ const retry = await attempt(
14700
+ [
14701
+ `Translate each value in this JSON object from "${sourceLocale}" to "${targetLocale}".`,
14702
+ `Respond with ONLY a JSON object \u2014 no prose, no code fences \u2014 containing the exact same keys and the translated values.`,
14703
+ contextSection,
14704
+ JSON.stringify(retryEntries, null, 2)
14705
+ ].join("\n")
14706
+ );
14707
+ translations = { ...translations, ...retry.translations };
14708
+ missing = retry.missing;
14709
+ }
14710
+ if (missing.length > 0) {
14711
+ const sample = missing.slice(0, 3).join('", "');
14712
+ throw new Error(
14713
+ `model returned no translation for ${missing.length} of ${keys.length} keys (e.g. "${sample}")`
14714
+ );
14715
+ }
14716
+ return translations;
14664
14717
  }
14665
14718
 
14666
14719
  // src/extract.ts
14667
14720
  var import_parser = __toESM(require_lib(), 1);
14668
- function extractStringsFromSource(code, filePath, warnings) {
14721
+ function extractStringsFromSource(code, filePath, warnings, options) {
14669
14722
  const results = [];
14670
14723
  const seen = /* @__PURE__ */ new Set();
14671
14724
  const warn = (line, message) => {
@@ -14687,26 +14740,158 @@ function extractStringsFromSource(code, filePath, warnings) {
14687
14740
  seen.add(entry.key);
14688
14741
  results.push(entry);
14689
14742
  };
14743
+ const bindings = collectModuleBindings(ast);
14744
+ const shadowStack = [];
14745
+ const resolveMarker = (localName) => {
14746
+ for (let i = shadowStack.length - 1; i >= 0; i--) {
14747
+ if (shadowStack[i].has(localName)) return null;
14748
+ }
14749
+ const imported = bindings.imports.get(localName);
14750
+ if (imported) {
14751
+ if (!isAcceptedImportSource(imported.source, options?.importSources)) {
14752
+ return null;
14753
+ }
14754
+ return MARKER_NAMES.has(imported.imported) ? imported.imported : null;
14755
+ }
14756
+ if (bindings.moduleLocals.has(localName)) return null;
14757
+ return MARKER_NAMES.has(localName) ? localName : null;
14758
+ };
14690
14759
  const visit = (node) => {
14760
+ const scopeBindings = collectScopeBindings(node);
14761
+ if (scopeBindings) shadowStack.push(scopeBindings);
14691
14762
  if (node.type === "JSXElement") {
14692
14763
  const name = jsxName(node);
14693
- if (name === "T") {
14764
+ const marker = name ? resolveMarker(name) : null;
14765
+ if (marker === "T") {
14694
14766
  const entry = processT(node, filePath, warn);
14695
14767
  if (entry) push(entry);
14696
- } else if (name === "Plural") {
14768
+ } else if (marker === "Plural") {
14697
14769
  for (const entry of processPlural(node, filePath, warn)) {
14698
14770
  push(entry);
14699
14771
  }
14700
14772
  }
14701
- } else if (node.type === "CallExpression" && node.callee?.type === "Identifier" && node.callee.name === "msg") {
14773
+ } else if (node.type === "CallExpression" && node.callee?.type === "Identifier" && resolveMarker(node.callee.name) === "msg") {
14702
14774
  const entry = processMsg(node, filePath, warn);
14703
14775
  if (entry) push(entry);
14704
14776
  }
14705
14777
  walkChildren(node, visit);
14778
+ if (scopeBindings) shadowStack.pop();
14706
14779
  };
14707
14780
  visit(ast);
14708
14781
  return results;
14709
14782
  }
14783
+ var MARKER_NAMES = /* @__PURE__ */ new Set([
14784
+ "msg",
14785
+ "T",
14786
+ "Var",
14787
+ "Num",
14788
+ "Currency",
14789
+ "DateTime",
14790
+ "Plural"
14791
+ ]);
14792
+ var DEFAULT_IMPORT_SOURCE_RE = /(^|\/)(solid-translate|i18n)(\.[cm]?[jt]sx?)?$/;
14793
+ function isAcceptedImportSource(source, importSources) {
14794
+ if (source === "solid-translate") return true;
14795
+ if (importSources) return importSources.includes(source);
14796
+ return DEFAULT_IMPORT_SOURCE_RE.test(source);
14797
+ }
14798
+ function collectModuleBindings(ast) {
14799
+ const imports = /* @__PURE__ */ new Map();
14800
+ const moduleLocals = /* @__PURE__ */ new Set();
14801
+ const body = ast.program?.body ?? [];
14802
+ for (const stmt of body) {
14803
+ if (stmt.type === "ImportDeclaration") {
14804
+ const source = String(stmt.source?.value ?? "");
14805
+ for (const spec of stmt.specifiers ?? []) {
14806
+ const local = spec.local?.name;
14807
+ if (typeof local !== "string") continue;
14808
+ if (spec.type === "ImportSpecifier") {
14809
+ const imported = spec.imported?.type === "Identifier" ? spec.imported.name : String(spec.imported?.value ?? "");
14810
+ imports.set(local, { imported, source });
14811
+ } else {
14812
+ imports.set(local, { imported: "*", source });
14813
+ }
14814
+ }
14815
+ } else {
14816
+ collectDeclaredNames(stmt, moduleLocals);
14817
+ }
14818
+ }
14819
+ return { imports, moduleLocals };
14820
+ }
14821
+ function collectDeclaredNames(stmt, into) {
14822
+ if (stmt.type === "ExportNamedDeclaration" || stmt.type === "ExportDefaultDeclaration") {
14823
+ if (stmt.declaration) collectDeclaredNames(stmt.declaration, into);
14824
+ return;
14825
+ }
14826
+ if (stmt.type === "VariableDeclaration") {
14827
+ for (const decl of stmt.declarations ?? []) {
14828
+ if (decl.id) collectPatternNames(decl.id, into);
14829
+ }
14830
+ return;
14831
+ }
14832
+ if ((stmt.type === "FunctionDeclaration" || stmt.type === "ClassDeclaration" || stmt.type === "TSEnumDeclaration") && stmt.id?.type === "Identifier") {
14833
+ addMarkerName(stmt.id.name, into);
14834
+ }
14835
+ }
14836
+ function collectPatternNames(pattern, into) {
14837
+ switch (pattern.type) {
14838
+ case "Identifier":
14839
+ addMarkerName(pattern.name, into);
14840
+ break;
14841
+ case "AssignmentPattern":
14842
+ collectPatternNames(pattern.left, into);
14843
+ break;
14844
+ case "RestElement":
14845
+ collectPatternNames(pattern.argument, into);
14846
+ break;
14847
+ case "ObjectPattern":
14848
+ for (const prop of pattern.properties ?? []) {
14849
+ if (prop.type === "ObjectProperty") {
14850
+ collectPatternNames(prop.value, into);
14851
+ } else if (prop.type === "RestElement") {
14852
+ collectPatternNames(prop.argument, into);
14853
+ }
14854
+ }
14855
+ break;
14856
+ case "ArrayPattern":
14857
+ for (const el of pattern.elements ?? []) {
14858
+ if (el) collectPatternNames(el, into);
14859
+ }
14860
+ break;
14861
+ }
14862
+ }
14863
+ function addMarkerName(name, into) {
14864
+ if (typeof name === "string" && MARKER_NAMES.has(name)) into.add(name);
14865
+ }
14866
+ var FUNCTION_TYPES = /* @__PURE__ */ new Set([
14867
+ "ArrowFunctionExpression",
14868
+ "FunctionExpression",
14869
+ "FunctionDeclaration",
14870
+ "ObjectMethod",
14871
+ "ClassMethod",
14872
+ "ClassPrivateMethod"
14873
+ ]);
14874
+ function collectScopeBindings(node) {
14875
+ const bound = /* @__PURE__ */ new Set();
14876
+ if (FUNCTION_TYPES.has(node.type)) {
14877
+ if (node.id?.type === "Identifier") addMarkerName(node.id.name, bound);
14878
+ for (const param of node.params ?? []) {
14879
+ collectPatternNames(param, bound);
14880
+ }
14881
+ } else if (node.type === "CatchClause" && node.param) {
14882
+ collectPatternNames(node.param, bound);
14883
+ } else if (node.type === "BlockStatement") {
14884
+ for (const stmt of node.body ?? []) {
14885
+ collectDeclaredNames(stmt, bound);
14886
+ }
14887
+ } else if (node.type === "ForStatement" || node.type === "ForOfStatement" || node.type === "ForInStatement") {
14888
+ const init = node.init ?? node.left;
14889
+ if (init?.type === "VariableDeclaration") {
14890
+ collectDeclaredNames(init, bound);
14891
+ }
14892
+ }
14893
+ return bound.size > 0 ? bound : null;
14894
+ }
14710
14895
  function walkChildren(node, visit) {
14711
14896
  for (const key of Object.keys(node)) {
14712
14897
  if (key === "loc" || key === "leadingComments" || key === "trailingComments" || key === "innerComments") {
@@ -15040,7 +15225,16 @@ async function syncLocaleFiles(options) {
15040
15225
  delete lock.keys[key];
15041
15226
  }
15042
15227
  const changedCount = Object.keys(changedKeys).length;
15043
- if (changedCount === 0 && deletedKeys.length === 0) {
15228
+ const missingByLocale = {};
15229
+ for (const targetLocale of targetLocales) {
15230
+ const existing = readTargetFile(join(localesDir, `${targetLocale}.json`));
15231
+ const missing = Object.keys(sourceDict).filter(
15232
+ (key) => !(key in changedKeys) && !(key in existing)
15233
+ );
15234
+ if (missing.length > 0) missingByLocale[targetLocale] = missing;
15235
+ }
15236
+ const missingLocaleCount = Object.keys(missingByLocale).length;
15237
+ if (changedCount === 0 && deletedKeys.length === 0 && missingLocaleCount === 0) {
15044
15238
  log("No changes detected in locale files.");
15045
15239
  return {
15046
15240
  status: "no-changes",
@@ -15049,7 +15243,7 @@ async function syncLocaleFiles(options) {
15049
15243
  failures: []
15050
15244
  };
15051
15245
  }
15052
- if (changedCount === 0) {
15246
+ if (changedCount === 0 && missingLocaleCount === 0) {
15053
15247
  for (const targetLocale of targetLocales) {
15054
15248
  const targetFilePath = join(localesDir, `${targetLocale}.json`);
15055
15249
  const existing = readTargetFile(targetFilePath);
@@ -15062,27 +15256,43 @@ async function syncLocaleFiles(options) {
15062
15256
  );
15063
15257
  return { status: "synced", translatedKeys: [], deletedKeys, failures: [] };
15064
15258
  }
15065
- log(
15066
- `Translating ${changedCount} key${changedCount > 1 ? "s" : ""} to ${targetLocales.length} locale${targetLocales.length > 1 ? "s" : ""}...`
15067
- );
15068
- const changedContexts = {};
15069
- for (const key of Object.keys(changedKeys)) {
15070
- const ctx = pendingEntries[key]?.context;
15071
- if (ctx) changedContexts[key] = ctx;
15259
+ if (changedCount > 0) {
15260
+ log(
15261
+ `Translating ${changedCount} key${changedCount > 1 ? "s" : ""} to ${targetLocales.length} locale${targetLocales.length > 1 ? "s" : ""}...`
15262
+ );
15072
15263
  }
15264
+ if (missingLocaleCount > 0) {
15265
+ const healTotal = Object.values(missingByLocale).reduce(
15266
+ (sum, keys) => sum + keys.length,
15267
+ 0
15268
+ );
15269
+ log(
15270
+ `Healing ${healTotal} key${healTotal > 1 ? "s" : ""} missing from ${missingLocaleCount} locale file${missingLocaleCount > 1 ? "s" : ""}...`
15271
+ );
15272
+ }
15273
+ const contextFor = (key) => pendingEntries[key]?.context ?? lock.keys[key]?.context;
15073
15274
  const failures = [];
15074
15275
  const failedKeys = /* @__PURE__ */ new Set();
15075
15276
  for (const targetLocale of targetLocales) {
15076
15277
  const targetFilePath = join(localesDir, `${targetLocale}.json`);
15077
15278
  const existing = readTargetFile(targetFilePath);
15078
- const entries = Object.entries(changedKeys);
15279
+ const localeEntries = { ...changedKeys };
15280
+ for (const key of missingByLocale[targetLocale] ?? []) {
15281
+ localeEntries[key] = sourceDict[key];
15282
+ }
15283
+ const localeContexts = {};
15284
+ for (const key of Object.keys(localeEntries)) {
15285
+ const ctx = contextFor(key);
15286
+ if (ctx) localeContexts[key] = ctx;
15287
+ }
15288
+ const entries = Object.entries(localeEntries);
15079
15289
  for (let i = 0; i < entries.length; i += batchSize) {
15080
15290
  const batch = Object.fromEntries(entries.slice(i, i + batchSize));
15081
15291
  try {
15082
15292
  const translated = await translate(
15083
15293
  batch,
15084
15294
  targetLocale,
15085
- changedContexts
15295
+ localeContexts
15086
15296
  );
15087
15297
  Object.assign(existing, translated);
15088
15298
  } catch (err) {
@@ -15152,7 +15362,8 @@ function solidTranslate(config) {
15152
15362
  batchSize = 50,
15153
15363
  translate = true,
15154
15364
  autoExtract = false,
15155
- include = ["src/**/*.tsx", "src/**/*.ts", "src/**/*.jsx"]
15365
+ include = ["src/**/*.tsx", "src/**/*.ts", "src/**/*.jsx"],
15366
+ extractImportSources
15156
15367
  } = config;
15157
15368
  let root;
15158
15369
  let resolvedLocalesDir;
@@ -15180,7 +15391,11 @@ function solidTranslate(config) {
15180
15391
  );
15181
15392
  let contexts = {};
15182
15393
  if (autoExtract) {
15183
- const extracted = await autoExtractStrings(root, include);
15394
+ const extracted = await autoExtractStrings(
15395
+ root,
15396
+ include,
15397
+ extractImportSources
15398
+ );
15184
15399
  contexts = extracted.contexts;
15185
15400
  let existingSource = {};
15186
15401
  if (existsSync2(sourceFilePath)) {
@@ -15343,7 +15558,7 @@ function solidTranslate(config) {
15343
15558
  };
15344
15559
  }
15345
15560
  var vite_default = solidTranslate;
15346
- async function autoExtractStrings(root, patterns) {
15561
+ async function autoExtractStrings(root, patterns, importSources) {
15347
15562
  const strings = {};
15348
15563
  const contexts = {};
15349
15564
  const warnings = [];
@@ -15356,7 +15571,8 @@ async function autoExtractStrings(root, patterns) {
15356
15571
  const extracted = extractStringsFromSource(
15357
15572
  code,
15358
15573
  relative(root, file),
15359
- warnings
15574
+ warnings,
15575
+ { importSources }
15360
15576
  );
15361
15577
  for (const entry of extracted) {
15362
15578
  strings[entry.key] = entry.source;