From 2e684812dc29d3f3ffbd9cc98c2d57a8cf34bd59 Mon Sep 17 00:00:00 2001 From: Laszlo Balla Date: Fri, 17 Jul 2026 16:04:36 +0200 Subject: [PATCH 1/2] Add translation workflow: issue/PR templates and language switcher Contributors can now claim a language via issue template, submit README/CONTRIBUTING/CODE_OF_CONDUCT translations under translations//, and have the language switcher auto-generated by a script that runs on push to main. README.md itself is untouched since it's regenerated externally from .github/templates/rootreadmetemplate.md. Co-Authored-By: Claude Sonnet 5 --- .github/ISSUE_TEMPLATE/translation.yml | 97 ++++++++++ .github/PULL_REQUEST_TEMPLATE/translation.md | 20 ++ .github/scripts/update-language-switcher.js | 191 +++++++++++++++++++ .github/templates/rootreadmetemplate.md | 3 + .github/workflows/language-switcher.yml | 46 +++++ CODE_OF_CONDUCT.md | 3 + CONTRIBUTING.md | 3 + translations/README.md | 9 + 8 files changed, 372 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/translation.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE/translation.md create mode 100644 .github/scripts/update-language-switcher.js create mode 100644 .github/workflows/language-switcher.yml create mode 100644 translations/README.md diff --git a/.github/ISSUE_TEMPLATE/translation.yml b/.github/ISSUE_TEMPLATE/translation.yml new file mode 100644 index 00000000..734f19fe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/translation.yml @@ -0,0 +1,97 @@ +name: Translation Request +description: Claim a language and translate the main documentation (README, CONTRIBUTING, CODE_OF_CONDUCT). +title: "[Translation] Add translation" +labels: ["translation", "hacktoberfest"] +body: + - type: markdown + attributes: + value: | + Thanks for helping translate this repository! + + **Please open this issue first** to claim a language before starting a PR — this avoids duplicate work. Wait for a maintainer to assign the issue to you before opening a PR. + + Read the whole template carefully — PRs that don't follow these conventions will be asked to revise before merge. + + - type: input + id: language + attributes: + label: Language + description: The name of the target language, in English. + placeholder: "e.g. Spanish, German, Brazilian Portuguese" + validations: + required: true + + - type: input + id: iso-code + attributes: + label: ISO 639-1 code (or BCP 47 tag for regional variants) + description: The 2-letter ISO code, or `-` for regional variants. Use lowercase. + placeholder: "e.g. es, de, fr, pt-br" + validations: + required: true + + - type: checkboxes + id: files + attributes: + label: Required files + description: All three files are required. Partial translations will not be merged. + options: + - label: "`README.md`" + required: true + - label: "`CONTRIBUTING.md`" + required: true + - label: "`CODE_OF_CONDUCT.md`" + required: true + + - type: checkboxes + id: structure + attributes: + label: Folder & filename conventions + options: + - label: I will create a new folder `translations//` (e.g. `translations/es/`). + required: true + - label: Translated files keep the exact original filenames (`README.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`) — no suffixes, no renaming. + required: true + - label: All three files land in a single PR. Splitting them across PRs will be closed and asked to consolidate. + required: true + + - type: checkboxes + id: links-and-assets + attributes: + label: Links & assets + options: + - label: Internal links between translated files point to the **same-language siblings** in the same folder. E.g. inside `translations/es/README.md`, a link to CONTRIBUTING points to `CONTRIBUTING.md` (Spanish), not `../../CONTRIBUTING.md` (English). + required: true + - label: Links to files that are **not translated** (e.g. `LICENSE`, source code) point back to the repo root using relative paths — e.g. `../../LICENSE`. + required: true + - label: Image references use relative paths back to the original assets (e.g. `../../images/logo.png`). **Do not duplicate images** or any non-markdown assets. + required: true + + - type: checkboxes + id: switcher + attributes: + label: Language switcher — DO NOT hand-write this + description: | + The language switcher at the top of each translated file is generated automatically by a script that scans the `translations/` folder. This keeps every file in sync as new languages are added. + options: + - label: | + I will place these two markers on their own lines, immediately after the H1 title in each translated file, with **nothing between them**: + + ``` + + + ``` + + The automation will fill in the list of available languages between these markers. If you write links yourself, they will be overwritten. + required: true + - label: I understand the same markers will be added to the English source files (`README.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`) at the repo root, and I will not remove them. + required: true + + - type: textarea + id: notes + attributes: + label: Notes (optional) + description: Anything relevant — regional variant reasoning, timeline, questions for maintainers. + placeholder: "e.g. I'm a native speaker; expect to have a PR ready within a week." + validations: + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE/translation.md b/.github/PULL_REQUEST_TEMPLATE/translation.md new file mode 100644 index 00000000..c154bfe7 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/translation.md @@ -0,0 +1,20 @@ + + +## Translation + +- Language: +- ISO code (folder name under `translations/`): +- Related issue: # + +## Checklist + +- [ ] All three files are included in this PR: `README.md`, `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md` +- [ ] Files live under `translations//` with the **exact same filenames** as the English originals +- [ ] Internal links between translated files point to same-language siblings (e.g. `translations/es/README.md` links to `CONTRIBUTING.md`, not `../../CONTRIBUTING.md`) +- [ ] Links to non-translated files (LICENSE, source code, etc.) point back to the repo root with relative paths +- [ ] Image references point back to the original `images/` assets — no images duplicated into the translation folder +- [ ] I left the `` / `` markers in place, empty, right after the H1 in each file — I did not write my own switcher links +- [ ] I have not modified any files outside `translations//` diff --git a/.github/scripts/update-language-switcher.js b/.github/scripts/update-language-switcher.js new file mode 100644 index 00000000..aa1fecda --- /dev/null +++ b/.github/scripts/update-language-switcher.js @@ -0,0 +1,191 @@ +#!/usr/bin/env node +'use strict'; + +// Regenerates the language-switcher block in README/CONTRIBUTING/CODE_OF_CONDUCT +// (and their translations// counterparts) by scanning the translations/ +// folder. Run via `node .github/scripts/update-language-switcher.js`. +// +// Do not hand-edit the text between the LANGUAGE_SWITCHER markers in any doc — +// this script owns that block and will overwrite it. + +const fs = require('fs'); +const path = require('path'); + +const ROOT = path.resolve(__dirname, '..', '..'); +const TRANSLATIONS_DIR = path.join(ROOT, 'translations'); +const START_MARKER = ''; +const END_MARKER = ''; + +// README's source of truth is the template, not the generated README.md — +// see AGENTS.md / CONTRIBUTING.md: README.md is regenerated by an external +// ServiceNow process and direct edits are overwritten. +const DOCS = { + README: { filename: 'README.md', rootPath: path.join(ROOT, '.github', 'templates', 'rootreadmetemplate.md') }, + CONTRIBUTING: { filename: 'CONTRIBUTING.md', rootPath: path.join(ROOT, 'CONTRIBUTING.md') }, + CODE_OF_CONDUCT: { filename: 'CODE_OF_CONDUCT.md', rootPath: path.join(ROOT, 'CODE_OF_CONDUCT.md') }, +}; + +const LANGUAGES = { + en: { name: 'English', flag: '🇬🇧' }, + es: { name: 'Español', flag: '🇪🇸' }, + de: { name: 'Deutsch', flag: '🇩🇪' }, + fr: { name: 'Français', flag: '🇫🇷' }, + it: { name: 'Italiano', flag: '🇮🇹' }, + pt: { name: 'Português', flag: '🇵🇹' }, + 'pt-br': { name: 'Português (Brasil)', flag: '🇧🇷' }, + nl: { name: 'Nederlands', flag: '🇳🇱' }, + pl: { name: 'Polski', flag: '🇵🇱' }, + sv: { name: 'Svenska', flag: '🇸🇪' }, + da: { name: 'Dansk', flag: '🇩🇰' }, + fi: { name: 'Suomi', flag: '🇫🇮' }, + no: { name: 'Norsk', flag: '🇳🇴' }, + cs: { name: 'Čeština', flag: '🇨🇿' }, + ro: { name: 'Română', flag: '🇷🇴' }, + hu: { name: 'Magyar', flag: '🇭🇺' }, + el: { name: 'Ελληνικά', flag: '🇬🇷' }, + ru: { name: 'Русский', flag: '🇷🇺' }, + uk: { name: 'Українська', flag: '🇺🇦' }, + tr: { name: 'Türkçe', flag: '🇹🇷' }, + ar: { name: 'العربية', flag: '🇸🇦' }, + he: { name: 'עברית', flag: '🇮🇱' }, + hi: { name: 'हिन्दी', flag: '🇮🇳' }, + ja: { name: '日本語', flag: '🇯🇵' }, + ko: { name: '한국어', flag: '🇰🇷' }, + 'zh-cn': { name: '简体中文', flag: '🇨🇳' }, + 'zh-tw': { name: '繁體中文', flag: '🇹🇼' }, + vi: { name: 'Tiếng Việt', flag: '🇻🇳' }, + th: { name: 'ไทย', flag: '🇹🇭' }, + id: { name: 'Bahasa Indonesia', flag: '🇮🇩' }, +}; + +function languageLabel(code) { + const entry = LANGUAGES[code.toLowerCase()]; + if (entry) return `${entry.flag} ${entry.name}`; + return code.toUpperCase(); +} + +function listTranslationLocales() { + if (!fs.existsSync(TRANSLATIONS_DIR)) return []; + return fs + .readdirSync(TRANSLATIONS_DIR, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); +} + +function rootRelativeLink(absPath) { + return '/' + path.relative(ROOT, absPath).split(path.sep).join('/'); +} + +function buildSwitcherLine(docKey, currentLocale, locales) { + const doc = DOCS[docKey]; + const entries = [{ code: 'en', linkPath: rootRelativeLink(doc.rootPath) }]; + + for (const locale of locales) { + const filePath = path.join(TRANSLATIONS_DIR, locale, doc.filename); + if (fs.existsSync(filePath)) { + entries.push({ code: locale, linkPath: rootRelativeLink(filePath) }); + } + } + + if (entries.length <= 1) return ''; // only English exists — nothing to switch to yet + + const links = entries.map(({ code, linkPath }) => { + const label = languageLabel(code); + return code === currentLocale ? `**${label}**` : `[${label}](${linkPath})`; + }); + + return `**Available in:** ${links.join(' · ')}`; +} + +function updateFile(filePath, docKey, currentLocale, locales) { + if (!fs.existsSync(filePath)) return { path: filePath, updated: false, reason: 'missing' }; + + const original = fs.readFileSync(filePath, 'utf8'); + const startIdx = original.indexOf(START_MARKER); + const endIdx = original.indexOf(END_MARKER); + + if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) { + return { path: filePath, updated: false, reason: 'no-markers' }; + } + + const switcherLine = buildSwitcherLine(docKey, currentLocale, locales); + const before = original.slice(0, startIdx + START_MARKER.length); + const after = original.slice(endIdx); + const middle = switcherLine ? `\n${switcherLine}\n` : '\n'; + const next = before + middle + after; + + if (next === original) return { path: filePath, updated: false, reason: 'unchanged' }; + + fs.writeFileSync(filePath, next, 'utf8'); + return { path: filePath, updated: true }; +} + +function updateTranslationsIndex(locales) { + const filePath = path.join(TRANSLATIONS_DIR, 'README.md'); + const lines = [ + '# Translations', + '', + "This folder holds community-contributed translations of this repo's main docs.", + '', + 'This file is generated automatically by [`.github/scripts/update-language-switcher.js`](../.github/scripts/update-language-switcher.js) — do not edit it by hand.', + '', + 'Want to add a language? Open a [Translation Request issue](../../../issues/new?template=translation.yml).', + '', + ]; + + if (locales.length === 0) { + lines.push('_No translations yet — be the first!_'); + } else { + lines.push('| Language | README | CONTRIBUTING | CODE_OF_CONDUCT |'); + lines.push('|---|---|---|---|'); + for (const locale of locales) { + const cells = Object.keys(DOCS).map((docKey) => { + const filePath = path.join(TRANSLATIONS_DIR, locale, DOCS[docKey].filename); + return fs.existsSync(filePath) ? '✅' : '—'; + }); + lines.push(`| ${languageLabel(locale)} (\`${locale}\`) | ${cells[0]} | ${cells[1]} | ${cells[2]} |`); + } + } + + const next = lines.join('\n') + '\n'; + + if (!fs.existsSync(TRANSLATIONS_DIR)) fs.mkdirSync(TRANSLATIONS_DIR, { recursive: true }); + + const original = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : null; + if (original === next) return { path: filePath, updated: false, reason: 'unchanged' }; + + fs.writeFileSync(filePath, next, 'utf8'); + return { path: filePath, updated: true }; +} + +function main() { + const locales = listTranslationLocales(); + const results = []; + + for (const [docKey, doc] of Object.entries(DOCS)) { + results.push(updateFile(doc.rootPath, docKey, 'en', locales)); + for (const locale of locales) { + const filePath = path.join(TRANSLATIONS_DIR, locale, doc.filename); + results.push(updateFile(filePath, docKey, locale, locales)); + } + } + + results.push(updateTranslationsIndex(locales)); + + const changed = results.filter((r) => r.updated); + const missingMarkers = results.filter((r) => r.reason === 'no-markers'); + + console.log(`Language switcher: ${changed.length} file(s) updated.`); + changed.forEach((r) => console.log(` wrote ${path.relative(ROOT, r.path)}`)); + if (missingMarkers.length) { + console.log(`Skipped ${missingMarkers.length} file(s) missing LANGUAGE_SWITCHER markers:`); + missingMarkers.forEach((r) => console.log(` no markers ${path.relative(ROOT, r.path)}`)); + } + + if (process.env.GITHUB_OUTPUT) { + fs.appendFileSync(process.env.GITHUB_OUTPUT, `changed=${changed.length > 0}\n`); + } +} + +main(); diff --git a/.github/templates/rootreadmetemplate.md b/.github/templates/rootreadmetemplate.md index 484c88aa..f17fc502 100644 --- a/.github/templates/rootreadmetemplate.md +++ b/.github/templates/rootreadmetemplate.md @@ -4,6 +4,9 @@ # 🎃 Hacktoberfest 2025 is Live! 🎃 + + + diff --git a/.github/workflows/language-switcher.yml b/.github/workflows/language-switcher.yml new file mode 100644 index 00000000..71675bb3 --- /dev/null +++ b/.github/workflows/language-switcher.yml @@ -0,0 +1,46 @@ +name: Update language switcher + +# Regenerates the language-switcher block in README/CONTRIBUTING/CODE_OF_CONDUCT +# (and their translations// counterparts) whenever a translation is +# added, removed, or the docs/templates themselves change. Self-healing: if +# the root README.md is ever regenerated externally and wipes the switcher +# from the template's copy, the next push touching those paths restores it. + +on: + push: + branches: [main] + paths: + - 'translations/**' + - 'CONTRIBUTING.md' + - 'CODE_OF_CONDUCT.md' + - '.github/templates/rootreadmetemplate.md' + - '.github/scripts/update-language-switcher.js' + workflow_dispatch: {} + +permissions: + contents: write + +concurrency: + group: language-switcher-${{ github.ref }} + cancel-in-progress: true + +jobs: + update: + if: github.repository == 'ServiceNowDevProgram/Hacktoberfest' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run language switcher generator + id: generate + run: node .github/scripts/update-language-switcher.js + + - name: Commit changes + if: steps.generate.outputs.changed == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add CONTRIBUTING.md CODE_OF_CONDUCT.md .github/templates/rootreadmetemplate.md translations/ + git commit -m "chore: update language switcher [skip ci]" + git push diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index ccc2ca84..47e90e39 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,5 +1,8 @@ # 🌟 Code of Conduct + + + ## Our Pledge 🤝 We as members, contributors, and leaders pledge to make participation in the ServiceNow Hacktoberfest event a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad8aec88..dca05207 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,8 @@ # Contributing to this repo + + + This is a host repo to another repos, so there isn't much to contribute to it. But if you do have something to contribute, the only requirements are: diff --git a/translations/README.md b/translations/README.md new file mode 100644 index 00000000..6adb96dd --- /dev/null +++ b/translations/README.md @@ -0,0 +1,9 @@ +# Translations + +This folder holds community-contributed translations of this repo's main docs. + +This file is generated automatically by [`.github/scripts/update-language-switcher.js`](../.github/scripts/update-language-switcher.js) — do not edit it by hand. + +Want to add a language? Open a [Translation Request issue](../../../issues/new?template=translation.yml). + +_No translations yet — be the first!_ From 55eb0e032341c62c0eb1c965a7c7f90f629264fb Mon Sep 17 00:00:00 2001 From: Laszlo Balla Date: Fri, 17 Jul 2026 16:25:05 +0200 Subject: [PATCH 2/2] Expand language map: Indian languages, dual flags, US flag for English Add Bengali, Tamil, Telugu, Marathi, Gujarati, Kannada, Malayalam, Punjabi, Odia, and Assamese alongside the existing Hindi entry, since Indian contributors are likely to be among the first translators. Bengali, Punjabi, and Urdu get dual flags since each is a national language of two countries. Switch English's flag from GB to US. --- .github/scripts/update-language-switcher.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/scripts/update-language-switcher.js b/.github/scripts/update-language-switcher.js index aa1fecda..21aba1c4 100644 --- a/.github/scripts/update-language-switcher.js +++ b/.github/scripts/update-language-switcher.js @@ -26,7 +26,7 @@ const DOCS = { }; const LANGUAGES = { - en: { name: 'English', flag: '🇬🇧' }, + en: { name: 'English', flag: '🇺🇸' }, es: { name: 'Español', flag: '🇪🇸' }, de: { name: 'Deutsch', flag: '🇩🇪' }, fr: { name: 'Français', flag: '🇫🇷' }, @@ -49,6 +49,17 @@ const LANGUAGES = { ar: { name: 'العربية', flag: '🇸🇦' }, he: { name: 'עברית', flag: '🇮🇱' }, hi: { name: 'हिन्दी', flag: '🇮🇳' }, + bn: { name: 'বাংলা', flag: '🇮🇳🇧🇩' }, + ta: { name: 'தமிழ்', flag: '🇮🇳' }, + te: { name: 'తెలుగు', flag: '🇮🇳' }, + mr: { name: 'मराठी', flag: '🇮🇳' }, + gu: { name: 'ગુજરાતી', flag: '🇮🇳' }, + kn: { name: 'ಕನ್ನಡ', flag: '🇮🇳' }, + ml: { name: 'മലയാളം', flag: '🇮🇳' }, + pa: { name: 'ਪੰਜਾਬੀ', flag: '🇮🇳🇵🇰' }, + or: { name: 'ଓଡ଼ିଆ', flag: '🇮🇳' }, + as: { name: 'অসমীয়া', flag: '🇮🇳' }, + ur: { name: 'اردو', flag: '🇮🇳🇵🇰' }, ja: { name: '日本語', flag: '🇯🇵' }, ko: { name: '한국어', flag: '🇰🇷' }, 'zh-cn': { name: '简体中文', flag: '🇨🇳' },
Visit the Code Snippets repository