Просмотр исходного кода

Merge branch 'master' into fix/emailDisabled

rugk 2 недель назад
Родитель
Сommit
25d19c1fb9
9 измененных файлов с 430 добавлено и 290 удалено
  1. 2 2
      .github/workflows/codeql-analysis.yml
  2. 1 1
      .github/workflows/snyk-scan.yml
  3. 0 2
      .styleci.yml
  4. 337 234
      js/package-lock.json
  5. 4 2
      js/package.json
  6. 2 18
      js/privatebin.js
  7. 17 5
      js/test/UiHelper.js
  8. 47 20
      lib/I18n.php
  9. 20 6
      tst/I18nTest.php

+ 2 - 2
.github/workflows/codeql-analysis.yml

@@ -43,7 +43,7 @@ jobs:
 
 
     # Initializes the CodeQL tools for scanning.
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@v4
+      uses: github/codeql-action/init@v4.37.4
       with:
       with:
         languages: ${{ matrix.language }}
         languages: ${{ matrix.language }}
         # If you wish to specify custom queries, you can do so here or in a config file.
         # If you wish to specify custom queries, you can do so here or in a config file.
@@ -52,4 +52,4 @@ jobs:
         # queries: ./path/to/local/query, your-org/your-repo/queries@main
         # queries: ./path/to/local/query, your-org/your-repo/queries@main
 
 
     - name: Perform CodeQL Analysis
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v4
+      uses: github/codeql-action/analyze@v4.37.4

+ 1 - 1
.github/workflows/snyk-scan.yml

@@ -35,6 +35,6 @@ jobs:
         with:
         with:
           args: --sarif-file-output=snyk.sarif
           args: --sarif-file-output=snyk.sarif
       - name: Upload result to GitHub Code Scanning
       - name: Upload result to GitHub Code Scanning
-        uses: github/codeql-action/upload-sarif@v4
+        uses: github/codeql-action/upload-sarif@v4.37.4
         with:
         with:
           sarif_file: snyk.sarif
           sarif_file: snyk.sarif

+ 0 - 2
.styleci.yml

@@ -3,7 +3,6 @@ preset: recommended
 risky: false
 risky: false
 
 
 enabled:
 enabled:
-  - align_equals
   - concat_with_spaces
   - concat_with_spaces
   - no_empty_comment
   - no_empty_comment
   - pre_increment
   - pre_increment
@@ -26,7 +25,6 @@ disabled:
   - phpdoc_summary
   - phpdoc_summary
   - post_increment
   - post_increment
   - single_line_after_imports
   - single_line_after_imports
-  - unalign_equals
 
 
 finder:
 finder:
   path:
   path:

+ 337 - 234
js/package-lock.json

@@ -9,15 +9,17 @@
       "version": "2.1.0",
       "version": "2.1.0",
       "license": "zlib-acknowledgement",
       "license": "zlib-acknowledgement",
       "devDependencies": {
       "devDependencies": {
+        "@eslint/js": "^10.0.1",
         "@peculiar/webcrypto": "^1.5.0",
         "@peculiar/webcrypto": "^1.5.0",
-        "eslint": "^9.37.0",
+        "eslint": "^10.13.0",
         "fast-check": "^4.10.2",
         "fast-check": "^4.10.2",
+        "globals": "^17.13.0",
         "jsdom": "^26.1.0",
         "jsdom": "^26.1.0",
         "jsdom-global": "^3.0.2",
         "jsdom-global": "^3.0.2",
         "jshint": "^2.13.6",
         "jshint": "^2.13.6",
         "jsverify": "^0.8.3",
         "jsverify": "^0.8.3",
         "mime-types": "^3.0.2",
         "mime-types": "^3.0.2",
-        "mocha": "^12.0.0",
+        "mocha": "^12.0.3",
         "nyc": "^18.0.0"
         "nyc": "^18.0.0"
       }
       }
     },
     },
@@ -309,6 +311,30 @@
         "node": ">=6.9.0"
         "node": ">=6.9.0"
       }
       }
     },
     },
+    "node_modules/@cacheable/memory": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz",
+      "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@cacheable/utils": "^2.5.0",
+        "@keyv/bigmap": "^1.3.1",
+        "hookified": "^1.15.1",
+        "keyv": "^5.6.0"
+      }
+    },
+    "node_modules/@cacheable/utils": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz",
+      "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hashery": "^1.5.1",
+        "keyv": "^5.6.0"
+      }
+    },
     "node_modules/@csstools/color-helpers": {
     "node_modules/@csstools/color-helpers": {
       "version": "5.1.0",
       "version": "5.1.0",
       "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
       "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
@@ -459,98 +485,128 @@
       }
       }
     },
     },
     "node_modules/@eslint/config-array": {
     "node_modules/@eslint/config-array": {
-      "version": "0.21.2",
-      "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
-      "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+      "version": "0.23.5",
+      "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
+      "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
       "dev": true,
       "dev": true,
+      "license": "Apache-2.0",
       "dependencies": {
       "dependencies": {
-        "@eslint/object-schema": "^2.1.7",
+        "@eslint/object-schema": "^3.0.5",
         "debug": "^4.3.1",
         "debug": "^4.3.1",
-        "minimatch": "^3.1.5"
+        "minimatch": "^10.2.4"
       },
       },
       "engines": {
       "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+        "node": "^20.19.0 || ^22.13.0 || >=24"
       }
       }
     },
     },
-    "node_modules/@eslint/config-helpers": {
-      "version": "0.4.2",
-      "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
-      "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+    "node_modules/@eslint/config-array/node_modules/balanced-match": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+      "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
       "dev": true,
       "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/@eslint/config-array/node_modules/brace-expansion": {
+      "version": "5.0.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
+      "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
+      "dev": true,
+      "license": "MIT",
       "dependencies": {
       "dependencies": {
-        "@eslint/core": "^0.17.0"
+        "balanced-match": "^4.0.2"
       },
       },
       "engines": {
       "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+        "node": "20 || >=22"
       }
       }
     },
     },
-    "node_modules/@eslint/core": {
-      "version": "0.17.0",
-      "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
-      "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+    "node_modules/@eslint/config-array/node_modules/minimatch": {
+      "version": "10.2.6",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
+      "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
       "dev": true,
       "dev": true,
+      "license": "BlueOak-1.0.0",
       "dependencies": {
       "dependencies": {
-        "@types/json-schema": "^7.0.15"
+        "brace-expansion": "^5.0.8"
       },
       },
       "engines": {
       "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
       }
       }
     },
     },
-    "node_modules/@eslint/eslintrc": {
-      "version": "3.3.5",
-      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
-      "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
+    "node_modules/@eslint/config-helpers": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz",
+      "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==",
       "dev": true,
       "dev": true,
+      "license": "Apache-2.0",
       "dependencies": {
       "dependencies": {
-        "ajv": "^6.14.0",
-        "debug": "^4.3.2",
-        "espree": "^10.0.1",
-        "globals": "^14.0.0",
-        "ignore": "^5.2.0",
-        "import-fresh": "^3.2.1",
-        "js-yaml": "^4.1.1",
-        "minimatch": "^3.1.5",
-        "strip-json-comments": "^3.1.1"
+        "@eslint/core": "^1.2.1"
       },
       },
       "engines": {
       "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+        "node": "^20.19.0 || ^22.13.0 || >=24"
+      }
+    },
+    "node_modules/@eslint/core": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
+      "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@types/json-schema": "^7.0.15"
       },
       },
-      "funding": {
-        "url": "https://opencollective.com/eslint"
+      "engines": {
+        "node": "^20.19.0 || ^22.13.0 || >=24"
       }
       }
     },
     },
     "node_modules/@eslint/js": {
     "node_modules/@eslint/js": {
-      "version": "9.39.4",
-      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
-      "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
+      "version": "10.0.1",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+      "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
       "dev": true,
       "dev": true,
+      "license": "MIT",
       "engines": {
       "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+        "node": "^20.19.0 || ^22.13.0 || >=24"
       },
       },
       "funding": {
       "funding": {
         "url": "https://eslint.org/donate"
         "url": "https://eslint.org/donate"
+      },
+      "peerDependencies": {
+        "eslint": "^10.0.0"
+      },
+      "peerDependenciesMeta": {
+        "eslint": {
+          "optional": true
+        }
       }
       }
     },
     },
     "node_modules/@eslint/object-schema": {
     "node_modules/@eslint/object-schema": {
-      "version": "2.1.7",
-      "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
-      "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
+      "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
       "dev": true,
       "dev": true,
+      "license": "Apache-2.0",
       "engines": {
       "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+        "node": "^20.19.0 || ^22.13.0 || >=24"
       }
       }
     },
     },
     "node_modules/@eslint/plugin-kit": {
     "node_modules/@eslint/plugin-kit": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
-      "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+      "version": "0.7.3",
+      "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz",
+      "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==",
       "dev": true,
       "dev": true,
+      "license": "Apache-2.0",
       "dependencies": {
       "dependencies": {
-        "@eslint/core": "^0.17.0",
+        "@eslint/core": "^1.2.1",
         "levn": "^0.4.1"
         "levn": "^0.4.1"
       },
       },
       "engines": {
       "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+        "node": "^20.19.0 || ^22.13.0 || >=24"
       }
       }
     },
     },
     "node_modules/@humanfs/core": {
     "node_modules/@humanfs/core": {
@@ -768,6 +824,30 @@
         "@jridgewell/sourcemap-codec": "^1.4.14"
         "@jridgewell/sourcemap-codec": "^1.4.14"
       }
       }
     },
     },
+    "node_modules/@keyv/bigmap": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz",
+      "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hashery": "^1.4.0",
+        "hookified": "^1.15.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "peerDependencies": {
+        "keyv": "^5.6.0"
+      }
+    },
+    "node_modules/@keyv/serialize": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz",
+      "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@peculiar/asn1-schema": {
     "node_modules/@peculiar/asn1-schema": {
       "version": "2.7.0",
       "version": "2.7.0",
       "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.7.0.tgz",
       "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.7.0.tgz",
@@ -816,6 +896,13 @@
         "node": ">=14.18.0"
         "node": ">=14.18.0"
       }
       }
     },
     },
+    "node_modules/@types/esrecurse": {
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+      "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/estree": {
     "node_modules/@types/estree": {
       "version": "1.0.9",
       "version": "1.0.9",
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
@@ -826,13 +913,15 @@
       "version": "7.0.15",
       "version": "7.0.15",
       "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
       "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
       "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
       "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     },
     "node_modules/acorn": {
     "node_modules/acorn": {
-      "version": "8.16.0",
-      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
-      "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+      "version": "8.19.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz",
+      "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==",
       "dev": true,
       "dev": true,
+      "license": "MIT",
       "bin": {
       "bin": {
         "acorn": "bin/acorn"
         "acorn": "bin/acorn"
       },
       },
@@ -845,6 +934,7 @@
       "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
       "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
       "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
       "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
       "dev": true,
       "dev": true,
+      "license": "MIT",
       "peerDependencies": {
       "peerDependencies": {
         "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
         "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
       }
       }
@@ -877,6 +967,7 @@
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
       "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
       "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
       "dev": true,
       "dev": true,
+      "license": "MIT",
       "dependencies": {
       "dependencies": {
         "fast-deep-equal": "^3.1.1",
         "fast-deep-equal": "^3.1.1",
         "fast-json-stable-stringify": "^2.0.0",
         "fast-json-stable-stringify": "^2.0.0",
@@ -1012,6 +1103,20 @@
         "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
         "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
       }
       }
     },
     },
+    "node_modules/cacheable": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz",
+      "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@cacheable/memory": "^2.2.0",
+        "@cacheable/utils": "^2.5.0",
+        "hookified": "^1.15.0",
+        "keyv": "^5.6.0",
+        "qified": "^0.10.1"
+      }
+    },
     "node_modules/caching-transform": {
     "node_modules/caching-transform": {
       "version": "4.0.0",
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz",
       "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz",
@@ -1027,15 +1132,6 @@
         "node": ">=8"
         "node": ">=8"
       }
       }
     },
     },
-    "node_modules/callsites": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
-      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
-      "dev": true,
-      "engines": {
-        "node": ">=6"
-      }
-    },
     "node_modules/camelcase": {
     "node_modules/camelcase": {
       "version": "5.3.1",
       "version": "5.3.1",
       "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
       "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
@@ -1066,22 +1162,6 @@
       ],
       ],
       "license": "CC-BY-4.0"
       "license": "CC-BY-4.0"
     },
     },
-    "node_modules/chalk": {
-      "version": "4.1.2",
-      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
-      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
-      "dev": true,
-      "dependencies": {
-        "ansi-styles": "^4.1.0",
-        "supports-color": "^7.1.0"
-      },
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/chalk/chalk?sponsor=1"
-      }
-    },
     "node_modules/chokidar": {
     "node_modules/chokidar": {
       "version": "5.0.0",
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
@@ -1412,43 +1492,43 @@
       }
       }
     },
     },
     "node_modules/eslint": {
     "node_modules/eslint": {
-      "version": "9.39.4",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
-      "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
+      "version": "10.13.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.13.0.tgz",
+      "integrity": "sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==",
       "dev": true,
       "dev": true,
+      "license": "MIT",
+      "workspaces": [
+        "packages/*"
+      ],
       "dependencies": {
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.8.0",
         "@eslint-community/eslint-utils": "^4.8.0",
-        "@eslint-community/regexpp": "^4.12.1",
-        "@eslint/config-array": "^0.21.2",
-        "@eslint/config-helpers": "^0.4.2",
-        "@eslint/core": "^0.17.0",
-        "@eslint/eslintrc": "^3.3.5",
-        "@eslint/js": "9.39.4",
-        "@eslint/plugin-kit": "^0.4.1",
+        "@eslint-community/regexpp": "^4.12.2",
+        "@eslint/config-array": "^0.23.5",
+        "@eslint/config-helpers": "^0.7.0",
+        "@eslint/core": "^1.2.1",
+        "@eslint/plugin-kit": "^0.7.3",
         "@humanfs/node": "^0.16.6",
         "@humanfs/node": "^0.16.6",
         "@humanwhocodes/module-importer": "^1.0.1",
         "@humanwhocodes/module-importer": "^1.0.1",
         "@humanwhocodes/retry": "^0.4.2",
         "@humanwhocodes/retry": "^0.4.2",
         "@types/estree": "^1.0.6",
         "@types/estree": "^1.0.6",
         "ajv": "^6.14.0",
         "ajv": "^6.14.0",
-        "chalk": "^4.0.0",
         "cross-spawn": "^7.0.6",
         "cross-spawn": "^7.0.6",
         "debug": "^4.3.2",
         "debug": "^4.3.2",
         "escape-string-regexp": "^4.0.0",
         "escape-string-regexp": "^4.0.0",
-        "eslint-scope": "^8.4.0",
-        "eslint-visitor-keys": "^4.2.1",
-        "espree": "^10.4.0",
-        "esquery": "^1.5.0",
+        "eslint-scope": "^9.1.2",
+        "eslint-visitor-keys": "^5.0.1",
+        "espree": "^11.2.0",
+        "esquery": "^1.7.0",
         "esutils": "^2.0.2",
         "esutils": "^2.0.2",
         "fast-deep-equal": "^3.1.3",
         "fast-deep-equal": "^3.1.3",
-        "file-entry-cache": "^8.0.0",
+        "file-entry-cache": "11.1.5 || >11.1.6 <12",
         "find-up": "^5.0.0",
         "find-up": "^5.0.0",
         "glob-parent": "^6.0.2",
         "glob-parent": "^6.0.2",
         "ignore": "^5.2.0",
         "ignore": "^5.2.0",
         "imurmurhash": "^0.1.4",
         "imurmurhash": "^0.1.4",
         "is-glob": "^4.0.0",
         "is-glob": "^4.0.0",
         "json-stable-stringify-without-jsonify": "^1.0.1",
         "json-stable-stringify-without-jsonify": "^1.0.1",
-        "lodash.merge": "^4.6.2",
-        "minimatch": "^3.1.5",
+        "minimatch": "^10.2.5",
         "natural-compare": "^1.4.0",
         "natural-compare": "^1.4.0",
         "optionator": "^0.9.3"
         "optionator": "^0.9.3"
       },
       },
@@ -1456,7 +1536,7 @@
         "eslint": "bin/eslint.js"
         "eslint": "bin/eslint.js"
       },
       },
       "engines": {
       "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+        "node": "^20.19.0 || ^22.13.0 || >=24"
       },
       },
       "funding": {
       "funding": {
         "url": "https://eslint.org/donate"
         "url": "https://eslint.org/donate"
@@ -1471,45 +1551,89 @@
       }
       }
     },
     },
     "node_modules/eslint-scope": {
     "node_modules/eslint-scope": {
-      "version": "8.4.0",
-      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
-      "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+      "version": "9.1.2",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+      "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
       "dev": true,
       "dev": true,
+      "license": "BSD-2-Clause",
       "dependencies": {
       "dependencies": {
+        "@types/esrecurse": "^4.3.1",
+        "@types/estree": "^1.0.8",
         "esrecurse": "^4.3.0",
         "esrecurse": "^4.3.0",
         "estraverse": "^5.2.0"
         "estraverse": "^5.2.0"
       },
       },
       "engines": {
       "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+        "node": "^20.19.0 || ^22.13.0 || >=24"
       },
       },
       "funding": {
       "funding": {
         "url": "https://opencollective.com/eslint"
         "url": "https://opencollective.com/eslint"
       }
       }
     },
     },
     "node_modules/eslint-visitor-keys": {
     "node_modules/eslint-visitor-keys": {
-      "version": "4.2.1",
-      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
-      "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+      "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
       "dev": true,
       "dev": true,
+      "license": "Apache-2.0",
       "engines": {
       "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+        "node": "^20.19.0 || ^22.13.0 || >=24"
       },
       },
       "funding": {
       "funding": {
         "url": "https://opencollective.com/eslint"
         "url": "https://opencollective.com/eslint"
       }
       }
     },
     },
+    "node_modules/eslint/node_modules/balanced-match": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+      "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/eslint/node_modules/brace-expansion": {
+      "version": "5.0.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
+      "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^4.0.2"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/eslint/node_modules/minimatch": {
+      "version": "10.2.6",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
+      "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "brace-expansion": "^5.0.8"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/espree": {
     "node_modules/espree": {
-      "version": "10.4.0",
-      "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
-      "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+      "version": "11.2.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+      "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
       "dev": true,
       "dev": true,
+      "license": "BSD-2-Clause",
       "dependencies": {
       "dependencies": {
-        "acorn": "^8.15.0",
+        "acorn": "^8.16.0",
         "acorn-jsx": "^5.3.2",
         "acorn-jsx": "^5.3.2",
-        "eslint-visitor-keys": "^4.2.1"
+        "eslint-visitor-keys": "^5.0.1"
       },
       },
       "engines": {
       "engines": {
-        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+        "node": "^20.19.0 || ^22.13.0 || >=24"
       },
       },
       "funding": {
       "funding": {
         "url": "https://opencollective.com/eslint"
         "url": "https://opencollective.com/eslint"
@@ -1545,6 +1669,7 @@
       "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
       "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
       "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
       "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
       "dev": true,
       "dev": true,
+      "license": "BSD-2-Clause",
       "dependencies": {
       "dependencies": {
         "estraverse": "^5.2.0"
         "estraverse": "^5.2.0"
       },
       },
@@ -1606,13 +1731,15 @@
       "version": "3.1.3",
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
       "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
       "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     },
     "node_modules/fast-json-stable-stringify": {
     "node_modules/fast-json-stable-stringify": {
       "version": "2.1.0",
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
       "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
       "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
       "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     },
     "node_modules/fast-levenshtein": {
     "node_modules/fast-levenshtein": {
       "version": "2.0.6",
       "version": "2.0.6",
@@ -1621,15 +1748,13 @@
       "dev": true
       "dev": true
     },
     },
     "node_modules/file-entry-cache": {
     "node_modules/file-entry-cache": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
-      "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+      "version": "11.1.5",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz",
+      "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==",
       "dev": true,
       "dev": true,
+      "license": "MIT",
       "dependencies": {
       "dependencies": {
-        "flat-cache": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=16.0.0"
+        "flat-cache": "^6.1.23"
       }
       }
     },
     },
     "node_modules/find-cache-dir": {
     "node_modules/find-cache-dir": {
@@ -1665,24 +1790,37 @@
         "url": "https://github.com/sponsors/sindresorhus"
         "url": "https://github.com/sponsors/sindresorhus"
       }
       }
     },
     },
+    "node_modules/find-up-simple": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz",
+      "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/flat-cache": {
     "node_modules/flat-cache": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
-      "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+      "version": "6.1.23",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz",
+      "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==",
       "dev": true,
       "dev": true,
+      "license": "MIT",
       "dependencies": {
       "dependencies": {
-        "flatted": "^3.2.9",
-        "keyv": "^4.5.4"
-      },
-      "engines": {
-        "node": ">=16"
+        "cacheable": "^2.5.0",
+        "flatted": "^3.4.2",
+        "hookified": "^1.15.0"
       }
       }
     },
     },
     "node_modules/flatted": {
     "node_modules/flatted": {
-      "version": "3.4.2",
-      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
-      "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
-      "dev": true
+      "version": "3.4.4",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
+      "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
+      "dev": true,
+      "license": "ISC"
     },
     },
     "node_modules/foreground-child": {
     "node_modules/foreground-child": {
       "version": "3.3.1",
       "version": "3.3.1",
@@ -1824,10 +1962,11 @@
       }
       }
     },
     },
     "node_modules/globals": {
     "node_modules/globals": {
-      "version": "14.0.0",
-      "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
-      "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+      "version": "17.13.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-17.13.0.tgz",
+      "integrity": "sha512-RwMTC61u7hrG3ZJMkZQOK4JLJrKS8QtoTii63EmWvFXHqycY9FObBJQCbsLxSO8kjKhWE5kV1GC+Vb1g3RI0Zg==",
       "dev": true,
       "dev": true,
+      "license": "MIT",
       "engines": {
       "engines": {
         "node": ">=18"
         "node": ">=18"
       },
       },
@@ -1866,6 +2005,26 @@
         "url": "https://github.com/sponsors/sindresorhus"
         "url": "https://github.com/sponsors/sindresorhus"
       }
       }
     },
     },
+    "node_modules/hashery": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz",
+      "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hookified": "^1.15.0"
+      },
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/hookified": {
+      "version": "1.15.1",
+      "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz",
+      "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/html-encoding-sniffer": {
     "node_modules/html-encoding-sniffer": {
       "version": "4.0.0",
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
       "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
@@ -1948,26 +2107,11 @@
       "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
       "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
       "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
       "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
       "dev": true,
       "dev": true,
+      "license": "MIT",
       "engines": {
       "engines": {
         "node": ">= 4"
         "node": ">= 4"
       }
       }
     },
     },
-    "node_modules/import-fresh": {
-      "version": "3.3.1",
-      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
-      "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
-      "dev": true,
-      "dependencies": {
-        "parent-module": "^1.0.0",
-        "resolve-from": "^4.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/imurmurhash": {
     "node_modules/imurmurhash": {
       "version": "0.1.4",
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
       "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -2037,12 +2181,16 @@
       }
       }
     },
     },
     "node_modules/is-path-inside": {
     "node_modules/is-path-inside": {
-      "version": "3.0.3",
-      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
-      "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz",
+      "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==",
       "dev": true,
       "dev": true,
+      "license": "MIT",
       "engines": {
       "engines": {
-        "node": ">=8"
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
       }
       }
     },
     },
     "node_modules/is-potential-custom-element-name": {
     "node_modules/is-potential-custom-element-name": {
@@ -2222,29 +2370,6 @@
       "dev": true,
       "dev": true,
       "license": "MIT"
       "license": "MIT"
     },
     },
-    "node_modules/js-yaml": {
-      "version": "4.3.2",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
-      "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
-      "dev": true,
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/puzrin"
-        },
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/nodeca"
-        }
-      ],
-      "license": "MIT",
-      "dependencies": {
-        "argparse": "^2.0.1"
-      },
-      "bin": {
-        "js-yaml": "bin/js-yaml.js"
-      }
-    },
     "node_modules/jsdom": {
     "node_modules/jsdom": {
       "version": "26.1.0",
       "version": "26.1.0",
       "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
       "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
@@ -2351,17 +2476,12 @@
         "node": ">=0.8.0"
         "node": ">=0.8.0"
       }
       }
     },
     },
-    "node_modules/json-buffer": {
-      "version": "3.0.1",
-      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
-      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
-      "dev": true
-    },
     "node_modules/json-schema-traverse": {
     "node_modules/json-schema-traverse": {
       "version": "0.4.1",
       "version": "0.4.1",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
       "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
       "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
       "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     },
     "node_modules/json-stable-stringify-without-jsonify": {
     "node_modules/json-stable-stringify-without-jsonify": {
       "version": "1.0.1",
       "version": "1.0.1",
@@ -2397,12 +2517,13 @@
       }
       }
     },
     },
     "node_modules/keyv": {
     "node_modules/keyv": {
-      "version": "4.5.4",
-      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
-      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz",
+      "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==",
       "dev": true,
       "dev": true,
+      "license": "MIT",
       "dependencies": {
       "dependencies": {
-        "json-buffer": "3.0.1"
+        "@keyv/serialize": "^1.1.1"
       }
       }
     },
     },
     "node_modules/lazy-seq": {
     "node_modules/lazy-seq": {
@@ -2455,12 +2576,6 @@
       "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==",
       "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==",
       "dev": true
       "dev": true
     },
     },
-    "node_modules/lodash.merge": {
-      "version": "4.6.2",
-      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
-      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
-      "dev": true
-    },
     "node_modules/lru-cache": {
     "node_modules/lru-cache": {
       "version": "10.4.3",
       "version": "10.4.3",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
       "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
@@ -2538,9 +2653,9 @@
       }
       }
     },
     },
     "node_modules/mocha": {
     "node_modules/mocha": {
-      "version": "12.0.0",
-      "resolved": "https://registry.npmjs.org/mocha/-/mocha-12.0.0.tgz",
-      "integrity": "sha512-NYNh5IFt6WYqm9bi4601m7vix8MZdXC0DwS4gY6WhXO2RgJWhivhISVmq1oklCif18OSI9l+vx5Mdm5oh1XGiQ==",
+      "version": "12.0.3",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-12.0.3.tgz",
+      "integrity": "sha512-beammieDdiWdkjANWeWPpkxUROYGi5FN88fPOkLiticz7HTg3Y+Pho3EYE6SUdcuWnZ3h2WD8EXCA55JF4o6wg==",
       "dev": true,
       "dev": true,
       "license": "MIT",
       "license": "MIT",
       "dependencies": {
       "dependencies": {
@@ -2548,15 +2663,15 @@
         "chokidar": "^5.0.0",
         "chokidar": "^5.0.0",
         "debug": "^4.3.5",
         "debug": "^4.3.5",
         "diff": "^9.0.0",
         "diff": "^9.0.0",
-        "find-up": "^5.0.0",
+        "find-up-simple": "^1.0.1",
         "glob": "^13.0.0",
         "glob": "^13.0.0",
-        "is-path-inside": "^3.0.3",
+        "is-path-inside": "^4.0.0",
         "is-unicode-supported": "^0.1.0",
         "is-unicode-supported": "^0.1.0",
         "js-yaml": "^5.0.0",
         "js-yaml": "^5.0.0",
         "minimatch": "^10.2.2",
         "minimatch": "^10.2.2",
         "ms": "^2.1.3",
         "ms": "^2.1.3",
         "picocolors": "^1.1.1",
         "picocolors": "^1.1.1",
-        "serialize-javascript": "^7.0.2",
+        "serialize-javascript": "^7.1.1",
         "strip-json-comments": "^5.0.3",
         "strip-json-comments": "^5.0.3",
         "supports-color": "^8.1.1",
         "supports-color": "^8.1.1",
         "workerpool": "^10.0.0"
         "workerpool": "^10.0.0"
@@ -3014,18 +3129,6 @@
       "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
       "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
       "dev": true
       "dev": true
     },
     },
-    "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==",
-      "dev": true,
-      "dependencies": {
-        "callsites": "^3.0.0"
-      },
-      "engines": {
-        "node": ">=6"
-      }
-    },
     "node_modules/parse5": {
     "node_modules/parse5": {
       "version": "7.3.0",
       "version": "7.3.0",
       "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
       "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
@@ -3228,6 +3331,26 @@
         "node": ">=16.0.0"
         "node": ">=16.0.0"
       }
       }
     },
     },
+    "node_modules/qified": {
+      "version": "0.10.1",
+      "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz",
+      "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hookified": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/qified/node_modules/hookified": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz",
+      "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/rc4": {
     "node_modules/rc4": {
       "version": "0.1.5",
       "version": "0.1.5",
       "resolved": "https://registry.npmjs.org/rc4/-/rc4-0.1.5.tgz",
       "resolved": "https://registry.npmjs.org/rc4/-/rc4-0.1.5.tgz",
@@ -3291,15 +3414,6 @@
       "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
       "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
       "dev": true
       "dev": true
     },
     },
-    "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==",
-      "dev": true,
-      "engines": {
-        "node": ">=4"
-      }
-    },
     "node_modules/rimraf": {
     "node_modules/rimraf": {
       "version": "6.1.3",
       "version": "6.1.3",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz",
@@ -3476,18 +3590,6 @@
         "node": ">=8"
         "node": ">=8"
       }
       }
     },
     },
-    "node_modules/strip-json-comments": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
-      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
-      "dev": true,
-      "engines": {
-        "node": ">=8"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/supports-color": {
     "node_modules/supports-color": {
       "version": "7.2.0",
       "version": "7.2.0",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
       "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -3689,6 +3791,7 @@
       "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
       "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
       "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
       "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
       "dev": true,
       "dev": true,
+      "license": "BSD-2-Clause",
       "dependencies": {
       "dependencies": {
         "punycode": "^2.1.0"
         "punycode": "^2.1.0"
       }
       }

+ 4 - 2
js/package.json

@@ -7,15 +7,17 @@
     "test": "test"
     "test": "test"
   },
   },
   "devDependencies": {
   "devDependencies": {
+    "@eslint/js": "^10.0.1",
     "@peculiar/webcrypto": "^1.5.0",
     "@peculiar/webcrypto": "^1.5.0",
-    "eslint": "^9.37.0",
+    "eslint": "^10.13.0",
     "fast-check": "^4.10.2",
     "fast-check": "^4.10.2",
+    "globals": "^17.13.0",
     "jsdom": "^26.1.0",
     "jsdom": "^26.1.0",
     "jsdom-global": "^3.0.2",
     "jsdom-global": "^3.0.2",
     "jshint": "^2.13.6",
     "jshint": "^2.13.6",
     "jsverify": "^0.8.3",
     "jsverify": "^0.8.3",
     "mime-types": "^3.0.2",
     "mime-types": "^3.0.2",
-    "mocha": "^12.0.0",
+    "mocha": "^12.0.3",
     "nyc": "^18.0.0"
     "nyc": "^18.0.0"
   },
   },
   "scripts": {
   "scripts": {

+ 2 - 18
js/privatebin.js

@@ -1587,11 +1587,11 @@ window.PrivateBin = (function () {
          * @name   UiHelper.historyChange
          * @name   UiHelper.historyChange
          * @private
          * @private
          * @function
          * @function
-         * @param  {Event} event
+         * @param  {PopStateEvent} event
          */
          */
         function historyChange(event) {
         function historyChange(event) {
             let currentLocation = Helper.baseUri();
             let currentLocation = Helper.baseUri();
-            if (event.originalEvent.state === null && // no state object passed
+            if (event.state === null && // no state object passed
                 event.target.location.href === currentLocation && // target location is home page
                 event.target.location.href === currentLocation && // target location is home page
                 window.location.href === currentLocation // and we are not already on the home page
                 window.location.href === currentLocation // and we are not already on the home page
             ) {
             ) {
@@ -1613,22 +1613,6 @@ window.PrivateBin = (function () {
         };
         };
 
 
 
 
-        /**
-         * trigger a history (pop) state change
-         *
-         * used to test the UiHelper.historyChange private function
-         *
-         * @name   UiHelper.mockHistoryChange
-         * @function
-         * @param  {string} state   (optional) state to mock
-         */
-        me.mockHistoryChange = function (state) {
-            if (typeof state === 'undefined') {
-                state = null;
-            }
-            historyChange({ originalEvent: new PopStateEvent('popstate', { state: state }), target: window });
-        };
-
         /**
         /**
          * initialize
          * initialize
          *
          *

+ 17 - 5
js/test/UiHelper.js

@@ -3,8 +3,6 @@ const common = require('../common');
 const fc = require('fast-check');
 const fc = require('fast-check');
 
 
 describe('UiHelper', function () {
 describe('UiHelper', function () {
-    // TODO: As per https://github.com/tmpvar/jsdom/issues/1565 there is no navigation support in jsdom, yet.
-    // for now we use a mock function to trigger the event
     describe('historyChange', function () {
     describe('historyChange', function () {
         this.timeout(30000);
         this.timeout(30000);
         beforeEach(function () {
         beforeEach(function () {
@@ -12,6 +10,21 @@ describe('UiHelper', function () {
             cleanup();
             cleanup();
         });
         });
 
 
+
+        /** @param {object?} state */
+        function dispatchPopState(state = null) {
+            let eventError = null;
+            const captureError = event => {
+                eventError = event.error;
+                event.preventDefault();
+            };
+            window.addEventListener('error', captureError);
+            PrivateBin.UiHelper.init();
+            window.dispatchEvent(new window.PopStateEvent('popstate', {state: state}));
+            window.removeEventListener('error', captureError);
+            assert.strictEqual(eventError, null);
+        }
+
         it('redirects to home, when the state is null', () => {
         it('redirects to home, when the state is null', () => {
             fc.assert(fc.property(
             fc.assert(fc.property(
                 common.fcUrl(false, false),
                 common.fcUrl(false, false),
@@ -19,7 +32,7 @@ describe('UiHelper', function () {
                     const expected = common.urlToString(url),
                     const expected = common.urlToString(url),
                         clean = globalThis.cleanup('', {url: expected});
                         clean = globalThis.cleanup('', {url: expected});
 
 
-                    PrivateBin.UiHelper.mockHistoryChange();
+                    dispatchPopState();
                     PrivateBin.Helper.reset();
                     PrivateBin.Helper.reset();
                     var result = window.location.href;
                     var result = window.location.href;
                     clean();
                     clean();
@@ -37,7 +50,7 @@ describe('UiHelper', function () {
                     const expected = common.urlToString(url),
                     const expected = common.urlToString(url),
                         clean = globalThis.cleanup('', {url: expected});
                         clean = globalThis.cleanup('', {url: expected});
 
 
-                    PrivateBin.UiHelper.mockHistoryChange([
+                    dispatchPopState([
                         {type: 'newpaste'}, '', expected
                         {type: 'newpaste'}, '', expected
                     ]);
                     ]);
                     PrivateBin.Helper.reset();
                     PrivateBin.Helper.reset();
@@ -77,4 +90,3 @@ describe('UiHelper', function () {
         */
         */
     });
     });
 });
 });
-

+ 47 - 20
lib/I18n.php

@@ -14,6 +14,9 @@ namespace PrivateBin;
 use AppendIterator;
 use AppendIterator;
 use GlobIterator;
 use GlobIterator;
 
 
+use function array_key_exists;
+use function in_array;
+
 /**
 /**
  * I18n
  * I18n
  *
  *
@@ -116,8 +119,8 @@ class I18n
         array_unshift($args, $messageId);
         array_unshift($args, $messageId);
         if (is_array(self::$_translations[$messageId])) {
         if (is_array(self::$_translations[$messageId])) {
             $number = (int) $args[1];
             $number = (int) $args[1];
-            $key    = self::_getPluralForm($number);
-            $max    = count(self::$_translations[$messageId]) - 1;
+            $key = self::_getPluralForm($number);
+            $max = count(self::$_translations[$messageId]) - 1;
             if ($key > $max) {
             if ($key > $max) {
                 $key = $max;
                 $key = $max;
             }
             }
@@ -176,23 +179,47 @@ class I18n
         }
         }
         // find a translation file matching the browsers language preferences
         // find a translation file matching the browsers language preferences
         else {
         else {
-            self::$_language = self::_getMatchingLanguage(
-                self::_normalizeBrowserLanguages(
-                    self::getBrowserLanguages(), $availableLanguages
-                ),
-                $availableLanguages
-            );
+            self::$_language = self::_getLanguageFromHttpHeader($availableLanguages);
         }
         }
 
 
         // load translations
         // load translations
         if (self::$_language === 'en') {
         if (self::$_language === 'en') {
             self::$_translations = [];
             self::$_translations = [];
         } else {
         } else {
-            $data                = file_get_contents(self::_getPath(self::$_language . '.json'));
+            $data = file_get_contents(self::_getPath(self::$_language . '.json'));
             self::$_translations = Json::decode($data);
             self::$_translations = Json::decode($data);
         }
         }
     }
     }
 
 
+    /**
+     * get languages bsed on the HTTP header
+     *
+     * @access private
+     * @static
+     * @param  array $availableLanguages
+     * @return string
+     */
+    private static function _getLanguageFromHttpHeader($availableLanguages)
+    {
+        if (!array_key_exists('HTTP_ACCEPT_LANGUAGE', $_SERVER)) {
+            return self::$_languageFallback;
+        }
+
+        // as per https://stackoverflow.com/a/9051957/5008962 we can use the I10n module
+        // if loaded, but it only returns the primary preference (aka one string)
+        if (function_exists('locale_accept_from_http')) {
+            $detectedBestLanguage = locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE']);
+            if (in_array($detectedBestLanguage, $availableLanguages)) {
+                return $detectedBestLanguage;
+            }
+        }
+
+        return self::_getMatchingLanguage(
+            self::_normalizeBrowserLanguages(self::getBrowserLanguages(), $availableLanguages),
+            $availableLanguages
+        );
+    }
+
     /**
     /**
      * get list of available translations based on files found
      * get list of available translations based on files found
      *
      *
@@ -204,7 +231,7 @@ class I18n
     {
     {
         if (count(self::$_availableLanguages) === 0) {
         if (count(self::$_availableLanguages) === 0) {
             self::$_availableLanguages[] = 'en'; // en.json is not part of the release archive
             self::$_availableLanguages[] = 'en'; // en.json is not part of the release archive
-            $languageIterator            = new AppendIterator();
+            $languageIterator = new AppendIterator();
             $languageIterator->append(new GlobIterator(self::_getPath('??.json')));
             $languageIterator->append(new GlobIterator(self::_getPath('??.json')));
             $languageIterator->append(new GlobIterator(self::_getPath('???.json'))); // for jbo
             $languageIterator->append(new GlobIterator(self::_getPath('???.json'))); // for jbo
             $languageIterator->append(new GlobIterator(self::_getPath('??-??.json'))); // for regional variants like zh-tw
             $languageIterator->append(new GlobIterator(self::_getPath('??-??.json'))); // for regional variants like zh-tw
@@ -234,7 +261,7 @@ class I18n
             $languageRanges = explode(',', trim($_SERVER['HTTP_ACCEPT_LANGUAGE']));
             $languageRanges = explode(',', trim($_SERVER['HTTP_ACCEPT_LANGUAGE']));
             foreach ($languageRanges as $languageRange) {
             foreach ($languageRanges as $languageRange) {
                 if (preg_match(
                 if (preg_match(
-                    '/(\*|[a-zA-Z0-9]{1,8}(?:-[a-zA-Z0-9]{1,8})*)(?:\s*;\s*q\s*=\s*(0(?:\.\d{0,3})|1(?:\.0{0,3})))?/',
+                    '/^(\*|[a-zA-Z0-9]{1,8}(?:-[a-zA-Z0-9]{1,8})*)(?:\s*;\s*q\s*=\s*(0(?:\.\d{0,3})|1(?:\.0{0,3})))?$/',
                     trim($languageRange), $match
                     trim($languageRange), $match
                 )) {
                 )) {
                     if (!isset($match[2])) {
                     if (!isset($match[2])) {
@@ -265,7 +292,7 @@ class I18n
     protected static function _normalizeBrowserLanguages($languages, $availableLanguages)
     protected static function _normalizeBrowserLanguages($languages, $availableLanguages)
     {
     {
         // Base zh stays before zh-tw because regional locales are appended after base locales.
         // Base zh stays before zh-tw because regional locales are appended after base locales.
-        $hasSimplifiedChinese  = in_array('zh', $availableLanguages, true);
+        $hasSimplifiedChinese = in_array('zh', $availableLanguages, true);
         $hasTraditionalChinese = in_array('zh-tw', $availableLanguages, true);
         $hasTraditionalChinese = in_array('zh-tw', $availableLanguages, true);
         foreach ($languages as $quality => $languageRanges) {
         foreach ($languages as $quality => $languageRanges) {
             foreach ($languageRanges as $index => $languageRange) {
             foreach ($languageRanges as $index => $languageRange) {
@@ -313,7 +340,7 @@ class I18n
     {
     {
         $file = self::_getPath('languages.json');
         $file = self::_getPath('languages.json');
         if (count(self::$_languageLabels) === 0 && is_readable($file)) {
         if (count(self::$_languageLabels) === 0 && is_readable($file)) {
-            $data                  = file_get_contents($file);
+            $data = file_get_contents($file);
             self::$_languageLabels = Json::decode($data);
             self::$_languageLabels = Json::decode($data);
         }
         }
         if (count($languages) === 0) {
         if (count($languages) === 0) {
@@ -442,18 +469,18 @@ class I18n
     protected static function _getMatchingLanguage($acceptedLanguages, $availableLanguages)
     protected static function _getMatchingLanguage($acceptedLanguages, $availableLanguages)
     {
     {
         $matches = [];
         $matches = [];
-        $any     = false;
+        $any = false;
         foreach ($acceptedLanguages as $acceptedQuality => $acceptedValues) {
         foreach ($acceptedLanguages as $acceptedQuality => $acceptedValues) {
             $acceptedQuality = floatval($acceptedQuality);
             $acceptedQuality = floatval($acceptedQuality);
             if ($acceptedQuality === 0.0) {
             if ($acceptedQuality === 0.0) {
                 continue;
                 continue;
             }
             }
-            foreach ($availableLanguages as $availableValue) {
-                $availableQuality = 1.0;
-                foreach ($acceptedValues as $acceptedValue) {
-                    if ($acceptedValue === '*') {
-                        $any = true;
-                    }
+            foreach ($acceptedValues as $acceptedValue) {
+                if ($acceptedValue === '*') {
+                    $any = true;
+                }
+                foreach ($availableLanguages as $availableValue) {
+                    $availableQuality = 1.0;
                     $matchingGrade = self::_matchLanguage($acceptedValue, $availableValue);
                     $matchingGrade = self::_matchLanguage($acceptedValue, $availableValue);
                     if ($matchingGrade > 0) {
                     if ($matchingGrade > 0) {
                         $q = (string) ($acceptedQuality * $availableQuality * $matchingGrade);
                         $q = (string) ($acceptedQuality * $availableQuality * $matchingGrade);

+ 20 - 6
tst/I18nTest.php

@@ -44,7 +44,7 @@ class I18nTest extends TestCase
     public function testTranslationFallback()
     public function testTranslationFallback()
     {
     {
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'foobar';
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'foobar';
-        $messageId                       = 'It does not matter if the message ID exists';
+        $messageId = 'It does not matter if the message ID exists';
         I18n::loadTranslations();
         I18n::loadTranslations();
         $this->assertEquals($messageId, I18n::_($messageId), 'fallback to en');
         $this->assertEquals($messageId, I18n::_($messageId), 'fallback to en');
         I18n::getLanguageLabels();
         I18n::getLanguageLabels();
@@ -80,6 +80,14 @@ class I18nTest extends TestCase
         $this->assertEquals('2 heures', I18n::_('%d hours', 2), '2 hours in French');
         $this->assertEquals('2 heures', I18n::_('%d hours', 2), '2 hours in French');
     }
     }
 
 
+    public function testEqualQualityPreservesBrowserOrder()
+    {
+        $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'fr;q=0.8,de;q=0.8';
+        I18n::loadTranslations();
+
+        $this->assertEquals('fr', I18n::getLanguage());
+    }
+
     public function testBulgarianMonthExpirationTranslation()
     public function testBulgarianMonthExpirationTranslation()
     {
     {
         $_COOKIE['lang'] = 'bg';
         $_COOKIE['lang'] = 'bg';
@@ -212,6 +220,12 @@ class I18nTest extends TestCase
         $this->assertEquals('14 minut',  I18n::_('%d minutes', 14), '14 minutes in Czech');
         $this->assertEquals('14 minut',  I18n::_('%d minutes', 14), '14 minutes in Czech');
     }
     }
 
 
+    public function testInvalidBrowserLanguageQualityIsIgnored()
+    {
+        $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'de;q=2,en;q=0.8';
+        $this->assertSame(['0.8' => ['en']], I18n::getBrowserLanguages());
+    }
+
     public function testBrowserLanguageAnyDetection()
     public function testBrowserLanguageAnyDetection()
     {
     {
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = '*';
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = '*';
@@ -230,7 +244,7 @@ class I18nTest extends TestCase
     {
     {
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'foobar';
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'foobar';
         I18n::loadTranslations();
         I18n::loadTranslations();
-        $input  = '&<>"\'/`=';
+        $input = '&<>"\'/`=';
         $result = htmlspecialchars($input, ENT_QUOTES | ENT_HTML5 | ENT_DISALLOWED, 'UTF-8', false);
         $result = htmlspecialchars($input, ENT_QUOTES | ENT_HTML5 | ENT_DISALLOWED, 'UTF-8', false);
         $this->assertEquals($result, I18n::encode($input), 'encodes HTML entities');
         $this->assertEquals($result, I18n::encode($input), 'encodes HTML entities');
         $this->assertEquals('<a>some ' . $result . ' + 1</a>', I18n::_('<a>some %s + %d</a>', $input, 1), 'encodes parameters in translations');
         $this->assertEquals('<a>some ' . $result . ' + 1</a>', I18n::_('<a>some %s + %d</a>', $input, 1), 'encodes parameters in translations');
@@ -313,7 +327,7 @@ class I18nTest extends TestCase
     public function testMessageIdsExistInAllLanguages()
     public function testMessageIdsExistInAllLanguages()
     {
     {
         $messageIds = [];
         $messageIds = [];
-        $languages  = [];
+        $languages = [];
         foreach (new DirectoryIterator(PATH . 'i18n') as $file) {
         foreach (new DirectoryIterator(PATH . 'i18n') as $file) {
             $fileNameLength = strlen($file->getFilename());
             $fileNameLength = strlen($file->getFilename());
             if ($fileNameLength === 7) {       // xx.json
             if ($fileNameLength === 7) {       // xx.json
@@ -325,9 +339,9 @@ class I18nTest extends TestCase
             } else {
             } else {
                 continue;
                 continue;
             }
             }
-            $languageJson         = file_get_contents($file->getPathname());
-            $languageMessageIds   = array_keys(Json::decode($languageJson));
-            $messageIds           = array_unique(array_merge($messageIds, $languageMessageIds));
+            $languageJson = file_get_contents($file->getPathname());
+            $languageMessageIds = array_keys(Json::decode($languageJson));
+            $messageIds = array_unique(array_merge($messageIds, $languageMessageIds));
             $languages[$language] = $languageMessageIds;
             $languages[$language] = $languageMessageIds;
         }
         }
         foreach ($messageIds as $messageId) {
         foreach ($messageIds as $messageId) {