champollion 0.3.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.
Files changed (170) hide show
  1. package/LICENSE +133 -0
  2. package/README.md +387 -0
  3. package/bin/cli.js +278 -0
  4. package/index.js +135 -0
  5. package/lib/api-key.js +127 -0
  6. package/lib/autofix.js +432 -0
  7. package/lib/bridge/method_bridge.py +430 -0
  8. package/lib/card-source-resolution.mjs +284 -0
  9. package/lib/cards/cache.js +169 -0
  10. package/lib/cards/env.js +82 -0
  11. package/lib/cards/fetch-card-child.js +38 -0
  12. package/lib/cards/reader.js +435 -0
  13. package/lib/cards/refresh.js +111 -0
  14. package/lib/cards/remote.js +387 -0
  15. package/lib/cldf-export.mjs +540 -0
  16. package/lib/cldf-terms.mjs +62 -0
  17. package/lib/command-help.js +790 -0
  18. package/lib/commands/audit.js +49 -0
  19. package/lib/commands/card.js +454 -0
  20. package/lib/commands/doctor.js +559 -0
  21. package/lib/commands/fonts.js +489 -0
  22. package/lib/commands/help.js +91 -0
  23. package/lib/commands/init.js +1259 -0
  24. package/lib/commands/integrity.js +148 -0
  25. package/lib/commands/leaderboard.js +478 -0
  26. package/lib/commands/lint.js +30 -0
  27. package/lib/commands/models.js +177 -0
  28. package/lib/commands/plugin.js +103 -0
  29. package/lib/commands/provenance.js +45 -0
  30. package/lib/commands/recommend.js +75 -0
  31. package/lib/commands/register-corpus.js +678 -0
  32. package/lib/commands/repair-script.js +42 -0
  33. package/lib/commands/seal-corpus.js +355 -0
  34. package/lib/commands/seo.js +72 -0
  35. package/lib/commands/serve.js +147 -0
  36. package/lib/commands/status.js +265 -0
  37. package/lib/commands/submit.js +332 -0
  38. package/lib/commands/sync.js +89 -0
  39. package/lib/commands/tm.js +573 -0
  40. package/lib/commands/verify.js +39 -0
  41. package/lib/commands/watch.js +20 -0
  42. package/lib/commands/wrap.js +138 -0
  43. package/lib/commands/xliff.js +327 -0
  44. package/lib/commercial-eligibility.js +235 -0
  45. package/lib/concurrent.js +87 -0
  46. package/lib/config.js +523 -0
  47. package/lib/contamination-lane.js +76 -0
  48. package/lib/content-sync.js +731 -0
  49. package/lib/content.js +733 -0
  50. package/lib/corpus-registration.mjs +608 -0
  51. package/lib/cost-report.js +346 -0
  52. package/lib/diff.js +155 -0
  53. package/lib/docusaurus-sync.js +1256 -0
  54. package/lib/flatten.js +55 -0
  55. package/lib/format.js +954 -0
  56. package/lib/hash.js +159 -0
  57. package/lib/icu.js +473 -0
  58. package/lib/integrity.js +689 -0
  59. package/lib/license-gate.mjs +478 -0
  60. package/lib/license-identify.mjs +229 -0
  61. package/lib/lint.js +629 -0
  62. package/lib/method-manifest.js +60 -0
  63. package/lib/methods/anthropic.js +140 -0
  64. package/lib/methods/apertium.js +163 -0
  65. package/lib/methods/api.js +316 -0
  66. package/lib/methods/base.js +184 -0
  67. package/lib/methods/content-separator.js +45 -0
  68. package/lib/methods/deepl.js +426 -0
  69. package/lib/methods/direct-llm.js +586 -0
  70. package/lib/methods/external.js +332 -0
  71. package/lib/methods/fetch-with-retry.js +124 -0
  72. package/lib/methods/gemini.js +147 -0
  73. package/lib/methods/google-translate.js +402 -0
  74. package/lib/methods/http-utils.js +122 -0
  75. package/lib/methods/libretranslate.js +314 -0
  76. package/lib/methods/llm-coached.js +670 -0
  77. package/lib/methods/llm.js +592 -0
  78. package/lib/methods/local.js +76 -0
  79. package/lib/methods/microsoft-translator.js +331 -0
  80. package/lib/methods/openai.js +131 -0
  81. package/lib/methods/openrouter-client.js +327 -0
  82. package/lib/methods/openrouter-pricing.js +156 -0
  83. package/lib/methods/provider-env.js +115 -0
  84. package/lib/methods/provider-pricing.js +310 -0
  85. package/lib/methods/tilde.js +150 -0
  86. package/lib/methods/translated.js +229 -0
  87. package/lib/methods/translation-error.js +80 -0
  88. package/lib/models.js +258 -0
  89. package/lib/no-translate.js +233 -0
  90. package/lib/output.js +238 -0
  91. package/lib/pairs.js +547 -0
  92. package/lib/plugins.js +447 -0
  93. package/lib/provenance.js +323 -0
  94. package/lib/recommend.js +648 -0
  95. package/lib/registers.js +1185 -0
  96. package/lib/repair-script.js +266 -0
  97. package/lib/scripts.js +994 -0
  98. package/lib/seal.mjs +464 -0
  99. package/lib/sealed-qualifier.mjs +211 -0
  100. package/lib/security.js +59 -0
  101. package/lib/segment.js +369 -0
  102. package/lib/seo.js +275 -0
  103. package/lib/serve.js +854 -0
  104. package/lib/string-classify.js +85 -0
  105. package/lib/submit.mjs +344 -0
  106. package/lib/sync.js +969 -0
  107. package/lib/tags/bcp47.js +202 -0
  108. package/lib/tags/resolve.js +314 -0
  109. package/lib/terminology.js +111 -0
  110. package/lib/tm-seed.js +294 -0
  111. package/lib/tm.js +515 -0
  112. package/lib/translate-pair.js +197 -0
  113. package/lib/translate.js +203 -0
  114. package/lib/types.js +230 -0
  115. package/lib/validate.js +510 -0
  116. package/lib/verify.js +451 -0
  117. package/lib/watch.js +145 -0
  118. package/lib/xliff.js +184 -0
  119. package/package.json +93 -0
  120. package/shared/ATTRIBUTION.md +145 -0
  121. package/shared/CORPORA-CARDS.md +288 -0
  122. package/shared/DATA-SOVEREIGNTY.md +500 -0
  123. package/shared/LANGUAGE-CARD-FIELDS.md +532 -0
  124. package/shared/card-lint-baseline.json +3189 -0
  125. package/shared/cards-fallback.json +1 -0
  126. package/shared/catalogue/card-config.json +6091 -0
  127. package/shared/catalogue/external-results.json +3888 -0
  128. package/shared/catalogue/gender-guidance.json +1038 -0
  129. package/shared/catalogue/method-coverage.json +1751 -0
  130. package/shared/catalogue/metric-coverage.json +170 -0
  131. package/shared/catalogue/metric-reliability.json +1 -0
  132. package/shared/catalogue/register-presets.json +3180 -0
  133. package/shared/catalogue/vitality-scales.json +55 -0
  134. package/shared/cldr-index.json +1115 -0
  135. package/shared/code-bridge.json +253 -0
  136. package/shared/corpora-cards-v1-reference.md +281 -0
  137. package/shared/curated-dictionary-flags.json +35 -0
  138. package/shared/curated-endonyms.json +35 -0
  139. package/shared/curated-fsts.json +51 -0
  140. package/shared/curated-orthography-conventions.json +26 -0
  141. package/shared/curated-sil-resources.json +374 -0
  142. package/shared/curated-tools.json +41 -0
  143. package/shared/docent/corpus.json +11333 -0
  144. package/shared/docent/faq.en.json +564 -0
  145. package/shared/docent/register-blocks.json +60 -0
  146. package/shared/docent/system-prompt.md +144 -0
  147. package/shared/domain-taxonomy.json +35 -0
  148. package/shared/explainers/glossary.json +2975 -0
  149. package/shared/explainers/tc-features.json +20112 -0
  150. package/shared/explainers/term-watchlist.json +147 -0
  151. package/shared/human-services.json +59 -0
  152. package/shared/license-corrections.json +261 -0
  153. package/shared/license-evidence.json +13452 -0
  154. package/shared/licenses.json +6781 -0
  155. package/shared/method-registry.json +236 -0
  156. package/shared/metric-registry.json +620 -0
  157. package/shared/model-aliases.json +7 -0
  158. package/shared/schemas/champollion-plugin.schema.json +206 -0
  159. package/shared/schemas/corpora-card.schema.json +957 -0
  160. package/shared/schemas/domain-taxonomy.schema.json +64 -0
  161. package/shared/schemas/external-results.schema.json +314 -0
  162. package/shared/schemas/human-services.schema.json +90 -0
  163. package/shared/schemas/language-card.schema.json +1308 -0
  164. package/shared/schemas/licenses.schema.json +155 -0
  165. package/shared/schemas/method-card.schema.json +412 -0
  166. package/shared/schemas/method-registry.schema.json +85 -0
  167. package/shared/schemas/metric-registry.schema.json +96 -0
  168. package/shared/schemas/metric-reliability.schema.json +178 -0
  169. package/shared/schemas/model-aliases.schema.json +27 -0
  170. package/shared/schemas/source-snapshot.schema.json +96 -0
package/lib/seo.js ADDED
@@ -0,0 +1,275 @@
1
+ /**
2
+ * SEO generators — deterministic, no LLM calls.
3
+ *
4
+ * Generates three types of SEO artifacts from the pair graph:
5
+ *
6
+ * 1. HREFLANG TAGS: HTML <link rel="alternate" hreflang="..."> elements
7
+ * for every page × locale combination. Includes x-default.
8
+ *
9
+ * 2. MULTILINGUAL SITEMAP: XML sitemap with <xhtml:link> alternates,
10
+ * following Google's multilingual sitemap spec.
11
+ *
12
+ * 3. JSON-LD SCHEMA: schema.org WebSite + SearchAction with
13
+ * @language annotations and workTranslation links.
14
+ *
15
+ * All output is deterministic: same config → same output, byte-for-byte.
16
+ * This means SEO files can be committed to git and diffed reliably.
17
+ */
18
+
19
+ import path from 'node:path';
20
+ import { parsePairKey } from './pairs.js';
21
+
22
+ // -----------------------------------------------------------------
23
+ // URL helpers
24
+ // -----------------------------------------------------------------
25
+
26
+ /**
27
+ * Build a localized URL from a base URL, URL pattern, and locale.
28
+ *
29
+ * Supported patterns:
30
+ * - /:locale/:path → https://example.com/fr/about
31
+ * - /:path/:locale → https://example.com/about/fr
32
+ * - subdomain → https://fr.example.com/about
33
+ *
34
+ * @param {string} baseUrl - Site base URL (no trailing slash)
35
+ * @param {string} urlPattern - One of '/:locale/:path', '/:path/:locale', 'subdomain'
36
+ * @param {string} locale - Language code
37
+ * @param {string} pagePath - Page path (e.g., '/about')
38
+ * @returns {string} Full localized URL
39
+ */
40
+ function buildLocalizedUrl(baseUrl, urlPattern, locale, pagePath) {
41
+ // Normalize: remove trailing slashes from baseUrl
42
+ const base = baseUrl.replace(/\/+$/, '');
43
+ // Normalize: ensure pagePath starts with /
44
+ const normalizedPath = pagePath.startsWith('/') ? pagePath : `/${pagePath}`;
45
+ // Root path special case
46
+ const cleanPath = normalizedPath === '/' ? '' : normalizedPath;
47
+
48
+ if (urlPattern === 'subdomain') {
49
+ // https://example.com → https://fr.example.com/about
50
+ const url = new URL(base);
51
+ url.hostname = `${locale}.${url.hostname}`;
52
+ return `${url.origin}${cleanPath}`;
53
+ }
54
+
55
+ if (urlPattern === '/:path/:locale') {
56
+ // https://example.com/about/fr
57
+ return `${base}${cleanPath}/${locale}`;
58
+ }
59
+
60
+ // Default: /:locale/:path
61
+ return `${base}/${locale}${cleanPath}`;
62
+ }
63
+
64
+ // -----------------------------------------------------------------
65
+ // Hreflang tags
66
+ // -----------------------------------------------------------------
67
+
68
+ /**
69
+ * Generate hreflang <link> tags for a single page.
70
+ *
71
+ * @param {object} options
72
+ * @param {string} options.baseUrl - Site base URL
73
+ * @param {string} options.urlPattern - URL localization pattern
74
+ * @param {string} options.inputLocale - Source locale code
75
+ * @param {string[]} options.locales - All locale codes (including source)
76
+ * @param {string} options.pagePath - Page path
77
+ * @returns {string} HTML link tags (one per line)
78
+ */
79
+ function generateHreflangTags({ baseUrl, urlPattern, inputLocale, locales, pagePath }) {
80
+ const allLocales = [...new Set([inputLocale, ...locales])];
81
+ const lines = [];
82
+
83
+ // Generate one <link> per locale
84
+ for (const locale of allLocales.sort()) {
85
+ const url = buildLocalizedUrl(baseUrl, urlPattern, locale, pagePath);
86
+ lines.push(`<link rel="alternate" hreflang="${locale}" href="${url}" />`);
87
+ }
88
+
89
+ // x-default points to the input (source) locale
90
+ const defaultUrl = buildLocalizedUrl(baseUrl, urlPattern, inputLocale, pagePath);
91
+ lines.push(`<link rel="alternate" hreflang="x-default" href="${defaultUrl}" />`);
92
+
93
+ return lines.join('\n');
94
+ }
95
+
96
+ /**
97
+ * Generate hreflang tags for all configured pages.
98
+ *
99
+ * @param {import('./types.js').ChampollionConfig} config - Resolved config
100
+ * @returns {string} Full hreflang output (one block per page)
101
+ */
102
+ function generateAllHreflangTags(config) {
103
+ const seo = config.seo || {};
104
+ const baseUrl = seo.baseUrl || config.baseUrl;
105
+ const urlPattern = seo.urlPattern || '/:locale/:path';
106
+ const pages = seo.pages || ['/'];
107
+ const inputLocale = config.inputLocale;
108
+
109
+ // Collect all target locales from config
110
+ const locales = [];
111
+ if (config.languages && Array.isArray(config.languages)) {
112
+ locales.push(...config.languages);
113
+ }
114
+ if (config.pairs) {
115
+ for (const [pairKey] of Object.entries(config.pairs)) {
116
+ const { target } = parsePairKey(pairKey);
117
+ if (target) locales.push(target);
118
+ }
119
+ }
120
+
121
+ if (!baseUrl) {
122
+ return '<!-- champollion: Set "baseUrl" in config to generate hreflang tags -->';
123
+ }
124
+
125
+ const blocks = [];
126
+ for (const pagePath of pages) {
127
+ blocks.push(`<!-- Page: ${pagePath} -->`);
128
+ blocks.push(generateHreflangTags({
129
+ baseUrl,
130
+ urlPattern,
131
+ inputLocale,
132
+ locales: [...new Set(locales)],
133
+ pagePath,
134
+ }));
135
+ }
136
+
137
+ return blocks.join('\n\n');
138
+ }
139
+
140
+ // -----------------------------------------------------------------
141
+ // Sitemap XML
142
+ // -----------------------------------------------------------------
143
+
144
+ /**
145
+ * Generate a multilingual sitemap.xml with xhtml:link alternates.
146
+ *
147
+ * Follows Google's spec for multilingual sitemaps:
148
+ * https://developers.google.com/search/docs/advanced/crawling/localized-versions
149
+ *
150
+ * @param {import('./types.js').ChampollionConfig} config - Resolved config
151
+ * @returns {string} Complete XML sitemap
152
+ */
153
+ function generateSitemap(config) {
154
+ const seo = config.seo || {};
155
+ const baseUrl = seo.baseUrl || config.baseUrl;
156
+ const urlPattern = seo.urlPattern || '/:locale/:path';
157
+ const pages = seo.pages || ['/'];
158
+ const inputLocale = config.inputLocale;
159
+
160
+ if (!baseUrl) {
161
+ return '<!-- champollion: Set "baseUrl" in config to generate sitemap -->';
162
+ }
163
+
164
+ // Collect all locales
165
+ const targetLocales = [];
166
+ if (config.languages && Array.isArray(config.languages)) {
167
+ targetLocales.push(...config.languages);
168
+ }
169
+ if (config.pairs) {
170
+ for (const [pairKey] of Object.entries(config.pairs)) {
171
+ const { target } = parsePairKey(pairKey);
172
+ if (target) targetLocales.push(target);
173
+ }
174
+ }
175
+ const allLocales = [...new Set([inputLocale, ...targetLocales])].sort();
176
+
177
+ const lines = [];
178
+ lines.push('<?xml version="1.0" encoding="UTF-8"?>');
179
+ lines.push('<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"');
180
+ lines.push(' xmlns:xhtml="http://www.w3.org/1999/xhtml">');
181
+
182
+ for (const pagePath of pages) {
183
+ for (const locale of allLocales) {
184
+ const url = buildLocalizedUrl(baseUrl, urlPattern, locale, pagePath);
185
+ lines.push(' <url>');
186
+ lines.push(` <loc>${escapeXml(url)}</loc>`);
187
+
188
+ // Add xhtml:link alternates for all other locales
189
+ for (const altLocale of allLocales) {
190
+ const altUrl = buildLocalizedUrl(baseUrl, urlPattern, altLocale, pagePath);
191
+ lines.push(` <xhtml:link rel="alternate" hreflang="${altLocale}" href="${escapeXml(altUrl)}" />`);
192
+ }
193
+
194
+ // x-default
195
+ const defaultUrl = buildLocalizedUrl(baseUrl, urlPattern, inputLocale, pagePath);
196
+ lines.push(` <xhtml:link rel="alternate" hreflang="x-default" href="${escapeXml(defaultUrl)}" />`);
197
+
198
+ lines.push(' </url>');
199
+ }
200
+ }
201
+
202
+ lines.push('</urlset>');
203
+ return lines.join('\n');
204
+ }
205
+
206
+ // -----------------------------------------------------------------
207
+ // JSON-LD Schema
208
+ // -----------------------------------------------------------------
209
+
210
+ /**
211
+ * Generate JSON-LD schema.org WebSite markup with language annotations.
212
+ *
213
+ * @param {object} config - Resolved config
214
+ * @returns {string} JSON-LD script block
215
+ */
216
+ function generateJsonLd(config) {
217
+ const seo = config.seo || {};
218
+ const baseUrl = seo.baseUrl || config.baseUrl;
219
+ const inputLocale = config.inputLocale;
220
+
221
+ if (!baseUrl) {
222
+ return '<!-- champollion: Set "baseUrl" in config to generate JSON-LD -->';
223
+ }
224
+
225
+ // Collect all locales
226
+ const targetLocales = [];
227
+ if (config.languages && Array.isArray(config.languages)) {
228
+ targetLocales.push(...config.languages);
229
+ }
230
+ if (config.pairs) {
231
+ for (const [pairKey] of Object.entries(config.pairs)) {
232
+ const { target } = parsePairKey(pairKey);
233
+ if (target) targetLocales.push(target);
234
+ }
235
+ }
236
+
237
+ const schema = {
238
+ '@context': 'https://schema.org',
239
+ '@type': 'WebSite',
240
+ 'url': baseUrl,
241
+ 'inLanguage': inputLocale,
242
+ 'availableLanguage': [...new Set(targetLocales)].sort(),
243
+ };
244
+
245
+ const scriptTag = `<script type="application/ld+json">\n${JSON.stringify(schema, null, 2)}\n</script>`;
246
+ return scriptTag;
247
+ }
248
+
249
+ // -----------------------------------------------------------------
250
+ // XML helpers
251
+ // -----------------------------------------------------------------
252
+
253
+ /**
254
+ * Escape special characters for XML output.
255
+ *
256
+ * @param {string} str - Raw string
257
+ * @returns {string} XML-safe escaped string
258
+ */
259
+ function escapeXml(str) {
260
+ return str
261
+ .replace(/&/g, '&amp;')
262
+ .replace(/</g, '&lt;')
263
+ .replace(/>/g, '&gt;')
264
+ .replace(/"/g, '&quot;')
265
+ .replace(/'/g, '&apos;');
266
+ }
267
+
268
+ export {
269
+ buildLocalizedUrl,
270
+ generateHreflangTags,
271
+ generateAllHreflangTags,
272
+ generateSitemap,
273
+ generateJsonLd,
274
+ escapeXml,
275
+ };