@dsbasko/cookbook-engine 0.1.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.
Potentially problematic release.
This version of @dsbasko/cookbook-engine might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +232 -0
- package/assets/fonts/jetbrains-mono/JetBrainsMono-Bold.woff2 +0 -0
- package/assets/fonts/jetbrains-mono/JetBrainsMono-BoldItalic.woff2 +0 -0
- package/assets/fonts/jetbrains-mono/JetBrainsMono-Italic.woff2 +0 -0
- package/assets/fonts/jetbrains-mono/JetBrainsMono-Medium.woff2 +0 -0
- package/assets/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2 +0 -0
- package/assets/fonts/jetbrains-mono/JetBrainsMono-SemiBold.woff2 +0 -0
- package/package.json +92 -0
- package/scripts/check-course-coverage.mts +32 -0
- package/scripts/fix-static-image-extensions.mjs +78 -0
- package/scripts/generate-readme-toc.mts +32 -0
- package/scripts/resolve-course-paths.mjs +28 -0
- package/scripts/sync-images.mjs +88 -0
- package/src/components/AppShell/AppShell.module.css +40 -0
- package/src/components/AppShell/AppShell.tsx +135 -0
- package/src/components/AppShell/index.ts +1 -0
- package/src/components/Callout/Callout.module.css +68 -0
- package/src/components/Callout/Callout.tsx +83 -0
- package/src/components/Callout/index.ts +1 -0
- package/src/components/CodeBlock/CodeBlock.module.css +68 -0
- package/src/components/CodeBlock/CodeBlock.tsx +65 -0
- package/src/components/CodeBlock/index.ts +1 -0
- package/src/components/GateProvider/GateProvider.tsx +207 -0
- package/src/components/GateProvider/index.ts +1 -0
- package/src/components/Header/Breadcrumbs.tsx +50 -0
- package/src/components/Header/Header.module.css +131 -0
- package/src/components/Header/Header.tsx +26 -0
- package/src/components/Header/HeaderLessonNav.tsx +118 -0
- package/src/components/Header/index.ts +1 -0
- package/src/components/HomePage/HomePage.module.css +538 -0
- package/src/components/HomePage/HomePage.tsx +295 -0
- package/src/components/HomePage/index.ts +1 -0
- package/src/components/LessonAwareLink/LessonAwareLink.module.css +12 -0
- package/src/components/LessonAwareLink/LessonAwareLink.tsx +86 -0
- package/src/components/LessonAwareLink/index.ts +1 -0
- package/src/components/LessonLayout/LessonLayout.module.css +35 -0
- package/src/components/LessonLayout/LessonLayout.tsx +18 -0
- package/src/components/LessonLayout/index.ts +1 -0
- package/src/components/LessonLockedInterstitial/LessonLockedInterstitial.module.css +367 -0
- package/src/components/LessonLockedInterstitial/LessonLockedInterstitial.tsx +256 -0
- package/src/components/LessonLockedInterstitial/index.ts +1 -0
- package/src/components/LessonNav/LessonNav.module.css +84 -0
- package/src/components/LessonNav/LessonNav.tsx +64 -0
- package/src/components/LessonNav/index.ts +1 -0
- package/src/components/LessonPageLayout/LessonPageLayout.module.css +118 -0
- package/src/components/LessonPageLayout/LessonPageLayout.tsx +46 -0
- package/src/components/LessonPageLayout/index.ts +1 -0
- package/src/components/LessonSideMeta/LessonSideMeta.module.css +68 -0
- package/src/components/LessonSideMeta/LessonSideMeta.tsx +87 -0
- package/src/components/LessonSideMeta/index.ts +1 -0
- package/src/components/ModulePage/ModulePage.module.css +693 -0
- package/src/components/ModulePage/ModulePage.tsx +301 -0
- package/src/components/ModulePage/index.ts +1 -0
- package/src/components/ProgramDrawer/LockIcon.tsx +19 -0
- package/src/components/ProgramDrawer/ProgramDrawer.module.css +563 -0
- package/src/components/ProgramDrawer/ProgramDrawer.tsx +481 -0
- package/src/components/ProgramDrawer/index.ts +1 -0
- package/src/components/ProgressBar/ProgressBar.module.css +46 -0
- package/src/components/ProgressBar/ProgressBar.tsx +45 -0
- package/src/components/ProgressBar/index.ts +1 -0
- package/src/components/ProgressModeProvider/ProgressModeProvider.tsx +87 -0
- package/src/components/ProgressModeProvider/index.ts +1 -0
- package/src/components/ReadingPrefsProvider/ReadingPrefsProvider.tsx +100 -0
- package/src/components/ReadingPrefsProvider/index.ts +1 -0
- package/src/components/ReadingProgress/ReadingProgress.module.css +19 -0
- package/src/components/ReadingProgress/ReadingProgress.tsx +53 -0
- package/src/components/ReadingProgress/index.ts +1 -0
- package/src/components/SettingsToggle/SettingsToggle.module.css +888 -0
- package/src/components/SettingsToggle/SettingsToggle.tsx +688 -0
- package/src/components/SettingsToggle/index.ts +1 -0
- package/src/components/Sidebar/Sidebar.module.css +157 -0
- package/src/components/Sidebar/Sidebar.tsx +63 -0
- package/src/components/Sidebar/icons/GitHubIcon.tsx +17 -0
- package/src/components/Sidebar/icons/HomeIcon.tsx +22 -0
- package/src/components/Sidebar/icons/LanguageIcon.tsx +24 -0
- package/src/components/Sidebar/icons/ProgramIcon.tsx +23 -0
- package/src/components/Sidebar/icons/SettingsIcon.tsx +26 -0
- package/src/components/Sidebar/icons/ThemeIcon.tsx +22 -0
- package/src/components/Sidebar/icons/index.ts +6 -0
- package/src/components/Sidebar/index.ts +1 -0
- package/src/components/ThemeProvider/ThemeProvider.tsx +68 -0
- package/src/components/ThemeProvider/index.ts +1 -0
- package/src/components/Toc/Toc.module.css +78 -0
- package/src/components/Toc/Toc.tsx +92 -0
- package/src/components/Toc/index.ts +1 -0
- package/src/components/TranslationBanner/TranslationBanner.module.css +32 -0
- package/src/components/TranslationBanner/TranslationBanner.tsx +40 -0
- package/src/components/TranslationBanner/index.ts +1 -0
- package/src/config.d.mts +12 -0
- package/src/config.mjs +110 -0
- package/src/index.ts +62 -0
- package/src/layout/lang.tsx +44 -0
- package/src/layout/root.tsx +223 -0
- package/src/lib/course-loader.ts +33 -0
- package/src/lib/course.ts +429 -0
- package/src/lib/coverage.ts +141 -0
- package/src/lib/description.ts +43 -0
- package/src/lib/extract-toc.ts +59 -0
- package/src/lib/format.ts +55 -0
- package/src/lib/frontier-link.ts +37 -0
- package/src/lib/gate-init-script.ts +40 -0
- package/src/lib/gate-mark-script.ts +324 -0
- package/src/lib/i18n.ts +474 -0
- package/src/lib/lang.ts +90 -0
- package/src/lib/lesson-gate.ts +79 -0
- package/src/lib/lesson.ts +66 -0
- package/src/lib/markdown-components.tsx +51 -0
- package/src/lib/markdown.ts +180 -0
- package/src/lib/mdx-plugins/rehype-callout.ts +80 -0
- package/src/lib/mdx-plugins/remark-lesson-images.ts +109 -0
- package/src/lib/mdx-plugins/remark-link-rewrite.ts +231 -0
- package/src/lib/paths.ts +36 -0
- package/src/lib/program-drawer.ts +8 -0
- package/src/lib/progress-mode.ts +69 -0
- package/src/lib/progress.ts +182 -0
- package/src/lib/reading-prefs.ts +127 -0
- package/src/lib/readme-toc.ts +69 -0
- package/src/lib/site-url.ts +33 -0
- package/src/lib/sitemap.ts +112 -0
- package/src/lib/slug.ts +15 -0
- package/src/lib/theme.ts +78 -0
- package/src/lib/use-i18n.ts +25 -0
- package/src/og/icon.tsx +40 -0
- package/src/og/opengraph-image.tsx +126 -0
- package/src/pages/home.tsx +66 -0
- package/src/pages/lesson.tsx +260 -0
- package/src/pages/module.tsx +80 -0
- package/src/pages/not-found-lang.tsx +51 -0
- package/src/pages/not-found-root.tsx +48 -0
- package/src/pages/root.tsx +44 -0
- package/src/seo/robots.ts +16 -0
- package/src/seo/sitemap.ts +10 -0
- package/src/styles/globals.css +139 -0
- package/src/styles/markdown.css +265 -0
- package/src/styles/reset.css +89 -0
- package/src/styles/tokens.css +270 -0
package/src/lib/i18n.ts
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import { type Lang } from './lang';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* UI string dictionary. Every key here must exist in every entry of
|
|
5
|
+
* UI_STRINGS — TypeScript enforces this via the `Record<Lang, UIDict>`
|
|
6
|
+
* shape below. Add new keys here first, then translations.
|
|
7
|
+
*
|
|
8
|
+
* Scope: strings rendered by components listed in Task 10 of the i18n
|
|
9
|
+
* plan (Header/Breadcrumbs, Sidebar, ProgramDrawer, LessonNav, Toc,
|
|
10
|
+
* ReadingProgress, LessonLockedInterstitial, SettingsToggle, HomePage,
|
|
11
|
+
* LessonPageLayout, not-found) plus a few keys for adjacent surfaces
|
|
12
|
+
* already touched by lang switching (settings popover, theme labels
|
|
13
|
+
* previously lived in `lib/theme.ts`).
|
|
14
|
+
*/
|
|
15
|
+
export type UIDict = {
|
|
16
|
+
// Sidebar
|
|
17
|
+
sidebarLabel: string;
|
|
18
|
+
navMainLabel: string;
|
|
19
|
+
home: string;
|
|
20
|
+
programCourse: string;
|
|
21
|
+
githubRepo: string;
|
|
22
|
+
|
|
23
|
+
// Header / Breadcrumbs
|
|
24
|
+
breadcrumbsLabel: string;
|
|
25
|
+
|
|
26
|
+
// ProgramDrawer
|
|
27
|
+
close: string;
|
|
28
|
+
moduleListLabel: string;
|
|
29
|
+
lessonLockTitle: string;
|
|
30
|
+
|
|
31
|
+
// LessonNav
|
|
32
|
+
lessonNavLabel: string;
|
|
33
|
+
prevLesson: string;
|
|
34
|
+
nextLesson: string;
|
|
35
|
+
|
|
36
|
+
// Mobile drawer lesson-nav: short eyebrow used next to a chevron icon
|
|
37
|
+
prevLessonShort: string;
|
|
38
|
+
nextLessonShort: string;
|
|
39
|
+
currentLessonEyebrow: string;
|
|
40
|
+
currentLessonNumberPrefix: string;
|
|
41
|
+
|
|
42
|
+
// Mobile settings: "Resources" section + GitHub link sub-label
|
|
43
|
+
settingsResourcesLabel: string;
|
|
44
|
+
githubRepoSub: string;
|
|
45
|
+
|
|
46
|
+
// HeaderLessonNav (chevron buttons reuse a shorter form without the arrow)
|
|
47
|
+
prevLessonAria: string;
|
|
48
|
+
nextLessonAria: string;
|
|
49
|
+
firstLessonTitle: string;
|
|
50
|
+
lastLessonTitle: string;
|
|
51
|
+
|
|
52
|
+
// Toc
|
|
53
|
+
tocLabel: string;
|
|
54
|
+
|
|
55
|
+
// ReadingProgress
|
|
56
|
+
readingProgressLabel: string;
|
|
57
|
+
|
|
58
|
+
// LessonLockedInterstitial
|
|
59
|
+
locked: string;
|
|
60
|
+
moduleNumberPrefix: string;
|
|
61
|
+
lockedTitle: string;
|
|
62
|
+
lockedDesc: string;
|
|
63
|
+
attemptedLessonLabel: string;
|
|
64
|
+
attemptedYouTried: string;
|
|
65
|
+
continueAction: string;
|
|
66
|
+
startFromFirst: string;
|
|
67
|
+
openProgram: string;
|
|
68
|
+
courseProgress: string;
|
|
69
|
+
progress: string;
|
|
70
|
+
nextStep: string;
|
|
71
|
+
untilThisLesson: string;
|
|
72
|
+
|
|
73
|
+
// SettingsToggle — theme section labels
|
|
74
|
+
themeLight: string;
|
|
75
|
+
themeDark: string;
|
|
76
|
+
themePaper: string;
|
|
77
|
+
|
|
78
|
+
// HomePage
|
|
79
|
+
heroTitleLead: string;
|
|
80
|
+
heroTitleAccent: string;
|
|
81
|
+
heroTitleTail: string;
|
|
82
|
+
continueLessonPrefix: string;
|
|
83
|
+
startFromScratch: string;
|
|
84
|
+
progressSummary: string;
|
|
85
|
+
modulesLabel: string;
|
|
86
|
+
lessonsLabel: string;
|
|
87
|
+
durationLabel: string;
|
|
88
|
+
stackLabel: string;
|
|
89
|
+
moduleLockTitle: string;
|
|
90
|
+
|
|
91
|
+
// LessonPageLayout
|
|
92
|
+
lessonInfoLabel: string;
|
|
93
|
+
|
|
94
|
+
// not-found
|
|
95
|
+
notFoundTitle: string;
|
|
96
|
+
notFoundDesc: string;
|
|
97
|
+
goHome: string;
|
|
98
|
+
|
|
99
|
+
// SettingsToggle — language section title (also used as radiogroup aria-label)
|
|
100
|
+
language: string;
|
|
101
|
+
|
|
102
|
+
// TranslationBanner (rendered on EN lesson pages that fall back to RU)
|
|
103
|
+
translationFallbackTitle: string;
|
|
104
|
+
translationFallbackBody: string;
|
|
105
|
+
|
|
106
|
+
// ModulePage
|
|
107
|
+
startModule: string;
|
|
108
|
+
continueModulePrefix: string;
|
|
109
|
+
rereadModule: string;
|
|
110
|
+
nextModule: string;
|
|
111
|
+
moduleProgress: string;
|
|
112
|
+
lessonsCount: string;
|
|
113
|
+
durationLabelShort: string;
|
|
114
|
+
stackLabelShort: string;
|
|
115
|
+
moduleLessonsHeading: string;
|
|
116
|
+
lessonLockShort: string;
|
|
117
|
+
lessonHintContinue: string;
|
|
118
|
+
lessonNeighbourModulesLabel: string;
|
|
119
|
+
prevModule: string;
|
|
120
|
+
|
|
121
|
+
// LessonSideMeta
|
|
122
|
+
moduleMetaKey: string;
|
|
123
|
+
readingTimeMetaKey: string;
|
|
124
|
+
tagsMetaKey: string;
|
|
125
|
+
markUnread: string;
|
|
126
|
+
|
|
127
|
+
// ProgressBar / aria
|
|
128
|
+
progressBarAriaLabel: string;
|
|
129
|
+
progressAriaConnector: string;
|
|
130
|
+
|
|
131
|
+
// Metadata fallbacks. The brand wordmark is composed in at the call site
|
|
132
|
+
// (notFound title = `${notFoundTitle} · <brand name>`); `ogImageAlt` is the
|
|
133
|
+
// default alt text, overridable via brand.ogImage.alt.
|
|
134
|
+
ogImageAlt: string;
|
|
135
|
+
|
|
136
|
+
// HomePage module status (CSS-only pseudo-element labels lifted into JSX)
|
|
137
|
+
statusNotStarted: string;
|
|
138
|
+
statusInProgress: string;
|
|
139
|
+
statusComplete: string;
|
|
140
|
+
|
|
141
|
+
// Callout titles (rendered server-side via MDX pipeline)
|
|
142
|
+
calloutNote: string;
|
|
143
|
+
calloutTip: string;
|
|
144
|
+
calloutWarning: string;
|
|
145
|
+
calloutImportant: string;
|
|
146
|
+
calloutCaution: string;
|
|
147
|
+
|
|
148
|
+
// CodeBlock copy button
|
|
149
|
+
codeBlockCopy: string;
|
|
150
|
+
codeBlockCopied: string;
|
|
151
|
+
codeBlockCopyAriaLabel: string;
|
|
152
|
+
codeBlockCopiedAriaLabel: string;
|
|
153
|
+
|
|
154
|
+
// SettingsToggle — trigger label and section titles
|
|
155
|
+
settingsLabel: string;
|
|
156
|
+
settingsEyebrow: string;
|
|
157
|
+
settingsThemeSection: string;
|
|
158
|
+
|
|
159
|
+
// SettingsToggle — prose/code sections
|
|
160
|
+
readingPrefsProseSection: string;
|
|
161
|
+
readingPrefsCodeSection: string;
|
|
162
|
+
readingPrefsSize: string;
|
|
163
|
+
readingPrefsFont: string;
|
|
164
|
+
readingPrefsDecrease: string;
|
|
165
|
+
readingPrefsIncrease: string;
|
|
166
|
+
readingPrefsFontSerif: string;
|
|
167
|
+
readingPrefsFontSans: string;
|
|
168
|
+
readingPrefsFontSlab: string;
|
|
169
|
+
readingPrefsFontJetBrains: string;
|
|
170
|
+
readingPrefsFontFira: string;
|
|
171
|
+
readingPrefsPreviewProse: string;
|
|
172
|
+
readingPrefsPreviewCode: string;
|
|
173
|
+
|
|
174
|
+
// SettingsToggle — free-reading (disable progress) section
|
|
175
|
+
freeReadingSection: string;
|
|
176
|
+
freeReadingTitle: string;
|
|
177
|
+
freeReadingDesc: string;
|
|
178
|
+
freeReadingResetWarning: string;
|
|
179
|
+
freeReadingResetConfirm: string;
|
|
180
|
+
freeReadingResetCancel: string;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const UI_STRINGS: Record<Lang, UIDict> = {
|
|
184
|
+
ru: {
|
|
185
|
+
sidebarLabel: 'Боковая навигация',
|
|
186
|
+
navMainLabel: 'Основная навигация',
|
|
187
|
+
home: 'Главная',
|
|
188
|
+
programCourse: 'Программа курса',
|
|
189
|
+
githubRepo: 'Репозиторий на GitHub',
|
|
190
|
+
|
|
191
|
+
breadcrumbsLabel: 'Хлебные крошки',
|
|
192
|
+
|
|
193
|
+
close: 'Закрыть',
|
|
194
|
+
moduleListLabel: 'Список модулей и уроков',
|
|
195
|
+
lessonLockTitle: 'Урок откроется после прохождения предыдущих',
|
|
196
|
+
|
|
197
|
+
lessonNavLabel: 'Навигация по урокам',
|
|
198
|
+
prevLesson: '← Предыдущий урок',
|
|
199
|
+
nextLesson: 'Следующий урок →',
|
|
200
|
+
|
|
201
|
+
prevLessonShort: 'пред.',
|
|
202
|
+
nextLessonShort: 'след.',
|
|
203
|
+
currentLessonEyebrow: '/ текущий урок',
|
|
204
|
+
currentLessonNumberPrefix: 'урок',
|
|
205
|
+
|
|
206
|
+
settingsResourcesLabel: 'Ресурсы',
|
|
207
|
+
githubRepoSub: 'исходный код курса',
|
|
208
|
+
|
|
209
|
+
prevLessonAria: 'Предыдущий урок',
|
|
210
|
+
nextLessonAria: 'Следующий урок',
|
|
211
|
+
firstLessonTitle: 'Это первый урок',
|
|
212
|
+
lastLessonTitle: 'Это последний урок',
|
|
213
|
+
|
|
214
|
+
tocLabel: 'Содержание',
|
|
215
|
+
|
|
216
|
+
readingProgressLabel: 'Прогресс чтения',
|
|
217
|
+
|
|
218
|
+
locked: 'LOCKED',
|
|
219
|
+
moduleNumberPrefix: 'Модуль',
|
|
220
|
+
lockedTitle: 'Этот урок ещё впереди',
|
|
221
|
+
lockedDesc:
|
|
222
|
+
'Курс изучается по порядку — чтобы открыть этот шаг, сначала завершите предыдущие. Так контекст накапливается без пропусков.',
|
|
223
|
+
attemptedLessonLabel: 'Урок, который вы открыли',
|
|
224
|
+
attemptedYouTried: '/ вы пытались открыть',
|
|
225
|
+
continueAction: 'Продолжить',
|
|
226
|
+
startFromFirst: 'Начать с первого урока',
|
|
227
|
+
openProgram: 'Открыть программу',
|
|
228
|
+
courseProgress: 'Прогресс курса',
|
|
229
|
+
progress: 'Прогресс',
|
|
230
|
+
nextStep: 'Следующий шаг',
|
|
231
|
+
untilThisLesson: 'До этого урока',
|
|
232
|
+
|
|
233
|
+
themeLight: 'Светлая',
|
|
234
|
+
themeDark: 'Тёмная',
|
|
235
|
+
themePaper: 'Бумага',
|
|
236
|
+
|
|
237
|
+
heroTitleLead: 'Kafka',
|
|
238
|
+
heroTitleAccent: 'для тех, кто',
|
|
239
|
+
heroTitleTail: 'пишет на Go',
|
|
240
|
+
continueLessonPrefix: 'Продолжить · урок',
|
|
241
|
+
startFromScratch: 'Начать с начала',
|
|
242
|
+
progressSummary: 'Сводка прогресса',
|
|
243
|
+
modulesLabel: 'Модулей',
|
|
244
|
+
lessonsLabel: 'Уроков',
|
|
245
|
+
durationLabel: 'Длительность',
|
|
246
|
+
stackLabel: 'Стек',
|
|
247
|
+
moduleLockTitle: 'Модуль откроется после прохождения предыдущих уроков',
|
|
248
|
+
|
|
249
|
+
lessonInfoLabel: 'Сведения об уроке',
|
|
250
|
+
|
|
251
|
+
notFoundTitle: 'Страница не найдена',
|
|
252
|
+
notFoundDesc: 'Похоже, такой лекции в курсе нет. Вернитесь на главную.',
|
|
253
|
+
goHome: 'На главную',
|
|
254
|
+
|
|
255
|
+
language: 'Язык',
|
|
256
|
+
|
|
257
|
+
translationFallbackTitle: 'Перевод в процессе',
|
|
258
|
+
translationFallbackBody:
|
|
259
|
+
'Английская версия этой лекции пока не готова. Показан оригинал на русском.',
|
|
260
|
+
|
|
261
|
+
startModule: 'Начать модуль',
|
|
262
|
+
continueModulePrefix: 'Продолжить',
|
|
263
|
+
rereadModule: 'Перечитать модуль',
|
|
264
|
+
nextModule: 'Следующий модуль',
|
|
265
|
+
moduleProgress: 'Прогресс модуля',
|
|
266
|
+
lessonsCount: 'Уроков',
|
|
267
|
+
durationLabelShort: 'Длительность',
|
|
268
|
+
stackLabelShort: 'Стек',
|
|
269
|
+
moduleLessonsHeading: 'Уроки модуля',
|
|
270
|
+
lessonLockShort: 'Урок откроется после прохождения предыдущих',
|
|
271
|
+
lessonHintContinue: '↳ продолжить отсюда',
|
|
272
|
+
lessonNeighbourModulesLabel: 'Соседние модули',
|
|
273
|
+
prevModule: '← Предыдущий модуль',
|
|
274
|
+
|
|
275
|
+
moduleMetaKey: 'модуль',
|
|
276
|
+
readingTimeMetaKey: 'время чтения',
|
|
277
|
+
tagsMetaKey: 'теги',
|
|
278
|
+
markUnread: 'Пометить непрочитанным',
|
|
279
|
+
|
|
280
|
+
progressBarAriaLabel: 'Прогресс прохождения курса',
|
|
281
|
+
progressAriaConnector: 'из',
|
|
282
|
+
|
|
283
|
+
ogImageAlt: 'Kafka Cookbook — курс по Apache Kafka на Go',
|
|
284
|
+
|
|
285
|
+
statusNotStarted: 'не начато',
|
|
286
|
+
statusInProgress: 'в процессе',
|
|
287
|
+
statusComplete: 'пройдено',
|
|
288
|
+
|
|
289
|
+
calloutNote: 'Заметка',
|
|
290
|
+
calloutTip: 'Подсказка',
|
|
291
|
+
calloutWarning: 'Внимание',
|
|
292
|
+
calloutImportant: 'Важно',
|
|
293
|
+
calloutCaution: 'Осторожно',
|
|
294
|
+
|
|
295
|
+
codeBlockCopy: 'copy',
|
|
296
|
+
codeBlockCopied: '✓ скопировано',
|
|
297
|
+
codeBlockCopyAriaLabel: 'Скопировать код',
|
|
298
|
+
codeBlockCopiedAriaLabel: 'Скопировано',
|
|
299
|
+
|
|
300
|
+
settingsLabel: 'Настройки',
|
|
301
|
+
settingsEyebrow: '/ config',
|
|
302
|
+
settingsThemeSection: 'Тема',
|
|
303
|
+
readingPrefsProseSection: 'Текст лекции',
|
|
304
|
+
readingPrefsCodeSection: 'Текст кода',
|
|
305
|
+
readingPrefsSize: 'Размер',
|
|
306
|
+
readingPrefsFont: 'Шрифт',
|
|
307
|
+
readingPrefsDecrease: 'A−',
|
|
308
|
+
readingPrefsIncrease: 'A+',
|
|
309
|
+
readingPrefsFontSerif: 'Literata',
|
|
310
|
+
readingPrefsFontSans: 'Inter',
|
|
311
|
+
readingPrefsFontSlab: 'Roboto Slab',
|
|
312
|
+
readingPrefsFontJetBrains: 'JetBrains',
|
|
313
|
+
readingPrefsFontFira: 'Fira',
|
|
314
|
+
readingPrefsPreviewProse:
|
|
315
|
+
'Шрифт и размер выбираются так, чтобы длинные лекции читались легко.',
|
|
316
|
+
readingPrefsPreviewCode: 'consumer.subscribe(topics)',
|
|
317
|
+
|
|
318
|
+
freeReadingSection: 'Прогресс',
|
|
319
|
+
freeReadingTitle: 'Свободное чтение',
|
|
320
|
+
freeReadingDesc: 'Открыть все уроки и скрыть прогресс',
|
|
321
|
+
freeReadingResetWarning: 'Накопленный прогресс будет сброшен.',
|
|
322
|
+
freeReadingResetConfirm: 'Сбросить',
|
|
323
|
+
freeReadingResetCancel: 'Отмена',
|
|
324
|
+
},
|
|
325
|
+
en: {
|
|
326
|
+
sidebarLabel: 'Side navigation',
|
|
327
|
+
navMainLabel: 'Main navigation',
|
|
328
|
+
home: 'Home',
|
|
329
|
+
programCourse: 'Course outline',
|
|
330
|
+
githubRepo: 'GitHub repository',
|
|
331
|
+
|
|
332
|
+
breadcrumbsLabel: 'Breadcrumbs',
|
|
333
|
+
|
|
334
|
+
close: 'Close',
|
|
335
|
+
moduleListLabel: 'Module and lesson list',
|
|
336
|
+
lessonLockTitle: 'Lesson unlocks after the previous ones are complete',
|
|
337
|
+
|
|
338
|
+
lessonNavLabel: 'Lesson navigation',
|
|
339
|
+
prevLesson: '← Previous lesson',
|
|
340
|
+
nextLesson: 'Next lesson →',
|
|
341
|
+
|
|
342
|
+
prevLessonShort: 'prev',
|
|
343
|
+
nextLessonShort: 'next',
|
|
344
|
+
currentLessonEyebrow: '/ current lesson',
|
|
345
|
+
currentLessonNumberPrefix: 'lesson',
|
|
346
|
+
|
|
347
|
+
settingsResourcesLabel: 'Resources',
|
|
348
|
+
githubRepoSub: 'course source code',
|
|
349
|
+
|
|
350
|
+
prevLessonAria: 'Previous lesson',
|
|
351
|
+
nextLessonAria: 'Next lesson',
|
|
352
|
+
firstLessonTitle: 'This is the first lesson',
|
|
353
|
+
lastLessonTitle: 'This is the last lesson',
|
|
354
|
+
|
|
355
|
+
tocLabel: 'Contents',
|
|
356
|
+
|
|
357
|
+
readingProgressLabel: 'Reading progress',
|
|
358
|
+
|
|
359
|
+
locked: 'LOCKED',
|
|
360
|
+
moduleNumberPrefix: 'Module',
|
|
361
|
+
lockedTitle: 'This lesson is still ahead',
|
|
362
|
+
lockedDesc:
|
|
363
|
+
'The course goes in order — to open this step, finish the previous ones first. Context builds up without gaps that way.',
|
|
364
|
+
attemptedLessonLabel: 'Lesson you tried to open',
|
|
365
|
+
attemptedYouTried: '/ you tried to open',
|
|
366
|
+
continueAction: 'Continue',
|
|
367
|
+
startFromFirst: 'Start from the first lesson',
|
|
368
|
+
openProgram: 'Open outline',
|
|
369
|
+
courseProgress: 'Course progress',
|
|
370
|
+
progress: 'Progress',
|
|
371
|
+
nextStep: 'Next step',
|
|
372
|
+
untilThisLesson: 'Until this lesson',
|
|
373
|
+
|
|
374
|
+
themeLight: 'Light',
|
|
375
|
+
themeDark: 'Dark',
|
|
376
|
+
themePaper: 'Paper',
|
|
377
|
+
|
|
378
|
+
heroTitleLead: 'Kafka',
|
|
379
|
+
heroTitleAccent: 'for people who',
|
|
380
|
+
heroTitleTail: 'write Go',
|
|
381
|
+
continueLessonPrefix: 'Continue · lesson',
|
|
382
|
+
startFromScratch: 'Start over',
|
|
383
|
+
progressSummary: 'Progress summary',
|
|
384
|
+
modulesLabel: 'Modules',
|
|
385
|
+
lessonsLabel: 'Lessons',
|
|
386
|
+
durationLabel: 'Duration',
|
|
387
|
+
stackLabel: 'Stack',
|
|
388
|
+
moduleLockTitle: 'Module unlocks after the previous lessons are complete',
|
|
389
|
+
|
|
390
|
+
lessonInfoLabel: 'Lesson info',
|
|
391
|
+
|
|
392
|
+
notFoundTitle: 'Page not found',
|
|
393
|
+
notFoundDesc: 'There is no such lesson in the course. Head back home.',
|
|
394
|
+
goHome: 'Go home',
|
|
395
|
+
|
|
396
|
+
language: 'Language',
|
|
397
|
+
|
|
398
|
+
translationFallbackTitle: 'Translation in progress',
|
|
399
|
+
translationFallbackBody:
|
|
400
|
+
'The English version of this lesson is not ready yet. Showing the original Russian text.',
|
|
401
|
+
|
|
402
|
+
startModule: 'Start module',
|
|
403
|
+
continueModulePrefix: 'Continue',
|
|
404
|
+
rereadModule: 'Reread module',
|
|
405
|
+
nextModule: 'Next module',
|
|
406
|
+
moduleProgress: 'Module progress',
|
|
407
|
+
lessonsCount: 'Lessons',
|
|
408
|
+
durationLabelShort: 'Duration',
|
|
409
|
+
stackLabelShort: 'Stack',
|
|
410
|
+
moduleLessonsHeading: 'Lessons',
|
|
411
|
+
lessonLockShort: 'Lesson unlocks after the previous ones are complete',
|
|
412
|
+
lessonHintContinue: '↳ continue from here',
|
|
413
|
+
lessonNeighbourModulesLabel: 'Neighbouring modules',
|
|
414
|
+
prevModule: '← Previous module',
|
|
415
|
+
|
|
416
|
+
moduleMetaKey: 'module',
|
|
417
|
+
readingTimeMetaKey: 'reading time',
|
|
418
|
+
tagsMetaKey: 'tags',
|
|
419
|
+
markUnread: 'Mark as unread',
|
|
420
|
+
|
|
421
|
+
progressBarAriaLabel: 'Course completion progress',
|
|
422
|
+
progressAriaConnector: 'of',
|
|
423
|
+
|
|
424
|
+
ogImageAlt: 'Kafka Cookbook — Apache Kafka course in Go',
|
|
425
|
+
|
|
426
|
+
statusNotStarted: 'not started',
|
|
427
|
+
statusInProgress: 'in progress',
|
|
428
|
+
statusComplete: 'complete',
|
|
429
|
+
|
|
430
|
+
calloutNote: 'Note',
|
|
431
|
+
calloutTip: 'Tip',
|
|
432
|
+
calloutWarning: 'Warning',
|
|
433
|
+
calloutImportant: 'Important',
|
|
434
|
+
calloutCaution: 'Caution',
|
|
435
|
+
|
|
436
|
+
codeBlockCopy: 'copy',
|
|
437
|
+
codeBlockCopied: '✓ copied',
|
|
438
|
+
codeBlockCopyAriaLabel: 'Copy code',
|
|
439
|
+
codeBlockCopiedAriaLabel: 'Copied',
|
|
440
|
+
|
|
441
|
+
settingsLabel: 'Settings',
|
|
442
|
+
settingsEyebrow: '/ config',
|
|
443
|
+
settingsThemeSection: 'Theme',
|
|
444
|
+
readingPrefsProseSection: 'Lesson text',
|
|
445
|
+
readingPrefsCodeSection: 'Code text',
|
|
446
|
+
readingPrefsSize: 'Size',
|
|
447
|
+
readingPrefsFont: 'Font',
|
|
448
|
+
readingPrefsDecrease: 'A−',
|
|
449
|
+
readingPrefsIncrease: 'A+',
|
|
450
|
+
readingPrefsFontSerif: 'Literata',
|
|
451
|
+
readingPrefsFontSans: 'Inter',
|
|
452
|
+
readingPrefsFontSlab: 'Roboto Slab',
|
|
453
|
+
readingPrefsFontJetBrains: 'JetBrains',
|
|
454
|
+
readingPrefsFontFira: 'Fira',
|
|
455
|
+
readingPrefsPreviewProse: 'Pick a typeface and size that make long lessons comfortable to read.',
|
|
456
|
+
readingPrefsPreviewCode: 'consumer.subscribe(topics)',
|
|
457
|
+
|
|
458
|
+
freeReadingSection: 'Progress',
|
|
459
|
+
freeReadingTitle: 'Free reading',
|
|
460
|
+
freeReadingDesc: 'Unlock every lesson and hide progress',
|
|
461
|
+
freeReadingResetWarning: 'Your accumulated progress will be reset.',
|
|
462
|
+
freeReadingResetConfirm: 'Reset',
|
|
463
|
+
freeReadingResetCancel: 'Cancel',
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Server-friendly accessor. Server components receive `lang` via route
|
|
469
|
+
* params; pass it in here. Client components should prefer `useT()` from
|
|
470
|
+
* `web/lib/use-i18n.ts` instead — it reads `lang` from `useParams()`.
|
|
471
|
+
*/
|
|
472
|
+
export function getDict(lang: Lang): UIDict {
|
|
473
|
+
return UI_STRINGS[lang];
|
|
474
|
+
}
|
package/src/lib/lang.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
export type Lang = 'ru' | 'en';
|
|
2
|
+
|
|
3
|
+
export const LANGS: readonly Lang[] = ['ru', 'en'] as const;
|
|
4
|
+
export const DEFAULT_LANG: Lang = 'en';
|
|
5
|
+
export const LANG_STORAGE_KEY = 'kafka-cookbook-lang';
|
|
6
|
+
|
|
7
|
+
export const LANG_LABELS: Record<Lang, string> = {
|
|
8
|
+
ru: 'Русский',
|
|
9
|
+
en: 'English',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function isLang(value: unknown): value is Lang {
|
|
13
|
+
return value === 'ru' || value === 'en';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getBrowserLang(): Lang {
|
|
17
|
+
if (typeof navigator === 'undefined') return DEFAULT_LANG;
|
|
18
|
+
const raw = navigator.language || (navigator.languages && navigator.languages[0]) || '';
|
|
19
|
+
return raw.toLowerCase().startsWith('ru') ? 'ru' : 'en';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function readStoredLang(): Lang | null {
|
|
23
|
+
if (typeof window === 'undefined') return null;
|
|
24
|
+
try {
|
|
25
|
+
const raw = window.localStorage.getItem(LANG_STORAGE_KEY);
|
|
26
|
+
return isLang(raw) ? raw : null;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function writeStoredLang(lang: Lang): void {
|
|
33
|
+
if (typeof window === 'undefined') return;
|
|
34
|
+
try {
|
|
35
|
+
window.localStorage.setItem(LANG_STORAGE_KEY, lang);
|
|
36
|
+
} catch {
|
|
37
|
+
/* storage may be unavailable (private mode, quota); ignore. */
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Strips a leading `/ru/` or `/en/` segment from a pathname. Used by URL
|
|
43
|
+
* parsers (LessonAwareLink, GateProvider, gate-scripts) so the remaining
|
|
44
|
+
* segments are `[moduleId, slug]` regardless of the active language.
|
|
45
|
+
*
|
|
46
|
+
* Examples:
|
|
47
|
+
* /ru/foo/bar -> { lang: 'ru', rest: '/foo/bar' }
|
|
48
|
+
* /en/ -> { lang: 'en', rest: '/' }
|
|
49
|
+
* /ru -> { lang: 'ru', rest: '/' }
|
|
50
|
+
* /foo/bar -> { lang: null, rest: '/foo/bar' }
|
|
51
|
+
* /enfoo -> { lang: null, rest: '/enfoo' }
|
|
52
|
+
* '' -> { lang: null, rest: '/' }
|
|
53
|
+
*/
|
|
54
|
+
export function stripLangFromPath(pathname: string): { lang: Lang | null; rest: string } {
|
|
55
|
+
if (!pathname) return { lang: null, rest: '/' };
|
|
56
|
+
const match = pathname.match(/^\/(ru|en)(\/.*)?$/);
|
|
57
|
+
if (!match) return { lang: null, rest: pathname };
|
|
58
|
+
const lang = match[1] as Lang;
|
|
59
|
+
const rest = match[2] && match[2].length > 0 ? match[2] : '/';
|
|
60
|
+
return { lang, rest };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Inline script for the root `/page.tsx`. Decides whether to redirect the
|
|
65
|
+
* visitor to `/{lang}/` based on (1) stored preference, (2) navigator.language.
|
|
66
|
+
* If the resolved language equals DEFAULT_LANG we skip the redirect to avoid
|
|
67
|
+
* a flash — the static `/index.html` already serves DEFAULT_LANG content.
|
|
68
|
+
*
|
|
69
|
+
* Side effect: when the language is auto-detected (no stored value yet) we
|
|
70
|
+
* persist the choice so subsequent visits are stable.
|
|
71
|
+
*
|
|
72
|
+
* basePath is derived from `window.location.pathname`: the root index.html
|
|
73
|
+
* sits at `/{basePath}/`, so appending `{lang}/` to that path yields the
|
|
74
|
+
* correct redirect target without needing a build-time substitution.
|
|
75
|
+
*/
|
|
76
|
+
export const LANG_INIT_SCRIPT = `(function(){try{var key=${JSON.stringify(
|
|
77
|
+
LANG_STORAGE_KEY,
|
|
78
|
+
)};var def=${JSON.stringify(
|
|
79
|
+
DEFAULT_LANG,
|
|
80
|
+
)};var stored=null;try{stored=window.localStorage.getItem(key);}catch(e){}var lang;if(stored==='ru'||stored==='en'){lang=stored;}else{var nav=(navigator&&(navigator.language||(navigator.languages&&navigator.languages[0])))||'';lang=String(nav).toLowerCase().indexOf('ru')===0?'ru':'en';try{window.localStorage.setItem(key,lang);}catch(e){}}if(lang===def)return;var p=window.location.pathname;if(p.charAt(p.length-1)!=='/')p+='/';window.location.replace(p+lang+'/');}catch(e){}})();`;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Inline script for `[lang]/layout.tsx`. Syncs `document.documentElement.lang`
|
|
84
|
+
* to the active language so in-browser APIs (Intl, screen readers) see the
|
|
85
|
+
* correct value — the static HTML always carries `<html lang={DEFAULT_LANG}>`
|
|
86
|
+
* because Next.js renders <html> once at the root layout.
|
|
87
|
+
*/
|
|
88
|
+
export function LANG_SYNC_SCRIPT(lang: Lang): string {
|
|
89
|
+
return `(function(){try{document.documentElement.lang=${JSON.stringify(lang)};}catch(e){}})();`;
|
|
90
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
flattenLessons,
|
|
3
|
+
getLessonIndex,
|
|
4
|
+
type Course,
|
|
5
|
+
type FlatLessonEntry,
|
|
6
|
+
} from './course';
|
|
7
|
+
import { isCompleted, lessonKey, type LessonKey, type ProgressMap } from './progress';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve the linear index of the user's furthest reached lesson. Returns
|
|
11
|
+
* the max of (a) the index of the stored pointer when it still maps to a
|
|
12
|
+
* real lesson, and (b) the highest index among completed entries that still
|
|
13
|
+
* exist in the course. Combining both sources protects against a cross-tab
|
|
14
|
+
* `bumpFurthest` race that could persist a lower pointer than the user has
|
|
15
|
+
* actually reached: the still-valid `progress` map heals the regression
|
|
16
|
+
* without waiting for another advance write. Returns -1 only when both
|
|
17
|
+
* sources are empty.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveFurthestIndex(
|
|
20
|
+
course: Course,
|
|
21
|
+
furthestKey: LessonKey | null,
|
|
22
|
+
progress: ProgressMap,
|
|
23
|
+
): number {
|
|
24
|
+
const flat = flattenLessons(course);
|
|
25
|
+
let max = -1;
|
|
26
|
+
|
|
27
|
+
if (furthestKey !== null) {
|
|
28
|
+
const idx = flat.findIndex((e) => lessonKey(e.moduleId, e.lesson.slug) === furthestKey);
|
|
29
|
+
if (idx > max) max = idx;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < flat.length; i += 1) {
|
|
33
|
+
const key = lessonKey(flat[i].moduleId, flat[i].lesson.slug);
|
|
34
|
+
if (isCompleted(progress, key) && i > max) {
|
|
35
|
+
max = i;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return max;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* A lesson is unlocked when its index is no further than `furthestIndex + 1`.
|
|
43
|
+
* That extra step is what makes "the next lesson after the furthest" reachable
|
|
44
|
+
* — without it, a fresh user (furthestIndex = -1) could not even open the
|
|
45
|
+
* first lesson (index 0).
|
|
46
|
+
*/
|
|
47
|
+
export function isLessonUnlocked(lessonIndex: number, furthestIndex: number): boolean {
|
|
48
|
+
if (lessonIndex < 0) return false;
|
|
49
|
+
return lessonIndex <= furthestIndex + 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isLessonKeyUnlocked(
|
|
53
|
+
course: Course,
|
|
54
|
+
moduleId: string,
|
|
55
|
+
slug: string,
|
|
56
|
+
furthestKey: LessonKey | null,
|
|
57
|
+
progress: ProgressMap,
|
|
58
|
+
): boolean {
|
|
59
|
+
const idx = getLessonIndex(course, moduleId, slug);
|
|
60
|
+
if (idx === -1) return false;
|
|
61
|
+
return isLessonUnlocked(idx, resolveFurthestIndex(course, furthestKey, progress));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The "current frontier" lesson — the one a fresh CTA should target. Always
|
|
66
|
+
* the first locked-but-reachable lesson (`furthestIndex + 1`), so the user
|
|
67
|
+
* lands on what they've not yet finished even when checkmarks have been
|
|
68
|
+
* toggled. Returns null when the course is empty or the user has finished it.
|
|
69
|
+
*/
|
|
70
|
+
export function getFrontierLesson(
|
|
71
|
+
course: Course,
|
|
72
|
+
furthestIndex: number,
|
|
73
|
+
): FlatLessonEntry | null {
|
|
74
|
+
const flat = flattenLessons(course);
|
|
75
|
+
if (flat.length === 0) return null;
|
|
76
|
+
const targetIndex = Math.min(furthestIndex + 1, flat.length - 1);
|
|
77
|
+
if (targetIndex < 0) return null;
|
|
78
|
+
return flat[targetIndex];
|
|
79
|
+
}
|