@docpensieve/core 0.4.0-beta.2 → 0.5.0-beta.1

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.
@@ -9,6 +9,8 @@
9
9
  * @module @docpensieve/core/search-index
10
10
  */
11
11
 
12
+ import { UI_STRINGS, pageCount } from '@docpensieve/shared';
13
+
12
14
  /** Path of the search page within a version. */
13
15
  export const SEARCH_SLUG = 'search';
14
16
 
@@ -56,21 +58,33 @@ function escapeHtml(value) {
56
58
  * @param {{ title: string, url: string, description: string }[]} entries
57
59
  * Pages of the version, in reading order.
58
60
  * @param {string} indexUrl URL of the version's index.
61
+ * @param {import('@docpensieve/shared').UiStrings} [strings] Wording of the page.
62
+ * @param {string} [lang] Language, which decides the plural of a count.
59
63
  * @returns {string}
60
64
  */
61
- export function searchPageContent(entries, indexUrl) {
65
+ export function searchPageContent(entries, indexUrl, strings = UI_STRINGS.en, lang = 'en') {
62
66
  const items = entries.map((entry) => {
63
67
  const description = entry.description ? `<p>${escapeHtml(entry.description)}</p>` : '';
64
68
  return `<li data-url="${escapeHtml(entry.url)}"><a href="${escapeHtml(entry.url)}">${escapeHtml(entry.title)}</a>${description}</li>`;
65
69
  });
66
70
 
67
71
  return [
68
- '<h1 id="search">Search</h1>',
69
- `<form class="dp-search-page" role="search" data-search-page data-index="${escapeHtml(indexUrl)}">`,
70
- '<label for="dp-search-query">Search the documentation</label>',
72
+ `<h1 id="search">${escapeHtml(strings.searchTitle)}</h1>`,
73
+ // The script is copied as it is, so its wording travels with the page
74
+ // rather than inside the file: one script, every language.
75
+ `<form class="dp-search-page" role="search" data-search-page data-index="${escapeHtml(indexUrl)}" data-strings="${escapeHtml(
76
+ JSON.stringify({
77
+ lang,
78
+ failed: strings.searchIndexFailed,
79
+ pages: strings.pages,
80
+ noResultFor: strings.noResultFor,
81
+ resultsFor: strings.resultsFor,
82
+ }),
83
+ )}">`,
84
+ `<label for="dp-search-query">${escapeHtml(strings.searchTheDocumentation)}</label>`,
71
85
  '<input id="dp-search-query" type="search" name="q" autocomplete="off" />',
72
86
  '</form>',
73
- `<p class="dp-search-status" data-search-status aria-live="polite">${entries.length} pages.</p>`,
87
+ `<p class="dp-search-status" data-search-status aria-live="polite">${escapeHtml(pageCount(entries.length, strings, lang))}.</p>`,
74
88
  `<ol class="dp-search-results" data-search-results>${items.join('')}</ol>`,
75
89
  ].join('');
76
90
  }
@@ -5,7 +5,7 @@ a row on a wide screen, behind a menu button on a narrow one. --}}
5
5
  <details class="{{{cls.versions}}}">
6
6
  {{!-- The accessible name contains the visible label: when dictated by voice,
7
7
  what is read on screen must find this button. --}}
8
- <summary aria-label="Version {{versionName}}, switch version">{{versionName}}</summary>
8
+ <summary aria-label="{{ui.versionSwitch}} {{versionName}}, {{ui.switchVersion}}">{{versionName}}</summary>
9
9
  <ul class="{{{cls.versionsList}}}">
10
10
  {{#each versions}}
11
11
  <li><a href="{{url}}"{{#if current}} aria-current="true"{{/if}}>{{name}}</a></li>
@@ -13,6 +13,24 @@ a row on a wide screen, behind a menu button on a narrow one. --}}
13
13
  </ul>
14
14
  </details>
15
15
  {{/if}}
16
+ {{#if languages.length}}
17
+ <details class="{{{cls.languages}}}">
18
+ <summary aria-label="{{ui.language}}">{{ui.language}}</summary>
19
+ <ul class="{{{cls.languagesList}}}">
20
+ {{#each languages}}
21
+ <li>
22
+ {{#if url}}
23
+ <a href="{{url}}"{{#if current}} aria-current="true"{{/if}}>{{label}}</a>
24
+ {{else}}
25
+ {{!-- Not translated: shown so that the reader knows the language exists,
26
+ inert so that they are not sent to a page that does not. --}}
27
+ <span aria-disabled="true">{{label}}</span>
28
+ {{/if}}
29
+ </li>
30
+ {{/each}}
31
+ </ul>
32
+ </details>
33
+ {{/if}}
16
34
  {{#if headerLinks.length}}
17
35
  <ul class="{{{cls.headerLinks}}}">
18
36
  {{#each headerLinks}}
@@ -43,6 +61,6 @@ a row on a wide screen, behind a menu button on a narrow one. --}}
43
61
  {{!-- A plain form: it leads to the search page, and needs no script. --}}
44
62
  {{#if searchUrl}}
45
63
  <form class="{{{cls.search}}}" role="search" action="{{searchUrl}}">
46
- <input type="search" name="q" placeholder="Search" aria-label="Search the documentation" />
64
+ <input type="search" name="q" placeholder="{{ui.search}}" aria-label="{{ui.searchTheDocumentation}}" />
47
65
  </form>
48
66
  {{/if}}
@@ -1,5 +1,5 @@
1
1
  <!doctype html>
2
- <html lang="{{lang}}"{{#if darkModeClass}} class="{{darkModeClass}}"{{/if}}>
2
+ <html lang="{{lang}}" dir="{{dir}}"{{#if darkModeClass}} class="{{darkModeClass}}"{{/if}}>
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -45,10 +45,22 @@
45
45
  {{#each scripts}}
46
46
  <script type="module" src="{{this}}"></script>
47
47
  {{/each}}
48
+ {{!-- The other languages of this page, for search engines: a reader
49
+ looking for the French page must not be sent to the English one. --}}
50
+ {{#each languages}}
51
+ {{#if url}}
52
+ <link rel="alternate" hreflang="{{lang}}" href="{{url}}" />
53
+ {{#if default}}
54
+ {{!-- x-default names the address to serve a reader whose language the site
55
+ does not have: the language it is written in. --}}
56
+ <link rel="alternate" hreflang="x-default" href="{{url}}" />
57
+ {{/if}}
58
+ {{/if}}
59
+ {{/each}}
48
60
  {{{jsonld}}}
49
61
  </head>
50
62
  <body>
51
- <a class="{{{cls.skip}}}" href="#content">Skip to content</a>
63
+ <a class="{{{cls.skip}}}" href="#content">{{ui.skipToContent}}</a>
52
64
 
53
65
  {{!-- Target of the back-to-top link: it brings the focus back, not only the
54
66
  view. It stays out of the header on purpose — the header is sticky, so it
@@ -56,28 +68,28 @@
56
68
  into view scrolled nowhere. --}}
57
69
  <div id="top" tabindex="-1"></div>
58
70
 
59
- <header class="{{{cls.header}}}">
71
+ <header class="{{#if stickyHeader}}{{{cls.header}}}{{else}}{{{cls.headerStatic}}}{{/if}}">
60
72
  {{!-- The logo's alt stays empty: the name that follows already says it. --}}
61
73
  <a class="{{{cls.brand}}}" href="{{homeUrl}}">{{#if logoUrl}}<img class="{{{cls.brandLogo}}}" src="{{logoUrl}}" alt="" />{{/if}}{{projectName}}</a>
62
74
  {{!-- Shown twice, one at a time: in a row on a wide screen, behind a button
63
75
  on a narrow one. The button is a details element, so that the menu opens
64
76
  without a script — as the version switcher does. --}}
65
77
  {{#if headerMenu}}
66
- <nav class="{{{cls.headerNav}}}" aria-label="Site">
78
+ <nav class="{{{cls.headerNav}}}" aria-label="{{ui.site}}">
67
79
  {{> headerMenu}}
68
80
  </nav>
69
81
  <details class="{{{cls.menu}}}">
70
- <summary aria-label="Menu">
82
+ <summary aria-label="{{ui.menu}}">
71
83
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" focusable="false"><path d="M4 7h16M4 12h16M4 17h16" /></svg>
72
84
  </summary>
73
- <nav class="{{{cls.menuPanel}}}" aria-label="Site">
85
+ <nav class="{{{cls.menuPanel}}}" aria-label="{{ui.site}}">
74
86
  {{> headerMenu}}
75
87
  </nav>
76
88
  </details>
77
89
  {{/if}}
78
90
  {{!-- Hidden until its script runs: without JavaScript, it would do nothing. --}}
79
91
  {{#if schemeToggle}}
80
- <button class="{{{cls.schemeToggle}}}" type="button" data-scheme-toggle hidden aria-label="Switch the colour scheme">
92
+ <button class="{{{cls.schemeToggle}}}" type="button" data-scheme-toggle hidden aria-label="{{ui.switchScheme}}">
81
93
  <svg class="dp-scheme-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" /></svg>
82
94
  <svg class="dp-scheme-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="4" /><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" /></svg>
83
95
  </button>
@@ -89,12 +101,12 @@
89
101
  menu above the content on a narrow one, where a full menu would eat the
90
102
  first screen. Only one of the two is displayed at a time. --}}
91
103
  {{#if sidebar.length}}
92
- <nav class="{{{cls.sidebar}}}" aria-label="Documentation navigation">
104
+ <nav class="{{{cls.sidebar}}}" aria-label="{{ui.documentationNavigation}}">
93
105
  {{> navItems sidebar}}
94
106
  </nav>
95
107
  <details class="{{{cls.sidebarMenu}}}">
96
- <summary>Documentation menu</summary>
97
- <nav aria-label="Documentation navigation">
108
+ <summary>{{ui.documentationMenu}}</summary>
109
+ <nav aria-label="{{ui.documentationNavigation}}">
98
110
  {{> navItems sidebar}}
99
111
  </nav>
100
112
  </details>
@@ -104,11 +116,11 @@
104
116
  {{#if notice}}
105
117
  <aside class="{{{cls.notice}}}" role="note">
106
118
  {{#if notice.prerelease}}
107
- This version is in preparation and may change.
119
+ {{ui.prereleaseNotice}}
108
120
  {{else}}
109
- This version is no longer maintained.
121
+ {{ui.archivedNotice}}
110
122
  {{/if}}
111
- The current version is <a href="{{notice.url}}">{{notice.name}}</a>.
123
+ {{ui.currentVersionIs}} <a href="{{notice.url}}">{{notice.name}}</a>.
112
124
  </aside>
113
125
  {{/if}}
114
126
  {{!-- Who wrote the page, and when. Absent when the page says neither,
@@ -139,26 +151,26 @@
139
151
  text, they say what it was about. Labels, not links — there is no page
140
152
  per tag. None on a home page. --}}
141
153
  {{#if tags.length}}
142
- <ul class="{{{cls.tags}}}" aria-label="Tags">
154
+ <ul class="{{{cls.tags}}}" aria-label="{{ui.tags}}">
143
155
  {{#each tags}}<li class="{{{@root.cls.tag}}}">{{this}}</li>{{/each}}
144
156
  </ul>
145
157
  {{/if}}
146
158
  </main>
147
159
 
148
160
  {{#if toc.length}}
149
- <nav class="{{{cls.toc}}}" aria-label="On this page">
150
- <p class="{{{cls.tocTitle}}}">On this page</p>
161
+ <nav class="{{{cls.toc}}}" aria-label="{{ui.onThisPage}}">
162
+ <p class="{{{cls.tocTitle}}}">{{ui.onThisPage}}</p>
151
163
  {{> tocItems toc}}
152
164
  </nav>
153
165
  {{/if}}
154
166
  </div>
155
167
 
156
168
  <footer class="{{{cls.footer}}}">
157
- <p>{{projectName}} — version {{versionName}}</p>
169
+ <p>{{projectName}} — {{ui.versionLabel}} {{versionName}}</p>
158
170
  </footer>
159
171
 
160
172
  {{#if scrollToTop}}
161
- <a class="{{{cls.scrollTop}}}" href="#top" aria-label="Back to top">
173
+ <a class="{{{cls.scrollTop}}}" href="#top" aria-label="{{ui.backToTop}}">
162
174
  <svg
163
175
  class="{{{cls.scrollTopIcon}}}"
164
176
  viewBox="0 0 24 24"
@@ -55,10 +55,11 @@ export type Byline = {
55
55
  * @param {unknown} value
56
56
  * @param {string} field Name of the field, for the error message.
57
57
  * @param {string} where Page the date comes from.
58
+ * @param {string} [locale] Locale the label is written in. Default: `en-GB`.
58
59
  * @returns {{ iso: string, label: string } | null} `null` when absent.
59
60
  * @throws {ConfigError} When the value is not a date.
60
61
  */
61
- export declare function readDate(value: unknown, field: string, where: string): {
62
+ export declare function readDate(value: unknown, field: string, where: string, locale?: string): {
62
63
  iso: string;
63
64
  label: string;
64
65
  } | null;
@@ -91,7 +92,8 @@ export declare function resolvePageAuthors(value: unknown, table?: Map<string, A
91
92
  * @param {Record<string, any>} frontmatter
92
93
  * @param {Map<string, Author>} [table]
93
94
  * @param {string} [where] Page named in a date error.
95
+ * @param {string} [locale] Locale the dates are written in.
94
96
  * @returns {Byline | null}
95
97
  * @throws {ConfigError} When a date cannot be read.
96
98
  */
97
- export declare function buildByline(frontmatter: Record<string, any>, table?: Map<string, Author>, where?: string): Byline | null;
99
+ export declare function buildByline(frontmatter: Record<string, any>, table?: Map<string, Author>, where?: string, locale?: string): Byline | null;
package/types/config.d.ts CHANGED
@@ -37,6 +37,12 @@ export type Version = {
37
37
  * Favicon of this version, instead of the project's.
38
38
  */
39
39
  favicon?: string;
40
+ /**
41
+ * Folder of each translation,
42
+ * by language code. The pages of the site language stay at the root of the
43
+ * version; a translation is served under its code.
44
+ */
45
+ translations?: Record<string, string>;
40
46
  };
41
47
  export type DocPensieveConfig = {
42
48
  /**
@@ -97,11 +103,26 @@ export type DocPensieveConfig = {
97
103
  * the branch of the page being read.
98
104
  */
99
105
  foldedSidebar?: boolean;
106
+ ui?: Record<string, Record<string, string>>;
107
+ /**
108
+ * Kinds of admonition the project adds to the ones shipped. `icon` names an
109
+ * SVG of the version folder, inlined in place of the tone's drawing.
110
+ */
111
+ admonitions?: Record<string, {
112
+ label: string;
113
+ tone: string;
114
+ icon?: string;
115
+ }>;
100
116
  globalComponents: boolean;
101
117
  /**
102
118
  * Back-to-top button on every page.
103
119
  */
104
120
  scrollToTop: boolean;
121
+ /**
122
+ * Header held at the top of the screen.
123
+ * `false` lets it scroll away with the page.
124
+ */
125
+ stickyHeader?: boolean;
105
126
  jsonld: {
106
127
  enabled: boolean;
107
128
  };
@@ -153,6 +174,9 @@ export type DocPensieveConfig = {
153
174
  * reference one. Its pages carry a notice and are not indexed.
154
175
  * @property {string} [logo] Logo of this version, instead of the project's.
155
176
  * @property {string} [favicon] Favicon of this version, instead of the project's.
177
+ * @property {Record<string, string>} [translations] Folder of each translation,
178
+ * by language code. The pages of the site language stay at the root of the
179
+ * version; a translation is served under its code.
156
180
  */
157
181
  /**
158
182
  * @typedef {object} DocPensieveConfig
@@ -169,8 +193,14 @@ export type DocPensieveConfig = {
169
193
  * `columns` opens a panel of links instead of leading anywhere itself.
170
194
  * @property {boolean} [foldedSidebar] Categories of the menu fold, opened on
171
195
  * the branch of the page being read.
196
+ * @property {Record<string, Record<string, string>>} [ui]
197
+ * @property {Record<string, { label: string, tone: string, icon?: string }>} [admonitions]
198
+ * Kinds of admonition the project adds to the ones shipped. `icon` names an
199
+ * SVG of the version folder, inlined in place of the tone's drawing.
172
200
  * @property {boolean} globalComponents
173
201
  * @property {boolean} scrollToTop Back-to-top button on every page.
202
+ * @property {boolean} [stickyHeader] Header held at the top of the screen.
203
+ * `false` lets it scroll away with the page.
174
204
  * @property {{ enabled: boolean }} jsonld
175
205
  * @property {string} [logo] Image beside the project name, in the header.
176
206
  * @property {string} [favicon] Icon of the browser tab: `.ico`, `.png` or `.svg`.
@@ -24,10 +24,12 @@ export declare function htmlToText(html: string): string;
24
24
  * @param {{ title: string, url: string, description: string }[]} entries
25
25
  * Pages of the version, in reading order.
26
26
  * @param {string} indexUrl URL of the version's index.
27
+ * @param {import('@docpensieve/shared').UiStrings} [strings] Wording of the page.
28
+ * @param {string} [lang] Language, which decides the plural of a count.
27
29
  * @returns {string}
28
30
  */
29
31
  export declare function searchPageContent(entries: {
30
32
  title: string;
31
33
  url: string;
32
34
  description: string;
33
- }[], indexUrl: string): string;
35
+ }[], indexUrl: string, strings?: import('@docpensieve/shared').UiStrings, lang?: string): string;