Add complete server-side internationalization support for non-JavaScript users
This commit extends the existing i18n system to fully support server-side translations for users who have JavaScript disabled or are using the /table route directly. Changes: - Complete server-side Spanish (es-MX) translation support for /table route - Language selector dropdown in table view with form-based locale switching - URL parameter support (?locale=es-MX) for direct language selection - Updated POST form handler to persist locale selection across submissions - Proper locale detection and fallback for server-rendered pages - Fixed language selector initialization timing in client-side JS - Removed unused dependencies (canvas, sharp) to clean up package.json - Added snowflake emoji to app name in both English and Spanish translations The /table route now provides a complete non-JavaScript experience in both English and Spanish, ensuring accessibility for all users regardless of their browser capabilities or JavaScript preferences. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
8b1787ec47
commit
47b495d8eb
6 changed files with 107 additions and 563 deletions
505
package-lock.json
generated
505
package-lock.json
generated
|
@ -10,13 +10,11 @@
|
|||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"canvas": "^3.1.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.0.1",
|
||||
"express": "^4.18.2",
|
||||
"express-rate-limit": "^7.5.1",
|
||||
"node-cron": "^3.0.3",
|
||||
"sharp": "^0.34.2",
|
||||
"sqlite3": "^5.1.6",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.1"
|
||||
|
@ -729,16 +727,6 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
|
||||
"integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint-community/eslint-utils": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
||||
|
@ -1059,402 +1047,6 @@
|
|||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.2.tgz",
|
||||
"integrity": "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.2.tgz",
|
||||
"integrity": "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz",
|
||||
"integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz",
|
||||
"integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz",
|
||||
"integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz",
|
||||
"integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz",
|
||||
"integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz",
|
||||
"integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz",
|
||||
"integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz",
|
||||
"integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz",
|
||||
"integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.2.tgz",
|
||||
"integrity": "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.2.tgz",
|
||||
"integrity": "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.2.tgz",
|
||||
"integrity": "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.2.tgz",
|
||||
"integrity": "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.2.tgz",
|
||||
"integrity": "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.2.tgz",
|
||||
"integrity": "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.2.tgz",
|
||||
"integrity": "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.2.tgz",
|
||||
"integrity": "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.2.tgz",
|
||||
"integrity": "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.2.tgz",
|
||||
"integrity": "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/load-nyc-config": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||
|
@ -3940,20 +3532,6 @@
|
|||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/canvas": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/canvas/-/canvas-3.1.2.tgz",
|
||||
"integrity": "sha512-Z/tzFAcBzoCvJlOSlCnoekh1Gu8YMn0J51+UAuXJAbW1Z6I9l2mZgdD7738MepoeeIcUdDtbMnOg6cC7GJxy/g==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "^7.0.0",
|
||||
"prebuild-install": "^7.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.12.0 || >= 20.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
|
@ -4104,23 +3682,11 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1",
|
||||
"color-string": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
|
@ -4133,18 +3699,9 @@
|
|||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
|
||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/color-support": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||
|
@ -8663,47 +8220,6 @@
|
|||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.34.2",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.2.tgz",
|
||||
"integrity": "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"color": "^4.2.3",
|
||||
"detect-libc": "^2.0.4",
|
||||
"semver": "^7.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.34.2",
|
||||
"@img/sharp-darwin-x64": "0.34.2",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.1.0",
|
||||
"@img/sharp-libvips-darwin-x64": "1.1.0",
|
||||
"@img/sharp-libvips-linux-arm": "1.1.0",
|
||||
"@img/sharp-libvips-linux-arm64": "1.1.0",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.1.0",
|
||||
"@img/sharp-libvips-linux-s390x": "1.1.0",
|
||||
"@img/sharp-libvips-linux-x64": "1.1.0",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.1.0",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.1.0",
|
||||
"@img/sharp-linux-arm": "0.34.2",
|
||||
"@img/sharp-linux-arm64": "0.34.2",
|
||||
"@img/sharp-linux-s390x": "0.34.2",
|
||||
"@img/sharp-linux-x64": "0.34.2",
|
||||
"@img/sharp-linuxmusl-arm64": "0.34.2",
|
||||
"@img/sharp-linuxmusl-x64": "0.34.2",
|
||||
"@img/sharp-wasm32": "0.34.2",
|
||||
"@img/sharp-win32-arm64": "0.34.2",
|
||||
"@img/sharp-win32-ia32": "0.34.2",
|
||||
"@img/sharp-win32-x64": "0.34.2"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
@ -8864,21 +8380,6 @@
|
|||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle/node_modules/is-arrayish": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
|
||||
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/simple-update-notifier": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
||||
|
@ -9581,7 +9082,7 @@
|
|||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initialize language selector
|
||||
if (window.i18n) {
|
||||
window.i18n.createLanguageSelector('language-selector-container');
|
||||
}
|
||||
|
||||
const map = L.map('map').setView([42.96008, -85.67403], 10);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
|
|
|
@ -31,6 +31,9 @@ class I18nService {
|
|||
|
||||
// Trigger translation update after loading
|
||||
this.updatePageTranslations();
|
||||
|
||||
// Create language selector after translations are loaded
|
||||
this.createLanguageSelector('language-selector-container');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -226,10 +229,16 @@ class I18nService {
|
|||
return;
|
||||
}
|
||||
|
||||
// Clear container first in case it's being recreated
|
||||
container.innerHTML = '';
|
||||
|
||||
const select = document.createElement('select');
|
||||
select.id = 'language-selector';
|
||||
select.className = 'language-selector';
|
||||
select.title = this.t('common.selectLanguage');
|
||||
|
||||
// Use fallback title if translations not loaded yet
|
||||
const titleText = this.translations.size > 0 ? this.t('common.selectLanguage') : 'Select language';
|
||||
select.title = titleText;
|
||||
|
||||
// Add options for each available locale
|
||||
this.availableLocales.forEach(locale => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"common": {
|
||||
"appName": "Great Lakes Ice Report",
|
||||
"appName": "❄️ Great Lakes Ice Report",
|
||||
"appNameShort": "Ice Report",
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
|
@ -14,7 +14,8 @@
|
|||
"login": "Login",
|
||||
"homepage": "Homepage",
|
||||
"darkMode": "Toggle dark mode",
|
||||
"privacyPolicy": "Privacy Policy"
|
||||
"privacyPolicy": "Privacy Policy",
|
||||
"selectLanguage": "Select language"
|
||||
},
|
||||
"navigation": {
|
||||
"mapView": "📍 Map View",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"common": {
|
||||
"appName": "Reporte de Hielo de los Grandes Lagos",
|
||||
"appName": "❄️ Reporte de Hielo de los Grandes Lagos",
|
||||
"appNameShort": "Reporte de Hielo",
|
||||
"loading": "Cargando...",
|
||||
"error": "Error",
|
||||
|
@ -14,7 +14,8 @@
|
|||
"login": "Iniciar sesión",
|
||||
"homepage": "Página principal",
|
||||
"darkMode": "Activar modo oscuro",
|
||||
"privacyPolicy": "Política de privacidad"
|
||||
"privacyPolicy": "Política de privacidad",
|
||||
"selectLanguage": "Seleccionar idioma"
|
||||
},
|
||||
"navigation": {
|
||||
"mapView": "📍 Vista del mapa",
|
||||
|
|
139
src/server.ts
139
src/server.ts
|
@ -209,14 +209,27 @@ function setupRoutes(): void {
|
|||
app.get('/table', async (req: Request, res: Response): Promise<void> => {
|
||||
console.log('Serving table view for non-JS users');
|
||||
try {
|
||||
// Get locale from query parameter or use detected locale
|
||||
const requestedLocale = req.query.locale as string;
|
||||
const locale = requestedLocale && i18nService.isLocaleSupported(requestedLocale)
|
||||
? requestedLocale
|
||||
: (req as any).locale;
|
||||
|
||||
// Helper function for translations
|
||||
const t = (key: string) => i18nService.t(key, locale);
|
||||
|
||||
const locations = await locationModel.getActive();
|
||||
|
||||
const formatTimeRemaining = (createdAt: string): string => {
|
||||
const formatTimeRemaining = (createdAt: string, isPersistent?: boolean): string => {
|
||||
if (isPersistent) {
|
||||
return t('time.persistent');
|
||||
}
|
||||
|
||||
const created = new Date(createdAt);
|
||||
const now = new Date();
|
||||
const diffMs = (48 * 60 * 60 * 1000) - (now.getTime() - created.getTime());
|
||||
|
||||
if (diffMs <= 0) return 'Expired';
|
||||
if (diffMs <= 0) return t('time.expired');
|
||||
|
||||
const hours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||
|
@ -245,19 +258,32 @@ function setupRoutes(): void {
|
|||
<tr>
|
||||
<td style="text-align: center; font-weight: bold;">${index + 1}</td>
|
||||
<td>${escapeHtml(location.address)}</td>
|
||||
<td>${escapeHtml(location.description || 'No additional details')}</td>
|
||||
<td>${location.created_at ? formatDate(location.created_at) : 'Unknown'}</td>
|
||||
<td>${location.persistent ? 'Persistent' : (location.created_at ? formatTimeRemaining(location.created_at) : 'Unknown')}</td>
|
||||
<td>${escapeHtml(location.description || t('table.status.noDetails'))}</td>
|
||||
<td>${location.created_at ? formatDate(location.created_at) : t('common.loading')}</td>
|
||||
<td style="text-align: center;">${location.created_at ? formatTimeRemaining(location.created_at, !!location.persistent) : t('common.loading')}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
// Generate language selector form for non-JS users
|
||||
const languageSelectorForm = `
|
||||
<form method="get" action="/table" style="display: inline-block; margin-left: 10px;">
|
||||
<select name="locale" onchange="this.form.submit();" style="padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px;">
|
||||
<option value="en" ${locale === 'en' ? 'selected' : ''}>English</option>
|
||||
<option value="es-MX" ${locale === 'es-MX' ? 'selected' : ''}>Español (México)</option>
|
||||
</select>
|
||||
<noscript>
|
||||
<input type="submit" value="${t('common.submit')}" style="margin-left: 5px; padding: 4px 8px;">
|
||||
</noscript>
|
||||
</form>
|
||||
`;
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="${locale}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Great Lakes Ice Report - Table View</title>
|
||||
<title>${t('common.appName')} - ${t('navigation.tableView')}</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
@ -265,48 +291,52 @@ function setupRoutes(): void {
|
|||
<header>
|
||||
<div class="header-content">
|
||||
<div class="header-text">
|
||||
<h1>❄️ Great Lakes Ice Report</h1>
|
||||
<p>Community-reported ICEy road conditions and winter hazards</p>
|
||||
<h1>${t('common.appName')}</h1>
|
||||
<p>${t('meta.subtitle')}</p>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
${languageSelectorForm}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<div class="form-section">
|
||||
<h2>Report ICEy Conditions</h2>
|
||||
<h2>${t('form.reportConditions')}</h2>
|
||||
<form method="POST" action="/submit-report">
|
||||
<input type="hidden" name="locale" value="${locale}">
|
||||
<div class="form-group">
|
||||
<label for="address">Address or Location *</label>
|
||||
<label for="address">${t('form.addressLabel')}</label>
|
||||
<input type="text" id="address" name="address" required
|
||||
placeholder="Enter address, intersection, or landmark">
|
||||
<small class="input-help">Examples: "123 Main St, City" or "Main St & Oak Ave, City"</small>
|
||||
placeholder="${t('form.addressPlaceholder')}">
|
||||
<small class="input-help">${t('form.addressExamples')}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">Additional Details (Optional)</label>
|
||||
<label for="description">${t('form.detailsLabel')}</label>
|
||||
<textarea id="description" name="description" rows="3"
|
||||
placeholder="Number of vehicles, time observed, etc."></textarea>
|
||||
<small class="input-help">Keep descriptions appropriate and relevant to road conditions.</small>
|
||||
placeholder="${t('form.detailsPlaceholder')}"></textarea>
|
||||
<small class="input-help">${t('form.detailsHelp')}</small>
|
||||
</div>
|
||||
|
||||
<button type="submit">Report Location</button>
|
||||
<button type="submit">${t('form.reportLocation')}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="map-section">
|
||||
<div class="reports-header">
|
||||
<h2>Current Reports (${locations.length} active)</h2>
|
||||
<p><a href="/">← Back to Interactive Map</a></p>
|
||||
<h2>${t('map.currentReports')} (${locations.length} ${locations.length === 1 ? t('map.activeReports') : t('map.activeReportsPlural')})</h2>
|
||||
<p><a href="/">${t('navigation.backToMap')}</a></p>
|
||||
</div>
|
||||
|
||||
${locations.length > 0 ? `
|
||||
<div style="text-align: center; margin: 24px 0;">
|
||||
<h3>Static Map Overview</h3>
|
||||
<h3>${t('map.staticMapOverview')}</h3>
|
||||
<img src="/map-image.png?width=800&height=400"
|
||||
alt="Static map showing ice report locations"
|
||||
alt="${t('map.staticMapOverview')}"
|
||||
style="max-width: 100%; height: auto; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
|
||||
<p style="font-size: 14px; color: #666; margin-top: 8px;">
|
||||
Red markers: Regular reports | Orange markers: Persistent reports
|
||||
${t('map.markerLegend')}
|
||||
</p>
|
||||
</div>
|
||||
` : ''}
|
||||
|
@ -316,14 +346,14 @@ function setupRoutes(): void {
|
|||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Location</th>
|
||||
<th>Details</th>
|
||||
<th>Reported</th>
|
||||
<th>Time Remaining</th>
|
||||
<th>${t('table.headers.location')}</th>
|
||||
<th>${t('table.headers.details')}</th>
|
||||
<th>${t('table.headers.reported')}</th>
|
||||
<th>${t('table.headers.timeRemaining')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${tableRows || '<tr><td colspan="5">No reports currently available</td></tr>'}
|
||||
${tableRows || `<tr><td colspan="5">${t('table.noReports')}</td></tr>`}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -331,9 +361,9 @@ function setupRoutes(): void {
|
|||
</div>
|
||||
|
||||
<footer>
|
||||
<p><strong>Safety Notice:</strong> This is a community tool for awareness. Stay safe and verify information independently.</p>
|
||||
<p><strong>${t('footer.safetyNotice')}</strong> <a href="https://www.aclu.org/know-your-rights/immigrants-rights" target="_blank" rel="noopener noreferrer">${t('footer.knowRights')}</a>.</p>
|
||||
<div class="disclaimer">
|
||||
<small>Reports are automatically deleted after 48 hours. • <a href="/privacy">Privacy Policy</a></small>
|
||||
<small>${t('footer.disclaimer')} <a href="/privacy">${t('common.privacyPolicy')}</a></small>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
@ -352,18 +382,26 @@ function setupRoutes(): void {
|
|||
app.post('/submit-report', async (req: Request, res: Response): Promise<void> => {
|
||||
console.log('Handling form submission for non-JS users');
|
||||
|
||||
const { address, description } = req.body;
|
||||
const { address, description, locale: formLocale } = req.body;
|
||||
|
||||
// Get locale from form or use detected locale
|
||||
const locale = formLocale && i18nService.isLocaleSupported(formLocale)
|
||||
? formLocale
|
||||
: (req as any).locale;
|
||||
|
||||
// Helper function for translations
|
||||
const t = (key: string) => i18nService.t(key, locale);
|
||||
|
||||
// Validate input
|
||||
if (!address || typeof address !== 'string' || address.trim().length === 0) {
|
||||
res.status(400).send(`
|
||||
<html>
|
||||
<head><title>Error</title><link rel="stylesheet" href="style.css"></head>
|
||||
<html lang="${locale}">
|
||||
<head><title>${t('common.error')}</title><link rel="stylesheet" href="style.css"></head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Error</h1>
|
||||
<p>Address is required.</p>
|
||||
<p><a href="/table">← Go Back</a></p>
|
||||
<h1>${t('common.error')}</h1>
|
||||
<p>${t('form.addressRequired')}</p>
|
||||
<p><a href="/table?locale=${locale}">${t('navigation.goBack')}</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -377,14 +415,13 @@ function setupRoutes(): void {
|
|||
const analysis = profanityFilter.analyzeProfanity(description);
|
||||
if (analysis.hasProfanity) {
|
||||
res.status(400).send(`
|
||||
<html>
|
||||
<head><title>Submission Rejected</title><link rel="stylesheet" href="style.css"></head>
|
||||
<html lang="${locale}">
|
||||
<head><title>${t('form.submissionRejected')}</title><link rel="stylesheet" href="style.css"></head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Submission Rejected</h1>
|
||||
<p>Your description contains inappropriate language and cannot be posted.</p>
|
||||
<p>Please revise your description to focus on road conditions and keep it professional.</p>
|
||||
<p><a href="/table">← Go Back</a></p>
|
||||
<h1>${t('form.submissionRejected')}</h1>
|
||||
<p>${t('errors.inappropriateLanguage')}</p>
|
||||
<p><a href="/table?locale=${locale}">${t('navigation.goBack')}</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -403,13 +440,13 @@ function setupRoutes(): void {
|
|||
});
|
||||
|
||||
res.send(`
|
||||
<html>
|
||||
<head><title>Report Submitted</title><link rel="stylesheet" href="style.css"></head>
|
||||
<html lang="${locale}">
|
||||
<head><title>${t('form.reportSubmitted')}</title><link rel="stylesheet" href="style.css"></head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>✅ Report Submitted Successfully</h1>
|
||||
<p>Your ice condition report has been added to the system.</p>
|
||||
<p><a href="/table">← View All Reports</a></p>
|
||||
<h1>✅ ${t('form.reportSubmitted')}</h1>
|
||||
<p>${t('form.reportSubmittedMsg')}</p>
|
||||
<p><a href="/table?locale=${locale}">${t('navigation.viewReports')}</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -417,13 +454,13 @@ function setupRoutes(): void {
|
|||
} catch (err) {
|
||||
console.error('Error creating location:', err);
|
||||
res.status(500).send(`
|
||||
<html>
|
||||
<head><title>Error</title><link rel="stylesheet" href="style.css"></head>
|
||||
<html lang="${locale}">
|
||||
<head><title>${t('common.error')}</title><link rel="stylesheet" href="style.css"></head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Error</h1>
|
||||
<p>Failed to submit report. Please try again.</p>
|
||||
<p><a href="/table">← Go Back</a></p>
|
||||
<h1>${t('common.error')}</h1>
|
||||
<p>${t('form.submitError')}</p>
|
||||
<p><a href="/table?locale=${locale}">${t('navigation.goBack')}</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue