| name | obsidian-localize-new |
| description | Bootstrap a brand-new locale for the obsidian-help repo from scratch — generating translated filenames, syncing stubs from EN, and running LLM translation. Use this skill whenever the user wants to add a new language to obsidian-help, start a new locale translation, or asks to localize/translate the help docs into a new language. |
Bootstrap a full translation of a new locale for the obsidian-help repo.
The working directory is the obsidian-help repo root. If the user hasn't specified a locale code (e.g. de, ja, ko), ask for it before proceeding.
Context
Scripts are in scripts/. The en/ directory is the source of truth. Each locale (e.g. fr/, de/) mirrors en/ with translated content. The obsidian-translations repo is assumed to be a sibling directory (i.e. ../obsidian-translations/).
Pipeline
Start by checking whether <locale>/filenames.txt exists and whether there are already files in <locale>/. This determines which steps to skip.
Step 1 — Create the locale directory and filenames.txt
mkdir -p <locale>
filenames.txt maps every EN file (by permalink) to its locale filename and folder:
[file.{permalink}]
original=EN filename
translation=Locale filename
[folder.EN Folder Name]
original=EN Folder Name
translation=Locale Folder Name
If filenames.txt doesn't exist yet, run the translate-filenames script to generate it:
npx tsx scripts/translate-filenames.ts <locale>
This uses the LLM to propose locale filenames. Review the output with the user before proceeding.
Step 2 — Sync stubs from EN
Creates a stub .md file for every EN page that doesn't yet exist in the locale. Stubs contain EN frontmatter + EN content as placeholder, marked localized: false.
npx tsx scripts/sync-locale.ts <locale> --dry-run
npx tsx scripts/sync-locale.ts <locale>
The sync script places files at locale paths (using filenames.txt), adds EN basename as aliases when filenames differ, and deletes any orphan locale files.
Step 2b — Add redirect aliases for old locale paths
Skip this step entirely if the locale has no prior history in this repo (i.e. git log -- <locale>/ returns nothing). This step only applies to locales that previously had files under a different structure.
If the locale previously had old-format files (before this migration), add backward-compat redirect aliases so old Obsidian Publish URLs continue to resolve.
npx tsx scripts/migrate-locale.ts <locale>
npx tsx scripts/migrate-locale.ts <locale> --apply
For uncertain or unmatched paths, create <locale>/migration-map.json with explicit mappings before running --apply:
{
"Old/Path without extension": "en-permalink",
"Another/Old Path": null
}
Set a permalink to null to skip a path. See es/migration-map.json or fr/migration-map.json as examples.
Since filenames.txt is created in this session and not yet committed, the script can't auto-detect the pre-migration commit. Pass the last commit that touched the locale:
git log --oneline -- <locale>/
npx tsx scripts/migrate-locale.ts <locale> --commit <sha>
npx tsx scripts/migrate-locale.ts <locale> --commit <sha> --apply
Step 3 — Translate all stubs
Sends each localized: false file to the LLM for full translation. After translating the body, descriptions are automatically translated in a batched pass.
npx tsx scripts/translate-locale.ts <locale> --dry-run
npx tsx scripts/translate-locale.ts <locale>
To re-translate descriptions for already-localized files (e.g. after EN descriptions changed):
npx tsx scripts/translate-locale.ts <locale> --translate-descriptions
For large locales (~170 files), run 8 parallel batches then a final cleanup pass:
npx tsx scripts/translate-locale.ts <locale> --limit 22 --offset 0 &
npx tsx scripts/translate-locale.ts <locale> --limit 22 --offset 22 &
npx tsx scripts/translate-locale.ts <locale> --limit 22 --offset 44 &
npx tsx scripts/translate-locale.ts <locale> --limit 22 --offset 66 &
npx tsx scripts/translate-locale.ts <locale> --limit 22 --offset 88 &
npx tsx scripts/translate-locale.ts <locale> --limit 22 --offset 110 &
npx tsx scripts/translate-locale.ts <locale> --limit 22 --offset 132 &
npx tsx scripts/translate-locale.ts <locale> --limit 22 --offset 154 &
wait
npx tsx scripts/translate-locale.ts <locale>
Parallel batches can claim overlapping work and leave some files untranslated — the final pass without --limit is not optional.
Step 4 — Validate
npx tsx scripts/check-links.ts <locale>
npx tsx scripts/check-terms.ts <locale>
npx tsx scripts/check-terms.ts <locale> --fix
npx tsx scripts/sort-core-plugins.ts <locale>
Fix any broken wikilinks before publishing. The lint script cross-references official plugin/feature names from obsidian-translations.
Two broken-link patterns recur in nearly every locale — look for these specifically after check-links:
[[Plugins/X]] — if the Plugins folder was renamed (e.g. to "Πρόσθετα", "Plugins"), any hardcoded EN path in content won't match. Do a grep for [[Plugins/ and update to the locale path.
[[Editing and formatting/Tags\|...]] in the Properties page — this EN path is hardcoded in the source. Update to [[<locale-folder>/Tags-translation\|...]].
Step 5 — Add publish UI strings and language switcher
Create <locale>/publish.strings.js with translated UI strings for the Publish site. Use official app strings from ../obsidian-translations/translations/<lang>.txt for accuracy:
(function () {
function apply() {
var el;
el = document.querySelector('.search-bar');
if (!el) return false;
el.placeholder = '<Search...>';
el = document.querySelector('.site-footer a');
if (el) el.textContent = '<Powered by Obsidian Publish>';
el = document.querySelector('.graph-view-outer span:last-child');
if (el) el.textContent = '<Interactive graph>';
el = document.querySelector('.graph-expand');
if (el) el.setAttribute('aria-label', '<Expand>');
el = document.querySelector('.graph-global');
if (el) el.setAttribute('aria-label', '<Global graph>');
el = document.querySelector('.outline-view-outer span:last-child');
if (el) el.textContent = '<On this page>';
return true;
}
function poll() { if (!apply()) requestAnimationFrame(poll); }
poll();
var blText = '<Backlinks>';
function applyBl() { document.querySelectorAll('.backlinks span:last-child').forEach(function(e) { if (e.textContent !== blText) e.textContent = blText; }); }
new MutationObserver(applyBl).observe(document.body, { childList: true, subtree: true });
applyBl();
})();
Add the locale to scripts/locales.json (single source of truth — keep alphabetical, zh last). Then build:
npx tsx scripts/build-publish-js.ts <locale>
npx tsx scripts/build-publish-js.ts
Label conventions: use the native language name, e.g. Português (Brasil) for pt-BR, Español for es. Use the correct locale code (e.g. pt-BR not pt-br) to match the Obsidian Publish URL.
Step 6 — Link Publish site
Before publishing, link the locale to its Obsidian Publish site. The site slug is help-<locale-slug> where the slug is the lowercase version of the locale code (e.g. zh-TW → help-zh-tw, pt-BR → help-pt-br):
cd <locale> && ob publish-setup --site help-<locale-slug>
This writes the Obsidian Publish credentials into the locale's .obsidian/publish.json with the correct path. Without this step, ob publish will report "No publish configuration found".
Note: setup-sites.ts (run automatically by publish-all.ts) re-runs ob publish-setup for every locale before publishing. It uses locales.json to resolve the correct directory name, so mixed-case locales like zh-TW are handled correctly.
Step 7 — Update README
Add the new locale to the language table in README.md. The table is sorted alphabetically by locale code, with en first and zh/zh-TW last. Insert the new row in the correct alphabetical position:
| `<locale>` | https://obsidian.md/<locale>/help/ |
Use the same locale code casing as in locales.json (e.g. pt-BR, zh-TW).
Step 8 — Publish
Publish the new locale using the publish-all script (it handles nav order and site options too):
npx tsx scripts/publish-all.ts <locale>
npx tsx scripts/publish-all.ts
Notes
ANTHROPIC_API_KEY must be set, or scripts/llm-config.json must exist
- The LLM never translates: Obsidian, Sync, Publish, Canvas, Markdown, CSS, API
translate-filenames.ts automatically loads official plugin name translations from ../obsidian-translations/translations/<lang>.txt and passes them to the LLM — this prevents mismatches between filename and content translations (e.g. "Backlinks" vs "Links inversos")
- Callout types (
[!warning]) include explicit titles in EN source — these get translated automatically
description frontmatter is translated alongside content
- Always run
check-links before publishing
Key files
<locale>/filenames.txt — permalink → locale filename/folder mapping
<locale>/headings.txt — EN heading → locale heading mapping (auto-maintained)
scripts/sync-locale.ts — syncs EN structure to locale
scripts/migrate-locale.ts — adds redirect aliases from old locale paths
scripts/translate-locale.ts — LLM translation
scripts/check-links.ts — broken wikilink checker
scripts/check-terms.ts — terminology checker
<locale>/publish.strings.js — locale UI strings for Publish site (search, footer, graph, outline, backlinks labels)
scripts/locales.json — single source of truth for active locales (code, label, base URL, optional dir)
scripts/build-publish-js.ts — reads locales.json, regenerates LOCALES block in en/publish.js, concatenates publish.strings.js into each locale's publish.js
scripts/sync-nav-order.ts — translates EN nav order to locale paths; run automatically by publish-all.ts