glotfile 0.5.2 → 0.5.4

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.
@@ -2734,7 +2734,7 @@ var init_local_settings = __esm({
2734
2734
  isEditorId = (v) => EDITOR_IDS.includes(v);
2735
2735
  DEFAULT_AI = {
2736
2736
  provider: "anthropic",
2737
- model: "claude-haiku-4-5-20251001",
2737
+ model: "claude-sonnet-4-6",
2738
2738
  endpoint: null,
2739
2739
  region: null,
2740
2740
  batchSize: 25
@@ -2744,6 +2744,46 @@ var init_local_settings = __esm({
2744
2744
  }
2745
2745
  });
2746
2746
 
2747
+ // src/server/glossary.ts
2748
+ function contains(haystack, needle, caseSensitive) {
2749
+ return caseSensitive ? haystack.includes(needle) : haystack.toLowerCase().includes(needle.toLowerCase());
2750
+ }
2751
+ function relevantGlossary(source, targetLocale, glossary) {
2752
+ const hints = [];
2753
+ for (const entry of glossary) {
2754
+ if (!contains(source, entry.term, entry.caseSensitive)) continue;
2755
+ hints.push({
2756
+ term: entry.term,
2757
+ doNotTranslate: entry.doNotTranslate,
2758
+ forced: entry.translations?.[targetLocale],
2759
+ notes: entry.notes
2760
+ });
2761
+ }
2762
+ return hints;
2763
+ }
2764
+ function glossaryViolations(source, value, targetLocale, glossary) {
2765
+ const out = [];
2766
+ for (const entry of glossary) {
2767
+ if (!contains(source, entry.term, entry.caseSensitive)) continue;
2768
+ if (entry.doNotTranslate) {
2769
+ if (!contains(value, entry.term, entry.caseSensitive)) {
2770
+ out.push({ term: entry.term, expected: entry.term, kind: "do-not-translate" });
2771
+ }
2772
+ continue;
2773
+ }
2774
+ const forced = entry.translations?.[targetLocale];
2775
+ if (forced && !contains(value, forced, entry.caseSensitive)) {
2776
+ out.push({ term: entry.term, expected: forced, kind: "forced" });
2777
+ }
2778
+ }
2779
+ return out;
2780
+ }
2781
+ var init_glossary = __esm({
2782
+ "src/server/glossary.ts"() {
2783
+ "use strict";
2784
+ }
2785
+ });
2786
+
2747
2787
  // src/server/glob.ts
2748
2788
  function globToRegExp(glob) {
2749
2789
  const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
@@ -2815,21 +2855,6 @@ function selectRequests(state, opts) {
2815
2855
  }
2816
2856
  return reqs;
2817
2857
  }
2818
- function relevantGlossary(source, targetLocale, glossary) {
2819
- const hints = [];
2820
- for (const entry of glossary) {
2821
- const haystack = entry.caseSensitive ? source : source.toLowerCase();
2822
- const needle = entry.caseSensitive ? entry.term : entry.term.toLowerCase();
2823
- if (!haystack.includes(needle)) continue;
2824
- hints.push({
2825
- term: entry.term,
2826
- doNotTranslate: entry.doNotTranslate,
2827
- forced: entry.translations?.[targetLocale],
2828
- notes: entry.notes
2829
- });
2830
- }
2831
- return hints;
2832
- }
2833
2858
  function attachScreenshots(reqs, state, projectRoot) {
2834
2859
  const cache2 = /* @__PURE__ */ new Map();
2835
2860
  for (const req of reqs) {
@@ -2936,6 +2961,7 @@ var MEDIA_TYPES, MAX_IMAGE_BYTES, DEFAULT_LOCALE_CONCURRENCY;
2936
2961
  var init_run = __esm({
2937
2962
  "src/server/ai/run.ts"() {
2938
2963
  "use strict";
2964
+ init_glossary();
2939
2965
  init_placeholders();
2940
2966
  init_plurals();
2941
2967
  init_state();
@@ -3113,7 +3139,7 @@ function findMissing(state) {
3113
3139
  const entry = state.keys[key];
3114
3140
  if (entry.skipTranslate) continue;
3115
3141
  for (const locale of targets) {
3116
- const v = entry.plural ? entry.values[locale]?.forms?.other?.trim() : entry.values[locale]?.value;
3142
+ const v = entry.plural ? entry.values[locale]?.forms?.other?.trim() : entry.values[locale]?.value?.trim();
3117
3143
  if (!v) out.push({ key, locale });
3118
3144
  }
3119
3145
  }
@@ -3581,24 +3607,83 @@ var init_context = __esm({
3581
3607
  }
3582
3608
  });
3583
3609
 
3584
- // src/server/lint/spelling.ts
3585
- function tokenize(text) {
3586
- return text.match(new RegExp("\\p{L}[\\p{L}'']*", "gu")) ?? [];
3610
+ // src/server/spell.ts
3611
+ function spellTokens(value) {
3612
+ return value.replace(ICU_BLOCK, " ").replace(MASK, " ").match(WORD) ?? [];
3587
3613
  }
3588
- function buildAllowWords(glossary, dictionary2 = []) {
3614
+ function ignoreWordsFor(glossary, customWords = []) {
3589
3615
  const set = /* @__PURE__ */ new Set();
3590
- const add = (s) => {
3591
- for (const w of tokenize(s)) set.add(w.toLowerCase());
3616
+ const add = (text) => {
3617
+ for (const w of text.match(WORD) ?? []) set.add(w.toLowerCase());
3592
3618
  };
3593
- for (const g of glossary) add(g.term);
3594
- for (const w of dictionary2) add(w);
3619
+ for (const e of glossary) {
3620
+ add(e.term);
3621
+ for (const t of Object.values(e.translations ?? {})) add(t);
3622
+ }
3623
+ for (const w of customWords) add(w);
3595
3624
  return set;
3596
3625
  }
3626
+ async function getSpeller(dictId) {
3627
+ const key = norm(dictId);
3628
+ const existing = instances.get(key);
3629
+ if (existing) return existing;
3630
+ if (unavailable.has(key)) return null;
3631
+ try {
3632
+ const nspellMod = await import("nspell");
3633
+ const nspell = nspellMod.default ?? nspellMod;
3634
+ const dictMod = await import(`dictionary-${key}`);
3635
+ const dictExport = dictMod.default ?? dictMod;
3636
+ const dict = typeof dictExport === "function" ? await dictExport() : dictExport;
3637
+ const speller = nspell(dict);
3638
+ instances.set(key, speller);
3639
+ return speller;
3640
+ } catch {
3641
+ unavailable.add(key);
3642
+ return null;
3643
+ } finally {
3644
+ loading.delete(key);
3645
+ }
3646
+ }
3647
+ function spellValue(dictId, value, ignore) {
3648
+ const key = norm(dictId);
3649
+ if (unavailable.has(key)) return [];
3650
+ const spell = instances.get(key);
3651
+ if (!spell) {
3652
+ if (!loading.has(key)) {
3653
+ loading.add(key);
3654
+ void getSpeller(key);
3655
+ }
3656
+ return null;
3657
+ }
3658
+ const cacheKey = key + " " + value;
3659
+ let allBad = cache.get(cacheKey);
3660
+ if (!allBad) {
3661
+ allBad = spellTokens(value).filter((w) => !spell.correct(w));
3662
+ cache.set(cacheKey, allBad);
3663
+ }
3664
+ return allBad.filter((w) => !ignore.has(w.toLowerCase()));
3665
+ }
3666
+ var instances, loading, unavailable, cache, norm, ICU_BLOCK, MASK, WORD;
3667
+ var init_spell = __esm({
3668
+ "src/server/spell.ts"() {
3669
+ "use strict";
3670
+ instances = /* @__PURE__ */ new Map();
3671
+ loading = /* @__PURE__ */ new Set();
3672
+ unavailable = /* @__PURE__ */ new Set();
3673
+ cache = /* @__PURE__ */ new Map();
3674
+ norm = (dictId) => dictId.toLowerCase();
3675
+ ICU_BLOCK = /\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g;
3676
+ MASK = /\{[^}]*\}|<[^>]*>|:\w+|%[sd]/g;
3677
+ WORD = new RegExp("\\p{L}[\\p{L}''\u2019-]*", "gu");
3678
+ }
3679
+ });
3680
+
3681
+ // src/server/lint/spelling.ts
3597
3682
  var spellingRule, defaultLoader;
3598
3683
  var init_spelling = __esm({
3599
3684
  "src/server/lint/spelling.ts"() {
3600
3685
  "use strict";
3601
- init_placeholders();
3686
+ init_spell();
3602
3687
  spellingRule = {
3603
3688
  id: "spelling",
3604
3689
  run(state, ctx) {
@@ -3610,10 +3695,8 @@ var init_spelling = __esm({
3610
3695
  if (!speller) continue;
3611
3696
  const value = entry.values[locale]?.value;
3612
3697
  if (!value) continue;
3613
- const placeholders = new Set(extractPlaceholders(value).map((p) => p.toLowerCase()));
3614
- for (const word of tokenize(value)) {
3615
- const lower = word.toLowerCase();
3616
- if (placeholders.has(lower) || ctx.allowWords.has(lower)) continue;
3698
+ for (const word of spellTokens(value)) {
3699
+ if (ctx.allowWords.has(word.toLowerCase())) continue;
3617
3700
  if (!speller.correct(word)) {
3618
3701
  out.push({ ruleId: "spelling", key, locale, message: `possible misspelling: "${word}"` });
3619
3702
  }
@@ -3623,18 +3706,7 @@ var init_spelling = __esm({
3623
3706
  return out;
3624
3707
  }
3625
3708
  };
3626
- defaultLoader = async (dictId) => {
3627
- try {
3628
- const nspellMod = await import("nspell");
3629
- const nspell2 = nspellMod.default ?? nspellMod;
3630
- const dictMod = await import(`dictionary-${dictId.toLowerCase()}`);
3631
- const dictExport = dictMod.default ?? dictMod;
3632
- const dict = typeof dictExport === "function" ? await dictExport() : dictExport;
3633
- return nspell2(dict);
3634
- } catch {
3635
- return null;
3636
- }
3637
- };
3709
+ defaultLoader = (dictId) => getSpeller(dictId);
3638
3710
  }
3639
3711
  });
3640
3712
 
@@ -3645,7 +3717,7 @@ var init_rules = __esm({
3645
3717
  "use strict";
3646
3718
  init_scan();
3647
3719
  init_placeholders();
3648
- init_run();
3720
+ init_glossary();
3649
3721
  init_spelling();
3650
3722
  emptySourceRule = {
3651
3723
  id: "empty-source",
@@ -3661,17 +3733,14 @@ var init_rules = __esm({
3661
3733
  };
3662
3734
  emptyTranslationRule = {
3663
3735
  id: "empty-translation",
3664
- run(state, ctx) {
3736
+ // findMissing is the shared "untranslated" walk (also behind the editor's
3737
+ // untranslated check and /scan/missing); a whitespace-only value counts as
3738
+ // missing there, so no separate whitespace pass is needed.
3739
+ run(state) {
3665
3740
  const out = [];
3666
3741
  for (const m of findMissing(state)) {
3667
3742
  out.push({ ruleId: "empty-translation", key: m.key, locale: m.locale, message: "translation is empty or missing" });
3668
3743
  }
3669
- for (const key of Object.keys(state.keys)) {
3670
- for (const locale of ctx.targetLocales) {
3671
- const v = state.keys[key].values[locale]?.value;
3672
- if (v && !v.trim()) out.push({ ruleId: "empty-translation", key, locale, message: "translation is whitespace-only" });
3673
- }
3674
- }
3675
3744
  return out;
3676
3745
  }
3677
3746
  };
@@ -3799,13 +3868,13 @@ var init_rules = __esm({
3799
3868
  for (const locale of ctx.targetLocales) {
3800
3869
  const v = entry.values[locale]?.value;
3801
3870
  if (!v) continue;
3802
- for (const hint of relevantGlossary(src, locale, ctx.glossary)) {
3803
- if (hint.doNotTranslate && !v.includes(hint.term)) {
3804
- out.push({ ruleId: "glossary-violation", key, locale, message: `do-not-translate term "${hint.term}" is missing or altered` });
3805
- }
3806
- if (hint.forced && !v.includes(hint.forced)) {
3807
- out.push({ ruleId: "glossary-violation", key, locale, message: `expected glossary translation "${hint.forced}" for "${hint.term}"` });
3808
- }
3871
+ for (const viol of glossaryViolations(src, v, locale, ctx.glossary)) {
3872
+ out.push({
3873
+ ruleId: "glossary-violation",
3874
+ key,
3875
+ locale,
3876
+ message: viol.kind === "do-not-translate" ? `do-not-translate term "${viol.term}" is missing or altered` : `expected glossary translation "${viol.expected}" for "${viol.term}"`
3877
+ });
3809
3878
  }
3810
3879
  }
3811
3880
  }
@@ -3866,7 +3935,7 @@ async function runLint(state, options = {}) {
3866
3935
  const active = rules.filter(isActive);
3867
3936
  const spellingOn = active.some((r) => r.id === "spelling");
3868
3937
  const spellers = spellingOn ? await loadSpellers(targetLocales, config, load, warn) : /* @__PURE__ */ new Map();
3869
- const allowWords = spellingOn ? buildAllowWords(state.glossary, state.config.spelling?.customWords) : /* @__PURE__ */ new Set();
3938
+ const allowWords = spellingOn ? ignoreWordsFor(state.glossary, state.config.spelling?.customWords) : /* @__PURE__ */ new Set();
3870
3939
  const ctx = {
3871
3940
  config,
3872
3941
  sourceLocale: state.config.sourceLocale,
@@ -3904,6 +3973,7 @@ var init_run2 = __esm({
3904
3973
  init_registry();
3905
3974
  init_rules();
3906
3975
  init_spelling();
3976
+ init_spell();
3907
3977
  init_suppress();
3908
3978
  }
3909
3979
  });
@@ -4718,120 +4788,22 @@ var init_stats = __esm({
4718
4788
  }
4719
4789
  });
4720
4790
 
4721
- // node_modules/dictionary-en/index.js
4722
- var dictionary_en_exports = {};
4723
- __export(dictionary_en_exports, {
4724
- default: () => dictionary_en_default
4725
- });
4726
- import fs from "fs/promises";
4727
- var aff, dic, dictionary, dictionary_en_default;
4728
- var init_dictionary_en = __esm({
4729
- async "node_modules/dictionary-en/index.js"() {
4730
- "use strict";
4731
- aff = await fs.readFile(new URL("index.aff", import.meta.url));
4732
- dic = await fs.readFile(new URL("index.dic", import.meta.url));
4733
- dictionary = { aff, dic };
4734
- dictionary_en_default = dictionary;
4735
- }
4736
- });
4737
-
4738
- // src/server/spell.ts
4739
- import nspell from "nspell";
4740
- async function loadDictionary(locale) {
4741
- const key = norm(locale);
4742
- if (instances.has(key) || unavailable.has(key)) return;
4743
- const loader = LOADERS[key];
4744
- if (!loader) {
4745
- unavailable.add(key);
4746
- return;
4747
- }
4748
- try {
4749
- const { default: dict } = await loader();
4750
- instances.set(key, nspell(dict));
4751
- } catch {
4752
- unavailable.add(key);
4753
- } finally {
4754
- loading.delete(key);
4755
- }
4756
- }
4757
- function spellValue(locale, value, ignore) {
4758
- const key = norm(locale);
4759
- if (unavailable.has(key)) return [];
4760
- const spell = instances.get(key);
4761
- if (!spell) {
4762
- if (!LOADERS[key]) {
4763
- unavailable.add(key);
4764
- return [];
4765
- }
4766
- if (!loading.has(key)) {
4767
- loading.add(key);
4768
- void loadDictionary(key);
4769
- }
4770
- return null;
4771
- }
4772
- const cacheKey = key + " " + value;
4773
- let allBad = cache.get(cacheKey);
4774
- if (!allBad) {
4775
- const words = value.replace(ICU_BLOCK, " ").replace(MASK, " ").match(WORD) ?? [];
4776
- allBad = words.filter((w) => !spell.correct(w));
4777
- cache.set(cacheKey, allBad);
4778
- }
4779
- return allBad.filter((w) => !ignore.has(w.toLowerCase()));
4780
- }
4781
- var LOADERS, instances, loading, unavailable, cache, norm, ICU_BLOCK, MASK, WORD;
4782
- var init_spell = __esm({
4783
- "src/server/spell.ts"() {
4784
- "use strict";
4785
- LOADERS = {
4786
- en: () => init_dictionary_en().then(() => dictionary_en_exports),
4787
- es: () => import("dictionary-es"),
4788
- fr: () => import("dictionary-fr")
4789
- };
4790
- instances = /* @__PURE__ */ new Map();
4791
- loading = /* @__PURE__ */ new Set();
4792
- unavailable = /* @__PURE__ */ new Set();
4793
- cache = /* @__PURE__ */ new Map();
4794
- norm = (locale) => locale.toLowerCase();
4795
- ICU_BLOCK = /\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g;
4796
- MASK = /\{[^}]*\}|<[^>]*>|:\w+|%[sd]/g;
4797
- WORD = new RegExp("\\p{L}[\\p{L}''\u2019-]*", "gu");
4798
- }
4799
- });
4800
-
4801
4791
  // src/server/checks.ts
4802
- function contains(haystack, needle, caseSensitive) {
4803
- return caseSensitive ? haystack.includes(needle) : haystack.toLowerCase().includes(needle.toLowerCase());
4804
- }
4805
4792
  function runChecks(state, opts = {}) {
4806
4793
  const ruleOff = (id) => state.config.lint?.rules?.[CHECK_RULE[id]] === "off";
4807
4794
  const on = (id) => (!opts.only || opts.only.includes(id)) && !ruleOff(id);
4808
4795
  const issues = [];
4809
4796
  let spellPending = false;
4810
4797
  const { sourceLocale } = state.config;
4811
- const byTerm = new Map(state.glossary.map((e) => [e.term, e]));
4812
- const ignore = /* @__PURE__ */ new Set();
4813
- for (const e of state.glossary) {
4814
- for (const w of e.term.toLowerCase().split(/\s+/)) if (w) ignore.add(w);
4815
- for (const t of Object.values(e.translations ?? {})) {
4816
- for (const w of t.toLowerCase().split(/\s+/)) if (w) ignore.add(w);
4798
+ const ignore = ignoreWordsFor(state.glossary, state.config.spelling?.customWords);
4799
+ if (on("untranslated")) {
4800
+ for (const m of findMissing(state)) {
4801
+ issues.push({ key: m.key, locale: m.locale, check: "untranslated", message: "Not translated yet" });
4817
4802
  }
4818
4803
  }
4819
- for (const word of state.config.spelling?.customWords ?? []) {
4820
- const w = word.trim().toLowerCase();
4821
- if (w) ignore.add(w);
4822
- }
4823
- const targetLocales = state.config.locales.filter((l) => l !== sourceLocale);
4824
4804
  for (const key of Object.keys(state.keys).sort()) {
4825
4805
  const entry = state.keys[key];
4826
4806
  const source = entry.values[sourceLocale]?.value ?? "";
4827
- if (on("untranslated") && !entry.skipTranslate) {
4828
- for (const locale of targetLocales) {
4829
- const translated = entry.plural ? (entry.values[locale]?.forms?.other ?? "").trim() !== "" : (entry.values[locale]?.value ?? "").trim() !== "";
4830
- if (!translated) {
4831
- issues.push({ key, locale, check: "untranslated", message: "Not translated yet" });
4832
- }
4833
- }
4834
- }
4835
4807
  if (entry.plural) {
4836
4808
  if (on("placeholder")) {
4837
4809
  const sourceForm = entry.values[sourceLocale]?.forms?.other ?? "";
@@ -4875,7 +4847,8 @@ function runChecks(state, opts = {}) {
4875
4847
  });
4876
4848
  }
4877
4849
  if (on("spelling") && !blank) {
4878
- const bad = spellValue(locale, value, ignore);
4850
+ const dictId = state.config.lint?.spelling?.locales?.[locale] ?? locale;
4851
+ const bad = spellValue(dictId, value, ignore);
4879
4852
  if (bad === null) spellPending = true;
4880
4853
  else if (bad.length) {
4881
4854
  issues.push({
@@ -4905,29 +4878,14 @@ function runChecks(state, opts = {}) {
4905
4878
  });
4906
4879
  }
4907
4880
  if (on("glossary") && source) {
4908
- for (const hint of relevantGlossary(source, locale, state.glossary)) {
4909
- const cs = byTerm.get(hint.term)?.caseSensitive;
4910
- if (hint.doNotTranslate) {
4911
- if (!contains(value, hint.term, cs)) {
4912
- issues.push({
4913
- key,
4914
- locale,
4915
- check: "glossary",
4916
- message: `Do-not-translate term "${hint.term}" is missing from the translation`,
4917
- detail: [hint.term]
4918
- });
4919
- }
4920
- } else if (hint.forced) {
4921
- if (!contains(value, hint.forced, cs)) {
4922
- issues.push({
4923
- key,
4924
- locale,
4925
- check: "glossary",
4926
- message: `Should use "${hint.forced}" for "${hint.term}"`,
4927
- detail: [hint.forced]
4928
- });
4929
- }
4930
- }
4881
+ for (const viol of glossaryViolations(source, value, locale, state.glossary)) {
4882
+ issues.push({
4883
+ key,
4884
+ locale,
4885
+ check: "glossary",
4886
+ message: viol.kind === "do-not-translate" ? `Do-not-translate term "${viol.term}" is missing from the translation` : `Should use "${viol.expected}" for "${viol.term}"`,
4887
+ detail: [viol.expected]
4888
+ });
4931
4889
  }
4932
4890
  }
4933
4891
  }
@@ -4943,7 +4901,8 @@ var init_checks = __esm({
4943
4901
  "src/server/checks.ts"() {
4944
4902
  "use strict";
4945
4903
  init_placeholders();
4946
- init_run();
4904
+ init_glossary();
4905
+ init_scan();
4947
4906
  init_spell();
4948
4907
  init_suppress();
4949
4908
  CHECK_IDS = ["untranslated", "placeholder", "spelling", "length", "glossary"];