Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .github/ISSUE_TEMPLATE/translation.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Translation Request
description: Claim a language and translate the main documentation (README, CONTRIBUTING, CODE_OF_CONDUCT).
title: "[Translation] Add <language> 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 `<lang>-<region>` 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/<iso-code>/` (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**:

```
<!-- LANGUAGE_SWITCHER:START -->
<!-- LANGUAGE_SWITCHER:END -->
```

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
20 changes: 20 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE/translation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!--
Translation PR. Please keep this checklist — reviewers use it to check the PR quickly.
If you haven't already, open a Translation Request issue first to claim your language.
-->

## 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/<iso-code>/` 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 `<!-- LANGUAGE_SWITCHER:START -->` / `<!-- LANGUAGE_SWITCHER:END -->` 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/<iso-code>/`
202 changes: 202 additions & 0 deletions .github/scripts/update-language-switcher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
#!/usr/bin/env node
'use strict';

// Regenerates the language-switcher block in README/CONTRIBUTING/CODE_OF_CONDUCT
// (and their translations/<locale>/ 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 = '<!-- LANGUAGE_SWITCHER:START -->';
const END_MARKER = '<!-- LANGUAGE_SWITCHER:END -->';

// 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();
3 changes: 3 additions & 0 deletions .github/templates/rootreadmetemplate.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

# 🎃 Hacktoberfest 2025 is Live! 🎃

<!-- LANGUAGE_SWITCHER:START -->
<!-- LANGUAGE_SWITCHER:END -->

<table role="presentation">
<tr>
<td><a href="https://github.com/ServiceNowDevProgram/code-snippets"><img src="/images/skinnymainbanner-code-snippets.png" alt="Visit the Code Snippets repository" width="427" height="250"></a></td>
Expand Down
46 changes: 46 additions & 0 deletions .github/workflows/language-switcher.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Update language switcher

# Regenerates the language-switcher block in README/CONTRIBUTING/CODE_OF_CONDUCT
# (and their translations/<locale>/ 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
3 changes: 3 additions & 0 deletions CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# 🌟 Code of Conduct

<!-- LANGUAGE_SWITCHER:START -->
<!-- LANGUAGE_SWITCHER:END -->

## 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.
Expand Down
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Contributing to this repo

<!-- LANGUAGE_SWITCHER:START -->
<!-- LANGUAGE_SWITCHER:END -->

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:
Expand Down
Loading