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..21aba1c4 --- /dev/null +++ b/.github/scripts/update-language-switcher.js @@ -0,0 +1,202 @@ +#!/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: '🇮🇳' }, + 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: '🇨🇳' }, + '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 b1145141..3dd6f5f6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,8 @@ # Contributing to this repo + + + This is a host repository for other repositories, 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!_
Visit the Code Snippets repository