Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
b8dbd95
Initial plan
Copilot Feb 5, 2026
f7d5d6e
Initial plan
Copilot Feb 5, 2026
0102f65
feat: add unsubscribe.yml issue template
Copilot Feb 5, 2026
a881caa
Initial plan
Copilot Feb 5, 2026
4170680
fix: correct French accent in unsubscribe.yml descriptions
Copilot Feb 5, 2026
0b31c76
fix: add accent to désabonner in template name
Copilot Feb 5, 2026
191fe1d
Fix labeler workflow by adding issues:write permission
Copilot Feb 5, 2026
2f99ca9
Improve error message for missing franc-min package
Copilot Feb 5, 2026
4f0b165
Initial plan
Copilot Feb 5, 2026
a9ef8e9
Testing complete
Copilot Feb 5, 2026
0caf286
Restore franc-min dependency that was accidentally removed during tes…
Copilot Feb 5, 2026
bdbba51
Fix French accent issues in all files
Copilot Feb 5, 2026
d22640b
Add error handling, validation, and browser compatibility improvements
Copilot Feb 5, 2026
a1d332f
Add input validation and sanitization to workflows
Copilot Feb 5, 2026
48d5e34
Address code review feedback: extract constants and improve clarity
Copilot Feb 5, 2026
2ed9139
chore: (deps): bump openai from 6.17.0 to 6.19.0
dependabot[bot] Feb 10, 2026
51dc428
Merge 2ed91398cc0be6c3f021a1ba6da733137447ae58 into 1dbb716021492d4b6…
dependabot[bot] Feb 10, 2026
ec705b1
docs: update release timestamp and changelog
github-actions[bot] Feb 10, 2026
0d8e368
chore: (deps): bump @csstools/css-syntax-patches-for-csstree
dependabot[bot] Feb 10, 2026
a569a31
Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/csstools…
github-actions[bot] Feb 10, 2026
661ca89
docs: update release timestamp and changelog
github-actions[bot] Feb 10, 2026
127c532
chore: (deps): bump @csstools/css-calc from 3.0.0 to 3.0.1
dependabot[bot] Feb 11, 2026
2836144
docs: update release timestamp and changelog
github-actions[bot] Feb 11, 2026
caeb7f9
Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/csstools…
github-actions[bot] Feb 11, 2026
1ae1aa3
docs: update release timestamp and changelog
github-actions[bot] Feb 11, 2026
dc8fd40
chore: (deps): bump lru-cache from 11.2.5 to 11.2.6
dependabot[bot] Feb 11, 2026
be0d1a3
Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/lru-cach…
github-actions[bot] Feb 11, 2026
beac8ef
docs: update release timestamp and changelog
github-actions[bot] Feb 11, 2026
0031bd2
chore: (deps): bump @exodus/bytes from 1.12.0 to 1.14.0
dependabot[bot] Feb 12, 2026
904b6d3
Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/exodus/b…
github-actions[bot] Feb 12, 2026
d6be80f
docs: update release timestamp and changelog
github-actions[bot] Feb 12, 2026
c2b3db0
chore: (deps): bump qs from 6.14.1 to 6.14.2
dependabot[bot] Feb 12, 2026
f2964a1
Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/qs-6.14.…
github-actions[bot] Feb 12, 2026
41dbd9e
docs: update release timestamp and changelog
github-actions[bot] Feb 12, 2026
aeb735b
docs: update release timestamp and changelog
github-actions[bot] Feb 13, 2026
1181c83
docs: update release timestamp and changelog
github-actions[bot] Feb 13, 2026
524c98a
chore: (deps): bump qs in the npm_and_yarn group across 1 directory
dependabot[bot] Feb 14, 2026
66ea648
Merge remote-tracking branch 'origin/dependabot/npm_and_yarn/npm_and_…
github-actions[bot] Feb 14, 2026
1ceefa4
docs: update release timestamp and changelog
github-actions[bot] Feb 14, 2026
a2ac2e6
Fix labeler workflow action_required status (#58)
EthanThePhoenix38 Feb 14, 2026
4a3bc6f
Add missing unsubscribe.yml issue template (#59)
EthanThePhoenix38 Feb 14, 2026
57e4464
Merge branch 'claude/configurable-news-reader-G1Gdx' into copilot/sub…
EthanThePhoenix38 Feb 14, 2026
e9928c7
Improve error message for missing franc-min dependency (#60)
EthanThePhoenix38 Feb 14, 2026
f524bfc
Merge branch 'claude/configurable-news-reader-G1Gdx' into copilot/sub…
EthanThePhoenix38 Feb 14, 2026
9b86b0c
Fix French accents, add validation, error handling, and browser compa…
EthanThePhoenix38 Feb 14, 2026
597b27d
Securite (#87)
EthanThePhoenix38 Feb 14, 2026
19f23b0
rebase (#88)
EthanThePhoenix38 Feb 14, 2026
06e843e
rebase (#88) (#89)
EthanThePhoenix38 Feb 14, 2026
c132663
Claude/configurable news reader g1 gdx (#90)
EthanThePhoenix38 Feb 14, 2026
479ae60
rebase (#91)
EthanThePhoenix38 Feb 14, 2026
6ce081b
rebase (#91) (#92)
EthanThePhoenix38 Feb 14, 2026
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
6 changes: 3 additions & 3 deletions .github/ISSUE_TEMPLATE/new-source.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ body:
- type: dropdown
id: category
attributes:
label: Categorie / Category
label: Catégorie / Category
options:
- AI / IA
- Cybersecurity / Cybersecurite
- Cybersecurity / Cybersécurité
- IoT
- Windows
- Mac / Apple
- Linux
- Tech Generale
- Tech Générale
- Entrepreneuriat / Entrepreneurship
- Bourse & Finance
- Crypto & Blockchain
Expand Down
8 changes: 4 additions & 4 deletions .github/ISSUE_TEMPLATE/subscribe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ body:
- type: checkboxes
id: categories
attributes:
label: Categories souhaitees / Desired categories
label: Catégories souhaitées / Desired categories
options:
- label: AI / IA
- label: Cybersecurity
Expand All @@ -30,17 +30,17 @@ body:
- type: dropdown
id: language
attributes:
label: Langue preferee / Preferred language
label: Langue préférée / Preferred language
options:
- Francais
- Français
- English
- Les deux / Both
validations:
required: true
- type: dropdown
id: frequency
attributes:
label: Frequence / Frequency
label: Fréquence / Frequency
options:
- Chaque mise a jour (toutes les 3h) / Every update
- Quotidien / Daily digest
Expand Down
21 changes: 21 additions & 0 deletions .github/ISSUE_TEMPLATE/unsubscribe.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Se désabonner de AI-Pulse / Unsubscribe from AI-Pulse
description: Retirez votre email de notre liste de diffusion
title: "[UNSUBSCRIBE] "
labels: ["unsubscription"]
body:
- type: input
id: email
attributes:
label: Votre email / Your email
description: L'adresse email que vous souhaitez désabonner / The email address you want to unsubscribe
placeholder: "you@example.com"
validations:
required: true
- type: textarea
id: reason
attributes:
label: Raison (optionnel) / Reason (optional)
description: Dites-nous pourquoi vous vous désabonnez (optionnel) / Tell us why you're unsubscribing (optional)
placeholder: "Je ne souhaite plus recevoir les digests / I no longer wish to receive digests"
validations:
required: false
Comment on lines +1 to +21
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new unsubscribe issue template has been added, but there is no corresponding workflow to handle unsubscription requests. The manage-subscriber.yml workflow only handles the 'subscription' label. You need to either add a handler for the 'unsubscription' label in the existing workflow or create a new workflow to process unsubscribe requests and remove emails from the subscribers.json file.

Copilot uses AI. Check for mistakes.
57 changes: 49 additions & 8 deletions .github/workflows/add-source.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ jobs:
const issue = context.payload.issue;
const body = issue.body;

// Constants
const MAX_SOURCE_NAME_LENGTH = 100;
const MAX_TAG_LENGTH = 50;

// Parse form fields from issue body
function getField(label) {
const regex = new RegExp(`### ${label}\\s*\\n\\s*(.+)`, 'i');
Expand All @@ -33,24 +37,61 @@ jobs:
const sourceName = getField('Nom de la source / Source Name');
const sourceUrl = getField('URL du site / Website URL');
const rssUrl = getField('URL du flux RSS / RSS Feed URL');
const categoryRaw = getField('Categorie / Category');
const categoryRaw = getField('Catégorie / Category');
const languageRaw = getField('Langue / Language');
const tags = getField('Tags / Mots-cles');

// Validate required fields
if (!sourceName || !rssUrl) {
console.log('Missing required fields');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: 'Missing required fields (source name or RSS URL). Please provide all required information.'
});
return;
}

// Sanitize and validate source name (prevent injection)
const sanitizedSourceName = sourceName.replace(/[<>'"]/g, '').slice(0, MAX_SOURCE_NAME_LENGTH);
if (sanitizedSourceName !== sourceName) {
console.log('Source name contains invalid characters');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: 'Source name contains invalid characters. Please use only alphanumeric characters, spaces, and basic punctuation.'
});
return;
}
Comment on lines +56 to +67
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The source name sanitization check will always reject valid names that exceed MAX_SOURCE_NAME_LENGTH (100 characters) even if they contain no invalid characters. The condition if (sanitizedSourceName !== sourceName) will be true both when invalid characters are removed AND when the string is truncated. Consider separating these checks: first check for invalid characters, then check length, to provide more specific error messages to users.

Copilot uses AI. Check for mistakes.

// Validate RSS URL format
try {
const urlObj = new URL(rssUrl);
if (!['http:', 'https:'].includes(urlObj.protocol)) {
throw new Error('Invalid protocol');
}
} catch (e) {
console.log('Invalid RSS URL:', rssUrl);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `Invalid RSS URL format: "${rssUrl}". Please provide a valid HTTP or HTTPS URL.`
});
return;
}

// Map category
const catMap = {
'AI / IA': 'ai',
'Cybersecurity / Cybersecurite': 'cybersecurity',
'Cybersecurity / Cybersécurité': 'cybersecurity',
'IoT': 'iot',
'Windows': 'windows',
'Mac / Apple': 'mac',
'Linux': 'linux',
'Tech Generale': 'tech',
'Tech Générale': 'tech',
'Entrepreneuriat / Entrepreneurship': 'entrepreneurship',
'Bourse & Finance': 'finance',
'Crypto & Blockchain': 'crypto',
Expand All @@ -60,10 +101,10 @@ jobs:
const category = catMap[categoryRaw] || 'tech';

// Map language
const lang = languageRaw.includes('Francais') ? 'fr' : 'en';
const lang = languageRaw.includes('Français') ? 'fr' : 'en';

// Parse tags
const tagList = tags ? tags.split(',').map(t => t.trim()).filter(t => t) : [category];
// Parse and sanitize tags
const tagList = tags ? tags.split(',').map(t => t.trim().replace(/[<>'"]/g, '').slice(0, MAX_TAG_LENGTH)).filter(t => t) : [category];

// Read and update config.json
const config = JSON.parse(fs.readFileSync('config.json', 'utf-8'));
Expand All @@ -85,9 +126,9 @@ jobs:
return;
}

// Add new feed
// Add new feed with sanitized data
config.categories[category].feeds.push({
name: sourceName,
name: sanitizedSourceName,
url: rssUrl,
tags: tagList,
lang: lang
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/label.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ jobs:
permissions:
contents: read
pull-requests: write
issues: write

steps:
- uses: actions/labeler@v4
Expand Down
52 changes: 44 additions & 8 deletions .github/workflows/manage-subscriber.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ jobs:
const issue = context.payload.issue;
const body = issue.body;

// Constants
const MAX_AT_SYMBOLS_THRESHOLD = 5; // Maximum @ symbols allowed to prevent email harvesting

function getField(label) {
const regex = new RegExp(`### ${label}\\s*\\n\\s*(.+)`, 'i');
const match = body.match(regex);
Expand All @@ -36,15 +39,48 @@ jobs:
return [...match[1].matchAll(/- \[X\] (.+)/gi)].map(m => m[1].trim());
}

const email = getField('Votre email / Your email');
if (!email || !email.includes('@')) {
console.log('Invalid email');
// Validate email address format
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return (
typeof email === 'string' &&
email.length > 0 &&
email.length <= 254 &&
emailRegex.test(email)
);
}

// Basic abuse heuristic: ignore issues that appear to contain many email-like strings
const atCount = (body.match(/@/g) || []).length;
if (atCount > MAX_AT_SYMBOLS_THRESHOLD) {
console.log('Suspicious issue body (too many @ symbols); skipping subscription.');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: 'This subscription request appears suspicious and has been rejected. If this is an error, please contact the maintainers.'
});
return;
}

const rawEmail = getField('Votre email / Your email');
// In case multiple emails or extra text are provided, only keep the first token
const email = rawEmail.split(/[,\s]+/).filter(Boolean)[0] || '';

if (!isValidEmail(email)) {
console.log('Invalid email:', rawEmail);
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
body: `Invalid email address format: "${rawEmail}". Please provide a valid email address.`
});
return;
}

const selectedCats = getCheckboxes('Categories souhaitees / Desired categories');
const langRaw = getField('Langue preferee / Preferred language');
const freqRaw = getField('Frequence / Frequency');
const selectedCats = getCheckboxes('Catégories souhaitées / Desired categories');
const langRaw = getField('Langue préférée / Preferred language');
const freqRaw = getField('Fréquence / Frequency');

const catMap = {
'AI / IA': 'ai',
Expand All @@ -53,7 +89,7 @@ jobs:
'Windows': 'windows',
'Mac / Apple': 'mac',
'Linux': 'linux',
'Tech Generale': 'tech',
'Tech Générale': 'tech',
'Entrepreneuriat': 'entrepreneurship',
'Bourse & Finance': 'finance',
'Crypto & Blockchain': 'crypto',
Expand All @@ -62,7 +98,7 @@ jobs:
};

const categories = selectedCats.map(c => catMap[c]).filter(Boolean);
const lang = langRaw.includes('Francais') ? 'fr' : langRaw.includes('English') ? 'en' : 'all';
const lang = langRaw.includes('Français') ? 'fr' : langRaw.includes('English') ? 'en' : 'all';
const frequency = freqRaw.includes('Quotidien') ? 'daily' : 'each_update';

// Read subscribers
Expand Down
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [1.1.0] - 2026-01-31
### Added
- **2026-02-14 11:26 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-13 06:16 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-13 06:15 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-12 06:17 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-12 06:16 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-11 06:17 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-11 06:16 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-11 06:16 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-10 06:16 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-10 06:16 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-09 06:35 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-09 06:34 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-09 06:33 UTC**: Automated Security Batch Update (DependabotSecureFlow)
Expand All @@ -29,6 +39,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [1.2.0] - 2026-02-01
### Added
- **2026-02-14 11:26 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-13 06:16 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-13 06:15 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-12 06:17 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-12 06:16 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-11 06:17 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-11 06:16 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-11 06:16 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-10 06:16 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-10 06:16 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-09 06:35 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-09 06:34 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-09 06:33 UTC**: Automated Security Batch Update (DependabotSecureFlow)
Expand Down Expand Up @@ -64,6 +84,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [1.0.0] - 2025-12-10
### Added
- **2026-02-14 11:26 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-13 06:16 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-13 06:15 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-12 06:17 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-12 06:16 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-11 06:17 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-11 06:16 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-11 06:16 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-10 06:16 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-10 06:16 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-09 06:35 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-09 06:34 UTC**: Automated Security Batch Update (DependabotSecureFlow)
- **2026-02-09 06:33 UTC**: Automated Security Batch Update (DependabotSecureFlow)
Expand Down
28 changes: 14 additions & 14 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
]
},
"cybersecurity": {
"labels": { "en": "Cybersecurity", "fr": "Cybersecurite" },
"labels": { "en": "Cybersecurity", "fr": "Cybersécurité" },
"icon": "shield",
"priority": 2,
"feeds": [
Expand Down Expand Up @@ -117,7 +117,7 @@
]
},
"tech": {
"labels": { "en": "General Tech", "fr": "Tech Generale" },
"labels": { "en": "General Tech", "fr": "Tech Générale" },
"icon": "zap",
"priority": 7,
"feeds": [
Expand Down Expand Up @@ -230,33 +230,33 @@
"artificial_intelligence": { "en": "Artificial Intelligence", "fr": "Intelligence Artificielle" },
"machine_learning": { "en": "Machine Learning", "fr": "Apprentissage Automatique" },
"deep_learning": { "en": "Deep Learning", "fr": "Apprentissage Profond" },
"cybersecurity": { "en": "Cybersecurity", "fr": "Cybersecurite" },
"data_breach": { "en": "Data Breach", "fr": "Fuite de Donnees" },
"privacy": { "en": "Privacy", "fr": "Vie Privee" },
"cybersecurity": { "en": "Cybersecurity", "fr": "Cybersécurité" },
"data_breach": { "en": "Data Breach", "fr": "Fuite de Données" },
"privacy": { "en": "Privacy", "fr": "Vie Privée" },
"home_automation": { "en": "Home Automation", "fr": "Domotique" },
"connected_objects": { "en": "Connected Objects", "fr": "Objets Connectes" },
"connected_objects": { "en": "Connected Objects", "fr": "Objets Connectés" },
"open_source": { "en": "Open Source", "fr": "Logiciel Libre" },
"update": { "en": "Update", "fr": "Mise a jour" },
"vulnerability": { "en": "Vulnerability", "fr": "Vulnerabilite" },
"neural_network": { "en": "Neural Network", "fr": "Reseau de Neurones" },
"update": { "en": "Update", "fr": "Mise à jour" },
"vulnerability": { "en": "Vulnerability", "fr": "Vulnérabilité" },
"neural_network": { "en": "Neural Network", "fr": "Réseau de Neurones" },
"computer_vision": { "en": "Computer Vision", "fr": "Vision par Ordinateur" },
"robotics": { "en": "Robotics", "fr": "Robotique" },
"quantum_computing": { "en": "Quantum Computing", "fr": "Informatique Quantique" },
"stock_market": { "en": "Stock Market", "fr": "Bourse" },
"cryptocurrency": { "en": "Cryptocurrency", "fr": "Cryptomonnaie" },
"fundraising": { "en": "Fundraising", "fr": "Levee de Fonds" },
"fundraising": { "en": "Fundraising", "fr": "Levée de Fonds" },
"entrepreneurship": { "en": "Entrepreneurship", "fr": "Entrepreneuriat" },
"startup": { "en": "Startup", "fr": "Startup" },
"investment": { "en": "Investment", "fr": "Investissement" },
"bug_bounty": { "en": "Bug Bounty", "fr": "Bug Bounty" },
"exploit": { "en": "Exploit", "fr": "Exploit" },
"regulation": { "en": "Regulation", "fr": "Regulation" },
"regulation": { "en": "Regulation", "fr": "Réglementation" },
"new_product": { "en": "New Product", "fr": "Nouveau Produit" },
"launch": { "en": "Launch", "fr": "Lancement" },
"crowdfunding": { "en": "Crowdfunding", "fr": "Financement Participatif" },
"repository": { "en": "Repository", "fr": "Depot" },
"self_hosted": { "en": "Self-hosted", "fr": "Auto-heberge" },
"library": { "en": "Library", "fr": "Bibliotheque" },
"repository": { "en": "Repository", "fr": "Dépôt" },
"self_hosted": { "en": "Self-hosted", "fr": "Auto-hébergé" },
"library": { "en": "Library", "fr": "Bibliothèque" },
"tool": { "en": "Tool", "fr": "Outil" }
}
}
Expand Down
Loading
Loading