diff --git a/.husky/commit-msg b/.husky/commit-msg index 4974c35b..a78cc751 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,4 +1 @@ -#!/usr/bin/env sh -. "$(dirname -- "$0")/_/husky.sh" - npx commitlint --edit $1 diff --git a/.husky/pre-commit b/.husky/pre-commit index 36af2198..cbf4795a 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,2 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - npx lint-staged +npm run i18n:validate-no-empty-translations diff --git a/angular.json b/angular.json index 7dea4b22..470435a6 100644 --- a/angular.json +++ b/angular.json @@ -13,6 +13,20 @@ "root": "", "sourceRoot": "src", "prefix": "app", + "i18n": { + "sourceLocale": { + "code": "en", + "subPath": "" + }, + "locales": { + "de": { + "translation": "src/locale/messages.de.json" + }, + "fr": { + "translation": "src/locale/messages.fr.json" + } + } + }, "architect": { "build": { "builder": "@angular/build:application", @@ -20,6 +34,7 @@ "browser": "src/main.ts", "index": "src/indexDefault.html", "tsConfig": "tsconfig.app.json", + "localize": true, "assets": [ { "glob": "**/*", @@ -31,7 +46,10 @@ "outputMode": "server", "ssr": { "entry": "src/server.ts" - } + }, + "polyfills": [ + "@angular/localize/init" + ] }, "configurations": { "production": { @@ -82,7 +100,12 @@ "input": "public" } ], - "styles": ["src/styles.css"] + "styles": [ + "src/styles.css" + ], + "polyfills": [ + "@angular/localize/init" + ] } } } diff --git a/messages.json b/messages.json new file mode 100644 index 00000000..387ef7cf --- /dev/null +++ b/messages.json @@ -0,0 +1,39 @@ +{ + "locale": "en", + "translations": { + "community-card.events": "Events", + "community-card.organizers": "Organizers", + "community-card.website": "Website", + "community-card.join": " Join the community ", + "event-card.starting-from": " Starting from ", + "event-card.tba": " To be announced ", + "event-card.registration": " Registration ", + "event-card.fee": "Free", + "event-card.attendees": "Attendees", + "event-card.register-now": " Register now ", + "event-card.no-registration": " No registration available ", + "event-card.organized-by": "Organized by", + "podcast-card.listen-now": " Listen Now ", + "message.empty-recommendation": "Try adjusting your search terms", + "message.browse-all": "or browse all events above", + "navigation.slogan": "The Angular community hub for events, communities, and podcasts", + "navigation.events": "Events", + "navigation.communities": "Communities", + "navigation.podcasts": "Podcasts", + "communities-page.meta-description": "Curated list of Angular communities", + "events-page.meta-description": "Curated list of Angular Events", + "events-page.og-title": "Curated list of Angular Events", + "events-page.empty-search-message-title": "No events found", + "events-page.empty-search-message-description": "We could not find any events matching your search criteria. Try different keywords or browse all available events.", + "events-page.empty-events-message-title": "No upcoming events", + "events-page.empty-events-message-description": "There are currently no upcoming Angular events scheduled. New events are added regularly, so please check back soon!", + "events-page.json-ld.description": "Curated list of Angular Communities, Events, Podcasts, and Call for Papers.", + "events-page.json-ld.audience-description": "Developers interested in Angular and related technologies.", + "events-page.json-ld.main-entity.name": "Angular Events", + "events-page.json-ld.main-entity.item.description": "Developers interested in Angular and related technologies.", + "events-page.json-ld.main-entity.item.online": "Online", + "not-found-page.not-found": "Page Not Found", + "not-found-page.go-home": "Go Back Home", + "podcasts-page.meta-description": "Curated list of Angular Talks" + } +} \ No newline at end of file diff --git a/package.json b/package.json index 01e40734..c5a98297 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,16 @@ "version": "0.0.0", "license": "MIT", "scripts": { + "prepare": "husky", "ng": "ng", "start": "ng serve", "build": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", - "serve:ssr:angular-hub": "node dist/angular-hub/server/server.mjs" + "serve:ssr:angular-hub": "node dist/angular-hub/server/server.mjs", + "i18n:extract": "ng extract-i18n --format json", + "i18n:merge-translations": "node scripts/merge-translations.mjs", + "i18n:validate-no-empty-translations": "npm run i18n:extract && npm run i18n:merge-translations && node scripts/validate-no-empty-translations.mjs" }, "private": true, "prettier": { @@ -56,6 +60,7 @@ "@angular/cli": "^20.0.5", "@angular/compiler-cli": "^20.0.5", "@angular/language-service": "18.2.0", + "@angular/localize": "20.0.6", "@commitlint/cli": "^19.3.0", "@commitlint/config-conventional": "^19.2.2", "husky": "^9.0.11", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d9961fc..fc7af387 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,7 +37,7 @@ importers: version: 20.0.6(@angular/common@20.0.6(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2))(@angular/platform-browser@20.0.6(@angular/animations@20.0.6(@angular/common@20.0.6(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)))(@angular/common@20.0.6(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)))(rxjs@7.8.2) '@angular/ssr': specifier: ^20.0.5 - version: 20.0.5(rk4qgahujxqvm5fipetnggqhp4) + version: 20.0.5(d988fdbf4be455d50a01cf66e2e6ac2a) '@elysiajs/static': specifier: ^1.3.0 version: 1.3.0(elysia@1.3.5(exact-mirror@0.1.2(@sinclair/typebox@0.34.37))(file-type@21.0.0)(typescript@5.8.3)) @@ -73,7 +73,7 @@ importers: version: 7.0.0 primeng: specifier: 20.0.0-rc.1 - version: 20.0.0-rc.1(f6l5qo6rumw4rqtjhm6oogse6e) + version: 20.0.0-rc.1(a053a4aebb2280960a0e00397cb9b09d) prismjs: specifier: ^1.29.0 version: 1.30.0 @@ -95,7 +95,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.5 - version: 20.0.5(oq6meoljci34syk4unyd34snra) + version: 20.0.5(067157068269d2fd3dbd812d74e62b9a) '@angular/cli': specifier: ^20.0.5 version: 20.0.5(@types/node@24.0.10)(chokidar@4.0.3) @@ -105,6 +105,9 @@ importers: '@angular/language-service': specifier: 18.2.0 version: 18.2.0 + '@angular/localize': + specifier: 20.0.6 + version: 20.0.6(@angular/compiler-cli@20.0.6(@angular/compiler@20.0.6)(typescript@5.8.3))(@angular/compiler@20.0.6) '@commitlint/cli': specifier: ^19.3.0 version: 19.8.1(@types/node@24.0.10)(typescript@5.8.3) @@ -264,6 +267,14 @@ packages: resolution: {integrity: sha512-brl5061YqfNnT7yZNMWmsgv6ve6p9+kfhX6mZWOGICcY2SYVtCNVHdqzwWTTwY7MvTVfycHxiAf9PEmc5lD4/g==} engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0} + '@angular/localize@20.0.6': + resolution: {integrity: sha512-Sn7PtTj+/rbvSitnZeF0Fn9rcASiu+9KZFbxSCMjK46K+aDF2BKDFnGN9+SVq8LBsfw5OaBKYsDQ4DWjWRzIaA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@angular/compiler': 20.0.6 + '@angular/compiler-cli': 20.0.6 + '@angular/platform-browser@20.0.6': resolution: {integrity: sha512-EZC6ILD0nXOddNuwqQqwTzvRgXs/1kZoRGzdG8zpHhRREBf6VFMZ+g7IN3EKnYN4hDL5EMxIKIsIcQjmCDsu2A==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} @@ -1313,6 +1324,18 @@ packages: resolution: {integrity: sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==} engines: {node: ^18.17.0 || >=20.5.0} + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.7': + resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + '@types/body-parser@1.19.6': resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} @@ -3087,7 +3110,7 @@ snapshots: '@angular/core': 20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2) tslib: 2.8.1 - '@angular/build@20.0.5(oq6meoljci34syk4unyd34snra)': + '@angular/build@20.0.5(067157068269d2fd3dbd812d74e62b9a)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2000.5(chokidar@4.0.3) @@ -3121,9 +3144,10 @@ snapshots: watchpack: 2.4.2 optionalDependencies: '@angular/core': 20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2) + '@angular/localize': 20.0.6(@angular/compiler-cli@20.0.6(@angular/compiler@20.0.6)(typescript@5.8.3))(@angular/compiler@20.0.6) '@angular/platform-browser': 20.0.6(@angular/animations@20.0.6(@angular/common@20.0.6(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)))(@angular/common@20.0.6(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)) '@angular/platform-server': 20.0.6(@angular/common@20.0.6(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/compiler@20.0.6)(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2))(@angular/platform-browser@20.0.6(@angular/animations@20.0.6(@angular/common@20.0.6(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)))(@angular/common@20.0.6(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)))(rxjs@7.8.2) - '@angular/ssr': 20.0.5(rk4qgahujxqvm5fipetnggqhp4) + '@angular/ssr': 20.0.5(d988fdbf4be455d50a01cf66e2e6ac2a) lmdb: 3.3.0 postcss: 8.5.6 tailwindcss: 4.1.11 @@ -3214,6 +3238,17 @@ snapshots: '@angular/language-service@18.2.0': {} + '@angular/localize@20.0.6(@angular/compiler-cli@20.0.6(@angular/compiler@20.0.6)(typescript@5.8.3))(@angular/compiler@20.0.6)': + dependencies: + '@angular/compiler': 20.0.6 + '@angular/compiler-cli': 20.0.6(@angular/compiler@20.0.6)(typescript@5.8.3) + '@babel/core': 7.27.7 + '@types/babel__core': 7.20.5 + tinyglobby: 0.2.14 + yargs: 18.0.0 + transitivePeerDependencies: + - supports-color + '@angular/platform-browser@20.0.6(@angular/animations@20.0.6(@angular/common@20.0.6(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)))(@angular/common@20.0.6(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2))': dependencies: '@angular/common': 20.0.6(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2))(rxjs@7.8.2) @@ -3240,7 +3275,7 @@ snapshots: rxjs: 7.8.2 tslib: 2.8.1 - '@angular/ssr@20.0.5(rk4qgahujxqvm5fipetnggqhp4)': + '@angular/ssr@20.0.5(d988fdbf4be455d50a01cf66e2e6ac2a)': dependencies: '@angular/common': 20.0.6(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2))(rxjs@7.8.2) '@angular/core': 20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2) @@ -4175,6 +4210,27 @@ snapshots: '@tufjs/canonical-json': 2.0.0 minimatch: 9.0.5 + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.7 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 + + '@types/babel__traverse@7.20.7': + dependencies: + '@babel/types': 7.28.0 + '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 @@ -4913,7 +4969,7 @@ snapshots: istanbul-lib-instrument@6.0.3: dependencies: - '@babel/core': 7.27.1 + '@babel/core': 7.27.7 '@babel/parser': 7.28.0 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 @@ -5436,7 +5492,7 @@ snapshots: primeicons@7.0.0: {} - primeng@20.0.0-rc.1(f6l5qo6rumw4rqtjhm6oogse6e): + primeng@20.0.0-rc.1(a053a4aebb2280960a0e00397cb9b09d): dependencies: '@angular/animations': 20.0.6(@angular/common@20.0.6(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2)) '@angular/cdk': 20.0.5(@angular/common@20.0.6(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@20.0.6(@angular/compiler@20.0.6)(rxjs@7.8.2))(rxjs@7.8.2) @@ -5852,7 +5908,7 @@ snapshots: picomatch: 4.0.2 postcss: 8.5.6 rollup: 4.40.2 - tinyglobby: 0.2.13 + tinyglobby: 0.2.14 optionalDependencies: '@types/node': 24.0.10 fsevents: 2.3.3 diff --git a/scripts/merge-translations.mjs b/scripts/merge-translations.mjs new file mode 100644 index 00000000..c6693ac0 --- /dev/null +++ b/scripts/merge-translations.mjs @@ -0,0 +1,33 @@ +import { readFile, writeFile } from "node:fs/promises"; +import messagesFile from '../messages.json' with { type: 'json' }; +import {getLocaleFilePaths} from "./translations/get-locale-files.mjs"; + +const localesFilePaths = getLocaleFilePaths(); + +const messages = Object.entries(messagesFile.translations); + +const newLocales = await Promise.all(localesFilePaths.map(async (localeFilePath) => { + const localeFile = await readFile(localeFilePath, { encoding: 'utf-8' }); + + const localeFileData = JSON.parse(localeFile); + + const updatedTranslations = messages.reduce((acc, [key, value]) => { + return { + ...acc, + [key]: key in localeFileData.translations ? localeFileData.translations[key] : "" + } + }, {}); + + return { + file: localeFilePath, + data: { + ...localeFileData, + translations: updatedTranslations + } + }; +})); + +await Promise.all(newLocales.map(async (newLocale) => { + return writeFile(newLocale.file, JSON.stringify(newLocale.data, null, "\t"), { encoding: 'utf-8' }) +})) + diff --git a/scripts/translations/get-locale-files.mjs b/scripts/translations/get-locale-files.mjs new file mode 100644 index 00000000..51c97545 --- /dev/null +++ b/scripts/translations/get-locale-files.mjs @@ -0,0 +1,3 @@ +import config from "../../angular.json" with { type: 'json' }; + + export const getLocaleFilePaths = () => Object.values(config.projects["angular-hub"].i18n.locales).map((locale) => locale.translation); diff --git a/scripts/validate-no-empty-translations.mjs b/scripts/validate-no-empty-translations.mjs new file mode 100644 index 00000000..c388eaf7 --- /dev/null +++ b/scripts/validate-no-empty-translations.mjs @@ -0,0 +1,22 @@ +import {getLocaleFilePaths} from "./translations/get-locale-files.mjs"; +import {readFile} from "node:fs/promises"; + +const localesFilePaths = getLocaleFilePaths(); + +const missingKeysPerLocale = await Promise.all(localesFilePaths.map(async (localeFilePath) => { + const localeFile = await readFile(localeFilePath, { encoding: 'utf-8' }); + + return { + localeFilePath, + missingTranslations: Object.entries(JSON.parse(localeFile).translations).filter(([_key, value]) => value === "").map(([key]) => key), + } +})); + +const hasMissingTranslations = missingKeysPerLocale.some((file) => file.missingTranslations.length); + +if (hasMissingTranslations) { + console.error('Missing translations', missingKeysPerLocale); + process.exitCode = 1; +}else { + process.exit(0); +} diff --git a/src/app/components/cards/community-card.ts b/src/app/components/cards/community-card.ts index eb00ae6b..36d2374b 100644 --- a/src/app/components/cards/community-card.ts +++ b/src/app/components/cards/community-card.ts @@ -32,7 +32,7 @@ import { Community } from '../../../models/community.model'; class="text-sm hover:underline flex items-center gap-2" > - Events + Events } @@ -44,7 +44,7 @@ import { Community } from '../../../models/community.model'; class="text-sm hover:underline flex items-center gap-2" > - Organizers + Organizers } @@ -56,7 +56,7 @@ import { Community } from '../../../models/community.model'; class="text-sm hover:underline flex items-center gap-2" > - Website + Website } @@ -121,6 +121,7 @@ import { Community } from '../../../models/community.model'; [href]="community().eventsUrl" target="_blank" class="w-full flex items-center justify-center text-sm bg-[#26A0D9] text-white p-2 rounded-lg" + i18n="@@community-card.join" > Join the community diff --git a/src/app/components/cards/event-card.ts b/src/app/components/cards/event-card.ts index 444b0e2e..8ce52fb4 100755 --- a/src/app/components/cards/event-card.ts +++ b/src/app/components/cards/event-card.ts @@ -45,7 +45,7 @@ import { CommunityEvent } from '../../../models/community-event.model'; > @if (!event().isFree) {
{{ description() }}