chaffjs 0.6.0 → 0.8.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.
Files changed (159) hide show
  1. package/README.md +11 -4
  2. package/dist/adapter-load.d.ts +12 -1
  3. package/dist/adapter-load.d.ts.map +1 -1
  4. package/dist/adapter-load.js +37 -13
  5. package/dist/adapter-load.js.map +1 -1
  6. package/dist/cli-args.d.ts +8 -0
  7. package/dist/cli-args.d.ts.map +1 -0
  8. package/dist/cli-args.js +8 -0
  9. package/dist/cli-args.js.map +1 -0
  10. package/dist/cli-text.d.ts +26 -0
  11. package/dist/cli-text.d.ts.map +1 -0
  12. package/dist/cli-text.js +151 -0
  13. package/dist/cli-text.js.map +1 -0
  14. package/dist/cli.d.ts +2 -0
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +83 -86
  17. package/dist/cli.js.map +1 -1
  18. package/dist/commands/cite.d.ts +2 -1
  19. package/dist/commands/cite.d.ts.map +1 -1
  20. package/dist/commands/cite.js +39 -18
  21. package/dist/commands/cite.js.map +1 -1
  22. package/dist/commands/eval.d.ts.map +1 -1
  23. package/dist/commands/eval.js +1 -1
  24. package/dist/commands/eval.js.map +1 -1
  25. package/dist/commands/skill.d.ts +22 -0
  26. package/dist/commands/skill.d.ts.map +1 -0
  27. package/dist/commands/skill.js +68 -0
  28. package/dist/commands/skill.js.map +1 -0
  29. package/dist/commands/tree.d.ts +11 -1
  30. package/dist/commands/tree.d.ts.map +1 -1
  31. package/dist/commands/tree.js +18 -6
  32. package/dist/commands/tree.js.map +1 -1
  33. package/dist/config/load.d.ts +9 -0
  34. package/dist/config/load.d.ts.map +1 -1
  35. package/dist/config/load.js +27 -1
  36. package/dist/config/load.js.map +1 -1
  37. package/dist/config/rule-problems.d.ts +11 -0
  38. package/dist/config/rule-problems.d.ts.map +1 -0
  39. package/dist/config/rule-problems.js +31 -0
  40. package/dist/config/rule-problems.js.map +1 -0
  41. package/dist/config/write.d.ts.map +1 -1
  42. package/dist/config/write.js +29 -9
  43. package/dist/config/write.js.map +1 -1
  44. package/dist/detectors/index.d.ts.map +1 -1
  45. package/dist/detectors/index.js +3 -0
  46. package/dist/detectors/index.js.map +1 -1
  47. package/dist/detectors/orthography.d.ts +9 -0
  48. package/dist/detectors/orthography.d.ts.map +1 -0
  49. package/dist/detectors/orthography.js +51 -0
  50. package/dist/detectors/orthography.js.map +1 -0
  51. package/dist/document.d.ts +3 -0
  52. package/dist/document.d.ts.map +1 -1
  53. package/dist/document.js +13 -3
  54. package/dist/document.js.map +1 -1
  55. package/dist/eval.d.ts +5 -1
  56. package/dist/eval.d.ts.map +1 -1
  57. package/dist/eval.js +10 -5
  58. package/dist/eval.js.map +1 -1
  59. package/dist/genre.d.ts +1 -1
  60. package/dist/genre.d.ts.map +1 -1
  61. package/dist/genre.js +3 -3
  62. package/dist/genre.js.map +1 -1
  63. package/dist/heading-text.d.ts +14 -0
  64. package/dist/heading-text.d.ts.map +1 -0
  65. package/dist/heading-text.js +28 -0
  66. package/dist/heading-text.js.map +1 -0
  67. package/dist/init.d.ts +2 -1
  68. package/dist/init.d.ts.map +1 -1
  69. package/dist/init.js +62 -13
  70. package/dist/init.js.map +1 -1
  71. package/dist/orthography.d.ts +21 -0
  72. package/dist/orthography.d.ts.map +1 -0
  73. package/dist/orthography.js +110 -0
  74. package/dist/orthography.js.map +1 -0
  75. package/dist/plugin.d.ts +6 -0
  76. package/dist/plugin.d.ts.map +1 -1
  77. package/dist/reasons.d.ts +15 -0
  78. package/dist/reasons.d.ts.map +1 -0
  79. package/dist/reasons.js +29 -0
  80. package/dist/reasons.js.map +1 -0
  81. package/dist/render/explain.d.ts.map +1 -1
  82. package/dist/render/explain.js +36 -12
  83. package/dist/render/explain.js.map +1 -1
  84. package/dist/render/friendly.d.ts.map +1 -1
  85. package/dist/render/friendly.js +32 -9
  86. package/dist/render/friendly.js.map +1 -1
  87. package/dist/render/rules-json.d.ts.map +1 -1
  88. package/dist/render/rules-json.js +57 -27
  89. package/dist/render/rules-json.js.map +1 -1
  90. package/dist/render/summary.d.ts +3 -2
  91. package/dist/render/summary.d.ts.map +1 -1
  92. package/dist/render/summary.js +24 -9
  93. package/dist/render/summary.js.map +1 -1
  94. package/dist/render/suppressions.d.ts +2 -1
  95. package/dist/render/suppressions.d.ts.map +1 -1
  96. package/dist/render/suppressions.js +34 -10
  97. package/dist/render/suppressions.js.map +1 -1
  98. package/dist/run.d.ts +3 -1
  99. package/dist/run.d.ts.map +1 -1
  100. package/dist/run.js +22 -13
  101. package/dist/run.js.map +1 -1
  102. package/dist/structure/build.d.ts.map +1 -1
  103. package/dist/structure/build.js +19 -10
  104. package/dist/structure/build.js.map +1 -1
  105. package/dist/structure/cite.d.ts.map +1 -1
  106. package/dist/structure/cite.js +3 -1
  107. package/dist/structure/cite.js.map +1 -1
  108. package/dist/structure/issues.d.ts +5 -2
  109. package/dist/structure/issues.d.ts.map +1 -1
  110. package/dist/structure/issues.js +27 -9
  111. package/dist/structure/issues.js.map +1 -1
  112. package/dist/structure/numbering.d.ts.map +1 -1
  113. package/dist/structure/numbering.js +11 -6
  114. package/dist/structure/numbering.js.map +1 -1
  115. package/dist/ui.d.ts +14 -0
  116. package/dist/ui.d.ts.map +1 -0
  117. package/dist/ui.js +14 -0
  118. package/dist/ui.js.map +1 -0
  119. package/dist/watch.d.ts +1 -1
  120. package/dist/watch.d.ts.map +1 -1
  121. package/dist/watch.js +2 -2
  122. package/dist/watch.js.map +1 -1
  123. package/package.json +3 -2
  124. package/rules/latin-spacing.yaml +27 -0
  125. package/rules/preferred-term.yaml +27 -0
  126. package/skills/chaff/SKILL.md +61 -0
  127. package/src/adapter-load.ts +41 -10
  128. package/src/cli-args.ts +9 -0
  129. package/src/cli-text.ts +182 -0
  130. package/src/cli.ts +100 -89
  131. package/src/commands/cite.ts +51 -18
  132. package/src/commands/eval.ts +1 -0
  133. package/src/commands/skill.ts +74 -0
  134. package/src/commands/tree.ts +27 -7
  135. package/src/config/load.ts +43 -1
  136. package/src/config/rule-problems.ts +43 -0
  137. package/src/config/write.ts +33 -9
  138. package/src/detectors/index.ts +3 -0
  139. package/src/detectors/orthography.ts +55 -0
  140. package/src/document.ts +23 -4
  141. package/src/eval.ts +16 -5
  142. package/src/genre.ts +4 -4
  143. package/src/heading-text.ts +26 -0
  144. package/src/init.ts +74 -15
  145. package/src/orthography.ts +115 -0
  146. package/src/plugin.ts +6 -0
  147. package/src/reasons.ts +44 -0
  148. package/src/render/explain.ts +46 -12
  149. package/src/render/friendly.ts +42 -10
  150. package/src/render/rules-json.ts +45 -10
  151. package/src/render/summary.ts +34 -9
  152. package/src/render/suppressions.ts +45 -10
  153. package/src/run.ts +35 -12
  154. package/src/structure/build.ts +20 -10
  155. package/src/structure/cite.ts +3 -1
  156. package/src/structure/issues.ts +33 -14
  157. package/src/structure/numbering.ts +12 -7
  158. package/src/ui.ts +23 -0
  159. package/src/watch.ts +2 -2
@@ -0,0 +1,115 @@
1
+ // 表記のそろい。書き方の決まりのうち、文字だけで決まるもの。
2
+
3
+ const escapeRegExp = (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
4
+
5
+ /** text の中で word が現れる [始まり, 終わり) を、左から順に。大文字小文字を区別しないときは i で探す(位置は元の文字列のまま)。 */
6
+ const spansOf = (text: string, word: string, ignoreCase: boolean): [number, number][] => {
7
+ const pattern = new RegExp(escapeRegExp(word), ignoreCase ? "giu" : "gu");
8
+ const found: [number, number][] = [];
9
+ for (let match = pattern.exec(text); match !== null; match = pattern.exec(text)) {
10
+ found.push([match.index, match.index + match[0].length]);
11
+ // 重なって現れるものも拾う。次は 1 つ先から探す。
12
+ pattern.lastIndex = match.index + 1;
13
+ }
14
+ return found;
15
+ };
16
+
17
+ /**
18
+ * avoid が現れる位置のうち、use の一部として現れたものを除いたもの。
19
+ * 「ユーザ」を避けて「ユーザー」と書く決まりのとき、「ユーザー」の中の「ユーザ」は指摘しない。
20
+ * 大文字小文字は区別しない(文頭の「E-mail」も「e-mail」)。ただし二つが大文字小文字だけ違う組
21
+ * (「Javascript」→「JavaScript」)は、それ自体が大文字小文字の決まりなので区別して探す。
22
+ */
23
+ export const occurrencesOutside = (text: string, avoid: string, use: string): number[] => {
24
+ if (avoid === "") return [];
25
+ const ignoreCase = avoid.toLowerCase() !== use.toLowerCase();
26
+ const covered = use === "" ? [] : spansOf(text, use, ignoreCase);
27
+ const found: number[] = [];
28
+ // covered は始まりの順に並ぶ。始まりが at 以前のものの終わりの最大が end 以上なら、どれかに収まっている。
29
+ let next = 0;
30
+ let reach = -1;
31
+ let at = 0;
32
+ for (const [start, end] of spansOf(text, avoid, ignoreCase)) {
33
+ if (start < at) continue;
34
+ while (next < covered.length && (covered[next]?.[0] ?? Infinity) <= start) {
35
+ reach = Math.max(reach, covered[next]?.[1] ?? -1);
36
+ next += 1;
37
+ }
38
+ if (reach < end) found.push(start);
39
+ at = end;
40
+ }
41
+ return found;
42
+ };
43
+
44
+ export type SpacingKind = "letter" | "before-digit" | "after-digit";
45
+ export type Boundary = { readonly offset: number; readonly kind: SpacingKind; readonly spaced: boolean };
46
+
47
+ const JAPANESE = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}ー]/u;
48
+ const LETTER = /[A-Za-z]/u;
49
+ const DIGIT = /\d/u;
50
+
51
+ /** Latin 側の字の種類。digitSide は、その数字が日本語の右(前に日本語)か左(後ろに日本語)か。 */
52
+ const kindOf = (char: string | undefined, digitSide: "before-digit" | "after-digit"): SpacingKind | undefined => {
53
+ if (char === undefined) return undefined;
54
+ if (LETTER.test(char)) return "letter";
55
+ return DIGIT.test(char) ? digitSide : undefined;
56
+ };
57
+
58
+ const isJapanese = (char: string | undefined): boolean => char !== undefined && JAPANESE.test(char);
59
+
60
+ /** japanese が日本語の字のとき、other の英字・数字の種類。 */
61
+ const latinBeside = (japanese: string | undefined, other: string | undefined, digitSide: "before-digit" | "after-digit"): SpacingKind | undefined =>
62
+ isJapanese(japanese) ? kindOf(other, digitSide) : undefined;
63
+
64
+ /** 日本語の字の隣にある英字・数字との境目。間が半角空白 1 つなら「空けている」、何も無ければ「詰めている」。 */
65
+ const ALPHANUMERIC = /[A-Za-z0-9.]/u;
66
+
67
+ /**
68
+ * 日本語の左にある英数字の並びの、先頭の字。「3GBの」の「GB」は数量の単位で、並びは数字で始まる。
69
+ * そうした並びと日本語の境目は、英字の空け方ではなく数字の後ろの空け方として数える(「3回」と同じ)。
70
+ */
71
+ const runStart = (chars: readonly string[], last: number): string | undefined => {
72
+ let first = last;
73
+ while (first > 0 && ALPHANUMERIC.test(chars[first - 1] ?? "")) first -= 1;
74
+ return chars[first];
75
+ };
76
+
77
+ /** 左の並びの種類。数字で始まる並び(3GB、10ms)は数字として扱う。 */
78
+ const leftRunBeside = (chars: readonly string[], last: number, japanese: string | undefined): SpacingKind | undefined => {
79
+ const kind = latinBeside(japanese, chars[last], "after-digit");
80
+ return kind === "letter" && DIGIT.test(runStart(chars, last) ?? "") ? "after-digit" : kind;
81
+ };
82
+
83
+ const boundaryAt = (chars: readonly string[], index: number): Omit<Boundary, "offset"> | undefined => {
84
+ const [left, right, after] = [chars[index], chars[index + 1], chars[index + 2]];
85
+ const touching = latinBeside(left, right, "before-digit") ?? leftRunBeside(chars, index, right);
86
+ if (touching !== undefined) return { kind: touching, spaced: false };
87
+ if (right !== " ") return undefined;
88
+ const spaced = latinBeside(left, after, "before-digit") ?? leftRunBeside(chars, index, after);
89
+ return spaced === undefined ? undefined : { kind: spaced, spaced: true };
90
+ };
91
+
92
+ /** 文の中の境目を全部。offset は境目の位置(左の字の直後、UTF-16)。配列を作り直さずに一度なめる。 */
93
+ export const latinBoundaries = (text: string): Boundary[] => {
94
+ const chars = [...text];
95
+ const found: Boundary[] = [];
96
+ let offset = 0;
97
+ chars.forEach((char, index) => {
98
+ offset += char.length;
99
+ const boundary = boundaryAt(chars, index);
100
+ if (boundary !== undefined) found.push({ ...boundary, offset });
101
+ });
102
+ return found;
103
+ };
104
+
105
+ /**
106
+ * 混ざっているとき、そろえるべき少数派。種類(英字・数字の前・数字の後ろ)ごとに数える。
107
+ * 同数なら、文書が先に使った書き方をその文書の書き方とみなし、後から出た書き方を少数派にする。
108
+ */
109
+ export const minorityStyle = (boundaries: readonly Boundary[]): boolean | undefined => {
110
+ const spaced = boundaries.filter((boundary) => boundary.spaced).length;
111
+ const touching = boundaries.length - spaced;
112
+ if (spaced === 0 || touching === 0) return undefined;
113
+ if (spaced !== touching) return spaced < touching;
114
+ return !(boundaries[0]?.spaced ?? false);
115
+ };
package/src/plugin.ts CHANGED
@@ -100,6 +100,8 @@ export type NumberedLine = {
100
100
  * 枝番号(第3条の2)は並びの外なので付けない。読み方を知っているのは言語パッケージだけなので、core は計算しない。
101
101
  */
102
102
  readonly ordinal?: number | undefined;
103
+ /** 一行で番号の範囲をまとめるとき(「第四十三条から第五十五条まで 削除」)の最後の位置。次の番号はここから数える。 */
104
+ readonly ordinalTo?: number | undefined;
103
105
  };
104
106
 
105
107
  /** 番号を読むときに見える周り。「(i)」がローマ数字か英字かは、開いている番号で決まる。 */
@@ -144,6 +146,10 @@ export type StructureNode = {
144
146
  readonly children: readonly StructureNode[];
145
147
  /** 番号付きのまとまりの、並びの中の位置(NumberedLine.ordinal)。S 式には出さない。 */
146
148
  readonly ordinal?: number;
149
+ /** 番号付きのまとまりの深さ(NumberedLine.depth)。項と号のように、同じ親の下で別の並びを分ける。S 式には出さない。 */
150
+ readonly level?: number;
151
+ /** 範囲をまとめた行の最後の位置(NumberedLine.ordinalTo)。S 式には出さない。 */
152
+ readonly ordinalTo?: number;
147
153
  };
148
154
 
149
155
  // ───────── 文書モデルと rule ─────────
package/src/reasons.ts ADDED
@@ -0,0 +1,44 @@
1
+ import type { Texts } from "./ui.ts";
2
+
3
+ /** Why a rule did not run, in the document's language. Listed under the findings so "0 findings" is never a silent pass. */
4
+ export type Reasons = {
5
+ readonly otherLanguage: (language: string) => string;
6
+ readonly noCapability: (capability: string) => string;
7
+ readonly noTags: string;
8
+ readonly semantic: string;
9
+ readonly experimental: string;
10
+ readonly turnedOff: string;
11
+ readonly noStructure: (language: string) => string;
12
+ readonly noDetector: (name: string) => string;
13
+ readonly noLexicon: (language: string, list: string) => string;
14
+ };
15
+
16
+ const CAPABILITY_NAME: Texts<Readonly<Record<string, string>>> = {
17
+ ja: { pos: "品詞解析", lemma: "原形" },
18
+ en: { pos: "part-of-speech tagging", lemma: "lemmas" },
19
+ };
20
+
21
+ export const REASONS: Texts<Reasons> = {
22
+ ja: {
23
+ otherLanguage: (language) => `${language} 向けの rule ではないため`,
24
+ noCapability: (capability) => `この言語では${CAPABILITY_NAME.ja[capability] ?? capability}が使えないため`,
25
+ noTags: "アダプタが品詞を返さなかったため",
26
+ semantic: "意味を読む検査のため(npx chaff test で動きます)",
27
+ experimental: "まだ試験中のため",
28
+ turnedOff: "設定で止めているため",
29
+ noStructure: (language) => `${language} のパッケージは文書の構造を読めないため`,
30
+ noDetector: (name) => `検出器 ${name} がないため`,
31
+ noLexicon: (language, list) => `${language} の語彙表 ${list} が無いため`,
32
+ },
33
+ en: {
34
+ otherLanguage: (language) => `not a rule for ${language}`,
35
+ noCapability: (capability) => `${CAPABILITY_NAME.en[capability] ?? capability} is not available for this language`,
36
+ noTags: "the language package returned no parts of speech",
37
+ semantic: "it reads meaning; npx chaff test runs it",
38
+ experimental: "still experimental",
39
+ turnedOff: "turned off in the settings",
40
+ noStructure: (language) => `the ${language} package cannot read a document's structure`,
41
+ noDetector: (name) => `no detector named ${name}`,
42
+ noLexicon: (language, list) => `the ${language} package has no word list ${list}`,
43
+ },
44
+ };
@@ -1,39 +1,73 @@
1
1
  import { definedLevels, resolve } from "../levels.ts";
2
2
  import { localized } from "./text.ts";
3
+ import { uiLanguageOf, type Texts } from "../ui.ts";
3
4
  import type { Level, RuleDefinition } from "../plugin.ts";
4
5
 
5
- const levelLine = (rule: RuleDefinition, level: Level, current: Level, genre: string | undefined): string => {
6
- const mark = level === current ? "\u2192" : " ";
6
+ const TEXT: Texts<{
7
+ readonly off: string;
8
+ readonly defaultGenre: string;
9
+ readonly genreNote: (genre: string, others: string) => string;
10
+ readonly experimental: string;
11
+ readonly howToFix: string;
12
+ readonly values: (unit: string) => string;
13
+ readonly now: (level: string) => string;
14
+ readonly change: (id: string) => string;
15
+ }> = {
16
+ ja: {
17
+ off: "見ない",
18
+ defaultGenre: "既定",
19
+ genreNote: (genre, others) => `この数字は ${genre} のものです。ほかに ${others} で別の数字を持っています。`,
20
+ experimental: "このルールはまだ試験中で、既定では動きません(--experimental で動きます)。",
21
+ howToFix: "直しかた",
22
+ values: (unit) => `設定できる値(単位: ${unit}):`,
23
+ now: (level) => `いまは ${level} です。`,
24
+ change: (id) => `変える: npx chaff relax ${id} --why "理由"`,
25
+ },
26
+ en: {
27
+ off: "not checked",
28
+ defaultGenre: "the default genre",
29
+ genreNote: (genre, others) => `These numbers are for ${genre}. ${others} have numbers of their own.`,
30
+ experimental: "This rule is still experimental and does not run by default (--experimental runs it).",
31
+ howToFix: "How to fix",
32
+ values: (unit) => `Levels (unit: ${unit}):`,
33
+ now: (level) => `Now: ${level}.`,
34
+ change: (id) => `Change it: npx chaff relax ${id} --why "reason"`,
35
+ },
36
+ };
37
+
38
+ const levelLine = (rule: RuleDefinition, level: Level, current: Level, genre: string | undefined, off: string): string => {
39
+ const mark = level === current ? "→" : " ";
7
40
  const name = level.padEnd(9);
8
- return level === "off" ? ` ${mark} ${name}見ない` : ` ${mark} ${name}${resolve(rule, level, genre).limit}`;
41
+ return level === "off" ? ` ${mark} ${name}${off}` : ` ${mark} ${name}${resolve(rule, level, genre).limit}`;
9
42
  };
10
43
 
11
44
  /** ジャンルで数字が変わる rule は、それを言わないと「設定したのに効かない」に見える。 */
12
- const genreNote = (rule: RuleDefinition, genre: string | undefined): string[] => {
45
+ const genreNote = (rule: RuleDefinition, genre: string | undefined, text: (typeof TEXT)["ja"]): string[] => {
13
46
  const others = Object.keys(rule.by_genre).filter((key) => key !== genre);
14
47
  if (others.length === 0) return [];
15
- return ["", ` この数字は ${genre ?? "既定"} のものです。ほかに ${others.join(" / ")} で別の数字を持っています。`];
48
+ return ["", ` ${text.genreNote(genre ?? text.defaultGenre, others.join(" / "))}`];
16
49
  };
17
50
 
18
51
  /** rule の意図と根拠を読む。指摘に納得できないときの入口。 */
19
52
  export const renderExplain = (rule: RuleDefinition, current: Level, language: string, unit: string, genre?: string): string => {
20
- const experimental = rule.status === "experimental" ? [" このルールはまだ試験中で、既定では動きません(--experimental で動きます)。"] : [];
53
+ const text = TEXT[uiLanguageOf(language)];
54
+ const experimental = rule.status === "experimental" ? [` ${text.experimental}`] : [];
21
55
  return [
22
56
  "",
23
57
  ` ${localized(rule.name, language)} (${rule.id})`,
24
58
  "",
25
59
  ` ${localized(rule.why, language)}`,
26
60
  "",
27
- ` 直しかた: ${localized(rule.how_to_fix, language)}`,
61
+ ` ${text.howToFix}: ${localized(rule.how_to_fix, language)}`,
28
62
  "",
29
- ` 設定できる値(単位: ${unit}):`,
30
- ...definedLevels(rule).map((level) => levelLine(rule, level, current, genre)),
31
- ...genreNote(rule, genre),
63
+ ` ${text.values(unit)}`,
64
+ ...definedLevels(rule).map((level) => levelLine(rule, level, current, genre, text.off)),
65
+ ...genreNote(rule, genre, text),
32
66
  "",
33
- ` いまは ${current} です。`,
67
+ ` ${text.now(current)}`,
34
68
  ...experimental,
35
69
  "",
36
- ` 変える: npx chaff relax ${rule.id} --why "理由"`,
70
+ ` ${text.change(rule.id)}`,
37
71
  "",
38
72
  ].join("\n");
39
73
  };
@@ -2,8 +2,41 @@ import type { Finding, RuleDefinition } from "../plugin.ts";
2
2
  import type { RunResult } from "../run.ts";
3
3
  import { MARK, localized, messageOf } from "./text.ts";
4
4
  import { tally } from "./summary.ts";
5
+ import { uiLanguageOf, type Texts } from "../ui.ts";
5
6
 
6
7
  const RULE = 60;
8
+
9
+ const TEXT: Texts<{
10
+ readonly line: (n: number) => string;
11
+ readonly relax: string;
12
+ readonly machine: string;
13
+ readonly deterministic: string;
14
+ readonly untouched: string;
15
+ readonly forced: (n: number, ids: string) => string;
16
+ readonly notRun: (n: number) => string;
17
+ readonly because: (why: string) => string;
18
+ }> = {
19
+ ja: {
20
+ line: (n) => `${n} 行目`,
21
+ relax: "このルールをゆるめる",
22
+ machine: "すべて機械による判定です",
23
+ deterministic: " (同じ文章なら何度実行しても同じ結果になります)",
24
+ untouched: "文章は書き換えていません。直すのは書いた人です。",
25
+ forced: (n, ids) => `試験中の rule を ${n} 件、設定により有効にしています: ${ids}`,
26
+ notRun: (n) => `${n} 件の rule は動いていません:`,
27
+ because: (why) => `(${why})`,
28
+ },
29
+ en: {
30
+ line: (n) => `line ${n}`,
31
+ relax: "Relax this rule",
32
+ machine: "All judged by machine",
33
+ deterministic: " (the same text gives the same result every time)",
34
+ untouched: "The text was not changed. Fixing it is the writer's job.",
35
+ forced: (n, ids) => `${n} experimental rule${n === 1 ? "" : "s"} turned on in the settings: ${ids}`,
36
+ notRun: (n) => `${n} rule${n === 1 ? "" : "s"} did not run:`,
37
+ because: (why) => ` (${why})`,
38
+ },
39
+ };
7
40
  const QUOTE_LIMIT = 120;
8
41
 
9
42
  const indent = (text: string, pad: string): string[] => text.split("\n").map((line) => `${pad}${line}`);
@@ -21,7 +54,8 @@ const quoteOf = (finding: Finding): string[] => {
21
54
  */
22
55
  const block = (finding: Finding, rule: RuleDefinition, language: string): string[] => [
23
56
  "",
24
- `─── ${finding.line} 行目 ${"─".repeat(Math.max(0, RULE - String(finding.line).length - 8))}`,
57
+ // "12 行目" and "line 12" take the same width on a terminal, so the rule ends in the same column in both.
58
+ `─── ${TEXT[uiLanguageOf(language)].line(finding.line)} ${"─".repeat(Math.max(0, RULE - String(finding.line).length - 8))}`,
25
59
  ...quoteOf(finding),
26
60
  "",
27
61
  ` ${MARK[finding.severity] ?? "·"} ${localized(rule.name, language)}`,
@@ -31,7 +65,7 @@ const block = (finding: Finding, rule: RuleDefinition, language: string): string
31
65
  "",
32
66
  ...indent(`→ ${localized(rule.how_to_fix, language)}`, " "),
33
67
  "",
34
- ` このルールをゆるめる: npx chaff relax ${finding.rule}`,
68
+ ` ${TEXT[uiLanguageOf(language)].relax}: npx chaff relax ${finding.rule}`,
35
69
  "",
36
70
  ];
37
71
 
@@ -41,22 +75,20 @@ export const renderFriendly = (header: string, result: RunResult, rules: readonl
41
75
  const rule = byId.get(finding.rule);
42
76
  return rule === undefined ? [] : block(finding, rule, language);
43
77
  });
78
+ const text = TEXT[uiLanguageOf(language)];
44
79
  const notes = [
45
80
  "",
46
81
  "─".repeat(RULE),
47
82
  "",
48
- ` ${tally(result.findings)} すべて機械による判定です`,
49
- " (同じ文章なら何度実行しても同じ結果になります)",
83
+ ` ${tally(result.findings, uiLanguageOf(language))} ${text.machine}`,
84
+ text.deterministic,
50
85
  "",
51
- " 文章は書き換えていません。直すのは書いた人です。",
86
+ ` ${text.untouched}`,
52
87
  ];
53
- const forced =
54
- result.forcedExperimental.length > 0
55
- ? ["", ` 試験中の rule を ${result.forcedExperimental.length} 件、設定により有効にしています: ${result.forcedExperimental.join(", ")}`]
56
- : [];
88
+ const forced = result.forcedExperimental.length > 0 ? ["", ` ${text.forced(result.forcedExperimental.length, result.forcedExperimental.join(", "))}`] : [];
57
89
  const skipped =
58
90
  result.skipped.length > 0
59
- ? ["", ` ${result.skipped.length} 件の rule は動いていません:`, ...result.skipped.map((entry) => ` ${entry.rule}(${entry.why})`)]
91
+ ? ["", ` ${text.notRun(result.skipped.length)}`, ...result.skipped.map((entry) => ` ${entry.rule}${text.because(entry.why)}`)]
60
92
  : [];
61
93
  return ["", header, ...blocks, ...notes, ...forced, ...skipped, ""].join("\n");
62
94
  };
@@ -1,17 +1,50 @@
1
1
  import { definedLevels, resolve } from "../levels.ts";
2
2
  import type { Config } from "../config/load.ts";
3
3
  import type { RuleDefinition } from "../plugin.ts";
4
+ import { uiLanguageOf, type Texts } from "../ui.ts";
4
5
 
5
- const now = (rule: RuleDefinition, config: Config, genre: string): Record<string, unknown> => {
6
+ const TEXT: Texts<{
7
+ readonly offBySetting: string;
8
+ readonly offExperimental: string;
9
+ readonly valuesNote: string;
10
+ readonly reason: string;
11
+ readonly byFile: string;
12
+ }> = {
13
+ ja: {
14
+ offBySetting: "設定で止めている",
15
+ offExperimental: "experimental な rule は既定で動かさない",
16
+ valuesNote: "この 4 語のかわりに数字を直接書いてもよい。4 語のほうを勧める。",
17
+ reason: "<理由>",
18
+ byFile: "chaff.yaml の rules に <rule-id>: <level> を足す。既定のままのものは書かない。",
19
+ },
20
+ en: {
21
+ offBySetting: "turned off in the settings",
22
+ offExperimental: "experimental rules do not run by default",
23
+ valuesNote: "A number may be written instead of these four words. The words are recommended.",
24
+ reason: "<reason>",
25
+ byFile: "Add <rule-id>: <level> under rules in chaff.yaml. Leave out anything at its default.",
26
+ },
27
+ };
28
+
29
+ const now = (rule: RuleDefinition, config: Config, genre: string, text: (typeof TEXT)["ja"]): Record<string, unknown> => {
6
30
  const explicit = config.rules[rule.id];
7
- if (explicit === "off") return { level: "off", why_off: "設定で止めている" };
31
+ if (explicit === "off") return { level: "off", why_off: text.offBySetting };
32
+ const limit = rule.layer === "L4" ? undefined : config.limits[rule.id];
33
+ if (limit !== undefined) return { level: "normal", limit, set_as: "number" };
8
34
  if (explicit !== undefined) return { level: explicit, limit: resolve(rule, explicit, genre).limit };
9
35
  if (rule.status === "experimental" && !config.experimental) {
10
- return { level: "off", why_off: "experimental な rule は既定で動かさない", turn_on_with: `npx chaff lint --experimental` };
36
+ return { level: "off", why_off: text.offExperimental, turn_on_with: `npx chaff lint --experimental` };
11
37
  }
12
38
  return { level: "normal", limit: resolve(rule, "normal", genre).limit };
13
39
  };
14
40
 
41
+ const yourSetting = (rule: RuleDefinition, config: Config): Record<string, unknown> | null => {
42
+ const level = config.rules[rule.id];
43
+ if (level === undefined) return null;
44
+ const limit = rule.layer === "L4" ? undefined : config.limits[rule.id];
45
+ return limit === undefined ? { level, from: config.path } : { level, limit, from: config.path };
46
+ };
47
+
15
48
  /** ジャンルで数字が変わる rule があるので、いま効いている表と既定の表の両方を出す。 */
16
49
  const levelsOf = (rule: RuleDefinition, genre: string): Record<string, unknown> => {
17
50
  const effective = Object.fromEntries(
@@ -24,14 +57,15 @@ const levelsOf = (rule: RuleDefinition, genre: string): Record<string, unknown>
24
57
  };
25
58
 
26
59
  /** AI に設定を書かせるときの入口。推測せずに書けるだけの情報を 1 つに入れる。spec §19.3。 */
27
- export const rulesJson = (rules: readonly RuleDefinition[], config: Config, language: string, genre: string): string =>
28
- JSON.stringify(
60
+ export const rulesJson = (rules: readonly RuleDefinition[], config: Config, language: string, genre: string): string => {
61
+ const text = TEXT[uiLanguageOf(language)];
62
+ return JSON.stringify(
29
63
  {
30
64
  schema_version: 1,
31
65
  config_file: "chaff.yaml",
32
66
  detected: { genre, language },
33
67
  values_you_can_use: ["strict", "normal", "relaxed", "off"],
34
- values_note: "この 4 語のかわりに数字を直接書いてもよい。4 語のほうを勧める。",
68
+ values_note: text.valuesNote,
35
69
  rules: rules.map((rule) => ({
36
70
  id: rule.id,
37
71
  layer: rule.layer,
@@ -42,14 +76,15 @@ export const rulesJson = (rules: readonly RuleDefinition[], config: Config, lang
42
76
  use_for: rule.use_for,
43
77
  ...levelsOf(rule, genre),
44
78
  levels_you_can_set: definedLevels(rule),
45
- your_setting: config.rules[rule.id] === undefined ? null : { level: config.rules[rule.id], from: config.path },
46
- now: now(rule, config, genre),
79
+ your_setting: yourSetting(rule, config),
80
+ now: now(rule, config, genre, text),
47
81
  })),
48
82
  how_to_change: {
49
- by_command: ['npx chaff relax <rule-id> --why "<理由>"', 'npx chaff strict <rule-id> --why "<理由>"', 'npx chaff off <rule-id> --why "<理由>"'],
50
- by_file: "chaff.yaml の rules に <rule-id>: <level> を足す。既定のままのものは書かない。",
83
+ by_command: ["relax", "strict", "off"].map((command) => `npx chaff ${command} <rule-id> --why "${text.reason}"`),
84
+ by_file: text.byFile,
51
85
  },
52
86
  },
53
87
  null,
54
88
  2,
55
89
  );
90
+ };
@@ -1,22 +1,47 @@
1
1
  import type { Finding } from "../plugin.ts";
2
+ import type { Texts, UiLanguage } from "../ui.ts";
2
3
 
3
4
  export type FileOutcome = { readonly path: string; readonly findings: readonly Finding[]; readonly notRun: number };
4
5
 
5
6
  const count = (findings: readonly Finding[], severity: string): number => findings.filter((finding) => finding.severity === severity).length;
6
7
 
7
- export const tally = (findings: readonly Finding[]): string => {
8
- const parts = [
9
- count(findings, "error") > 0 ? `エラー ${count(findings, "error")} 件` : undefined,
10
- count(findings, "warning") > 0 ? `注意 ${count(findings, "warning")} 件` : undefined,
11
- count(findings, "info") > 0 ? `参考 ${count(findings, "info")} 件` : undefined,
12
- ].filter((part) => part !== undefined);
13
- return parts.length === 0 ? "指摘はありません" : parts.join("、");
8
+ type Severity = "error" | "warning" | "info";
9
+
10
+ const TEXT: Texts<{
11
+ readonly counts: Readonly<Record<Severity, (n: number) => string>>;
12
+ readonly join: string;
13
+ readonly none: string;
14
+ readonly files: (seen: number, touched: number) => string;
15
+ }> = {
16
+ ja: {
17
+ counts: { error: (n) => `エラー ${n} 件`, warning: (n) => `注意 ${n} 件`, info: (n) => `参考 ${n} 件` },
18
+ join: "、",
19
+ none: "指摘はありません",
20
+ files: (seen, touched) => `${seen} ファイルを見て、${touched} ファイルに指摘がありました`,
21
+ },
22
+ en: {
23
+ counts: {
24
+ error: (n) => `${n} error${n === 1 ? "" : "s"}`,
25
+ warning: (n) => `${n} warning${n === 1 ? "" : "s"}`,
26
+ info: (n) => `${n} note${n === 1 ? "" : "s"}`,
27
+ },
28
+ join: ", ",
29
+ none: "No findings",
30
+ files: (seen, touched) => `${seen} files checked, ${touched} with findings`,
31
+ },
32
+ };
33
+
34
+ const SEVERITIES: readonly Severity[] = ["error", "warning", "info"];
35
+
36
+ export const tally = (findings: readonly Finding[], ui: UiLanguage): string => {
37
+ const parts = SEVERITIES.filter((severity) => count(findings, severity) > 0).map((severity) => TEXT[ui].counts[severity](count(findings, severity)));
38
+ return parts.length === 0 ? TEXT[ui].none : parts.join(TEXT[ui].join);
14
39
  };
15
40
 
16
41
  /** 複数ファイルを見たときの締め。1 ファイルのときは出さない。 */
17
- export const renderSummary = (outcomes: readonly FileOutcome[]): string[] => {
42
+ export const renderSummary = (outcomes: readonly FileOutcome[], ui: UiLanguage): string[] => {
18
43
  if (outcomes.length < 2) return [];
19
44
  const all = outcomes.flatMap((outcome) => outcome.findings);
20
45
  const touched = outcomes.filter((outcome) => outcome.findings.length > 0).length;
21
- return ["", "═".repeat(60), "", ` ${outcomes.length} ファイルを見て、${touched} ファイルに指摘がありました`, ` ${tally(all)}`, ""];
46
+ return ["", "═".repeat(60), "", ` ${TEXT[ui].files(outcomes.length, touched)}`, ` ${tally(all, ui)}`, ""];
22
47
  };
@@ -1,4 +1,37 @@
1
1
  import type { Suppressed, Suppression } from "../stet.ts";
2
+ import type { Texts, UiLanguage } from "../ui.ts";
3
+
4
+ const TEXT: Texts<{
5
+ readonly nudge: string;
6
+ readonly more: (n: number) => string;
7
+ readonly count: (n: number) => string;
8
+ readonly reasons: (list: string) => string;
9
+ readonly relaxAll: (rule: string) => string;
10
+ readonly none: string;
11
+ readonly total: (n: number) => string;
12
+ readonly reasonless: (n: number) => string;
13
+ }> = {
14
+ ja: {
15
+ nudge: " ← 設定の見直しを検討してください",
16
+ more: (n) => ` ほか ${n} ファイル`,
17
+ count: (n) => `${String(n).padStart(3)} 件`,
18
+ reasons: (list) => `理由: ${list}`,
19
+ relaxAll: (rule) => `ルールごとゆるめる: npx chaff relax ${rule} --why "..."`,
20
+ none: "\n 抑制されている指摘はありません。\n",
21
+ total: (n) => `抑制されている指摘: ${n} 件`,
22
+ reasonless: (n) => `理由が書かれていない抑制: ${n} 件`,
23
+ },
24
+ en: {
25
+ nudge: " <- consider changing the setting instead",
26
+ more: (n) => ` and ${n} more file${n === 1 ? "" : "s"}`,
27
+ count: (n) => `${String(n).padStart(3)}`,
28
+ reasons: (list) => `reasons: ${list}`,
29
+ relaxAll: (rule) => `relax the whole rule: npx chaff relax ${rule} --why "..."`,
30
+ none: "\n No findings are silenced.\n",
31
+ total: (n) => `Silenced findings: ${n}`,
32
+ reasonless: (n) => `Silenced without a reason: ${n}`,
33
+ },
34
+ };
2
35
 
3
36
  export type PerFile = { readonly path: string; readonly suppressed: readonly Suppressed[]; readonly reasonless: readonly Suppression[] };
4
37
 
@@ -23,16 +56,17 @@ const groupByRule = (files: readonly PerFile[]): Group[] => {
23
56
  .sort((left, right) => right.count - left.count);
24
57
  };
25
58
 
26
- const groupLines = (group: Group): string[] => {
27
- const nudge = group.count >= NUDGE_AT ? " ← 設定の見直しを検討してください" : "";
59
+ const groupLines = (group: Group, ui: UiLanguage): string[] => {
60
+ const text = TEXT[ui];
61
+ const nudge = group.count >= NUDGE_AT ? text.nudge : "";
28
62
  const shown = group.files.slice(0, 3).join(", ");
29
- const rest = group.files.length > 3 ? ` ほか ${group.files.length - 3} ファイル` : "";
63
+ const rest = group.files.length > 3 ? text.more(group.files.length - 3) : "";
30
64
  return [
31
65
  "",
32
- ` ${group.rule.padEnd(26)}${String(group.count).padStart(3)} 件${nudge}`,
66
+ ` ${group.rule.padEnd(26)}${text.count(group.count)}${nudge}`,
33
67
  ` ${shown}${rest}`,
34
68
  ...(group.count >= NUDGE_AT && group.reasons.length > 0
35
- ? [` 理由: ${group.reasons.slice(0, 2).join(" / ")}`, ` ルールごとゆるめる: npx chaff relax ${group.rule} --why "..."`]
69
+ ? [` ${text.reasons(group.reasons.slice(0, 2).join(" / "))}`, ` ${text.relaxAll(group.rule)}`]
36
70
  : []),
37
71
  ];
38
72
  };
@@ -42,16 +76,17 @@ const groupLines = (group: Group): string[] => {
42
76
  * 同じ rule を何度も黙らせているなら、それは (b) ではなく (c) のサイン。
43
77
  * workflow spec §7.3。
44
78
  */
45
- export const renderSuppressions = (files: readonly PerFile[]): string => {
79
+ export const renderSuppressions = (files: readonly PerFile[], ui: UiLanguage = "ja"): string => {
80
+ const text = TEXT[ui];
46
81
  const groups = groupByRule(files);
47
82
  const total = groups.reduce((sum, group) => sum + group.count, 0);
48
- if (total === 0) return "\n 抑制されている指摘はありません。\n";
83
+ if (total === 0) return text.none;
49
84
  const reasonless = files.flatMap((file) => file.reasonless.map(() => file.path));
50
85
  return [
51
86
  "",
52
- ` 抑制されている指摘: ${total} 件`,
53
- ...groups.flatMap(groupLines),
54
- ...(reasonless.length > 0 ? ["", ` 理由が書かれていない抑制: ${reasonless.length} 件`, ` ${[...new Set(reasonless)].join(", ")}`] : []),
87
+ ` ${text.total(total)}`,
88
+ ...groups.flatMap((group) => groupLines(group, ui)),
89
+ ...(reasonless.length > 0 ? ["", ` ${text.reasonless(reasonless.length)}`, ` ${[...new Set(reasonless)].join(", ")}`] : []),
55
90
  "",
56
91
  ].join("\n");
57
92
  };