diff --git a/package-lock.json b/package-lock.json index 9cfbbaf..994129d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@angular/platform-browser": "^21.2.7", "@angular/platform-browser-dynamic": "^21.2.7", "@angular/router": "^21.2.7", + "@jsverse/transloco": "^8.3.0", "@ngneat/until-destroy": "^10.0.0", "@sentry/angular": "^10.50.0", "@sentry/cli": "^2.58.5", @@ -692,7 +693,6 @@ "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", @@ -887,7 +887,6 @@ "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -2099,6 +2098,40 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsverse/transloco": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@jsverse/transloco/-/transloco-8.3.0.tgz", + "integrity": "sha512-p69/MhhAsTabDAffaiMALx4u9AwAqx24CjdECaCkUKSpCGbiYQUJPCsV4OsWjaSOxDNm9HuIX6NmqbfNZ0S3KA==", + "license": "MIT", + "dependencies": { + "@jsverse/transloco-utils": "^8.2.1", + "@jsverse/utils": "1.0.0-beta.5", + "tslib": "^2.2.0" + }, + "peerDependencies": { + "@angular/core": ">=16.0.0", + "rxjs": ">=6.0.0" + } + }, + "node_modules/@jsverse/transloco-utils": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@jsverse/transloco-utils/-/transloco-utils-8.3.0.tgz", + "integrity": "sha512-HZPDKadKiL3l4iZ51PF7gv7txBgrR7gQB6crdfLSrXsCvCTGDnnhd3y5qO02pBWbWMDKRXKU0clv2dd3jeTSFA==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.1.3", + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@jsverse/utils": { + "version": "1.0.0-beta.5", + "resolved": "https://registry.npmjs.org/@jsverse/utils/-/utils-1.0.0-beta.5.tgz", + "integrity": "sha512-z7IdlV6BdSeF3Veii8Yyk64KuyTjNIQnFaW5PAhmDx0wN29lB2BFp8WO6+tJPLPjtlz2yKeNrjkp1XqnMPaeHA==", + "license": "MIT" + }, "node_modules/@listr2/prompt-adapter-inquirer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz", @@ -4637,6 +4670,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -4860,6 +4899,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001788", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", @@ -5140,6 +5188,32 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -5432,6 +5506,15 @@ "dev": true, "license": "MIT" }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -6107,6 +6190,22 @@ "dev": true, "license": "MIT" }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -6144,6 +6243,12 @@ "node": ">= 0.10" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -6279,9 +6384,20 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/jsdom": { "version": "28.1.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", @@ -6390,6 +6506,12 @@ ], "license": "MIT" }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/listr2": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", @@ -7342,6 +7464,42 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/parse5": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", @@ -7467,6 +7625,15 @@ "url": "https://opencollective.com/express" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -7478,7 +7645,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -7732,6 +7898,15 @@ "node": ">=0.10.0" } }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -8525,7 +8700,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 55ab50a..29419ac 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@angular/platform-browser": "^21.2.7", "@angular/platform-browser-dynamic": "^21.2.7", "@angular/router": "^21.2.7", + "@jsverse/transloco": "^8.3.0", "@ngneat/until-destroy": "^10.0.0", "@sentry/angular": "^10.50.0", "@sentry/cli": "^2.58.5", diff --git a/public/i18n/en_GB.json b/public/i18n/en_GB.json new file mode 100644 index 0000000..7275766 --- /dev/null +++ b/public/i18n/en_GB.json @@ -0,0 +1,43 @@ +{ + "about": { + "paragraph1": "Have you thought about maybe joining the 1st of May parade and wondering when it starts?", + "paragraph2": "But when you search on the internet you find either no references or just old programs for previous years?", + "paragraph3": "With this website you never have to doubt anymore and can always find the current year's program directly.", + "back_button": "Back" + }, + "footer": { + "about": "About this website", + "contributing": "Contributing updates?", + "source_code": "Source code" + }, + "kommune": { + "contribute": "Can you contribute?", + "missing": "Unfortunately, information for this municipality is missing.", + "program": "Program" + }, + "language_selector": { + "select_language": "Select language" + }, + "main": { + "title": "Find the 1st of May program for your area" + }, + "menu_service": { + "select_year": "Select year", + "select_fylke": "Select county", + "select_kommune": "Select municipality" + }, + "updates": { + "example": "Example", + "email_line1": "Subject: 1st of May program link for Minby", + "email_line2": "Description: Minby municipality's program", + "email_line3": "Best regards", + "paragraph1a": "You can send email with updates to", + "paragraph1b": "which contains", + "paragraph1c": "as well as a very short description/text for the link.", + "paragraph2": "To filter out spam, make sure to include the word \"skomakermelk\" somewhere in the text, all emails that do not do this will be automatically deleted.", + "paragraph3": "NB, do not send in links to Facebook or X (previous Twitter). They will under absolutely no circumstances be used!", + "paragraph4a": "If you send in multiple links at once, you can best use", + "paragraph4b": "either as text in the email or as an attached file (remember to still include \"skomakermelk\" in the email text).", + "back_button": "Back" + } +} diff --git a/public/i18n/en_GB.json.license b/public/i18n/en_GB.json.license new file mode 100644 index 0000000..667f787 --- /dev/null +++ b/public/i18n/en_GB.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2026 Håkon Løvdal + +SPDX-License-Identifier: GPL-3.0-or-later diff --git a/public/i18n/no_NB.json b/public/i18n/no_NB.json new file mode 100644 index 0000000..f1e5a4f --- /dev/null +++ b/public/i18n/no_NB.json @@ -0,0 +1,43 @@ +{ + "about": { + "paragraph1": "Har du tenkt å kanskje bli med å gå i 1. maitog og så lurer du på når det starter?", + "paragraph2": "Men når du søker på intenett så finner du enten ingen referanser eller så er det bare gamle program for tidligere år?", + "paragraph3": "Med denne websiden trenger du aldri å være i tvil lenger og kan alltid finne årets program direkte.", + "back_button": "Tilbake" + }, + "footer": { + "about": "Om denne siden", + "contributing": "Bidra med oppdateringer?", + "source_code": "Kildekode" + }, + "kommune": { + "contribute": "Kan du bidra?", + "missing": "Dessverre mangler informasjon for denne kommunen.", + "program": "Program" + }, + "language_selector": { + "select_language": "Velg språk" + }, + "main": { + "title": "Finn 1. mai program for der du bor" + }, + "menu_service": { + "select_year": "Velg år", + "select_fylke": "Velg fylke", + "select_kommune": "Velg kommune" + }, + "updates": { + "example": "Eksempel", + "email_line1": "Subject/emne: 1. mai link til Minby", + "email_line2": "Beskrivelse: Minby kommunes program", + "email_line3": "Hilsen", + "paragraph1a": "Du kan sende epost med oppdateringer til", + "paragraph1b": "som inneholder", + "paragraph1c": "samt en veldig kort beskrivelse/tekst til linken.", + "paragraph2": "For å filtere vekk spam, pass på å ha med ordet \"skomakermelk\" en eller annen plass i teksten, alle eposter som ikke gjør det blir automatisk slettet.", + "paragraph3": "NB, ikke send inn linker til noe på Facebook eller X (tidligere Twitter). De vil under absolutt ingen omstendigheter bli brukt!", + "paragraph4a": "Hvis du sender inn flere på en gang kan du helst bruke", + "paragraph4b": "enten som tekst i eposten eller som en vedlagt fil (husk å fremdeles få med \"skomakermelk\" i epostteksten).", + "back_button": "Tilbake" + } +} diff --git a/public/i18n/no_NB.json.license b/public/i18n/no_NB.json.license new file mode 100644 index 0000000..667f787 --- /dev/null +++ b/public/i18n/no_NB.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2026 Håkon Løvdal + +SPDX-License-Identifier: GPL-3.0-or-later diff --git a/public/i18n/no_NN.json b/public/i18n/no_NN.json new file mode 100644 index 0000000..3c2b60c --- /dev/null +++ b/public/i18n/no_NN.json @@ -0,0 +1,43 @@ +{ + "about": { + "paragraph1": "Har du tenkt å kanskje bli med å gå i 1. maitog og så lurar du på når det startar?", + "paragraph2": "Men når du søkar på intenett så finnar du anten ingen referanser eller så er det berre gamle program for tidlegare år?", + "paragraph3": "Med denne websida treng du aldri å være i tvil lenger og kan alltid finne årets program direkte.", + "back_button": "Tilbake" + }, + "footer": { + "about": "Om denne sida", + "contributing": "Hjelpe til med oppdateringar?", + "source_code": "Kjeldekode" + }, + "kommune": { + "contribute": "Kan du bidra?", + "missing": "Dessverre manglar informasjon for denne kommunen.", + "program": "Program" + }, + "language_selector": { + "select_language": "Velj språk" + }, + "main": { + "title": "Finn 1. mai program for der du bur" + }, + "menu_service": { + "select_year": "Velj år", + "select_fylke": "Velj fylke", + "select_kommune": "Velj kommune" + }, + "updates": { + "example": "Eksempel", + "email_line1": "Subject/emne: 1. mai link til Minby", + "email_line2": "Tittel: Minby kommunes program", + "email_line3": "Helsing", + "paragraph1a": "Du kan senda epost med oppdateringar til", + "paragraph1b": "som inneheld", + "paragraph1c": "samt ein veldig kort tittel til lenkja.", + "paragraph2": "For å filtere vekk spam, pass på å ha med ordet \"skomakermelk\" ein eller annan plass i teksta, alle epostar som ikkje gjer det vert sletta automatisk.", + "paragraph3": "NB, ikkje send inn lenkjar til noko på Facebook eller X (tidlegere Twitter). Dei vil under absolutt inga høve verte brukt!", + "paragraph4a": "Hvis du sendar inn fleire på ei gang kan du helst bruke", + "paragraph4b": "anten som tekst i eposten eller som ei vedlagt fil (husk å framleis få med \"skomakermelk\" i epostteksta).", + "back_button": "Tilbake" + } +} diff --git a/public/i18n/no_NN.json.license b/public/i18n/no_NN.json.license new file mode 100644 index 0000000..667f787 --- /dev/null +++ b/public/i18n/no_NN.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2026 Håkon Løvdal + +SPDX-License-Identifier: GPL-3.0-or-later diff --git a/public/i18n/no_SE.json b/public/i18n/no_SE.json new file mode 100644 index 0000000..369a3d3 --- /dev/null +++ b/public/i18n/no_SE.json @@ -0,0 +1,43 @@ +{ + "about": { + "paragraph1": "Har du tenkt å kanskje bli med å gå i 1. maitog og så lurer du på når det starter?", + "paragraph2": "Men når du søker på intenett så finner du enten ingen referanser eller så er det bare gamle program for tidligere år?", + "paragraph3": "Med denne websiden trenger du aldri å være i tvil lenger og kan alltid finne årets program direkte.", + "back_button": "Tilbake" + }, + "footer": { + "about": "Om denne siden", + "contributing": "Bidra med oppdateringer?", + "source_code": "Kildekode" + }, + "kommune": { + "contribute": "Kan du bidra?", + "missing": "Dessverre manglar informasjon for denne kommunen.", + "program": "Program" + }, + "language_selector": { + "select_language": "Velg språk" + }, + "main": { + "title": "Finn 1. mai program for der du bor" + }, + "menu_service": { + "select_year": "Velg år", + "select_fylke": "Velg fylke", + "select_kommune": "Velg kommune" + }, + "updates": { + "example": "Eksempel", + "email_line1": "Subject/emne: 1. mai link til Minby", + "email_line2": "Beskrivelse: Minby kommunes program", + "email_line3": "Hilsen", + "paragraph1a": "Du kan sende epost med oppdateringer til", + "paragraph1b": "som inneholder", + "paragraph1c": "samt en veldig kort beskrivelse/tekst til linken.", + "paragraph2": "For å filtere vekk spam, pass på å ha med ordet \"skomakermelk\" en eller annen plass i teksten, alle eposter som ikke gjør det blir automatisk slettet.", + "paragraph3": "NB, ikke send inn linker til noe på Facebook eller X (tidligere Twitter). De vil under absolutt ingen omstendigheter bli brukt!", + "paragraph4a": "Hvis du sender inn flere på en gang kan du helst bruke", + "paragraph4b": "enten som tekst i eposten eller som en vedlagt fil (husk å fremdeles få med \"skomakermelk\" i epostteksten).", + "back_button": "Tilbake" + } +} diff --git a/public/i18n/no_SE.json.license b/public/i18n/no_SE.json.license new file mode 100644 index 0000000..667f787 --- /dev/null +++ b/public/i18n/no_SE.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2026 Håkon Løvdal + +SPDX-License-Identifier: GPL-3.0-or-later diff --git a/src/app/about/about.component.html b/src/app/about/about.component.html index e144772..84b5a0e 100644 --- a/src/app/about/about.component.html +++ b/src/app/about/about.component.html @@ -4,18 +4,11 @@ SPDX-License-Identifier: GPL-3.0-or-later --> +
-

- Har du tenkt å kanskje bli med å gå i 1. maitog og så lurer du på når det - starter? -

-

- Men når du søker på intenett så finner du enten ingen referanser - eller så er det bare gamle program for tidligere år? -

-

- Med denne websiden trenger du aldri å være i tvil lenger og kan alltid - finne årets program direkte. -

- +

{{ t('paragraph1') }}

+

{{ t('paragraph2') }}

+

{{ t('paragraph3') }}

+
+
diff --git a/src/app/about/about.component.spec.ts b/src/app/about/about.component.spec.ts index e2dede3..6be5544 100644 --- a/src/app/about/about.component.spec.ts +++ b/src/app/about/about.component.spec.ts @@ -8,6 +8,7 @@ import { createSpyFromClass } from "@copy/vitest-auto-spies/create-spy-from-clas import { AboutComponent } from "./about.component"; import { MenuService } from "../menu.service"; +import { getTranslocoModule } from "../transloco-testing.module"; describe("AboutComponent", () => { let component: AboutComponent; @@ -19,6 +20,7 @@ describe("AboutComponent", () => { await TestBed.configureTestingModule({ imports: [ AboutComponent, + getTranslocoModule({}), ], providers: [ { provide: MenuService, useValue: menuServiceSpy }, diff --git a/src/app/about/about.component.ts b/src/app/about/about.component.ts index 45b96dd..1e54ab1 100644 --- a/src/app/about/about.component.ts +++ b/src/app/about/about.component.ts @@ -4,13 +4,14 @@ import { Component } from "@angular/core"; import { NgxBackButtonDirective } from "ngx-back-button"; +import { TranslocoModule } from "@jsverse/transloco"; import { MenuService } from "../menu.service"; - @Component({ selector: "app-about", imports: [ NgxBackButtonDirective, + TranslocoModule, ], templateUrl: "./about.component.html", styleUrl: "./about.component.scss", diff --git a/src/app/app.component.html b/src/app/app.component.html index 10a2eae..3992fe7 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -4,8 +4,9 @@ SPDX-License-Identifier: GPL-3.0-or-later --> +
-

Finn 1. mai program for der du bor

+

{{ t('title') }}

@for (menuItem of menuItems; track menuItem) { @@ -18,13 +19,17 @@

Finn 1. mai program for der du bor

+

- + - + + + diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index c3c5cdc..6cea196 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -8,6 +8,7 @@ import { createSpyFromClass, Spy } from "@copy/vitest-auto-spies"; import { AppComponent } from "./app.component"; import { MenuService } from "./menu.service"; +import { getTranslocoModule } from "./transloco-testing.module"; describe("AppComponent", () => { let activatedRouteSpy: Spy; @@ -20,6 +21,7 @@ describe("AppComponent", () => { await TestBed.configureTestingModule({ imports: [ AppComponent, + getTranslocoModule({}), ], providers: [ { provide: MenuService, useValue: menuServiceSpy }, diff --git a/src/app/app.component.ts b/src/app/app.component.ts index c52c89a..1c78196 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -5,14 +5,18 @@ import { Component } from "@angular/core"; import { RouterLink, RouterOutlet } from "@angular/router"; +import { TranslocoModule, TranslocoService } from "@jsverse/transloco"; import { MenuItem, MenuService } from "./menu.service"; +import { LanguageSelectorComponent } from "./selector/language-selector/language-selector.component"; @Component({ selector: "app-root", imports: [ RouterOutlet, RouterLink, + LanguageSelectorComponent, + TranslocoModule, ], templateUrl: "./app.component.html", styleUrl: "./app.component.scss", @@ -22,6 +26,7 @@ export class AppComponent { constructor( menuService: MenuService, + private translocoService: TranslocoService, ) { this.menuItems = menuService.getMenuItems(); } diff --git a/src/app/app.config.ts b/src/app/app.config.ts index c9a1b4a..967513c 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -4,17 +4,32 @@ import { ApplicationConfig, + isDevMode, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection, } from "@angular/core"; import { provideRouter } from "@angular/router"; +import { provideHttpClient } from "@angular/common/http"; +import { provideTransloco } from "@jsverse/transloco"; import { routes } from "./app.routes"; +import { TranslocoHttpLoader } from "./transloco-loader"; +import { availableLangs, defaultLang } from "./available-langs"; export const appProviders = [ provideBrowserGlobalErrorListeners(), provideZonelessChangeDetection(), provideRouter(routes), + provideHttpClient(), + provideTransloco({ + config: { + availableLangs: availableLangs, + defaultLang: defaultLang, + reRenderOnLangChange: true, + prodMode: !isDevMode(), + }, + loader: TranslocoHttpLoader, + }), ]; export const appConfig: ApplicationConfig = { diff --git a/src/app/available-langs.ts b/src/app/available-langs.ts new file mode 100644 index 0000000..8190c16 --- /dev/null +++ b/src/app/available-langs.ts @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2026 Håkon Løvdal +// +// SPDX-License-Identifier: GPL-3.0-or-later + +export type SupportedLanuages = "no_NB" | "no_NN" | "no_SE" | "en_GB"; + +export interface Language { + iso639_1code: SupportedLanuages; + displayName: string; + flagEmoji: string; // Possible source: https://emojipedia.org/flags +} + +export const defaultLang = "no_NB"; + +export const languages: Language[] = [ + { iso639_1code: "no_NB", displayName: "Norsk, bokmål", flagEmoji: "🇳🇴" }, + { iso639_1code: "no_NN", displayName: "Norsk, nynorsk", flagEmoji: "🇳🇴" }, + { iso639_1code: "no_SE", displayName: "Davvisámegiella (Nordsamisk)", flagEmoji: "🇳🇴" }, + { iso639_1code: "en_GB", displayName: "English", flagEmoji: "🇬🇧" }, +]; + +export const availableLangs = languages.map((lang) => lang.iso639_1code); diff --git a/src/app/kommune/kommune/kommune.component.html b/src/app/kommune/kommune/kommune.component.html index 52fc0df..6b8554b 100644 --- a/src/app/kommune/kommune/kommune.component.html +++ b/src/app/kommune/kommune/kommune.component.html @@ -4,20 +4,20 @@ SPDX-License-Identifier: GPL-3.0-or-later --> +
@if (loading$ | async) { } @else { @if(noLinks$ | async) { -
- Desverre mangler informasjon for denne kommunen. - Kan du bidra? +
{{ t('missing') }}{{ t('contribute') }}
} }
+ diff --git a/src/app/kommune/kommune/kommune.component.spec.ts b/src/app/kommune/kommune/kommune.component.spec.ts index 954bc3c..4972850 100644 --- a/src/app/kommune/kommune/kommune.component.spec.ts +++ b/src/app/kommune/kommune/kommune.component.spec.ts @@ -14,6 +14,7 @@ import { createSpyFromClass, Spy } from "@copy/vitest-auto-spies"; import { KommuneComponent } from "./kommune.component"; import { MenuItem, MenuService } from "../../menu.service"; import { appProviders } from "../../app.config"; +import { getTranslocoModule } from "../../transloco-testing.module"; describe("KommuneComponent", () => { let component: KommuneComponent; @@ -27,6 +28,7 @@ describe("KommuneComponent", () => { await TestBed.configureTestingModule({ imports: [ KommuneComponent, + getTranslocoModule({}), ], providers: [ ...appProviders, diff --git a/src/app/kommune/kommune/kommune.component.ts b/src/app/kommune/kommune/kommune.component.ts index 53d80e3..96653f3 100644 --- a/src/app/kommune/kommune/kommune.component.ts +++ b/src/app/kommune/kommune/kommune.component.ts @@ -20,6 +20,7 @@ import { switchMap, } from "rxjs"; import { UntilDestroy, untilDestroyed } from "@ngneat/until-destroy"; +import { TranslocoModule } from "@jsverse/transloco"; import { MenuItem, MenuService } from "../../menu.service"; import { @@ -36,6 +37,7 @@ import { SpinnerComponent } from "../../shared/spinner/spinner.component"; CommonModule, SpinnerComponent, RouterLink, + TranslocoModule, ], templateUrl: "./kommune.component.html", styleUrl: "./kommune.component.scss", diff --git a/src/app/menu.service.ts b/src/app/menu.service.ts index f417e1b..9db420e 100644 --- a/src/app/menu.service.ts +++ b/src/app/menu.service.ts @@ -9,6 +9,7 @@ import { map, } from "rxjs"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; +import { TranslocoService } from "@jsverse/transloco"; import { yearSelectorRoutePath } from "./app.routes.constants"; @@ -49,7 +50,20 @@ export class MenuService { constructor( private router: Router, + private translocoService: TranslocoService ) { + this.translocoService.selectTranslate("menu_service.select_year") + .pipe(takeUntilDestroyed()) + .subscribe(value => this.yearSelectorMenuItem.textSignal.set(value)); + + this.translocoService.selectTranslate("menu_service.select_fylke") + .pipe(takeUntilDestroyed()) + .subscribe(value => this.fylkeSelectorMenuItem.textSignal.set(value)); + + this.translocoService.selectTranslate("menu_service.select_kommune") + .pipe(takeUntilDestroyed()) + .subscribe(value => this.kommuneSelectorMenuItem.textSignal.set(value)); + this.yearSelectorMenuItem.child = this.fylkeSelectorMenuItem; this.router.events diff --git a/src/app/oppdatering/oppdatering.component.html b/src/app/oppdatering/oppdatering.component.html index d722f0b..7ee5334 100644 --- a/src/app/oppdatering/oppdatering.component.html +++ b/src/app/oppdatering/oppdatering.component.html @@ -4,42 +4,26 @@ SPDX-License-Identifier: GPL-3.0-or-later --> +
-

- Du kan sende epost med oppdateringer til +

{{ t('paragraph1a') }} - oppdatering.2026@1maiprogram.no - som inneholder fylke, kommune, url samt en veldig - kort beskrivelse/tekst til linken. -

- Eksempel: + oppdatering.2026@1maiprogram.no{{ t('paragraph1b') }} fylke, kommune, url {{ t('paragraph1c') }}

+ {{ t('example') }}:
-Subject/emne: 1. mai link til Minby
+{{ t('email_line1') }}
 
 Fylke: Mittfylke
 Kommune: Minby
 Url: https://minby.kommune.no/1mai/2026
-Beskrivelse: Minby kommunes program
+{{ t('email_line2') }}
 
 skomakermelk
 
-Hilsen ...
-

- - For å filtere vekk spam, pass på å ha med ordet "skomakermelk" en - eller annen plass i teksten, alle eposter som ikke gjør det blir - automatisk slettet. - -

-

- NB, ikke send inn linker til noe på Facebook eller X (tidligere Twitter). - De vil under absolutt ingen omstendigheter bli brukt! -

-

- Hvis du sender inn flere på en gang kan du helst bruke - CSV format, enten som - tekst i eposten eller som en vedlagt fil (husk å fremdeles få med - "skomakermelk" i epostteksten). +{{ t('email_line3') }} ... +

{{ t('paragraph2') }}

+

{{ t('paragraph3') }}

+

{{ t('paragraph4a') }} CSV format, {{ t('paragraph4b') }}

fylke,kommune,link,description
 Innlandet,Sel,https://www.lo.no/hvem-vi-er/regioner/innlandet/1.-mai-2025/1.-mai-i-innlandet/1.-mai-pa-otta,"LO program, Otta"
@@ -48,5 +32,6 @@
 Innlandet,Hamar,https://www.lo.no/hvem-vi-er/regioner/innlandet/1.-mai-2025/1.-mai-i-innlandet/1.-mai-i-hamar,"LO program, Hamar"
 Telemark,Porsgrunn,https://www.nito.no/kurs-og-arrangementer/politikk-og-samfunn/bli-med-pa-1.-mai-markering/,"NITO program, Porsgrunn"
 ...
- +
+
diff --git a/src/app/oppdatering/oppdatering.component.spec.ts b/src/app/oppdatering/oppdatering.component.spec.ts index 759b9a9..9397068 100644 --- a/src/app/oppdatering/oppdatering.component.spec.ts +++ b/src/app/oppdatering/oppdatering.component.spec.ts @@ -8,6 +8,7 @@ import { createSpyFromClass } from "@copy/vitest-auto-spies/create-spy-from-clas import { OppdateringComponent } from "./oppdatering.component"; import { MenuService } from "../menu.service"; +import { getTranslocoModule } from "../transloco-testing.module"; describe("OppdateringComponent", () => { let component: OppdateringComponent; @@ -19,6 +20,8 @@ describe("OppdateringComponent", () => { await TestBed.configureTestingModule({ imports: [ OppdateringComponent, + getTranslocoModule({}), + ], providers: [ { provide: MenuService, useValue: menuServiceSpy }, diff --git a/src/app/oppdatering/oppdatering.component.ts b/src/app/oppdatering/oppdatering.component.ts index 643c904..431e1ec 100644 --- a/src/app/oppdatering/oppdatering.component.ts +++ b/src/app/oppdatering/oppdatering.component.ts @@ -4,6 +4,7 @@ import { Component } from "@angular/core"; import { NgxBackButtonDirective } from "ngx-back-button" +import { TranslocoModule } from "@jsverse/transloco"; import { MenuService } from "../menu.service"; @@ -11,6 +12,7 @@ import { MenuService } from "../menu.service"; selector: "app-oppdatering", imports: [ NgxBackButtonDirective, + TranslocoModule, ], templateUrl: "./oppdatering.component.html", styleUrl: "./oppdatering.component.scss", diff --git a/src/app/selector/language-selector/language-selector.component.html b/src/app/selector/language-selector/language-selector.component.html new file mode 100644 index 0000000..ef15e55 --- /dev/null +++ b/src/app/selector/language-selector/language-selector.component.html @@ -0,0 +1,22 @@ + + + + + {{ t('select_language') }}: + + + diff --git a/src/app/selector/language-selector/language-selector.component.scss b/src/app/selector/language-selector/language-selector.component.scss new file mode 100644 index 0000000..1fe7606 --- /dev/null +++ b/src/app/selector/language-selector/language-selector.component.scss @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2026 Håkon Løvdal + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +.content { + background-color: white; + padding: 0.3em; +} diff --git a/src/app/selector/language-selector/language-selector.component.spec.ts b/src/app/selector/language-selector/language-selector.component.spec.ts new file mode 100644 index 0000000..3c32931 --- /dev/null +++ b/src/app/selector/language-selector/language-selector.component.spec.ts @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2026 Håkon Løvdal +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import { ComponentFixture, TestBed } from "@angular/core/testing"; + +import { LanguageSelectorComponent } from "./language-selector.component"; +import { getTranslocoModule } from "../../transloco-testing.module"; + +describe("LanguageSelectorComponent", () => { + let component: LanguageSelectorComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + LanguageSelectorComponent, + getTranslocoModule({}), + ], + providers: [ + ], + }).compileComponents(); + + fixture = TestBed.createComponent(LanguageSelectorComponent); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/selector/language-selector/language-selector.component.ts b/src/app/selector/language-selector/language-selector.component.ts new file mode 100644 index 0000000..f2e5a61 --- /dev/null +++ b/src/app/selector/language-selector/language-selector.component.ts @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2026 Håkon Løvdal +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import { Component, OnInit } from "@angular/core"; +import { TranslocoModule, TranslocoService } from "@jsverse/transloco"; + +import { availableLangs, defaultLang, languages, SupportedLanuages } from "../../available-langs"; +import { isEventTarget } from "@shared/utils"; + +const LANG_KEY = 'language'; + +@Component({ + selector: "app-language-selector", + imports: [ + TranslocoModule, + ], + templateUrl: "./language-selector.component.html", + styleUrl: "./language-selector.component.scss", +}) +export class LanguageSelectorComponent implements OnInit { + selectedLanguage: string = defaultLang; + languages = languages; + + constructor( + private translocoService: TranslocoService, + ) { + const savedLanguage = localStorage.getItem(LANG_KEY); + if (savedLanguage) { + if (availableLangs.includes(savedLanguage as SupportedLanuages)) { + this.selectedLanguage = savedLanguage; + } else { + console.warn(`Saved language ${savedLanguage} is not in available languages, falling back to default language ${defaultLang}`); + } + } + } + + ngOnInit() { + this.translocoService.setActiveLang(this.selectedLanguage); + } + + onLanguageChange(e: Event) { + if (!isEventTarget(e.target, HTMLSelectElement)) { + throw new Error(`Expected e.target to be HTMLSelectElement, e was ${e?.constructor?.name ?? e}`); + } + this.selectedLanguage = e.target.value; + localStorage.setItem(LANG_KEY, this.selectedLanguage); + this.translocoService.setActiveLang(this.selectedLanguage); + } +} diff --git a/src/app/transloco-loader.ts b/src/app/transloco-loader.ts new file mode 100644 index 0000000..9db3ac8 --- /dev/null +++ b/src/app/transloco-loader.ts @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2026 Håkon Løvdal +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import { inject, Injectable } from "@angular/core"; +import { Translation, TranslocoLoader } from "@jsverse/transloco"; +import { HttpClient } from "@angular/common/http"; + +@Injectable({ providedIn: "root" }) +export class TranslocoHttpLoader implements TranslocoLoader { + private http = inject(HttpClient); + + getTranslation(lang: string) { + return this.http.get(`/i18n/${lang}.json`); + } +} diff --git a/src/app/transloco-testing.module.ts b/src/app/transloco-testing.module.ts new file mode 100644 index 0000000..9d0f370 --- /dev/null +++ b/src/app/transloco-testing.module.ts @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2026 Håkon Løvdal +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import { TranslocoTestingModule, TranslocoTestingOptions } from "@jsverse/transloco"; + +import { availableLangs, defaultLang, SupportedLanuages } from "./available-langs"; + +import no_NB from '@public/i18n/no_NB.json'; +import no_NN from '@public/i18n/no_NN.json'; +import no_SE from '@public/i18n/no_SE.json'; +import en_GB from '@public/i18n/en_GB.json'; + +type LangsObjType = { [key in SupportedLanuages]: any }; +const LangsObj: LangsObjType = { + no_NB: no_NB, + no_NN: no_NN, + no_SE: no_SE, + en_GB: en_GB, +}; + +export function getTranslocoModule(options: TranslocoTestingOptions = {}) { + const { langs, translocoConfig, ...rest } = options; + return TranslocoTestingModule.forRoot({ + langs: { + ...LangsObj, + ...langs, + }, + translocoConfig: { + availableLangs: availableLangs, + defaultLang: defaultLang, + ...translocoConfig, + }, + ...rest, + }); +} diff --git a/src/shared/utils.ts b/src/shared/utils.ts index a2eac64..0b4398d 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -10,3 +10,10 @@ const collator = new Intl.Collator("no", { export function nameSortFunction(a: T, b: T) { return collator.compare(a.name, b.name); } + +export function isEventTarget( + target: EventTarget | null, + ctor: new (...args: any[]) => T, +): target is T { + return target instanceof ctor; +} diff --git a/transloco.config.ts b/transloco.config.ts new file mode 100644 index 0000000..de97cef --- /dev/null +++ b/transloco.config.ts @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2026 Håkon Løvdal +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import { TranslocoGlobalConfig } from "@jsverse/transloco-utils"; + +import { availableLangs } from "./src/app/available-langs"; + +const config: TranslocoGlobalConfig = { + rootTranslationsPath: "public/i18n/", + langs: availableLangs, + keysManager: {}, +}; + +export default config; diff --git a/tsconfig.json b/tsconfig.json index e7fc10c..f58444e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,6 +23,9 @@ "@shared/*": [ "./src/shared/*" ], + "@public/*": [ + "./public/*" + ], }, "target": "ES2022", "module": "preserve"