docs: move English sources into docs/en/ locale folder (#5501)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich
2026-05-19 12:06:44 -07:00
committed by GitHub
parent 92cfbaee9b
commit 11bc37c968
42 changed files with 155 additions and 110 deletions
+17 -17
View File
@@ -39,7 +39,7 @@ jobs:
'^feature/.*/src/commonMain/.*/(ui|component|screen)/|^feature/.*/src/androidMain/.*/ui/|^core/ui/src/commonMain/' \ '^feature/.*/src/commonMain/.*/(ui|component|screen)/|^feature/.*/src/androidMain/.*/ui/|^core/ui/src/commonMain/' \
| grep -v 'Test\|Preview\|__Snapshots__' || true) | grep -v 'Test\|Preview\|__Snapshots__' || true)
docs_changed=$(echo "$changed" | grep -E '^docs/(user|developer)/' || true) docs_changed=$(echo "$changed" | grep -E '^docs/en/(user|developer)/' || true)
echo "views_changed<<EOF" >> "$GITHUB_OUTPUT" echo "views_changed<<EOF" >> "$GITHUB_OUTPUT"
echo "$views_changed" >> "$GITHUB_OUTPUT" echo "$views_changed" >> "$GITHUB_OUTPUT"
@@ -64,9 +64,9 @@ jobs:
const body = [ const body = [
'## 📄 Docs staleness check — advisory', '## 📄 Docs staleness check — advisory',
'', '',
'This PR modifies user-facing UI source files but does not update any page under `docs/user/` or `docs/developer/`.', 'This PR modifies user-facing UI source files but does not update any page under `docs/en/user/` or `docs/en/developer/`.',
'', '',
'> ⚠️ Doc changes propagate to **3 consumers**: in-app docs browser, Jekyll site (GitHub Pages), and meshtastic.org (Docusaurus sync). Updating a page in `docs/` automatically flows to all three.', '> ⚠️ Doc changes propagate to **3 consumers**: in-app docs browser, Jekyll site (GitHub Pages), and meshtastic.org (Docusaurus sync). Updating a page in `docs/en/` automatically flows to all three.',
'', '',
'**Changed source files:**', '**Changed source files:**',
'```', '```',
@@ -76,19 +76,19 @@ jobs:
'**What to check:**', '**What to check:**',
'| Changed area | Likely doc page |', '| Changed area | Likely doc page |',
'|---|---|', '|---|---|',
'| `feature/messaging/` | `docs/user/messages-and-channels.md` |', '| `feature/messaging/` | `docs/en/user/messages-and-channels.md` |',
'| `feature/node/` | `docs/user/nodes.md` or `docs/user/node-metrics.md` |', '| `feature/node/` | `docs/en/user/nodes.md` or `docs/en/user/node-metrics.md` |',
'| `feature/map/` | `docs/user/map-and-waypoints.md` |', '| `feature/map/` | `docs/en/user/map-and-waypoints.md` |',
'| `feature/connections/` | `docs/user/connections.md` |', '| `feature/connections/` | `docs/en/user/connections.md` |',
'| `feature/settings/` | `docs/user/settings-radio-user.md` or `docs/user/settings-module-admin.md` |', '| `feature/settings/` | `docs/en/user/settings-radio-user.md` or `docs/en/user/settings-module-admin.md` |',
'| `feature/firmware/` | `docs/user/firmware.md` |', '| `feature/firmware/` | `docs/en/user/firmware.md` |',
'| `feature/intro/` | `docs/user/onboarding.md` |', '| `feature/intro/` | `docs/en/user/onboarding.md` |',
'| `feature/discovery/` | `docs/user/discovery.md` |', '| `feature/discovery/` | `docs/en/user/discovery.md` |',
'| `feature/docs/` | Internal docs infrastructure |', '| `feature/docs/` | Internal docs infrastructure |',
'| `core/ui/` | `docs/developer/codebase.md` or component-specific user pages |', '| `core/ui/` | `docs/en/developer/codebase.md` or component-specific user pages |',
'', '',
'**New page checklist** (if adding a new doc page):', '**New page checklist** (if adding a new doc page):',
'1. Create the `.md` file in `docs/user/` or `docs/developer/` with `last_updated` frontmatter', '1. Create the `.md` file in `docs/en/user/` or `docs/en/developer/` with `last_updated` frontmatter',
'2. Register in `DocBundleLoader.kt` with string resources (in-app browser)', '2. Register in `DocBundleLoader.kt` with string resources (in-app browser)',
'3. Jekyll and Docusaurus sync pick up new pages automatically — no config change needed', '3. Jekyll and Docusaurus sync pick up new pages automatically — no config change needed',
'', '',
@@ -141,14 +141,14 @@ jobs:
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
comment_id: existing.id, comment_id: existing.id,
body: '## ✅ Docs staleness check passed\n\nThis PR includes updates to `docs/` alongside the source changes. Thank you!', body: '## ✅ Docs staleness check passed\n\nThis PR includes updates to `docs/en/` alongside the source changes. Thank you!',
}); });
} }
- name: Advisory status - name: Advisory status
if: steps.changed.outputs.stale == 'true' if: steps.changed.outputs.stale == 'true'
run: | run: |
echo "::warning::UI source files changed without corresponding docs/ updates." echo "::warning::UI source files changed without corresponding docs/en/ updates."
echo "Add the 'skip-docs-check' label if this PR does not require a doc update." echo "Add the 'skip-docs-check' label if this PR does not require a doc update."
echo "NOTE: This check is advisory while docs coverage matures across platforms." echo "NOTE: This check is advisory while docs coverage matures across platforms."
echo "To upgrade to blocking, change this step to 'exit 1'." echo "To upgrade to blocking, change this step to 'exit 1'."
@@ -170,7 +170,7 @@ jobs:
node-version: "24" node-version: "24"
- name: Validate internal links - name: Validate internal links
run: node scripts/validate-doc-links.js docs run: node scripts/validate-doc-links.js docs/en
- name: Check doc coverage - name: Check doc coverage
run: node scripts/check-doc-coverage.js . run: node scripts/check-doc-coverage.js .
@@ -179,7 +179,7 @@ jobs:
run: | run: |
loader="feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt" loader="feature/docs/src/commonMain/kotlin/org/meshtastic/feature/docs/data/DocBundleLoader.kt"
missing=0 missing=0
for f in docs/user/*.md docs/developer/*.md; do for f in docs/en/user/*.md docs/en/developer/*.md; do
slug=$(basename "$f" .md) slug=$(basename "$f" .md)
if ! grep -q "\"$slug\"" "$loader"; then if ! grep -q "\"$slug\"" "$loader"; then
echo "ERROR: $slug not registered in DocBundleLoader.kt" echo "ERROR: $slug not registered in DocBundleLoader.kt"
@@ -100,9 +100,10 @@ abstract class GenerateDocsBundleTask : DefaultTask() {
val indexEntries = mutableListOf<String>() val indexEntries = mutableListOf<String>()
var pageCount = 0 var pageCount = 0
// Process English user and developer directories // Process English user and developer directories (under docs/en/)
val enDir = File(src, "en")
listOf("user", "developer").forEach { section -> listOf("user", "developer").forEach { section ->
val sectionDir = File(src, section) val sectionDir = File(enDir, section)
if (!sectionDir.exists()) return@forEach if (!sectionDir.exists()) return@forEach
sectionDir.listFiles { f -> f.extension == "md" }?.sortedBy { it.name }?.forEach { mdFile -> sectionDir.listFiles { f -> f.extension == "md" }?.sortedBy { it.name }?.forEach { mdFile ->
@@ -143,8 +144,9 @@ abstract class GenerateDocsBundleTask : DefaultTask() {
// Process Crowdin locale directories: docs/{qualifier}/user/*.md // Process Crowdin locale directories: docs/{qualifier}/user/*.md
// Crowdin %android_code% produces: fr, pt-rBR, zh-rCN, zh-rTW // Crowdin %android_code% produces: fr, pt-rBR, zh-rCN, zh-rTW
// Skip "en" since English sources are handled above.
val localePattern = Regex("^[a-z]{2,3}(-r[A-Z]{2})?$") val localePattern = Regex("^[a-z]{2,3}(-r[A-Z]{2})?$")
src.listFiles { f -> f.isDirectory && localePattern.matches(f.name) } src.listFiles { f -> f.isDirectory && localePattern.matches(f.name) && f.name != "en" }
?.sortedBy { it.name } ?.sortedBy { it.name }
?.forEach { localeDir -> ?.forEach { localeDir ->
val locale = localeDir.name val locale = localeDir.name
@@ -198,7 +200,7 @@ abstract class GenerateDocsBundleTask : DefaultTask() {
File(cssDir, "docs.css").writeText(generateCss()) File(cssDir, "docs.css").writeText(generateCss())
// Write locales manifest (for consumers that need to know available translations) // Write locales manifest (for consumers that need to know available translations)
val localesManifest = src.listFiles { f -> f.isDirectory && localePattern.matches(f.name) } val localesManifest = src.listFiles { f -> f.isDirectory && localePattern.matches(f.name) && f.name != "en" }
?.map { it.name }?.sorted() ?: emptyList() ?.map { it.name }?.sorted() ?: emptyList()
val manifestFile = File(out, "locales.json") val manifestFile = File(out, "locales.json")
manifestFile.writeText(localesManifest.joinToString(", ", "[", "]") { "\"$it\"" }) manifestFile.writeText(localesManifest.joinToString(", ", "[", "]") { "\"$it\"" })
@@ -258,7 +260,7 @@ abstract class GenerateDocsBundleTask : DefaultTask() {
.replace(Regex("^---[\\s\\S]*?---\\s*", RegexOption.MULTILINE), "") .replace(Regex("^---[\\s\\S]*?---\\s*", RegexOption.MULTILINE), "")
.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;") .replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
val dir = if (locale == "ar") "rtl" else "ltr" val dir = if (locale == "ar") "rtl" else "ltr"
// Locale pages are one level deeper: docs/{locale}/user/foo.html vs docs/user/foo.html // Locale pages are one level deeper: docs/{locale}/user/foo.html vs docs/en/user/foo.html
val cssPath = if (locale != "en") "../../styles/docs.css" else "../styles/docs.css" val cssPath = if (locale != "en") "../../styles/docs.css" else "../styles/docs.css"
return """ return """
|<!DOCTYPE html> |<!DOCTYPE html>
+3 -2
View File
@@ -11,10 +11,11 @@ files:
- source: /fastlane/metadata/android/en-US/changelogs/default.txt - source: /fastlane/metadata/android/en-US/changelogs/default.txt
translation: /fastlane/metadata/android/%locale%/changelogs/%original_file_name% translation: /fastlane/metadata/android/%locale%/changelogs/%original_file_name%
# In-app docs — user guide only (developer guide is English-only) # In-app docs — user guide only (developer guide is English-only)
# English sources live under docs/en/; translations land at docs/{android_code}/
# Uses %android_code% to output Android/CMP qualifier format directly (pt-rBR, zh-rCN, fr) # Uses %android_code% to output Android/CMP qualifier format directly (pt-rBR, zh-rCN, fr)
- source: /docs/user/*.md - source: /docs/en/user/*.md
translation: /docs/%android_code%/user/%original_file_name% translation: /docs/%android_code%/user/%original_file_name%
type: md type: md
- source: /docs/index.md - source: /docs/en/index.md
translation: /docs/%android_code%/%original_file_name% translation: /docs/%android_code%/%original_file_name%
type: md type: md
+45
View File
@@ -0,0 +1,45 @@
# Documentation Structure
This directory contains the source documentation for the Meshtastic Android/Desktop/iOS app.
It serves three consumers:
1. **In-app docs browser** — bundled via Compose Resources at build time
2. **Jekyll site** — GitHub Pages (this directory is the Jekyll source root)
3. **meshtastic.org** — Docusaurus sync (upstream consumption)
## Locale Layout
```
docs/
├── _config.yml, _data/, _layouts/, _sass/ ← Jekyll site infrastructure
├── en/ ← English source (edit here)
│ ├── user/ ← User Guide pages
│ ├── developer/ ← Developer Guide pages
│ ├── index.md ← Site home page
│ ├── user.md ← User Guide nav parent
│ ├── developer.md ← Developer Guide nav parent
│ └── translations.md ← Translations landing page
├── fr-rFR/ ← French (Crowdin-generated)
│ └── user/ ← Translated user guide
├── de-rDE/ ← German (Crowdin-generated)
│ └── user/
└── ... ← Other locales
```
## Editing Guidelines
- **English source**: Edit files under `docs/en/`. These are the authoritative source.
- **Translations**: Do **not** edit files in locale folders directly. They are auto-generated
by [Crowdin](https://crowdin.com/project/meshtastic-android) and will be overwritten on sync.
Contribute translations via Crowdin instead.
- **Adding a page**: Create the `.md` file in `docs/en/user/` or `docs/en/developer/`, then
register it in `feature/docs/.../DocBundleLoader.kt` for in-app bundling.
## How Translations Work
1. English source files (`docs/en/user/*.md`) are uploaded to Crowdin as translation sources
2. Volunteers translate via the Crowdin web UI
3. Crowdin PRs land translated files at `docs/{android_code}/user/*.md` (e.g., `fr-rFR`, `pt-rBR`)
4. At build time, the Gradle `syncTranslatedDocsToComposeResources` task bundles them into
locale-qualified Compose Resources for the in-app reader
5. The in-app `DocBundleLoader` tries the user's locale first, then falls back to English
+41 -40
View File
@@ -28,12 +28,12 @@ color_scheme: meshtastic
# Default front-matter for pages in subdirectories # Default front-matter for pages in subdirectories
defaults: defaults:
- scope: - scope:
path: "user/" path: "en/user/"
values: values:
parent: User Guide parent: User Guide
layout: default layout: default
- scope: - scope:
path: "developer/" path: "en/developer/"
values: values:
parent: Developer Guide parent: Developer Guide
layout: default layout: default
@@ -41,229 +41,229 @@ defaults:
# They use a dedicated locale layout with a back-link to the English version. # They use a dedicated locale layout with a back-link to the English version.
# Auto-generated from Android app locales (values-* resource dirs). # Auto-generated from Android app locales (values-* resource dirs).
- scope: - scope:
path: "ar/" path: "ar"
values: values:
layout: locale_page layout: locale_page
locale: ar locale: ar
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "be/" path: "be"
values: values:
layout: locale_page layout: locale_page
locale: be locale: be
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "bg/" path: "bg"
values: values:
layout: locale_page layout: locale_page
locale: bg locale: bg
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "ca/" path: "ca"
values: values:
layout: locale_page layout: locale_page
locale: ca locale: ca
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "cs/" path: "cs"
values: values:
layout: locale_page layout: locale_page
locale: cs locale: cs
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "de/" path: "de"
values: values:
layout: locale_page layout: locale_page
locale: de locale: de
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "el/" path: "el"
values: values:
layout: locale_page layout: locale_page
locale: el locale: el
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "es/" path: "es"
values: values:
layout: locale_page layout: locale_page
locale: es locale: es
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "et/" path: "et"
values: values:
layout: locale_page layout: locale_page
locale: et locale: et
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "fi/" path: "fi"
values: values:
layout: locale_page layout: locale_page
locale: fi locale: fi
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "fr/" path: "fr"
values: values:
layout: locale_page layout: locale_page
locale: fr locale: fr
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "ga/" path: "ga"
values: values:
layout: locale_page layout: locale_page
locale: ga locale: ga
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "gl/" path: "gl"
values: values:
layout: locale_page layout: locale_page
locale: gl locale: gl
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "he/" path: "he"
values: values:
layout: locale_page layout: locale_page
locale: he locale: he
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "hr/" path: "hr"
values: values:
layout: locale_page layout: locale_page
locale: hr locale: hr
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "ht/" path: "ht"
values: values:
layout: locale_page layout: locale_page
locale: ht locale: ht
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "hu/" path: "hu"
values: values:
layout: locale_page layout: locale_page
locale: hu locale: hu
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "is/" path: "is"
values: values:
layout: locale_page layout: locale_page
locale: is locale: is
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "it/" path: "it"
values: values:
layout: locale_page layout: locale_page
locale: it locale: it
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "ja/" path: "ja"
values: values:
layout: locale_page layout: locale_page
locale: ja locale: ja
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "ko/" path: "ko"
values: values:
layout: locale_page layout: locale_page
locale: ko locale: ko
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "lt/" path: "lt"
values: values:
layout: locale_page layout: locale_page
locale: lt locale: lt
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "nl/" path: "nl"
values: values:
layout: locale_page layout: locale_page
locale: nl locale: nl
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "no/" path: "no"
values: values:
layout: locale_page layout: locale_page
locale: no locale: no
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "pl/" path: "pl"
values: values:
layout: locale_page layout: locale_page
locale: pl locale: pl
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "pt/" path: "pt"
values: values:
layout: locale_page layout: locale_page
locale: pt locale: pt
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "pt-rBR/" path: "pt-rBR"
values: values:
layout: locale_page layout: locale_page
locale: pt-rBR locale: pt-rBR
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "ro/" path: "ro"
values: values:
layout: locale_page layout: locale_page
locale: ro locale: ro
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "ru/" path: "ru"
values: values:
layout: locale_page layout: locale_page
locale: ru locale: ru
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "sk/" path: "sk"
values: values:
layout: locale_page layout: locale_page
locale: sk locale: sk
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "sl/" path: "sl"
values: values:
layout: locale_page layout: locale_page
locale: sl locale: sl
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "sq/" path: "sq"
values: values:
layout: locale_page layout: locale_page
locale: sq locale: sq
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "sr/" path: "sr"
values: values:
layout: locale_page layout: locale_page
locale: sr locale: sr
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "sv/" path: "sv"
values: values:
layout: locale_page layout: locale_page
locale: sv locale: sv
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "tr/" path: "tr"
values: values:
layout: locale_page layout: locale_page
locale: tr locale: tr
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "uk/" path: "uk"
values: values:
layout: locale_page layout: locale_page
locale: uk locale: uk
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "zh-rCN/" path: "zh-rCN"
values: values:
layout: locale_page layout: locale_page
locale: zh-rCN locale: zh-rCN
nav_exclude: true nav_exclude: true
- scope: - scope:
path: "zh-rTW/" path: "zh-rTW"
values: values:
layout: locale_page layout: locale_page
locale: zh-rTW locale: zh-rTW
@@ -285,5 +285,6 @@ exclude:
- Gemfile - Gemfile
- Gemfile.lock - Gemfile.lock
- assets/screenshots/.gitkeep - assets/screenshots/.gitkeep
- README.md
- "*.sh" - "*.sh"
+2 -1
View File
@@ -7,7 +7,8 @@ layout: default
{% assign page_path = page.path %} {% assign page_path = page.path %}
{% assign path_parts = page_path | split: "/" %} {% assign path_parts = page_path | split: "/" %}
{% assign remaining_parts = path_parts | slice: 1, path_parts.size %} {% assign remaining_parts = path_parts | slice: 1, path_parts.size %}
{% assign en_path = remaining_parts | join: "/" | replace: ".md", "" %} {% assign en_relative = remaining_parts | join: "/" | replace: ".md", "" %}
{% assign en_path = "en/" | append: en_relative %}
<div class="locale-page-banner" {% if locale_info.dir == "rtl" %}dir="rtl"{% endif %}> <div class="locale-page-banner" {% if locale_info.dir == "rtl" %}dir="rtl"{% endif %}>
<p class="locale-notice"> <p class="locale-notice">
View File
View File
+6 -13
View File
@@ -59,24 +59,16 @@ kotlin {
*/ */
val syncDocsToComposeResources by val syncDocsToComposeResources by
tasks.registering(Sync::class) { tasks.registering(Sync::class) {
description = "Syncs docs/ markdown source into composeResources for in-app bundling" description = "Syncs docs/en/ markdown source into composeResources for in-app bundling"
group = "docs" group = "docs"
val docsSourceDir = rootProject.layout.projectDirectory.dir("docs") val docsEnDir = rootProject.layout.projectDirectory.dir("docs/en")
val screenshotsDir = rootProject.layout.projectDirectory.dir("docs/screenshots") val screenshotsDir = rootProject.layout.projectDirectory.dir("docs/screenshots")
val composeResourcesTarget = layout.projectDirectory.dir("src/commonMain/composeResources/files/docs") val composeResourcesTarget = layout.projectDirectory.dir("src/commonMain/composeResources/files/docs")
from(docsSourceDir) { from(docsEnDir) {
include("user/**/*.md") include("user/**/*.md")
include("developer/**/*.md") include("developer/**/*.md")
// Exclude Jekyll/site-only files that are not needed in-app
exclude("_config.yml")
exclude("_data/**")
exclude("_includes/**")
exclude("_layouts/**")
exclude("index.md")
exclude("assets/**")
exclude("Gemfile*")
} }
// FR-038: Bundle screenshots into assets/screenshots/ to match markdown image paths. // FR-038: Bundle screenshots into assets/screenshots/ to match markdown image paths.
@@ -117,8 +109,7 @@ val syncTranslatedDocsToComposeResources by
from(docsDir) { from(docsDir) {
// Crowdin outputs dirs in Android qualifier format (fr, pt-rBR, zh-rCN) // Crowdin outputs dirs in Android qualifier format (fr, pt-rBR, zh-rCN)
include("*/user/**/*.md") include("*/user/**/*.md")
exclude("user/**") exclude("en/**")
exclude("developer/**")
exclude("_*/**") exclude("_*/**")
exclude("assets/**") exclude("assets/**")
exclude("screenshots/**") exclude("screenshots/**")
@@ -133,6 +124,8 @@ val syncTranslatedDocsToComposeResources by
if (segments.size >= 3) { if (segments.size >= 3) {
val qualifier = segments[0] val qualifier = segments[0]
val rest = segments.drop(1).joinToString("/") val rest = segments.drop(1).joinToString("/")
// Output: files/{locale}/docs/user/page.md
// English source lives at: docs/en/$rest
path = "$qualifier/docs/$rest" path = "$qualifier/docs/$rest"
} }
} }
@@ -241,7 +241,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"onboarding", "onboarding",
CoreRes.string.doc_title_onboarding, CoreRes.string.doc_title_onboarding,
CoreRes.string.doc_keywords_onboarding, CoreRes.string.doc_keywords_onboarding,
"docs/user/onboarding.html", "en/user/onboarding.html",
1, 1,
listOf("first-launch", "setup", "intro"), listOf("first-launch", "setup", "intro"),
3200, 3200,
@@ -251,7 +251,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"connections", "connections",
CoreRes.string.doc_title_connections, CoreRes.string.doc_title_connections,
CoreRes.string.doc_keywords_connections, CoreRes.string.doc_keywords_connections,
"docs/user/connections.html", "en/user/connections.html",
2, 2,
listOf("bluetooth", "usb", "tcp", "pairing"), listOf("bluetooth", "usb", "tcp", "pairing"),
4100, 4100,
@@ -261,7 +261,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"messages-and-channels", "messages-and-channels",
CoreRes.string.doc_title_messages, CoreRes.string.doc_title_messages,
CoreRes.string.doc_keywords_messages, CoreRes.string.doc_keywords_messages,
"docs/user/messages-and-channels.html", "en/user/messages-and-channels.html",
3, 3,
listOf("channels", "direct-messages", "messaging", "conversations"), listOf("channels", "direct-messages", "messaging", "conversations"),
4500, 4500,
@@ -271,7 +271,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"nodes", "nodes",
CoreRes.string.doc_title_nodes, CoreRes.string.doc_title_nodes,
CoreRes.string.doc_keywords_nodes, CoreRes.string.doc_keywords_nodes,
"docs/user/nodes.html", "en/user/nodes.html",
4, 4,
listOf("node-list", "mesh-nodes", "peers"), listOf("node-list", "mesh-nodes", "peers"),
3800, 3800,
@@ -281,7 +281,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"node-metrics", "node-metrics",
CoreRes.string.doc_title_node_metrics, CoreRes.string.doc_title_node_metrics,
CoreRes.string.doc_keywords_node_metrics, CoreRes.string.doc_keywords_node_metrics,
"docs/user/node-metrics.html", "en/user/node-metrics.html",
5, 5,
listOf("metrics", "telemetry", "device-metrics", "signal"), listOf("metrics", "telemetry", "device-metrics", "signal"),
5200, 5200,
@@ -291,7 +291,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"map-and-waypoints", "map-and-waypoints",
CoreRes.string.doc_title_map, CoreRes.string.doc_title_map,
CoreRes.string.doc_keywords_map, CoreRes.string.doc_keywords_map,
"docs/user/map-and-waypoints.html", "en/user/map-and-waypoints.html",
6, 6,
listOf("map", "waypoints", "gps", "location"), listOf("map", "waypoints", "gps", "location"),
3600, 3600,
@@ -301,7 +301,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"settings-radio-user", "settings-radio-user",
CoreRes.string.doc_title_settings_radio, CoreRes.string.doc_title_settings_radio,
CoreRes.string.doc_keywords_settings_radio, CoreRes.string.doc_keywords_settings_radio,
"docs/user/settings-radio-user.html", "en/user/settings-radio-user.html",
7, 7,
listOf("settings", "radio-config", "user-config", "lora"), listOf("settings", "radio-config", "user-config", "lora"),
6800, 6800,
@@ -311,7 +311,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"settings-module-admin", "settings-module-admin",
CoreRes.string.doc_title_settings_module, CoreRes.string.doc_title_settings_module,
CoreRes.string.doc_keywords_settings_module, CoreRes.string.doc_keywords_settings_module,
"docs/user/settings-module-admin.html", "en/user/settings-module-admin.html",
8, 8,
listOf("modules", "module-config", "administration"), listOf("modules", "module-config", "administration"),
5500, 5500,
@@ -321,7 +321,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"telemetry-and-sensors", "telemetry-and-sensors",
CoreRes.string.doc_title_telemetry, CoreRes.string.doc_title_telemetry,
CoreRes.string.doc_keywords_telemetry, CoreRes.string.doc_keywords_telemetry,
"docs/user/telemetry-and-sensors.html", "en/user/telemetry-and-sensors.html",
9, 9,
listOf("sensors", "environment", "weather", "power-metrics"), listOf("sensors", "environment", "weather", "power-metrics"),
4800, 4800,
@@ -331,7 +331,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"tak", "tak",
CoreRes.string.doc_title_tak, CoreRes.string.doc_title_tak,
CoreRes.string.doc_keywords_tak, CoreRes.string.doc_keywords_tak,
"docs/user/tak.html", "en/user/tak.html",
10, 10,
listOf("tak", "atak", "team-awareness-kit"), listOf("tak", "atak", "team-awareness-kit"),
2400, 2400,
@@ -341,7 +341,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"mqtt", "mqtt",
CoreRes.string.doc_title_mqtt, CoreRes.string.doc_title_mqtt,
CoreRes.string.doc_keywords_mqtt, CoreRes.string.doc_keywords_mqtt,
"docs/user/mqtt.html", "en/user/mqtt.html",
11, 11,
listOf("mqtt", "internet-bridge", "broker"), listOf("mqtt", "internet-bridge", "broker"),
4200, 4200,
@@ -351,7 +351,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"discovery", "discovery",
CoreRes.string.doc_title_discovery, CoreRes.string.doc_title_discovery,
CoreRes.string.doc_keywords_discovery, CoreRes.string.doc_keywords_discovery,
"docs/user/discovery.html", "en/user/discovery.html",
12, 12,
listOf("mesh-discovery", "local-discovery", "network-scan"), listOf("mesh-discovery", "local-discovery", "network-scan"),
2800, 2800,
@@ -361,7 +361,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"firmware", "firmware",
CoreRes.string.doc_title_firmware, CoreRes.string.doc_title_firmware,
CoreRes.string.doc_keywords_firmware, CoreRes.string.doc_keywords_firmware,
"docs/user/firmware.html", "en/user/firmware.html",
13, 13,
listOf("firmware", "update", "ota", "flash"), listOf("firmware", "update", "ota", "flash"),
3400, 3400,
@@ -371,7 +371,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"desktop", "desktop",
CoreRes.string.doc_title_desktop, CoreRes.string.doc_title_desktop,
CoreRes.string.doc_keywords_desktop, CoreRes.string.doc_keywords_desktop,
"docs/user/desktop.html", "en/user/desktop.html",
14, 14,
listOf("desktop", "linux", "macos", "windows", "jvm"), listOf("desktop", "linux", "macos", "windows", "jvm"),
3900, 3900,
@@ -381,7 +381,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"signal-meter", "signal-meter",
CoreRes.string.doc_title_signal_meter, CoreRes.string.doc_title_signal_meter,
CoreRes.string.doc_keywords_signal_meter, CoreRes.string.doc_keywords_signal_meter,
"docs/user/signal-meter.html", "en/user/signal-meter.html",
15, 15,
listOf("signal-quality", "signal-strength", "rssi", "snr"), listOf("signal-quality", "signal-strength", "rssi", "snr"),
3500, 3500,
@@ -391,7 +391,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"units-and-locale", "units-and-locale",
CoreRes.string.doc_title_units, CoreRes.string.doc_title_units,
CoreRes.string.doc_keywords_units, CoreRes.string.doc_keywords_units,
"docs/user/units-and-locale.html", "en/user/units-and-locale.html",
16, 16,
listOf("measurement", "units", "locale", "metric", "imperial"), listOf("measurement", "units", "locale", "metric", "imperial"),
3800, 3800,
@@ -401,7 +401,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"translate", "translate",
CoreRes.string.doc_title_translate, CoreRes.string.doc_title_translate,
CoreRes.string.doc_keywords_translate, CoreRes.string.doc_keywords_translate,
"docs/user/translate.html", "en/user/translate.html",
17, 17,
listOf("crowdin", "localization", "language", "i18n", "contribute"), listOf("crowdin", "localization", "language", "i18n", "contribute"),
3700, 3700,
@@ -433,7 +433,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"architecture", "architecture",
"Architecture", "Architecture",
"developer", "developer",
"docs/developer/architecture.html", "en/developer/architecture.html",
1, 1,
listOf("architecture", "kmp", "module", "layer", "core", "feature", "compose"), listOf("architecture", "kmp", "module", "layer", "core", "feature", "compose"),
listOf("layers", "module-architecture", "kmp"), listOf("layers", "module-architecture", "kmp"),
@@ -444,7 +444,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"codebase", "codebase",
"Codebase", "Codebase",
"developer", "developer",
"docs/developer/codebase.html", "en/developer/codebase.html",
2, 2,
listOf("codebase", "repository", "layout", "gradle", "build", "namespace", "convention"), listOf("codebase", "repository", "layout", "gradle", "build", "namespace", "convention"),
listOf("repository-layout", "project-structure", "source-code"), listOf("repository-layout", "project-structure", "source-code"),
@@ -455,7 +455,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"adding-a-feature-module", "adding-a-feature-module",
"Adding a Feature Module", "Adding a Feature Module",
"developer", "developer",
"docs/developer/adding-a-feature-module.html", "en/developer/adding-a-feature-module.html",
3, 3,
listOf("module", "feature", "new", "create", "plugin", "di", "koin"), listOf("module", "feature", "new", "create", "plugin", "di", "koin"),
listOf("new-module", "feature-module", "module-guide"), listOf("new-module", "feature-module", "module-guide"),
@@ -466,7 +466,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"navigation-and-deep-links", "navigation-and-deep-links",
"Navigation & Deep Links", "Navigation & Deep Links",
"developer", "developer",
"docs/developer/navigation-and-deep-links.html", "en/developer/navigation-and-deep-links.html",
4, 4,
listOf("navigation", "deeplink", "route", "navkey", "backstack", "typed"), listOf("navigation", "deeplink", "route", "navkey", "backstack", "typed"),
listOf("deeplinks", "navigation-3", "routes"), listOf("deeplinks", "navigation-3", "routes"),
@@ -477,7 +477,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"transport", "transport",
"Transport", "Transport",
"developer", "developer",
"docs/developer/transport.html", "en/developer/transport.html",
5, 5,
listOf("transport", "ble", "serial", "tcp", "radio", "connection"), listOf("transport", "ble", "serial", "tcp", "radio", "connection"),
listOf("ble", "serial", "tcp", "radio-transport"), listOf("ble", "serial", "tcp", "radio-transport"),
@@ -488,7 +488,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"persistence", "persistence",
"Persistence", "Persistence",
"developer", "developer",
"docs/developer/persistence.html", "en/developer/persistence.html",
6, 6,
listOf("room", "database", "datastore", "prefs", "storage", "migration"), listOf("room", "database", "datastore", "prefs", "storage", "migration"),
listOf("room", "database", "datastore", "prefs"), listOf("room", "database", "datastore", "prefs"),
@@ -499,7 +499,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"testing", "testing",
"Testing", "Testing",
"developer", "developer",
"docs/developer/testing.html", "en/developer/testing.html",
7, 7,
listOf("test", "unit", "screenshot", "compose", "roborazzi", "ci"), listOf("test", "unit", "screenshot", "compose", "roborazzi", "ci"),
listOf("tests", "unit-tests", "screenshot-tests"), listOf("tests", "unit-tests", "screenshot-tests"),
@@ -510,7 +510,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"contributing", "contributing",
"Contributing", "Contributing",
"developer", "developer",
"docs/developer/contributing.html", "en/developer/contributing.html",
8, 8,
listOf("contributing", "pull-request", "branch", "commit", "style", "pr"), listOf("contributing", "pull-request", "branch", "commit", "style", "pr"),
listOf("contributing", "pull-request", "branch-naming"), listOf("contributing", "pull-request", "branch-naming"),
@@ -521,7 +521,7 @@ class DefaultDocBundleLoader : DocBundleLoader {
"measurement", "measurement",
"Measurement & Formatting", "Measurement & Formatting",
"developer", "developer",
"docs/developer/measurement.html", "en/developer/measurement.html",
9, 9,
listOf("formatter", "metric", "number", "locale", "temperature", "conversion", "api"), listOf("formatter", "metric", "number", "locale", "temperature", "conversion", "api"),
listOf("metric-formatter", "number-formatter", "measurement"), listOf("metric-formatter", "number-formatter", "measurement"),
+1 -1
View File
@@ -12,7 +12,7 @@ const path = require("path");
const { forEachDocPage } = require("./lib/frontmatter"); const { forEachDocPage } = require("./lib/frontmatter");
const REPO_ROOT = path.resolve(process.argv[2] || "."); const REPO_ROOT = path.resolve(process.argv[2] || ".");
const DOCS_DIR = path.join(REPO_ROOT, "docs"); const DOCS_DIR = path.join(REPO_ROOT, "docs", "en");
// Map of feature module directory names to expected doc page slugs. // Map of feature module directory names to expected doc page slugs.
// Modules not listed here are considered internal (no user-facing docs required). // Modules not listed here are considered internal (no user-facing docs required).
+1 -1
View File
@@ -13,7 +13,7 @@ const { parseFrontmatter, forEachDocPage } = require("./lib/frontmatter");
const args = process.argv.slice(2); const args = process.argv.slice(2);
const positional = args.filter(a => !a.startsWith("--")); const positional = args.filter(a => !a.startsWith("--"));
const DOCS_DIR = path.resolve(positional[0] || "docs"); const DOCS_DIR = path.resolve(positional[0] || path.join("docs", "en"));
const maxAgeArg = args.find(a => a.startsWith("--max-age-days=")); const maxAgeArg = args.find(a => a.startsWith("--max-age-days="));
const MAX_AGE_DAYS = maxAgeArg ? parseInt(maxAgeArg.split("=")[1], 10) : 180; const MAX_AGE_DAYS = maxAgeArg ? parseInt(maxAgeArg.split("=")[1], 10) : 180;
+2 -2
View File
@@ -37,8 +37,8 @@ const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".web
const ANDROID_REPO_ROOT = positionalArgs.length > 0 const ANDROID_REPO_ROOT = positionalArgs.length > 0
? path.resolve(positionalArgs[0]) ? path.resolve(positionalArgs[0])
: path.resolve(__dirname, ".."); : path.resolve(__dirname, "..");
const SRC_DOCS_DIR = path.join(ANDROID_REPO_ROOT, "docs"); const SRC_DOCS_DIR = path.join(ANDROID_REPO_ROOT, "docs", "en");
const SRC_SCREENSHOTS_DIR = path.join(SRC_DOCS_DIR, "assets", "screenshots"); const SRC_SCREENSHOTS_DIR = path.join(ANDROID_REPO_ROOT, "docs", "assets", "screenshots");
if (!fs.existsSync(SRC_DOCS_DIR)) { if (!fs.existsSync(SRC_DOCS_DIR)) {
console.error(`Error: docs directory not found at ${SRC_DOCS_DIR}`); console.error(`Error: docs directory not found at ${SRC_DOCS_DIR}`);
+4 -2
View File
@@ -11,7 +11,9 @@ const fs = require("fs");
const path = require("path"); const path = require("path");
const { discoverSlugs, forEachDocPage } = require("./lib/frontmatter"); const { discoverSlugs, forEachDocPage } = require("./lib/frontmatter");
const DOCS_DIR = path.resolve(process.argv[2] || "docs"); const DOCS_DIR = path.resolve(process.argv[2] || path.join("docs", "en"));
// Assets (screenshots) live at docs/ root, not inside the en/ locale folder
const DOCS_ROOT = path.resolve(DOCS_DIR, "..");
const IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"]); const IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"]);
// Collect known page slugs from both sections // Collect known page slugs from both sections
@@ -64,7 +66,7 @@ forEachDocPage(DOCS_DIR, (filePath, slug, section) => {
if (/^https?:/.test(imgPath)) continue; if (/^https?:/.test(imgPath)) continue;
const resolved = imgPath.startsWith("/") const resolved = imgPath.startsWith("/")
? path.join(DOCS_DIR, imgPath) ? path.join(DOCS_ROOT, imgPath)
: path.resolve(path.dirname(filePath), imgPath); : path.resolve(path.dirname(filePath), imgPath);
if (!fs.existsSync(resolved)) { if (!fs.existsSync(resolved)) {
console.log(` ERROR: ${section}/${slug}.md:${lineNum} — missing image '${imgPath}'`); console.log(` ERROR: ${section}/${slug}.md:${lineNum} — missing image '${imgPath}'`);