diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 303ce4d8aa6..51a2cdf6f50 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -43,6 +43,7 @@ If your change is **user-facing** please prepend the first line of your commit w - ✨ Feature - 🎨 Improvement / change - 🐛 Bug Fix +- 🌐 i18n (translation) submissions - 💡 Anything else flagged to users or whoever is writing release notes Good commit message examples: [one](https://github.com/TryGhost/Ghost/commit/61db6defde3b10a4022c86efac29cf15ae60983f), [two](https://github.com/TryGhost/Ghost/commit/b392d1925a9f961d7b4bf781ee86393a7773ed4b) and [three](https://github.com/TryGhost/Ghost/commit/e4807a779c28a754e3f8ae871a26a8aad12ca9a9). diff --git a/.github/scripts/bump-version.js b/.github/scripts/bump-version.js new file mode 100644 index 00000000000..95ece268ab3 --- /dev/null +++ b/.github/scripts/bump-version.js @@ -0,0 +1,45 @@ +const fs = require('fs/promises'); +const exec = require('util').promisify(require('child_process').exec); +const path = require('path'); + +const core = require('@actions/core'); +const semver = require('semver'); + +(async () => { + const corePackageJsonPath = path.join(__dirname, '../../ghost/core/package.json'); + const corePackageJson = require(corePackageJsonPath); + + const current_version = corePackageJson.version; + console.log(`Current version: ${current_version}`); + + const firstArg = process.argv[2]; + console.log('firstArg', firstArg); + + const buildString = await exec('git rev-parse --short HEAD').then(({stdout}) => stdout.trim()); + + let newVersion; + + if (firstArg === 'canary') { + const bumpedVersion = semver.inc(current_version, 'minor'); + newVersion = `${bumpedVersion}-pre-g${buildString}`; + } else { + const gitVersion = await exec('git describe --long HEAD').then(({stdout}) => stdout.trim().replace(/^v/, '')); + newVersion = gitVersion; + } + + newVersion += '+moya'; + console.log('newVersion', newVersion); + + corePackageJson.version = newVersion; + await fs.writeFile(corePackageJsonPath, JSON.stringify(corePackageJson, null, 2)); + + const adminPackageJsonPath = path.join(__dirname, '../../ghost/admin/package.json'); + const adminPackageJson = require(adminPackageJsonPath); + adminPackageJson.version = newVersion; + await fs.writeFile(adminPackageJsonPath, JSON.stringify(adminPackageJson, null, 2)); + + console.log('Version bumped to', newVersion); + + core.setOutput('BUILD_VERSION', newVersion); + core.setOutput('GIT_COMMIT_HASH', buildString) +})(); diff --git a/.github/scripts/dev.js b/.github/scripts/dev.js index a494511e0a8..712d163d6aa 100644 --- a/.github/scripts/dev.js +++ b/.github/scripts/dev.js @@ -231,6 +231,10 @@ async function handleStripe() { process.exit(0); } + process.env.NX_DISABLE_DB = "true"; + await exec("yarn nx reset --onlyDaemon"); + await exec("yarn nx daemon --start"); + console.log(`Running projects: ${commands.map(c => chalk.green(c.name)).join(', ')}`); const {result} = concurrently(commands, { diff --git a/.github/scripts/docker-compose.yml b/.github/scripts/docker-compose.yml index c9ba7eda1c6..e9ac3fe1021 100644 --- a/.github/scripts/docker-compose.yml +++ b/.github/scripts/docker-compose.yml @@ -28,5 +28,27 @@ services: ports: - "6379:6379" restart: always + prometheus: + profiles: [monitoring] + image: prom/prometheus:v2.30.3 + container_name: ghost-prometheus + ports: + - "9090:9090" + restart: always + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + grafana: + profiles: [monitoring] + image: grafana/grafana:8.2.3 + container_name: ghost-grafana + ports: + - "3000:3000" + restart: always + environment: + - GF_AUTH_ANONYMOUS_ENABLED=true + volumes: + - ./grafana/datasources:/etc/grafana/provisioning/datasources + - ./grafana/dashboard.yml:/etc/grafana/provisioning/dashboards/main.yaml + - ./grafana/dashboards:/var/lib/grafana/dashboards volumes: mysql-data: diff --git a/.github/scripts/grafana/dashboard.yml b/.github/scripts/grafana/dashboard.yml new file mode 100644 index 00000000000..d50b2658697 --- /dev/null +++ b/.github/scripts/grafana/dashboard.yml @@ -0,0 +1,15 @@ +## This file is used to point to the folder where the dashboards are stored +## To edit or create a dashboard, add a .json file to the ./dashboards folder + +apiVersion: 1 + +providers: + - name: "Dashboard provider" + orgId: 1 + type: file + disableDeletion: false + updateIntervalSeconds: 10 + allowUiUpdates: false + options: + path: /var/lib/grafana/dashboards + foldersFromFilesStructure: true \ No newline at end of file diff --git a/.github/scripts/grafana/dashboards/main-dashboard.json b/.github/scripts/grafana/dashboards/main-dashboard.json new file mode 100644 index 00000000000..a753bdc2071 --- /dev/null +++ b/.github/scripts/grafana/dashboards/main-dashboard.json @@ -0,0 +1,1634 @@ +{ + "__inputs": [], + "__requires": [ + { + "id": "grafana", + "name": "Grafana", + "type": "grafana", + "version": "7.4.3" + }, + { + "id": "graph", + "name": "Graph", + "type": "panel", + "version": "" + }, + { + "id": "prometheus", + "name": "Prometheus", + "type": "datasource", + "version": "1.0.0" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + }, + { + "datasource": "$datasource", + "enable": true, + "expr": "ghost_process_start_time_seconds{job=~\"$job\", instance=~\"$instance\"} * 1000", + "hide": false, + "iconColor": "#B877D9", + "name": "Ghost Start", + "showIn": 0, + "textFormat": "{{instance}}", + "titleFormat": "Ghost Start", + "useValueForTime": true + } + ] + }, + "description": "An overview of the Ghost process metrics.", + "editable": true, + "gnetId": 14058, + "graphTooltip": 0, + "id": 1, + "iteration": 1608497517213, + "links": [], + "panels": [ + { + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 1, + "title": "Overview", + "type": "row" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "$datasource", + "description": "The version of Node.js.", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 2, + "w": 3, + "x": 0, + "y": 1 + }, + "id": 2, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "pluginVersion": "6.6.2", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false, + "ymax": null, + "ymin": null + }, + "tableColumn": "version", + "targets": [ + { + "expr": "ghost_nodejs_version_info", + "format": "table", + "instant": true, + "legendFormat": "", + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "Node.js Version", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "first" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "$datasource", + "description": "The number of times Node.js restarted.", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "format": "none", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 2, + "w": 3, + "x": 3, + "y": 1 + }, + "id": 3, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "pluginVersion": "6.6.2", + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false, + "ymax": null, + "ymin": null + }, + "tableColumn": "", + "targets": [ + { + "expr": "sum(changes(ghost_process_start_time_seconds{job=~\"$job\", instance=~\"$instance\", hostname=~\"$hostname\"}[$__range]))", + "instant": false, + "legendFormat": "Node.js", + "refId": "A" + } + ], + "thresholds": "", + "timeFrom": null, + "timeShift": null, + "title": "Node.js Restarts", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [], + "valueName": "current" + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 3 + }, + "id": 4, + "panels": [], + "title": "Process", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "description": "CPU usage.", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 4 + }, + "hiddenSeries": false, + "id": 5, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": false, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.0-beta2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "irate(ghost_process_cpu_user_seconds_total{job=~\"$job\", instance=~\"$instance\"}[1m]) * 100", + "interval": "", + "legendFormat": "User CPU - {{instance}}", + "refId": "A" + }, + { + "expr": "irate(ghost_process_cpu_system_seconds_total{job=~\"$job\", instance=~\"$instance\"}[1m]) * 100", + "interval": "", + "legendFormat": "System CPU - {{instance}}", + "refId": "B" + }, + { + "expr": "irate(ghost_process_cpu_seconds_total{job=~\"$job\", instance=~\"$instance\"}[1m]) * 100", + "interval": "", + "legendFormat": "Total CPU - {{instance}}", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "CPU Usage", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "percent", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "description": "CPU time spent.", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 4 + }, + "hiddenSeries": false, + "id": 6, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": false, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.0-beta2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(ghost_process_cpu_seconds_total{job=~\"$job\", instance=~\"$instance\"}[$interval])", + "interval": "", + "legendFormat": "{{instance}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "CPU Time Spent", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "description": "Memory usage.", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 4 + }, + "hiddenSeries": false, + "id": 7, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.0-beta2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "process_resident_memory_bytes{job=~\"$job\", instance=~\"$instance\"}", + "interval": "", + "legendFormat": "Process Memory - {{instance}}", + "refId": "A" + }, + { + "expr": "ghost_nodejs_heap_size_total_bytes{job=~\"$job\", instance=~\"$instance\"}", + "interval": "", + "legendFormat": "Heap Total - {{instance}}", + "refId": "B" + }, + { + "expr": "ghost_nodejs_heap_size_used_bytes{job=~\"$job\", instance=~\"$instance\"}", + "interval": "", + "legendFormat": "Heap Used - {{instance}}", + "refId": "C" + }, + { + "expr": "ghost_nodejs_external_memory_bytes{job=~\"$job\", instance=~\"$instance\"}", + "interval": "", + "legendFormat": "External Memory - {{instance}}", + "refId": "D" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Memory Usage", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "description": "Number of active handle and active requests.", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 13 + }, + "hiddenSeries": false, + "id": 8, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.0-beta2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ghost_nodejs_active_handles_total{job=~\"$job\", instance=~\"$instance\"}", + "interval": "", + "legendFormat": "Active Handler - {{instance}}", + "refId": "A" + }, + { + "expr": "ghost_nodejs_active_requests_total{job=~\"$job\", instance=~\"$instance\"}", + "interval": "", + "legendFormat": "Active Request - {{instance}}", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Active Handlers and Requests", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "description": "Latency of the event loop.", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 13 + }, + "hiddenSeries": false, + "id": 9, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": false, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.0-beta2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ghost_nodejs_eventloop_lag_seconds{job=~\"$job\", instance=~\"$instance\"}", + "hide": false, + "instant": false, + "interval": "", + "legendFormat": "Last - {{instance}}", + "refId": "A" + }, + { + "expr": "ghost_nodejs_eventloop_lag_p99_seconds{job=~\"$job\", instance=~\"$instance\"}", + "interval": "", + "legendFormat": "P99 - {{instance}}", + "refId": "B" + }, + { + "expr": "ghost_nodejs_eventloop_lag_p50_seconds{job=~\"$job\", instance=~\"$instance\"}", + "interval": "", + "legendFormat": "P50 - {{instance}}", + "refId": "C" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Event Loop Latency", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "collapsed": false, + "datasource": null, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 22 + }, + "id": 10, + "panels": [], + "title": "Garbage Collector", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "description": "Rate of garbage collection duration.", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 23 + }, + "hiddenSeries": false, + "id": 11, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.0-beta2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(ghost_nodejs_gc_duration_seconds_sum{job=~\"$job\", instance=~\"$instance\"}[$interval])", + "interval": "", + "legendFormat": "{{kind}} - {{instance}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "GC Duration Rate", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "s", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "description": "Duration of garbage collection.", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 23 + }, + "hiddenSeries": false, + "id": 12, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.0-beta2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ghost_nodejs_gc_duration_seconds_sum{job=~\"$job\", instance=~\"$instance\"}", + "interval": "", + "legendFormat": "{{kind}} - {{instance}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "GC Duration", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "s", + "label": "", + "logBase": 2, + "max": null, + "min": null, + "show": true + }, + { + "format": "s", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "description": "Usage of heap memory.", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 23 + }, + "hiddenSeries": false, + "id": 13, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.0-beta2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ghost_nodejs_heap_size_total_bytes{job=~\"$job\", instance=~\"$instance\"}", + "interval": "", + "legendFormat": "Total - {{instance}}", + "refId": "A" + }, + { + "expr": "ghost_nodejs_heap_size_used_bytes{job=~\"$job\", instance=~\"$instance\"}", + "interval": "", + "legendFormat": "Used - {{instance}}", + "refId": "B" + }, + { + "expr": "process_resident_memory_bytes{job=~\"$job\", instance=~\"$instance\"}", + "interval": "", + "legendFormat": "Resident - {{instance}}", + "refId": "C" + }, + { + "expr": "ghost_nodejs_external_memory_bytes{job=~\"$job\", instance=~\"$instance\"}", + "interval": "", + "legendFormat": "External - {{instance}}", + "refId": "D" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Heap Memory Usage", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "description": "Rate of garbage collection.", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 31 + }, + "hiddenSeries": false, + "id": 14, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.0-beta2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(ghost_nodejs_gc_duration_seconds_count{job=~\"$job\", instance=~\"$instance\"}[$interval])", + "interval": "", + "legendFormat": "{{instance}} - {{kind}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "GC Rate", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "description": "Count of garbage collection.", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 31 + }, + "hiddenSeries": false, + "id": 15, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.0-beta2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "ghost_nodejs_gc_duration_seconds_count{job=~\"$job\", instance=~\"$instance\"}", + "interval": "", + "legendFormat": "{{kind}} - {{instance}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "GC Count", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "decimals": null, + "format": "short", + "label": "", + "logBase": 2, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": false + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "$datasource", + "description": "Usage of heap space.", + "fieldConfig": { + "defaults": { + "custom": {}, + "links": [] + }, + "overrides": [] + }, + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 31 + }, + "hiddenSeries": false, + "id": 16, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "sort": "current", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 1, + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "7.3.0-beta2", + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "expr": "ghost_nodejs_heap_space_size_used_bytes{instance=~\"$instance\"}", + "interval": "", + "legendFormat": "{{space}} - {{instance}}", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Heap Space Used", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": false, + "schemaVersion": 26, + "style": "dark", + "tags": [ + "node.js", + "nodejs" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "prometheus", + "value": "prometheus" + }, + "error": null, + "hide": 0, + "includeAll": false, + "label": "Datasource", + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "allValue": null, + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": "$datasource", + "definition": "label_values(ghost_nodejs_eventloop_lag_seconds, job)", + "error": null, + "hide": 0, + "includeAll": true, + "label": "Job", + "multi": true, + "name": "job", + "options": [], + "query": "label_values(ghost_nodejs_eventloop_lag_seconds, job)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": null, + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": "$datasource", + "definition": "label_values(ghost_nodejs_eventloop_lag_seconds, instance)", + "error": null, + "hide": 0, + "includeAll": true, + "label": "Instance", + "multi": true, + "name": "instance", + "options": [], + "query": "label_values(ghost_nodejs_eventloop_lag_seconds, instance)", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tags": [], + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "auto": false, + "auto_count": 30, + "auto_min": "10s", + "current": { + "selected": false, + "text": "1m", + "value": "1m" + }, + "error": null, + "hide": 0, + "label": "Interval", + "name": "interval", + "options": [ + { + "selected": true, + "text": "1m", + "value": "1m" + }, + { + "selected": false, + "text": "10m", + "value": "10m" + }, + { + "selected": false, + "text": "30m", + "value": "30m" + }, + { + "selected": false, + "text": "1h", + "value": "1h" + }, + { + "selected": false, + "text": "6h", + "value": "6h" + }, + { + "selected": false, + "text": "12h", + "value": "12h" + }, + { + "selected": false, + "text": "1d", + "value": "1d" + }, + { + "selected": false, + "text": "7d", + "value": "7d" + }, + { + "selected": false, + "text": "14d", + "value": "14d" + }, + { + "selected": false, + "text": "30d", + "value": "30d" + } + ], + "query": "1m,10m,30m,1h,6h,12h,1d,7d,14d,30d", + "refresh": 2, + "skipUrlSync": false, + "type": "interval" + } + ] + }, + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ] + }, + "timezone": "", + "title": "Ghost Dashboard", + "uid": "yX2d7k1Gk", + "version": 1 + } \ No newline at end of file diff --git a/.github/scripts/grafana/datasources/datasource.yml b/.github/scripts/grafana/datasources/datasource.yml new file mode 100644 index 00000000000..c6d21399a60 --- /dev/null +++ b/.github/scripts/grafana/datasources/datasource.yml @@ -0,0 +1,9 @@ +apiVersion: 1 + +datasources: +- name: Prometheus + type: prometheus + url: http://prometheus:9090 + isDefault: true + access: proxy + editable: true \ No newline at end of file diff --git a/.github/scripts/prometheus/prometheus.yml b/.github/scripts/prometheus/prometheus.yml new file mode 100644 index 00000000000..f9852993a55 --- /dev/null +++ b/.github/scripts/prometheus/prometheus.yml @@ -0,0 +1,31 @@ +global: + scrape_interval: 15s # By default, scrape targets every 15 seconds. + + # Attach these labels to any time series or alerts when communicating with + # external systems (federation, remote storage, Alertmanager). + external_labels: + monitor: 'codelab-monitor' + +# A scrape configuration containing exactly one endpoint to scrape: +# Here it's Prometheus itself. +scrape_configs: + # The job name is added as a label `job=` to any timeseries scraped from this config. + - job_name: 'prometheus' + + # Override the global default and scrape targets from this job every 5 seconds. + scrape_interval: 5s + + static_configs: + - targets: ['localhost:9090'] + + - job_name: 'ghost' + + scrape_interval: 1s + + static_configs: + - targets: ['host.docker.internal:9416'] + + metrics_path: '/metrics' + +remote_write: + - url: http://grafana:3000/api/prom/push \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b6d2fcfd19..fa31eeda90d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,10 +5,7 @@ on: push: branches: - main - - arch - 'v5.*' - - 3.x - - 2.x env: FORCE_COLOR: 1 @@ -61,6 +58,23 @@ jobs: echo "Setting BASE_COMMIT to $BASE_COMMIT" echo "BASE_COMMIT=$BASE_COMMIT" >> $GITHUB_ENV + - name: Check user org membership + id: check_user_org_membership + if: github.event_name == 'pull_request' + run: | + echo "Looking up: ${{ github.event.pull_request.user.login }}" + ENCODED_USERNAME=$(printf '%s' '${{ github.event.pull_request.user.login }}' | jq -sRr @uri) + + LOOKUP_USER=$(curl --write-out "%{http_code}" --silent --output /dev/null --location "https://api.github.com/orgs/tryghost/members/$ENCODED_USERNAME" --header "Authorization: Bearer ${{ secrets.CANARY_DOCKER_BUILD }}") + + if [ "$LOOKUP_USER" == "204" ]; then + echo "User is in the org" + echo "is_member=true" >> $GITHUB_OUTPUT + else + echo "User is not in the org" + echo "is_member=false" >> $GITHUB_OUTPUT + fi + - name: Determine added packages uses: dorny/paths-filter@v2.12.0 id: added @@ -179,8 +193,8 @@ jobs: changed_any_code: ${{ steps.changed.outputs.any-code }} changed_new_package: ${{ steps.added.outputs.new-package }} base_commit: ${{ env.BASE_COMMIT }} - is_canary_branch: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/arch') }} is_main: ${{ env.IS_MAIN }} + member_is_in_org: ${{ steps.check_user_org_membership.outputs.is_member }} has_browser_tests_label: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'browser-tests') }} dependency_cache_key: ${{ env.cachekey }} @@ -192,7 +206,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 100 + fetch-depth: 1000 - uses: actions/setup-node@v4 env: FORCE_COLOR: 0 @@ -225,6 +239,7 @@ jobs: if: | needs.job_setup.outputs.changed_comments_ui == 'true' || needs.job_setup.outputs.changed_signup_form == 'true' + || needs.job_setup.outputs.changed_sodo_search == 'true' || needs.job_setup.outputs.changed_portal == 'true' || needs.job_setup.outputs.changed_core == 'true' steps: @@ -418,7 +433,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 100 + fetch-depth: 1000 - uses: actions/setup-node@v4 env: FORCE_COLOR: 0 @@ -814,8 +829,7 @@ jobs: env: DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }} - - run: npm --no-git-tag-version version minor # We need to artificially bump the minor version to get migrations to run - working-directory: ghost/core + - run: node .github/scripts/bump-version.js canary - run: npm pack working-directory: ghost/core @@ -950,14 +964,29 @@ jobs: ] name: Canary runs-on: ubuntu-latest - if: always() && needs.job_setup.outputs.is_canary_branch == 'true' && needs.job_setup.result == 'success' && needs.job_setup.result == 'success' + if: | + always() + && needs.job_setup.result == 'success' + && needs.job_required_tests.result == 'success' + && ( + needs.job_setup.outputs.is_main == 'true' + || ( + github.event_name == 'pull_request' + && needs.job_setup.outputs.member_is_in_org == 'true' + && contains(github.event.pull_request.labels.*.name, 'deploy-to-staging') + ) + ) steps: - name: Output needs (for debugging) run: echo "${{ toJson(needs) }}" - - name: Set env variables - run: | - echo "CANARY_BUILD_INPUTS={\"version\":\"canary\",\"environment\":\"staging\"}" >> $GITHUB_ENV + - name: Compute branch name (push) + if: github.event_name == 'push' + run: echo "branch_name=${{ github.ref_name }}" >> $GITHUB_ENV + + - name: Compute branch name (pull_request) + if: github.event_name == 'pull_request' + run: echo "branch_name=${{ github.ref }}" >> $GITHUB_ENV - name: Invoke build uses: aurelien-baudet/workflow-dispatch@v2 @@ -966,6 +995,67 @@ jobs: workflow: .github/workflows/deploy.yml ref: 'refs/heads/main' repo: TryGhost/Ghost-Moya - inputs: ${{ env.CANARY_BUILD_INPUTS }} + inputs: '{"version":"canary","environment":"staging","version_extra":"${{ env.branch_name }}"}' wait-for-completion-timeout: 25m wait-for-completion-interval: 30s + + publish_admin_x_activitypub: + needs: [ + job_setup, + job_lint, + job_unit-tests + ] + name: Publish @tryghost/admin-x-activitypub + runs-on: ubuntu-latest + if: always() && needs.job_setup.result == 'success' && needs.job_lint.result == 'success' && needs.job_unit-tests.result == 'success' && needs.job_setup.outputs.is_main == 'true' + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18.12.1' + + - name: Restore caches + uses: ./.github/actions/restore-cache + env: + DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }} + + - name: Build the package + run: yarn run nx build @tryghost/admin-x-activitypub + + - name: Check if version changed + id: version_check + working-directory: apps/admin-x-activitypub + run: | + CURRENT_VERSION=$(cat package.json | jq -r .version) + PUBLISHED_VERSION=$(npm show @tryghost/admin-x-activitypub version || echo "0.0.0") + echo "Current version: $CURRENT_VERSION" + echo "Published version: $PUBLISHED_VERSION" + if [ "$CURRENT_VERSION" = "$PUBLISHED_VERSION" ]; then + echo "Version is unchanged." + echo "version_changed=false" >> $GITHUB_ENV + else + echo "Version has changed." + echo "version_changed=true" >> $GITHUB_ENV + fi + + - name: Configure .npmrc + run: | + echo "@tryghost:registry=https://registry.npmjs.org/" >> ~/.npmrc + echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc + + - name: Publish to npm + if: env.version_changed == 'true' + working-directory: apps/admin-x-activitypub + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --access public + + - name: Purge jsdelivr cache + if: env.version_changed == 'true' + uses: gacts/purge-jsdelivr-cache@v1 + with: + url: | + https://cdn.jsdelivr.net/npm/@tryghost/admin-x-activitypub@0/dist/admin-x-activitypub.js diff --git a/.gitignore b/.gitignore index b3e179754d2..2c9f7aaae75 100644 --- a/.gitignore +++ b/.gitignore @@ -48,8 +48,10 @@ typings/ # Optional npm cache directory .npm -# Nx cache +# Nx .nxcache +.nx/cache +.nx/workspace-data # Optional eslint cache .eslintcache diff --git a/apps/admin-x-activitypub/package.json b/apps/admin-x-activitypub/package.json index ea67be64a2d..46908c82f6e 100644 --- a/apps/admin-x-activitypub/package.json +++ b/apps/admin-x-activitypub/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/admin-x-activitypub", - "version": "0.0.1", + "version": "0.1.1", "license": "MIT", "repository": { "type": "git", @@ -14,10 +14,7 @@ ], "main": "./dist/admin-x-activitypub.umd.cjs", "module": "./dist/admin-x-activitypub.js", - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - }, + "private": false, "scripts": { "dev": "vite build --watch", "dev:start": "vite", @@ -34,15 +31,10 @@ "devDependencies": { "@playwright/test": "1.46.1", "@testing-library/react": "14.3.1", - "@tryghost/admin-x-design-system": "0.0.0", - "@tryghost/admin-x-framework": "0.0.0", "@types/jest": "29.5.12", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", - "@radix-ui/react-form": "0.0.3", "jest": "29.7.0", - "react": "18.3.1", - "react-dom": "18.3.1", "ts-jest": "29.1.5" }, "nx": { @@ -70,5 +62,13 @@ ] } } + }, + "dependencies": { + "@radix-ui/react-form": "0.0.3", + "use-debounce": "10.0.3", + "@tryghost/admin-x-design-system": "0.0.0", + "@tryghost/admin-x-framework": "0.0.0", + "react": "18.3.1", + "react-dom": "18.3.1" } } diff --git a/apps/admin-x-activitypub/src/api/activitypub.test.ts b/apps/admin-x-activitypub/src/api/activitypub.test.ts index 17abd51cb29..88b7dab110b 100644 --- a/apps/admin-x-activitypub/src/api/activitypub.test.ts +++ b/apps/admin-x-activitypub/src/api/activitypub.test.ts @@ -454,13 +454,13 @@ describe('ActivityPubAPI', function () { }] }) }, - 'https://activitypub.api/.ghost/activitypub/activities/index?limit=50&includeOwn=false': { + 'https://activitypub.api/.ghost/activitypub/activities/index?limit=50': { response: JSONResponse({ items: [{type: 'Create', object: {type: 'Note'}}], nextCursor: 'next-cursor' }) }, - 'https://activitypub.api/.ghost/activitypub/activities/index?limit=50&includeOwn=false&cursor=next-cursor': { + 'https://activitypub.api/.ghost/activitypub/activities/index?limit=50&cursor=next-cursor': { response: JSONResponse({ items: [{type: 'Announce', object: {type: 'Article'}}], nextCursor: null @@ -515,5 +515,617 @@ describe('ActivityPubAPI', function () { expect(actual).toEqual(expected); }); + + test('It fetches activities with replies', async function () { + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + 'https://activitypub.api/.ghost/activitypub/activities/index?limit=50&includeReplies=true': { + response: JSONResponse({ + items: [{type: 'Create', object: {type: 'Note'}}], + nextCursor: null + }) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getAllActivities(false, true); + const expected: Activity[] = [ + {type: 'Create', object: {type: 'Note'}} + ]; + + expect(actual).toEqual(expected); + }); + + test('It fetches filtered activities', async function () { + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + [`https://activitypub.api/.ghost/activitypub/activities/index?limit=50&filter=%7B%22type%22%3A%5B%22Create%3ANote%22%5D%7D`]: { + response: JSONResponse({ + items: [{type: 'Create', object: {type: 'Note'}}], + nextCursor: null + }) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getAllActivities(false, false, {type: ['Create:Note']}); + const expected: Activity[] = [ + {type: 'Create', object: {type: 'Note'}} + ]; + + expect(actual).toEqual(expected); + }); + }); + + describe('search', function () { + test('It returns the results of the search', async function () { + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + 'https://activitypub.api/.ghost/activitypub/actions/search?query=%40foo%40bar.baz': { + response: JSONResponse({ + profiles: [ + { + handle: '@foo@bar.baz', + name: 'Foo Bar' + } + ] + }) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.search('@foo@bar.baz'); + const expected = { + profiles: [ + { + handle: '@foo@bar.baz', + name: 'Foo Bar' + } + ] + }; + + expect(actual).toEqual(expected); + }); + }); + + describe('getFollowersForProfile', function () { + test('It returns an array of followers for a profile', async function () { + const handle = '@foo@bar.baz'; + + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + [`https://activitypub.api/.ghost/activitypub/profile/${handle}/followers`]: { + response: JSONResponse({ + followers: [ + { + actor: { + id: 'https://example.com/users/bar' + }, + isFollowing: false + }, + { + actor: { + id: 'https://example.com/users/baz' + }, + isFollowing: false + } + ], + next: null + }) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getFollowersForProfile(handle); + + expect(actual.followers).toEqual([ + { + actor: { + id: 'https://example.com/users/bar' + }, + isFollowing: false + }, + { + actor: { + id: 'https://example.com/users/baz' + }, + isFollowing: false + } + ]); + }); + + test('It returns next if it is present in the response', async function () { + const handle = '@foo@bar.baz'; + + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + [`https://activitypub.api/.ghost/activitypub/profile/${handle}/followers`]: { + response: JSONResponse({ + followers: [], + next: 'abc123' + }) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getFollowersForProfile(handle); + + expect(actual.next).toEqual('abc123'); + }); + + test('It includes next in the query when provided', async function () { + const handle = '@foo@bar.baz'; + const next = 'abc123'; + + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + [`https://activitypub.api/.ghost/activitypub/profile/${handle}/followers?next=${next}`]: { + response: JSONResponse({ + followers: [ + { + actor: { + id: 'https://example.com/users/qux' + }, + isFollowing: false + } + ], + next: null + }) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getFollowersForProfile(handle, next); + const expected = { + followers: [ + { + actor: { + id: 'https://example.com/users/qux' + }, + isFollowing: false + } + ], + next: null + }; + + expect(actual).toEqual(expected); + }); + + test('It returns a default return value when the response is null', async function () { + const handle = '@foo@bar.baz'; + + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + [`https://activitypub.api/.ghost/activitypub/profile/${handle}/followers`]: { + response: JSONResponse(null) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getFollowersForProfile(handle); + const expected = { + followers: [], + next: null + }; + + expect(actual).toEqual(expected); + }); + + test('It returns a default return value if followers is not present in the response', async function () { + const handle = '@foo@bar.baz'; + + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + [`https://activitypub.api/.ghost/activitypub/profile/${handle}/followers`]: { + response: JSONResponse({}) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getFollowersForProfile(handle); + const expected = { + followers: [], + next: null + }; + + expect(actual).toEqual(expected); + }); + + test('It returns an empty array of followers if followers in the response is not an array', async function () { + const handle = '@foo@bar.baz'; + + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + [`https://activitypub.api/.ghost/activitypub/profile/${handle}/followers`]: { + response: JSONResponse({ + followers: {} + }) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getFollowersForProfile(handle); + + expect(actual.followers).toEqual([]); + }); + }); + + describe('getFollowingForProfile', function () { + test('It returns a following arrayfor a profile', async function () { + const handle = '@foo@bar.baz'; + + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + [`https://activitypub.api/.ghost/activitypub/profile/${handle}/following`]: { + response: JSONResponse({ + following: [ + { + actor: { + id: 'https://example.com/users/bar' + }, + isFollowing: false + }, + { + actor: { + id: 'https://example.com/users/baz' + }, + isFollowing: false + } + ], + next: null + }) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getFollowingForProfile(handle); + + expect(actual.following).toEqual([ + { + actor: { + id: 'https://example.com/users/bar' + }, + isFollowing: false + }, + { + actor: { + id: 'https://example.com/users/baz' + }, + isFollowing: false + } + ]); + }); + + test('It returns next if it is present in the response', async function () { + const handle = '@foo@bar.baz'; + + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + [`https://activitypub.api/.ghost/activitypub/profile/${handle}/following`]: { + response: JSONResponse({ + following: [], + next: 'abc123' + }) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getFollowingForProfile(handle); + + expect(actual.next).toEqual('abc123'); + }); + + test('It includes next in the query when provided', async function () { + const handle = '@foo@bar.baz'; + const next = 'abc123'; + + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + [`https://activitypub.api/.ghost/activitypub/profile/${handle}/following?next=${next}`]: { + response: JSONResponse({ + following: [ + { + actor: { + id: 'https://example.com/users/qux' + }, + isFollowing: false + } + ], + next: null + }) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getFollowingForProfile(handle, next); + const expected = { + following: [ + { + actor: { + id: 'https://example.com/users/qux' + }, + isFollowing: false + } + ], + next: null + }; + + expect(actual).toEqual(expected); + }); + + test('It returns a default return value when the response is null', async function () { + const handle = '@foo@bar.baz'; + + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + [`https://activitypub.api/.ghost/activitypub/profile/${handle}/following`]: { + response: JSONResponse(null) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getFollowingForProfile(handle); + const expected = { + following: [], + next: null + }; + + expect(actual).toEqual(expected); + }); + + test('It returns a default return value if following is not present in the response', async function () { + const handle = '@foo@bar.baz'; + + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + [`https://activitypub.api/.ghost/activitypub/profile/${handle}/following`]: { + response: JSONResponse({}) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getFollowingForProfile(handle); + const expected = { + following: [], + next: null + }; + + expect(actual).toEqual(expected); + }); + + test('It returns an empty following array if following in the response is not an array', async function () { + const handle = '@foo@bar.baz'; + + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + [`https://activitypub.api/.ghost/activitypub/profile/${handle}/following`]: { + response: JSONResponse({ + following: {} + }) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getFollowingForProfile(handle); + + expect(actual.following).toEqual([]); + }); + }); + + describe('getProfile', function () { + test('It returns a profile', async function () { + const handle = '@foo@bar.baz'; + + const fakeFetch = Fetch({ + 'https://auth.api/': { + response: JSONResponse({ + identities: [{ + token: 'fake-token' + }] + }) + }, + [`https://activitypub.api/.ghost/activitypub/profile/${handle}`]: { + response: JSONResponse({ + handle, + name: 'Foo Bar' + }) + } + }); + + const api = new ActivityPubAPI( + new URL('https://activitypub.api'), + new URL('https://auth.api'), + 'index', + fakeFetch + ); + + const actual = await api.getProfile(handle); + const expected = { + handle, + name: 'Foo Bar' + }; + + expect(actual).toEqual(expected); + }); }); }); diff --git a/apps/admin-x-activitypub/src/api/activitypub.ts b/apps/admin-x-activitypub/src/api/activitypub.ts index e65a1ec8836..977c3ef75a1 100644 --- a/apps/admin-x-activitypub/src/api/activitypub.ts +++ b/apps/admin-x-activitypub/src/api/activitypub.ts @@ -3,6 +3,34 @@ export type Actor = any; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Activity = any; +export interface Profile { + actor: Actor; + handle: string; + followerCount: number; + isFollowing: boolean; + posts: Activity[]; +} + +export interface SearchResults { + profiles: Profile[]; +} + +export interface GetFollowersForProfileResponse { + followers: { + actor: Actor; + isFollowing: boolean; + }[]; + next: string | null; +} + +export interface GetFollowingForProfileResponse { + following: { + actor: Actor; + isFollowing: boolean; + }[]; + next: string | null; +} + export class ActivityPubAPI { constructor( private readonly apiUrl: URL, @@ -113,6 +141,68 @@ export class ActivityPubAPI { return 0; } + async getFollowersForProfile(handle: string, next?: string): Promise { + const url = new URL(`.ghost/activitypub/profile/${handle}/followers`, this.apiUrl); + if (next) { + url.searchParams.set('next', next); + } + + const json = await this.fetchJSON(url); + + if (json === null) { + return { + followers: [], + next: null + }; + } + + if (!('followers' in json)) { + return { + followers: [], + next: null + }; + } + + const followers = Array.isArray(json.followers) ? json.followers : []; + const nextPage = 'next' in json && typeof json.next === 'string' ? json.next : null; + + return { + followers, + next: nextPage + }; + } + + async getFollowingForProfile(handle: string, next?: string): Promise { + const url = new URL(`.ghost/activitypub/profile/${handle}/following`, this.apiUrl); + if (next) { + url.searchParams.set('next', next); + } + + const json = await this.fetchJSON(url); + + if (json === null) { + return { + following: [], + next: null + }; + } + + if (!('following' in json)) { + return { + following: [], + next: null + }; + } + + const following = Array.isArray(json.following) ? json.following : []; + const nextPage = 'next' in json && typeof json.next === 'string' ? json.next : null; + + return { + following, + next: nextPage + }; + } + async follow(username: string): Promise { const url = new URL(`.ghost/activitypub/actions/follow/${username}`, this.apiUrl); await this.fetchJSON(url, 'POST'); @@ -152,7 +242,59 @@ export class ActivityPubAPI { return new URL(`.ghost/activitypub/activities/${this.handle}`, this.apiUrl); } - async getAllActivities(includeOwn: boolean = false): Promise { + async getActivities( + includeOwn: boolean = false, + includeReplies: boolean = false, + filter: {type?: string[]} | null = null, + cursor?: string + ): Promise<{data: Activity[], nextCursor: string | null}> { + const LIMIT = 50; + + const url = new URL(this.activitiesApiUrl); + url.searchParams.set('limit', LIMIT.toString()); + if (includeOwn) { + url.searchParams.set('includeOwn', includeOwn.toString()); + } + if (includeReplies) { + url.searchParams.set('includeReplies', includeReplies.toString()); + } + if (filter) { + url.searchParams.set('filter', JSON.stringify(filter)); + } + if (cursor) { + url.searchParams.set('cursor', cursor); + } + + const json = await this.fetchJSON(url); + + if (json === null) { + return { + data: [], + nextCursor: null + }; + } + + if (!('items' in json)) { + return { + data: [], + nextCursor: null + }; + } + + const data = Array.isArray(json.items) ? json.items : []; + const nextCursor = 'nextCursor' in json && typeof json.nextCursor === 'string' ? json.nextCursor : null; + + return { + data, + nextCursor + }; + } + + async getAllActivities( + includeOwn: boolean = false, + includeReplies: boolean = false, + filter: {type?: string[]} | null = null + ): Promise { const LIMIT = 50; const fetchActivities = async (url: URL): Promise => { @@ -179,7 +321,15 @@ export class ActivityPubAPI { nextUrl.searchParams.set('cursor', json.nextCursor); nextUrl.searchParams.set('limit', LIMIT.toString()); - nextUrl.searchParams.set('includeOwn', includeOwn.toString()); + if (includeOwn) { + nextUrl.searchParams.set('includeOwn', includeOwn.toString()); + } + if (includeReplies) { + nextUrl.searchParams.set('includeReplies', includeReplies.toString()); + } + if (filter) { + nextUrl.searchParams.set('filter', JSON.stringify(filter)); + } const nextItems = await fetchActivities(nextUrl); @@ -192,7 +342,15 @@ export class ActivityPubAPI { // Make a copy of the activities API URL and set the limit const url = new URL(this.activitiesApiUrl); url.searchParams.set('limit', LIMIT.toString()); - url.searchParams.set('includeOwn', includeOwn.toString()); + if (includeOwn) { + url.searchParams.set('includeOwn', includeOwn.toString()); + } + if (includeReplies) { + url.searchParams.set('includeReplies', includeReplies.toString()); + } + if (filter) { + url.searchParams.set('filter', JSON.stringify(filter)); + } // Fetch the activities return fetchActivities(url); @@ -212,4 +370,30 @@ export class ActivityPubAPI { const json = await this.fetchJSON(this.userApiUrl); return json; } + + get searchApiUrl() { + return new URL('.ghost/activitypub/actions/search', this.apiUrl); + } + + async search(query: string): Promise { + const url = this.searchApiUrl; + + url.searchParams.set('query', query); + + const json = await this.fetchJSON(url, 'GET'); + + if (json && 'profiles' in json) { + return json as SearchResults; + } + + return { + profiles: [] + }; + } + + async getProfile(handle: string): Promise { + const url = new URL(`.ghost/activitypub/profile/${handle}`, this.apiUrl); + const json = await this.fetchJSON(url); + return json as Profile; + } } diff --git a/apps/admin-x-activitypub/src/components/Activities.tsx b/apps/admin-x-activitypub/src/components/Activities.tsx index f7e7966e65b..3ef64477a03 100644 --- a/apps/admin-x-activitypub/src/components/Activities.tsx +++ b/apps/admin-x-activitypub/src/components/Activities.tsx @@ -1,15 +1,16 @@ +import React, {useEffect, useRef} from 'react'; + import NiceModal from '@ebay/nice-modal-react'; -import React from 'react'; -import {Button, NoValueLabel} from '@tryghost/admin-x-design-system'; -import {ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub'; +import {LoadingIndicator, NoValueLabel} from '@tryghost/admin-x-design-system'; import APAvatar, {AvatarBadge} from './global/APAvatar'; import ActivityItem, {type Activity} from './activities/ActivityItem'; import ArticleModal from './feed/ArticleModal'; +import FollowButton from './global/FollowButton'; import MainNavigation from './navigation/MainNavigation'; import getUsername from '../utils/get-username'; -import {useAllActivitiesForUser, useSiteUrl} from '../hooks/useActivityPubQueries'; +import {useActivitiesForUser} from '../hooks/useActivityPubQueries'; import {useFollowersForUser} from '../MainContent'; interface ActivitiesProps {} @@ -21,13 +22,11 @@ enum ACTVITY_TYPE { FOLLOW = 'Follow' } -const getActivityDescription = (activity: Activity, activityObjectsMap: Map): string => { +const getActivityDescription = (activity: Activity): string => { switch (activity.type) { case ACTVITY_TYPE.CREATE: - const object = activityObjectsMap.get(activity.object?.inReplyTo || ''); - - if (object?.name) { - return `Commented on your article "${object.name}"`; + if (activity.object?.inReplyTo && typeof activity.object?.inReplyTo !== 'string') { + return `Commented on your article "${activity.object.inReplyTo.name}"`; } return ''; @@ -88,65 +87,46 @@ const getActivityBadge = (activity: Activity): AvatarBadge => { const Activities: React.FC = ({}) => { const user = 'index'; - let {data: activities = []} = useAllActivitiesForUser({handle: 'index', includeOwn: true}); - const siteUrl = useSiteUrl(); - - // Create a map of activity objects from activities in the inbox and outbox. - // This allows us to quickly look up an object associated with an activity - // We could just make a http request to get the object, but this is more - // efficient seeming though we already have the data in the inbox and outbox - const activityObjectsMap = new Map(); - - activities.forEach((activity) => { - if (activity.object) { - activityObjectsMap.set(activity.object.id, activity.object); + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage + } = useActivitiesForUser({ + handle: user, + includeOwn: true, + includeReplies: true, + filter: { + type: ['Follow', 'Like', `Create:Note:isReplyToOwn`] } }); - // Filter the activities to show - activities = activities.filter((activity) => { - if (activity.type === ACTVITY_TYPE.CREATE) { - // Only show "Create" activities that are replies to a post created - // by the user + const activities = (data?.pages.flatMap(page => page.data) ?? []); - const replyToObject = activityObjectsMap.get(activity.object?.inReplyTo || ''); + const observerRef = useRef(null); + const loadMoreRef = useRef(null); - // If the reply object is not found, or it doesn't have a URL or - // name, do not show the activity - if (!replyToObject || !replyToObject.url || !replyToObject.name) { - return false; - } - - // Verify that the reply is to a post created by the user by - // checking that the hostname associated with the reply object - // is the same as the hostname of the site. This is not a bullet - // proof check, but it's a good enough for now - const hostname = new URL(siteUrl).hostname; - const replyToObjectHostname = new URL(replyToObject.url).hostname; - - return hostname === replyToObjectHostname; + useEffect(() => { + if (observerRef.current) { + observerRef.current.disconnect(); } - return [ACTVITY_TYPE.FOLLOW, ACTVITY_TYPE.LIKE].includes(activity.type); - }); - - // Create a map of activity comments, grouping them by the parent activity - // This allows us to quickly look up all comments for a given activity - const commentsMap = new Map(); - - for (const activity of activities) { - if (activity.type === ACTVITY_TYPE.CREATE && activity.object?.inReplyTo) { - const comments = commentsMap.get(activity.object.inReplyTo) ?? []; - - comments.push(activity); + observerRef.current = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }); - commentsMap.set(activity.object.inReplyTo, comments.reverse()); + if (loadMoreRef.current) { + observerRef.current.observe(loadMoreRef.current); } - } - const getCommentsForObject = (id: string) => { - return commentsMap.get(id) ?? []; - }; + return () => { + if (observerRef.current) { + observerRef.current.disconnect(); + } + }; + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); // Retrieve followers for the user const {data: followers = []} = useFollowersForUser(user); @@ -167,41 +147,47 @@ const Activities: React.FC = ({}) => { )} {activities.length > 0 && ( -
- {activities?.map(activity => ( - { - NiceModal.show(ArticleModal, { - object: activity.object, - actor: activity.actor, - comments: getCommentsForObject(activity.object.id), - allComments: commentsMap - }); - } : undefined - } - > - -
-
- {activity.actor.name} - {getUsername(activity.actor)} + <> +
+ {activities?.map(activity => ( + { + NiceModal.show(ArticleModal, { + object: activity.object, + actor: activity.actor, + comments: activity.object.replies + }); + } : undefined + } + > + +
+
+ {activity.actor.name} + {getUsername(activity.actor)} +
+
{getActivityDescription(activity)}
+ {getExtendedDescription(activity)}
-
{getActivityDescription(activity, activityObjectsMap)}
- {getExtendedDescription(activity)} -
- {isFollower(activity.actor.id) === false && ( -
+ + + ))} +
+
+ {isFetchingNextPage && ( +
+ +
+ )} + )}
diff --git a/apps/admin-x-activitypub/src/components/Inbox.tsx b/apps/admin-x-activitypub/src/components/Inbox.tsx index b78923416b1..aaba45f6b39 100644 --- a/apps/admin-x-activitypub/src/components/Inbox.tsx +++ b/apps/admin-x-activitypub/src/components/Inbox.tsx @@ -3,11 +3,11 @@ import ArticleModal from './feed/ArticleModal'; import FeedItem from './feed/FeedItem'; import MainNavigation from './navigation/MainNavigation'; import NiceModal from '@ebay/nice-modal-react'; -import React, {useState} from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import {type Activity} from './activities/ActivityItem'; import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub'; -import {Button, Heading} from '@tryghost/admin-x-design-system'; -import {useAllActivitiesForUser} from '../hooks/useActivityPubQueries'; +import {Button, Heading, LoadingIndicator} from '@tryghost/admin-x-design-system'; +import {useActivitiesForUser} from '../hooks/useActivityPubQueries'; interface InboxProps {} @@ -16,38 +16,25 @@ const Inbox: React.FC = ({}) => { const [, setArticleActor] = useState(null); const [layout, setLayout] = useState('inbox'); - // Retrieve all activities for the user - let {data: activities = []} = useAllActivitiesForUser({handle: 'index'}); - - activities = activities.filter((activity: Activity) => { - const isCreate = activity.type === 'Create' && ['Article', 'Note'].includes(activity.object.type); - const isAnnounce = activity.type === 'Announce' && activity.object.type === 'Note'; - - return isCreate || isAnnounce; - }); - - // Create a map of activity comments, grouping them by the parent activity - // This allows us to quickly look up all comments for a given activity - const commentsMap = new Map(); - - for (const activity of activities) { - if (activity.type === 'Create' && activity.object.inReplyTo) { - const comments = commentsMap.get(activity.object.inReplyTo) ?? []; - - comments.push(activity); - - commentsMap.set(activity.object.inReplyTo, comments.reverse()); + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage + } = useActivitiesForUser({ + handle: 'index', + includeReplies: true, + filter: { + type: ['Create:Article', 'Create:Note', 'Announce:Note'] } - } + }); - const getCommentsForObject = (id: string) => { - return commentsMap.get(id) ?? []; - }; + const activities = (data?.pages.flatMap(page => page.data) ?? []); const handleViewContent = (object: ObjectProperties, actor: ActorProperties, comments: Activity[], focusReply = false) => { setArticleContent(object); setArticleActor(actor); - NiceModal.show(ArticleModal, {object, actor, comments, allComments: commentsMap, focusReply}); + NiceModal.show(ArticleModal, {object, actor, comments, focusReply}); }; function getContentAuthor(activity: Activity) { @@ -78,42 +65,77 @@ const Inbox: React.FC = ({}) => { setLayout(newLayout); }; + // Intersection observer to fetch more activities when the user scrolls + // to the bottom of the page + const observerRef = useRef(null); + const loadMoreRef = useRef(null); + + useEffect(() => { + if (observerRef.current) { + observerRef.current.disconnect(); + } + + observerRef.current = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }); + + if (loadMoreRef.current) { + observerRef.current.observe(loadMoreRef.current); + } + + return () => { + if (observerRef.current) { + observerRef.current.disconnect(); + } + }; + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); + return ( <>
{activities.length > 0 ? ( -
    - {activities.map((activity, index) => ( -
  • handleViewContent( - activity.object, - getContentAuthor(activity), - getCommentsForObject(activity.object.id) - )} - > - handleViewContent( + <> +
      + {activities.map((activity, index) => ( +
    • handleViewContent( activity.object, getContentAuthor(activity), - getCommentsForObject(activity.object.id), - true + activity.object.replies + )} + > + handleViewContent( + activity.object, + getContentAuthor(activity), + activity.object.replies, + true + )} + /> + {index < activities.length - 1 && ( +
      )} - /> - {index < activities.length - 1 && ( -
      - )} -
    • - ))} -
    +
  • + ))} +
+
+ {isFetchingNextPage && ( +
+ +
+ )} + ) : (
diff --git a/apps/admin-x-activitypub/src/components/Profile.tsx b/apps/admin-x-activitypub/src/components/Profile.tsx index 7ee8b798669..9a3fc14ca04 100644 --- a/apps/admin-x-activitypub/src/components/Profile.tsx +++ b/apps/admin-x-activitypub/src/components/Profile.tsx @@ -145,7 +145,10 @@ const Profile: React.FC = ({}) => { Building ActivityPub @index@activitypub.ghost.org

Ghost is federating over ActivityPub to become part of the world's largest publishing network

- activitypub.ghost.org + + Website + activitypub.ghost.org + containerClassName='mt-6' selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} />
diff --git a/apps/admin-x-activitypub/src/components/Search.tsx b/apps/admin-x-activitypub/src/components/Search.tsx index 71956458c4e..94bb0ced4a8 100644 --- a/apps/admin-x-activitypub/src/components/Search.tsx +++ b/apps/admin-x-activitypub/src/components/Search.tsx @@ -1,52 +1,176 @@ +import React, {useEffect, useRef, useState} from 'react'; + +import {Activity, ActorProperties} from '@tryghost/admin-x-framework/api/activitypub'; +import {Button, Icon, LoadingIndicator, NoValueLabel, TextField} from '@tryghost/admin-x-design-system'; +import {useDebounce} from 'use-debounce'; + import APAvatar from './global/APAvatar'; import ActivityItem from './activities/ActivityItem'; +import FollowButton from './global/FollowButton'; import MainNavigation from './navigation/MainNavigation'; -import React from 'react'; -import {Button, Icon} from '@tryghost/admin-x-design-system'; + +import NiceModal from '@ebay/nice-modal-react'; +import ProfileSearchResultModal from './search/ProfileSearchResultModal'; + +import {useSearchForUser, useSuggestedProfiles} from '../hooks/useActivityPubQueries'; + +interface SearchResultItem { + actor: ActorProperties; + handle: string; + followerCount: number; + followingCount: number; + isFollowing: boolean; + posts: Activity[]; +} + +interface SearchResultProps { + result: SearchResultItem; + update: (id: string, updated: Partial) => void; +} interface SearchProps {} +const SearchResult: React.FC = ({result, update}) => { + const onFollow = () => { + update(result.actor.id!, { + isFollowing: true, + followerCount: result.followerCount + 1 + }); + }; + + const onUnfollow = () => { + update(result.actor.id!, { + isFollowing: false, + followerCount: result.followerCount - 1 + }); + }; + + return ( + { + NiceModal.show(ProfileSearchResultModal, {profile: result, onFollow, onUnfollow}); + }} + > + +
+
+ {result.actor.name} {result.handle} +
+
{new Intl.NumberFormat().format(result.followerCount)} followers
+
+ +
+ ); +}; + const Search: React.FC = ({}) => { + // Initialise suggested profiles + const {suggestedProfilesQuery, updateSuggestedProfile} = useSuggestedProfiles('index', ['@quillmatiq@mastodon.social', '@miaq@flipboard.social', '@mallory@techpolicy.social']); + const {data: suggested = [], isLoading: isLoadingSuggested} = suggestedProfilesQuery; + + // Initialise search query + const queryInputRef = useRef(null); + const [query, setQuery] = useState(''); + const [debouncedQuery] = useDebounce(query, 300); + const [isQuerying, setIsQuerying] = useState(false); + const {searchQuery, updateProfileSearchResult: updateResult} = useSearchForUser('index', query !== '' ? debouncedQuery : query); + const {data, isFetching, isFetched} = searchQuery; + + const results = data?.profiles || []; + const showLoading = (isFetching || isQuerying) && !isFetched; + const showNoResults = isFetched && results.length === 0; + const showSuggested = query === '' || (isFetched && results.length === 0); + + useEffect(() => { + if (query !== '') { + setIsQuerying(true); + } else { + setIsQuerying(false); + } + }, [query]); + + // Focus the query input on initial render + useEffect(() => { + if (queryInputRef.current) { + queryInputRef.current.focus(); + } + }, []); + return ( <>
-
Search the Fediverse
- - -
-
Lydia Mango @username@domain.com
-
1,535 followers
-
-
+ {showLoading && ( + + )} + {showNoResults && ( + + No users matching this username + + )} + {results.map(result => ( + + ))} + {showSuggested && ( + <> + Suggested accounts + {isLoadingSuggested && ( + + )} + {suggested.map(profile => ( + + ))} + + )}
); }; -export default Search; \ No newline at end of file +export default Search; diff --git a/apps/admin-x-activitypub/src/components/_ObsoleteListIndex.tsx b/apps/admin-x-activitypub/src/components/_ObsoleteListIndex.tsx deleted file mode 100644 index 021cbbf1b65..00000000000 --- a/apps/admin-x-activitypub/src/components/_ObsoleteListIndex.tsx +++ /dev/null @@ -1,538 +0,0 @@ -import ActivityPubWelcomeImage from '../assets/images/ap-welcome.png'; -import React, {useEffect, useRef, useState} from 'react'; -import articleBodyStyles from './articleBodyStyles'; -import getRelativeTimestamp from '../utils/get-relative-timestamp'; -import getUsername from '../utils/get-username'; -import {ActivityPubAPI} from '../api/activitypub'; -import {ActorProperties, ObjectProperties} from '@tryghost/admin-x-framework/api/activitypub'; -import {Avatar, Button, ButtonGroup, Heading, Icon, List, ListItem, Page, SelectOption, SettingValue, ViewContainer, ViewTab} from '@tryghost/admin-x-design-system'; -import {useBrowseSite} from '@tryghost/admin-x-framework/api/site'; -import {useQuery} from '@tanstack/react-query'; -import {useRouting} from '@tryghost/admin-x-framework/routing'; - -interface ViewArticleProps { - object: ObjectProperties, - onBackToList: () => void; -} - -type Activity = { - type: string, - object: { - type: string - } -} - -export function useBrowseInboxForUser(handle: string) { - const site = useBrowseSite(); - const siteData = site.data?.site; - const siteUrl = siteData?.url ?? window.location.origin; - const api = new ActivityPubAPI( - new URL(siteUrl), - new URL('/ghost/api/admin/identities/', window.location.origin), - handle - ); - return useQuery({ - queryKey: [`inbox:${handle}`], - async queryFn() { - return api.getInbox(); - } - }); -} - -function useFollowersCountForUser(handle: string) { - const site = useBrowseSite(); - const siteData = site.data?.site; - const siteUrl = siteData?.url ?? window.location.origin; - const api = new ActivityPubAPI( - new URL(siteUrl), - new URL('/ghost/api/admin/identities/', window.location.origin), - handle - ); - return useQuery({ - queryKey: [`followersCount:${handle}`], - async queryFn() { - return api.getFollowersCount(); - } - }); -} - -function useFollowingCountForUser(handle: string) { - const site = useBrowseSite(); - const siteData = site.data?.site; - const siteUrl = siteData?.url ?? window.location.origin; - const api = new ActivityPubAPI( - new URL(siteUrl), - new URL('/ghost/api/admin/identities/', window.location.origin), - handle - ); - return useQuery({ - queryKey: [`followingCount:${handle}`], - async queryFn() { - return api.getFollowingCount(); - } - }); -} - -const ActivityPubComponent: React.FC = () => { - const {updateRoute} = useRouting(); - - // TODO: Replace with actual user ID - const {data: activities = []} = useBrowseInboxForUser('index'); - const {data: followersCount = 0} = useFollowersCountForUser('index'); - const {data: followingCount = 0} = useFollowingCountForUser('index'); - - const [articleContent, setArticleContent] = useState(null); - const [, setArticleActor] = useState(null); - - const handleViewContent = (object: ObjectProperties, actor: ActorProperties) => { - setArticleContent(object); - setArticleActor(actor); - }; - - const handleBackToList = () => { - setArticleContent(null); - }; - - const [selectedOption, setSelectedOption] = useState({label: 'Feed', value: 'feed'}); - - const [selectedTab, setSelectedTab] = useState('inbox'); - - const inboxTabActivities = activities.filter((activity: Activity) => { - const isCreate = activity.type === 'Create' && ['Article', 'Note'].includes(activity.object.type); - const isAnnounce = activity.type === 'Announce' && activity.object.type === 'Note'; - - return isCreate || isAnnounce; - }); - const activityTabActivities = activities.filter((activity: Activity) => activity.type === 'Create' && activity.object.type === 'Article'); - const likeTabActivies = activities.filter((activity: Activity) => activity.type === 'Like'); - - const tabs: ViewTab[] = [ - { - id: 'inbox', - title: 'Inbox', - contents: ( -
- {inboxTabActivities.length > 0 ? ( -
    - {inboxTabActivities.reverse().map(activity => ( -
  • handleViewContent(activity.object, activity.actor)} - > - -
  • - ))} -
- ) : ( -
-
- Ghost site logos - - Welcome to ActivityPub - -

- We’re so glad to have you on board! At the moment, you can follow other Ghost sites and enjoy their content right here inside Ghost. -

-

- You can see all of the users on the right—find your favorite ones and give them a follow. -

-
-
- )} -
- ) - }, - { - id: 'activity', - title: 'Activity', - contents: ( -
-
    - {activityTabActivities.reverse().map(activity => ( -
  • handleViewContent(activity.object, activity.actor)} - > - -
  • - ))} -
-
- ) - }, - { - id: 'likes', - title: 'Likes', - contents: ( -
- - {likeTabActivies.reverse().map(activity => ( - } - id='list-item' - title={ -
- {activity.actor.name} - liked your post - {activity.object.name} -
- } - /> - ))} -
-
- ) - }, - { - id: 'profile', - title: 'Profile', - contents: ( -
-
-
-
-
updateRoute('/view-following')}> - {followingCount} - Following -
-
updateRoute('/view-followers')}> - {followersCount} - Followers -
-
-
-
- ) - } - ]; - - return ( - <> - - {!articleContent ? ( - { - setSelectedOption({label: 'Feed', value: 'feed'}); - } - - }, - { - icon: 'cardview', - size: 'sm', - iconColorClass: selectedOption.value === 'inbox' ? 'text-black' : 'text-grey-500', - onClick: () => { - setSelectedOption({label: 'Inbox', value: 'inbox'}); - } - } - ]} clearBg={true} link outlineOnMobile />]} - firstOnPage={true} - primaryAction={{ - title: 'Follow', - onClick: () => { - updateRoute('follow-site'); - }, - icon: 'add' - }} - selectedTab={selectedTab} - stickyHeader={true} - tabs={tabs} - title='ActivityPub' - toolbarBorder={true} - type='page' - onTabChange={setSelectedTab} - > - - - ) : ( - - )} - - - - ); -}; - -const ArticleBody: React.FC<{heading: string, image: string|undefined, html: string}> = ({heading, image, html}) => { - const site = useBrowseSite(); - const siteData = site.data?.site; - - const iframeRef = useRef(null); - - const cssContent = articleBodyStyles(siteData?.url.replace(/\/$/, '')); - - const htmlContent = ` - - - ${cssContent} - - -
-

${heading}

-${image && - `
- ${heading} -
` -} -
-
- ${html} -
- - -`; - - useEffect(() => { - const iframe = iframeRef.current; - if (iframe) { - iframe.srcdoc = htmlContent; - } - }, [htmlContent]); - - return ( -
- -
- ); -}; - -function renderAttachment(object: ObjectProperties) { - let attachment; - if (object.image) { - attachment = object.image; - } - - if (object.type === 'Note' && !attachment) { - attachment = object.attachment; - } - - if (!attachment) { - return null; - } - - if (Array.isArray(attachment)) { - const attachmentCount = attachment.length; - - let gridClass = ''; - if (attachmentCount === 1) { - gridClass = 'grid-cols-1'; // Single image, full width - } else if (attachmentCount === 2) { - gridClass = 'grid-cols-2'; // Two images, side by side - } else if (attachmentCount === 3 || attachmentCount === 4) { - gridClass = 'grid-cols-2'; // Three or four images, two per row - } - - return ( -
- {attachment.map((item, index) => ( - {`attachment-${index}`} - ))} -
- ); - } - - switch (attachment.mediaType) { - case 'image/jpeg': - case 'image/png': - case 'image/gif': - return attachment; - case 'video/mp4': - case 'video/webm': - return
-
; - - case 'audio/mpeg': - case 'audio/ogg': - return
-
; - default: - return null; - } -} - -const ObjectContentDisplay: React.FC<{actor: ActorProperties, object: ObjectProperties, layout: string, type: string }> = ({actor, object, layout, type}) => { - const parser = new DOMParser(); - const doc = parser.parseFromString(object.content || '', 'text/html'); - - const plainTextContent = doc.body.textContent; - let previewContent = ''; - if (object.preview) { - const previewDoc = parser.parseFromString(object.preview.content || '', 'text/html'); - previewContent = previewDoc.body.textContent || ''; - } else if (object.type === 'Note') { - previewContent = plainTextContent || ''; - } - - const timestamp = - new Date(object?.published ?? new Date()).toLocaleDateString('default', {year: 'numeric', month: 'short', day: '2-digit'}) + ', ' + new Date(object?.published ?? new Date()).toLocaleTimeString('default', {hour: '2-digit', minute: '2-digit'}); - - const date = new Date(object?.published ?? new Date()); - - const [isClicked, setIsClicked] = useState(false); - const [isLiked, setIsLiked] = useState(false); - - const handleLikeClick = (event: React.MouseEvent | undefined) => { - event?.stopPropagation(); - setIsClicked(true); - setIsLiked(!isLiked); - setTimeout(() => setIsClicked(false), 300); // Reset the animation class after 300ms - }; - - let author = actor; - if (type === 'Announce' && object.type === 'Note') { - author = typeof object.attributedTo === 'object' ? object.attributedTo as ActorProperties : actor; - } - - if (layout === 'feed') { - return ( - <> - {object && ( -
- {(type === 'Announce' && object.type === 'Note') &&
-
- {actor.name} reposted -
} -
- -
-
-
- {author.name} - {getRelativeTimestamp(date)} -
-
- {getUsername(author)} -
-
-
-
- {object.name && {object.name}} -
- {/*

{object.content}

*/} - {renderAttachment(object)} -
-
-
-
-
-
-
-
- )} - - ); - } else if (layout === 'inbox') { - return ( - <> - {object && ( -
-
- - {actor.name} - {/* {getUsername(actor)} */} - {timestamp} -
-
-
-
- {object.name} -
-

{previewContent}

-
-
-
- {/* {image &&
- -
} */} -
-
- {/*
*/} -
- )} - - ); - } -}; - -const ViewArticle: React.FC = ({object, onBackToList}) => { - const {updateRoute} = useRouting(); - - const [isClicked, setIsClicked] = useState(false); - const [isLiked, setIsLiked] = useState(false); - - const handleLikeClick = (event: React.MouseEvent | undefined) => { - event?.stopPropagation(); - setIsClicked(true); - setIsLiked(!isLiked); - setTimeout(() => setIsClicked(false), 300); // Reset the animation class after 300ms - }; - - return ( - - -
-
-
-
-
-
-
-
-
-
-
- {object.type === 'Note' && ( -
- {object.content &&
} - {renderAttachment(object)} -
)} - {object.type === 'Article' && } -
-
-
- ); -}; - -export default ActivityPubComponent; diff --git a/apps/admin-x-activitypub/src/components/activities/ActivityItem.tsx b/apps/admin-x-activitypub/src/components/activities/ActivityItem.tsx index e48e04323a1..3dbe87f37b5 100644 --- a/apps/admin-x-activitypub/src/components/activities/ActivityItem.tsx +++ b/apps/admin-x-activitypub/src/components/activities/ActivityItem.tsx @@ -6,7 +6,8 @@ export type Activity = { type: string, actor: ActorProperties, object: ObjectProperties & { - inReplyTo: string | null // TODO: Move this to the ObjectProperties type + inReplyTo: ObjectProperties | string | null + replies: Activity[] } } @@ -20,7 +21,7 @@ const ActivityItem: React.FC = ({children, url = null, onClic const childrenArray = React.Children.toArray(children); const Item = ( -
{ +
{ if (!url && onClick) { onClick(); } diff --git a/apps/admin-x-activitypub/src/components/feed/ArticleModal.tsx b/apps/admin-x-activitypub/src/components/feed/ArticleModal.tsx index 9358212cc3b..171beaad65a 100644 --- a/apps/admin-x-activitypub/src/components/feed/ArticleModal.tsx +++ b/apps/admin-x-activitypub/src/components/feed/ArticleModal.tsx @@ -15,7 +15,6 @@ interface ArticleModalProps { object: ObjectProperties; actor: ActorProperties; comments: Activity[]; - allComments: Map; focusReply: boolean; } @@ -74,7 +73,7 @@ const FeedItemDivider: React.FC = () => (
); -const ArticleModal: React.FC = ({object, actor, comments, allComments, focusReply}) => { +const ArticleModal: React.FC = ({object, actor, comments, focusReply}) => { const MODAL_SIZE_SM = 640; const MODAL_SIZE_LG = 2800; const [commentsState, setCommentsState] = useState(comments); @@ -108,8 +107,7 @@ const ArticleModal: React.FC = ({object, actor, comments, all modal.show({ object: previousObject, actor: previousActor, - comments: previousComments, - allComments: allComments + comments: previousComments }); }; const navigateForward = (nextObject: ObjectProperties, nextActor: ActorProperties, nextComments: Activity[]) => { @@ -119,8 +117,7 @@ const ArticleModal: React.FC = ({object, actor, comments, all modal.show({ object: nextObject, actor: nextActor, - comments: nextComments, - allComments: allComments + comments: nextComments }); }; const toggleModalSize = () => { @@ -174,8 +171,8 @@ const ArticleModal: React.FC = ({object, actor, comments, all {commentsState.map((comment, index) => { - const showDivider = index !== commentsState.length - 1; - const nestedComments = allComments.get(comment.object.id) ?? []; + const showDivider = index !== comments.length - 1; + const nestedComments = comment.object.replies ?? []; const hasNestedComments = nestedComments.length > 0; return ( @@ -194,7 +191,7 @@ const ArticleModal: React.FC = ({object, actor, comments, all /> {hasNestedComments && } {nestedComments.map((nestedComment, nestedCommentIndex) => { - const nestedNestedComments = allComments.get(nestedComment.object.id) ?? []; + const nestedNestedComments = nestedComment.object.replies ?? []; return (
; default: + if (object.image) { + return attachment; + } return null; } } @@ -95,9 +99,9 @@ function renderInboxAttachment(object: ObjectProperties) { const attachmentCount = attachment.length; return ( -
+
- +
+ {attachmentCount - 1}
@@ -109,16 +113,18 @@ function renderInboxAttachment(object: ObjectProperties) { case 'image/png': case 'image/gif': return ( -
- +
+
); case 'video/mp4': case 'video/webm': return ( -
-
-
); default: + if (object.image) { + return
+ +
; + } return null; } } +function renderTimestamp(object: ObjectProperties) { + const timestamp = + new Date(object?.published ?? new Date()).toLocaleDateString('default', {year: 'numeric', month: 'short', day: '2-digit'}) + ', ' + new Date(object?.published ?? new Date()).toLocaleTimeString('default', {hour: '2-digit', minute: '2-digit'}); + + const date = new Date(object?.published ?? new Date()); + return ({getRelativeTimestamp(date)}); +} + +const truncateHTML = (html: string, maxLength: number) => { + const tempElement = document.createElement('div'); + tempElement.innerHTML = html; + + const textContent = tempElement.textContent || tempElement.innerText || ''; + + const truncatedText = textContent.substring(0, maxLength); + + return `“${truncatedText}…”`; +}; + const FeedItemStats: React.FC<{ object: ObjectProperties; likeCount: number; commentCount: number; + layout: string; onLikeClick: () => void; onCommentClick: () => void; -}> = ({object, likeCount, commentCount, onLikeClick, onCommentClick}) => { +}> = ({object, likeCount, commentCount, layout, onLikeClick, onCommentClick}) => { const [isClicked, setIsClicked] = useState(false); const [isLiked, setIsLiked] = useState(object.liked); const likeMutation = useLikeMutationForUser('index'); @@ -163,10 +194,10 @@ const FeedItemStats: React.FC<{ setTimeout(() => setIsClicked(false), 300); }; - return (
+ return (
); @@ -217,6 +248,7 @@ const FeedItem: React.FC = ({actor, object, layout, type, comment new Date(object?.published ?? new Date()).toLocaleDateString('default', {year: 'numeric', month: 'short', day: '2-digit'}) + ', ' + new Date(object?.published ?? new Date()).toLocaleTimeString('default', {hour: '2-digit', minute: '2-digit'}); const date = new Date(object?.published ?? new Date()); + const [isCopied, setIsCopied] = useState(false); const onLikeClick = () => { @@ -263,10 +295,10 @@ const FeedItem: React.FC = ({actor, object, layout, type, comment const UserMenuTrigger = (
} -
+
@@ -291,20 +323,30 @@ const FeedItem: React.FC = ({actor, object, layout, type, comment {author.name} {getUsername(author)}
- {getRelativeTimestamp(date)} + {renderTimestamp(object)}
- {object.name && {object.name}} -
- {renderFeedAttachment(object, layout)} + {(object.type === 'Article') && renderFeedAttachment(object, layout)} + {object.name && {object.name}} + {(object.preview && object.type === 'Article') ? object.preview.content :
} + {(object.type === 'Note') && renderFeedAttachment(object, layout)} + {(object.type === 'Article') &&
= ({actor, object, layout, type, comment
{/*
*/}
-
+ {/*
*/}
)} @@ -338,8 +380,8 @@ const FeedItem: React.FC = ({actor, object, layout, type, comment {/*
*/}
- {author.name} - {getRelativeTimestamp(date)} + {author.name} + {renderTimestamp(object)}
{getUsername(author)} @@ -353,6 +395,7 @@ const FeedItem: React.FC = ({actor, object, layout, type, comment
= ({actor, object, layout, type, comment
- {/*
*/}
- {author.name} - {getRelativeTimestamp(date)} + {author.name} + {renderTimestamp(object)}
{getUsername(author)} @@ -403,6 +445,7 @@ const FeedItem: React.FC = ({actor, object, layout, type, comment
= ({actor, object, layout, type, comment
- {/*
*/}
{!last &&
} @@ -424,33 +466,32 @@ const FeedItem: React.FC = ({actor, object, layout, type, comment return ( <> {object && ( -
-
- -
-
- {author.name} -  {getUsername(author)} - {getRelativeTimestamp(date)} -
-
-
- {object.name && {object.name}} -
+
+
+
+ +
+
+ {author.name} +  {getUsername(author)} + {getRelativeTimestamp(date)}
- {renderInboxAttachment(object)} -
-
- -
+ {object.name ? object.name : {author.name}: } +
+
+ {renderInboxAttachment(object)} +
+ +
)} @@ -461,4 +502,4 @@ const FeedItem: React.FC = ({actor, object, layout, type, comment return (<>); }; -export default FeedItem; +export default FeedItem; \ No newline at end of file diff --git a/apps/admin-x-activitypub/src/components/global/APAvatar.tsx b/apps/admin-x-activitypub/src/components/global/APAvatar.tsx index 5709bbc96cc..f0d7db2cc19 100644 --- a/apps/admin-x-activitypub/src/components/global/APAvatar.tsx +++ b/apps/admin-x-activitypub/src/components/global/APAvatar.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useState} from 'react'; import {ActorProperties} from '@tryghost/admin-x-framework/api/activitypub'; import {Icon} from '@tryghost/admin-x-design-system'; @@ -15,8 +15,9 @@ const APAvatar: React.FC = ({author, size, badge}) => { let iconSize = 18; let containerClass = ''; let imageClass = 'z-10 rounded w-10 h-10 object-cover'; - const badgeClass = `w-6 h-6 rounded-full absolute -bottom-2 -right-2 border-2 border-white content-box flex items-center justify-center `; + const badgeClass = `w-6 h-6 z-20 rounded-full absolute -bottom-2 -right-2 border-2 border-white content-box flex items-center justify-center `; let badgeColor = ''; + const [iconUrl, setIconUrl] = useState(author?.icon?.url); switch (badge) { case 'user-fill': @@ -29,52 +30,54 @@ const APAvatar: React.FC = ({author, size, badge}) => { badgeColor = ' bg-purple-500'; break; } - + switch (size) { case 'xs': iconSize = 12; - containerClass = 'z-10 rounded bg-grey-100 flex items-center justify-center p-[3px] w-6 h-6'; - imageClass = 'z-10 rounded w-6 h-6 object-cover'; + containerClass = 'z-10 relative rounded bg-grey-100 shrink-0 flex items-center justify-center w-5 h-5'; + imageClass = 'z-10 rounded w-5 h-5 object-cover'; break; case 'sm': - containerClass = 'z-10 rounded bg-grey-100 flex items-center justify-center p-[10px] w-10 h-10'; + containerClass = 'z-10 relative rounded bg-grey-100 shrink-0 flex items-center justify-center w-10 h-10'; break; case 'lg': - containerClass = 'z-10 rounded bg-grey-100 flex items-center justify-center p-[10px] w-22 h-22'; + containerClass = 'z-10 relative rounded-xl bg-grey-100 shrink-0 flex items-center justify-center w-22 h-22'; + imageClass = 'z-10 rounded-xl w-22 h-22 object-cover'; break; default: - containerClass = 'z-10 rounded bg-grey-100 flex items-center justify-center p-[10px] w-10 h-10'; + containerClass = 'z-10 relative rounded bg-grey-100 shrink-0 flex items-center justify-center w-10 h-10'; break; } + if (iconUrl) { + return ( + + setIconUrl(undefined)} + /> + {badge && ( +
+ +
+ )} +
+ ); + } + return ( - <> - {author && author.icon?.url ? ( - - - {badge && ( -
- -
- )} -
- ) : ( -
- -
- )} - +
+ +
); }; diff --git a/apps/admin-x-activitypub/src/components/global/FollowButton.tsx b/apps/admin-x-activitypub/src/components/global/FollowButton.tsx new file mode 100644 index 00000000000..fd4114eb870 --- /dev/null +++ b/apps/admin-x-activitypub/src/components/global/FollowButton.tsx @@ -0,0 +1,70 @@ +import {useEffect, useState} from 'react'; + +import {Button} from '@tryghost/admin-x-design-system'; + +import {useFollow} from '../../hooks/useActivityPubQueries'; + +interface FollowButtonProps { + className?: string; + following: boolean; + handle: string; + type?: 'button' | 'link'; + onFollow?: () => void; + onUnfollow?: () => void; +} + +const noop = () => {}; + +const FollowButton: React.FC = ({ + className, + following, + handle, + type = 'button', + onFollow = noop, + onUnfollow = noop +}) => { + const [isFollowing, setIsFollowing] = useState(following); + + const mutation = useFollow('index', + noop, + () => { + setIsFollowing(false); + onUnfollow(); + } + ); + + const handleClick = async () => { + if (isFollowing) { + setIsFollowing(false); + onUnfollow(); + + // @TODO: Implement unfollow mutation + } else { + setIsFollowing(true); + onFollow(); + + mutation.mutate(handle); + } + }; + + useEffect(() => { + setIsFollowing(following); + }, [following]); + + return ( +
+
+
+
+
+ {profile.actor.image && (
+ {profile.actor.name} +
)} +
+
+
+ +
+ +
+ {profile.actor.name} + {profile.handle} + {(profile.actor.summary || attachments.length > 0) && (
p]:mb-3 ${isExpanded ? 'max-h-none pb-7' : 'max-h-[160px] overflow-hidden'} relative`}> +
+ {attachments.map(attachment => ( + + {attachment.name} + + + ))} + {!isExpanded && isOverflowing && ( +
+ )} + {isOverflowing &&
)} + + containerClassName='mt-6' + selectedTab={selectedTab} + tabs={tabs} + onTabChange={setSelectedTab} + /> +
+
+
+ + ); +}; + +export default NiceModal.create(ProfileSearchResultModal); diff --git a/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts b/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts index 0b6a9d6d09c..da35eb02d68 100644 --- a/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts +++ b/apps/admin-x-activitypub/src/hooks/useActivityPubQueries.ts @@ -1,12 +1,18 @@ import {Activity} from '../components/activities/ActivityItem'; -import {ActivityPubAPI} from '../api/activitypub'; -import {useBrowseSite} from '@tryghost/admin-x-framework/api/site'; -import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; +import {ActivityPubAPI, type Profile, type SearchResults} from '../api/activitypub'; +import {useInfiniteQuery, useMutation, useQuery, useQueryClient} from '@tanstack/react-query'; -export function useSiteUrl() { - const site = useBrowseSite(); - return site.data?.site?.url ?? window.location.origin; -}; +let SITE_URL: string; + +async function getSiteUrl() { + if (!SITE_URL) { + const response = await fetch('/ghost/api/admin/site'); + const json = await response.json(); + SITE_URL = json.site.url; + } + + return SITE_URL; +} function createActivityPubAPI(handle: string, siteUrl: string) { return new ActivityPubAPI( @@ -17,21 +23,21 @@ function createActivityPubAPI(handle: string, siteUrl: string) { } export function useLikedForUser(handle: string) { - const siteUrl = useSiteUrl(); - const api = createActivityPubAPI(handle, siteUrl); return useQuery({ queryKey: [`liked:${handle}`], async queryFn() { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); return api.getLiked(); } }); } export function useReplyMutationForUser(handle: string) { - const siteUrl = useSiteUrl(); - const api = createActivityPubAPI(handle, siteUrl); return useMutation({ async mutationFn({id, content}: {id: string, content: string}) { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); return await api.reply(id, content) as Activity; } }); @@ -39,10 +45,10 @@ export function useReplyMutationForUser(handle: string) { export function useLikeMutationForUser(handle: string) { const queryClient = useQueryClient(); - const siteUrl = useSiteUrl(); - const api = createActivityPubAPI(handle, siteUrl); return useMutation({ - mutationFn(id: string) { + async mutationFn(id: string) { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); return api.like(id); }, onMutate: (id) => { @@ -82,10 +88,10 @@ export function useLikeMutationForUser(handle: string) { export function useUnlikeMutationForUser(handle: string) { const queryClient = useQueryClient(); - const siteUrl = useSiteUrl(); - const api = createActivityPubAPI(handle, siteUrl); return useMutation({ - mutationFn: (id: string) => { + async mutationFn(id: string) { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); return api.unlike(id); }, onMutate: async (id) => { @@ -133,67 +139,219 @@ export function useUnlikeMutationForUser(handle: string) { } export function useUserDataForUser(handle: string) { - const siteUrl = useSiteUrl(); - const api = createActivityPubAPI(handle, siteUrl); return useQuery({ queryKey: [`user:${handle}`], async queryFn() { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); return api.getUser(); } }); } export function useFollowersCountForUser(handle: string) { - const siteUrl = useSiteUrl(); - const api = createActivityPubAPI(handle, siteUrl); return useQuery({ queryKey: [`followersCount:${handle}`], async queryFn() { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); return api.getFollowersCount(); } }); } export function useFollowingCountForUser(handle: string) { - const siteUrl = useSiteUrl(); - const api = createActivityPubAPI(handle, siteUrl); return useQuery({ queryKey: [`followingCount:${handle}`], async queryFn() { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); return api.getFollowingCount(); } }); } export function useFollowingForUser(handle: string) { - const siteUrl = useSiteUrl(); - const api = createActivityPubAPI(handle, siteUrl); return useQuery({ queryKey: [`following:${handle}`], async queryFn() { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); return api.getFollowing(); } }); } export function useFollowersForUser(handle: string) { - const siteUrl = useSiteUrl(); - const api = createActivityPubAPI(handle, siteUrl); return useQuery({ queryKey: [`followers:${handle}`], async queryFn() { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); return api.getFollowers(); } }); } -export function useAllActivitiesForUser({handle, includeOwn = false}: {handle: string, includeOwn?: boolean}) { - const siteUrl = useSiteUrl(); - const api = createActivityPubAPI(handle, siteUrl); +export function useAllActivitiesForUser({ + handle, + includeOwn = false, + includeReplies = false, + filter = null +}: { + handle: string; + includeOwn?: boolean; + includeReplies?: boolean; + filter?: {type?: string[]} | null; +}) { + return useQuery({ + queryKey: [`activities:${JSON.stringify({handle, includeOwn, includeReplies, filter})}`], + async queryFn() { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); + return api.getAllActivities(includeOwn, includeReplies, filter); + } + }); +} + +export function useActivitiesForUser({ + handle, + includeOwn = false, + includeReplies = false, + filter = null +}: { + handle: string; + includeOwn?: boolean; + includeReplies?: boolean; + filter?: {type?: string[]} | null; +}) { + return useInfiniteQuery({ + queryKey: [`activities:${JSON.stringify({handle, includeOwn, includeReplies, filter})}`], + async queryFn({pageParam}: {pageParam?: string}) { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); + return api.getActivities(includeOwn, includeReplies, filter, pageParam); + }, + getNextPageParam(prevPage) { + return prevPage.nextCursor; + } + }); +} + +export function useSearchForUser(handle: string, query: string) { + const queryClient = useQueryClient(); + const queryKey = ['search', {handle, query}]; + + const searchQuery = useQuery({ + queryKey, + async queryFn() { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); + return api.search(query); + } + }); + + const updateProfileSearchResult = (id: string, updated: Partial) => { + queryClient.setQueryData(queryKey, (current: SearchResults | undefined) => { + if (!current) { + return current; + } + + return { + ...current, + profiles: current.profiles.map((item: Profile) => { + if (item.actor.id === id) { + return {...item, ...updated}; + } + return item; + }) + }; + }); + }; + + return {searchQuery, updateProfileSearchResult}; +} + +export function useFollow(handle: string, onSuccess: () => void, onError: () => void) { + return useMutation({ + async mutationFn(username: string) { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); + return api.follow(username); + }, + onSuccess, + onError + }); +} + +export function useFollowersForProfile(handle: string) { + return useInfiniteQuery({ + queryKey: [`followers:${handle}`], + async queryFn({pageParam}: {pageParam?: string}) { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); + return api.getFollowersForProfile(handle, pageParam); + }, + getNextPageParam(prevPage) { + return prevPage.next; + } + }); +} + +export function useFollowingForProfile(handle: string) { + return useInfiniteQuery({ + queryKey: [`following:${handle}`], + async queryFn({pageParam}: {pageParam?: string}) { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); + return api.getFollowingForProfile(handle, pageParam); + }, + getNextPageParam(prevPage) { + return prevPage.next; + } + }); +} + +export function useSuggestedProfiles(handle: string, handles: string[]) { + const queryClient = useQueryClient(); + const queryKey = ['profiles', {handles}]; + + const suggestedProfilesQuery = useQuery({ + queryKey, + async queryFn() { + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); + return Promise.all( + handles.map(h => api.getProfile(h)) + ); + } + }); + + const updateSuggestedProfile = (id: string, updated: Partial) => { + queryClient.setQueryData(queryKey, (current: Profile[] | undefined) => { + if (!current) { + return current; + } + + return current.map((item: Profile) => { + if (item.actor.id === id) { + return {...item, ...updated}; + } + return item; + }); + }); + }; + + return {suggestedProfilesQuery, updateSuggestedProfile}; +} + +export function useProfileForUser(handle: string, fullHandle: string) { return useQuery({ - queryKey: [`activities:${handle}:includeOwn=${includeOwn.toString()}`], + queryKey: [`profile:${fullHandle}`], async queryFn() { - return api.getAllActivities(includeOwn); + const siteUrl = await getSiteUrl(); + const api = createActivityPubAPI(handle, siteUrl); + return api.getProfile(fullHandle); } }); } diff --git a/apps/admin-x-activitypub/src/styles/index.css b/apps/admin-x-activitypub/src/styles/index.css index 5190e70eda5..e484a933554 100644 --- a/apps/admin-x-activitypub/src/styles/index.css +++ b/apps/admin-x-activitypub/src/styles/index.css @@ -24,11 +24,12 @@ animation: bump 0.3s ease-in-out; fill: #F50B23; } -.ap-note-content a { +.ap-note-content a, .ap-profile-content a { color: rgb(236 72 153) !important; + word-break: break-all; } -.ap-note-content a:hover { +.ap-note-content a:hover, .ap-profile-content a:hover { color: rgb(190, 25, 99) !important; text-decoration: underline !important; } diff --git a/apps/admin-x-demo/package.json b/apps/admin-x-demo/package.json index a314be47621..32697ca79be 100644 --- a/apps/admin-x-demo/package.json +++ b/apps/admin-x-demo/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/admin-x-demo", - "version": "0.0.20", + "version": "0.0.0", "license": "MIT", "repository": { "type": "git", @@ -14,10 +14,7 @@ ], "main": "./dist/admin-x-demo.umd.cjs", "module": "./dist/admin-x-demo.js", - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - }, + "private": true, "scripts": { "dev": "vite build --watch", "dev:start": "vite", @@ -25,10 +22,6 @@ "lint": "yarn run lint:code && yarn run lint:test", "lint:code": "eslint --ext .js,.ts,.cjs,.tsx --cache src", "lint:test": "eslint -c test/.eslintrc.cjs --ext .js,.ts,.cjs,.tsx --cache test", - "test:unit": "vitest run", - "test:acceptance": "NODE_OPTIONS='--experimental-specifier-resolution=node --no-warnings' VITE_TEST=true playwright test", - "test:acceptance:slowmo": "TIMEOUT=100000 PLAYWRIGHT_SLOWMO=100 yarn test:acceptance --headed", - "test:acceptance:full": "ALL_BROWSERS=1 yarn test:acceptance", "preview": "vite preview" }, "devDependencies": { diff --git a/apps/admin-x-demo/test/acceptance/example.test.ts b/apps/admin-x-demo/test/acceptance/example.test.ts index a3b454c793f..fe270584130 100644 --- a/apps/admin-x-demo/test/acceptance/example.test.ts +++ b/apps/admin-x-demo/test/acceptance/example.test.ts @@ -1,7 +1,7 @@ import {expect, test} from '@playwright/test'; import {mockApi, responseFixtures} from '@tryghost/admin-x-framework/test/acceptance'; -test.describe('Demo', async () => { +test.describe.skip('Demo', async () => { test('Renders the list page', async ({page}) => { await mockApi({page, requests: { browseSettings: { diff --git a/apps/admin-x-demo/test/unit/example.test.tsx b/apps/admin-x-demo/test/unit/example.test.tsx index e84c7562658..bb440e034d3 100644 --- a/apps/admin-x-demo/test/unit/example.test.tsx +++ b/apps/admin-x-demo/test/unit/example.test.tsx @@ -1,7 +1,7 @@ import ListPage from '../../src/ListPage'; import {render, screen} from '@testing-library/react'; -describe('Demo', function () { +describe.skip('Demo', function () { it('renders a component', async function () { render(); diff --git a/apps/admin-x-design-system/package.json b/apps/admin-x-design-system/package.json index 468d58230ba..3bafccfe4bb 100644 --- a/apps/admin-x-design-system/package.json +++ b/apps/admin-x-design-system/package.json @@ -76,7 +76,7 @@ "@sentry/react": "7.119.0", "@tailwindcss/forms": "0.5.9", "@tailwindcss/line-clamp": "0.4.4", - "@uiw/react-codemirror": "4.23.3", + "@uiw/react-codemirror": "4.23.5", "autoprefixer": "10.4.19", "clsx": "2.1.1", "postcss": "8.4.39", @@ -84,7 +84,7 @@ "react-colorful": "5.6.1", "react-hot-toast": "2.4.1", "react-select": "5.8.1", - "tailwindcss": "3.4.12" + "tailwindcss": "3.4.13" }, "peerDependencies": { "react": "^18.2.0", diff --git a/apps/admin-x-design-system/src/assets/icons/play-fill.svg b/apps/admin-x-design-system/src/assets/icons/play-fill.svg new file mode 100644 index 00000000000..228930ad019 --- /dev/null +++ b/apps/admin-x-design-system/src/assets/icons/play-fill.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/apps/admin-x-design-system/src/global/TabView.tsx b/apps/admin-x-design-system/src/global/TabView.tsx index 2e8757b90db..f3ccb9ddacd 100644 --- a/apps/admin-x-design-system/src/global/TabView.tsx +++ b/apps/admin-x-design-system/src/global/TabView.tsx @@ -50,7 +50,11 @@ export const TabButton: React.FC = ({ > {icon && } {title} - {(typeof counter === 'number') && {counter}} + {(typeof counter === 'number') && + + {new Intl.NumberFormat().format(counter)} + + } ); }; diff --git a/apps/admin-x-framework/src/api/activitypub.ts b/apps/admin-x-framework/src/api/activitypub.ts index 97201808e0f..81d1efb9a2b 100644 --- a/apps/admin-x-framework/src/api/activitypub.ts +++ b/apps/admin-x-framework/src/api/activitypub.ts @@ -23,13 +23,22 @@ export type ObjectProperties = { export type ActorProperties = { '@context': string | (string | object)[]; - attachment: object[]; + attachment?: { + type: string; + name: string; + value: string; + }[]; discoverable: boolean; featured: string; followers: string; following: string; id: string | null; - image: string; + image: { + url: string; + }; + icon: { + url: string; + }; inbox: string; manuallyApprovesFollowers: boolean; name: string; diff --git a/apps/admin-x-framework/src/test/responses/config.json b/apps/admin-x-framework/src/test/responses/config.json index b876dea9bed..a16a3a2d671 100644 --- a/apps/admin-x-framework/src/test/responses/config.json +++ b/apps/admin-x-framework/src/test/responses/config.json @@ -25,7 +25,7 @@ }, "signupForm": { "url": "https://signup-form.test/signup-form.min.js", - "version": "0.1" + "version": "0.2" } } } diff --git a/apps/admin-x-settings/package.json b/apps/admin-x-settings/package.json index 4fc01263954..dc89488b1d8 100644 --- a/apps/admin-x-settings/package.json +++ b/apps/admin-x-settings/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/admin-x-settings", - "version": "0.0.20", + "version": "0.0.0", "license": "MIT", "repository": { "type": "git", @@ -20,10 +20,7 @@ "require": "./dist/admin-x-settings.umd.cjs" } }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - }, + "private": true, "scripts": { "dev": "vite build --watch", "dev:start": "vite", diff --git a/apps/comments-ui/package.json b/apps/comments-ui/package.json index 61336120008..986df5efc93 100644 --- a/apps/comments-ui/package.json +++ b/apps/comments-ui/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/comments-ui", - "version": "0.17.3", + "version": "0.19.0", "license": "MIT", "repository": "git@github.com:TryGhost/comments-ui.git", "author": "Ghost Foundation", @@ -44,16 +44,16 @@ }, "dependencies": { "@headlessui/react": "1.7.19", - "@tiptap/core": "2.6.0", - "@tiptap/extension-blockquote": "2.6.0", - "@tiptap/extension-document": "2.6.1", - "@tiptap/extension-hard-break": "2.6.0", - "@tiptap/extension-link": "2.6.0", - "@tiptap/extension-paragraph": "2.6.0", - "@tiptap/extension-placeholder": "2.6.1", - "@tiptap/extension-text": "2.6.0", - "@tiptap/pm": "2.6.0", - "@tiptap/react": "2.6.1", + "@tiptap/core": "2.8.0", + "@tiptap/extension-blockquote": "2.8.0", + "@tiptap/extension-document": "2.8.0", + "@tiptap/extension-hard-break": "2.8.0", + "@tiptap/extension-link": "2.8.0", + "@tiptap/extension-paragraph": "2.8.0", + "@tiptap/extension-placeholder": "2.8.0", + "@tiptap/extension-text": "2.8.0", + "@tiptap/pm": "2.8.0", + "@tiptap/react": "2.8.0", "react": "17.0.2", "react-dom": "17.0.2", "react-string-replace": "1.1.1" @@ -75,7 +75,7 @@ "eslint-plugin-tailwindcss": "3.13.0", "jsdom": "24.1.3", "postcss": "8.4.39", - "tailwindcss": "3.4.12", + "tailwindcss": "3.4.13", "vite": "4.5.3", "vite-plugin-css-injected-by-js": "3.3.0", "vite-plugin-svgr": "3.3.0", diff --git a/apps/comments-ui/src/AppContext.ts b/apps/comments-ui/src/AppContext.ts index eb5bf3035c9..f1284d1df2a 100644 --- a/apps/comments-ui/src/AppContext.ts +++ b/apps/comments-ui/src/AppContext.ts @@ -67,7 +67,7 @@ export type EditableAppContext = { commentCount: number, secundaryFormCount: number, popup: Page | null, - labs: LabsContextType | null + labs: LabsContextType } export type TranslationFunction = (key: string, replacements?: Record) => string; @@ -92,8 +92,8 @@ export const useAppContext = () => useContext(AppContext); export const useLabs = () => { try { const context = useAppContext(); - return context.labs; + return context.labs || {}; } catch (e) { - return null; + return {}; } }; diff --git a/apps/comments-ui/src/components/content/CTABox.tsx b/apps/comments-ui/src/components/content/CTABox.tsx index 51dc5f12389..4ea6885b6d6 100644 --- a/apps/comments-ui/src/components/content/CTABox.tsx +++ b/apps/comments-ui/src/components/content/CTABox.tsx @@ -32,16 +32,16 @@ const CTABox: React.FC = ({isFirst, isPaid}) => { return (
-

+

{titleText}

-

+

{text}

- - {!member && (

+ {!member && (

{t('Already a member?')}

)} diff --git a/apps/comments-ui/src/components/content/Comment.tsx b/apps/comments-ui/src/components/content/Comment.tsx index fbc252e5d13..3cd826a6fd3 100644 --- a/apps/comments-ui/src/components/content/Comment.tsx +++ b/apps/comments-ui/src/components/content/Comment.tsx @@ -1,11 +1,11 @@ import EditForm from './forms/EditForm'; import LikeButton from './buttons/LikeButton'; import MoreButton from './buttons/MoreButton'; -import Replies from './Replies'; +import Replies, {RepliesProps} from './Replies'; import ReplyButton from './buttons/ReplyButton'; import ReplyForm from './forms/ReplyForm'; import {Avatar, BlankAvatar} from './Avatar'; -import {Comment, useAppContext} from '../../AppContext'; +import {Comment, useAppContext, useLabs} from '../../AppContext'; import {Transition} from '@headlessui/react'; import {formatExplicitTime, isCommentPublished} from '../../utils/helpers'; import {useRelativeTime} from '../../utils/hooks'; @@ -14,9 +14,10 @@ import {useState} from 'react'; type AnimatedCommentProps = { comment: Comment; parent?: Comment; + toggleParentReplyMode?: () => Promise; }; -const AnimatedComment: React.FC = ({comment, parent}) => { +const AnimatedComment: React.FC = ({comment, parent, toggleParentReplyMode}) => { return ( = ({comment, parent}) => { show={true} appear > - + ); }; type EditableCommentProps = AnimatedCommentProps; -const EditableComment: React.FC = ({comment, parent}) => { +const EditableComment: React.FC = ({comment, parent, toggleParentReplyMode}) => { const [isInEditMode, setIsInEditMode] = useState(false); const closeEditMode = () => { @@ -50,27 +51,31 @@ const EditableComment: React.FC = ({comment, parent}) => { ); } else { - return (); + return (); } }; type CommentProps = AnimatedCommentProps & { openEditMode: () => void; }; -const CommentComponent: React.FC = ({comment, parent, openEditMode}) => { +const CommentComponent: React.FC = ({comment, parent, openEditMode, toggleParentReplyMode}) => { const isPublished = isCommentPublished(comment); if (isPublished) { - return (); + return (); } return (); }; -const PublishedComment: React.FC = ({comment, parent, openEditMode}) => { +const PublishedComment: React.FC = ({comment, parent, openEditMode, toggleParentReplyMode}) => { const [isInReplyMode, setIsInReplyMode] = useState(false); const {dispatchAction} = useAppContext(); const toggleReplyMode = async () => { + if (parent && toggleParentReplyMode) { + return await toggleParentReplyMode(); + } + if (!isInReplyMode) { // First load all the replies before opening the reply model await dispatchAction('loadMoreReplies', {comment, limit: 'all'}); @@ -91,7 +96,7 @@ const PublishedComment: React.FC = ({comment, parent, openEditMode - + ); @@ -156,7 +161,7 @@ const EditedInfo: React.FC<{comment: Comment}> = ({comment}) => { ); }; -const RepliesContainer: React.FC<{comment: Comment}> = ({comment}) => { +const RepliesContainer: React.FC = ({comment, toggleReplyMode}) => { const hasReplies = comment.replies && comment.replies.length > 0; if (!hasReplies) { @@ -165,7 +170,7 @@ const RepliesContainer: React.FC<{comment: Comment}> = ({comment}) => { return (
- +
); }; @@ -181,7 +186,7 @@ const ReplyFormBox: React.FC = ({comment, isInReplyMode, clos } return ( -
+
); @@ -208,7 +213,7 @@ const CommentHeader: React.FC<{comment: Comment}> = ({comment}) => { const memberExpertise = member && comment.member && comment.member.uuid === member.uuid ? member.expertise : comment?.member?.expertise; return ( -
+
@@ -241,10 +246,11 @@ const CommentMenu: React.FC = ({comment, toggleReplyMode, isIn // If this comment is from the current member, always override member // with the member from the context, so we update the expertise in existing comments when we change it const {member, commentsEnabled} = useAppContext(); + const labs = useLabs(); const paidOnly = commentsEnabled === 'paid'; const isPaidMember = member && !!member.paid; - const canReply = member && (isPaidMember || !paidOnly) && !parent; + const canReply = member && (isPaidMember || !paidOnly) && (labs.commentImprovements ? true : !parent); return (
@@ -264,7 +270,7 @@ const RepliesLine: React.FC<{hasReplies: boolean}> = ({hasReplies}) => { return null; } - return (
); + return (
); }; type CommentLayoutProps = { diff --git a/apps/comments-ui/src/components/content/Replies.tsx b/apps/comments-ui/src/components/content/Replies.tsx index 5d8d231bba9..2a25e94de19 100644 --- a/apps/comments-ui/src/components/content/Replies.tsx +++ b/apps/comments-ui/src/components/content/Replies.tsx @@ -2,10 +2,11 @@ import CommentComponent from './Comment'; import RepliesPagination from './RepliesPagination'; import {Comment, useAppContext} from '../../AppContext'; -type Props = { - comment: Comment +export type RepliesProps = { + comment: Comment, + toggleReplyMode?: () => Promise }; -const Replies: React.FC = ({comment}) => { +const Replies: React.FC = ({comment, toggleReplyMode}) => { const {dispatchAction} = useAppContext(); const repliesLeft = comment.count.replies - comment.replies.length; @@ -16,7 +17,7 @@ const Replies: React.FC = ({comment}) => { return (
- {comment.replies.map((reply => ))} + {comment.replies.map((reply => ))} {repliesLeft > 0 && }
); diff --git a/apps/comments-ui/src/components/content/forms/EditForm.tsx b/apps/comments-ui/src/components/content/forms/EditForm.tsx index 7f701be91e2..467e559f634 100644 --- a/apps/comments-ui/src/components/content/forms/EditForm.tsx +++ b/apps/comments-ui/src/components/content/forms/EditForm.tsx @@ -68,7 +68,9 @@ const EditForm: React.FC = ({comment, parent, close}) => { }, [editor, close, comment.html]); return ( - +
+ +
); }; diff --git a/apps/comments-ui/src/components/content/forms/Form.tsx b/apps/comments-ui/src/components/content/forms/Form.tsx index 0c9b402b24f..9a63f94c467 100644 --- a/apps/comments-ui/src/components/content/forms/Form.tsx +++ b/apps/comments-ui/src/components/content/forms/Form.tsx @@ -103,9 +103,9 @@ const FormEditor: React.FC = ({submit, progress, setProgress, c }, [editor, close, submitForm]); return ( -
+
= ({show, name, expertise, editName, leaveTo="opacity-0" show={show} > -
+
@@ -165,7 +165,7 @@ const FormHeader: React.FC = ({show, name, expertise, editName, type="button" onClick={editExpertise} > - ·{expertise ? expertise : 'Add your expertise'} + ·{expertise ? expertise : 'Add your expertise'} {expertise && }
@@ -265,16 +265,16 @@ const Form: React.FC = ({comment, submit, submitText, submitSize, clo }, [editor, memberName, progress]); return ( -
+
-
+
-
+
diff --git a/apps/comments-ui/src/components/content/forms/MainForm.tsx b/apps/comments-ui/src/components/content/forms/MainForm.tsx index f53af0b5bde..59883905194 100644 --- a/apps/comments-ui/src/components/content/forms/MainForm.tsx +++ b/apps/comments-ui/src/components/content/forms/MainForm.tsx @@ -94,7 +94,7 @@ const MainForm: React.FC = ({commentsCount}) => { const isOpen = editor?.isFocused ?? false; return ( -
+
); diff --git a/apps/comments-ui/src/components/content/forms/SecundaryForm.tsx b/apps/comments-ui/src/components/content/forms/SecundaryForm.tsx index 3f3453b8f86..ab8b014abc2 100644 --- a/apps/comments-ui/src/components/content/forms/SecundaryForm.tsx +++ b/apps/comments-ui/src/components/content/forms/SecundaryForm.tsx @@ -38,7 +38,7 @@ const SecundaryForm: React.FC = ({editor, submit, close, closeIfNotChange const reduced = isMobile(); return ( -
+
); diff --git a/apps/comments-ui/src/components/popups/AddDetailsPopup.tsx b/apps/comments-ui/src/components/popups/AddDetailsPopup.tsx index 10c5e70abc2..5f6ac18be30 100644 --- a/apps/comments-ui/src/components/popups/AddDetailsPopup.tsx +++ b/apps/comments-ui/src/components/popups/AddDetailsPopup.tsx @@ -85,7 +85,7 @@ const AddDetailsPopup = (props: Props) => {
{profile.name}
-
+
{profile.expertise}
@@ -124,11 +124,11 @@ const AddDetailsPopup = (props: Props) => {
-

{t('Complete your profile')}.

-

{t('Add context to your comment, share your name and expertise to foster a healthy discussion.')}

+

{t('Complete your profile')}.

+

{t('Add context to your comment, share your name and expertise to foster a healthy discussion.')}

- + { }} />
- -
{charsText}
+ +
{charsText}
{ }} /> ); }; diff --git a/apps/comments-ui/src/components/popups/ReportPopup.tsx b/apps/comments-ui/src/components/popups/ReportPopup.tsx index e8ae827b76d..21f05770f72 100644 --- a/apps/comments-ui/src/components/popups/ReportPopup.tsx +++ b/apps/comments-ui/src/components/popups/ReportPopup.tsx @@ -14,7 +14,7 @@ const ReportPopup = ({comment}: {comment: Comment}) => { buttonColor = 'bg-green-600'; } - let buttonText = t('Report this comment'); + let buttonText = t('Report'); const defaultButtonText = buttonText; if (progress === 'sending') { @@ -59,29 +59,30 @@ const ReportPopup = ({comment}: {comment: Comment}) => { return (
-

- {t('Report this comment?')} - {t('You want to report this comment?')} -

-

{t('Your request will be sent to the owner of this site.')}

-
- - +
+

+ {t('Report this comment?')} +

+

{t('Your request will be sent to the owner of this site.')}

+
+ + +
+
-
); }; diff --git a/apps/comments-ui/src/images/icons/close.svg b/apps/comments-ui/src/images/icons/close.svg index 9022ec0abff..30cba2dc8e0 100644 --- a/apps/comments-ui/src/images/icons/close.svg +++ b/apps/comments-ui/src/images/icons/close.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/comments-ui/src/styles/iframe.css b/apps/comments-ui/src/styles/iframe.css index c353307d7db..2386285eec4 100644 --- a/apps/comments-ui/src/styles/iframe.css +++ b/apps/comments-ui/src/styles/iframe.css @@ -51,6 +51,14 @@ body { margin: 0; } +.gh-comment-content p.is-editor-empty:first-child::before { + color: rgba(0,0,0,.25) !important; +} + +.dark .gh-comment-content p.is-editor-empty:first-child::before { + color: rgba(255,255,255,.35) !important; +} + /* The following lines are needed for the editor */ /* Placeholder */ diff --git a/apps/comments-ui/test/e2e/actions.test.ts b/apps/comments-ui/test/e2e/actions.test.ts index 72c815209b7..7db634237f6 100644 --- a/apps/comments-ui/test/e2e/actions.test.ts +++ b/apps/comments-ui/test/e2e/actions.test.ts @@ -2,10 +2,14 @@ import {MockedApi, initialize, waitEditorFocused} from '../utils/e2e'; import {expect, test} from '@playwright/test'; test.describe('Actions', async () => { - test('Can like and unlike a comment', async ({page}) => { - const mockedApi = new MockedApi({}); + let mockedApi: MockedApi; + + test.beforeEach(async () => { + mockedApi = new MockedApi({}); mockedApi.setMember({}); + }); + test('Can like and unlike a comment', async ({page}) => { mockedApi.addComment({ html: '

This is comment 1

' }); @@ -58,9 +62,6 @@ test.describe('Actions', async () => { }); test('Can reply to a comment', async ({page}) => { - const mockedApi = new MockedApi({}); - mockedApi.setMember({}); - mockedApi.addComment({ html: '

This is comment 1

' }); @@ -106,8 +107,67 @@ test.describe('Actions', async () => { await expect(frame.getByText('This is a reply 123')).toHaveCount(1); }); + test('Reply-to-reply action not shown without labs flag', async ({page}) => { + mockedApi.addComment({ + html: '

This is comment 1

', + replies: [ + mockedApi.buildReply({ + html: '

This is a reply to 1

' + }) + ] + }); + + const {frame} = await initialize({ + mockedApi, + page, + publication: 'Publisher Weekly' + }); + + const parentComment = frame.getByTestId('comment-component').nth(0); + const replyComment = parentComment.getByTestId('comment-component').nth(0); + + expect(replyComment.getByTestId('reply-button')).not.toBeVisible(); + }); + + test('Can reply to a reply', async ({page}) => { + mockedApi.addComment({ + html: '

This is comment 1

', + replies: [ + mockedApi.buildReply({ + html: '

This is a reply to 1

' + }) + ] + }); + + const {frame} = await initialize({ + mockedApi, + page, + publication: 'Publisher Weekly', + labs: { + commentImprovements: true + } + }); + + const parentComment = frame.getByTestId('comment-component').nth(0); + const replyComment = parentComment.getByTestId('comment-component').nth(0); + + const replyReplyButton = replyComment.getByTestId('reply-button'); + await replyReplyButton.click(); + + const editor = frame.getByTestId('form-editor'); + await expect(editor).toBeVisible(); + await waitEditorFocused(editor); + + await page.keyboard.type('This is a reply to a reply'); + + const submitButton = parentComment.getByTestId('submit-form-button'); + await submitButton.click(); + + await expect(frame.getByTestId('comment-component')).toHaveCount(3); + await expect(frame.getByText('This is a reply to a reply')).toHaveCount(1); + }); + test('Can add expertise', async ({page}) => { - const mockedApi = new MockedApi({}); mockedApi.setMember({name: 'John Doe', expertise: null}); mockedApi.addComment({ @@ -143,6 +203,11 @@ test.describe('Actions', async () => { await expect(profileModal).not.toBeVisible(); + // playwright can lose focus on the editor which hides the member details, + // re-clicking here brings the member details back into view + await editor.click({force: true}); + await waitEditorFocused(editor); + await expect(frame.getByTestId('member-name')).toHaveText('Testy McTest'); await expect(frame.getByTestId('expertise-button')).toHaveText('·Software development'); }); diff --git a/apps/comments-ui/test/e2e/editor.test.ts b/apps/comments-ui/test/e2e/editor.test.ts index 7c286d5c607..53afdf3cd5c 100644 --- a/apps/comments-ui/test/e2e/editor.test.ts +++ b/apps/comments-ui/test/e2e/editor.test.ts @@ -1,4 +1,4 @@ -import {MockedApi, getHeight, getModifierKey, initialize, selectText, setClipboard, waitEditorFocused} from '../utils/e2e'; +import {MockedApi, getModifierKey, initialize, selectText, setClipboard, waitEditorFocused} from '../utils/e2e'; import {expect, test} from '@playwright/test'; test.describe('Editor', async () => { @@ -22,7 +22,6 @@ test.describe('Editor', async () => { await expect(frame.getByTestId('count')).toHaveText('1 comment'); const editor = frame.getByTestId('form-editor'); - const editorHeight = await getHeight(editor); await editor.click({force: true}); @@ -31,9 +30,6 @@ test.describe('Editor', async () => { // Wait for animation to finish await page.waitForTimeout(200); - const newEditorHeight = await getHeight(editor); - - expect(newEditorHeight).toBeGreaterThan(editorHeight); // Type in the editor await editor.type('Newly added comment'); @@ -57,22 +53,10 @@ test.describe('Editor', async () => { const mockedApi = new MockedApi({}); mockedApi.setMember({}); - const {frame} = await initialize({ - mockedApi, - page, - publication: 'Publisher Weekly' - }); - - const editor = frame.getByTestId('form-editor'); - const editorHeight = await getHeight(editor); - await page.keyboard.press('c'); // Wait for animation to finish await page.waitForTimeout(200); - const newEditorHeight = await getHeight(editor); - - expect(newEditorHeight).toBeGreaterThan(editorHeight); }); test('Can use CMD+ENTER to submmit', async ({page}) => { @@ -95,7 +79,6 @@ test.describe('Editor', async () => { await expect(frame.getByTestId('count')).toHaveText('1 comment'); const editor = frame.getByTestId('form-editor'); - const editorHeight = await getHeight(editor); await editor.click({force: true}); // Wait for focused @@ -103,9 +86,6 @@ test.describe('Editor', async () => { // Wait for animation to finish await page.waitForTimeout(200); - const newEditorHeight = await getHeight(editor); - - expect(newEditorHeight).toBeGreaterThan(editorHeight); // Type in the editor await editor.type('Newly added comment'); diff --git a/apps/portal/package.json b/apps/portal/package.json index 0df62ed86fa..ed4edd380a9 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/portal", - "version": "2.43.1", + "version": "2.44.0", "license": "MIT", "repository": { "type": "git", @@ -81,7 +81,6 @@ "@babel/eslint-parser": "7.23.3", "@doist/react-interpolate": "1.1.1", "@sentry/react": "7.119.0", - "@sentry/tracing": "7.114.0", "@testing-library/jest-dom": "5.17.0", "@testing-library/react": "12.1.5", "@tryghost/i18n": "0.0.0", diff --git a/apps/portal/src/AppContext.js b/apps/portal/src/AppContext.js index 73555c60bba..a4262c63b52 100644 --- a/apps/portal/src/AppContext.js +++ b/apps/portal/src/AppContext.js @@ -10,7 +10,9 @@ const AppContext = React.createContext({ pageData: {}, onAction: (action, data) => { return {action, data}; - } + }, + t: () => {} + }); export default AppContext; diff --git a/apps/portal/src/actions.js b/apps/portal/src/actions.js index 96cc784d9a1..1f052045e2d 100644 --- a/apps/portal/src/actions.js +++ b/apps/portal/src/actions.js @@ -1,5 +1,5 @@ import setupGhostApi from './utils/api'; -import {HumanReadableError} from './utils/errors'; +import {chooseBestErrorMessage} from './utils/errors'; import {createPopupNotification, getMemberEmail, getMemberName, getProductCadenceFromPrice, removePortalLinkFromUrl, getRefDomain} from './utils/helpers'; function switchPage({data, state}) { @@ -67,17 +67,19 @@ async function signout({api, state}) { action: 'signout:success' }; } catch (e) { + const {t} = state; return { action: 'signout:failed', popupNotification: createPopupNotification({ type: 'signout:failed', autoHide: false, closeable: true, state, status: 'error', - message: 'Failed to log out, please try again' + message: t('Failed to log out, please try again') }) }; } } async function signin({data, api, state}) { + const {t} = state; try { const integrityToken = await api.member.getIntegrityToken(); await api.member.sendMagicLink({...data, emailType: 'signin', integrityToken}); @@ -90,7 +92,7 @@ async function signin({data, api, state}) { action: 'signin:failed', popupNotification: createPopupNotification({ type: 'signin:failed', autoHide: false, closeable: true, state, status: 'error', - message: HumanReadableError.getMessageFromError(e, 'Failed to log in, please try again') + message: chooseBestErrorMessage(e, t('Failed to log in, please try again'), t) }) }; } @@ -119,12 +121,13 @@ async function signup({data, state, api}) { lastPage: 'signup' }; } catch (e) { - const message = e?.message || 'Failed to sign up, please try again'; + const {t} = state; + const message = chooseBestErrorMessage(e, t('Failed to sign up, please try again'), t); return { action: 'signup:failed', popupNotification: createPopupNotification({ type: 'signup:failed', autoHide: false, closeable: true, state, status: 'error', - message + message: message }) }; } @@ -146,17 +149,19 @@ async function checkoutPlan({data, state, api}) { } }); } catch (e) { + const {t} = state; return { action: 'checkoutPlan:failed', popupNotification: createPopupNotification({ type: 'checkoutPlan:failed', autoHide: false, closeable: true, state, status: 'error', - message: 'Failed to process checkout, please try again' + message: t('Failed to process checkout, please try again') }) }; } } async function updateSubscription({data, state, api}) { + const {t} = state; try { const {plan, planId, subscriptionId, cancelAtPeriodEnd} = data; const {tierId, cadence} = getProductCadenceFromPrice({site: state?.site, priceId: planId}); @@ -175,7 +180,7 @@ async function updateSubscription({data, state, api}) { action, popupNotification: createPopupNotification({ type: action, autoHide: true, closeable: true, state, status: 'success', - message: 'Subscription plan updated successfully' + message: t('Subscription plan updated successfully') }), page: 'accountHome', member: member @@ -185,7 +190,7 @@ async function updateSubscription({data, state, api}) { action: 'updateSubscription:failed', popupNotification: createPopupNotification({ type: 'updateSubscription:failed', autoHide: false, closeable: true, state, status: 'error', - message: 'Failed to update subscription, please try again' + message: t('Failed to update subscription, please try again') }) }; } @@ -205,11 +210,12 @@ async function cancelSubscription({data, state, api}) { member: member }; } catch (e) { + const {t} = state; return { action: 'cancelSubscription:failed', popupNotification: createPopupNotification({ type: 'cancelSubscription:failed', autoHide: false, closeable: true, state, status: 'error', - message: 'Failed to cancel subscription, please try again' + message: t('Failed to cancel subscription, please try again') }) }; } @@ -229,11 +235,12 @@ async function continueSubscription({data, state, api}) { member: member }; } catch (e) { + const {t} = state; return { action: 'continueSubscription:failed', popupNotification: createPopupNotification({ type: 'continueSubscription:failed', autoHide: false, closeable: true, state, status: 'error', - message: 'Failed to cancel subscription, please try again' + message: t('Failed to cancel subscription, please try again') }) }; } @@ -243,11 +250,12 @@ async function editBilling({data, state, api}) { try { await api.member.editBilling(data); } catch (e) { + const {t} = state; return { action: 'editBilling:failed', popupNotification: createPopupNotification({ type: 'editBilling:failed', autoHide: false, closeable: true, state, status: 'error', - message: 'Failed to update billing information, please try again' + message: t('Failed to update billing information, please try again') }) }; } @@ -294,18 +302,20 @@ async function updateNewsletterPreference({data, state, api}) { member }; } catch (e) { + const {t} = state; return { action: 'updateNewsletterPref:failed', popupNotification: createPopupNotification({ type: 'updateNewsletter:failed', autoHide: true, closeable: true, state, status: 'error', - message: 'Failed to update newsletter settings' + message: t('Failed to update newsletter settings') }) }; } } async function removeEmailFromSuppressionList({state, api}) { + const {t} = state; try { await api.member.deleteSuppression(); const action = 'removeEmailFromSuppressionList:success'; @@ -313,7 +323,7 @@ async function removeEmailFromSuppressionList({state, api}) { action, popupNotification: createPopupNotification({ type: 'removeEmailFromSuppressionList:success', autoHide: true, closeable: true, state, status: 'success', - message: 'You have been successfully resubscribed' + message: t('You have been successfully resubscribed') }) }; } catch (e) { @@ -322,13 +332,14 @@ async function removeEmailFromSuppressionList({state, api}) { popupNotification: createPopupNotification({ type: 'removeEmailFromSuppressionList:failed', autoHide: true, closeable: true, state, status: 'error', - message: 'Your email has failed to resubscribe, please try again' + message: t('Your email has failed to resubscribe, please try again') }) }; } } async function updateNewsletter({data, state, api}) { + const {t} = state; try { const {subscribed} = data; const member = await api.member.update({subscribed}); @@ -341,7 +352,7 @@ async function updateNewsletter({data, state, api}) { member: member, popupNotification: createPopupNotification({ type: action, autoHide: true, closeable: true, state, status: 'success', - message: 'Email newsletter settings updated' + message: t('Email newsletter settings updated') }) }; } catch (e) { @@ -349,7 +360,7 @@ async function updateNewsletter({data, state, api}) { action: 'updateNewsletter:failed', popupNotification: createPopupNotification({ type: 'updateNewsletter:failed', autoHide: true, closeable: true, state, status: 'error', - message: 'Failed to update newsletter settings' + message: t('Failed to update newsletter settings') }) }; } @@ -421,6 +432,7 @@ async function refreshMemberData({state, api}) { } async function updateProfile({data, state, api}) { + const {t} = state; const [dataUpdate, emailUpdate] = await Promise.all([updateMemberData({data, state, api}), updateMemberEmail({data, state, api})]); if (dataUpdate && emailUpdate) { if (emailUpdate.success) { @@ -430,11 +442,11 @@ async function updateProfile({data, state, api}) { page: 'accountHome', popupNotification: createPopupNotification({ type: 'updateProfile:success', autoHide: true, closeable: true, status: 'success', state, - message: 'Check your inbox to verify email update' + message: t('Check your inbox to verify email update') }) }; } - const message = !dataUpdate.success ? 'Failed to update account data' : 'Failed to send verification email'; + const message = !dataUpdate.success ? t('Failed to update account data') : t('Failed to send verification email'); return { action: 'updateProfile:failed', @@ -446,7 +458,7 @@ async function updateProfile({data, state, api}) { } else if (dataUpdate) { const action = dataUpdate.success ? 'updateProfile:success' : 'updateProfile:failed'; const status = dataUpdate.success ? 'success' : 'error'; - const message = !dataUpdate.success ? 'Failed to update account details' : 'Account details updated successfully'; + const message = !dataUpdate.success ? t('Failed to update account details') : t('Account details updated successfully'); return { action, ...(dataUpdate.success ? {member: dataUpdate.member} : {}), @@ -458,7 +470,7 @@ async function updateProfile({data, state, api}) { } else if (emailUpdate) { const action = emailUpdate.success ? 'updateProfile:success' : 'updateProfile:failed'; const status = emailUpdate.success ? 'success' : 'error'; - const message = !emailUpdate.success ? 'Failed to send verification email' : 'Check your inbox to verify email update'; + const message = !emailUpdate.success ? t('Failed to send verification email') : t('Check your inbox to verify email update'); return { action, ...(emailUpdate.success ? {page: 'accountHome'} : {}), @@ -472,7 +484,7 @@ async function updateProfile({data, state, api}) { page: 'accountHome', popupNotification: createPopupNotification({ type: 'updateProfile:success', autoHide: true, closeable: true, status: 'success', state, - message: 'Account details updated successfully' + message: t('Account details updated successfully') }) }; } diff --git a/apps/portal/src/components/pages/FeedbackPage.js b/apps/portal/src/components/pages/FeedbackPage.js index 1720ac52d40..03c6d15d7ca 100644 --- a/apps/portal/src/components/pages/FeedbackPage.js +++ b/apps/portal/src/components/pages/FeedbackPage.js @@ -4,7 +4,7 @@ import {ReactComponent as ThumbDownIcon} from '../../images/icons/thumbs-down.sv import {ReactComponent as ThumbUpIcon} from '../../images/icons/thumbs-up.svg'; import {ReactComponent as ThumbErrorIcon} from '../../images/icons/thumbs-error.svg'; import setupGhostApi from '../../utils/api'; -import {HumanReadableError} from '../../utils/errors'; +import {chooseBestErrorMessage} from '../../utils/errors'; import ActionButton from '../common/ActionButton'; import CloseButton from '../common/CloseButton'; import LoadingPage from './LoadingPage'; @@ -317,7 +317,7 @@ export default function FeedbackPage() { await sendFeedback({siteUrl: site.url, uuid, key, postId, score: selectedScore}, api); setScore(selectedScore); } catch (e) { - const text = HumanReadableError.getMessageFromError(e, t('There was a problem submitting your feedback. Please try again a little later.')); + const text = chooseBestErrorMessage(e, t('There was a problem submitting your feedback. Please try again a little later.'), t); setError(text); } setLoading(false); diff --git a/apps/portal/src/data-attributes.js b/apps/portal/src/data-attributes.js index 9f4e9daefeb..44b50b03393 100644 --- a/apps/portal/src/data-attributes.js +++ b/apps/portal/src/data-attributes.js @@ -1,8 +1,12 @@ /* eslint-disable no-console */ import {getCheckoutSessionDataFromPlanAttribute, getUrlHistory} from './utils/helpers'; -import {HumanReadableError} from './utils/errors'; +import {HumanReadableError, chooseBestErrorMessage} from './utils/errors'; +import i18nLib from '@tryghost/i18n'; -export function formSubmitHandler({event, form, errorEl, siteUrl, submitHandler}) { +export function formSubmitHandler({event, form, errorEl, siteUrl, submitHandler}, + t = (str) => { + return str; + }) { form.removeEventListener('submit', submitHandler); event.preventDefault(); if (errorEl) { @@ -84,13 +88,16 @@ export function formSubmitHandler({event, form, errorEl, siteUrl, submitHandler} }).catch((err) => { if (errorEl) { // This theme supports a custom error element - errorEl.innerText = HumanReadableError.getMessageFromError(err, 'There was an error sending the email, please try again'); + errorEl.innerText = chooseBestErrorMessage(err, t('There was an error sending the email, please try again'), t); } form.classList.add('error'); }); } export function planClickHandler({event, el, errorEl, siteUrl, site, member, clickHandler}) { + const i18nLanguage = site.locale | 'en'; + const i18n = i18nLib(i18nLanguage, 'portal'); + const t = i18n.t; el.removeEventListener('click', clickHandler); event.preventDefault(); let plan = el.dataset.membersPlan; @@ -143,7 +150,7 @@ export function planClickHandler({event, el, errorEl, siteUrl, site, member, cli }) }).then(function (res) { if (!res.ok) { - throw new Error('Could not create stripe checkout session'); + throw new Error(t('Could not create stripe checkout session')); } return res.json(); }); @@ -171,6 +178,9 @@ export function planClickHandler({event, el, errorEl, siteUrl, site, member, cli } export function handleDataAttributes({siteUrl, site, member}) { + const i18nLanguage = site.locale | 'en'; + const i18n = i18nLib(i18nLanguage, 'portal'); + const t = i18n.t; if (!siteUrl) { return; } @@ -178,7 +188,7 @@ export function handleDataAttributes({siteUrl, site, member}) { Array.prototype.forEach.call(document.querySelectorAll('form[data-members-form]'), function (form) { let errorEl = form.querySelector('[data-members-error]'); function submitHandler(event) { - formSubmitHandler({event, errorEl, form, siteUrl, submitHandler}); + formSubmitHandler({event, errorEl, form, siteUrl, submitHandler}, t); } form.addEventListener('submit', submitHandler); }); @@ -234,7 +244,7 @@ export function handleDataAttributes({siteUrl, site, member}) { }) }).then(function (res) { if (!res.ok) { - throw new Error('Could not create stripe checkout session'); + throw new Error(t('Could not create stripe checkout session')); } return res.json(); }); @@ -245,7 +255,7 @@ export function handleDataAttributes({siteUrl, site, member}) { }); }).then(function (result) { if (result.error) { - throw new Error(result.error.message); + throw new Error(t(result.error.message)); } }).catch(function (err) { console.error(err); @@ -323,7 +333,7 @@ export function handleDataAttributes({siteUrl, site, member}) { el.classList.add('error'); if (errorEl) { - errorEl.innerText = 'There was an error cancelling your subscription, please try again.'; + errorEl.innerText = t('There was an error cancelling your subscription, please try again.'); } } }); @@ -373,7 +383,7 @@ export function handleDataAttributes({siteUrl, site, member}) { el.classList.add('error'); if (errorEl) { - errorEl.innerText = 'There was an error continuing your subscription, please try again.'; + errorEl.innerText = t('There was an error continuing your subscription, please try again.'); } } }); diff --git a/apps/portal/src/tests/errors.test.js b/apps/portal/src/tests/errors.test.js new file mode 100644 index 00000000000..599bf38a3bf --- /dev/null +++ b/apps/portal/src/tests/errors.test.js @@ -0,0 +1,58 @@ +import {HumanReadableError, chooseBestErrorMessage} from '../utils/errors'; + +describe('error messages are set correctly', () => { + test('handles 400 error without defaultMessage', async () => { + function t(message) { + return 'translated ' + message; + } + const error = new Response('{"errors":[{"message":"This is a 400 error"}]}', {status: 400}); + const humanReadableError = await HumanReadableError.fromApiResponse(error); + expect(chooseBestErrorMessage(humanReadableError, null, t)).toEqual('translated This is a 400 error'); + }); + + test('handles an error with defaultMessage not a special message', async () => { + function t(message) { + return 'translated ' + message; + } + const error = new Response('{"errors":[{"message":"This is a 400 error"}]}', {status: 400}); + const humanReadableError = await HumanReadableError.fromApiResponse(error); + // note that the default message is passed in already-translated. + expect(chooseBestErrorMessage(humanReadableError, 'translated default message', t)).toEqual('translated default message'); + }); + + test('handles an error with defaultMessage that is a special message', async () => { + function t(message) { + return 'translated ' + message; + } + const error = new Response('{"errors":[{"message":"Too many attempts try again in {{number}} minutes."}]}', {status: 400}); + const humanReadableError = await HumanReadableError.fromApiResponse(error); + expect(chooseBestErrorMessage(humanReadableError, 'this is the default message', t)).toEqual('translated Too many attempts try again in {{number}} minutes.'); + }); + + test('handles an error when the message has a number', async () => { + function t(message, {number: number}) { + return 'translated ' + message + ' ' + number; + } + const error = new Response('{"errors":[{"message":"Too many attempts try again in 10 minutes."}]}', {status: 400}); + const humanReadableError = await HumanReadableError.fromApiResponse(error); + expect(chooseBestErrorMessage(humanReadableError, 'this is the default message', t)).toEqual('translated Too many attempts try again in {{number}} minutes. 10'); + }); + + test('handles a 500 error', async () => { + function t(message) { + return 'translated ' + message; + } + const error = new Response('{"errors":[{"message":"This is a 500 error"}]}', {status: 500}); + const humanReadableError = await HumanReadableError.fromApiResponse(error); + expect(chooseBestErrorMessage(humanReadableError, null, t)).toEqual('translated A server error occurred'); + }); + + test('gets the magic link error message correctly', async () => { + function t(message) { + return 'translated ' + message; + } + const error = new Response('{"errors":[{"message":"Failed to send magic link email"}]}', {status: 400}); + const humanReadableError = await HumanReadableError.fromApiResponse(error); + expect(chooseBestErrorMessage(humanReadableError, null, t)).toEqual('translated Failed to send magic link email'); + }); +}); \ No newline at end of file diff --git a/apps/portal/src/utils/errors.js b/apps/portal/src/utils/errors.js index 7f695160c33..c15c6d9d9a0 100644 --- a/apps/portal/src/utils/errors.js +++ b/apps/portal/src/utils/errors.js @@ -17,16 +17,86 @@ export class HumanReadableError extends Error { return false; } } + if (res.status === 500) { + return new HumanReadableError('A server error occurred'); + } } +} - /** - * Only output the message of an error if it is a human readable error and should be exposed to the user. - * Otherwise it returns a default generic message. - */ - static getMessageFromError(error, defaultMessage) { - if (error instanceof HumanReadableError) { - return error.message; +export const specialMessages = []; + +/** + * Attempt to return the best available message to the user, after translating it. + * We detect special messages coming from the API for which we want to serve a specific translation. + * Many "alreadyTranslatedDefaultMessages" are pretty vague, so we want to replace them with a more specific message + * whenever one is available. + */ +export function chooseBestErrorMessage(error, alreadyTranslatedDefaultMessage, t) { + // helper functions + const translateMessage = (message, number = null) => { + if (number) { + return t(message, {number}); + } else { + return t(message); + } + }; + const setupSpecialMessages = () => { + // eslint-disable-next-line no-shadow + const t = message => specialMessages.push(message); + if (specialMessages.length === 0) { + // This formatting is intentionally weird. It causes the i18n-parser to pick these strings up. + // Do not redefine this t. It's a local function and needs to stay that way. + t('No member exists with this e-mail address. Please sign up first.'); + t('No member exists with this e-mail address.'); + t('This site is invite-only, contact the owner for access.'); + t('Unable to initiate checkout session'); + t('This site is not accepting payments at the moment.'); + t('Too many attempts try again in {{number}} minutes.'); + t('Too many attempts try again in {{number}} hours.'); + t('Too many attempts try again in {{number}} days.'); + t('Too many different sign-in attempts, try again in {{number}} minutes'); + t('Too many different sign-in attempts, try again in {{number}} hours'); + t('Too many different sign-in attempts, try again in {{number}} days'); + t('Failed to send magic link email'); + } + }; + const isSpecialMessage = (message) => { + if (specialMessages.length === 0) { + setupSpecialMessages(); + } + if (specialMessages.includes(message)) { + return true; + } + return false; + }; + + const prepareErrorMessage = (message = null) => { + // Check for a number in message, if found, replace the number with {{number}} and return the number. + // Assumes there's only one number in the message. + if (!message) { + return {preparedMessage: 'An error occurred', number: null}; + } + const number = message.match(/\d+/); + if (number) { + message = message.replace(number[0], '{{number}}'); + } + return {preparedMessage: message, number: number ? number[0] : null}; + }; + + // main function + if (!error && !alreadyTranslatedDefaultMessage) { + return t('An error occurred'); + } + if (error instanceof HumanReadableError || error.message) { + const {preparedMessage, number} = prepareErrorMessage(error.message); + if (isSpecialMessage(preparedMessage)) { + return translateMessage(preparedMessage, number); + } else { + return alreadyTranslatedDefaultMessage || translateMessage(error?.message); } - return defaultMessage; + } else { + // use the default message if there's no error message + // if we don't have a default message either, fall back to a generic message plus the actual error. + return alreadyTranslatedDefaultMessage || t('An error occurred') + ': ' + error.toString(); } } diff --git a/apps/signup-form/package.json b/apps/signup-form/package.json index a6186019527..005f5f8d008 100644 --- a/apps/signup-form/package.json +++ b/apps/signup-form/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/signup-form", - "version": "0.1.6", + "version": "0.2.0", "license": "MIT", "repository": { "type": "git", @@ -65,7 +65,7 @@ "rollup-plugin-node-builtins": "2.1.2", "storybook": "7.6.20", "stylelint": "15.10.3", - "tailwindcss": "3.4.12", + "tailwindcss": "3.4.13", "vite": "4.5.3", "vite-plugin-commonjs": "0.10.1", "vite-plugin-svgr": "3.3.0", diff --git a/apps/sodo-search/package.json b/apps/sodo-search/package.json index 0d90d478650..775538018d1 100644 --- a/apps/sodo-search/package.json +++ b/apps/sodo-search/package.json @@ -1,6 +1,6 @@ { "name": "@tryghost/sodo-search", - "version": "1.1.1", + "version": "1.3.0", "license": "MIT", "repository": { "type": "git", @@ -18,7 +18,7 @@ }, "dependencies": { "@tryghost/content-api": "1.11.21", - "flexsearch": "0.7.21", + "flexsearch": "0.7.43", "react": "17.0.2", "react-dom": "17.0.2" }, diff --git a/apps/sodo-search/src/App.js b/apps/sodo-search/src/App.js index f67ba9f80d8..03bd4a5b6b5 100644 --- a/apps/sodo-search/src/App.js +++ b/apps/sodo-search/src/App.js @@ -3,21 +3,29 @@ import './App.css'; import AppContext from './AppContext'; import PopupModal from './components/PopupModal'; import SearchIndex from './search-index.js'; +import i18nLib from '@tryghost/i18n'; export default class App extends React.Component { constructor(props) { super(props); + const i18nLanguage = this.props.locale || 'en'; + const i18n = i18nLib(i18nLanguage, 'search'); + const dir = i18n.dir() || 'ltr'; + const searchIndex = new SearchIndex({ adminUrl: props.adminUrl, - apiKey: props.apiKey + apiKey: props.apiKey, + dir: dir }); this.state = { searchIndex, showPopup: false, indexStarted: false, - indexComplete: false + indexComplete: false, + t: i18n.t, + dir: dir }; this.inputRef = React.createRef(); @@ -163,7 +171,9 @@ export default class App extends React.Component { ...data }); } - } + }, + t: this.state.t, + dir: this.state.dir }}> diff --git a/apps/sodo-search/src/AppContext.js b/apps/sodo-search/src/AppContext.js index e8648eaa9f6..801ef025506 100644 --- a/apps/sodo-search/src/AppContext.js +++ b/apps/sodo-search/src/AppContext.js @@ -13,7 +13,9 @@ const AppContext = React.createContext({ dispatch: (_action, _data) => {}, searchIndex: null, indexComplete: false, - searchValue: '' + searchValue: '', + t: () => {}, + dir: 'ltr' }); export default AppContext; diff --git a/apps/sodo-search/src/components/Frame.js b/apps/sodo-search/src/components/Frame.js index 3f76de9a218..e2308a05962 100644 --- a/apps/sodo-search/src/components/Frame.js +++ b/apps/sodo-search/src/components/Frame.js @@ -17,6 +17,8 @@ export default class Frame extends Component { setupFrameBaseStyle() { if (this.node.contentDocument) { this.iframeHtml = this.node.contentDocument.documentElement; + // set the iframeHtml dir attribute + this.iframeHtml.setAttribute('dir', this.props.searchdir); this.iframeHead = this.node.contentDocument.head; this.iframeRoot = this.node.contentDocument.body; this.forceUpdate(); diff --git a/apps/sodo-search/src/components/PopupModal.js b/apps/sodo-search/src/components/PopupModal.js index 443cd144794..abd3ec3b64a 100644 --- a/apps/sodo-search/src/components/PopupModal.js +++ b/apps/sodo-search/src/components/PopupModal.js @@ -71,7 +71,7 @@ class PopupContent extends React.Component { } function SearchBox() { - const {searchValue, dispatch, inputRef} = useContext(AppContext); + const {searchValue, dispatch, inputRef, t} = useContext(AppContext); const containerRef = useRef(null); useEffect(() => { setTimeout(() => { @@ -100,7 +100,7 @@ function SearchBox() { return (
-
+
@@ -154,18 +154,18 @@ function Loading() { } function CancelButton() { - const {dispatch} = useContext(AppContext); + const {dispatch, t} = useContext(AppContext); return ( ); } @@ -188,13 +188,15 @@ function TagListItem({tag, selectedResult, setSelectedResult}) { setSelectedResult(id); }} > -

#

+

#

{name}

); } function TagResults({tags, selectedResult, setSelectedResult}) { + const {t} = useContext(AppContext); + if (!tags?.length) { return null; } @@ -210,7 +212,7 @@ function TagResults({tags, selectedResult, setSelectedResult}) { }); return (
-

Tags

+

{t('Tags')}

{TagItems}
); @@ -355,6 +357,8 @@ function HighlightWord({word, isExcerpt}) { } function ShowMoreButton({posts, maxPosts, setMaxPosts}) { + const {t} = useContext(AppContext); + if (!posts?.length || maxPosts >= posts?.length) { return null; } @@ -366,12 +370,13 @@ function ShowMoreButton({posts, maxPosts, setMaxPosts}) { setMaxPosts(updatedMaxPosts); }} > - Show more results + {t('Show more results')} ); } function PostResults({posts, selectedResult, setSelectedResult}) { + const {t} = useContext(AppContext); const [maxPosts, setMaxPosts] = useState(DEFAULT_MAX_POSTS); useEffect(() => { setMaxPosts(DEFAULT_MAX_POSTS); @@ -392,7 +397,7 @@ function PostResults({posts, selectedResult, setSelectedResult}) { }); return (
-

Posts

+

{t('Posts')}

{PostItems}
@@ -428,15 +433,17 @@ function AuthorAvatar({name, avatar}) { const Character = name.charAt(0); if (Avatar) { return ( - {name}/ + {name}/ ); } return ( -
{Character}
+
{Character}
); } function AuthorResults({authors, selectedResult, setSelectedResult}) { + const {t} = useContext(AppContext); + if (!authors?.length) { return null; } @@ -453,7 +460,7 @@ function AuthorResults({authors, selectedResult, setSelectedResult}) { return (
-

Authors

+

{t('Authors')}

{AuthorItems}
); @@ -572,9 +579,10 @@ function Results({posts, authors, tags}) { } function NoResultsBox() { + const {t} = useContext(AppContext); return (
-

No matches found

+

{t('No matches found')}

); } @@ -664,7 +672,7 @@ export default class PopupModal extends React.Component { return (
- +
this.handlePopupClose(e)} className='absolute top-0 bottom-0 left-0 right-0 block backdrop-blur-[2px] animate-fadein z-0 bg-gradient-to-br from-[rgba(0,0,0,0.2)] to-[rgba(0,0,0,0.1)]' /> diff --git a/apps/sodo-search/src/index.js b/apps/sodo-search/src/index.js index fa5627c15bc..f5e9fff7332 100644 --- a/apps/sodo-search/src/index.js +++ b/apps/sodo-search/src/index.js @@ -20,7 +20,8 @@ function getSiteData() { const adminUrl = scriptTag.dataset.sodoSearch; const apiKey = scriptTag.dataset.key; const stylesUrl = scriptTag.dataset.styles; - return {adminUrl, apiKey, stylesUrl}; + const locale = scriptTag.dataset.locale || 'en'; + return {adminUrl, apiKey, stylesUrl, locale}; } return {}; } @@ -30,14 +31,14 @@ function setup() { } function init() { - const {adminUrl, apiKey, stylesUrl} = getSiteData(); + const {adminUrl, apiKey, stylesUrl, locale} = getSiteData(); const adminBaseUrl = (adminUrl || window.location.origin)?.replace(/\/+$/, ''); setup(); ReactDOM.render( , document.getElementById(ROOT_DIV_ID) diff --git a/apps/sodo-search/src/search-index.js b/apps/sodo-search/src/search-index.js index a70595e42d2..6b9967bde66 100644 --- a/apps/sodo-search/src/search-index.js +++ b/apps/sodo-search/src/search-index.js @@ -2,36 +2,43 @@ import Flexsearch from 'flexsearch'; import GhostContentAPI from '@tryghost/content-api'; export default class SearchIndex { - constructor({adminUrl, apiKey}) { + constructor({adminUrl, apiKey, dir}) { this.api = new GhostContentAPI({ url: adminUrl, key: apiKey, version: 'v5.0' }); - + const rtl = (dir === 'rtl'); + const tokenize = (dir === 'rtl') ? 'reverse' : 'forward'; this.postsIndex = new Flexsearch.Document({ - tokenize: 'forward', + tokenize: tokenize, + rtl: rtl, document: { id: 'id', index: ['title', 'excerpt'], store: true - } + }, + ...this.#getEncodeOptions() }); this.authorsIndex = new Flexsearch.Document({ - tokenize: 'forward', + tokenize: tokenize, + rtl: rtl, document: { id: 'id', index: ['name'], store: true - } + }, + ...this.#getEncodeOptions() }); this.tagsIndex = new Flexsearch.Document({ - tokenize: 'forward', + tokenize: tokenize, + rtl: rtl, document: { id: 'id', index: ['name'], store: true - } + }, + ...this.#getEncodeOptions() }); this.init = this.init.bind(this); @@ -133,4 +140,17 @@ export default class SearchIndex { tags: this.#normalizeSearchResult(tags) }; } + + #getEncodeOptions() { + const regex = new RegExp( + `[\u{4E00}-\u{9FFF}\u{3040}-\u{309F}\u{30A0}-\u{30FF}\u{AC00}-\u{D7A3}\u{3400}-\u{4DBF}\u{20000}-\u{2A6DF}\u{2A700}-\u{2B73F}\u{2B740}-\u{2B81F}\u{2B820}-\u{2CEAF}\u{2CEB0}-\u{2EBEF}\u{30000}-\u{3134F}\u{31350}-\u{323AF}\u{2EBF0}-\u{2EE5F}\u{F900}-\u{FAFF}\u{2F800}-\u{2FA1F}]|[0-9A-Za-zа-я\u00C0-\u017F\u0400-\u04FF\u0600-\u06FF\u0980-\u09FF\u1E00-\u1EFF]+`, + 'mug' + ); + + return { + encode: (str) => { + return ('' + str).toLowerCase().match(regex) ?? []; + } + }; + } } diff --git a/apps/sodo-search/src/search-index.test.js b/apps/sodo-search/src/search-index.test.js index d76ffd4626f..99f9f069445 100644 --- a/apps/sodo-search/src/search-index.test.js +++ b/apps/sodo-search/src/search-index.test.js @@ -72,7 +72,7 @@ describe('search index', function () { url: 'http://localhost/ghost/tags/barcelona-tag/' }] }); - + await searchIndex.init(); let searchResults = searchIndex.search('Barcelo'); @@ -93,5 +93,164 @@ describe('search index', function () { expect(searchResults.posts.length).toEqual(0); expect(searchResults.authors.length).toEqual(0); expect(searchResults.tags.length).toEqual(0); + + // confirms that search works in the forward direction for ltr languages: + let searchWithStartResults = searchIndex.search('Barce'); + expect(searchWithStartResults.posts.length).toEqual(1); + + let searchWithEndResults = searchIndex.search('celona'); + expect(searchWithEndResults.posts.length).toEqual(0); + }); + + test('searching works when dir = rtl also', async () => { + const adminUrl = 'http://localhost:3000'; + const apiKey = '69010382388f9de5869ad6e558'; + const searchIndex = new SearchIndex({adminUrl, apiKey, dir: 'ltr', storage: localStorage}); + + nock('http://localhost:3000/ghost/api/content') + .get('/posts/?key=69010382388f9de5869ad6e558&limit=10000&fields=id%2Cslug%2Ctitle%2Cexcerpt%2Curl%2Cupdated_at%2Cvisibility&order=updated_at%20DESC') + .reply(200, { + posts: [{ + id: 'sounique', + title: 'أُظهر المثابرة كل يوم', + excerpt: 'أظهر المثابرة كل يوم. كتابة الاختبارات تحدٍ كبير!', + url: 'http://localhost/ghost/awesome-barcelona-life/' + }, + { + id: 'sounique2', + title: 'هذا منشور عن السعادة', + excerpt: 'هذا منشور عن السعادة. لا يتطابق مع استعلام البحث.', + url: 'http://localhost/ghost/awesome-barcelona-life2/' + }] + }) + .get('/authors/?key=69010382388f9de5869ad6e558&limit=10000&fields=id,slug,name,url,profile_image&order=updated_at%20DESC') + .reply(200, { + authors: [{ + id: 'different_uniq', + slug: 'barcelona-author', + name: 'اسمي المثابرة', + profile_image: 'https://url_to_avatar/barcelona.png', + url: 'http://localhost/ghost/authors/barcelona-author/' + }, { + id: 'different_uniq_2', + slug: 'bob', + name: 'Bob', + profile_image: 'https://url_to_avatar/barcelona.png', + url: 'http://localhost/ghost/authors/bob/' + }] + }) + .get('/tags/?key=69010382388f9de5869ad6e558&&limit=10000&fields=id,slug,name,url&order=updated_at%20DESC&filter=visibility%3Apublic') + .reply(200, { + tags: [{ + id: 'uniq_tag', + slug: 'barcelona-tag', + name: 'المثابرة', + url: 'http://localhost/ghost/tags/barcelona-tag/' + }] + }); + + await searchIndex.init(); + + let searchResults = searchIndex.search('المثابرة'); + expect(searchResults.posts.length).toEqual(1); + expect(searchResults.posts[0].title).toEqual('أُظهر المثابرة كل يوم'); + expect(searchResults.posts[0].url).toEqual('http://localhost/ghost/awesome-barcelona-life/'); + + expect(searchResults.authors.length).toEqual(1); + expect(searchResults.authors[0].name).toEqual('اسمي المثابرة'); + expect(searchResults.authors[0].url).toEqual('http://localhost/ghost/authors/barcelona-author/'); + expect(searchResults.authors[0].profile_image).toEqual('https://url_to_avatar/barcelona.png'); + + expect(searchResults.tags.length).toEqual(1); + expect(searchResults.tags[0].name).toEqual('المثابرة'); + expect(searchResults.tags[0].url).toEqual('http://localhost/ghost/tags/barcelona-tag/'); + + searchResults = searchIndex.search('Nothing like this'); + expect(searchResults.posts.length).toEqual(0); + expect(searchResults.authors.length).toEqual(0); + expect(searchResults.tags.length).toEqual(0); + + let searchWithStartResults = searchIndex.search('المثا'); + expect(searchWithStartResults.posts.length).toEqual(1); + expect(searchWithStartResults.posts[0].title).toEqual('أُظهر المثابرة كل يوم'); + + let searchWithEndResults = searchIndex.search('ثابرة'); + expect(searchWithEndResults.posts.length).toEqual(0); + }); + + test('searching handles CJK characters correctly', async () => { + const adminUrl = 'http://localhost:3000'; + const apiKey = '69010382388f9de5869ad6e558'; + const searchIndex = new SearchIndex({adminUrl, apiKey, dir: 'ltr', storage: localStorage}); + + nock('http://localhost:3000/ghost/api/content') + .get('/posts/?key=69010382388f9de5869ad6e558&limit=10000&fields=id%2Cslug%2Ctitle%2Cexcerpt%2Curl%2Cupdated_at%2Cvisibility&order=updated_at%20DESC') + .reply(200, { + posts: [{ + id: 'sounique', + title: '接收電子報 Regisztráljon fizetős', + excerpt: '要是系統發送電子報時遇到永久失敗的情形,English 該帳號將停止接收電子報 Regisztráljon fizetős fiókot يتطابق a المثابرة كل يوم hozzásćzólások írásához あなたのリクエストはこのサイトの管理者に送信されます。Пријавете го овој коментар Dołączdo płatnej społeczności {{publication}}, by zaąćcąć komećantować. vietnamese: Yêu cầu nhà cung cấp dịch vụ email hỗ trợ bengali: নিউরো সার্জন', + url: 'http://localhost/ghost/visting-china-as-a-polyglot/' + }, + { + id: 'sounique2', + title: 'هذا منشور عن السعادة', + excerpt: 'هذا منشور عن السعادة. لا يتطابق مع استعلام البحث.', + url: 'http://localhost/ghost/a-post-in-arabic/' + }, + { + id: 'sounique3', + title: '毅力和运气', + excerpt: '凭借运气和毅力,Cathy 将通过所有测试。', + url: 'http://localhost/ghost/a-post-in-chinese/' + }] + }) + .get('/authors/?key=69010382388f9de5869ad6e558&limit=10000&fields=id,slug,name,url,profile_image&order=updated_at%20DESC') + .reply(200, { + authors: [{ + id: 'different_uniq', + slug: 'barcelona-author', + name: 'Barcelona Author', + profile_image: 'https://url_to_avatar/barcelona.png', + url: 'http://localhost/ghost/authors/barcelona-author/' + }, { + id: 'different_uniq_2', + slug: 'bob', + name: 'Bob', + profile_image: 'https://url_to_avatar/barcelona.png', + url: 'http://localhost/ghost/authors/bob/' + }] + }) + .get('/tags/?key=69010382388f9de5869ad6e558&&limit=10000&fields=id,slug,name,url&order=updated_at%20DESC&filter=visibility%3Apublic') + .reply(200, { + tags: [{ + id: 'uniq_tag', + slug: 'barcelona-tag', + name: 'Barcelona Tag', + url: 'http://localhost/ghost/tags/barcelona-tag/' + }] + }); + + await searchIndex.init(); + + let searchResults = searchIndex.search('Regisztrálj'); + expect(searchResults.posts.length).toEqual(1); + expect(searchResults.posts[0].url).toEqual('http://localhost/ghost/visting-china-as-a-polyglot/'); + + searchResults = searchIndex.search('Nothing like this'); + expect(searchResults.posts.length).toEqual(0); + + searchResults = searchIndex.search('報'); + expect(searchResults.posts.length).toEqual(1); + expect(searchResults.posts[0].url).toEqual('http://localhost/ghost/visting-china-as-a-polyglot/'); + + // out of order Chinese: + searchResults = searchIndex.search('接子收電'); + expect(searchResults.posts.length).toEqual(1); + expect(searchResults.posts[0].url).toEqual('http://localhost/ghost/visting-china-as-a-polyglot/'); + + // out of order English: + searchResults = searchIndex.search('glenish'); + expect(searchResults.posts.length).toEqual(0); }); }); diff --git a/ghost/admin/app/components/admin-x/admin-x-component.js b/ghost/admin/app/components/admin-x/admin-x-component.js index 0be6116504f..058a046c815 100644 --- a/ghost/admin/app/components/admin-x/admin-x-component.js +++ b/ghost/admin/app/components/admin-x/admin-x-component.js @@ -58,7 +58,12 @@ export const importComponent = async (packageName) => { } const baseUrl = (config.cdnUrl ? `${config.cdnUrl}assets/` : ghostPaths().assetRootWithHost); - const url = new URL(`${baseUrl}${relativePath}/${config[`${configKey}Filename`]}?v=${config[`${configKey}Hash`]}`); + let url = new URL(`${baseUrl}${relativePath}/${config[`${configKey}Filename`]}?v=${config[`${configKey}Hash`]}`); + + const customUrl = config[`${configKey}CustomUrl`]; + if (customUrl) { + url = new URL(customUrl); + } if (url.protocol === 'http:') { window[packageName] = await import(`http://${url.host}${url.pathname}${url.search}`); diff --git a/ghost/admin/app/components/editor/modals/publish-flow/confirm.js b/ghost/admin/app/components/editor/modals/publish-flow/confirm.js index 30706a1216e..9592bd3f974 100644 --- a/ghost/admin/app/components/editor/modals/publish-flow/confirm.js +++ b/ghost/admin/app/components/editor/modals/publish-flow/confirm.js @@ -95,20 +95,28 @@ export default class PublishFlowOptions extends Component { yield this.args.saveTask.perform(); if (this.args.publishOptions.isScheduled) { - localStorage.setItem('ghost-last-scheduled-post', JSON.stringify({ - id: this.args.publishOptions.post.id, - type: this.args.publishOptions.post.displayName - })); + try { + localStorage.setItem('ghost-last-scheduled-post', JSON.stringify({ + id: this.args.publishOptions.post.id, + type: this.args.publishOptions.post.displayName + })); + } catch (e) { + // ignore localStorage errors + } if (this.args.publishOptions.post.displayName !== 'page') { this.router.transitionTo('posts'); } else { this.router.transitionTo('pages'); } } else { - localStorage.setItem('ghost-last-published-post', JSON.stringify({ - id: this.args.publishOptions.post.id, - type: this.args.publishOptions.post.displayName - })); + try { + localStorage.setItem('ghost-last-published-post', JSON.stringify({ + id: this.args.publishOptions.post.id, + type: this.args.publishOptions.post.displayName + })); + } catch (e) { + // ignore localStorage errors + } if (this.args.publishOptions.post.displayName !== 'page') { if (this.args.publishOptions.post.hasEmail) { this.router.transitionTo('posts.analytics', this.args.publishOptions.post.id); diff --git a/ghost/admin/app/components/gh-post-settings-menu.hbs b/ghost/admin/app/components/gh-post-settings-menu.hbs index ee91b2af809..d8143beea1d 100644 --- a/ghost/admin/app/components/gh-post-settings-menu.hbs +++ b/ghost/admin/app/components/gh-post-settings-menu.hbs @@ -126,7 +126,7 @@
- +
\ No newline at end of file diff --git a/ghost/admin/app/components/stats/technical-overview.js b/ghost/admin/app/components/stats/technical-overview.js index 8c7b836cc6a..9e0a7d9ed39 100644 --- a/ghost/admin/app/components/stats/technical-overview.js +++ b/ghost/admin/app/components/stats/technical-overview.js @@ -2,7 +2,7 @@ import Component from '@glimmer/component'; import {action} from '@ember/object'; import {tracked} from '@glimmer/tracking'; -export default class KpisOverview extends Component { +export default class TechnicalOverview extends Component { @tracked selected = 'devices'; @tracked totals = null; diff --git a/ghost/admin/app/controllers/lexical-editor.js b/ghost/admin/app/controllers/lexical-editor.js index 7a6b2e30063..fc7a0b76502 100644 --- a/ghost/admin/app/controllers/lexical-editor.js +++ b/ghost/admin/app/controllers/lexical-editor.js @@ -196,7 +196,7 @@ export default class LexicalEditorController extends Controller { _pushPostState() { const post = this.post; - if (!post) { + if (!post || !post.currentState) { return; } @@ -316,14 +316,10 @@ export default class LexicalEditorController extends Controller { updateScratch(lexical) { const lexicalString = JSON.stringify(lexical); this.set('post.lexicalScratch', lexicalString); - try { - // schedule a local revision save - if (this.post.status === 'draft') { - this.localRevisions.scheduleSave(this.post.displayName, {...this.post.serialize({includeId: true}), lexical: lexicalString}); - } - } catch (err) { - // ignore errors + this.localRevisions.scheduleSave(this.post.displayName, {...this.post.serialize({includeId: true}), lexical: lexicalString}); + } catch (e) { + // ignore revision save errors } // save 3 seconds after last edit @@ -341,12 +337,9 @@ export default class LexicalEditorController extends Controller { updateTitleScratch(title) { this.set('post.titleScratch', title); try { - // schedule a local revision save - if (this.post.status === 'draft') { - this.localRevisions.scheduleSave(this.post.displayName, {...this.post.serialize({includeId: true}), title: title}); - } - } catch (err) { - // ignore errors + this.localRevisions.scheduleSave(this.post.displayName, {...this.post.serialize({includeId: true}), title: title}); + } catch (e) { + // ignore revision save errors } } @@ -505,7 +498,9 @@ export default class LexicalEditorController extends Controller { @action setFeatureImageCaption(html) { - this.post.set('featureImageCaption', html); + if (!this.post.isDestroyed || !this.post.isDestroying) { + this.post.set('featureImageCaption', html); + } } @action @@ -1117,13 +1112,12 @@ export default class LexicalEditorController extends Controller { // triggered any time the admin tab is closed, we need to use a native // dialog here instead of our custom modal - window.onbeforeunload = () => { + window.onbeforeunload = (event) => { if (this.hasDirtyAttributes) { - return '==============================\n\n' - + 'Hey there! It looks like you\'re in the middle of writing' - + ' something and you haven\'t saved all of your content.' - + '\n\nSave before you go!\n\n' - + '=============================='; + console.log('Preventing unload due to hasDirtyAttributes'); // eslint-disable-line + event.preventDefault(); + // Included for legacy support, e.g. Chrome/Edge < 119 + event.returnValue = true; } }; } @@ -1168,6 +1162,11 @@ export default class LexicalEditorController extends Controller { let hasDirtyAttributes = this.hasDirtyAttributes; let state = post.getProperties('isDeleted', 'isSaving', 'hasDirtyAttributes', 'isNew'); + if (state.isDeleted) { + // if the post is deleted, we don't need to save it + hasDirtyAttributes = false; + } + // Check if anything has changed since the last revision let postRevisions = post.get('postRevisions').toArray(); let latestRevision = postRevisions[postRevisions.length - 1]; diff --git a/ghost/admin/app/controllers/restore-posts.js b/ghost/admin/app/controllers/restore-posts.js new file mode 100644 index 00000000000..48e539d4cb9 --- /dev/null +++ b/ghost/admin/app/controllers/restore-posts.js @@ -0,0 +1,22 @@ +import Controller from '@ember/controller'; +import {inject as service} from '@ember/service'; +import {task} from 'ember-concurrency'; + +export default class RestorePostsController extends Controller { + @service localRevisions; + @service notifications; + + @task + *restorePostTask(revision) { + try { + yield this.localRevisions.restore(revision.key); + this.notifications.showNotification('Post restored successfully', {type: 'success'}); + return true; + } catch (error) { + this.notifications.showNotification('Failed to restore post', {type: 'error'}); + // eslint-disable-next-line no-console + console.error('Failed to restore post:', error); + return false; + } + } +} \ No newline at end of file diff --git a/ghost/admin/app/controllers/stats.js b/ghost/admin/app/controllers/stats.js index 8a24429eaa6..a4688ccb43d 100644 --- a/ghost/admin/app/controllers/stats.js +++ b/ghost/admin/app/controllers/stats.js @@ -1,9 +1,21 @@ import Controller from '@ember/controller'; +import countries from 'i18n-iso-countries'; +import enLocale from 'i18n-iso-countries/langs/en.json'; import {AUDIENCE_TYPES, RANGE_OPTIONS} from 'ghost-admin/utils/stats'; import {action} from '@ember/object'; import {tracked} from '@glimmer/tracking'; +countries.registerLocale(enLocale); + export default class StatsController extends Controller { + queryParams = ['device', 'browser', 'location', 'source', 'pathname']; + + @tracked device = null; + @tracked browser = null; + @tracked location = null; + @tracked source = null; + @tracked pathname = null; + rangeOptions = RANGE_OPTIONS; audienceOptions = AUDIENCE_TYPES; /** @@ -17,6 +29,8 @@ export default class StatsController extends Controller { */ @tracked audience = []; @tracked excludedAudiences = ''; + @tracked showStats = true; + @tracked locationHumanReadable = this.location ? (countries.getName(this.location, 'en') || 'Unknown') : null; @action onRangeChange(selected) { @@ -30,14 +44,36 @@ export default class StatsController extends Controller { this.audience = this.audienceOptions .filter(a => !this.excludedAudiences.includes(a.value)) .map(a => a.value); - // this.audience = this.audienceOptions.filter(a => !this.excludedAudiences.includes(a.value)); } else { this.excludedAudiences = ''; this.audience = this.audienceOptions.map(a => a.value); } + + const excludedArray = this.excludedAudiences.split(','); + this.showStats = this.audienceOptions.length !== excludedArray.length; + } + + @action + clearAudienceFilter() { + this.excludedAudiences = ''; + this.audience = this.audienceOptions.map(a => a.value); + this.showStats = true; + } + + @action + clearFilters() { + this.device = null; + this.browser = null; + this.location = null; + this.source = null; + this.pathname = null; } get selectedRangeOption() { return this.rangeOptions.find(d => d.value === this.chartRange); } + + get humanReadableLocation() { + return this.location ? (countries.getName(this.location, 'en') || 'Unknown') : null; + } } diff --git a/ghost/admin/app/router.js b/ghost/admin/app/router.js index 843a86b2e5c..0a82fe0c87a 100644 --- a/ghost/admin/app/router.js +++ b/ghost/admin/app/router.js @@ -33,6 +33,8 @@ Router.map(function () { this.route('posts.analytics', {path: '/posts/analytics/:post_id'}); this.route('posts.mentions', {path: '/posts/analytics/:post_id/mentions'}); this.route('posts.debug', {path: '/posts/analytics/:post_id/debug'}); + + this.route('restore-posts', {path: '/restore'}); this.route('pages'); diff --git a/ghost/admin/app/routes/application.js b/ghost/admin/app/routes/application.js index 58d4a537e85..aa66fce00ca 100644 --- a/ghost/admin/app/routes/application.js +++ b/ghost/admin/app/routes/application.js @@ -7,9 +7,7 @@ import Route from '@ember/routing/route'; import ShortcutsRoute from 'ghost-admin/mixins/shortcuts-route'; import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd'; import windowProxy from 'ghost-admin/utils/window-proxy'; -import {Debug} from '@sentry/integrations'; -import {Replay} from '@sentry/replay'; -import {beforeSend} from 'ghost-admin/utils/sentry'; +import {getSentryConfig} from '../utils/sentry'; import {importComponent} from '../components/admin-x/admin-x-component'; import {inject} from 'ghost-admin/decorators/inject'; import { @@ -184,68 +182,7 @@ export default Route.extend(ShortcutsRoute, { // init Sentry here rather than app.js so that we can use API-supplied // sentry_dsn and sentry_env rather than building it into release assets if (this.config.sentry_dsn) { - const sentryConfig = { - dsn: this.config.sentry_dsn, - environment: this.config.sentry_env, - release: `ghost@${this.config.version}`, - beforeSend, - ignoreErrors: [ - // Browser autoplay policies (this regex covers a few) - /The play\(\) request was interrupted.*/, - /The request is not allowed by the user agent or the platform in the current context/, - - // Network errors that we don't control - /Server was unreachable/, - /NetworkError when attempting to fetch resource./, - /Failed to fetch/, - /Load failed/, - /The operation was aborted./, - - // TransitionAborted errors surface from normal application behaviour - // - https://github.com/emberjs/ember.js/issues/12505 - /^TransitionAborted$/, - // ResizeObserver loop errors occur often from extensions and - // embedded content, generally harmless and not useful to report - /^ResizeObserver loop completed with undelivered notifications/, - /^ResizeObserver loop limit exceeded/, - // When tasks in ember-concurrency are canceled, they sometimes lead to unhandled Promise rejections - // This doesn't affect the application and is not useful to report - // - http://ember-concurrency.com/docs/cancelation - 'TaskCancelation' - ], - integrations: [], - beforeBreadcrumb(breadcrumb) { - // ignore breadcrumbs for event tracking to reduce noise in error reports - if (breadcrumb.category === 'http' && breadcrumb.data?.url?.match(/\/e\.ghost\.org|plausible\.io/)) { - return null; - } - return breadcrumb; - } - }; - - try { - // Session Replay on errors - // Docs: https://docs.sentry.io/platforms/javascript/session-replay - sentryConfig.replaysOnErrorSampleRate = 0.5; - sentryConfig.integrations.push( - // Replace with `Sentry.replayIntegration()` once we've migrated to @sentry/ember 8.x - // Docs: https://docs.sentry.io/platforms/javascript/migration/v7-to-v8/#removal-of-sentryreplay-package - new Replay({ - mask: ['.koenig-lexical', '.gh-dashboard'], - unmask: ['[role="menu"]', '[data-testid="settings-panel"]', '.gh-nav'], - maskAllText: false, - maskAllInputs: true, - blockAllMedia: true - }) - ); - } catch (e) { - // no-op, Session Replay is not critical - console.error('Error enabling Sentry Replay:', e); // eslint-disable-line no-console - } - - if (this.config.sentry_env === 'development') { - sentryConfig.integrations.push(new Debug()); - } + const sentryConfig = getSentryConfig(this.config.sentry_dsn, this.config.sentry_env, this.config.version); Sentry.init(sentryConfig); } diff --git a/ghost/admin/app/routes/lexical-editor/new.js b/ghost/admin/app/routes/lexical-editor/new.js index 1097c518b2c..50ab07d6e13 100644 --- a/ghost/admin/app/routes/lexical-editor/new.js +++ b/ghost/admin/app/routes/lexical-editor/new.js @@ -1,4 +1,8 @@ +import * as Sentry from '@sentry/ember'; import AuthenticatedRoute from 'ghost-admin/routes/authenticated'; +import windowProxy from 'ghost-admin/utils/window-proxy'; +import {action} from '@ember/object'; +import {scheduleOnce} from '@ember/runloop'; export default class NewRoute extends AuthenticatedRoute { model(params, transition) { @@ -13,12 +17,42 @@ export default class NewRoute extends AuthenticatedRoute { } // there's no specific controller for this route, instead all editor - // handling is done on the editor route/controler + // handling is done on the editor route/controller setupController(controller, newPost) { + // logging here because at this stage the controller has definitely not + // had a hand in doing anything to the post + if (!newPost.isNew) { + console.error('New post route did not generate a new model'); // eslint-disable-line no-console + } + let editor = this.controllerFor('lexical-editor'); editor.setPost(newPost); } + // We've seen rare occurrences of getting a newly created model with + // isNew: false which will result in errors when saving because it tries + // a PUT request with no id. This is a safety check to get out of that bad + // state to avoid potential data loss from failed post creation saves. + // + // Because we trigger a browser refresh to get out of this state we need to + // have completed the transition so the refresh occurs on the right URL which + // is why we do this in `didTransition` rather than earlier hooks. + @action + didTransition() { + const controller = this.controllerFor('lexical-editor'); + const post = controller.post; + + if (!post.isNew) { + const newPostAttempt = this.store?.createRecord('post', {authors: [this.session.user]}); + + console.error('New post route transitioned with post.isNew=false', {recreatedPostIsGood: newPostAttempt.isNew}); // eslint-disable-line no-console + Sentry.captureMessage('New post route transitioned with post.isNew=false', {tags: {savePostTask: true}, extra: {recreatedPostIsGood: newPostAttempt.isNew}}); + + // still need to schedule the refresh to allow the transition to fully complete and URL to update + scheduleOnce('afterRender', this, windowProxy.reload); + } + } + buildRouteInfoMetadata() { return { mainClasses: ['editor-new'] diff --git a/ghost/admin/app/routes/restore-posts.js b/ghost/admin/app/routes/restore-posts.js new file mode 100644 index 00000000000..649163ed5ed --- /dev/null +++ b/ghost/admin/app/routes/restore-posts.js @@ -0,0 +1,10 @@ +import AuthenticatedRoute from 'ghost-admin/routes/authenticated'; +import {inject as service} from '@ember/service'; + +export default class RevisionsRoute extends AuthenticatedRoute { + @service localRevisions; + + model() { + return this.localRevisions.findAll(); + } +} \ No newline at end of file diff --git a/ghost/admin/app/services/local-revisions.js b/ghost/admin/app/services/local-revisions.js index 65088e8e944..f1a78b7cd0f 100644 --- a/ghost/admin/app/services/local-revisions.js +++ b/ghost/admin/app/services/local-revisions.js @@ -1,3 +1,4 @@ +import * as Sentry from '@sentry/ember'; import Service, {inject as service} from '@ember/service'; import config from 'ghost-admin/config/environment'; import {task, timeout} from 'ember-concurrency'; @@ -13,6 +14,7 @@ export default class LocalRevisionsService extends Service { } this.MIN_REVISION_TIME = this.isTesting ? 50 : 60000; // 1 minute in ms this.performSave = this.performSave.bind(this); + this.storage = window.localStorage; } @service store; @@ -21,9 +23,6 @@ export default class LocalRevisionsService extends Service { _prefix = 'post-revision'; latestRevisionTime = null; - // key to store a simple index of all revisions - _indexKey = 'ghost-revisions'; - /** * * @param {object} data - serialized post data, must include id and revisionTimestamp @@ -42,15 +41,19 @@ export default class LocalRevisionsService extends Service { */ @task({keepLatest: true}) *saveTask(type, data) { - const currentTime = Date.now(); - if (!this.lastRevisionTime || currentTime - this.lastRevisionTime > this.MIN_REVISION_TIME) { - yield this.performSave(type, data); - this.lastRevisionTime = currentTime; - } else { - const waitTime = this.MIN_REVISION_TIME - (currentTime - this.lastRevisionTime); - yield timeout(waitTime); - yield this.performSave(type, data); - this.lastRevisionTime = Date.now(); + try { + const currentTime = Date.now(); + if (!this.lastRevisionTime || currentTime - this.lastRevisionTime > this.MIN_REVISION_TIME) { + yield this.performSave(type, data); + this.lastRevisionTime = currentTime; + } else { + const waitTime = this.MIN_REVISION_TIME - (currentTime - this.lastRevisionTime); + yield timeout(waitTime); + yield this.performSave(type, data); + this.lastRevisionTime = Date.now(); + } + } catch (err) { + Sentry.captureException(err, {tags: {localRevisions: 'saveTaskError'}}); } } @@ -68,10 +71,10 @@ export default class LocalRevisionsService extends Service { data.revisionTimestamp = Date.now(); const key = this.generateKey(data); try { - const allKeys = this.keys(); - allKeys.push(key); - localStorage.setItem(this._indexKey, JSON.stringify(allKeys)); - localStorage.setItem(key, JSON.stringify(data)); + this.storage.setItem(key, JSON.stringify(data)); + // Apply the filter after saving + this.filterRevisions(data.id); + return key; } catch (err) { if (err.name === 'QuotaExceededError') { @@ -80,11 +83,17 @@ export default class LocalRevisionsService extends Service { // If there are any revisions, remove the oldest one and try to save again if (this.keys().length) { + Sentry.captureMessage('LocalStorage quota exceeded. Removing old revisions.', {tags: {localRevisions: 'quotaExceeded'}}); this.removeOldest(); return this.performSave(type, data); } // LocalStorage is full and there are no revisions to remove // We can't save the revision + Sentry.captureMessage('LocalStorage quota exceeded. Unable to save revision.', {tags: {localRevisions: 'quotaExceededNoSpace'}}); + return; + } else { + Sentry.captureException(err, {tags: {localRevisions: 'saveError'}}); + return; } } } @@ -95,7 +104,9 @@ export default class LocalRevisionsService extends Service { * @param {object} data - serialized post data */ scheduleSave(type, data) { - this.saveTask.perform(type, data); + if (data && data.status && data.status === 'draft') { + this.saveTask.perform(type, data); + } } /** @@ -104,20 +115,27 @@ export default class LocalRevisionsService extends Service { * @returns {string | null} */ find(key) { - return JSON.parse(localStorage.getItem(key)); + return JSON.parse(this.storage.getItem(key)); } /** - * Returns all revisions from localStorage, optionally filtered by key prefix + * Returns all revisions from localStorage as an array, optionally filtered by key prefix and ordered by timestamp * @param {string | undefined} prefix - optional prefix to filter revision keys - * @returns + * @returns {Array} - all revisions matching the prefix, ordered by timestamp (newest first) */ - findAll(prefix = undefined) { + findAll(prefix = this._prefix) { const keys = this.keys(prefix); - const revisions = {}; - for (const key of keys) { - revisions[key] = JSON.parse(localStorage.getItem(key)); - } + const revisions = keys.map((key) => { + const revision = JSON.parse(this.storage.getItem(key)); + return { + key, + ...revision + }; + }); + + // Sort revisions by timestamp, newest first + revisions.sort((a, b) => b.revisionTimestamp - a.revisionTimestamp); + return revisions; } @@ -126,13 +144,7 @@ export default class LocalRevisionsService extends Service { * @param {string} key */ remove(key) { - localStorage.removeItem(key); - const keys = this.keys(); - let index = keys.indexOf(key); - if (index !== -1) { - keys.splice(index, 1); - } - localStorage.setItem(this._indexKey, JSON.stringify(keys)); + this.storage.removeItem(key); } /** @@ -161,11 +173,15 @@ export default class LocalRevisionsService extends Service { * @returns {string[]} */ keys(prefix = undefined) { - let keys = JSON.parse(localStorage.getItem(this._indexKey) || '[]'); - if (prefix) { - keys = keys.filter(key => key.startsWith(prefix)); + const allKeys = []; + const filterPrefix = prefix || this._prefix; + for (let i = 0; i < this.storage.length; i++) { + const key = this.storage.key(i); + if (key.startsWith(filterPrefix)) { + allKeys.push(key); + } } - return keys; + return allKeys; } /** @@ -243,4 +259,22 @@ export default class LocalRevisionsService extends Service { console.warn(err); } } + + /** + * Filters revisions to keep only the most recent 5 for a given post ID + * @param {string} postId - ID of the post to filter revisions for + */ + filterRevisions(postId) { + if (postId === 'draft') { + return; // Ignore filter for drafts + } + + const allRevisions = this.findAll(`${this._prefix}-${postId}`); + if (allRevisions.length > 5) { + const revisionsToRemove = allRevisions.slice(5); + revisionsToRemove.forEach((revision) => { + this.remove(revision.key); + }); + } + } } \ No newline at end of file diff --git a/ghost/admin/app/styles/app-dark.css b/ghost/admin/app/styles/app-dark.css index 46e54fa022a..8436cfef407 100644 --- a/ghost/admin/app/styles/app-dark.css +++ b/ghost/admin/app/styles/app-dark.css @@ -259,7 +259,7 @@ input:focus, } .gh-btn:not(.gh-btn-green):not(.gh-btn-blue):not(.gh-btn-red):not(.gh-btn-primary):not(.gh-btn-black):not(.gh-btn-text):not(.gh-btn-accent):not(.gh-btn-link):not(.gh-editor-preview-trigger) { - background: var(--lightgrey); + border: 1px solid var(--lightgrey); color: var(--black); } @@ -663,6 +663,17 @@ input:focus, background: var(--dark-main-bg-color); } +.gh-posts-list-item-group, +.gh-list-scrolling tbody .gh-list-data, +.gh-list-scrolling thead th { + border-color: var(--lightgrey); +} + +.gh-list-scrolling thead th:first-child::before, +.gh-list-scrolling tbody .gh-list-data:first-child::before { + background: var(--lightgrey); +} + .gh-posts-list-item:hover, .ember-power-select-group .ember-power-select-option[aria-current=true], .settings-tag .tag-edit-button.active, @@ -841,7 +852,6 @@ input:focus, } .gh-post-list-cta { - background: #26282b; color: #ffffff; border-color: #33373d; } @@ -871,6 +881,10 @@ input:focus, } /* Members */ +.members-header .view-actions input.gh-members-list-searchfield { + border-color: var(--lightgrey) +} + .gh-members-help-card, .gh-offers-help-card { background: var(--dark-main-bg-color); @@ -1081,6 +1095,13 @@ kbd { /* Dashboard */ +.gh-dashboard-box.is-secondary, .gh-dashboard-resource-box, +.gh-main-section-content.grey, +.gh-expandable, +.gh-expandable-content { + background: #1C1E22; +} + .gh-dashboard .gh-dashboard-anchor .gh-dashboard-stats { background: transparent; border-top-color: #2b2d31; @@ -1465,4 +1486,10 @@ Publish flow: Share modal */ .modal-post-success .modal-footer .gh-btn:is(.twitter, .threads) svg path { fill: var(--darkgrey); +} + + +/* --------------------------------- */ +.gh-contentfilter-menu-trigger.bordered { + border-color: var(--lightgrey); } \ No newline at end of file diff --git a/ghost/admin/app/styles/components/dropdowns.css b/ghost/admin/app/styles/components/dropdowns.css index 96a1b808a13..2b8c0bb4696 100644 --- a/ghost/admin/app/styles/components/dropdowns.css +++ b/ghost/admin/app/styles/components/dropdowns.css @@ -20,7 +20,7 @@ left: 0; float: left; margin: 2px 0 0; - padding: 6px 0; + padding: 4px 0; min-width: 200px; background-color: #fff; background-clip: padding-box; @@ -61,15 +61,17 @@ display: flex; align-items: center; clear: both; - padding: 6px 14px; - width: 100%; + padding: 7px 10px; + margin: 0 4px; + width: calc(100% - 8px); color: var(--darkgrey); text-align: left; white-space: nowrap; font-size: 1.3rem; + font-weight: 500; line-height: 1.4em; - font-weight: normal; transition: none; + border-radius: var(--border-radius); } .dropdown-menu li > button:disabled { @@ -370,14 +372,9 @@ Post context menu transform: translate(-100%, -100%); } -.gh-posts-context-menu li > button { - padding: 8px 16px; -} - .gh-posts-context-menu li > button span { display: flex; align-items: center; - font-size: 1.35rem; } .gh-posts-context-menu li > button span svg, @@ -401,5 +398,5 @@ Post context menu margin: 5px 0; width: 100%; height: 1px; - background-color: var(--whitegrey-d1); + background-color: var(--whitegrey); } diff --git a/ghost/admin/app/styles/components/modals-new.css b/ghost/admin/app/styles/components/modals-new.css index 84909400f16..66032a15a3f 100644 --- a/ghost/admin/app/styles/components/modals-new.css +++ b/ghost/admin/app/styles/components/modals-new.css @@ -169,7 +169,7 @@ padding: 32px; background-color: #fff; background-clip: padding-box; - border-radius: 3px; + border-radius: 8px; box-shadow: 0 2.8px 2.2px rgba(0, 0, 0, 0.02), 0 6.7px 5.3px rgba(0, 0, 0, 0.028), @@ -218,6 +218,7 @@ line-height: 1.15em; font-weight: 600; letter-spacing: -0.025em; + text-wrap: pretty; } .modal-header.icon-center { diff --git a/ghost/admin/app/styles/components/power-select.css b/ghost/admin/app/styles/components/power-select.css index f4de5724081..4479ca6ed22 100644 --- a/ghost/admin/app/styles/components/power-select.css +++ b/ghost/admin/app/styles/components/power-select.css @@ -21,10 +21,9 @@ } .ember-power-select-trigger:not(.ember-power-select-multiple-trigger):not(.gh-preview-newsletter-trigger) svg { - height: 4px; - width: 6.11px; + height: 6px; margin-left: 2px; - margin-top: -2px; + margin-right: -4px; vertical-align: middle; } @@ -70,15 +69,20 @@ border-top: none; } +.ember-basic-dropdown-trigger--below.ember-power-select-trigger[aria-expanded="true"] { + border-radius: var(--border-radius); +} + .ember-power-select-dropdown.ember-basic-dropdown-content--above { border-top: 1px solid var(--input-border-color); - border-radius: var(--border-radius) var(--border-radius) 0 0; + border-radius: var(--border-radius); } .ember-power-select-option { margin: 0; padding: 6px 14px; color: var(--darkgrey); + height: 32px; } .ember-power-select-option[aria-current="true"] { diff --git a/ghost/admin/app/styles/components/publishmenu.css b/ghost/admin/app/styles/components/publishmenu.css index 7df3f6515fc..cf960e5c43a 100644 --- a/ghost/admin/app/styles/components/publishmenu.css +++ b/ghost/admin/app/styles/components/publishmenu.css @@ -1026,7 +1026,7 @@ .modal-post-success .modal-footer .gh-btn { min-width: 64px; height: 40px; - border-radius: 4px; + border-radius: var(--border-radius); display: flex; justify-content: center; align-items: center; @@ -1058,6 +1058,7 @@ .modal-post-success .modal-footer .gh-btn:is(.twitter, .threads, .facebook, .linkedin) { width: 56px; background: var(--whitegrey-l1); + border-color: transparent; } .modal-post-success .modal-footer .gh-btn:is(.twitter, .threads, .facebook, .linkedin, .copy-link, .copy-preview-link):hover { diff --git a/ghost/admin/app/styles/components/settings-menu.css b/ghost/admin/app/styles/components/settings-menu.css index fa2e25f10fa..9680961464f 100644 --- a/ghost/admin/app/styles/components/settings-menu.css +++ b/ghost/admin/app/styles/components/settings-menu.css @@ -319,7 +319,7 @@ li.nav-list-item .switch { padding: 2rem 2.4rem; } -li.nav-list-item .for-switch.x-small label { +li.nav-list-item .for-switch.xs label { width: initial; height: initial !important; } diff --git a/ghost/admin/app/styles/layouts/content.css b/ghost/admin/app/styles/layouts/content.css index fce010544ae..b59a8c8168a 100644 --- a/ghost/admin/app/styles/layouts/content.css +++ b/ghost/admin/app/styles/layouts/content.css @@ -44,35 +44,47 @@ color: var(--darkgrey); } -.gh-contentfilter-menu-trigger, -.gh-contentfilter-menu-trigger:focus, -.gh-contentfilter-menu-trigger--active { +.gh-contentfilter-menu-trigger { + display: flex; + align-items: center; + gap: 4px; background: var(--white); - font-size: 1.35rem; - font-weight: 400; - color: var(--darkgrey); - letter-spacing: 0.2px; + font-size: 1.3rem; + font-weight: 500; height: 34px; padding: 6px 12px 6px; margin-right: 6px; outline: none; - border: none; - border-radius: 2px !important; + border: 1px solid transparent; + border-radius: 5px !important; white-space: nowrap; transition: all 0.25s ease; overflow: hidden; } +.gh-contentfilter-menu-trigger.ember-power-select-trigger[aria-expanded="true"] { + border: 1px solid transparent !important; +} + +.gh-contentfilter-menu-trigger:focus, +.gh-contentfilter-menu-trigger--active { + border: 1px solid transparent; + background: var(--whitegrey-l1); +} + +.gh-contentfilter-menu-trigger.bordered { + border: 1px solid var(--whitegrey); +} + .gh-contentfilter-menu-trigger:hover { cursor: pointer; color: var(--darkgrey); - background: var(--whitegrey); + background: var(--whitegrey-l1); } .gh-contentfilter-selected .gh-contentfilter-menu-trigger, .gh-contentfilter-selected .gh-contentfilter-menu-trigger:hover { - font-weight: 600; - background: var(--whitegrey-l1); + border: 1px solid var(--whitegrey); } .gh-contentfilter-selected:not(.no-highlight) .gh-contentfilter-menu-trigger, @@ -90,6 +102,10 @@ stroke: var(--black); } +.gh-btn-save-view { + border: 1px solid var(--whitegrey) !important; +} + .gh-btn-save-view svg { margin-top: 3px !important; } @@ -102,7 +118,7 @@ .gh-contentfilter-menu-dropdown { width: 180px; margin-top: 6px; - padding: 6px 0; + padding: 4px 0; border: none !important; font-size: 1.35rem; box-shadow: 0 0 0 1px rgba(0,0,0,.04), 0 7px 20px -5px rgba(0,0,0,.15); @@ -120,6 +136,10 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + padding: 7px 10px; + margin: 0 4px; + border-radius: 3px; + font-size: 1.3rem; } .gh-contentfilter-sort .gh-contentfilter-menu-trigger { @@ -142,7 +162,7 @@ height: 33px; margin: 0 0 0 6px; line-height: 33px; - background: color-mod(var(--whitegrey-l1) l(-3%)); + border: 1px solid var(--whitegrey); } @@ -306,14 +326,11 @@ font-size: 1.3rem; } -.gh-post-list-button { - width: 78px; -} - .gh-list-data.gh-post-list-metrics, .gh-list-data.gh-post-list-button { vertical-align: top; padding-left: 0; + padding-right: 16px; } .gh-list .gh-content-entry-title { @@ -1460,10 +1477,6 @@ padding: 0 0 0 1px; } -.gh-post-list-button { - width: 78px; -} - .gh-post-list-cta { display: flex; align-items: center; @@ -1476,19 +1489,18 @@ border-radius: var(--border-radius); transition: all .2s ease; white-space: nowrap; - height: 38px; + height: 36px; overflow: hidden; transition: all .1s linear; } .gh-post-list-cta:hover { border-color: var(--lightgrey-l2); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); } .gh-list-data .gh-post-list-cta { justify-content: center; - width: 56px; + width: 52px; } .gh-post-analytics-header .gh-post-list-cta.edit { @@ -1498,17 +1510,14 @@ .gh-post-analytics-header .share svg { margin-top: -2px; -} - -.gh-post-analytics-header .gh-btn.refresh { - border: 1px solid var(--whitegrey-d1); - background: var(--white); + width: 1.5rem; + height: 1.5rem; color: var(--darkgrey); + stroke: var(--darkgrey); } -.gh-post-analytics-header .gh-btn.refresh:hover { - border-color: var(--lightgrey-l2); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); +.gh-post-analytics-header .gh-btn-action-icon { + margin-right: 0; } .gh-post-analytics-header .gh-btn.refresh svg path { @@ -1519,23 +1528,13 @@ border-color: var(--whitegrey-d2); } -.gh-post-list-cta.stats.is-hovered { - color: var(--green); -} - .gh-post-list-cta.stats.is-hovered:hover { border-color: var(--lightgrey-l2); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); -} - -.gh-post-list-cta.edit.is-hovered { - color: var(--pink); } .gh-post-list-cta.edit.is-hovered:hover, .gh-post-list-cta.edit:not(.is-hovered):hover { border-color: var(--lightgrey-l2); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); } .gh-post-list-cta > svg { @@ -1562,17 +1561,6 @@ span.dropdown .gh-post-list-cta > span { padding: 0; } -.gh-post-list-cta.edit.is-hovered > *, -.gh-post-list-cta.edit.is-hovered:hover > *, -.gh-post-list-cta.edit:not(.is-hovered):hover > * { - color: var(--pink-d1); -} - -.gh-post-list-cta.stats.is-hovered > *, -.gh-post-list-cta.stats.is-hovered:hover > * { - color: var(--green-d1); -} - @media screen and (max-width: 1200px) { .gh-post-analytics-box.resources { flex-direction: column; diff --git a/ghost/admin/app/styles/layouts/dashboard.css b/ghost/admin/app/styles/layouts/dashboard.css index 49ef8ed9db4..9febf7936fd 100644 --- a/ghost/admin/app/styles/layouts/dashboard.css +++ b/ghost/admin/app/styles/layouts/dashboard.css @@ -344,12 +344,6 @@ Dashboard Layout */ } .gh-dashboard-select .ember-power-select-selected-item { - font-size: 1.25rem; - font-weight: 600; - letter-spacing: -.1px; - line-height: 1em; - padding: 0 0 10px; - color: var(--middarkgrey); white-space: nowrap; } @@ -2925,7 +2919,7 @@ Onboarding checklist */ .gh-onboarding-item-content { padding-right: 16px; } - + .gh-onboarding-item--next { padding: 24px 20px; } @@ -3155,7 +3149,7 @@ Share publication modal */ flex-direction: row; } -.gh-share-links li { +.gh-share-links li { font-size: 1.5rem; font-weight: 600; color: var(--darkgrey); @@ -3169,7 +3163,7 @@ Share publication modal */ margin: 0; } -.gh-share-links li a { +.gh-share-links li a { display: block; padding: 20px 0; text-align: center; @@ -3226,7 +3220,7 @@ span.gh-tip { display: block; } -span.gh-tip a { +span.gh-tip a { text-decoration: underline; color: var(--midgrey); } diff --git a/ghost/admin/app/styles/layouts/editor.css b/ghost/admin/app/styles/layouts/editor.css index 2a9ad8304f9..51e25574b54 100644 --- a/ghost/admin/app/styles/layouts/editor.css +++ b/ghost/admin/app/styles/layouts/editor.css @@ -331,6 +331,7 @@ .gh-btn-editor { background: var(--white) !important; + border-color: transparent !important; } .gh-btn-editor:not(.gh-publish-trigger) span { @@ -338,11 +339,11 @@ } .gh-btn-editor:hover { - background: var(--whitegrey) !important; + background: var(--whitegrey-l1) !important; } .gh-btn-editor.active { - background: var(--whitegrey) !important; + background: var(--whitegrey-l1) !important; } .gh-btn-editor.green span { @@ -437,7 +438,7 @@ bottom: 22px; display: flex; align-items: center; - border-radius: 3px; + border-radius: var(--border-radius); background: #fff; cursor: pointer; } @@ -448,7 +449,7 @@ .gh-editor-feedback-dropdown { min-width: 400px; - border-radius: 3px; + border-radius: var(--border-radius); box-shadow: 0 0 0 1px rgba(0, 0, 0, .04), 0 8px 20px -3px rgba(0, 0, 0, .2); padding: 20px; background-color: #fff; @@ -878,7 +879,7 @@ body[data-user-is-dragging] .gh-editor-feature-image-dropzone { line-height: 34px; white-space: nowrap; background: var(--white); - border-radius: 3px; + border-radius: var(--border-radius); transition: all 0.25s ease; transition-property: color, border-color, background, width, height, box-shadow; } @@ -921,7 +922,7 @@ body[data-user-is-dragging] .gh-editor-feature-image-dropzone { letter-spacing: .2px; line-height: 34px; background: var(--white); - border-radius: 3px; + border-radius: var(--border-radius); } @media (max-width: 500px) { diff --git a/ghost/admin/app/styles/layouts/main.css b/ghost/admin/app/styles/layouts/main.css index c542e368ff6..c1b8688adc9 100644 --- a/ghost/admin/app/styles/layouts/main.css +++ b/ghost/admin/app/styles/layouts/main.css @@ -1616,9 +1616,7 @@ } .view-actions .gh-btn:not(.gh-btn-primary):not(.gh-btn-blue):not(.gh-btn-green):not(.gh-btn-link) { - border: none; box-shadow: none; - background: color-mod(var(--whitegrey-l1) l(-3%)); } .view-actions .gh-btn:not(.gh-btn-primary):not(.gh-btn-blue):not(.gh-btn-green):not(.gh-btn-link):hover { diff --git a/ghost/admin/app/styles/layouts/member-activity.css b/ghost/admin/app/styles/layouts/member-activity.css index 99fc969dd75..162d866e8e6 100644 --- a/ghost/admin/app/styles/layouts/member-activity.css +++ b/ghost/admin/app/styles/layouts/member-activity.css @@ -316,13 +316,19 @@ display: flex; align-items: center; justify-content: space-between; + font-size: 13px; + border-radius: calc(var(--border-radius) - 2px); +} + +.gh-member-activity-actions-menu-item:hover { + background: color-mod(var(--whitegrey) a(60%) s(-12%)); } .gh-member-activity-actions-menu--suppression { min-width: 260px; } .gh-member-activity-actions-menu--suppression.gh-member-activity-actions-menu { - padding: 1rem 0; + padding: 4px 0; } .gh-member-activity-actions-menu--suppression.gh-member-activity-actions-menu .ember-power-select-option:first-child { @@ -332,13 +338,13 @@ padding-bottom: 0; } -.gh-member-activity-actions-menu--suppression.gh-member-activity-actions-menu .for-switch.x-small label { +.gh-member-activity-actions-menu--suppression.gh-member-activity-actions-menu .for-switch.xs label { width: 34px !important; height: 20px !important; padding: 0; } -.gh-member-activity-actions-menu--suppression.gh-member-activity-actions-menu .for-switch.x-small { +.gh-member-activity-actions-menu--suppression.gh-member-activity-actions-menu .for-switch.xs { display: flex; align-items: center; } @@ -355,7 +361,9 @@ } .gh-member-activity-actions-menu--suppression.gh-member-activity-actions-menu .ember-power-select-option { - padding: 0.3rem 1.6rem; + padding: 0 1.0rem; + min-height: 32px; + height: 32px; } .gh-member-activity-actions-menu--suppression.gh-member-activity-actions-menu .ember-power-select-option label { @@ -365,8 +373,9 @@ .gh-member-activity-actions-menu--suppression .gh-member-activity-actions-menu-divider { height: 1px; - margin: 1rem 0; + margin: 1rem -4px; background: var(--lightgrey-l1); + padding: 0; } /* End of styles for dropdown for suppressionList feature */ @@ -380,6 +389,7 @@ .gh-member-activity-actions-menu .ember-power-select-options[role=listbox] { max-height: 60vh; + padding: 0 4px; } .gh-member-activity-actions-menu .ember-power-select-option { diff --git a/ghost/admin/app/styles/layouts/members.css b/ghost/admin/app/styles/layouts/members.css index bfbd1ce2e3d..c220f9e0ff7 100644 --- a/ghost/admin/app/styles/layouts/members.css +++ b/ghost/admin/app/styles/layouts/members.css @@ -101,9 +101,10 @@ .members-header .view-actions input.gh-members-list-searchfield { min-width: 220px; padding-left: 32px; - border: none; - background: var(--white); + height: 34px; + background: var(--whitegrey-l1); border: var(--input-border); + border-color: transparent; } .members-header.grey .view-actions .gh-btn, @@ -121,7 +122,7 @@ height: 16px; top: 9px; left: 9px; - fill: var(--middarkgrey); + fill: var(--midlightgrey); } .members-header.black .view-actions input.gh-members-list-searchfield { diff --git a/ghost/admin/app/styles/layouts/post-history.css b/ghost/admin/app/styles/layouts/post-history.css index 590550a85f3..45e5c944537 100644 --- a/ghost/admin/app/styles/layouts/post-history.css +++ b/ghost/admin/app/styles/layouts/post-history.css @@ -161,7 +161,7 @@ background: var(--white); } -.gh-post-history-footer .for-switch.x-small { +.gh-post-history-footer .for-switch.xs { width: 100%; } @@ -174,7 +174,7 @@ font-weight: 500; } -.gh-post-history-footer .for-switch.x-small label { +.gh-post-history-footer .for-switch.xs label { width: inherit !important; height: inherit !important; } diff --git a/ghost/admin/app/styles/layouts/stats.css b/ghost/admin/app/styles/layouts/stats.css index e0e82be9836..56f2f481536 100644 --- a/ghost/admin/app/styles/layouts/stats.css +++ b/ghost/admin/app/styles/layouts/stats.css @@ -1,3 +1,13 @@ +.gh-stats-header header { + display: grid; + grid-template-columns: auto 1fr; + gap: 20px; +} + +.gh-stats-header header .view-actions { + justify-self: end; +} + .gh-stats .view-container { display: flex; flex-direction: column; @@ -143,7 +153,7 @@ .gh-stats-piechart-container { display: flex; gap: 16px; - align-items: flex-start; + align-items: center; width: 100%; } @@ -164,7 +174,7 @@ width: 100%; height: 100%; flex-grow: 1; - margin-right: -20px; + margin-left: -20px; } .gh-stats-section-dropdown { @@ -198,7 +208,7 @@ } } -.gh-stats-detail-header { +.gh-stats-data-header { font-size: 12px; font-weight: 500; text-transform: none; @@ -216,10 +226,22 @@ color: var(--green); } -.gh-stats-detail-label, -.gh-stats-detail-value { - font-size: 13.5px; +.gh-stats-data-label, +.gh-stats-data-value { font-weight: 500; + font-size: 13px; +} + +.gh-stats-data-label { + color: var(--black); +} + +a.gh-stats-data-label:hover { + text-decoration: underline; +} + +.gh-stats-data-value { + color: var(--middarkgrey); } .gh-stats-see-all-container { @@ -227,6 +249,7 @@ } .gh-stats-see-all-container::before { + pointer-events: none; position: absolute; display: block; content: ''; @@ -238,6 +261,17 @@ z-index: 9999; } +.gh-stats-all-container { + max-height: calc(100vh - 12vw - 172px); + overflow: auto; + margin: -4px -32px; + padding: 4px 32px; +} + +.gh-stats-all-container > div > div { + overflow-y: unset !important; +} + .gh-stats-kpis-chart-container { margin-top: -20px; } @@ -246,9 +280,174 @@ display: flex; align-items: center; gap: 6px; + color: var(--black); + line-height: 1.3em; +} + +.gh-stats-domain:hover span { + text-decoration: underline; } .gh-stats-favicon { width: 16px; height: 16px; +} + +.gh-stats-domain span { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; +} + +.gh-stats-tooltip-header { + font-weight: 600; + font-size: 13px; +} + +.gh-stats-tooltip-data { + display: flex; + align-items: baseline; +} + +.gh-stats-tooltip-label { + font-weight: 400; + font-size: 13px; + color: var(--middarkgrey) +} + +.gh-stats-tooltip-value { + font-family: var(--font-family-mono); + font-size: 12.5px; + margin-left: 10px; +} + +.gh-stats-tooltip-marker { + display: block; + width: 10px; + height: 10px; + border-radius: 3px; + margin-right: 3px; +} + +/* Filters */ +.gh-stats-filters { + display: flex; + gap: 8px; + margin-bottom: -24px; +} + +.gh-stats-filters { + grid-column: span 2; +} + +.gh-stats-filter-pill { + display: flex; + gap: 4px; + align-items: center; + font-size: 13px; + font-weight: 500; + /* border: 1px solid var(--purple); */ + border: 1px solid rgba(142,68,254,0.75); + border-radius: 999px; + padding: 4px 12px; + background-color: rgba(142,68,254,0.06); +} + +.gh-stats-filter-pill .value { + font-weight: 700; +} + +.gh-btn-clear-filters { + display: flex; + align-items: center; + gap: 4px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + border-radius: 999px; + color: var(--black); + padding: 4px 12px; + border: 1px solid var(--whitegrey); + transition: all ease-in-out 0.3s; +} + +.gh-btn-clear-filters:hover { + background-color: var(--whitegrey-l2); +} + +.gh-btn-clear-filters svg { + width: 10px; + height: 10px; + stroke: var(--red); +} + +.gh-btn-clear-filters svg path { + stroke-width: 2px; +} + +.gh-stats-audience-filter-menu { + max-width: 240px; + min-width: 220px; +} + +.gh-stats-placeholder { + width: 60px; + color: var(--lightgrey); + margin-bottom: -8px; +} + +.gh-stats-technical-container { + height: 100%; + display: flex; + flex-direction: column; +} + +.gh-stats-technical-data { + flex-grow: 1; + display: flex; + flex-direction: row; + align-items: center; +} + +.gh-stats-audience-filter-btn-label { + padding: 0 !important; + margin: 0 !important; +} + +@media (max-width: 1440px) { + .gh-stats-filters { + margin-bottom: 0; + } +} + +@media (max-width: 1140px) { + .gh-stats-grid.cols-2 { + grid-template-columns: 1fr; + } + + .gh-stats-tab.is-selected:before { + display: none; + } + + .gh-stats-tab.is-selected { + border-bottom: 2px solid var(--black); + } +} + +@media (max-width: 990px) { + .gh-stats-tabs { + max-width: 100%; + overflow: auto; + } +} + +@media (max-width: 390px) { + .gh-stats-audience-filter-btn-label { + display: none !important; + } + + .gh-stats-btn-audience-filter svg { + margin-right: 0 !important; + } } \ No newline at end of file diff --git a/ghost/admin/app/styles/patterns/buttons.css b/ghost/admin/app/styles/patterns/buttons.css index 50bf16a8091..70516c32cdd 100644 --- a/ghost/admin/app/styles/patterns/buttons.css +++ b/ghost/admin/app/styles/patterns/buttons.css @@ -3,35 +3,36 @@ /* Base button style */ /* Should only be applied to tags */ -.gh-btn, -.gh-btn-grey { +.gh-btn { display: inline-block; outline: none; - background: var(--whitegrey); + border: 1px solid var(--whitegrey-d1); color: var(--darkgrey); text-decoration: none !important; user-select: none; fill: var(--white); font-weight: 500; - border-radius: 3px; + border-radius: var(--border-radius); transition: all 0.2s ease; transition-property: color, border-color, background, width, height, box-shadow; -webkit-font-smoothing: subpixel-antialiased; } +.gh-btn.no-border { + border: none; +} + /* ALL buttons must have a span for content */ -.gh-btn span, -.gh-btn-grey span { +.gh-btn span { display: block; overflow: hidden; padding: 0 14px; height: 34px; - font-size: 1.35rem; + font-size: 1.3rem; line-height: 34px; text-align: center; - letter-spacing: 0.2px; - border-radius: 3px; + border-radius: var(--border-radius); white-space: nowrap; text-overflow: ellipsis; } @@ -45,10 +46,8 @@ letter-spacing: .4px; } -.gh-btn:hover, -.gh-btn-grey:hover { - color: var(--darkgrey); - background: var(--whitegrey-d1); +.gh-btn:hover { + background: var(--whitegrey-l1); } .gh-btn svg { @@ -76,7 +75,7 @@ fieldset[disabled] .gh-btn { .gh-btn-black { color: var(--white); background: var(--black); - font-weight: 500; + border: none; } .gh-btn-primary:hover, @@ -93,18 +92,20 @@ fieldset[disabled] .gh-btn { color: #fff; fill: #fff; background: var(--blue); - font-weight: 500; + border: none; } .gh-btn-blue:hover { color: #fff !important; background: color-mod(var(--blue) l(-4%)) !important; + border: none; } /* When clicked or focused with keyboard */ .gh-btn-blue:active, .gh-btn-blue:focus { background: color-mod(var(--blue) l(-7%)) !important; + border: none; } /* Green button @@ -115,7 +116,6 @@ fieldset[disabled] .gh-btn { color: #fff; fill: #fff; background: var(--green); - font-weight: 500; } .gh-btn-green:hover { @@ -139,7 +139,7 @@ fieldset[disabled] .gh-btn { fill: #fff; box-shadow: none; background: var(--red); - font-weight: 500; + border: none; } .gh-btn-red:hover { @@ -263,7 +263,7 @@ fieldset[disabled] .gh-btn { .gh-btn:not(.gh-btn-blue):not(.gh-btn-green):not(.gh-btn-red) svg.gh-icon-spinner rect { fill: color-mod(var(--midgrey) l(-7%)); -} +} .gh-btn-icon-right svg, svg.gh-btn-icon-right { @@ -377,6 +377,13 @@ svg.gh-btn-icon-right { width: 100%; } +.gh-btn-dropdown-arrow { + margin-left: 5px !important; + margin-right: -4px !important; + height: 6px !important; + width: auto !important; +} + /* /* Button Variations @@ -418,7 +425,7 @@ Usage: CTA buttons grouped together horizontally. display: flex; align-items: center; background: var(--whitegrey-l1); - border-radius: 3px; + border-radius: var(--border-radius); line-height: 0; } @@ -437,7 +444,7 @@ Usage: CTA buttons grouped together horizontally. border-radius: 0; height: 30px; line-height: 30px; - border-radius: 3px; + border-radius: calc(var(--border-radius) - 1px); background: transparent !important; font-weight: 500 !important; } diff --git a/ghost/admin/app/styles/patterns/forms.css b/ghost/admin/app/styles/patterns/forms.css index bf0a74b0859..cb3eba844d9 100644 --- a/ghost/admin/app/styles/patterns/forms.css +++ b/ghost/admin/app/styles/patterns/forms.css @@ -540,17 +540,12 @@ textarea { display: inline-block; } -.for-switch label:not(.x-small .switch), +.for-switch label:not(.xs .switch):not(.xxs .switch), .for-switch .container { width: 50px !important; height: 28px !important; } -.for-switch.x-small label { - width: 34px !important; - height: 20px !important; -} - .for-switch label p, .for-switch .container p { overflow: auto; @@ -576,7 +571,7 @@ textarea { width: 48px !important; height: 26px !important; border-radius: 999px; - transition: background 0.15s ease-in-out, border-color 0.15s ease-in-out; + transition: all 0.15s ease-in-out; } .for-switch label:hover input:not(:checked) + .input-toggle-component, @@ -595,6 +590,7 @@ textarea { transition: .3s; box-shadow: 0 1px 3px rgba(0,0,0,.15); border-radius: 999px; + transition: all 0.15s ease-in-out; } .for-switch input:checked + .input-toggle-component { @@ -625,22 +621,47 @@ textarea { .for-switch.small input:checked + .input-toggle-component:before { transform: translateX(16px); + transition: all 0.15s ease-in-out; } -.for-switch.x-small .input-toggle-component { +.for-switch.xs label { width: 34px !important; height: 20px !important; } -.for-switch.x-small .input-toggle-component:before { +.for-switch.xs .input-toggle-component { + width: 34px !important; + height: 20px !important; +} + +.for-switch.xs .input-toggle-component:before { height: 16px !important; width: 16px !important; } -.for-switch.x-small input:checked + .input-toggle-component:before { +.for-switch.xs input:checked + .input-toggle-component:before { transform: translateX(14px); } +.for-switch.xxs label { + width: 28px !important; + height: 16px !important; +} + +.for-switch.xxs .input-toggle-component { + width: 28px !important; + height: 16px !important; +} + +.for-switch.xxs .input-toggle-component:before { + height: 12px !important; + width: 12px !important; +} + +.for-switch.xxs input:checked + .input-toggle-component:before { + transform: translateX(12px); +} + .for-switch.disabled { opacity: 0.5; pointer-events: none; diff --git a/ghost/admin/app/styles/patterns/global.css b/ghost/admin/app/styles/patterns/global.css index 4c8769a93c1..bee100121d8 100644 --- a/ghost/admin/app/styles/patterns/global.css +++ b/ghost/admin/app/styles/patterns/global.css @@ -169,7 +169,7 @@ --main-layout-section-vpadding: 3vw; /* Style values */ - --border-radius: 3px; + --border-radius: 5px; --font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Droid Sans", "Helvetica Neue", sans-serif; --font-family-mono: Consolas, "Liberation Mono", Menlo, Courier, monospace; @@ -199,7 +199,7 @@ /* Inputs */ --input-bg-color: var(--white); - --input-border-color: var(--whitegrey-d2); + --input-border-color: var(--whitegrey-d1); --input-border: var(--input-border-color) 1px solid; } diff --git a/ghost/admin/app/templates/dashboard.hbs b/ghost/admin/app/templates/dashboard.hbs index ce1f815381d..0a37f4748c6 100644 --- a/ghost/admin/app/templates/dashboard.hbs +++ b/ghost/admin/app/templates/dashboard.hbs @@ -68,7 +68,7 @@ @searchEnabled={{false}} @onChange={{this.onDaysChange}} @triggerComponent={{component "gh-power-select/trigger"}} - @triggerClass="gh-contentfilter-menu-trigger" + @triggerClass="gh-contentfilter-menu-trigger bordered" @dropdownClass="gh-contentfilter-menu-dropdown is-narrow" @matchTriggerWidth={{false}} @horizontalPosition="right" diff --git a/ghost/admin/app/templates/restore-posts.hbs b/ghost/admin/app/templates/restore-posts.hbs new file mode 100644 index 00000000000..f04cd2e6209 --- /dev/null +++ b/ghost/admin/app/templates/restore-posts.hbs @@ -0,0 +1,50 @@ +
+ +
+

+ Restore Posts +

+
+
+
+

Posts are regularly saved locally on your device. If you've lost a post, you can restore it from here as long as too much time hasn't passed.

+
    + {{#if this.model.length}} +
  1. +
    Title
    +
    Created
    +
    +
  2. + +
  3. +
    +

    {{if revision.title revision.title "(no title)"}}

    +

    {{truncate revision.excerpt 100}}

    +
    +
    + {{moment-format revision.revisionTimestamp "MMM D, YYYY HH:mm"}} +
    +
    + +
    +
  4. +
    + {{else}} +
  5. +
    + {{svg-jar "revision-placeholder" class="gh-revisions-placeholder"}} +

    No local revisions found.

    +
    +
  6. + {{/if}} +
+
+
\ No newline at end of file diff --git a/ghost/admin/app/templates/stats.hbs b/ghost/admin/app/templates/stats.hbs index 96e513308da..7345109668b 100644 --- a/ghost/admin/app/templates/stats.hbs +++ b/ghost/admin/app/templates/stats.hbs @@ -1,51 +1,140 @@
- + -
- +
+ - - {{#if option.name}}{{option.name}}{{else}}Unknown option{{/if}} - -
+ + {{#if option.name}}{{option.name}}{{else}}Unknown option{{/if}} + +
+ + {{#if (or this.device this.browser this.location this.source this.pathname)}} +
+ {{/if}}
+ + {{#if this.showStats}}
- +
- +
- +
- +
- +
+ {{else}} +
+
+ {{svg-jar "stats-placeholder" class="gh-stats-placeholder"}} +

No stats available for the current filter

+ +
+
+ No audience selected + {{/if}}
diff --git a/ghost/admin/app/utils/sentry.js b/ghost/admin/app/utils/sentry.js index 4677f68fa25..11a60d1ad53 100644 --- a/ghost/admin/app/utils/sentry.js +++ b/ghost/admin/app/utils/sentry.js @@ -1,6 +1,99 @@ -import { - isAjaxError -} from 'ember-ajax/errors'; +import {Debug} from '@sentry/integrations'; +import {Replay} from '@sentry/replay'; +import {isAjaxError} from 'ember-ajax/errors'; + +export function getSentryConfig(dsn, environment, appVersion, transport) { + const extraIntegrations = []; + + const config = { + dsn, + transport, + environment, + release: `ghost@${appVersion}`, + beforeSend, + ignoreErrors: [ + // Browser autoplay policies (this regex covers a few) + /The play\(\) request was interrupted.*/, + /The request is not allowed by the user agent or the platform in the current context/, + + // Network errors that we don't control + /Server was unreachable/, + /NetworkError when attempting to fetch resource./, + /Failed to fetch/, + /Load failed/, + /The operation was aborted./, + + // TransitionAborted errors surface from normal application behaviour + // - https://github.com/emberjs/ember.js/issues/12505 + /^TransitionAborted$/, + // ResizeObserver loop errors occur often from extensions and + // embedded content, generally harmless and not useful to report + /^ResizeObserver loop completed with undelivered notifications/, + /^ResizeObserver loop limit exceeded/, + // When tasks in ember-concurrency are canceled, they sometimes lead to unhandled Promise rejections + // This doesn't affect the application and is not useful to report + // - http://ember-concurrency.com/docs/cancelation + 'TaskCancelation' + ], + integrations: function (integrations) { + // integrations will be all default integrations + const defaultIntegrations = integrations.filter((integration) => { + // Don't dedupe events when testing + if (environment === 'testing' && integration.name === 'Dedupe') { + return false; + } + + return true; + }); + + return [...defaultIntegrations, ...extraIntegrations]; + }, + beforeBreadcrumb(breadcrumb) { + // ignore breadcrumbs for event tracking to reduce noise in error reports + if (breadcrumb.category === 'http' && breadcrumb.data?.url?.match(/\/e\.ghost\.org|plausible\.io/)) { + return null; + } + return breadcrumb; + } + }; + + if (environment !== 'testing') { + try { + // Session Replay on errors + // Docs: https://docs.sentry.io/platforms/javascript/session-replay + config.replaysOnErrorSampleRate = 0.5; + extraIntegrations.push( + // Replace with `Sentry.replayIntegration()` once we've migrated to @sentry/ember 8.x + // Docs: https://docs.sentry.io/platforms/javascript/migration/v7-to-v8/#removal-of-sentryreplay-package + new Replay({ + mask: ['.koenig-lexical', '.gh-dashboard'], + unmask: ['[role="menu"]', '[data-testid="settings-panel"]', '.gh-nav'], + maskAllText: false, + maskAllInputs: true, + blockAllMedia: true + }) + ); + } catch (e) { + // no-op, Session Replay is not critical + console.error('Error enabling Sentry Replay:', e); // eslint-disable-line no-console + } + } + + if (environment === 'development') { + extraIntegrations.push(new Debug()); + } + + return config; +} + +export function getSentryTestConfig(transport) { + return getSentryConfig( + 'https://abcdef0123456789abcdef0123456789@o12345.ingest.sentry.io/1234567', + 'testing', + '5.0.0', + transport + ); +} export function beforeSend(event, hint) { try { diff --git a/ghost/admin/app/utils/stats.js b/ghost/admin/app/utils/stats.js index 1e77f2913d6..df68a2719ea 100644 --- a/ghost/admin/app/utils/stats.js +++ b/ghost/admin/app/utils/stats.js @@ -1,3 +1,5 @@ +import moment from 'moment-timezone'; + export const RANGE_OPTIONS = [ {name: 'Last 24 hours', value: 1}, {name: 'Last 7 days', value: 7}, @@ -109,7 +111,7 @@ export function generateMonochromePalette(baseColor, count = 10) { export const barListColor = '#F1F3F4'; export const statsStaticColors = [ - '#8E42FF', '#B07BFF', '#C7A0FF', '#DDC6FF', '#EBDDFF', '#F7EDFF' + '#A568FF', '#7B7BFF', '#B3CEFF', '#D4ECF7', '#EFFDFD', '#F7F7F7' ]; export const getCountryFlag = (countryCode) => { @@ -118,4 +120,48 @@ export const getCountryFlag = (countryCode) => { } return countryCode.toUpperCase().replace(/./g, char => String.fromCodePoint(char.charCodeAt(0) + 127397) ); -}; \ No newline at end of file +}; + +export function getDateRange(chartRange) { + const endDate = moment().endOf('day'); + const startDate = moment().subtract(chartRange - 1, 'days').startOf('day'); + return {startDate, endDate}; +} + +export function getStatsParams(config, props, additionalParams = {}) { + const {chartRange, audience, device, browser, location, source, pathname} = props; + const {startDate, endDate} = getDateRange(chartRange); + + const params = { + site_uuid: config.stats.id, + date_from: startDate.format('YYYY-MM-DD'), + date_to: endDate.format('YYYY-MM-DD'), + ...additionalParams + }; + + if (audience.length > 0) { + params.member_status = audience.join(','); + } + + if (device) { + params.device = device; + } + + if (browser) { + params.browser = browser; + } + + if (location) { + params.location = location; + } + + if (source) { + params.source = source === 'direct' ? '' : source; + } + + if (pathname) { + params.pathname = pathname; + } + + return params; +} diff --git a/ghost/admin/app/utils/window-proxy.js b/ghost/admin/app/utils/window-proxy.js index 1ac66bd1b14..20e1c20cf94 100644 --- a/ghost/admin/app/utils/window-proxy.js +++ b/ghost/admin/app/utils/window-proxy.js @@ -9,5 +9,9 @@ export default { replaceState(params, title, url) { window.history.replaceState(params, title, url); + }, + + reload() { + window.location.reload(); } }; diff --git a/ghost/admin/ember-cli-build.js b/ghost/admin/ember-cli-build.js index 684a6af97a5..cbea8de0425 100644 --- a/ghost/admin/ember-cli-build.js +++ b/ghost/admin/ember-cli-build.js @@ -205,6 +205,9 @@ module.exports = function (defaults) { }, autoImport: { publicAssetURL, + alias: { + 'sentry-testkit/browser': 'sentry-testkit/dist/browser' + }, webpack: { devtool: 'source-map', resolve: { diff --git a/ghost/admin/lib/asset-delivery/index.js b/ghost/admin/lib/asset-delivery/index.js index d48581a34f8..17a291afa63 100644 --- a/ghost/admin/lib/asset-delivery/index.js +++ b/ghost/admin/lib/asset-delivery/index.js @@ -43,6 +43,8 @@ module.exports = { for (const [key, value] of Object.entries(this.packageConfig)) { console.log(`Asset-Delivery: ${key} = ${value}`); } + + this.packageConfig[`adminXActivitypubCustomUrl`] = 'https://cdn.jsdelivr.net/npm/@tryghost/admin-x-activitypub@0/dist/admin-x-activitypub.js' } return this.packageConfig; diff --git a/ghost/admin/mirage/config/posts.js b/ghost/admin/mirage/config/posts.js index 916869416b7..37337951608 100644 --- a/ghost/admin/mirage/config/posts.js +++ b/ghost/admin/mirage/config/posts.js @@ -125,10 +125,10 @@ export default function mockPosts(server) { return posts.create(attrs); }); - server.put('/posts/bulk/', function ({tags}, {requestBody}) { + server.put('/posts/bulk/', function ({posts, tags}, {queryParams, requestBody}) { const bulk = JSON.parse(requestBody).bulk; const action = bulk.action; - // const ids = extractFilterParam('id', queryParams.filter); + const ids = extractFilterParam('id', queryParams.filter); if (action === 'addTag') { // create tag so we have an id from the server @@ -144,5 +144,27 @@ export default function mockPosts(server) { // const postsToUpdate = posts.find(ids); // getting the posts is fine, but within this we CANNOT manipulate them (???) not even iterate with .forEach } + + if (action === 'access') { + const postsToUpdate = posts.find(ids); + postsToUpdate.models.forEach((post) => { + post.visibility = bulk.meta.visibility; + post.tierIds = bulk.meta.tiers.map(tier => tier.id); + post.save(); + }); + } + + return { + bulk: { + meta: { + errors: [], + stats: { + successful: ids.length, + unsuccessful: 0 + }, + unsuccessfulData: [] + } + } + }; }); } diff --git a/ghost/admin/mirage/models/post.js b/ghost/admin/mirage/models/post.js index 46ea92d1faa..cb97fe8ed15 100644 --- a/ghost/admin/mirage/models/post.js +++ b/ghost/admin/mirage/models/post.js @@ -5,5 +5,6 @@ export default Model.extend({ authors: hasMany('user'), email: belongsTo(), newsletter: belongsTo(), - postRevisions: hasMany() + postRevisions: hasMany(), + tiers: hasMany() }); diff --git a/ghost/admin/mirage/serializers/post.js b/ghost/admin/mirage/serializers/post.js index 2985c0073f2..0db130d37e9 100644 --- a/ghost/admin/mirage/serializers/post.js +++ b/ghost/admin/mirage/serializers/post.js @@ -11,6 +11,7 @@ export default BaseSerializer.extend({ // embedded records that are included by default in the API includes.add('tags'); includes.add('authors'); + includes.add('tiers'); // clean up some things that mirage doesn't understand includes.delete('authorsRoles'); diff --git a/ghost/admin/package.json b/ghost/admin/package.json index 3cf08dee909..c565a66762d 100644 --- a/ghost/admin/package.json +++ b/ghost/admin/package.json @@ -1,6 +1,6 @@ { "name": "ghost-admin", - "version": "5.94.2", + "version": "5.96.0", "description": "Ember.js admin client for Ghost", "author": "Ghost Foundation", "homepage": "http://ghost.org", @@ -44,7 +44,7 @@ "@sentry/ember": "7.119.0", "@sentry/integrations": "7.114.0", "@sentry/replay": "7.116.0", - "@tinybirdco/charts": "0.2.0-beta.2", + "@tinybirdco/charts": "0.2.1", "@tryghost/color-utils": "0.2.2", "@tryghost/ember-promise-modals": "2.0.1", "@tryghost/helpers": "1.1.90", @@ -147,6 +147,8 @@ "react-dom": "18.3.1", "reframe.js": "4.0.2", "semver": "7.6.3", + "sentry-testkit": "5.0.9", + "sinon-chai": "4.0.0", "testem": "3.15.1", "tracked-built-ins": "3.3.0", "util": "0.12.5", @@ -174,9 +176,10 @@ "*.js": "eslint" }, "dependencies": { + "i18n-iso-countries": "7.12.0", "jose": "4.15.9", "path-browserify": "1.0.1", - "webpack": "5.94.0" + "webpack": "5.95.0" }, "nx": { "targets": { diff --git a/ghost/admin/public/assets/icons/close.svg b/ghost/admin/public/assets/icons/close.svg index 9080d36d208..de1f2e17414 100644 --- a/ghost/admin/public/assets/icons/close.svg +++ b/ghost/admin/public/assets/icons/close.svg @@ -1,4 +1,3 @@ - close \ No newline at end of file diff --git a/ghost/admin/public/assets/icons/stats-placeholder.svg b/ghost/admin/public/assets/icons/stats-placeholder.svg new file mode 100644 index 00000000000..f573addbac1 --- /dev/null +++ b/ghost/admin/public/assets/icons/stats-placeholder.svg @@ -0,0 +1 @@ +Analytics Board Graph Line Streamline Icon: https://streamlinehq.comanalytics-board-graph-line \ No newline at end of file diff --git a/ghost/admin/tests/acceptance/content-test.js b/ghost/admin/tests/acceptance/content-test.js index 44cf74b19db..e4eb0e6e6ae 100644 --- a/ghost/admin/tests/acceptance/content-test.js +++ b/ghost/admin/tests/acceptance/content-test.js @@ -1,5 +1,6 @@ import ctrlOrCmd from 'ghost-admin/utils/ctrl-or-cmd'; import sinon from 'sinon'; +import windowProxy from 'ghost-admin/utils/window-proxy'; import {authenticateSession, invalidateSession} from 'ember-simple-auth/test-support'; import {beforeEach, describe, it} from 'mocha'; import {blur, click, currentURL, fillIn, find, findAll, triggerEvent, triggerKeyEvent, visit} from '@ember/test-helpers'; @@ -108,6 +109,9 @@ describe('Acceptance: Posts / Pages', function () { let admin, editor, publishedPost, scheduledPost, draftPost, authorPost; beforeEach(async function () { + this.server.loadFixtures('settings'); + this.server.loadFixtures('tiers'); + let adminRole = this.server.create('role', {name: 'Administrator'}); admin = this.server.create('user', {roles: [adminRole]}); let editorRole = this.server.create('role', {name: 'Editor'}); @@ -447,52 +451,117 @@ describe('Acceptance: Posts / Pages', function () { expect(JSON.parse(lastRequest.requestBody).bulk.action, 'add tag request action').to.equal('addTag'); }); - // TODO: Skip for now. This causes the member creation test to fail ('New member' text doesn't show... ???). - it.skip('can change access', async function () { + it('cannot change access when members is disabled', async function () { await visit('/posts'); + const settingsService = this.owner.lookup('service:settings'); + await settingsService.set('membersEnabled', false); + // get all posts const posts = findAll('[data-test-post-id]'); expect(posts.length, 'all posts count').to.equal(4); - const postThreeContainer = posts[2].parentElement; // draft post - const postFourContainer = posts[3].parentElement; // published post + const postThreeContainer = posts[2].parentElement; // published post + const postFourContainer = posts[3].parentElement; // author post await click(postThreeContainer, {metaKey: ctrlOrCmd === 'command', ctrlKey: ctrlOrCmd === 'ctrl'}); await click(postFourContainer, {metaKey: ctrlOrCmd === 'command', ctrlKey: ctrlOrCmd === 'ctrl'}); + await triggerEvent(postFourContainer, 'contextmenu'); - expect(postFourContainer.getAttribute('data-selected'), 'postFour selected').to.exist; - expect(postThreeContainer.getAttribute('data-selected'), 'postThree selected').to.exist; + expect(find('[data-test-post-context-menu]'), 'context menu').to.exist; + expect(find('[data-test-post-context-menu] [data-test-button="change-access"]'), 'change access button').not.to.exist; + }); + + it('can change access', async function () { + await visit('/posts'); + + const settingsService = this.owner.lookup('service:settings'); + await settingsService.set('membersEnabled', true); + + let posts = findAll('[data-test-post-id]'); + let postThreeContainer = posts[2].parentElement; // published post + let postFourContainer = posts[3].parentElement; // author post + + await click(postThreeContainer, {metaKey: ctrlOrCmd === 'command', ctrlKey: ctrlOrCmd === 'ctrl'}); + await click(postFourContainer, {metaKey: ctrlOrCmd === 'command', ctrlKey: ctrlOrCmd === 'ctrl'}); await triggerEvent(postFourContainer, 'contextmenu'); + let contextMenu = find('.gh-posts-context-menu'); // this is a
    element - expect(contextMenu, 'context menu').to.exist; let buttons = contextMenu.querySelectorAll('button'); let changeAccessButton = findButton('Change access', buttons); - expect(changeAccessButton, 'change access button').not.to.exist; + await click(changeAccessButton); - const settingsService = this.owner.lookup('service:settings'); - await settingsService.set('membersEnabled', true); + let changeAccessModal = find('[data-test-modal="edit-posts-access"]'); + let selectElement = changeAccessModal.querySelector('select'); + await fillIn(selectElement, 'members'); + await click('[data-test-button="confirm"]'); + + // check API request + let [lastRequest] = this.server.pretender.handledRequests.slice(-1); + expect(lastRequest.queryParams.filter, 'change access request id').to.equal(`id:['${publishedPost.id}','${authorPost.id}']`); + expect(JSON.parse(lastRequest.requestBody).bulk.action, 'change access request action').to.equal('access'); + // ensure modal matches the new state when accessed again + // NOTE: we only show the selected visibility/tiers state for single selections + await click(postThreeContainer, {metaKey: ctrlOrCmd === 'command', ctrlKey: ctrlOrCmd === 'ctrl'}); + postFourContainer = findAll('[data-test-post-id]')[3].parentElement; // published post await triggerEvent(postFourContainer, 'contextmenu'); contextMenu = find('.gh-posts-context-menu'); // this is a
      element - expect(contextMenu, 'context menu').to.exist; buttons = contextMenu.querySelectorAll('button'); changeAccessButton = findButton('Change access', buttons); - - expect(changeAccessButton, 'change access button').to.exist; await click(changeAccessButton); + changeAccessModal = find('[data-test-modal="edit-posts-access"]'); + selectElement = changeAccessModal.querySelector('select'); + expect(selectElement, 'access select value after changing').to.have.value('members'); + await click(changeAccessModal.querySelector('[data-test-button="cancel"]')); + + // ensure creating new posts still works + // (we had a bug where newly created records in the store had `isNew: false` set meaning any saves failed + // because Ember Data attempted a PUT with no id) + sinon.stub(windowProxy, 'reload'); // we had a force-reload in place to workaround the bug + await visit('/editor/post'); + await fillIn('[data-test-editor-title-input]', 'New post'); + await blur('[data-test-editor-title-input]'); + expect(this.server.db.posts.length, 'posts count after new post save').to.equal(5); + }); - const changeAccessModal = find('[data-test-modal="edit-posts-access"]'); - const selectElement = changeAccessModal.querySelector('select'); - await fillIn(selectElement, 'members'); - await click('[data-test-button="confirm"]'); + it('can change access with custom tiers', async function () { + await visit('/posts'); + + const settingsService = this.owner.lookup('service:settings'); + await settingsService.set('membersEnabled', true); + + const postContainer = findAll('[data-test-post-id]')[2].parentElement; // published post + await triggerEvent(postContainer, 'contextmenu'); + await click('[data-test-post-context-menu] [data-test-button="change-access"]'); + + const modalSelector = '[data-test-modal="edit-posts-access"]'; + const tiersSelector = `${modalSelector} [data-test-visibility-segment-select]`; + + expect(find(tiersSelector)).not.to.exist; + await fillIn(`${modalSelector} select`, 'tiers'); + expect(find(tiersSelector)).to.exist; + expect(findAll(`${tiersSelector} [data-test-visibility-segment-option]`)).to.have.length(0); + + await clickTrigger(tiersSelector); + await selectChoose(tiersSelector, 'Default Tier'); + await click(`${modalSelector} [data-test-button="confirm"]`); // check API request let [lastRequest] = this.server.pretender.handledRequests.slice(-1); - expect(lastRequest.queryParams.filter, 'change access request id').to.equal(`id:['${publishedPost.id}','${authorPost.id}']`); + expect(lastRequest.queryParams.filter, 'change access request id').to.equal(`id:['${publishedPost.id}']`); expect(JSON.parse(lastRequest.requestBody).bulk.action, 'change access request action').to.equal('access'); + expect(JSON.parse(lastRequest.requestBody).bulk.meta.visibility, 'change access request visibility').to.equal('tiers'); + expect(JSON.parse(lastRequest.requestBody).bulk.meta.tiers[0].id, 'change access request tier').to.equal(this.server.schema.tiers.findBy({slug: 'default-tier'}).id); + + // check correct data is shown when re-accessing change access modal + await triggerEvent(postContainer, 'contextmenu'); + await click('[data-test-post-context-menu] [data-test-button="change-access"]'); + expect(find(`${modalSelector} select`).value).to.equal('tiers'); + expect(findAll(`${tiersSelector} [data-test-visibility-segment-option]`)).to.have.length(1); + expect(find(`${tiersSelector} [data-test-visibility-segment-option]`).textContent.trim()).to.equal('Default Tier'); }); it('can unpublish', async function () { @@ -539,6 +608,46 @@ describe('Acceptance: Posts / Pages', function () { expect(postFourContainer.querySelector('.gh-content-entry-status').textContent, 'postThree status').to.contain('Draft'); }); + it('can unschedule', async function () { + await visit('/posts'); + + // get all posts + const posts = findAll('[data-test-post-id]'); + expect(posts.length, 'all posts count').to.equal(4); + + const postOneContainer = posts[0].parentElement; // scheduled post + + await click(postOneContainer, {metaKey: ctrlOrCmd === 'command', ctrlKey: ctrlOrCmd === 'ctrl'}); + + expect(postOneContainer.getAttribute('data-selected'), 'postOne selected').to.exist; + + // NOTE: right clicks don't seem to work in these tests + // contextmenu is the event triggered - https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event + await triggerEvent(postOneContainer, 'contextmenu'); + + let contextMenu = find('.gh-posts-context-menu'); // this is a
        element + expect(contextMenu, 'context menu').to.exist; + + // unschedule the post + let buttons = contextMenu.querySelectorAll('button'); + let unscheduleButton = findButton('Unschedule', buttons); + expect(unscheduleButton, 'unschedule button').to.exist; + await click(unscheduleButton); + + // handle modal + const modal = find('[data-test-modal="unschedule-posts"]'); + expect(modal, 'unschedule modal').to.exist; + await click('[data-test-button="confirm"]'); + + // API request is correct - note, we don't mock the actual model updates + let [lastRequest] = this.server.pretender.handledRequests.slice(-1); + expect(lastRequest.queryParams.filter, 'unschedule request id').to.equal(`id:['${scheduledPost.id}']`); + expect(JSON.parse(lastRequest.requestBody).bulk.action, 'unschedule request action').to.equal('unschedule'); + + // ensure ui shows these are now unpublished + expect(postOneContainer.querySelector('.gh-content-entry-status').textContent, 'postOne status').to.contain('Draft'); + }); + it('can delete', async function () { await visit('/posts'); @@ -624,7 +733,7 @@ describe('Acceptance: Posts / Pages', function () { }); it('can navigate to custom views', async function () { - this.server.create('setting', { + this.server.schema.settings.findBy({key: 'shared_views'}).update({ group: 'site', key: 'shared_views', value: JSON.stringify([{ @@ -639,10 +748,10 @@ describe('Acceptance: Posts / Pages', function () { await visit('/posts'); // nav bar contains default + custom views - expect(find('[data-test-nav-custom="posts-Drafts"]')).to.exist; - expect(find('[data-test-nav-custom="posts-Scheduled"]')).to.exist; - expect(find('[data-test-nav-custom="posts-Published"]')).to.exist; - expect(find('[data-test-nav-custom="posts-My posts"]')).to.exist; + expect(find('[data-test-nav-custom="posts-Drafts"]'), 'drafts nav').to.exist; + expect(find('[data-test-nav-custom="posts-Scheduled"]'), 'scheduled nav').to.exist; + expect(find('[data-test-nav-custom="posts-Published"]'), 'published nav').to.exist; + expect(find('[data-test-nav-custom="posts-My posts"]'), 'my posts nav').to.exist; // screen has default title and sidebar is showing inactive custom view expect(find('[data-test-screen-title]')).to.have.rendered.trimmed.text('Posts'); diff --git a/ghost/admin/tests/acceptance/editor/feature-image-test.js b/ghost/admin/tests/acceptance/editor/feature-image-test.js new file mode 100644 index 00000000000..adbf8675671 --- /dev/null +++ b/ghost/admin/tests/acceptance/editor/feature-image-test.js @@ -0,0 +1,37 @@ +import loginAsRole from '../../helpers/login-as-role'; +import {click, currentURL, find} from '@ember/test-helpers'; +import {expect} from 'chai'; +import {setupApplicationTest} from 'ember-mocha'; +import {setupMirage} from 'ember-cli-mirage/test-support'; +import {visit} from '../../helpers/visit'; + +describe('Acceptance: Feature Image', function () { + let hooks = setupApplicationTest(); + setupMirage(hooks); + + beforeEach(async function () { + this.server.loadFixtures(); + await loginAsRole('Administrator', this.server); + }); + + it('can display feature image with caption', async function () { + const post = this.server.create('post', {status: 'published', featureImage: 'https://static.ghost.org/v4.0.0/images/feature-image.jpg', featureImageCaption: 'Hello dogggos'}); + await visit(`/editor/post/${post.id}`); + expect(await find('.gh-editor-feature-image img').src).to.equal('https://static.ghost.org/v4.0.0/images/feature-image.jpg'); + expect(await find('.gh-editor-feature-image-caption').textContent).to.contain('Hello dogggos'); + }); + + it('does not attempt to save if already deleted and goes back to posts', async function () { + // avoids an infinite loop when the post is deleted and the save button is clicked, potential race condition + const post = this.server.create('post', {status: 'published', featureImage: 'https://static.ghost.org/v4.0.0/images/feature-image.jpg', featureImageCaption: 'Hello dogggos'}); + await visit(`/editor/post/${post.id}`); + + this.server.db.posts.update(post.id, {isDeleted: true}); + + await click('[data-test-psm-trigger]'); + await click('[data-test-button="delete-post"]'); + await click('[data-test-button="delete-post-confirm"]'); + + expect(currentURL()).to.equal('/posts'); + }); +}); \ No newline at end of file diff --git a/ghost/admin/tests/acceptance/restore-post-test.js b/ghost/admin/tests/acceptance/restore-post-test.js new file mode 100644 index 00000000000..26eb1ebeac4 --- /dev/null +++ b/ghost/admin/tests/acceptance/restore-post-test.js @@ -0,0 +1,49 @@ +import {authenticateSession} from 'ember-simple-auth/test-support'; +import {beforeEach, describe, it} from 'mocha'; +import {click} from '@ember/test-helpers'; +import {expect} from 'chai'; +import {find, visit} from '@ember/test-helpers'; +import {setupApplicationTest} from 'ember-mocha'; +import {setupMirage} from 'ember-cli-mirage/test-support'; + +describe('Acceptance: Restore', function () { + let hooks = setupApplicationTest(); + setupMirage(hooks); + + beforeEach(async function () { + // Create a user and authenticate the session + let role = this.server.create('role', {name: 'Owner'}); + this.server.create('user', {roles: [role], slug: 'owner'}); + await authenticateSession(); + }); + + it('restores a post from a revision', async function () { + // Create a post revision in localStorage + const revisionData = { + id: 'test-id', + title: 'Test Post', + lexical: '{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"Test content","type":"extended-text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}', + revisionTimestamp: Date.now() + }; + const revisionKey = `post-revision-${revisionData.id}-${revisionData.revisionTimestamp}`; + localStorage.setItem(revisionKey, JSON.stringify(revisionData)); + localStorage.setItem('ghost-revisions', JSON.stringify([revisionKey])); + + // Visit the restore route + await visit(`/restore/`); + + // Verify that the post title is displayed + const postTitle = find('[data-test-id="restore-post-title"]').textContent.trim(); + expect(postTitle).to.equal('Test Post'); + + // Verify that the restore button is present + const restoreButton = find('[data-test-id="restore-post-button"]'); + expect(restoreButton).to.exist; + + // Click the restore button + await click(restoreButton); + + // Verify that the post is restored (notification will show) + expect(find('.gh-notification-title').textContent.trim()).to.equal('Post restored successfully'); + }); +}); \ No newline at end of file diff --git a/ghost/admin/tests/test-helper.js b/ghost/admin/tests/test-helper.js index 756802bcd08..9152f800af2 100644 --- a/ghost/admin/tests/test-helper.js +++ b/ghost/admin/tests/test-helper.js @@ -6,7 +6,9 @@ import {setApplication} from '@ember/test-helpers'; import chai from 'chai'; import chaiDom from 'chai-dom'; +import sinonChai from 'sinon-chai'; chai.use(chaiDom); +chai.use(sinonChai); setApplication(Application.create(config.APP)); diff --git a/ghost/admin/tests/unit/routes/lexical-editor.new-test.js b/ghost/admin/tests/unit/routes/lexical-editor.new-test.js new file mode 100644 index 00000000000..b59396aae9b --- /dev/null +++ b/ghost/admin/tests/unit/routes/lexical-editor.new-test.js @@ -0,0 +1,101 @@ +import * as Sentry from '@sentry/ember'; +import sentryTestkit from 'sentry-testkit/browser'; +import windowProxy from 'ghost-admin/utils/window-proxy'; +import {describe, it} from 'mocha'; +import {expect} from 'chai'; +import {getSentryTestConfig} from 'ghost-admin/utils/sentry'; +import {run} from '@ember/runloop'; +import {setupTest} from 'ember-mocha'; +import {waitUntil} from '@ember/test-helpers'; + +const {sentryTransport, testkit} = sentryTestkit(); + +describe('Unit: Route: lexical-editor.new', function () { + setupTest(); + + let controller; + let route; + let createRecordStub; + let reloadStub; + + before(function () { + Sentry.init(getSentryTestConfig(sentryTransport)); + }); + + beforeEach(async function () { + // ensure tags don't leak in from earlier (esp. acceptance) tests + Sentry.getCurrentHub().getIsolationScope().clear(); + Sentry.getCurrentScope().clear(); + + testkit.reset(); + + controller = this.owner.lookup('controller:lexical-editor'); + route = this.owner.lookup('route:lexical-editor.new'); + createRecordStub = sinon.stub(route.store, 'createRecord').returns({}); + reloadStub = sinon.stub(windowProxy, 'reload'); + }); + + afterEach(function () { + sinon.restore(); + }); + + describe('didTransition', function () { + function callDidTransition() { + // we schedule reload to be called in afterRender so we need a runloop + run(() => { + route.didTransition(); + }); + } + + describe('with model.isNew === true', function () { + it('does not call reload', function () { + const model = {isNew: true}; + controller.post = model; + callDidTransition(); + expect(reloadStub.called).to.be.false; + }); + }); + + describe('with model.isNew === false', function () { + it('calls reload', async function () { + const model = {isNew: false}; + controller.post = model; + callDidTransition(); + expect(reloadStub.calledOnce).to.be.true; + }); + + it('logs to console', async function () { + // we attempt to re-create the new post for debug logging purposes + // (recreatedPostIsGood = true when the secondary createRecord call results in a good model state) + createRecordStub.returns({isNew: true}); + + const consoleStub = sinon.stub(console, 'error'); + const model = {isNew: false}; + controller.post = model; + callDidTransition(); + + expect(createRecordStub.calledOnce, 'createRecordStub called').to.be.true; + expect(consoleStub).to.have.been.calledWith('New post route transitioned with post.isNew=false', {recreatedPostIsGood: true}); + }); + + it('logs to Sentry', async function () { + // we attempt to re-create the new post for debug logging purposes + // (recreatedPostIsGood = true when the secondary createRecord call results in a good model state) + createRecordStub.returns({isNew: true}); + + const model = {isNew: false}; + controller.post = model; + callDidTransition(); + + // Sentry reports are delivered async so it's best to wait for them to avoid flaky tests + await waitUntil(() => testkit.reports().length > 0); + + expect(testkit.reports()).to.have.length(1); + const report = testkit.reports()[0]; + expect(report.message).to.equal('New post route transitioned with post.isNew=false'); + expect(report.tags).to.deep.equal({shown_to_user: false, grammarly: false, savePostTask: true}); + expect(report.extra).to.deep.equal({recreatedPostIsGood: true}); + }); + }); + }); +}); diff --git a/ghost/admin/tests/unit/services/local-revisions-test.js b/ghost/admin/tests/unit/services/local-revisions-test.js index 782d8c13e70..d5566175363 100644 --- a/ghost/admin/tests/unit/services/local-revisions-test.js +++ b/ghost/admin/tests/unit/services/local-revisions-test.js @@ -1,8 +1,14 @@ +import * as Sentry from '@sentry/ember'; import Service from '@ember/service'; +import sentryTestKit from 'sentry-testkit/browser'; import sinon from 'sinon'; import {describe, it} from 'mocha'; import {expect} from 'chai'; +import {getSentryTestConfig} from 'ghost-admin/utils/sentry'; import {setupTest} from 'ember-mocha'; +import {waitUntil} from '@ember/test-helpers'; + +const {sentryTransport, testkit} = sentryTestKit(); const sleep = ms => new Promise((resolve) => { setTimeout(resolve, ms); @@ -11,19 +17,43 @@ const sleep = ms => new Promise((resolve) => { describe('Unit: Service: local-revisions', function () { setupTest(); - let localStore, setItemStub; + let localStore, setItemStub, getItemStub, removeItemStub, clearStub, localStorageMock; + + before(function () { + Sentry.init(getSentryTestConfig(sentryTransport)); + }); this.beforeEach(function () { + // Reset the Sentry testkit + testkit.reset(); + // Mock localStorage sinon.restore(); localStore = {}; - sinon.stub(localStorage, 'getItem').callsFake(key => localStore[key] || null); - setItemStub = sinon.stub(localStorage, 'setItem').callsFake((key, value) => localStore[key] = value + ''); - sinon.stub(localStorage, 'removeItem').callsFake(key => delete localStore[key]); - sinon.stub(localStorage, 'clear').callsFake(() => localStore = {}); + getItemStub = sinon.stub().callsFake(key => localStore[key] || null); + setItemStub = sinon.stub().callsFake((key, value) => localStore[key] = value + ''); + removeItemStub = sinon.stub().callsFake(key => delete localStore[key]); + clearStub = sinon.stub().callsFake(() => localStore = {}); + localStorageMock = { + getItem: getItemStub, + setItem: setItemStub, + removeItem: removeItemStub, + clear: clearStub + }; + Object.defineProperty(localStorageMock, 'length', { + get: function () { + return Object.keys(localStore).length; + } + }); + Object.defineProperty(localStorageMock, 'key', { + value: function (n) { + return Object.keys(localStore)[n]; + } + }); // Create the service this.service = this.owner.lookup('service:local-revisions'); + this.service.storage = localStorageMock; this.service.clear(); }); @@ -48,7 +78,7 @@ describe('Unit: Service: local-revisions', function () { describe('performSave', function () { it('saves a revision without a post id', function () { // save a revision - const key = this.service.performSave('post', {id: 'draft', lexical: 'test'}); + const key = this.service.performSave('post', {id: 'draft', lexical: 'test', status: 'draft'}); const revision = this.service.find(key); expect(key).to.match(/post-revision-draft-\d+/); expect(revision.id).to.equal('draft'); @@ -57,7 +87,7 @@ describe('Unit: Service: local-revisions', function () { it('saves a revision with a post id', function () { // save a revision - const key = this.service.performSave('post', {id: 'test-id', lexical: 'test'}); + const key = this.service.performSave('post', {id: 'test-id', lexical: 'test', status: 'draft'}); const revision = this.service.find(key); expect(key).to.match(/post-revision-test-id-\d+/); expect(revision.id).to.equal('test-id'); @@ -66,7 +96,7 @@ describe('Unit: Service: local-revisions', function () { it('evicts the oldest version if localStorage is full', async function () { // save a few revisions - const keyToRemove = this.service.performSave('post', {id: 'test-id', lexical: 'test'}); + const keyToRemove = this.service.performSave('post', {id: 'test-id', lexical: 'test', status: 'draft'}); await sleep(1); this.service.performSave('post', {id: 'test-id', lexical: 'data-2'}); await sleep(1); @@ -76,8 +106,7 @@ describe('Unit: Service: local-revisions', function () { quotaError.name = 'QuotaExceededError'; const callCount = setItemStub.callCount; setItemStub.onCall(callCount).throws(quotaError); - const keyToAdd = this.service.performSave('post', {id: 'test-id', lexical: 'data-3'}); - + const keyToAdd = this.service.performSave('post', {id: 'test-id', lexical: 'data-3', status: 'draft'}); // Ensure the oldest revision was removed expect(this.service.find(keyToRemove)).to.be.null; @@ -87,19 +116,17 @@ describe('Unit: Service: local-revisions', function () { it('evicts multiple oldest versions if localStorage is full', async function () { // save a few revisions - const keyToRemove = this.service.performSave('post', {id: 'test-id-1', lexical: 'test'}); + const keyToRemove = this.service.performSave('post', {id: 'test-id-1', lexical: 'test', status: 'draft'}); await sleep(1); - const nextKeyToRemove = this.service.performSave('post', {id: 'test-id-2', lexical: 'data-2'}); + const nextKeyToRemove = this.service.performSave('post', {id: 'test-id-2', lexical: 'data-2', status: 'draft'}); await sleep(1); // Simulate a quota exceeded error const quotaError = new Error('QuotaExceededError'); quotaError.name = 'QuotaExceededError'; setItemStub.onCall(setItemStub.callCount).throws(quotaError); - // remove calls setItem() to remove the key from the index - // it's called twice for each quota error, hence the + 3 - setItemStub.onCall(setItemStub.callCount + 3).throws(quotaError); - const keyToAdd = this.service.performSave('post', {id: 'test-id-3', lexical: 'data-3'}); + setItemStub.onCall(setItemStub.callCount + 1).throws(quotaError); + const keyToAdd = this.service.performSave('post', {id: 'test-id-3', lexical: 'data-3', status: 'draft'}); // Ensure the oldest revision was removed expect(this.service.find(keyToRemove)).to.be.null; @@ -108,12 +135,106 @@ describe('Unit: Service: local-revisions', function () { // Ensure the latest revision saved expect(this.service.find(keyToAdd)).to.not.be.null; }); + + it('logs to Sentry when it has to evict older revisions', async function () { + // save a few revisions + this.service.performSave('post', {id: 'test-id', lexical: 'test', status: 'draft'}); + await sleep(1); + this.service.performSave('post', {id: 'test-id', lexical: 'data-2', status: 'draft'}); + await sleep(1); + + // Simulate a quota exceeded error + const quotaError = new Error('QuotaExceededError'); + quotaError.name = 'QuotaExceededError'; + setItemStub.onCall(setItemStub.callCount).throws(quotaError); + this.service.performSave('post', {id: 'test-id', lexical: 'data-3', status: 'draft'}); + + await waitUntil(() => testkit.reports().length > 0); + expect(testkit.reports()).to.have.lengthOf(1); + + const report = testkit.reports()[0]; + expect(report.tags.localRevisions).to.equal('quotaExceeded'); + expect(report.message).to.equal('LocalStorage quota exceeded. Removing old revisions.'); + }); + + it('logs to sentry when it is unable to save a revision due to quota exceeded', async function () { + // Simulate a quota exceeded error + const quotaError = new Error('QuotaExceededError'); + quotaError.name = 'QuotaExceededError'; + setItemStub.onCall(setItemStub.callCount).throws(quotaError); + this.service.performSave('post', {id: 'test-id', lexical: 'data-3', status: 'draft'}); + + await waitUntil(() => testkit.reports().length > 0); + expect(testkit.reports()).to.have.lengthOf(1); + + const report = testkit.reports()[0]; + expect(report.tags.localRevisions).to.equal('quotaExceededNoSpace'); + expect(report.message).to.equal('LocalStorage quota exceeded. Unable to save revision.'); + }); + + it('logs to sentry if an unexpected error occurs while saving a revision', async function () { + // Simulate an unexpected error + const error = new Error('Test error'); + setItemStub.throws(error); + this.service.performSave('post', {id: 'test-id', lexical: 'data-3', status: 'draft'}); + + await waitUntil(() => testkit.reports().length > 0); + expect(testkit.reports()).to.have.lengthOf(1); + + const report = testkit.reports()[0]; + expect(report.error.message).to.equal('Test error'); + expect(report.tags.localRevisions).to.equal('saveError'); + }); + + it('keeps only the latest 5 revisions for a given post ID', async function () { + const postId = 'test-id'; + const revisionCount = 7; + + // Save 7 revisions for the same post ID + for (let i = 0; i < revisionCount; i++) { + await sleep(1); // Ensure unique timestamps + this.service.performSave('post', {id: postId, lexical: `test-${i}`, status: 'draft'}); + } + + // Get all revisions for the post ID + const revisions = this.service.findAll(`post-revision-${postId}`); + + // Check that only 5 revisions are kept + expect(revisions).to.have.lengthOf(5); + + // Check that the kept revisions are the latest ones + for (let i = 0; i < 5; i++) { + expect(revisions[i].lexical).to.equal(`test-${revisionCount - 1 - i}`); + } + }); + + it('does not limit revisions for drafts', async function () { + const postId = 'draft'; + const revisionCount = 7; + + // Save 7 revisions for a draft + for (let i = 0; i < revisionCount; i++) { + await sleep(1); // Ensure unique timestamps + this.service.performSave('post', {id: postId, lexical: `test-${i}`, status: 'draft'}); + } + + // Get all revisions for the draft + const revisions = this.service.findAll(`post-revision-${postId}`); + + // Check that all 7 revisions are kept for the draft + expect(revisions).to.have.lengthOf(revisionCount); + + // Check that all revisions are present + for (let i = 0; i < revisionCount; i++) { + expect(revisions[i].lexical).to.equal(`test-${revisionCount - 1 - i}`); + } + }); }); describe('scheduleSave', function () { - it('saves a revision', function () { + it('saves a revision if the post is a draft', function () { // save a revision - this.service.scheduleSave('post', {id: 'draft', lexical: 'test'}); + this.service.scheduleSave('post', {id: 'draft', lexical: 'test', status: 'draft'}); const key = this.service.keys()[0]; const revision = this.service.find(key); expect(key).to.match(/post-revision-draft-\d+/); @@ -121,9 +242,16 @@ describe('Unit: Service: local-revisions', function () { expect(revision.lexical).to.equal('test'); }); + it('does not save a revision if the post is not a draft', function () { + // save a revision + this.service.scheduleSave('post', {id: 'draft', lexical: 'test', status: 'published'}); + const keys = this.service.keys(); + expect(keys).to.have.lengthOf(0); + }); + it('does not save a revision more than once if scheduled multiple times', async function () { // interval is set to 200 ms in testing - this.service.scheduleSave('post', {id: 'draft', lexical: 'test'}); + this.service.scheduleSave('post', {id: 'draft', lexical: 'test', status: 'draft'}); await sleep(40); this.service.scheduleSave('post', {id: 'draft', lexical: 'test'}); const keys = this.service.keys(); @@ -132,18 +260,31 @@ describe('Unit: Service: local-revisions', function () { it('saves another revision if it has been longer than the revision interval', async function () { // interval is set to 200 ms in testing - this.service.scheduleSave('post', {id: 'draft', lexical: 'test'}); + this.service.scheduleSave('post', {id: 'draft', lexical: 'test', status: 'draft'}); await sleep(100); - this.service.scheduleSave('post', {id: 'draft', lexical: 'test'}); + this.service.scheduleSave('post', {id: 'draft', lexical: 'test', status: 'draft'}); const keys = this.service.keys(); expect(keys).to.have.lengthOf(2); }); + + it('logs any errors that occur during the save to Sentry', async function () { + sinon.stub(this.service, 'performSave').throws(new Error('Test error')); + + this.service.scheduleSave('post', {id: 'draft', lexical: 'test', status: 'draft'}); + + await waitUntil(() => testkit.reports().length > 0); + expect(testkit.reports()).to.have.lengthOf(1); + + const report = testkit.reports()[0]; + expect(report.error.message).to.equal('Test error'); + expect(report.tags.localRevisions).to.equal('saveTaskError'); + }); }); describe('find', function () { it('gets a revision by key', function () { // save a revision - const key = this.service.performSave('post', {lexical: 'test'}); + const key = this.service.performSave('post', {lexical: 'test', status: 'draft'}); const result = this.service.find(key); expect(result.id).to.equal('draft'); expect(result.lexical).to.equal('test'); @@ -159,23 +300,23 @@ describe('Unit: Service: local-revisions', function () { describe('findAll', function () { it('gets all revisions if no prefix is provided', function () { // save a revision - this.service.performSave('post', {id: 'test-id', lexical: 'test'}); - this.service.performSave('post', {lexical: 'data-2'}); + this.service.performSave('post', {id: 'test-id', lexical: 'test', status: 'draft'}); + this.service.performSave('post', {lexical: 'data-2', status: 'draft'}); const result = this.service.findAll(); expect(Object.keys(result)).to.have.lengthOf(2); }); it('gets revisions filtered by prefix', function () { // save a revision - this.service.performSave('post', {id: 'test-id', lexical: 'test'}); - this.service.performSave('post', {lexical: 'data-2'}); + this.service.performSave('post', {id: 'test-id', lexical: 'test', status: 'draft'}); + this.service.performSave('post', {lexical: 'data-2', status: 'draft'}); const result = this.service.findAll('post-revision-test-id'); expect(Object.keys(result)).to.have.lengthOf(1); }); it('returns an empty object if there are no revisions', function () { const result = this.service.findAll(); - expect(result).to.deep.equal({}); + expect(result).to.deep.equal([]); }); }); @@ -185,20 +326,22 @@ describe('Unit: Service: local-revisions', function () { expect(result).to.deep.equal([]); }); - it('returns the keys for all revisions if not prefix is provided', function () { + it('returns the keys for all revisions if no prefix is provided', async function () { // save revision - this.service.performSave('post', {id: 'test-id', lexical: 'data'}); + this.service.performSave('post', {id: 'test-id', lexical: 'data', status: 'draft'}); + await sleep(1); + this.service.performSave('post', {id: 'draft', lexical: 'data', status: 'draft'}); const result = this.service.keys(); - expect(Object.keys(result)).to.have.lengthOf(1); + expect(result).to.have.lengthOf(2); expect(result[0]).to.match(/post-revision-test-id-\d+/); }); it('returns the keys filtered by prefix if provided', function () { // save revision - this.service.performSave('post', {id: 'test-id', lexical: 'data'}); - this.service.performSave('post', {id: 'draft', lexical: 'data'}); + this.service.performSave('post', {id: 'test-id', lexical: 'data', status: 'draft'}); + this.service.performSave('post', {id: 'draft', lexical: 'data', status: 'draft'}); const result = this.service.keys('post-revision-test-id'); - expect(Object.keys(result)).to.have.lengthOf(1); + expect(result).to.have.lengthOf(1); expect(result[0]).to.match(/post-revision-test-id-\d+/); }); }); @@ -206,8 +349,8 @@ describe('Unit: Service: local-revisions', function () { describe('remove', function () { it('removes the specified key', function () { // save revision - const key = this.service.performSave('post', {id: 'test-id', lexical: 'data'}); - this.service.performSave('post', {id: 'test-2', lexical: 'data'}); + const key = this.service.performSave('post', {id: 'test-id', lexical: 'data', status: 'draft'}); + this.service.performSave('post', {id: 'test-2', lexical: 'data', status: 'draft'}); this.service.remove(key); const updatedKeys = this.service.keys(); expect(updatedKeys).to.have.lengthOf(1); @@ -216,8 +359,8 @@ describe('Unit: Service: local-revisions', function () { it('does nothing if the key does not exist', function () { // save revision - this.service.performSave('post', {id: 'test-id', lexical: 'data'}); - this.service.performSave('post', {id: 'test-2', lexical: 'data'}); + this.service.performSave('post', {id: 'test-id', lexical: 'data', status: 'draft'}); + this.service.performSave('post', {id: 'test-2', lexical: 'data', status: 'draft'}); this.service.remove('non-existent-key'); const updatedKeys = this.service.keys(); expect(updatedKeys).to.have.lengthOf(2); @@ -227,11 +370,11 @@ describe('Unit: Service: local-revisions', function () { describe('removeOldest', function () { it('removes the oldest revision', async function () { // save revision - const keyToRemove = this.service.performSave('post', {id: 'test-id', lexical: 'data'}); + const keyToRemove = this.service.performSave('post', {id: 'test-id', lexical: 'data', status: 'draft'}); await sleep(1); - this.service.performSave('post', {id: 'test-2', lexical: 'data'}); + this.service.performSave('post', {id: 'test-2', lexical: 'data', status: 'draft'}); await sleep(1); - this.service.performSave('post', {id: 'test-3', lexical: 'data'}); + this.service.performSave('post', {id: 'test-3', lexical: 'data', status: 'draft'}); this.service.removeOldest(); const updatedKeys = this.service.keys(); expect(updatedKeys).to.have.lengthOf(2); @@ -258,7 +401,7 @@ describe('Unit: Service: local-revisions', function () { queryRecord: queryRecordStub })); // create a post to restore - const key = this.service.performSave('post', {id: 'test-id', authors: [{id: '1'}], lexical: '{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"\\"{\\\\\\"root\\\\\\":{\\\\\\"children\\\\\\":[{\\\\\\"children\\\\\\":[{\\\\\\"detail\\\\\\":0,\\\\\\"format\\\\\\":0,\\\\\\"mode\\\\\\":\\\\\\"normal\\\\\\",\\\\\\"style\\\\\\":\\\\\\"\\\\\\",\\\\\\"text\\\\\\":\\\\\\"T\\\\\\",\\\\\\"type\\\\\\":\\\\\\"extended-text\\\\\\",\\\\\\"version\\\\\\":1}],\\\\\\"direction\\\\\\":\\\\\\"ltr\\\\\\",\\\\\\"format\\\\\\":\\\\\\"\\\\\\",\\\\\\"indent\\\\\\":0,\\\\\\"type\\\\\\":\\\\\\"paragraph\\\\\\",\\\\\\"version\\\\\\":1}],\\\\\\"direction\\\\\\":\\\\\\"ltr\\\\\\",\\\\\\"format\\\\\\":\\\\\\"\\\\\\",\\\\\\"indent\\\\\\":0,\\\\\\"type\\\\\\":\\\\\\"root\\\\\\",\\\\\\"version\\\\\\":1}}\\"","type":"extended-text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}'}); + const key = this.service.performSave('post', {id: 'test-id', status: 'draft', authors: [{id: '1'}], lexical: '{"root":{"children":[{"children":[{"detail":0,"format":0,"mode":"normal","style":"","text":"\\"{\\\\\\"root\\\\\\":{\\\\\\"children\\\\\\":[{\\\\\\"children\\\\\\":[{\\\\\\"detail\\\\\\":0,\\\\\\"format\\\\\\":0,\\\\\\"mode\\\\\\":\\\\\\"normal\\\\\\",\\\\\\"style\\\\\\":\\\\\\"\\\\\\",\\\\\\"text\\\\\\":\\\\\\"T\\\\\\",\\\\\\"type\\\\\\":\\\\\\"extended-text\\\\\\",\\\\\\"version\\\\\\":1}],\\\\\\"direction\\\\\\":\\\\\\"ltr\\\\\\",\\\\\\"format\\\\\\":\\\\\\"\\\\\\",\\\\\\"indent\\\\\\":0,\\\\\\"type\\\\\\":\\\\\\"paragraph\\\\\\",\\\\\\"version\\\\\\":1}],\\\\\\"direction\\\\\\":\\\\\\"ltr\\\\\\",\\\\\\"format\\\\\\":\\\\\\"\\\\\\",\\\\\\"indent\\\\\\":0,\\\\\\"type\\\\\\":\\\\\\"root\\\\\\",\\\\\\"version\\\\\\":1}}\\"","type":"extended-text","version":1}],"direction":"ltr","format":"","indent":0,"type":"paragraph","version":1}],"direction":"ltr","format":"","indent":0,"type":"root","version":1}}'}); // restore the post const post = await this.service.restore(key); diff --git a/ghost/core/content/themes/casper b/ghost/core/content/themes/casper index bf3617a726b..d147bb158e5 160000 --- a/ghost/core/content/themes/casper +++ b/ghost/core/content/themes/casper @@ -1 +1 @@ -Subproject commit bf3617a726b1b72f45663397d1ca11fb126c2546 +Subproject commit d147bb158e5705b7e903d0b18915380842ab5e83 diff --git a/ghost/core/core/boot.js b/ghost/core/core/boot.js index bee8ed9b5a7..d021a0fe0dd 100644 --- a/ghost/core/core/boot.js +++ b/ghost/core/core/boot.js @@ -253,6 +253,42 @@ async function initExpressApps({frontend, backend, config}) { return parentApp; } +/** + * Initialize prometheus client + */ +function initPrometheusClient({config}) { + if (config.get('metrics_server:enabled')) { + debug('Begin: initPrometheusClient'); + const prometheusClient = require('./shared/prometheus-client'); + debug('End: initPrometheusClient'); + return prometheusClient; + } + return null; +} + +/** + * Starts the standalone metrics server and registers a cleanup task to stop it when the ghost server shuts down + * @param {object} ghostServer + */ +async function initMetricsServer({prometheusClient, ghostServer, config}) { + debug('Begin: initMetricsServer'); + if (ghostServer && config.get('metrics_server:enabled')) { + const {MetricsServer} = require('@tryghost/metrics-server'); + const serverConfig = { + host: config.get('metrics_server:host') || '127.0.0.1', + port: config.get('metrics_server:port') || 9416 + }; + const handler = prometheusClient.handleMetricsRequest.bind(prometheusClient); + const metricsServer = new MetricsServer({serverConfig, handler}); + await metricsServer.start(); + // Ensure the metrics server is cleaned up when the ghost server is shut down + ghostServer.registerCleanupTask(async () => { + await metricsServer.shutdown(); + }); + } + debug('End: initMetricsServer'); +} + /** * Dynamic routing is generated from the routes.yaml file * When Ghost's DB and core are loaded, we can access this file and call routing.routingManager.start @@ -448,14 +484,6 @@ async function initBackgroundServices({config}) { const milestonesService = require('./server/services/milestones'); milestonesService.initAndRun(); - // Ideally OpenTelemetry should be configured as early as possible - // However, it can take a long time to initialize, so we load it here - // This prevents open telemetry from impacting boot time at the cost of not being able to trace the boot process - debug('Begin: Load OpenTelemetry'); - const opentelemetryInstrumentation = require('./shared/instrumentation'); - opentelemetryInstrumentation.initOpenTelemetry({config}); - debug('End: Load OpenTelemetry'); - debug('End: initBackgroundServices'); } @@ -523,6 +551,10 @@ async function bootGhost({backend = true, frontend = true, server = true} = {}) const sentry = require('./shared/sentry'); debug('End: Load sentry'); + // Initialize prometheus client early to enable metrics collection during boot + // Note: this does not start the metrics server yet to avoid increasing boot time + const prometheusClient = initPrometheusClient({config}); + // Step 2 - Start server with minimal app in global maintenance mode debug('Begin: load server + minimal app'); const rootApp = require('./app')(); @@ -591,6 +623,9 @@ async function bootGhost({backend = true, frontend = true, server = true} = {}) // Step 7 - Init our background services, we don't wait for this to finish initBackgroundServices({config}); + // Step 8 - Init our metrics server, we don't wait for this to finish + initMetricsServer({prometheusClient, ghostServer, config}); + // If we pass the env var, kill Ghost if (process.env.GHOST_CI_SHUTDOWN_AFTER_BOOT) { process.exit(0); diff --git a/ghost/core/core/frontend/helpers/content_api_key.js b/ghost/core/core/frontend/helpers/content_api_key.js new file mode 100644 index 00000000000..5101fa043f0 --- /dev/null +++ b/ghost/core/core/frontend/helpers/content_api_key.js @@ -0,0 +1,19 @@ +const {SafeString} = require('../services/handlebars'); +const logging = require('@tryghost/logging'); +const {getFrontendKey} = require('../services/proxy'); + +module.exports = async function content_api_key() { // eslint-disable-line camelcase + try { + const frontendKey = await getFrontendKey(); + + if (!frontendKey) { + logging.warn('contentkey: No content key found'); + return ''; + } + return new SafeString(frontendKey); + } catch (error) { + logging.error(error); + return ''; + } +}; +module.exports.async = true; \ No newline at end of file diff --git a/ghost/core/core/frontend/helpers/ghost_head.js b/ghost/core/core/frontend/helpers/ghost_head.js index c8ffc9158b0..cd2e3f2664c 100644 --- a/ghost/core/core/frontend/helpers/ghost_head.js +++ b/ghost/core/core/frontend/helpers/ghost_head.js @@ -86,7 +86,8 @@ function getSearchHelper(frontendKey) { const attrs = { key: frontendKey, styles: stylesUrl, - 'sodo-search': adminUrl + 'sodo-search': adminUrl, + locale: settingsCache.get('locale') || 'en' }; const dataAttrs = getDataAttributes(attrs); let helper = ``; diff --git a/ghost/core/core/server/data/schema/schema.js b/ghost/core/core/server/data/schema/schema.js index 7d1b1136181..17c536b13ae 100644 --- a/ghost/core/core/server/data/schema/schema.js +++ b/ghost/core/core/server/data/schema/schema.js @@ -113,7 +113,7 @@ module.exports = { meta_description: {type: 'string', maxlength: 2000, nullable: true, validations: {isLength: {max: 500}}}, email_subject: {type: 'string', maxlength: 300, nullable: true}, frontmatter: {type: 'text', maxlength: 65535, nullable: true}, - feature_image_alt: {type: 'string', maxlength: 191, nullable: true, validations: {isLength: {max: 125}}}, + feature_image_alt: {type: 'string', maxlength: 191, nullable: true}, feature_image_caption: {type: 'text', maxlength: 65535, nullable: true}, email_only: {type: 'boolean', nullable: false, defaultTo: false} }, @@ -418,7 +418,7 @@ module.exports = { post_status: {type: 'string', maxlength: 50, nullable: true, validations: {isIn: [['draft', 'published', 'scheduled', 'sent']]}}, reason: {type: 'string', maxlength: 50, nullable: true}, feature_image: {type: 'string', maxlength: 2000, nullable: true}, - feature_image_alt: {type: 'string', maxlength: 191, nullable: true, validations: {isLength: {max: 125}}}, + feature_image_alt: {type: 'string', maxlength: 191, nullable: true}, feature_image_caption: {type: 'text', maxlength: 65535, nullable: true}, custom_excerpt: {type: 'string', maxlength: 2000, nullable: true, validations: {isLength: {max: 300}}} }, diff --git a/ghost/core/core/shared/config/defaults.json b/ghost/core/core/shared/config/defaults.json index 817353439da..9595d05ef21 100644 --- a/ghost/core/core/shared/config/defaults.json +++ b/ghost/core/core/shared/config/defaults.json @@ -182,12 +182,12 @@ }, "portal": { "url": "https://cdn.jsdelivr.net/ghost/portal@~{version}/umd/portal.min.js", - "version": "2.43" + "version": "2.44" }, "sodoSearch": { "url": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/sodo-search.min.js", "styles": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/main.css", - "version": "1.1" + "version": "1.3" }, "announcementBar": { "url": "https://cdn.jsdelivr.net/ghost/announcement-bar@~{version}/umd/announcement-bar.min.js", @@ -195,11 +195,11 @@ }, "comments": { "url": "https://cdn.jsdelivr.net/ghost/comments-ui@~{version}/umd/comments-ui.min.js", - "version": "0.17" + "version": "0.19" }, "signupForm": { "url": "https://cdn.jsdelivr.net/ghost/signup-form@~{version}/umd/signup-form.min.js", - "version": "0.1" + "version": "0.2" }, "tenor": { "googleApiKey": null, diff --git a/ghost/core/core/shared/config/env/config.testing-browser.json b/ghost/core/core/shared/config/env/config.testing-browser.json index ee2ed003864..2a99eca8a4a 100644 --- a/ghost/core/core/shared/config/env/config.testing-browser.json +++ b/ghost/core/core/shared/config/env/config.testing-browser.json @@ -11,6 +11,9 @@ "server": { "port": 2369 }, + "metrics_server": { + "port": 9417 + }, "logging": { "level": "fatal" }, diff --git a/ghost/core/core/shared/config/env/config.testing-mysql.json b/ghost/core/core/shared/config/env/config.testing-mysql.json index 3c15227b1c2..31eb21e1012 100644 --- a/ghost/core/core/shared/config/env/config.testing-mysql.json +++ b/ghost/core/core/shared/config/env/config.testing-mysql.json @@ -3,6 +3,9 @@ "server": { "port": 2369 }, + "metrics_server": { + "port": 9417 + }, "database": { "client": "mysql", "connection": { diff --git a/ghost/core/core/shared/config/env/config.testing.json b/ghost/core/core/shared/config/env/config.testing.json index 74e2f1ec78f..25ba79f6bc8 100644 --- a/ghost/core/core/shared/config/env/config.testing.json +++ b/ghost/core/core/shared/config/env/config.testing.json @@ -11,6 +11,9 @@ "server": { "port": 2369 }, + "metrics_server": { + "port": 9417 + }, "logging": { "level": "error" }, diff --git a/ghost/core/core/shared/instrumentation.js b/ghost/core/core/shared/instrumentation.js deleted file mode 100644 index 1dd03720f75..00000000000 --- a/ghost/core/core/shared/instrumentation.js +++ /dev/null @@ -1,46 +0,0 @@ -const logging = require('@tryghost/logging'); - -async function initOpenTelemetry({config}) { - // Only enable if explicitly enabled via config `opentelemetry:enabled` - try { - const enabled = config.get('opentelemetry:enabled'); - if (!enabled) { - logging.debug('OpenTelemetry is not enabled'); - return false; - } - const perf = require('perf_hooks').performance; - perf.mark('opentelemetry:init:start'); - logging.debug('Initializing OpenTelemetry'); - - // Lazyloaded to avoid boot time overhead when not enabled - const {NodeSDK} = require('@opentelemetry/sdk-node'); - const {PrometheusExporter} = require('@opentelemetry/exporter-prometheus'); - const {RuntimeNodeInstrumentation} = require('@opentelemetry/instrumentation-runtime-node'); - - const prometheusExporter = new PrometheusExporter({ - port: config.get('opentelemetry:prometheus:port'), - startServer: true - }); - - const sdk = new NodeSDK({ - serviceName: 'ghost', - metricReader: prometheusExporter, - instrumentations: [ - new RuntimeNodeInstrumentation({ - eventLoopUtilizationMeasurementInterval: 5000 - }) - ] - }); - sdk.start(); - perf.mark('opentelemetry:init:finished'); - logging.debug('OpenTelemetry initialized in', Math.round(perf.measure('opentelemetry:init:duration', 'opentelemetry:init:start', 'opentelemetry:init:finished').duration), 'ms'); - return true; - } catch (error) { - logging.error('Error initializing OpenTelemetry', error); - return false; - } -} - -module.exports = { - initOpenTelemetry -}; diff --git a/ghost/core/core/shared/prometheus-client.js b/ghost/core/core/shared/prometheus-client.js new file mode 100644 index 00000000000..9d3271bb86b --- /dev/null +++ b/ghost/core/core/shared/prometheus-client.js @@ -0,0 +1,36 @@ +class PrometheusClient { + constructor() { + this.client = require('prom-client'); + this.prefix = 'ghost_'; + this.collectDefaultMetrics(); + } + + collectDefaultMetrics() { + this.client.collectDefaultMetrics({prefix: this.prefix}); + } + + async handleMetricsRequest(req, res) { + try { + res.set('Content-Type', this.getContentType()); + res.end(await this.getMetrics()); + } catch (err) { + res.status(500).end(err.message); + } + } + + async getMetrics() { + return this.client.register.metrics(); + } + + getRegistry() { + return this.client.register; + } + + getContentType() { + return this.getRegistry().contentType; + } +} + +// Create a singleton instance and export it as the default export +const prometheusClient = new PrometheusClient(); +module.exports = prometheusClient; diff --git a/ghost/core/package.json b/ghost/core/package.json index 792947d315d..6b73ac9159d 100644 --- a/ghost/core/package.json +++ b/ghost/core/package.json @@ -1,6 +1,6 @@ { "name": "ghost", - "version": "5.94.2", + "version": "5.96.0", "description": "The professional publishing platform", "author": "Ghost Foundation", "homepage": "https://ghost.org", @@ -58,12 +58,6 @@ }, "dependencies": { "@extractus/oembed-extractor": "3.2.1", - "@opentelemetry/api": "1.9.0", - "@opentelemetry/exporter-prometheus": "0.52.1", - "@opentelemetry/instrumentation-runtime-node": "0.6.0", - "@opentelemetry/sdk-metrics": "1.25.1", - "@opentelemetry/sdk-node": "0.52.1", - "@opentelemetry/sdk-trace-node": "1.25.1", "@sentry/node": "7.119.0", "@slack/webhook": "7.0.3", "@tryghost/adapter-base-cache": "0.1.12", @@ -212,13 +206,14 @@ "moment": "2.24.0", "moment-timezone": "0.5.45", "multer": "1.4.4", - "mysql2": "3.11.0", + "mysql2": "3.11.3", "nconf": "0.12.1", "node-jose": "2.2.0", "path-match": "1.2.4", "probe-image-size": "7.2.3", + "prom-client": "15.1.3", "rss": "1.2.2", - "sanitize-html": "2.13.0", + "sanitize-html": "2.13.1", "semver": "7.6.3", "stoppable": "1.1.0", "superagent": "5.1.0", @@ -254,6 +249,7 @@ "mock-knex": "TryGhost/mock-knex#d8b93b1c20d4820323477f2c60db016ab3e73192", "nock": "13.3.3", "papaparse": "5.3.2", + "parse-prometheus-text-format": "1.1.1", "postcss": "8.4.39", "postcss-cli": "11.0.0", "rewire": "6.0.0", diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap index 9553d856dde..3d86cbea3ca 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap @@ -39,7 +39,7 @@ Object { "mailgunIsConfigured": false, "signupForm": Object { "url": "https://cdn.jsdelivr.net/ghost/signup-form@~{version}/umd/signup-form.min.js", - "version": "0.1", + "version": "0.2", }, "stripeDirect": false, "tenor": Object { diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap index 3e90545e516..e666a2cdb23 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/posts.test.js.snap @@ -1457,6 +1457,24 @@ Object { } `; +exports[`Posts API Create Errors if feature_image_alt is too long 1: [body] 1`] = ` +Object { + "errors": Array [ + Object { + "code": null, + "context": StringMatching /\\.\\*post_revisions\\\\\\.feature_image_alt\\] exceeds maximum length of 191 characters\\.\\*/, + "details": null, + "ghostErrorCode": null, + "help": null, + "id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + "message": "Validation error, cannot save post.", + "property": null, + "type": "ValidationError", + }, + ], +} +`; + exports[`Posts API Create Errors with an invalid lexical state object 1: [body] 1`] = ` Object { "errors": Array [ diff --git a/ghost/core/test/e2e-api/admin/images.test.js b/ghost/core/test/e2e-api/admin/images.test.js index ee882b35cb4..8d4aad72eae 100644 --- a/ghost/core/test/e2e-api/admin/images.test.js +++ b/ghost/core/test/e2e-api/admin/images.test.js @@ -342,7 +342,7 @@ describe('Images API', function () { const brokenPayload = '--boundary\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example.png\"\r\nContent-Type: image/png\r\n\r\n'; // eslint-disable-next-line no-undef - const brokenDataBlob = await (new Blob([brokenPayload, blob.slice(0, blob.size / 2)], { + const brokenDataBlob = await (new Blob([brokenPayload, blob.slice(0, Math.floor(blob.size / 2))], { type: 'multipart/form-data; boundary=boundary' })).text(); @@ -367,7 +367,7 @@ describe('Images API', function () { const brokenPayload = '--boundary\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example.png\"\r\nContent-Type: image/png\r\n'; // eslint-disable-next-line no-undef - const brokenDataBlob = await (new Blob([brokenPayload, blob.slice(0, blob.size / 2)], { + const brokenDataBlob = await (new Blob([brokenPayload, blob.slice(0, Math.floor(blob.size / 2))], { type: 'multipart/form-data; boundary=boundary' })).text(); diff --git a/ghost/core/test/e2e-api/admin/media.test.js b/ghost/core/test/e2e-api/admin/media.test.js index 0baaef3e5ad..5f88cb489ea 100644 --- a/ghost/core/test/e2e-api/admin/media.test.js +++ b/ghost/core/test/e2e-api/admin/media.test.js @@ -140,7 +140,7 @@ describe('Media API', function () { const brokenPayload = '--boundary\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example.png\"\r\nContent-Type: image/png\r\n\r\n'; // eslint-disable-next-line no-undef - const brokenDataBlob = await (new Blob([brokenPayload, blob.slice(0, blob.size / 2)], { + const brokenDataBlob = await (new Blob([brokenPayload, blob.slice(0, Math.floor(blob.size / 2))], { type: 'multipart/form-data; boundary=boundary' })).text(); @@ -164,7 +164,7 @@ describe('Media API', function () { const brokenPayload = '--boundary\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example.png\"\r\nContent-Type: image/png\r\n'; // eslint-disable-next-line no-undef - const brokenDataBlob = await (new Blob([brokenPayload, blob.slice(0, blob.size / 2)], { + const brokenDataBlob = await (new Blob([brokenPayload, blob.slice(0, Math.floor(blob.size / 2))], { type: 'multipart/form-data; boundary=boundary' })).text(); diff --git a/ghost/core/test/e2e-api/admin/posts.test.js b/ghost/core/test/e2e-api/admin/posts.test.js index 7b837f9ff11..04c64403e1d 100644 --- a/ghost/core/test/e2e-api/admin/posts.test.js +++ b/ghost/core/test/e2e-api/admin/posts.test.js @@ -390,6 +390,25 @@ describe('Posts API', function () { }); }); + it('Errors if feature_image_alt is too long', async function () { + const post = { + title: 'Feature image alt too long', + feature_image_alt: 'a'.repeat(201) + }; + + await agent + .post('/posts/?formats=mobiledoc,lexical,html') + .body({posts: [post]}) + .expectStatus(422) + .matchBodySnapshot({ + errors: [{ + id: anyErrorId, + // TODO: this should be `posts.feature_image_alt` but we're hitting revision errors first + context: stringMatching(/.*post_revisions\.feature_image_alt] exceeds maximum length of 191 characters.*/) + }] + }); + }); + it('Clears all page html fields when creating published post', async function () { const totalPageCount = await models.Post.where({type: 'page'}).count(); should.exist(totalPageCount, 'total page count'); @@ -877,7 +896,7 @@ describe('Posts API', function () { 'content-version': anyContentVersion, etag: anyEtag }); - + const convertedPost = conversionResponse.body.posts[0]; const expectedConvertedLexical = convertedPost.lexical; await agent diff --git a/ghost/core/test/e2e-api/members-comments/__snapshots__/comments.test.js.snap b/ghost/core/test/e2e-api/members-comments/__snapshots__/comments.test.js.snap index a4c7f211982..9e25be50ea1 100644 --- a/ghost/core/test/e2e-api/members-comments/__snapshots__/comments.test.js.snap +++ b/ghost/core/test/e2e-api/members-comments/__snapshots__/comments.test.js.snap @@ -147,7 +147,7 @@ Object { "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/comments\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, "vary": "Accept-Encoding", - "x-cache-invalidate": "/api/members/comments/post/618ba1ffbe2896088840a6df/, /api/members/comments/6195c6a1e792de832cd08144/replies/", + "x-cache-invalidate": StringMatching /\\\\/api\\\\/members\\\\/comments\\\\/post\\\\/\\[0-9a-f\\]\\{24\\}\\\\/, \\\\/api\\\\/members\\\\/comments\\\\/\\[0-9a-f\\]\\{24\\}\\\\/replies\\\\//, "x-powered-by": "Express", } `; @@ -1678,7 +1678,7 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "

        First.

        ", + "html": "

        This is a comment

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -1695,7 +1695,7 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "

        Really original

        ", + "html": "

        This is a reply

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -1717,14 +1717,14 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "

        This is a message

        New line

        ", + "html": "

        This is a comment

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { "avatar_image": null, "expertise": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": null, + "name": "Egon Spengler", "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, "replies": Array [], @@ -1766,7 +1766,7 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "

        First.

        ", + "html": "

        This is a comment

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -1783,7 +1783,7 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "

        Really original

        ", + "html": "

        This is a reply

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -1805,14 +1805,14 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "

        This is a message

        New line

        ", + "html": "

        This is a comment

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { "avatar_image": null, "expertise": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": null, + "name": "Egon Spengler", "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, "replies": Array [], @@ -1854,14 +1854,14 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "

        This is a message

        New line

        ", + "html": "

        This is a comment

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { "avatar_image": null, "expertise": null, "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": null, + "name": "Egon Spengler", "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, "replies": Array [], @@ -1874,7 +1874,7 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "

        First.

        ", + "html": "

        This is a comment

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -1891,7 +1891,7 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "

        Really original

        ", + "html": "

        This is a reply

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -1979,7 +1979,7 @@ Object { Object { "count": Object { "likes": Any, - "replies": Any, + "replies": 0, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -1993,26 +1993,7 @@ Object { "name": null, "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, - "replies": Array [ - Object { - "count": Object { - "likes": Any, - }, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "edited_at": null, - "html": "This is a reply", - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "liked": Any, - "member": Object { - "avatar_image": null, - "expertise": null, - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": null, - "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - }, - "status": "published", - }, - ], + "replies": Array [], "status": "published", }, ], @@ -2023,7 +2004,7 @@ exports[`Comments API when commenting enabled for all when authenticated Can edi Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "678", + "content-length": "370", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", @@ -2034,9 +2015,9 @@ Object { exports[`Comments API when commenting enabled for all when authenticated Can fetch counts 1: [body] 1`] = ` Object { - "618ba1ffbe2896088840a6df": 15, - "618ba1ffbe2896088840a6e1": 0, - "618ba1ffbe2896088840a6e3": 0, + "618ba1ffbe2896088840a6df": 1, + "618ba1ffbe2896088840a6e1": 2, + "618ba1ffbe2896088840a6e3": 3, } `; @@ -2044,7 +2025,7 @@ exports[`Comments API when commenting enabled for all when authenticated Can fet Object { "access-control-allow-origin": "*", "cache-control": "public, max-age=0", - "content-length": "89", + "content-length": "88", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", @@ -2064,6 +2045,7 @@ Object { "edited_at": null, "html": "

        This is a message

        New line

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "in_reply_to_id": null, "liked": Any, "member": Object { "avatar_image": null, @@ -2081,6 +2063,7 @@ Object { "edited_at": null, "html": "This is a reply", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "in_reply_to_id": null, "liked": Any, "member": Object { "avatar_image": null, @@ -2098,11 +2081,48 @@ Object { } `; +exports[`Comments API when commenting enabled for all when authenticated Can like a comment 1: [headers] 1`] = ` +Object { + "access-control-allow-origin": "*", + "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, + "x-cache-invalidate": "/api/members/comments/post/618ba1ffbe2896088840a6df/", + "x-powered-by": "Express", +} +`; + +exports[`Comments API when commenting enabled for all when authenticated Can like a comment 2: [body] 1`] = ` +Object { + "comments": Array [ + Object { + "count": Object { + "likes": Any, + "replies": 0, + }, + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "edited_at": null, + "html": "

        This is a comment

        ", + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "liked": Any, + "member": Object { + "avatar_image": null, + "expertise": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": "Egon Spengler", + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + "replies": Array [], + "status": "published", + }, + ], +} +`; + exports[`Comments API when commenting enabled for all when authenticated Can like a comment 2: [headers] 1`] = ` Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "687", + "content-length": "731", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", @@ -2114,8 +2134,10 @@ exports[`Comments API when commenting enabled for all when authenticated Can lik Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", + "content-length": "367", + "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, - "x-cache-invalidate": "/api/members/comments/post/618ba1ffbe2896088840a6df/", + "vary": "Accept-Encoding", "x-powered-by": "Express", } `; @@ -2132,6 +2154,7 @@ Object { "edited_at": null, "html": "

        This is a message

        New line

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "in_reply_to_id": null, "liked": Any, "member": Object { "avatar_image": null, @@ -2149,6 +2172,7 @@ Object { "edited_at": null, "html": "This is a reply", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "in_reply_to_id": null, "liked": Any, "member": Object { "avatar_image": null, @@ -2170,7 +2194,7 @@ exports[`Comments API when commenting enabled for all when authenticated Can lik Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "686", + "content-length": "730", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", @@ -2183,7 +2207,7 @@ Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, - "x-cache-invalidate": "/api/members/comments/post/618ba1ffbe2896088840a6df/, /api/members/comments/6195c6a1e792de832cd08144/replies/", + "x-cache-invalidate": StringMatching /\\\\/api\\\\/members\\\\/comments\\\\/post\\\\/\\[0-9a-f\\]\\{24\\}\\\\/, \\\\/api\\\\/members\\\\/comments\\\\/\\[0-9a-f\\]\\{24\\}\\\\/replies\\\\//, "x-powered-by": "Express", } `; @@ -2194,11 +2218,11 @@ Object { Object { "count": Object { "likes": Any, - "replies": Any, + "replies": 0, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "This is a reply 0", + "html": "

        This is a comment

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -2219,7 +2243,7 @@ exports[`Comments API when commenting enabled for all when authenticated Can lik Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "349", + "content-length": "356", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", @@ -2233,7 +2257,7 @@ Object { Object { "count": Object { "likes": Any, - "replies": Any, + "replies": 0, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, @@ -2247,26 +2271,7 @@ Object { "name": null, "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, - "replies": Array [ - Object { - "count": Object { - "likes": Any, - }, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "edited_at": null, - "html": "This is a reply", - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "liked": Any, - "member": Object { - "avatar_image": null, - "expertise": null, - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": null, - "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - }, - "status": "published", - }, - ], + "replies": Array [], "status": "published", }, ], @@ -2277,7 +2282,7 @@ exports[`Comments API when commenting enabled for all when authenticated Can not Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "685", + "content-length": "377", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", @@ -2361,11 +2366,11 @@ Object { Object { "count": Object { "likes": Any, - "replies": Any, + "replies": 0, }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "

        This is a message

        New line

        ", + "html": "

        This is a comment

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -2375,26 +2380,7 @@ Object { "name": null, "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, }, - "replies": Array [ - Object { - "count": Object { - "likes": Any, - }, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "edited_at": null, - "html": "This is a reply", - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "liked": Any, - "member": Object { - "avatar_image": null, - "expertise": null, - "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, - "name": null, - "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, - }, - "status": "published", - }, - ], + "replies": Array [], "status": "published", }, ], @@ -2405,7 +2391,7 @@ exports[`Comments API when commenting enabled for all when authenticated Can rem Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "687", + "content-length": "357", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", @@ -2449,7 +2435,7 @@ Object { "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/comments\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, "vary": "Accept-Encoding", - "x-cache-invalidate": "/api/members/comments/post/618ba1ffbe2896088840a6df/, /api/members/comments/6195c6a1e792de832cd08144/replies/", + "x-cache-invalidate": StringMatching /\\\\/api\\\\/members\\\\/comments\\\\/post\\\\/\\[0-9a-f\\]\\{24\\}\\\\/, \\\\/api\\\\/members\\\\/comments\\\\/\\[0-9a-f\\]\\{24\\}\\\\/replies\\\\//, "x-powered-by": "Express", } `; @@ -2490,7 +2476,7 @@ Object { "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/comments\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, "vary": "Accept-Encoding", - "x-cache-invalidate": "/api/members/comments/post/618ba1ffbe2896088840a6df/, /api/members/comments/6195c6a1e792de832cd08144/replies/", + "x-cache-invalidate": StringMatching /\\\\/api\\\\/members\\\\/comments\\\\/post\\\\/\\[0-9a-f\\]\\{24\\}\\\\/, \\\\/api\\\\/members\\\\/comments\\\\/\\[0-9a-f\\]\\{24\\}\\\\/replies\\\\//, "x-powered-by": "Express", } `; @@ -2531,7 +2517,7 @@ Object { "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/comments\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, "vary": "Accept-Encoding", - "x-cache-invalidate": "/api/members/comments/post/618ba1ffbe2896088840a6df/, /api/members/comments/6195c6a1e792de832cd08144/replies/", + "x-cache-invalidate": StringMatching /\\\\/api\\\\/members\\\\/comments\\\\/post\\\\/\\[0-9a-f\\]\\{24\\}\\\\/, \\\\/api\\\\/members\\\\/comments\\\\/\\[0-9a-f\\]\\{24\\}\\\\/replies\\\\//, "x-powered-by": "Express", } `; @@ -2595,7 +2581,7 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "This is a reply", + "html": "

        This is a reply

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -2625,7 +2611,7 @@ exports[`Comments API when commenting enabled for all when authenticated Can req Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "407", + "content-length": "414", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", @@ -2707,7 +2693,7 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "

        Really original

        ", + "html": "

        This is a reply

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -2725,7 +2711,7 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "This is a reply", + "html": "

        This is a reply

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -2743,7 +2729,7 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "This is a reply 0", + "html": "

        This is a reply

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -2761,7 +2747,7 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "This is a reply 1", + "html": "

        This is a reply

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -2779,7 +2765,7 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "This is a reply 2", + "html": "

        This is a reply

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -2797,7 +2783,7 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "This is a reply", + "html": "

        This is a reply

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -2815,7 +2801,7 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "This is a reply", + "html": "

        This is a reply

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -2845,7 +2831,7 @@ exports[`Comments API when commenting enabled for all when authenticated Can ret Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "2277", + "content-length": "2313", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", @@ -2932,7 +2918,7 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "

        First.

        ", + "html": "

        This is a comment

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -2949,7 +2935,7 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "

        Really original

        ", + "html": "

        This is a reply

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -2967,7 +2953,25 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "This is a reply", + "html": "

        This is a reply

        ", + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "liked": Any, + "member": Object { + "avatar_image": null, + "expertise": null, + "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "name": null, + "uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/, + }, + "status": "published", + }, + Object { + "count": Object { + "likes": Any, + }, + "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, + "edited_at": null, + "html": "

        This is a reply

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -2990,7 +2994,7 @@ exports[`Comments API when commenting enabled for all when authenticated Limits Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "974", + "content-length": "1308", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", @@ -3010,6 +3014,7 @@ Object { "edited_at": null, "html": "This is a reply 0", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "in_reply_to_id": null, "liked": Any, "member": Object { "avatar_image": null, @@ -3029,7 +3034,7 @@ exports[`Comments API when commenting enabled for all when authenticated Limits Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "350", + "content-length": "372", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/comments\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -3051,6 +3056,7 @@ Object { "edited_at": null, "html": "This is a reply 1", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "in_reply_to_id": null, "liked": Any, "member": Object { "avatar_image": null, @@ -3070,7 +3076,7 @@ exports[`Comments API when commenting enabled for all when authenticated Limits Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "350", + "content-length": "372", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/comments\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -3092,6 +3098,7 @@ Object { "edited_at": null, "html": "This is a reply 2", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "in_reply_to_id": null, "liked": Any, "member": Object { "avatar_image": null, @@ -3111,7 +3118,7 @@ exports[`Comments API when commenting enabled for all when authenticated Limits Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "350", + "content-length": "372", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/comments\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, @@ -3133,6 +3140,7 @@ Object { "edited_at": null, "html": "

        First.

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "in_reply_to_id": null, "liked": Any, "member": Object { "avatar_image": null, @@ -3150,6 +3158,7 @@ Object { "edited_at": null, "html": "

        Really original

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "in_reply_to_id": null, "liked": Any, "member": Object { "avatar_image": null, @@ -3168,6 +3177,7 @@ Object { "edited_at": null, "html": "This is a reply", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "in_reply_to_id": null, "liked": Any, "member": Object { "avatar_image": null, @@ -3186,6 +3196,7 @@ Object { "edited_at": null, "html": "This is a reply 0", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, + "in_reply_to_id": null, "liked": Any, "member": Object { "avatar_image": null, @@ -3207,7 +3218,7 @@ exports[`Comments API when commenting enabled for all when authenticated Limits Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "1285", + "content-length": "1373", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", @@ -3225,7 +3236,7 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "

        First.

        ", + "html": "

        This is a comment

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -3242,7 +3253,7 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "

        Really original

        ", + "html": "

        This is a reply

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -3275,7 +3286,7 @@ exports[`Comments API when commenting enabled for all when not authenticated Can Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "753", + "content-length": "764", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", @@ -3293,7 +3304,7 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "

        First.

        ", + "html": "

        This is a comment

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -3310,7 +3321,7 @@ Object { }, "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, "edited_at": null, - "html": "

        Really original

        ", + "html": "

        This is a reply

        ", "id": StringMatching /\\[a-f0-9\\]\\{24\\}/, "liked": Any, "member": Object { @@ -3343,7 +3354,7 @@ exports[`Comments API when commenting enabled for all when not authenticated Can Object { "access-control-allow-origin": "*", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "753", + "content-length": "764", "content-type": "application/json; charset=utf-8", "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "vary": "Accept-Encoding", @@ -3834,7 +3845,7 @@ Object { "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, "location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/comments\\\\/\\[a-f0-9\\]\\{24\\}\\\\//, "vary": "Accept-Encoding", - "x-cache-invalidate": "/api/members/comments/post/618ba1ffbe2896088840a6df/, /api/members/comments/6195c6a1e792de832cd08144/replies/", + "x-cache-invalidate": StringMatching /\\\\/api\\\\/members\\\\/comments\\\\/post\\\\/\\[0-9a-f\\]\\{24\\}\\\\/, \\\\/api\\\\/members\\\\/comments\\\\/\\[0-9a-f\\]\\{24\\}\\\\/replies\\\\//, "x-powered-by": "Express", } `; diff --git a/ghost/core/test/e2e-api/members-comments/comments.test.js b/ghost/core/test/e2e-api/members-comments/comments.test.js index 7deff815954..4a023eafa0b 100644 --- a/ghost/core/test/e2e-api/members-comments/comments.test.js +++ b/ghost/core/test/e2e-api/members-comments/comments.test.js @@ -1,5 +1,5 @@ const assert = require('assert/strict'); -const {agentProvider, mockManager, fixtureManager, matchers, configUtils} = require('../../utils/e2e-framework'); +const {agentProvider, mockManager, fixtureManager, matchers, configUtils, dbUtils} = require('../../utils/e2e-framework'); const {anyEtag, anyObjectId, anyLocationFor, anyISODateTime, anyErrorId, anyUuid, anyNumber, anyBoolean} = matchers; const should = require('should'); const models = require('../../../core/server/models'); @@ -8,12 +8,89 @@ const settingsCache = require('../../../core/shared/settings-cache'); const sinon = require('sinon'); const DomainEvents = require('@tryghost/domain-events'); -let membersAgent, membersAgent2, postId, postTitle, commentId; +let membersAgent, membersAgent2, postId, postAuthorEmail, postTitle; async function getPaidProduct() { return await models.Product.findOne({type: 'paid'}); } +const dbFns = { + /** + * @typedef {Object} AddCommentData + * @property {string} [post_id=postId] + * @property {string} member_id + * @property {string} [parent_id] + * @property {string} [html='This is a comment'] + */ + /** + * @typedef {Object} AddCommentReplyData + * @property {string} member_id + * @property {string} [html='This is a reply'] + */ + /** + * @typedef {AddCommentData & {replies: AddCommentReplyData[]}} AddCommentWithRepliesData + */ + + /** + * @param {AddCommentData} data + * @returns {Promise} + */ + addComment: async (data) => { + return await models.Comment.add({ + post_id: data.post_id || postId, + member_id: data.member_id, + parent_id: data.parent_id, + html: data.html || '

        This is a comment

        ' + }); + }, + /** + * @param {AddCommentWithRepliesData} data + * @returns {Promise} + */ + addCommentWithReplies: async (data) => { + const {replies, ...commentData} = data; + + const parent = await dbFns.addComment(commentData); + const createdReplies = []; + + for (const reply of replies) { + const createdReply = await dbFns.addComment({ + post_id: parent.get('post_id'), + member_id: reply.member_id, + parent_id: parent.get('id'), + html: reply.html || '

        This is a reply

        ' + }); + createdReplies.push(createdReply); + } + + return {parent, replies: createdReplies}; + }, + /** + * @param {Object} data + * @param {string} data.comment_id + * @param {string} data.member_id + * @returns {Promise} + */ + addLike: async (data) => { + return await models.CommentLike.add({ + comment_id: data.comment_id, + member_id: data.member_id + }); + }, + /** + * @param {Object} data + * @param {string} data.comment_id + * @param {string} data.member_id + * @returns {Promise} + */ + addReport: async (data) => { + return await models.CommentReport.add({ + comment_id: data.comment_id, + member_id: data.member_id + }); + } +}; + const commentMatcher = { id: anyObjectId, created_at: anyISODateTime, @@ -53,31 +130,103 @@ function escapeRegExp(string) { return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } -async function testCanCommentOnPost(member) { - await models.Member.edit({last_seen_at: null, last_commented_at: null}, {id: member.get('id')}); +/** + * @param {string} method + * @param {string} url + * @param {number} status + * @param {Array} [errors] + * @returns {any} ExpectRequest + */ +function testBasicErrorResponse(method, url, status, errors) { + if (!errors) { + errors = [{id: anyUuid}]; + } + + return membersAgent[method](url) + .expectStatus(status) + .matchHeaderSnapshot({etag: anyEtag}) + .matchBodySnapshot({errors}); +} + +/** + * @param {string} method + * @param {string} url + * @param {number} status + * @returns {any} ExpectRequest + */ +function testBasicEmptyResponse(method, url, status) { + return membersAgent[method](url) + .expectStatus(status) + .matchHeaderSnapshot({etag: anyEtag}) + .expectEmptyBody(); +} - const {body} = await membersAgent +/** + * @param {string} url + * @param {Object} commentsMatcher + */ +function testGetComments(url, commentsMatcher) { + return membersAgent + .get(url) + .expectStatus(200) + .matchHeaderSnapshot({ + etag: anyEtag + }) + .matchBodySnapshot({ + comments: commentsMatcher + }); +} + +/** + * @param {Object} data + * @param {string} data.post_id + * @param {string} data.html + * @param {string} [data.parent_id] + * @param {Object} [options] + * @param {number} [options.status = 201] + * @param {Object} [options.matchHeaderSnapshot] + * @param {Object} [options.matchBodySnapshot] + * @returns {any} ExpectRequest + */ +function testPostComment({post_id, html, parent_id}, {status = 201, matchHeaderSnapshot = {}, matchBodySnapshot} = {}) { + return membersAgent .post(`/api/comments/`) .body({comments: [{ - post_id: postId, - html: '

        This is a message

        New line

        ' + post_id, + parent_id, + html }]}) - .expectStatus(201) + .expectStatus(status) .matchHeaderSnapshot({ etag: anyEtag, - location: anyLocationFor('comments') + location: anyLocationFor('comments'), + ...matchHeaderSnapshot }) .matchBodySnapshot({ - comments: [commentMatcher] + comments: [commentMatcher], + ...matchBodySnapshot }); - // Save for other tests - commentId = body.comments[0].id; +} + +function assertAuthorEmailSent(email, title, extraAssertions = {}) { + mockManager.assert.sentEmail({ + subject: '💬 New comment on your post: ' + title, + to: email, + ...extraAssertions + }); +} + +async function testCanCommentOnPost(member) { + await models.Member.edit({last_seen_at: null, last_commented_at: null}, {id: member.get('id')}); + + await testPostComment({ + post_id: postId, + html: '

        This is a message

        New line

        ' + }); // Check if author got an email mockManager.assert.sentEmailCount(1); - mockManager.assert.sentEmail({ - subject: '💬 New comment on your post: ' + postTitle, - to: fixtureManager.get('users', 0).email, + assertAuthorEmailSent(postAuthorEmail, postTitle, { // Note that the tag is removed by the sanitizer html: new RegExp(escapeRegExp('

        This is a message

        New line

        ')) }); @@ -94,35 +243,32 @@ async function testCanCommentOnPost(member) { } async function testCanReply(member, emailMatchers = {}) { + const parentComment = await dbFns.addComment({ + member_id: fixtureManager.get('members', 2).id + }); + const date = new Date(0); await models.Member.edit({last_seen_at: date, last_commented_at: date}, {id: member.get('id')}); - await membersAgent - .post(`/api/comments/`) - .body({comments: [{ - post_id: postId, - parent_id: fixtureManager.get('comments', 0).id, - html: 'This is a reply' - }]}) - .expectStatus(201) - .matchHeaderSnapshot({ - etag: anyEtag, - location: anyLocationFor('comments') - }) - .matchBodySnapshot({ - comments: [commentMatcher] - }); + await testPostComment({ + post_id: postId, + parent_id: parentComment.get('id'), + html: 'This is a reply' + }, { + matchHeaderSnapshot: { + 'x-cache-invalidate': matchers.stringMatching( + new RegExp('/api/members/comments/post/[0-9a-f]{24}/, /api/members/comments/[0-9a-f]{24}/replies/') + ) + } + }); mockManager.assert.sentEmailCount(2); - mockManager.assert.sentEmail({ - subject: '💬 New comment on your post: ' + postTitle, - to: fixtureManager.get('users', 0).email - }); + assertAuthorEmailSent(postAuthorEmail, postTitle); mockManager.assert.sentEmail({ ...emailMatchers, subject: '↪️ New reply to your comment on Ghost', - to: fixtureManager.get('members', 0).email + to: fixtureManager.get('members', 2).email }); // Wait for the dispatched events (because this happens async) @@ -154,12 +300,12 @@ async function testCannotCommentOnPost(status = 403) { }); } -async function testCannotReply(status = 403) { +async function testCannotReply(parentId, status = 403) { await membersAgent .post(`/api/comments/`) .body({comments: [{ post_id: postId, - parent_id: fixtureManager.get('comments', 0).id, + parent_id: parentId, html: 'This is a reply' }]}) .expectStatus(status) @@ -174,20 +320,26 @@ async function testCannotReply(status = 403) { } describe('Comments API', function () { - let member; + let loggedInMember; before(async function () { membersAgent = await agentProvider.getMembersAPIAgent(); membersAgent2 = membersAgent.duplicate(); - await fixtureManager.init('posts', 'members', 'comments'); + await fixtureManager.init('posts', 'members'); postId = fixtureManager.get('posts', 0).id; postTitle = fixtureManager.get('posts', 0).title; + postAuthorEmail = fixtureManager.get('users', 0).email; }); - beforeEach(function () { + beforeEach(async function () { mockManager.mockMail(); + + // ensure we don't have data dependencies across tests + await dbUtils.truncate('comments'); + await dbUtils.truncate('comment_likes'); + await dbUtils.truncate('comment_reports'); }); afterEach(async function () { @@ -211,28 +363,27 @@ describe('Comments API', function () { sinon.restore(); }); + async function setupBrowseCommentsData() { + await dbFns.addCommentWithReplies({ + member_id: fixtureManager.get('members', 0).id, + replies: [{ + member_id: fixtureManager.get('members', 1).id + }] + }); + } + it('Can browse all comments of a post (legacy)', async function () { - await membersAgent - .get(`/api/comments/?filter=post_id:'${postId}'`) - .expectStatus(200) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .matchBodySnapshot({ - comments: [commentMatcherWithReplies({replies: 1})] - }); + await setupBrowseCommentsData(); + await testGetComments(`/api/comments/?filter=post_id:'${postId}'`, [ + commentMatcherWithReplies({replies: 1}) + ]); }); it('Can browse all comments of a post', async function () { - await membersAgent - .get(`/api/comments/post/${postId}/`) - .expectStatus(200) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .matchBodySnapshot({ - comments: [commentMatcherWithReplies({replies: 1})] - }); + await setupBrowseCommentsData(); + await testGetComments(`/api/comments/post/${postId}/`, [ + commentMatcherWithReplies({replies: 1}) + ]); }); it('cannot comment on a post', async function () { @@ -240,15 +391,19 @@ describe('Comments API', function () { }); it('cannot reply on a post', async function () { - await testCannotReply(401); + const comment = await dbFns.addComment({ + member_id: fixtureManager.get('members', 0).id + }); + await testCannotReply(comment.get('id'), 401); }); it('cannot report a comment', async function () { - commentId = fixtureManager.get('comments', 0).id; + const comment = await dbFns.addComment({ + member_id: fixtureManager.get('members', 2).id + }); - // Create a temporary comment await membersAgent - .post(`/api/comments/${commentId}/report/`) + .post(`/api/comments/${comment.get('id')}/report/`) .expectStatus(401) .matchHeaderSnapshot({ etag: anyEtag @@ -261,33 +416,23 @@ describe('Comments API', function () { }); it('cannot like a comment', async function () { - // Create a temporary comment - await membersAgent - .post(`/api/comments/${commentId}/like/`) - .expectStatus(401) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .matchBodySnapshot({ - errors: [{ - id: anyUuid - }] - }); + const comment = await dbFns.addComment({ + member_id: fixtureManager.get('members', 2).id + }); + + await testBasicErrorResponse('post', `/api/comments/${comment.get('id')}/like/`, 401); }); it('cannot unlike a comment', async function () { - // Create a temporary comment - await membersAgent - .delete(`/api/comments/${commentId}/like/`) - .expectStatus(401) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .matchBodySnapshot({ - errors: [{ - id: anyUuid - }] - }); + const comment = await dbFns.addComment({ + member_id: fixtureManager.get('members', 2).id + }); + await dbFns.addLike({ + comment_id: comment.get('id'), + member_id: fixtureManager.get('members', 0).id + }); + + await testBasicErrorResponse('delete', `/api/comments/${comment.get('id')}/like/`, 401); }); }); @@ -296,7 +441,7 @@ describe('Comments API', function () { before(async function () { await membersAgent.loginAs('member@example.com'); - member = await models.Member.findOne({email: 'member@example.com'}, {require: true}); + loggedInMember = await models.Member.findOne({email: 'member@example.com'}, {require: true}); await membersAgent2.loginAs('member2@example.com'); }); @@ -315,153 +460,98 @@ describe('Comments API', function () { }); it('Can comment on a post', async function () { - await testCanCommentOnPost(member); + await testCanCommentOnPost(loggedInMember); }); + async function setupBrowseCommentsData() { + await dbFns.addCommentWithReplies({ + member_id: fixtureManager.get('members', 0).id, + replies: [{ + member_id: fixtureManager.get('members', 1).id + }] + }); + await dbFns.addComment({ + member_id: fixtureManager.get('members', 2).id + }); + } + it('Can browse all comments of a post (legacy)', async function () { + await setupBrowseCommentsData(); // uses explicit order to match db ordering - await membersAgent - .get(`/api/comments/?filter=post_id:'${postId}'&order=id%20ASC`) - .expectStatus(200) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .matchBodySnapshot({ - comments: [ - commentMatcherWithReplies({replies: 1}), - commentMatcher - ] - }); + await testGetComments(`/api/comments/?filter=post_id:'${postId}'&order=id%20ASC`, [ + commentMatcherWithReplies({replies: 1}), + commentMatcher + ]); }); it('Can browse all comments of a post', async function () { + await setupBrowseCommentsData(); // uses explicit order to match db ordering - await membersAgent - .get(`/api/comments/post/${postId}/?order=id%20ASC`) - .expectStatus(200) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .matchBodySnapshot({ - comments: [ - commentMatcherWithReplies({replies: 1}), - commentMatcher - ] - }); + await testGetComments(`/api/comments/post/${postId}/?order=id%20ASC`, [ + commentMatcherWithReplies({replies: 1}), + commentMatcher + ]); }); it('Can browse all comments of a post with default order', async function () { - await membersAgent - .get(`/api/comments/post/${postId}/`) - .expectStatus(200) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .matchBodySnapshot({ - comments: [ - commentMatcher, - commentMatcherWithReplies({replies: 1}) - ] - }); + await setupBrowseCommentsData(); + await testGetComments(`/api/comments/post/${postId}/`, [ + commentMatcher, + commentMatcherWithReplies({replies: 1}) + ]); }); it('Can reply to your own comment', async function () { // Should not update last_seen_at or last_commented_at when both are already set to a value on the same day const timezone = settingsCache.get('timezone'); const date = moment.utc(new Date()).tz(timezone).startOf('day').toDate(); - await models.Member.edit({last_seen_at: date, last_commented_at: date}, {id: member.get('id')}); + await models.Member.edit({last_seen_at: date, last_commented_at: date}, {id: loggedInMember.get('id')}); - await membersAgent - .post(`/api/comments/`) - .body({comments: [{ - post_id: postId, - parent_id: commentId, - html: 'This is a reply' - }]}) - .expectStatus(201) - .matchHeaderSnapshot({ - etag: anyEtag, - location: anyLocationFor('comments'), + const parentComment = await dbFns.addComment({ + member_id: loggedInMember.id + }); + + await testPostComment({ + post_id: postId, + parent_id: parentComment.id, + html: 'This is a reply' + }, { + matchHeaderSnapshot: { 'x-cache-invalidate': matchers.stringMatching( new RegExp('/api/members/comments/post/[0-9a-f]{24}/, /api/members/comments/[0-9a-f]{24}/replies/') ) - }) - .matchBodySnapshot({ - comments: [commentMatcher] - }); + } + }); // Check only the author got an email (because we are the author of this parent comment) mockManager.assert.sentEmailCount(1); - mockManager.assert.sentEmail({ - subject: '💬 New comment on your post: ' + postTitle, - to: fixtureManager.get('users', 0).email - }); + assertAuthorEmailSent(postAuthorEmail, postTitle); // Wait for the dispatched events (because this happens async) await DomainEvents.allSettled(); // Check last updated_at is not changed? - member = await models.Member.findOne({id: member.id}); - should.equal(member.get('last_seen_at').getTime(), date.getTime(), 'The member should not update `last_seen_at` if last seen at is same day'); + loggedInMember = await models.Member.findOne({id: loggedInMember.id}); + should.equal(loggedInMember.get('last_seen_at').getTime(), date.getTime(), 'The member should not update `last_seen_at` if last seen at is same day'); // Check last_commented_at changed? - should.equal(member.get('last_commented_at').getTime(), date.getTime(), 'The member should not update `last_commented_at` f last seen at is same day'); + should.equal(loggedInMember.get('last_commented_at').getTime(), date.getTime(), 'The member should not update `last_commented_at` f last seen at is same day'); }); it('Can reply to a comment', async function () { - await testCanReply(member); + await testCanReply(loggedInMember); }); - let testReplyId; it('Limits returned replies to 3', async function () { - const parentId = fixtureManager.get('comments', 0).id; - - // Check initial status: two replies before test - await membersAgent - .get(`/api/comments/${parentId}/`) - .expectStatus(200) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .matchBodySnapshot({ - comments: [commentMatcherWithReplies({replies: 2})] + const {parent} = await dbFns.addCommentWithReplies({ + member_id: fixtureManager.get('members', 0).id, + replies: new Array(5).fill({ + member_id: fixtureManager.get('members', 1).id }) - .expect(({body}) => { - body.comments[0].count.replies.should.eql(2); - }); - - // Add some replies - for (let index = 0; index < 3; index++) { - const {body: reply} = await membersAgent - .post(`/api/comments/`) - .body({comments: [{ - post_id: postId, - parent_id: parentId, - html: 'This is a reply ' + index - }]}) - .expectStatus(201) - .matchHeaderSnapshot({ - etag: anyEtag, - location: anyLocationFor('comments') - }) - .matchBodySnapshot({ - comments: [commentMatcher] - }); - if (index === 0) { - testReplyId = reply.comments[0].id; - } - } + }); // Check if we have count.replies = 4, and replies.length == 3 - await membersAgent - .get(`/api/comments/${parentId}/`) - .expectStatus(200) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .matchBodySnapshot({ - comments: [commentMatcherWithReplies({replies: 3})] - }) + await testGetComments(`/api/comments/${parent.get('id')}/`, [commentMatcherWithReplies({replies: 3})]) .expect(({body}) => { body.comments[0].count.replies.should.eql(5); }); @@ -470,7 +560,7 @@ describe('Comments API', function () { it('Can reply to a comment with www domain', async function () { // Test that the www. is stripped from the default configUtils.set('url', 'http://www.domain.example/'); - await testCanReply(member, {from: '"Ghost" '}); + await testCanReply(loggedInMember, {from: '"Ghost" '}); }); it('Can reply to a comment with custom support email', async function () { @@ -484,28 +574,17 @@ describe('Comments API', function () { } return getStub.wrappedMethod.call(settingsCache, key, options); }); - await testCanReply(member, {from: '"Ghost" '}); + await testCanReply(loggedInMember, {from: '"Ghost" '}); }); it('Can like a comment', async function () { - // Check not liked - await membersAgent - .get(`/api/comments/${commentId}/`) - .expectStatus(200) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .matchBodySnapshot({ - comments: new Array(1).fill(commentMatcherWithReplies({replies: 1})) - }) - .expect(({body}) => { - body.comments[0].liked.should.eql(false); - body.comments[0].count.likes.should.eql(0); - }); + const comment = await dbFns.addComment({ + member_id: fixtureManager.get('members', 2).id + }); - // Create a temporary comment + // Like the comment await membersAgent - .post(`/api/comments/${commentId}/like/`) + .post(`/api/comments/${comment.get('id')}/like/`) .expectStatus(204) .matchHeaderSnapshot({ etag: anyEtag @@ -513,15 +592,7 @@ describe('Comments API', function () { .expectEmptyBody(); // Check liked - await membersAgent - .get(`/api/comments/${commentId}/`) - .expectStatus(200) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .matchBodySnapshot({ - comments: new Array(1).fill(commentMatcherWithReplies({replies: 1})) - }) + await testGetComments(`/api/comments/${comment.get('id')}/`, [commentMatcher]) .expect(({body}) => { body.comments[0].liked.should.eql(true); body.comments[0].count.likes.should.eql(1); @@ -529,40 +600,41 @@ describe('Comments API', function () { }); it('Cannot like a comment multiple times', async function () { - // Create a temporary comment - await membersAgent - .post(`/api/comments/${commentId}/like/`) - .expectStatus(400) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .matchBodySnapshot({ - errors: [{ - id: anyUuid - }] - }); + const comment = await dbFns.addComment({ + member_id: fixtureManager.get('members', 2).id + }); + await dbFns.addLike({ + comment_id: comment.get('id'), + member_id: loggedInMember.id + }); + + // Comment was already liked above + await testBasicErrorResponse('post', `/api/comments/${comment.get('id')}/like/`, 400); }); it('Can like a reply', async function () { - // Check initial status: two replies before test + const comment = await dbFns.addComment({ + member_id: fixtureManager.get('members', 2).id + }); + const reply = await dbFns.addComment({ + member_id: fixtureManager.get('members', 1).id, + parent_id: comment.get('id') + }); + + // Like the reply await membersAgent - .post(`/api/comments/${testReplyId}/like/`) + .post(`/api/comments/${reply.get('id')}/like/`) .expectStatus(204) .matchHeaderSnapshot({ - etag: anyEtag + etag: anyEtag, + 'x-cache-invalidate': matchers.stringMatching( + new RegExp('/api/members/comments/post/[0-9a-f]{24}/, /api/members/comments/[0-9a-f]{24}/replies/') + ) }) .expectEmptyBody(); // Check liked - await membersAgent - .get(`/api/comments/${testReplyId}/`) - .expectStatus(200) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .matchBodySnapshot({ - comments: new Array(1).fill(commentMatcherWithReplies({replies: 0})) - }) + await testGetComments(`/api/comments/${reply.id}/`, [commentMatcher]) .expect(({body}) => { body.comments[0].liked.should.eql(true); body.comments[0].count.likes.should.eql(1); @@ -570,43 +642,39 @@ describe('Comments API', function () { }); it('Can return replies', async function () { - const parentId = fixtureManager.get('comments', 0).id; - - // Check initial status: two replies before test - await membersAgent - .get(`/api/comments/${parentId}/replies/`) - .expectStatus(200) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .matchBodySnapshot({ - comments: new Array(7).fill(commentMatcher) + const {parent, replies} = await dbFns.addCommentWithReplies({ + member_id: fixtureManager.get('members', 0).id, + replies: new Array(7).fill({ + member_id: fixtureManager.get('members', 1).id }) + }); + await dbFns.addLike({ + comment_id: replies[2].get('id'), + member_id: loggedInMember.id + }); + + await testGetComments(`/api/comments/${parent.get('id')}/replies/`, new Array(7).fill(commentMatcher)) .expect(({body}) => { should(body.comments[0].count.replies).be.undefined(); should(body.meta.pagination.total).eql(7); should(body.meta.pagination.next).eql(null); // Check liked + likes working for replies too - should(body.comments[2].id).eql(testReplyId); + should(body.comments[2].id).eql(replies[2].get('id')); should(body.comments[2].count.likes).eql(1); should(body.comments[2].liked).eql(true); }); }); it('Can request last page of replies', async function () { - const parentId = fixtureManager.get('comments', 0).id; - - // Check initial status: two replies before test - await membersAgent - .get(`/api/comments/${parentId}/replies/?page=3&limit=3`) - .expectStatus(200) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .matchBodySnapshot({ - comments: new Array(1).fill(commentMatcher) + const {parent} = await dbFns.addCommentWithReplies({ + member_id: fixtureManager.get('members', 0).id, + replies: new Array(7).fill({ + member_id: fixtureManager.get('members', 1).id }) + }); + + await testGetComments(`/api/comments/${parent.get('id')}/replies/?page=3&limit=3`, [commentMatcher]) .expect(({body}) => { should(body.comments[0].count.replies).be.undefined(); should(body.meta.pagination.total).eql(7); @@ -615,25 +683,19 @@ describe('Comments API', function () { }); it('Can remove a like (unlike)', async function () { + const comment = await dbFns.addComment({ + member_id: loggedInMember.id + }); + await dbFns.addLike({ + comment_id: comment.get('id'), + member_id: loggedInMember.id + }); + // Unlike - await membersAgent - .delete(`/api/comments/${commentId}/like/`) - .expectStatus(204) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .expectEmptyBody(); + await testBasicEmptyResponse('delete', `/api/comments/${comment.get('id')}/like/`, 204); // Check not liked - await membersAgent - .get(`/api/comments/${commentId}/`) - .expectStatus(200) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .matchBodySnapshot({ - comments: new Array(1).fill(commentMatcherWithReplies({replies: 1})) - }) + await testGetComments(`/api/comments/${comment.get('id')}/`, [commentMatcher]) .expect(({body}) => { body.comments[0].liked.should.eql(false); body.comments[0].count.likes.should.eql(0); @@ -641,68 +703,64 @@ describe('Comments API', function () { }); it('Cannot unlike a comment if it has not been liked', async function () { - // Remove like - await membersAgent - .delete(`/api/comments/${commentId}/like/`) - //.expectStatus(404) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .matchBodySnapshot({ - errors: [{ - id: anyErrorId - }] - }); + const comment = await dbFns.addComment({ + member_id: loggedInMember.id + }); + + await testBasicErrorResponse('delete', `/api/comments/${comment.get('id')}/like/`, 404); }); it('Can report a comment', async function () { - // Create a temporary comment - await membersAgent - .post(`/api/comments/${commentId}/report/`) - .expectStatus(204) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .expectEmptyBody(); + const comment = await dbFns.addComment({ + member_id: fixtureManager.get('members', 2).id, + html: '

        This is a message

        New line

        ' + }); + + await testBasicEmptyResponse('post', `/api/comments/${comment.get('id')}/report/`, 204); // Check report - const reports = await models.CommentReport.findAll({filter: 'comment_id:\'' + commentId + '\''}); + const reports = await models.CommentReport.findAll({filter: 'comment_id:\'' + comment.get('id') + '\''}); reports.models.length.should.eql(1); const report = reports.models[0]; - report.get('member_id').should.eql(member.id); + report.get('member_id').should.eql(loggedInMember.id); mockManager.assert.sentEmail({ subject: '🚩 A comment has been reported on your post', - to: fixtureManager.get('users', 0).email, + to: postAuthorEmail, html: new RegExp(escapeRegExp('

        This is a message

        New line

        ')), text: new RegExp(escapeRegExp('This is a message\n\nNew line')) }); }); it('Cannot report a comment twice', async function () { - // Create a temporary comment - await membersAgent - .post(`/api/comments/${commentId}/report/`) - .expectStatus(204) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .expectEmptyBody(); + const comment = await dbFns.addComment({ + member_id: fixtureManager.get('members', 2).id + }); + await dbFns.addReport({ + comment_id: comment.get('id'), + member_id: loggedInMember.id + }); + + await testBasicEmptyResponse('post', `/api/comments/${comment.get('id')}/report/`, 204); // Check report should be the same (no extra created) - const reports = await models.CommentReport.findAll({filter: 'comment_id:\'' + commentId + '\''}); + const reports = await models.CommentReport.findAll({filter: 'comment_id:\'' + comment.get('id') + '\''}); reports.models.length.should.eql(1); const report = reports.models[0]; - report.get('member_id').should.eql(member.id); + report.get('member_id').should.eql(loggedInMember.id); mockManager.assert.sentEmailCount(0); }); it('Can edit a comment on a post', async function () { + const comment = await dbFns.addComment({ + member_id: loggedInMember.id + }); + const {body} = await membersAgent - .put(`/api/comments/${commentId}`) + .put(`/api/comments/${comment.get('id')}`) .body({comments: [{ html: 'Updated comment' }]}) @@ -712,7 +770,7 @@ describe('Comments API', function () { }) .matchBodySnapshot({ comments: [{ - ...commentMatcherWithReplies({replies: 1}), + ...commentMatcher, edited_at: anyISODateTime }] }); @@ -721,9 +779,14 @@ describe('Comments API', function () { }); it('Can not edit a comment post_id', async function () { + const comment = await dbFns.addComment({ + member_id: loggedInMember.id + }); + const anotherPostId = fixtureManager.get('posts', 1).id; + await membersAgent - .put(`/api/comments/${commentId}`) + .put(`/api/comments/${comment.get('id')}`) .body({comments: [{ post_id: anotherPostId }]}); @@ -731,12 +794,16 @@ describe('Comments API', function () { const {body} = await membersAgent .get(`/api/comments/?filter=post_id:'${anotherPostId}'`); - assert(!body.comments.find(comment => comment.id === commentId), 'The comment should not have moved post'); + assert(!body.comments.find(c => c.id === comment.get('id')), 'The comment should not have moved post'); }); it('Can not edit a comment which does not belong to you', async function () { + const comment = await dbFns.addComment({ + member_id: fixtureManager.get('members', 2).id + }); + await membersAgent2 - .put(`/api/comments/${commentId}`) + .put(`/api/comments/${comment.get('id')}`) .body({comments: [{ html: 'Illegal comment update' }]}) @@ -753,9 +820,12 @@ describe('Comments API', function () { }); it('Can not edit a comment as a member who is not you', async function () { + const comment = await dbFns.addComment({ + member_id: loggedInMember.id + }); const memberId = fixtureManager.get('members', 1).id; await membersAgent - .put(`/api/comments/${commentId}`) + .put(`/api/comments/${comment.get('id')}`) .body({comments: [{ html: 'Illegal comment update', member_id: memberId @@ -764,57 +834,32 @@ describe('Comments API', function () { const { body: { comments: [ - comment + fetchedComment ] } - } = await membersAgent.get(`/api/comments/${commentId}`) - .expectStatus(200) - .matchHeaderSnapshot({ - etag: anyEtag - }) - .matchBodySnapshot({ - comments: [{ - ...commentMatcherWithReplies({replies: 1}), - edited_at: anyISODateTime - }] - }); + } = await testGetComments(`/api/comments/${comment.get('id')}`, [{ + ...commentMatcher, + edited_at: anyISODateTime + }]); - assert(comment.member.id !== memberId); + assert(fetchedComment.member.id !== memberId); }); it('Can not reply to a reply', async function () { - const { - body: { - comments: [{ - id: parentId - }] - } - } = await membersAgent - .post(`/api/comments/`) - .body({comments: [{ - post_id: postId, - html: 'Parent' - }]}); - - const { - body: { - comments: [{ - id: replyId - }] - } - } = await membersAgent - .post(`/api/comments/`) - .body({comments: [{ - post_id: postId, - parent_id: parentId, + const {replies} = await dbFns.addCommentWithReplies({ + member_id: fixtureManager.get('members', 1).id, + html: 'Parent', + replies: [{ + member_id: fixtureManager.get('members', 3).id, html: 'Reply' - }]}); + }] + }); await membersAgent .post(`/api/comments/`) .body({comments: [{ post_id: postId, - parent_id: replyId, + parent_id: replies[0].get('id'), html: 'Reply to a reply!' }]}) .expectStatus(400) @@ -830,45 +875,21 @@ describe('Comments API', function () { }); it('Can not edit a replies parent', async function () { - const { - body: { - comments: [{ - id: parentId - }] - } - } = await membersAgent - .post(`/api/comments/`) - .body({comments: [{ - post_id: postId, - html: 'Parent' - }]}); - - const { - body: { - comments: [{ - id: newParentId - }] - } - } = await membersAgent - .post(`/api/comments/`) - .body({comments: [{ - post_id: postId, - html: 'New Parent' - }]}); - - const { - body: { - comments: [{ - id: replyId - }] - } - } = await membersAgent - .post(`/api/comments/`) - .body({comments: [{ - post_id: postId, - parent_id: parentId, - html: 'Reply' - }]}); + const parentId = (await dbFns.addComment({ + member_id: loggedInMember.id, + html: 'Parent' + })).get('id'); + + const newParentId = (await dbFns.addComment({ + member_id: loggedInMember.id, + html: 'New Parent' + })).get('id'); + + const replyId = (await dbFns.addComment({ + member_id: loggedInMember.id, + parent_id: parentId, + html: 'Reply' + })).get('id'); // Attempt to edit the parent await membersAgent @@ -889,6 +910,17 @@ describe('Comments API', function () { fixtureManager.get('posts', 1).id, fixtureManager.get('posts', 2).id ]; + + for (const i of ids.keys()) { + // add i+1 comments so we have a different count for each post + for (let j = 0; j < i + 1; j++) { + await dbFns.addComment({ + post_id: ids[i], + member_id: loggedInMember.id + }); + } + } + await membersAgent .get(`api/comments/counts/?ids=${ids.join(',')}`) .expectStatus(200) @@ -899,18 +931,10 @@ describe('Comments API', function () { }); it('Can delete a comment, and it is redacted from', async function () { - const { - body: { - comments: [{ - id: commentToDeleteId - }] - } - } = await membersAgent - .post(`/api/comments/`) - .body({comments: [{ - post_id: postId, - html: 'Comment to delete' - }]}); + const commentToDeleteId = (await dbFns.addComment({ + member_id: loggedInMember.id, + html: 'Comment to delete' + })).get('id'); const { body: { @@ -972,7 +996,7 @@ describe('Comments API', function () { describe('Members with access', function () { before(async function () { await membersAgent.loginAs('paid@example.com'); - member = await models.Member.findOne({email: 'paid@example.com'}, {require: true}); + loggedInMember = await models.Member.findOne({email: 'paid@example.com'}, {require: true}); const product = await getPaidProduct(); @@ -984,15 +1008,15 @@ describe('Comments API', function () { id: product.id } ] - }, {id: member.id}); + }, {id: loggedInMember.id}); }); it('Can comment on a post', async function () { - await testCanCommentOnPost(member); + await testCanCommentOnPost(loggedInMember); }); it('Can reply to a comment', async function () { - await testCanReply(member); + await testCanReply(loggedInMember); }); }); @@ -1049,7 +1073,7 @@ describe('Comments API', function () { describe('Members with access', function () { before(async function () { await membersAgent.loginAs('member-premium@example.com'); - member = await models.Member.findOne({email: 'member-premium@example.com'}, {require: true}); + loggedInMember = await models.Member.findOne({email: 'member-premium@example.com'}, {require: true}); // Attach comped subscription to this member await models.Member.edit({ @@ -1059,15 +1083,15 @@ describe('Comments API', function () { id: product.id } ] - }, {id: member.id}); + }, {id: loggedInMember.id}); }); it('Can comment on a post', async function () { - await testCanCommentOnPost(member); + await testCanCommentOnPost(loggedInMember); }); it('Can reply to a comment', async function () { - await testCanReply(member); + await testCanReply(loggedInMember); }); }); diff --git a/ghost/core/test/e2e-browser/admin/publishing.spec.js b/ghost/core/test/e2e-browser/admin/publishing.spec.js index cd7c482d890..2bd54057dec 100644 --- a/ghost/core/test/e2e-browser/admin/publishing.spec.js +++ b/ghost/core/test/e2e-browser/admin/publishing.spec.js @@ -312,6 +312,7 @@ test.describe('Publishing', () => { const date = DateTime.now(); await createPostDraft(adminPage, {title: 'Testing publish update', body: 'This is the initial published text.'}); + const editorUrl = await adminPage.url(); await publishPost(adminPage); const frontendPage = await openPublishedPostBookmark(adminPage); await closePublishFlow(adminPage); @@ -323,7 +324,7 @@ test.describe('Publishing', () => { await expect(publishedHeader).toContainText(date.toFormat('LLL d, yyyy')); // add some extra text to the post - await adminPage.locator('li[data-test-post-id]').first().click(); + await adminPage.goto(editorUrl); await adminPage.locator('[data-kg="editor"]').first().click(); await adminPage.waitForTimeout(500); await adminPage.keyboard.type(' This is some updated text.'); @@ -455,6 +456,8 @@ test.describe('Publishing', () => { await sharedPage.goto('/ghost'); await createPostDraft(sharedPage, postData); + const editorUrl = await sharedPage.url(); + // Schedule far in the future await publishPost(sharedPage, {date: '2050-01-01', time: '10:09'}); await closePublishFlow(sharedPage); @@ -469,7 +472,7 @@ test.describe('Publishing', () => { await checkPostNotPublished(testsharedPage, postData); // Now unschedule this post - await sharedPage.locator('li[data-test-post-id]').first().click(); + await sharedPage.goto(editorUrl); await sharedPage.locator('[data-test-button="update-flow"]').first().click(); await sharedPage.locator('[data-test-button="revert-to-draft"]').click(); diff --git a/ghost/core/test/e2e-server/click-tracking.test.js b/ghost/core/test/e2e-server/click-tracking.test.js index 49b08b8e52e..7df8e836a64 100644 --- a/ghost/core/test/e2e-server/click-tracking.test.js +++ b/ghost/core/test/e2e-server/click-tracking.test.js @@ -8,11 +8,11 @@ const membersEventsService = require('../../core/server/services/members-events' describe('Click Tracking', function () { let agent; + let ghostServer; let webhookMockReceiver; before(async function () { - const {adminAgent} = await agentProvider.getAgentsWithFrontend(); - agent = adminAgent; + ({adminAgent: agent, ghostServer} = await agentProvider.getAgentsWithFrontend()); await fixtureManager.init('newsletters', 'members:newsletters', 'integrations'); await agent.loginAsOwner(); }); @@ -28,6 +28,10 @@ describe('Click Tracking', function () { mockManager.restore(); }); + after(async function () { + await ghostServer.stop(); + }); + it('Full test', async function () { const siteUrl = new URL(urlUtils.urlFor('home', true)); diff --git a/ghost/core/test/e2e-server/metrics-server/metrics-server.test.js b/ghost/core/test/e2e-server/metrics-server/metrics-server.test.js new file mode 100644 index 00000000000..de1823a349c --- /dev/null +++ b/ghost/core/test/e2e-server/metrics-server/metrics-server.test.js @@ -0,0 +1,121 @@ +const assert = require('node:assert/strict'); +const testUtils = require('../../utils'); +const request = require('supertest'); +const parsePrometheusTextFormat = require('parse-prometheus-text-format'); +const configUtils = require('../../utils/configUtils'); + +describe('Metrics Server', function () { + before(function () { + configUtils.set('metrics_server:enabled', true); + }); + + it('should start up when Ghost boots and stop when Ghost stops', async function () { + // Ensure the metrics server is running after Ghost boots + await testUtils.startGhost({forceStart: true}); + await request('http://127.0.0.1:9417').get('/metrics').expect(200); + + // Stop Ghost and ensure the metrics server is no longer running + await testUtils.stopGhost(); + + // Requesting the metrics endpoint should throw an error + let error; + try { + await request('http://127.0.0.1:9417').get('/metrics'); + } catch (err) { + error = err; + } + assert.ok(error); + }); + + it('should not start if enabled is false', async function () { + configUtils.set('metrics_server:enabled', false); + await testUtils.startGhost({forceStart: true}); + // Requesting the metrics endpoint should throw an error + let error; + try { + await request('http://127.0.0.1:9417').get('/metrics'); + } catch (err) { + error = err; + } + assert.ok(error); + await testUtils.stopGhost(); + }); + + describe('metrics and format', function () { + let metricsResponse; + let metricsText; + before(async function () { + configUtils.set('metrics_server:enabled', true); + await testUtils.startGhost({forceStart: true}); + metricsResponse = await request('http://127.0.0.1:9417').get('/metrics'); + metricsText = metricsResponse.text; + await testUtils.stopGhost(); + }); + it('should export the metrics in the right format', async function () { + const metricsJson = parsePrometheusTextFormat(metricsText); + assert.ok(metricsJson); + }); + + it('should use the right prefix for all metrics', async function () { + const metricsJson = parsePrometheusTextFormat(metricsText); + const metricNames = metricsJson.map(metric => metric.name); + metricNames.forEach((metricName) => { + assert.match(metricName, /^ghost_/); + }); + }); + + it('should have help text for all metrics', async function () { + const metricsJson = parsePrometheusTextFormat(metricsText); + metricsJson.forEach((metric) => { + assert.ok(metric.help); + }); + }); + + it('should have type for all metrics', async function () { + const metricsJson = parsePrometheusTextFormat(metricsText); + metricsJson.forEach((metric) => { + assert.ok(metric.type); + }); + }); + + it('should have all the right metrics', async function () { + // Ensures we have all the metrics we expect exported + // This could be a snapshot test in the future, but for now just check the names of the metrics + // Add new metrics to this list as they are added + const expectedMetrics = [ + 'ghost_process_cpu_user_seconds_total', + 'ghost_process_cpu_system_seconds_total', + 'ghost_process_cpu_seconds_total', + 'ghost_process_start_time_seconds', + 'ghost_process_resident_memory_bytes', + 'ghost_nodejs_eventloop_lag_seconds', + 'ghost_nodejs_eventloop_lag_min_seconds', + 'ghost_nodejs_eventloop_lag_max_seconds', + 'ghost_nodejs_eventloop_lag_mean_seconds', + 'ghost_nodejs_eventloop_lag_stddev_seconds', + 'ghost_nodejs_eventloop_lag_p50_seconds', + 'ghost_nodejs_eventloop_lag_p90_seconds', + 'ghost_nodejs_eventloop_lag_p99_seconds', + 'ghost_nodejs_active_resources', + 'ghost_nodejs_active_resources_total', + 'ghost_nodejs_active_handles', + 'ghost_nodejs_active_handles_total', + 'ghost_nodejs_active_requests', + 'ghost_nodejs_active_requests_total', + 'ghost_nodejs_heap_size_total_bytes', + 'ghost_nodejs_heap_size_used_bytes', + 'ghost_nodejs_external_memory_bytes', + 'ghost_nodejs_heap_space_size_total_bytes', + 'ghost_nodejs_heap_space_size_used_bytes', + 'ghost_nodejs_heap_space_size_available_bytes', + 'ghost_nodejs_version_info', + 'ghost_nodejs_gc_duration_seconds' + ]; + const metricsJson = parsePrometheusTextFormat(metricsText); + const metricNames = metricsJson.map(metric => metric.name); + for (const metricName of expectedMetrics) { + assert.ok(metricNames.includes(metricName)); + } + }); + }); +}); \ No newline at end of file diff --git a/ghost/core/test/unit/frontend/helpers/__snapshots__/ghost_head.test.js.snap b/ghost/core/test/unit/frontend/helpers/__snapshots__/ghost_head.test.js.snap index b9bd03697e3..fe165a6472f 100644 --- a/ghost/core/test/unit/frontend/helpers/__snapshots__/ghost_head.test.js.snap +++ b/ghost/core/test/unit/frontend/helpers/__snapshots__/ghost_head.test.js.snap @@ -131,7 +131,7 @@ Object { .gh-post-upgrade-cta a.gh-btn:hover { opacity: 0.92; } - + ", @@ -275,7 +275,7 @@ Object { - + ", } @@ -286,7 +286,7 @@ Object { "rendered": " - + @@ -299,7 +299,7 @@ Object { "rendered": " - + ", } @@ -373,7 +373,7 @@ Object { - + ", } @@ -446,7 +446,7 @@ Object { - + ", } @@ -839,7 +839,7 @@ Object { .gh-post-upgrade-cta a.gh-btn:hover { opacity: 0.92; } - + ", @@ -953,7 +953,7 @@ Object { .gh-post-upgrade-cta a.gh-btn:hover { opacity: 0.92; } - + ", } @@ -1022,7 +1022,7 @@ Object { - + ", @@ -1092,7 +1092,7 @@ Object { - + ", @@ -1143,7 +1143,7 @@ Object { - + ", @@ -1194,7 +1194,7 @@ Object { - + ", @@ -1308,7 +1308,7 @@ Object { .gh-post-upgrade-cta a.gh-btn:hover { opacity: 0.92; } - + ", @@ -1422,7 +1422,7 @@ Object { .gh-post-upgrade-cta a.gh-btn:hover { opacity: 0.92; } - + ", @@ -1473,7 +1473,7 @@ Object { - + ", } @@ -1586,7 +1586,7 @@ Object { .gh-post-upgrade-cta a.gh-btn:hover { opacity: 0.92; } - + ", @@ -1637,7 +1637,7 @@ Object { - + ", } @@ -1651,7 +1651,7 @@ Object { - + ", } @@ -1665,7 +1665,7 @@ Object { - + ", @@ -1680,7 +1680,7 @@ Object { - + ", @@ -1695,7 +1695,7 @@ Object { - + @@ -1711,7 +1711,7 @@ Object { - + ", @@ -1796,7 +1796,7 @@ Object { - + ", } @@ -1812,7 +1812,7 @@ Object { - + ", } @@ -1838,7 +1838,7 @@ Object { - + ", } @@ -1888,7 +1888,7 @@ Object { - + ", } @@ -1901,7 +1901,7 @@ Object { - + ", } @@ -1914,7 +1914,7 @@ Object { - + ", } @@ -1974,7 +1974,7 @@ Object { - + ", @@ -2025,7 +2025,7 @@ Object { - + ", @@ -2048,7 +2048,7 @@ Object { - + ", } @@ -2113,7 +2113,7 @@ Object { - + ", } @@ -2163,7 +2163,7 @@ Object { - + ", } @@ -2185,7 +2185,7 @@ Object { - + ", } @@ -2198,7 +2198,7 @@ Object { - + ", } @@ -2213,7 +2213,7 @@ Object { - + ", } @@ -2228,7 +2228,7 @@ Object { - + ", } @@ -2280,7 +2280,7 @@ Object { - + ", } @@ -2330,7 +2330,7 @@ Object { - + ", } @@ -2411,7 +2411,7 @@ Object { - + ", } @@ -2537,7 +2537,7 @@ Object { - + ", } @@ -2618,7 +2618,7 @@ Object { - + ", } @@ -2699,7 +2699,7 @@ Object { - + ", } @@ -2768,7 +2768,7 @@ Object { - + ", } @@ -2841,7 +2841,7 @@ Object { - + ", } @@ -2914,7 +2914,7 @@ Object { - + ", } @@ -2987,7 +2987,7 @@ Object { - + ", } @@ -3061,7 +3061,7 @@ Object { - + ", } @@ -3127,7 +3127,7 @@ Object { - + ", } @@ -3175,7 +3175,7 @@ Object { - + ", } @@ -3227,7 +3227,7 @@ Object { - + ", } diff --git a/ghost/core/test/unit/frontend/helpers/content_api_key.test.js b/ghost/core/test/unit/frontend/helpers/content_api_key.test.js new file mode 100644 index 00000000000..cc4862c7fcb --- /dev/null +++ b/ghost/core/test/unit/frontend/helpers/content_api_key.test.js @@ -0,0 +1,19 @@ +/* eslint-disable no-regex-spaces */ +const proxy = require('../../../../core/frontend/services/proxy'); +const {getFrontendKey} = proxy; +const should = require('should'); + +// Stuff we are testing +const content_api_key = require('../../../../core/frontend/helpers/content_api_key'); + +describe('{{content_api_key}} helper', function () { + describe('compare to settings', function () { + it('returns the content API key', async function () { + const result = await content_api_key(); + const expected = await getFrontendKey(); + should.exist(result); + String(result).should.equal(expected); + }); + }); +}); + diff --git a/ghost/core/test/unit/frontend/services/theme-engine/handlebars/helpers.test.js b/ghost/core/test/unit/frontend/services/theme-engine/handlebars/helpers.test.js index 5cb6dc1d800..d0d7be1019c 100644 --- a/ghost/core/test/unit/frontend/services/theme-engine/handlebars/helpers.test.js +++ b/ghost/core/test/unit/frontend/services/theme-engine/handlebars/helpers.test.js @@ -8,7 +8,7 @@ const helpers = require('../../../../../../core/frontend/services/helpers'); describe('Helpers', function () { const hbsHelpers = ['each', 'if', 'unless', 'with', 'helperMissing', 'blockHelperMissing', 'log', 'lookup', 'block', 'contentFor']; const ghostHelpers = [ - 'asset', 'authors', 'body_class', 'cancel_link', 'concat', 'content', 'date', 'encode', 'excerpt', 'facebook_url', 'foreach', 'get', + 'asset', 'authors', 'body_class', 'cancel_link', 'concat', 'content', 'content_api_key', 'date', 'encode', 'excerpt', 'facebook_url', 'foreach', 'get', 'ghost_foot', 'ghost_head', 'has', 'img_url', 'is', 'link', 'link_class', 'meta_description', 'meta_title', 'navigation', 'next_post', 'page_url', 'pagination', 'plural', 'post_class', 'prev_post', 'price', 'raw', 'reading_time', 't', 'tags', 'title','total_members', 'total_paid_members', 'twitter_url', 'url', 'comment_count', 'collection', 'recommendations', 'readable_url' diff --git a/ghost/core/test/unit/shared/instrumentation.test.js b/ghost/core/test/unit/shared/instrumentation.test.js deleted file mode 100644 index 794c392f9a2..00000000000 --- a/ghost/core/test/unit/shared/instrumentation.test.js +++ /dev/null @@ -1,23 +0,0 @@ -const assert = require('assert/strict'); -const sinon = require('sinon'); - -describe('UNIT: instrumentation', function () { - it('should initialize OpenTelemetry if configured', async function () { - const config = { - get: sinon.stub().returns(true) - }; - const instrumentation = require('../../../core/shared/instrumentation'); - const result = await instrumentation.initOpenTelemetry({config}); - assert.equal(result, true); - }); - - it('should not initialize OpenTelemetry if not configured', async function () { - const config = { - get: sinon.stub().returns(false) - }; - const instrumentation = require('../../../core/shared/instrumentation'); - - const result = await instrumentation.initOpenTelemetry({config}); - assert.equal(result, false); - }); -}); \ No newline at end of file diff --git a/ghost/core/test/unit/shared/prometheus-client.test.js b/ghost/core/test/unit/shared/prometheus-client.test.js new file mode 100644 index 00000000000..51daaf24be4 --- /dev/null +++ b/ghost/core/test/unit/shared/prometheus-client.test.js @@ -0,0 +1,49 @@ +const assert = require('node:assert/strict'); +const prometheusClient = require('../../../core/shared/prometheus-client'); +const sinon = require('sinon'); + +describe('PrometheusClient', function () { + describe('getMetrics', function () { + it('should return metrics', async function () { + const metrics = await prometheusClient.getMetrics(); + assert.match(metrics, /^# HELP/); + }); + }); + + describe('getContentType', function () { + it('should return the content type', function () { + assert.equal(prometheusClient.getContentType(), 'text/plain; version=0.0.4; charset=utf-8'); + }); + }); + + describe('getRegistry', function () { + it('should return the registry', function () { + assert.ok(prometheusClient.getRegistry()); + }); + }); + + describe('handleMetricsRequest', function () { + it('should return metrics', async function () { + const req = {}; + const res = { + set: sinon.stub(), + end: sinon.stub() + }; + await prometheusClient.handleMetricsRequest(req, res); + assert.ok(res.set.calledWith('Content-Type', prometheusClient.getContentType())); + assert.ok(res.end.calledOnce); + }); + + it('should return an error if getting metrics fails', async function () { + sinon.stub(prometheusClient, 'getMetrics').throws(new Error('Failed to get metrics')); + const req = {}; + const res = { + set: sinon.stub(), + end: sinon.stub(), + status: sinon.stub().returnsThis() + }; + await prometheusClient.handleMetricsRequest(req, res); + assert.ok(res.status.calledWith(500)); + }); + }); +}); diff --git a/ghost/email-analytics-service/test/email-analytics-service.test.js b/ghost/email-analytics-service/test/email-analytics-service.test.js index 66a42237787..404ab48ce01 100644 --- a/ghost/email-analytics-service/test/email-analytics-service.test.js +++ b/ghost/email-analytics-service/test/email-analytics-service.test.js @@ -10,6 +10,16 @@ const { const EventProcessingResult = require('../lib/EventProcessingResult'); describe('EmailAnalyticsService', function () { + let clock; + + beforeEach(function () { + clock = sinon.useFakeTimers(new Date(2024, 0, 1)); + }); + + afterEach(function () { + clock.restore(); + }); + describe('getStatus', function () { it('returns status object', function () { // these are null because we're not running them before calling this diff --git a/ghost/email-service/package.json b/ghost/email-service/package.json index 35cc32b083d..f4e57832482 100644 --- a/ghost/email-service/package.json +++ b/ghost/email-service/package.json @@ -19,7 +19,7 @@ ], "devDependencies": { "c8": "8.0.1", - "html-validate": "8.23.0", + "html-validate": "8.24.1", "mocha": "10.2.0", "should": "13.2.3", "sinon": "15.2.0" diff --git a/ghost/i18n/lib/i18n.js b/ghost/i18n/lib/i18n.js index 42aa92e6850..baedae519cf 100644 --- a/ghost/i18n/lib/i18n.js +++ b/ghost/i18n/lib/i18n.js @@ -2,19 +2,25 @@ const i18next = require('i18next'); const SUPPORTED_LOCALES = [ 'af', // Afrikaans + 'ar', // Arabic 'bg', // Bulgarian + 'bn', // Bengali 'bs', // Bosnian 'ca', // Catalan 'cs', // Czech 'da', // Danish 'de', // German + 'de-CH', // Swiss German + 'el', // Greek 'en', // English 'eo', // Esperanto 'es', // Spanish + 'et', // Estonian 'fa', // Persian/Farsi 'fi', // Finnish 'fr', // French 'gd', // Gaelic (Scottish) + 'hi', // Hindi 'hr', // Croatian 'hu', // Hungarian 'id', // Indonesian @@ -22,7 +28,9 @@ const SUPPORTED_LOCALES = [ 'it', // Italian 'ja', // Japanese 'ko', // Korean + 'kz', // Kazach 'lt', // Lithuanian + 'mk', // Macedonian 'mn', // Mongolian 'ms', // Malay 'nl', // Dutch @@ -38,18 +46,22 @@ const SUPPORTED_LOCALES = [ 'sl', // Slovenian 'sq', // Albanian 'sr', // Serbian + 'sr-Cyrl', // Serbian (Cyrillic) 'sv', // Swedish + 'th', // Thai 'tr', // Turkish 'uk', // Ukrainian + 'ur', // Urdu 'uz', // Uzbek 'vi', // Vietnamese 'zh', // Chinese - 'zh-Hant' // Traditional Chinese + 'zh-Hant', // Traditional Chinese + 'sw' // Swahili ]; /** * @param {string} [lng] - * @param {'ghost'|'portal'|'test'|'signup-form'|'comments'} ns + * @param {'ghost'|'portal'|'test'|'signup-form'|'comments'|'search'} ns */ module.exports = (lng = 'en', ns = 'portal') => { const i18nextInstance = i18next.createInstance(); diff --git a/ghost/i18n/locales/af/comments.json b/ghost/i18n/locales/af/comments.json index 63d1dab5b66..3e5bac02338 100644 --- a/ghost/i18n/locales/af/comments.json +++ b/ghost/i18n/locales/af/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Antwoord op kommentaar", "Report": "Rapporteer", "Report comment": "Rapporteer kommentaar", - "Report this comment": "Rapporteer hierdie kommentaar", "Report this comment?": "Rapporteer hierdie kommentaar?", "Save": "Stoor", "Sending": "Besig om te stuur", @@ -68,6 +67,5 @@ "This comment has been removed.": "Hierdie kommentaar is verwyder.", "Upgrade now": "Gradeer nou op", "Yesterday": "Gister", - "You want to report this comment?": "Wil jy hierdie kommentaar rapporteer?", "Your request will be sent to the owner of this site.": "Jou versoek sal aan die eienaar van hierdie webwerf gestuur word." } diff --git a/ghost/i18n/locales/af/portal.json b/ghost/i18n/locales/af/portal.json index 3df64523a1b..516f5604032 100644 --- a/ghost/i18n/locales/af/portal.json +++ b/ghost/i18n/locales/af/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "'n Aanmeldskakel is na u epos gestuur. As dit nie binne 3 minute aankom nie, moet u asseblief u spam-vouer nagaan.", "Account": "Rekening", + "Account details updated successfully": "", "Account settings": "Rekening instellings", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Na afloop van die gratis proeftydperk sal u die vasgestelde pry vir die vlak wat u gekies het, betaal. U kan altyd voor die tyd kanselleer.", "Already a member?": "Is u reeds 'n lid?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "'n Onverwagte fout het voorgekom. Probeer asseblief weer of kontak kliëntediens as die fout voortduur.", "Back": "Terug", "Back to Log in": "Terug na aanmelding", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Kyk in spam & promosie vouers", "Check with your mail provider": "Kyk by u e-posverskaffer", + "Check your inbox to verify email update": "", "Choose": "Kies", "Choose a different plan": "Kies 'n ander plan", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Kontak kliëntediens", "Continue": "Gaan voort", "Continue subscription": "Gaan voort met inskrywing", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Kon nie aanmeld nie. Aanmeldskakel het verval.", "Could not update email! Invalid link.": "Kon nie e-pos opdateer nie! Ongeldige skakel.", "Create a new contact": "Skep 'n nuwe kontak", @@ -52,6 +56,7 @@ "Edit": "Wysig", "Email": "E-pos", "Email newsletter": "Epos nuusbrief", + "Email newsletter settings updated": "", "Email preferences": "E-pos instellings", "Emails": "E-posse", "Emails disabled": "E-posse afgeskakel", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Fout", "Expires {{expiryDate}}": "Verval {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Verewig", "Free Trial – Ends {{trialEnd}}": "Gratis proeftydperk – Eindig {{trialEnd}}", "Get help": "Kry hulp", @@ -91,6 +108,8 @@ "Name": "Naam", "Need more help? Contact support": "Benodig meer hulp? Kontak kliëntediens", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Nuusbriewe kan op u rekening afgeskakel word vir twee redes: 'n Vorige e-pos is as spam gemerk, of 'n poging om 'n e-pos te stuur het tot 'n permanente fout gelei (bounce).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Ontvang u nie e-posse nie?", "Now check your email!": "Kyk nou u e-pos!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "As u weer ingeskryf is, en u sien steeds nie e-posse in u posbus nie, kontroleer u spam vouer. Sommige posbusverskaffers hou 'n rekord van vorige spam klagtes en sal steeds e-posse merk. As dit gebeur, merk die nuutste nuusbrief as 'Not spam' om dit terug te skuif na u primêre posbus.", @@ -126,6 +145,7 @@ "Submit feedback": "Stuur terugvoering", "Subscribe": "Teken in", "Subscribed": "Ingeteken", + "Subscription plan updated successfully": "", "Success": "Sukses", "Success! Check your email for magic link to sign-in.": "Sukses! Kyk in jou e-pos vir 'n magiese skakel om aan te meld.", "Success! Your account is fully activated, you now have access to all content.": "Sukses! U rekening is ten volle geaktiveer, u het nou toegang tot alle inhoud.", @@ -138,12 +158,22 @@ "That didn't go to plan": "Dit het nie volgens plan verloop nie", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Die epos addres wat ons vir u het is {{memberEmail}} — as dit nie korrek is nie, kan u dit opdateer in u .", "There was a problem submitting your feedback. Please try again a little later.": "Daar was 'n probleem om u terugvoering in te dien. Probeer asseblief later weer.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Hierdie webwerf is slegs op uitnodiging, kontak die eienaar vir toegang.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Om die registrasie te voltooi, kliek op die bevestigingskakel in jou inboks. As dit nie binne 3 minute aankom nie, kontroleer asseblief jou spam-vouer!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Probeer gratis vir {{amount}} dae, dan {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Ontsluit toegang tot alle nuusbriewe deur 'n betaalde intekenaar te word.", "Unsubscribe from all emails": "Meld af van alle e-posse", "Unsubscribed": "Afgemeld", @@ -170,6 +200,7 @@ "You've successfully signed in.": "U het suksesvol aangemeld.", "You've successfully subscribed to": "Jy het suksesvol ingeteken op", "Your account": "U rekening", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "U insette help om te bepaal wat gepubliseer word.", "Your subscription will expire on {{expiryDate}}": "U intekening sal verval op {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "U intekening sal hernu op {{renewalDate}}", diff --git a/ghost/i18n/locales/af/search.json b/ghost/i18n/locales/af/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/af/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/ar/comments.json b/ghost/i18n/locales/ar/comments.json new file mode 100644 index 00000000000..77ef1db9693 --- /dev/null +++ b/ghost/i18n/locales/ar/comments.json @@ -0,0 +1,71 @@ +{ + "{{amount}} characters left": "", + "{{amount}} comments": "", + "{{amount}} days ago": "", + "{{amount}} hrs ago": "", + "{{amount}} mins ago": "", + "{{amount}} months ago": "", + "{{amount}} more": "", + "{{amount}} seconds ago": "", + "{{amount}} weeks ago": "", + "{{amount}} years ago": "", + "1 comment": "", + "Add comment": "", + "Add context to your comment, share your name and expertise to foster a healthy discussion.": "", + "Add reply": "", + "Already a member?": "", + "Anonymous": "", + "Become a member of {{publication}} to start commenting.": "", + "Become a paid member of {{publication}} to start commenting.": "", + "Cancel": "", + "Comment": "", + "Complete your profile": "", + "Delete": "", + "Deleted member": "", + "Discussion": "", + "Edit": "", + "Edit this comment": "", + "edited": "", + "Enter your name": "", + "Expertise": "", + "Founder @ Acme Inc": "", + "Full-time parent": "", + "Head of Marketing at Acme, Inc": "", + "Hide": "", + "Hide comment": "", + "Jamie Larson": "", + "Join the discussion": "", + "Just now": "", + "Local resident": "", + "Member discussion": "", + "Name": "", + "Neurosurgeon": "", + "One day ago": "", + "One hour ago": "", + "One min ago": "", + "One month ago": "", + "One week ago": "", + "One year ago": "", + "Reply": "", + "Reply to comment": "", + "Report": "", + "Report comment": "", + "Report this comment?": "", + "Save": "", + "Sending": "", + "Sent": "", + "Show": "", + "Show {{amount}} more replies": "", + "Show {{amount}} previous comments": "", + "Show 1 more reply": "", + "Show 1 previous comment": "", + "Show comment": "", + "Sign in": "", + "Sign up now": "", + "Start the conversation": "", + "This comment has been hidden.": "", + "This comment has been removed.": "", + "Upgrade now": "", + "Yesterday": "", + "Your request will be sent to the owner of this site.": "" +} diff --git a/ghost/i18n/locales/ar/ghost.json b/ghost/i18n/locales/ar/ghost.json index e4c9ec14b41..2a531eff76e 100644 --- a/ghost/i18n/locales/ar/ghost.json +++ b/ghost/i18n/locales/ar/ghost.json @@ -1,20 +1,27 @@ { "All the best!": "مع خالص التقدير!", + "Complete signup for {{siteTitle}}!": "", "Complete your sign up to {{siteTitle}}!": "أكمل تسجيلك في {{siteTitle}}!", + "Confirm email address": "", + "Confirm signup": "", + "Confirm your email address": "", "Confirm your email update for {{siteTitle}}!": "تأكيد تحديث بريدك الاكتروني لـ {{siteTitle}}!", "Confirm your subscription to {{siteTitle}}": "تأكيد اشتراكك في {{siteTitle}}!", "For your security, the link will expire in 24 hours time.": "لدواعي الحماية، سيتنتهي فعالية الرابط خلال اربع وعشرون ساعة.", "Hey there,": "مرحبا بك،", + "Hey there!": "", "If you did not make this request, you can safely ignore this email.": "إذا لم تقم بهذا الطلب، يمكنك تجاهل هذه الرسالة بكل امان.", "If you did not make this request, you can simply delete this message.": "إذا لم تقم بهذا الطلب، يمكنك ببساطة حذف هذه الرسالة.", "Please confirm your email address with this link:": "برجاء تأكيد بريدك الاكتروني من خلال الرابط:", "Secure sign in link for {{siteTitle}}": "رابط تسجيل الدخول الامن الى {{siteTitle}}", "See you soon!": "نراكم قريبا!", "Sent to {{email}}": "تم الارسال الى {{email}}", + "Sign in": "", "Sign in to {{siteTitle}}": "تسجيل الدخول الى {{siteTitle}}", "Tap the link below to complete the signup process for {{siteTitle}}, and be automatically signed in:": "انقر الرابط ادناه لاكمال عملية التسجيل في {{siteTitle}}, والدخول مباشرة:", "Thank you for signing up to {{siteTitle}}!": "شكرا لتسجيلك في {{siteTitle}}!", "Thank you for subscribing to {{siteTitle}}!": "شكرا لاشتراكك في {{siteTitle}}!", + "Thank you for subscribing to {{siteTitle}}.": "", "Thank you for subscribing to {{siteTitle}}. Tap the link below to be automatically signed in:": "شكرا لاشتراكك في {{siteTitle}}. انقر الرابط ادناه لتسجيل الدخول:", "This email address will not be used.": "هذا البريد الاكتروني لن يتم استخدامه.", "Welcome back to {{siteTitle}}!": "مرحبا بك مرة اخرى في {{siteTitle}}!", @@ -22,5 +29,6 @@ "You can also copy & paste this URL into your browser:": "بامكانك ايضا نسخ ولصق هذا الرابط في متصفحك:", "You will not be signed up, and no account will be created for you.": "لم يتم تسجيلك او لم بتم انشاء حساب لك.", "You will not be subscribed.": "لم يتم اشتراكك.", - "You're one tap away from subscribing to {{siteTitle}} — please confirm your email address with this link:": "أنت على بعد خطوة من الاشتراك في {{siteTitle}} - برجاء تأكيد البريد الاكتروني من خلال هذا الرابط:" + "You're one tap away from subscribing to {{siteTitle}} — please confirm your email address with this link:": "أنت على بعد خطوة من الاشتراك في {{siteTitle}} - برجاء تأكيد البريد الاكتروني من خلال هذا الرابط:", + "You're one tap away from subscribing to {{siteTitle}}!": "" } diff --git a/ghost/i18n/locales/ar/portal.json b/ghost/i18n/locales/ar/portal.json index 77a63929f78..90281f79958 100644 --- a/ghost/i18n/locales/ar/portal.json +++ b/ghost/i18n/locales/ar/portal.json @@ -1,60 +1,208 @@ { + "(save {{highestYearlyDiscount}}%)": "", + "{{amount}} days free": "", + "{{amount}} off": "", + "{{amount}} off for first {{number}} months.": "", + "{{amount}} off for first {{period}}.": "", + "{{amount}} off forever.": "", "{{discount}}% discount": "{{discount}}% خصم", + "{{memberEmail}} will no longer receive {{newsletterName}} newsletter.": "", + "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "", + "{{memberEmail}} will no longer receive this newsletter.": "", "{{trialDays}} days free": "{{trialDays}} أيام مجانية", + "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "رابط الدخول تم ارساله الى بريدك الاكتروني. اذا لم تتلقى الرسالة خلال 3 دقائق، برجاء التأكد من مجلد المهملات.", "Account": "حسابي", + "Account details updated successfully": "", "Account settings": "اعدادات الحساب", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "بعد انتهاء المدة التجريبية، سيتم خصم السعر العادي للباقة التي اخترتها. بامكانك الغاء الاشتراك قبل ذلك.", "Already a member?": "هل انت عضو؟", + "An error occurred": "", + "An unexpected error occured. Please try again or contact support if the error persists.": "", "Back": "الرجوع", "Back to Log in": "الرجوع الى تسجيل الدخول", + "Billing info": "", + "Black Friday": "", + "Cancel anytime.": "", "Cancel subscription": "إلغاء الاشتراك", "Cancellation reason": "سبب الإلغاء", + "Change": "", + "Change plan": "", + "Check spam & promotions folders": "", + "Check with your mail provider": "", + "Check your inbox to verify email update": "", + "Choose": "", "Choose a different plan": "اختر اشتراك مختلف", + "Choose a plan": "", "Choose your newsletters": "اختر مجموعة بريدية مفضلة", + "Click here to retry": "", "Close": "إغلاق", "Comments": "تعليق", + "Complimentary": "", "Confirm": "تأكيد", + "Confirm cancellation": "", + "Confirm subscription": "", + "Contact support": "", "Continue": "استمرار", + "Continue subscription": "", + "Could not create stripe checkout session": "", + "Could not sign in. Login link expired.": "", + "Could not update email! Invalid link.": "", + "Create a new contact": "", + "Current plan": "", "Delete account": "حذف الحساب", + "Didn't mean to do this? Manage your preferences .": "", "Don't have an account?": "هل لديك حساب؟", + "Edit": "", "Email": "بريد الكترني", - "Email preference updated.": "تم تحديث تفضيلات البريد الاكتروني", + "Email newsletter": "", + "Email newsletter settings updated": "", "Email preferences": "تفضيلات", "Emails": "البريد الاكتروني", "Emails disabled": "تم تعطيل جميع بريد الاكترونية", + "Ends {{offerEndDate}}": "", + "Enter your email address": "", + "Enter your name": "", + "Error": "", + "Expires {{expiryDate}}": "", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", + "Forever": "", + "Free Trial – Ends {{trialEnd}}": "", "Get help": "المساعدة", + "Get in touch for help": "", "Get notified when someone replies to your comment": "استلم اشعار في حال تم الرد على تعليقك", "Give feedback on this post": "شارك رأيك في هذا المنشور", + "Help! I'm not receiving emails": "", + "Here are a few other sites you may enjoy.": "", + "If a newsletter is flagged as spam, emails are automatically disabled for that address to make sure you no longer receive any unwanted messages.": "", + "If the spam complaint was accidental, or you would like to begin receiving emails again, you can resubscribe to emails by clicking the button on the previous screen.": "", + "If you cancel your subscription now, you will continue to have access until {{periodEnd}}.": "", + "If you have a corporate or government email account, reach out to your IT department and ask them to allow emails to be received from {{senderEmail}}": "", + "If you would like to start receiving emails again, the best next steps are to check your email address on file for any issues and then click resubscribe on the previous screen.": "", + "If you're not receiving the email newsletter you've subscribed to, here are a few things to check.": "", + "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "", + "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "", + "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "", + "Invalid email address": "", + "Jamie Larson": "", + "jamie@example.com": "", "Less like this": "أقل من هذا", + "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "", "Manage": "إدارة", + "Maybe later": "", + "Memberships unavailable, contact the owner for access.": "", + "month": "", "Monthly": "شهري", "More like this": "أكثر من هذا", "Name": "الاسم", + "Need more help? Contact support": "", + "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "لم تستلم رسالة بريدية؟", "Now check your email!": "الان تأكد من بريدك الاكتروني!", + "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "", + "Permanent failure (bounce)": "", + "Phone number": "", + "Plan": "", + "Plan checkout was cancelled.": "", + "Plan upgrade was cancelled.": "", + "Please contact {{supportAddress}} to adjust your complimentary subscription.": "", + "Please enter {{fieldName}}": "", + "Please fill in required fields": "", "Price": "السعر", "Re-enable emails": "اعد تفعيل رسال البريد", + "Recommendations": "", + "Renews at {{price}}.": "", "Retry": "اعد المحاولة", "Save": "حفظ", + "Send an email and say hi!": "", + "Send an email to {{senderEmail}} and say hello. This can also help signal to your mail provider that emails to and from this address should be trusted.": "", "Sending login link...": "جار ارسال رابط التسجيل...", "Sending...": "جار الارسال...", + "Show all": "", "Sign in": "تسجيل الدخول", + "Sign out": "", "Sign up": "انشاء حساب", + "Signup error: Invalid link": "", + "Something went wrong, please try again later.": "", + "Sorry, that didn’t work.": "", + "Spam complaints": "", "Start {{amount}}-day free trial": "ابدأ {{amount}}-ايام تجربة مجانية", + "Starting {{startDate}}": "", + "Starting today": "", "Submit feedback": "تسليم رأيك", + "Subscribe": "", + "Subscribed": "", + "Subscription plan updated successfully": "", + "Success": "", + "Success! Check your email for magic link to sign-in.": "", + "Success! Your account is fully activated, you now have access to all content.": "", + "Success! Your email is updated.": "", "Successfully unsubscribed": "تم الغاء الاشتراك بنجاح", + "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "", + "Thank you for your support": "", + "Thank you for your support!": "", "Thanks for the feedback!": "شكرا لمشاركتك رأيك!", "That didn't go to plan": "لم تسر الامور على ما يرام", + "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "", + "There was a problem submitting your feedback. Please try again a little later.": "", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", + "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "هذا الموقع للمشتركين فقط، تواصل مع ادارة الموقع للحصول على اشتراك.", + "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "لاكمال انشاء حسابك، استخدم رابط التأكيد المرسل الى بريد. اذا لم تتلقى الرسالة خلال 3 دقائق، برجاء التأكد من مجلد المهملات.", + "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Try free for {{amount}} days, then {{originalPrice}}.": "", + "Unable to initiate checkout session": "", + "Unlock access to all newsletters by becoming a paid subscriber.": "", "Unsubscribe from all emails": "الغ الاشتراك من جميع الرسائل", + "Unsubscribed": "", + "Unsubscribed from all emails.": "", "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "الغاء اشتراك من الرسائل لا يعني إلغاء اشتراكك المدفوع في {{title}}", + "Update": "", "Update your preferences": "تحديث تفضيلاتك", + "Verification link sent, check your inbox": "", + "Verify your email address is correct": "", + "View plans": "", "We couldn't unsubscribe you as the email address was not found. Please contact the site owner.": "لم نتمكن من إلغاء اشتراكك في الرسائل لانه لم يتم العثور على بريدك الاكتروني. برجاء التواصل مع ادارة الموقع.", + "Welcome back, {{name}}!": "", + "Welcome back!": "", + "Welcome to {{siteTitle}}": "", + "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "", + "Why has my email been disabled?": "", + "year": "", "Yearly": "سنوي", + "You currently have a free membership, upgrade to a paid subscription for full access.": "", "You have been successfully resubscribed": "تم اعادة اشتراكك بنجاح", + "You're currently not receiving emails": "", + "You're not receiving emails": "", "You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "لم تستلم رسالة بريدية لربما انك جعلت الرسائل مهملة او ان الرسائل لا تصل الى بريدك الذي سجلته.", + "You've successfully signed in.": "", + "You've successfully subscribed to": "", "Your account": "حسابك", - "Your input helps shape what gets published.": "آرائك تساهم في تحسين ما ينشر." + "Your email has failed to resubscribe, please try again": "", + "Your input helps shape what gets published.": "آرائك تساهم في تحسين ما ينشر.", + "Your subscription will expire on {{expiryDate}}": "", + "Your subscription will renew on {{renewalDate}}": "", + "Your subscription will start on {{subscriptionStart}}": "" } diff --git a/ghost/i18n/locales/ar/search.json b/ghost/i18n/locales/ar/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/ar/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/ar/signup-form.json b/ghost/i18n/locales/ar/signup-form.json new file mode 100644 index 00000000000..cc957d9d0d1 --- /dev/null +++ b/ghost/i18n/locales/ar/signup-form.json @@ -0,0 +1,9 @@ +{ + "Email sent": "", + "Now check your email!": "", + "Please enter a valid email address": "", + "Something went wrong, please try again.": "", + "Subscribe": "", + "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "", + "Your email address": "" +} diff --git a/ghost/i18n/locales/bg/comments.json b/ghost/i18n/locales/bg/comments.json index c2ddef24373..1d64c4915c9 100644 --- a/ghost/i18n/locales/bg/comments.json +++ b/ghost/i18n/locales/bg/comments.json @@ -1,5 +1,5 @@ { - "{{amount}} characters left": "Остават {{amount}} символа", + "{{amount}} characters left": "Остават {{amount}} знака", "{{amount}} comments": "{{amount}} коментара", "{{amount}} days ago": "Преди {{amount}} дни", "{{amount}} hrs ago": "Преди {{amount}} часа", @@ -10,8 +10,8 @@ "{{amount}} weeks ago": "Преди {{amount}} седмици", "{{amount}} years ago": "Преди {{amount}} години", "1 comment": "1 коментар", - "Add comment": "Добавяне на коментар", - "Add context to your comment, share your name and expertise to foster a healthy discussion.": "Добавете контекст към вашия коментар, споделете вашето име и опит, за да насърчите конструктивна дискусия.", + "Add comment": "Нов коментар", + "Add context to your comment, share your name and expertise to foster a healthy discussion.": "Добавете контекст към коментара си, споделете името си и опита си, за да насърчите полезна дискусия.", "Add reply": "Отговор", "Already a member?": "Вече сте абонат?", "Anonymous": "Анонимен", @@ -19,55 +19,53 @@ "Become a paid member of {{publication}} to start commenting.": "Станете платен абонат на {{publication}}, за да коментирате.", "Cancel": "Отказ", "Comment": "Коментар", - "Complete your profile": "Завършете своя профил", + "Complete your profile": "Попълнете профила си", "Delete": "Изтриване", "Deleted member": "Изтрит абонат", "Discussion": "Дискусия", "Edit": "Редактиране", - "Edit this comment": "Редактиране на този коментар", - "edited": "pедактиран", - "Enter your name": "Въведете вашето име", + "Edit this comment": "Редактиране на коментара", + "edited": "редактиран", + "Enter your name": "Попълнете името си", "Expertise": "Опит", - "Founder @ Acme Inc": "Основател @ Компания ООД", - "Full-time parent": "Родител на пълен работен ден", - "Head of Marketing at Acme, Inc": "Ръководител маркетинг в Компания ООД", + "Founder @ Acme Inc": "Основател на Компания ООД", + "Full-time parent": "Родител на пълно работно време", + "Head of Marketing at Acme, Inc": "Директор маркетинг в Компания ООД", "Hide": "Скриване", - "Hide comment": "Скриване на коментар", + "Hide comment": "Скриване на коментара", "Jamie Larson": "Иван Иванов", - "Join the discussion": "Включете се в дискусията", + "Join the discussion": "Участвайте в дискусията", "Just now": "Току-що", "Local resident": "Местен жител", - "Member discussion": "Дискусия на абонатите", + "Member discussion": "Дискусия за абонатите", "Name": "Име", "Neurosurgeon": "Неврохирург", - "One day ago": "Преди един ден", - "One hour ago": "Преди един час", - "One min ago": "Преди една минута", - "One month ago": "Преди един месец", - "One week ago": "Преди една седмица", - "One year ago": "Преди една година", + "One day ago": "Преди ден", + "One hour ago": "Преди час", + "One min ago": "Преди минута", + "One month ago": "Преди месец", + "One week ago": "Преди седмица", + "One year ago": "Преди година", "Reply": "Отговор", - "Reply to comment": "Отгговор на коментар", + "Reply to comment": "Отгговор на коментара", "Report": "Докладване", - "Report comment": "Докладване на коментар", - "Report this comment": "Докладване на този коментар", - "Report this comment?": "Искате ли да докладвате този коментар?", + "Report comment": "Докладване на коментара", + "Report this comment?": "Ще докладвате ли коментара?", "Save": "Запис", "Sending": "Изпращане", "Sent": "Изпратен", "Show": "Показване", - "Show {{amount}} more replies": "Показване на още {{amount}} отговора", - "Show {{amount}} previous comments": "Показване на {{amount}} предишни коментара", - "Show 1 more reply": "Показване на още 1 отговор", - "Show 1 previous comment": "Показване на 1 предишен коментар", - "Show comment": "Показване на коментар", - "Sign in": "Влизане", - "Sign up now": "Абонирайте се сега", - "Start the conversation": "Започнете дискусията", + "Show {{amount}} more replies": "Покажи още {{amount}} отговора", + "Show {{amount}} previous comments": "Покажи {{amount}} предишни коментара", + "Show 1 more reply": "Покажи още един отговор", + "Show 1 previous comment": "Покажи един предишен коментар", + "Show comment": "Покажи коментар", + "Sign in": "Вход", + "Sign up now": "Регистрация", + "Start the conversation": "Започнете дискусия", "This comment has been hidden.": "Този коментар е скрит.", "This comment has been removed.": "Този коментар е премахнат.", - "Upgrade now": "Преминете към платен абонамент", + "Upgrade now": "Надградете сега", "Yesterday": "Вчера", - "You want to report this comment?": "Искате да докладвате този коментар?", - "Your request will be sent to the owner of this site.": "Вашето искане ще бъде изпратено до собственика на сайта." + "Your request will be sent to the owner of this site.": "Искането ви ще бъде изпратено до собственика на сайта." } diff --git a/ghost/i18n/locales/bg/ghost.json b/ghost/i18n/locales/bg/ghost.json index 97d5ca6d5dd..be046e9d827 100644 --- a/ghost/i18n/locales/bg/ghost.json +++ b/ghost/i18n/locales/bg/ghost.json @@ -10,8 +10,8 @@ "For your security, the link will expire in 24 hours time.": "За ваша сигурност връзката ще бъде валидна само 24 часа.", "Hey there,": "Здравейте,", "Hey there!": "Здравейте!", - "If you did not make this request, you can safely ignore this email.": "Ако не сте направили тази заявка, можете да игноритрате това писмо.", - "If you did not make this request, you can simply delete this message.": "Ако не сте направили тази заявка, можете просто да изтриете това писмо.", + "If you did not make this request, you can safely ignore this email.": "Ако не сте направили тази заявка, може да игнорирате това писмо.", + "If you did not make this request, you can simply delete this message.": "Ако не сте направили тази заявка, може просто да изтриете това писмо.", "Please confirm your email address with this link:": "Моля, потвърдете Вашия имейл адрес чрез тази връзка:", "Secure sign in link for {{siteTitle}}": "Връзка за сигурно влизане в сайта {{siteTitle}}", "See you soon!": "До скоро!", @@ -27,7 +27,7 @@ "Welcome back to {{siteTitle}}!": "Добре дошли отново в {{siteTitle}}!", "Welcome back! Use this link to securely sign in to your {{siteTitle}} account:": "Добре дошли отново! Използвайте тази връзка за влизане в своя акаунт за сайта {{siteTitle}}:", "You can also copy & paste this URL into your browser:": "Може да копирате този адрес в своя браузър:", - "You will not be signed up, and no account will be created for you.": "Няма да бъде направена регистрация и няма да бъде създаван акаунт за вас.", + "You will not be signed up, and no account will be created for you.": "Няма да бъде направена регистрация и няма да бъде създаден акаунт за вас.", "You will not be subscribed.": "Няма да бъдете абониран.", "You're one tap away from subscribing to {{siteTitle}} — please confirm your email address with this link:": "Вие сте на стъпка от това да се абонирате за {{siteTitle}} - моля, потвърдете имейла си като ползвате тази връзка:", "You're one tap away from subscribing to {{siteTitle}}!": "Вие сте на една стъпка от това да се абонирате за {{siteTitle}}!" diff --git a/ghost/i18n/locales/bg/portal.json b/ghost/i18n/locales/bg/portal.json index 544718ad97b..3470e167fc3 100644 --- a/ghost/i18n/locales/bg/portal.json +++ b/ghost/i18n/locales/bg/portal.json @@ -1,5 +1,5 @@ { - "(save {{highestYearlyDiscount}}%)": "", + "(save {{highestYearlyDiscount}}%)": "(спестете {{highestYearlyDiscount}}%)", "{{amount}} days free": "{{amount}} дни безплатно", "{{amount}} off": "{{amount}} отстъпка", "{{amount}} off for first {{number}} months.": "{{amount}} отстъпка за първите {{number}} месеца.", @@ -10,12 +10,14 @@ "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "{{memberEmail}} повече няма да получава имейли, когато някой отговаря на ваш коментар.", "{{memberEmail}} will no longer receive this newsletter.": "{{memberEmail}} повече няма да получава този бюлетин.", "{{trialDays}} days free": "{{trialDays}} дни безплатен достъп", - "+1 (123) 456-7890": "", - "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Изпратен Ви е имейл с препратка за влизане. Ако не пристигне до 3 минути, проверете дали не е категоризиран като нежелано писмо.", + "+1 (123) 456-7890": "+359 88 123-4567", + "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Връзка за влизане ви беше изпратена по имейл. Ако писмото не пристигне до 3 минути, проверете дали не е в папката за спам.", "Account": "Профил", + "Account details updated successfully": "", "Account settings": "Настройки на профила Ви", - "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "След приключване на безплатния достъп ще бъдете таксувани според обявените цени. Можете да се откажете преди изтичането на безплатния достъп.", - "Already a member?": "Вече сте абонат на сайта?", + "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "След приключване на безплатния период ще бъдете таксувани според обявените цени. Можете да се откажете преди изтичането на безплатния период.", + "Already a member?": "Абонат ли сте вече?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Възникна неочаквана грешка. Моля, опитайте отново или потърсете поддръжката ако това се повтаря.", "Back": "Обратно", "Back to Log in": "Обратно към формата за влизане", @@ -25,13 +27,14 @@ "Cancel subscription": "Откажи абонамент", "Cancellation reason": "Причина за отказ", "Change": "Промени", - "Change plan": "", + "Change plan": "Промени плана", "Check spam & promotions folders": "Провери папките за спам и промоции", "Check with your mail provider": "Уведомете доставчика ви на ел. поща", + "Check your inbox to verify email update": "", "Choose": "Избери", - "Choose a different plan": "Избеи различен план", - "Choose a plan": "", - "Choose your newsletters": "Избери твоят бюлетин", + "Choose a different plan": "Избери различен план", + "Choose a plan": "Изберете план", + "Choose your newsletters": "Изберете бюлетини", "Click here to retry": "Щракнете за нов опит", "Close": "Затвори", "Comments": "Коментари", @@ -42,6 +45,7 @@ "Contact support": "Връзка с поддръжката", "Continue": "Продължи", "Continue subscription": "Продължете абонамента", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Не можете да влезете. Връзката за вход е изветряла.", "Could not update email! Invalid link.": "Не успяхме да обновим имейла! Невалиден линк.", "Create a new contact": "Създайте нов контакт", @@ -52,22 +56,35 @@ "Edit": "Редакция", "Email": "Имейл адрес", "Email newsletter": "Имейл бюлетин", + "Email newsletter settings updated": "", "Email preferences": "Имейл настройки ", "Emails": "Имейли", - "Emails disabled": "Писмата са преустновени", + "Emails disabled": "Писмата са спрени", "Ends {{offerEndDate}}": "До {{offerEndDate}}", - "Enter your email address": "", - "Enter your name": "", + "Enter your email address": "Попълнете вашия имейл адрес", + "Enter your name": "Попълнете вашето име", "Error": "Грешка", "Expires {{expiryDate}}": "Изтича {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Завинаги", "Free Trial – Ends {{trialEnd}}": "Безплатен тест – до {{trialEnd}}", "Get help": "Получете помощ", "Get in touch for help": "Свържете се за помощ", - "Get notified when someone replies to your comment": "Бъдете уведомявани, ако някой отговори на Ваш коментар", + "Get notified when someone replies to your comment": "Бъдете уведомявани, ако някой отговори на ваш коментар", "Give feedback on this post": "Вашият отзив за публикацията", "Help! I'm not receiving emails": "Помощ! Не получавам имейли", - "Here are a few other sites you may enjoy.": "", + "Here are a few other sites you may enjoy.": "Други сайтове, които може да харесате.", "If a newsletter is flagged as spam, emails are automatically disabled for that address to make sure you no longer receive any unwanted messages.": "Ако даден информационен бюлетин бъде отбелязан като спам, имейлите за този адрес се деактивират автоматично, за да е сигурно, че няма да получавате нежелани съобщения.", "If the spam complaint was accidental, or you would like to begin receiving emails again, you can resubscribe to emails by clicking the button on the previous screen.": "Ако оплакването за спам е било случайно или искате отново да започнете да получавате имейли, можете да активирате това, като кликнете върху бутона на предишния екран.", "If you cancel your subscription now, you will continue to have access until {{periodEnd}}.": "Ако отмените абонамента си сега, ще продължите да имате достъп до {{periodEnd}}.", @@ -77,34 +94,36 @@ "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "Ако сте извършили всички тези проверки и все още не получавате имейли, можете да се свържете с поддръжката на {{supportAddress}}.", "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "В случай че при опит за изпращане на бюлетин се получава някакъв постоянен проблем, имейлите ще бъдат деактивирани в акаунта.", "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "Добавете {{senderEmail}} в списъка си с контакти. Това сигнализира на вашия доставчик, че имейлите, изпратени от този адрес са надеждни.", - "Invalid email address": "", - "Jamie Larson": "", - "jamie@example.com": "", + "Invalid email address": "Невалиден имейл адрес", + "Jamie Larson": "Ангел Петров", + "jamie@example.com": "petrov@example.com", "Less like this": "По-малко такива", - "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "Уверете се, че имейлите не попадат случайно в папките Спам или Промоции на входящата ви поща. Ако това е така, щракнете върху \"Не е спам\" и/или \"Премести във входяща поща\".", + "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "Уверете се, че имейлите не попадат случайно в папките за спам и промоции на входящата ви поща. Ако това е така, щракнете върху \"Не е спам\" и/или \"Премести във входяща поща\".", "Manage": "Управлявай", - "Maybe later": "", - "Memberships unavailable, contact the owner for access.": "", - "month": "", + "Maybe later": "Може би по-късно", + "Memberships unavailable, contact the owner for access.": "Няма възможност за абонамент, свържете се със собственика на сайта за достъп.", + "month": "месец", "Monthly": "Месечно", "More like this": "Повече такива", "Name": "Име", "Need more help? Contact support": "Още имате нужда от помощ? Потърсете поддръжката", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Информационните бюлетини могат да бъдат деактивирани в профила ви по две причини: Предишен имейл е бил маркиран като спам или опитът за изпращане на имейл е довел до траен неуспех (отказ).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Не получавате поща?", - "Now check your email!": "Проверете си пощенската кутия!", + "Now check your email!": "Проверете имейла си!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "След като се абонирате отново, ако все още не виждате имейли във входящата си поща, проверете папката за спам. Някои доставчици пазят история с предишни оплаквания за спам и ще продължат да маркират имейлите. Ако вашият случай е такъв, маркирайте последния бюлетин като 'Не е спам', за да го преместите обратно в основната си пощенска кутия.", - "Permanent failure (bounce)": "Постоянен проблем (отскок)", - "Phone number": "", + "Permanent failure (bounce)": "Постоянен проблем (отказ)", + "Phone number": "Телефонен номер", "Plan": "План", "Plan checkout was cancelled.": "Плащането на плана е прекъснато.", "Plan upgrade was cancelled.": "Надграждането на плана е прекъснато.", - "Please contact {{supportAddress}} to adjust your complimentary subscription.": "", - "Please enter {{fieldName}}": "", + "Please contact {{supportAddress}} to adjust your complimentary subscription.": "Моля, свържете се с {{supportAddress}}, за корекции относно безплатния ви абонамент.", + "Please enter {{fieldName}}": "Моля, попълнете {{fieldName}}", "Please fill in required fields": "Моля, попълнете задължителните полета", "Price": "Цена", "Re-enable emails": "Позволете отново изпращането на писма", - "Recommendations": "", + "Recommendations": "Препоръки", "Renews at {{price}}.": "Подновяване за {{price}}.", "Retry": "Отново", "Save": "Запиши", @@ -112,12 +131,12 @@ "Send an email to {{senderEmail}} and say hello. This can also help signal to your mail provider that emails to and from this address should be trusted.": "Изпратете имейл до {{senderEmail}} за здрасти. Това също може да сигнализира на вашия доставчик на ел. поща, че на имейлите до и от този адрес може да се има доверие.", "Sending login link...": "Изпращане на връзка за влизане...", "Sending...": "Изпращане...", - "Show all": "", + "Show all": "Покажи всички", "Sign in": "Вход", "Sign out": "Изход", "Sign up": "Регистриране", "Signup error: Invalid link": "Грешка при влизане: Невалиден линк", - "Something went wrong, please try again later.": "", + "Something went wrong, please try again later.": "Нещо се обърка, опитайте отново.", "Sorry, that didn’t work.": "Жалко, така не става.", "Spam complaints": "Оплаквания от спам", "Start {{amount}}-day free trial": "Започване на {{amount}}-дневен безплатен достъп.", @@ -126,50 +145,62 @@ "Submit feedback": "Изпрати отзив", "Subscribe": "Абонамент", "Subscribed": "Абониран", + "Subscription plan updated successfully": "", "Success": "Чудесно", "Success! Check your email for magic link to sign-in.": "Чудесно! Проверете имейла си за вашия магически линк за влизане.", "Success! Your account is fully activated, you now have access to all content.": "Чудесно! Вашият акаунт е активиран и вече имате достъп до цялото съдържание.", "Success! Your email is updated.": "Чудесно! Вашият имейл е актуализиран.", "Successfully unsubscribed": "Успешно разабониране", - "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "", - "Thank you for your support": "", - "Thank you for your support!": "", - "Thanks for the feedback!": "Благодарим за изпратеният отзив!", - "That didn't go to plan": "Нещо се обърка и не се случи както трябваше", + "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "Благодарим ви за абонамента. Преди да започнете да четете, още няколко сайта, които може да ви харесат.", + "Thank you for your support": "Благодарности за подкрепата ви", + "Thank you for your support!": "Благодарности за подкрепата ви!", + "Thanks for the feedback!": "Благодарности за обратната връзка!", + "That didn't go to plan": "Нещо се обърка и не стана както трябваше", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Имейлът, който имаме за вас, е {{memberEmail}} - ако не е верен, можете да го актуализирате в областта за .", "There was a problem submitting your feedback. Please try again a little later.": "Имаше проблем при изпращането на обратната връзка. Моля, опитайте отново малко по-късно.", - "There was an error processing your payment. Please try again.": "", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", + "There was an error processing your payment. Please try again.": "Получи се грешка при обработката на вашето плащане. Моля, опитайте отново.", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Сайтът е само с покани. Свържете се със собственика за да получите достъп.", - "This site is not accepting payments at the moment.": "", + "This site is not accepting payments at the moment.": "В момента сайтът не приема плащания.", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "За да приключите регистрацията, последвайте препратката в съобщението, изпратено Ви по имейл. Ако не пристигне до 3 минути, проверете дали не е категоризирано като нежелано писмо.", - "To continue to stay up to date, subscribe to {{publication}} below.": "", + "To continue to stay up to date, subscribe to {{publication}} below.": "За да останете информирани, запишете се за {{publication}} по-долу.", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Тествайте безплатно за {{amount}} дни, след това {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Отключете достъпа до всички бюлетини, като станете платен абонат.", "Unsubscribe from all emails": "Прекрати изпращането на всякакви писма", "Unsubscribed": "Неабониран", - "Unsubscribed from all emails.": "", - "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "Спирането на изпращането на писма, няма да ти прекрати абонамента и регистрацията в {{title}}", + "Unsubscribed from all emails.": "Разабониране от всички имейли.", + "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "Спирането на изпращането на писма, не прекратява абонамента и регистрацията в {{title}}", "Update": "Обнови", - "Update your preferences": "Обнови твоите предпочитания/настройки", - "Verification link sent, check your inbox": "", + "Update your preferences": "Обнови твоите настройки", + "Verification link sent, check your inbox": "Изпратен ви е линк за проверка, проверете електронната си поща", "Verify your email address is correct": "Проверете дали имейл адресът ви е верен", "View plans": "Вижте плановете", "We couldn't unsubscribe you as the email address was not found. Please contact the site owner.": "Този пощенски адрес който опитвате да разабонирате, изглежда невалиден. Моля, свържете се със собственика на сайта.", - "Welcome back, {{name}}!": "Добре дошли отново, {{name}}!", + "Welcome back, {{name}}!": "Здравейте отново, {{name}}!", "Welcome back!": "Добре дошли отново!", - "Welcome to {{siteTitle}}": "", + "Welcome to {{siteTitle}}": "Добре дошли в {{siteTitle}}", "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "Когато дадена пощенска кутия не успее да приеме даден имейл, това обикновено се нарича отказ. В много случаи това е временно. В някои случаи обаче, имейлът може да се отхвърля постоянно, например когато имейл адресът е невалиден или не съществува.", "Why has my email been disabled?": "Защо имейлът ми е деактивиран?", - "year": "", + "year": "година", "Yearly": "Годишно", "You currently have a free membership, upgrade to a paid subscription for full access.": "В момента имате безплатно членство, преминете към платен абонамент за пълен достъп.", - "You have been successfully resubscribed": "Успешно беше подновен абонаментът", + "You have been successfully resubscribed": "Абонаментът ви беше успешно подновен", "You're currently not receiving emails": "Понастоящем не получавате имейли", "You're not receiving emails": "Не получавате имейли", - "You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "Не получавате писма защото или пощенският адрес е невалиден, или писмата се класифицират като нежелана поща.", + "You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "Не получавате писма, защото или пощенският адрес е невалиден, или писмата се класифицират като нежелана поща.", "You've successfully signed in.": "Влязохте успешно.", - "You've successfully subscribed to": "", + "You've successfully subscribed to": "Успешно се абонирахте за", "Your account": "Твоят профил", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Вашият принос помага да се оформи това, което се публикува.", "Your subscription will expire on {{expiryDate}}": "Абонаментът ви ще изтече на {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Абонаментът ви ще се поднови на {{renewalDate}}", diff --git a/ghost/i18n/locales/bg/search.json b/ghost/i18n/locales/bg/search.json new file mode 100644 index 00000000000..7a641a6c362 --- /dev/null +++ b/ghost/i18n/locales/bg/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "Автори", + "Cancel": "Отказ", + "No matches found": "Няма намерени резултати", + "Posts": "Публикации", + "Search posts, tags and authors": "Търсене в публикации, етикети и автори", + "Show more results": "Показване на още резултати", + "Tags": "Етикети" +} diff --git a/ghost/i18n/locales/bn/comments.json b/ghost/i18n/locales/bn/comments.json new file mode 100644 index 00000000000..1354b52b9a6 --- /dev/null +++ b/ghost/i18n/locales/bn/comments.json @@ -0,0 +1,71 @@ +{ + "{{amount}} characters left": "{{amount}} অক্ষর বাকি", + "{{amount}} comments": "{{amount}} মন্তব্য", + "{{amount}} days ago": "{{amount}} দিন পূর্বে", + "{{amount}} hrs ago": "", + "{{amount}} mins ago": "", + "{{amount}} months ago": "{{amount}} মাস পূর্বে", + "{{amount}} more": "{{amount}}টি আরও", + "{{amount}} seconds ago": "{{amount}} সেকেন্ড পূর্বে", + "{{amount}} weeks ago": "{{amount}} সপ্তাহ পূর্বে", + "{{amount}} years ago": "{{amount}} বছর পূর্বে", + "1 comment": "১টি মন্তব্য", + "Add comment": "মন্তব্য যোগ করুন", + "Add context to your comment, share your name and expertise to foster a healthy discussion.": "আপনার মন্তব্যে প্রসঙ্গ যোগ করুন, একটি সুস্থ আলোচনা করতে আপনার নাম ও অভিজ্ঞতা শেয়ার করুন।", + "Add reply": "উত্তর যোগ করুন", + "Already a member?": "ইত:পূর্বে সদস্য?", + "Anonymous": "অজ্ঞাতনামা", + "Become a member of {{publication}} to start commenting.": "{{publication}} এর সদস্য হন মন্তব্য করার জন্য।", + "Become a paid member of {{publication}} to start commenting.": "{{publication}} এর পেইড সদস্য হন মন্তব্য করার জন্য।", + "Cancel": "বাতিল করুন", + "Comment": "মন্তব্য", + "Complete your profile": "আপনার প্রোফাইল সম্পূর্ণ করুন", + "Delete": "মুছে ফেলুন", + "Deleted member": "মুছে ফেলা সদস্য", + "Discussion": "আলোচনা", + "Edit": "সম্পাদনা", + "Edit this comment": "এই মন্তব্য সম্পাদনা করুন", + "edited": "", + "Enter your name": "আপনার নাম লিখুন", + "Expertise": "বিশেষজ্ঞতা", + "Founder @ Acme Inc": "প্রতিষ্ঠাতা @ বাংলাদেশ ট্রেড হাব", + "Full-time parent": "পূর্ণকালীন অভিভাবক", + "Head of Marketing at Acme, Inc": "মার্কেটিং প্রধান @ বাংলাদেশ ট্রেড হাব", + "Hide": "লুকান", + "Hide comment": "মন্তব্য লুকান", + "Jamie Larson": "শাহ নেওয়াজ", + "Join the discussion": "আলোচনায় যোগ দিন", + "Just now": "এখনই", + "Local resident": "স্থানীয় বাসিন্দা", + "Member discussion": "সদস্য আলোচনা", + "Name": "নাম", + "Neurosurgeon": "নিউরোসার্জন", + "One day ago": "একদিন পূর্বে", + "One hour ago": "এক ঘণ্টা পূর্বে", + "One min ago": "", + "One month ago": "এক মাস পূর্বে", + "One week ago": "এক সপ্তাহ পূর্বে", + "One year ago": "এক বছর পূর্বে", + "Reply": "উত্তর", + "Reply to comment": "মন্তব্যের উত্তর দিন", + "Report": "প্রতিবেদন", + "Report comment": "মন্তব্য প্রতিবেদন করুন", + "Report this comment?": "এই মন্তব্য প্রতিবেদন করবেন?", + "Save": "সংরক্ষণ করুন", + "Sending": "পাঠানো হচ্ছে", + "Sent": "পাঠানো হয়েছে", + "Show": "দেখান", + "Show {{amount}} more replies": " আরো {{amount}}টি উত্তর দেখান", + "Show {{amount}} previous comments": "পূর্বের {{amount}}টি মন্তব্য দেখান", + "Show 1 more reply": "১টি আরো উত্তর দেখান", + "Show 1 previous comment": "১টি পূর্বের মন্তব্য দেখান", + "Show comment": "মন্তব্য দেখান", + "Sign in": "সাইন ইন করুন", + "Sign up now": "এখনই সাইন আপ করুন", + "Start the conversation": "আলোচনা শুরু করুন", + "This comment has been hidden.": "এই মন্তব্য লুকানো হয়েছে।", + "This comment has been removed.": "এই মন্তব্য মুছে ফেলা হয়েছে।", + "Upgrade now": "এখনই আপগ্রেড করুন", + "Yesterday": "গতকাল", + "Your request will be sent to the owner of this site.": "আপনার অনুরোধ সাইটের মালিকের কাছে পাঠানো হবে।" +} diff --git a/ghost/i18n/locales/bn/ghost.json b/ghost/i18n/locales/bn/ghost.json new file mode 100644 index 00000000000..04c20151c19 --- /dev/null +++ b/ghost/i18n/locales/bn/ghost.json @@ -0,0 +1,34 @@ +{ + "All the best!": "শুভেচ্ছা!", + "Complete signup for {{siteTitle}}!": "{{siteTitle}} এর জন্য সাইন আপ সম্পূর্ণ করুন!", + "Complete your sign up to {{siteTitle}}!": "{{siteTitle}} এ আপনার সাইন আপ সম্পূর্ণ করুন!", + "Confirm email address": "ইমেল ঠিকানা নিশ্চিত করুন", + "Confirm signup": "সাইন আপ নিশ্চিত করুন", + "Confirm your email address": "আপনার ইমেল ঠিকানা নিশ্চিত করুন", + "Confirm your email update for {{siteTitle}}!": "{{siteTitle}} এর জন্য আপনার ইমেল আপডেট নিশ্চিত করুন!", + "Confirm your subscription to {{siteTitle}}": "{{siteTitle}} এর সাবস্ক্রিপশন নিশ্চিত করুন", + "For your security, the link will expire in 24 hours time.": "আপনার নিরাপত্তার জন্য, লিঙ্কটি ২৪ ঘণ্টার মধ্যে মেয়াদোত্তীর্ণ হবে।", + "Hey there,": "হ্যালো,", + "Hey there!": "হ্যালো!", + "If you did not make this request, you can safely ignore this email.": "আপনি যদি এই অনুরোধ না করে থাকেন, তবে আপনি নিরাপদে এই ইমেলটি উপেক্ষা করতে পারেন।", + "If you did not make this request, you can simply delete this message.": "আপনি যদি এই অনুরোধ না করে থাকেন, তবে আপনি সহজেই এই বার্তাটি মুছে ফেলতে পারেন।", + "Please confirm your email address with this link:": "এই লিঙ্ক দিয়ে আপনার ইমেল ঠিকানা নিশ্চিত করুন:", + "Secure sign in link for {{siteTitle}}": "{{siteTitle}} এর জন্য নিরাপদ সাইন ইন লিঙ্ক", + "See you soon!": "শীঘ্রই দেখা হবে!", + "Sent to {{email}}": "{{email}} এ পাঠানো হয়েছে", + "Sign in": "সাইন ইন করুন", + "Sign in to {{siteTitle}}": "{{siteTitle}} এ সাইন ইন করুন", + "Tap the link below to complete the signup process for {{siteTitle}}, and be automatically signed in:": "{{siteTitle}} এর সাইন আপ প্রক্রিয়া সম্পূর্ণ করতে নিচের লিঙ্কে ট্যাপ করুন, এবং স্বয়ংক্রিয়ভাবে সাইন ইন হবেন:", + "Thank you for signing up to {{siteTitle}}!": "{{siteTitle}} এ সাইন আপ করার জন্য ধন্যবাদ!", + "Thank you for subscribing to {{siteTitle}}!": "{{siteTitle}} এ সাবস্ক্রাইব করার জন্য ধন্যবাদ!", + "Thank you for subscribing to {{siteTitle}}.": "{{siteTitle}} এ সাবস্ক্রাইব করার জন্য ধন্যবাদ।", + "Thank you for subscribing to {{siteTitle}}. Tap the link below to be automatically signed in:": "{{siteTitle}} এ সাবস্ক্রাইব করার জন্য ধন্যবাদ। স্বয়ংক্রিয়ভাবে সাইন ইন হওয়ার জন্য নিচের লিঙ্কে ট্যাপ করুন:", + "This email address will not be used.": "এই ইমেল ঠিকানা ব্যবহার করা হবে না।", + "Welcome back to {{siteTitle}}!": "{{siteTitle}} এ আবার স্বাগতম!", + "Welcome back! Use this link to securely sign in to your {{siteTitle}} account:": "আবার স্বাগতম! আপনার {{siteTitle}} অ্যাকাউন্টে নিরাপদে সাইন ইন করতে এই লিঙ্কটি ব্যবহার করুন:", + "You can also copy & paste this URL into your browser:": "আপনি এই URL টি কপি করে আপনার ব্রাউজারে পেস্ট ও করতে পারেন:", + "You will not be signed up, and no account will be created for you.": "আপনার সাইন আপ হবে না, এবং আপনার জন্য কোনো অ্যাকাউন্ট তৈরি করা হবে না।", + "You will not be subscribed.": "আপনি সাবস্ক্রাইব হবেন না।", + "You're one tap away from subscribing to {{siteTitle}} — please confirm your email address with this link:": "আপনি {{siteTitle}} এ সাবস্ক্রাইব করতে এক ট্যাপ দূরে রয়েছেন — অনুগ্রহ করে এই লিঙ্ক দিয়ে আপনার ইমেল ঠিকানা নিশ্চিত করুন:", + "You're one tap away from subscribing to {{siteTitle}}!": "আপনি {{siteTitle}} এ সাবস্ক্রাইব করতে এক ট্যাপ দূরে রয়েছেন!" +} diff --git a/ghost/i18n/locales/bn/portal.json b/ghost/i18n/locales/bn/portal.json new file mode 100644 index 00000000000..f2ad9c3e02c --- /dev/null +++ b/ghost/i18n/locales/bn/portal.json @@ -0,0 +1,208 @@ +{ + "(save {{highestYearlyDiscount}}%)": "({{highestYearlyDiscount}}% সঞ্চয়)", + "{{amount}} days free": "{{amount}} দিন ফ্রি", + "{{amount}} off": "{{amount}} ছাড়", + "{{amount}} off for first {{number}} months.": "প্রথম {{number}} মাসের জন্য {{amount}} ছাড়।", + "{{amount}} off for first {{period}}.": "প্রথম {{period}} এর জন্য {{amount}} ছাড়।", + "{{amount}} off forever.": "চিরতরে {{amount}} ছাড়।", + "{{discount}}% discount": "{{discount}}% ছাড়", + "{{memberEmail}} will no longer receive {{newsletterName}} newsletter.": "{{memberEmail}} আর {{newsletterName}} নিউজলেটার পাবেন না।", + "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "{{memberEmail}} আপনার মন্তব্যের উত্তর দিলে আর ইমেল পাবেন না।", + "{{memberEmail}} will no longer receive this newsletter.": "{{memberEmail}} আর এই নিউজলেটার পাবেন না।", + "{{trialDays}} days free": "{{trialDays}} দিন ফ্রি", + "+1 (123) 456-7890": "", + "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "একটি লগইন লিঙ্ক আপনার ইনবক্সে পাঠানো হয়েছে। যদি এটি ৩ মিনিটের মধ্যে না পৌঁছায়, তবে আপনার স্প্যাম ফোল্ডার চেক করুন।", + "Account": "অ্যাকাউন্ট", + "Account details updated successfully": "", + "Account settings": "অ্যাকাউন্ট সেটিংস", + "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "একটি ফ্রি ট্রায়াল শেষ হওয়ার পরে, আপনি যেই টিয়ারটি বেছে নিয়েছেন তার নিয়মিত মূল্য চার্জ করা হবে। আপনি যে কোনো সময় বাতিল করতে পারেন।", + "Already a member?": "ইতিমধ্যেই সদস্য?", + "An error occurred": "", + "An unexpected error occured. Please try again or contact support if the error persists.": "", + "Back": "ফিরে যান", + "Back to Log in": "লগইনে ফিরে যান", + "Billing info": "বিলিং তথ্য", + "Black Friday": "ঈদ-উল-ফিতর", + "Cancel anytime.": "যে কোনো সময় বাতিল করুন।", + "Cancel subscription": "সাবস্ক্রিপশন বাতিল করুন", + "Cancellation reason": "বাতিলের কারণ", + "Change": "পরিবর্তন", + "Change plan": "", + "Check spam & promotions folders": "স্প্যাম ও প্রমোশন ফোল্ডার চেক করুন", + "Check with your mail provider": "আপনার মেইল প্রোভাইডারের সাথে চেক করুন", + "Check your inbox to verify email update": "", + "Choose": "বেছে নিন", + "Choose a different plan": "একটি ভিন্ন পরিকল্পনা বেছে নিন", + "Choose a plan": "", + "Choose your newsletters": "আপনার নিউজলেটার বেছে নিন", + "Click here to retry": "আবার চেষ্টা করতে এখানে ক্লিক করুন", + "Close": "বন্ধ করুন", + "Comments": "মন্তব্য", + "Complimentary": "সম্পূর্ণ বিনামূল্যে", + "Confirm": "নিশ্চিত করুন", + "Confirm cancellation": "বাতিল নিশ্চিত করুন", + "Confirm subscription": "সাবস্ক্রিপশন নিশ্চিত করুন", + "Contact support": "সাপোর্টের সাথে যোগাযোগ করুন", + "Continue": "চালিয়ে যান", + "Continue subscription": "সাবস্ক্রিপশন চালিয়ে যান", + "Could not create stripe checkout session": "", + "Could not sign in. Login link expired.": "সাইন ইন করতে পারেনি। লগইন লিঙ্কের মেয়াদ শেষ হয়েছে।", + "Could not update email! Invalid link.": "ইমেল আপডেট করতে পারেনি! অবৈধ লিঙ্ক।", + "Create a new contact": "একটি নতুন যোগাযোগ তৈরি করুন", + "Current plan": "বর্তমান পরিকল্পনা", + "Delete account": "অ্যাকাউন্ট মুছে ফেলুন", + "Didn't mean to do this? Manage your preferences .": "এটি করার উদ্দেশ্য ছিল না? আপনার পছন্দগুলি পরিচালনা করুন ।", + "Don't have an account?": "অ্যাকাউন্ট নেই?", + "Edit": "সম্পাদনা করুন", + "Email": "ইমেল", + "Email newsletter": "ইমেল নিউজলেটার", + "Email newsletter settings updated": "", + "Email preferences": "ইমেল পছন্দ", + "Emails": "ইমেল", + "Emails disabled": "ইমেল নিষ্ক্রিয়", + "Ends {{offerEndDate}}": "{{offerEndDate}} এ শেষ হবে", + "Enter your email address": "", + "Enter your name": "", + "Error": "ত্রুটি", + "Expires {{expiryDate}}": "{{expiryDate}} এ মেয়াদ শেষ", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", + "Forever": "চিরকাল", + "Free Trial – Ends {{trialEnd}}": "ফ্রি ট্রায়াল – {{trialEnd}} এ শেষ হবে", + "Get help": "সাহায্য পান", + "Get in touch for help": "সহায়তার জন্য যোগাযোগ করুন", + "Get notified when someone replies to your comment": "কেউ আপনার মন্তব্যের উত্তর দিলে জানানো হবে", + "Give feedback on this post": "এই পোস্টে প্রতিক্রিয়া দিন", + "Help! I'm not receiving emails": "সাহায্য! আমি ইমেল পাচ্ছি না", + "Here are a few other sites you may enjoy.": "এখানে কয়েকটি অন্যান্য সাইট রয়েছে যা আপনি উপভোগ করতে পারেন।", + "If a newsletter is flagged as spam, emails are automatically disabled for that address to make sure you no longer receive any unwanted messages.": "যদি একটি নিউজলেটার স্প্যাম হিসেবে চিহ্নিত হয়, তবে ইমেলগুলি স্বয়ংক্রিয়ভাবে সেই ঠিকানার জন্য নিষ্ক্রিয় করা হয় যাতে আপনি আর কোনো অবাঞ্ছিত বার্তা না পান।", + "If the spam complaint was accidental, or you would like to begin receiving emails again, you can resubscribe to emails by clicking the button on the previous screen.": "যদি স্প্যাম অভিযোগটি দুর্ঘটনাক্রমে হয়ে থাকে, অথবা আপনি আবার ইমেলগুলি পেতে শুরু করতে চান, তবে পূর্ববর্তী স্ক্রিনে বোতামটি ক্লিক করে আপনি ইমেলগুলিতে পুনঃসাবস্ক্রাইব করতে পারেন।", + "If you cancel your subscription now, you will continue to have access until {{periodEnd}}.": "আপনি যদি এখন আপনার সাবস্ক্রিপশন বাতিল করেন, তবে {{periodEnd}} পর্যন্ত আপনার প্রবেশাধিকার থাকবে।", + "If you have a corporate or government email account, reach out to your IT department and ask them to allow emails to be received from {{senderEmail}}": "যদি আপনার কর্পোরেট বা সরকারি ইমেল অ্যাকাউন্ট থাকে, তবে আপনার আইটি বিভাগের সাথে যোগাযোগ করুন এবং তাদেরকে বলুন {{senderEmail}} থেকে ইমেলগুলি গ্রহণের অনুমতি দিতে।", + "If you would like to start receiving emails again, the best next steps are to check your email address on file for any issues and then click resubscribe on the previous screen.": "আপনি যদি আবার ইমেল পেতে শুরু করতে চান, তবে সেরা পরবর্তী পদক্ষেপগুলি হল যে কোনো সমস্যার জন্য আপনার ফাইলে থাকা ইমেল ঠিকানাটি পরীক্ষা করা এবং তারপর পূর্ববর্তী স্ক্রিনে পুনঃসাবস্ক্রাইব ক্লিক করা।", + "If you're not receiving the email newsletter you've subscribed to, here are a few things to check.": "আপনি যদি সাবস্ক্রাইব করা ইমেল নিউজলেটারটি না পেয়ে থাকেন, তবে এখানে কয়েকটি বিষয় পরীক্ষা করতে পারেন।", + "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "আপনি যদি এই সমস্ত পরীক্ষা সম্পূর্ণ করেন এবং এখনও ইমেল না পান, তবে {{supportAddress}} এর সাথে যোগাযোগ করে সাপোর্ট পেতে পারেন।", + "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "একটি নিউজলেটার পাঠানোর চেষ্টা করার সময় স্থায়ী ব্যর্থতা পাওয়া গেলে, অ্যাকাউন্টে ইমেলগুলি নিষ্ক্রিয় করা হবে।", + "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "আপনার ইমেল ক্লায়েন্টে {{senderEmail}} কে আপনার যোগাযোগ তালিকায় যোগ করুন। এটি আপনার মেইল প্রোভাইডারকে সংকেত দেয় যে এই ঠিকানা থেকে পাঠানো ইমেলগুলি বিশ্বাসযোগ্য হওয়া উচিত।", + "Invalid email address": "", + "Jamie Larson": "", + "jamie@example.com": "", + "Less like this": "এটির মতো কম", + "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "নিশ্চিত করুন যে ইমেলগুলি দুর্ঘটনাক্রমে আপনার ইনবক্সের স্প্যাম বা প্রমোশন ফোল্ডারে শেষ হচ্ছে না। যদি তারা হয়, তাহলে \"স্প্যাম নয়\" চিহ্নিত করুন এবং/অথবা \"ইনবক্সে সরান\" ক্লিক করুন।", + "Manage": "পরিচালনা করুন", + "Maybe later": "সম্ভবত পরে", + "Memberships unavailable, contact the owner for access.": "সদস্যতা অপ্রাপ্য, প্রবেশাধিকার পেতে মালিকের সাথে যোগাযোগ করুন।", + "month": "", + "Monthly": "মাসিক", + "More like this": "এটির মতো আরো", + "Name": "নাম", + "Need more help? Contact support": "আরও সাহায্য প্রয়োজন? সাপোর্টের সাথে যোগাযোগ করুন", + "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "আপনার অ্যাকাউন্টে দুটি কারণে নিউজলেটার নিষ্ক্রিয় করা যেতে পারে: একটি পূর্ববর্তী ইমেল স্প্যাম হিসাবে চিহ্নিত করা হয়েছিল, অথবা একটি ইমেল পাঠানোর চেষ্টা স্থায়ী ব্যর্থতার (বাউন্স) ফলাফল।", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", + "Not receiving emails?": "ইমেল পাচ্ছেন না?", + "Now check your email!": "এখন আপনার ইমেল চেক করুন!", + "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "একবার পুনঃসাবস্ক্রাইব করার পর, যদি আপনি এখনও আপনার ইনবক্সে ইমেল না দেখেন, তবে আপনার স্প্যাম ফোল্ডার চেক করুন। কিছু ইনবক্স প্রোভাইডার পূর্ববর্তী স্প্যাম অভিযোগগুলির একটি রেকর্ড রাখে এবং ইমেলগুলি চিহ্নিত করতে থাকে। যদি এটি ঘটে, তাহলে সর্বশেষ নিউজলেটারটিকে 'স্প্যাম নয়' হিসাবে চিহ্নিত করুন যাতে এটি আপনার প্রাথমিক ইনবক্সে ফিরে যায়।", + "Permanent failure (bounce)": "স্থায়ী ব্যর্থতা (বাউন্স)", + "Phone number": "", + "Plan": "পরিকল্পনা", + "Plan checkout was cancelled.": "পরিকল্পনার চেকআউট বাতিল করা হয়েছে।", + "Plan upgrade was cancelled.": "পরিকল্পনার আপগ্রেড বাতিল করা হয়েছে।", + "Please contact {{supportAddress}} to adjust your complimentary subscription.": "আপনার বিনামূল্যের সাবস্ক্রিপশন সমন্বয় করতে অনুগ্রহ করে {{supportAddress}} এর সাথে যোগাযোগ করুন।", + "Please enter {{fieldName}}": "", + "Please fill in required fields": "প্রয়োজনীয় ক্ষেত্রগুলি পূরণ করুন", + "Price": "মূল্য", + "Re-enable emails": "ইমেলগুলি পুনরায় সক্রিয় করুন", + "Recommendations": "প্রস্তাবনা", + "Renews at {{price}}.": "{{price}} এ নবায়ন হবে।", + "Retry": "আবার চেষ্টা করুন", + "Save": "সংরক্ষণ করুন", + "Send an email and say hi!": "একটি ইমেল পাঠান এবং হাই বলুন!", + "Send an email to {{senderEmail}} and say hello. This can also help signal to your mail provider that emails to and from this address should be trusted.": "{{senderEmail}} এ একটি ইমেল পাঠান এবং হ্যালো বলুন। এটি আপনার মেইল প্রোভাইডারকে সংকেত দিতে সাহায্য করতে পারে যে এই ঠিকানায় এবং এই ঠিকানা থেকে ইমেলগুলি বিশ্বাসযোগ্য হওয়া উচিত।", + "Sending login link...": "লগইন লিঙ্ক পাঠানো হচ্ছে...", + "Sending...": "পাঠানো হচ্ছে...", + "Show all": "সব দেখান", + "Sign in": "সাইন ইন করুন", + "Sign out": "সাইন আউট করুন", + "Sign up": "সাইন আপ করুন", + "Signup error: Invalid link": "সাইন আপ ত্রুটি: অবৈধ লিঙ্ক", + "Something went wrong, please try again later.": "", + "Sorry, that didn’t work.": "দুঃখিত, এটি কাজ করেনি।", + "Spam complaints": "স্প্যাম অভিযোগ", + "Start {{amount}}-day free trial": "{{amount}}-দিনের ফ্রি ট্রায়াল শুরু করুন", + "Starting {{startDate}}": "{{startDate}} থেকে শুরু", + "Starting today": "আজ থেকে শুরু", + "Submit feedback": "প্রতিক্রিয়া জমা দিন", + "Subscribe": "সাবস্ক্রাইব করুন", + "Subscribed": "সাবস্ক্রাইব করা হয়েছে", + "Subscription plan updated successfully": "", + "Success": "সফলতা", + "Success! Check your email for magic link to sign-in.": "সফলতা! সাইন ইন করার জন্য ম্যাজিক লিঙ্কের জন্য আপনার ইমেল চেক করুন।", + "Success! Your account is fully activated, you now have access to all content.": "সফলতা! আপনার অ্যাকাউন্ট সম্পূর্ণরূপে সক্রিয় হয়েছে, এখন আপনি সমস্ত কন্টেন্টে প্রবেশ করতে পারবেন।", + "Success! Your email is updated.": "সফলতা! আপনার ইমেল আপডেট হয়েছে।", + "Successfully unsubscribed": "সফলভাবে আনসাবস্ক্রাইব করা হয়েছে", + "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "সাবস্ক্রাইব করার জন্য ধন্যবাদ। পড়া শুরু করার আগে, নিচে কয়েকটি অন্যান্য সাইট রয়েছে যা আপনি উপভোগ করতে পারেন।", + "Thank you for your support": "", + "Thank you for your support!": "", + "Thanks for the feedback!": "প্রতিক্রিয়ার জন্য ধন্যবাদ!", + "That didn't go to plan": "পরিকল্পনা অনুযায়ী হয়নি", + "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "আমাদের কাছে আপনার জন্য যে ইমেল ঠিকানাটি রয়েছে তা হল {{memberEmail}} — যদি এটি সঠিক না হয়, তবে আপনি আপনার এটি আপডেট করতে পারেন।", + "There was a problem submitting your feedback. Please try again a little later.": "আপনার প্রতিক্রিয়া জমা দেওয়ার সময় একটি সমস্যা হয়েছে। অনুগ্রহ করে একটু পরে আবার চেষ্টা করুন।", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", + "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", + "This site is invite-only, contact the owner for access.": "এই সাইটটি কেবল আমন্ত্রণের মাধ্যমে, প্রবেশাধিকার পেতে মালিকের সাথে যোগাযোগ করুন।", + "This site is not accepting payments at the moment.": "", + "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "সাইন আপ সম্পূর্ণ করতে, আপনার ইনবক্সে নিশ্চিতকরণ লিঙ্কে ক্লিক করুন। যদি এটি ৩ মিনিটের মধ্যে না আসে, তবে আপনার স্প্যাম ফোল্ডার চেক করুন!", + "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Try free for {{amount}} days, then {{originalPrice}}.": "{{amount}} দিনের জন্য ফ্রি চেষ্টা করুন, তারপর {{originalPrice}}।", + "Unable to initiate checkout session": "", + "Unlock access to all newsletters by becoming a paid subscriber.": "পেইড সাবস্ক্রাইবার হয়ে সমস্ত নিউজলেটারে প্রবেশাধিকার আনলক করুন।", + "Unsubscribe from all emails": "সমস্ত ইমেল থেকে আনসাবস্ক্রাইব করুন", + "Unsubscribed": "আনসাবস্ক্রাইব করা হয়েছে", + "Unsubscribed from all emails.": "সমস্ত ইমেল থেকে আনসাবস্ক্রাইব করা হয়েছে।", + "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "ইমেল থেকে আনসাবস্ক্রাইব করা {{title}} এর জন্য আপনার পেইড সাবস্ক্রিপশন বাতিল করবে না", + "Update": "আপডেট করুন", + "Update your preferences": "আপনার পছন্দগুলি আপডেট করুন", + "Verification link sent, check your inbox": "যাচাইকরণ লিঙ্ক পাঠানো হয়েছে, আপনার ইনবক্স চেক করুন", + "Verify your email address is correct": "আপনার ইমেল ঠিকানা সঠিক কিনা যাচাই করুন", + "View plans": "পরিকল্পনা দেখুন", + "We couldn't unsubscribe you as the email address was not found. Please contact the site owner.": "আমরা আপনাকে আনসাবস্ক্রাইব করতে পারিনি কারণ ইমেল ঠিকানা পাওয়া যায়নি। অনুগ্রহ করে সাইট মালিকের সাথে যোগাযোগ করুন।", + "Welcome back, {{name}}!": "আবার স্বাগতম, {{name}}!", + "Welcome back!": "আবার স্বাগতম!", + "Welcome to {{siteTitle}}": "{{siteTitle}} এ স্বাগতম", + "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "যখন একটি ইনবক্স একটি ইমেল গ্রহণ করতে ব্যর্থ হয় তখন এটি সাধারণত বাউন্স হিসাবে পরিচিত। অনেক ক্ষেত্রে, এটি অস্থায়ী হতে পারে। তবে, কিছু ক্ষেত্রে, একটি বাউন্সড ইমেল একটি স্থায়ী ব্যর্থতা হিসাবে ফেরত আসতে পারে যখন একটি ইমেল ঠিকানা অবৈধ বা অস্তিত্বহীন হয়।", + "Why has my email been disabled?": "আমার ইমেল নিষ্ক্রিয় করা হয়েছে কেন?", + "year": "", + "Yearly": "বার্ষিক", + "You currently have a free membership, upgrade to a paid subscription for full access.": "আপনার বর্তমানে একটি ফ্রি সদস্যতা রয়েছে, সম্পূর্ণ প্রবেশাধিকারের জন্য একটি পেইড সাবস্ক্রিপশনে আপগ্রেড করুন।", + "You have been successfully resubscribed": "আপনি সফলভাবে পুনঃসাবস্ক্রাইব হয়েছেন", + "You're currently not receiving emails": "আপনি বর্তমানে ইমেল পাচ্ছেন না", + "You're not receiving emails": "আপনি ইমেল পাচ্ছেন না", + "You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "আপনি ইমেল পাচ্ছেন না কারণ আপনি হয়তো একটি সাম্প্রতিক বার্তা স্প্যাম হিসেবে চিহ্নিত করেছেন, অথবা বার্তাগুলি আপনার প্রদত্ত ইমেল ঠিকানায় বিতরণ করা যায়নি।", + "You've successfully signed in.": "আপনি সফলভাবে সাইন ইন করেছেন।", + "You've successfully subscribed to": "আপনি সফলভাবে সাবস্ক্রাইব করেছেন", + "Your account": "আপনার অ্যাকাউন্ট", + "Your email has failed to resubscribe, please try again": "", + "Your input helps shape what gets published.": "আপনার ইনপুট প্রকাশিত হওয়ার বিষয়টি নির্ধারণ করতে সহায়তা করে।", + "Your subscription will expire on {{expiryDate}}": "আপনার সাবস্ক্রিপশনের মেয়াদ {{expiryDate}} এ শেষ হবে", + "Your subscription will renew on {{renewalDate}}": "আপনার সাবস্ক্রিপশন {{renewalDate}} এ নবায়ন হবে", + "Your subscription will start on {{subscriptionStart}}": "আপনার সাবস্ক্রিপশন {{subscriptionStart}} থেকে শুরু হবে" +} diff --git a/ghost/i18n/locales/bn/search.json b/ghost/i18n/locales/bn/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/bn/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/bn/signup-form.json b/ghost/i18n/locales/bn/signup-form.json new file mode 100644 index 00000000000..69d5a8fec64 --- /dev/null +++ b/ghost/i18n/locales/bn/signup-form.json @@ -0,0 +1,9 @@ +{ + "Email sent": "ইমেইল পাঠানো হয়েছে", + "Now check your email!": "আপনার ইমেল চেক করুন ", + "Please enter a valid email address": "অনুগ্রহপূর্বক একটি বৈধ ইমেইল ঠিকানা ইনপুট করুন", + "Something went wrong, please try again.": "কিছু সমস্যা হয়েছে, অনুগ্রহ করে আবার চেষ্টা করুন।", + "Subscribe": "সাবস্ক্রাইব করুন", + "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "সাইন আপ সম্পূর্ণ করতে, আপনার ইনবক্সে নিশ্চিতকরণ লিঙ্কে ক্লিক করুন। যদি এটি ৩ মিনিটের মধ্যে না আসে, আপনার স্প্যাম ফোল্ডারটি পরীক্ষা করে দেখুন!", + "Your email address": "আপনার ইমেইল ঠিকানা" +} diff --git a/ghost/i18n/locales/bs/comments.json b/ghost/i18n/locales/bs/comments.json index 66e53172f24..1a3145f69f4 100644 --- a/ghost/i18n/locales/bs/comments.json +++ b/ghost/i18n/locales/bs/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Odgovori na komentar", "Report": "Prijavi", "Report comment": "Prijavi komentar", - "Report this comment": "Prijavi ovaj komentar", "Report this comment?": "Prijavi ovaj komentar?", "Save": "Sačuvaj", "Sending": "Šaljem", @@ -68,6 +67,5 @@ "This comment has been removed.": "Ovaj komentar je uklonjen.", "Upgrade now": "Nadogradi račun sada", "Yesterday": "Jučer", - "You want to report this comment?": "Želiš li prijaviti ovaj komentar?", "Your request will be sent to the owner of this site.": "Tvoj zahtjev je poslan vlasniku stranice." } diff --git a/ghost/i18n/locales/bs/portal.json b/ghost/i18n/locales/bs/portal.json index 029078a381e..35f34ef4397 100644 --- a/ghost/i18n/locales/bs/portal.json +++ b/ghost/i18n/locales/bs/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Link za prijavu poslan je na tvoju Email adresu. Ako ne stigne u roku od 3 minute, provjeri spam folder.", "Account": "Račun", + "Account details updated successfully": "", "Account settings": "Postavke računa", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Nakon isteka besplatnog probnog perioda, bit će ti naplaćena redovna cijena za plan koji si odabrao. Možeš otkazati članarinu prije toga.", "Already a member?": "Već si član?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Došlo je do neočekivane greška. Pokušaj ponovo ili kontaktiraj podršku ako se greška ponovi.", "Back": "Nazad", "Back to Log in": "Nazad na prijavu", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Provjeri spam folder i posebne pod-foldere", "Check with your mail provider": "Provjeri s Email providerom", + "Check your inbox to verify email update": "", "Choose": "Odaberi", "Choose a different plan": "Odaberi drugi plan", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Kontaktiraj podršku", "Continue": "Nastavi", "Continue subscription": "Produži pretplatu", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Nije se moguće prijaviti. Link za prijavu je istekao.", "Could not update email! Invalid link.": "Nije moguće promijeniti Email! Pogrešan link.", "Create a new contact": "Dodaj novi kontakt", @@ -52,6 +56,7 @@ "Edit": "Uredi", "Email": "Email", "Email newsletter": "Email newsletter", + "Email newsletter settings updated": "", "Email preferences": "Postavke Email-a", "Emails": "Email adrese", "Emails disabled": "Onemogućene Email adrese", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Greška", "Expires {{expiryDate}}": "Ističe {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Zauvijek", "Free Trial – Ends {{trialEnd}}": "Besplatni probni period – Ističe {{trialEnd}}", "Get help": "Zatraži pomoć", @@ -91,6 +108,8 @@ "Name": "Ime", "Need more help? Contact support": "Treba ti dodatna pomoć? Kontaktiraj podršku", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Newletteri na vašem računu se mogu onemogućiti iz dva razloga: Prethodni Email označena je kao spam ili pokušaj slanja Email poruke rezultirao je greškom (došlo je do odbacivanja nakon više neuspjelih pokušaja).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Ne primaš Email poruke?", "Now check your email!": "Provjeri svoju Email adresu!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Nakon što se ponovno pretplatite, ako još uvijek ne vidiš Email u svom inboxu, provjeri spam folder. Neki pružatelji Email usluga čuvaju zapis prethodnih pritužbi na spam i nastavit će ih označavati kao takve. Ako se to dogodi, označite najnoviji newsletter Email kao \"Nije neželjena pošta\" da biste ga vratili u primarni inbox.", @@ -126,6 +145,7 @@ "Submit feedback": "Pošalji povratne informacije", "Subscribe": "Kreiraj račun", "Subscribed": "Pretplata uspješna", + "Subscription plan updated successfully": "", "Success": "Uspjeh", "Success! Check your email for magic link to sign-in.": "Uspjeh! Provjeri svoju Email adresu za link za prijavu.", "Success! Your account is fully activated, you now have access to all content.": "Uspjeh! Tvoj je račun aktiviran, sada imaš pristup svim sadržajima.", @@ -138,12 +158,22 @@ "That didn't go to plan": "To nije pošlo prema planu", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Email adresa koju smo spasili za tebe je {{memberEmail}} — ako to nije tačno, možeš je promijeniti u .", "There was a problem submitting your feedback. Please try again a little later.": "Došlo je do problema prilikom slanja tvojih povratnih informacija. Molimo pokušaj ponovo malo kasnije.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Ova je stranica samo na poziv, kontaktiraj vlasnika za pristup.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Za nastavak procesa registracije, klikni na potvrdni link u svom inboxu. Ako ne stigne u roku od 3 minute, provjeri spam folder!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Isprobaj besplatno {{amount}} dana, a zatim plati {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Otključaj pristup svim newsletterima tako što ćete postati plaćeni član.", "Unsubscribe from all emails": "Odjava sa svih Email poruka", "Unsubscribed": "Odjavljeno", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Uspješna prijava.", "You've successfully subscribed to": "Uspješna pretplata na", "Your account": "Tvoj račun", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Tvoj feedback pomaže pri odabiru tema koje se objavljuju.", "Your subscription will expire on {{expiryDate}}": "Tvoja pretplata istieče {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Tvoja pretplata će se obnoviti {{renewalDate}}", diff --git a/ghost/i18n/locales/bs/search.json b/ghost/i18n/locales/bs/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/bs/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/ca/comments.json b/ghost/i18n/locales/ca/comments.json index a7307ef1367..346d2291d43 100644 --- a/ghost/i18n/locales/ca/comments.json +++ b/ghost/i18n/locales/ca/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Respondre al comentari", "Report": "Reportar", "Report comment": "Reportar comentari", - "Report this comment": "Reportar aquest comentari", "Report this comment?": "Reportar aquest comentari?", "Save": "Desa", "Sending": "Enviant", @@ -68,6 +67,5 @@ "This comment has been removed.": "Aquest comantari s'ha esborrat.", "Upgrade now": "Millora ara", "Yesterday": "Ahir", - "You want to report this comment?": "Vols reportar aquest comentari?", "Your request will be sent to the owner of this site.": "La vostra sol·licitud s'enviarà al propietari d'aquest lloc." } diff --git a/ghost/i18n/locales/ca/portal.json b/ghost/i18n/locales/ca/portal.json index 26b1cb1eee1..a6686cce4bf 100644 --- a/ghost/i18n/locales/ca/portal.json +++ b/ghost/i18n/locales/ca/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "S'ha enviat un enllaç d'inici de sessió al teu correu electrònic. Si no t'arriba en 3 minuts, reivsa la teva carpeta de correu brossa.", "Account": "Compte", + "Account details updated successfully": "", "Account settings": "Configuració del compte", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Després que finalitzi el període de prova gratuït, se't cobrarà el preu regular del nivell que hagis triat. Sempre pots cancel·lar abans d'això.", "Already a member?": "Ja ets membre?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Un error inesperat ha passat. Si us plau, torneu a provar o contacti amb suport tècnic si l'error persisteix.", "Back": "Tornar enrere", "Back to Log in": "Tornar al inici de sessió", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Comproveu les carpetes de correu brossa i promocions", "Check with your mail provider": "Consulteu amb el vostre proveïdor de correu", + "Check your inbox to verify email update": "", "Choose": "Tria", "Choose a different plan": "Seleccioneu un pla diferent", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Contactar amb suport tècnic", "Continue": "Continuar", "Continue subscription": "Continuar subscripció", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "No s'ha pogut iniciar sessió. L'enllaç d'inici de sessió ha expirat.", "Could not update email! Invalid link.": "No s'ha pogut actualitzar el correu electrònic! Enllaç no vàlid.", "Create a new contact": "Crear un nou contacte", @@ -52,6 +56,7 @@ "Edit": "Editar", "Email": "Correu electrònic", "Email newsletter": "Butlletí informatiu per correu electrònic", + "Email newsletter settings updated": "", "Email preferences": "Preferència del correu electrònic", "Emails": "Correus electrònics", "Emails disabled": "Correus electrònics desactivats", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Error", "Expires {{expiryDate}}": "Expira el {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "per sempre", "Free Trial – Ends {{trialEnd}}": "Prova gratuita - Finalitza el {{trialEnd}}", "Get help": "Demanar ajuda", @@ -91,6 +108,8 @@ "Name": "Nom", "Need more help? Contact support": "Necessites més ajuda? Contacte de suport", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Els butlletins de notícies es poden desactivar al vostre compte per dos motius: un correu electrònic anterior s'ha marcat com a correu brossa o si s'ha intentat enviar un correu electrònic ha provocat un error permanent (rebot).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "No reps els correus electrònics?", "Now check your email!": "Revisar ara el teu correu electrònic!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Un cop us torneu a subscriure, si encara no veieu correus electrònics a la vostra safata d'entrada, comproveu la vostra carpeta de correu brossa. Alguns proveïdors de safates d'entrada mantenen un registre de les queixes de correu brossa anteriors i continuaran marcant els correus electrònics. Si això passa, marqueu el darrer butlletí com a \"No és correu brossa\". ' per tornar-lo a moure a la safata d'entrada principal.", @@ -126,6 +145,7 @@ "Submit feedback": "Enviar comentaris", "Subscribe": "Subscriu-te", "Subscribed": "Subscrit", + "Subscription plan updated successfully": "", "Success": "Amb èxit", "Success! Check your email for magic link to sign-in.": "Amb èxit! Comproveu el vostre correu electrònic per trobar l'enllaç màgic per iniciar la sessió.", "Success! Your account is fully activated, you now have access to all content.": "Amb èxit! El vostre compte està totalment activat, ara teniu accés a tot el contingut.", @@ -138,12 +158,22 @@ "That didn't go to plan": "Això no ha anat com estava previst", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "'adreça electrònica que tenim per a vostè és {{memberEmail}}; si no és correcta, podeu actualitzar-la a l'.", "There was a problem submitting your feedback. Please try again a little later.": "Hi ha hagut un problema en enviar els vostres comentaris. Torneu-ho a provar una mica més tard.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Aquest llog és només per invitació, contacta amb el propietari per obtenir accés.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Per completar el registre, fes clic sobre l'enllaç de confirmació al teu correu electrònic. Si no t'arria en 3 minuts, revisa la teva carpeta de correu brossa!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Prova gratis durant {{amount}} dies i després {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Desbloqueja l'accés a tots els butlletins de notícies fent-te un subscriptor de pagament.", "Unsubscribe from all emails": "Cancel·la les subscripcions a tots els correus electrònics", "Unsubscribed": "S'ha cancel·lat la subscripció", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Heu iniciat la sessió correctament.", "You've successfully subscribed to": "Us heu subscrit correctament a", "Your account": "El teu copte", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "La teva opinió ajuda a definir el que es publica.", "Your subscription will expire on {{expiryDate}}": "La vostra subscripció caducarà el {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "La vostra subscripció es renovarà el {{renewalDate}}", diff --git a/ghost/i18n/locales/ca/search.json b/ghost/i18n/locales/ca/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/ca/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/context.json b/ghost/i18n/locales/context.json index 13cf9b627bb..94a341c3326 100644 --- a/ghost/i18n/locales/context.json +++ b/ghost/i18n/locales/context.json @@ -4,6 +4,7 @@ "1 comment": "Comment count displayed above the comments section in case there is only one", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "A message displayed during signup process.", "Account": "A label in Portal for your account area", + "Account details updated successfully": "", "Account settings": "A label in Portal for your account settings", "Add comment": "Button text to post a comment", "Add context to your comment, share your name and expertise to foster a healthy discussion.": "Invitation to include additional info when commenting", @@ -11,8 +12,10 @@ "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Confirmation message explaining how free trials work during signup", "All the best!": "A light-hearted ending to an email", "Already a member?": "A link displayed on signup screen, inviting people to log in if they have already signed up previously", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Notification if an unexpected error occurs.", "Anonymous": "Comment placed by a member without a name", + "Authors": "", "Back": "A button to return to the previous page", "Back to Log in": "A button to return to the login screen", "Become a member of {{publication}} to start commenting.": "A call to action letting people know they need to sign up before commenting", @@ -27,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "A section title in email receiving FAQ", "Check with your mail provider": "A section title in email receiving FAQ", + "Check your inbox to verify email update": "", "Choose": "A button to select a plan", "Choose a different plan": "A button for members to switch between plans", "Choose a plan": "", @@ -50,6 +54,7 @@ "Contact support": "Button to send an email to support", "Continue": "Continue button", "Continue subscription": "Button to continue a (cancelled) paid subscription", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Message when a login link has expired", "Could not update email! Invalid link.": "Message when an email update link is invalid", "Create a new contact": "A section title in email receiving FAQ", @@ -64,6 +69,7 @@ "Edit this comment": "Context menu action to edit a comment", "Email": "A label for email address input", "Email newsletter": "Title for the email newsletter settings", + "Email newsletter settings updated": "", "Email preferences": "A label for email settings", "Email sent": "Button text after being clicked, when an email has been sent to confirm a new subscription. Should be short.", "Emails": "A label for a list of emails", @@ -74,6 +80,18 @@ "Error": "Status indicator for a notification", "Expertise": "Input label when editing your expertise in the comments app", "Expires {{expiryDate}}": "Label for a subscription which expires", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "For your security, the link will expire in 24 hours time.": "Descriptive text in emails about authentication links", "Forever": "Label for a discounted price which has no expiry date", "Founder @ Acme Inc": "Placeholder value for the input box when editing your expertise", @@ -118,6 +136,9 @@ "Need more help? Contact support": "A link to contact support", "Neurosurgeon": "Example of an expertise of a person used in comments when editing your expertise", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "A paragraph in the email suppression FAQ", + "No matches found": "", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "A link in portal to take members to an FAQ area about what to do if you're not receiving emails", "Now check your email!": "A confirmation message after logging in or signing up", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "A paragraph in the email suppression FAQ", @@ -137,18 +158,19 @@ "Please enter a valid email address": "Err message when an email address is invalid", "Please enter {{fieldName}}": "Error message when a required field is missing", "Please fill in required fields": "Error message when a required field is missing", + "Posts": "", "Price": "A label to indicate price of a tier", "Re-enable emails": "A button for members to turn-back-on emails, if they have been previously disabled as a result of delivery failures", "Recommendations": "A suggestion by/for another site of interest to users", "Renews at {{price}}.": "Label for a discounted subscription, explains the price it will renew at", "Reply": "Button to reply to a comment", "Reply to comment": "Placeholder value of the input box when placing a reply to a comment", - "Report": "Used in the context menu of a comment on smaller devices", + "Report": "Button to report a comment", "Report comment": "Used in the context menu", - "Report this comment": "Button to report a comment", - "Report this comment?": "Title of the modal to report a comment on smaller devices", + "Report this comment?": "Title of the modal to report a comment", "Retry": "When something goes wrong, this link allows people to re-attempt the same action", "Save": "A button to save", + "Search posts, tags and authors": "", "Secure sign in link for {{siteTitle}}": "The subject line of member login emails", "See you soon!": "A sign-off/ending to a signup email", "Send an email and say hi!": "A section title in email receiving FAQ", @@ -163,6 +185,7 @@ "Show 1 previous comment": "Button in comments app to load previous comments, when there is only one", "Show all": "Show all recommendations", "Show comment": "Context menu action for a comment, for administrators", + "Show more results": "", "Show {{amount}} more replies": "Button text to load more replies for a comment", "Show {{amount}} previous comments": "Button in comments appto load previous comments", "Sign in": "A button to sign in", @@ -182,11 +205,13 @@ "Submit feedback": "A button for submitting member feedback", "Subscribe": "Title of a section for subscribing to a newsletter", "Subscribed": "Status of a newsletter which a member has subscribed to", + "Subscription plan updated successfully": "", "Success": "Status indicator for a notification", "Success! Check your email for magic link to sign-in.": "Notification text when the user has been sent a magic link to sign-in", "Success! Your account is fully activated, you now have access to all content.": "Notification text when a user has activated their email and can access content", "Success! Your email is updated.": "Notification text when a user has updated their email address", "Successfully unsubscribed": "A confirmation message when a member clicks an unsubscribe link", + "Tags": "", "Tap the link below to complete the signup process for {{siteTitle}}, and be automatically signed in:": "Descriptive text in signup emails, right before the button to confirm signup", "Thank you for signing up to {{siteTitle}}!": "A success message, confirming that a member has 'signed up' and created an account", "Thank you for subscribing to {{siteTitle}}!": "A success message, confirming that a member has 'subscribed' to a newsletter", @@ -199,7 +224,10 @@ "That didn't go to plan": "An error message indicating something went wrong", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Message confirming the user's email is correct, with a button linking to the account settings page", "There was a problem submitting your feedback. Please try again a little later.": "An error message for when submitting feedback has failed", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This comment has been hidden.": "Text for a comment thas was hidden", "This comment has been removed.": "Text for a comment thas was removed", "This email address will not be used.": "This is in the footer of signup verification emails, and comes right after 'If you did not make this request, you can simply delete this message.'", @@ -207,7 +235,14 @@ "This site is not accepting payments at the moment.": "An error message shown when a tips or donations link is opened but the site has donations disabled", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "A confirmation message displayed during the signup process, indicating that the person signing up needs to go and check their email - and reminding them to check their spam folder, too", "To continue to stay up to date, subscribe to {{publication}} below.": "A message shown in the success modal after a non-member has made a tip or donation", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "A label for an offer with a free trial", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "A message to encourage members to upgrade to a paid subscription", "Unsubscribe from all emails": "A button on the unsubscribe page, offering a shortcut to unsubscribe from every newsletter at the same time", "Unsubscribed": "Status of a newsletter which a user has not subscribed to", @@ -232,7 +267,6 @@ "You can also copy & paste this URL into your browser:": "Descriptive text displayed underneath the buttons in login/signup emails, right before authentication URLs which can be copy/pasted", "You currently have a free membership, upgrade to a paid subscription for full access.": "A message indicating that the member is using a free subcription, and could access more content with a paid subscription", "You have been successfully resubscribed": "A confirmation message when a member has had emails turned off, but they have been successfully turned back on", - "You want to report this comment?": "Title of the modal to report a comment on larger devices", "You will not be signed up, and no account will be created for you.": "Descriptive text in signup emails indicating that if someone does NOT click on the confirmation button, then they will not be signed up to anything. This text is intended to reassure people who receive a signup confirmation email that they did not ask for.", "You will not be subscribed.": "Descriptive text in signup emails indicating that if someone does NOT click on the confirmation button, then they will not be signed up to anything. This text is intended to reassure people who receive a signup confirmation email that they did not ask for.", "You're currently not receiving emails": "Message for user who have newsletters disabled", @@ -244,6 +278,7 @@ "You've successfully subscribed to": "A notification displayed when the user subscribed", "Your account": "A label indicating member account details", "Your email address": "Placeholder text in an input field", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Descriptive text displayed on the member feedback UI, telling people how their feedback is used", "Your request will be sent to the owner of this site.": "Descriptive text displayed in the report comment modal", "Your subscription will expire on {{expiryDate}}": "A message indicating when the member's subscription will expire", diff --git a/ghost/i18n/locales/cs/comments.json b/ghost/i18n/locales/cs/comments.json index 978d3b2e71d..88a862c56c6 100644 --- a/ghost/i18n/locales/cs/comments.json +++ b/ghost/i18n/locales/cs/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Odpovědět na komentář", "Report": "Nahlásit", "Report comment": "Nahlásit komentář", - "Report this comment": "Nahlásit tento komentář", "Report this comment?": "Nahlásit tento komentář?", "Save": "Uložit", "Sending": "Odesílání", @@ -68,6 +67,5 @@ "This comment has been removed.": "Tento komentář byl odstraněn.", "Upgrade now": "Upgradovat", "Yesterday": "Včera", - "You want to report this comment?": "Chcete nahlásit tento komentář?", "Your request will be sent to the owner of this site.": "Vaše žádost bude odeslána vlastníkovi tohoto webu." } diff --git a/ghost/i18n/locales/cs/portal.json b/ghost/i18n/locales/cs/portal.json index baa554bb643..71a3543f8b6 100644 --- a/ghost/i18n/locales/cs/portal.json +++ b/ghost/i18n/locales/cs/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "+1 (123) 456-7890", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Odkaz pro přihlášení byl odeslán do vaší e-mailové schránky. Pokud nepřijde do 3 minut, zkontrolujte prosím svou složku nevyžádaných zpráv (spam).", "Account": "Účet", + "Account details updated successfully": "", "Account settings": "Nastavení účtu", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Po skončení zkušební doby vám bude účtována běžná cena pro vybranou úroveň. Vždy můžete předtím zrušit odběr.", "Already a member?": "Již jste členem?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Došlo k neočekávané chybě. Zkuste to prosím znovu nebo kontaktujte podporu, pokud chyba přetrvává.", "Back": "Zpět", "Back to Log in": "Zpět na přihlášení", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Zkontrolujte složky spam a propagace", "Check with your mail provider": "Zkontrolujte u svého poskytovatele e-mailu", + "Check your inbox to verify email update": "", "Choose": "Vybrat", "Choose a different plan": "Vybrat jiný plán", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Kontaktovat podporu", "Continue": "Pokračovat", "Continue subscription": "Pokračovat v odběru", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Nelze se přihlásit. Přihlašovací odkaz vypršel.", "Could not update email! Invalid link.": "Nelze aktualizovat e-mail! Neplatný odkaz.", "Create a new contact": "Vytvořit nový kontakt", @@ -52,6 +56,7 @@ "Edit": "Upravit", "Email": "E-mail", "Email newsletter": "E-mailový newsletter", + "Email newsletter settings updated": "", "Email preferences": "Předvolby e-mailu", "Emails": "E-maily", "Emails disabled": "E-maily vypnuty", @@ -60,6 +65,18 @@ "Enter your name": "Zadejte své jméno", "Error": "Chyba", "Expires {{expiryDate}}": "Vyprší {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Navždy", "Free Trial – Ends {{trialEnd}}": "Zkušební verze zdarma – Končí {{trialEnd}}", "Get help": "Získat pomoc", @@ -91,6 +108,8 @@ "Name": "Jméno", "Need more help? Contact support": "Potřebujete další pomoc? Kontaktujte podporu", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Newslettery mohou být na vašem účtu deaktivovány ze dvou důvodů: Předchozí e-mail byl označen jako spam, nebo pokus o odeslání e-mailu skončil trvalým selháním (odrazem).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Nepřicházejí vám e-maily?", "Now check your email!": "Zkontrolujte svou e-mailovou schránku!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Po opětovném přihlášení, pokud stále nevidíte e-maily ve své schránce, zkontrolujte složku spam. Někteří poskytovatelé e-mailových schránek si uchovávají záznam o předchozích stížnostech na spam a budou nadále označovat e-maily. Pokud k tomu dojde, označte nejnovější newsletter jako 'Není spam', aby se přesunul zpět do vaší hlavní schránky.", @@ -126,6 +145,7 @@ "Submit feedback": "Odeslat zpětnou vazbu", "Subscribe": "Odebírat", "Subscribed": "odebíráte", + "Subscription plan updated successfully": "", "Success": "Úspěch", "Success! Check your email for magic link to sign-in.": "Úspěch! Zkontrolujte svůj e-mail pro magický odkaz k přihlášení.", "Success! Your account is fully activated, you now have access to all content.": "Úspěch! Váš účet je plně aktivován, nyní máte přístup k veškerému obsahu.", @@ -138,12 +158,22 @@ "That didn't go to plan": "Něco se nepovedlo", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "E-mailová adresa, kterou pro vás máme, je {{memberEmail}} — pokud to není správně, můžete ji aktualizovat v .", "There was a problem submitting your feedback. Please try again a little later.": "Při odesílání vaší zpětné vazby došlo k problému. Zkuste to prosím znovu o něco později.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "Při zpracování vaší platby došlo k chybě. Zkuste to prosím znovu.", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Tento web je pouze pro pozvané, kontaktujte provozovatele pro přístup.", "This site is not accepting payments at the moment.": "Tento web momentálně nepřijímá platby.", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Pro dokončení registrace klikněte na potvrzovací odkaz ve vaší e-mailové schránce. Pokud nepřijde do 3 minut, zkontrolujte prosím složku nevyžádaných zpráv (spam)!", "To continue to stay up to date, subscribe to {{publication}} below.": "Chcete-li zůstat informováni, přihlaste se k odběru {{publication}} níže.", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Vyzkoušejte zdarma na {{amount}} dní, poté {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Odemkněte přístup ke všem newsletterům tím, že se stanete placeným odběratelem.", "Unsubscribe from all emails": "Odhlásit se od všech e-mailů", "Unsubscribed": "Odhlášeno", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Úspěšně jste se přihlásili.", "You've successfully subscribed to": "Úspěšně jste se přihlásili k odběru", "Your account": "Váš účet", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Vaše připomínky pomáhají ladit a tvořit obsah webu.", "Your subscription will expire on {{expiryDate}}": "Vaše předplatné vyprší {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Vaše předplatné se obnoví {{renewalDate}}", diff --git a/ghost/i18n/locales/cs/search.json b/ghost/i18n/locales/cs/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/cs/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/da/comments.json b/ghost/i18n/locales/da/comments.json index 3457dcf21be..8e405f07d64 100644 --- a/ghost/i18n/locales/da/comments.json +++ b/ghost/i18n/locales/da/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Svar på kommentar", "Report": "Rapportér", "Report comment": "Rapportér kommentar", - "Report this comment": "Rapportér denne kommentar", "Report this comment?": "Rapportér denne kommentar?", "Save": "Gem", "Sending": "Sender", @@ -68,6 +67,5 @@ "This comment has been removed.": "Denne kommentar er blevet fjernet.", "Upgrade now": "Opgrader nu", "Yesterday": "I går", - "You want to report this comment?": "Vil du Rapportere denne kommentar?", "Your request will be sent to the owner of this site.": "Din anmodning vil blive sendt til ejeren af dette site." } diff --git a/ghost/i18n/locales/da/portal.json b/ghost/i18n/locales/da/portal.json index b3a6f518501..9853c28aeb0 100644 --- a/ghost/i18n/locales/da/portal.json +++ b/ghost/i18n/locales/da/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Der er blevet sendt en link til din email, du kan bruge til at logge ind med. Hvis det ikke kommer frem indenfor 3 minutter, så tjek din spam mappe.", "Account": "Konto", + "Account details updated successfully": "", "Account settings": "Kontoindstillinger", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Når din gratis prøveperiode udløber, vil du blive opkrævet den normale pris for det abonnement du har valgt. Du kan selvfølgelig altid annullere dit abonnement inden.", "Already a member?": "Er du allerede medlem?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Der opstod en uventet fejl. Prøv venligst igen eller kontakt support hvis fejlen fortsætter.", "Back": "Tilbage", "Back to Log in": "Tilbage til log ind", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Tjek spam og kampagner mapper", "Check with your mail provider": "Tjek med din e-mailudbyder", + "Check your inbox to verify email update": "", "Choose": "Vælg", "Choose a different plan": "Vælg et andet abonnement", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Kontakt support", "Continue": "Fortsæt", "Continue subscription": "Fortsæt tilmelding", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Kunne ikke log ind. Log ind link udløbet.", "Could not update email! Invalid link.": "Kunne ikke opdatere e-mail! Ugyldigt link.", "Create a new contact": "Opret ny kontakt", @@ -52,6 +56,7 @@ "Edit": "Rediger", "Email": "E-mail", "Email newsletter": "E-mail nyhedsbrev", + "Email newsletter settings updated": "", "Email preferences": "E-mail præferencer", "Emails": "E-mails", "Emails disabled": "E-mails deaktiveret", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Fejl", "Expires {{expiryDate}}": "Udløber {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "For evigt", "Free Trial – Ends {{trialEnd}}": "Gratis prøveperiode - Udløber {{trialEnd}}", "Get help": "Få hjælp", @@ -91,6 +108,8 @@ "Name": "Navn", "Need more help? Contact support": "Brug for hjælp? Kontakt support", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Nyhedsbreve kan deaktiveres på din konto af to årsager: En tidligere e-mail blev markeret som spam, eller forsøg på at sende en e-mail resulterede i en permanent fejl (bounce).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Modtager du ikke e-mails?", "Now check your email!": "Tjek din e-mail!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Når du har tilmeldt dig igen, hvis du stadig ikke kan se e-mails i din indbakke, skal du tjekke din spam-mappe. Nogle e-mailudbydere registrerer tidligere spam-klager og vil blive ved med at markere e-mails. Hvis dette sker, skal du markere det seneste nyhedsbrev som 'Ikke spam' for at flytte det tilbage til din primære indbakke.", @@ -126,6 +145,7 @@ "Submit feedback": "Send feedback", "Subscribe": "Tilmeld", "Subscribed": "Tilmeldt", + "Subscription plan updated successfully": "", "Success": "Succes", "Success! Check your email for magic link to sign-in.": "Sådan! Tjek din indbakke for et magisk link til at logge ind.", "Success! Your account is fully activated, you now have access to all content.": "Sådan! Din konto er fuldt aktiveret, du har nu adgang til alt indhold.", @@ -138,12 +158,22 @@ "That didn't go to plan": "Det gik ikke helt efter planen", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Den e-mailadresse, vi har på dig, er {{memberEmail}} – hvis det ikke er korrekt, kan du opdatere den i dit .", "There was a problem submitting your feedback. Please try again a little later.": "Der opstod et problem med at sende din feedback. Prøv venligst igen lidt senere.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Denne sider kræver at du skal være inviteret. Kontakt ejeres for at få adgang.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "For at gennemføre oprettelsen af din konto, skal du klikke på bekræftelseslinket i din e-mail indbakke. Hvis det ikke kommer indenfor 3 minutter, så tjek venligst din spam mappe!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Prøv gratis i {{amount}} dage, derefter {{original Price}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Lås op for adgang til alle nyhedsbreve ved at blive en betalingsabonnent.", "Unsubscribe from all emails": "Afmeld alle e-mails", "Unsubscribed": "Afmeldt", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Du er nu logget ind.", "You've successfully subscribed to": "Du er nu tilmeldt til", "Your account": "Din konto", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Dit input hjælper med at forme det der bliver publiceret.", "Your subscription will expire on {{expiryDate}}": "Dit abonnement udløber {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Dit abonnement fornyes {{renewalDate}}", diff --git a/ghost/i18n/locales/da/search.json b/ghost/i18n/locales/da/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/da/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/de-CH/comments.json b/ghost/i18n/locales/de-CH/comments.json index aca409c2797..afe3056f6af 100644 --- a/ghost/i18n/locales/de-CH/comments.json +++ b/ghost/i18n/locales/de-CH/comments.json @@ -1,5 +1,71 @@ { + "{{amount}} characters left": "", + "{{amount}} comments": "", + "{{amount}} days ago": "", + "{{amount}} hrs ago": "", + "{{amount}} mins ago": "", + "{{amount}} months ago": "", + "{{amount}} more": "", + "{{amount}} seconds ago": "", + "{{amount}} weeks ago": "", + "{{amount}} years ago": "", + "1 comment": "", + "Add comment": "", + "Add context to your comment, share your name and expertise to foster a healthy discussion.": "", + "Add reply": "", + "Already a member?": "", + "Anonymous": "", + "Become a member of {{publication}} to start commenting.": "", + "Become a paid member of {{publication}} to start commenting.": "", + "Cancel": "", + "Comment": "", + "Complete your profile": "", + "Delete": "", + "Deleted member": "", + "Discussion": "", + "Edit": "", + "Edit this comment": "", + "edited": "", + "Enter your name": "", + "Expertise": "", + "Founder @ Acme Inc": "", + "Full-time parent": "", + "Head of Marketing at Acme, Inc": "", + "Hide": "", + "Hide comment": "", + "Jamie Larson": "", + "Join the discussion": "", + "Just now": "", + "Local resident": "", + "Member discussion": "", + "Name": "", + "Neurosurgeon": "", + "One day ago": "", + "One hour ago": "", + "One min ago": "", + "One month ago": "", + "One week ago": "", + "One year ago": "", "Reply": "Antworten", + "Reply to comment": "", + "Report": "", + "Report comment": "", + "Report this comment?": "", + "Save": "", + "Sending": "", + "Sent": "", + "Show": "", + "Show {{amount}} more replies": "", "Show {{amount}} previous comments": "{{amount}} vorherige Kommentare anzeigen", - "Show 1 previous comment": "Einen vorherigen Kommentar anzeigen" + "Show 1 more reply": "", + "Show 1 previous comment": "Einen vorherigen Kommentar anzeigen", + "Show comment": "", + "Sign in": "", + "Sign up now": "", + "Start the conversation": "", + "This comment has been hidden.": "", + "This comment has been removed.": "", + "Upgrade now": "", + "Yesterday": "", + "Your request will be sent to the owner of this site.": "" } diff --git a/ghost/i18n/locales/de-CH/portal.json b/ghost/i18n/locales/de-CH/portal.json index 0945d0373fa..36fd060bc98 100644 --- a/ghost/i18n/locales/de-CH/portal.json +++ b/ghost/i18n/locales/de-CH/portal.json @@ -1,4 +1,5 @@ { + "(save {{highestYearlyDiscount}}%)": "", "{{amount}} days free": "{{amount}} Tage kostenfrei", "{{amount}} off": "{{amount}} Rabatt", "{{amount}} off for first {{number}} months.": "{{amount}} Rabatt für die ersten {{number}} Monate.", @@ -9,11 +10,14 @@ "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "{{memberEmail}} wird keine weiteren E-Mail-Benachrichtigungen für Kommentar-Antworten erhalten.", "{{memberEmail}} will no longer receive this newsletter.": "{{memberEmail}} wird diesen Newsletter nicht länger erhalten.", "{{trialDays}} days free": "{{trialDays}} Tage kostenfrei", + "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Ein Login-Link wurde an Ihre E-Mail-Adresse gesendet. Falls die E-Mail nicht innerhalb von 3 Minuten ankommt, überprüfen Sie bitte den Spam-Ordner.", "Account": "Konto", + "Account details updated successfully": "", "Account settings": "Konto-Einstellungen", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Wenn das kostenlose Testabo endet, bezahlen Sie den regulären Preis für den gewählten Tarif. Sie können Ihr Abonnement jederzeit kündigen.", "Already a member?": "Bereits Mitglied?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es erneut oder wenden Sie sich an unseren Support, falls der Fehler weiter besteht.", "Back": "Zurück", "Back to Log in": "Zurück zur Anmeldung", @@ -23,10 +27,13 @@ "Cancel subscription": "Abonnement kündigen", "Cancellation reason": "Kündigungsgrund", "Change": "Ändern", + "Change plan": "", "Check spam & promotions folders": "Spam- und Werbeordner prüfen", "Check with your mail provider": "Erkundigen Sie sich bei Ihrem E-Mail-Anbieter", + "Check your inbox to verify email update": "", "Choose": "Auswählen", "Choose a different plan": "Wählen Sie einen anderen Tarif", + "Choose a plan": "", "Choose your newsletters": "Wählen Sie Ihren Newsletter", "Click here to retry": "Hier klicken zum Wiederholen", "Close": "Schliessen", @@ -38,6 +45,7 @@ "Contact support": "Support kontaktieren", "Continue": "Weiter", "Continue subscription": "Abonnement fortführen", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Anmeldung nicht möglich. Der Login-Link ist abgelaufen.", "Could not update email! Invalid link.": "E-Mail-Aktualisierung fehlgeschlagen. Link nicht gültig.", "Create a new contact": "Neuen Kontakt anlegen", @@ -48,13 +56,27 @@ "Edit": "Bearbeiten", "Email": "E-Mail", "Email newsletter": "E-Mail-Newsletter", - "Email preference updated.": "E-Mail-Einstellungen aktualisiert.", + "Email newsletter settings updated": "", "Email preferences": "E-Mail-Einstellungen", "Emails": "E-Mails", "Emails disabled": "E-Mails deaktiviert", "Ends {{offerEndDate}}": "Endet am {{offerEndDate}}", + "Enter your email address": "", + "Enter your name": "", "Error": "Fehler", "Expires {{expiryDate}}": "Läuft am {{expiryDate}} ab", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Für immer", "Free Trial – Ends {{trialEnd}}": "Die kostenlose Testphase – endet am {{trialEnd}}", "Get help": "Hilfe erhalten", @@ -62,6 +84,7 @@ "Get notified when someone replies to your comment": "Erhalten Sie eine Benachrichtigung, wenn jemand auf Ihren Kommentar antwortet", "Give feedback on this post": "Zu diesem Kommentar Feedback geben", "Help! I'm not receiving emails": "Hilfe! Ich erhalte keine E-Mails", + "Here are a few other sites you may enjoy.": "", "If a newsletter is flagged as spam, emails are automatically disabled for that address to make sure you no longer receive any unwanted messages.": "Wenn ein Newsletter als Spam markiert wird, werden E-Mails für diese Adresse automatisch deaktiviert, um sicherzustellen, dass Sie keine unerwünschten Nachrichten mehr erhalten.", "If the spam complaint was accidental, or you would like to begin receiving emails again, you can resubscribe to emails by clicking the button on the previous screen.": "Wenn die Spam-Beschwerde versehentlich war oder Sie wieder E-Mails erhalten möchten, können Sie sich durch Klicken auf den Button auf dem vorherigen Bildschirm erneut für E-Mails anmelden.", "If you cancel your subscription now, you will continue to have access until {{periodEnd}}.": "Wenn Sie Ihr Abo jetzt kündigen, haben Sie noch Zugang bis zum {{periodEnd}}.", @@ -71,26 +94,36 @@ "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "Wenn Sie all diese Punkte erledigt haben und immer noch keine E-Mails erhalten, wenden Sie sich bitte an {{supportAddress}}.", "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "Im Falle eines dauerhaften Fehlers beim Versuch, einen Newsletter zu senden, werden E-Mails auf dem Konto deaktiviert.", "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "Fügen Sie {{senderEmail}} in Ihrem E-Mail-Client zu Ihren Kontakten hinzu. So signalisieren Sie Ihrem Mail-Anbieter, dass E-Mails von dieser Adresse vertrauenswürdig sind.", + "Invalid email address": "", + "Jamie Larson": "", + "jamie@example.com": "", "Less like this": "Weniger davon", "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "Stellen Sie sicher, dass E-Mails nicht unbeabsichtigt im Spam-Ordner. Wenn das der Fall sein sollte, klicken Sie auf \"Kein Spam\" und/oder \"In den Posteingang bewegen\".", "Manage": "Verwalten", + "Maybe later": "", "Memberships unavailable, contact the owner for access.": "Keine Abonnements verfügbar, bitte wenden Sie sich an {{supportAddress}}, um für einen Zugang anzufragen.", + "month": "", "Monthly": "Monatlich", "More like this": "Mehr davon", "Name": "Name", "Need more help? Contact support": "Sie benötigen weitere Hilfe? Kontaktieren Sie den Support.", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Newsletter können aus zwei Gründen in Ihrem Konto deaktiviert werden: Eine frühere E-Mail wurde als Spam markiert oder der Versuch, eine E-Mail zu senden, führte zu einem dauerhaften Fehler (Bounce).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Kein E-Mail erhalten?", "Now check your email!": "Überprüfen Sie bitte Ihren Posteingang.", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Wenn Sie nach Ihrer Registrierung keine E-Mail von uns im Posteingang haben, überprüfen Sie Ihren Spam-Ordner. Einige E-Mail-Anbieter speichern frühere Spam-Beschwerden und kennzeichnen E-Mails weiterhin. Für diesen Fall markieren Sie den neusten Newsletter als \"Kein Spam\", um ihn in Ihren Posteingang zu verschieben.", "Permanent failure (bounce)": "Permanenter Fehler (Bounce)", + "Phone number": "", "Plan": "Abo", "Plan checkout was cancelled.": "Der Abschluss des Abos wurde abgebrochen.", "Plan upgrade was cancelled.": "Das Upgrade Ihres Abos wurde abgebrochen.", "Please contact {{supportAddress}} to adjust your complimentary subscription.": "Bitte kontaktieren Sie {{supportAddress}} für den Abschluss eines zusätzlichen Abos.", + "Please enter {{fieldName}}": "", "Please fill in required fields": "Bitte alle Pflichtfelder ausfüllen.", "Price": "Preis", "Re-enable emails": "E-Mails wieder aktivieren", + "Recommendations": "", "Renews at {{price}}.": "Wird verlängert zum Preis von {{price}}.", "Retry": "Nochmals versuchen", "Save": "Speichern", @@ -98,10 +131,12 @@ "Send an email to {{senderEmail}} and say hello. This can also help signal to your mail provider that emails to and from this address should be trusted.": "Schicken Sie uns ein E-Mail an {{senderEmail}} und sagen Sie Hallo. Das kann auch dazu beitragen, Ihrem E-Mail-Anbieter zu signalisieren, dass E-Mails an und von dieser Adresse vertrauenswürdig sind.", "Sending login link...": "Login-Link wird gesendet…", "Sending...": "Wird gesendet…", + "Show all": "", "Sign in": "Einloggen", "Sign out": "Abmelden", "Sign up": "Registrieren", "Signup error: Invalid link": "Fehler bei der Registrierung: Ungültiger Link.", + "Something went wrong, please try again later.": "", "Sorry, that didn’t work.": "Entschuldigen Sie, das hat leider nicht funktioniert.", "Spam complaints": "Spam-Beschwerden", "Start {{amount}}-day free trial": "Starten Sie Ihr kostenloses Probeabo für {{amount}} Tage.", @@ -110,31 +145,52 @@ "Submit feedback": "Feedback senden", "Subscribe": "Abonnieren", "Subscribed": "Abonniert", + "Subscription plan updated successfully": "", "Success": "Erfolg", "Success! Check your email for magic link to sign-in.": "Super! Bitte sehen Sie in Ihrem Posteingang nach und klicken Sie auf den Link im Mail, das wir Ihnen geschickt haben, um sich anzumelden.", "Success! Your account is fully activated, you now have access to all content.": "Geschafft! Ihr Konto ist nun vollständig eingerichtet und Ihr Zugang zu allen Inhalten freigeschaltet.", "Success! Your email is updated.": "Ihre E-Mail-Adresse wurde erfolgreich aktualisiert.", "Successfully unsubscribed": "Erfolgreich abgemeldet", + "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "", + "Thank you for your support": "", + "Thank you for your support!": "", "Thanks for the feedback!": "Danke für das Feedback!", "That didn't go to plan": "Entschuldigung, da ist etwas schief gelaufen.", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Die E-Mail-Adresse, die wir von dir haben, lautet {{memberEmail}} - wenn Sie diese ändern möchten, können Sie das in den tun.", "There was a problem submitting your feedback. Please try again a little later.": "Bei der Übermittlung Ihres Feedbacks ist ein Problem aufgetreten. Bitte versuchen Sie es später nochmal.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", + "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Der Zugang zu diesem Inhalt ist eingeschränkt. Bitte kontaktieren Sie uns, wenn Sie Zugang wünschen.", + "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Um Ihre Registrierung abzuschliessen, klicken Sie auf den Bestätigungslink in dem Email, das wir Ihnen gesendet haben. Falls Sie nach drei Minuten noch keine E-Mail erhalten haben, überprüfen Sie bitte Ihren Spam-Ordner.", + "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Probieren Sie uns für {{amount}} Tage kostenlos aus. Danach folgt ein Abo zum Preis von {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Schalten Sie mit einem Abo den Zugang zu allen Newslettern frei.", "Unsubscribe from all emails": "Von allen E-Mails abmelden", "Unsubscribed": "Abgemeldet", + "Unsubscribed from all emails.": "", "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "Wenn du dich von diesen E-Mails abmeldest, bleibt Ihr bezahltes Abo bestehen. Falls Sie Ihr Abo kündigen möchten, tun Sie das bitte separat.", "Update": "Aktualisieren", "Update your preferences": "Einstellungen aktualisieren", + "Verification link sent, check your inbox": "", "Verify your email address is correct": "Überprüfen Sie bitte, ob Ihre E-Mail-Adresse korrekt ist.", "View plans": "Tarife ansehen", "We couldn't unsubscribe you as the email address was not found. Please contact the site owner.": "Da ist etwas schief gelaufen, wir konnten Sie nicht abmelden. Bitte kontaktieren Sie {{supportAddress}}.", "Welcome back, {{name}}!": "Willkommen zurück, {{name}}!", "Welcome back!": "Willkommen zurück!", + "Welcome to {{siteTitle}}": "", "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "Wenn ein Posteingang ein E-Mail nicht annimmt, spricht man von einem Bounce. Das ist häufig ein vorübergehendes Problem (Soft Bounce). In einigen Fällen kann ein E-Mail jedoch dauerhaft abgewiesen werden, etwa wenn eine E-Mail-Adresse ungültig oder nicht vorhanden ist (Hard Bounce).", "Why has my email been disabled?": "Warum wurde mein E-Mail deaktiviert?", + "year": "", "Yearly": "Jährlich", "You currently have a free membership, upgrade to a paid subscription for full access.": "Sie haben ein kostenloses Abo. Wechseln Sie bitte auf ein Bezahl-Abo, um vollen Zugang zu erhalten.", "You have been successfully resubscribed": "Sie haben Ihr Abo erfolgreich erneuert.", @@ -142,7 +198,9 @@ "You're not receiving emails": "Sie erhalten keine E-Mails.", "You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "Sie erhalten keine E-Mails, weil Sie entweder eine kürzlich empfangene E-Mail von uns als Spam markiert haben oder weil unsere E-Mails nicht an die angegebene E-Mail-Adresse zugestellt werden konnten.", "You've successfully signed in.": "Sie haben sich erfolgreich angemeldet.", + "You've successfully subscribed to": "", "Your account": "Ihr Konto", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Ihr Beitrag kann unsere Berichterstattung beeinflussen und mitprägen.", "Your subscription will expire on {{expiryDate}}": "Ihr Abo endet am {{expiryDate}}.", "Your subscription will renew on {{renewalDate}}": "Ihr Abo wird am {{renewalDate}} erneuert.", diff --git a/ghost/i18n/locales/de-CH/search.json b/ghost/i18n/locales/de-CH/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/de-CH/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/de/comments.json b/ghost/i18n/locales/de/comments.json index 896fc5b9569..cc2db3dd1c3 100644 --- a/ghost/i18n/locales/de/comments.json +++ b/ghost/i18n/locales/de/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Auf Kommentar antworten", "Report": "Melden", "Report comment": "Kommentar melden", - "Report this comment": "Diesen Kommentar melden", "Report this comment?": "Diesen Kommentar melden?", "Save": "Speichern", "Sending": "Senden", @@ -61,13 +60,12 @@ "Show 1 more reply": "1 weitere Antwort anzeigen", "Show 1 previous comment": "1 vorherigen Kommentar anzeigen", "Show comment": "Kommentar anzeigen", - "Sign in": "Anmelden", - "Sign up now": "Jetzt anmelden", + "Sign in": "Einloggen", + "Sign up now": "Jetzt registrieren", "Start the conversation": "Beginne das Gespräch", "This comment has been hidden.": "Dieser Kommentar wurde ausgeblendet.", "This comment has been removed.": "Dieser Kommentar wurde entfernt.", "Upgrade now": "Jetzt upgraden", "Yesterday": "Gestern", - "You want to report this comment?": "Möchtest du diesen Kommentar melden?", "Your request will be sent to the owner of this site.": "Deine Anfrage wird an den Besitzer dieser Website gesendet." } diff --git a/ghost/i18n/locales/de/portal.json b/ghost/i18n/locales/de/portal.json index 67721f3acd8..a1d76ac89f6 100644 --- a/ghost/i18n/locales/de/portal.json +++ b/ghost/i18n/locales/de/portal.json @@ -1,22 +1,24 @@ { - "(save {{highestYearlyDiscount}}%)": "", + "(save {{highestYearlyDiscount}}%)": "(jetzt {{highestYearlyDiscount}}% sparen)", "{{amount}} days free": "{{amount}} Tage kostenfrei", "{{amount}} off": "{{amount}} Rabatt", "{{amount}} off for first {{number}} months.": "{{amount}} Rabatt für die ersten {{number}} Monate.", "{{amount}} off for first {{period}}.": "{{amount}} Rabatt für den/das erste/n {{period}}.", - "{{amount}} off forever.": "{{amount}} dauerhaft günstiger.", + "{{amount}} off forever.": "dauerhaft {{amount}} günstiger.", "{{discount}}% discount": "{{discount}}% Rabatt", "{{memberEmail}} will no longer receive {{newsletterName}} newsletter.": "{{memberEmail}} wird den Newsletter {{newsletterName}} nicht länger erhalten.", "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "{{memberEmail}} wird keine weiteren E-Mail-Benachrichtigungen für Kommentar-Antworten erhalten.", "{{memberEmail}} will no longer receive this newsletter.": "{{memberEmail}} wird diesen Newsletter nicht länger erhalten.", "{{trialDays}} days free": "{{trialDays}} Tage kostenfrei", - "+1 (123) 456-7890": "", + "+1 (123) 456-7890": "+49 123 4567890", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Ein Login-Link wurde an deine E-Mail-Adresse gesendet. Falls die E-Mail nicht innerhalb von 3 Minuten ankommt, überprüfe bitte deinen Spam-Ordner.", "Account": "Konto", + "Account details updated successfully": "Kontodaten erfolgreich aktualisiert", "Account settings": "Konto-Einstellungen", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Wenn das kostenlose Testabo endet, bezahlst du den regulären Preis für den gewählten Tarif. Du kannst dein Abonnement jederzeit kündigen.", "Already a member?": "Bereits Mitglied?", - "An unexpected error occured. Please try again or contact support if the error persists.": "Ein unerwarteter Fehler ist aufgetreten. Bitte erneut versuchen oder wende dich an den Support, falls der Fehler weiter besteht.", + "An error occurred": "Ein Fehler ist aufgetreten", + "An unexpected error occured. Please try again or contact support if the error persists.": "Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es erneut oder wende dich an den Support, falls der Fehler weiter besteht.", "Back": "Zurück", "Back to Log in": "Zurück zur Anmeldung", "Billing info": "Abrechnungsinformationen", @@ -25,12 +27,13 @@ "Cancel subscription": "Abonnement kündigen", "Cancellation reason": "Kündigungsgrund", "Change": "Ändern", - "Change plan": "", - "Check spam & promotions folders": "Überprüfe dein Spam- und Werbeordner", + "Change plan": "Tarif ändern", + "Check spam & promotions folders": "Überprüfe deine Spam- und Werbeordner", "Check with your mail provider": "Erkundige dich bei deinem E-Mail-Anbieter", + "Check your inbox to verify email update": "Prüfe nun dein E-Mail Postfach, um die Änderung deiner E-Mail Adresse zu bestätigen", "Choose": "Auswählen", "Choose a different plan": "Wähle einen anderen Tarif", - "Choose a plan": "", + "Choose a plan": "Tarif wählen", "Choose your newsletters": "Wähle deine Newsletter", "Click here to retry": "Hier klicken zum Wiederholen", "Close": "Schließen", @@ -42,6 +45,7 @@ "Contact support": "Support kontaktieren", "Continue": "Weiter", "Continue subscription": "Abonnement fortführen", + "Could not create stripe checkout session": "Zahlungsabwicklung über Stripe konnte nicht erstellt werden", "Could not sign in. Login link expired.": "Anmeldung nicht möglich. Der Login-Link ist abgelaufen.", "Could not update email! Invalid link.": "E-Mail-Aktualisierung fehlgeschlagen. Link nicht gültig.", "Create a new contact": "Neuen Kontakt anlegen", @@ -52,14 +56,27 @@ "Edit": "Bearbeiten", "Email": "E-Mail", "Email newsletter": "E-Mail-Newsletter", + "Email newsletter settings updated": "E-Mail Newsletter Einstellungen wurden aktualisiert", "Email preferences": "E-Mail-Einstellungen", "Emails": "E-Mails", "Emails disabled": "E-Mails deaktiviert", "Ends {{offerEndDate}}": "Endet am {{offerEndDate}}", - "Enter your email address": "", - "Enter your name": "", + "Enter your email address": "Gebe deine Email-Adresse ein", + "Enter your name": "Gebe deinen Namen ein", "Error": "Fehler", "Expires {{expiryDate}}": "Läuft am {{expiryDate}} ab", + "Failed to cancel subscription, please try again": "Abonnement konnte nicht beendet werden. Bitte versuche es erneut.", + "Failed to log in, please try again": "Login ist fehlgeschlagen. Bitte versuche es erneut.", + "Failed to log out, please try again": "Abmelden ist fehlgeschlagen. Bitte versuche es erneut.", + "Failed to process checkout, please try again": "Zahlungsabwicklung ist fehlgeschlagen. Bitte versuche es erneut.", + "Failed to send magic link email": "Magic Link konnte nicht versendet werden", + "Failed to send verification email": "Bestätigungsmail konnte nicht versendet werden", + "Failed to sign up, please try again": "Anmeldung ist fehlgeschlagen. Bitte versuche es erneut.", + "Failed to update account data": "Kontodaten konnten nicht aktualisiert werden", + "Failed to update account details": "Kontodetails konnten nicht aktualisiert werden", + "Failed to update billing information, please try again": "Zahlungsinformationen konnten nicht aktualisiert werden. Bitte versuche es erneut.", + "Failed to update newsletter settings": "Newsletter Einstellungen konnten nicht aktualisiert werden", + "Failed to update subscription, please try again": "Abonnement konnte nicht aktualisiert werden. Bitte versuche es erneut.", "Forever": "Für immer", "Free Trial – Ends {{trialEnd}}": "Kostenlose Testphase – endet am {{trialEnd}}", "Get help": "Hilfe erhalten", @@ -67,109 +84,123 @@ "Get notified when someone replies to your comment": "Erhalte eine Benachrichtigung, wenn jemand auf deinen Kommentar antwortet", "Give feedback on this post": "Gib Feedback zu diesem Beitrag", "Help! I'm not receiving emails": "Hilfe! Ich erhalte keine E-Mails", - "Here are a few other sites you may enjoy.": "", + "Here are a few other sites you may enjoy.": "Hier sind ein paar andere Websites, die dir gefallen könnten.", "If a newsletter is flagged as spam, emails are automatically disabled for that address to make sure you no longer receive any unwanted messages.": "Wenn ein Newsletter als Spam markiert wird, werden E-Mails für diese Adresse automatisch deaktiviert, um sicherzustellen, dass du keine unerwünschten Nachrichten mehr erhältst.", "If the spam complaint was accidental, or you would like to begin receiving emails again, you can resubscribe to emails by clicking the button on the previous screen.": "Wenn die Spam-Beschwerde versehentlich war oder du wieder E-Mails erhalten möchtest, kannst du dich durch Klicken auf den Button auf dem vorherigen Bildschirm erneut für E-Mails anmelden.", - "If you cancel your subscription now, you will continue to have access until {{periodEnd}}.": "Wenn du dein Abonnement jetzt kündigst, hast du noch Zugang bis zum {{periodEnd}}.", + "If you cancel your subscription now, you will continue to have access until {{periodEnd}}.": "Wenn du dein Abonnement jetzt kündigst, hast du noch bis zum {{periodEnd}} Zugang.", "If you have a corporate or government email account, reach out to your IT department and ask them to allow emails to be received from {{senderEmail}}": "Wenn du eine Firmen- oder Regierungs-E-Mail-Adresse nutzt, wende dich an deine IT-Abteilung und bitte sie, E-Mails von {{senderEmail}} zu erlauben", - "If you would like to start receiving emails again, the best next steps are to check your email address on file for any issues and then click resubscribe on the previous screen.": "Wenn du wieder E-Mails erhalten möchtest, solltest du deine hinterlegte E-Mail-Adresse auf Probleme zu überprüfen und dann auf dem vorherigen Bildschirm auf erneut abonnieren zu klicken.", + "If you would like to start receiving emails again, the best next steps are to check your email address on file for any issues and then click resubscribe on the previous screen.": "Wenn du wieder E-Mails erhalten möchtest, solltest du deine hinterlegte E-Mail-Adresse auf Probleme überprüfen und dann auf dem vorherigen Bildschirm auf \"erneut abonnieren\" klicken.", "If you're not receiving the email newsletter you've subscribed to, here are a few things to check.": "Wenn du den Newsletter, für den du dich angemeldet hast, nicht erhältst, gibt es einige Dinge, die du überprüfen kannst.", "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "Wenn du all diese Punkte erledigt hast und immer noch keine E-Mails erhältst, kannst du Unterstützung erhalten, indem du dich an {{supportAddress}} wendest.", "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "Im Falle eines dauerhaften Fehlers beim Versuch, einen Newsletter zu senden, werden die E-Mails auf dem Konto deaktiviert.", "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "Füge in deinem E-Mail-Client {{senderEmail}} zu deiner Kontaktliste hinzu. Dies signalisiert deinem Mail-Anbieter, dass E-Mails von dieser Adresse vertrauenswürdig sind.", - "Invalid email address": "", + "Invalid email address": "Ungültige Email-Adresse", "Jamie Larson": "", "jamie@example.com": "", "Less like this": "Weniger davon", - "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "Stelle sicher, dass E-Mails nicht unbeabsichtigt im Spam-Ordner deines Posteingangs landen. Wenn das der Fall sein sollte, klicke auf \"Kein Spam\" und/oder \"In den Posteingang bewegen\".", + "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "Stelle sicher, dass E-Mails nicht unbeabsichtigt im Spam-Ordner deines Posteingangs landen. Wenn das der Fall sein sollte, klicke auf \"Kein Spam\" und/oder \"In den Posteingang bewegen\".", "Manage": "Verwalten", "Maybe later": "Vielleicht später", - "Memberships unavailable, contact the owner for access.": "", - "month": "", + "Memberships unavailable, contact the owner for access.": "Mitgliedschaft nicht verfügbar. Kontaktiere den/die Besitzer*in für Zugang.", + "month": "Monat", "Monthly": "Monatlich", "More like this": "Mehr davon", "Name": "Name", "Need more help? Contact support": "Du benötigst weitere Hilfe? Kontaktiere den Support", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Newsletter können aus zwei Gründen in deinem Konto deaktiviert werden: Eine frühere E-Mail wurde als Spam markiert oder der Versuch, eine E-Mail zu senden, führte zu einem dauerhaften Fehler (Bounce).", + "No member exists with this e-mail address.": "Mit dieser E-Mail Adresse existiert kein Mitglied.", + "No member exists with this e-mail address. Please sign up first.": "Mit dieser E-Mail Adresse existiert kein Mitglied. Bitte versuche es erneut.", "Not receiving emails?": "Keine E-Mails erhalten?", "Now check your email!": "Überprüfe jetzt deinen Posteingang!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Wenn du nach der Registrierung immer noch keine E-Mails in deinem Posteingang siehst, überprüfen deinen Spam-Ordner. Einige E-Mail-Anbieter speichern frühere Spam-Beschwerden und kennzeichnen E-Mails weiterhin. Wenn dies der Fall ist, markiere den neuesten Newsletter als \"Kein Spam\", um ihn wieder in deinen Posteingang zu verschieben.", "Permanent failure (bounce)": "Permanenter Fehler (Bounce)", - "Phone number": "", + "Phone number": "Telefonnummer", "Plan": "Tarif", "Plan checkout was cancelled.": "Der Abschluss des Tarifs wurde abgebrochen.", "Plan upgrade was cancelled.": "Das Upgrade deines Tarifs wurde abgebrochen.", - "Please contact {{supportAddress}} to adjust your complimentary subscription.": "", - "Please enter {{fieldName}}": "", + "Please contact {{supportAddress}} to adjust your complimentary subscription.": "Bitte kontaktiere {{supportAddress}}, um dein kostenloses Abonnement zu bearbeiten.", + "Please enter {{fieldName}}": "Bitte gebe eine/n {{fieldName}} ein", "Please fill in required fields": "Bitte alle Pflichtfelder ausfüllen", "Price": "Preis", "Re-enable emails": "E-Mails wieder aktivieren", "Recommendations": "Empfehlungen", - "Renews at {{price}}.": "Wird verlängert zum Preis von {{price}}.", + "Renews at {{price}}.": "Wird zum Preis von {{price}} verlängert.", "Retry": "Nochmals versuchen", "Save": "Speichern", - "Send an email and say hi!": "Sende eine E-Mail und sag Hi!", + "Send an email and say hi!": "Sende eine E-Mail und sage Hallo!", "Send an email to {{senderEmail}} and say hello. This can also help signal to your mail provider that emails to and from this address should be trusted.": "Schicke eine E-Mail an {{senderEmail}} und sag Hallo. Dies kann auch dazu beitragen, deinem E-Mail-Anbieter zu signalisieren, dass E-Mails an und von dieser Adresse vertrauenswürdig sind.", - "Sending login link...": "Login-Link senden …", - "Sending...": "Sende …", + "Sending login link...": "Login-Link senden...", + "Sending...": "Sende...", "Show all": "Alle anzeigen", "Sign in": "Einloggen", "Sign out": "Abmelden", "Sign up": "Registrieren", "Signup error: Invalid link": "Fehler bei der Registrierung: Ungültiger Link", - "Something went wrong, please try again later.": "", + "Something went wrong, please try again later.": "Etwas ist schief gelaufen, bitte versuche es später noch einmal.", "Sorry, that didn’t work.": "Entschuldige, das hat nicht funktioniert.", "Spam complaints": "Spam-Beschwerden", "Start {{amount}}-day free trial": "Starte dein {{amount}}-tägiges kostenfreies Testabo", - "Starting {{startDate}}": "Beginnend am {{startDate}}", + "Starting {{startDate}}": "Beginnt am {{startDate}}", "Starting today": "Beginnt heute", "Submit feedback": "Feedback senden", "Subscribe": "Abonnieren", "Subscribed": "Abonniert", + "Subscription plan updated successfully": "Abonnement wurde erfolgreich aktualisiert", "Success": "Erfolg", - "Success! Check your email for magic link to sign-in.": "Super! Sieh in deinem Posteingang nach und klicke auf den magischen Link zum Anmelden.", + "Success! Check your email for magic link to sign-in.": "Super! Sieh in deinem Posteingang nach und klicke zum Einloggen auf den magischen Link.", "Success! Your account is fully activated, you now have access to all content.": "Prima! Dein Konto ist nun vollständig und du hast jetzt Zugang zu allen Inhalten.", "Success! Your email is updated.": "Super! Deine E-Mail wurde erfolgreich aktualisiert.", "Successfully unsubscribed": "Erfolgreich abgemeldet", - "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "", - "Thank you for your support": "", - "Thank you for your support!": "", + "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "Danke fürs Abonnieren. Unten findest du noch ein paar weitere Websites, die dir gefallen könnten.", + "Thank you for your support": "Vielen Dank für Deine Unterstützung", + "Thank you for your support!": "Vielen Dank für Deine Unterstützung!", "Thanks for the feedback!": "Danke für das Feedback!", "That didn't go to plan": "Das ist nicht nach Plan verlaufen", - "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Die E-Mail-Adresse, die wir von dir haben, lautet {{memberEmail}} - wenn das nicht korrekt ist, kannst du sie in deinem aktualisieren.", + "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Die E-Mail-Adresse, die wir von dir haben, lautet {{memberEmail}} - wenn das nicht korrekt ist, kannst du sie in deinen aktualisieren.", "There was a problem submitting your feedback. Please try again a little later.": "Bei der Übermittlung deines Feedbacks ist ein Problem aufgetreten. Bitte versuche es etwas später noch einmal.", - "There was an error processing your payment. Please try again.": "", - "This site is invite-only, contact the owner for access.": "Diese Seite benötigt eine Einladung. Bitte kontaktiere den Inhaber.", - "This site is not accepting payments at the moment.": "", + "There was an error cancelling your subscription, please try again.": "Beim Kündigen deines Abonnements ist ein Fehler aufgetreten. Bitte versuche es erneut.", + "There was an error continuing your subscription, please try again.": "Beim Erneuern deines Abonnements ist ein Fehler aufgetreten. Bitte versuche es erneut.", + "There was an error processing your payment. Please try again.": "Bei der Verarbeitung deiner Zahlung gab es einen Fehler. Bitte versuche es noch einmal.", + "There was an error sending the email, please try again": "Beim Versand der E-Mail ist ein Fehler aufgetreten. Bitte versuche es erneut.", + "This site is invite-only, contact the owner for access.": "Für diese Seite benötigst du eine Einladung. Bitte kontaktiere den Inhaber.", + "This site is not accepting payments at the moment.": "Diese Website nimmt zur Zeit keine Zahlungen entgegen.", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Um deine Registrierung abzuschließen, klicke auf den Bestätigungslink in deinem Posteingang. Falls die E-Mail nicht innerhalb von 3 Minuten ankommt, überprüfe bitte deinen Spam-Ordner!", - "To continue to stay up to date, subscribe to {{publication}} below.": "", + "To continue to stay up to date, subscribe to {{publication}} below.": "Abonniere unten {{publication}}, um auf dem Laufenden zu bleiben.", + "Too many attempts try again in {{number}} days.": "Zu viele Versuche. Versuche es in {{number}} Tagen erneut.", + "Too many attempts try again in {{number}} hours.": "Zu viele Versuche. Versuche es in {{number}} Stunden erneut.", + "Too many attempts try again in {{number}} minutes.": "Zu viele Versuche. Versuche es in {{number}} Minuten erneut.", + "Too many different sign-in attempts, try again in {{number}} days": "Zu viele verschiedene Anmeldeversuche. Versuche es in {{number}} Tagen erneut.", + "Too many different sign-in attempts, try again in {{number}} hours": "Zu viele verschiedene Anmeldeversuche. Versuche es in {{number}} Stunden erneut.", + "Too many different sign-in attempts, try again in {{number}} minutes": "Zu viele verschiedene Anmeldeversuche. Versuche es in {{number}} Minuten erneut.", "Try free for {{amount}} days, then {{originalPrice}}.": "Kostenfreier Testzugang für {{amount}} Tage, danach {{originalPrice}}.", - "Unlock access to all newsletters by becoming a paid subscriber.": "Schalte den Zugang zu allen Newslettern frei, indem du ein zahlender Abonnent wirst.", + "Unable to initiate checkout session": "Zahlungsabwicklung konnte nicht gestartet werden", + "Unlock access to all newsletters by becoming a paid subscriber.": "Schalte den Zugang zu allen Newslettern frei, indem du zahlende/r Abonnent*in wirst.", "Unsubscribe from all emails": "Von allen E-Mails abmelden", "Unsubscribed": "Abmelden", - "Unsubscribed from all emails.": "", + "Unsubscribed from all emails.": "Von allen E-Mails abgemeldet", "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "Wenn du dich von diesen E-Mails abmeldest, wird dein bezahltes Abonnement bei {{title}} nicht automatisch gekündigt", "Update": "Aktualisieren", "Update your preferences": "Aktualisiere deine Einstellungen", - "Verification link sent, check your inbox": "", + "Verification link sent, check your inbox": "Verifizierungs-Link gesendet — überprüfe deinen Posteingang", "Verify your email address is correct": "Überprüfe, ob deine E-Mail-Adresse stimmt", "View plans": "Tarife ansehen", - "We couldn't unsubscribe you as the email address was not found. Please contact the site owner.": "Wir konnten dich nicht abmelden, da die E-Mail-Adresse nicht gefunden wurde. Bitte kontaktiere den Seitenbetreiber.", + "We couldn't unsubscribe you as the email address was not found. Please contact the site owner.": "Wir konnten dich nicht abmelden, da die E-Mail-Adresse nicht gefunden wurde. Bitte kontaktiere den/dir Seitenbetreiber*in.", "Welcome back, {{name}}!": "Willkommen zurück, {{name}}!", "Welcome back!": "Willkommen zurück!", "Welcome to {{siteTitle}}": "Willkommen bei {{siteTitle}}", "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "Wenn ein Posteingang eine E-Mail nicht annimmt, spricht man von einem Bounce. In vielen Fällen kann dies vorübergehend sein (Soft Bounce). In einigen Fällen kann eine E-Mail jedoch als dauerhafter Fehler zurückgeschickt werden, wenn eine E-Mail-Adresse ungültig oder nicht vorhanden ist (Hard Bounce).", "Why has my email been disabled?": "Warum wurde meine E-Mail deaktiviert?", - "year": "", + "year": "Jahr", "Yearly": "Jährlich", - "You currently have a free membership, upgrade to a paid subscription for full access.": "Du hast derzeit eine kostenlose Mitgliedschaft. Wechsele zu einem kostenpflichtigen Abonnement, um vollen Zugang zu erhalten.", + "You currently have a free membership, upgrade to a paid subscription for full access.": "Du hast derzeit eine kostenlose Mitgliedschaft. Wechsle zu einem kostenpflichtigen Abonnement, um vollen Zugang zu erhalten.", "You have been successfully resubscribed": "Du wurdest erfolgreich wieder angemeldet", "You're currently not receiving emails": "Du erhältst im Moment keine E-Mails", "You're not receiving emails": "Du erhältst keine E-Mails", "You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "Du erhältst keine E-Mails, da du entweder eine kürzlich empfangene Nachricht als Spam markiert hast oder weil Nachrichten nicht an deine angegebene E-Mail-Adresse zugestellt werden konnten.", "You've successfully signed in.": "Du hast dich erfolgreich angemeldet.", - "You've successfully subscribed to": "", + "You've successfully subscribed to": "Du hast dich erfolgreich angemeldet bei", "Your account": "Dein Konto", + "Your email has failed to resubscribe, please try again": "Deine E-Mailadresse konnte nicht angemeldet werden. Bitte versuche es noch einmal.", "Your input helps shape what gets published.": "Dein Beitrag trägt dazu bei, was veröffentlicht wird.", "Your subscription will expire on {{expiryDate}}": "Dein Abonnement wird am {{expiryDate}} ablaufen.", "Your subscription will renew on {{renewalDate}}": "Dein Abonnement wird am {{renewalDate}} erneuert.", diff --git a/ghost/i18n/locales/de/search.json b/ghost/i18n/locales/de/search.json new file mode 100644 index 00000000000..154afdc9ce5 --- /dev/null +++ b/ghost/i18n/locales/de/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "Autor*innen", + "Cancel": "Abbrechen", + "No matches found": "Keine Treffer gefunden", + "Posts": "Posts", + "Search posts, tags and authors": "Posts, Tags und Autor*innen durchsuchen", + "Show more results": "Mehr Ergebnisse anzeigen", + "Tags": "Tags" +} diff --git a/ghost/i18n/locales/el/comments.json b/ghost/i18n/locales/el/comments.json index fd6d9d9caaf..2d13668b57d 100644 --- a/ghost/i18n/locales/el/comments.json +++ b/ghost/i18n/locales/el/comments.json @@ -2,8 +2,8 @@ "{{amount}} characters left": "Απομένουν {{amount}} χαρακτήρες", "{{amount}} comments": "{{amount}} σχόλια", "{{amount}} days ago": "πριν από {{amount}} ημέρες", - "{{amount}} hours ago": "πριν από {{amount}} ώρες", - "{{amount}} minutes ago": "πριν από {{amount}} λεπτά", + "{{amount}} hrs ago": "πριν από {{amount}} ώρες", + "{{amount}} mins ago": "πριν από {{amount}} λεπτά", "{{amount}} months ago": "πριν από {{amount}} μήνες", "{{amount}} more": "{{amount}} περισσότερα", "{{amount}} seconds ago": "πριν από {{amount}} δευτερόλεπτα", @@ -25,7 +25,7 @@ "Discussion": "Συζήτηση", "Edit": "Επεξεργασία", "Edit this comment": "Επεξεργασία αυτού του σχολίου", - "Edited": "Επεξεργασμένο", + "edited": "Επεξεργασμένο", "Enter your name": "Εισάγετε το όνομά σας", "Expertise": "Εξειδίκευση", "Founder @ Acme Inc": "Ιδρυτής @ Acme Inc", @@ -42,7 +42,7 @@ "Neurosurgeon": "Νευροχειρουργός", "One day ago": "Πριν από μία ημέρα", "One hour ago": "Πριν από μία ώρα", - "One minute ago": "Πριν από ένα λεπτό", + "One min ago": "Πριν από ένα λεπτό", "One month ago": "Πριν από ένα μήνα", "One week ago": "Πριν από μία εβδομάδα", "One year ago": "Πριν από ένα χρόνο", @@ -50,7 +50,6 @@ "Reply to comment": "Απάντηση στο σχόλιο", "Report": "Αναφορά", "Report comment": "Αναφορά σχολίου", - "Report this comment": "Αναφορά αυτού του σχολίου", "Report this comment?": "Αναφορά αυτού του σχολίου;", "Save": "Αποθήκευση", "Sending": "Αποστολή", @@ -68,6 +67,5 @@ "This comment has been removed.": "Αυτό το σχόλιο έχει αφαιρεθεί.", "Upgrade now": "Αναβάθμιση τώρα", "Yesterday": "Χθες", - "You want to report this comment?": "Θέλετε να αναφέρετε αυτό το σχόλιο;", "Your request will be sent to the owner of this site.": "Το αίτημά σας θα σταλεί στον ιδιοκτήτη αυτού του ιστότοπου." } diff --git a/ghost/i18n/locales/el/portal.json b/ghost/i18n/locales/el/portal.json index 80df3874c6d..b13a6ee8938 100644 --- a/ghost/i18n/locales/el/portal.json +++ b/ghost/i18n/locales/el/portal.json @@ -10,11 +10,14 @@ "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "Το {{memberEmail}} δεν θα λαμβάνει πλέον email όταν κάποιος απαντά στα σχόλιά σας.", "{{memberEmail}} will no longer receive this newsletter.": "Το {{memberEmail}} δεν θα λαμβάνει πλέον αυτό το ενημερωτικό δελτίο.", "{{trialDays}} days free": "{{trialDays}} ημέρες δωρεάν", + "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Ένας σύνδεσμος σύνδεσης έχει σταλεί στα εισερχόμενά σας. Αν δεν φτάσει σε 3 λεπτά, ελέγξτε τον φάκελο ανεπιθύμητης αλληλογραφίας σας.", "Account": "Λογαριασμός", + "Account details updated successfully": "", "Account settings": "Ρυθμίσεις λογαριασμού", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Μετά το τέλος της δωρεάν δοκιμής, θα χρεωθείτε την κανονική τιμή για το επίπεδο που έχετε επιλέξει. Μπορείτε πάντα να ακυρώσετε πριν από τότε.", "Already a member?": "Ήδη μέλος;", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Παρουσιάστηκε ένα απρόβλεπτο σφάλμα. Δοκιμάστε ξανά ή επικοινωνήστε με την υποστήριξη εάν το σφάλμα παραμένει.", "Back": "Πίσω", "Back to Log in": "Επιστροφή στη σύνδεση", @@ -24,10 +27,13 @@ "Cancel subscription": "Ακύρωση συνδρομής", "Cancellation reason": "Λόγος ακύρωσης", "Change": "Αλλαγή", + "Change plan": "", "Check spam & promotions folders": "Ελέγξτε τους φακέλους ανεπιθύμητης αλληλογραφίας και προσφορών", "Check with your mail provider": "Ελέγξτε με τον παροχέα email σας", + "Check your inbox to verify email update": "", "Choose": "Επιλέξτε", "Choose a different plan": "Επιλέξτε ένα διαφορετικό πρόγραμμα", + "Choose a plan": "", "Choose your newsletters": "Επιλέξτε τα ενημερωτικά δελτία σας", "Click here to retry": "Κάντε κλικ εδώ για επανάληψη", "Close": "Κλείσιμο", @@ -39,6 +45,7 @@ "Contact support": "Επικοινωνήστε με την υποστήριξη", "Continue": "Συνέχεια", "Continue subscription": "Συνέχεια συνδρομής", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Δεν ήταν δυνατή η σύνδεση. Ο σύνδεσμος σύνδεσης έχει λήξει.", "Could not update email! Invalid link.": "Δεν ήταν δυνατή η ενημέρωση του email! Μη έγκυρος σύνδεσμος.", "Create a new contact": "Δημιουργήστε νέα επαφή", @@ -49,12 +56,27 @@ "Edit": "Επεξεργασία", "Email": "Email", "Email newsletter": "Ενημερωτικό δελτίο email", + "Email newsletter settings updated": "", "Email preferences": "Προτιμήσεις email", "Emails": "Emails", "Emails disabled": "Τα emails είναι απενεργοποιημένα", "Ends {{offerEndDate}}": "Λήγει {{offerEndDate}}", + "Enter your email address": "", + "Enter your name": "", "Error": "Σφάλμα", "Expires {{expiryDate}}": "Λήγει {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Για πάντα", "Free Trial – Ends {{trialEnd}}": "Δωρεάν Δοκιμή – Λήγει {{trialEnd}}", "Get help": "Λάβετε βοήθεια", @@ -72,24 +94,32 @@ "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "Αν έχετε ολοκληρώσει όλους αυτούς τους ελέγχους και εξακολουθείτε να μην λαμβάνετε emails, μπορείτε να επικοινωνήσετε με την υποστήριξη επικοινωνώντας με {{supportAddress}}.", "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "Σε περίπτωση που ληφθεί μόνιμη αποτυχία κατά την αποστολή ενός ενημερωτικού δελτίου, τα emails θα απενεργοποιηθούν στον λογαριασμό.", "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "Στον πελάτη email σας προσθέστε το {{senderEmail}} στη λίστα επαφών σας. Αυτό σηματοδοτεί στον πάροχο email σας ότι τα emails που αποστέλλονται από αυτήν τη διεύθυνση πρέπει να είναι αξιόπιστα.", + "Invalid email address": "", + "Jamie Larson": "", + "jamie@example.com": "", "Less like this": "Λιγότερο σαν αυτό", "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "Βεβαιωθείτε ότι τα emails δεν καταλήγουν κατά λάθος στους φακέλους Ανεπιθύμητη αλληλογραφία ή Προσφορές των εισερχομένων σας. Αν είναι εκεί, κάντε κλικ στο \"Σήμανση ως μη ανεπιθύμητη αλληλογραφία\" και/ή \"Μεταφορά στα εισερχόμενα\".", "Manage": "Διαχείριση", "Maybe later": "Ίσως αργότερα", "Memberships unavailable, contact the owner for access.": "Τα μέλη δεν είναι διαθέσιμα, επικοινωνήστε με τον ιδιοκτήτη για πρόσβαση.", + "month": "", "Monthly": "Μηνιαία", "More like this": "Περισσότερα σαν αυτό", "Name": "Όνομα", "Need more help? Contact support": "Χρειάζεστε περισσότερη βοήθεια; Επικοινωνήστε με την υποστήριξη", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Τα ενημερωτικά δελτία μπορούν να απενεργοποιηθούν στον λογαριασμό σας για δύο λόγους: Ένα προηγούμενο email επισημάνθηκε ως ανεπιθύμητο ή η προσπάθεια αποστολής email είχε ως αποτέλεσμα μόνιμη αποτυχία (αναπήδηση).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Δεν λαμβάνετε emails;", "Now check your email!": "Τώρα ελέγξτε το email σας!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Μόλις εγγραφείτε ξανά, αν εξακολουθείτε να μην βλέπετε emails στα εισερχόμενά σας, ελέγξτε τον φάκελο ανεπιθύμητης αλληλογραφίας. Ορισμένοι πάροχοι εισερχομένων διατηρούν αρχείο προηγούμενων αναφορών ανεπιθύμητης αλληλογραφίας και θα συνεχίσουν να επισημαίνουν emails. Αν συμβεί αυτό, επισημάνετε το τελευταίο ενημερωτικό δελτίο ως 'Μη ανεπιθύμητο' για να το μετακινήσετε ξανά στα κύρια εισερχόμενά σας.", "Permanent failure (bounce)": "Μόνιμη αποτυχία (αναπήδηση)", + "Phone number": "", "Plan": "Πρόγραμμα", "Plan checkout was cancelled.": "Η πληρωμή του προγράμματος ακυρώθηκε.", "Plan upgrade was cancelled.": "Η αναβάθμιση του προγράμματος ακυρώθηκε.", "Please contact {{supportAddress}} to adjust your complimentary subscription.": "Επικοινωνήστε με το {{supportAddress}} για να προσαρμόσετε τη δωρεάν συνδρομή σας.", + "Please enter {{fieldName}}": "", "Please fill in required fields": "Συμπληρώστε τα απαιτούμενα πεδία", "Price": "Τιμή", "Re-enable emails": "Ενεργοποιήστε ξανά τα emails", @@ -106,6 +136,7 @@ "Sign out": "Αποσύνδεση", "Sign up": "Εγγραφή", "Signup error: Invalid link": "Σφάλμα εγγραφής: Μη έγκυρος σύνδεσμος", + "Something went wrong, please try again later.": "", "Sorry, that didn’t work.": "Συγγνώμη, αυτό δεν λειτούργησε.", "Spam complaints": "Αναφορές ανεπιθύμητης αλληλογραφίας", "Start {{amount}}-day free trial": "Ξεκινήστε {{amount}}-ημερών δωρεάν δοκιμή", @@ -114,19 +145,35 @@ "Submit feedback": "Υποβολή σχολίων", "Subscribe": "Εγγραφή", "Subscribed": "Εγγεγραμμένος", + "Subscription plan updated successfully": "", "Success": "Επιτυχία", "Success! Check your email for magic link to sign-in.": "Επιτυχία! Ελέγξτε το email σας για να ολοκληρώσετε την εγγραφή.", "Success! Your account is fully activated, you now have access to all content.": "Επιτυχία! Ο λογαριασμός σας είναι πλήρως ενεργοποιημένος, έχετε πλέον πρόσβαση σε όλο το περιεχόμενο.", "Success! Your email is updated.": "Επιτυχία! Το email σας ενημερώθηκε.", "Successfully unsubscribed": "Επιτυχής διαγραφή από τη λίστα", "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "Ευχαριστούμε που εγγραφήκατε. Πριν ξεκινήσετε να διαβάζετε, εδώ είναι μερικοί άλλοι ιστότοποι που μπορεί να σας αρέσουν.", + "Thank you for your support": "", + "Thank you for your support!": "", "Thanks for the feedback!": "Ευχαριστούμε για το σχόλιο!", "That didn't go to plan": "Αυτό δεν πήγε σύμφωνα με το σχέδιο", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Η διεύθυνση email που έχουμε για εσάς είναι {{memberEmail}} — αν δεν είναι σωστή, μπορείτε να την ενημερώσετε στην .", "There was a problem submitting your feedback. Please try again a little later.": "Παρουσιάστηκε πρόβλημα κατά την υποβολή των σχολίων σας. Δοκιμάστε ξανά αργότερα.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", + "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Αυτός ο ιστότοπος είναι μόνο με πρόσκληση, επικοινωνήστε με τον ιδιοκτήτη για πρόσβαση.", + "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Για να ολοκληρώσετε την εγγραφή, κάντε κλικ στον σύνδεσμο επιβεβαίωσης στα εισερχόμενά σας. Αν δεν φτάσει εντός 3 λεπτών, ελέγξτε τον φάκελο ανεπιθύμητης αλληλογραφίας!", + "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Δοκιμάστε δωρεάν για {{amount}} ημέρες, μετά {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Ξεκλειδώστε την πρόσβαση σε όλα τα ενημερωτικά δελτία, ενεργοποιόντας την premium συνδρομή.", "Unsubscribe from all emails": "Διαγραφή από όλα τα emails", "Unsubscribed": "Διαγραμμένος", @@ -143,6 +190,7 @@ "Welcome to {{siteTitle}}": "Καλώς ήρθατε στο {{siteTitle}}", "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "Όταν τα εισερχόμενα αποτυγχάνουν να αποδεχθούν ένα email, συνήθως ονομάζεται αναπήδηση. Σε πολλές περιπτώσεις, αυτό μπορεί να είναι προσωρινό. Ωστόσο, σε ορισμένες περιπτώσεις, ένα αναπηδήμένο email μπορεί να επιστραφεί ως μόνιμη αποτυχία όταν μια διεύθυνση email είναι άκυρη ή ανύπαρκτη.", "Why has my email been disabled?": "Γιατί έχει απενεργοποιηθεί το email μου;", + "year": "", "Yearly": "Ετήσια", "You currently have a free membership, upgrade to a paid subscription for full access.": "Αυτή τη στιγμή έχετε δωρεάν μέλος, αναβαθμίστε σε premium συνδρομή για πλήρη πρόσβαση.", "You have been successfully resubscribed": "Έχετε εγγραφεί ξανά με επιτυχία", @@ -152,6 +200,7 @@ "You've successfully signed in.": "Έχετε συνδεθεί επιτυχώς.", "You've successfully subscribed to": "Έχετε εγγραφεί επιτυχώς στο", "Your account": "Ο λογαριασμός σας", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Η συνεισφορά σας βοηθά να διαμορφωθεί το τι δημοσιεύεται.", "Your subscription will expire on {{expiryDate}}": "Η συνδρομή σας θα λήξει στις {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Η συνδρομή σας θα ανανεωθεί στις {{renewalDate}}", diff --git a/ghost/i18n/locales/el/search.json b/ghost/i18n/locales/el/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/el/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/en/comments.json b/ghost/i18n/locales/en/comments.json index c5391eab775..77ef1db9693 100644 --- a/ghost/i18n/locales/en/comments.json +++ b/ghost/i18n/locales/en/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "", "Report": "", "Report comment": "", - "Report this comment": "", "Report this comment?": "", "Save": "", "Sending": "", @@ -68,6 +67,5 @@ "This comment has been removed.": "", "Upgrade now": "", "Yesterday": "", - "You want to report this comment?": "", "Your request will be sent to the owner of this site.": "" } diff --git a/ghost/i18n/locales/en/portal.json b/ghost/i18n/locales/en/portal.json index 3b1476de045..b8b8f0b7159 100644 --- a/ghost/i18n/locales/en/portal.json +++ b/ghost/i18n/locales/en/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "", "Account": "", + "Account details updated successfully": "", "Account settings": "", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "", "Already a member?": "", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "", "Back": "", "Back to Log in": "", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "", "Check with your mail provider": "", + "Check your inbox to verify email update": "", "Choose": "", "Choose a different plan": "", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "", "Continue": "", "Continue subscription": "", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "", "Could not update email! Invalid link.": "", "Create a new contact": "", @@ -52,6 +56,7 @@ "Edit": "", "Email": "", "Email newsletter": "", + "Email newsletter settings updated": "", "Email preferences": "", "Emails": "", "Emails disabled": "", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "", "Expires {{expiryDate}}": "", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "", "Free Trial – Ends {{trialEnd}}": "", "Get help": "", @@ -91,6 +108,8 @@ "Name": "", "Need more help? Contact support": "", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "", "Now check your email!": "", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "", @@ -126,6 +145,7 @@ "Submit feedback": "", "Subscribe": "", "Subscribed": "", + "Subscription plan updated successfully": "", "Success": "", "Success! Check your email for magic link to sign-in.": "", "Success! Your account is fully activated, you now have access to all content.": "", @@ -138,12 +158,22 @@ "That didn't go to plan": "", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "", "There was a problem submitting your feedback. Please try again a little later.": "", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "", "Unsubscribe from all emails": "", "Unsubscribed": "", @@ -170,6 +200,7 @@ "You've successfully signed in.": "", "You've successfully subscribed to": "", "Your account": "", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "", "Your subscription will expire on {{expiryDate}}": "", "Your subscription will renew on {{renewalDate}}": "", diff --git a/ghost/i18n/locales/en/search.json b/ghost/i18n/locales/en/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/en/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/eo/comments.json b/ghost/i18n/locales/eo/comments.json index c5391eab775..77ef1db9693 100644 --- a/ghost/i18n/locales/eo/comments.json +++ b/ghost/i18n/locales/eo/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "", "Report": "", "Report comment": "", - "Report this comment": "", "Report this comment?": "", "Save": "", "Sending": "", @@ -68,6 +67,5 @@ "This comment has been removed.": "", "Upgrade now": "", "Yesterday": "", - "You want to report this comment?": "", "Your request will be sent to the owner of this site.": "" } diff --git a/ghost/i18n/locales/eo/portal.json b/ghost/i18n/locales/eo/portal.json index ae95e5db8a7..d51d4e26d3a 100644 --- a/ghost/i18n/locales/eo/portal.json +++ b/ghost/i18n/locales/eo/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Sendis ensalutan ligilon al via enirkesto. Se ĝi ne alvenas post 3 minutoj, nepre kontrolu vian trudmesaĝdosieron.", "Account": "Konto", + "Account details updated successfully": "", "Account settings": "Kontagordoj", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Post senpaga provo finiĝos, vi pagos la regulan prezon por la nivelo, kiun vi elektis. Vi ĉiam povas nuligi antaŭ tiam.", "Already a member?": "Ĉu membro jam?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "", "Back": "Reen", "Back to Log in": "Reen al Ensaluto", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "", "Check with your mail provider": "", + "Check your inbox to verify email update": "", "Choose": "", "Choose a different plan": "Elektu alian planon", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "", "Continue": "Daŭrigu", "Continue subscription": "", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "", "Could not update email! Invalid link.": "", "Create a new contact": "", @@ -52,6 +56,7 @@ "Edit": "", "Email": "Retadreso", "Email newsletter": "", + "Email newsletter settings updated": "", "Email preferences": "Retpoŝtaj agordoj", "Emails": "Retpoŝtoj", "Emails disabled": "Retpoŝtoj malŝaltitaj", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "", "Expires {{expiryDate}}": "", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "", "Free Trial – Ends {{trialEnd}}": "", "Get help": "Ricevi helpon", @@ -91,6 +108,8 @@ "Name": "Nomo", "Need more help? Contact support": "", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "", "Now check your email!": "Bonvolu kontroli vian retpoŝton nun!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "", @@ -126,6 +145,7 @@ "Submit feedback": "Sendu rimarkojn", "Subscribe": "", "Subscribed": "", + "Subscription plan updated successfully": "", "Success": "", "Success! Check your email for magic link to sign-in.": "", "Success! Your account is fully activated, you now have access to all content.": "", @@ -138,12 +158,22 @@ "That didn't go to plan": "Tio ne iris laŭ la intenco", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "", "There was a problem submitting your feedback. Please try again a little later.": "", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Ĉi tiu retejo estas nur por invitiĝuloj, kontaktu la proprietulo por alireblo.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Por kompletigi aliĝon, premu la konfirman ligilon en via enirkesto. Se ĝi ne alvenas ene de 3 minutoj, kontrolu vian trudmesaĝdosieron!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "", "Unsubscribe from all emails": "Malabonu de ĉiuj retpoŝtoj", "Unsubscribed": "", @@ -170,6 +200,7 @@ "You've successfully signed in.": "", "You've successfully subscribed to": "", "Your account": "Via konto", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Via enigo helpas formi kio estas aperigita.", "Your subscription will expire on {{expiryDate}}": "", "Your subscription will renew on {{renewalDate}}": "", diff --git a/ghost/i18n/locales/eo/search.json b/ghost/i18n/locales/eo/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/eo/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/es/comments.json b/ghost/i18n/locales/es/comments.json index ea37e93fcae..fda15392312 100644 --- a/ghost/i18n/locales/es/comments.json +++ b/ghost/i18n/locales/es/comments.json @@ -5,18 +5,18 @@ "{{amount}} hrs ago": "{{amount}} horas atrás", "{{amount}} mins ago": "{{amount}} minutos atrás", "{{amount}} months ago": "{{amount}} meses atrás", - "{{amount}} more": "", + "{{amount}} more": "{{amount}} más", "{{amount}} seconds ago": "{{amount}} segundos atrás", "{{amount}} weeks ago": "{{amount}} semanas atrás", "{{amount}} years ago": "{{amount}} años atrás", "1 comment": "1 comentario", "Add comment": "Agregar comentario", - "Add context to your comment, share your name and expertise to foster a healthy discussion.": "Agrega contexto a tu comentario, comparte tu nombre y experiencia para fomentar una discusión sana", + "Add context to your comment, share your name and expertise to foster a healthy discussion.": "Agrega contexto a tu comentario, comparte tu nombre y experiencia para fomentar una discusión sana.", "Add reply": "Agregar respuesta", "Already a member?": "¿Ya eres miembro?", "Anonymous": "Anónimo", - "Become a member of {{publication}} to start commenting.": "Hazte miembro de {{publication}} para comenzar a comentar", - "Become a paid member of {{publication}} to start commenting.": "Hazte miembro pago de {{publication}} para comenzar a comentar", + "Become a member of {{publication}} to start commenting.": "Hazte miembro de {{publication}} para poder comentar.", + "Become a paid member of {{publication}} to start commenting.": "Hazte miembro de pago para poder comentar en {{publication}}.", "Cancel": "Cancelar", "Comment": "Comentar", "Complete your profile": "Completa tu perfil", @@ -29,7 +29,7 @@ "Enter your name": "Ingresa tu nombre", "Expertise": "Experiencia", "Founder @ Acme Inc": "Fundador @ Acme Inc", - "Full-time parent": "Pariente de tiempo completo", + "Full-time parent": "Padres de tiempo completo", "Head of Marketing at Acme, Inc": "Jefe de Marketing en Acme, Inc", "Hide": "Ocultar", "Hide comment": "Ocultar comentario", @@ -50,13 +50,12 @@ "Reply to comment": "Responder al comentario", "Report": "Reportar", "Report comment": "Reportar comentario", - "Report this comment": "Reportar este comentario", "Report this comment?": "¿Reportar este comentario?", "Save": "Guardar", "Sending": "Enviando", "Sent": "Enviar", "Show": "Mostrar", - "Show {{amount}} more replies": "Mostrar {{amount}} más respuestas", + "Show {{amount}} more replies": "Mostrar {{amount}} respuestas más", "Show {{amount}} previous comments": "Mostrar {{amount}} comentarios previos", "Show 1 more reply": "Mostrar 1 respuesta más", "Show 1 previous comment": "Mostrar 1 comentario previo", @@ -65,9 +64,8 @@ "Sign up now": "Regístrate ahora", "Start the conversation": "Comienza la conversación", "This comment has been hidden.": "Este comentario ha sido ocultado.", - "This comment has been removed.": "Este comentario ha sido removido", - "Upgrade now": "Mejora ahora", + "This comment has been removed.": "Este comentario ha sido borrado.", + "Upgrade now": "Actualiza tu cuenta ahora", "Yesterday": "Ayer", - "You want to report this comment?": "¿Quieres reportar este comentario?", - "Your request will be sent to the owner of this site.": "Tu requerimiento será enviado al propietario de este sitio web." + "Your request will be sent to the owner of this site.": "Tu requerimiento será enviado al dueño de este sitio web." } diff --git a/ghost/i18n/locales/es/ghost.json b/ghost/i18n/locales/es/ghost.json index 869c7cf39cb..da00c8c7a56 100644 --- a/ghost/i18n/locales/es/ghost.json +++ b/ghost/i18n/locales/es/ghost.json @@ -1,34 +1,34 @@ { - "All the best!": "¡Todo lo mejor!", + "All the best!": "¡Todo lo mejor para ti!", "Complete signup for {{siteTitle}}!": "¡Completa el registro en {{siteTitle}}!", "Complete your sign up to {{siteTitle}}!": "¡Completa tu registro en {{siteTitle}}!", - "Confirm email address": "Confirmar dirección de correo", - "Confirm signup": "Confirmar registro", + "Confirm email address": "Confirma tu dirección de correo", + "Confirm signup": "Confirma tu registro", "Confirm your email address": "Confirma tu correo electrónico", - "Confirm your email update for {{siteTitle}}!": "¡Confirma la actualización de tu correo electrónico para {{siteTitle}}!", + "Confirm your email update for {{siteTitle}}!": "¡Confirma tu correo electrónico actualizado para {{siteTitle}}!", "Confirm your subscription to {{siteTitle}}": "Confirma tu suscripción a {{siteTitle}}", "For your security, the link will expire in 24 hours time.": "Por tu seguridad, el enlace expirará en 24 horas.", "Hey there,": "Hola,", "Hey there!": "¡Hola!", - "If you did not make this request, you can safely ignore this email.": "Si no hiciste esta solicitud, puedes ignorar este correo electrónico sin problemas.", - "If you did not make this request, you can simply delete this message.": "Si no hiciste esta solicitud, simplemente puedes eliminar este mensaje.", - "Please confirm your email address with this link:": "Por favor, confirma tu dirección de correo electrónico con este enlace:", - "Secure sign in link for {{siteTitle}}": "Enlace seguro de inicio de sesión para {{siteTitle}}", + "If you did not make this request, you can safely ignore this email.": "Si no hiciste esta solicitud, ignora este correo electrónico.", + "If you did not make this request, you can simply delete this message.": "Si no hiciste esta solicitud, elimina este mensaje.", + "Please confirm your email address with this link:": "Por favor, confirma tu dirección de correo electrónico dando click a este vínculo:", + "Secure sign in link for {{siteTitle}}": "Inicio de sesión seguro para {{siteTitle}}", "See you soon!": "¡Hasta pronto!", "Sent to {{email}}": "Enviado a {{email}}", - "Sign in": "Iniciar sesión", + "Sign in": "Inicia sesión", "Sign in to {{siteTitle}}": "Inicia sesión en {{siteTitle}}", - "Tap the link below to complete the signup process for {{siteTitle}}, and be automatically signed in:": "Toca el enlace a continuación para completar el proceso de registro en {{siteTitle}} e iniciar sesión automáticamente:", - "Thank you for signing up to {{siteTitle}}!": "¡Gracias por registrarte en {{siteTitle}}!", - "Thank you for subscribing to {{siteTitle}}!": "¡Gracias por suscribirte a {{siteTitle}}!", - "Thank you for subscribing to {{siteTitle}}.": "Gracias por suscribirte a {{siteTitle}}.", - "Thank you for subscribing to {{siteTitle}}. Tap the link below to be automatically signed in:": "Gracias por suscribirte a {{siteTitle}}. Toca el enlace a continuación para iniciar sesión automáticamente:", - "This email address will not be used.": "Esta dirección de correo electrónico no se utilizará.", - "Welcome back to {{siteTitle}}!": "¡Bienvenido de nuevo a {{siteTitle}}!", - "Welcome back! Use this link to securely sign in to your {{siteTitle}} account:": "¡Bienvenido de nuevo! Usa este enlace para iniciar sesión de manera segura en tu cuenta de {{siteTitle}}:", - "You can also copy & paste this URL into your browser:": "También puedes copiar y pegar esta URL en tu navegador:", + "Tap the link below to complete the signup process for {{siteTitle}}, and be automatically signed in:": "Da click en el vínculo dado a continuación para completar el registro en {{siteTitle}}, e iniciar sesión automáticamente:", + "Thank you for signing up to {{siteTitle}}!": "¡Gracias por tu registro en {{siteTitle}}!", + "Thank you for subscribing to {{siteTitle}}!": "¡Gracias por tu suscripción a {{siteTitle}}!", + "Thank you for subscribing to {{siteTitle}}.": "Gracias por tu suscripción a {{siteTitle}}.", + "Thank you for subscribing to {{siteTitle}}. Tap the link below to be automatically signed in:": "Gracias por tu suscripción a {{siteTitle}}. Da click en el vínculo dado a continuación para iniciar sesión automáticamente:", + "This email address will not be used.": "Esta dirección de correo electrónico no se usará.", + "Welcome back to {{siteTitle}}!": "¡Bienvenido de vuelta a {{siteTitle}}!", + "Welcome back! Use this link to securely sign in to your {{siteTitle}} account:": "¡Bienvenido de vuelta! Usa este vínculo para iniciar sesión de manera segura en {{siteTitle}} con tu cuenta:", + "You can also copy & paste this URL into your browser:": "También puedes copiar y pegar este vínculo en tu navegador:", "You will not be signed up, and no account will be created for you.": "No te registrarás y no se creará ninguna cuenta para ti.", - "You will not be subscribed.": "No estarás suscrito.", - "You're one tap away from subscribing to {{siteTitle}} — please confirm your email address with this link:": "Estás a un toque de suscribirte a {{siteTitle}} — por favor confirma tu dirección de correo electrónico con este enlace:", - "You're one tap away from subscribing to {{siteTitle}}!": "¡Estás a un toque de suscribirte a {{siteTitle}}!" + "You will not be subscribed.": "No serás suscrito.", + "You're one tap away from subscribing to {{siteTitle}} — please confirm your email address with this link:": "Estás muy cerca para suscribirte a {{siteTitle}} — por favor confirma tu correo electrónico dando click a este enlace:", + "You're one tap away from subscribing to {{siteTitle}}!": "¡Estás muy cerca para suscribirte a {{siteTitle}}!" } diff --git a/ghost/i18n/locales/es/portal.json b/ghost/i18n/locales/es/portal.json index a3436456049..3de6f1ac420 100644 --- a/ghost/i18n/locales/es/portal.json +++ b/ghost/i18n/locales/es/portal.json @@ -1,5 +1,5 @@ { - "(save {{highestYearlyDiscount}}%)": "", + "(save {{highestYearlyDiscount}}%)": "(ahorra {{highestYearlyDiscount}}%)", "{{amount}} days free": "{{amount}} días gratis", "{{amount}} off": "{{amount}} de descuento", "{{amount}} off for first {{number}} months.": "{{amount}} de descuento por los primeros {{number}} meses.", @@ -10,12 +10,14 @@ "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "{{memberEmail}} ya no recibirá correos electrónicos cuando alguien responde a tus comentarios.", "{{memberEmail}} will no longer receive this newsletter.": "{{memberEmail}} ya no recibirá este boletín.", "{{trialDays}} days free": "{{trialDays}} días gratis", - "+1 (123) 456-7890": "", + "+1 (123) 456-7890": "+1 (123) 456-7890", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Se ha enviado un enlace de inicio de sesión a tu correo. Si no llega en 3 minutos, revisa tu carpeta de spam.", "Account": "Cuenta", + "Account details updated successfully": "Los detalles de la cuenta se actualizaron con éxito", "Account settings": "Configuración de la cuenta", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Después de que finalice el período de prueba gratuito, se te cobrará el precio regular del nivel que hayas elegido. Siempre puedes cancelar antes de eso.", "Already a member?": "¿Ya eres miembro?", + "An error occurred": "Ocurrió un error", "An unexpected error occured. Please try again or contact support if the error persists.": "Un error inesperado ha ocurrido. Por favor intenta de nuevo o contacta a soporte técnico si el error persiste.", "Back": "Atrás", "Back to Log in": "Volver al inicio de sesión", @@ -25,9 +27,10 @@ "Cancel subscription": "Cancelar suscripción", "Cancellation reason": "Razón de cancelación", "Change": "Cambiar", - "Change plan": "", + "Change plan": "Cambiar plan", "Check spam & promotions folders": "Comprueba la carpeta de spam y promociones", "Check with your mail provider": "Comprueba con tu proveedor de correo", + "Check your inbox to verify email update": "Revisa tu bandeja de entrada para actualizar el correo electrónico", "Choose": "Elige", "Choose a different plan": "Elige un plan diferente", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Contacta a soporte técnico", "Continue": "Continuar", "Continue subscription": "Continuar suscripción", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "No se pudo iniciar sesión. El enlace de inicio ha expirado.", "Could not update email! Invalid link.": "¡No se pudo actualizar el correo electrónico! Enlace inválido.", "Create a new contact": "Crea un nuevo contacto", @@ -52,15 +56,28 @@ "Edit": "Editar", "Email": "Correo electrónico", "Email newsletter": "Boletín informativo por correo electrónico", + "Email newsletter settings updated": "", "Email preferences": "Preferencias", "Emails": "Correos electrónicos", "Emails disabled": "Correos electrónicos desactivados", "Ends {{offerEndDate}}": "Termina el {{offerEndDate}}", - "Enter your email address": "", - "Enter your name": "", + "Enter your email address": "Escribe tu correo electrónico", + "Enter your name": "Escribe tu nombre", "Error": "Error", "Expires {{expiryDate}}": "Expira el {{expiryDate}}", - "Forever": "para siempre", + "Failed to cancel subscription, please try again": "Falló cancelar tu suscripción, inténtalo de nuevo por favor", + "Failed to log in, please try again": "Falló inciar sesión, inténtalo de nuevo por favor", + "Failed to log out, please try again": "Falló salir de la sesión, inténtalo de nuevo por favor", + "Failed to process checkout, please try again": "Falló procesar el pago, inténtalo de nuevo por favor", + "Failed to send magic link email": "Falló de enviar el link mágico para iniciar sesión", + "Failed to send verification email": "Falló enviar el correo electrónico de verificación", + "Failed to sign up, please try again": "Falló tu registró, inténtalo de nuevo por favor", + "Failed to update account data": "Falló actualizar los datos de la cuenta", + "Failed to update account details": "Falló actualizar los detalles de la cuenta", + "Failed to update billing information, please try again": "Falló actualizar la información de pago, inténtalo de nuevo por favor", + "Failed to update newsletter settings": "Falló actualizar los parámetros del boletín", + "Failed to update subscription, please try again": "Falló actualizar la suscripción, inténtalo de nuevo por favor", + "Forever": "Para siempre", "Free Trial – Ends {{trialEnd}}": "Prueba gratis - Termina el {{trialEnd}}", "Get help": "Obtener ayuda", "Get in touch for help": "Ponte en contacto para obtener ayuda", @@ -77,25 +94,27 @@ "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "Si haz comprobado todo y aun así no estás recibiendo los correos electrónicos, puedes comunicarte con soporte técnico contactando a {{supportAddress}}.", "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "En el caso de que haya un problema al intentar enviar el boletín, los correos electrónicos van a ser desabilitados en su cuenta.", "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "Añade {{senderEmail}} a tu lista de contactos. Esto le indica a tu provedor de correo electrónico que puede confiar en esta dirección de correo.", - "Invalid email address": "", - "Jamie Larson": "", - "jamie@example.com": "", + "Invalid email address": "Correo electrónico inválido", + "Jamie Larson": "Jamie Larson", + "jamie@example.com": "jamie@example.com", "Less like this": "Menos como esto", "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "Asegúrate de que los correos electrónicos no terminen accidentalmente en las carpetas de correo no deseado o promociones de su bandeja de entrada. Si lo son, haz clic en \"Marcar como no spam\" y/o \"Mover a la bandeja de entrada\".", "Manage": "Administrar", "Maybe later": "Tal vez más tarde", "Memberships unavailable, contact the owner for access.": "Membresía no disponible, contacta al propietario para obtener acceso.", - "month": "", + "month": "mes", "Monthly": "Mensual", "More like this": "Más como esto", "Name": "Nombre", "Need more help? Contact support": "¿Necesitas más ayuda? Contacta a soporte técnico.", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Los boletines se pueden deshabilitar en tu cuenta por dos razones: Un correo electrónico anterior se marcó como correo no deseado o el intento de enviar un correo electrónico resultó en una falla permanente (bounce).", + "No member exists with this e-mail address.": "No hay miembro existente con éste correo electrónico", + "No member exists with this e-mail address. Please sign up first.": "No hay miembro existente con este correo electrónico. Por favor registrate primerro.", "Not receiving emails?": "¿No recibes correos electrónicos?", "Now check your email!": "¡Ahora revisa tu correo electrónico!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Una vez que te hayas vuelto a suscribir, si aún no ves los correos electrónicos en tu bandeja de entrada, verifica tu carpeta de spam. Algunos proveedores de bandejas de entrada mantienen un registro de quejas de spam anteriores y continuarán marcando los correos electrónicos. Si esto sucede, marca el último boletín como \"No es spam\" para moverlo nuevamente a tu bandeja de entrada principal.", "Permanent failure (bounce)": "Fallo permanente (bounce)", - "Phone number": "", + "Phone number": "Número telefónico", "Plan": "Plan", "Plan checkout was cancelled.": "El pago de plan fue cancelado.", "Plan upgrade was cancelled.": "La actualización de plan fue cancelada.", @@ -126,28 +145,39 @@ "Submit feedback": "Enviar comentarios", "Subscribe": "Suscribir", "Subscribed": "Suscrito", + "Subscription plan updated successfully": "Suscripción actualizada a nuevo plan fue exitosa", "Success": "Éxito", "Success! Check your email for magic link to sign-in.": "¡Éxito! Revisa tu correo electrónico para ver el enlace mágico para iniciar sesión.", "Success! Your account is fully activated, you now have access to all content.": "¡Éxito! Tu cuenta está completamente activada, ahora tienes acceso a todo el contenido.", "Success! Your email is updated.": "¡Éxito! Tu correo electrónico está actualizado.", "Successfully unsubscribed": "Te has dado de baja correctamente", "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "Gracias por suscribirte. Antes de leer, aquí abajo hay otros sitios que te pueden gustar.", - "Thank you for your support": "", - "Thank you for your support!": "", + "Thank you for your support": "Gracias por tu apoyo", + "Thank you for your support!": "¡Gracias por tu apoyo!", "Thanks for the feedback!": "¡Gracias por tus comentarios!", "That didn't go to plan": "Eso no salió según lo planeado", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "La dirección de correo electrónico que tenemos para ti es {{memberEmail}} — si no es correcta, puedes actualizarla en tu .", "There was a problem submitting your feedback. Please try again a little later.": "Hubo un problema al enviar tus comentarios. Vuelve a intentarlo un poco más tarde.", - "There was an error processing your payment. Please try again.": "", + "There was an error cancelling your subscription, please try again.": "Hubo un error cancelando la suscripción, inténtalo de nuevo por favor.", + "There was an error continuing your subscription, please try again.": "Hubo un error en continuar la suscripción, inténtalo de nuevo por favor.", + "There was an error processing your payment. Please try again.": "Hubo un error procesando tu pago. Intentalo de nuevvo por favor.", + "There was an error sending the email, please try again": "Hubo un error enviando el correo electrónico, intentalo de nuevo por favor.", "This site is invite-only, contact the owner for access.": "Este sitio es solo por invitación, contacta al propietario para obtener acceso.", - "This site is not accepting payments at the moment.": "", + "This site is not accepting payments at the moment.": "Este sitio no acepta pagos en este momento.", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Para completar el registro, haz clic en el enlace de confirmación en tu correo electrónico. Si no llega en 3 minutos, ¡revisa tu carpeta de spam!", - "To continue to stay up to date, subscribe to {{publication}} below.": "", + "To continue to stay up to date, subscribe to {{publication}} below.": "Continúa para mantenerte actualizado, suscribete a la {{publication}} justo abajo.", + "Too many attempts try again in {{number}} days.": "Has alcanzado el límite de intentos en {{number}} días.", + "Too many attempts try again in {{number}} hours.": "Has alcanzado el límite de intentos en {{number}} horas.", + "Too many attempts try again in {{number}} minutes.": "Has alcanzzado el límite de intentos en {{number}} minutos.", + "Too many different sign-in attempts, try again in {{number}} days": "Demasiados intentos de iniciar sesión, intentalo de nuevo en {{number}} días", + "Too many different sign-in attempts, try again in {{number}} hours": "Demasiados intentos de iniciar sesión, intentalo de nuevo en {{number}} horas", + "Too many different sign-in attempts, try again in {{number}} minutes": "Demasiados intentos de iniciar sesión, intentalo de nuevo en {{number}} minutos", "Try free for {{amount}} days, then {{originalPrice}}.": "Prueba gratis por {{amount}} dias, luego {{originalPrice}}.", + "Unable to initiate checkout session": "No se pudo iniciar la sesión de pago", "Unlock access to all newsletters by becoming a paid subscriber.": "Desbloquea el acceso a todos los boletines convirtiéndote en un suscriptor pago.", "Unsubscribe from all emails": "Cancelar suscripción a todos los correos electrónicos", "Unsubscribed": "Dado de baja", - "Unsubscribed from all emails.": "", + "Unsubscribed from all emails.": "De suscribirse de todos los correos electrónicos", "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "Cancelar la suscripción a los correos electrónicos no cancelará tu suscripción de pago a {{title}}", "Update": "Actualizar", "Update your preferences": "Actualiza tus preferencias", @@ -160,7 +190,7 @@ "Welcome to {{siteTitle}}": "Bienvenido a {{siteTitle}}", "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "Cuando una bandeja de entrada no acepta un correo electrónico, comúnmente se le llama rebote. En muchos casos, esto puede ser temporal. Sin embargo, en algunos casos, un correo electrónico rebotado puede devolverse como una falla permanente cuando una dirección de correo electrónico no es válida o no existe.", "Why has my email been disabled?": "¿Por qué mi correo electrónico ha sido desabilitado?", - "year": "", + "year": "año", "Yearly": "Anual", "You currently have a free membership, upgrade to a paid subscription for full access.": "Tienes una membresía gratuita, actualiza a una suscripción pagada para obtener acceso completo.", "You have been successfully resubscribed": "Te has vuelto a suscribir con éxito", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Has iniciado sesión correctamente.", "You've successfully subscribed to": "Te has suscrito correctamente a", "Your account": "Tu cuenta", + "Your email has failed to resubscribe, please try again": "Tu correo electrónico ha fallado para re suscribirse, intentalo de nuevo por favor", "Your input helps shape what gets published.": "Tu opinión ayuda a definir lo que se publica.", "Your subscription will expire on {{expiryDate}}": "Tu suscripción caducará el {{expiryDate}} ", "Your subscription will renew on {{renewalDate}}": "Tu suscripción se renovará el {{renewalDate}}", diff --git a/ghost/i18n/locales/es/search.json b/ghost/i18n/locales/es/search.json new file mode 100644 index 00000000000..4150f6f92bd --- /dev/null +++ b/ghost/i18n/locales/es/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "Autores", + "Cancel": "Cancelar", + "No matches found": "No se encontraron coincidencias", + "Posts": "Publicaciones", + "Search posts, tags and authors": "Buscar publicaciones, etiquetas y autores", + "Show more results": "Mostrar más resultados", + "Tags": "Etiquetas" +} diff --git a/ghost/i18n/locales/es/signup-form.json b/ghost/i18n/locales/es/signup-form.json index f4bbf95206a..7e1ca19d232 100644 --- a/ghost/i18n/locales/es/signup-form.json +++ b/ghost/i18n/locales/es/signup-form.json @@ -1,9 +1,9 @@ { "Email sent": "Correo electrónico enviado", - "Now check your email!": "Ahora, ¡revisa tu email!", - "Please enter a valid email address": "Por favor, ingresa una dirección de email válida", + "Now check your email!": "¡Revisa tu correo ahora!", + "Please enter a valid email address": "Por favor, ingresa una dirección de correo válida", "Something went wrong, please try again.": "Algo salió mal, por favor intenta nuevamente.", - "Subscribe": "Suscribirse", - "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Para completar el registro, haz clic en el link en tu bandeja de entrada. Si no llega en los siguientes 3 minutos, ¡revisa tu carpeta de spam!", + "Subscribe": "Suscribete", + "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Para completar el registro, haz clic en el link en la bandeja de entrada. Si no llega en los siguientes 3 minutos, ¡Revisa tu carpeta de spam!", "Your email address": "Tu dirección de correo electrónico" } diff --git a/ghost/i18n/locales/et/comments.json b/ghost/i18n/locales/et/comments.json new file mode 100644 index 00000000000..c9b9d3471d5 --- /dev/null +++ b/ghost/i18n/locales/et/comments.json @@ -0,0 +1,71 @@ +{ + "{{amount}} characters left": "{{amount}} tähemärki jäänud", + "{{amount}} comments": "{{amount}} kommentaari", + "{{amount}} days ago": "{{amount}} päeva tagasi", + "{{amount}} hrs ago": "{{amount}} tundi tagasi", + "{{amount}} mins ago": "{{amount}} minutit tagasi", + "{{amount}} months ago": "{{amount}} kuud tagasi", + "{{amount}} more": "{{amount}} veel", + "{{amount}} seconds ago": "{{amount}} sekundit tagasi", + "{{amount}} weeks ago": "{{amount}} nädalat tagasi", + "{{amount}} years ago": "{{amount}} aastat tagasi", + "1 comment": "1 kommentaar", + "Add comment": "Lisa kommentaar", + "Add context to your comment, share your name and expertise to foster a healthy discussion.": "Lisa oma kommentaarile konteksti, jaga oma nime ja kogemusi, et soodustada tervislikku arutelu.", + "Add reply": "Lisa vastus", + "Already a member?": "Juba liige?", + "Anonymous": "Anonüümne", + "Become a member of {{publication}} to start commenting.": "Kommenteerimise alustamiseks saage {{publication}} liikmeks.", + "Become a paid member of {{publication}} to start commenting.": "Kommenteerimise alustamiseks saage {{publication}} tasuliseks liikmeks.", + "Cancel": "Tühista", + "Comment": "Kommentaar", + "Complete your profile": "Täida oma profiil", + "Delete": "Kustuta", + "Deleted member": "Kustutatud liige", + "Discussion": "Arutelu", + "Edit": "Muuda", + "Edit this comment": "Muuda seda kommentaari", + "edited": "muudetud", + "Enter your name": "Sisesta oma nimi", + "Expertise": "Oskused", + "Founder @ Acme Inc": "Asutaja @ Acme Inc", + "Full-time parent": "Täiskohaga lapsevanem", + "Head of Marketing at Acme, Inc": "Turundusjuht Acme, Inc-s", + "Hide": "Peida", + "Hide comment": "Peida kommentaar", + "Jamie Larson": "Jamie Larson", + "Join the discussion": "Liitu aruteluga", + "Just now": "Just praegu", + "Local resident": "Kohalik elanik", + "Member discussion": "Liikmete arutelu", + "Name": "Nimi", + "Neurosurgeon": "Neurokirurg", + "One day ago": "Üks päev tagasi", + "One hour ago": "Üks tund tagasi", + "One min ago": "Üks minut tagasi", + "One month ago": "Üks kuu tagasi", + "One week ago": "Üks nädal tagasi", + "One year ago": "Üks aasta tagasi", + "Reply": "Vasta", + "Reply to comment": "Vasta kommentaarile", + "Report": "Teata", + "Report comment": "Teata kommentaarist", + "Report this comment?": "Teatada sellest kommentaarist?", + "Save": "Salvesta", + "Sending": "Saatmine", + "Sent": "Saadetud", + "Show": "Näita", + "Show {{amount}} more replies": "Näita {{amount}} vastust veel", + "Show {{amount}} previous comments": "Näita {{amount}} eelmist kommentaari", + "Show 1 more reply": "Näita 1 vastus veel", + "Show 1 previous comment": "Näita eelmist kommentaari", + "Show comment": "Näita kommentaari", + "Sign in": "Logi sisse", + "Sign up now": "Registreeru nüüd", + "Start the conversation": "Alusta vestlust", + "This comment has been hidden.": "See kommentaar on peidetud.", + "This comment has been removed.": "See kommentaar on eemaldatud.", + "Upgrade now": "Uuenda nüüd", + "Yesterday": "Eile", + "Your request will be sent to the owner of this site.": "Teie taotlus saadetakse selle saidi omanikule." +} diff --git a/ghost/i18n/locales/et/ghost.json b/ghost/i18n/locales/et/ghost.json new file mode 100644 index 00000000000..26834390f15 --- /dev/null +++ b/ghost/i18n/locales/et/ghost.json @@ -0,0 +1,34 @@ +{ + "All the best!": "Kõike head!", + "Complete signup for {{siteTitle}}!": "Vii lõpule {{siteTitle}} lehel registreerumine!", + "Complete your sign up to {{siteTitle}}!": "Vii lõpule {{siteTitle}} lehel oma registreerumine!", + "Confirm email address": "Kinnita e-posti aadress", + "Confirm signup": "Kinnita registreerimine", + "Confirm your email address": "Kinnita oma e-posti aadress", + "Confirm your email update for {{siteTitle}}!": "Kinnita oma e-posti aadressi uuendus {{siteTitle}} jaoks!", + "Confirm your subscription to {{siteTitle}}": "Kinnita oma {{siteTitle}} tellimus", + "For your security, the link will expire in 24 hours time.": "Teie turvalisuse huvides aegub link 24 tunni pärast.", + "Hey there,": "Tere,", + "Hey there!": "Tere!", + "If you did not make this request, you can safely ignore this email.": "Kui sa ei teinud seda päringut, võid seda e-kirja julgelt ignoreerida.", + "If you did not make this request, you can simply delete this message.": "Kui sa ei teinud seda päringut, võid selle sõnumi lihtsalt ära kustutada.", + "Please confirm your email address with this link:": "Palun kinnita oma e-posti aadress selle lingiga:", + "Secure sign in link for {{siteTitle}}": "Turvaline sisselogimislink {{siteTitle}} lehele", + "See you soon!": "Kohtumiseni!", + "Sent to {{email}}": "Saadetud aadressile {{email}}", + "Sign in": "Logi sisse", + "Sign in to {{siteTitle}}": "Logi sisse {{siteTitle}} lehele", + "Tap the link below to complete the signup process for {{siteTitle}}, and be automatically signed in:": "Vajuta alloleval lingil, et lõpetada {{siteTitle}} registreerimisprotsess ja et automaatselt sisse logida:", + "Thank you for signing up to {{siteTitle}}!": "Aitäh, et registreerusid {{siteTitle}} kasutajaks!", + "Thank you for subscribing to {{siteTitle}}!": "Aitäh, et hakkasid {{siteTitle}} jälgijaks!", + "Thank you for subscribing to {{siteTitle}}.": "Aitäh, et hakkasid {{siteTitle}} jälgijaks.", + "Thank you for subscribing to {{siteTitle}}. Tap the link below to be automatically signed in:": "Aitäh, et hakkasid {{siteTitle}} jälgijaks. Vajuta alloleval lingil, et automaatselt sisse logida:", + "This email address will not be used.": "Seda e-posti aadressi ei kasutata.", + "Welcome back to {{siteTitle}}!": "Tere tulemast tagasi {{siteTitle}} lehele!", + "Welcome back! Use this link to securely sign in to your {{siteTitle}} account:": "Tere tulemast tagasi! Kasuta seda linki, et turvaliselt oma {{siteTitle}} kontole sisse logida:", + "You can also copy & paste this URL into your browser:": "Võid ka kopeerida ja kleepida selle URL-i oma brauserisse:", + "You will not be signed up, and no account will be created for you.": "Sind ei registreerita ja sulle ei looda kontot.", + "You will not be subscribed.": "Sind ei lisata jälgijate hulka.", + "You're one tap away from subscribing to {{siteTitle}} — please confirm your email address with this link:": "Oled vaid ühe klõpsu kaugusel {{siteTitle}} jälgijaks hakkamisest — palun kinnita oma e-posti aadress selle lingiga:", + "You're one tap away from subscribing to {{siteTitle}}!": "Oled vaid ühe klõpsu kaugusel {{siteTitle}} jälgijaks hakkamisest!" +} diff --git a/ghost/i18n/locales/et/portal.json b/ghost/i18n/locales/et/portal.json new file mode 100644 index 00000000000..69fe5e8a16c --- /dev/null +++ b/ghost/i18n/locales/et/portal.json @@ -0,0 +1,208 @@ +{ + "(save {{highestYearlyDiscount}}%)": "(säästa {{highestYearlyDiscount}}%)", + "{{amount}} days free": "{{amount}} päeva tasuta", + "{{amount}} off": "{{amount}} soodustust", + "{{amount}} off for first {{number}} months.": "{{amount}} soodustust esimeseks {{number}} kuuks.", + "{{amount}} off for first {{period}}.": "{{amount}} soodustust esimeseks {{period}}ks.", + "{{amount}} off forever.": "{{amount}} soodustust igaveseks.", + "{{discount}}% discount": "{{discount}}% allahindlus", + "{{memberEmail}} will no longer receive {{newsletterName}} newsletter.": "{{memberEmail}} ei saa enam {{newsletterName}} uudiskirja.", + "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "{{memberEmail}} ei saa enam e-kirju, kui keegi vastab teie kommentaaridele.", + "{{memberEmail}} will no longer receive this newsletter.": "{{memberEmail}} ei saa enam seda uudiskirja.", + "{{trialDays}} days free": "{{trialDays}} päeva tasuta", + "+1 (123) 456-7890": "+1 (123) 456-7890", + "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Sisselogimislink on saadetud teie postkasti. Kui see ei saabu 3 minuti jooksul, kontrollige kindlasti oma rämpsposti kausta.", + "Account": "Konto", + "Account details updated successfully": "", + "Account settings": "Konto seaded", + "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Pärast tasuta prooviversiooni lõppu võetakse teilt tasu valitud taseme tavahinna eest. Saate alati enne seda tühistada.", + "Already a member?": "Juba liige?", + "An error occurred": "", + "An unexpected error occured. Please try again or contact support if the error persists.": "Tekkis ootamatu viga. Palun proovige uuesti või võtke ühendust toega, kui viga püsib.", + "Back": "Tagasi", + "Back to Log in": "Tagasi sisselogimise juurde", + "Billing info": "Arveldusinfo", + "Black Friday": "Must Reede", + "Cancel anytime.": "Tühistage igal ajal.", + "Cancel subscription": "Tühista tellimus", + "Cancellation reason": "Tühistamise põhjus", + "Change": "Muuda", + "Change plan": "Muuda paketti", + "Check spam & promotions folders": "Kontrollige rämpsposti ja reklaamide kaustu", + "Check with your mail provider": "Kontrollige oma e-posti teenusepakkujaga", + "Check your inbox to verify email update": "", + "Choose": "Vali", + "Choose a different plan": "Vali teine pakett", + "Choose a plan": "Vali pakett", + "Choose your newsletters": "Vali oma uudiskirjad", + "Click here to retry": "Klõpsake siia, et uuesti proovida", + "Close": "Sulge", + "Comments": "Kommentaarid", + "Complimentary": "Tasuta", + "Confirm": "Kinnita", + "Confirm cancellation": "Kinnita tühistamine", + "Confirm subscription": "Kinnita tellimus", + "Contact support": "Võta ühendust toega", + "Continue": "Jätka", + "Continue subscription": "Jätka tellimust", + "Could not create stripe checkout session": "", + "Could not sign in. Login link expired.": "Ei saanud sisse logida. Sisselogimislink on aegunud.", + "Could not update email! Invalid link.": "E-posti ei saanud uuendada! Vigane link.", + "Create a new contact": "Loo uus kontakt", + "Current plan": "Praegune pakett", + "Delete account": "Kustuta konto", + "Didn't mean to do this? Manage your preferences .": "Ei soovinud seda teha? Halda oma eelistusi .", + "Don't have an account?": "Pole kontot?", + "Edit": "Muuda", + "Email": "E-post", + "Email newsletter": "E-posti uudiskiri", + "Email newsletter settings updated": "", + "Email preferences": "E-posti eelistused", + "Emails": "E-kirjad", + "Emails disabled": "E-kirjad keelatud", + "Ends {{offerEndDate}}": "Lõpeb {{offerEndDate}}", + "Enter your email address": "Sisestage oma e-posti aadress", + "Enter your name": "Sisestage oma nimi", + "Error": "Viga", + "Expires {{expiryDate}}": "Aegub {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", + "Forever": "Igavesti", + "Free Trial – Ends {{trialEnd}}": "Tasuta prooviversioon – Lõpeb {{trialEnd}}", + "Get help": "Hangi abi", + "Get in touch for help": "Võta ühendust abi saamiseks", + "Get notified when someone replies to your comment": "Saa teavitus, kui keegi vastab sinu kommentaarile", + "Give feedback on this post": "Anna tagasisidet sellele postitusele", + "Help! I'm not receiving emails": "Aidake! Ma ei saa e-kirju", + "Here are a few other sites you may enjoy.": "Siin on mõned teised saidid, mis võiksid teile meeldida.", + "If a newsletter is flagged as spam, emails are automatically disabled for that address to make sure you no longer receive any unwanted messages.": "Kui uudiskiri märgitakse rämpspostiks, keelatakse sellele aadressile automaatselt e-kirjad, et te ei saaks enam soovimatuid sõnumeid.", + "If the spam complaint was accidental, or you would like to begin receiving emails again, you can resubscribe to emails by clicking the button on the previous screen.": "Kui rämpspostiteatamise kaebus oli juhuslik või soovite uuesti e-kirju saada, saate e-kirjade tellimuse uuesti tellida, klõpsates eelmisel ekraanil olevat nuppu.", + "If you cancel your subscription now, you will continue to have access until {{periodEnd}}.": "Kui tühistate oma tellimuse nüüd, jääb teile juurdepääs kuni {{periodEnd}}.", + "If you have a corporate or government email account, reach out to your IT department and ask them to allow emails to be received from {{senderEmail}}": "Kui teil on ettevõtte või valitsuse e-posti konto, pöörduge oma IT-osakonna poole ja paluge neil lubada e-kirjade vastuvõtmine aadressilt {{senderEmail}}", + "If you would like to start receiving emails again, the best next steps are to check your email address on file for any issues and then click resubscribe on the previous screen.": "Kui soovite uuesti e-kirju saada, on järgmiseks parimaks sammuks kontrollida oma registreeritud e-posti aadressi probleemide suhtes ja seejärel klõpsata eelmisel ekraanil uuesti tellimisnuppu.", + "If you're not receiving the email newsletter you've subscribed to, here are a few things to check.": "Kui te ei saa e-posti uudiskirja, mille olete tellinud, on siin mõned asjad, mida kontrollida.", + "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "Kui olete kõik need kontrollid läbi viinud ja te ikka ei saa e-kirju, võite abi saamiseks pöörduda aadressil {{supportAddress}}.", + "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "Kui uudiskirja saatmise katsel saadakse püsiv tõrge, keelatakse kontol e-kirjad.", + "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "Lisage oma e-posti kliendis {{senderEmail}} oma kontaktide loendisse. See annab teie e-posti teenusepakkujale märku, et sellelt aadressilt saadetud e-kirju tuleks usaldada.", + "Invalid email address": "Vigane e-posti aadress", + "Jamie Larson": "Jamie Larson", + "jamie@example.com": "jamie@example.com", + "Less like this": "Vähem sellist", + "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "Veenduge, et e-kirjad ei satuks kogemata teie postkasti rämpsposti või reklaamide kausta. Kui need on seal, klõpsake \"Märgi mitte-rämpspostiks\" ja/või \"Liiguta postkasti\".", + "Manage": "Halda", + "Maybe later": "Võib-olla hiljem", + "Memberships unavailable, contact the owner for access.": "Liikmestaatus pole saadaval, võtke juurdepääsu saamiseks ühendust omanikuga.", + "month": "kuu", + "Monthly": "Igakuine", + "More like this": "Rohkem sellist", + "Name": "Nimi", + "Need more help? Contact support": "Vajate rohkem abi? Võtke ühendust toega", + "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Uudiskirjad võidakse teie kontol keelata kahel põhjusel: eelmine e-kiri märgiti rämpspostiks või e-kirja saatmise katse põhjustas püsiva tõrke (tagasipõrke).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", + "Not receiving emails?": "Ei saa e-kirju?", + "Now check your email!": "Nüüd kontrollige oma e-posti!", + "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Kui olete uuesti tellinud ja te ikka ei näe e-kirju oma postkastis, kontrollige oma rämpsposti kausta. Mõned postkasti teenusepakkujad säilitavad varasemaid rämpspostikaebusi ja jätkavad e-kirjade märkimist. Kui see juhtub, märkige viimane uudiskiri 'Mitte rämpspost', et see liiguks tagasi teie põhipostkasti.", + "Permanent failure (bounce)": "Püsiv tõrge (tagasipõrge)", + "Phone number": "Telefoninumber", + "Plan": "Pakett", + "Plan checkout was cancelled.": "Paketi tellimuse vormistamine tühistati.", + "Plan upgrade was cancelled.": "Paketi uuendamine tühistati.", + "Please contact {{supportAddress}} to adjust your complimentary subscription.": "Palun võtke ühendust {{supportAddress}}, et kohandada oma tasuta tellimust.", + "Please enter {{fieldName}}": "Palun sisestage {{fieldName}}", + "Please fill in required fields": "Palun täitke kohustuslikud väljad", + "Price": "Hind", + "Re-enable emails": "Luba e-kirjad uuesti", + "Recommendations": "Soovitused", + "Renews at {{price}}.": "Uueneb hinnaga {{price}}.", + "Retry": "Proovi uuesti", + "Save": "Salvesta", + "Send an email and say hi!": "Saatke e-kiri ja öelge tere!", + "Send an email to {{senderEmail}} and say hello. This can also help signal to your mail provider that emails to and from this address should be trusted.": "Saatke e-kiri aadressile {{senderEmail}} ja öelge tere. See võib aidata anda teie e-posti teenusepakkujale märku, et selle aadressiga seotud e-kirju tuleks usaldada.", + "Sending login link...": "Sisselogimislingi saatmine...", + "Sending...": "Saatmine...", + "Show all": "Näita kõiki", + "Sign in": "Logi sisse", + "Sign out": "Logi välja", + "Sign up": "Registreeru", + "Signup error: Invalid link": "Registreerimise viga: Vigane link", + "Something went wrong, please try again later.": "Midagi läks valesti, palun proovige hiljem uuesti.", + "Sorry, that didn’t work.": "", + "Spam complaints": "Rämpsposti kaebused", + "Start {{amount}}-day free trial": "Alusta {{amount}}-päevast tasuta prooviversiooni", + "Starting {{startDate}}": "Algab {{startDate}}", + "Starting today": "Algab täna", + "Submit feedback": "Saada tagasiside", + "Subscribe": "Telli", + "Subscribed": "Tellitud", + "Subscription plan updated successfully": "", + "Success": "Õnnestus", + "Success! Check your email for magic link to sign-in.": "Õnnestus! Kontrollige oma e-posti, et leida maagiline sisselogimislink.", + "Success! Your account is fully activated, you now have access to all content.": "Õnnestus! Teie konto on täielikult aktiveeritud, teil on nüüd juurdepääs kogu sisule.", + "Success! Your email is updated.": "Õnnestus! Teie e-post on uuendatud.", + "Successfully unsubscribed": "Tellimus on edukalt tühistatud", + "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "Täname tellimise eest. Enne kui alustate lugemist, on allpool mõned teised saidid, mis võiksid teile meeldida.", + "Thank you for your support": "Täname teid toetuse eest", + "Thank you for your support!": "Täname teid toetuse eest!", + "Thanks for the feedback!": "Täname tagasiside eest!", + "That didn't go to plan": "See ei läinud plaanipäraselt", + "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Meie käsutuses olev e-posti aadress teie jaoks on {{memberEmail}} — kui see pole õige, saate seda muuta oma ", + "There was a problem submitting your feedback. Please try again a little later.": "Teie tagasiside esitamisel tekkis probleem. Palun proovige natuke hiljem uuesti.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", + "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", + "This site is invite-only, contact the owner for access.": "See sait on ainult kutsetega, juurdepääsu saamiseks võtke ühendust omanikuga.", + "This site is not accepting payments at the moment.": "", + "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Registreerimise lõpetamiseks klõpsake oma postkastis kinnituslingile. Kui see ei saabu 3 minuti jooksul, kontrollige oma rämpsposti kausta!", + "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Try free for {{amount}} days, then {{originalPrice}}.": "Proovige tasuta {{amount}} päeva, seejärel {{originalPrice}}.", + "Unable to initiate checkout session": "", + "Unlock access to all newsletters by becoming a paid subscriber.": "Avage juurdepääs kõigile uudiskirjadele, hakates tasuliseks tellijaks.", + "Unsubscribe from all emails": "Tühista kõikide e-kirjade tellimus", + "Unsubscribed": "Tellimus tühistatud", + "Unsubscribed from all emails.": "Kõikide e-kirjade tellimus tühistatud", + "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "E-kirjade tellimusest loobumine ei tühista teie tasulist tellimust {{title}}-le", + "Update": "Uuenda", + "Update your preferences": "Uuendage oma eelistusi", + "Verification link sent, check your inbox": "Kinnituslink saadetud, kontrollige oma postkasti", + "Verify your email address is correct": "Veenduge, et teie e-posti aadress on õige", + "View plans": "Vaata plaane", + "We couldn't unsubscribe you as the email address was not found. Please contact the site owner.": "Me ei saanud teie tellimust tühistada, kuna e-posti aadressi ei leitud. Palun võtke ühendust saidi omanikuga.", + "Welcome back, {{name}}!": "Tere tulemast tagasi, {{name}}!", + "Welcome back!": "Tere tulemast tagasi!", + "Welcome to {{siteTitle}}": "Tere tulemast {{siteTitle}}-sse", + "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "Kui postkast ei võta e-kirja vastu, nimetatakse seda tavaliselt tagasipõrkeks. Paljudel juhtudel võib see olla ajutine. Kuid mõnel juhul võib tagasipõrkunud e-kiri tagastada püsiva tõrke, kui e-posti aadress on kehtetu või seda pole olemas.", + "Why has my email been disabled?": "Miks on minu e-post välja lülitatud?", + "year": "", + "Yearly": "Aastane", + "You currently have a free membership, upgrade to a paid subscription for full access.": "Teil on praegu tasuta liikmelisus, täieliku juurdepääsu saamiseks uuendage tasulisele tellimusele.", + "You have been successfully resubscribed": "Olete edukalt uuesti tellinud", + "You're currently not receiving emails": "Te ei saa praegu e-kirju", + "You're not receiving emails": "Te ei saa e-kirju", + "You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "Te ei saa e-kirju, kuna kas märkisite hiljutise sõnumi rämpspostiks või sõnumeid ei saanud teie antud e-posti aadressile kohale toimetada.", + "You've successfully signed in.": "Olete edukalt sisse loginud.", + "You've successfully subscribed to": "Olete edukalt tellinud", + "Your account": "Teie konto", + "Your email has failed to resubscribe, please try again": "", + "Your input helps shape what gets published.": "Teie sisend aitab kujundada seda, mida avaldatakse.", + "Your subscription will expire on {{expiryDate}}": "Teie tellimus aegub {{expiryDate}}", + "Your subscription will renew on {{renewalDate}}": "Teie tellimus uueneb {{renewalDate}}", + "Your subscription will start on {{subscriptionStart}}": "Teie tellimus algab {{subscriptionStart}}" +} diff --git a/ghost/i18n/locales/et/search.json b/ghost/i18n/locales/et/search.json new file mode 100644 index 00000000000..fe5c0603acc --- /dev/null +++ b/ghost/i18n/locales/et/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "Autorid", + "Cancel": "Tühista", + "No matches found": "Vasteid ei leitud", + "Posts": "Postitused", + "Search posts, tags and authors": "Otsi postitusi, silte ja autoreid", + "Show more results": "Näita rohkem tulemusi", + "Tags": "Sildid" +} diff --git a/ghost/i18n/locales/et/signup-form.json b/ghost/i18n/locales/et/signup-form.json new file mode 100644 index 00000000000..f5ab62cd01a --- /dev/null +++ b/ghost/i18n/locales/et/signup-form.json @@ -0,0 +1,9 @@ +{ + "Email sent": "E-kiri saadetud", + "Now check your email!": "Nüüd kontrolli oma e-posti!", + "Please enter a valid email address": "Palun sisesta kehtiv e-posti aadress", + "Something went wrong, please try again.": "Midagi läks valesti, palun proovi uuesti.", + "Subscribe": "Telli", + "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Registreerimise lõpetamiseks klõpsa kinnituslingil oma postkastis. Kui see ei saabu 3 minuti jooksul, kontrolli oma rämpsposti kausta!", + "Your email address": "Sinu e-posti aadress" +} diff --git a/ghost/i18n/locales/fa/comments.json b/ghost/i18n/locales/fa/comments.json index ea15e2c0913..e966ee2b993 100644 --- a/ghost/i18n/locales/fa/comments.json +++ b/ghost/i18n/locales/fa/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "پاسخ دادن به دیدگاه", "Report": "گزارش", "Report comment": "گزارش دیدگاه", - "Report this comment": "گزارش کردن این دیدگاه", "Report this comment?": "می\u200cخواهید این دیدگاه را گزارش کنید؟", "Save": "ذخیره کردن", "Sending": "در حال ارسال", @@ -68,6 +67,5 @@ "This comment has been removed.": "این دیدگاه پاک شده است", "Upgrade now": "ارتقاء دهید", "Yesterday": "دیروز", - "You want to report this comment?": "آیا می\u200cخواهید این دیدگاه را گزارش دهید؟", "Your request will be sent to the owner of this site.": "درخواست شما برای مدیر این وب\u200cسایت ارسال خواهد شد." } diff --git a/ghost/i18n/locales/fa/portal.json b/ghost/i18n/locales/fa/portal.json index 554bac1b214..25978a4fd2a 100644 --- a/ghost/i18n/locales/fa/portal.json +++ b/ghost/i18n/locales/fa/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "یک پیوند ورود برای ایمیل شما ارسال شد. در صورتی که به دست شما نرسید، پوشه اسپم خود را برررسی کنید", "Account": "حساب کاربری", + "Account details updated successfully": "", "Account settings": "تنظیمات حساب کاربری", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "پس از این\u200cکه دوره رایکان شما پایان یابد، براساس بسته\u200cی انتخابی شما مبلغی از حساب شما برداشت می\u200cشود. شما همیشه می\u200cتوانید قبل از آن تاریخ، بسته\u200cی خود را تغییر و یا لغو کنید.", "Already a member?": "عضو هستید؟", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "خطایی غیرمنتظره رخ داد. خواهشمند است که دوباره تلاش کنید و یا با پشتیبانی در صورتی که خطا ادامه\u200cدار بود تماس بگیرید.", "Back": "بازگشت", "Back to Log in": "بازگشت به برگه ورود", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "پوشه اسپم و یا تبلیغات خود را بررسی کنید", "Check with your mail provider": "موضوع را با ارائه\u200cدهنده ایمیل خود بررسی کنید", + "Check your inbox to verify email update": "", "Choose": "انتخاب", "Choose a different plan": "بسته\u200cای دیگر انتخاب کنید", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "تماس با پشتیبانی", "Continue": "ادامه دادن", "Continue subscription": "ادامه اشتراک", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "امکان ورود وجود نداشت، پیوند ورود منقضی شده است.", "Could not update email! Invalid link.": "امکان به\u200cروزرسانی ایمیل وجود نداشت! پیوند اشتباه بود.", "Create a new contact": "مخاطبی تازه بسازید", @@ -52,6 +56,7 @@ "Edit": "ویرایش", "Email": "ایمیل", "Email newsletter": "ایمیل خبرنامه", + "Email newsletter settings updated": "", "Email preferences": "تنظیمات ایمیل", "Emails": "ایمیل\u200cها", "Emails disabled": "ایمیل\u200cها غیرفعال هستند", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "خطا", "Expires {{expiryDate}}": "در {{expiryDate}} منقضی می\u200cشود", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "برای همیشه", "Free Trial – Ends {{trialEnd}}": "دوره رایگان - بعد از {{trialEnd}} تمام می\u200cشود", "Get help": "پشتیبانی بگیرید", @@ -91,6 +108,8 @@ "Name": "نام", "Need more help? Contact support": "کمک بیشتری لازم دارید؟ با پشتیبانی تماس بگیرید", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "خبرنامه\u200cها ممکن است به خاطر دو دلیل برای شما غیرفعال شده باشند: ایمیلی که قبلاً برای شما ارسال شده به عنوان اسپم علامت\u200cگذاری شده باشد و یا این که با یک شکست دائمی (bounce) روبرو شده باشد.", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "ایمیلی دریافت نمی\u200cکنید؟", "Now check your email!": "حالا صندوق ورودی ایمیل خود را بررسی کنید!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "پس از دریافت مجدد اشتراک، در صورتی که کماکان ایمیل\u200cها را در ایمیل خود نمی\u200cبینید، پوشه اسپم را بررسی کنید. برخی از سرویس\u200cدهندگان تاریخچه گزارش اسپم را نگهداری می\u200cکنند و همچنان ایمیل\u200cها را به عنوان اسپم علامت\u200cگذاری می\u200cکنند. در صورتی که این مورد وجود داشت، ایمیل را با عنوان «اسپم نیست» علامت\u200cگذاری کنید تا آن را به صندوق ورودی انتقال دهد.", @@ -126,6 +145,7 @@ "Submit feedback": "ثبت بازخورد", "Subscribe": "دریافت اشتراک", "Subscribed": "مشترک هستید", + "Subscription plan updated successfully": "", "Success": "کار با موفقیت انجام شد", "Success! Check your email for magic link to sign-in.": "انجام شد! ایمیل خود را برای لینک ورود بررسی کنید.", "Success! Your account is fully activated, you now have access to all content.": "انجام شد! حساب کاربری شما به طور کامل فعال شد، شما حالا می\u200cتوانید به تمام محتواها دسترسی داشته باشید.", @@ -138,12 +158,22 @@ "That didn't go to plan": "کار به درستی پیش نرفت", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "آدرس ایمیلی که ما از شما داریم {{memberEmail}} است - اگر که این آدرس درست نیست می\u200cتوانید در آن را به\u200cروز کنید.", "There was a problem submitting your feedback. Please try again a little later.": "خطائی در زمان ثبت بازخورد شما اتفاق افتاد. خواهشمند است دوباره تلاش کنید.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "دسترسی به این وب\u200cسایت نیازمند دعوت\u200cنامه است، با مالک آن برای دریافت دسترسی تماس بگیرید.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "برای تکمیل ثبت نام، برروی پیوند تأیید در صندوق ورودی ایمیل خود کلیک کنید. در صورتی که به دست شما نرسید، پوشه اسپم خود را برررسی کنید!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "برای {{amount}} روز به صورت رایگان امتحان کنید، سپس با قیمت {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "با دریافت اشتراک پولی به شما دسترسی به تمامی خبرنامه\u200cها داده می\u200cشود.", "Unsubscribe from all emails": "لغو اشتراک دریافت تمامی ایمیل\u200cها", "Unsubscribed": "اشتراک لغو شد", @@ -170,6 +200,7 @@ "You've successfully signed in.": "شما با موفقیت وارد شدید.", "You've successfully subscribed to": "شما با موفقیت مشترک این موارد شدید:", "Your account": "حساب کاربری شما", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "تلاش شما به آنچه که منتشر می\u200cشود، شکل می\u200cدهد.", "Your subscription will expire on {{expiryDate}}": "اشتراک شما در تاریخ {{expiryDate}} منقضی می\u200cشود", "Your subscription will renew on {{renewalDate}}": "اشتراک شما در تاریخ {{renewalDate}} تمدید می\u200cشود", diff --git a/ghost/i18n/locales/fa/search.json b/ghost/i18n/locales/fa/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/fa/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/fi/comments.json b/ghost/i18n/locales/fi/comments.json index 2018d85fbac..9c2e0b9d68b 100644 --- a/ghost/i18n/locales/fi/comments.json +++ b/ghost/i18n/locales/fi/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Vastaa kommenttiin", "Report": "Ilmianna", "Report comment": "Ilmianna kommentti", - "Report this comment": "Ilmianna tämä kommentti", "Report this comment?": "Haluatko ilmiantaa tämän kommentin?", "Save": "Tallenna", "Sending": "Lähetetään", @@ -68,6 +67,5 @@ "This comment has been removed.": "Tämä kommentti on poistettu", "Upgrade now": "Korota nyt", "Yesterday": "Eilen", - "You want to report this comment?": "Haluatko ilmiantaa tämän kommentin?", "Your request will be sent to the owner of this site.": "Sinun pyyntösi lähetetään sivun omistajalle" } diff --git a/ghost/i18n/locales/fi/portal.json b/ghost/i18n/locales/fi/portal.json index c125591a84f..537d2b65f51 100644 --- a/ghost/i18n/locales/fi/portal.json +++ b/ghost/i18n/locales/fi/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Kirjautumislinkki on lähetetty sähköpostiisi. Jos se ei tule 3 minuutin kuluessa, muista katsoa spam-kansiosi.", "Account": "Oma tili", + "Account details updated successfully": "", "Account settings": "Tilin asetukset", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Kun ilmainen kokeilusi loppuu, sinulta veloitetaan valitsemasi tilauksen kuukausimaksu. Voit aina peruuttaa tilauksesi ennen tätä.", "Already a member?": "Oletko jo jäsen?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Odottamaton virhe syntyi, yritäthän uudestaan tai contact support jos ongelma jatkuu.", "Back": "Takaisin", "Back to Log in": "Takaisin kirjautumiseen", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Katso spam & promotions kansiot", "Check with your mail provider": "Tarkista sähköpostitarjoajaltasi", + "Check your inbox to verify email update": "", "Choose": "Valitse", "Choose a different plan": "Valitse toinen tilaus", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Ota yhteyttä tukeen", "Continue": "Jatka", "Continue subscription": "Jatka tilaustasi", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Kirjautuminen epäonnistui. Kirjautumislinkki on vanhentunut.", "Could not update email! Invalid link.": "Sähköpostia ei pystytty päivittämään! Linkki ei toimi.", "Create a new contact": "Luo uusi kontakti", @@ -52,6 +56,7 @@ "Edit": "Muokkaa", "Email": "Sähköposti", "Email newsletter": "Uutiskirje sähköpostiin", + "Email newsletter settings updated": "", "Email preferences": "Sähköpostiasetukset", "Emails": "Sähköpostit", "Emails disabled": "Sähköpostit pois käytöstä", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Virhe", "Expires {{expiryDate}}": "Vanhenee {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Ikuisesti", "Free Trial – Ends {{trialEnd}}": "Ilmainen kokeilu – Loppuu {{trialEnd}}", "Get help": "Pyydä apua", @@ -91,6 +108,8 @@ "Name": "Nimi", "Need more help? Contact support": "Tarvitsetko lisää apua? Ota yhteyttä tukeen", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Uutiskirjeet sähköpostiisi voivat peruuntua kahdesta syystä: edellinen sähköposti oli merkattu spammiksi, tai sähköpostin lähetyksestä tuli bounce.", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Etkö saa sähköposteja?", "Now check your email!": "Nyt tarkista sähköpostisi", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Kun olet tilannut sähköpostit uudelleen ja et vieläkään saa posteja, katso ensimmäiseksi spam-kansio. Jotkut tarjoajat pitävät listaa ennen merkityistä viesteistä ja estävät niitä jatkossakin. Jos näin tapahtuu, merkitse viesti Not Spam ja siirrä se postilaatikkoosi.", @@ -126,6 +145,7 @@ "Submit feedback": "Anna palautetta", "Subscribe": "Tilaa", "Subscribed": "Tilaus onnistunut", + "Subscription plan updated successfully": "", "Success": "Onnistunut", "Success! Check your email for magic link to sign-in.": "Onnistui! Katso sähköpostisi kirjautumislinkkiä varten", "Success! Your account is fully activated, you now have access to all content.": "Onnistui! Tilisi on aktivoitu ja sinulla on pääsy sisältöihin", @@ -138,12 +158,22 @@ "That didn't go to plan": "Tämä ei mennyt suunnitelmien mukaan", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "", "There was a problem submitting your feedback. Please try again a little later.": "Meillä oli ongelma palautteesi lähetyksen kanssa. Kokeleithan myöhemmin uudestaan.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Tämä sivu on vain kutsutuille, ota yhteyttä omistajaan saadaksesi pääsyoikeuden.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Viimeistelläksesi rekisteröitymisen, klikkaa vahvistuslinkkiä sähköpostissasi. Jos sitä ei saavu 3 minuutin kuluessa, tarkista roskapostikansiosi!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Kokeile ilmaiseksi {{amount}} päivää, sen jälkeen hinta on {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Avaa pääsy kaikkiin uutiskirjeisiin maksullisella tilauksella.", "Unsubscribe from all emails": "Peruuta kaikki sähköpostit", "Unsubscribed": "Tilaus peruutettu", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Olet kirjautunut sisään onnistuneesti", "You've successfully subscribed to": "", "Your account": "Tilisi", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Antamasi palautteen avulla muokataan julkaistavaa sisältöä", "Your subscription will expire on {{expiryDate}}": "Tilauksesi päättyy {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Tilauksesi uusiutuu {{renewalDate}}", diff --git a/ghost/i18n/locales/fi/search.json b/ghost/i18n/locales/fi/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/fi/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/fr/comments.json b/ghost/i18n/locales/fr/comments.json index e17aaffdc77..e2a41f6c783 100644 --- a/ghost/i18n/locales/fr/comments.json +++ b/ghost/i18n/locales/fr/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Répondre au commentaire", "Report": "Signaler", "Report comment": "Signalez le commentaire", - "Report this comment": "Signalez ce commentaire", "Report this comment?": "Signaler ce commentaire ?", "Save": "Enregistrer", "Sending": "Envoi en cours", @@ -68,6 +67,5 @@ "This comment has been removed.": "Ce commentaire a été supprimé.", "Upgrade now": "Mettre à jour maintenant", "Yesterday": "Hier", - "You want to report this comment?": "Souhaitez-vous signaler ce commentaire ?", "Your request will be sent to the owner of this site.": "Votre demande sera transférée au propriétaire de ce site." } diff --git a/ghost/i18n/locales/fr/portal.json b/ghost/i18n/locales/fr/portal.json index 1ce1093a270..83053bfbfdd 100644 --- a/ghost/i18n/locales/fr/portal.json +++ b/ghost/i18n/locales/fr/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Un lien de connexion a été envoyé dans votre boîte de réception. S’il n’arrive pas dans les 3 minutes, vérifiez votre dossier d'indésirables.", "Account": "Compte", + "Account details updated successfully": "", "Account settings": "Paramètres de compte", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "À la fin de la période d’essai gratuite, le prix normal de l’abonnement choisi sera facturé. Vous pourrez toujours l'annuler d’ici là.", "Already a member?": "Déjà membre ?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Une erreur inattendue s'est produite. Veuillez réessayer ou écrire à l'assistance si l'erreur persiste.", "Back": "Retour", "Back to Log in": "Retour à Connexion", @@ -28,6 +30,7 @@ "Change plan": "Changez d'abonnement", "Check spam & promotions folders": "Vérifiez les dossiers d'indésirables et de promotion", "Check with your mail provider": "Vérifiez auprès de votre fournisseur d'email", + "Check your inbox to verify email update": "", "Choose": "Choisir", "Choose a different plan": "Choisir un autre abonnement", "Choose a plan": "Choisir un abonnement", @@ -42,6 +45,7 @@ "Contact support": "Écrire à l'assistance", "Continue": "Continuer", "Continue subscription": "Poursuivre l'abonnement", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Impossible de se connecter. Le lien de connexion a expiré.", "Could not update email! Invalid link.": "Impossible de mettre à jour l'email ! Le lien est invalide.", "Create a new contact": "Créer un nouveau contact", @@ -52,6 +56,7 @@ "Edit": "Modifier", "Email": "Email", "Email newsletter": "Email de la newsletter", + "Email newsletter settings updated": "", "Email preferences": "Préférences email", "Emails": "Emails", "Emails disabled": "Emails désactivés", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Erreur", "Expires {{expiryDate}}": "Expire le {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "Échec de l'envoi de l'email avec le lien magique", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Permanent", "Free Trial – Ends {{trialEnd}}": "Essai gratuit - Se termine le {{trialEnd}}", "Get help": "Obtenir de l’aide", @@ -91,6 +108,8 @@ "Name": "Nom", "Need more help? Contact support": "Besoin d'aide? Écrivez à l'assistance", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Les newsletters peuvent être désactivées de votre compte pour deux raisons : un précédent email a été marqué indésirable ou une tentative d'envoi d'email a entrapiné en une erreur persistante (renvoi).", + "No member exists with this e-mail address.": "Aucun membre n'existe avec cette adresse email.", + "No member exists with this e-mail address. Please sign up first.": "Aucun membre n'existe avec cette adresse email. Veuillez vous inscrire d'abord.", "Not receiving emails?": "Vous n’avez pas reçu d’emails ?", "Now check your email!": "Veuillez vérifier votre boîte de réception.", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Après vote résinscription, si vous ne voyez toujours pas d'emails dans votre boîte de réception, veuillez vérifier votre dossier d'indésirables. Certains fournisseurs gardent en mémoire les précédents signalements et continuent de marquer ces emails comme indésirables. Si tel était le cas, veuillez signaler la dernière newsletter comme 'désirable' et placez-la dans votre boîte de réception.", @@ -126,6 +145,7 @@ "Submit feedback": "Envoyez votre avis", "Subscribe": "S'abonner", "Subscribed": "Abonné-e", + "Subscription plan updated successfully": "", "Success": "Réussi", "Success! Check your email for magic link to sign-in.": "C'est fait ! Vérifiez votre boîte de réception pour le lien de connexion.", "Success! Your account is fully activated, you now have access to all content.": "Ça y est ! Votre compte est entièrement activé et vous avez désormais accès à tout le contenu.", @@ -138,12 +158,22 @@ "That didn't go to plan": "Cela n’a pas fonctionné comme prévu", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "L'adresse email qui nous a été indiquée est {{memberEmail}} — Si celle-ci est incorrecte, vous pourrez la modifier dans ", "There was a problem submitting your feedback. Please try again a little later.": "Un problème est survenu lors de la soumission de votre commentaire. Veuillez réessayer un peu plus tard.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Ce site est réservé aux invités. Veuillez écrire au propriétaire pour en demander l'accès.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Pour confirmer votre inscription, veuillez cliquer le lien de confirmation dans l'email que vous allez recevoir. S’il ne vous parvient pas dans les 3 minutes, vérifiez votre dossier d'indésirables.", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "Trop de tentatives de connexion différentes, réessayez dans {{number}} jours", + "Too many different sign-in attempts, try again in {{number}} hours": "Trop de tentatives de connexion différentes, réessayez dans {{number}} heures", + "Too many different sign-in attempts, try again in {{number}} minutes": "Trop de tentatives de connexion différentes, réessayez dans {{number}} minutes", "Try free for {{amount}} days, then {{originalPrice}}.": "Essayez gratuitement pendant {{amount}} jours, puis {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Débloquez l'accès à toutes les newsletters en souscrivant un abonnement payant.", "Unsubscribe from all emails": "Se désabonner de tous les emails", "Unsubscribed": "Désabonné-e", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Vous vous êtes connecté avec succès.", "You've successfully subscribed to": "Vous vous êtes abonné à", "Your account": "Votre compte", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Votre avis aide à améliorer ce qui est publié.", "Your subscription will expire on {{expiryDate}}": "Votre abonnement expirera le {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Votre abonnement sera renouvelé le {{renewalDate}}", diff --git a/ghost/i18n/locales/fr/search.json b/ghost/i18n/locales/fr/search.json new file mode 100644 index 00000000000..898b1f4f328 --- /dev/null +++ b/ghost/i18n/locales/fr/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "Auteurs", + "Cancel": "Annuler", + "No matches found": "Aucun résultat trouvé", + "Posts": "Articles", + "Search posts, tags and authors": "Rechercher des articles, des catégories et des auteurs", + "Show more results": "Plus de résultats", + "Tags": "Catégories" +} diff --git a/ghost/i18n/locales/gd/comments.json b/ghost/i18n/locales/gd/comments.json index 9e7dd6a91a2..0c7e0fcc5d8 100644 --- a/ghost/i18n/locales/gd/comments.json +++ b/ghost/i18n/locales/gd/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Cuir freagaist ri beachd", "Report": "Dèan aithris air", "Report comment": "Dèan aithris air beachd", - "Report this comment": "Dèan aithris air a’ bheachd seo", "Report this comment?": "’Eil thu airson aithris a dhèanamh air a’ bheachd seo?", "Save": "Sabhail", "Sending": "’Ga chur", @@ -68,6 +67,5 @@ "This comment has been removed.": "Chaidh am beachd seo a sguabadh às.", "Upgrade now": "Àrdaich a-nis", "Yesterday": "An-dè", - "You want to report this comment?": "A bheil thu airson aithris a dhèanamh air a’ bheachd seo?", "Your request will be sent to the owner of this site.": "Thèid an iarrtas agad a chur gu neach-seilbh an làrach-lìn seo." } diff --git a/ghost/i18n/locales/gd/portal.json b/ghost/i18n/locales/gd/portal.json index 6578607fef6..f049369e7c5 100644 --- a/ghost/i18n/locales/gd/portal.json +++ b/ghost/i18n/locales/gd/portal.json @@ -1,5 +1,5 @@ { - "(save {{highestYearlyDiscount}}%)": "", + "(save {{highestYearlyDiscount}}%)": "(caomhain {{highestYearlyDiscount}}%)", "{{amount}} days free": "{{amount}}l an-asgaidh", "{{amount}} off": "{{amount}} dheth", "{{amount}} off for first {{number}} months.": "{{amount}} dheth fad {{number}}m", @@ -10,14 +10,16 @@ "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "Chan fhaigh {{memberEmail}} post-d nuair a bhios freagairtean ùra ann.", "{{memberEmail}} will no longer receive this newsletter.": "Chan fhaigh {{memberEmail}} a’ chuairt-litir seo tuilleadh.", "{{trialDays}} days free": "{{trialDays}}l an-asgaidh", - "+1 (123) 456-7890": "", + "+1 (123) 456-7890": "+44 1234 567890", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Chaidh ceangal a chur dhan phost-d agad. Thoir sùil air a’ phasgan spama mura faigh thu taobh a-staigh 3 mionaidean e.", "Account": "Cunntas", + "Account details updated successfully": "", "Account settings": "Roghainnean", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Bidh agad ri pàigheadh nuair a dh’fhalbhachas an ùine air an tionndadh triail agad. Faodaidh tu ga sguir dheth ron a shin ge-tà.", "Already a member?": "’Eil thu nad bhall mar-thà?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Thachair mearachd. Feuch a-rithist, no leig fios dhan sgioba-taice.", - "Back": "Air air", + "Back": "Air ais", "Back to Log in": "Thill gus clàradh a-steach", "Billing info": "Fiosrachadh bileachaidh", "Black Friday": "Black Friday", @@ -25,12 +27,13 @@ "Cancel subscription": "Sguir dhen fo-sgrìobhadh", "Cancellation reason": "Adbhar airson sguir dheth", "Change": "Atharraich", - "Change plan": "", + "Change plan": "Atharraich a' phlana", "Check spam & promotions folders": "Thoir sùil air a’ phasgan spama / margaidheachd agad", "Check with your mail provider": "Faighnich air solaraiche a’ phuist-d agad", + "Check your inbox to verify email update": "", "Choose": "Tagh", "Choose a different plan": "Tagh plana eile", - "Choose a plan": "", + "Choose a plan": "Tagh plana", "Choose your newsletters": "Tagh na cuairt-litrichean agad", "Click here to retry": "Briog an seo gus feuchainn a-rithist", "Close": "Dùin", @@ -42,6 +45,7 @@ "Contact support": "Leig fios dhan sgioba-taice", "Continue": "Lèan ort", "Continue subscription": "Cùm am fo-sgrìobhadh a’ dol", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Chan urrainn dhut clàradh a-steach. Dh’fhalbh an ùine air a’ cheangal a lean thu.", "Could not update email! Invalid link.": "Cha do ghabh am post-d ùrachadh! Ceangal mì-dhligheach", "Create a new contact": "Cruthaich neach-aithne ùr", @@ -52,14 +56,27 @@ "Edit": "Deasaich", "Email": "Post-d", "Email newsletter": "Cuairt-litir", + "Email newsletter settings updated": "", "Email preferences": "Roghainnean puist-d", "Emails": "Puist-d", "Emails disabled": "Puist-d à comas", "Ends {{offerEndDate}}": "Falbhaidh an ùine air: {{offerEndDate}}", - "Enter your email address": "", - "Enter your name": "", + "Enter your email address": "Cuir a-steach seòladh a' phuist-d agad", + "Enter your name": "Cuir a-steach d' ainm", "Error": "Mearachd", "Expires {{expiryDate}}": "Falbhaidh an ùine air: {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Gu bràth", "Free Trial – Ends {{trialEnd}}": "Falbhaidh an ùine air an tionndadh triail an-asgadh: {{trialEnd}}", "Get help": "Iarr cobhair", @@ -77,30 +94,32 @@ "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "Leig fios gu {{supportAddress}} mura h-eil thu a’ faighinn puist-d an dèidh sùil a thoirt air na rudan seo.", "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "Thèid puist-d a chur à comas ma thacras mearachd seasmhach.", "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "Cuir {{senderEmail}} ri liosta nan luchd-aithne agad agus cuidichidh seo le bhith ag innse don t-solaraiche puist-d agad gur e seòladh post-d earbsach a tha seo.", - "Invalid email address": "", - "Jamie Larson": "", - "jamie@example.com": "", + "Invalid email address": "Seòladh puist-d ceàrr", + "Jamie Larson": "Ainm Sloinneadh", + "jamie@example.com": "ainm@eisimpleir.com", "Less like this": "Nas lugha mar seo", "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "Dh’fhaodadh gu bheil puist-d a’ dol dhan phasgan spama / margaidheachd agad. Ma tha, Comharraich \"nach e spama\" a th’ annta no briog air \"gluais dhan bhogsa a-steach\".", "Manage": "Rianaich", "Maybe later": "’S mathaid an ceann greis", "Memberships unavailable, contact the owner for access.": "Leig fios dhan rianaire airson cothrom fhaighinn air na ballrachdan.", - "month": "", + "month": "mìos", "Monthly": "Gach mìos", "More like this": "Barrachd mar seo", "Name": "Ainm", "Need more help? Contact support": "’Eil thu feumach air barrachd chobhair? Leig fios dhan sgioba-taice", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Dh’fhaodadh gu bheil cuairt-litrichean à comas air sgàth ’s gun deach post-d roimhe a chomharradh mar spama, no air sgàth ’s nach do ghabh e a lìbhreachadh.", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Nach eil thu a’ faighinn puist-d?", "Now check your email!": "Thoir sùil air a’ phost-d agad", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Thoir sùil air a’ phasgan spama agad mura faigh thu puist-d aon uair ’s gu bheil thu air fo-sgrìobhadh a-rithist. Cumaidh cuid de sholaraichean puist de chlàr de sheann ghearanan spama agus ma dh’fhaoidte gu bheil iad fhathast gan comharradh mar spama. Comharraich \"nach e spama\" a th’ annta gus an gluasad air ais dhan bhogsa a-steach agad.", "Permanent failure (bounce)": "Fàilleadh maireannach", - "Phone number": "", + "Phone number": "Àireamh-fòn", "Plan": "Plana", "Plan checkout was cancelled.": "Chaidh an t-ordugh a chur dheth.", "Plan upgrade was cancelled.": "Chaidh an t-àrdachadh a chur dheth.", "Please contact {{supportAddress}} to adjust your complimentary subscription.": "Leig fios gu {{supportAddress}} gus am fo-sgrìobhadh an-asgaidh a atharrachadh.", - "Please enter {{fieldName}}": "", + "Please enter {{fieldName}}": "Lìon a-steach an roinn seo: {{fieldName}}", "Please fill in required fields": "Lìon a-steach na raointean riatanach", "Price": "Prìs", "Re-enable emails": "Cuir an comas puist-d a-rithist", @@ -117,7 +136,7 @@ "Sign out": "Clàraich a-mach", "Sign up": "Clàraich", "Signup error: Invalid link": "Mearachd: Ceangal mì-dhligheach", - "Something went wrong, please try again later.": "", + "Something went wrong, please try again later.": "Thachair mearachd, feuch a-rithist an ceann greis.", "Sorry, that didn’t work.": "Duilich, cha do dh’obraich sin.", "Spam complaints": "Gearanan spama", "Start {{amount}}-day free trial": "Cleachd tionndadh triail fad {{amount}}l", @@ -126,28 +145,39 @@ "Submit feedback": "Fàg beachd air", "Subscribe": "Fo-sgrìobh", "Subscribed": "Air fho-sgrìobhadh", + "Subscription plan updated successfully": "", "Success": "Dèanta", "Success! Check your email for magic link to sign-in.": "Dèanta! Thoir sùil air a’ phost-d agad airson a’ cheangal clàraidh a-steach.", "Success! Your account is fully activated, you now have access to all content.": "Dèanta! Chaidh an cunntas agad a chur an gnìomh agus tha cothrom-inntrigidh agad air a h-uile rud a-nis.", "Success! Your email is updated.": "Dèanta! Chaidh am post-d agad ùrachadh.", "Successfully unsubscribed": "Chan eil thu a’ fo-sgrìobhadh tuilleadh.", "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "Tapadh leat airson fo-sgrìobhadh. Tha grunn làraich eil gu h-ìosal a dh’fhaodadh a bhith còrdadh riut.", - "Thank you for your support": "", - "Thank you for your support!": "", - "Thanks for the feedback!": "Mòran taing airson leigeil fios.", + "Thank you for your support": "Mòran taig airson do thaic a thoirt dhuinn", + "Thank you for your support!": "Mòran taing airson do taic a thoirt dhuinn!", + "Thanks for the feedback!": "Mòran taing airson leigeil fios dhuinn!", "That didn't go to plan": "Cha deach sin mar bu chòir", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "’S e {{memberEmaill}} am post-d a th’ againn dhut - faodaidh tu a cheartachadh anns na agad.", "There was a problem submitting your feedback. Please try again a little later.": "Chaidh rudeigin ceàrr. Feuch a-rithist an ceann greis", - "There was an error processing your payment. Please try again.": "", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", + "There was an error processing your payment. Please try again.": "Thachair mearachd fhad 's a bhathar a' làimhseachadh a' phàighidh agad. Feuch a-rithist an ceann greis.", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Feumar cuireadh airson an làrach-lìn seo, leig fios dhan rianaire ma tha thu ag iarraidh cothrom-inntrigidh.", - "This site is not accepting payments at the moment.": "", + "This site is not accepting payments at the moment.": "Chan eil an làrach seo a' gabhail ri phàighidhean an-dràsta.", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Dèan briog air a’ cheangal dearbhaidh a chaidh a chur dhan phost-d agad gus crìoch a chur an an clàradh agad. Thoir sùil air a’ phasgan spama mura faigh thu taobh a-staigh 3 mionaidean e.", - "To continue to stay up to date, subscribe to {{publication}} below.": "", + "To continue to stay up to date, subscribe to {{publication}} below.": "Fo-sgrìobh gu h-ìosal gus cumail suas ris an fhoillseachadh, \"{{publication}}\".", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "An-asgaidh airson {{amount}}l, agus {{originalPrice}} an dèidh sin ", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Tig nad bhall phaighte gus cothrom fhaighinn air na cuairt-litrichean gu lèir.", "Unsubscribe from all emails": "Na fo-sgrìobh ri puist-d tuilleadh", "Unsubscribed": "Chan fhaigh thu puist-d tuilleadh", - "Unsubscribed from all emails.": "", + "Unsubscribed from all emails.": "Chan fhaigh thu puist-d tuilleadh.", "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "Cuiridh seo stad air puist-d, cha cuir e stad air na tha thu a’ paigheadh airson a bhallrachd agad.", "Update": "Ùraich", "Update your preferences": "Ùraich na roghainnean agad", @@ -160,7 +190,7 @@ "Welcome to {{siteTitle}}": "Fàilte dhan làrach-lìn, {{siteTitle}}", "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "Dh’fhaodadh gur e mearachd sealach a tha seo, no dh’fhaodadh gur e post-d mì-dhligheach a th’ ann.", "Why has my email been disabled?": "Carson a chaidh am post-d agam a chur à comas?", - "year": "", + "year": "bliadhna", "Yearly": "Gach bliadhna", "You currently have a free membership, upgrade to a paid subscription for full access.": "Tha ballrachd an-asgaidh agad an-dràsta, àrdaich gu ballrachd pàighte airson cothrom-inntrigidh air a h-uile rud.", "You have been successfully resubscribed": "Fo-sgrìobh thu a-rithist", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Chlàraich thu a-steach gu soirbheachail.", "You've successfully subscribed to": "Fo-sgrìobh thu gu soirbheachail gu", "Your account": "An cunntas agad", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Bheir na beachdan agad buaidh air na foillseachaidhean ri teachd.", "Your subscription will expire on {{expiryDate}}": "Falbhaidh an ùine air am fo-sgrìobhadh agad: {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Ath-nuadhaichidh am fo-sgrìobhadh agad: {{expiryDate}}", diff --git a/ghost/i18n/locales/gd/search.json b/ghost/i18n/locales/gd/search.json new file mode 100644 index 00000000000..548d6777e97 --- /dev/null +++ b/ghost/i18n/locales/gd/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "Ùghdaran", + "Cancel": "Sguir dheth", + "No matches found": "Cha deach dad a lorg a fhreagras ris na lorg thu", + "Posts": "Postaichean", + "Search posts, tags and authors": "Lorg sna postaichean, tagaichean agus ùghdaran", + "Show more results": "Seall barrachd thoraidhean", + "Tags": "Tagaichean" +} diff --git a/ghost/i18n/locales/hi/comments.json b/ghost/i18n/locales/hi/comments.json new file mode 100644 index 00000000000..da48ab5d5af --- /dev/null +++ b/ghost/i18n/locales/hi/comments.json @@ -0,0 +1,71 @@ +{ + "{{amount}} characters left": "{{amount}} अक्षर बाकी", + "{{amount}} comments": "{{amount}} टिप्पणियाँ", + "{{amount}} days ago": "{{amount}} दिन पहले", + "{{amount}} hrs ago": "", + "{{amount}} mins ago": "", + "{{amount}} months ago": "{{amount}} महीने पहले", + "{{amount}} more": "{{amount}} और", + "{{amount}} seconds ago": "{{amount}} सेकंड पहले", + "{{amount}} weeks ago": "{{amount}} सप्ताह पहले", + "{{amount}} years ago": "{{amount}} साल पहले", + "1 comment": "1 टिप्पणी", + "Add comment": "टिप्पणी जोड़ें", + "Add context to your comment, share your name and expertise to foster a healthy discussion.": "अपनी टिप्पणी में संदर्भ जोड़ें, स्वस्थ चर्चा के लिए अपना नाम और विशेषज्ञता साझा करें।", + "Add reply": "जवाब जोड़ें", + "Already a member?": "पहले से सदस्य हैं?", + "Anonymous": "अनाम", + "Become a member of {{publication}} to start commenting.": "टिप्पणी शुरू करने के लिए {{publication}} का सदस्य बनें।", + "Become a paid member of {{publication}} to start commenting.": "टिप्पणी शुरू करने के लिए {{publication}} के भुगतान सदस्य बनें।", + "Cancel": "रद्द करें", + "Comment": "टिप्पणी", + "Complete your profile": "अपनी प्रोफ़ाइल पूरी करें", + "Delete": "हटाएं", + "Deleted member": "हटाए गए सदस्य", + "Discussion": "चर्चा", + "Edit": "संपादित करें", + "Edit this comment": "इस टिप्पणी को संपादित करें", + "edited": "", + "Enter your name": "अपना नाम दर्ज करें", + "Expertise": "विशेषज्ञता", + "Founder @ Acme Inc": "संस्थापक @ Acme Inc", + "Full-time parent": "पूर्णकालिक माता-पिता", + "Head of Marketing at Acme, Inc": "Acme, Inc में विपणन प्रमुख", + "Hide": "छिपाएं", + "Hide comment": "टिप्पणी छिपाएं", + "Jamie Larson": "राहुल शर्मा", + "Join the discussion": "चर्चा में शामिल हों", + "Just now": "अभी", + "Local resident": "स्थानीय निवासी", + "Member discussion": "सदस्य चर्चा", + "Name": "नाम", + "Neurosurgeon": "न्यूरोसर्जन", + "One day ago": "एक दिन पहले", + "One hour ago": "एक घंटा पहले", + "One min ago": "", + "One month ago": "एक महीने पहले", + "One week ago": "एक सप्ताह पहले", + "One year ago": "एक साल पहले", + "Reply": "जवाब दें", + "Reply to comment": "टिप्पणी का जवाब दें", + "Report": "रिपोर्ट", + "Report comment": "टिप्पणी की रिपोर्ट करें", + "Report this comment?": "क्या आप इस टिप्पणी की रिपोर्ट करना चाहते हैं?", + "Save": "सहेजें", + "Sending": "भेजा जा रहा है", + "Sent": "भेजा गया", + "Show": "दिखाएं", + "Show {{amount}} more replies": "{{amount}} और जवाब दिखाएं", + "Show {{amount}} previous comments": "{{amount}} पिछली टिप्पणियाँ दिखाएं", + "Show 1 more reply": "1 और जवाब दिखाएं", + "Show 1 previous comment": "1 पिछली टिप्पणी दिखाएं", + "Show comment": "टिप्पणी दिखाएं", + "Sign in": "साइन इन करें", + "Sign up now": "अभी साइन अप करें", + "Start the conversation": "बातचीत शुरू करें", + "This comment has been hidden.": "इस टिप्पणी को छिपा दिया गया है।", + "This comment has been removed.": "इस टिप्पणी को हटा दिया गया है।", + "Upgrade now": "अब उन्नत करें", + "Yesterday": "कल", + "Your request will be sent to the owner of this site.": "आपका अनुरोध इस साइट के मालिक को भेजा जाएगा।" +} diff --git a/ghost/i18n/locales/hi/ghost.json b/ghost/i18n/locales/hi/ghost.json new file mode 100644 index 00000000000..1fe256d6748 --- /dev/null +++ b/ghost/i18n/locales/hi/ghost.json @@ -0,0 +1,34 @@ +{ + "All the best!": "सभी को शुभकामनाएं!", + "Complete signup for {{siteTitle}}!": "{{siteTitle}} के लिए साइनअप पूरा करें!", + "Complete your sign up to {{siteTitle}}!": "{{siteTitle}} के लिए अपना साइनअप पूरा करें!", + "Confirm email address": "ईमेल पता पुष्टि करें", + "Confirm signup": "साइनअप की पुष्टि करें", + "Confirm your email address": "अपना ईमेल पता पुष्टि करें", + "Confirm your email update for {{siteTitle}}!": "{{siteTitle}} के लिए अपना ईमेल अपडेट पुष्टि करें!", + "Confirm your subscription to {{siteTitle}}": "{{siteTitle}} के लिए अपनी सदस्यता पुष्टि करें", + "For your security, the link will expire in 24 hours time.": "आपकी सुरक्षा के लिए, यह लिंक 24 घंटों में समाप्त हो जाएगा।", + "Hey there,": "नमस्ते,", + "Hey there!": "नमस्ते!", + "If you did not make this request, you can safely ignore this email.": "यदि आपने यह अनुरोध नहीं किया है, तो आप इस ईमेल को सुरक्षित रूप से नजरअंदाज कर सकते हैं।", + "If you did not make this request, you can simply delete this message.": "यदि आपने यह अनुरोध नहीं किया है, तो आप इस संदेश को बस हटा सकते हैं।", + "Please confirm your email address with this link:": "कृपया इस लिंक से अपना ईमेल पता पुष्टि करें:", + "Secure sign in link for {{siteTitle}}": "{{siteTitle}} के लिए सुरक्षित साइन इन लिंक", + "See you soon!": "जल्द ही मिलते हैं!", + "Sent to {{email}}": "{{email}} पर भेजा गया", + "Sign in": "साइन इन करें", + "Sign in to {{siteTitle}}": "{{siteTitle}} में साइन इन करें", + "Tap the link below to complete the signup process for {{siteTitle}}, and be automatically signed in:": "{{siteTitle}} के लिए साइनअप प्रक्रिया को पूरा करने के लिए नीचे दिए गए लिंक पर टैप करें, और स्वचालित रूप से साइन इन हो जाएं:", + "Thank you for signing up to {{siteTitle}}!": "{{siteTitle}} के लिए साइन अप करने के लिए धन्यवाद!", + "Thank you for subscribing to {{siteTitle}}!": "{{siteTitle}} की सदस्यता लेने के लिए धन्यवाद!", + "Thank you for subscribing to {{siteTitle}}.": "{{siteTitle}} की सदस्यता लेने के लिए धन्यवाद।", + "Thank you for subscribing to {{siteTitle}}. Tap the link below to be automatically signed in:": "{{siteTitle}} की सदस्यता लेने के लिए धन्यवाद। स्वचालित रूप से साइन इन होने के लिए नीचे दिए गए लिंक पर टैप करें:", + "This email address will not be used.": "इस ईमेल पते का उपयोग नहीं किया जाएगा।", + "Welcome back to {{siteTitle}}!": "{{siteTitle}} में फिर से स्वागत है!", + "Welcome back! Use this link to securely sign in to your {{siteTitle}} account:": "फिर से स्वागत है! अपने {{siteTitle}} खाते में सुरक्षित रूप से साइन इन करने के लिए इस लिंक का उपयोग करें:", + "You can also copy & paste this URL into your browser:": "आप इस URL को अपने ब्राउज़र में कॉपी और पेस्ट भी कर सकते हैं:", + "You will not be signed up, and no account will be created for you.": "आप साइन अप नहीं होंगे, और आपके लिए कोई खाता नहीं बनाया जाएगा।", + "You will not be subscribed.": "आप सदस्यता नहीं लेंगे।", + "You're one tap away from subscribing to {{siteTitle}} — please confirm your email address with this link:": "आप {{siteTitle}} की सदस्यता लेने से एक टैप दूर हैं — कृपया इस लिंक से अपना ईमेल पता पुष्टि करें:", + "You're one tap away from subscribing to {{siteTitle}}!": "आप {{siteTitle}} की सदस्यता लेने से एक टैप दूर हैं!" +} diff --git a/ghost/i18n/locales/hi/portal.json b/ghost/i18n/locales/hi/portal.json new file mode 100644 index 00000000000..577d535abc1 --- /dev/null +++ b/ghost/i18n/locales/hi/portal.json @@ -0,0 +1,208 @@ +{ + "(save {{highestYearlyDiscount}}%)": "({{highestYearlyDiscount}}% बचाएं)", + "{{amount}} days free": "{{amount}} दिन मुफ्त", + "{{amount}} off": "{{amount}} की छूट", + "{{amount}} off for first {{number}} months.": "पहले {{number}} महीनों के लिए {{amount}} की छूट।", + "{{amount}} off for first {{period}}.": "पहले {{period}} के लिए {{amount}} की छूट।", + "{{amount}} off forever.": "हमेशा के लिए {{amount}} की छूट।", + "{{discount}}% discount": "{{discount}}% छूट", + "{{memberEmail}} will no longer receive {{newsletterName}} newsletter.": "{{memberEmail}} को अब {{newsletterName}} न्यूज़लेटर प्राप्त नहीं होगा।", + "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "{{memberEmail}} को अब आपके कमेंट्स पर कोई उत्तर देने पर ईमेल नहीं मिलेगा।", + "{{memberEmail}} will no longer receive this newsletter.": "{{memberEmail}} को अब यह न्यूज़लेटर प्राप्त नहीं होगा।", + "{{trialDays}} days free": "{{trialDays}} दिन मुफ्त", + "+1 (123) 456-7890": "", + "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "एक लॉगिन लिंक आपके इनबॉक्स में भेजा गया है। यदि यह 3 मिनट में नहीं आता है, तो कृपया अपना स्पैम फ़ोल्डर जांचें।", + "Account": "खाता", + "Account details updated successfully": "", + "Account settings": "खाता सेटिंग्स", + "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "मुफ्त परीक्षण समाप्त होने के बाद, आपको आपके द्वारा चुने गए स्तर के लिए नियमित कीमत पर शुल्क लिया जाएगा। आप तब तक किसी भी समय रद्द कर सकते हैं।", + "Already a member?": "पहले से ही सदस्य हैं?", + "An error occurred": "", + "An unexpected error occured. Please try again or contact support if the error persists.": "एक अप्रत्याशित त्रुटि हुई। कृपया फिर से प्रयास करें या यदि त्रुटि बनी रहती है तो समर्थन से संपर्क करें।", + "Back": "वापस", + "Back to Log in": "लॉगिन पर वापस जाएं", + "Billing info": "बिलिंग जानकारी", + "Black Friday": "ब्लैक फ्राइडे", + "Cancel anytime.": "कभी भी रद्द करें।", + "Cancel subscription": "सदस्यता रद्द करें", + "Cancellation reason": "रद्द करने का कारण", + "Change": "परिवर्तन", + "Change plan": "", + "Check spam & promotions folders": "स्पैम और प्रचार फ़ोल्डरों की जांच करें", + "Check with your mail provider": "अपने मेल प्रदाता से जांचें", + "Check your inbox to verify email update": "", + "Choose": "चुनें", + "Choose a different plan": "एक अलग योजना चुनें", + "Choose a plan": "", + "Choose your newsletters": "अपने न्यूज़लेटर चुनें", + "Click here to retry": "फिर से प्रयास करने के लिए यहां क्लिक करें", + "Close": "बंद करें", + "Comments": "टिप्पणियाँ", + "Complimentary": "नि:शुल्क", + "Confirm": "पुष्टि करें", + "Confirm cancellation": "रद्द करने की पुष्टि करें", + "Confirm subscription": "सदस्यता की पुष्टि करें", + "Contact support": "सहायता के लिए संपर्क करें", + "Continue": "जारी रखें", + "Continue subscription": "सदस्यता जारी रखें", + "Could not create stripe checkout session": "", + "Could not sign in. Login link expired.": "साइन इन नहीं कर सके। लॉगिन लिंक समाप्त हो गया।", + "Could not update email! Invalid link.": "ईमेल अपडेट नहीं कर सके! अमान्य लिंक।", + "Create a new contact": "एक नया संपर्क बनाएं", + "Current plan": "वर्तमान योजना", + "Delete account": "खाता हटाएं", + "Didn't mean to do this? Manage your preferences .": "क्या ऐसा करने का इरादा नहीं था? अपनी प्राथमिकताएँ प्रबंधित करें।", + "Don't have an account?": "क्या आपके पास खाता नहीं है?", + "Edit": "बदलाव करें", + "Email": "ईमेल", + "Email newsletter": "ईमेल न्यूज़लेटर", + "Email newsletter settings updated": "", + "Email preferences": "ईमेल प्राथमिकताएँ", + "Emails": "ईमेल", + "Emails disabled": "ईमेल निष्क्रिय", + "Ends {{offerEndDate}}": "{{offerEndDate}} को समाप्त होता है", + "Enter your email address": "", + "Enter your name": "", + "Error": "गड़बड़ी", + "Expires {{expiryDate}}": "{{expiryDate}} को समाप्त होता है", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", + "Forever": "हमेशा के लिए", + "Free Trial – Ends {{trialEnd}}": "मुफ्त परीक्षण – {{trialEnd}} को समाप्त होता है", + "Get help": "मदद प्राप्त करें", + "Get in touch for help": "मदद के लिए संपर्क करें", + "Get notified when someone replies to your comment": "जब कोई आपके टिप्पणी का उत्तर देता है तो सूचित करें", + "Give feedback on this post": "इस पोस्ट पर प्रतिक्रिया दें", + "Help! I'm not receiving emails": "मदद! मुझे ईमेल नहीं मिल रहे हैं", + "Here are a few other sites you may enjoy.": "यहाँ कुछ अन्य साइटें हैं जिन्हें आप पसंद कर सकते हैं।", + "If a newsletter is flagged as spam, emails are automatically disabled for that address to make sure you no longer receive any unwanted messages.": "यदि किसी न्यूज़लेटर को स्पैम के रूप में चिन्हित किया जाता है, तो उस पते के लिए ईमेल स्वचालित रूप से निष्क्रिय हो जाते हैं ताकि यह सुनिश्चित हो सके कि आपको अब कोई अवांछित संदेश प्राप्त नहीं हो।", + "If the spam complaint was accidental, or you would like to begin receiving emails again, you can resubscribe to emails by clicking the button on the previous screen.": "यदि स्पैम शिकायत दुर्घटनावश थी, या आप फिर से ईमेल प्राप्त करना शुरू करना चाहते हैं, तो आप पिछली स्क्रीन पर बटन पर क्लिक करके ईमेल की सदस्यता पुनः प्राप्त कर सकते हैं।", + "If you cancel your subscription now, you will continue to have access until {{periodEnd}}.": "यदि आप अब अपनी सदस्यता रद्द करते हैं, तो आपके पास {{periodEnd}} तक पहुंच बनी रहेगी।", + "If you have a corporate or government email account, reach out to your IT department and ask them to allow emails to be received from {{senderEmail}}": "यदि आपके पास एक कॉर्पोरेट या सरकारी ईमेल खाता है, तो अपनी आईटी विभाग से संपर्क करें और उनसे {{senderEmail}} से ईमेल प्राप्त करने की अनुमति माँगें", + "If you would like to start receiving emails again, the best next steps are to check your email address on file for any issues and then click resubscribe on the previous screen.": "यदि आप फिर से ईमेल प्राप्त करना शुरू करना चाहते हैं, तो अगला सबसे अच्छा कदम अपने फ़ाइल पर ईमेल पते की किसी भी समस्या के लिए जाँच करना है और फिर पिछली स्क्रीन पर पुनः सदस्यता पर क्लिक करें।", + "If you're not receiving the email newsletter you've subscribed to, here are a few things to check.": "यदि आपको वह ईमेल न्यूज़लेटर नहीं मिल रहा है जिसकी आपने सदस्यता ली है, तो यहाँ कुछ चीजें हैं जिन्हें जांचें।", + "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "यदि आपने इन सभी जाँचों को पूरा कर लिया है और आपको अभी भी ईमेल प्राप्त नहीं हो रहे हैं, तो आप {{supportAddress}} से संपर्क करके समर्थन प्राप्त कर सकते हैं।", + "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "यदि न्यूज़लेटर भेजने का प्रयास करते समय एक स्थायी विफलता प्राप्त होती है, तो खाते पर ईमेल निष्क्रिय कर दिए जाएंगे।", + "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "अपने ईमेल क्लाइंट में {{senderEmail}} को अपने संपर्क सूची में जोड़ें। यह आपके मेल प्रदाता को संकेत देता है कि इस पते से भेजे गए ईमेल पर विश्वास किया जाना चाहिए।", + "Invalid email address": "", + "Jamie Larson": "", + "jamie@example.com": "", + "Less like this": "इस तरह का कम", + "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "सुनिश्चित करें कि ईमेल गलती से आपके इनबॉक्स के स्पैम या प्रचार फ़ोल्डरों में समाप्त नहीं हो रहे हैं। यदि वे हैं, तो \"स्पैम नहीं\" और/या \"इनबॉक्स में ले जाएं\" पर क्लिक करें।", + "Manage": "प्रबंधित करें", + "Maybe later": "शायद बाद में", + "Memberships unavailable, contact the owner for access.": "सदस्यता उपलब्ध नहीं है, पहुँच के लिए मालिक से संपर्क करें।", + "month": "", + "Monthly": "मासिक", + "More like this": "इस तरह के और", + "Name": "नाम", + "Need more help? Contact support": "और अधिक मदद चाहिए? समर्थन से संपर्क करें", + "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "आपके खाते पर न्यूज़लेटर को दो कारणों से निष्क्रिय किया जा सकता है: एक पिछला ईमेल स्पैम के रूप में चिह्नित किया गया था, या एक ईमेल भेजने का प्रयास एक स्थायी विफलता (बाउंस) के कारण हुआ।", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", + "Not receiving emails?": "ईमेल प्राप्त नहीं हो रहे?", + "Now check your email!": "अब अपना ईमेल जांचें!", + "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "पुनः सदस्यता लेने के बाद, यदि आप अभी भी अपने इनबॉक्स में ईमेल नहीं देखते हैं, तो अपना स्पैम फ़ोल्डर जांचें। कुछ इनबॉक्स प्रदाता पिछली स्पैम शिकायतों का रिकॉर्ड रखते हैं और ईमेल को फ्लैग करना जारी रखेंगे। यदि ऐसा होता है, तो नवीनतम न्यूज़लेटर को 'स्पैम नहीं' के रूप में चिह्नित करें ताकि इसे वापस आपके मुख्य इनबॉक्स में ले जाया जा सके।", + "Permanent failure (bounce)": "स्थायी विफलता (बाउंस)", + "Phone number": "", + "Plan": "योजना", + "Plan checkout was cancelled.": "योजना चेकआउट रद्द कर दिया गया।", + "Plan upgrade was cancelled.": "योजना उन्नयन रद्द कर दिया गया।", + "Please contact {{supportAddress}} to adjust your complimentary subscription.": "अपनी नि:शुल्क सदस्यता को समायोजित करने के लिए कृपया {{supportAddress}} से संपर्क करें।", + "Please enter {{fieldName}}": "", + "Please fill in required fields": "कृपया आवश्यक फ़ील्ड भरें", + "Price": "कीमत", + "Re-enable emails": "ईमेल पुनः सक्षम करें", + "Recommendations": "सिफारिशें", + "Renews at {{price}}.": "{{price}} पर नवीनीकृत होता है।", + "Retry": "पुनः प्रयास करें", + "Save": "सेव करें", + "Send an email and say hi!": "ईमेल भेजें और नमस्ते कहें!", + "Send an email to {{senderEmail}} and say hello. This can also help signal to your mail provider that emails to and from this address should be trusted.": "{{senderEmail}} को एक ईमेल भेजें और नमस्ते कहें। यह आपके मेल प्रदाता को संकेत देने में भी मदद कर सकता है कि इस पते से और इस पते पर भेजे गए ईमेल पर विश्वास किया जाना चाहिए।", + "Sending login link...": "लॉगिन लिंक भेज रहा है...", + "Sending...": "भेजा जा रहा है...", + "Show all": "सभी दिखाएँ", + "Sign in": "साइन इन करें", + "Sign out": "साइन आउट करें", + "Sign up": "साइन अप करें", + "Signup error: Invalid link": "साइनअप त्रुटि: अमान्य लिंक", + "Something went wrong, please try again later.": "", + "Sorry, that didn’t work.": "क्षमा करें, वह काम नहीं किया।", + "Spam complaints": "स्पैम शिकायतें", + "Start {{amount}}-day free trial": "{{amount}}-दिवसीय मुफ्त परीक्षण शुरू करें", + "Starting {{startDate}}": "{{startDate}} से शुरू", + "Starting today": "आज से शुरू", + "Submit feedback": "प्रतिक्रिया सबमिट करें", + "Subscribe": "सदस्यता लें", + "Subscribed": "सदस्यता ली", + "Subscription plan updated successfully": "", + "Success": "सफलता", + "Success! Check your email for magic link to sign-in.": "सफलता! साइन-इन के लिए जादुई लिंक के लिए अपना ईमेल जांचें।", + "Success! Your account is fully activated, you now have access to all content.": "सफलता! आपका खाता पूरी तरह से सक्रिय हो गया है, अब आपके पास सभी सामग्री तक पहुंच है।", + "Success! Your email is updated.": "सफलता! आपका ईमेल अपडेट हो गया है।", + "Successfully unsubscribed": "सफलतापूर्वक सदस्यता समाप्त की", + "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "सदस्यता लेने के लिए धन्यवाद। पढ़ना शुरू करने से पहले, नीचे कुछ अन्य साइटें हैं जिन्हें आप पसंद कर सकते हैं।", + "Thank you for your support": "", + "Thank you for your support!": "", + "Thanks for the feedback!": "प्रतिक्रिया के लिए धन्यवाद!", + "That didn't go to plan": "वह योजना के अनुसार नहीं हुआ", + "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "हमारे पास आपके लिए ईमेल पता {{memberEmail}} है — यदि वह सही नहीं है, तो आप इसे अपने में अपडेट कर सकते हैं।", + "There was a problem submitting your feedback. Please try again a little later.": "आपकी प्रतिक्रिया सबमिट करने में समस्या हुई। कृपया थोड़ी देर बाद फिर से प्रयास करें।", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", + "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", + "This site is invite-only, contact the owner for access.": "यह साइट केवल निमंत्रण द्वारा है, पहुँच के लिए मालिक से संपर्क करें।", + "This site is not accepting payments at the moment.": "", + "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "साइनअप पूरा करने के लिए, अपने इनबॉक्स में पुष्टिकरण लिंक पर क्लिक करें। यदि यह 3 मिनट के भीतर नहीं आता है, तो अपना स्पैम फ़ोल्डर जांचें!", + "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Try free for {{amount}} days, then {{originalPrice}}.": "{{amount}} दिनों के लिए मुफ्त प्रयास करें, फिर {{originalPrice}}।", + "Unable to initiate checkout session": "", + "Unlock access to all newsletters by becoming a paid subscriber.": "एक सशुल्क सदस्य बनकर सभी न्यूज़लेटर्स तक पहुंच अनलॉक करें।", + "Unsubscribe from all emails": "सभी ईमेल से सदस्यता समाप्त करें", + "Unsubscribed": "सदस्यता समाप्त की", + "Unsubscribed from all emails.": "सभी ईमेल से सदस्यता समाप्त की।", + "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "ईमेल से सदस्यता समाप्त करने से आपकी {{title}} की सशुल्क सदस्यता रद्द नहीं होगी", + "Update": "अपडेट करें", + "Update your preferences": "अपनी प्राथमिकताएँ अपडेट करें", + "Verification link sent, check your inbox": "पुष्टिकरण लिंक भेजा गया, अपना इनबॉक्स जांचें", + "Verify your email address is correct": "पुष्टि करें कि आपका ईमेल पता सही है", + "View plans": "योजनाएं देखें", + "We couldn't unsubscribe you as the email address was not found. Please contact the site owner.": "हम आपको सदस्यता समाप्त नहीं कर सके क्योंकि ईमेल पता नहीं मिला। कृपया साइट मालिक से संपर्क करें।", + "Welcome back, {{name}}!": "वापसी पर स्वागत है, {{name}}!", + "Welcome back!": "वापसी पर स्वागत है!", + "Welcome to {{siteTitle}}": "{{siteTitle}} में आपका स्वागत है", + "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "जब एक इनबॉक्स एक ईमेल को स्वीकार करने में विफल रहता है तो इसे आमतौर पर बाउंस कहा जाता है। कई मामलों में, यह अस्थायी हो सकता है। हालांकि, कुछ मामलों में, एक बाउंस ईमेल को स्थायी विफलता के रूप में लौटाया जा सकता है जब एक ईमेल पता अमान्य या गैर-मौजूद होता है।", + "Why has my email been disabled?": "मेरा ईमेल निष्क्रिय क्यों किया गया है?", + "year": "", + "Yearly": "वार्षिक", + "You currently have a free membership, upgrade to a paid subscription for full access.": "आपके पास वर्तमान में एक मुफ्त सदस्यता है, पूर्ण पहुंच के लिए सशुल्क सदस्यता में अपग्रेड करें।", + "You have been successfully resubscribed": "आपने सफलतापूर्वक पुनः सदस्यता प्राप्त कर ली है", + "You're currently not receiving emails": "आप वर्तमान में ईमेल प्राप्त नहीं कर रहे हैं", + "You're not receiving emails": "आप ईमेल प्राप्त नहीं कर रहे हैं", + "You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "आप ईमेल प्राप्त नहीं कर रहे हैं क्योंकि आपने हाल की एक संदेश को स्पैम के रूप में चिन्हित किया है, या क्योंकि संदेश आपके द्वारा प्रदान किए गए ईमेल पते पर डिलीवर नहीं किए जा सके।", + "You've successfully signed in.": "आपने सफलतापूर्वक साइन इन कर लिया है।", + "You've successfully subscribed to": "आपने सफलतापूर्वक सदस्यता ली है", + "Your account": "आपका खाता", + "Your email has failed to resubscribe, please try again": "", + "Your input helps shape what gets published.": "आपका इनपुट प्रकाशित होने वाली चीज़ों को आकार देने में मदद करता है।", + "Your subscription will expire on {{expiryDate}}": "आपकी सदस्यता {{expiryDate}} को समाप्त हो जाएगी", + "Your subscription will renew on {{renewalDate}}": "आपकी सदस्यता {{renewalDate}} को नवीनीकृत होगी", + "Your subscription will start on {{subscriptionStart}}": "आपकी सदस्यता {{subscriptionStart}} को शुरू होगी" +} diff --git a/ghost/i18n/locales/hi/search.json b/ghost/i18n/locales/hi/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/hi/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/hi/signup-form.json b/ghost/i18n/locales/hi/signup-form.json new file mode 100644 index 00000000000..d1e3de321f0 --- /dev/null +++ b/ghost/i18n/locales/hi/signup-form.json @@ -0,0 +1,9 @@ +{ + "Email sent": "ईमेल भेज दी गई", + "Now check your email!": "अब अपना ईमेल चेक करें!", + "Please enter a valid email address": "कृपया एक वैध ईमेल पता दर्ज करें", + "Something went wrong, please try again.": "कुछ गड़बड़ हो गई है, कृपया फिर से कोशिश करें।", + "Subscribe": "सब्सक्राइब करें", + "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "साइन अप पूरा करने के लिए, अपने इनबाक्स में कन्फर्मेशन लिंक पर क्लिक करें। अगर यह 3 मिनट के भीतर नहीं आता है, तो अपना स्पैम फ़ोल्डर चेक करें!", + "Your email address": "आपका ईमेल पता" +} diff --git a/ghost/i18n/locales/hr/comments.json b/ghost/i18n/locales/hr/comments.json index 3db1401e95c..57ccdea7060 100644 --- a/ghost/i18n/locales/hr/comments.json +++ b/ghost/i18n/locales/hr/comments.json @@ -5,7 +5,7 @@ "{{amount}} hrs ago": "Prije {{amount}} sati", "{{amount}} mins ago": "Prije {{amount}} minuta", "{{amount}} months ago": "Prije {{amount}} mjeseci", - "{{amount}} more": "", + "{{amount}} more": "{{amount}} više", "{{amount}} seconds ago": "Prije {{amount}} sekundi", "{{amount}} weeks ago": "Prije {{amount}} tjedana", "{{amount}} years ago": "Prije {{amount}} godine", @@ -50,7 +50,6 @@ "Reply to comment": "Odgovori na komentar", "Report": "Prijavi", "Report comment": "Prijavi komentar", - "Report this comment": "Prijavi ovaj komentar", "Report this comment?": "Prijaviti ovaj komentar?", "Save": "Spremi", "Sending": "Slanje", @@ -66,8 +65,7 @@ "Start the conversation": "Pokreni razgovor", "This comment has been hidden.": "Ovaj komentar je skriven", "This comment has been removed.": "Ovaj komentar je maknut", - "Upgrade now": "", + "Upgrade now": "Nadogradite sada", "Yesterday": "Jučer", - "You want to report this comment?": "Da li želite prijaviti ovaj komentar?", "Your request will be sent to the owner of this site.": "Vaš zahtjev će se poslati vasniku ovih web stranica." } diff --git a/ghost/i18n/locales/hr/portal.json b/ghost/i18n/locales/hr/portal.json index ecb6fccbc3f..31712fcbd1a 100644 --- a/ghost/i18n/locales/hr/portal.json +++ b/ghost/i18n/locales/hr/portal.json @@ -1,5 +1,5 @@ { - "(save {{highestYearlyDiscount}}%)": "", + "(save {{highestYearlyDiscount}}%)": "(uštedi {{highestYearlyDiscount}}%)", "{{amount}} days free": "{{amount}} dana besplatno", "{{amount}} off": "Snižena cijena {{amount}}", "{{amount}} off for first {{number}} months.": "Snižena cijena {{amount}} za prvih {{number}} mjeseci", @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Link za prijavu je poslan na Vašu adresu e-pošte. Ako poruku niste dobili za 3 minute, provjerite spam folder", "Account": "Vaš račun", + "Account details updated successfully": "", "Account settings": "Podešavanje vašeg računa", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Nakon isteka probnog perioda, izvršit će se naplata odabranog plana pretplate. Uvijek možete otkazati pretplatu prije toga.", "Already a member?": "Već imate račun?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Desila se neočekivana greška. Pokušajte ponovo ili kontaktirajte podršku ako će se greška ponavljati.", "Back": "Natrag", "Back to Log in": "Natrag na prijavu", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Provjerite mape za spam i promociju", "Check with your mail provider": "Provjerite s vašim pružateljem usluge e-pošte", + "Check your inbox to verify email update": "", "Choose": "Odaberite", "Choose a different plan": "Odaberite drugi plan", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Kontaktirajte podršku", "Continue": "Nastavite", "Continue subscription": "Nastavite pretplatu", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Neuspio pokušaj prijave. Link za prijavu je istekao.", "Could not update email! Invalid link.": "Neuspio pokušaj izmjene adrese e-pošte! Neispravan link.", "Create a new contact": "Napravite novi kontakt:", @@ -52,6 +56,7 @@ "Edit": "Uredi", "Email": "E-pošta", "Email newsletter": "Newsletter e-poštom", + "Email newsletter settings updated": "", "Email preferences": "Postavke e-pošte", "Emails": "E-pošta", "Emails disabled": "Isključena e-pošta", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Greška", "Expires {{expiryDate}}": "Ističe {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Zauvijek", "Free Trial – Ends {{trialEnd}}": "Besplatan probni period - završava {{trialEnd}}", "Get help": "Potražite pomoć", @@ -67,7 +84,7 @@ "Get notified when someone replies to your comment": "Budite obaviješteni ako netko odgovori na vaš komentar:", "Give feedback on this post": "Ostavite komentar na ovaj post", "Help! I'm not receiving emails": "Pomoć! Ne primam e-poštu", - "Here are a few other sites you may enjoy.": "", + "Here are a few other sites you may enjoy.": "Ovo su još nekoliko drugih stranica koje bi vam se mogle svidjeti.", "If a newsletter is flagged as spam, emails are automatically disabled for that address to make sure you no longer receive any unwanted messages.": "Ako je newsletter označen kao neželjena e-pošta, poruka se automatski onemogućuje za tu adresu kako biste bili sigurni da više nećete primati neželjene poruke.", "If the spam complaint was accidental, or you would like to begin receiving emails again, you can resubscribe to emails by clicking the button on the previous screen.": "Ako je pritužba na spam bila slučajna ili želite ponovno početi primati e-poštu, možete se ponovno pretplatiti klikom na link na prethodnom ekranu.", "If you cancel your subscription now, you will continue to have access until {{periodEnd}}.": "Ako otkažete vašu pretplatu sada, zadržat će te pristup do {{periodEnd}}.", @@ -83,7 +100,7 @@ "Less like this": "Manje sadržaja poput ovoga", "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "Pobrinite se da e-poruke slučajno ne završe u mapi Spam ili Promocije u vašoj pristigloj pošti. Ako jesu, kliknite \"Označi da nije neželjena pošta\" i/ili \"Premjesti u pristiglu poštu\".", "Manage": "Upravljanje", - "Maybe later": "", + "Maybe later": "Možda kasnije", "Memberships unavailable, contact the owner for access.": "Pretplate nisu dostupne, kontaktirajte vlasnika za pristup.", "month": "", "Monthly": "Mjesečno", @@ -91,6 +108,8 @@ "Name": "Ime i prezime", "Need more help? Contact support": "Kontaktirajte nas ako trebate dodatnu pomoć", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Slanje newslettera se može onemogućiti na vašem računu iz dva razloga: prethodna e-pošta je označena kao neželjena pošta (spam) ili je pokušaj slanja e-pošte rezultirao trajnim neuspjehom (odbijanje).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Ne dobivate e-poštu?", "Now check your email!": "Provjerite vašu e-poštu!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Nakon što se ponovno pretplatite, ako i dalje ne vidite e-poštu u svojoj pristigloj pošti, provjerite mapu neželjene pošte. Neki pružatelji usluga e-pošte vode evidenciju prethodnih pritužbi na neželjenu poštu i nastavit će označavati e-poštu. Ako se to dogodi, označite najnoviji newsletter kao \"Nije neželjena pošta\" da biste ga vratili u svoju primarnu pristiglu poštu.", @@ -104,7 +123,7 @@ "Please fill in required fields": "Molimo vas da ispunite obvezna polja", "Price": "Cijena", "Re-enable emails": "Ponovo omogući slanje e-pošte", - "Recommendations": "", + "Recommendations": "Preporuke", "Renews at {{price}}.": "Obnovi po cijeni {{price}}", "Retry": "Pokušaj opet", "Save": "Spremi", @@ -112,7 +131,7 @@ "Send an email to {{senderEmail}} and say hello. This can also help signal to your mail provider that emails to and from this address should be trusted.": "Pošalji e-poruku na {{senderEmail}} i reci bok. Ovo također može pomoći vašem pružatelju usluga e-pošte da vjeruje e-pošti koja dolazi i odlazi s ove adrese.", "Sending login link...": "Link za prijavu se šalje...", "Sending...": "Slanje...", - "Show all": "", + "Show all": "Pokaži sve", "Sign in": "Prijava", "Sign out": "Odjava", "Sign up": "Registracija", @@ -126,38 +145,49 @@ "Submit feedback": "Pošalji povratne informacije", "Subscribe": "Pretplati se", "Subscribed": "Pretplaćen", + "Subscription plan updated successfully": "", "Success": "Uspjeh", "Success! Check your email for magic link to sign-in.": "Uspjeh! Provjerite vašu e-poštu i pronađite poruku s linkom za prijavu.", "Success! Your account is fully activated, you now have access to all content.": "Uspjeh! Vaš račun je uspješno aktiviran i sada imate pristup svim sadržajima.", "Success! Your email is updated.": "Uspjeh! Vaš račun e-pošte je ažuriran.", "Successfully unsubscribed": "Uspješna odjava", - "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "", + "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "Hvala što ste se pretplatili. Prije nego počnete čitati, u nastavku je nekoliko drugih stranica koje bi vam se mogle svidjeti.", "Thank you for your support": "", "Thank you for your support!": "", "Thanks for the feedback!": "Hvala na povratnim informacijama!", "That didn't go to plan": "Nešto je pošlo po zlu", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Adresa e-pošte vašeg računa je {{memberEmail}} - ako to nije točno, možete ga ažurirati u .", "There was a problem submitting your feedback. Please try again a little later.": "Došlo je do problema sa slanjem vaše poruke. Pokušajte ponovno malo kasnije.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Ove stranice su samo za članove, kontaktirajte vlasnika kako biste dobili pristup.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Kliknite na link za završetak registracije. Ako poruku niste dobili za 3 minute, provjerite spam folder!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Probajte besplatno na {{amount}} dana, zatim {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Otključajte pristup svim newsletterima plaćanjem pretplate.", "Unsubscribe from all emails": "Otkažite primanje svih poruka e-poštom", "Unsubscribed": "Otkazani ste", - "Unsubscribed from all emails.": "", + "Unsubscribed from all emails.": "Otkazani ste od svih poruka e-poštom", "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "Otkazivanjem primanja poruka e-pošte nećete otkazati Vašu pretplatu na {{title}}", "Update": "Ažuriranje", "Update your preferences": "Ažurirajte vaše postavke", - "Verification link sent, check your inbox": "", + "Verification link sent, check your inbox": "Poslali smo vam verifikacijski link, provjerite svoju e-poštu", "Verify your email address is correct": "Potvrdite točnost računa vaše e-pošte", "View plans": "Prikaži sve planove pretplate", "We couldn't unsubscribe you as the email address was not found. Please contact the site owner.": "Nismo uspjeli otkazati vašu e-poštu jer je nismo pronašli u sustavu. Molimo vas kontaktirajte vlasnika stranice.", "Welcome back, {{name}}!": "Dobrodošli natrag {{name}}!", "Welcome back!": "Dobrodošli natrag!", - "Welcome to {{siteTitle}}": "", + "Welcome to {{siteTitle}}": "Dobrodošli na {{siteTitle}}", "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "Kada vaš email server ne prihvati e-poštu to se obično naziva odbijanje. U mnogim slučajevima to može biti privremeno. Međutim, u nekim slučajevima odbijena e-pošta može se vratiti kao trajna greška kada je adresa e-pošte nevažeća ili nepostojeća.", "Why has my email been disabled?": "Zašto je moja adresa e-pošte blokirana?", "year": "", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Uspješno ste prijavljeni.", "You've successfully subscribed to": "Uspješno ste pretplaćeni na", "Your account": "Vaš korisnički račun", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Vaš doprinos pomaže u oblikovanju sadržaja kojeg objavljujemo.", "Your subscription will expire on {{expiryDate}}": "Vaša pretplata istječe {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Vaša pretplata će se obnoviti {{renewalDate}}", diff --git a/ghost/i18n/locales/hr/search.json b/ghost/i18n/locales/hr/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/hr/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/hu/comments.json b/ghost/i18n/locales/hu/comments.json index a52fc97cdd3..9e85484a09d 100644 --- a/ghost/i18n/locales/hu/comments.json +++ b/ghost/i18n/locales/hu/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Válasz erre", "Report": "Jelentés", "Report comment": "Hozzászólás jelentése", - "Report this comment": "Hozzászólás jelentése", "Report this comment?": "Hozzászólás jelentése?", "Save": "Mentés", "Sending": "Küldés", @@ -68,6 +67,5 @@ "This comment has been removed.": "Ezt a hozzászólást töröltük.", "Upgrade now": "Fizessen elő most", "Yesterday": "Tegnap", - "You want to report this comment?": "Szeretné jelenteni ezt a hozzászólást?", "Your request will be sent to the owner of this site.": "A kérését a website tulajdonosának fogjuk küldeni." } diff --git a/ghost/i18n/locales/hu/portal.json b/ghost/i18n/locales/hu/portal.json index 8a5923c39ec..298abc2b178 100644 --- a/ghost/i18n/locales/hu/portal.json +++ b/ghost/i18n/locales/hu/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "A bejelentkezéshez szükséges linket elküldtük a megadott email címre. Ha nem érkezne meg 3 percen belül, kérjük ellenőrizze a spam mappát!", "Account": "Fiók", + "Account details updated successfully": "", "Account settings": "Fiók beállítások", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Az ingyenes próbaidőszak lejárta után a kiválasztott csomag normál díját fogjuk felszámolni. A feliratkozás bármikor ingyenesen lemondható a próbaidőszak alatt.", "Already a member?": "Már van fiókja?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Egy nem várt hiba történt! Kérjük próbálkozzon újra vagy lépjen kapcsolatba velünk ha a hiba továbbra is fennál.", "Back": "Vissza", "Back to Log in": "Vissza a bejelentkezéshez", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Kérjük ellenőrizze a spam és promóciók mappát", "Check with your mail provider": "Lépjen kapcsolatba az email szolgáltatójával", + "Check your inbox to verify email update": "", "Choose": "Kiválaszt", "Choose a different plan": "Válasszon másik csomagot", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Kapcsolat", "Continue": "Folytatás", "Continue subscription": "Feliratkozás folytatása", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "A bejelentkezési link lejárt, a regisztráció sikertelen.", "Could not update email! Invalid link.": "Hibás link, az email cím változtatása sikertelen!", "Create a new contact": "Új kapcsolat létrehozása", @@ -52,6 +56,7 @@ "Edit": "Szerkesztés", "Email": "Email", "Email newsletter": "Hírlevél", + "Email newsletter settings updated": "", "Email preferences": "Email beállítások", "Emails": "Email-ek", "Emails disabled": "Email-ek kikapcsolva", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Hiba", "Expires {{expiryDate}}": "Lejárat: ", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Örökké", "Free Trial – Ends {{trialEnd}}": "Ingyenes próbaidőszak — Vége ekkor: {{trialEnd}}", "Get help": "Kérjen segítséget", @@ -91,6 +108,8 @@ "Name": "Név", "Need more help? Contact support": "További segítségre van szüksége? Lépjen kapcsolatba velünk", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "A hírlevelek letilthatók a fiókjában két okból: egy előző e-mailt spamként jelöltek meg, vagy egy e-mail küldési kísérlet tartós hibához (visszapattanáshoz) vezetett.", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Nem érkeznek meg az email-ek?", "Now check your email!": "Ellenőrizze az postafiókját", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Miután újra feliratkozott, ha még mindig nem látja az e-maileket a beérkező mappájában, ellenőrizze a spam mappát. Néhány email szolgáltató nyilvántartást vezet korábbi spam panaszokról, és továbbra is megjelölheti az e-maileket. Ha ez megtörténik, a legújabb hírlevelet jelölje meg 'Nem spamként', hogy visszakerüljön a fő beérkező mappájába.", @@ -126,6 +145,7 @@ "Submit feedback": "Visszajelzés küldése", "Subscribe": "Feliratkozás", "Subscribed": "Feliratkozva", + "Subscription plan updated successfully": "", "Success": "Siker", "Success! Check your email for magic link to sign-in.": "Siker! Nézze meg az email-jét a bejelentkezéshez szükséges linkhez.", "Success! Your account is fully activated, you now have access to all content.": "Siker! A fiókja teljesen aktiválva van, most már hozzáférése van az összes tartalomhoz.", @@ -138,12 +158,22 @@ "That didn't go to plan": "Hiba történt", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Az Ön nálunk regisztrált e-mail címe: {{memberEmail}} — ha ez nem helyes, frissítheti a fiókbeállításoknál.", "There was a problem submitting your feedback. Please try again a little later.": "Probléma volt a visszajelzés beküldésével. Kérjük, próbálja újra kicsit később.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "A website csak meghívóval látogatható. Meghívóért lépjen kapcsolatba az oldal tulajdonosával!", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "A regisztráció befejezéséhez kérjük kattintson az email-ben kapott linkre. Ha a link nem érkezne meg 3 percen belül kérjük ellenőrizze a spam mappát.", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Próbálja ki ingyen {{amount}} napig, utána {{originalPrice}}", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Előfizetéssel hozzáférhet minden hírlevélhez!", "Unsubscribe from all emails": "Leiratkozás minden email-ről", "Unsubscribed": "Sikeres leiratkozás", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Sikeres bejelentkezés.", "You've successfully subscribed to": "Sikeres bejelentkezés ide: ", "Your account": "Fiók", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "A visszajelzése segít abban, hogy mit publikáljunk", "Your subscription will expire on {{expiryDate}}": "Az előfizetése lejár ekkor: {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Az előfizetése megújul ekkor: {{expiryDate}}", diff --git a/ghost/i18n/locales/hu/search.json b/ghost/i18n/locales/hu/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/hu/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/id/comments.json b/ghost/i18n/locales/id/comments.json index f1ddf539c36..806c4e744a7 100644 --- a/ghost/i18n/locales/id/comments.json +++ b/ghost/i18n/locales/id/comments.json @@ -33,7 +33,7 @@ "Head of Marketing at Acme, Inc": "Direktur Pemasaran di Acme, Inc", "Hide": "Sembunyikan", "Hide comment": "Sembunyikan komentar", - "Jamie Larson": "Jamie Larson", + "Jamie Larson": "Sutan T. Alisjahbana", "Join the discussion": "Bergabung dalam diskusi", "Just now": "Baru saja", "Local resident": "Penduduk lokal", @@ -50,7 +50,6 @@ "Reply to comment": "Balas komentar", "Report": "Laporkan", "Report comment": "Laporkan komentar", - "Report this comment": "Laporkan komentar ini", "Report this comment?": "Laporkan komentar ini?", "Save": "Simpan", "Sending": "Mengirim", @@ -68,6 +67,5 @@ "This comment has been removed.": "Komentar ini telah dihapus.", "Upgrade now": "Daftar sekarang", "Yesterday": "Kemarin", - "You want to report this comment?": "Anda ingin melaporkan komentar ini?", "Your request will be sent to the owner of this site.": "Permintaan Anda akan dikirim kepada pemilik situs ini." } diff --git a/ghost/i18n/locales/id/portal.json b/ghost/i18n/locales/id/portal.json index 5294ea6d746..6dbd266d968 100644 --- a/ghost/i18n/locales/id/portal.json +++ b/ghost/i18n/locales/id/portal.json @@ -10,12 +10,14 @@ "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "{{memberEmail}} tidak akan menerima email lagi ketika seseorang membalas komentar Anda.", "{{memberEmail}} will no longer receive this newsletter.": "{{memberEmail}} tidak akan menerima buletin ini lagi.", "{{trialDays}} days free": "Gratis {{trialDays}} hari", - "+1 (123) 456-7890": "", + "+1 (123) 456-7890": "+62 123-4567-8900", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Tautan masuk telah dikirim ke kotak masuk Anda. Jika tidak diterima dalam waktu 3 menit, pastikan untuk memeriksa folder spam Anda.", "Account": "Akun", + "Account details updated successfully": "Detail akun berhasil diperbarui", "Account settings": "Pengaturan akun", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Setelah percobaan gratis berakhir, Anda akan dikenai harga normal untuk tingkatan yang dipilih. Anda selalu dapat membatalkannya sebelum masa percobaan gratis berakhir.", "Already a member?": "Sudah menjadi anggota?", + "An error occurred": "Terjadi kesalahan", "An unexpected error occured. Please try again or contact support if the error persists.": "Terjadi kesalahan tak terduga. Harap coba lagi atau hubungi layanan dukungan jika kesalahan masih berlanjut.", "Back": "Kembali", "Back to Log in": "Kembali ke Halaman Masuk", @@ -25,12 +27,13 @@ "Cancel subscription": "Batalkan langganan", "Cancellation reason": "Alasan pembatalan", "Change": "Ubah", - "Change plan": "", + "Change plan": "Ubah paket", "Check spam & promotions folders": "Periksa folder spam & promosi", "Check with your mail provider": "Hubungi penyedia layanan email Anda", + "Check your inbox to verify email update": "Periksa kotak masuk Anda untuk memverifikasi pembaruan email", "Choose": "Pilih", "Choose a different plan": "Pilih paket yang berbeda", - "Choose a plan": "", + "Choose a plan": "Pilih paket", "Choose your newsletters": "Pilih buletin Anda", "Click here to retry": "Klik di sini untuk mencoba lagi", "Close": "Tutup", @@ -42,6 +45,7 @@ "Contact support": "Hubungi layanan dukungan", "Continue": "Lanjutkan", "Continue subscription": "Lanjutkan berlangganan", + "Could not create stripe checkout session": "Tidak dapat membuat sesi checkout Stripe", "Could not sign in. Login link expired.": "Tidak dapat masuk. Tautan masuk telah kedaluwarsa.", "Could not update email! Invalid link.": "Tidak dapat memperbarui email! Tautan tidak valid.", "Create a new contact": "Buat kontak baru", @@ -52,14 +56,27 @@ "Edit": "Edit", "Email": "Email", "Email newsletter": "Buletin email", + "Email newsletter settings updated": "Pengaturan buletin email berhasil diperbarui", "Email preferences": "Preferensi email.", "Emails": "Email", "Emails disabled": "Email dinonaktifkan", "Ends {{offerEndDate}}": "Berakhir {{offerEndDate}}", - "Enter your email address": "", - "Enter your name": "", + "Enter your email address": "Masukkan alamat email Anda", + "Enter your name": "Masukkan nama Anda", "Error": "Eror", "Expires {{expiryDate}}": "Kedaluwarsa {{expiryDate}}", + "Failed to cancel subscription, please try again": "Gagal membatalkan langganan, harap coba lagi", + "Failed to log in, please try again": "Gagal masuk, harap coba lagi", + "Failed to log out, please try again": "Gagal keluar, harap coba lagi", + "Failed to process checkout, please try again": "Gagal memproses checkout, harap coba lagi", + "Failed to send magic link email": "Gagal mengirim email magic link", + "Failed to send verification email": "Gagal mengirim email verifikasi", + "Failed to sign up, please try again": "Gagal mendaftar, harap coba lagi", + "Failed to update account data": "Gagal memperbarui data akun", + "Failed to update account details": "Gagal memperbarui detail akun", + "Failed to update billing information, please try again": "Gagal memperbarui informasi penagihan, harap coba lagi", + "Failed to update newsletter settings": "Gagal memperbarui pengaturan buletin", + "Failed to update subscription, please try again": "Gagal memperbarui langganan, harap coba lagi", "Forever": "Selamanya", "Free Trial – Ends {{trialEnd}}": "Percobaan Gratis – Berakhir {{trialEnd}}", "Get help": "Dapatkan bantuan", @@ -77,30 +94,32 @@ "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "Jika Anda telah melakukan semua pemeriksaan tersebut dan masih belum menerima email, Anda dapat menghubungi kami melalui kontak {{supportAddress}} untuk mendapatkan bantuan.", "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "Jika terjadi kegagalan permanen saat mencoba mengirim buletin, email akan dinonaktifkan pada akun tersebut.", "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "Pada klien email Anda, tambahkan {{senderEmail}} ke daftar kontak Anda. Hal ini akan memberikan sinyal kepada layanan email Anda bahwa email yang dikirim dari alamat ini harus dipercaya.", - "Invalid email address": "", - "Jamie Larson": "", - "jamie@example.com": "", + "Invalid email address": "Alamat email tidak valid", + "Jamie Larson": "Sutan T. Alisjahbana", + "jamie@example.com": "sutan@example.com", "Less like this": "Kurangi yang seperti ini", "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "Pastikan email tidak berakhir di kotak masuk Spam atau Promosi Anda secara tidak sengaja. Jika ya, klik \"Tandai sebagai bukan spam\" dan/atau \"Pindahkan ke kotak masuk\".", "Manage": "Kelola", "Maybe later": "Mungkin nanti", "Memberships unavailable, contact the owner for access.": "Keanggotaan tidak tersedia, hubungi pemilik situs untuk mendapatkan akses.", - "month": "", + "month": "bulan", "Monthly": "Bulanan", "More like this": "Lebih banyak yang seperti ini", "Name": "Nama", "Need more help? Contact support": "Perlu bantuan lebih lanjut? Hubungi layanan dukungan", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Buletin dapat dinonaktifkan pada akun Anda dengan dua alasan: Email sebelumnya ditandai sebagai spam, atau percobaan pengiriman email menghasilkan kegagalan permanen (bounce).", + "No member exists with this e-mail address.": "Tidak ada anggota yang terdaftar dengan alamat email ini.", + "No member exists with this e-mail address. Please sign up first.": "Tidak ada anggota yang terdaftar dengan alamat email ini. Silakan daftar terlebih dahulu.", "Not receiving emails?": "Tidak menerima email?", "Now check your email!": "Sekarang periksa email Anda!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Setelah berlangganan kembali, jika Anda masih tidak melihat email di kotak masuk Anda, periksa folder spam. Beberapa penyedia layanan kotak masuk menyimpan catatan keluhan spam sebelumnya dan akan terus menandai email tersebut. Jika hal ini terjadi, tandai buletin terbaru sebagai 'Bukan spam' untuk memindahkannya kembali ke kotak masuk utama Anda.", "Permanent failure (bounce)": "Kegagalan permanen (bounce)", - "Phone number": "", + "Phone number": "Nomor telepon", "Plan": "Paket", "Plan checkout was cancelled.": "Pembayaran paket dibatalkan.", "Plan upgrade was cancelled.": "Peningkatan paket dibatalkan.", "Please contact {{supportAddress}} to adjust your complimentary subscription.": "Harap hubungi {{supportAddress}} untuk mengubah langganan gratis Anda.", - "Please enter {{fieldName}}": "", + "Please enter {{fieldName}}": "Silakan masukkan {{fieldName}}", "Please fill in required fields": "Harap isi kolom yang wajib diisi", "Price": "Harga", "Re-enable emails": "Aktifkan kembali email", @@ -117,7 +136,7 @@ "Sign out": "Keluar", "Sign up": "Daftar", "Signup error: Invalid link": "Kesalahan pendaftaran: Tautan tidak valid", - "Something went wrong, please try again later.": "", + "Something went wrong, please try again later.": "Terjadi kesalahan, silakan coba lagi nanti.", "Sorry, that didn’t work.": "Maaf, itu tidak berhasil.", "Spam complaints": "Keluhan spam", "Start {{amount}}-day free trial": "Mulai percobaan gratis selama {{amount}} hari", @@ -126,24 +145,35 @@ "Submit feedback": "Kirim masukan", "Subscribe": "Berlangganan", "Subscribed": "Telah berlangganan", + "Subscription plan updated successfully": "Paket langganan berhasil diperbarui", "Success": "Berhasil", "Success! Check your email for magic link to sign-in.": "Berhasil! Periksa email Anda untuk mendapatkan tautan ajaib untuk masuk.", "Success! Your account is fully activated, you now have access to all content.": "Berhasil! Akun Anda telah diaktifkan sepenuhnya, sekarang Anda memiliki akses ke semua konten.", "Success! Your email is updated.": "Berhasil! Email Anda telah diperbarui.", "Successfully unsubscribed": "Berhasil berhenti berlangganan", "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "Terima kasih telah berlangganan. Sebelum Anda mulai membaca, berikut adalah beberapa situs lain yang mungkin akan Anda nikmati.", - "Thank you for your support": "", - "Thank you for your support!": "", + "Thank you for your support": "Terima kasih atas dukungan Anda", + "Thank you for your support!": "Terima kasih atas dukungan Anda!", "Thanks for the feedback!": "Terima kasih atas masukannya!", "That didn't go to plan": "Itu tidak berjalan sesuai rencana", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Alamat email Anda yang kami miliki adalah {{memberEmail}} — jika itu tidak benar, Anda dapat memperbarui di Anda.", - "There was a problem submitting your feedback. Please try again a little later.": "Terjadi masalah saat mengirimkan masukan Anda. Coba lagi nanti.", - "There was an error processing your payment. Please try again.": "", + "There was a problem submitting your feedback. Please try again a little later.": "Terjadi masalah saat mengirimkan masukan Anda. Silakan coba sebentar lagi.", + "There was an error cancelling your subscription, please try again.": "Terjadi kesalahan saat membatalkan langganan Anda, harap coba lagi.", + "There was an error continuing your subscription, please try again.": "Terjadi kesalahan saat melanjutkan langganan Anda, harap coba lagi.", + "There was an error processing your payment. Please try again.": "Terjadi kesalahan saat memproses pembayaran Anda. Harap coba lagi.", + "There was an error sending the email, please try again": "Terjadi kesalahan saat mengirim email, harap coba lagi", "This site is invite-only, contact the owner for access.": "Situs ini hanya untuk yang diundang, hubungi pemiliknya untuk mendapatkan akses.", - "This site is not accepting payments at the moment.": "", + "This site is not accepting payments at the moment.": "Situs ini tidak menerima pembayaran saat ini.", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Untuk menyelesaikan pendaftaran, klik tautan konfirmasi di kotak masuk Anda. Jika tidak diterima dalam waktu 3 menit, pastikan untuk memeriksa folder spam Anda!", - "To continue to stay up to date, subscribe to {{publication}} below.": "", + "To continue to stay up to date, subscribe to {{publication}} below.": "Untuk tetap mendapatkan informasi terkini, silakan berlangganan ke {{publication}} di bawah ini.", + "Too many attempts try again in {{number}} days.": "Terlalu banyak percobaan, coba lagi dalam {{number}} hari.", + "Too many attempts try again in {{number}} hours.": "Terlalu banyak percobaan, coba lagi dalam {{number}} jam.", + "Too many attempts try again in {{number}} minutes.": "Terlalu banyak percobaan, coba lagi dalam {{number}} menit.", + "Too many different sign-in attempts, try again in {{number}} days": "Terlalu banyak percobaan masuk, coba lagi dalam {{number}} hari", + "Too many different sign-in attempts, try again in {{number}} hours": "Terlalu banyak percobaan masuk, coba lagi dalam {{number}} jam", + "Too many different sign-in attempts, try again in {{number}} minutes": "Terlalu banyak percobaan masuk, coba lagi dalam {{number}} menit", "Try free for {{amount}} days, then {{originalPrice}}.": "Coba gratis selama {{amount}} hari, kemudian {{originalPrice}}.", + "Unable to initiate checkout session": "Tidak dapat memulai sesi checkout", "Unlock access to all newsletters by becoming a paid subscriber.": "Buka akses ke semua buletin dengan menjadi pelanggan berbayar.", "Unsubscribe from all emails": "Berhenti berlangganan dari semua email", "Unsubscribed": "Tidak berlangganan", @@ -160,7 +190,7 @@ "Welcome to {{siteTitle}}": "Selamat datang di {{siteTitle}}", "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "Ketika sebuah kotak masuk gagal menerima sebuah email, hal tersebut umumnya disebut sebagai bounce. Dalam banyak kasus, ini bisa bersifat sementara. Namun, dalam beberapa kasus, email yang terbounce dapat dikembalikan sebagai kegagalan permanen ketika alamat email yang dituju tidak valid atau tidak ada.", "Why has my email been disabled?": "Mengapa email saya dinonaktifkan?", - "year": "", + "year": "tahun", "Yearly": "Tahunan", "You currently have a free membership, upgrade to a paid subscription for full access.": "Saat ini Anda memiliki keanggotaan gratis, tingkatkan ke langganan berbayar untuk akses penuh", "You have been successfully resubscribed": "Anda telah berhasil berlangganan kembali", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Anda telah berhasil masuk.", "You've successfully subscribed to": "Anda telah berhasil berlangganan ke", "Your account": "Akun Anda", + "Your email has failed to resubscribe, please try again": "Email Anda gagal berlangganan ulang, harap coba lagi", "Your input helps shape what gets published.": "Masukan Anda membantu membentuk apa yang dipublikasikan.", "Your subscription will expire on {{expiryDate}}": "Langganan Anda akan berakhir pada {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Langganan Anda akan diperpanjang pada {{renewalDate}}", diff --git a/ghost/i18n/locales/id/search.json b/ghost/i18n/locales/id/search.json new file mode 100644 index 00000000000..c2076995606 --- /dev/null +++ b/ghost/i18n/locales/id/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "Penulis", + "Cancel": "Batalkan", + "No matches found": "Tidak ada hasil yang ditemukan", + "Posts": "Postingan", + "Search posts, tags and authors": "Cari postingan, tag, dan penulis", + "Show more results": "Tampilkan lebih banyak hasil", + "Tags": "Tag" +} diff --git a/ghost/i18n/locales/is/comments.json b/ghost/i18n/locales/is/comments.json index c5391eab775..77ef1db9693 100644 --- a/ghost/i18n/locales/is/comments.json +++ b/ghost/i18n/locales/is/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "", "Report": "", "Report comment": "", - "Report this comment": "", "Report this comment?": "", "Save": "", "Sending": "", @@ -68,6 +67,5 @@ "This comment has been removed.": "", "Upgrade now": "", "Yesterday": "", - "You want to report this comment?": "", "Your request will be sent to the owner of this site.": "" } diff --git a/ghost/i18n/locales/is/portal.json b/ghost/i18n/locales/is/portal.json index 9000e6a9e72..47ade2f422d 100644 --- a/ghost/i18n/locales/is/portal.json +++ b/ghost/i18n/locales/is/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Innskráningarhlekkur hefur verið sendur á netfangið þitt. Ef hann er ekki kominn innan 3ja mínútna skaltu athuga spam-möppuna.", "Account": "Aðgangur", + "Account details updated successfully": "", "Account settings": "Aðgangsstillingar", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Þegar prufutímabili lýkur muntu greiða venjulegt verð í samræmi við áskriftarleiðina sem þú valdir. Þú getur ávallt sagt upp áskriftinni áður en til þess kemur.", "Already a member?": "Ertu nú þegar með áskrift?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Óvænt villa kom upp. Vinsamlegast reynið aftur eða hafið samband ef villan reynist þrálát.", "Back": "Til baka", "Back to Log in": "Aftur til innskráningar", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Athugið ruslpósta- eða kynningarefnismöppur", "Check with your mail provider": "Hafið samband við þjónustuveitanda netfangsins", + "Check your inbox to verify email update": "", "Choose": "Velja", "Choose a different plan": "Velja aðra áskriftarleið", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Hafa samband við þjónustuver", "Continue": "Áfram", "Continue subscription": "Halda áfram í áskrift", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Innskráning mistókst. Hlekkurinn varð óvirkur.", "Could not update email! Invalid link.": "Breyting á netfangi mistókst! Ógildur hlekkur.", "Create a new contact": "Skrá nýjan tengilið", @@ -52,6 +56,7 @@ "Edit": "Breyta", "Email": "Netfang", "Email newsletter": "Fréttabréf", + "Email newsletter settings updated": "", "Email preferences": "Stillingar netfangs", "Emails": "Netföng", "Emails disabled": "Netföng gerð óvirk", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Villa", "Expires {{expiryDate}}": "Rennur út {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Að eilífu", "Free Trial – Ends {{trialEnd}}": "Ókeypis prufutímabil – Lýkur {{trialEnd}}", "Get help": "Fá aðstoð", @@ -91,6 +108,8 @@ "Name": "Nafn", "Need more help? Contact support": "Þarftu meiri aðstoð? Hafðu samband við þjónustuverið", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Færðu ekki tölvupósta?", "Now check your email!": "Athugaðu nú tölvupósthólfið þitt!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Ef þú sérð ekki tölvupósta eftir að hafa endurvakið áskrift, athugaðu spam-möppuna. Enn kunna að vera skráðar kvartanir um ruslpóst og tölvupóstarnir því flokkaður á þann veg. Ef svo er skaltu merkja síðasta fréttabréf sem 'Not spam' og færa yfir í aðalpósthólfið.", @@ -126,6 +145,7 @@ "Submit feedback": "Gefa endurgjöf", "Subscribe": "Áskrift", "Subscribed": "Í áskrift", + "Subscription plan updated successfully": "", "Success": "Þetta heppnaðist", "Success! Check your email for magic link to sign-in.": "Þetta heppnaðist! Athugaðu tölvupósthólfið til að finna hlekk til innskráningar.", "Success! Your account is fully activated, you now have access to all content.": "Þetta heppnaðist! Aðgangurinn þinn er virkur, þú hefur nú aðgang að öllu efni.", @@ -138,12 +158,22 @@ "That didn't go to plan": "Þetta fór ekki samkvæmt áætlun", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Netfangið sem þú ert skráður fyrir er {{memberEmail}} — ef það er rangt geturðu breytt því í .", "There was a problem submitting your feedback. Please try again a little later.": "Villa kom upp við sendingu athugasemdar. Vinsamlegast reynið aftur örlítið síðar.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Aðgangur krefst boðsmiða, hafið samband við eiganda síðunnar til að fá aðgang.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Til að ljúka nýskráningu skaltu smella á staðfestingarhlekkinn sem var sendur á netfangið þitt. Ef hann er ekki kominn innan 3 mínútna skaltu athuga spam-möppuna.", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Prófaðu í {{amount}} daga án endurgjalds og síðan fyrir {{originalPrice}}", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Fáðu aðgang að öllum fréttabréfum með því að gerast áskrifandi.", "Unsubscribe from all emails": "Segja upp öllum tölvupóstum", "Unsubscribed": "Ekki í áskrift", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Þér tókst að skrá þig inn", "You've successfully subscribed to": "", "Your account": "Aðgangurinn þinn", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "", "Your subscription will expire on {{expiryDate}}": "Áskrift þinni lýkur {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Áskrift þín verður endurnýjuð {{expiryDate}}", diff --git a/ghost/i18n/locales/is/search.json b/ghost/i18n/locales/is/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/is/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/it/comments.json b/ghost/i18n/locales/it/comments.json index 31ce236bf75..ec19b1490ba 100644 --- a/ghost/i18n/locales/it/comments.json +++ b/ghost/i18n/locales/it/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Rispondi al commento", "Report": "Segnala", "Report comment": "Segnala commento", - "Report this comment": "Segnala questo commento", "Report this comment?": "Segnalare questo commento?", "Save": "Salva", "Sending": "Invio", @@ -68,6 +67,5 @@ "This comment has been removed.": "Questo commento è stato rimosso.", "Upgrade now": "Abbonati ora", "Yesterday": "Ieri", - "You want to report this comment?": "Vuoi segnalare questo commento?", "Your request will be sent to the owner of this site.": "La tua richiesta sarà inviata al proprietario del sito." } diff --git a/ghost/i18n/locales/it/portal.json b/ghost/i18n/locales/it/portal.json index 9451110b353..37b81e7ed6c 100644 --- a/ghost/i18n/locales/it/portal.json +++ b/ghost/i18n/locales/it/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Un link di accesso è stato inviato alla tua casella di posta. Se non lo ricevi entro 3 minuti, controlla nello spam.", "Account": "Account", + "Account details updated successfully": "", "Account settings": "Impostazioni account", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Al termine della prova gratuita, ti verrà addebitato il prezzo regolare del piano scelto. Puoi annullare in qualsiasi momento prima della scadenza.", "Already a member?": "Sei già iscritto?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Si è verificato un errore imprevisto. Contatta l'assistenza se l'errore persiste.", "Back": "Indietro", "Back to Log in": "Torna alla pagina d'accesso", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Controlla nello spam", "Check with your mail provider": "Contatta il tuo provider di posta elettronica", + "Check your inbox to verify email update": "", "Choose": "Scegli", "Choose a different plan": "Scegli un piano differente", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Contatta l'assistenza", "Continue": "Continua", "Continue subscription": "Riabbonati", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Accesso non effettuato. Il link è scaduto.", "Could not update email! Invalid link.": "Email non aggiornata! Link non valido.", "Create a new contact": "Crea un nuovo contatto", @@ -52,6 +56,7 @@ "Edit": "Modifica", "Email": "Email", "Email newsletter": "Newsletter", + "Email newsletter settings updated": "", "Email preferences": "Preferenze email", "Emails": "Email", "Emails disabled": "Email disattivate", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Errore", "Expires {{expiryDate}}": "Scade il {{offerEndDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Per sempre", "Free Trial – Ends {{trialEnd}}": "Prova gratuita – finisce il {{trialEnd}}", "Get help": "Chiedi aiuto", @@ -91,6 +108,8 @@ "Name": "Nome", "Need more help? Contact support": "Hai ancora bisogno di aiuto? Contatta l'assistenza", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Le newsletter possono essere disabilitate nel tuo account per due ragioni: un'email precedente è stata segnalata come spam, o l'invio di un'email ha restituito un fallimento permanente (rimbalzo).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Non ricevi le email?", "Now check your email!": "Ora controlla la tua email!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Se ancora non vedi le email una volta reiscritto, controlla nello spam. Alcuni provider tengono nota dei reclami e continuano a segnalare le email. Se questo dovesse succedere, segnala l'ultima email ricevuta come \"non spam\" e spostala nella tua posta in arrivo.", @@ -126,6 +145,7 @@ "Submit feedback": "Invia feedback", "Subscribe": "Abbonati", "Subscribed": "Abbonato", + "Subscription plan updated successfully": "", "Success": "Fatto", "Success! Check your email for magic link to sign-in.": "Fatto! Controlla la tua email per il magico link d'accesso.", "Success! Your account is fully activated, you now have access to all content.": "Fatto! Il tuo account è stato attivato, ora hai accesso a tutti i contenuti.", @@ -138,12 +158,22 @@ "That didn't go to plan": "Questo non era previsto", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "L'indirizzo email registrato è {{memberEmail}} — se non è corretto, puoi modificarlo nelle tue .", "There was a problem submitting your feedback. Please try again a little later.": "C'è stato un errore durante l'invio del tuo feedback. Si prega di riprovare più tardi.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Questo sito è accessibile solo su invito, contatta il proprietario per poter accedere.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Per completare l'iscrizione, clicca il link di conferma inviato alla tua email. Se non lo ricevi entro 3 minuti, controlla nello spam!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Prova gratis per {{amount}} giorni, poi {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Abbonati per sbloccare l'accesso a tutte le newsletter.", "Unsubscribe from all emails": "Disiscriviti da tutte le email", "Unsubscribed": "Disiscritto", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Accesso effettuato.", "You've successfully subscribed to": "Iscrizione effettuata a", "Your account": "Il tuo account", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Il tuo contributo aiuta a dare forma a ciò che viene pubblicato.", "Your subscription will expire on {{expiryDate}}": "Il tuo abbonamento scadrà il {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Il tuo abbonamento verrà rinnovato il {{renewalDate}}", diff --git a/ghost/i18n/locales/it/search.json b/ghost/i18n/locales/it/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/it/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/ja/comments.json b/ghost/i18n/locales/ja/comments.json index 6fbe20fc3b6..1ba9bf26bf7 100644 --- a/ghost/i18n/locales/ja/comments.json +++ b/ghost/i18n/locales/ja/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "コメントに返信する", "Report": "通報する", "Report comment": "コメントを通報する", - "Report this comment": "このコメントを通報する", "Report this comment?": "このコメントを通報しますか?", "Save": "保存", "Sending": "送信中", @@ -68,6 +67,5 @@ "This comment has been removed.": "このコメントは削除されました。", "Upgrade now": "今すぐアップデートする", "Yesterday": "昨日", - "You want to report this comment?": "このコメントを通報しますか?", "Your request will be sent to the owner of this site.": "あなたのリクエストはこのサイトの管理者に送信されます。" } diff --git a/ghost/i18n/locales/ja/portal.json b/ghost/i18n/locales/ja/portal.json index 037fdfdf102..efdb9fc943e 100644 --- a/ghost/i18n/locales/ja/portal.json +++ b/ghost/i18n/locales/ja/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "ログインリンクが受信箱に送信されました。3分以内にメールが届かない場合は、迷惑メールのフォルダーをご確認ください。", "Account": "アカウント", + "Account details updated successfully": "", "Account settings": "アカウント設定", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "無料期間が終了すると、選択したプランの通常価格が請求されます。それまではいつでもキャンセルできます。", "Already a member?": "すでに会員ですか?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "予期しないエラーが発生しました。もう一度試すか、エラーが解決しない場合はサポートにお問い合わせください。", "Back": "戻る", "Back to Log in": "ログインに戻る", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "スパムとプロモーションフォルダを確認してください", "Check with your mail provider": "メールプロバイダーに確認してください", + "Check your inbox to verify email update": "", "Choose": "選択", "Choose a different plan": "別のプランを選択", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "サポートに連絡", "Continue": "続ける", "Continue subscription": "購読を続ける", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "ログインできませんでした。ログインリンクの有効期限が切れています。", "Could not update email! Invalid link.": "メールアドレスを更新できませんでした。無効なリンクです。", "Create a new contact": "新しい連絡先を作成", @@ -52,6 +56,7 @@ "Edit": "編集", "Email": "メール", "Email newsletter": "ニュースレターのメール", + "Email newsletter settings updated": "", "Email preferences": "メールの設定", "Emails": "メール", "Emails disabled": "メールが無効になっています", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "エラー", "Expires {{expiryDate}}": "{{expiryDate}}まで有効", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "永久", "Free Trial – Ends {{trialEnd}}": "無料期間 - {{trialEnd}}まで", "Get help": "サポート", @@ -91,6 +108,8 @@ "Name": "名前", "Need more help? Contact support": "サポートが必要ですか?お問い合わせください。", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "ニュースレターは、2つの理由によってアカウント上で無効になる場合があります: 以前のメールがスパムとしてマークされた場合、またはメールの送信が永続的な障害によって失敗した場合です。", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "メールが受信されない場合", "Now check your email!": "メールを確認してください", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "再購読した後も受信トレイにメールが表示されない場合は、スパムフォルダを確認してください。一部の受信トレイは以前のスパムの記録を保持し、引き続きメールを判定します。これが起こった場合は、最新のニュースレターを「スパムではない」とマークし、受信トレイに移動してください。", @@ -126,6 +145,7 @@ "Submit feedback": "フィードバックを送信", "Subscribe": "購読する", "Subscribed": "購読済み", + "Subscription plan updated successfully": "", "Success": "成功", "Success! Check your email for magic link to sign-in.": "成功しました!ログイン用のマジックリンクをメールで確認してください。", "Success! Your account is fully activated, you now have access to all content.": "成功しました!アカウントが完全にアクティブ化されました。これですべてのコンテンツにアクセスできます。", @@ -138,12 +158,22 @@ "That didn't go to plan": "計画通りにいきませんでした", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "メールアドレスは{{memberEmail}}です。もし正しくない場合は、で更新することができます。", "There was a problem submitting your feedback. Please try again a little later.": "フィードバックの送信中に問題が発生しました。しばらくしてからもう一度お試しください。", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "このサイトは招待制です。アクセスするにはオーナーに連絡してください。", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "新規登録を完了するには、受信トレイの確認リンクをクリックしてください。3分経っても届かない場合は、スパムフォルダを確認してください。", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "{{amount}}日間無料でお試しください、その後は{{originalPrice}}です。", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "有料の購読者になることで、すべてのニュースレターへのアクセスが可能になります。", "Unsubscribe from all emails": "すべてのメールの購読解除", "Unsubscribed": "購読解除されました", @@ -170,6 +200,7 @@ "You've successfully signed in.": "ログインに成功しました", "You've successfully subscribed to": "の購読に成功しました", "Your account": "あなたのアカウント", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "あなたの感想を今後の内容の参考にさせていただきます。", "Your subscription will expire on {{expiryDate}}": "あなたの購読は{{expiryDate}}に期限切れになります。", "Your subscription will renew on {{renewalDate}}": "あなたの購読は{{renewalDate}}に更新されます。", diff --git a/ghost/i18n/locales/ja/search.json b/ghost/i18n/locales/ja/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/ja/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/ko/comments.json b/ghost/i18n/locales/ko/comments.json index a8721e5de74..786c9f2aa9d 100644 --- a/ghost/i18n/locales/ko/comments.json +++ b/ghost/i18n/locales/ko/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "댓글에 답변", "Report": "신고", "Report comment": "댓글 신고", - "Report this comment": "이 댓글을 신고", "Report this comment?": "이 댓글을 신고하시겠어요?", "Save": "저장", "Sending": "전송중", @@ -68,6 +67,5 @@ "This comment has been removed.": "삭제 된 댓글이에요.", "Upgrade now": "업그레이드", "Yesterday": "어제", - "You want to report this comment?": "이 댓글을 신고하시겠어요?", "Your request will be sent to the owner of this site.": "운영자에게 요구사항이 전달되었어요." } diff --git a/ghost/i18n/locales/ko/portal.json b/ghost/i18n/locales/ko/portal.json index 29bf5281478..6e99b98358d 100644 --- a/ghost/i18n/locales/ko/portal.json +++ b/ghost/i18n/locales/ko/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "로그인 링크가 이메일로 전송되었어요. 3분 내에 도착하지 않으면 스팸 폴더를 확인해 주세요.", "Account": "계정", + "Account details updated successfully": "", "Account settings": "계정 설정", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "무료 체험이 종료되면 선택한 요금제의 정상 가격이 청구돼요. 그 전에 언제든지 취소할 수 있어요.", "Already a member?": "이미 회원이신가요?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "예기치 않은 오류가 발생했어요. 계속해서 오류가 발생하면 다시 시도하거나 지원팀에 문의해 주세요.", "Back": "뒤로", "Back to Log in": "로그인으로 돌아가기", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "스팸 & 프로모션 폴더를 확인해 주세요", "Check with your mail provider": "메일 제공업체에 문의해 주세요", + "Check your inbox to verify email update": "", "Choose": "선택", "Choose a different plan": "다른 요금제 선택", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "지원팀에 문의", "Continue": "계속", "Continue subscription": "구독 계속", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "로그인할 수 없어요. 로그인 링크가 만료되었어요.", "Could not update email! Invalid link.": "이메일을 업데이트할 수 없어요! 잘못된 링크이에요.", "Create a new contact": "새 연락처 만들기", @@ -52,6 +56,7 @@ "Edit": "편집", "Email": "이메일", "Email newsletter": "이메일 뉴스레터", + "Email newsletter settings updated": "", "Email preferences": "이메일 설정", "Emails": "이메일", "Emails disabled": "이메일 사용 중지됨", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "오류", "Expires {{expiryDate}}": "{{expiryDate}}에 만료돼요", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "영원히", "Free Trial – Ends {{trialEnd}}": "무료 체험 – {{trialEnd}}에 종료돼요", "Get help": "도움 요청", @@ -91,6 +108,8 @@ "Name": "이름", "Need more help? Contact support": "더 많은 도움이 필요하신가요? 지원팀에 문의해 주세요", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "뉴스레터는 계정에서 두 가지 이유로 사용 중지될 수 있어요: 이전 이메일이 스팸으로 표시되었거나, 이메일을 보내려고 시도했지만 영구적인 실패가 발생했을 때(바운스).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "이메일을 받지 못하고 계신가요?", "Now check your email!": "지금 이메일을 확인해 주세요!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "다시 구독한 후에도 받은 편지함에 이메일이 표시되지 않는다면 스팸 폴더를 확인해 주세요. 일부 받은 편지함 제공업체는 이전 스팸 신고 기록을 유지하고 계속해서 이메일을 표시해요. 이런 경우 최신 뉴스레터를 '스팸이 아님'으로 표시하여 기본 받은 편지함으로 옮겨주세요.", @@ -126,6 +145,7 @@ "Submit feedback": "의견 제출", "Subscribe": "구독", "Subscribed": "구독 완료", + "Subscription plan updated successfully": "", "Success": "성공", "Success! Check your email for magic link to sign-in.": "성공! 로그인을 위한 링크가 이메일로 전송되었어요.", "Success! Your account is fully activated, you now have access to all content.": "성공! 계정이 활성화되었어요. 이제 모든 콘텐츠에 접근할 수 있어요.", @@ -138,12 +158,22 @@ "That didn't go to plan": "계획대로 진행되지 않았어요", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "회원님의 이메일 주소는 {{memberEmail}}이에요. 이 주소가 맞지 않다면 에서 업데이트할 수 있어요.", "There was a problem submitting your feedback. Please try again a little later.": "의견을 제출하는 중에 문제가 발생했어요. 잠시 후 다시 시도해 주세요.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "위 사이트는 초대된 사용자만 사용이 가능해요. 접근을 위해서는 관리자에게 연락해 주세요.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "가입을 완료하려면 이메일의 확인 링크를 클릭해 주세요. 3분 이내에 도착하지 않으면 스팸 폴더를 확인해 주세요!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "{{amount}}일 동안 무료로 사용한 후 {{originalPrice}}로 결제해 주세요.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "유료 구독자가 되어 모든 뉴스레터에 접근해 주세요.", "Unsubscribe from all emails": "모든 이메일 구독 취소", "Unsubscribed": "구독 취소 완료", @@ -170,6 +200,7 @@ "You've successfully signed in.": "성공적으로 로그인되었어요.", "You've successfully subscribed to": "", "Your account": "계정", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "회원님의 의견은 게시물을 제작하는 것에 큰 도움이 돼요.", "Your subscription will expire on {{expiryDate}}": "회원님의 구독은 {{expiryDate}}에 만료돼요", "Your subscription will renew on {{renewalDate}}": "회원님의 구독은 {{renewalDate}}에 갱신돼요", diff --git a/ghost/i18n/locales/ko/search.json b/ghost/i18n/locales/ko/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/ko/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/kz/comments.json b/ghost/i18n/locales/kz/comments.json new file mode 100644 index 00000000000..774db609baa --- /dev/null +++ b/ghost/i18n/locales/kz/comments.json @@ -0,0 +1,71 @@ +{ + "{{amount}} characters left": "{{amount}} таңба қалды", + "{{amount}} comments": "{{amount}} пікір", + "{{amount}} days ago": "{{amount}} күн бұрын", + "{{amount}} hrs ago": "{{amount}} сағат бұрын", + "{{amount}} mins ago": "{{amount}} минут бұрын", + "{{amount}} months ago": "{{amount}} ай бұрын", + "{{amount}} more": "тағы {{amount}}", + "{{amount}} seconds ago": "{{amount}} секунд бұрын", + "{{amount}} weeks ago": "{{amount}} апта бұрын", + "{{amount}} years ago": "{{amount}} жыл бұрын", + "1 comment": "1 пікір", + "Add comment": "Пікір қосу", + "Add context to your comment, share your name and expertise to foster a healthy discussion.": "Талқы қызық болуы үшін өзіңіз туралы мәліметтерді қосыңыз.", + "Add reply": "Жауап қосу", + "Already a member?": "Әлдеқашан қосылдыңыз ба?", + "Anonymous": "Аноним", + "Become a member of {{publication}} to start commenting.": "Пікір қалдыру үшін {{publication}} сайтына аккаунт қажет.", + "Become a paid member of {{publication}} to start commenting.": "Пікір қалдыру үшін {{publication}} сайтына ақылы аккаунт қажет.", + "Cancel": "Бас тарту", + "Comment": "Пікір", + "Complete your profile": "Профиліңізді толтырыңыз", + "Delete": "Жою", + "Deleted member": "Жойылған аккаунт", + "Discussion": "Талқы", + "Edit": "Өңдеу", + "Edit this comment": "Бұл пікірді өңдеу", + "edited": "Өңделді", + "Enter your name": "Есіміңізді жазыңыз", + "Expertise": "Қызмет", + "Founder @ Acme Inc": "@ Acme Inc құрылтайшысы", + "Full-time parent": "Толық күн жұмыс істейтін ата-ана", + "Head of Marketing at Acme, Inc": "@ Acme маркетинг жетекшісі ", + "Hide": "Жасыру", + "Hide comment": "Пікірді жасыру", + "Jamie Larson": "Пәленше Түгеншиев", + "Join the discussion": "Талқыға қосылу", + "Just now": "Дәл қазір", + "Local resident": "Жергілікті тұрғын", + "Member discussion": "Мүшелер талқысы", + "Name": "Есімі", + "Neurosurgeon": "Нейрохирург", + "One day ago": "Бір күн бұрын", + "One hour ago": "Бір сағат бұрын", + "One min ago": "Бір минут бұрын", + "One month ago": "Бір ай бұрын", + "One week ago": "Бір апта бұрын", + "One year ago": "Бір жыл бұрын", + "Reply": "Жауап беру", + "Reply to comment": "Пікірге жауап беру", + "Report": "Шағымдану", + "Report comment": "Пікірге шағымдану", + "Report this comment?": "Осы пікірге шағым жіберілсін бе?", + "Save": "Сақтау", + "Sending": "Жіберілуде", + "Sent": "Жіберілді", + "Show": "Көрсету", + "Show {{amount}} more replies": "Тағы {{amount}} жауапты көрсету", + "Show {{amount}} previous comments": "Алдыңғы {{amount}} пікірді көрсету", + "Show 1 more reply": "Тағы 1 жауап көрсету", + "Show 1 previous comment": "Алдыңғы 1 пікірді көрсету", + "Show comment": "Пікірді көрсету", + "Sign in": "Кіру", + "Sign up now": "Қазір тіркелу", + "Start the conversation": "Талқыны бастау", + "This comment has been hidden.": "Бұл пікір жасырылды.", + "This comment has been removed.": "Бұл пікір өшірілді.", + "Upgrade now": "Жаңарту ", + "Yesterday": "Кеше", + "Your request will be sent to the owner of this site.": "Өтінішіңіз сайт иесіне жіберіледі." +} diff --git a/ghost/i18n/locales/kz/ghost.json b/ghost/i18n/locales/kz/ghost.json new file mode 100644 index 00000000000..0c6cded664d --- /dev/null +++ b/ghost/i18n/locales/kz/ghost.json @@ -0,0 +1,34 @@ +{ + "All the best!": "Ізгі ниетпен!", + "Complete signup for {{siteTitle}}!": "{{siteTitle}} сайтына тіркелуді аяқтаңыз!", + "Complete your sign up to {{siteTitle}}!": "{{siteTitle}} сайтына тіркелуді аяқтаңыз!", + "Confirm email address": "Email адресті растаңыз", + "Confirm signup": "Тіркелуді мақұлдаңыз", + "Confirm your email address": "Email адресіңізді растаңыз", + "Confirm your email update for {{siteTitle}}!": "{{siteTitle}} үшін email жаңартуын растаңыз", + "Confirm your subscription to {{siteTitle}}": "{{siteTitle}} сайтына жазылымды растаңыз", + "For your security, the link will expire in 24 hours time.": "Қауіпсіздік мақсатында сілтеме күші 24 сағат ішінде жойылады.", + "Hey there,": "Сәлем,", + "Hey there!": "Сәлем!", + "If you did not make this request, you can safely ignore this email.": "Егер мұндай сұраныс жасамаған болсаңыз, бұл хатқа мән бермесеңіз болады.", + "If you did not make this request, you can simply delete this message.": "Егер мұндай сұраныс жасамаған болсаңыз, бұл хатты өшіре берсеңіз болады.", + "Please confirm your email address with this link:": "Email поштаңызды осы сілтеме арқылы растауыңызды сұраймыз:", + "Secure sign in link for {{siteTitle}}": "{{siteTitle}} сайтына қауіпсіз кіруге арналған сілтеме", + "See you soon!": "Көріскенше!", + "Sent to {{email}}": "{{email}} поштасына жіберілді", + "Sign in": "Кіру", + "Sign in to {{siteTitle}}": "{{siteTitle}} сайтына кіру", + "Tap the link below to complete the signup process for {{siteTitle}}, and be automatically signed in:": "{{siteTitle}} сайтына тіркелу процесін аяқтау үшін және жүйеге автоматты түрде кіру үшін төмендегі сілтемені басыңыз:", + "Thank you for signing up to {{siteTitle}}!": "{{siteTitle}} сайтына тіркелгеніңіз үшін алғыс білдіреміз!", + "Thank you for subscribing to {{siteTitle}}!": "{{siteTitle}} сайтына жазылғаныңыз үшін алғыс білдіреміз!", + "Thank you for subscribing to {{siteTitle}}.": "{{siteTitle}} сайтына жазылғаныңыз үшін алғыс білдіреміз.", + "Thank you for subscribing to {{siteTitle}}. Tap the link below to be automatically signed in:": "{{siteTitle}} сайтына жазылғаныңыз үшін алғыс білдіреміз. Автоматты түрде кіру үшін төмендегі сілтемені басыңыз:", + "This email address will not be used.": "Бұл email адрес қолданылмайды.", + "Welcome back to {{siteTitle}}!": "{{siteTitle}} сайтына қайта қош келдіңіз!", + "Welcome back! Use this link to securely sign in to your {{siteTitle}} account:": "Қош келдіңіз! {{siteTitle}} аккаунтыңызға қауіпсіз кіру үшін мына сілтемені пайдаланыңыз:", + "You can also copy & paste this URL into your browser:": "Сондай-ақ, мына сілтемені браузер бағанына қоя аласыз:", + "You will not be signed up, and no account will be created for you.": "Сізді тіркемейміз әрі сіз үшін аккаунт құрылмайды.", + "You will not be subscribed.": "Сіз үшін жазылым жасамаймыз.", + "You're one tap away from subscribing to {{siteTitle}} — please confirm your email address with this link:": "{{siteTitle}} жазылу үшін бір-ақ қадам қалды — мына сілтеме арқылы email адресіңізді растауыңызды сұраймыз:", + "You're one tap away from subscribing to {{siteTitle}}!": "{{siteTitle}} жазылу үшін бір-ақ қадам қалды!" +} diff --git a/ghost/i18n/locales/kz/portal.json b/ghost/i18n/locales/kz/portal.json new file mode 100644 index 00000000000..218044857fb --- /dev/null +++ b/ghost/i18n/locales/kz/portal.json @@ -0,0 +1,208 @@ +{ + "(save {{highestYearlyDiscount}}%)": "(жылдық {{highestYearlyDiscount}}% үнемдеңіз)", + "{{amount}} days free": "{{amount}} күн тегін", + "{{amount}} off": "{{amount}} жеңілдік", + "{{amount}} off for first {{number}} months.": "алғашқы {{number}} айға {{amount}} жеңілдік.", + "{{amount}} off for first {{period}}.": "алғашқы {{period}} мерзімге {{amount}} жеңілдік.", + "{{amount}} off forever.": "{{amount}} мәңгі жеңілдік.", + "{{discount}}% discount": "{{discount}}% жеңілдік", + "{{memberEmail}} will no longer receive {{newsletterName}} newsletter.": "{{memberEmail}} енді {{newsletterName}} ақпараттық бюллетенін алмайды.", + "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "Пікірлеріңізге жауап берген кезде {{memberEmail}} хаттар алмайды.", + "{{memberEmail}} will no longer receive this newsletter.": "{{memberEmail}} енді бұл ақпараттық бюллетенді алмайды.", + "{{trialDays}} days free": "{{trialDays}} күн тегін", + "+1 (123) 456-7890": "", + "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Кіру сілтемесі кіріс жәшігіңізге жіберілді. Егер ол 3 минут ішінде келмесе, спам қалтасын тексеріңіз.", + "Account": "Аккаунт", + "Account details updated successfully": "", + "Account settings": "Аккаунт баптаулары", + "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Тегін сынақ мерзімі аяқталғаннан кейін таңдаған деңгейіңіз үшін қалыпты баға төленеді. Оған дейін әрқашан бас тартуға болады.", + "Already a member?": "Аккаунт бар ма?", + "An error occurred": "", + "An unexpected error occured. Please try again or contact support if the error persists.": "Күтпеген қате пайда болды. Қайта көріңіз немесе қате жалғаса берсе қолдауға хабарласыңыз.", + "Back": "Қайту", + "Back to Log in": "Кіру бетіне қайту", + "Billing info": "Төлем туралы ақпарат", + "Black Friday": "Қара Жұма", + "Cancel anytime.": "Кез келген уақытта бас тартуға болады.", + "Cancel subscription": "Жазылымнан бас тарту", + "Cancellation reason": "Бас тарту себебі", + "Change": "Өзгерту", + "Change plan": "", + "Check spam & promotions folders": "Спам және жарнамалар қалталарын тексеріңіз", + "Check with your mail provider": "Пошта провайдері арқылы тексеріңіз", + "Check your inbox to verify email update": "", + "Choose": "Таңдау", + "Choose a different plan": "Басқа жоспарды таңдау", + "Choose a plan": "", + "Choose your newsletters": "Ақпараттық бюллетеньдерді таңдау", + "Click here to retry": "Қайта көру үшін мұнда басыңыз", + "Close": "Жабу", + "Comments": "Пікірлер", + "Complimentary": "Ақысыз", + "Confirm": "Растау", + "Confirm cancellation": "Бас тартуды растау", + "Confirm subscription": "Жазылымды растау", + "Contact support": "Қолдау бөліміне хабарласу", + "Continue": "Жалғастыру", + "Continue subscription": "Жазылымды жалғастыру", + "Could not create stripe checkout session": "", + "Could not sign in. Login link expired.": "Кіру мүмкін болмады. Кіру сілтемесінің мерзімі өтіп кетті.", + "Could not update email! Invalid link.": "Email поштаны жаңарту мүмкін болмады! Жарамсыз сілтеме.", + "Create a new contact": "Жаңа контакт құру", + "Current plan": "Ағымдағы жоспар", + "Delete account": "Аккаунтты жою", + "Didn't mean to do this? Manage your preferences .": "Мұны жасағыңыз келмеді ме? Теңшелімдерді басқарыңыз.", + "Don't have an account?": "Аккаунтыңыз жоқ па?", + "Edit": "Өңдеу", + "Email": "Email", + "Email newsletter": "Email бюллетені", + "Email newsletter settings updated": "", + "Email preferences": "Email теңшелімдері", + "Emails": "Электрондық хаттар", + "Emails disabled": "Электрондық хаттар өшірілген", + "Ends {{offerEndDate}}": "{{offerEndDate}} аяқталады", + "Enter your email address": "", + "Enter your name": "", + "Error": "Қате", + "Expires {{expiryDate}}": "{{expiryDate}} аяқталады", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", + "Forever": "Үздіксіз", + "Free Trial – Ends {{trialEnd}}": "Тегін сынақ мерзімі – {{trialEnd}} аяқталады", + "Get help": "Көмек алу", + "Get in touch for help": "Көмек алу үшін байланысу", + "Get notified when someone replies to your comment": "Біреу пікіріңізге жауап берген кезде хабарландыру алу", + "Give feedback on this post": "Осы жазбаға кері байланыс беру", + "Help! I'm not receiving emails": "Көмек! Маған электрондық хаттар келмеді", + "Here are a few other sites you may enjoy.": "Сізге ұнауы мүмкін басқа да сайттар.", + "If a newsletter is flagged as spam, emails are automatically disabled for that address to make sure you no longer receive any unwanted messages.": "Егер ақпараттық бюллетень спам ретінде белгіленсе, қажетсіз хабарламаларды енді алмас үшін электрондық хаттар автоматты түрде сол мекенжай үшін өшіріледі.", + "If the spam complaint was accidental, or you would like to begin receiving emails again, you can resubscribe to emails by clicking the button on the previous screen.": "Егер спам шағымы кездейсоқ болса немесе сіз қайтадан электрондық хаттарды алғыңыз келсе, алдыңғы экрандағы түймені басып, қайта жазылуға болады.", + "If you cancel your subscription now, you will continue to have access until {{periodEnd}}.": "Егер жазылымнан қазір бас тартсаңыз, қолжетімділік {{periodEnd}} мерзіміне дейін сақталады.", + "If you have a corporate or government email account, reach out to your IT department and ask them to allow emails to be received from {{senderEmail}}": "Егер сізде корпоративтік немесе үкіметтік электрондық пошта аккаунты болса, IT бөліміне хабарласып, {{senderEmail}} мекенжайынан хаттарды қабылдауға рұқсат беруін сұраңыз.", + "If you would like to start receiving emails again, the best next steps are to check your email address on file for any issues and then click resubscribe on the previous screen.": "Егер қайтадан электрондық хаттарды алғыңыз келсе, ең абзалы электрондық пошта мекенжайыңызда бәрі дұрыстығын тексеру және алдыңғы экрандағы қайта жазылу түймесін басу.", + "If you're not receiving the email newsletter you've subscribed to, here are a few things to check.": "Егер сіз жазылған электрондық бюллетень келмей жатса, тексеру керек бірнеше нәрсе бар.", + "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "Егер тексеру керек барлық нәрсені жасап, хаттар әлі де келмесе, {{supportAddress}} мекенжайына хабарласып, көмекке жүгініңіз.", + "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "Ақпараттық бюллетеньді жіберу сәтінде тұрақты қате шықса, аккаунттағы хаттар өшіріледі.", + "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "Электрондық пошта клиентінде {{senderEmail}} адресін контактілер тізіміне қосыңыз. Бұл провайдерге осы адрестен жіберілген хаттардың сенімді екенін көрсетеді.", + "Invalid email address": "", + "Jamie Larson": "", + "jamie@example.com": "", + "Less like this": "Мұндайдан азырақ", + "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "Хаттардың кездейсоқ Спам немесе Жарнамалар қалтасына түсіп кетпеуін қадағалаңыз. Егер олар сол жерге түссе, \"Спам емес\" және/немесе \"Кіріс жәшігіне көшіру\" түймесін басыңыз.", + "Manage": "Басқару", + "Maybe later": "Мүмкін кейінірек", + "Memberships unavailable, contact the owner for access.": "Аккаунт қосу мүмкін емес, қолжетімділік үшін иесіне хабарласыңыз.", + "month": "", + "Monthly": "Ай сайын", + "More like this": "Мұндайдан көбірек", + "Name": "Есімі", + "Need more help? Contact support": "Қосымша көмек керек пе? Қолдау бөліміне хабарласыңыз", + "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Ақпараттық бюллетеньдер аккаунтыңызда екі себеп бойынша өшірілуі мүмкін: Алдыңғы хат спам ретінде белгіленген немесе хат жіберу әрекеті тұрақты қате (қайту) шыққан.", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", + "Not receiving emails?": "Хаттар келмей жатыр ма?", + "Now check your email!": "Енді электрондық поштаңызды тексеріңіз!", + "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Қайта жазылғаннан кейін, егер әлі де кіріс жәшігіңізде хаттар көрінбесе, спам қалтасын тексеріңіз. Кейбір пошта провадерлері бұрынғы спам шағымдарын сақтап, хаттарды белгілеуін жалғастырады. Егер солай болса, соңғы ақпараттық бюллетеньді \"Спам емес\" деп белгілеңіз және оны негізгі кіріс жәшігіне жылжытыңыз.", + "Permanent failure (bounce)": "Тұрақты қате (қайту)", + "Phone number": "", + "Plan": "Жоспар", + "Plan checkout was cancelled.": "Жоспарды сатып алу тоқтатылды.", + "Plan upgrade was cancelled.": "Жоспарды жаңарту тоқтатылды.", + "Please contact {{supportAddress}} to adjust your complimentary subscription.": "Ақысыз жазылымды реттеу үшін {{supportAddress}} мекенжайына хабарласыңыз.", + "Please enter {{fieldName}}": "", + "Please fill in required fields": "Қажет жерлерді толтырыңыз", + "Price": "Бағасы", + "Re-enable emails": "Электрондық хаттарды қайта қосу", + "Recommendations": "Ұсыныстар", + "Renews at {{price}}.": "{{price}} бағасымен жаңартылады.", + "Retry": "Қайта көру", + "Save": "Сақтау", + "Send an email and say hi!": "Электрондық хат жіберіп, сәлем жолдаңыз!", + "Send an email to {{senderEmail}} and say hello. This can also help signal to your mail provider that emails to and from this address should be trusted.": "{{senderEmail}} мекенжайына электрондық хат жіберіп, сәлем жолдаңыз. Бұл сондай-ақ, пошта провайдеріне осы мекенжай бойынша кіріс-шығыс хаттардың сенімді екенін көрсетеді.", + "Sending login link...": "Кіру сілтемесі жіберілуде...", + "Sending...": "Жіберілуде...", + "Show all": "Барлығын көрсету", + "Sign in": "Кіру", + "Sign out": "Шығу", + "Sign up": "Тіркелу", + "Signup error: Invalid link": "Тіркелу қатесі: Жарамсыз сілтеме", + "Something went wrong, please try again later.": "", + "Sorry, that didn’t work.": "Өкінішті, бұдан ештеңе шықпады.", + "Spam complaints": "Спам шағымдары", + "Start {{amount}}-day free trial": "{{amount}} күндік тегін сынақты бастау", + "Starting {{startDate}}": "{{startDate}} бастап", + "Starting today": "Бүгіннен бастап", + "Submit feedback": "Кері байланыс жіберу", + "Subscribe": "Жазылу", + "Subscribed": "Жазылған", + "Subscription plan updated successfully": "", + "Success": "Тамаша", + "Success! Check your email for magic link to sign-in.": "Тамаша! Кіруге арналған ғажайып сілтемені электрондық поштаңыздан қараңыз.", + "Success! Your account is fully activated, you now have access to all content.": "Тамаша! Аккаунтыңыз толық іске қосылды, енді сізге барлық мазмұн қолжетімді.", + "Success! Your email is updated.": "Тамаша! Электрондық поштаңыз жаңартылды.", + "Successfully unsubscribed": "Жазылымнан бас тарту сәтті орындалды", + "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "Жазылғаныңызға рахмет. Оқуды бастамас бұрын, төменде сізге ұнауы мүмкін басқа да сайттар бар.", + "Thank you for your support": "", + "Thank you for your support!": "", + "Thanks for the feedback!": "Кері байланыс үшін рахмет!", + "That didn't go to plan": "Бірнәрсе қате болды", + "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Сіздің электрондық пошта мекенжайыңыз — {{memberEmail}} — егер бұл қате болса, оны жаңарта аласыз.", + "There was a problem submitting your feedback. Please try again a little later.": "Кері байланысты жіберу кезінде олқылық пайда болды. Кейінірек қайта көріңіз.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", + "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", + "This site is invite-only, contact the owner for access.": "Бұл сайтқа тек шақырту бойынша кіруге болады, рұқсат алу үшін иесіне хабарласыңыз.", + "This site is not accepting payments at the moment.": "", + "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "іркелуді аяқтау үшін, кіріс жәшігіңіздегі растау сілтемесін басыңыз. Егер ол 3 минут ішінде келмесе, спам қалтасын тексеріңіз!", + "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Try free for {{amount}} days, then {{originalPrice}}.": "{{amount}} күн тегін қолданып көріңіз, содан кейінгі бағасы {{originalPrice}}.", + "Unable to initiate checkout session": "", + "Unlock access to all newsletters by becoming a paid subscriber.": "Ақылы түрде жазылу арқылы барлық ақпараттық бюллетеньдерге қол жеткізіңіз.", + "Unsubscribe from all emails": "Барлық хаттарға жазылымнан бас тарту", + "Unsubscribed": "Жазылымнан бас тартылды", + "Unsubscribed from all emails.": "Барлық хаттарға жазылымынан бас тартылды", + "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "Хаттарға жазылымнан бас тарту {{title}} үшін төленген жазылымыңызды жоймайды", + "Update": "Жаңарту", + "Update your preferences": "Таңдауларыңызды жаңарту", + "Verification link sent, check your inbox": "Растау сілтемесі жіберілді, кіріс жәшігіңізді тексеріңіз", + "Verify your email address is correct": "Email адресінің дұрыстығын тексеріңіз", + "View plans": "Жоспарларды көру", + "We couldn't unsubscribe you as the email address was not found. Please contact the site owner.": "Біз сізді жазылымнан айыра алмадық, өйткені электрондық пошта мекенжайы табылмады. Сайт иесіне хабарласыңыз.", + "Welcome back, {{name}}!": "Қош келдіңіз, {{name}}!", + "Welcome back!": "Қош келдіңіз!", + "Welcome to {{siteTitle}}": "{{siteTitle}} сайтына қош келдіңіз", + "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "Кіріс жәшігі хатты қабылдай алмаса, бұл әдетте \"қайту\" деп аталады. Көп жағдайда бұл уақытша нәрсе. Алайда электрондық пошта мекенжайы жарамсыз я жоқ болған жағдайда қайтқан хат тұрақты қате ретінде танылуы мүмкін.", + "Why has my email been disabled?": "Менің электрондық поштам неге өшірілді?", + "year": "", + "Yearly": "Жыл сайын", + "You currently have a free membership, upgrade to a paid subscription for full access.": "Сізде әзірге тегін аккаунт бар, оның мүмкіндіктерін толық пайдалана алу үшін ақылы жазылымға жаңартыңыз.", + "You have been successfully resubscribed": "Қайта жазылу сәтті орындалды", + "You're currently not receiving emails": "Сізге әзірге электрондық хаттар келіп жатқан жоқ", + "You're not receiving emails": "Сізге электрондық хаттар келіп жатқан жоқ", + "You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "Сізге электрондық хаттар келіп жатқан жоқ, себебі жақында келген хат спам ретінде белгіленген немесе хабарламаларды сіз көрсеткен электрондық пошта мекенжайына жеткізу мүмкін емес.", + "You've successfully signed in.": "Кіру сәтті орындалды.", + "You've successfully subscribed to": "Жазылым сәтті орындалды", + "Your account": "Сіздің аккаунт", + "Your email has failed to resubscribe, please try again": "", + "Your input helps shape what gets published.": "Сіздің пікіріңіз жарияланатын мазмұнды қалыптастыруға көмектеседі.", + "Your subscription will expire on {{expiryDate}}": "Сіздің жазылым {{expiryDate}} күні аяқталады", + "Your subscription will renew on {{renewalDate}}": "Сіздің жазылым {{renewalDate}} күні қайта жаңартылады", + "Your subscription will start on {{subscriptionStart}}": "Сіздің жазылым {{subscriptionStart}} күні басталады" +} diff --git a/ghost/i18n/locales/kz/search.json b/ghost/i18n/locales/kz/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/kz/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/kz/signup-form.json b/ghost/i18n/locales/kz/signup-form.json new file mode 100644 index 00000000000..3c07f6033ff --- /dev/null +++ b/ghost/i18n/locales/kz/signup-form.json @@ -0,0 +1,9 @@ +{ + "Email sent": "Хат жіберілді", + "Now check your email!": "Енді email поштаңызды тексеріңіз!", + "Please enter a valid email address": "Жарамды email адресін енгізіңіз", + "Something went wrong, please try again.": "Қателік орын алды, қайтадан көріңіз.", + "Subscribe": "Жазылу", + "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Тіркеуді аяқтау үшін кіріс жәшігіңіздегі растау сілтемесін басыңыз. Егер ол 3 минут ішінде келмесе, спам қалтасын тексеріңіз!", + "Your email address": "Сіздің email адресіңіз" +} diff --git a/ghost/i18n/locales/lt/comments.json b/ghost/i18n/locales/lt/comments.json index 53232d2e001..6393569ad6e 100644 --- a/ghost/i18n/locales/lt/comments.json +++ b/ghost/i18n/locales/lt/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Atsakyti į komentarą", "Report": "Pranešti", "Report comment": "Pranešti apie komentarą", - "Report this comment": "Pranešti apie šį komentarą", "Report this comment?": "Pranešti apie šį komentarą?", "Save": "Išsaugoti", "Sending": "Siunčiama", @@ -68,6 +67,5 @@ "This comment has been removed.": "Šis komentaras buvo pašalintas.", "Upgrade now": "Prenumeruokite", "Yesterday": "Vakar", - "You want to report this comment?": "Ar norite pranešti apie šį komentarą?", "Your request will be sent to the owner of this site.": "Jūsų pranešimas bus išsiųstas svetainės savininkui." } diff --git a/ghost/i18n/locales/lt/portal.json b/ghost/i18n/locales/lt/portal.json index 0cb6230278a..ac0720d777d 100644 --- a/ghost/i18n/locales/lt/portal.json +++ b/ghost/i18n/locales/lt/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Prisijungimo nuoroda buvo išsiųsta į jūsų el. pašto dėžutę. Jei laiškas neatkeliauja per 3 minutes, patikrinkite šlamšto (spam) aplanką.", "Account": "Paskyra", + "Account details updated successfully": "", "Account settings": "Paskyros nustatymai", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Pasibaigus nemokamam bandomajam laikotarpiui, bus nuskaičiuota įprasta pasirinkto prenumeratos tipo kaina. Bet kuriuo metu galite atšaukti prenumeratą.", "Already a member?": "Jau turite paskyrą?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Įvyko klaida. Bandykite dar kartą arba susisiekite su palaikymo komanda, jei klaida išlieka.", "Back": "Atgal", "Back to Log in": "Sugrįžti į prisijungimą", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Patikrinkite šlamšto (spam) ir reklamos aplankus", "Check with your mail provider": "Pasitarkite su savo el. pašto paslaugų teikėju", + "Check your inbox to verify email update": "", "Choose": "Pasirinkite", "Choose a different plan": "Pasirinkite kitą planą", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Susisiekti su pagalba", "Continue": "Tęsti", "Continue subscription": "Pratęsti prenumeratą", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Nepavyko prijungti. Prisijungimo nuoroda nebegalioja.", "Could not update email! Invalid link.": "Nepavyko atnaujinti el. pašto adreso! Negaliojanti nuoroda.", "Create a new contact": "Sukurti naują kontaktą", @@ -52,6 +56,7 @@ "Edit": "Redaguoti", "Email": "El. paštas", "Email newsletter": "Naujienlaiškis el. paštu", + "Email newsletter settings updated": "", "Email preferences": "El. pašto nustatymai", "Emails": "Laiškai", "Emails disabled": "El. laiškai deaktyvuoti", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Klaida", "Expires {{expiryDate}}": "Nustoja galioti {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Neribotam laikui", "Free Trial – Ends {{trialEnd}}": "Nemokamas bandomasis laikotarpis – Baigiasi {{trialEnd}}", "Get help": "Gauti pagalbos", @@ -91,6 +108,8 @@ "Name": "Vardas", "Need more help? Contact support": "Reikia daugiau pagalbos? Susisiekite", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Naujienlaiškiai gali būti išjungti paskyroje dėl dviejų priežasčių: ankstesnis el. laiškas buvo pažymėtas kaip šlamštas arba bandymai išsiųsti el. laišką buvo nuolatos atmetami.", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Negaunate el. laiškų?", "Now check your email!": "Dabar patikrinkite savo el. paštą", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Jei užsiprenumeravę iš naujo nematote el. laiškų gautuosiuose, patikrinkite šlamšto aplanką. Kai kurie el. paslaugų teikėjai registruoja ankstesnius laiškų žymėjimus dėl šlamšto ir toliau juos žymi. Jei taip atsitiks, pažymėkite naujausią gautą laišką kaip „Ne šlamštą“, kad grąžintumėte jį į pagrindinių gautųjų sąrašą.", @@ -126,6 +145,7 @@ "Submit feedback": "Pateikti atsiliepimą", "Subscribe": "Prenumeruoti", "Subscribed": "Užsiprenumeruota", + "Subscription plan updated successfully": "", "Success": "Viskas pavyko", "Success! Check your email for magic link to sign-in.": "Viskas pavyko! Dabar savo el. pašto dėžutėje ieškokite specialios nuorodos prisijungimui. ", "Success! Your account is fully activated, you now have access to all content.": "Pavyko! Jūsų paskyra pilnai aktyvuota, nuo šiol turite prieigą prie specialaus turinio.", @@ -138,12 +158,22 @@ "That didn't go to plan": "Kažkas nepavyko", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Nustatytas El. pašto adresas yra {{memberEmail}} – jei jis neteisingas, galite jį atnaujinti .", "There was a problem submitting your feedback. Please try again a little later.": "Pateikiant atsiliepimą iškilo problema. Bandykite dar kartą vėliau.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Ši svetainė pasiekiama tik su pakvietimu, susisiekite su savininku dėl prieigos. ", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Jei norite užbaigti registraciją, gautame el. laiške spustelėkite patvirtinimo nuorodą. Jei laiško negaunate per 3 minutes, patikrinkite šlamšto (spam) aplanką!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Išbandykite {{amount}} d. nemokamai, vėliau {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Gaukite prieigą prie visų naujienlaiškių įsigiję mokamą prenumeratą.", "Unsubscribe from all emails": "Atšaukti visas laiškų prenumeratas.", "Unsubscribed": "Nebeprenumeruojama", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Sėkmingai prisijungėte.", "You've successfully subscribed to": "Sėkmingai užsiprenumeravote", "Your account": "Jūsų paskyra", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Jūsų indėlis padeda kurti tai, kas yra viešinama.", "Your subscription will expire on {{expiryDate}}": "Jūsų prenumerata baigsis {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Jūsų prenumerata atsinaujins {{renewalDate}}", diff --git a/ghost/i18n/locales/lt/search.json b/ghost/i18n/locales/lt/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/lt/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/mk/comments.json b/ghost/i18n/locales/mk/comments.json index 03bc9ee6fc9..888d4f4fe6a 100644 --- a/ghost/i18n/locales/mk/comments.json +++ b/ghost/i18n/locales/mk/comments.json @@ -2,8 +2,8 @@ "{{amount}} characters left": "Преостануваат {{amount}} знаци.", "{{amount}} comments": "{{amount}} коментари", "{{amount}} days ago": "Пред {{amount}} дена", - "{{amount}} hours ago": "Пред {{amount}} часа", - "{{amount}} minutes ago": "Пред {{amount}} минути", + "{{amount}} hrs ago": "", + "{{amount}} mins ago": "", "{{amount}} months ago": "Пред {{amount}} месеци", "{{amount}} more": "Уште {{amount}}", "{{amount}} seconds ago": "Пред {{amount}} секунди", @@ -25,7 +25,7 @@ "Discussion": "Разговор", "Edit": "Изменете", "Edit this comment": "Изменете го овој коментар", - "Edited": "Изменето", + "edited": "", "Enter your name": "Внесете го вашето име", "Expertise": "Експертиза", "Founder @ Acme Inc": "Основач @ Примерна Компанија", @@ -42,7 +42,7 @@ "Neurosurgeon": "Неврохирург", "One day ago": "Пред еден ден", "One hour ago": "Пред еден час", - "One minute ago": "Пред една минута", + "One min ago": "", "One month ago": "Пред еден месец", "One week ago": "Пред една недела", "One year ago": "Пред една година", @@ -50,7 +50,6 @@ "Reply to comment": "Одговор на коментарот", "Report": "Пријавете", "Report comment": "Пријавете коментар", - "Report this comment": "Пријавете го овој коментар", "Report this comment?": "Пријавете го овој коментар?", "Save": "Зачувајте", "Sending": "Се испраќа", @@ -68,6 +67,5 @@ "This comment has been removed.": "Овој коментар беше отстранет.", "Upgrade now": "Надградете", "Yesterday": "Вчера", - "You want to report this comment?": "Дали сакате да го пријавите овој коментар?", "Your request will be sent to the owner of this site.": "Вашето барање ќе биде испратено на сопственикот на оваа страница." } diff --git a/ghost/i18n/locales/mk/portal.json b/ghost/i18n/locales/mk/portal.json index da03650d777..9077f77748d 100644 --- a/ghost/i18n/locales/mk/portal.json +++ b/ghost/i18n/locales/mk/portal.json @@ -10,11 +10,14 @@ "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "{{memberEmail}} нема веќе да добива пораки кога некој ќе одговори на вашиот коментар.", "{{memberEmail}} will no longer receive this newsletter.": "{{memberEmail}} нема веќе да го добива овој билтен.", "{{trialDays}} days free": "{{trialDays}} денови бесплатно", + "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Линк за најава беше испратен кон вашиот email. Ако не пристигне за повеќе од 3 минути, проверете во спам.", "Account": "Сметка", + "Account details updated successfully": "", "Account settings": "Поставки", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Откако пробниот период ќе истече, ќе ви биде наплатена регуларната цена за пакетот кој сте го избрале. Секогаш можете да откажете претходно.", "Already a member?": "Веќе сте член", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Настана неочекувана грешка. Ве молиме обидете се повторно или контактирајте ја подршката ако грешката се повторува.", "Back": "Назад", "Back to Log in": "Назад кон најава", @@ -24,10 +27,13 @@ "Cancel subscription": "Откажете претплата", "Cancellation reason": "Причина за откажување", "Change": "Променете", + "Change plan": "", "Check spam & promotions folders": "Проверете спам и промотивни папки", "Check with your mail provider": "Проверете со вашиот mail сервис", + "Check your inbox to verify email update": "", "Choose": "Изберете", "Choose a different plan": "Изберете друг пакет", + "Choose a plan": "", "Choose your newsletters": "Изберете ги вашите билтени", "Click here to retry": "Кликнете тука за да се обидите повторно", "Close": "Затворете", @@ -39,6 +45,7 @@ "Contact support": "Контактирајте подршка", "Continue": "Продолжете", "Continue subscription": "Продолжете ја претплатата", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Не можете да се најавите. Линкот за најава истече.", "Could not update email! Invalid link.": "Не може да се промени email адресата! Линкот е невалиден.", "Create a new contact": "Создајте нов контакт", @@ -49,12 +56,27 @@ "Edit": "Променете", "Email": "Email", "Email newsletter": "Email билтен", + "Email newsletter settings updated": "", "Email preferences": "Email поставки", "Emails": "Електронски пошти", "Emails disabled": "Оневозможени електронски пошти", "Ends {{offerEndDate}}": "Завршува на {{offerEndDate}}", + "Enter your email address": "", + "Enter your name": "", "Error": "Грешка", "Expires {{expiryDate}}": "Истекува на {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Засекогаш", "Free Trial – Ends {{trialEnd}}": "Пробниот период - Истекува на {{trialEnd}}", "Get help": "Побарајте помош", @@ -72,24 +94,32 @@ "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "Доколку сте ги завршиле сите овие проверки и сè уште не добивате пораки, контактирајте на {{supportAddress}} за да добиете подршка.", "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "Во случај кога настане траен неуспех при обид за испраќање на билтен, email пораките ќе бидат оневозможени за сметката.", "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "Додајте го {{senderEmail}} во листата на контакти од вашиот email сервис додајте. Ова ќе му сигналира на вашиот email сервис дека пораките добиени од оваа email адреса треба да бидат прифатени.", + "Invalid email address": "", + "Jamie Larson": "", + "jamie@example.com": "", "Less like this": "Помалку како ова", "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "Осигурајте се дека пораките не завршуваат несакано во папките за спам или промоции од вашиот email. Доколку тоа се случува, обележете ги дека не се спам и префрлете ги во главното поштенско сандаче.", "Manage": "Управувајте", "Maybe later": "Можеби подоцна", "Memberships unavailable, contact the owner for access.": "Зачленувањето не е достапно. За пристап контактирајте го сопственикот.", + "month": "", "Monthly": "Месечно", "More like this": "Повеќе како ова", "Name": "Име", "Need more help? Contact support": "Ви треба помош? Контактирајте подршка", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Билтените можат да бидат оневозможени за вашата сметка поради две причини: претходна порака била обележана како спам, или обидот за исппраќање на порака резултирал со траен неуспех (отскокнување).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Не добивате пораки?", "Now check your email!": "Сега проверете ги пораките1", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Кога ќе се претплатите повторно, доколку сè уште не добивате пораки, проверте ја папката за спам. Некои сервиси чуваат записи од претходни поплаки за спам и продолжуваат да ги обележуваат пораките како такви. Ако ова се случи, обележете ја последната порака од билтенот дека не е спам и префрлете ја во главното поштенско сандаче.", "Permanent failure (bounce)": "Траен неуспех (отскокнување)", + "Phone number": "", "Plan": "Пакет", "Plan checkout was cancelled.": "Плаќањето на пакетот беше откажано.", "Plan upgrade was cancelled.": "Надградбата на пакетот беше откажана.", "Please contact {{supportAddress}} to adjust your complimentary subscription.": "Ве молиме контактирајте на {{supportAddress}} за прилагодување на вашата додатна претплата.", + "Please enter {{fieldName}}": "", "Please fill in required fields": "Ве молиме пополнете ги задолжителните полиња", "Price": "Цена", "Re-enable emails": "Овозможете пораки", @@ -106,6 +136,7 @@ "Sign out": "Одјавете се", "Sign up": "Регистрирајте се", "Signup error: Invalid link": "Грешка при регистрација: Невалиден линк", + "Something went wrong, please try again later.": "", "Sorry, that didn’t work.": "Се извинуваме, тоа не проработи.", "Spam complaints": "Поплаки за спам", "Start {{amount}}-day free trial": "Започнете {{amount}} дневен пробен период", @@ -114,19 +145,35 @@ "Submit feedback": "Испратете забелешка", "Subscribe": "Претплатете се", "Subscribed": "Претплатено", + "Subscription plan updated successfully": "", "Success": "Успех", "Success! Check your email for magic link to sign-in.": "Успех! Проверете го вашиот email за линк за најава.", "Success! Your account is fully activated, you now have access to all content.": "Успех! Вашата сметка е целосно активирана и сега имате пристап до целата содржина.", "Success! Your email is updated.": "Успех! Вашиот email е ажуриран.", "Successfully unsubscribed": "Успешно се отпишавте", "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "Ви благодариме за претплатувањето. Пред да започнете со читање, подолу се неколку други страници кои може да ви се допаднат.", + "Thank you for your support": "", + "Thank you for your support!": "", "Thanks for the feedback!": "Ви благодариме за забелешката!", "That didn't go to plan": "Тоа се случи според планот!", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Email адресата која ја имаме за вас е {{memberEmail}} - ако ова не е точна адреса, можете да ја ажурирате во на сметката.", "There was a problem submitting your feedback. Please try again a little later.": "Настана проблем при испраќање на вашата забелешка. Обидете се повторно малку подоцна.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", + "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Оваа страница е достапна само со покана. За пристап контактирајте го сопственикот.", + "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "За да ја завршите регистрацијата, кликнете на линкот за потврда испратен на вашата email адреса. Ако не пристигне во рок од 3 минути, проверте во спам!", + "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Пробајте бесплатно за {{amount}} денови, потоа по цена од {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Добијте пристап до сите билтени преку платена претплата.", "Unsubscribe from all emails": "Отпишете се од сите пораки", "Unsubscribed": "Отпишано", @@ -143,6 +190,7 @@ "Welcome to {{siteTitle}}": "Добредојдовте на {{siteTitle}}", "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "Кога email адреса не успева да добие порака, тоа се нарекува отскокнување. Во повеќе случаи, ова може да биде привремено. Но, во некои случаи, отскокнувањето може да биде вратено како траен неуспех кога email адресата е невалидна или непостоечка.", "Why has my email been disabled?": "Зошто мојата email адреса беше оневозможена?", + "year": "", "Yearly": "Годишно", "You currently have a free membership, upgrade to a paid subscription for full access.": "Моментално имате бесплатно членство. Надградете на платена претплата за целосен пристап.", "You have been successfully resubscribed": "Бевте успешно претплатени повторно", @@ -152,8 +200,9 @@ "You've successfully signed in.": "Успешно се најавивте.", "You've successfully subscribed to": "Успешно се претплативте на", "Your account": "Вашата сметка", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Вашиот прилог помага да се оформи она кое ќе биде објавувано.", "Your subscription will expire on {{expiryDate}}": "Вашата претплата ќе истече на {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Вашата претплата ќе биде обновена на {{renewalDate}}", - "Your subscription will start on {{subscriptionStart}}": "Вашата претплата ќе започне на {subscriptionStart}}" + "Your subscription will start on {{subscriptionStart}}": "Вашата претплата ќе започне на {{subscriptionStart}}" } diff --git a/ghost/i18n/locales/mk/search.json b/ghost/i18n/locales/mk/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/mk/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/mn/comments.json b/ghost/i18n/locales/mn/comments.json index c5391eab775..77ef1db9693 100644 --- a/ghost/i18n/locales/mn/comments.json +++ b/ghost/i18n/locales/mn/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "", "Report": "", "Report comment": "", - "Report this comment": "", "Report this comment?": "", "Save": "", "Sending": "", @@ -68,6 +67,5 @@ "This comment has been removed.": "", "Upgrade now": "", "Yesterday": "", - "You want to report this comment?": "", "Your request will be sent to the owner of this site.": "" } diff --git a/ghost/i18n/locales/mn/portal.json b/ghost/i18n/locales/mn/portal.json index 3eb2f4c8c0e..931e505b20b 100644 --- a/ghost/i18n/locales/mn/portal.json +++ b/ghost/i18n/locales/mn/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Таны имэйл рүү нэвтрэх холбоосыг илгээлээ. Хэрвээ 3 минутын дотор ирэхгүй бол спамаа шалгана уу.", "Account": "Бүртгэл", + "Account details updated successfully": "", "Account settings": "Бүртгэлийн тохиргоо", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Үнэгүй туршилтын хугацаа дуусахад таны данснаас сонгосон багцын үнэ хасагдана. Гэвч та өмнө нь цуцлах боломжтой.", "Already a member?": "Бүртгэлтэй юу?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "", "Back": "Буцах", "Back to Log in": "Нэвтрэх хэсэг рүү буцах", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "", "Check with your mail provider": "", + "Check your inbox to verify email update": "", "Choose": "", "Choose a different plan": "Өөр багц сонгох", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "", "Continue": "Үргэлжлүүлэх", "Continue subscription": "", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "", "Could not update email! Invalid link.": "", "Create a new contact": "", @@ -52,6 +56,7 @@ "Edit": "", "Email": "Имэйл", "Email newsletter": "", + "Email newsletter settings updated": "", "Email preferences": "Имэйлийн тохиргоо", "Emails": "Имэйлүүд", "Emails disabled": "Имэйлийг идэхгүй болгосон", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "", "Expires {{expiryDate}}": "", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "", "Free Trial – Ends {{trialEnd}}": "", "Get help": "Тусламж", @@ -91,6 +108,8 @@ "Name": "Нэр", "Need more help? Contact support": "", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Имэйл ирээгүй юу?", "Now check your email!": "Одоо имэйлээ шалгана уу!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "", @@ -126,6 +145,7 @@ "Submit feedback": "Саналаа илгээх", "Subscribe": "", "Subscribed": "", + "Subscription plan updated successfully": "", "Success": "", "Success! Check your email for magic link to sign-in.": "", "Success! Your account is fully activated, you now have access to all content.": "", @@ -138,12 +158,22 @@ "That didn't go to plan": "", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "", "There was a problem submitting your feedback. Please try again a little later.": "", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Энэхүү сайт руу зөвхөн урилгаар нэвтрэх боломжтой тул та админд нь хандана уу.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Тан руу илгээсэн баталгаажуулах холбоос дээр дарж бүртгэлээ дуусгана уу. Хэрвээ 3 минутын дотор ирэхгүй бол спамаа шалгана уу!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "", "Unsubscribe from all emails": "Бүх имэйлийг зогсоох", "Unsubscribed": "", @@ -170,6 +200,7 @@ "You've successfully signed in.": "", "You've successfully subscribed to": "", "Your account": "Таны бүртгэл", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Таны санал дараа дараагийн нийтлэлийг илүү чанартай болгоход туслана", "Your subscription will expire on {{expiryDate}}": "", "Your subscription will renew on {{renewalDate}}": "", diff --git a/ghost/i18n/locales/mn/search.json b/ghost/i18n/locales/mn/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/mn/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/ms/comments.json b/ghost/i18n/locales/ms/comments.json index c5391eab775..77ef1db9693 100644 --- a/ghost/i18n/locales/ms/comments.json +++ b/ghost/i18n/locales/ms/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "", "Report": "", "Report comment": "", - "Report this comment": "", "Report this comment?": "", "Save": "", "Sending": "", @@ -68,6 +67,5 @@ "This comment has been removed.": "", "Upgrade now": "", "Yesterday": "", - "You want to report this comment?": "", "Your request will be sent to the owner of this site.": "" } diff --git a/ghost/i18n/locales/ms/portal.json b/ghost/i18n/locales/ms/portal.json index 788c6c57735..6524962f546 100644 --- a/ghost/i18n/locales/ms/portal.json +++ b/ghost/i18n/locales/ms/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Pautan log masuk telah dihantar ke peti masuk anda. Jika ia tidak sampai dalam masa 3 minit, pastikan anda menyemak folder spam anda.", "Account": "Akaun", + "Account details updated successfully": "", "Account settings": "Tetapan akaun", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Selepas tempoh percubaan percuma tamat, anda akan dicaj harga biasa untuk peringkat yang anda pilih. Anda sentiasa boleh membatalkan sebelum itu.", "Already a member?": "Sudah menjadi ahli?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Ralat yang tidak dijangka berlaku. Sila cuba lagi atau hubungi sokongan jika ralat berterusan.", "Back": "Kembali", "Back to Log in": "Kembali ke Log masuk", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Semak folder spam & promosi", "Check with your mail provider": "Semak dengan pembekal mel anda", + "Check your inbox to verify email update": "", "Choose": "Pilih", "Choose a different plan": "Pilih pelan yang berbeza", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Hubungi sokongan", "Continue": "Teruskan", "Continue subscription": "Teruskan langganan", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Tidak dapat log masuk. Pautan log masuk tamat tempoh.", "Could not update email! Invalid link.": "Tidak dapat mengemas kini e-mel! Pautan tidak sah.", "Create a new contact": "Cipta kenalan baru", @@ -52,6 +56,7 @@ "Edit": "Sunting", "Email": "E-mel", "Email newsletter": "Newsletter e-mel", + "Email newsletter settings updated": "", "Email preferences": "Emel pilihan", "Emails": "E-mel", "Emails disabled": "E-mel dilumpuhkan", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Ralat", "Expires {{expiryDate}}": "Luput pada {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Selamanya", "Free Trial – Ends {{trialEnd}}": "Percubaan Percuma – Tamat {{trialEnd}}", "Get help": "Dapatkan bantuan", @@ -91,6 +108,8 @@ "Name": "Nama", "Need more help? Contact support": "Perlukan bantuan lagi? Hubungi sokongan", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Newsletter boleh dilumpuhkan pada akaun anda atas dua sebab: E-mel sebelumnya telah ditandakan sebagai spam atau percubaan menghantar e-mel mengakibatkan kegagalan kekal (bounce).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Tidak menerima e-mel?", "Now check your email!": "Semak e-mel anda sekarang!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Setelah melanggan semula, jika anda masih tidak melihat e-mel dalam peti masuk anda, semak folder spam anda. Sesetengah pembekal peti masuk menyimpan rekod aduan spam sebelumnya dan akan terus membenderakan e-mel. Jika ini berlaku, tandakan surat berita terkini sebagai 'Bukan spam' untuk mengalihkannya kembali ke peti masuk utama anda.", @@ -126,6 +145,7 @@ "Submit feedback": "Serahkan maklum balas", "Subscribe": "Langgan", "Subscribed": "Dilanggan", + "Subscription plan updated successfully": "", "Success": "Berjaya", "Success! Check your email for magic link to sign-in.": "Berjaya! Semak e-mel anda untuk magic link untuk log masuk.", "Success! Your account is fully activated, you now have access to all content.": "Berjaya! Akaun anda telah diaktifkan sepenuhnya, anda kini mempunyai akses ke semua kandungan.", @@ -138,12 +158,22 @@ "That didn't go to plan": "Itu tidak berjalan sesuai dengan rancangan", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Alamat e-mel yang kami miliki untuk anda adalah {{memberEmail}} — jika itu tidak betul, anda boleh mengemas kini di anda.", "There was a problem submitting your feedback. Please try again a little later.": "Terdapat masalah menghantar maklum balas anda. Sila cuba sebentar lagi.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Laman web ini hanya untuk jemputan, hubungi pemilik untuk akses.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Untuk melengkapkan pendaftaran, klik pautan pengesahan di peti masuk anda. Jika ia tidak tiba dalam masa 3 minit, semak folder spam anda!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Cuba secara percuma selama {{amount}} hari, kemudian {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Buka akses ke semua newsletter dengan menjadi pelanggan berbayar.", "Unsubscribe from all emails": "Berhenti langganan dari semua e-mel", "Unsubscribed": "Langganan diberhentikan", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Anda telah berjaya log masuk.", "You've successfully subscribed to": "", "Your account": "Akaun anda", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Input anda membantu membentuk apa yang diterbitkan.", "Your subscription will expire on {{expiryDate}}": "Langganan anda akan tamat tempoh pada {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Langganan anda akan diperbaharui pada {{renewalDate}}", diff --git a/ghost/i18n/locales/ms/search.json b/ghost/i18n/locales/ms/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/ms/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/nl/comments.json b/ghost/i18n/locales/nl/comments.json index e057409ea53..0da38ac6760 100644 --- a/ghost/i18n/locales/nl/comments.json +++ b/ghost/i18n/locales/nl/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Beantwoord deze reactie", "Report": "Melden", "Report comment": "Meld reactie", - "Report this comment": "Meld deze reactie", "Report this comment?": "Meld deze reactie?", "Save": "Opslaan", "Sending": "Verzenden", @@ -68,6 +67,5 @@ "This comment has been removed.": "Deze reactie is verwijderd.", "Upgrade now": "Upgrade nu", "Yesterday": "Gisteren", - "You want to report this comment?": "Wil je deze reactie melden?", "Your request will be sent to the owner of this site.": "Jouw melding zal worden verzonden naar de eigenaar van deze site." } diff --git a/ghost/i18n/locales/nl/portal.json b/ghost/i18n/locales/nl/portal.json index f2e20dd026a..2c926d2890f 100644 --- a/ghost/i18n/locales/nl/portal.json +++ b/ghost/i18n/locales/nl/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Je hebt een email met een login link ontvangen. Check je spamfolder als hij niet binnen de 3 minuten aankomt.", "Account": "Account", + "Account details updated successfully": "", "Account settings": "Gegevens", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Na de proefperiode zal het normale tarief in rekening worden gebracht voor het door jou gekozen abonnement.", "Already a member?": "Al lid?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "", "Back": "Terug", "Back to Log in": "Terug naar inloggen", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Check je spam folders", "Check with your mail provider": "Check bij je e-mail provider", + "Check your inbox to verify email update": "", "Choose": "Kies", "Choose a different plan": "Kies een ander abonnement", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "", "Continue": "Doorgaan", "Continue subscription": "", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "", "Could not update email! Invalid link.": "", "Create a new contact": "", @@ -52,6 +56,7 @@ "Edit": "Bewerken", "Email": "E-mail", "Email newsletter": "Nieuwsbrief", + "Email newsletter settings updated": "", "Email preferences": "E-mailinstellingen", "Emails": "E-mails", "Emails disabled": "E-mails zijn uitgeschakeld", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Fout", "Expires {{expiryDate}}": "Verloopt op {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Voor altijd", "Free Trial – Ends {{trialEnd}}": "", "Get help": "Lees meer", @@ -91,6 +108,8 @@ "Name": "Naam", "Need more help? Contact support": "", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Ontvang je geen e-mails?", "Now check your email!": "Check nu je e-mail!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "", @@ -126,6 +145,7 @@ "Submit feedback": "Deel je feedback", "Subscribe": "", "Subscribed": "", + "Subscription plan updated successfully": "", "Success": "", "Success! Check your email for magic link to sign-in.": "", "Success! Your account is fully activated, you now have access to all content.": "", @@ -138,12 +158,22 @@ "That didn't go to plan": "Er ging iets mis", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "", "There was a problem submitting your feedback. Please try again a little later.": "", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Deze site is alleen toegankelijk op uitnodiging, neem contact op met de eigenaar.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Klik op de bevestigingslink in de e-mail om je registratie af te ronden. Check ook je spamfolder als hij niet binnen de 3 minuten aankomt.", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "", "Unsubscribe from all emails": "Uitschrijven voor alles", "Unsubscribed": "", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Je bent succesvol ingelogd.", "You've successfully subscribed to": "Je bent succesvol geabonneerd op", "Your account": "Jouw account", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Jouw mening helpt bepalen wat er gepubliceerd wordt.", "Your subscription will expire on {{expiryDate}}": "", "Your subscription will renew on {{renewalDate}}": "", diff --git a/ghost/i18n/locales/nl/search.json b/ghost/i18n/locales/nl/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/nl/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/nn/comments.json b/ghost/i18n/locales/nn/comments.json index c5391eab775..77ef1db9693 100644 --- a/ghost/i18n/locales/nn/comments.json +++ b/ghost/i18n/locales/nn/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "", "Report": "", "Report comment": "", - "Report this comment": "", "Report this comment?": "", "Save": "", "Sending": "", @@ -68,6 +67,5 @@ "This comment has been removed.": "", "Upgrade now": "", "Yesterday": "", - "You want to report this comment?": "", "Your request will be sent to the owner of this site.": "" } diff --git a/ghost/i18n/locales/nn/portal.json b/ghost/i18n/locales/nn/portal.json index 05c3b59bd7c..73ca00d8291 100644 --- a/ghost/i18n/locales/nn/portal.json +++ b/ghost/i18n/locales/nn/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Ei innlogginslenke har blitt sendt til innboksen din. Sjekk søppelposten din om lenka ikkje kjem innan 3 minutt.", "Account": "Brukar", + "Account details updated successfully": "", "Account settings": "Brukarinnstillingar", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Når den gratis prøveperioden er over vil du bli belasta den normale prisen for abonnementet du har vald. Du kan alltids avslutta abonnementet au.", "Already a member?": "Allereie medlem?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Ein uventa feil har skjedd. Ver gild å prøv igjen eller ta kontakt viss feilen fortset.", "Back": "Tilbake", "Back to Log in": "Tilbake til innlogginga", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Sjekk søppelpostmappa di", "Check with your mail provider": "Sjekk med e-postleverandøren din", + "Check your inbox to verify email update": "", "Choose": "Vel", "Choose a different plan": "Vel eit anna abonnement", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Få hjelp", "Continue": "Fortset", "Continue subscription": "Fortset abonnement", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Kunne ikkje logga inn. Innlogginslenka er utgått.", "Could not update email! Invalid link.": "Kunne ikkje oppdatera e-post! Ugyldig lenke.", "Create a new contact": "Lag ein ny kontakt", @@ -52,6 +56,7 @@ "Edit": "Endra", "Email": "E-post", "Email newsletter": "E-post nyheitsbrev", + "Email newsletter settings updated": "", "Email preferences": "E-post preferansar.", "Emails": "E-postar.", "Emails disabled": "E-postar skrudd av", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Feil", "Expires {{expiryDate}}": "Går ut {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "For evig", "Free Trial – Ends {{trialEnd}}": "Gratis prøveperiode – sluttar {{trialEnd}}", "Get help": "Få hjelp", @@ -91,6 +108,8 @@ "Name": "Namn", "Need more help? Contact support": "Treng du meir hjelp? Ta kontakt med brukarstøtte", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Nyheitsbrev kan bli skrudd av for brukaren din av to grunner: Ein tidlegare e-post har blitt markert som spam. Eller så har eit forsøk på å senda ein e-post resultert i ein permanent feil.", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Får du ikkje e-postar?", "Now check your email!": "Sjekk e-posten din!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Sjekk spam-mappa di om du framleis ikkje ser e-postane etter å ha abonnert på ny. Nokon e-postleverandørar kan flagga e-postar som spam basert på tidlegare e-postar. Viss dette skjer, marker den siste du mottok i spam-mappa som 'ikkje spam' og flytt tilbake til innboksen din.", @@ -126,6 +145,7 @@ "Submit feedback": "Gje oss tilbakemeldinger", "Subscribe": "Abonner", "Subscribed": "Abonnert", + "Subscription plan updated successfully": "", "Success": "Vellykka", "Success! Check your email for magic link to sign-in.": "Vellykka! Sjekk e-posten din for ei innlogginslenke.", "Success! Your account is fully activated, you now have access to all content.": "Vellykka! Brukaren din er aktivert, du har no tilgang til alt innhald.", @@ -138,12 +158,22 @@ "That didn't go to plan": "Det gjekk ikkje etter planen", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "E-postadressa me har på deg er {{memberEmail}} – viss det ikkje er riktig, kan du oppdatera i .", "There was a problem submitting your feedback. Please try again a little later.": "Det var eit problem med å senda tilbakemeldinga di. Vennligst prøv igjen litt seinare.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Denne sida er kun for inviterte, ta kontakt med eigaren for tilgang.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Klikk på bekreftelseslenka i innboksen din for å fullføra registreringa. Sjekk spam-mappa di om lenka ikkje har kome innan 3 minutt.", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Prøv gratis i {{amount}} dagar, deretter {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Få tilgang til alle nyheitsbreva med å bli ein betalande abonnent.", "Unsubscribe from all emails": "Slutt å motta e-postar", "Unsubscribed": "Abonnement avslutta", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Vellykka innlogging.", "You've successfully subscribed to": "", "Your account": "Din brukar", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Dine tilbakemeldinger hjelper oss å forma tilbodet vårt.", "Your subscription will expire on {{expiryDate}}": "Ditt abonnement går ut den {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Ditt abonnement vil fornyast den {{renewalDate}}", diff --git a/ghost/i18n/locales/nn/search.json b/ghost/i18n/locales/nn/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/nn/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/no/comments.json b/ghost/i18n/locales/no/comments.json index c5391eab775..77ef1db9693 100644 --- a/ghost/i18n/locales/no/comments.json +++ b/ghost/i18n/locales/no/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "", "Report": "", "Report comment": "", - "Report this comment": "", "Report this comment?": "", "Save": "", "Sending": "", @@ -68,6 +67,5 @@ "This comment has been removed.": "", "Upgrade now": "", "Yesterday": "", - "You want to report this comment?": "", "Your request will be sent to the owner of this site.": "" } diff --git a/ghost/i18n/locales/no/portal.json b/ghost/i18n/locales/no/portal.json index 61123658779..c7c3353864a 100644 --- a/ghost/i18n/locales/no/portal.json +++ b/ghost/i18n/locales/no/portal.json @@ -7,27 +7,30 @@ "{{amount}} off forever.": "{{amount}} rabatt for alltid.", "{{discount}}% discount": "{{discount}}% rabatt", "{{memberEmail}} will no longer receive {{newsletterName}} newsletter.": "{{memberEmail}} vil ikke lengre motta nyhetsbrevet {{newsletterName}}.", - "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "{{memberEmail}} vil ikke lengre motta eposter når noen svarer på dine kommentarer.", + "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "{{memberEmail}} vil ikke lengre motta e-poster når noen svarer på dine kommentarer.", "{{memberEmail}} will no longer receive this newsletter.": "{{memberEmail}} vil ikke lengre motta nyhetsbrevet.", "{{trialDays}} days free": "{{trialDays}} dager gratis", "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "En påloggingslenke har blitt sendt til innboksen din. Hvis den ikke kommer innen 3 minutter, må du sjekke søppelposten din.", "Account": "Konto", + "Account details updated successfully": "", "Account settings": "Kontoinnstillinger", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Etter at prøveperioden er over, vil du bli belastet den vanlige prisen for nivået du har valgt. Du kan alltid avbryte før det.", "Already a member?": "Allerede medlem?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "En uforutsett feil oppstod. Vennligst prøv igjen eller ta kontakt om feilen vedvarer.", "Back": "Tilbake", - "Back to Log in": "Tilbake til logginn", + "Back to Log in": "Tilbake til logg inn", "Billing info": "Fakturainformasjon", "Black Friday": "", "Cancel anytime.": "Ingen bindingstid", - "Cancel subscription": "Avbryt abonnement", - "Cancellation reason": "Grunn til avbryting", + "Cancel subscription": "Kanseller abonnement", + "Cancellation reason": "Årsak til kansellering", "Change": "Endre", "Change plan": "", "Check spam & promotions folders": "Sjekk foldere for søppelpost og reklame", "Check with your mail provider": "Ta kontakt med din epostleverandør", + "Check your inbox to verify email update": "", "Choose": "Velg", "Choose a different plan": "Velg et annet nivå", "Choose a plan": "", @@ -42,44 +45,58 @@ "Contact support": "", "Continue": "Fortsett", "Continue subscription": "Fortsett abonnement", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Kunne ikke logge på. Tiden for lenken har utgått.", - "Could not update email! Invalid link.": "Kunne ikke oppdatere epost! Ugyldig lenke.", + "Could not update email! Invalid link.": "Kunne ikke oppdatere e-post! Ugyldig lenke.", "Create a new contact": "Registrer en ny kontakt", "Current plan": "Gjeldende plan", "Delete account": "Slett konto", "Didn't mean to do this? Manage your preferences .": "Var dett en feil? Du kan endre dine preferanser ", "Don't have an account?": "Har du ikke konto?", "Edit": "Rediger", - "Email": "Epost", - "Email newsletter": "Epostbasert nyhetsbrev", - "Email preferences": "Innstillinger for epost", - "Emails": "Epost", - "Emails disabled": "Epost deaktivert", + "Email": "E-post", + "Email newsletter": "E-postbasert nyhetsbrev", + "Email newsletter settings updated": "", + "Email preferences": "Innstillinger for e-post", + "Emails": "E-poster", + "Emails disabled": "E-poster deaktivert", "Ends {{offerEndDate}}": "Avsluttes {{offerEndDate}}", - "Enter your email address": "", - "Enter your name": "", + "Enter your email address": "Oppgi din e-postadresse", + "Enter your name": "Oppgi ditt navn", "Error": "Feil", "Expires {{expiryDate}}": "Avsluttes {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "For alltid", "Free Trial – Ends {{trialEnd}}": "Gratis prøveperiode – avsluttes {{trialEnd}}", "Get help": "Få hjelp", "Get in touch for help": "Ta kontakt for hjelp", "Get notified when someone replies to your comment": "Få varsel dersom noen svarer på kommentaren din", "Give feedback on this post": "Gi tilbakemelding på dette innlegget", - "Help! I'm not receiving emails": "Hjelp, jeg mottar ikke eposter", + "Help! I'm not receiving emails": "Hjelp, jeg mottar ikke e-poster", "Here are a few other sites you may enjoy.": "Her en noen andre nettsteder du kan like.", - "If a newsletter is flagged as spam, emails are automatically disabled for that address to make sure you no longer receive any unwanted messages.": "", - "If the spam complaint was accidental, or you would like to begin receiving emails again, you can resubscribe to emails by clicking the button on the previous screen.": "", - "If you cancel your subscription now, you will continue to have access until {{periodEnd}}.": "", - "If you have a corporate or government email account, reach out to your IT department and ask them to allow emails to be received from {{senderEmail}}": "", - "If you would like to start receiving emails again, the best next steps are to check your email address on file for any issues and then click resubscribe on the previous screen.": "", - "If you're not receiving the email newsletter you've subscribed to, here are a few things to check.": "", + "If a newsletter is flagged as spam, emails are automatically disabled for that address to make sure you no longer receive any unwanted messages.": "Hvis et nyhetsbrev er flagget som søppelpost, deaktiveres e-poster automatisk for den adressen for å sikre at du ikke lenger mottar uønskede meldinger.", + "If the spam complaint was accidental, or you would like to begin receiving emails again, you can resubscribe to emails by clicking the button on the previous screen.": "Hvis du markerte som søppelpost ved et uhell eller du ønsker å begynne å motta e-poster igjen, kan du abonnere på nytt på e-poster ved å klikke på knappen på forrige skjerm.", + "If you cancel your subscription now, you will continue to have access until {{periodEnd}}.": "Hvis du kansellerer abonnementet ditt nå, vil du fortsette å ha tilgang til {{periodEnd}}.", + "If you have a corporate or government email account, reach out to your IT department and ask them to allow emails to be received from {{senderEmail}}": "Hvis du har en bedrifts-e-postkonto, ta kontakt med IT-avdelingen din og be dem om å tillate at e-poster mottas fra {{senderEmail}}", + "If you would like to start receiving emails again, the best next steps are to check your email address on file for any issues and then click resubscribe on the previous screen.": "Hvis du ønsker å begynne å motta e-poster igjen, sjekk e-postadressen din for eventuelle problemer og deretter klikke på abonner på nytt på forrige skjermbilde.", + "If you're not receiving the email newsletter you've subscribed to, here are a few things to check.": "Hvis du ikke mottar nyhetsbrevet på e-post du har abonnert på, er det noen ting du bør sjekke.", "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "", "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "", "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "", - "Invalid email address": "", - "Jamie Larson": "", - "jamie@example.com": "", + "Invalid email address": "Ugyldig e-postadresse", + "Jamie Larson": "Ola Nordmann", + "jamie@example.com": "ola.nordmann@example.com", "Less like this": "Mindre som dette", "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "", "Manage": "Administrer", @@ -91,24 +108,26 @@ "Name": "Navn", "Need more help? Contact support": "Trenger du mer hjelp? Ta kontakt", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "", - "Not receiving emails?": "Mottar du ikke epost?", - "Now check your email!": "Sjekk eposten din!", - "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Blir eposten markert som søppelpost? Sjekk epostfolderen for søppelpost og merk som 'ikke søppel'.", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", + "Not receiving emails?": "Mottar du ikke e-poster?", + "Now check your email!": "Sjekk e-posten din!", + "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Blir e-poster markert som søppelpost? Sjekk e-postmappen for søppelpost og merk som 'ikke søppel'.", "Permanent failure (bounce)": "Permanent feil (bounce)", - "Phone number": "", + "Phone number": "Telefonnummer", "Plan": "Plan", "Plan checkout was cancelled.": "Påmelding til plan ble kansellert. ", "Plan upgrade was cancelled.": "Oppgradering ble kansellert.", "Please contact {{supportAddress}} to adjust your complimentary subscription.": "Vennligst kontakt {{supportAddress}} for å justere ditt gratis abonnement.", - "Please enter {{fieldName}}": "", - "Please fill in required fields": "Vennligst fyll in påkrevde felt", + "Please enter {{fieldName}}": "Vennligst oppgi {{fieldName}}", + "Please fill in required fields": "Vennligst fyll inn påkrevde felt", "Price": "Pris", - "Re-enable emails": "Re-aktiver epost", + "Re-enable emails": "Re-aktiver e-poster", "Recommendations": "Anbefalinger", "Renews at {{price}}.": "Fornyes til {{price}}.", "Retry": "Prøv på nytt", "Save": "Lagre", - "Send an email and say hi!": "Send en epost og si hei!", + "Send an email and say hi!": "Send en e-post og si hei!", "Send an email to {{senderEmail}} and say hello. This can also help signal to your mail provider that emails to and from this address should be trusted.": "", "Sending login link...": "Sender påloggingslenke", "Sending...": "Sender...", @@ -117,7 +136,7 @@ "Sign out": "Logg ut", "Sign up": "Opprett bruker", "Signup error: Invalid link": "En feil oppstod: Ugyldig lenke", - "Something went wrong, please try again later.": "", + "Something went wrong, please try again later.": "Noe gikk galt. Prøv igjen senere.", "Sorry, that didn’t work.": "Beklager, det fungerte ikke", "Spam complaints": "Klager om søppelpost", "Start {{amount}}-day free trial": "Start gratis prøveperiode på {{amount}} dager", @@ -126,33 +145,44 @@ "Submit feedback": "Send tilbakemelding", "Subscribe": "Påmelding", "Subscribed": "Påmeldt", + "Subscription plan updated successfully": "", "Success": "Suksess", - "Success! Check your email for magic link to sign-in.": "Suksess! Sjekk din epost for magisk lenke på pålogging.", + "Success! Check your email for magic link to sign-in.": "Suksess! Sjekk din e-post for magisk lenke på pålogging.", "Success! Your account is fully activated, you now have access to all content.": "Suksess! Din konto er nå aktivert, og du har tilgang til alt innhold.", - "Success! Your email is updated.": "Suksess, din epost er oppdatert", + "Success! Your email is updated.": "Suksess, din e-post er oppdatert", "Successfully unsubscribed": "Avmelding vellykket", - "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "", - "Thank you for your support": "", - "Thank you for your support!": "", + "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "Takk for at du abonnerer. Før du begynner å lese, nedenfor er noen andre nettsteder du kan ha glede av.", + "Thank you for your support": "Takk for din støtte", + "Thank you for your support!": "Takk for din støtte!", "Thanks for the feedback!": "Takk for tilbakemeldingen!", "That didn't go to plan": "Det gikk ikke som planlagt", - "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "", + "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "E-postadressen vi har til deg er {{memberEmail}} – hvis det ikke er riktig, kan du oppdatere den i .", "There was a problem submitting your feedback. Please try again a little later.": "Det oppstod en feil. Vennligst prøv igjen senere.", - "There was an error processing your payment. Please try again.": "", - "This site is invite-only, contact the owner for access.": "Denne siten er kun fo inviterte. Kontakt eieren for invitasjon.", - "This site is not accepting payments at the moment.": "", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", + "There was an error processing your payment. Please try again.": "Det oppsto en feil under behandling av betalingen din. Vennligst prøv igjen.", + "There was an error sending the email, please try again": "", + "This site is invite-only, contact the owner for access.": "Denne nettsiden er kun fo inviterte. Kontakt eieren for invitasjon.", + "This site is not accepting payments at the moment.": "Denne nettsiden godtar ikke betalinger for øyeblikket.", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "For å fullføre registreringen, klikk på bekreftelseslenken i innboksen din. Hvis den ikke kommer innen 3 minutter, må du sjekke søppelposten din!", - "To continue to stay up to date, subscribe to {{publication}} below.": "", - "Try free for {{amount}} days, then {{originalPrice}}.": "", + "To continue to stay up to date, subscribe to {{publication}} below.": "For å fortsette å holde deg oppdatert, abonner på {{publication}} nedenfor.", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Try free for {{amount}} days, then {{originalPrice}}.": "Prøv gratis i {{amount}} dager, deretter {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Få tilgang til alle nyhetsbrevene ved å oppgradere ditt abonnement.", - "Unsubscribe from all emails": "Meld deg av mottak av all epost", + "Unsubscribe from all emails": "Meld deg alle e-poster", "Unsubscribed": "Avmeldt", - "Unsubscribed from all emails.": "", + "Unsubscribed from all emails.": "Avmeldt fra alle e-poster", "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "Å melde seg av e-poster vil ikke avbryte abonnementet ditt på {{title}}", "Update": "Oppdater", "Update your preferences": "Oppdater dine valg", "Verification link sent, check your inbox": "Lenke for verifisering er sent. Sjekk innboksen din.", - "Verify your email address is correct": "Verifiser at eposten din er korrekt", + "Verify your email address is correct": "Verifiser at e-posten din er korrekt", "View plans": "Se planer", "We couldn't unsubscribe you as the email address was not found. Please contact the site owner.": "Vi kunne ikke melde deg av siden e-postadressen ikke ble funnet. Vennligst kontakt nettstedseieren.", "Welcome back, {{name}}!": "Velkommen tilbake {{name}}!", @@ -164,12 +194,13 @@ "Yearly": "Årlig", "You currently have a free membership, upgrade to a paid subscription for full access.": "Du har for tiden et gratis abonnement, oppgrader for full tilgang.", "You have been successfully resubscribed": "Du har blitt meldt på igjen", - "You're currently not receiving emails": "Du mottar for tiden ikke eposter", - "You're not receiving emails": "Du mottar ikke eppster", - "You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "Du mottar ikke e-post fordi du enten nylig har merket en melding som spam, eller fordi meldinger ikke kunne leveres til den oppgitte e-postadressen din.", + "You're currently not receiving emails": "Du mottar for tiden ikke e-poster", + "You're not receiving emails": "Du mottar ikke e-poster", + "You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "Du mottar ikke e-poster fordi du enten nylig har merket en melding som spam, eller fordi meldinger ikke kunne leveres til den oppgitte e-postadressen din.", "You've successfully signed in.": "Du har logged på igjen.", "You've successfully subscribed to": "Du har meldt deg på", "Your account": "Din konto", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Din tilbakemelding bidrar til å forme hva som blir publisert.", "Your subscription will expire on {{expiryDate}}": "Ditt abonnement vil avsluttes den {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Ditt abonnemnet vil fornyes den {{renewalDate}}", diff --git a/ghost/i18n/locales/no/search.json b/ghost/i18n/locales/no/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/no/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/pl/comments.json b/ghost/i18n/locales/pl/comments.json index 8f222908375..43111c5d6de 100644 --- a/ghost/i18n/locales/pl/comments.json +++ b/ghost/i18n/locales/pl/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Napisz odpowiedź do komentarza", "Report": "Zgłoś", "Report comment": "Zgłoś komentarz", - "Report this comment": "Zgłoś ten komentarz", "Report this comment?": "Czy chcesz zgłosić ten komentarz?", "Save": "Zapisz", "Sending": "Wysyłanie", @@ -68,6 +67,5 @@ "This comment has been removed.": "Komentarz został usunięty.", "Upgrade now": "Ulepsz teraz", "Yesterday": "Wczoraj", - "You want to report this comment?": "Czy chcesz zgłosić ten komentarz?", "Your request will be sent to the owner of this site.": "Twoje zgłoszenie zostało przesłane do administratora strony." } diff --git a/ghost/i18n/locales/pl/portal.json b/ghost/i18n/locales/pl/portal.json index ee85b3b2eb6..8a521b97980 100644 --- a/ghost/i18n/locales/pl/portal.json +++ b/ghost/i18n/locales/pl/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Link do logowania został wysłany do Twojej skrzynki odbiorczej. Jeśli nie dotrze w ciągu 3 minut, sprawdź folder spam.", "Account": "Konto", + "Account details updated successfully": "", "Account settings": "Ustawienia konta", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Po zakończeniu okresu próbnego zostanie naliczona regularna opłata za wybrany poziom subskrypcji. Pamiętaj, że możesz anulować subskrypcję zanim to nastąpi.", "Already a member?": "Masz już konto?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Wystąpił nieoczekiwany błąd. Spróbuj ponownie lub jeśli błąd będzie nadal występował, to skontaktuj z pomocą techniczną.", "Back": "Wstecz", "Back to Log in": "Wróć do logowania", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Sprawdź foldery spamu i promocji", "Check with your mail provider": "Skontaktuj się z dostawcą poczty elektronicznej", + "Check your inbox to verify email update": "", "Choose": "Wybierz", "Choose a different plan": "Wybierz inny plan", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Kontakt z pomocą techniczną", "Continue": "Kontynuuj", "Continue subscription": "Kontynuuj subskrypcję", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Nie można się zalogować, ponieważ link do logowania wygasł.", "Could not update email! Invalid link.": "Nie można zaktualizować adresu email, ponieważ link jest nieprawidłowy.", "Create a new contact": "Utwórz nowy kontakt", @@ -52,6 +56,7 @@ "Edit": "Edytuj", "Email": "Email", "Email newsletter": "Newsletter email", + "Email newsletter settings updated": "", "Email preferences": "Ustawienia email", "Emails": "Emaile", "Emails disabled": "Wysyłanie emaili zablokowane", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Błąd", "Expires {{expiryDate}}": "Wygasa {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Na zawsze", "Free Trial – Ends {{trialEnd}}": "Bezpłatny okres próbny - Koniec {{trialEnd}}", "Get help": "Uzyskaj pomoc", @@ -91,6 +108,8 @@ "Name": "Imię", "Need more help? Contact support": "Potrzebujesz pomocy? Skontaktuj się z pomocą techniczną", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Newslettery mogą zostać wyłączone na Twoim koncie z dwóch powodów: poprzedni email został oznaczony jako spam lub próba wysłania wiadomości zakończyła się niepowodzeniem (odesłaniem).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Nie dostajesz emaili?", "Now check your email!": "Teraz sprawdź swoją pocztę!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "", @@ -126,6 +145,7 @@ "Submit feedback": "Wyślij ocenę", "Subscribe": "Subskrybuj", "Subscribed": "Zasubskrybowane", + "Subscription plan updated successfully": "", "Success": "Sukces", "Success! Check your email for magic link to sign-in.": "Sukces! Sprawdź swój email, aby uzyskać link do logowania.", "Success! Your account is fully activated, you now have access to all content.": "Sukces! Twoje konto zostało w pełni aktywowane, masz teraz dostęp do serwisu.", @@ -138,12 +158,22 @@ "That didn't go to plan": "Coś poszło nie tak", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Adres email, który mamy to {{memberEmail}} — jeśli nie jest poprawny, możesz go zaktualizować w swoich .", "There was a problem submitting your feedback. Please try again a little later.": "Wystąpił problem z przesyłaniem opinii. Spróbuj ponownie nieco później.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Ta strona posiada zamknięty dostęp. Skontaktuj się z właścicielem, aby uzyskać dostęp.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Aby dokończyć rejestrację, kliknij w link przesłany na twoją skrzynkę pocztową. Jeśli nie dotrze w ciągu 3 minut, sprawdź folder spamu!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Wypróbuj za darmo przez {{amount}} dni, później {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Zostań płatnym subskrybentem i odblokuj dostęp do wszystkich biuletynów.", "Unsubscribe from all emails": "Wypisz się w wszystkich emaili", "Unsubscribed": "Niezasubskrybowany", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Logowanie powiodło się.", "You've successfully subscribed to": "Pomyślnie zasubskrybowano", "Your account": "Twoje konto", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Twoja ocena pomoże nam lepiej kształtować nasz publikacje.", "Your subscription will expire on {{expiryDate}}": "Subskrypcja wygaśnie w dniu {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Subskrypcja zostanie odnowiona w dniu {{renewalDate}}", diff --git a/ghost/i18n/locales/pl/search.json b/ghost/i18n/locales/pl/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/pl/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/pt-BR/comments.json b/ghost/i18n/locales/pt-BR/comments.json index 7a0fc34034e..d59464e183b 100644 --- a/ghost/i18n/locales/pt-BR/comments.json +++ b/ghost/i18n/locales/pt-BR/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Responder ao comentário", "Report": "Denunciar", "Report comment": "Denunciar comentário", - "Report this comment": "Denunciar este comentário", "Report this comment?": "Denunciar este comentário?", "Save": "Salvar", "Sending": "Enviando", @@ -68,6 +67,5 @@ "This comment has been removed.": "Este comentário foi removido.", "Upgrade now": "Atualize agora", "Yesterday": "Ontem", - "You want to report this comment?": "Você deseja denunciar este comentário?", "Your request will be sent to the owner of this site.": "Sua solicitação será enviada ao proprietário deste site." } diff --git a/ghost/i18n/locales/pt-BR/portal.json b/ghost/i18n/locales/pt-BR/portal.json index 488227b6791..955c0282a8c 100644 --- a/ghost/i18n/locales/pt-BR/portal.json +++ b/ghost/i18n/locales/pt-BR/portal.json @@ -13,24 +13,27 @@ "+1 (123) 456-7890": "+55 (00) 0 0000-0000", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Um link de acesso foi enviado para o seu e-mail. Se a mensagem não chegar dentro de 3 minutos, verifique sua pasta de spam.", "Account": "Conta", + "Account details updated successfully": "Detalhes da conta atualizados com sucesso", "Account settings": "Configurações de conta", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Quando o teste grátis acabar, será cobrado o preço normal do plano que você escolheu. Você sempre pode cancelar antes.", "Already a member?": "Já é membro?", + "An error occurred": "Ocorreu um erro", "An unexpected error occured. Please try again or contact support if the error persists.": "Ocorreu um erro inesperado. Tente novamente ou entre em contato com o suporte se o erro persistir.", "Back": "Voltar", "Back to Log in": "Voltar para login", "Billing info": "Informações de cobrança", "Black Friday": "Black Friday", "Cancel anytime.": "Cancele quando quiser.", - "Cancel subscription": "Cancelar inscrição", + "Cancel subscription": "Cancelar assinatura", "Cancellation reason": "Motivo do cancelamento", "Change": "Alterar", - "Change plan": "", + "Change plan": "Alterar plano", "Check spam & promotions folders": "Verificar pastas de spam e promoções", "Check with your mail provider": "Verificar com seu provedor de e-mail", + "Check your inbox to verify email update": "Verifique sua caixa de entrada para confirmar a atualização de e-mail", "Choose": "Escolher", "Choose a different plan": "Escolher um plano diferente", - "Choose a plan": "", + "Choose a plan": "Escolher um plano", "Choose your newsletters": "Escolher suas newsletters", "Click here to retry": "Clique aqui para tentar novamente", "Close": "Fechar", @@ -38,10 +41,11 @@ "Complimentary": "Cortesia", "Confirm": "Confirmar", "Confirm cancellation": "Confirmar cancelamento", - "Confirm subscription": "Confirmar inscrição", + "Confirm subscription": "Confirmar assinatura", "Contact support": "Contatar suporte", "Continue": "Continuar", - "Continue subscription": "Continuar inscrição", + "Continue subscription": "Continuar assinatura", + "Could not create stripe checkout session": "Não foi possível criar sessão de pagamento no Stripe", "Could not sign in. Login link expired.": "Não foi possível fazer login. O link de acesso expirou.", "Could not update email! Invalid link.": "Não foi possível atualizar o e-mail! Link inválido.", "Create a new contact": "Criar um novo contato", @@ -52,6 +56,7 @@ "Edit": "Editar", "Email": "E-mail", "Email newsletter": "Newsletter por e-mail", + "Email newsletter settings updated": "Configurações de newsletter por e-mail atualizadas com sucesso", "Email preferences": "Preferências de e-mail", "Emails": "E-mails", "Emails disabled": "E-mails desativados", @@ -60,6 +65,18 @@ "Enter your name": "Insira seu nome", "Error": "Erro", "Expires {{expiryDate}}": "Expira em {{expiryDate}}", + "Failed to cancel subscription, please try again": "Falha ao cancelar a assinatura, por favor, tente novamente", + "Failed to log in, please try again": "Falha ao fazer login, por favor, tente novamente", + "Failed to log out, please try again": "Falha ao sair, por favor, tente novamente", + "Failed to process checkout, please try again": "Falha ao processar o pagamento, por favor, tente novamente", + "Failed to send magic link email": "Falha ao enviar o e-mail com link mágico", + "Failed to send verification email": "Falha ao enviar o e-mail de verificação", + "Failed to sign up, please try again": "Falha ao se inscrever, por favor, tente novamente", + "Failed to update account data": "Falha ao atualizar os dados da conta", + "Failed to update account details": "Falha ao atualizar os detalhes da conta", + "Failed to update billing information, please try again": "Falha ao atualizar as informações de cobrança, por favor, tente novamente", + "Failed to update newsletter settings": "Falha ao atualizar as configurações da newsletter", + "Failed to update subscription, please try again": "Falha ao atualizar a assinatura, por favor, tente novamente", "Forever": "Para sempre", "Free Trial – Ends {{trialEnd}}": "Teste grátis – Termina em {{trialEnd}}", "Get help": "Obter ajuda", @@ -70,7 +87,7 @@ "Here are a few other sites you may enjoy.": "Aqui estão alguns outros sites que você pode gostar.", "If a newsletter is flagged as spam, emails are automatically disabled for that address to make sure you no longer receive any unwanted messages.": "Se uma newsletter for marcada como spam, os e-mails são automaticamente desativados para esse endereço para garantir que você não receba mais mensagens indesejadas.", "If the spam complaint was accidental, or you would like to begin receiving emails again, you can resubscribe to emails by clicking the button on the previous screen.": "Se a reclamação de spam foi acidental ou se você deseja começar a receber e-mails novamente, pode se inscrever novamente para receber e-mails clicando no botão na tela anterior.", - "If you cancel your subscription now, you will continue to have access until {{periodEnd}}.": "Se você cancelar sua inscrição agora, continuará tendo acesso até {{periodEnd}}.", + "If you cancel your subscription now, you will continue to have access until {{periodEnd}}.": "Se você cancelar sua assinatura agora, continuará tendo acesso até {{periodEnd}}.", "If you have a corporate or government email account, reach out to your IT department and ask them to allow emails to be received from {{senderEmail}}": "Se você tiver uma conta de e-mail corporativa ou governamental, entre em contato com o departamento de TI e peça para permitir que os e-mails sejam recebidos de {{senderEmail}}", "If you would like to start receiving emails again, the best next steps are to check your email address on file for any issues and then click resubscribe on the previous screen.": "Se você deseja começar a receber e-mails novamente, os próximos passos são verificar seu endereço de e-mail no arquivo para verificar se há problemas e, em seguida, clicar em se inscrever novamente na tela anterior.", "If you're not receiving the email newsletter you've subscribed to, here are a few things to check.": "Se você não estiver recebendo a newsletter por e-mail à qual se inscreveu, verifique algumas coisas.", @@ -78,30 +95,32 @@ "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "No caso de uma falha permanente ser recebida ao tentar enviar uma newsletter, os e-mails serão desativados na conta.", "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "No seu cliente de e-mail, adicione {{senderEmail}} à sua lista de contatos. Isso sinaliza ao seu provedor de e-mail que os e-mails enviados deste endereço devem ser confiáveis.", "Invalid email address": "Endereço de e-mail inválido", - "Jamie Larson": "Fulano de tal", + "Jamie Larson": "Fulano(a) de tal", "jamie@example.com": "fulano@exemplo.com", "Less like this": "Menos como este", "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "Verifique se os e-mails não estão indo parar acidentalmente nas pastas Spam ou Promoções da sua caixa de entrada. Se estiverem, clique em \"Marcar como não spam\" e/ou \"Mover para a caixa de entrada\".", "Manage": "Gerenciar", "Maybe later": "Talvez mais tarde", "Memberships unavailable, contact the owner for access.": "Assinaturas indisponíveis, entre em contato para obter acesso.", - "month": "", + "month": "mês", "Monthly": "Mensal", "More like this": "Relacionados", "Name": "Nome", "Need more help? Contact support": "Precisa de mais ajuda? Contate o suporte", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "As newsletters podem ser desativadas na sua conta por dois motivos: um e-mail anterior foi marcado como spam ou a tentativa de enviar um e-mail resultou em uma falha permanente (bounce).", + "No member exists with this e-mail address.": "Nenhum membro existe com este endereço de e-mail.", + "No member exists with this e-mail address. Please sign up first.": "Nenhum membro existe com este endereço de e-mail. Por favor, cadastre-se primeiro.", "Not receiving emails?": "Não está recebendo e-mails?", "Now check your email!": "Agora veja seu e-mail!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Depois de se inscrever novamente, se você ainda não vir e-mails na sua caixa de entrada, verifique sua pasta de spam. Alguns provedores de caixa de entrada mantêm um registro de reclamações anteriores de spam e continuarão a sinalizar e-mails. Se isso acontecer, marque a newsletter mais recente como 'Não é spam' para movê-la de volta para sua caixa de entrada principal.", "Permanent failure (bounce)": "Falha permanente (bounce)", - "Phone number": "", + "Phone number": "Número de telefone", "Plan": "Plano", - "Plan checkout was cancelled.": "Plano de checkout foi cancelado.", - "Plan upgrade was cancelled.": "Upgrade de plano foi cancelado.", - "Please contact {{supportAddress}} to adjust your complimentary subscription.": "Entre em contato com {{supportAddress}} para ajustar sua assinatura.", - "Please enter {{fieldName}}": "", - "Please fill in required fields": "Preencha os campos obrigatórios", + "Plan checkout was cancelled.": "O checkout do plano foi cancelado.", + "Plan upgrade was cancelled.": "O upgrade de plano foi cancelado.", + "Please contact {{supportAddress}} to adjust your complimentary subscription.": "Entre em contato com {{supportAddress}} para ajustar sua assinatura de cortesia.", + "Please enter {{fieldName}}": "Por favor, insira {{fieldName}}", + "Please fill in required fields": "Por favor, preencha os campos obrigatórios", "Price": "Preço", "Re-enable emails": "Reativar e-mails", "Recommendations": "Recomendações", @@ -117,7 +136,7 @@ "Sign out": "Sair", "Sign up": "Cadastrar", "Signup error: Invalid link": "Erro de inscrição: link inválido", - "Something went wrong, please try again later.": "", + "Something went wrong, please try again later.": "Algo deu errado, tente novamente mais tarde.", "Sorry, that didn’t work.": "Desculpe, isso não funcionou.", "Spam complaints": "Reclamações de spam", "Start {{amount}}-day free trial": "Começar teste grátis de {{amount}} dias", @@ -126,6 +145,7 @@ "Submit feedback": "Enviar avaliação", "Subscribe": "Inscrever-se", "Subscribed": "Inscrito", + "Subscription plan updated successfully": "Plano de assinatura atualizado com sucesso", "Success": "Sucesso", "Success! Check your email for magic link to sign-in.": "Sucesso! Verifique seu e-mail para o link mágico de acesso.", "Success! Your account is fully activated, you now have access to all content.": "Sucesso! Sua conta está totalmente ativada, agora você tem acesso a todo o conteúdo.", @@ -138,19 +158,29 @@ "That didn't go to plan": "Algo não saiu como planejado", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "O endereço de e-mail que temos para você é {{memberEmail}} — se isso não estiver correto, você pode atualizá-lo na sua .", "There was a problem submitting your feedback. Please try again a little later.": "Houve um problema ao enviar sua avaliação. Tente novamente mais tarde.", + "There was an error cancelling your subscription, please try again.": "Houve um erro ao cancelar sua assinatura, por favor, tente novamente.", + "There was an error continuing your subscription, please try again.": "Houve um erro ao continuar sua assinatura, por favor, tente novamente.", "There was an error processing your payment. Please try again.": "Houve um erro ao processar seu pagamento. Por favor, tente novamente.", + "There was an error sending the email, please try again": "Houve um erro ao enviar o e-mail, por favor, tente novamente.", "This site is invite-only, contact the owner for access.": "Este site é apenas para convidados. Contate o proprietário para obter acesso.", "This site is not accepting payments at the moment.": "Este site não está aceitando pagamentos no momento.", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Para completar o cadastro, clique no link de confirmação enviado para sua caixa de entrada. Se o link não chegar dentro de 3 minutos, confira a pasta de spam!", "To continue to stay up to date, subscribe to {{publication}} below.": "Para continuar atualizado, inscreva-se em {{publication}} abaixo.", + "Too many attempts try again in {{number}} days.": "Muitas tentativas. Tente novamente em {{number}} dias.", + "Too many attempts try again in {{number}} hours.": "Muitas tentativas. Tente novamente em {{number}} horas.", + "Too many attempts try again in {{number}} minutes.": "Muitas tentativas. Tente novamente em {{number}} minutos.", + "Too many different sign-in attempts, try again in {{number}} days": "Muitas tentativas de login diferentes, tente novamente em {{number}} dias.", + "Too many different sign-in attempts, try again in {{number}} hours": "Muitas tentativas de login diferentes, tente novamente em {{number}} horas.", + "Too many different sign-in attempts, try again in {{number}} minutes": "Muitas tentativas de login diferentes, tente novamente em {{number}} minutos.", "Try free for {{amount}} days, then {{originalPrice}}.": "Experimente grátis por {{amount}} dias, depois {{originalPrice}}.", + "Unable to initiate checkout session": "Não foi possível iniciar a sessão de pagamento.", "Unlock access to all newsletters by becoming a paid subscriber.": "Desbloqueie o acesso a todas as newsletters se tornando um assinante pago.", "Unsubscribe from all emails": "Cancelar inscrição em todos os e-mails", "Unsubscribed": "Cancelado", "Unsubscribed from all emails.": "Cancelada a inscrição de todos os e-mails.", "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "Cancelar a inscrição nos e-mails não cancelará sua assinatura paga em {{title}}", - "Update": "atualizar", - "Update your preferences": "Atualizar preferências", + "Update": "Atualizar", + "Update your preferences": "Atualizar suas preferências", "Verification link sent, check your inbox": "Link de verificação enviado, verifique sua caixa de entrada", "Verify your email address is correct": "Verifique se o endereço de e-mail está correto", "View plans": "Ver planos", @@ -160,7 +190,7 @@ "Welcome to {{siteTitle}}": "Bem-vindo ao {{siteTitle}}", "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "Quando uma caixa de entrada falha ao aceitar um e-mail, isso é comumente chamado de bounce. Em muitos casos, isso pode ser temporário. No entanto, em alguns casos, um e-mail com falha pode ser retornado como uma falha permanente quando um endereço de e-mail é inválido ou não existe.", "Why has my email been disabled?": "Por que meu e-mail foi desativado?", - "year": "", + "year": "ano", "Yearly": "Anualmente", "You currently have a free membership, upgrade to a paid subscription for full access.": "Você atualmente tem uma assinatura gratuita, faça um upgrade para uma assinatura paga para ter acesso completo.", "You have been successfully resubscribed": "Você foi reinscrito com sucesso", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Você entrou com sucesso.", "You've successfully subscribed to": "Você se inscreveu com sucesso", "Your account": "Sua conta", + "Your email has failed to resubscribe, please try again": "Não foi possível reinscrever seu e-mail, por favor, tente novamente.", "Your input helps shape what gets published.": "Sua resposta ajuda a moldar o que será publicado.", "Your subscription will expire on {{expiryDate}}": "Sua assinatura expirará em {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Sua assinatura será renovada em {{renewalDate}}", diff --git a/ghost/i18n/locales/pt-BR/search.json b/ghost/i18n/locales/pt-BR/search.json new file mode 100644 index 00000000000..5e6150dff68 --- /dev/null +++ b/ghost/i18n/locales/pt-BR/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "Autores", + "Cancel": "Cancelar", + "No matches found": "Nenhum resultado encontrado", + "Posts": "Publicações", + "Search posts, tags and authors": "Buscar posts, tags e autores", + "Show more results": "Mostrar mais resultados", + "Tags": "Tags" +} diff --git a/ghost/i18n/locales/pt/comments.json b/ghost/i18n/locales/pt/comments.json index 3bf518238d1..b80418137c7 100644 --- a/ghost/i18n/locales/pt/comments.json +++ b/ghost/i18n/locales/pt/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Responder ao comentário", "Report": "Reportar", "Report comment": "Reportar comentário", - "Report this comment": "Reportar este comentário", "Report this comment?": "Reportar este comentário?", "Save": "Guardar", "Sending": "Enviar", @@ -68,6 +67,5 @@ "This comment has been removed.": "Este comentário foi removido", "Upgrade now": "Atualize agora", "Yesterday": "Ontem", - "You want to report this comment?": "Deseja reportar este comentário?", "Your request will be sent to the owner of this site.": "O seu pedido será enviado para o dono deste site." } diff --git a/ghost/i18n/locales/pt/portal.json b/ghost/i18n/locales/pt/portal.json index df0d6b25dea..8a5374ed0dd 100644 --- a/ghost/i18n/locales/pt/portal.json +++ b/ghost/i18n/locales/pt/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Um link de acesso foi enviado para o seu email. Se o email não chegar dentro de 3 minutos, verifique a pasta de spam/lixo do seu email.", "Account": "Conta", + "Account details updated successfully": "", "Account settings": "Definições de conta", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Após o término do teste grátis, será cobrado o valor normal para o nível escolhido. Poderá sempre cancelar até esse momento, caso não deseje ser cobrado.", "Already a member?": "Já é membro?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Ocorreu um erro inesperado. Por favor tente novamente ou contacte o suporte se o erro persistir.", "Back": "Voltar", "Back to Log in": "Voltar ao login", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Verifique a pasta de spam e promoções", "Check with your mail provider": "Entre em contacto com o seu serviço de email", + "Check your inbox to verify email update": "", "Choose": "Escolher", "Choose a different plan": "Escolha um plano diferente", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Contactar suporte", "Continue": "Continuar", "Continue subscription": "Continuar subscrição", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Não foi possível registar. O link de login expirou.", "Could not update email! Invalid link.": "Não foi possível atualizar o email! Link inválido.", "Create a new contact": "Criar novo contacto", @@ -52,6 +56,7 @@ "Edit": "Editar", "Email": "Email", "Email newsletter": "Newsletter", + "Email newsletter settings updated": "", "Email preferences": "Preferências de email", "Emails": "Emails", "Emails disabled": "Email desativado", @@ -60,6 +65,18 @@ "Enter your name": "Insira o seu nome", "Error": "Erro", "Expires {{expiryDate}}": "Expira {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Para sempre", "Free Trial – Ends {{trialEnd}}": "Período de Teste Gratuito - Termina {{trialEnd}}", "Get help": "Obter ajuda", @@ -91,6 +108,8 @@ "Name": "Nome", "Need more help? Contact support": "Precisa de mais ajuda? Entre em contacto com o suporte", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "As newsletters podem ser desativadas na sua conta por dois motivos: um email anterior foi marcado como spam, ou a tentativa de enviar um email resultou numa falha permanente (bounce).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Não está a receber emails?", "Now check your email!": "Verifica o teu email agora!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Se depois de ser inscrever novamente, os emails não chegaram à sua caixa de entrada, verifique por favor a sua pasta de spam. Alguns provedores de mail têm disponível um registro de reclamações anteriores de spam e continuarão a sinalizar emails. Se isso acontecer, marque a newsletter mais recente como 'Não é spam' para movê-la de volta para sua caixa de entrada principal.", @@ -126,6 +145,7 @@ "Submit feedback": "Enviar avaliação", "Subscribe": "Inscrever-se", "Subscribed": "Inscrito", + "Subscription plan updated successfully": "", "Success": "Sucesso", "Success! Check your email for magic link to sign-in.": "Sucesso! Verifique o seu email para o link mágico de acesso.", "Success! Your account is fully activated, you now have access to all content.": "Sucesso! A sua conta está totalmente ativada, agora tem acesso a todo o conteúdo.", @@ -138,12 +158,22 @@ "That didn't go to plan": "Algo não correu como planeado", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "O endereço de email que temos disponível para si é {{memberEmail}} — se desejar alterá-lo, poderá fazê-lo na sua .", "There was a problem submitting your feedback. Please try again a little later.": "Houve um problema ao enviar sua avaliação. Tente novamente mais tarde por favor.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "Houve um problema ao processar o seu pagamento. Tente novamente por favor.", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "O acesso a este site é feito apenas por convite. Entre em contacto com o proprietário para obter acesso.", "This site is not accepting payments at the moment.": "Este site não está a aceitar pagamentos de momento", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Para completar o registo, clique no link de confirmação enviado para a sua caixa de entrada. Se não o receber dentro de 3 minutos, verifique a sua pasta de spam!", "To continue to stay up to date, subscribe to {{publication}} below.": "Para continuar a par das novidades, subscreva o {{publication}} aqui.", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Experimente grátis por {{amount}} dias, depois {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Desbloqueie o acesso a todas as newsletters tornando-se um assinante pago.", "Unsubscribe from all emails": "Cancelar subscrição de todos os emails", "Unsubscribed": "Subscrição cancelada", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Registou-se com sucesso.", "You've successfully subscribed to": "Subscreveu com sucesso", "Your account": "A sua conta", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "O seu feedback ajudará a decidir o conteúdo que será publicado no futuro.", "Your subscription will expire on {{expiryDate}}": "A sua assinatura expirará em {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "A sua assinatura será renovada em {{renewalDate}}", diff --git a/ghost/i18n/locales/pt/search.json b/ghost/i18n/locales/pt/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/pt/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/ro/comments.json b/ghost/i18n/locales/ro/comments.json index 681d8e3c0b0..3ba90b7763f 100644 --- a/ghost/i18n/locales/ro/comments.json +++ b/ghost/i18n/locales/ro/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Răspunde la comentariu", "Report": "Raportează", "Report comment": "Raportează comentariul", - "Report this comment": "Raportează acest comentariu", "Report this comment?": "Raportezi acest comentariu?", "Save": "Salvează", "Sending": "Se trimite", @@ -68,6 +67,5 @@ "This comment has been removed.": "Acest comentariu a fost eliminat.", "Upgrade now": "Actualizează acum", "Yesterday": "Ieri", - "You want to report this comment?": "Dorești să raportezi acest comentariu?", "Your request will be sent to the owner of this site.": "Cererea dumneavoastră va fi trimisă proprietarului acestui site." } diff --git a/ghost/i18n/locales/ro/portal.json b/ghost/i18n/locales/ro/portal.json index 792af665148..607a185638e 100644 --- a/ghost/i18n/locales/ro/portal.json +++ b/ghost/i18n/locales/ro/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Un link de autentificare a fost trimis în inbox-ul tău. Dacă nu ajunge în 3 minute, asigură-te că verifici folderul de spam.", "Account": "Cont", + "Account details updated successfully": "", "Account settings": "Setări cont", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "După ce expiră perioada de încercare gratuită, vei fi taxat la prețul obișnuit pentru nivelul pe care l-ai ales. Poți anula în orice moment înainte de aceasta.", "Already a member?": "Ești deja membru?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "A apărut o eroare neașteptată. Te rog încearcă din nou sau contactează suportul dacă eroarea persistă.", "Back": "Înapoi", "Back to Log in": "Înapoi la Autentificare", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Verifică dosarele spam & promoții", "Check with your mail provider": "Verifică cu furnizorul tău de email", + "Check your inbox to verify email update": "", "Choose": "Alege", "Choose a different plan": "Alege un plan diferit", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Contactează suportul", "Continue": "Continuă", "Continue subscription": "Continuă abonamentul", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Nu s-a putut autentifica. Link-ul de autentificare a expirat.", "Could not update email! Invalid link.": "Nu s-a putut actualiza emailul! Link invalid.", "Create a new contact": "Creează un nou contact", @@ -52,6 +56,7 @@ "Edit": "Editează", "Email": "Email", "Email newsletter": "Newsletter prin email", + "Email newsletter settings updated": "", "Email preferences": "Preferințe email", "Emails": "Emailuri", "Emails disabled": "Emailuri dezactivate", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Eroare", "Expires {{expiryDate}}": "Expiră {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Pentru totdeauna", "Free Trial – Ends {{trialEnd}}": "Perioadă de probă gratuită - Se încheie {{trialEnd}}", "Get help": "Obține ajutor", @@ -91,6 +108,8 @@ "Name": "Nume", "Need more help? Contact support": "Aveți nevoie de mai mult ajutor? Contactați suportul", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Buletinele informative pot fi dezactivate pe contul dvs. din două motive: Un email anterior a fost marcat ca spam, sau încercarea de a trimite un email a rezultat într-o eroare permanentă (respins).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Nu primești emailuri?", "Now check your email!": "Acum verifică-ți emailul!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Odată reabonat, dacă încă nu vedeți emailuri în inbox, verificați dosarul de spam. Unii furnizori de inbox păstrează un istoric al sesizărilor de spam anterioare și vor continua să marcheze emailurile. Dacă se întâmplă acest lucru, marcați cel mai recent buletin informativ ca 'Nu este spam' pentru a-l muta înapoi în inbox-ul primar.", @@ -126,6 +145,7 @@ "Submit feedback": "Trimite feedback", "Subscribe": "Abonează-te", "Subscribed": "Abonat", + "Subscription plan updated successfully": "", "Success": "Succes", "Success! Check your email for magic link to sign-in.": "Succes! Verifică-ți emailul pentru link-ul magic de autentificare.", "Success! Your account is fully activated, you now have access to all content.": "Succes! Contul tău este complet activat, acum ai acces la tot conținutul.", @@ -138,12 +158,22 @@ "That didn't go to plan": "Asta nu a mers conform planului", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Adresa de email pe care o avem pentru tine este {{memberEmail}} — dacă nu este corectă, o poți actualiza în .", "There was a problem submitting your feedback. Please try again a little later.": "A fost o problemă cu trimiterea feedback-ului tău. Te rog încearcă din nou puțin mai târziu.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Acest site este disponibil doar pe bază de invitație, contactează proprietarul pentru acces.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Pentru a finaliza înregistrarea, apasă pe link-ul de confirmare din inbox-ul tău. Dacă nu ajunge în 3 minute, verifică folderul de spam!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Încearcă gratuit pentru {{amount}} zile, apoi {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Deblochează accesul la toate buletinele informative devenind un abonat plătit.", "Unsubscribe from all emails": "Dezabonează-te de la toate emailurile", "Unsubscribed": "Dezabonat", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Te-ai autentificat cu succes.", "You've successfully subscribed to": "Te-ai abonat cu succes la", "Your account": "Contul tău", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Contribuția ta ajută la conturarea a ceea ce se publică.", "Your subscription will expire on {{expiryDate}}": "Abonamentul tău va expira pe {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Abonamentul tău se va reînnoi pe {{renewalDate}}", diff --git a/ghost/i18n/locales/ro/search.json b/ghost/i18n/locales/ro/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/ro/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/ru/comments.json b/ghost/i18n/locales/ru/comments.json index f25712d144f..afc99f683cb 100644 --- a/ghost/i18n/locales/ru/comments.json +++ b/ghost/i18n/locales/ru/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Ответить на комментарий", "Report": "Пожаловаться", "Report comment": "Пожаловаться на комментарий", - "Report this comment": "Пожаловаться на этот комментарий", "Report this comment?": "Пожаловаться на этот комментарий?", "Save": "Сохранить", "Sending": "Отправка", @@ -68,6 +67,5 @@ "This comment has been removed.": "Этот комментарий был удалён", "Upgrade now": "Обновить ", "Yesterday": "Вчера", - "You want to report this comment?": "Вы хотите пожаловаться на этот комментарий?", "Your request will be sent to the owner of this site.": "Ваш запрос будет отправлен владельцу сайта." } diff --git a/ghost/i18n/locales/ru/portal.json b/ghost/i18n/locales/ru/portal.json index 0bc3e7cfb4d..5048d3885ba 100644 --- a/ghost/i18n/locales/ru/portal.json +++ b/ghost/i18n/locales/ru/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "+7 (987) 654-3210", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Ссылка для входа была отправлена вам на email. Если письмо не пришло в течение 3 минут, проверьте папку «Спам».", "Account": "Аккаунт", + "Account details updated successfully": "", "Account settings": "Настройки аккаунта", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "После окончания бесплатного периода с вас будут взиматься регулярные платежи по выбранному тарифу. До этого момента вы можете отменить подписку в любое время.", "Already a member?": "Уже есть аккаунт?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Произошла непредвиденная ошибка. Пожалуйста, повторите попытку или обратитесь в службу поддержки, если ошибка повторится.", "Back": "Назад", "Back to Log in": "Вернуться на страницу входа", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Проверьте папки «Спам», «Рассылки» или «Промоакции»", "Check with your mail provider": "Обратитесь к своему почтовому провайдеру", + "Check your inbox to verify email update": "", "Choose": "Выбрать", "Choose a different plan": "Выбрать другой план", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Написать в техподдержку", "Continue": "Продолжить", "Continue subscription": "Возобновить подписку", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Не удалось войти. Срок действия ссылки для входа истёк.", "Could not update email! Invalid link.": "Не удалось обновить email адрес! Неверная ссылка.", "Create a new contact": "Создать новый контакт", @@ -52,6 +56,7 @@ "Edit": "Редактировать", "Email": "Email", "Email newsletter": "Email рассылки", + "Email newsletter settings updated": "", "Email preferences": "Настройки email адреса", "Emails": "Письма", "Emails disabled": "Доставка писем отключена", @@ -60,6 +65,18 @@ "Enter your name": "Введите ваше имя", "Error": "Ошибка", "Expires {{expiryDate}}": "Подписка заканчивается {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Навсегда", "Free Trial – Ends {{trialEnd}}": "Бесплатный период – заканчивается {{trialEnd}}", "Get help": "Получить помощь", @@ -91,6 +108,8 @@ "Name": "Имя", "Need more help? Contact support": "Нужна дополнительная помощь? Обратитесь в службу поддержки", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Рассылка могла быть отключена в вашей учётной записи (для вашего email адреса) по двум причинам: предыдущее электронное письмо было помечено как спам или попытка отправки приводила к постоянной ошибке (например: авто-возврату).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Не получаете письма?", "Now check your email!": "Теперь проверьте свою электронную почту!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Если после повторной подписки вы всё ещё не видите писем в своём почтовом ящике, проверьте папку со спамом. Некоторые провайдеры электронной почты сохраняют записи предыдущих жалоб на спам и продолжают помечать письма как спам. Если именно это и происходит, отметьте последнюю рассылку как «Не спам», чтобы переместить письмо обратно в основную папку входящих писем вашего почтового ящика.", @@ -126,6 +145,7 @@ "Submit feedback": "Отправить отзыв", "Subscribe": "Подписки", "Subscribed": "Подписан", + "Subscription plan updated successfully": "", "Success": "Успех", "Success! Check your email for magic link to sign-in.": "Успех! Проверьте свою электронную почту на наличие волшебной ссылки для входа в систему.", "Success! Your account is fully activated, you now have access to all content.": "Успех! Ваша учётная запись полностью активирована, теперь у вас есть доступ ко всему содержимому.", @@ -138,12 +158,22 @@ "That didn't go to plan": "Что-то пошло не так", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Ваш email адрес: {{memberEmail}} — если он указан неправильно, вы можете обновить его в .", "There was a problem submitting your feedback. Please try again a little later.": "Возникла проблема с отправкой отзыва. Попробуйте ещё раз немного позже.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "Произошла ошибка при обработке вашего платежа. Попробуйте ещё раз.", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Доступ к материалам этого сайта возможен только по приглашению. Для получения доступа свяжитесь с владельцем сайта.", "This site is not accepting payments at the moment.": "В данный момент сайт не принимает платежи.", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Чтобы завершить регистрацию, нажмите на ссылку подтверждения в электронном письме, которое мы прислали. Если письмо не пришло в течение 3 минут, проверьте папку «Спам»!", "To continue to stay up to date, subscribe to {{publication}} below.": "Чтобы оставаться в курсе событий, подпишитесь на {{publication}} ниже.", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Попробуйте бесплатно в течение {{amount}} дня(ей), затем за {{original Price}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Получите доступ ко всем рассылкам, оформив платную подписку.", "Unsubscribe from all emails": "Отписаться от всех рассылок", "Unsubscribed": "Отписан", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Вы успешно вошли.", "You've successfully subscribed to": "Вы успешно подписались на", "Your account": "Ваш аккаунт", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Ваш отзыв помогает формировать понимание, что вам интересно и что будет опубликовано в будущем.", "Your subscription will expire on {{expiryDate}}": "Срок действия вашей подписки истекает {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Ваша подписка будет продлена {{renewalDate}}", diff --git a/ghost/i18n/locales/ru/search.json b/ghost/i18n/locales/ru/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/ru/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/si/comments.json b/ghost/i18n/locales/si/comments.json index f812b9ee610..8aef2939229 100644 --- a/ghost/i18n/locales/si/comments.json +++ b/ghost/i18n/locales/si/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "අදහසට \u200dරතිචාර දක්වන්\u200bන", "Report": "වාර්තා කරන්\u200bන", "Report comment": "අදහසට වාර්තා කරන්\u200bන", - "Report this comment": "මෙම අදහසට වාර්තා කරන්\u200bන", "Report this comment?": "මෙම අදහසට වාර්තා කරන්නෙහිද ?", "Save": "සුරකින්\u200bන", "Sending": "යවමි\u200bන්", @@ -68,6 +67,5 @@ "This comment has been removed.": "මෙම අදහස ඉවත් කර ඇත.", "Upgrade now": "දැන් upgrade කරන්න", "Yesterday": "ඊ\u200bයෙ", - "You want to report this comment?": "ඔබට මෙම අදහස report කිරීමට අවශ්\u200dයද?", "Your request will be sent to the owner of this site.": "ඔබගේ ඉල්ලීම මෙම අඩවියේ හිමිකරු වෙත යවනු ලැබේ." } diff --git a/ghost/i18n/locales/si/portal.json b/ghost/i18n/locales/si/portal.json index bb10ed4da13..22ccf936ec7 100644 --- a/ghost/i18n/locales/si/portal.json +++ b/ghost/i18n/locales/si/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "වෙබ් අඩවියට පිවිසීම සඳහා link එකක් ඔබගේ email ලිපිනය වෙත යවා ඇත. එය විනාඩි 3ක් ඇතුළත නොපැමිණියේ නම් spam ෆෝල්ඩරය පරීක්ෂා කරන්න.", "Account": "ගිණුම", + "Account details updated successfully": "", "Account settings": "ගිණුම් සැකසුම්", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "නොමිලයේ ලබාදෙන අත්හදාබැලීමේ කාලය අවසන් වූ පසුව, ඔබ තෝරාගන්නා ලද tier එක අනුව එහි සාමාන්\u200dයය මිල ගණන් අය වනු ඇත. ඊට පෙර ඕනෑම අවස්ථාවක මෙය අවලංගු කිරීමට ඔබට හැකියාව ඇත.", "Already a member?": "දැනටමත් සාමාජිකයෙක්ද?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "එම උත්සාහය අසාර්ථක විය. තව ටික වේලාවකින් නැවත උත්සාහ කරන්න, මෙම ගැටළුව තවදුරටත් පවතින්නේ නම්, සහායක අංශය සම්බන්ධ කරගන්න.", "Back": "ආපසු", "Back to Log in": "නැවත log in වීම සඳහා", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Spam සහ promotions folders පරීක්ෂා කරන්න", "Check with your mail provider": "ඔබගේ email සේවා සපයන්නා සමඟින් පරීක්ෂා කර බලන්න", + "Check your inbox to verify email update": "", "Choose": "තෝරන්න", "Choose a different plan": "වෙනත් plan එකක් තෝරන්න", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "සහායක අංශය සම්බන්ධ කරගන්න", "Continue": "ඉදිරියට යන්න", "Continue subscription": "Subscription එක පවත්වාගෙන යන්න", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Sign in වීමට නොහැකි විය. Login link එක කල් ඉකුත් වී ඇත.", "Could not update email! Invalid link.": "Email ලිපිනය update කළ නොහැකි විය. වැරදි link එකකි.", "Create a new contact": "අලුත් contact එකක් නිර්මාණය කරන්න", @@ -52,6 +56,7 @@ "Edit": "Edit කරන්න", "Email": "Email", "Email newsletter": "Email newsletter", + "Email newsletter settings updated": "", "Email preferences": "Email preferences", "Emails": "ඊමේල්", "Emails disabled": "Emails නවත්වා ඇත", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Error එකක්", "Expires {{expiryDate}}": "{{expiryDate}} දින අවසන් වෙයි", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "හැමදාටම", "Free Trial – Ends {{trialEnd}}": "අත්හදාබැලීමේ කාලසීමාව {{trialEnd}} දින අවසන් වෙයි", "Get help": "සහාය ලබාගන්න", @@ -91,6 +108,8 @@ "Name": "නම", "Need more help? Contact support": "තවදුරටත් සහාය අවශ්\u200dයයි ද? සහායක සේවාව සම්බන්ධ කරගන්න", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "ඔබගේ ගිණුමෙහි newsletters අක්\u200dරීය වීමට හේතු දෙකක් දැක්විය හැකිය: පෙර යවන ලද email එකක් spam ලෙස සටහන් කිරීම, හෝ email එකක් යැවීමට උත්සාහ කිරීමේදී permenent faulure (bounce) එකක් වීමක් වාර්ථා වීම.", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Emails ලැබෙන්නේ නැද්ද?", "Now check your email!": "දැන් ඔබගේ email එක පරික්ෂා කරන්න!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "නැවත subscribe කළ විටත් ඔබගේ inbox එකට email ලැබෙන්නේ නැතිනම්, spam folder එක පරීක්ෂා කරන්න. ඇතැම් සේවා සපයන්නන් මීට පෙර spam සම්බන්ධව ලැබුණු පැමිණිලි පාදක කරගෙන තවදුරටත් emails spam ලෙස ලකුණු කරනු ලබනවා. එසේ වී ඇත්නම්, අලුතින්ම ලැබුණු newsletter එක ඔබගේ primary inbox එකට යැවීමට 'Not spam' ලෙස සළකුනු කරන්න.", @@ -126,6 +145,7 @@ "Submit feedback": "ප්\u200dරතිචාරය යොමුකරන්න", "Subscribe": "Subscribe කරන්න", "Subscribed": "Subscribe කරන ලදී", + "Subscription plan updated successfully": "", "Success": "සාර්ථකයි", "Success! Check your email for magic link to sign-in.": "සාර්ථකයි! sign-in වීමේ magic link එක සඳහා ඔබ\u200dෙගේ emails පරීක්ෂා කරන්න.", "Success! Your account is fully activated, you now have access to all content.": "සාර්ථකයි! ඔබගේ ගිණුම සම්පූර්ණයෙන්ම activate කර ඇති අතර, දැන් ඔබට සියළුම content සඳහා access ලැබී තිබෙනවා.", @@ -138,12 +158,22 @@ "That didn't go to plan": "එය සැලැස්මට අනුකූලව සිදු වුණේ නෑ", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "ඔබගේ email ලිපිනය ලෙස අප සතුව තිබෙන්නේ {{memberEmail}} යන email ලිපිනයයි - මෙය වැරදියි නම්, ඔබගේ හරහා update කළ හැක.", "There was a problem submitting your feedback. Please try again a little later.": "ඔබගේ feedback එක යොමු කිරීමේදී ගැටළුවක් ඇති විය. තව ටික වේලාවකින් නැවත උත්සාහ කරන්න.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "මෙම වෙබ් අඩවිය ආරාධිතයන් සඳහා පමණි, ප්\u200dරවේශ වීම සඳහා හිමිකරු අමතන්න.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Signup වීම සම්පූර්ණ කිරීම සඳහා, ඔබ\u200dගේ inbox එකට ලැබුණු email එකෙහි ඇති confirmation link එක click කරන්න. එය මිනිත්තු 3ක් ඇතුලත නොපැමිණියේ නම්, spam folder එක පරීක්ෂා කරන්න!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "දින {{amount}}ක් නොමිලයේ භාවිතා කරන්න, ඉන් පසුව {{originalPrice}}ක් පමණි.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Paid subscriber කෙනෙකු වීම හරහා සියළුම newsletters වලට access ලබාගන්න.", "Unsubscribe from all emails": "සියළුම email වලින් unsubscribe කරන්න", "Unsubscribed": "Unsubscribe කරන ලදී", @@ -170,6 +200,7 @@ "You've successfully signed in.": "ඔබ සාර්ථකව sign in වන ලදී.", "You've successfully subscribed to": "ඔබ සාර්ථකව subscribe ක\u200bර ඇත", "Your account": "ඔබගේ ගිණුම", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "ඔබගේ අදහස් ඉදිරියේදී සිදු කරන පළකිරීම් වැඩිදියුණු කිරීමට උදව් කරනු ඇත.", "Your subscription will expire on {{expiryDate}}": "ඔබගේ subscription එක {{expiryDate}} වැනි දින කල් ඉකුත් වනු ඇත", "Your subscription will renew on {{renewalDate}}": "ඔබගේ subscription එක {{expiryDate}} වැනි දින renew වනු ඇත", diff --git a/ghost/i18n/locales/si/search.json b/ghost/i18n/locales/si/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/si/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/sk/comments.json b/ghost/i18n/locales/sk/comments.json index 3dae25127c6..4c0fe6b9be8 100644 --- a/ghost/i18n/locales/sk/comments.json +++ b/ghost/i18n/locales/sk/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Odpovedať na komentár", "Report": "Nahlásiť", "Report comment": "Nahlásiť komentár", - "Report this comment": "Nahlásiť tento komentár", "Report this comment?": "Nahlásiť tento komentár?", "Save": "Uložiť", "Sending": "Odosielanie", @@ -68,6 +67,5 @@ "This comment has been removed.": "Tento komentár bol vymazaný.", "Upgrade now": "Upgradovať teraz", "Yesterday": "Včera", - "You want to report this comment?": "Chcete nahlásiť tento komentár?", "Your request will be sent to the owner of this site.": "Vaša požiadavka bola odoslaná vlastníkovi stránky." } diff --git a/ghost/i18n/locales/sk/portal.json b/ghost/i18n/locales/sk/portal.json index be46a57f98c..09663c48748 100644 --- a/ghost/i18n/locales/sk/portal.json +++ b/ghost/i18n/locales/sk/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Prihlasovací odkaz bol odoslaný na váš e-mail. Ak nedorazi do 3 minút, skontrolujte priečinok so spamom", "Account": "Účet", + "Account details updated successfully": "", "Account settings": "Nastavenia účtu", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Po uplynutí skúšobného obdobia vám bute účtovaná bežná cena pre vybrunú úrovenň. Vždy to môžte pred tým zrušiť", "Already a member?": "Ste už členom?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Nastala neočakávaná chyba. Prosím skúste neskôr alebo kontaktujte podporu ak problém pretrváva.", "Back": "Späť", "Back to Log in": "Späť na prihlásenie", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Skontrolujte spam", "Check with your mail provider": "Overte si to s vašim e-mailovým poskytovateľom.", + "Check your inbox to verify email update": "", "Choose": "Vybrať", "Choose a different plan": "Vybrať iný plán", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Kontaktovať podporu", "Continue": "Pokračovať", "Continue subscription": "Pokračovať s odberom", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Prihlásenie sa nepodarilo. Odkaz na prihlásenie vyexpiroval.", "Could not update email! Invalid link.": "Zmena e-mailu sa nepodarila. Neplatný odkaz.", "Create a new contact": "Vytvoriť nový kontakt", @@ -52,6 +56,7 @@ "Edit": "Upraviť", "Email": "", "Email newsletter": "", + "Email newsletter settings updated": "", "Email preferences": "E-mailové nastavnia", "Emails": "E-maily", "Emails disabled": "E-maily vypnuté", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Chyba", "Expires {{expiryDate}}": "Expiruje {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Navždy", "Free Trial – Ends {{trialEnd}}": "Skúšobná verzia – Vyprší {{trialEnd}}", "Get help": "Získať pomoc", @@ -91,6 +108,8 @@ "Name": "Meno", "Need more help? Contact support": "Potrebujete pomoc? Kontaktujte podporu", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Nedostávate e-maily?", "Now check your email!": "Skontrolujte svoju emailovú schránku!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "", @@ -126,6 +145,7 @@ "Submit feedback": "Odoslať spätnú väzbu", "Subscribe": "Odoberať", "Subscribed": "Prihásený k odberu", + "Subscription plan updated successfully": "", "Success": "Blahoželáme", "Success! Check your email for magic link to sign-in.": "Super! Skontrolujte Vašu emailovú schránku, kde nájdete odkaz na prihlásenie.", "Success! Your account is fully activated, you now have access to all content.": "Super! Váš účet je aktivovaný. Máte prístup ku všetkému obsahu.", @@ -138,12 +158,22 @@ "That didn't go to plan": "Niečo sa nepodarilo", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "", "There was a problem submitting your feedback. Please try again a little later.": "Nepodarilo sa odoslať váš feedback. Prosím skúste to neskôr.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Táto stránka je iba pre pozvaných úžívateľov, kontaktujte vlastníka stránky.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Vyskúšajte zadarmo na {{amount}} dní, potom {{originalPrice}}", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "", "Unsubscribe from all emails": "Odhlásený z odberu všetkých email-ov", "Unsubscribed": "Odhlásený z odberu", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Úspešne ste sa prihlásili", "You've successfully subscribed to": "Úspešne ste sa prihlásili na odber:", "Your account": "Váš účet", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Vaše pripomienky pomáhajú spoluvytvárať obsah webu.", "Your subscription will expire on {{expiryDate}}": "Váše predplatné expiruje {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Váše predplatné bude obnovené {{renewalDate}}", diff --git a/ghost/i18n/locales/sk/search.json b/ghost/i18n/locales/sk/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/sk/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/sl/comments.json b/ghost/i18n/locales/sl/comments.json index c5391eab775..77ef1db9693 100644 --- a/ghost/i18n/locales/sl/comments.json +++ b/ghost/i18n/locales/sl/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "", "Report": "", "Report comment": "", - "Report this comment": "", "Report this comment?": "", "Save": "", "Sending": "", @@ -68,6 +67,5 @@ "This comment has been removed.": "", "Upgrade now": "", "Yesterday": "", - "You want to report this comment?": "", "Your request will be sent to the owner of this site.": "" } diff --git a/ghost/i18n/locales/sl/portal.json b/ghost/i18n/locales/sl/portal.json index f6d9d1e18f5..9b80d042f42 100644 --- a/ghost/i18n/locales/sl/portal.json +++ b/ghost/i18n/locales/sl/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Povezava za prijavo je bila poslana na vašo e-pošto. Če ne prispe v treh minutah, preverite mapo za neželeno pošto.", "Account": "Račun", + "Account details updated successfully": "", "Account settings": "Nastavitve računa", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Po koncu brezplačnega preizkusa se vam zaračuna redna cena za izbrano stopnjo. Pred tem jo lahko vedno prekličete.", "Already a member?": "Ste že član?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "", "Back": "Nazaj", "Back to Log in": "Nazaj na prijavo", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "", "Check with your mail provider": "", + "Check your inbox to verify email update": "", "Choose": "", "Choose a different plan": "Izberite drug načrt", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "", "Continue": "Nadaljuj", "Continue subscription": "", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "", "Could not update email! Invalid link.": "", "Create a new contact": "", @@ -52,6 +56,7 @@ "Edit": "", "Email": "E-pošta", "Email newsletter": "", + "Email newsletter settings updated": "", "Email preferences": "Nastavitve e-pošte", "Emails": "E-pošta", "Emails disabled": "E-pošta onemogočena", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "", "Expires {{expiryDate}}": "", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "", "Free Trial – Ends {{trialEnd}}": "", "Get help": "Pomoč", @@ -91,6 +108,8 @@ "Name": "Ime", "Need more help? Contact support": "", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Ne prejemate e-poštnih sporočil?", "Now check your email!": "Preverite e-pošto!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "", @@ -126,6 +145,7 @@ "Submit feedback": "Pošljite povratno informacijo", "Subscribe": "", "Subscribed": "", + "Subscription plan updated successfully": "", "Success": "", "Success! Check your email for magic link to sign-in.": "", "Success! Your account is fully activated, you now have access to all content.": "", @@ -138,12 +158,22 @@ "That didn't go to plan": "To ni šlo po načrtu", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "", "There was a problem submitting your feedback. Please try again a little later.": "", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "To spletno mesto je dostopno samo s povabilom, obrnite se na lastnika.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Dokončatje prijavo s klikom na povezavo, ki ste jo dobili na vaš e-poštni naslov. Če je ne prejmete v treh minutah, preverite mapo za neželeno pošto!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "", "Unsubscribe from all emails": "Odjava od vseh e-poštnih sporočil", "Unsubscribed": "", @@ -170,6 +200,7 @@ "You've successfully signed in.": "", "You've successfully subscribed to": "", "Your account": "Vaš račun", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Vaš prispevek se nam pomaga odločati, kaj objavimo.", "Your subscription will expire on {{expiryDate}}": "", "Your subscription will renew on {{renewalDate}}": "", diff --git a/ghost/i18n/locales/sl/search.json b/ghost/i18n/locales/sl/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/sl/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/sq/comments.json b/ghost/i18n/locales/sq/comments.json index c5391eab775..77ef1db9693 100644 --- a/ghost/i18n/locales/sq/comments.json +++ b/ghost/i18n/locales/sq/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "", "Report": "", "Report comment": "", - "Report this comment": "", "Report this comment?": "", "Save": "", "Sending": "", @@ -68,6 +67,5 @@ "This comment has been removed.": "", "Upgrade now": "", "Yesterday": "", - "You want to report this comment?": "", "Your request will be sent to the owner of this site.": "" } diff --git a/ghost/i18n/locales/sq/portal.json b/ghost/i18n/locales/sq/portal.json index eb889520460..5e776df5223 100644 --- a/ghost/i18n/locales/sq/portal.json +++ b/ghost/i18n/locales/sq/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Një lidhje identifikimi është dërguar në kutinë tuaj hyrëse. Nëse nuk arrin për 3 minuta, sigurohuni që të kontrolloni dosjen tuaj të postës së padëshiruar.", "Account": "Llogaria", + "Account details updated successfully": "", "Account settings": "Cilësimet e llogarisë", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Pasi të përfundojë një provë falas, ju do të tarifoheni me çmimin e rregullt për nivelin që keni zgjedhur. Mund të anuloni gjithmonë përpara kësaj.", "Already a member?": "Tashme nje antar?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Ndodhi një gabim i papritur. Provo sërish ose kontakto mbështetjen nëse gabimi vazhdon.", "Back": "Kthehu", "Back to Log in": "Kthehu tek identifikimi", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Kontrolloni dosjet e postës së padëshiruar dhe promovimeve ", "Check with your mail provider": "Kontrolloni me ofruesin tuaj të postës", + "Check your inbox to verify email update": "", "Choose": "Zgjidh", "Choose a different plan": "Zgjidh nje plan ndryshe", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Kontakto suportin", "Continue": "Vazhdoni", "Continue subscription": "Vazhdoni abonimin", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "", "Could not update email! Invalid link.": "", "Create a new contact": "Shto nje kontakt te ri", @@ -52,6 +56,7 @@ "Edit": "Modifiko", "Email": "Email", "Email newsletter": "Buletini i emailit", + "Email newsletter settings updated": "", "Email preferences": "Preferenat e emailit", "Emails": "Emailet", "Emails disabled": "Emailet e çaktivizuara", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Gabim", "Expires {{expiryDate}}": "Mbaron", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Pergjithmone", "Free Trial – Ends {{trialEnd}}": "Prova falas – mbaron {{trialEnd}}", "Get help": "Merr ndihme", @@ -91,6 +108,8 @@ "Name": "Emri", "Need more help? Contact support": "Te duhet me shume ndihme? Kontakto suportin", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Buletinet mund të çaktivizohen në llogarinë tuaj për dy arsye: Një email i mëparshëm u shënua si i padëshiruar, ose përpjekja për të dërguar një email rezultoi në një dështim të përhershëm (kercim).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Nuk po merr emaile?", "Now check your email!": "Kontrollo emailin tend tani!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Pasi të regjistroheni përsëri, nëse ende nuk i shihni emailet në kutinë tuaj hyrëse, kontrolloni dosjen tuaj të postës së padëshiruar. Disa ofrues të kutisë hyrëse mbajnë një regjistër të ankesave të mëparshme të postës së padëshiruar dhe do të vazhdojnë të raportojnë emailet. Nëse kjo ndodh, shëno buletinin më të fundit si 'Jo e padëshiruar' për ta zhvendosur atë në kutinë hyrëse kryesore.", @@ -126,6 +145,7 @@ "Submit feedback": "Dergo komente", "Subscribe": "Abonohu", "Subscribed": "Abonuar", + "Subscription plan updated successfully": "", "Success": "Sukses", "Success! Check your email for magic link to sign-in.": "Sukses! Kontrollo emailin tend per linkun magjik te indentifikimit.", "Success! Your account is fully activated, you now have access to all content.": "Sukses! Llogaria jote eshte plotesisht e aktivizuar, tashme ju keni akses ne te githe kontentin", @@ -138,12 +158,22 @@ "That didn't go to plan": "Kjo nuk shkoi sipas planit", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "", "There was a problem submitting your feedback. Please try again a little later.": "Pati një problem me dërgimin e komenteve tuaja. Ju lutemi provoni sërish pak më vonë.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Kjo faqe eshte vetem me ftesa, kontaktoni zoteruesin per akses.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Për të përfunduar regjistrimin, klikoni lidhjen e konfirmimit në kutinë tuaj hyrëse. Nëse nuk arrin brenda 3 minutash, kontrolloni dosjen tuaj të postës elektronike!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Provo falas per {{amount}} dite, pastaj {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Zhbllokoni aksesin per te gjitha buletinet duke u bere nje abonues me pagese.", "Unsubscribe from all emails": "Çregjistrohu nga të gjitha emailet", "Unsubscribed": "I çabonuar", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Ju jeni identifikuar me sukses.", "You've successfully subscribed to": "", "Your account": "Llogaria juar", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Të dhënat tuaja ndihmojnë në formimin e asaj që publikohet.", "Your subscription will expire on {{expiryDate}}": "Abonimi juaj do te skadoje ne {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Abonimi juaj to te rinovohet ne {{renewalDate}}", diff --git a/ghost/i18n/locales/sq/search.json b/ghost/i18n/locales/sq/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/sq/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/sr-Cyrl/comments.json b/ghost/i18n/locales/sr-Cyrl/comments.json index f52d6455193..e0025601243 100644 --- a/ghost/i18n/locales/sr-Cyrl/comments.json +++ b/ghost/i18n/locales/sr-Cyrl/comments.json @@ -2,8 +2,8 @@ "{{amount}} characters left": "Преостало још {{amount}} карактера", "{{amount}} comments": "{{amount}} коментара", "{{amount}} days ago": "пре {{amount}} дана", - "{{amount}} hours ago": "пре {{amount}} сати", - "{{amount}} minutes ago": "пре {{amount}} минута", + "{{amount}} hrs ago": "", + "{{amount}} mins ago": "", "{{amount}} months ago": "пре {{amount}} месеци", "{{amount}} more": "још {{amount}}", "{{amount}} seconds ago": "пре {{amount}} секунди", @@ -25,7 +25,7 @@ "Discussion": "Дискусија", "Edit": "Уреди", "Edit this comment": "Уредите овај коментар", - "Edited": "Измењено", + "edited": "", "Enter your name": "Унесите своје име", "Expertise": "Експертиза", "Founder @ Acme Inc": "Оснивач @ Acme Inc", @@ -42,7 +42,7 @@ "Neurosurgeon": "Неурохирург", "One day ago": "Пре један дан", "One hour ago": "Пре један сат", - "One minute ago": "Пре један минут", + "One min ago": "", "One month ago": "Пре један месец", "One week ago": "Пре једну недељу", "One year ago": "Пре једну годину", @@ -50,7 +50,6 @@ "Reply to comment": "Одговорите на коментар", "Report": "Пријави", "Report comment": "Пријави коментар", - "Report this comment": "Пријави овај коментар", "Report this comment?": "Пријавити овај коментар?", "Save": "Сачувај", "Sending": "Слање", @@ -68,6 +67,5 @@ "This comment has been removed.": "Овај коментар је уклоњен.", "Upgrade now": "Надоградите сада", "Yesterday": "Јуче", - "You want to report this comment?": "Желите да пријавите овај коментар?", "Your request will be sent to the owner of this site.": "Ваш захтев ће бити послат власнику овог сајта." } diff --git a/ghost/i18n/locales/sr-Cyrl/portal.json b/ghost/i18n/locales/sr-Cyrl/portal.json index b43f4f7bc18..abd67727e55 100644 --- a/ghost/i18n/locales/sr-Cyrl/portal.json +++ b/ghost/i18n/locales/sr-Cyrl/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "+1 (123) 456-7890", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Линк за пријаву је послат на вашу мејл адресу. Ако не стигне за 3 минута, проверите фасциклу за нежељену пошту.", "Account": "Налог", + "Account details updated successfully": "", "Account settings": "Подешавања налога", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Након што истекне бесплатан пробни период, наплатиће вам се редовна цена за ниво који сте одабрали. Увек можете отказати пре тог рока.", "Already a member?": "Већ сте члан?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Дошло је до неочекиване грешке. Покушајте поново или контактирајте подршку ако грешка потраје.", "Back": "Назад", "Back to Log in": "Назад на пријаву", @@ -25,10 +27,13 @@ "Cancel subscription": "Откажите претплату", "Cancellation reason": "Разлог отказивања", "Change": "Промените", + "Change plan": "", "Check spam & promotions folders": "Проверите фасцикле за нежељену пошту и промоције", "Check with your mail provider": "Проверите са вашим провајдером мејла", + "Check your inbox to verify email update": "", "Choose": "Изаберите", "Choose a different plan": "Изаберите другачији план", + "Choose a plan": "", "Choose your newsletters": "Изаберите своје билтене", "Click here to retry": "Кликните овде да покушате поново", "Close": "Затвори", @@ -40,6 +45,7 @@ "Contact support": "Контактирајте подршку", "Continue": "Наставите", "Continue subscription": "Наставите претплату", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Није могуће пријавити се. Линк за пријаву је истекао.", "Could not update email! Invalid link.": "Није могуће ажурирати мејл! Неважећи линк.", "Create a new contact": "Направите нови контакт", @@ -50,6 +56,7 @@ "Edit": "Уреди", "Email": "Мејл", "Email newsletter": "Мејл билтен", + "Email newsletter settings updated": "", "Email preferences": "Преференције за мејл", "Emails": "Мејлови", "Emails disabled": "Мејлови онемогућени", @@ -58,6 +65,18 @@ "Enter your name": "Унесите ваше име", "Error": "Грешка", "Expires {{expiryDate}}": "Истиче {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Заувек", "Free Trial – Ends {{trialEnd}}": "Бесплатан пробни период – Завршава се {{trialEnd}}", "Get help": "Затражите помоћ", @@ -83,11 +102,14 @@ "Manage": "Управљајте", "Maybe later": "Можда касније", "Memberships unavailable, contact the owner for access.": "Чланства нису доступна, контактирајте власника ради приступа.", + "month": "", "Monthly": "Месечно", "More like this": "Више оваквих", "Name": "Име", "Need more help? Contact support": "Треба вам више помоћи? Контактирајте подршку", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Билтени могу бити онемогућени на вашем налогу из два разлога: претходни мејл је означен као нежељена пошта или је покушај слања мејла резултирао трајним неуспехом (одбијен мејл).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Не примате мејлове?", "Now check your email!": "Сада проверите свој мејл!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Када се поново претплатите, ако и даље не видите мејлове у свом сандучету, проверите фасциклу за нежељену пошту. Неки провајдери сандучића чувају записе о претходним жалбама на нежељену пошту и наставиће да означавају мејлове. Ако се то деси, означите најновији билтен као 'Није нежељено' да га преместите назад у своје примарно сандуче.", @@ -123,6 +145,7 @@ "Submit feedback": "Пошаљите повратну информацију", "Subscribe": "Претплатите се", "Subscribed": "Претплаћени", + "Subscription plan updated successfully": "", "Success": "Успех", "Success! Check your email for magic link to sign-in.": "Успех! Проверите свој мејл за магични линк за пријаву.", "Success! Your account is fully activated, you now have access to all content.": "Успех! Ваш налог је потпуно активиран, сада имате приступ свим садржајима.", @@ -135,12 +158,22 @@ "That didn't go to plan": "То није ишло по плану", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Мејл адреса коју имамо за вас је {{memberEmail}} — ако то није тачно, можете је ажурирати у .", "There was a problem submitting your feedback. Please try again a little later.": "Дошло је до проблема при слању ваше повратне информације. Молимо вас покушајте касније.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "Дошло је до грешке при обради ваше уплате. Молимо вас покушајте поново.", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Овај сајт је само на позив, контактирајте власника ради приступа.", "This site is not accepting payments at the moment.": "Овај сајт тренутно не прихвата уплате.", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Да бисте завршили пријаву, кликните на линк за потврду у свом сандучету. Ако не стигне у року од 3 минута, проверите фасциклу за нежељену пошту!", "To continue to stay up to date, subscribe to {{publication}} below.": "Да бисте остали у току, претплатите се на {{publication}} у наставку.", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Пробајте бесплатно за {{amount}} дана, након тога {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Откључајте приступ свим билтенима тако што ћете постати плаћени претплатник.", "Unsubscribe from all emails": "Одјавите се са свих мејлова", "Unsubscribed": "Одјављени", @@ -157,6 +190,7 @@ "Welcome to {{siteTitle}}": "Добродошли на {{siteTitle}}", "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "Када сандуче не прихвата мејл, то се обично назива одбијањем. У многим случајевима то може бити привремено. Међутим, у неким случајевима, одбијени мејл може бити враћен као трајни неуспех ако је мејл адреса неважећа или непостојећа.", "Why has my email been disabled?": "Зашто је мој мејл онемогућен?", + "year": "", "Yearly": "Годишње", "You currently have a free membership, upgrade to a paid subscription for full access.": "Тренутно имате бесплатну претплату, надоградите на плаћену претплату за пуни приступ.", "You have been successfully resubscribed": "Успешно сте поново претплаћени", @@ -166,6 +200,7 @@ "You've successfully signed in.": "Успешно сте се пријавили.", "You've successfully subscribed to": "Успешно сте се претплатили на", "Your account": "Ваш налог", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Ваше учешће помаже у обликовању онога што ће бити објављено.", "Your subscription will expire on {{expiryDate}}": "Ваша претплата ће истећи {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Ваша претплата ће бити обновљена {{renewalDate}}", diff --git a/ghost/i18n/locales/sr-Cyrl/search.json b/ghost/i18n/locales/sr-Cyrl/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/sr-Cyrl/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/sr/comments.json b/ghost/i18n/locales/sr/comments.json index c5391eab775..77ef1db9693 100644 --- a/ghost/i18n/locales/sr/comments.json +++ b/ghost/i18n/locales/sr/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "", "Report": "", "Report comment": "", - "Report this comment": "", "Report this comment?": "", "Save": "", "Sending": "", @@ -68,6 +67,5 @@ "This comment has been removed.": "", "Upgrade now": "", "Yesterday": "", - "You want to report this comment?": "", "Your request will be sent to the owner of this site.": "" } diff --git a/ghost/i18n/locales/sr/portal.json b/ghost/i18n/locales/sr/portal.json index 3b6c7fe39b0..4614d209fbb 100644 --- a/ghost/i18n/locales/sr/portal.json +++ b/ghost/i18n/locales/sr/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Link za prijavljivanje je poslat na Vašu imejl adresu. Ukoliko ne stigne za 3 minuta, proverite folder sa nepoželjnim porukama.", "Account": "Nalog", + "Account details updated successfully": "", "Account settings": "Podešavanja naloga", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Nakon što besplatni probni period istekne, izvršiće se naplata po regularnoj ceni za nivo koji ste izabrali. Uvek možete da otkažete pretplatu pre toga.", "Already a member?": "Već ste član?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Desila se neočekivana greška. Molimo probajte opet ili kontaktirajte podršku ako greška i dalje postoji.", "Back": "Nazad", "Back to Log in": "Nazad na prijavu", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Proverite foldere za nepoželjne poruke i promocije", "Check with your mail provider": "Proverite sa svojim pružaocem imejl usluga", + "Check your inbox to verify email update": "", "Choose": "Izaberi", "Choose a different plan": "Izaberi drugi plan", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Kontaktiraj podršku", "Continue": "Nastavi", "Continue subscription": "Nastavi pretplatu", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Prijavljivanje nije uspelo. Link za prijavljivanje je istekao.", "Could not update email! Invalid link.": "Izmena mejla nije uspela! Neispravan link.", "Create a new contact": "Kreiraj novi kontakt", @@ -52,6 +56,7 @@ "Edit": "Izmeni", "Email": "Imejl", "Email newsletter": "Imejl bilten", + "Email newsletter settings updated": "", "Email preferences": "Imejl podešavanja", "Emails": "Imejlovi", "Emails disabled": "Onemogućeni imejlovi", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Greška", "Expires {{expiryDate}}": "Ističe {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Zauvek", "Free Trial – Ends {{trialEnd}}": "Besplatni probni period – Završava se {{trialEnd}}", "Get help": "Nađite pomoć", @@ -91,6 +108,8 @@ "Name": "Ime", "Need more help? Contact support": "Potrebna Vam je pomoć? Kontaktirajte podršku", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Ne dobijate e-poštu?", "Now check your email!": "Proverite svoj imejl!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "", @@ -126,6 +145,7 @@ "Submit feedback": "Pošalji povratne informacije", "Subscribe": "", "Subscribed": "", + "Subscription plan updated successfully": "", "Success": "", "Success! Check your email for magic link to sign-in.": "", "Success! Your account is fully activated, you now have access to all content.": "", @@ -138,12 +158,22 @@ "That didn't go to plan": "Nešto nije kako treba", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "", "There was a problem submitting your feedback. Please try again a little later.": "", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Ovaj sajt je samo za članove, kontaktirajte vlasnika kako bi dobili pristup.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Kliknite na link da biste završili registraciju. Ukoliko ne stigne za 3 minuta proverite spam folder!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "", "Unsubscribe from all emails": "Odjavite se sa svih email-ova", "Unsubscribed": "", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Uspešno ste se prijavili", "You've successfully subscribed to": "Uspešno ste se pretplatili na", "Your account": "Vaš nalog", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Vaš doprinos pomaže u oblikovanju onoga što se objavljuje.", "Your subscription will expire on {{expiryDate}}": "Vaša pretplata će isteći {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Vaša pretplata će biti obnovljena {{renewalDate}}", diff --git a/ghost/i18n/locales/sr/search.json b/ghost/i18n/locales/sr/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/sr/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/sv/comments.json b/ghost/i18n/locales/sv/comments.json index 33148f53382..418f1081039 100644 --- a/ghost/i18n/locales/sv/comments.json +++ b/ghost/i18n/locales/sv/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Svara på kommentar", "Report": "Anmäl", "Report comment": "Anmäl kommentar", - "Report this comment": "Anmäl den här kommentaren", "Report this comment?": "Anmäl den här kommentaren?", "Save": "Spara", "Sending": "Skickar", @@ -68,6 +67,5 @@ "This comment has been removed.": "Denna kommentar är borttagen", "Upgrade now": "Uppgradera nu", "Yesterday": "I går", - "You want to report this comment?": "Vill du anmäla denna kommentar?", "Your request will be sent to the owner of this site.": "Din anmälan skickas till ansvarig för webbplatsen" } diff --git a/ghost/i18n/locales/sv/portal.json b/ghost/i18n/locales/sv/portal.json index 6d5572203cb..d11ead5d641 100644 --- a/ghost/i18n/locales/sv/portal.json +++ b/ghost/i18n/locales/sv/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "En inloggningslänk har skickats till din inkorg. Om den inte anländer inom 3 minuter, kontrollera din skräppostmapp.", "Account": "Konto", + "Account details updated successfully": "", "Account settings": "Kontoinställningar", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Efter att en gratis provperiod avslutas debiteras du det ordinarie priset för den nivå du har valt. Du kan alltid avbryta innan dess.", "Already a member?": "Redan medlem?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Ett oväntat fel inträffade. Försök igen eller kontakta administratören om felet kvarstår.", "Back": "Tillbaka", "Back to Log in": "Tillbaka till inloggning", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "Titta i skräppostmappen", "Check with your mail provider": "Verifirera att e-posten fungerar för ditt konto", + "Check your inbox to verify email update": "", "Choose": "Välj", "Choose a different plan": "Välj en annan prenumeration", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "Kontakta supporten", "Continue": "Fortsätt", "Continue subscription": "Förläng prenumeration", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Kunde inte logga in. Inloggningslänken har slutat gälla.", "Could not update email! Invalid link.": "Kunde inte uppdatera e-postadressen. Länken fungerande inte.", "Create a new contact": "Skapa en ny kontakt", @@ -52,6 +56,7 @@ "Edit": "Editera", "Email": "E-post", "Email newsletter": "Nyhetsbrev via e-post", + "Email newsletter settings updated": "", "Email preferences": "E-postinställningar", "Emails": "E-postmeddelanden", "Emails disabled": "E-post inaktiverad", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "Fel", "Expires {{expiryDate}}": "Utgår {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Tillsvidare", "Free Trial – Ends {{trialEnd}}": "Gratispepriod – slutar {{trialEnd}}", "Get help": "Få hjälp", @@ -91,6 +108,8 @@ "Name": "Namn", "Need more help? Contact support": "Behöver du mer hjälp? Kontakta administratören.", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Nyhetsbrev kan inaktiveras på ditt konto av två anledningar: ett tidigare utskick markerades som spam, eller ett försök att skicka ett e-postmeddelande resulterade i ett permanent fel.", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Får du inga e-postmeddelanden?", "Now check your email!": "Kolla nu din e-post!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Om du fortfarande inte ser e-post i din inkorg efter att du återaktiverat utskicken, kontrollera din skräppostmapp. Vissa e-postleverantörer behåller en historik över tidigare spamklagomål och fortsätter att markera e-post som spam. Om detta händer, markera det senaste nyhetsbrevet som 'Inte spam' för att flytta tillbaka det till din huvudsakliga inkorg.", @@ -126,6 +145,7 @@ "Submit feedback": "Skicka feedback", "Subscribe": "Anmäl dig", "Subscribed": "Anmäld", + "Subscription plan updated successfully": "", "Success": "Det gick bra", "Success! Check your email for magic link to sign-in.": "Det gick bra. Titta i din e-post efter ett meddelande från oss med en inloggningslänk.", "Success! Your account is fully activated, you now have access to all content.": "Det gick bra! Ditt konto är uppdaterat och du har tillgång till allt material.", @@ -138,12 +158,22 @@ "That didn't go to plan": "Det där fungerade inte som tänkt", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "E-postadressen vi har för dig är {{memberEmail}} — om det inte stämmer kan du uppdatera den i .", "There was a problem submitting your feedback. Please try again a little later.": "Det fungerande inte att skicka in din feedbak. Försök igen lite senare.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Den här sidan är endast för inbjudna, kontakta ägaren för åtkomst.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "För att slutföra registreringen, klicka på bekräftelselänken i din inkorg. Om den inte kommer fram inom 3 minuter, kolla din skräppostmapp!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "Prova gratis i {{amount}} dagar, sen betalar du {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Få tillgång till alla nyhetsbrev genom att bli betalande prenumerant", "Unsubscribe from all emails": "Avregistrera från alla e-postutskick", "Unsubscribed": "Avregistrerad", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Du är nu inloggad.", "You've successfully subscribed to": "Du är nu anmäld till", "Your account": "Ditt konto", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Din åsikt hjälper till att forma vad som publiceras.", "Your subscription will expire on {{expiryDate}}": "Din prenumeration avslutas {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "Din prenumeration förnyas {{renewalDate}}", diff --git a/ghost/i18n/locales/sv/search.json b/ghost/i18n/locales/sv/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/sv/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/sw/comments.json b/ghost/i18n/locales/sw/comments.json new file mode 100644 index 00000000000..41112f5551e --- /dev/null +++ b/ghost/i18n/locales/sw/comments.json @@ -0,0 +1,71 @@ +{ + "{{amount}} characters left": "{{amount}} herufi zimebaki", + "{{amount}} comments": "{{amount}} maoni", + "{{amount}} days ago": "siku {{amount}} zilizopita", + "{{amount}} hrs ago": "", + "{{amount}} mins ago": "", + "{{amount}} months ago": "miezi {{amount}} iliyopita", + "{{amount}} more": "{{amount}} zaidi", + "{{amount}} seconds ago": "sekunde {{amount}} zilizopita", + "{{amount}} weeks ago": "wiki {{amount}} zilizopita", + "{{amount}} years ago": "miaka {{amount}} iliyopita", + "1 comment": "maoni 1", + "Add comment": "Ongeza maoni", + "Add context to your comment, share your name and expertise to foster a healthy discussion.": "Ongeza muktadha kwa maoni yako, shiriki jina lako na utaalamu ili kuendeleza mjadala wenye afya.", + "Add reply": "Ongeza jibu", + "Already a member?": "Tayari ni mwanachama?", + "Anonymous": "Asiyetambulika", + "Become a member of {{publication}} to start commenting.": "Jiunge kama mwanachama wa {{publication}} ili kuanza kutoa maoni.", + "Become a paid member of {{publication}} to start commenting.": "Jiunge kama mwanachama anayelipa wa {{publication}} ili kuanza kutoa maoni.", + "Cancel": "Ghairi", + "Comment": "Maoni", + "Complete your profile": "Kamilisha wasifu wako", + "Delete": "Futa", + "Deleted member": "Mwanachama aliyefutwa", + "Discussion": "Majadiliano", + "Edit": "Hariri", + "Edit this comment": "Hariri maoni haya", + "edited": "", + "Enter your name": "Weka jina lako", + "Expertise": "Utaalamu", + "Founder @ Acme Inc": "Mwanzilishi @ Acme Inc", + "Full-time parent": "Mzazi wa muda wote", + "Head of Marketing at Acme, Inc": "Mkuu wa Masoko katika Acme, Inc", + "Hide": "Ficha", + "Hide comment": "Ficha maoni", + "Jamie Larson": "Jamie Larson", + "Join the discussion": "Jiunge na mjadala", + "Just now": "Hivi sasa", + "Local resident": "Mkazi wa eneo", + "Member discussion": "Mjadala wa wanachama", + "Name": "Jina", + "Neurosurgeon": "Daktari wa ubongo", + "One day ago": "Siku moja iliyopita", + "One hour ago": "Saa moja iliyopita", + "One min ago": "", + "One month ago": "Mwezi mmoja uliopita", + "One week ago": "Wiki moja iliyopita", + "One year ago": "Mwaka mmoja uliopita", + "Reply": "Jibu", + "Reply to comment": "Jibu maoni", + "Report": "Ripoti", + "Report comment": "Ripoti maoni", + "Report this comment?": "Unataka kuripoti maoni haya?", + "Save": "Hifadhi", + "Sending": "Inatuma", + "Sent": "Imetumwa", + "Show": "Onyesha", + "Show {{amount}} more replies": "Onyesha majibu {{amount}} zaidi", + "Show {{amount}} previous comments": "Onyesha maoni {{amount}} ya awali", + "Show 1 more reply": "Onyesha jibu 1 zaidi", + "Show 1 previous comment": "Onyesha maoni 1 ya awali", + "Show comment": "Onyesha maoni", + "Sign in": "Ingia", + "Sign up now": "Jisajili sasa", + "Start the conversation": "Anzisha mazungumzo", + "This comment has been hidden.": "Maoni haya yamefichwa.", + "This comment has been removed.": "Maoni haya yameondolewa.", + "Upgrade now": "Boresha sasa", + "Yesterday": "Jana", + "Your request will be sent to the owner of this site.": "Ombi lako litatumwa kwa mmiliki wa tovuti hii." +} diff --git a/ghost/i18n/locales/sw/ghost.json b/ghost/i18n/locales/sw/ghost.json new file mode 100644 index 00000000000..b24aeb280b0 --- /dev/null +++ b/ghost/i18n/locales/sw/ghost.json @@ -0,0 +1,34 @@ +{ + "All the best!": "Kila la kheri!", + "Complete signup for {{siteTitle}}!": "Kamilisha usajili kwa {{siteTitle}}!", + "Complete your sign up to {{siteTitle}}!": "Kamilisha usajili wako kwa {{siteTitle}}!", + "Confirm email address": "Thibitisha anwani ya barua pepe", + "Confirm signup": "Thibitisha usajili", + "Confirm your email address": "Thibitisha anwani yako ya barua pepe", + "Confirm your email update for {{siteTitle}}!": "Thibitisha masasisho ya barua pepe yako kwa {{siteTitle}}!", + "Confirm your subscription to {{siteTitle}}": "Thibitisha usajili wako kwa {{siteTitle}}", + "For your security, the link will expire in 24 hours time.": "Kwa usalama wako, kiungo hiki kitaisha muda baada ya saa 24.", + "Hey there,": "Habari,", + "Hey there!": "Habari!", + "If you did not make this request, you can safely ignore this email.": "Ikiwa hukufanya ombi hili, unaweza kupuuza barua pepe hii kwa usalama.", + "If you did not make this request, you can simply delete this message.": "Ikiwa hukufanya ombi hili, unaweza kufuta ujumbe huu tu.", + "Please confirm your email address with this link:": "Tafadhali thibitisha anwani yako ya barua pepe na kiungo hiki:", + "Secure sign in link for {{siteTitle}}": "Kiungo salama cha kuingia kwa {{siteTitle}}", + "See you soon!": "Tutaonana hivi karibuni!", + "Sent to {{email}}": "Imetumwa kwa {{email}}", + "Sign in": "Ingia", + "Sign in to {{siteTitle}}": "Ingia kwa {{siteTitle}}", + "Tap the link below to complete the signup process for {{siteTitle}}, and be automatically signed in:": "Gusa kiungo kilicho chini kumaliza mchakato wa usajili kwa {{siteTitle}}, na utaingia kiotomati:", + "Thank you for signing up to {{siteTitle}}!": "Asante kwa kujisajili kwa {{siteTitle}}!", + "Thank you for subscribing to {{siteTitle}}!": "Asante kwa kujiunga na {{siteTitle}}!", + "Thank you for subscribing to {{siteTitle}}.": "Asante kwa kujiunga na {{siteTitle}}.", + "Thank you for subscribing to {{siteTitle}}. Tap the link below to be automatically signed in:": "Asante kwa kujiunga na {{siteTitle}}. Gusa kiungo kilicho chini kujiandikisha kiotomati:", + "This email address will not be used.": "Anwani hii ya barua pepe haitatumika.", + "Welcome back to {{siteTitle}}!": "Karibu tena kwa {{siteTitle}}!", + "Welcome back! Use this link to securely sign in to your {{siteTitle}} account:": "Karibu tena! Tumia kiungo hiki kuingia kwa usalama kwenye akaunti yako ya {{siteTitle}}:", + "You can also copy & paste this URL into your browser:": "Unaweza pia kunakili na kubandika URL hii kwenye kivinjari chako:", + "You will not be signed up, and no account will be created for you.": "Hutajiandikishwa, na hakuna akaunti itakayoundwa kwa ajili yako.", + "You will not be subscribed.": "Hutajiandikisha.", + "You're one tap away from subscribing to {{siteTitle}} — please confirm your email address with this link:": "Uko umbali wa mguso mmoja tu kujiunga na {{siteTitle}} — tafadhali thibitisha anwani yako ya barua pepe na kiungo hiki:", + "You're one tap away from subscribing to {{siteTitle}}!": "Uko umbali wa mguso mmoja tu kujiunga na {{siteTitle}}!" +} diff --git a/ghost/i18n/locales/sw/portal.json b/ghost/i18n/locales/sw/portal.json new file mode 100644 index 00000000000..95aba456d32 --- /dev/null +++ b/ghost/i18n/locales/sw/portal.json @@ -0,0 +1,208 @@ +{ + "(save {{highestYearlyDiscount}}%)": "(okoa {{highestYearlyDiscount}}%)", + "{{amount}} days free": "siku {{amount}} bila malipo", + "{{amount}} off": "punguzo la {{amount}}", + "{{amount}} off for first {{number}} months.": "punguzo la {{amount}} kwa miezi {{number}} ya kwanza.", + "{{amount}} off for first {{period}}.": "punguzo la {{amount}} kwa {{period}} ya kwanza.", + "{{amount}} off forever.": "punguzo la {{amount}} milele.", + "{{discount}}% discount": "punguzo la {{discount}}%", + "{{memberEmail}} will no longer receive {{newsletterName}} newsletter.": "{{memberEmail}} haitapokea jarida la {{newsletterName}} tena.", + "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "{{memberEmail}} haitapokea barua pepe tena wakati mtu anapojibu maoni yako.", + "{{memberEmail}} will no longer receive this newsletter.": "{{memberEmail}} haitapokea jarida hili tena.", + "{{trialDays}} days free": "siku {{trialDays}} bila malipo", + "+1 (123) 456-7890": "", + "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Kiungo cha kuingia kimetumwa kwenye inbox yako. Kama hakifiki ndani ya dakika 3, hakikisha unakagua folda yako ya spam.", + "Account": "Akaunti", + "Account details updated successfully": "", + "Account settings": "Mipangilio ya akaunti", + "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Baada ya kipindi cha majaribio bila malipo kukamilika, utatozwa bei ya kawaida kwa kiwango ulichochagua. Unaweza kughairi kabla ya hapo.", + "Already a member?": "Tayari ni mwanachama?", + "An error occurred": "", + "An unexpected error occured. Please try again or contact support if the error persists.": "Kumetokea hitilafu isiyotarajiwa. Tafadhali jaribu tena au wasiliana na usaidizi ikiwa hitilafu itaendelea.", + "Back": "Rudi", + "Back to Log in": "Rudi kuingia", + "Billing info": "Taarifa za malipo", + "Black Friday": "Ijumaa ya punguzo (Black Friday)", + "Cancel anytime.": "Ghairi wakati wowote.", + "Cancel subscription": "Ghairi usajili", + "Cancellation reason": "Sababu ya kughairi", + "Change": "Badilisha", + "Change plan": "", + "Check spam & promotions folders": "Kagua folda za spam & matangazo", + "Check with your mail provider": "Thibitisha na mtoa huduma wako wa barua", + "Check your inbox to verify email update": "", + "Choose": "Chagua", + "Choose a different plan": "Chagua mpango tofauti", + "Choose a plan": "", + "Choose your newsletters": "Chagua majarida yako", + "Click here to retry": "Bofya hapa kurudia", + "Close": "Funga", + "Comments": "Maoni", + "Complimentary": "Bure", + "Confirm": "Thibitisha", + "Confirm cancellation": "Thibitisha kughairi", + "Confirm subscription": "Thibitisha usajili", + "Contact support": "Wasiliana na wasaidizi", + "Continue": "Endelea", + "Continue subscription": "Endelea na usajili", + "Could not create stripe checkout session": "", + "Could not sign in. Login link expired.": "Haikuweza kuingia. Kiungo cha kuingia kimeisha muda.", + "Could not update email! Invalid link.": "Haikuweza kusasisha barua pepe! Kiungo batili.", + "Create a new contact": "Unda mawasiliano mapya", + "Current plan": "Mpango wa sasa", + "Delete account": "Futa akaunti", + "Didn't mean to do this? Manage your preferences .": "Hukumaanisha kufanya hivi? Dhibiti mapendeleo yako .", + "Don't have an account?": "Huna akaunti?", + "Edit": "Hariri", + "Email": "Barua pepe", + "Email newsletter": "Jarida la barua pepe", + "Email newsletter settings updated": "", + "Email preferences": "Mapendeleo ya barua pepe", + "Emails": "Barua pepe", + "Emails disabled": "Barua pepe zimezimwa", + "Ends {{offerEndDate}}": "Inaisha {{offerEndDate}}", + "Enter your email address": "", + "Enter your name": "", + "Error": "Hitilafu", + "Expires {{expiryDate}}": "Inaisha {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", + "Forever": "Milele", + "Free Trial – Ends {{trialEnd}}": "Jaribio la Bure – linaisha {{trialEnd}}", + "Get help": "Pata msaada", + "Get in touch for help": "Wasiliana kupata msaada", + "Get notified when someone replies to your comment": "Pata taarifa wakati mtu anapojibu maoni yako", + "Give feedback on this post": "Toa maoni kuhusu chapisho hili", + "Help! I'm not receiving emails": "Msaada! Sipokei barua pepe", + "Here are a few other sites you may enjoy.": "Hapa kuna tovuti chache nyingine unazoweza kufurahia.", + "If a newsletter is flagged as spam, emails are automatically disabled for that address to make sure you no longer receive any unwanted messages.": "Ikiwa jarida limewekwa alama kama spam, barua pepe zinazimwa kiotomatiki kwa anwani hiyo kuhakikisha haupokei ujumbe usiotakiwa tena.", + "If the spam complaint was accidental, or you would like to begin receiving emails again, you can resubscribe to emails by clicking the button on the previous screen.": "Ikiwa malalamiko ya spam yalikuwa ya bahati mbaya, au ungependa kuanza kupokea barua pepe tena, unaweza kujiandikisha tena kwa barua pepe kwa kubofya kitufe kwenye skrini iliyopita.", + "If you cancel your subscription now, you will continue to have access until {{periodEnd}}.": "Ikiwa utaghairi usajili wako sasa, utaendelea kupata huduma hadi {{periodEnd}}.", + "If you have a corporate or government email account, reach out to your IT department and ask them to allow emails to be received from {{senderEmail}}": "Ikiwa una akaunti ya barua pepe ya kampuni au serikali, wasiliana na idara yako ya IT na uwaombe waruhusu kupokea barua pepe kutoka {{senderEmail}}", + "If you would like to start receiving emails again, the best next steps are to check your email address on file for any issues and then click resubscribe on the previous screen.": "Ikiwa ungependa kuanza kupokea barua pepe tena, hatua bora zinazofuata ni kukagua anwani yako ya barua pepe kwa ajili ya matatizo yoyote kisha ubofye jiandikishe tena kwenye skrini iliyopita.", + "If you're not receiving the email newsletter you've subscribed to, here are a few things to check.": "Ikiwa hupokei jarida la barua pepe ulilojiandikisha nalo, hapa kuna mambo machache ya kukagua.", + "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "Ikiwa umekamilisha ukaguzi huu wote na bado hupokei barua pepe, unaweza kupata usaidizi kwa kuwasiliana na {{supportAddress}}.", + "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "Ikiwa itapokea hitilafu ya kudumu wakati wa kujaribu kutuma jarida, barua pepe zitazimwa kwenye akaunti.", + "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "Kwenye mteja wako wa barua pepe ongeza {{senderEmail}} kwenye orodha yako ya mawasiliano. Hii inaashiria kwa mtoa huduma wako wa barua kwamba barua pepe zilizotumwa kutoka anwani hii zinapaswa kuaminika.", + "Invalid email address": "", + "Jamie Larson": "", + "jamie@example.com": "", + "Less like this": "Punguza kama hii", + "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "Hakikisha barua pepe hazijakosekana kufikia folda za Spam au Matangazo ya kikasha chako. Ikiwa ziko, bonyeza \"Alama kama si spam\" na/au \"Hamisha kwenye kikasha\".", + "Manage": "Dhibiti", + "Maybe later": "Labda baadaye", + "Memberships unavailable, contact the owner for access.": "Uanachama haupatikani, wasiliana na mmiliki kupata ufikiaji.", + "month": "", + "Monthly": "Kila mwezi", + "More like this": "Zaidi kama hii", + "Name": "Jina", + "Need more help? Contact support": "Unahitaji msaada zaidi? Wasiliana na usaidizi", + "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Majarida yanaweza kuzimwa kwenye akaunti yako kwa sababu mbili: Barua pepe ya awali iliyowekwa alama kama spam, au jaribio la kutuma barua pepe lilisababisha hitilafu ya kudumu (bounce).", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", + "Not receiving emails?": "Hupokei barua pepe?", + "Now check your email!": "Sasa angalia barua pepe yako!", + "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Mara baada ya kujiandikisha tena, ikiwa bado huoni barua pepe kwenye kikasha chako, kagua folda yako ya spam. Watoa huduma wengine wa kikasha huweka rekodi ya malalamiko ya awali ya spam na wataendelea kuweka alama kwenye barua pepe. Ikiwa hili litatokea, weka alama kwenye jarida la hivi karibuni kama 'Si spam' ili kuirudisha kwenye kikasha chako kikuu.", + "Permanent failure (bounce)": "Hitilafu ya kudumu (bounce)", + "Phone number": "", + "Plan": "Mpango", + "Plan checkout was cancelled.": "Malipo ya mpango yalighairiwa.", + "Plan upgrade was cancelled.": "Uboreshaji wa mpango ulighairiwa.", + "Please contact {{supportAddress}} to adjust your complimentary subscription.": "Tafadhali wasiliana na {{supportAddress}} kurekebisha usajili wako wa bure.", + "Please enter {{fieldName}}": "", + "Please fill in required fields": "Tafadhali jaza sehemu zinazohitajika", + "Price": "Bei", + "Re-enable emails": "Washa tena barua pepe", + "Recommendations": "Mapendekezo", + "Renews at {{price}}.": "Inajirudia kwa bei ya {{price}}.", + "Retry": "Jaribu tena", + "Save": "Hifadhi", + "Send an email and say hi!": "Tuma barua pepe na sema hi!", + "Send an email to {{senderEmail}} and say hello. This can also help signal to your mail provider that emails to and from this address should be trusted.": "Tuma barua pepe kwa {{senderEmail}} na sema hello. Hii pia inaweza kusaidia kuashiria kwa mtoa huduma wako wa barua kwamba barua pepe zinazotoka na kuingia kwenye anwani hii zinapaswa kuaminika.", + "Sending login link...": "Inatuma kiungo cha kuingia...", + "Sending...": "Inatuma...", + "Show all": "Onyesha yote", + "Sign in": "Ingia", + "Sign out": "Toka", + "Sign up": "Jisajili", + "Signup error: Invalid link": "Kosa la usajili: Kiungo batili", + "Something went wrong, please try again later.": "", + "Sorry, that didn’t work.": "Samahani, hiyo haikufanya kazi.", + "Spam complaints": "Malalamiko ya Spam", + "Start {{amount}}-day free trial": "Anza majaribio ya siku {{amount}} bila malipo", + "Starting {{startDate}}": "Inaanza {{startDate}}", + "Starting today": "Inaanza leo", + "Submit feedback": "Tuma maoni", + "Subscribe": "Jiunge", + "Subscribed": "Umejiunga", + "Subscription plan updated successfully": "", + "Success": "Mafanikio", + "Success! Check your email for magic link to sign-in.": "Mafanikio! Angalia barua pepe yako kwa kiungo cha kuingia.", + "Success! Your account is fully activated, you now have access to all content.": "Mafanikio! Akaunti yako imeamilishwa kikamilifu, sasa una ufikiaji wa maudhui yote.", + "Success! Your email is updated.": "Mafanikio! Barua pepe yako imesasishwa.", + "Successfully unsubscribed": "Umejiondoa kwa mafanikio", + "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "Asante kwa kujiunga. Kabla hujanza kusoma, hapa chini ni tovuti chache nyingine unazoweza kufurahia.", + "Thank you for your support": "", + "Thank you for your support!": "", + "Thanks for the feedback!": "Asante kwa maoni!", + "That didn't go to plan": "Hiyo haikwenda kama ilivyopangwa", + "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Anwani ya barua pepe tuliyonayo kwako ni {{memberEmail}} — ikiwa hiyo si sahihi, unaweza kuisasisha kwenye yako.", + "There was a problem submitting your feedback. Please try again a little later.": "Kulikuwa na tatizo la kutuma maoni yako. Tafadhali jaribu tena baadaye kidogo.", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", + "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", + "This site is invite-only, contact the owner for access.": "Tovuti hii ni ya mialiko pekee, wasiliana na mmiliki kupata ufikiaji.", + "This site is not accepting payments at the moment.": "", + "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Kukamilisha usajili, bonyeza kiungo cha uthibitisho kwenye kikasha chako. Kama hakifiki ndani ya dakika 3, kagua folda yako ya spam!", + "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Try free for {{amount}} days, then {{originalPrice}}.": "Jaribu bila malipo kwa siku {{amount}}, kisha {{originalPrice}}.", + "Unable to initiate checkout session": "", + "Unlock access to all newsletters by becoming a paid subscriber.": "Fungua ufikiaji wa majarida yote kwa kuwa mwanachama anayelipa.", + "Unsubscribe from all emails": "Jiondoe kwenye barua pepe zote", + "Unsubscribed": "Umejiondoa", + "Unsubscribed from all emails.": "Umejiondoa kwenye barua pepe zote.", + "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "Kujiondoa kutoka barua pepe hakuwezi kughairi usajili wako uliolipwa wa {{title}}", + "Update": "Sasisha", + "Update your preferences": "Sasisha mapendeleo yako", + "Verification link sent, check your inbox": "Kiungo cha uthibitisho kimetumwa, angalia kikasha chako", + "Verify your email address is correct": "Hakikisha anwani yako ya barua pepe ni sahihi", + "View plans": "Tazama mipango", + "We couldn't unsubscribe you as the email address was not found. Please contact the site owner.": "Hatukuweza kukuondoa kwenye usajili kwa sababu anwani ya barua pepe haikupatikana. Tafadhali wasiliana na mmiliki wa tovuti.", + "Welcome back, {{name}}!": "Karibu tena, {{name}}!", + "Welcome back!": "Karibu tena!", + "Welcome to {{siteTitle}}": "Karibu kwa {{siteTitle}}", + "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "Wakati kikasha kinaposhindwa kupokea barua pepe kwa kawaida huitwa bounce. Mara nyingi, hii inaweza kuwa ya muda. Hata hivyo, katika baadhi ya matukio, barua pepe iliyorudishwa inaweza kuonekana kama hitilafu ya kudumu wakati anwani ya barua pepe ni batili au haipo.", + "Why has my email been disabled?": "Kwa nini barua pepe yangu imezimwa?", + "year": "", + "Yearly": "Kila mwaka", + "You currently have a free membership, upgrade to a paid subscription for full access.": "Kwa sasa una uanachama bila malipo, boresha kuwa usajili uliolipwa kwa ufikiaji kamili.", + "You have been successfully resubscribed": "Umejiandikisha tena kwa mafanikio", + "You're currently not receiving emails": "Kwa sasa hupokei barua pepe", + "You're not receiving emails": "Hupokei barua pepe", + "You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "Hupokei barua pepe kwa sababu umeashiria ujumbe wa hivi karibuni kama spam, au kwa sababu ujumbe haukuweza kuwasilishwa kwenye anwani yako ya barua pepe iliyotolewa.", + "You've successfully signed in.": "Umeingia kwa mafanikio.", + "You've successfully subscribed to": "Umejiunga kwa mafanikio na", + "Your account": "Akaunti yako", + "Your email has failed to resubscribe, please try again": "", + "Your input helps shape what gets published.": "Maoni yako yanasaidia kuunda yaliyochapishwa.", + "Your subscription will expire on {{expiryDate}}": "Usajili wako utaisha tarehe {{expiryDate}}", + "Your subscription will renew on {{renewalDate}}": "Usajili wako utaongezwa tarehe {{renewalDate}}", + "Your subscription will start on {{subscriptionStart}}": "Usajili wako utaanza tarehe {{subscriptionStart}}" +} diff --git a/ghost/i18n/locales/sw/search.json b/ghost/i18n/locales/sw/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/sw/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/sw/signup-form.json b/ghost/i18n/locales/sw/signup-form.json new file mode 100644 index 00000000000..6525ad3582f --- /dev/null +++ b/ghost/i18n/locales/sw/signup-form.json @@ -0,0 +1,9 @@ +{ + "Email sent": "Barua pepe imetumwa", + "Now check your email!": "Sasa angalia barua pepe yako!", + "Please enter a valid email address": "Tafadhali weka anwani sahihi ya barua pepe", + "Something went wrong, please try again.": "Kumetokea hitilafu, tafadhali jaribu tena.", + "Subscribe": "Jiunge", + "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Kukamilisha usajili, bonyeza kiungo cha uthibitisho kwenye inbox yako. Kama hakifiki ndani ya dakika 3, kagua folda yako ya spam!", + "Your email address": "Anwani yako ya barua pepe" +} diff --git a/ghost/i18n/locales/th/comments.json b/ghost/i18n/locales/th/comments.json index 3c133253d1f..dfb02a31e54 100644 --- a/ghost/i18n/locales/th/comments.json +++ b/ghost/i18n/locales/th/comments.json @@ -2,8 +2,8 @@ "{{amount}} characters left": "คงเหลือ {{amount}} ตัวอักษร", "{{amount}} comments": "{{amount}} ความคิดเห็น", "{{amount}} days ago": "{{amount}} วันที่แล้ว", - "{{amount}} hours ago": "{{amount}} ชั่วโมงที่แล้ว", - "{{amount}} minutes ago": "{{amount}} นาทีที่แล้ว", + "{{amount}} hrs ago": "", + "{{amount}} mins ago": "", "{{amount}} months ago": "{{amount}} เดือนที่แล้ว", "{{amount}} more": "อีก {{amount}}", "{{amount}} seconds ago": "{{amount}} วินาทีที่แล้ว", @@ -25,7 +25,7 @@ "Discussion": "การสนทนา", "Edit": "แก้ไข", "Edit this comment": "แก้ไขความคิดเห็นนี้", - "Edited": "แก้ไขแล้ว", + "edited": "", "Enter your name": "ระบุชื่อของคุณ", "Expertise": "ความเชี่ยวชาญ", "Founder @ Acme Inc": "ผู้ก่อตั้ง @ Acme Inc", @@ -42,7 +42,7 @@ "Neurosurgeon": "ศัลยแพทย์ระบบประสาท", "One day ago": "1 วันที่แล้ว", "One hour ago": "1 ชั่วโมงที่แล้ว", - "One minute ago": "1 นาทีที่แล้ว", + "One min ago": "", "One month ago": "1 เดือนที่แล้ว", "One week ago": "1 สัปดาห์ที่แล้ว", "One year ago": "1 ปีที่แล้ว", @@ -50,7 +50,6 @@ "Reply to comment": "ตอบกลับความคิดเห็น", "Report": "รายงาน", "Report comment": "รายงานความคิดเห็น", - "Report this comment": "รายงานความคิดเห็นนี้", "Report this comment?": "รายงานความคิดเห็นนี้?", "Save": "บันทึก", "Sending": "กำลังส่ง", @@ -68,6 +67,5 @@ "This comment has been removed.": "ความคิดเห็นนี้ถูกลบ", "Upgrade now": "อัพเกรดตอนนี้เลย", "Yesterday": "เมื่อวาน", - "You want to report this comment?": "ต้องการรายงานความคิดเห็นนี้?", "Your request will be sent to the owner of this site.": "คำขอของคุณจะถูกส่งไปยังผู้ดูแลเว็บไซต์" } diff --git a/ghost/i18n/locales/th/portal.json b/ghost/i18n/locales/th/portal.json index cc6f94ab3c7..1960bc9bede 100644 --- a/ghost/i18n/locales/th/portal.json +++ b/ghost/i18n/locales/th/portal.json @@ -1,4 +1,5 @@ { + "(save {{highestYearlyDiscount}}%)": "", "{{amount}} days free": "ฟรี {{amount}} วัน", "{{amount}} off": "ลด {{amount}}", "{{amount}} off for first {{number}} months.": "ลด {{amount}} สำหรับ {{number}} เดือนแรก", @@ -9,11 +10,14 @@ "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "{{memberEmail}} จะไม่ได้รับอีเมลอีกต่อไป เมื่อมีคนตอบกลับความคิดเห็นของคุณ", "{{memberEmail}} will no longer receive this newsletter.": "{{memberEmail}} จะไม่ได้รับจดหมายข่าวนี้อีกต่อไป", "{{trialDays}} days free": "ฟรี {{trialDays}} วัน", + "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "ลิงค์เข้าสู่ระบบถูกส่งไปยังกล่องจดหมายของคุณแล้ว หากไม่ได้รับภายใน 3 นาที โปรดตรวจสอบโฟลเดอร์สแปมของคุณ", "Account": "บัญชี", + "Account details updated successfully": "", "Account settings": "ตั้งค่าบัญชี", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "หลังจากช่วงทดลองใช้ฟรีสิ้นสุดลง, คุณจะถูกเรียกเก็บเงินตามราคาปกติตามระดับที่คุณเลือกไว้ คุณสามารถยกเลิกก่อนเวลาดังกล่าวได้เสมอ", "Already a member?": "เป็นสมาชิกอยู่แล้ว?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "เกิดข้อผิดพลาดที่ไม่คาดคิด โปรดลองอีกครั้งหรือ ติดต่อฝ่ายสนับสนุน หากข้อผิดพลาดยังคงอยู่", "Back": "ย้อนกลับ", "Back to Log in": "กลับไปยังเข้าสู่ระบบ", @@ -23,10 +27,13 @@ "Cancel subscription": "ยกเลิกการรับสมัครข้อมูล", "Cancellation reason": "เหตุผลการยกเลิก", "Change": "เปลี่ยน", + "Change plan": "", "Check spam & promotions folders": "ตรวจสอบโฟลเดอร์สแปมและโปรโมชัน", "Check with your mail provider": "ตรวจสอบกับผู้ให้บริการอีเมลของคุณ", + "Check your inbox to verify email update": "", "Choose": "เลือก", "Choose a different plan": "เลือกแผนอื่น", + "Choose a plan": "", "Choose your newsletters": "เลือกอีเมลที่ต้องการรับจดหมายข่าว", "Click here to retry": "คลิกที่นี่เพื่อลองอีกครั้ง", "Close": "ปิด", @@ -38,6 +45,7 @@ "Contact support": "ติดต่อฝ่ายสนับสนุน", "Continue": "ดำเนินการต่อ", "Continue subscription": "รับสมัครข้อมูลต่อ", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "ไม่สามารถลงชื่อเข้าใช้ได้ ลิงก์เข้าสู่ระบบหมดอายุ", "Could not update email! Invalid link.": "ไม่สามารถอัปเดตอีเมลได้! ลิงก์ไม่ถูกต้อง", "Create a new contact": "สร้างผู้ติดต่อใหม่", @@ -48,13 +56,27 @@ "Edit": "แก้ไข", "Email": "อีเมล", "Email newsletter": "จดหมายข่าวทางอีเมล", - "Email preference updated.": "อัปเดตการตั้งค่าอีเมลแล้ว", + "Email newsletter settings updated": "", "Email preferences": "การตั้งค่าอีเมล", "Emails": "อีเมล", "Emails disabled": "อีเมลถูกปิดใช้งาน", "Ends {{offerEndDate}}": "สิ้นสุด {{offerEndDate}}", + "Enter your email address": "", + "Enter your name": "", "Error": "ข้อผิดพลาด", "Expires {{expiryDate}}": "หมดอายุ {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "ตลอดไป", "Free Trial – Ends {{trialEnd}}": "ทดลองใช้ฟรี - สิ้นสุด {{trialEnd}}", "Get help": "ต้องการความช่วยเหลือ", @@ -72,24 +94,32 @@ "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "หากคุณตรวจสอบทั้งหมดนี้แล้ว แต่ยังไม่ได้รับอีเมล คุณสามารถติดต่อเพื่อแก้ไขปัญหาได้โดยติดต่อ {{supportAddress}}", "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "ในกรณีที่พยายามส่งจดหมายข่าวและได้รับความล้มเหลวอย่างถาวร, ที่อยู่อีเมลจะถูกปิดการใช้งานจากบัญชี", "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "ในโปรแกรมรับส่งอีเมลของคุณ ให้เพิ่ม {{senderEmail}} ลงในรายชื่อผู้ติดต่อของคุณ นี่เป็นการส่งสัญญาณไปยังผู้ให้บริการอีเมลของคุณ ว่าอีเมลที่ส่งจากที่อยู่นี้เชื่อถือได้", + "Invalid email address": "", + "Jamie Larson": "", + "jamie@example.com": "", "Less like this": "เห็นแบบนี้ให้น้อยลง", "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "ตรวจสอบให้แน่ใจว่าอีเมลไม่ได้ไปอยู่ในโฟลเดอร์สแปมหรือโปรโมชั่นในกล่องจดหมายของคุณโดยไม่ได้ตั้งใจ หากเป็นเช่นนั้น ให้คลิก \"ทำเครื่องหมายว่าไม่ใช่สแปม\" และ/หรือ \"ย้ายไปที่กล่องจดหมาย\"", "Manage": "จัดการ", "Maybe later": "ไว้ก่อน", "Memberships unavailable, contact the owner for access.": "การเป็นสมาชิกไม่พร้อมใช้งาน, โปรดติดต่อเจ้าของเพื่อขอสิทธิ์ในการเข้าถึง", + "month": "", "Monthly": "รายเดือน", "More like this": "เห็นแบบนี้ให้มากขึ้น", "Name": "ชื่อ", "Need more help? Contact support": "ต้องการความช่วยเหลือเพิ่มเติม? ติดต่อฝ่ายสนับสนุน", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "จดหมายข่าวสามารถปิดการใช้งานในบัญชีของคุณด้วยเหตุผลสองประการ: อีเมลก่อนหน้านี้ถูกทำเครื่องหมายว่าเป็นสแปม หรือการพยายามที่ส่งอีเมล ส่งผลให้เกิดความล้มเหลวถาวร (อีเมลตีกลับ)", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "ไม่ได้รับอีเมล?", "Now check your email!": "ตรวจสอบอีเมลของคุณตอนนี้!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "เมื่อรับสมัครข้อมูลใหม่แล้ว, หากคุณยังคงไม่เห็นอีเมลในกล่องจดหมายของคุณ ให้ตรวจสอบโฟลเดอร์สแปมของคุณ. ผู้ให้บริการกล่องจดหมายบางราย เก็บบันทึกการร้องเรียนเกี่ยวกับสแปมก่อนหน้านี้ และจะทำการตั้งค่าสถานะอีเมลต่อไป หากเกิดเหตุการณ์เช่นนี้ ให้ทำเครื่องหมายจดหมายข่าวล่าสุดว่า 'ไม่ใช่สแปม' เพื่อย้ายกลับไปยังกล่องจดหมายหลักของคุณ", "Permanent failure (bounce)": "ความล้มเหลวอย่างถาวร (อีเมลถูกตีกลับ)", + "Phone number": "", "Plan": "แผน", "Plan checkout was cancelled.": "การชำระเงินตามแผนถูกยกเลิก", "Plan upgrade was cancelled.": "การอัปเกรดแผนถูกยกเลิก", "Please contact {{supportAddress}} to adjust your complimentary subscription.": "โปรดติดต่อ {{supportAddress}} เพื่อปรับเปลี่ยนการรับสมัครข้อมูลฟรีของคุณ", + "Please enter {{fieldName}}": "", "Please fill in required fields": "กรุณากรอกข้อมูลในช่องให้ครบถ้วน", "Price": "ราคา", "Re-enable emails": "เปิดใช้งานอีเมลนี้อีกครั้ง", @@ -106,6 +136,7 @@ "Sign out": "ออกจากระบบ", "Sign up": "สมัครใช้งาน", "Signup error: Invalid link": "มีข้อผิดพลาดในการสมัครใช้งาน: ลิงก์ไม่ถูกต้อง", + "Something went wrong, please try again later.": "", "Sorry, that didn’t work.": "ขออภัย, ไม่สามารถส่งได้", "Spam complaints": "การร้องเรียนเกี่ยวกับสแปม", "Start {{amount}}-day free trial": "เริ่มทดลองใช้ฟรี {{amount}} วัน", @@ -114,22 +145,39 @@ "Submit feedback": "ส่งข้อเสนอแนะ", "Subscribe": "รับสมัครข้อมูล", "Subscribed": "รับสมัครข้อมูลแล้ว", + "Subscription plan updated successfully": "", "Success": "สำเร็จ", "Success! Check your email for magic link to sign-in.": "สำเร็จ! ตรวจสอบอีเมลของคุณเพื่อดูลิงก์วิเศษสำหรับลงชื่อเข้าใช้", "Success! Your account is fully activated, you now have access to all content.": "สำเร็จ! บัญชีของคุณเปิดใช้งานโดยสมบูรณ์แล้ว ตอนนี้คุณสามารถเข้าถึงเนื้อหาทั้งหมดได้แล้ว", "Success! Your email is updated.": "อัปเดตอีเมลสำเร็จแล้ว!", "Successfully unsubscribed": "ยกเลิกการรับสมัครข้อมูลเรียบร้อยแล้ว", "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "ขอบคุณสำหรับการรับสมัครข้อมูล ก่อนที่คุณจะเริ่มอ่าน ด้านล่างนี้คือเว็บไซต์อื่นๆ บางส่วนที่คุณอาจชอบ", + "Thank you for your support": "", + "Thank you for your support!": "", "Thanks for the feedback!": "ขอบคุณสำหรับความคิดเห็น!", "That didn't go to plan": "บางอย่างไม่เป็นไปตามแผน", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "ที่อยู่อีเมลของคุณที่เรามีคือ {{memberEmail}} — หากไม่ถูกต้อง คุณสามารถอัปเดตได้ใน", "There was a problem submitting your feedback. Please try again a little later.": "เกิดปัญหาในการส่งความคิดเห็นของคุณ โปรดลองอีกครั้งในภายหลัง", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", + "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "เว็บไซต์นี้สำหรับผู้ได้รับเชิญเท่านั้น โปรดติดต่อเจ้าของเพื่อเข้าถึง", + "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "เพื่อทำการลงทะเบียนให้เสร็จสิ้น คลิกลิงก์ยืนยันในกล่องจดหมายของคุณ หากไม่ได้รับภายใน 3 นาที ให้ตรวจสอบโฟลเดอร์สแปมของคุณ!", + "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "ทดลองใช้ฟรี {{amount}} วัน จากนั้นจ่ายเป็น {{ราคาเดิม}}", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "ปลดล็อกการเข้าถึงจดหมายข่าวทั้งหมดโดยสมัครเป็นสมาชิกแบบชำระเงิน", "Unsubscribe from all emails": "ยกเลิกการรับสมัครข้อมูลทางอีเมลทั้งหมด", "Unsubscribed": "ยกเลิกการรับสมัครข้อมูลทางอีเมลแล้ว", + "Unsubscribed from all emails.": "", "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "การยกเลิกการสมัครรับอีเมล จะไม่ยกเลิกการรับสมัครข้อมูลแบบชำระเงินของคุณกับ {{title}}", "Update": "อัปเดต", "Update your preferences": "อัปเดตการตั้งค่าของคุณ", @@ -142,6 +190,7 @@ "Welcome to {{siteTitle}}": "ยินดีต้อนรับสู่ {{siteTitle}}", "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "เมื่อกล่องจดหมายเข้าไม่ยอมรับอีเมล โดยทั่วไปจะเรียกว่าการตีกลับ. ในหลายกรณี, การดำเนินการนี้อาจเกิดขึ้นชั่วคราว. อย่างไรก็ตาม ในบางกรณี อีเมลที่ถูกตีกลับ สามารถส่งคืนเป็นความล้มเหลวถาวรเมื่อที่อยู่อีเมลไม่ถูกต้องหรือไม่มีอยู่จริง", "Why has my email been disabled?": "เหตุใดอีเมลของฉันจึงถูกปิดการใช้งาน?", + "year": "", "Yearly": "รายปี", "You currently have a free membership, upgrade to a paid subscription for full access.": "ขณะนี้คุณเป็นสมาชิกฟรี, อัปเกรดเป็นการรับสมัครข้อมูลแบบชำระเงินเพื่อการเข้าถึงเต็มรูปแบบ", "You have been successfully resubscribed": "คุณรับสมัครข้อมูลทางอีเมลอีกครั้งสำเร็จแล้ว", @@ -151,6 +200,7 @@ "You've successfully signed in.": "คุณลงชื่อเข้าใช้สำเร็จแล้ว", "You've successfully subscribed to": "คุณรับสมัครข้อมูลสำเร็จแล้ว", "Your account": "บัญชีของคุณ", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "ข้อมูลของคุณ จะช่วยกำหนดสิ่งที่จะได้รับการเผยแพร่ในอนาคต", "Your subscription will expire on {{expiryDate}}": "การรับสมัครข้อมูลของคุณจะหมดอายุในวันที่ {{expiryDate}}", "Your subscription will renew on {{renewalDate}}": "การรับสมัครข้อมูลของคุณจะต่ออายุในวันที่ {{renewalDate}}", diff --git a/ghost/i18n/locales/th/search.json b/ghost/i18n/locales/th/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/th/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/tr/comments.json b/ghost/i18n/locales/tr/comments.json index af1bd3994e9..d8286ca8704 100644 --- a/ghost/i18n/locales/tr/comments.json +++ b/ghost/i18n/locales/tr/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "Yoruma cevap ver", "Report": "Rapor et", "Report comment": "Yorumu rapor et", - "Report this comment": "Bu yorumu rapor et", "Report this comment?": "Bu yorum rapor edilsin mi?", "Save": "Kaydet", "Sending": "Gönderiliyor", @@ -68,6 +67,5 @@ "This comment has been removed.": "Bu yorum kaldırıldı.", "Upgrade now": "Şimdi yükselt", "Yesterday": "Dün", - "You want to report this comment?": "Bu yorumu rapor etmek istiyor musunuz?", "Your request will be sent to the owner of this site.": "Talebiniz bu sitenin sahibine gönderilecektir." } diff --git a/ghost/i18n/locales/tr/portal.json b/ghost/i18n/locales/tr/portal.json index 3213bc8c645..5760ccc5cd5 100644 --- a/ghost/i18n/locales/tr/portal.json +++ b/ghost/i18n/locales/tr/portal.json @@ -10,12 +10,14 @@ "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "{{memberEmail}}, yorumlarınıza yanıt verildiğinde artık e-posta almayacak.", "{{memberEmail}} will no longer receive this newsletter.": "{{memberEmail}} artık bu bülteni almayacak.", "{{trialDays}} days free": "{{trialDays}} gün ücretsiz", - "+1 (123) 456-7890": "", + "+1 (123) 456-7890": "+90 (123) 456-7890", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Gelen kutuna bir giriş linki gönderildi. Eğer 3 dakika içinde ulaşmazsa spam klasörünü kontrol ettiğinden emin ol.", "Account": "Hesap", + "Account details updated successfully": "", "Account settings": "Hesap ayarları", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Ücretsiz deneme süresi bittikten sonra seçtiğin kategorinin normal fiyatından ücretlendirileceksin. O zamana kadar her an iptal edebilirsin.", "Already a member?": "Zaten üye misin?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "Beklenmeyen bir hata oluştu. Lütfen tekrar deneyin veya hata devam ederse destek ile iletişime geçin.", "Back": "Geri dön", "Back to Log in": "Giriş ekranına geri dön", @@ -25,12 +27,13 @@ "Cancel subscription": "Aboneliği iptal et", "Cancellation reason": "İptal sebebi", "Change": "Değiştir", - "Change plan": "", + "Change plan": "Plan değiştir", "Check spam & promotions folders": "Spam ve promosyonlar klasörlerini kontrol edin", "Check with your mail provider": "Posta sağlayıcınızla kontrol edin", + "Check your inbox to verify email update": "", "Choose": "Seç", "Choose a different plan": "Farklı bir plan seç", - "Choose a plan": "", + "Choose a plan": "Bir plan seçin", "Choose your newsletters": "Bültenleri seç", "Click here to retry": "Tekrar denemek için buraya tıkla", "Close": "Kapat", @@ -42,6 +45,7 @@ "Contact support": "Desteğe başvurun", "Continue": "Devam et", "Continue subscription": "Aboneliğe devam et", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "Oturum açılamadı. Oturum açma bağlantısının süresi doldu.", "Could not update email! Invalid link.": "E-posta güncellenemedi! Geçersiz link.", "Create a new contact": "Yeni bir kullanıcı oluştur", @@ -52,14 +56,27 @@ "Edit": "Düzenle", "Email": "E-posta", "Email newsletter": "E-posta bülteni", + "Email newsletter settings updated": "", "Email preferences": "E-posta tercihleri", "Emails": "E-postalar", "Emails disabled": "E-postalar devre dışı", "Ends {{offerEndDate}}": "{{offerEndDate}} tarihinde bitiyor", - "Enter your email address": "", - "Enter your name": "", + "Enter your email address": "E-posta adresinizi girin", + "Enter your name": "Adınızı girin", "Error": "Hata", "Expires {{expiryDate}}": "{{expiryDate}} tarihinde sona eriyor", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "Süresiz", "Free Trial – Ends {{trialEnd}}": "Ücretsiz Deneme – Bitiş Tarihi {{trialEnd}}", "Get help": "Yardım al", @@ -77,30 +94,32 @@ "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "Tüm bu kontrolleri tamamlamanıza rağmen hala e-posta almıyorsanız, {{supportAddress}} ile iletişime geçerek destek almak için ulaşabilirsiniz.", "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "Haber bülteni göndermeye çalışırken kalıcı bir hata alınması durumunda, hesapta e-postalar devre dışı bırakılır.", "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "E-posta istemcinizde {{senderEmail}} adresini kişi listenize ekleyin. Bu, posta sağlayıcınıza bu adresten gönderilen e-postaların güvenilir olması gerektiğini bildirir.", - "Invalid email address": "", - "Jamie Larson": "", - "jamie@example.com": "", + "Invalid email address": "Geçersiz e-posta adresi", + "Jamie Larson": "Ahmet Yılmaz", + "jamie@example.com": "ahmet@example.com", "Less like this": "Bunun gibi daha az", "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "E-postaların yanlışlıkla gelen kutunuzun Spam veya Promosyonlar klasörlerine düşmediğinden emin olun. Varsa, \"Spam değil olarak işaretle\" ve/veya \"Gelen kutusuna taşı\"yı tıklayın.", "Manage": "Yönet", "Maybe later": "Belki daha sonra", "Memberships unavailable, contact the owner for access.": "Üyelikler müsait değil, erişim için sahibe başvurun.", - "month": "", + "month": "ay", "Monthly": "Aylık", "More like this": "Bunun gibi daha fazla", "Name": "İsim", "Need more help? Contact support": "Daha fazla yardıma mı ihtiyacınız var? Desteğe başvurun", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Bültenler, hesabınızda iki nedenden dolayı devre dışı bırakılabilir: Önceki bir e-posta istenmeyen posta olarak işaretlendi veya bir e-posta gönderilmeye çalışıldığında kalıcı bir başarısızlıkla (geri dönme) sonuçlandı.", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "E-posta almıyor musun?", "Now check your email!": "Şimdi e-posta kutunu kontrol et!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Yeniden abone olduktan sonra hala e-postaları gelen kutunuzda görmüyorsanız, spam klasörünü kontrol edin. Bazı e-posta sağlayıcıları önceki spam şikayetlerini kaydedebilir ve e-postaları işaretlemeye devam edebilir. Bu durumda, en son bülteni 'Spam değil' olarak işaretleyerek ana gelen kutunuza geri taşıyabilirsiniz.", "Permanent failure (bounce)": "Kalıcı başarısızlık (sıçra)", - "Phone number": "", + "Phone number": "Telefon numarası", "Plan": "Plan", "Plan checkout was cancelled.": "Plan ödemesi iptal edildi.", "Plan upgrade was cancelled.": "Plan yükseltme iptal edildi.", "Please contact {{supportAddress}} to adjust your complimentary subscription.": "Ücretsiz aboneliğinizi ayarlamak için lütfen {{supportAddress}} ile iletişime geçin.", - "Please enter {{fieldName}}": "", + "Please enter {{fieldName}}": "Lütfen {{fieldName}} girin", "Please fill in required fields": "Lütfen gerekli alanları doldurunuz", "Price": "Fiyat", "Re-enable emails": "E-postaları yeniden etkinleştir", @@ -117,7 +136,7 @@ "Sign out": "Çıkış yap", "Sign up": "Kayıt ol", "Signup error: Invalid link": "Kayıt hatası: Geçersiz bağlantı", - "Something went wrong, please try again later.": "", + "Something went wrong, please try again later.": "Bir şeyler ters gitti, lütfen daha sonra tekrar deneyin.", "Sorry, that didn’t work.": "Üzgünüm, bu işe yaramadı.", "Spam complaints": "Spam şikayetleri", "Start {{amount}}-day free trial": "{{amount}} gün ücretsiz deneme süresini başlat", @@ -126,24 +145,35 @@ "Submit feedback": "Geri bildirim gönder", "Subscribe": "Abone", "Subscribed": "Abone olundu", + "Subscription plan updated successfully": "", "Success": "Başarılı", "Success! Check your email for magic link to sign-in.": "Başarılı! Oturum açmak için sihirli bağlantı için e-postanızı kontrol edin.", "Success! Your account is fully activated, you now have access to all content.": "Başarılı! Hesabınız tamamen etkinleştirildi, artık tüm içeriğe erişebilirsiniz.", "Success! Your email is updated.": "Başarılı! E-postanız güncellendi.", "Successfully unsubscribed": "Abonelikten başarıyla çıkıldı", "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "Abone olduğunuz için teşekkür ederiz. Okumaya başlamadan önce, aşağıda keyif alabileceğiniz birkaç başka site bulunmaktadır.", - "Thank you for your support": "", - "Thank you for your support!": "", + "Thank you for your support": "Desteğiniz için teşekkür ederiz", + "Thank you for your support!": "Desteğiniz için teşekkür ederiz!", "Thanks for the feedback!": "Geri bildirim için teşekkürler!", "That didn't go to plan": "Bir şeyler ters gitti", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Sizin için kayıtlı olan e-posta adresi {{memberEmail}} — eğer bu doğru değilse, bunu güncelleyebilirsiniz.", "There was a problem submitting your feedback. Please try again a little later.": "Geri bildiriminiz gönderilirken bir sorun oluştu. Lütfen biraz sonra tekrar deneyin.", - "There was an error processing your payment. Please try again.": "", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", + "There was an error processing your payment. Please try again.": "Ödemeniz işlenirken bir hata oluştu. Lütfen tekrar deneyiniz.", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Bu site sadece davetiyesi olanlar içindir, erişim için site sahibiyle iletişime geç.", - "This site is not accepting payments at the moment.": "", + "This site is not accepting payments at the moment.": "Bu site şu anda ödeme kabul etmemektedir.", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Kaydınızı tamamlamak için gelen kutunuzdaki onay bağlantısına tıklayın. Eğer 3 dakika içinde gelmezse, spam klasörünüzü kontrol edin!", - "To continue to stay up to date, subscribe to {{publication}} below.": "", + "To continue to stay up to date, subscribe to {{publication}} below.": "Güncel kalmaya devam etmek için, aşağıdaki {{publication}} abone olun.", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "{{amount}} gün ücretsiz deneyin, ardından {{originalPrice}}.", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "Tüm bültenlere erişimi açmak için ücretli bir abone olun.", "Unsubscribe from all emails": "Tüm e-postaların aboneliğinden çık", "Unsubscribed": "Abonelikten çıkıldı", @@ -160,7 +190,7 @@ "Welcome to {{siteTitle}}": "{{siteTitle}} hoş geldiniz", "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "Gelen kutusu bir e-postayı kabul etmeyi reddettiğinde, buna genellikle bir geri dönen denir. Çoğu durumda bu geçici olabilir. Ancak, bazı durumlarda, bir e-posta adresi geçersiz veya mevcut olmadığında bir geri dönen e-postası kalıcı bir başarısızlık olarak geri dönebilir.", "Why has my email been disabled?": "E-postam neden devre dışı bırakıldı?", - "year": "", + "year": "yıl", "Yearly": "Yıllık", "You currently have a free membership, upgrade to a paid subscription for full access.": "Şu anda ücretsiz üyeliğiniz var, tam erişim için ücretli aboneliğe yükseltin.", "You have been successfully resubscribed": "Başarıyla yeniden abone oldun", @@ -170,6 +200,7 @@ "You've successfully signed in.": "Başarıyla oturum açtınız.", "You've successfully subscribed to": "Başarıyla abone oldunuz", "Your account": "Hesabın", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "Yorumun yayımlanan içeriklerin şekillenmesine yardımcı olur.", "Your subscription will expire on {{expiryDate}}": "Aboneliğiniz {{expiryDate}} tarihinde sona erecek", "Your subscription will renew on {{renewalDate}}": "Aboneliğiniz {{renewalDate}} tarihinde yenilenecek", diff --git a/ghost/i18n/locales/tr/search.json b/ghost/i18n/locales/tr/search.json new file mode 100644 index 00000000000..a25204a1283 --- /dev/null +++ b/ghost/i18n/locales/tr/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "Yazarlar", + "Cancel": "İptal", + "No matches found": "Eşleşme bulunamadı", + "Posts": "Yazılar", + "Search posts, tags and authors": "Gönderileri, etiketleri ve yazarları ara", + "Show more results": "Daha fazla sonuç göster", + "Tags": "Etiketler" +} diff --git a/ghost/i18n/locales/uk/comments.json b/ghost/i18n/locales/uk/comments.json index c5391eab775..dd151c6efec 100644 --- a/ghost/i18n/locales/uk/comments.json +++ b/ghost/i18n/locales/uk/comments.json @@ -1,73 +1,71 @@ { - "{{amount}} characters left": "", - "{{amount}} comments": "", - "{{amount}} days ago": "", - "{{amount}} hrs ago": "", - "{{amount}} mins ago": "", - "{{amount}} months ago": "", - "{{amount}} more": "", - "{{amount}} seconds ago": "", - "{{amount}} weeks ago": "", - "{{amount}} years ago": "", - "1 comment": "", - "Add comment": "", - "Add context to your comment, share your name and expertise to foster a healthy discussion.": "", - "Add reply": "", - "Already a member?": "", - "Anonymous": "", - "Become a member of {{publication}} to start commenting.": "", - "Become a paid member of {{publication}} to start commenting.": "", - "Cancel": "", - "Comment": "", - "Complete your profile": "", - "Delete": "", - "Deleted member": "", - "Discussion": "", - "Edit": "", - "Edit this comment": "", - "edited": "", - "Enter your name": "", - "Expertise": "", - "Founder @ Acme Inc": "", - "Full-time parent": "", - "Head of Marketing at Acme, Inc": "", - "Hide": "", - "Hide comment": "", - "Jamie Larson": "", - "Join the discussion": "", - "Just now": "", - "Local resident": "", - "Member discussion": "", - "Name": "", - "Neurosurgeon": "", - "One day ago": "", - "One hour ago": "", - "One min ago": "", - "One month ago": "", - "One week ago": "", - "One year ago": "", - "Reply": "", - "Reply to comment": "", - "Report": "", - "Report comment": "", - "Report this comment": "", - "Report this comment?": "", - "Save": "", - "Sending": "", - "Sent": "", - "Show": "", - "Show {{amount}} more replies": "", - "Show {{amount}} previous comments": "", - "Show 1 more reply": "", - "Show 1 previous comment": "", - "Show comment": "", - "Sign in": "", - "Sign up now": "", - "Start the conversation": "", - "This comment has been hidden.": "", - "This comment has been removed.": "", - "Upgrade now": "", - "Yesterday": "", - "You want to report this comment?": "", - "Your request will be sent to the owner of this site.": "" + "{{amount}} characters left": "{{amount}} символів залишилось", + "{{amount}} comments": "{{amount}} коментарів", + "{{amount}} days ago": "{{amount}} днів тому", + "{{amount}} hrs ago": "{{amount}} годин тому", + "{{amount}} mins ago": "{{amount}} хвилин тому", + "{{amount}} months ago": "{{amount}} місяців тому", + "{{amount}} more": "ще {{amount}}", + "{{amount}} seconds ago": "{{amount}} секунд тому", + "{{amount}} weeks ago": "{{amount}} тижнів тому", + "{{amount}} years ago": "{{amount}} років тому", + "1 comment": "1 коментар", + "Add comment": "Додати коментар", + "Add context to your comment, share your name and expertise to foster a healthy discussion.": "Додайте контекст до свого коментаря, поділіться своїм іменем і досвідом, щоб сприяти здоровій дискусії.", + "Add reply": "Додати відповідь", + "Already a member?": "Ви є учасником?", + "Anonymous": "Анонім", + "Become a member of {{publication}} to start commenting.": "Станьте учасником {{publication}} щоб почати коментувати.", + "Become a paid member of {{publication}} to start commenting.": "Станьте платним учасником {{publication}} щоб почати коментувати.", + "Cancel": "Відмінити", + "Comment": "Коментувати", + "Complete your profile": "Заповніть свій профіль", + "Delete": "Видалити", + "Deleted member": "Видалений учасник", + "Discussion": "Обговорення", + "Edit": "Редагувати", + "Edit this comment": "Редагувати цей коментар", + "edited": "Відредагований", + "Enter your name": "Введіть своє імʼя", + "Expertise": "Експертність", + "Founder @ Acme Inc": "Засновник @ Acme Inc", + "Full-time parent": "Виховую дітей", + "Head of Marketing at Acme, Inc": "Голова продажів в Acme, Inc", + "Hide": "Сховати", + "Hide comment": "Сховати коментар", + "Jamie Larson": "Ваше імʼя", + "Join the discussion": "Долучитися до обговорення", + "Just now": "Прямо зараз", + "Local resident": "Місцевий експерт", + "Member discussion": "Обговорення учасників", + "Name": "Імʼя", + "Neurosurgeon": "Нейрохірург", + "One day ago": "Один день тому", + "One hour ago": "Одну годину тому", + "One min ago": "Одну хвилину тому", + "One month ago": "Один місяць тому", + "One week ago": "Один тиждень тому", + "One year ago": "Один рік тому", + "Reply": "Відповісти", + "Reply to comment": "Відповісти на коментар", + "Report": "Поскаржитися", + "Report comment": "Поскаржитися на коментар", + "Report this comment?": "Поскаржитися на цей коментар?", + "Save": "Зберегти", + "Sending": "Відправка", + "Sent": "Відправлено", + "Show": "Показати", + "Show {{amount}} more replies": "Показати ще {{amount}} відповідей", + "Show {{amount}} previous comments": "Показати {{amount}} попередніх коментарів", + "Show 1 more reply": "Показати ще одну відповідь", + "Show 1 previous comment": "Показати один попередній коментар", + "Show comment": "Показати коментар", + "Sign in": "Увійти", + "Sign up now": "Зареєструватись зараз", + "Start the conversation": "Почніть розмову", + "This comment has been hidden.": "Цей коментар було приховано.", + "This comment has been removed.": "Цей коментар було видалено.", + "Upgrade now": "Оновити зараз", + "Yesterday": "Вчора", + "Your request will be sent to the owner of this site.": "Ваш запит буде надіслано власнику цього сайту." } diff --git a/ghost/i18n/locales/uk/ghost.json b/ghost/i18n/locales/uk/ghost.json index c4e18118bf5..b8fe2536cb8 100644 --- a/ghost/i18n/locales/uk/ghost.json +++ b/ghost/i18n/locales/uk/ghost.json @@ -1,34 +1,34 @@ { "All the best!": "Всього найкращого!", - "Complete signup for {{siteTitle}}!": "", - "Complete your sign up to {{siteTitle}}!": "Закінч реєстрацію на {{siteTitle}}!", - "Confirm email address": "", - "Confirm signup": "", - "Confirm your email address": "", - "Confirm your email update for {{siteTitle}}!": "Підтвердь оновлення електронної пошти для {{siteTitle}}!", - "Confirm your subscription to {{siteTitle}}": "Підтвердь підписку на {{siteTitle}}", - "For your security, the link will expire in 24 hours time.": "Для твоєї безпеки, посилання буде дійсним протягом 24 годин.", + "Complete signup for {{siteTitle}}!": "Завершіть реєстрацію для {{siteTitle}}!", + "Complete your sign up to {{siteTitle}}!": "Завершіть вашу реєстрацію для {{siteTitle}}!", + "Confirm email address": "Підтвердити електронну пошту", + "Confirm signup": "Підтвердити реєстрацію", + "Confirm your email address": "Підтвердити вашу електронну пошту", + "Confirm your email update for {{siteTitle}}!": "Підтвердь оновлення вашої електронної пошти для {{siteTitle}}!", + "Confirm your subscription to {{siteTitle}}": "Підтвердь вашу підписку на {{siteTitle}}", + "For your security, the link will expire in 24 hours time.": "Для вашої безпеки, посилання буде дійсним протягом 24 годин.", "Hey there,": "Привіт,", - "Hey there!": "", + "Hey there!": "Привіт!", "If you did not make this request, you can safely ignore this email.": "Якщо цей запит не був зроблений тобою, можеш безпечно ігнорувати це повідомлення.", "If you did not make this request, you can simply delete this message.": "Якщо цей запит не був зроблений тобою, можеш просто видалити це повідомлення.", "Please confirm your email address with this link:": "Будь ласка, підтвердь свою електронну пошту за допомогою цього посилання:", "Secure sign in link for {{siteTitle}}": "Безпечне посилання для входу до {{siteTitle}}", "See you soon!": "До зустрічі!", "Sent to {{email}}": "Відправлено на {{email}}", - "Sign in": "", + "Sign in": "Вхід", "Sign in to {{siteTitle}}": "Увійти до {{siteTitle}}", "Tap the link below to complete the signup process for {{siteTitle}}, and be automatically signed in:": "Натисни посилання нижче, щоб завершити процес реєстрації на {{siteTitle}} і автоматично увійти:", "Thank you for signing up to {{siteTitle}}!": "Дякуємо за реєстрацію на {{siteTitle}}!", "Thank you for subscribing to {{siteTitle}}!": "Дякуємо за підписку на {{siteTitle}}!", - "Thank you for subscribing to {{siteTitle}}.": "", - "Thank you for subscribing to {{siteTitle}}. Tap the link below to be automatically signed in:": "Дякуємо за підписку на {{siteTitle}}. Натисни посилання нижче, щоб автоматично увійти:", + "Thank you for subscribing to {{siteTitle}}.": "Дякуємо за підписку на {{siteTitle}}.", + "Thank you for subscribing to {{siteTitle}}. Tap the link below to be automatically signed in:": "Дякуємо за підписку на {{siteTitle}}. Натисніть посилання нижче, щоб автоматично увійти:", "This email address will not be used.": "Ця електронна пошта не буде використовуватися.", - "Welcome back to {{siteTitle}}!": "Ласкаво просимо до {{siteTitle}}!", - "Welcome back! Use this link to securely sign in to your {{siteTitle}} account:": "Ласкаво просимо! Ужий це посилання, щоб безпечно увійти до свого облікового запису {{siteTitle}}:", - "You can also copy & paste this URL into your browser:": "Можеш також скопіювати та вставити це посилання в свій браузер:", - "You will not be signed up, and no account will be created for you.": "Не відбудеться твоя реєстрація, і жоден обліковий запис не буде створений для тебе.", + "Welcome back to {{siteTitle}}!": "З поверненням до {{siteTitle}}!", + "Welcome back! Use this link to securely sign in to your {{siteTitle}} account:": "З поверненням! Використовуйте це посилання, щоб безпечно увійти до свого облікового запису {{siteTitle}}:", + "You can also copy & paste this URL into your browser:": "Можете також скопіювати та вставити це посилання в свій браузер:", + "You will not be signed up, and no account will be created for you.": "Вас не буде зареєстровано, а також обліковий запис не буде створено для вас.", "You will not be subscribed.": "Тебе не буде підписано.", - "You're one tap away from subscribing to {{siteTitle}} — please confirm your email address with this link:": "Ти один натиск від підписки на {{siteTitle}} — будь ласка, підтвердь свою електронну пошту за допомогою цього посилання:", - "You're one tap away from subscribing to {{siteTitle}}!": "" + "You're one tap away from subscribing to {{siteTitle}} — please confirm your email address with this link:": "Ви за один крок до підписки на {{siteTitle}} — будь ласка, підтвердь свою електронну пошту за допомогою цього посилання:", + "You're one tap away from subscribing to {{siteTitle}}!": "Ви за один крок до підписки на {{siteTitle}}!" } diff --git a/ghost/i18n/locales/uk/portal.json b/ghost/i18n/locales/uk/portal.json index 067af713305..f11de1f74d9 100644 --- a/ghost/i18n/locales/uk/portal.json +++ b/ghost/i18n/locales/uk/portal.json @@ -1,177 +1,208 @@ { - "(save {{highestYearlyDiscount}}%)": "", - "{{amount}} days free": "", - "{{amount}} off": "", - "{{amount}} off for first {{number}} months.": "", - "{{amount}} off for first {{period}}.": "", - "{{amount}} off forever.": "", + "(save {{highestYearlyDiscount}}%)": "(зекономте {{highestYearlyDiscount}}%)", + "{{amount}} days free": "{{amount}} днів безкоштовно", + "{{amount}} off": "Знижка {{amount}}", + "{{amount}} off for first {{number}} months.": "Знижка {{amount}} для перших {{number}} місяців.", + "{{amount}} off for first {{period}}.": "Знижка {{amount}} для першого {{period}}.", + "{{amount}} off forever.": "Знижка {{amount}} назавжди.", "{{discount}}% discount": "Знижка {{discount}}%", - "{{memberEmail}} will no longer receive {{newsletterName}} newsletter.": "", - "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "", - "{{memberEmail}} will no longer receive this newsletter.": "", + "{{memberEmail}} will no longer receive {{newsletterName}} newsletter.": "{{memberEmail}} більше не отримуватиме розсилку {{newsletterName}}.", + "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "{{memberEmail}} більше не отримуватиме електронні листи, коли хтось відповідає на ваші коментарі.", + "{{memberEmail}} will no longer receive this newsletter.": "{{memberEmail}} більше не отримуватиме цю розсилку.", "{{trialDays}} days free": "Безплатно {{trialDays}} дні(-в)", - "+1 (123) 456-7890": "", - "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Посилання для входу було надіслано на твою пошту. Якщо воно не прийде протягом 3 хвилин, перевірь папку спам.", + "+1 (123) 456-7890": "+1 (123) 456-7890", + "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Посилання для входу було надіслано на твою пошту. Якщо воно не прийде протягом 3 хвилин, перевірь папку зі спамом.", "Account": "Oбліковий запис", + "Account details updated successfully": "Дані облікового запису успішно оновлено", "Account settings": "Налаштування облікового запису", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Після закінчення безплатного періоду, з тебе буде стягнена вартість за обраний тариф. Ти завжди можеш скасувати послугу до цього часу.", - "Already a member?": "Вже зареєстрований?", - "An unexpected error occured. Please try again or contact support if the error persists.": "", + "Already a member?": "Вже є учасником?", + "An error occurred": "Сталася помилка", + "An unexpected error occured. Please try again or contact support if the error persists.": "Сталася неочікувана помилка. Спробуйте ще раз або зв’яжіться зі службою підтримки, якщо помилка не зникне.", "Back": "Назад", "Back to Log in": "Повернутись до входу", - "Billing info": "", - "Black Friday": "", - "Cancel anytime.": "", + "Billing info": "Платіжна інформація", + "Black Friday": "Чорна п`ятниця", + "Cancel anytime.": "Скасуй будь-коли.", "Cancel subscription": "Скасуй підписку", "Cancellation reason": "Причина скасування", - "Change": "", - "Change plan": "", - "Check spam & promotions folders": "", - "Check with your mail provider": "", - "Choose": "", - "Choose a different plan": "Вибари інший план", - "Choose a plan": "", - "Choose your newsletters": "Вибери свої підписки", - "Click here to retry": "", - "Close": "Закрий", + "Change": "Змінити", + "Change plan": "Змінити план", + "Check spam & promotions folders": "Перевірте папки зі спамом і рекламними акціями", + "Check with your mail provider": "Зверніться до свого постачальника послуг електронної пошти", + "Check your inbox to verify email update": "Перевірте свою поштову скриньку, щоб підтвердити оновлення електронної пошти", + "Choose": "Оберіть", + "Choose a different plan": "Оберіть інший план", + "Choose a plan": "Оберіть план", + "Choose your newsletters": "Оберіть свої підписки", + "Click here to retry": "Натисніть тут, щоб повторити спробу", + "Close": "Закрити", "Comments": "Коментарі", - "Complimentary": "", + "Complimentary": "Безкоштовно", "Confirm": "Підтвердь", - "Confirm cancellation": "", - "Confirm subscription": "", - "Contact support": "", - "Continue": "Далі", - "Continue subscription": "", - "Could not sign in. Login link expired.": "", - "Could not update email! Invalid link.": "", - "Create a new contact": "", - "Current plan": "", + "Confirm cancellation": "Підтвердити скасусання", + "Confirm subscription": "Підтвердити підписку", + "Contact support": "Звернутись до служби підтримки", + "Continue": "Продовжити", + "Continue subscription": "Продовжити підписку", + "Could not create stripe checkout session": "Не вдалося створити сеанс оформлення замовлення Stripe", + "Could not sign in. Login link expired.": "Неможливо увійти. Термін дії посилання для входу закінчився.", + "Could not update email! Invalid link.": "Неможливо оновити електронну адресу! Недійсне посилання.", + "Create a new contact": "Створити новий контакт", + "Current plan": "Поточний план", "Delete account": "Видалити обліковий запис", - "Didn't mean to do this? Manage your preferences .": "", + "Didn't mean to do this? Manage your preferences .": "Ви не хотіли цього робити? Керуйте своїми параметрами .", "Don't have an account?": "Не маєш облікового запису?", - "Edit": "", + "Edit": "Редагувати", "Email": "Електронна пошта", - "Email newsletter": "", + "Email newsletter": "Електронна розсилка", + "Email newsletter settings updated": "Оновлено параметри електронної розсилки", "Email preferences": "Налаштування електронної пошти", "Emails": "Електронні листи", "Emails disabled": "Електронна пошта вимкнена", - "Ends {{offerEndDate}}": "", - "Enter your email address": "", - "Enter your name": "", - "Error": "", - "Expires {{expiryDate}}": "", - "Forever": "", - "Free Trial – Ends {{trialEnd}}": "", + "Ends {{offerEndDate}}": "Закунчується {{offerEndDate}}", + "Enter your email address": "Введіть свій імейл", + "Enter your name": "Введіть імʼя", + "Error": "Помилка", + "Expires {{expiryDate}}": "Термін дії закінчується {{expiryDate}}", + "Failed to cancel subscription, please try again": "Не вдалося скасувати підписку, повторіть спробу", + "Failed to log in, please try again": "Не вдалося ввійти, спробуйте ще раз", + "Failed to log out, please try again": "Не вдалося вийти, спробуйте ще раз", + "Failed to process checkout, please try again": "Не вдалося обробити замовлення, спробуйте ще раз", + "Failed to send magic link email": "Не вдалося надіслати електронний лист із одноразовим посиланням", + "Failed to send verification email": "Не вдалося надіслати електронний лист для підтвердження", + "Failed to sign up, please try again": "Не вдалося зареєструватися, повторіть спробу", + "Failed to update account data": "Не вдалося оновити дані облікового запису", + "Failed to update account details": "Не вдалося оновити дані облікового запису", + "Failed to update billing information, please try again": "Не вдалося оновити платіжну інформацію, повторіть спробу", + "Failed to update newsletter settings": "Не вдалося оновити налаштування розсилки", + "Failed to update subscription, please try again": "Не вдалося оновити підписку, повторіть спробу", + "Forever": "Назавжди", + "Free Trial – Ends {{trialEnd}}": "Безкоштовна пробна версія – закінчується {{trialEnd}}", "Get help": "Отримати допомогу", - "Get in touch for help": "", - "Get notified when someone replies to your comment": "Повідомляй коли хтось відповість на твій коментар", + "Get in touch for help": "Звертайтесь по допомогу", + "Get notified when someone replies to your comment": "Отримувати повідомлення коли хтось відповість на твій коментар", "Give feedback on this post": "Дати відгук на цю публікацію", - "Help! I'm not receiving emails": "", - "Here are a few other sites you may enjoy.": "", - "If a newsletter is flagged as spam, emails are automatically disabled for that address to make sure you no longer receive any unwanted messages.": "", - "If the spam complaint was accidental, or you would like to begin receiving emails again, you can resubscribe to emails by clicking the button on the previous screen.": "", - "If you cancel your subscription now, you will continue to have access until {{periodEnd}}.": "", - "If you have a corporate or government email account, reach out to your IT department and ask them to allow emails to be received from {{senderEmail}}": "", - "If you would like to start receiving emails again, the best next steps are to check your email address on file for any issues and then click resubscribe on the previous screen.": "", - "If you're not receiving the email newsletter you've subscribed to, here are a few things to check.": "", - "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "", - "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "", - "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "", - "Invalid email address": "", - "Jamie Larson": "", - "jamie@example.com": "", + "Help! I'm not receiving emails": "Допоможіть! Я не отримую електронні листи", + "Here are a few other sites you may enjoy.": "Ось кілька інших сайтів, які можуть вам сподобатися.", + "If a newsletter is flagged as spam, emails are automatically disabled for that address to make sure you no longer receive any unwanted messages.": "Якщо розсилку позначено як спам, електронні листи для цієї адреси автоматично вимикаються, щоб ви більше не отримували небажаних повідомлень.", + "If the spam complaint was accidental, or you would like to begin receiving emails again, you can resubscribe to emails by clicking the button on the previous screen.": "Якщо скарга на спам була випадковою або ви хочете знову отримувати електронні листи, ви можете знову підписатися на електронні листи, натиснувши кнопку на попередньому екрані.", + "If you cancel your subscription now, you will continue to have access until {{periodEnd}}.": "Якщо ви скасуєте свою підписку зараз, ви матимете доступ до {{periodEnd}}.", + "If you have a corporate or government email account, reach out to your IT department and ask them to allow emails to be received from {{senderEmail}}": "Якщо у вас є корпоративний або державний обліковий запис електронної пошти, зверніться до свого ІТ-відділу та попросіть їх дозволити отримувати електронні листи від {{senderEmail}}", + "If you would like to start receiving emails again, the best next steps are to check your email address on file for any issues and then click resubscribe on the previous screen.": "Якщо ви хочете знову почати отримувати електронні листи, найкращим наступним кроком буде перевірити свою адресу електронної пошти на наявність проблем, а потім на попередньому екрані натиснути Підписатися знову.", + "If you're not receiving the email newsletter you've subscribed to, here are a few things to check.": "Якщо ви не отримуєте розсилку електронною поштою, на яку підписалися, то ось декілька речей щоб перевірити.", + "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "Якщо ви виконали всі ці перевірки, але досі не отримуєте електронних листів, ви можете звернутись до нас по допомогу, зв’язавшись з {{supportAddress}}.", + "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "Якщо під час спроби надсилання розсилки виникає постійна помилка, електронні листи в обліковому записі буде вимкнено.", + "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "У своєму поштовому клієнті додайте {{senderEmail}} до списку контактів. Це сигналізує вашому постачальнику послуг електронної пошти, що листам, надісланим із цієї адреси, слід довіряти.", + "Invalid email address": "Недійсна адреса електронної пошти", + "Jamie Larson": "Джеймі Ларсон", + "jamie@example.com": "jamie@example.com", "Less like this": "Менше подібних", - "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "", - "Manage": "Управління", - "Maybe later": "", - "Memberships unavailable, contact the owner for access.": "", - "month": "", - "Monthly": "Місячно", + "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "Переконайтеся, що електронні листи випадково не потрапляють у папку \"Спам\" або \"Реклама\" вашої папки \"Вхідні\". Якщо вони так і є, натисніть \"Позначити як не спам\" і/або \"Перемістити до вхідних\".", + "Manage": "Керувати", + "Maybe later": "Можливо пізніше", + "Memberships unavailable, contact the owner for access.": "Членство недоступне, зв’яжіться з власником, щоб отримати доступ.", + "month": "Місяць", + "Monthly": "Щомісяця", "More like this": "Більше подібних", "Name": "Ім'я", - "Need more help? Contact support": "", - "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "", + "Need more help? Contact support": "Потрібна допомога? Зверніться до служби підтримки", + "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Розсилки можуть бути вимкнені у вашому обліковому записі з двох причин: попередній електронний лист було позначено як спам або спроба надіслати електронний лист призвела до збою (чи відмови).", + "No member exists with this e-mail address.": "Не існує жодного учасника з цією адресою електронної пошти.", + "No member exists with this e-mail address. Please sign up first.": "Не існує жодного учасника з цією адресою електронної пошти. Будь ласка, спочатку зареєструйтеся.", "Not receiving emails?": "Не приходять листи?", "Now check your email!": "А тепер перевір свою пошту!", - "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "", - "Permanent failure (bounce)": "", - "Phone number": "", - "Plan": "", - "Plan checkout was cancelled.": "", - "Plan upgrade was cancelled.": "", - "Please contact {{supportAddress}} to adjust your complimentary subscription.": "", - "Please enter {{fieldName}}": "", - "Please fill in required fields": "", + "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Якщо після повторної підписки ви все ще не бачите електронних листів у папці \"Вхідні\", перевірте папку зі спамом. Деякі постачальники вхідних повідомлень реєструють попередні скарги на спам і продовжуватимуть позначати електронні листи. Якщо це станеться, позначте устанню розсилку як \"Не спам\", щоб повернути його до основної папки \"Вхідні\".", + "Permanent failure (bounce)": "Постійний збій (відмова)", + "Phone number": "Номер телефона", + "Plan": "План", + "Plan checkout was cancelled.": "Оформлення плану скасовано.", + "Plan upgrade was cancelled.": "Оновлення плану скасовано.", + "Please contact {{supportAddress}} to adjust your complimentary subscription.": "Будь ласка, зв’яжіться з {{supportAddress}}, щоб налаштувати безкоштовну підписку.", + "Please enter {{fieldName}}": "Будь ласка, введіть {{fieldName}}", + "Please fill in required fields": "Будь ласка, заповніть обов'язкові поля", "Price": "Ціна", "Re-enable emails": "Знову включити пошту", - "Recommendations": "", - "Renews at {{price}}.": "", + "Recommendations": "Рекомендації", + "Renews at {{price}}.": "Поновлення за {{price}}.", "Retry": "Повтори спробу", - "Save": "Збережи", - "Send an email and say hi!": "", - "Send an email to {{senderEmail}} and say hello. This can also help signal to your mail provider that emails to and from this address should be trusted.": "", + "Save": "Зберегти", + "Send an email and say hi!": "Надішліть електронний лист і привітайтеся!", + "Send an email to {{senderEmail}} and say hello. This can also help signal to your mail provider that emails to and from this address should be trusted.": "Надішліть електронний лист на адресу {{senderEmail}} і привітайтеся. Це також може допомогти повідомити вашому постачальнику послуг електронної пошти, що електронні листи на цю адресу та з неї слід довіряти.", "Sending login link...": "Відправляється посилання для входу...", "Sending...": "Відправляється...", - "Show all": "", + "Show all": "Показати все", "Sign in": "Вхід", - "Sign out": "", + "Sign out": "Вихід", "Sign up": "Реєстрація", - "Signup error: Invalid link": "", - "Something went wrong, please try again later.": "", - "Sorry, that didn’t work.": "", - "Spam complaints": "", + "Signup error: Invalid link": "Помилка реєстрації: недійсне посилання", + "Something went wrong, please try again later.": "Щось пішло не так, спробуйте пізніше.", + "Sorry, that didn’t work.": "Вибачте, це не спрацювало.", + "Spam complaints": "Скарги на спам", "Start {{amount}}-day free trial": "Почни {{amount}}-денний безплатний період", - "Starting {{startDate}}": "", - "Starting today": "", - "Submit feedback": "Вишли відгук", - "Subscribe": "", - "Subscribed": "", - "Success": "", - "Success! Check your email for magic link to sign-in.": "", - "Success! Your account is fully activated, you now have access to all content.": "", - "Success! Your email is updated.": "", + "Starting {{startDate}}": "Починаючи з {{startDate}}", + "Starting today": "Починаючи з сьогоднішнього дня", + "Submit feedback": "Надіслати відгук", + "Subscribe": "Підписатися", + "Subscribed": "Підписаний", + "Subscription plan updated successfully": "План підписки успішно оновлено", + "Success": "Успіх", + "Success! Check your email for magic link to sign-in.": "Успіх! Перевірте свою електронну пошту на наявність посилання для входу.", + "Success! Your account is fully activated, you now have access to all content.": "Успіх! Ваш обліковий запис повністю активовано, тепер ви маєте доступ до всього вмісту.", + "Success! Your email is updated.": "Успіх! Ваша електронна адреса оновлена.", "Successfully unsubscribed": "Підписку успішно скасовано", - "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "", - "Thank you for your support": "", - "Thank you for your support!": "", + "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "Дякуємо за підписку. Перш ніж почати читати, нижче наведено кілька інших сайтів, які можуть вам сподобатися.", + "Thank you for your support": "Дякуємо за вашу підтримку", + "Thank you for your support!": "Дякуємо за підтримку!", "Thanks for the feedback!": "Дякуємо за відгук!", "That didn't go to plan": "Щось пішло не так", - "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "", - "There was a problem submitting your feedback. Please try again a little later.": "", - "There was an error processing your payment. Please try again.": "", + "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Адреса електронної пошти, яку є внас — {{memberEmail}} — якщо вона не вірна, ви можете оновити її в .", + "There was a problem submitting your feedback. Please try again a little later.": "Під час надсилання відгуку виникла проблема. Спробуйте ще раз трохи пізніше.", + "There was an error cancelling your subscription, please try again.": "Під час скасування вашої підписки сталася помилка. Спробуйте ще раз.", + "There was an error continuing your subscription, please try again.": "Під час продовження підписки сталася помилка. Спробуйте ще раз.", + "There was an error processing your payment. Please try again.": "Під час обробки вашого платежу сталася помилка. Спробуйте ще раз.", + "There was an error sending the email, please try again": "Під час надсилання листа сталася помилка. Повторіть спробу", "This site is invite-only, contact the owner for access.": "Цей сайт доступний тільки за запрошенням, звернись до власника сайта для доступу.", - "This site is not accepting payments at the moment.": "", + "This site is not accepting payments at the moment.": "Цей сайт на даний момент не приймає платежі.", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Щоб завершити реєстрацію, натисни посилання в своїй електронній пошті для підтвердження. Якщо електронний лист не прийде протягом 3 хвилин, перевір папку спам!", - "To continue to stay up to date, subscribe to {{publication}} below.": "", - "Try free for {{amount}} days, then {{originalPrice}}.": "", - "Unlock access to all newsletters by becoming a paid subscriber.": "", + "To continue to stay up to date, subscribe to {{publication}} below.": "Щоб і надалі бути в курсі подій, підпишіться на {{publication}}.", + "Too many attempts try again in {{number}} days.": "Забагато спроб, повторіть спробу через {{number}} днів.", + "Too many attempts try again in {{number}} hours.": "Забагато спроб, повторіть спробу через {{number}} годин.", + "Too many attempts try again in {{number}} minutes.": "Забагато спроб, повторіть спробу через {{number}} хвилин.", + "Too many different sign-in attempts, try again in {{number}} days": "Забагато різних спроб входу. Повторіть спробу через {{number}} днів", + "Too many different sign-in attempts, try again in {{number}} hours": "Забагато різних спроб входу. Повторіть спробу через {{number}} годин", + "Too many different sign-in attempts, try again in {{number}} minutes": "Забагато різних спроб входу. Повторіть спробу через {{number}} хвилин", + "Try free for {{amount}} days, then {{originalPrice}}.": "Спробуйте безкоштовно протягом {{amount}} днів, надалі за {{originalPrice}}.", + "Unable to initiate checkout session": "Не вдалося розпочати сеанс оформлення замовлення", + "Unlock access to all newsletters by becoming a paid subscriber.": "Розблокуйте доступ до всіх розсилок, ставши платним підписником.", "Unsubscribe from all emails": "Відписатись від усіх листів", - "Unsubscribed": "", - "Unsubscribed from all emails.": "", + "Unsubscribed": "Скасував підписку", + "Unsubscribed from all emails.": "Скасував підписку на всі листи.", "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "Відписка від листів не скасує твою платну підписку на {{title}}", - "Update": "", + "Update": "Оновити", "Update your preferences": "Онови свої налаштування", - "Verification link sent, check your inbox": "", - "Verify your email address is correct": "", - "View plans": "", - "We couldn't unsubscribe you as the email address was not found. Please contact the site owner.": "Ми не можемо відписати тебе, оскільки адреса електронної пошти не знайдена. Будь ласка, зв'яжись з власником сайту.", - "Welcome back, {{name}}!": "", - "Welcome back!": "", - "Welcome to {{siteTitle}}": "", - "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "", - "Why has my email been disabled?": "", - "year": "", - "Yearly": "Річно", - "You currently have a free membership, upgrade to a paid subscription for full access.": "", - "You have been successfully resubscribed": "Тебе успішно підписано знову", - "You're currently not receiving emails": "", - "You're not receiving emails": "", - "You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "Не приходять листи, оскільки останній отриманий лист був тобою позначений як спам, або листи не можуть бути доставлені на твою адресу електронної пошти.", - "You've successfully signed in.": "", - "You've successfully subscribed to": "", - "Your account": "Твій обліковий запис", - "Your input helps shape what gets published.": "Твій відгук допоможе вирішити що публікувати далі.", - "Your subscription will expire on {{expiryDate}}": "", - "Your subscription will renew on {{renewalDate}}": "", - "Your subscription will start on {{subscriptionStart}}": "" + "Verification link sent, check your inbox": "Посилання для підтвердження надіслано, перевірте свою поштову скриньку", + "Verify your email address is correct": "Переконайтеся, що ваша електронна адреса правильна", + "View plans": "Переглянути плани", + "We couldn't unsubscribe you as the email address was not found. Please contact the site owner.": "Ми не можемо вас відписати, оскільки адреса електронної пошти не знайдена. Будь ласка, зв'яжись з власником сайту.", + "Welcome back, {{name}}!": "З поверненням, {{name}}!", + "Welcome back!": "З поверненням!", + "Welcome to {{siteTitle}}": "Вітаємо на {{siteTitle}}", + "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "Коли скринька \"Вхідні\" не може прийняти електронний лист, це зазвичай називають відмовою. У багатьох випадках це може бути тимчасовим. Однак у деяких випадках відхилений електронний лист може бути повернуто як постійна помилка, якщо адреса електронної пошти недійсна або не існує.", + "Why has my email been disabled?": "Чому мою електронну пошту вимкнено?", + "year": "Рік", + "Yearly": "Щорічно", + "You currently have a free membership, upgrade to a paid subscription for full access.": "Наразі у вас є безкоштовне членство, перейдіть на платну підписку для повного доступу.", + "You have been successfully resubscribed": "Ви успішно повторно підписалися", + "You're currently not receiving emails": "Зараз ви не отримуєте електронних листів", + "You're not receiving emails": "Ви не отримуєте електронних листів", + "You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "Ви не отримуєте електронні листи, тому що ви або позначили останнє повідомлення як спам, або тому, що повідомлення не можуть бути доставлені на надану вами адресу електронної пошти.", + "You've successfully signed in.": "Ви успішно увійшли.", + "You've successfully subscribed to": "Ви успішно підписалися на", + "Your account": "Ваш обліковий запис", + "Your email has failed to resubscribe, please try again": "Не вдалося повторно підписатися за вашою електронну адресою, спробуйте ще раз", + "Your input helps shape what gets published.": "Ваш відгук допомагає обирати що публікувати далі.", + "Your subscription will expire on {{expiryDate}}": "Термін дії вашої підписки закінчується {{expiryDate}}", + "Your subscription will renew on {{renewalDate}}": "Ваша підписка буде продовжена {{renewalDate}}", + "Your subscription will start on {{subscriptionStart}}": "Ваша підписка почне діяти {{subscriptionStart}}" } diff --git a/ghost/i18n/locales/uk/search.json b/ghost/i18n/locales/uk/search.json new file mode 100644 index 00000000000..2a69ee4ea5e --- /dev/null +++ b/ghost/i18n/locales/uk/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "Автори", + "Cancel": "Скасувати", + "No matches found": "Збігів не знайдено", + "Posts": "Публікації", + "Search posts, tags and authors": "Шукайте публікацій, теги та авторів", + "Show more results": "Показати більше результатів", + "Tags": "Теги" +} diff --git a/ghost/i18n/locales/uk/signup-form.json b/ghost/i18n/locales/uk/signup-form.json index cc957d9d0d1..53b0422fea5 100644 --- a/ghost/i18n/locales/uk/signup-form.json +++ b/ghost/i18n/locales/uk/signup-form.json @@ -1,9 +1,9 @@ { - "Email sent": "", - "Now check your email!": "", - "Please enter a valid email address": "", - "Something went wrong, please try again.": "", - "Subscribe": "", - "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "", - "Your email address": "" + "Email sent": "Листа відправлено", + "Now check your email!": "Тепер перевірте свою поштову скриньку!", + "Please enter a valid email address": "Будь ласка, введіть коректну електронну адресу", + "Something went wrong, please try again.": "Щось пішло не так, спробуйте ще раз.", + "Subscribe": "Підписатися", + "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Щоб завершити реєстрацію, натисніть на посилання для підтверження, яке надійде до вашої поштової скриньки. Якщо лист не надійщов впродож 3 хвилин, то перевірте каталог зі спамом!", + "Your email address": "Ваша електронна адреса" } diff --git a/ghost/i18n/locales/ur/comments.json b/ghost/i18n/locales/ur/comments.json new file mode 100644 index 00000000000..6769f3f4217 --- /dev/null +++ b/ghost/i18n/locales/ur/comments.json @@ -0,0 +1,71 @@ +{ + "{{amount}} characters left": "{{amount}} حروف باقی ہیں", + "{{amount}} comments": "{{amount}} تبادلے", + "{{amount}} days ago": "{{amount}} دن پہلے", + "{{amount}} hrs ago": "", + "{{amount}} mins ago": "", + "{{amount}} months ago": "{{amount}} مہینے پہلے", + "{{amount}} more": "", + "{{amount}} seconds ago": "{{amount}} سیکنڈ پہلے", + "{{amount}} weeks ago": "{{amount}} ہفتے پہلے", + "{{amount}} years ago": "{{amount}} سال پہلے", + "1 comment": "1 تبادلہ", + "Add comment": "تبادلہ شامل کریں", + "Add context to your comment, share your name and expertise to foster a healthy discussion.": "اپنے تبادلے میں سیاق و سباق شامل کریں، اچھی گفتگو کو بڑھانے کے لئے اپنا نام اور ماہریت شیئر کریں۔", + "Add reply": "جواب شامل کریں", + "Already a member?": "پہلے ہی رکن ہیں؟", + "Anonymous": "ناشناس", + "Become a member of {{publication}} to start commenting.": "تبادلہ شروع کرنے کے لئے {{publication}} کا رکن بنیں۔", + "Become a paid member of {{publication}} to start commenting.": "تبادلہ شروع کرنے کے لئے {{publication}} کا ادائیگی رکن بنیں۔", + "Cancel": "منسوخ کریں", + "Comment": "تبادلہ", + "Complete your profile": "اپنا پروفائل مکمل کریں", + "Delete": "حذف کریں", + "Deleted member": "حذف شدہ رکن", + "Discussion": "تبادلہ", + "Edit": "ترتیب دیں", + "Edit this comment": "اس تبادلے کو ترتیب دیں", + "edited": "", + "Enter your name": "اپنا نام درج کریں", + "Expertise": "اہلیت", + "Founder @ Acme Inc": "ناظم @ Acme Inc", + "Full-time parent": "پورے وقت کا والد یا والدہ", + "Head of Marketing at Acme, Inc": "Acme، Inc کے مارکیٹنگ کا سربراہ", + "Hide": "چھپائیں", + "Hide comment": "تبادلہ چھپائیں", + "Jamie Larson": "جیمی لارسن", + "Join the discussion": "تبادلے میں شامل ہوں", + "Just now": "ابھی", + "Local resident": "مقامی رہائشی", + "Member discussion": "رکن کا تبادلہ", + "Name": "نام", + "Neurosurgeon": "نیوروسرجن", + "One day ago": "ایک دن پہلے", + "One hour ago": "ایک گھنٹہ پہلے", + "One min ago": "", + "One month ago": "ایک مہینہ پہلے", + "One week ago": "ایک ہفتہ پہلے", + "One year ago": "ایک سال پہلے", + "Reply": "جواب", + "Reply to comment": "تبادلے کا جواب دیں", + "Report": "رپورٹ", + "Report comment": "تبادلے کی رپورٹ", + "Report this comment?": "کیا آپ اس تبادلے کو رپورٹ کرنا چاہتے ہیں؟", + "Save": "محفوظ کریں", + "Sending": "بھیج رہا ہے", + "Sent": "بھیجا گیا", + "Show": "دکھائیں", + "Show {{amount}} more replies": "{{amount}} مزید جوابات دکھائیں", + "Show {{amount}} previous comments": "{{amount}} پچھلے تبادلات دکھائیں", + "Show 1 more reply": "1 مزید جواب دکھائیں", + "Show 1 previous comment": "1 پچھلا تبادلہ دکھائیں", + "Show comment": "تبادلہ دکھائیں", + "Sign in": "سائن ان کریں", + "Sign up now": "ابھی رجسٹر ہوں", + "Start the conversation": "بات چیت شروع کریں", + "This comment has been hidden.": "یہ تبادلہ چھپا گیا ہے۔", + "This comment has been removed.": "یہ تبادلہ ہٹا دیا گیا ہے۔", + "Upgrade now": "اب اپ گریڈ کریں", + "Yesterday": "کل", + "Your request will be sent to the owner of this site.": "آپ کا درخواست اس سائٹ کے مالک کو بھیجا جائے گا۔" +} diff --git a/ghost/i18n/locales/ur/ghost.json b/ghost/i18n/locales/ur/ghost.json new file mode 100644 index 00000000000..06b3dde19b1 --- /dev/null +++ b/ghost/i18n/locales/ur/ghost.json @@ -0,0 +1,34 @@ +{ + "All the best!": "سب سے بہترین خواہشات!", + "Complete signup for {{siteTitle}}!": "{{siteTitle}} کے لئے رجسٹریشن مکمل کریں!", + "Complete your sign up to {{siteTitle}}!": "{{siteTitle}} پر اپنے سائن اپ کو مکمل کریں!", + "Confirm email address": "ای میل کا پتہ تصدیق کریں", + "Confirm signup": "رجسٹریشن کی تصدیق کریں", + "Confirm your email address": "آپ کا ای میل پتہ تصدیق کریں", + "Confirm your email update for {{siteTitle}}!": "{{siteTitle}} کے لئے آپ کی ای میل کا اپ ڈیٹ تصدیق کریں!", + "Confirm your subscription to {{siteTitle}}": "{{siteTitle}} کو سبسکرائب کرنے کی تصدیق کریں", + "For your security, the link will expire in 24 hours time.": "آپ کی حفاظت کے لئے، لنک 24 گھنٹے میں ختم ہو جائے گا۔", + "Hey there,": "ہی وہاں،", + "Hey there!": "ہی وہاں!", + "If you did not make this request, you can safely ignore this email.": "اگر آپ نے یہ درخواست نہیں کی ہے تو، آپ یہ ای میل بے خوف نظر انداز کر سکتے ہیں۔", + "If you did not make this request, you can simply delete this message.": "اگر آپ نے یہ درخواست نہیں کی ہے تو، بس یہ پیغام ہٹا دیں۔", + "Please confirm your email address with this link:": "براہ کرم اس لنک کے ساتھ آپ کا ای میل پتہ تصدیق کریں:", + "Secure sign in link for {{siteTitle}}": "{{siteTitle}} کے لئے محفوظ سائن ان لنک", + "See you soon!": "جلد ہی ملیں گے!", + "Sent to {{email}}": "{{email}} کو بھیجا گیا", + "Sign in": "سائن ان", + "Sign in to {{siteTitle}}": "{{siteTitle}} میں سائن ان کریں", + "Tap the link below to complete the signup process for {{siteTitle}}, and be automatically signed in:": "نیچے دیے گئے لنک پر ٹیپ کریں تاکہ {{siteTitle}} کے لئے رجسٹریشن کا عمل مکمل ہو اور آپ خود بخود سائن ان ہو جائیں:", + "Thank you for signing up to {{siteTitle}}!": "{{siteTitle}} میں رجسٹر ہونے کا شکریہ!", + "Thank you for subscribing to {{siteTitle}}!": "{{siteTitle}} کو سبسکرائب کرنے کا شکریہ!", + "Thank you for subscribing to {{siteTitle}}.": "{{siteTitle}} کو سبسکرائب کرنے کا شکریہ!", + "Thank you for subscribing to {{siteTitle}}. Tap the link below to be automatically signed in:": "{{siteTitle}} کو سبسکرائب کرنے کا شکریہ. نیچے دیے گئے لنک پر ٹیپ کریں تاکہ آپ خود بخود سائن ان ہو جائیں:", + "This email address will not be used.": "یہ ای میل پتہ استعمال نہیں ہو گا۔", + "Welcome back to {{siteTitle}}!": "{{siteTitle}} میں خوش آمدید!", + "Welcome back! Use this link to securely sign in to your {{siteTitle}} account:": "خوش آمدید! اس لنک کا استعمال کریں تاکہ آپ {{siteTitle}} کے اکاؤنٹ میں محفوظ طریقے سے سائن ان کر سکیں:", + "You can also copy & paste this URL into your browser:": "آپ یہ URL کا نسخہ اور پیسٹ بھی اپنے براؤزر میں کر سکتے ہیں:", + "You will not be signed up, and no account will be created for you.": "آپ کو رجسٹر نہیں کیا جائے گا، اور آپ کے لئے کوئی اکاؤنٹ نہیں بنایا جائے گا۔", + "You will not be subscribed.": "آپ کو سبسکرائب نہیں کیا جائے گا۔", + "You're one tap away from subscribing to {{siteTitle}} — please confirm your email address with this link:": "{{siteTitle}} کو سبسکرائب ہونے کے لئے آپ ایک ٹیپ کے فاصلے پر ہیں — براہ کرم اس لنک کے ساتھ آپ کا ای میل پتہ تصدیق کریں:", + "You're one tap away from subscribing to {{siteTitle}}!": "{{siteTitle}} کو سبسکرائب ہونے کے لئے آپ ایک ٹیپ کے فاصلے پر ہیں!" +} diff --git a/ghost/i18n/locales/ur/portal.json b/ghost/i18n/locales/ur/portal.json new file mode 100644 index 00000000000..8a49ad03bc0 --- /dev/null +++ b/ghost/i18n/locales/ur/portal.json @@ -0,0 +1,208 @@ +{ + "(save {{highestYearlyDiscount}}%)": "", + "{{amount}} days free": "{{amount}} دن مفت پڑھیں", + "{{amount}} off": "{{amount}} کم", + "{{amount}} off for first {{number}} months.": "{{number}} مہینوں کے لئے {{amount}} کمی", + "{{amount}} off for first {{period}}.": "{{period}} کے لئے {{amount}} کمی", + "{{amount}} off forever.": "{{amount}} ہمیشہ کے لئے کم", + "{{discount}}% discount": "{{discount}}٪ چھوٹ", + "{{memberEmail}} will no longer receive {{newsletterName}} newsletter.": "{{memberEmail}} کو مزید {{newsletterName}} نیوزلیٹر نہیں ملے گا۔", + "{{memberEmail}} will no longer receive emails when someone replies to your comments.": "{{memberEmail}} کو مزید ای میل نہیں ملے گا جب کوئی آپکے تبادلوں کا جواب دیتا ہے۔", + "{{memberEmail}} will no longer receive this newsletter.": "{{memberEmail}} کو مزید یہ نیوزلیٹر نہیں ملے گا۔", + "{{trialDays}} days free": "{{trialDays}} دن مفت پڑھیں", + "+1 (123) 456-7890": "", + "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "ایک لاگ ان لنک آپکے ان باکس میں بھیج دیا گیا ہے۔ اگر 3 منٹ میں نہیں آئی تو یہ یقینی بنائیں کہ آپکے اسپیم فولڈر کو چیک کریں۔", + "Account": "اکاؤنٹ", + "Account details updated successfully": "", + "Account settings": "اکاؤنٹ کی ترتیبات", + "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "جب مفت ڈھانچا ختم ہو جائے گا، آپکو اس ڈھانچے کے لئے عام قیمت چارج ہوگی جس پر آپ نے فیصلہ کیا ہوا ہے۔ آپ ہمیشہ اس سے پہلے منسلک کر سکتے ہیں۔", + "Already a member?": "پہلے ہی ممبر ہیں؟", + "An error occurred": "", + "An unexpected error occured. Please try again or contact support if the error persists.": "ایک غیر متوقعہ خطا واقع ہوگئی ہے۔ براہ کرم دوبارہ کوشش کریں یا اگر خطا جاری رہے تو سپورٹ سے رابطہ کریں۔", + "Back": "پیچھے", + "Back to Log in": "لاگ ان پر واپسی", + "Billing info": "بلنگ کی معلومات", + "Black Friday": "کالا جمعہ", + "Cancel anytime.": "کسی بھی وقت منسلک کریں۔", + "Cancel subscription": "سبسکرپشن منسلک کریں", + "Cancellation reason": "منسلک کرنے کا وجہ", + "Change": "تبدیل کریں", + "Change plan": "", + "Check spam & promotions folders": "اسپیم اور پروموشن فولڈر چیک کریں", + "Check with your mail provider": "آپکے میل فراہم کنندہ کے ساتھ چیک کریں", + "Check your inbox to verify email update": "", + "Choose": "منتخب کریں", + "Choose a different plan": "مختلف منصوبہ منتخب کریں", + "Choose a plan": "", + "Choose your newsletters": "اپنے نیوزلیٹر کو منتخب کریں", + "Click here to retry": "دوبارہ کوشش کرنے کے لئے یہاں کلک کریں", + "Close": "بند کریں", + "Comments": "تبادلے", + "Complimentary": "تعریف", + "Confirm": "تصدیق", + "Confirm cancellation": "منسلک کرنے کی تصدیق", + "Confirm subscription": "سبسکرپشن کی تصدیق", + "Contact support": "سپورٹ سے رابطہ کریں", + "Continue": "جاری رہے", + "Continue subscription": "سبسکرپشن جاری رکھیں", + "Could not create stripe checkout session": "", + "Could not sign in. Login link expired.": "سائن ان نہیں ہو سکا۔ لاگ ان لنک کا وقت ختم ہو گیا ہے۔", + "Could not update email! Invalid link.": "ای میل اپ ڈیٹ نہیں ہو سکا! غیر معتبر لنک۔", + "Create a new contact": "نیا رابطہ بنائیں", + "Current plan": "موجودہ منصوبہ", + "Delete account": "اکاؤنٹ حذف کریں", + "Didn't mean to do this? Manage your preferences .": "یہ کرنا میرا مطلب نہیں تھا؟ اپنی ترجیحات کا منظم کریں ۔", + "Don't have an account?": "اکاؤنٹ نہیں ہے؟", + "Edit": "ترتیب", + "Email": "ای میل", + "Email newsletter": "ای میل نیوزلیٹر", + "Email newsletter settings updated": "", + "Email preferences": "ای میل کی ترجیحات", + "Emails": "ای میلز", + "Emails disabled": "ای میلز غیر فعال ہیں", + "Ends {{offerEndDate}}": "ختم ہوتا ہے {{offerEndDate}}", + "Enter your email address": "", + "Enter your name": "", + "Error": "خطا", + "Expires {{expiryDate}}": "ختم ہوتا ہے {{expiryDate}}", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", + "Forever": "ہمیشہ کے لئے", + "Free Trial – Ends {{trialEnd}}": "مفت ڈھانچا – ختم ہوتا ہے {{trialEnd}}", + "Get help": "مدد حاصل کریں", + "Get in touch for help": "مدد کے لئے رابطہ کریں", + "Get notified when someone replies to your comment": "جب کوئی آپکے تبادلے کا جواب دیتا ہے تو مطلع ہوں", + "Give feedback on this post": "اس پوسٹ پر فیڈبیک دیں", + "Help! I'm not receiving emails": "مدد! میرے پاس ای میلز نہیں آ رہے", + "Here are a few other sites you may enjoy.": "", + "If a newsletter is flagged as spam, emails are automatically disabled for that address to make sure you no longer receive any unwanted messages.": "اگر کوئی نیوزلیٹر اسپیم کے طور پر چھانا گیا ہے تو اس پتہ کے لئے ای میلز خود بخود غیر فعال ہو جاتے ہیں تاکہ یہ یقینی ہو کہ آپکو مزید کوئی غیر مطلوبہ پیغام نہیں ملے گا۔", + "If the spam complaint was accidental, or you would like to begin receiving emails again, you can resubscribe to emails by clicking the button on the previous screen.": "اگر اسپیم کے خیالات کی شکایت صدف ہوئی ہو یا آپ مزید ای میلز حاصل کرنا چاہتے ہیں تو آپ پچھلے اسکرین پر دیے گئے بٹن پر کلک کر کے دوبارہ ای میل کی سبسکرپشن کر سکتے ہیں۔", + "If you cancel your subscription now, you will continue to have access until {{periodEnd}}.": "اگر آپ اپنی سبسکرپشن اب منسلک کرتے ہیں، آپکو {{periodEnd}} تک رسائی ملتی رہے گی۔", + "If you have a corporate or government email account, reach out to your IT department and ask them to allow emails to be received from {{senderEmail}}": "اگر آپ کا کارپوریٹ یا حکومتی ای میل اکاؤنٹ ہے، اپنے آئٹی ڈیپارٹمنٹ سے رابطہ کریں اور ان سے کہیں کہ وہ ای میلز کو {{senderEmail}} سے ملنے دیں۔", + "If you would like to start receiving emails again, the best next steps are to check your email address on file for any issues and then click resubscribe on the previous screen.": "اگر آپ مزید ای میلز حاصل کرنا چاہتے ہیں، تو بہترین اگلا قدم یہ ہے کہ آپ اپنے فائل میں اپنا ای میل ایڈریس چیک کریں اور پھر پچھلے اسکرین پر دئیے گئے دوبارہ سبسکرائب پر کلک کریں۔", + "If you're not receiving the email newsletter you've subscribed to, here are a few things to check.": "", + "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "اگر آپ نے تمام چیکس مکمل کی ہیں اور آپکو مزید ای میلز نہیں مل رہے ہیں، تو آپ {{supportAddress}} سے رابطہ کر کے حمایت حاصل کر سکتے ہیں۔", + "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "اگر کوشش کرتے وقت نیوزلیٹر بھیجنے پر ہمیشہ کے لئے ناکامی ہوتی ہے تو اکاؤنٹ پر ای میلز غیر فعال ہو جائیں گے۔", + "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "اپنے ای میل کلائنٹ میں {{senderEmail}} کو اپنی رابطہ فہرست میں شامل کریں۔ یہ آپکے میل فراہم کنندہ کو اشارہ ہوتا ہے کہ اس پتہ سے بھیجے گئے ای میلز پر اعتماد کیا جانا چاہئے۔", + "Invalid email address": "", + "Jamie Larson": "", + "jamie@example.com": "", + "Less like this": "اس طرح کم", + "Make sure emails aren't accidentally ending up in the Spam or Promotions folders of your inbox. If they are, click on \"Mark as not spam\" and/or \"Move to inbox\".": "یہ یہ سنجیدہ ہو کہ ای میلز اتفاقی طور پر آپکے ان باکس کے اسپیم یا پروموشن فولڈرز میں ختم نہیں ہو رہے ہیں۔ اگر ہیں تو \"غیر فعال ہے\" پر کلک کریں اور/یا \"ان باکس میں منتقل کریں\"۔", + "Manage": "منظم کریں", + "Maybe later": "", + "Memberships unavailable, contact the owner for access.": "", + "month": "", + "Monthly": "ہر مہینے", + "More like this": "اس کی مزید", + "Name": "نام", + "Need more help? Contact support": "مزید مدد چاہیے؟ حمایت سے رابطہ کریں", + "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "نیوزلیٹرز کو آپکے اکاؤنٹ پر غیر فعال کرنے کے دو ممکنہ وجوہات ہیں: پچھلا ای میل اسپیم مارک کیا گیا تھا یا ای میل بھیجنے کی کوشش نے ہمیشہ کے لئے ناکامی (باؤنس) کا نتیجہ دیا۔", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", + "Not receiving emails?": "ای میلز نہیں آ رہے؟", + "Now check your email!": "اب اپنے ای میل چیک کریں!", + "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "جب ایک بار دوبارہ سبسکرائب ہو جائے، اگر آپکو مزید ای میلز اپنے ان باکس میں نہیں دکھتے ہیں تو اپنے ان باکس کی سپیم فولڈر چیک کریں۔ کچھ ان باکس فراہم کنندگان نے پچھلے اسپیم شکایات کا ریکارڈ رکھا ہوتا ہے اور وہ ای میلز کو جاری رکھتے ہیں۔ اگر یہ ہوتا ہے، تو آخری نیوزلیٹر کو 'غیر فعال' مارک کریں تاکہ یہ اسے دوبارہ آپکے پرائمری ان باکس میں منتقل کریں۔", + "Permanent failure (bounce)": "ہمیشہ کے لئے ناکامی (باؤنس)", + "Phone number": "", + "Plan": "پلان", + "Plan checkout was cancelled.": "پلان چیک آؤٹ منسوخ ہوگیا تھا۔", + "Plan upgrade was cancelled.": "پلان اپگریڈ منسوخ ہوگیا تھا۔", + "Please contact {{supportAddress}} to adjust your complimentary subscription.": "", + "Please enter {{fieldName}}": "", + "Please fill in required fields": "براہ کرم مطلوبہ شعبے بھریں", + "Price": "قیمت", + "Re-enable emails": "ای میلز کو دوبارہ چالو کریں", + "Recommendations": "", + "Renews at {{price}}.": "نیا کرتا ہے {{price}} پر۔", + "Retry": "مزید کوشش", + "Save": "محفوظ کریں", + "Send an email and say hi!": "ایک ای میل بھیجیں اور ہائی کہیں!", + "Send an email to {{senderEmail}} and say hello. This can also help signal to your mail provider that emails to and from this address should be trusted.": "", + "Sending login link...": "لاگ ان لنک بھیجا جا رہا ہے...", + "Sending...": "بھیجا جا رہا ہے...", + "Show all": "", + "Sign in": "سائن ان", + "Sign out": "لاگ آؤٹ", + "Sign up": "سائن اپ", + "Signup error: Invalid link": "سائن اپ خطا: غیر معتبر لنک", + "Something went wrong, please try again later.": "", + "Sorry, that didn’t work.": "معاف کریں، یہ کام نہیں کیا گیا۔", + "Spam complaints": "سپیم شکایتیں", + "Start {{amount}}-day free trial": "{{amount}} دن کا مفت ٹرائل شروع کریں", + "Starting {{startDate}}": "{{startDate}} سے شروع ہو رہا ہے", + "Starting today": "آج سے شروع ہو رہا ہے", + "Submit feedback": "رائے دیں", + "Subscribe": "سبسکرائب کریں", + "Subscribed": "سبسکرائب ہوگیا", + "Subscription plan updated successfully": "", + "Success": "کامیابی", + "Success! Check your email for magic link to sign-in.": "کامیابی! سائن ان کے لئے جادوی لنک کے لئے اپنا ای میل چیک کریں۔", + "Success! Your account is fully activated, you now have access to all content.": "کامیابی! آپ کا اکاؤنٹ مکمل طور پر چالو ہوا ہے، آپ کو اب تمام مواد تک رسائی ہے۔", + "Success! Your email is updated.": "کامیابی! آپ کا ای میل اپ ڈیٹ ہوا ہے۔", + "Successfully unsubscribed": "کامیابی سے ہوشیاری ہٹا دی گئی ہے", + "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "", + "Thank you for your support": "", + "Thank you for your support!": "", + "Thanks for the feedback!": "رائے کے لئے شکریہ!", + "That didn't go to plan": "وہ خواہش تھی، لیکن ہو نہ سکی", + "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "ہمارے پاس آپ کا ای میل ایڈریس ہے {{memberEmail}} — اگر یہ درست نہیں ہے، تو آپ اسے اپنے میں اپ ڈیٹ کر سکتے ہیں۔", + "There was a problem submitting your feedback. Please try again a little later.": "آپ کی رائے جمع کرنے میں مشکل ہوئی۔ براہ کرم ٹھوڑی دیر بعد دوبارہ کوشش کریں۔", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", + "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", + "This site is invite-only, contact the owner for access.": "یہ سائٹ صرف دعوتی ہے، دستیابی کے لئے مالک سے رابطہ کریں۔", + "This site is not accepting payments at the moment.": "", + "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "سائن اپ مکمل کرنے کے لئے، اپنے ان باکس میں تصدیق کے لنک پر کلک کریں۔ اگر 3 منٹ کے اندر نہ آئے تو، اپنا اسپیم فولڈر چیک کریں!", + "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", + "Try free for {{amount}} days, then {{originalPrice}}.": "مفت ٹرائل کے لئے کوشش کریں {{amount}} دن، پھر {{originalPrice}}۔", + "Unable to initiate checkout session": "", + "Unlock access to all newsletters by becoming a paid subscriber.": "ایک ادائیگی چکچکی بن کر تمام نیوزلیٹرز کا رسائی کھولیں۔", + "Unsubscribe from all emails": "تمام ای میلز سے ہوشیار ہو جائیں", + "Unsubscribed": "ہوشیار ہوگیا ہے", + "Unsubscribed from all emails.": "", + "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "ای میلز سے ہوشیار ہونا آپ کا پردہ فوریہ ہوشیاری {{title}} کی طرف سے منسوخ نہیں کرے گا", + "Update": "اپ ڈیٹ", + "Update your preferences": "اپنی ترجیحات کو اپ ڈیٹ کریں", + "Verification link sent, check your inbox": "", + "Verify your email address is correct": "اپنا ای میل ایڈریس درست ہے یہ تصدیق کریں", + "View plans": "پلان دیکھیں", + "We couldn't unsubscribe you as the email address was not found. Please contact the site owner.": "ہم آپ کو ہوشیار نہیں کر سکے کیونکہ ای میل ایڈریس نہیں ملا۔ براہ کرم سائٹ کے مالک سے رابطہ کریں۔", + "Welcome back, {{name}}!": "خوش آمدید واپس، {{name}}!", + "Welcome back!": "خوش آمدید واپس!", + "Welcome to {{siteTitle}}": "", + "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "جب ایک ان باکس ایک ای میل قبول کرنے میں ناکام ہوتا ہے تو عام طور پر اسے باؤنس کہا جاتا ہے۔ بہت سے مواقعوں میں، یہ عارضی ہوتا ہے۔ تاہم، کچھ مواقعوں میں، ایک باؤنس ای میل غیر معتبر یا موجود نہ ہونے کی صورت میں ہمیشہ کے لئے واپس بھیجا جا سکتا ہے۔", + "Why has my email been disabled?": "میرا ای میل کیوں غیر فعال ہوگیا ہے؟", + "year": "", + "Yearly": "سالانہ", + "You currently have a free membership, upgrade to a paid subscription for full access.": "آپ کا موجودہ مفت ممبرشپ ہے، پورے رسائی حاصل کرنے کے لئے ایک ادائیگی چکچکی پر اپگریڈ کریں۔", + "You have been successfully resubscribed": "آپ نے کامیابی سے دوبارہ ہوشیاری حاصل کی ہے", + "You're currently not receiving emails": "آپ حال ہی میلز نہیں پارہ ہیں", + "You're not receiving emails": "آپ کو ای میل نہیں آ رہا ہے", + "You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "آپ کو ای میل نہیں مل رہا ہے کیونکہ آپ نے حال ہی میسج کو سپیم مارک کیا ہوا ہے یا یہاں فراہم کردہ ای میل ایڈریس پر پیغامات فراہم نہیں کیے جا سکتے ہیں۔", + "You've successfully signed in.": "آپ نے کامیابی سے سائن ان کیا ہے۔", + "You've successfully subscribed to": "", + "Your account": "آپ کا اکاؤنٹ", + "Your email has failed to resubscribe, please try again": "", + "Your input helps shape what gets published.": "آپ کا مدخلہ وہ چیزیں شکل دینے میں مدد فراہم کرتا ہے جو شائع ہوتی ہیں۔", + "Your subscription will expire on {{expiryDate}}": "آپ کی ہوشیاری {{expiryDate}} تک چلے گی", + "Your subscription will renew on {{renewalDate}}": "آپ کی ہوشیاری {{renewalDate}} تک تجدید ہوگی", + "Your subscription will start on {{subscriptionStart}}": "آپ کی ہوشیاری {{subscriptionStart}} پر شروع ہوگی" +} diff --git a/ghost/i18n/locales/ur/search.json b/ghost/i18n/locales/ur/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/ur/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/ur/signup-form.json b/ghost/i18n/locales/ur/signup-form.json new file mode 100644 index 00000000000..59f5d6230c5 --- /dev/null +++ b/ghost/i18n/locales/ur/signup-form.json @@ -0,0 +1,9 @@ +{ + "Email sent": "ای میل بھیجا گیا ہے", + "Now check your email!": "اب اپنا ای میل چیک کریں!", + "Please enter a valid email address": "براہ کرم ایک درست ای میل ایڈریس درج کریں", + "Something went wrong, please try again.": "کچھ غلط ہوگیا ہے، براہ کرم دوبارہ کوشش کریں۔", + "Subscribe": "تشہیر", + "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "سائن اپ مکمل کرنے کے لئے، اپنے ان باکس میں تصدیقی لنک پر کلک کریں۔ اگر 3 منٹ کے اندر نہ آئے تو، اپنا اسپیم فولڈر چیک کریں!", + "Your email address": "آپ کا ای میل ایڈریس" +} diff --git a/ghost/i18n/locales/uz/comments.json b/ghost/i18n/locales/uz/comments.json index c5391eab775..77ef1db9693 100644 --- a/ghost/i18n/locales/uz/comments.json +++ b/ghost/i18n/locales/uz/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "", "Report": "", "Report comment": "", - "Report this comment": "", "Report this comment?": "", "Save": "", "Sending": "", @@ -68,6 +67,5 @@ "This comment has been removed.": "", "Upgrade now": "", "Yesterday": "", - "You want to report this comment?": "", "Your request will be sent to the owner of this site.": "" } diff --git a/ghost/i18n/locales/uz/portal.json b/ghost/i18n/locales/uz/portal.json index 2154060a6dd..88a5fcfbe42 100644 --- a/ghost/i18n/locales/uz/portal.json +++ b/ghost/i18n/locales/uz/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Kirish havolasi email pochtangizga yuborildi. Agar u 3 daqiqada kelmasa, spam bo'limini tekshiring", "Account": "Hisob", + "Account details updated successfully": "", "Account settings": "Hisob sozlamalari", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Bepul sinov muddati tugagandan so'ng, siz tanlagan daraja uchun odatdagi narxdan undiriladi. Undan oldin istalgan vaqtda bekor qilishingiz mumkin.", "Already a member?": "Allaqachon a'zomisiz?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "", "Back": "Orqaga", "Back to Log in": "Kirish sahifasiga qaytish", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "", "Check with your mail provider": "", + "Check your inbox to verify email update": "", "Choose": "", "Choose a different plan": "Boshqa rejani tanlang", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "", "Continue": "Davom etmoq", "Continue subscription": "", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "", "Could not update email! Invalid link.": "", "Create a new contact": "", @@ -52,6 +56,7 @@ "Edit": "", "Email": "Email", "Email newsletter": "", + "Email newsletter settings updated": "", "Email preferences": "Email sozlamalari", "Emails": "Elektron xatlar", "Emails disabled": "Elektron pochta xabarlari o‘chirilgan", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "", "Expires {{expiryDate}}": "", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "", "Free Trial – Ends {{trialEnd}}": "", "Get help": "Yordam olmoq", @@ -91,6 +108,8 @@ "Name": "Ism", "Need more help? Contact support": "", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "Elektron xatlar olmayapsizmi?", "Now check your email!": "Endi elektron pochtangizni tekshiring!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "", @@ -126,6 +145,7 @@ "Submit feedback": "Izoh yuboring", "Subscribe": "", "Subscribed": "", + "Subscription plan updated successfully": "", "Success": "", "Success! Check your email for magic link to sign-in.": "", "Success! Your account is fully activated, you now have access to all content.": "", @@ -138,12 +158,22 @@ "That didn't go to plan": "Bu rejaga mos kelmadi", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "", "There was a problem submitting your feedback. Please try again a little later.": "", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "Bu saytda faqat taklif qilinadi, kirish uchun egasiga murojaat qiling.", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Ro‘yxatdan o‘tishni yakunlash uchun pochta qutingizdagi tasdiqlash havolasini bosing. Agar u 3 daqiqada kelmasa, spam jildini tekshiring!", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "", "Unsubscribe from all emails": "Barcha elektron pochta xabarlariga obunani bekor qiling", "Unsubscribed": "", @@ -170,6 +200,7 @@ "You've successfully signed in.": "", "You've successfully subscribed to": "", "Your account": "Sizning hisobingiz", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "", "Your subscription will expire on {{expiryDate}}": "", "Your subscription will renew on {{renewalDate}}": "", diff --git a/ghost/i18n/locales/uz/search.json b/ghost/i18n/locales/uz/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/uz/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/vi/comments.json b/ghost/i18n/locales/vi/comments.json index cc1624f15ca..db2f9e20a6c 100644 --- a/ghost/i18n/locales/vi/comments.json +++ b/ghost/i18n/locales/vi/comments.json @@ -25,11 +25,11 @@ "Discussion": "Thảo luận", "Edit": "Sửa", "Edit this comment": "Sửa bình luận này", - "edited": "Đã sửa", + "edited": "đã sửa", "Enter your name": "Nhập tên của bạn", "Expertise": "Chuyên môn", - "Founder @ Acme Inc": "Người sáng lập @ Acme Inc", - "Full-time parent": "Cha mẹ toàn thời gian", + "Founder @ Acme Inc": "Nhà sáng lập @ Acme Inc", + "Full-time parent": "Phụ huynh toàn thời gian", "Head of Marketing at Acme, Inc": "Trưởng phòng Marketing tại Acme, Inc", "Hide": "Ẩn", "Hide comment": "Ẩn bình luận", @@ -50,17 +50,16 @@ "Reply to comment": "Trả lời bình luận", "Report": "Báo cáo", "Report comment": "Báo cáo bình luận", - "Report this comment": "Báo cáo bình luận", "Report this comment?": "Báo cáo bình luận này?", "Save": "Lưu", "Sending": "Đang gửi", "Sent": "Đã gửi", "Show": "Hiển thị", - "Show {{amount}} more replies": "Hiển thị {{amount}} trả lời khác", - "Show {{amount}} previous comments": "Hiển thị {{amount}} bình luận trước", - "Show 1 more reply": "Hiển thị 1 trả lời khác", - "Show 1 previous comment": "Hiển thị 1 bình luận trước", - "Show comment": "Hiển thị bình luận", + "Show {{amount}} more replies": "Xem {{amount}} trả lời khác", + "Show {{amount}} previous comments": "Đọc {{amount}} bình luận trước", + "Show 1 more reply": "Xem 1 trả lời", + "Show 1 previous comment": "Đọc 1 bình luận trước", + "Show comment": "Đọc bình luận", "Sign in": "Đăng nhập", "Sign up now": "Đăng ký ngay", "Start the conversation": "Bắt đầu cuộc trò chuyện", @@ -68,6 +67,5 @@ "This comment has been removed.": "Bình luận này đã bị xóa.", "Upgrade now": "Nâng cấp", "Yesterday": "Hôm qua", - "You want to report this comment?": "Bạn muốn báo cáo bình luận này?", "Your request will be sent to the owner of this site.": "Yêu cầu của bạn sẽ được gửi đến chủ sở hữu trang web này." } diff --git a/ghost/i18n/locales/vi/ghost.json b/ghost/i18n/locales/vi/ghost.json index 7c35a09a844..c42b7743bc8 100644 --- a/ghost/i18n/locales/vi/ghost.json +++ b/ghost/i18n/locales/vi/ghost.json @@ -2,7 +2,7 @@ "All the best!": "Chúc bạn mọi điều tốt đẹp nhất!", "Complete signup for {{siteTitle}}!": "Hoàn tất đăng ký {{siteTitle}}!", "Complete your sign up to {{siteTitle}}!": "Hoàn tất đăng ký của bạn trên {{siteTitle}}!", - "Confirm email address": "Xác minh email", + "Confirm email address": "Xác nhận email", "Confirm signup": "Xác nhận đăng ký", "Confirm your email address": "Nhập lại email", "Confirm your email update for {{siteTitle}}!": "Xác nhận cập nhật địa chỉ email của bạn trên {{siteTitle}}!", @@ -12,7 +12,7 @@ "Hey there!": "Chào bạn!", "If you did not make this request, you can safely ignore this email.": "Nếu bạn không thực hiện yêu cầu này, vui lòng bỏ qua email này.", "If you did not make this request, you can simply delete this message.": "Nếu bạn không thực hiện yêu cầu này, hãy xóa email này.", - "Please confirm your email address with this link:": "Vui lòng xác minh địa chỉ email của bạn bằng liên kết này:", + "Please confirm your email address with this link:": "Vui lòng xác nhận địa chỉ email của bạn bằng liên kết này:", "Secure sign in link for {{siteTitle}}": "Liên kết đăng nhập an toàn trên {{siteTitle}}", "See you soon!": "Hẹn gặp lại bạn!", "Sent to {{email}}": "Đã gửi đến {{email}}", @@ -22,12 +22,12 @@ "Thank you for signing up to {{siteTitle}}!": "Cám ơn bạn đã đăng ký thành viên trên {{siteTitle}}!", "Thank you for subscribing to {{siteTitle}}!": "Cám ơn bạn đã theo dõi {{siteTitle}}!", "Thank you for subscribing to {{siteTitle}}.": "Cám ơn bạn đã theo dõi {{siteTitle}}!", - "Thank you for subscribing to {{siteTitle}}. Tap the link below to be automatically signed in:": "Cám ơn bạn đã theo dõi {{siteTitle}}. Nhấp vào liên kết bên dưới để đăng nhập tự động:", + "Thank you for subscribing to {{siteTitle}}. Tap the link below to be automatically signed in:": "Cám ơn bạn đã theo dõi {{siteTitle}}. Nhấn vào liên kết bên dưới để đăng nhập tự động:", "This email address will not be used.": "Địa chỉ email này sẽ không còn sử dụng.", "Welcome back to {{siteTitle}}!": "Chào mừng trở lại {{siteTitle}}!", "Welcome back! Use this link to securely sign in to your {{siteTitle}} account:": "Chào mừng trở lại! Sử dụng liên kết này để đăng nhập an toàn vào tài khoản {{siteTitle}} của bạn:", "You can also copy & paste this URL into your browser:": "Bạn cũng có thể sao chép và dán URL này vào trình duyệt:", - "You will not be signed up, and no account will be created for you.": "Bạn sẽ không đăng ký và sẽ không tạo tài khoản.", + "You will not be signed up, and no account will be created for you.": "Bạn sẽ không đăng ký và không tạo tài khoản.", "You will not be subscribed.": "Bạn sẽ không theo dõi.", "You're one tap away from subscribing to {{siteTitle}} — please confirm your email address with this link:": "Còn một bước nữa để theo dõi {{siteTitle}} — vui lòng xác nhận địa chỉ email của bạn bằng liên kết này:", "You're one tap away from subscribing to {{siteTitle}}!": "Còn một bước nữa để theo dõi {{siteTitle}}!" diff --git a/ghost/i18n/locales/vi/portal.json b/ghost/i18n/locales/vi/portal.json index e41688f8955..42a9d94355c 100644 --- a/ghost/i18n/locales/vi/portal.json +++ b/ghost/i18n/locales/vi/portal.json @@ -11,11 +11,13 @@ "{{memberEmail}} will no longer receive this newsletter.": "{{memberEmail}} sẽ không còn được nhận bản tin này.", "{{trialDays}} days free": "{{trialDays}} ngày đọc thử miễn phí", "+1 (123) 456-7890": "+84 (987) 654-321", - "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Liên kết đăng nhập đã được gửi đến hộp thư của bạn. Sau 3 phút mà chưa thấy, hãy kiểm tra thư hộp thư spam.", + "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "Liên kết đăng nhập đã được gửi đến email của bạn. Sau 3 phút mà chưa thấy, hãy kiểm tra thư hộp thư spam.", "Account": "Tài khoản", + "Account details updated successfully": "Đã cập nhật chi tiết tài khoản thành công", "Account settings": "Cài đặt", - "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Sau khi hết thời gian đọc thử miễn phí, sẽ áp dụng giá của gói theo dõi mà bạn đã chọn. Bạn có thể hủy trước khi hết thời gian đọc thử.", + "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "Sau khi hết thời gian đọc thử miễn phí sẽ tính phí của gói theo dõi mà bạn đã chọn. Bạn có thể hủy trước khi hết thời gian đọc thử.", "Already a member?": "Bạn đã là thành viên?", + "An error occurred": "Xảy ra lỗi", "An unexpected error occured. Please try again or contact support if the error persists.": "Xảy ra lỗi chưa biết. Hãy thử lại hoặc liên hệ hỗ trợ nếu vẫn tiếp tục lỗi.", "Back": "Quay về", "Back to Log in": "Quay về đăng nhập", @@ -25,12 +27,13 @@ "Cancel subscription": "Hủy gói", "Cancellation reason": "Lý do hủy gói", "Change": "Thay đổi", - "Change plan": "", + "Change plan": "Thay đổi gói", "Check spam & promotions folders": "Kiểm tra hộp thư spam & quảng cáo", "Check with your mail provider": "Yêu cầu nhà cung cấp dịch vụ email hỗ trợ", + "Check your inbox to verify email update": "Kiểm tra email của bạn để xác minh cập nhật email", "Choose": "Chọn", "Choose a different plan": "Chọn gói khác", - "Choose a plan": "", + "Choose a plan": "Chọn một gói", "Choose your newsletters": "Chọn bản tin bạn muốn nhận", "Click here to retry": "Nhấn vào đây thử lại", "Close": "Đóng", @@ -42,26 +45,40 @@ "Contact support": "Liên hệ hỗ trợ", "Continue": "Tiếp tục", "Continue subscription": "Tiếp tục theo dõi", - "Could not sign in. Login link expired.": "Không đăng nhập được. Liên kết đăng nhập hết hạn.", - "Could not update email! Invalid link.": "Không cập nhật email được. Liên kết không hợp lệ.", + "Could not create stripe checkout session": "Không thể tạo phiên thanh toán Stripe", + "Could not sign in. Login link expired.": "Không thể đăng nhập. Liên kết đăng nhập hết hạn.", + "Could not update email! Invalid link.": "Không thể cập nhật email. Liên kết không hợp lệ.", "Create a new contact": "Tạo liên hệ mới", "Current plan": "Gói hiện tại", "Delete account": "Xóa tài khoản", "Didn't mean to do this? Manage your preferences .": "Không muốn như vậy? Quản lý thiết lập .", - "Don't have an account?": "Bạn chưa có tài khoản?", + "Don't have an account?": "Chưa có tài khoản?", "Edit": "Chỉnh sửa", "Email": "Email", "Email newsletter": "Bản tin email", + "Email newsletter settings updated": "Đã cập nhật thiết lập bản tin email", "Email preferences": "Thiết lập email", "Emails": "Emails", "Emails disabled": "Vô hiệu hóa email", - "Ends {{offerEndDate}}": "Kết thúc vào {{offerEndDate}}", + "Ends {{offerEndDate}}": "Kết thúc {{offerEndDate}}", "Enter your email address": "Nhập địa chỉ email của bạn", "Enter your name": "Nhập tên của bạn", "Error": "Lỗi", - "Expires {{expiryDate}}": "Hết hạn vào {{expiryDate}}", + "Expires {{expiryDate}}": "Hết hạn {{expiryDate}}", + "Failed to cancel subscription, please try again": "Không thể hủy gói, vui lòng thử lại", + "Failed to log in, please try again": "Không thể đăng nhập, vui lòng thử lại", + "Failed to log out, please try again": "Không thể đăng xuất, vui lòng thử lại", + "Failed to process checkout, please try again": "Không thể thanh toán, vui lòng thử lại", + "Failed to send magic link email": "Không thể gửi liên kết đăng nhập qua email", + "Failed to send verification email": "Không thể gửi email xác minh", + "Failed to sign up, please try again": "Không thể đăng ký, vui lòng thử lại", + "Failed to update account data": "Không thể cập nhật thông tin tài khoản", + "Failed to update account details": "Không thể cập nhật chi tiết tài khoản", + "Failed to update billing information, please try again": "Không thể cập nhật thông tin thanh toán, vui lòng thử lại", + "Failed to update newsletter settings": "Không thể cập nhật thiết lập bản tin email", + "Failed to update subscription, please try again": "Không thể cập nhật theo dõi, vui lòng thử lại", "Forever": "Vĩnh viễn", - "Free Trial – Ends {{trialEnd}}": "Dùng Thử – Hết hạn vào {{trialEnd}}", + "Free Trial – Ends {{trialEnd}}": "Đọc Thử – Hết hạn vào {{trialEnd}}", "Get help": "Trợ giúp", "Get in touch for help": "Yêu cầu nhận trợ giúp", "Get notified when someone replies to your comment": "Nhận thông báo khi có ai đó trả lời bình luận", @@ -69,11 +86,11 @@ "Help! I'm not receiving emails": "Giúp tôi! Tôi không nhận được email", "Here are a few other sites you may enjoy.": "Đây là vài trang khác mà bạn có thể thích.", "If a newsletter is flagged as spam, emails are automatically disabled for that address to make sure you no longer receive any unwanted messages.": "Nếu một bản tin bị gắn nhãn thư rác, email sẽ tự động bị vô hiệu hóa đối với địa chỉ đó để đảm bảo bạn không còn nhận được bất kỳ email không mong muốn nào nữa.", - "If the spam complaint was accidental, or you would like to begin receiving emails again, you can resubscribe to emails by clicking the button on the previous screen.": "Nếu khiếu nại về thư rác là vô tình hoặc bạn muốn bắt đầu nhận lại email, bạn có thể đăng ký lại email bằng cách nhấp vào nút trên màn hình trước đó.", - "If you cancel your subscription now, you will continue to have access until {{periodEnd}}.": "Nếu bạn hủy đăng ký ngay bây giờ, bạn sẽ tiếp tục có quyền truy cập cho đến {{periodEnd}}.", + "If the spam complaint was accidental, or you would like to begin receiving emails again, you can resubscribe to emails by clicking the button on the previous screen.": "Nếu khiếu nại về thư rác là vô tình hoặc bạn muốn bắt đầu nhận lại email, bạn có thể theo dõi lại bằng cách nhấn vào nút trên màn hình trước đó.", + "If you cancel your subscription now, you will continue to have access until {{periodEnd}}.": "Nếu bạn hủy theo dõi bây giờ, bạn sẽ tiếp tục có quyền truy cập cho đến {{periodEnd}}.", "If you have a corporate or government email account, reach out to your IT department and ask them to allow emails to be received from {{senderEmail}}": "Nếu bạn dùng tài khoản email của công ty hoặc chính phủ, hãy liên hệ với bộ phận IT và yêu cầu họ cho phép nhận email từ {{senderEmail}}", - "If you would like to start receiving emails again, the best next steps are to check your email address on file for any issues and then click resubscribe on the previous screen.": "Nếu bạn muốn bắt đầu nhận lại email, tốt nhất là kiểm tra địa chỉ email của bạn trong hồ sơ xem có vấn đề gì không và sau đó nhấn vào đăng ký lại trên màn hình trước đó.", - "If you're not receiving the email newsletter you've subscribed to, here are a few things to check.": "Nếu bạn không nhận được bản tin email mà bạn đã đăng ký, đây là một số việc cần kiểm tra.", + "If you would like to start receiving emails again, the best next steps are to check your email address on file for any issues and then click resubscribe on the previous screen.": "Nếu bạn muốn bắt đầu nhận lại email, tốt nhất là kiểm tra địa chỉ email của bạn trong hồ sơ xem có vấn đề gì không và sau đó nhấn vào theo dõi lại trên màn hình trước đó.", + "If you're not receiving the email newsletter you've subscribed to, here are a few things to check.": "Nếu bạn không nhận được bản tin email mà bạn đã theo dõi, đây là một số việc cần kiểm tra.", "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "Nếu bạn đã hoàn thành tất cả các bước kiểm tra này mà vẫn không nhận được email, hãy liên hệ với {{supportAddress}} để được hỗ trợ.", "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "Trong trường hợp nhận được lỗi vĩnh viễn khi cố gắng gửi bản tin, email sẽ bị vô hiệu hóa trên tài khoản.", "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "Trong ứng dụng email của bạn, hãy thêm {{senderEmail}} vào danh sách liên hệ của bạn. Điều này báo hiệu cho nhà cung cấp dịch vụ email của bạn rằng các email được gửi từ địa chỉ này là đáng tin cậy.", @@ -85,27 +102,29 @@ "Manage": "Quản lý", "Maybe later": "Để sau", "Memberships unavailable, contact the owner for access.": "Chưa phải là thành viên, liên hệ với chủ sở hữu để truy cập.", - "month": "", + "month": "tháng", "Monthly": "Hàng tháng", "More like this": "Thích bài viết như này", "Name": "Tên", "Need more help? Contact support": "Cần giúp đỡ? Liên hệ hỗ trợ", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "Bản tin có thể bị vô hiệu hóa trên tài khoản của bạn vì hai lý do: Email trước đó đã bị đánh dấu là thư rác hoặc lỗi thất bại vĩnh viễn (thư trả lại).", + "No member exists with this e-mail address.": "Chưa có thành viên nào với email này", + "No member exists with this e-mail address. Please sign up first.": "Chưa có thành viên nào với email này. Vui lòng đăng ký trước.", "Not receiving emails?": "Bạn không nhận được email?", "Now check your email!": "Kiểm tra hộp thư ngay!", - "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Sau khi đăng ký lại, nếu bạn vẫn không thấy email trong hộp thư đến của mình, hãy kiểm tra mục thư rác. Một số nhà cung cấp email lưu giữ hồ sơ về các khiếu nại thư rác trước đây và sẽ tiếp tục gắn nhãn email. Nếu điều này xảy ra, hãy đánh dấu bản tin mới nhất là 'Không phải thư rác' để chuyển nó trở lại hộp thư đến chính của bạn.", + "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "Sau khi theo dõi lại, nếu bạn vẫn không thấy email trong hộp thư đến của mình, hãy kiểm tra mục thư rác. Một số nhà cung cấp email lưu giữ hồ sơ về các khiếu nại thư rác trước đây và sẽ tiếp tục gắn nhãn email. Nếu điều này xảy ra, hãy đánh dấu bản tin mới nhất là 'Không phải thư rác' để chuyển nó trở lại hộp thư đến chính của bạn.", "Permanent failure (bounce)": "Thất bại vĩnh viễn (thư bị trả lại)", "Phone number": "Số điện thoại", "Plan": "Gói", "Plan checkout was cancelled.": "Đã hủy thanh toán.", "Plan upgrade was cancelled.": "Đã hủy nâng cấp gói.", - "Please contact {{supportAddress}} to adjust your complimentary subscription.": "Vui lòng liên hệ {{supportAddress}} để điều chỉnh gói đăng ký.", - "Please enter {{fieldName}}": "Vui lòng nhập {{fieldName}}", + "Please contact {{supportAddress}} to adjust your complimentary subscription.": "Vui lòng liên hệ {{supportAddress}} để điều chỉnh gói theo dõi.", + "Please enter {{fieldName}}": "Hãy nhập {{fieldName}}", "Please fill in required fields": "Vui lòng điền các mục bắt buộc", - "Price": "Giá", + "Price": "Phí", "Re-enable emails": "Kích hoạt lại email", "Recommendations": "Đề xuất", - "Renews at {{price}}.": "Giá gia hạn {{price}}.", + "Renews at {{price}}.": "Phí gia hạn {{price}}.", "Retry": "Thử lại", "Save": "Lưu", "Send an email and say hi!": "Gửi một email và nói xin chào!", @@ -121,11 +140,12 @@ "Sorry, that didn’t work.": "Rất tiếc, không dùng được.", "Spam complaints": "Than phiền", "Start {{amount}}-day free trial": "Bắt đầu đọc thử {{amount}} ngày", - "Starting {{startDate}}": "Bắt đầu vào {{startDate}}", + "Starting {{startDate}}": "Bắt đầu {{startDate}}", "Starting today": "Bắt đầu hôm nay", "Submit feedback": "Gửi phản hồi", "Subscribe": "Theo dõi", "Subscribed": "Đã theo dõi", + "Subscription plan updated successfully": "Đã cập nhật gói theo dõi thành công", "Success": "Thành công", "Success! Check your email for magic link to sign-in.": "Xong! Kiểm tra hộp thư để nhận liên kết đăng nhập.", "Success! Your account is fully activated, you now have access to all content.": "Xong! Đã kích hoạt tài khoản, giờ bạn có toàn quyền truy cập nội dung.", @@ -137,22 +157,32 @@ "Thanks for the feedback!": "Cám ơn phản hồi của bạn!", "That didn't go to plan": "Không thực hiện được", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "Địa chỉ email của bạn là {{memberEmail}} — nếu sai, bạn có thể đổi trong .", - "There was a problem submitting your feedback. Please try again a little later.": "Có vấn đề khi gửi phản hồi. Xin thử lại sau.", - "There was an error processing your payment. Please try again.": "Xảy ra lỗi khi tiến hành thanh toán. Xin thử lại sau.", + "There was a problem submitting your feedback. Please try again a little later.": "Có vấn đề khi gửi phản hồi. Hãy thử lại sau.", + "There was an error cancelling your subscription, please try again.": "Xảy ra lỗi khi hủy gói theo dõi, vui lòng thử lại", + "There was an error continuing your subscription, please try again.": "Xảy ra lỗi khi tiếp tục gói theo dõi, vui lòng thử lại", + "There was an error processing your payment. Please try again.": "Xảy ra lỗi khi tiến hành thanh toán. Hãy thử lại sau.", + "There was an error sending the email, please try again": "Xảy ra lỗi khi gửi email, vui lòng thử lại", "This site is invite-only, contact the owner for access.": "Trang web này chỉ dành cho những người được mời, hãy liên hệ với chủ sở hữu để cấp quyền truy cập.", "This site is not accepting payments at the moment.": "Trang web này hiện chưa chấp nhận thanh toán.", - "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Để hoàn tất đăng ký, nhấn vào liên kết xác nhận trong hộp thư đến của bạn. Sau 3 phút mà không thấy, hãy kiểm tra hộp thư spam của bạn!", - "To continue to stay up to date, subscribe to {{publication}} below.": "Để tiếp tục được cập nhật, hãy đăng ký {{publication}} như bên dưới.", + "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Để hoàn tất đăng ký, nhấn vào liên kết xác nhận được gửi tới email của bạn. Sau 3 phút mà không thấy, hãy kiểm tra hộp thư spam!", + "To continue to stay up to date, subscribe to {{publication}} below.": "Để tiếp tục được cập nhật, hãy theo dõi {{publication}} bên dưới.", + "Too many attempts try again in {{number}} days.": "Thử quá nhiều, hãy thử lại sau {{number}} ngày.", + "Too many attempts try again in {{number}} hours.": "Thử quá nhiều, hãy thử lại sau {{number}} giờ.", + "Too many attempts try again in {{number}} minutes.": "Thử quá nhiều, hãy thử lại sau {{number}} phút.", + "Too many different sign-in attempts, try again in {{number}} days": "Thử đăng nhập quá nhiều, hãy thử lại sau {{number}} ngày.", + "Too many different sign-in attempts, try again in {{number}} hours": "Thử đăng nhập quá nhiều, hãy thử lại sau {{number}} giờ.", + "Too many different sign-in attempts, try again in {{number}} minutes": "Thử đăng nhập quá nhiều, hãy thử lại sau {{number}} phút.", "Try free for {{amount}} days, then {{originalPrice}}.": "Đọc thử {{amount}} ngày, phí sau đọc thử là {{originalPrice}}.", + "Unable to initiate checkout session": "Không thể bắt đầu phiên thanh toán", "Unlock access to all newsletters by becoming a paid subscriber.": "Trở thành thành viên trả phí để mở khóa truy cập toàn bộ bản tin.", "Unsubscribe from all emails": "Hủy theo dõi tất cả email", "Unsubscribed": "Đã hủy theo dõi", "Unsubscribed from all emails.": "Đã hủy theo dõi toàn bộ email", - "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "Việc hủy theo dõi qua email sẽ không hủy gói đăng ký trả phí của bạn đối với {{title}}", + "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "Việc hủy theo dõi qua email sẽ không hủy gói theo dõi trả phí của bạn đối với {{title}}", "Update": "Cập nhật", "Update your preferences": "Cập nhật thiết lập", - "Verification link sent, check your inbox": "Đã gửi liên kết xác minh, hãy kiểm tra hộp thư đến", - "Verify your email address is correct": "Xác minh địa chỉ mail của bạn là đúng", + "Verification link sent, check your inbox": "Đã gửi liên kết xác minh, hãy kiểm tra email", + "Verify your email address is correct": "Xác minh địa chỉ email của bạn là đúng", "View plans": "Xem các gói", "We couldn't unsubscribe you as the email address was not found. Please contact the site owner.": "Chúng tôi không thể hủy theo dõi vì không tìm thấy địa chỉ email. Vui lòng liên hệ với chủ sở hữu trang web.", "Welcome back, {{name}}!": "Chào mừng trở lại, {{name}}!", @@ -160,18 +190,19 @@ "Welcome to {{siteTitle}}": "Chào mừng tham gia {{siteTitle}}", "When an inbox fails to accept an email it is commonly called a bounce. In many cases, this can be temporary. However, in some cases, a bounced email can be returned as a permanent failure when an email address is invalid or non-existent.": "Khi hộp thư đến không nhận được email, nó thường được gọi là email bị trả lại. Điều này có thể là tạm thời. Tuy nhiên, trong một số trường hợp, email bị trả lại có thể là lỗi vĩnh viễn nếu địa chỉ email không hợp lệ hoặc không tồn tại.", "Why has my email been disabled?": "Tại sao email của tôi bị vô hiệu hóa?", - "year": "", + "year": "năm", "Yearly": "Hàng năm", - "You currently have a free membership, upgrade to a paid subscription for full access.": "Bạn đang là thành viên thường, hãy nâng cấp gói trả phí để có toàn quyền truy cập.", + "You currently have a free membership, upgrade to a paid subscription for full access.": "Bạn đang là thành viên thường, hãy nâng cấp gói theo dõi trả phí để có toàn quyền truy cập.", "You have been successfully resubscribed": "Bạn đã theo dõi lại thành công", - "You're currently not receiving emails": "Những email bạn không nhận được gần đây", - "You're not receiving emails": "Bạn không nhận được email", - "You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "Bạn không nhận được email vì bạn đã đánh dấu một email gần đây là thư rác, hoặc vì không thể gửi đến địa chỉ email bạn đã cung cấp.", - "You've successfully signed in.": "Bạn đã đăng nhập.", - "You've successfully subscribed to": "Bạn đã hoàn tất đăng ký", + "You're currently not receiving emails": "Hiện tại bạn không nhận email", + "You're not receiving emails": "Bạn không nhận email", + "You're not receiving emails because you either marked a recent message as spam, or because messages could not be delivered to your provided email address.": "Bạn không nhận email vì bạn đã đánh dấu một email gần đây là thư rác, hoặc vì địa chỉ email bạn đã cung cấp không thể gửi được.", + "You've successfully signed in.": "Bạn đã đăng nhập thành công.", + "You've successfully subscribed to": "Bạn đã theo dõi thành công", "Your account": "Tài khoản của bạn", + "Your email has failed to resubscribe, please try again": "Email của bạn không thể theo dõi lại, vui lòng thử lại", "Your input helps shape what gets published.": "Thông tin của bạn giúp định hình nội dung được xuất bản.", - "Your subscription will expire on {{expiryDate}}": "Gói của bạn sẽ hết hạn vào {{expiryDate}}", - "Your subscription will renew on {{renewalDate}}": "Gói của bạn sẽ tự động gia hạn vào {{renewalDate}}", - "Your subscription will start on {{subscriptionStart}}": "Gói của bạn bắt đầu có hiệu lực vào {{subscriptionStart}}" + "Your subscription will expire on {{expiryDate}}": "Gói theo dõi của bạn sẽ hết hạn vào {{expiryDate}}", + "Your subscription will renew on {{renewalDate}}": "Gói theo dõi của bạn sẽ tự động gia hạn vào {{renewalDate}}", + "Your subscription will start on {{subscriptionStart}}": "Gói theo dõi của bạn bắt đầu có hiệu lực vào {{subscriptionStart}}" } diff --git a/ghost/i18n/locales/vi/search.json b/ghost/i18n/locales/vi/search.json new file mode 100644 index 00000000000..c14cdb5bd75 --- /dev/null +++ b/ghost/i18n/locales/vi/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "Tác giả", + "Cancel": "Hủy bỏ", + "No matches found": "Không tìm thấy", + "Posts": "Bài viết", + "Search posts, tags and authors": "Tìm bài viết, chuyên mục và tác giả", + "Show more results": "Xem thêm kết quả", + "Tags": "Chuyên mục" +} diff --git a/ghost/i18n/locales/vi/signup-form.json b/ghost/i18n/locales/vi/signup-form.json index 8653f3e7cd8..8e0a17bf8b7 100644 --- a/ghost/i18n/locales/vi/signup-form.json +++ b/ghost/i18n/locales/vi/signup-form.json @@ -1,9 +1,9 @@ { "Email sent": "Đã gửi email", - "Now check your email!": "Giờ hãy kiểm tra hộp thư của bạn!", + "Now check your email!": "Giờ hãy kiểm tra email của bạn!", "Please enter a valid email address": "Vui lòng nhập địa chỉ email hợp lệ", "Something went wrong, please try again.": "Xảy ra lỗi, vui lòng thử lại.", "Subscribe": "Theo dõi", - "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Để hoàn tất đăng ký, nhấn vào liên kết xác minh trong email. Sau 3 phút mà chưa thấy, hãy kiểm tra thư spam!", + "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "Để hoàn tất đăng ký, nhấn vào liên kết xác nhận được gửi tới email của bạn. Sau 3 phút mà chưa thấy, hãy kiểm tra hộp thư spam!", "Your email address": "Địa chỉ email của bạn" } diff --git a/ghost/i18n/locales/zh-Hant/comments.json b/ghost/i18n/locales/zh-Hant/comments.json index ade8ce4957f..126d9a1fe06 100644 --- a/ghost/i18n/locales/zh-Hant/comments.json +++ b/ghost/i18n/locales/zh-Hant/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "回覆留言", "Report": "舉報", "Report comment": "舉報留言", - "Report this comment": "舉報此留言", "Report this comment?": "舉報此留言?", "Save": "儲存", "Sending": "正在發送", @@ -68,6 +67,5 @@ "This comment has been removed.": "該則留言已被移除。", "Upgrade now": "立即升級", "Yesterday": "昨天", - "You want to report this comment?": "你想要舉報這則留言嗎?", "Your request will be sent to the owner of this site.": "您的請求將被發送給所有者。" } diff --git a/ghost/i18n/locales/zh-Hant/portal.json b/ghost/i18n/locales/zh-Hant/portal.json index 75cb0b8f95d..06039748255 100644 --- a/ghost/i18n/locales/zh-Hant/portal.json +++ b/ghost/i18n/locales/zh-Hant/portal.json @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "登入連結已經發送到您的收件匣。如果在 3 分鐘內未收到,請務必檢查您的垃圾郵件。", "Account": "帳號", + "Account details updated successfully": "", "Account settings": "帳號設定", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "免費試用結束後,您將支付所選方案的定價金額。在此之前,您可以隨時取消。", "Already a member?": "已經是會員了?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "發生了意外錯誤,請再試一次。如果錯誤持續出現,請聯繫客服。", "Back": "返回上一頁", "Back to Log in": "返回登入畫面", @@ -28,6 +30,7 @@ "Change plan": "", "Check spam & promotions folders": "檢查垃圾郵件或促銷郵件", "Check with your mail provider": "請向您的電子信箱服務提供商確認", + "Check your inbox to verify email update": "", "Choose": "選擇", "Choose a different plan": "選擇其他訂閱方案", "Choose a plan": "", @@ -42,6 +45,7 @@ "Contact support": "聯繫客服", "Continue": "繼續", "Continue subscription": "繼續訂閱", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "無法登入。登入連結已過期。", "Could not update email! Invalid link.": "無法更新 email。連結無效。", "Create a new contact": "建立新的聯絡人", @@ -52,6 +56,7 @@ "Edit": "編輯", "Email": "email", "Email newsletter": "電子報", + "Email newsletter settings updated": "", "Email preferences": "email 偏好設定", "Emails": "電子報", "Emails disabled": "已停止接收電子報", @@ -60,6 +65,18 @@ "Enter your name": "", "Error": "錯誤", "Expires {{expiryDate}}": "於 {{expiryDate}} 過期", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "永久", "Free Trial – Ends {{trialEnd}}": "免費試用──於 {{trialEnd}} 结束", "Get help": "取得協助", @@ -91,6 +108,8 @@ "Name": "名字", "Need more help? Contact support": "需要更多協助?聯繫客服", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "您的帳號可能會因為兩個原因而停止接收電子報:先前的郵件被標記為垃圾郵件,或者嘗試發送郵件時出現永久失敗(郵件遭到退回)。", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "沒有收到 email?", "Now check your email!": "立即檢查您的 email。", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "重新訂閱後,如果您在收件夾中仍然看不到郵件,請檢查您的垃圾郵件匣。一些 email 服務商會保留先前的垃圾郵件記錄並持續標記此類郵件。如果發生這種情況,請將最新的電子報標記為「非垃圾郵件」,將其移回您的主要收件匣。", @@ -126,6 +145,7 @@ "Submit feedback": "提交意見", "Subscribe": "訂閱", "Subscribed": "已訂閱", + "Subscription plan updated successfully": "", "Success": "成功", "Success! Check your email for magic link to sign-in.": "成功了!請檢查您的 email 以取得快速登入連結。", "Success! Your account is fully activated, you now have access to all content.": "成功了!您的帳號已完全啟用,您現在可以存取所有內容。", @@ -138,12 +158,22 @@ "That didn't go to plan": "發生錯誤", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "就我們所知,您的 email 地址是 {{memberEmail}}。如果有誤,您可以在進行更新。", "There was a problem submitting your feedback. Please try again a little later.": "提交您的意見時遇到問題。請稍後再試。", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", "There was an error processing your payment. Please try again.": "", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "此網站僅限受邀請者觀看,請聯繫網站擁有者取得存取權限。", "This site is not accepting payments at the moment.": "", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "要完成註冊,請點擊您收件匣中的確認連結。如果在 3 分鐘內沒有收到,請檢查您的垃圾郵件。", "To continue to stay up to date, subscribe to {{publication}} below.": "", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "免費試用 {{amount}} 天,然後以 {{originalPrice}} 開始訂閱。", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "成為付費會員以解鎖所有電子報內容。", "Unsubscribe from all emails": "取消所有電子報訂閱", "Unsubscribed": "未訂閱", @@ -170,6 +200,7 @@ "You've successfully signed in.": "您已成功登入。", "You've successfully subscribed to": "", "Your account": "您的帳號", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "您的建議有助於改善我們的內容。", "Your subscription will expire on {{expiryDate}}": "您的訂閱將於 {{expiryDate}} 到期", "Your subscription will renew on {{renewalDate}}": "您的訂閱將於 {{renewalDate}} 自動續訂", diff --git a/ghost/i18n/locales/zh-Hant/search.json b/ghost/i18n/locales/zh-Hant/search.json new file mode 100644 index 00000000000..8902015528f --- /dev/null +++ b/ghost/i18n/locales/zh-Hant/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "", + "Cancel": "", + "No matches found": "", + "Posts": "", + "Search posts, tags and authors": "", + "Show more results": "", + "Tags": "" +} diff --git a/ghost/i18n/locales/zh/comments.json b/ghost/i18n/locales/zh/comments.json index 5ffb9b7753e..977152697d4 100644 --- a/ghost/i18n/locales/zh/comments.json +++ b/ghost/i18n/locales/zh/comments.json @@ -50,7 +50,6 @@ "Reply to comment": "回复评论", "Report": "举报", "Report comment": "举报评论", - "Report this comment": "举报此评论", "Report this comment?": "举报此评论?", "Save": "保存", "Sending": "正在发送", @@ -68,6 +67,5 @@ "This comment has been removed.": "该条评论已被移除。", "Upgrade now": "立刻升级", "Yesterday": "昨天", - "You want to report this comment?": "你想要举报这条评论吗?", "Your request will be sent to the owner of this site.": "您的请求将被发送给站点所有者。" } diff --git a/ghost/i18n/locales/zh/portal.json b/ghost/i18n/locales/zh/portal.json index 7bd035f26f7..37d9e70d43d 100644 --- a/ghost/i18n/locales/zh/portal.json +++ b/ghost/i18n/locales/zh/portal.json @@ -1,5 +1,5 @@ { - "(save {{highestYearlyDiscount}}%)": "", + "(save {{highestYearlyDiscount}}%)": "可省{{highestYearlyDiscount}}%", "{{amount}} days free": "{{amount}}天免费", "{{amount}} off": "减免{{amount}}", "{{amount}} off for first {{number}} months.": "前{{number}}月减免{{amount}}", @@ -13,9 +13,11 @@ "+1 (123) 456-7890": "", "A login link has been sent to your inbox. If it doesn't arrive in 3 minutes, be sure to check your spam folder.": "登录链接已经发送到您的收件箱。如果在3分钟内还没有收到,请务必检查您的垃圾邮件文件夹。", "Account": "账户", + "Account details updated successfully": "", "Account settings": "账户设置", "After a free trial ends, you will be charged the regular price for the tier you've chosen. You can always cancel before then.": "免费试用结束后,您将被收取所选套餐的标定价格。在此之前,您可以随时取消。", "Already a member?": "已经是会员了?", + "An error occurred": "", "An unexpected error occured. Please try again or contact support if the error persists.": "遇到意外错误。请重试,若持续出现请联系支持服务。", "Back": "返回", "Back to Log in": "返回登录", @@ -25,12 +27,13 @@ "Cancel subscription": "取消订阅", "Cancellation reason": "取消原因", "Change": "变更", - "Change plan": "", + "Change plan": "更改订阅方案", "Check spam & promotions folders": "检查垃圾邮件与促销邮件目录", "Check with your mail provider": "与您的邮件服务商确认", + "Check your inbox to verify email update": "", "Choose": "选择", "Choose a different plan": "选择其他订阅方案", - "Choose a plan": "", + "Choose a plan": "选择一个订阅方案", "Choose your newsletters": "选择您的刊物", "Click here to retry": "请点此处重试", "Close": "关闭", @@ -42,6 +45,7 @@ "Contact support": "联系支持服务", "Continue": "继续", "Continue subscription": "继续订阅", + "Could not create stripe checkout session": "", "Could not sign in. Login link expired.": "无法登录。登录链接已过期。", "Could not update email! Invalid link.": "无法更新电子邮件!链接无效。", "Create a new contact": "创建新联系", @@ -52,14 +56,27 @@ "Edit": "编辑", "Email": "电子邮件", "Email newsletter": "电子邮件快报", + "Email newsletter settings updated": "", "Email preferences": "电子邮件偏好设置", "Emails": "电子邮件列表", "Emails disabled": "关闭电子邮件列表", "Ends {{offerEndDate}}": "于{{offerEndDate}}结束", - "Enter your email address": "", - "Enter your name": "", + "Enter your email address": "输入您的电子邮箱地址", + "Enter your name": "输入您的名字", "Error": "错误", "Expires {{expiryDate}}": "于{{expiryDate}}过期", + "Failed to cancel subscription, please try again": "", + "Failed to log in, please try again": "", + "Failed to log out, please try again": "", + "Failed to process checkout, please try again": "", + "Failed to send magic link email": "", + "Failed to send verification email": "", + "Failed to sign up, please try again": "", + "Failed to update account data": "", + "Failed to update account details": "", + "Failed to update billing information, please try again": "", + "Failed to update newsletter settings": "", + "Failed to update subscription, please try again": "", "Forever": "永久", "Free Trial – Ends {{trialEnd}}": "免费试用 - {{trialEnd}}结束", "Get help": "获取帮助", @@ -77,7 +94,7 @@ "If you've completed all these checks and you're still not receiving emails, you can reach out to get support by contacting {{supportAddress}}.": "如果您已经完成全部检查项目却依旧没有收到邮件,您可以联系{{supportAddress}}获取支持。", "In the event a permanent failure is received when attempting to send a newsletter, emails will be disabled on the account.": "当尝试发送快报时遇到永久错误,向该账户的发送邮件的功能将被禁用。", "In your email client add {{senderEmail}} to your contacts list. This signals to your mail provider that emails sent from this address should be trusted.": "在您的电子邮件客户端将 {{senderEmail}} 加入联系人列表。这将向您的邮件供应商表明来自该地址的邮件是可信的。", - "Invalid email address": "", + "Invalid email address": "无效的电子邮件地址", "Jamie Larson": "", "jamie@example.com": "", "Less like this": "不喜欢", @@ -91,20 +108,22 @@ "Name": "名字", "Need more help? Contact support": "需要更多帮助?联系支持服务", "Newsletters can be disabled on your account for two reasons: A previous email was marked as spam, or attempting to send an email resulted in a permanent failure (bounce).": "您账户的快报被禁用的可能原因有两个:先前的电子邮件被标记为垃圾邮件,或者发送邮件遇到永久错误 (bounce)", + "No member exists with this e-mail address.": "", + "No member exists with this e-mail address. Please sign up first.": "", "Not receiving emails?": "无法收到电子邮件?", "Now check your email!": "现在请检查您的电子邮件!", "Once resubscribed, if you still don't see emails in your inbox, check your spam folder. Some inbox providers keep a record of previous spam complaints and will continue to flag emails. If this happens, mark the latest newsletter as 'Not spam' to move it back to your primary inbox.": "重新订阅后在收件箱依旧没有看到邮件,请检查您的垃圾邮件箱。一些服务商会保留之前的垃圾邮件记录并持续标记。如果是这样,请将最新的快报标记为“非垃圾邮件”并将其移动到收件箱。", "Permanent failure (bounce)": "永久错误 (bounce)", - "Phone number": "", + "Phone number": "电话号码", "Plan": "订阅计划", "Plan checkout was cancelled.": "订阅付款已取消。", "Plan upgrade was cancelled.": "订阅升级已取消。", "Please contact {{supportAddress}} to adjust your complimentary subscription.": "请联系 {{supportAddress}} 调整您的免费订阅。", - "Please enter {{fieldName}}": "", + "Please enter {{fieldName}}": "请输入{{fieldName}}", "Please fill in required fields": "请填写必须项目", "Price": "价格", "Re-enable emails": "重启电子邮件", - "Recommendations": "", + "Recommendations": "推荐", "Renews at {{price}}.": "以{{price}}的价格续费。", "Retry": "重试", "Save": "保存", @@ -117,7 +136,7 @@ "Sign out": "退出", "Sign up": "注册", "Signup error: Invalid link": "注册错误:链接无效", - "Something went wrong, please try again later.": "", + "Something went wrong, please try again later.": "出了点问题,请稍后再试。", "Sorry, that didn’t work.": "抱歉,该操作无法完成。", "Spam complaints": "垃圾邮件", "Start {{amount}}-day free trial": "开始 {{amount}}-天免费试用", @@ -126,28 +145,39 @@ "Submit feedback": "提交建议", "Subscribe": "订阅", "Subscribed": "已订阅", + "Subscription plan updated successfully": "", "Success": "成功", "Success! Check your email for magic link to sign-in.": "成功!检查您的电子邮箱以获取登录链接。", "Success! Your account is fully activated, you now have access to all content.": "成功!您的账户已经完全激活,您现在可以访问全部内容了。", "Success! Your email is updated.": "成功!您的电子邮件已更新。", "Successfully unsubscribed": "成功取消订阅", "Thank you for subscribing. Before you start reading, below are a few other sites you may enjoy.": "感谢您的订阅。在开始阅读之前,以下是您可能会喜欢的一些其他网站。", - "Thank you for your support": "", - "Thank you for your support!": "", + "Thank you for your support": "感谢您的支持", + "Thank you for your support!": "感谢您的支持!", "Thanks for the feedback!": "感谢您的建议!", "That didn't go to plan": "似乎出错了", "The email address we have for you is {{memberEmail}} — if that's not correct, you can update it in your .": "您的电子邮件地址是 {{memberEmail}} - 如果该邮箱不正确,您可以在中更新它。", "There was a problem submitting your feedback. Please try again a little later.": "提交您的反馈时遇到错误。请稍后重试。", - "There was an error processing your payment. Please try again.": "", + "There was an error cancelling your subscription, please try again.": "", + "There was an error continuing your subscription, please try again.": "", + "There was an error processing your payment. Please try again.": "您的付款处理失败,请重试。", + "There was an error sending the email, please try again": "", "This site is invite-only, contact the owner for access.": "此网站仅限邀请,联系网站所有者以获取访问", - "This site is not accepting payments at the moment.": "", + "This site is not accepting payments at the moment.": "本网站目前暂不接受付款。", "To complete signup, click the confirmation link in your inbox. If it doesn't arrive within 3 minutes, check your spam folder!": "要完成注册,请点击您收件箱中的确认链接。如果在3分钟内没有收到,请检查一下您的垃圾邮件文件夹!", - "To continue to stay up to date, subscribe to {{publication}} below.": "", + "To continue to stay up to date, subscribe to {{publication}} below.": "如需持续获取最新资讯,请在下方订阅{{publication}}", + "Too many attempts try again in {{number}} days.": "", + "Too many attempts try again in {{number}} hours.": "", + "Too many attempts try again in {{number}} minutes.": "", + "Too many different sign-in attempts, try again in {{number}} days": "", + "Too many different sign-in attempts, try again in {{number}} hours": "", + "Too many different sign-in attempts, try again in {{number}} minutes": "", "Try free for {{amount}} days, then {{originalPrice}}.": "{{amount}}天免费试用,之后{{originalPrice}}。", + "Unable to initiate checkout session": "", "Unlock access to all newsletters by becoming a paid subscriber.": "成为付费订阅用户以解锁全部快报。", "Unsubscribe from all emails": "取消所有邮件订阅", - "Unsubscribed": "取消订阅", - "Unsubscribed from all emails.": "", + "Unsubscribed": "已取消订阅", + "Unsubscribed from all emails.": "已取消所有邮件订阅", "Unsubscribing from emails will not cancel your paid subscription to {{title}}": "取消邮件订阅不会取消您对 {{title}} 的付费订阅。", "Update": "更新", "Update your preferences": "更新您的偏好设置", @@ -170,6 +200,7 @@ "You've successfully signed in.": "您已成功登录。", "You've successfully subscribed to": "您已成功订阅", "Your account": "您的账户", + "Your email has failed to resubscribe, please try again": "", "Your input helps shape what gets published.": "您的建议将使我们变得更好。", "Your subscription will expire on {{expiryDate}}": "您的订阅将在{{expiryDate}}到期", "Your subscription will renew on {{renewalDate}}": "您的订阅将在{{renewalDate}}续费", diff --git a/ghost/i18n/locales/zh/search.json b/ghost/i18n/locales/zh/search.json new file mode 100644 index 00000000000..714ed690020 --- /dev/null +++ b/ghost/i18n/locales/zh/search.json @@ -0,0 +1,9 @@ +{ + "Authors": "作者", + "Cancel": "取消", + "No matches found": "未找到匹配项", + "Posts": "文章", + "Search posts, tags and authors": "搜索文章、标签和作者", + "Show more results": "显示更多结果", + "Tags": "标签" +} diff --git a/ghost/i18n/package.json b/ghost/i18n/package.json index ba1b8c69b6f..34fb3128311 100644 --- a/ghost/i18n/package.json +++ b/ghost/i18n/package.json @@ -13,11 +13,12 @@ "lint:code": "eslint *.js lib/ --ext .js --cache", "lint": "yarn lint:code && yarn lint:test", "lint:test": "eslint -c test/.eslintrc.js test/ --ext .js --cache", - "translate": "yarn translate:ghost && yarn translate:portal && yarn translate:signup-form && yarn translate:comments && node generate-context.js", + "translate": "yarn translate:ghost && yarn translate:portal && yarn translate:signup-form && yarn translate:comments && yarn translate:search && node generate-context.js", "translate:ghost": "NAMESPACE=ghost i18next '../core/core/{frontend,server,shared}/**/*.{js,jsx}'", "translate:portal": "NAMESPACE=portal i18next '../../apps/portal/src/**/*.{js,jsx}'", "translate:signup-form": "NAMESPACE=signup-form i18next '../../apps/signup-form/src/**/*.{ts,tsx}'", - "translate:comments": "NAMESPACE=comments i18next '../../apps/comments-ui/src/**/*.{ts,tsx}'" + "translate:comments": "NAMESPACE=comments i18next '../../apps/comments-ui/src/**/*.{ts,tsx}'", + "translate:search": "NAMESPACE=search i18next '../../apps/sodo-search/src/**/*.{js,jsx,ts,tsx}'" }, "files": [ "index.js", @@ -30,6 +31,6 @@ "mocha": "10.2.0" }, "dependencies": { - "i18next": "23.15.1" + "i18next": "23.15.2" } } diff --git a/ghost/i18n/test/i18n.test.js b/ghost/i18n/test/i18n.test.js index 3de13004e54..1c755b0f640 100644 --- a/ghost/i18n/test/i18n.test.js +++ b/ghost/i18n/test/i18n.test.js @@ -69,4 +69,25 @@ describe('i18n', function () { }); }); }); + describe('directories and locales in i18n.js will match', function () { + it('should have a key for each directory in the locales directory', async function () { + const locales = await fs.readdir(path.join(__dirname, '../locales')); + const supportedLocales = i18n.SUPPORTED_LOCALES; + + for (const locale of locales) { + if (locale !== 'context.json') { + assert(supportedLocales.includes(locale), `The locale ${locale} is not in the list of supported locales`); + } + } + }); + it('should have a directory for each key in lib/i18n.js', async function () { + const supportedLocales = i18n.SUPPORTED_LOCALES; + + for (const locale of supportedLocales) { + const localeDir = path.join(__dirname, `../locales/${locale}`); + const stats = await fs.stat(localeDir); + assert(stats.isDirectory(), `The locale ${locale} does not have a directory`); + } + }); + }); }); diff --git a/ghost/metrics-server/.eslintrc.js b/ghost/metrics-server/.eslintrc.js new file mode 100644 index 00000000000..ecc28524e8d --- /dev/null +++ b/ghost/metrics-server/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + parser: '@typescript-eslint/parser', + plugins: ['ghost'], + extends: [ + 'plugin:ghost/node' + ] +}; diff --git a/ghost/metrics-server/README.md b/ghost/metrics-server/README.md new file mode 100644 index 00000000000..09d7b977006 --- /dev/null +++ b/ghost/metrics-server/README.md @@ -0,0 +1,23 @@ +# Metrics Server + +A standalone server for exporting prometheus metrics from Ghost + + +## Usage + + +## Develop + +This is a monorepo package. + +Follow the instructions for the top-level repo. +1. `git clone` this repo & `cd` into it as usual +2. Run `yarn` to install top-level dependencies. + + + +## Test + +- `yarn lint` run just eslint +- `yarn test` run lint and tests + diff --git a/ghost/metrics-server/package.json b/ghost/metrics-server/package.json new file mode 100644 index 00000000000..e8f88a20595 --- /dev/null +++ b/ghost/metrics-server/package.json @@ -0,0 +1,38 @@ +{ + "name": "@tryghost/metrics-server", + "version": "0.0.0", + "repository": "https://github.com/TryGhost/Ghost/tree/main/packages/metrics-server", + "author": "Ghost Foundation", + "private": true, + "main": "build/index.js", + "types": "build/index.d.ts", + "scripts": { + "dev": "tsc --watch --preserveWatchOutput --sourceMap", + "build": "yarn build:ts", + "build:ts": "tsc", + "prepare": "tsc", + "test:unit": "NODE_ENV=testing c8 --src src --all --check-coverage --100 --reporter text --reporter cobertura mocha -r ts-node/register './test/**/*.test.ts'", + "test": "yarn test:types && yarn test:unit", + "test:types": "tsc --noEmit", + "lint:code": "eslint src/ --ext .ts --cache", + "lint": "yarn lint:code && yarn lint:test", + "lint:test": "eslint -c test/.eslintrc.js test/ --ext .ts --cache" + }, + "files": [ + "build" + ], + "devDependencies": { + "@types/express": "4.17.21", + "@types/stoppable": "1.1.0", + "c8": "10.1.2", + "mocha": "10.7.3", + "sinon": "19.0.2", + "supertest": "7.0.0", + "ts-node": "10.9.2", + "typescript": "5.6.2" + }, + "dependencies": { + "express": "4.21.0", + "stoppable": "1.1.0" + } +} diff --git a/ghost/metrics-server/src/MetricsServer.ts b/ghost/metrics-server/src/MetricsServer.ts new file mode 100644 index 00000000000..bb28e8e7278 --- /dev/null +++ b/ghost/metrics-server/src/MetricsServer.ts @@ -0,0 +1,59 @@ +import debugModule from '@tryghost/debug'; +import express from 'express'; +import stoppable from 'stoppable'; + +const debug = debugModule('metrics-server'); + +type ServerConfig = { + host: string; + port: number; +}; + +export class MetricsServer { + private serverConfig: ServerConfig; + private handler: express.Handler; + private app: express.Application | null; + private httpServer: stoppable.StoppableServer | null; + private isShuttingDown: boolean; + + constructor({serverConfig, handler}: {serverConfig: ServerConfig, handler: express.Handler}) { + // initialize local variables + this.serverConfig = serverConfig; + this.handler = handler; + this.app = null; + this.httpServer = null; + this.isShuttingDown = false; + } + + async start() { + // start the server + debug('Starting metrics server'); + this.app = express(); + this.app.get('/metrics', this.handler); + const httpServer = this.app.listen(this.serverConfig.port, this.serverConfig.host, () => { + debug(`Metrics server listening at ${this.serverConfig.host}:${this.serverConfig.port}`); + }); + this.httpServer = stoppable(httpServer, 0); + + process.on('SIGINT', () => this.shutdown()); + process.on('SIGTERM', () => this.shutdown()); + return {app: this.app, httpServer: this.httpServer}; + } + + async stop() { + // stop the server + debug('Stopping metrics server'); + if (this.httpServer && this.httpServer.listening) { + await this.httpServer.stop(); + } + } + + async shutdown() { + if (this.isShuttingDown) { + return; + } + this.isShuttingDown = true; + await this.stop(); + this.isShuttingDown = false; + } +} \ No newline at end of file diff --git a/ghost/metrics-server/src/index.ts b/ghost/metrics-server/src/index.ts new file mode 100644 index 00000000000..79ca36d6128 --- /dev/null +++ b/ghost/metrics-server/src/index.ts @@ -0,0 +1 @@ +export * from './MetricsServer'; diff --git a/ghost/metrics-server/src/libraries.d.ts b/ghost/metrics-server/src/libraries.d.ts new file mode 100644 index 00000000000..c99c02f0f0c --- /dev/null +++ b/ghost/metrics-server/src/libraries.d.ts @@ -0,0 +1 @@ +declare module '@tryghost/debug'; \ No newline at end of file diff --git a/ghost/metrics-server/test/.eslintrc.js b/ghost/metrics-server/test/.eslintrc.js new file mode 100644 index 00000000000..6fe6dc1504a --- /dev/null +++ b/ghost/metrics-server/test/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + parser: '@typescript-eslint/parser', + plugins: ['ghost'], + extends: [ + 'plugin:ghost/test' + ] +}; diff --git a/ghost/metrics-server/test/metrics-server.test.ts b/ghost/metrics-server/test/metrics-server.test.ts new file mode 100644 index 00000000000..63082ba0974 --- /dev/null +++ b/ghost/metrics-server/test/metrics-server.test.ts @@ -0,0 +1,79 @@ +import assert from 'assert/strict'; +import {MetricsServer} from '../src'; +import request from 'supertest'; +import express from 'express'; +import * as sinon from 'sinon'; + +describe('Metrics Server', function () { + let metricsServer: MetricsServer; + let serverConfig = { + host: '127.0.0.1', + port: 9416 + }; + let handler = (req: express.Request, res: express.Response) => { + res.send('metrics'); + }; + + afterEach(async function () { + await metricsServer.stop(); + }); + + after(async function () { + await metricsServer.shutdown(); + }); + + describe('constructor', function () { + it('should create a new instance', function () { + metricsServer = new MetricsServer({serverConfig, handler}); + assert.ok(metricsServer); + }); + }); + + describe('start', function () { + before(function () { + metricsServer = new MetricsServer({serverConfig, handler}); + }); + it('should start the server', async function () { + const server = await metricsServer.start(); + assert.ok(server); + }); + + it('should use the provided handler', async function () { + const {app} = await metricsServer.start(); + const response = await request(app).get('/metrics'); + assert.ok(response.status === 200); + assert.ok(response.text === 'metrics'); + }); + }); + + describe('stop', function () { + before(function () { + metricsServer = new MetricsServer({serverConfig, handler}); + }); + it('should stop the server', async function () { + const server = await metricsServer.start(); + await metricsServer.stop(); + assert.ok(server); + }); + }); + + describe('shutdown', function () { + before(function () { + metricsServer = new MetricsServer({serverConfig, handler}); + }); + it('should shutdown the server', async function () { + const server = await metricsServer.start(); + await metricsServer.shutdown(); + assert.ok(server); + }); + + it('should not shutdown the server if it is already shutting down', async function () { + const stopSpy = sinon.spy(metricsServer, 'stop'); + await metricsServer.start(); + // Call shutdown multiple times simultaneously + Promise.all([metricsServer.shutdown(), metricsServer.shutdown()]); + // It should only call stop() once + sinon.assert.calledOnce(stopSpy); + }); + }); +}); diff --git a/ghost/metrics-server/tsconfig.json b/ghost/metrics-server/tsconfig.json new file mode 100644 index 00000000000..749654ff94c --- /dev/null +++ b/ghost/metrics-server/tsconfig.json @@ -0,0 +1,111 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": ["es2019"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + "rootDir": "src", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "build", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src/**/*"] +} diff --git a/ghost/minifier/package.json b/ghost/minifier/package.json index 6b6bc1c991d..b22ab607d9b 100644 --- a/ghost/minifier/package.json +++ b/ghost/minifier/package.json @@ -28,7 +28,7 @@ "@tryghost/errors": "1.3.5", "@tryghost/tpl": "0.1.32", "csso": "5.0.5", - "terser": "5.33.0", + "terser": "5.34.1", "tiny-glob": "0.2.9" } } diff --git a/ghost/post-events/src/PostsBulkUnscheduledEvent.ts b/ghost/post-events/src/PostsBulkUnscheduledEvent.ts new file mode 100644 index 00000000000..c1fcc4c4ecf --- /dev/null +++ b/ghost/post-events/src/PostsBulkUnscheduledEvent.ts @@ -0,0 +1,13 @@ +export class PostsBulkUnscheduledEvent { + data: string[]; + timestamp: Date; + + constructor(data: string[], timestamp: Date) { + this.data = data; + this.timestamp = timestamp; + } + + static create(data: string[], timestamp = new Date()) { + return new PostsBulkUnscheduledEvent(data, timestamp); + } +} diff --git a/ghost/post-events/src/index.ts b/ghost/post-events/src/index.ts index b8b4e20f5f8..bd0b96e881e 100644 --- a/ghost/post-events/src/index.ts +++ b/ghost/post-events/src/index.ts @@ -2,6 +2,7 @@ export * from './PostDeletedEvent'; export * from './PostsBulkDestroyedEvent'; export * from './PostsBulkUnpublishedEvent'; +export * from './PostsBulkUnscheduledEvent'; export * from './PostsBulkFeaturedEvent'; export * from './PostsBulkUnfeaturedEvent'; export * from './PostsBulkAddTagsEvent'; diff --git a/ghost/post-events/test/post-events.test.ts b/ghost/post-events/test/post-events.test.ts index ef8e434756c..1308094fb0b 100644 --- a/ghost/post-events/test/post-events.test.ts +++ b/ghost/post-events/test/post-events.test.ts @@ -3,6 +3,7 @@ import { PostDeletedEvent, PostsBulkDestroyedEvent, PostsBulkUnpublishedEvent, + PostsBulkUnscheduledEvent, PostsBulkFeaturedEvent, PostsBulkUnfeaturedEvent, PostsBulkAddTagsEvent @@ -27,6 +28,12 @@ describe('Post Events', function () { assert.equal(event.data.length, 3); }); + it('Can instantiate PostsBulkUnscheduledEvent', function () { + const event = PostsBulkUnscheduledEvent.create(['1', '2', '3']); + assert.ok(event); + assert.equal(event.data.length, 3); + }); + it('Can instantiate PostsBulkFeaturedEvent', function () { const event = PostsBulkFeaturedEvent.create(['1', '2', '3']); assert.ok(event); diff --git a/ghost/posts-service/lib/PostsService.js b/ghost/posts-service/lib/PostsService.js index 1299161eecc..98cf806dd14 100644 --- a/ghost/posts-service/lib/PostsService.js +++ b/ghost/posts-service/lib/PostsService.js @@ -8,6 +8,7 @@ const DomainEvents = require('@tryghost/domain-events'); const { PostsBulkDestroyedEvent, PostsBulkUnpublishedEvent, + PostsBulkUnscheduledEvent, PostsBulkFeaturedEvent, PostsBulkUnfeaturedEvent, PostsBulkAddTagsEvent @@ -246,6 +247,19 @@ class PostsService { return updateResult; } + if (data.action === 'unschedule') { + const updateResult = await this.#updatePosts({status: 'draft', published_at: null}, {filter: this.#mergeFilters('status:scheduled', options.filter), context: options.context, actionName: 'unscheduled'}); + // makes sure `email_only` value is reverted for the unscheduled posts + await this.models.Post.bulkEdit(updateResult.editIds, 'posts_meta', { + data: {email_only: false}, + column: 'post_id', + transacting: options.transacting, + throwErrors: true + }); + DomainEvents.dispatch(PostsBulkUnscheduledEvent.create(updateResult.editIds)); + + return updateResult; + } if (data.action === 'feature') { const updateResult = await this.#updatePosts({featured: true}, {filter: options.filter, context: options.context, actionName: 'featured'}); DomainEvents.dispatch(PostsBulkFeaturedEvent.create(updateResult.editIds)); diff --git a/ghost/tinybird/datasources/analytics_pages_mv.datasource b/ghost/tinybird/datasources/analytics_pages_mv.datasource index 1ffdf55f640..38a37577587 100644 --- a/ghost/tinybird/datasources/analytics_pages_mv.datasource +++ b/ghost/tinybird/datasources/analytics_pages_mv.datasource @@ -5,11 +5,12 @@ SCHEMA > `device` String, `browser` String, `location` String, + `source` String, `pathname` String, `member_status` SimpleAggregateFunction(any, String), `visits` AggregateFunction(uniq, String), - `hits` AggregateFunction(count) + `pageviews` AggregateFunction(count) ENGINE AggregatingMergeTree ENGINE_PARTITION_KEY toYYYYMM(date) -ENGINE_SORTING_KEY date, device, browser, location, pathname, post_uuid, site_uuid +ENGINE_SORTING_KEY date, device, browser, location, source, pathname, post_uuid, site_uuid diff --git a/ghost/tinybird/datasources/analytics_sessions_mv.datasource b/ghost/tinybird/datasources/analytics_sessions_mv.datasource index 7356906fe80..c1efa7c2079 100644 --- a/ghost/tinybird/datasources/analytics_sessions_mv.datasource +++ b/ghost/tinybird/datasources/analytics_sessions_mv.datasource @@ -7,9 +7,11 @@ SCHEMA > `device` SimpleAggregateFunction(any, String), `browser` SimpleAggregateFunction(any, String), `location` SimpleAggregateFunction(any, String), - `first_hit` SimpleAggregateFunction(min, DateTime), - `latest_hit` SimpleAggregateFunction(max, DateTime), - `hits` AggregateFunction(count) + `source` SimpleAggregateFunction(any, String), + `pathname` SimpleAggregateFunction(any, String), + `first_view` SimpleAggregateFunction(min, DateTime), + `latest_view` SimpleAggregateFunction(max, DateTime), + `pageviews` AggregateFunction(count) ENGINE AggregatingMergeTree ENGINE_PARTITION_KEY toYYYYMM(date) diff --git a/ghost/tinybird/datasources/analytics_sources_mv.datasource b/ghost/tinybird/datasources/analytics_sources_mv.datasource index 3219f0773e5..740b945c7d4 100644 --- a/ghost/tinybird/datasources/analytics_sources_mv.datasource +++ b/ghost/tinybird/datasources/analytics_sources_mv.datasource @@ -4,11 +4,12 @@ SCHEMA > `device` String, `browser` String, `location` String, - `referrer` String, + `source` String, + `pathname` String, `member_status` SimpleAggregateFunction(any, String), `visits` AggregateFunction(uniq, String), - `hits` AggregateFunction(count) + `pageviews` AggregateFunction(count) ENGINE AggregatingMergeTree ENGINE_PARTITION_KEY toYYYYMM(date) -ENGINE_SORTING_KEY date, device, browser, location, referrer, site_uuid +ENGINE_SORTING_KEY date, device, browser, location, source, pathname, site_uuid diff --git a/ghost/tinybird/datasources/fixtures/README.md b/ghost/tinybird/datasources/fixtures/README.md index 6130978f2ad..f32d91bbd31 100644 --- a/ghost/tinybird/datasources/fixtures/README.md +++ b/ghost/tinybird/datasources/fixtures/README.md @@ -1,5 +1,9 @@ # Datasource fixtures +## NDJSON files +These files are fixtures used for running tests + +## Schema JSON files The file mockingbird-schema.json is a schema for generating fake data using the Mockingbird CLI. The CLI is installed via npm: diff --git a/ghost/tinybird/datasources/fixtures/analytics_events.ndjson b/ghost/tinybird/datasources/fixtures/analytics_events.ndjson new file mode 100644 index 00000000000..2018c2d4d77 --- /dev/null +++ b/ghost/tinybird/datasources/fixtures/analytics_events.ndjson @@ -0,0 +1,31 @@ +{"timestamp":"2100-01-01 00:06:15","session_id":"e5c37e25-ed9e-4940-a2be-bc49149d991a","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"undefined\",\"member_status\":\"undefined\",\"post_uuid\":\"6b8635fb-292f-4422-9fe4-d76cfab2ba31\",\"user-agent\":\"AhrefsBot/7.0; +http://ahrefs.com/robot/\",\"locale\":\"en-GB\",\"location\":\"GB\",\"referrer\":\"https://petty-queen.com\",\"pathname\":\"/blog/hello-world/\",\"href\":\"https://my-ghost-site.com/blog/hello-world/\"}"} +{"timestamp":"2100-01-01 01:21:17","session_id":"1267b782-e5a1-4334-8cf6-771d72bbc28e","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"d4678fdf-824c-4d5f-a5fe-c713d409faac\",\"member_status\":\"free\",\"post_uuid\":\"undefined\",\"user-agent\":\"Mozilla/5.0 (Windows; U; Windows NT 5.2) AppleWebKit/533.2.1 (KHTML, like Gecko) Chrome/13.0.868.0 Safari/533.2.1\",\"locale\":\"es-ES\",\"location\":\"ES\",\"referrer\":\"\",\"pathname\":\"/\",\"href\":\"https://my-ghost-site.com/\"}"} +{"timestamp":"2100-01-01 01:39:48","session_id":"1267b782-e5a1-4334-8cf6-771d72bbc28e","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"d4678fdf-824c-4d5f-a5fe-c713d409faac\",\"member_status\":\"free\",\"post_uuid\":\"undefined\",\"user-agent\":\"Mozilla/5.0 (Windows; U; Windows NT 5.2) AppleWebKit/533.2.1 (KHTML, like Gecko) Chrome/13.0.868.0 Safari/533.2.1\",\"locale\":\"es-ES\",\"location\":\"ES\",\"referrer\":\"https://my-ghost-site.com\",\"pathname\":\"/\",\"href\":\"https://my-ghost-site.com/\"}"} +{"timestamp":"2100-01-01 02:21:13","session_id":"2a31286e-53b4-41da-a7fd-89d966072af5","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"df8343d2-e89d-45b7-ba12-988734efcc56\",\"member_status\":\"free\",\"post_uuid\":\"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc\",\"user-agent\":\"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/4.0)\",\"locale\":\"en-GB\",\"location\":\"GB\",\"referrer\":\"https://www.bing.com/\",\"pathname\":\"/about/\",\"href\":\"https://my-ghost-site.com/about/\"}"} +{"timestamp":"2100-01-01 02:31:43","session_id":"2a31286e-53b4-41da-a7fd-89d966072af5","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"df8343d2-e89d-45b7-ba12-988734efcc56\",\"member_status\":\"free\",\"post_uuid\":\"undefined\",\"user-agent\":\"Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0; Trident/4.0)\",\"locale\":\"en-GB\",\"location\":\"GB\",\"referrer\":\"https://my-ghost-site.com\",\"pathname\":\"/\",\"href\":\"https://my-ghost-site.com/\"}"} +{"timestamp":"2100-01-02 00:59:45","session_id":"f253b9b7-0a1a-4168-8fcf-b20a1668ce4d","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"65bacac2-8122-4ed0-a11f-ac52aa82beb0\",\"member_status\":\"paid\",\"post_uuid\":\"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc\",\"user-agent\":\"Mozilla/5.0 (Windows NT 5.3; Win64; x64; rv:11.6) Gecko/20100101 Firefox/11.6.2\",\"locale\":\"en-GB\",\"location\":\"GB\",\"referrer\":\"https://www.google.com/\",\"pathname\":\"/about/\",\"href\":\"https://my-ghost-site.com/about/\"}"} +{"timestamp":"2100-01-02 01:12:56","session_id":"f253b9b7-0a1a-4168-8fcf-b20a1668ce4d","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"65bacac2-8122-4ed0-a11f-ac52aa82beb0\",\"member_status\":\"paid\",\"post_uuid\":\"undefined\",\"user-agent\":\"Mozilla/5.0 (Windows NT 5.3; Win64; x64; rv:11.6) Gecko/20100101 Firefox/11.6.2\",\"locale\":\"en-GB\",\"location\":\"GB\",\"referrer\":\"https://my-ghost-site.com\",\"pathname\":\"/\",\"href\":\"https://my-ghost-site.com/\"}"} +{"timestamp":"2100-01-02 01:16:52","session_id":"f253b9b7-0a1a-4168-8fcf-b20a1668ce4d","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"65bacac2-8122-4ed0-a11f-ac52aa82beb0\",\"member_status\":\"paid\",\"post_uuid\":\"undefined\",\"user-agent\":\"Mozilla/5.0 (Windows NT 5.3; Win64; x64; rv:11.6) Gecko/20100101 Firefox/11.6.2\",\"locale\":\"en-GB\",\"location\":\"GB\",\"referrer\":\"https://my-ghost-site.com\",\"pathname\":\"/\",\"href\":\"https://my-ghost-site.com/\"}"} +{"timestamp":"2100-01-03 00:01:24","session_id":"9c15f99e-c8b1-4145-a073-e7f8649d2fa4","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"4c14393f-d792-403e-bbdc-aa5af3abbdd9\",\"member_status\":\"free\",\"post_uuid\":\"undefined\",\"user-agent\":\"Mozilla/5.0 (Windows NT 5.0; rv:10.7) Gecko/20100101 Firefox/10.7.1\",\"locale\":\"en-US\",\"location\":\"US\",\"referrer\":\"https://duckduckgo.com/\",\"pathname\":\"/\",\"href\":\"https://my-ghost-site.com/\"}"} +{"timestamp":"2100-01-03 01:28:09","session_id":"9c15f99e-c8b1-4145-a073-e7f8649d2fa4","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"4c14393f-d792-403e-bbdc-aa5af3abbdd9\",\"member_status\":\"free\",\"post_uuid\":\"6b8635fb-292f-4422-9fe4-d76cfab2ba31\",\"user-agent\":\"Mozilla/5.0 (Windows NT 5.0; rv:10.7) Gecko/20100101 Firefox/10.7.1\",\"locale\":\"en-US\",\"location\":\"US\",\"referrer\":\"https://my-ghost-site.com\",\"pathname\":\"/blog/hello-world/\",\"href\":\"https://my-ghost-site.com/blog/hello-world/\"}"} +{"timestamp":"2100-01-03 01:41:44","session_id":"8a2461a8-91cd-4f01-b066-3de6dc946995","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"f4c738bc-7327-440c-8007-6a0b306c05e3\",\"member_status\":\"free\",\"post_uuid\":\"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc\",\"user-agent\":\"Mozilla/5.0 (Windows; U; Windows NT 5.0) AppleWebKit/533.2.0 (KHTML, like Gecko) Chrome/39.0.887.0 Safari/533.2.0\",\"locale\":\"de-DE\",\"location\":\"DE\",\"referrer\":\"https://www.bing.com/\",\"pathname\":\"/about/\",\"href\":\"https://my-ghost-site.com/about/\"}"} +{"timestamp":"2100-01-03 01:53:31","session_id":"8a2461a8-91cd-4f01-b066-3de6dc946995","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"f4c738bc-7327-440c-8007-6a0b306c05e3\",\"member_status\":\"free\",\"post_uuid\":\"6b8635fb-292f-4422-9fe4-d76cfab2ba31\",\"user-agent\":\"Mozilla/5.0 (Windows; U; Windows NT 5.0) AppleWebKit/533.2.0 (KHTML, like Gecko) Chrome/39.0.887.0 Safari/533.2.0\",\"locale\":\"de-DE\",\"location\":\"DE\",\"referrer\":\"https://my-ghost-site.com\",\"pathname\":\"/blog/hello-world/\",\"href\":\"https://my-ghost-site.com/blog/hello-world/\"}"} +{"timestamp":"2100-01-03 02:00:19","session_id":"8a2461a8-91cd-4f01-b066-3de6dc946995","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"f4c738bc-7327-440c-8007-6a0b306c05e3\",\"member_status\":\"free\",\"post_uuid\":\"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc\",\"user-agent\":\"Mozilla/5.0 (Windows; U; Windows NT 5.0) AppleWebKit/533.2.0 (KHTML, like Gecko) Chrome/39.0.887.0 Safari/533.2.0\",\"locale\":\"de-DE\",\"location\":\"DE\",\"referrer\":\"https://my-ghost-site.com\",\"pathname\":\"/about/\",\"href\":\"https://my-ghost-site.com/about/\"}"} +{"timestamp":"2100-01-03 02:51:20","session_id":"50785df1-3232-4ff7-8495-d93e06d63f5c","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"3675e750-09bf-44c9-bc3f-b9aebac37c5d\",\"member_status\":\"paid\",\"post_uuid\":\"undefined\",\"user-agent\":\"Mozilla/5.0 (Windows NT 6.3; rv:14.7) Gecko/20100101 Firefox/14.7.1\",\"locale\":\"fr-FR\",\"location\":\"FR\",\"referrer\":\"https://search.yahoo.com/\",\"pathname\":\"/\",\"href\":\"https://my-ghost-site.com/\"}"} +{"timestamp":"2100-01-03 03:52:39","session_id":"50785df1-3232-4ff7-8495-d93e06d63f5c","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"3675e750-09bf-44c9-bc3f-b9aebac37c5d\",\"member_status\":\"paid\",\"post_uuid\":\"undefined\",\"user-agent\":\"Mozilla/5.0 (Windows NT 6.3; rv:14.7) Gecko/20100101 Firefox/14.7.1\",\"locale\":\"fr-FR\",\"location\":\"FR\",\"referrer\":\"https://my-ghost-site.com\",\"pathname\":\"/\",\"href\":\"https://my-ghost-site.com/\"}"} +{"timestamp":"2100-01-04 00:25:39","session_id":"59478d87-ce95-40fd-a081-65d1e497bcfc","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"97c79891-2ae9-4eb2-ada8-89d2a998747d\",\"member_status\":\"paid\",\"post_uuid\":\"6b8635fb-292f-4422-9fe4-d76cfab2ba31\",\"user-agent\":\"Mozilla/5.0 (Windows; U; Windows NT 6.3) AppleWebKit/531.2.2 (KHTML, like Gecko) Chrome/31.0.808.0 Safari/531.2.2\",\"locale\":\"en-GB\",\"location\":\"GB\",\"referrer\":\"\",\"pathname\":\"/blog/hello-world/\",\"href\":\"https://my-ghost-site.com/blog/hello-world/\"}"} +{"timestamp":"2100-01-04 01:10:48","session_id":"a6b6c4e6-19e3-47a9-afc6-d9870592652e","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"undefined\",\"member_status\":\"undefined\",\"post_uuid\":\"6b8635fb-292f-4422-9fe4-d76cfab2ba31\",\"user-agent\":\"Mozilla/5.0 (Windows; U; Windows NT 5.2) AppleWebKit/533.0.1 (KHTML, like Gecko) Chrome/32.0.856.0 Safari/533.0.1\",\"locale\":\"en-GB\",\"location\":\"GB\",\"referrer\":\"\",\"pathname\":\"/blog/hello-world/\",\"href\":\"https://my-ghost-site.com/blog/hello-world/\"}"} +{"timestamp":"2100-01-04 01:16:10","session_id":"a6b6c4e6-19e3-47a9-afc6-d9870592652e","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"undefined\",\"member_status\":\"undefined\",\"post_uuid\":\"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc\",\"user-agent\":\"Mozilla/5.0 (Windows; U; Windows NT 5.2) AppleWebKit/533.0.1 (KHTML, like Gecko) Chrome/32.0.856.0 Safari/533.0.1\",\"locale\":\"en-GB\",\"location\":\"GB\",\"referrer\":\"https://my-ghost-site.com\",\"pathname\":\"/about/\",\"href\":\"https://my-ghost-site.com/about/\"}"} +{"timestamp":"2100-01-04 01:20:15","session_id":"a6b6c4e6-19e3-47a9-afc6-d9870592652e","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"undefined\",\"member_status\":\"undefined\",\"post_uuid\":\"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc\",\"user-agent\":\"Mozilla/5.0 (Windows; U; Windows NT 5.2) AppleWebKit/533.0.1 (KHTML, like Gecko) Chrome/32.0.856.0 Safari/533.0.1\",\"locale\":\"en-GB\",\"location\":\"GB\",\"referrer\":\"https://my-ghost-site.com\",\"pathname\":\"/about/\",\"href\":\"https://my-ghost-site.com/about/\"}"} +{"timestamp":"2100-01-04 01:35:41","session_id":"e22a7f6f-28da-4715-a199-6f0338b593d4","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"5369031a-a5cd-4176-83d8-d6ffcb3bcfb8\",\"member_status\":\"free\",\"post_uuid\":\"6b8635fb-292f-4422-9fe4-d76cfab2ba31\",\"user-agent\":\"Mozilla/5.0 (Windows; U; Windows NT 6.1) AppleWebKit/538.0.1 (KHTML, like Gecko) Chrome/16.0.814.0 Safari/538.0.1\",\"locale\":\"en-GB\",\"location\":\"GB\",\"referrer\":\"\",\"pathname\":\"/blog/hello-world/\",\"href\":\"https://my-ghost-site.com/blog/hello-world/\"}"} +{"timestamp":"2100-01-04 01:36:33","session_id":"e22a7f6f-28da-4715-a199-6f0338b593d4","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"5369031a-a5cd-4176-83d8-d6ffcb3bcfb8\",\"member_status\":\"free\",\"post_uuid\":\"undefined\",\"user-agent\":\"Mozilla/5.0 (Windows; U; Windows NT 6.1) AppleWebKit/538.0.1 (KHTML, like Gecko) Chrome/16.0.814.0 Safari/538.0.1\",\"locale\":\"en-GB\",\"location\":\"GB\",\"referrer\":\"https://my-ghost-site.com\",\"pathname\":\"/\",\"href\":\"https://my-ghost-site.com/\"}"} +{"timestamp":"2100-01-04 01:54:50","session_id":"e22a7f6f-28da-4715-a199-6f0338b593d4","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"5369031a-a5cd-4176-83d8-d6ffcb3bcfb8\",\"member_status\":\"free\",\"post_uuid\":\"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc\",\"user-agent\":\"Mozilla/5.0 (Windows; U; Windows NT 6.1) AppleWebKit/538.0.1 (KHTML, like Gecko) Chrome/16.0.814.0 Safari/538.0.1\",\"locale\":\"en-GB\",\"location\":\"GB\",\"referrer\":\"https://my-ghost-site.com\",\"pathname\":\"/about/\",\"href\":\"https://my-ghost-site.com/about/\"}"} +{"timestamp":"2100-01-05 01:51:00","session_id":"d8e4622f-95cc-4fba-b31b-f38ff72e0975","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"75a190eb-62da-46d2-972d-a9763c954f42\",\"member_status\":\"paid\",\"post_uuid\":\"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc\",\"user-agent\":\"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/3.0)\",\"locale\":\"es-ES\",\"location\":\"ES\",\"referrer\":\"\",\"pathname\":\"/about/\",\"href\":\"https://my-ghost-site.com/about/\"}"} +{"timestamp":"2100-01-05 01:53:03","session_id":"d8e4622f-95cc-4fba-b31b-f38ff72e0975","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"75a190eb-62da-46d2-972d-a9763c954f42\",\"member_status\":\"paid\",\"post_uuid\":\"6b8635fb-292f-4422-9fe4-d76cfab2ba31\",\"user-agent\":\"Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/3.0)\",\"locale\":\"es-ES\",\"location\":\"ES\",\"referrer\":\"https://my-ghost-site.com\",\"pathname\":\"/blog/hello-world/\",\"href\":\"https://my-ghost-site.com/blog/hello-world/\"}"} +{"timestamp":"2100-01-05 00:29:59","session_id":"490475f1-1fb7-4672-9edd-daa1b411b5f9","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"undefined\",\"member_status\":\"undefined\",\"post_uuid\":\"6b8635fb-292f-4422-9fe4-d76cfab2ba31\",\"user-agent\":\"Mozilla/5.0 (Windows; U; Windows NT 5.1) AppleWebKit/532.2.0 (KHTML, like Gecko) Chrome/20.0.898.0 Safari/532.2.0\",\"locale\":\"en-GB\",\"location\":\"GB\",\"referrer\":\"https://www.baidu.com/\",\"pathname\":\"/blog/hello-world/\",\"href\":\"https://my-ghost-site.com/blog/hello-world/\"}"} +{"timestamp":"2100-01-05 00:37:42","session_id":"490475f1-1fb7-4672-9edd-daa1b411b5f9","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"undefined\",\"member_status\":\"undefined\",\"post_uuid\":\"undefined\",\"user-agent\":\"Mozilla/5.0 (Windows; U; Windows NT 5.1) AppleWebKit/532.2.0 (KHTML, like Gecko) Chrome/20.0.898.0 Safari/532.2.0\",\"locale\":\"en-GB\",\"location\":\"GB\",\"referrer\":\"https://my-ghost-site.com\",\"pathname\":\"/\",\"href\":\"https://my-ghost-site.com/\"}"} +{"timestamp":"2100-01-05 00:38:12","session_id":"490475f1-1fb7-4672-9edd-daa1b411b5f9","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"undefined\",\"member_status\":\"undefined\",\"post_uuid\":\"6b8635fb-292f-4422-9fe4-d76cfab2ba31\",\"user-agent\":\"Mozilla/5.0 (Windows; U; Windows NT 5.1) AppleWebKit/532.2.0 (KHTML, like Gecko) Chrome/20.0.898.0 Safari/532.2.0\",\"locale\":\"en-GB\",\"location\":\"GB\",\"referrer\":\"https://my-ghost-site.com\",\"pathname\":\"/blog/hello-world/\",\"href\":\"https://my-ghost-site.com/blog/hello-world/\"}"} +{"timestamp":"2100-01-06 00:51:26","session_id":"8d975128-2027-40c6-834a-972cc0293d21","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"b7e0fca6-27ce-46c0-af57-c591f20dcd51\",\"member_status\":\"free\",\"post_uuid\":\"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc\",\"user-agent\":\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_7 rv:2.0; KW) AppleWebKit/537.0.1 (KHTML, like Gecko) Version/5.0.10 Safari/537.0.1\",\"locale\":\"fr-FR\",\"location\":\"FR\",\"referrer\":\"\",\"pathname\":\"/about/\",\"href\":\"https://my-ghost-site.com/about/\"}"} +{"timestamp":"2100-01-06 01:28:38","session_id":"61a2896b-7cf8-4853-86a6-a0e4f87c1e21","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"undefined\",\"member_status\":\"undefined\",\"post_uuid\":\"6b8635fb-292f-4422-9fe4-d76cfab2ba31\",\"user-agent\":\"Mozilla/5.0 (Windows; U; Windows NT 5.1) AppleWebKit/533.1.0 (KHTML, like Gecko) Chrome/18.0.852.0 Safari/533.1.0\",\"locale\":\"en-GB\",\"location\":\"GB\",\"referrer\":\"https://search.yahoo.com/\",\"pathname\":\"/blog/hello-world/\",\"href\":\"https://my-ghost-site.com/blog/hello-world/\"}"} +{"timestamp":"2100-01-07 01:44:10","session_id":"7f1e88e1-da8e-46df-bc69-d04fb29d603d","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"undefined\",\"member_status\":\"undefined\",\"post_uuid\":\"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc\",\"user-agent\":\"Mozilla/5.0 (Windows NT 5.0; WOW64; rv:13.9) Gecko/20100101 Firefox/13.9.7\",\"locale\":\"en-US\",\"location\":\"US\",\"referrer\":\"http://wilted-tick.com\",\"pathname\":\"/about/\",\"href\":\"https://my-ghost-site.com/about/\"}"} +{"timestamp":"2100-01-07 02:23:19","session_id":"98159299-8111-4dc8-9156-bb339fe9508c","action":"page_hit","version":"1","payload":"{\"site_uuid\":\"mock_site_uuid\",\"member_uuid\":\"undefined\",\"member_status\":\"undefined\",\"post_uuid\":\"06b1b0c9-fb53-4a15-a060-3db3fde7b1dd\",\"user-agent\":\"Mozilla/5.0 (Windows NT 5.0; WOW64; rv:13.9) Gecko/20100101 Firefox/13.9.7\",\"locale\":\"en-US\",\"location\":\"US\",\"referrer\":\"https://my-ghost-site.com\",\"pathname\":\"/blog/hello-world/\",\"href\":\"https://my-ghost-site.com/blog/hello-world/\"}"} diff --git a/ghost/tinybird/datasources/fixtures/utils/ndjson_to_csv.sh b/ghost/tinybird/datasources/fixtures/utils/ndjson_to_csv.sh new file mode 100755 index 00000000000..d037bfa9e56 --- /dev/null +++ b/ghost/tinybird/datasources/fixtures/utils/ndjson_to_csv.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash + +# Check if the correct number of arguments is provided +if [ "$#" -ne 2 ]; then + echo "Usage: $0 " + exit 1 +fi + +# Assign input and output file paths from arguments +input_file="$1" +output_file="$2" + +# Create the header row +header="timestamp,session_id,action,version,site_uuid,member_uuid,member_status,post_uuid,user-agent,locale,location,referrer,pathname,href" + +# Write the header to the output file +echo "$header" > "$output_file" + +# Convert NDJSON to CSV and append to the output file +jq -r ' + [ + .timestamp, + .session_id, + .action, + .version, + (.payload | fromjson | .site_uuid), + (.payload | fromjson | .member_uuid), + (.payload | fromjson | .member_status), + (.payload | fromjson | .post_uuid), + (.payload | fromjson | .["user-agent"]), + (.payload | fromjson | .locale), + (.payload | fromjson | .location), + (.payload | fromjson | .referrer), + (.payload | fromjson | .pathname), + (.payload | fromjson | .href) + ] | @csv +' "$input_file" >> "$output_file" # Append to the output file + +echo "Conversion complete: $output_file" diff --git a/ghost/tinybird/pipes/analytics_hits.pipe b/ghost/tinybird/pipes/analytics_hits.pipe index ed41d251eff..a24a1811e4b 100644 --- a/ghost/tinybird/pipes/analytics_hits.pipe +++ b/ghost/tinybird/pipes/analytics_hits.pipe @@ -39,7 +39,7 @@ SQL > member_status, post_uuid, location, - referrer, + domainWithoutWWW(referrer) as source, pathname, href, case diff --git a/ghost/tinybird/pipes/analytics_pages.pipe b/ghost/tinybird/pipes/analytics_pages.pipe index ee3ed64bb6c..f72f0a7953e 100644 --- a/ghost/tinybird/pipes/analytics_pages.pipe +++ b/ghost/tinybird/pipes/analytics_pages.pipe @@ -1,6 +1,6 @@ NODE analytics_pages_1 DESCRIPTION > - Aggregate by pathname and calculate session and hits + Aggregate by pathname and calculate session and views SQL > SELECT @@ -10,15 +10,16 @@ SQL > device, browser, location, + source, pathname, maxIf( member_status, member_status IN ('paid', 'free', 'undefined') ) AS member_status, uniqState(session_id) AS visits, - countState() AS hits + countState() AS pageviews FROM analytics_hits - GROUP BY date, device, browser, location, pathname, post_uuid,site_uuid + GROUP BY date, device, browser, location, source, pathname, post_uuid,site_uuid TYPE MATERIALIZED DATASOURCE analytics_pages_mv diff --git a/ghost/tinybird/pipes/analytics_sessions.pipe b/ghost/tinybird/pipes/analytics_sessions.pipe index 52cc8977ecc..6c29b7c3dfd 100644 --- a/ghost/tinybird/pipes/analytics_sessions.pipe +++ b/ghost/tinybird/pipes/analytics_sessions.pipe @@ -15,9 +15,11 @@ SQL > anySimpleState(device) AS device, anySimpleState(browser) AS browser, anySimpleState(location) AS location, - minSimpleState(timestamp) AS first_hit, - maxSimpleState(timestamp) AS latest_hit, - countState() AS hits + anySimpleState(source) AS source, + anySimpleState(pathname) AS pathname, + minSimpleState(timestamp) AS first_view, + maxSimpleState(timestamp) AS latest_view, + countState() AS pageviews FROM analytics_hits GROUP BY date, session_id, site_uuid diff --git a/ghost/tinybird/pipes/analytics_sources.pipe b/ghost/tinybird/pipes/analytics_sources.pipe index 460430eb314..c7b8286b4a3 100644 --- a/ghost/tinybird/pipes/analytics_sources.pipe +++ b/ghost/tinybird/pipes/analytics_sources.pipe @@ -1,25 +1,31 @@ NODE analytics_sources_1 DESCRIPTION > - Aggregate by referral and calculate session and hits + Aggregate by referral and calculate session and views SQL > - WITH (SELECT domainWithoutWWW(href) FROM analytics_hits LIMIT 1) AS current_domain + WITH (SELECT domainWithoutWWW(href) FROM analytics_hits LIMIT 1) AS current_domain, + sessions AS ( + SELECT + session_id, argMin(source, timestamp) AS source, + maxIf(member_status, member_status IN ('paid', 'free', 'undefined')) AS member_status + FROM analytics_hits + GROUP BY session_id + ) SELECT - site_uuid, - toDate(timestamp) AS date, - device, - browser, - location, - referrer, - maxIf( - member_status, - member_status IN ('paid', 'free', 'undefined') - ) AS member_status, - uniqState(session_id) AS visits, - countState() AS hits - FROM analytics_hits - WHERE domainWithoutWWW(referrer) != current_domain - GROUP BY date, device, browser, location, referrer, site_uuid + a.site_uuid, + toDate(a.timestamp) AS date, + a.device, + a.browser, + a.location, + b.source AS source, + a.pathname, + b.member_status AS member_status, + uniqState(a.session_id) AS visits, + countState() AS pageviews + FROM analytics_hits as a + INNER JOIN sessions AS b ON a.session_id = b.session_id + GROUP BY a.site_uuid, toDate(a.timestamp), a.device, a.browser, a.location, b.member_status, b.source, a.pathname + HAVING b.source != current_domain TYPE MATERIALIZED DATASOURCE analytics_sources_mv diff --git a/ghost/tinybird/pipes/kpis.pipe b/ghost/tinybird/pipes/kpis.pipe index 7a78022fa13..17b2d8e2bd6 100644 --- a/ghost/tinybird/pipes/kpis.pipe +++ b/ghost/tinybird/pipes/kpis.pipe @@ -62,9 +62,8 @@ SQL > ) ) as date {% end %} - where date <= now() -NODE hits +NODE pageviews DESCRIPTION > Group by sessions and calculate metrics at that level @@ -76,25 +75,35 @@ SQL > toStartOfHour(timestamp) as date, session_id, member_status, + device, + browser, + location, + source, + pathname, uniq(session_id) as visits, count() as pageviews, case when min(timestamp) = max(timestamp) then 1 else 0 end as is_bounce, - max(timestamp) as latest_hit_aux, - min(timestamp) as first_hit_aux + max(timestamp) as latest_view_aux, + min(timestamp) as first_view_aux from analytics_hits where toDate(timestamp) = {{ Date(date_from) }} - group by toStartOfHour(timestamp), session_id, site_uuid, member_status + group by toStartOfHour(timestamp), session_id, site_uuid, member_status, device, browser, location, source, pathname {% else %} select site_uuid, date, member_status, + device, + browser, + location, + source, + pathname, session_id, uniq(session_id) as visits, - countMerge(hits) as pageviews, - case when min(first_hit) = max(latest_hit) then 1 else 0 end as is_bounce, - max(latest_hit) as latest_hit_aux, - min(first_hit) as first_hit_aux + countMerge(pageviews) as pageviews, + case when min(first_view) = max(latest_view) then 1 else 0 end as is_bounce, + max(latest_view) as latest_view_aux, + min(first_view) as first_view_aux from analytics_sessions_mv where {% if defined(date_from) %} date >= {{ Date(date_from) }} @@ -103,7 +112,7 @@ SQL > {% if defined(date_to) %} and date <= {{ Date(date_to) }} {% else %} and date <= today() {% end %} - group by date, session_id, site_uuid, member_status + group by date, session_id, site_uuid, member_status, device, browser, location, source, pathname {% end %} NODE data @@ -115,12 +124,17 @@ SQL > site_uuid, date, member_status, + device, + browser, + location, + source, + pathname, uniq(session_id) as visits, sum(pageviews) as pageviews, - sum(case when latest_hit_aux = first_hit_aux then 1 else 0 end) / visits as bounce_rate, - avg(latest_hit_aux - first_hit_aux) as avg_session_sec - from hits - group by date, site_uuid, member_status + sum(case when latest_view_aux = first_view_aux then 1 else 0 end) / visits as bounce_rate, + avg(latest_view_aux - first_view_aux) as avg_session_sec + from pageviews + group by date, site_uuid, member_status, device, browser, location, source, pathname NODE endpoint DESCRIPTION > @@ -132,14 +146,18 @@ SQL > a.date as date, sum(b.visits) as visits, sum(b.pageviews) as pageviews, - sum(b.bounce_rate) as bounce_rate, + avg(b.bounce_rate) as bounce_rate, sum(b.avg_session_sec) as avg_session_sec from timeseries a left join data b using date where site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}} - {% if defined(member_status) %} - and member_status IN {{ Array(member_status) }} - {% end %} + + {% if defined(member_status) %} and member_status IN {{ Array(member_status, "'undefined', 'free', 'paid'", description="Member status to filter on", required=False) }} {% end %} + {% if defined(device) %} and device = {{ String(device, description="Device to filter on", required=False) }} {% end %} + {% if defined(browser) %} and browser = {{ String(browser, description="Browser to filter on", required=False) }} {% end %} + {% if defined(source) %} and source = {{ String(source, description="Source to filter on", required=False) }} {% end %} + {% if defined(location) %} and location = {{ String(location, description="Location to filter on", required=False) }} {% end %} + {% if defined(pathname) %} and pathname = {{ String(pathname, description="Pathname to filter on", required=False) }} {% end %} group by date order by date WITH FILL STEP 1 diff --git a/ghost/tinybird/pipes/top_browsers.pipe b/ghost/tinybird/pipes/top_browsers.pipe index 60ad2275c35..d857a9a5f4a 100644 --- a/ghost/tinybird/pipes/top_browsers.pipe +++ b/ghost/tinybird/pipes/top_browsers.pipe @@ -8,17 +8,15 @@ TOKEN "stats page" READ NODE endpoint DESCRIPTION > - Group by browser and calculate hits and visits + Group by browser and calculate views and visits SQL > % - select browser, uniqMerge(visits) as visits, countMerge(hits) as hits - from analytics_sources_mv + select browser, uniq(session_id) as visits, countMerge(pageviews) as pageviews + from analytics_sessions_mv where site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}} - {% if defined(member_status) %} - and member_status IN {{ Array(member_status,'String') }} - {% end %} + {% if defined(date_from) %} and date >= @@ -32,6 +30,13 @@ SQL > {{ Date(date_to, description="Finishing day for filtering a date range", required=False) }} {% else %} and date <= today() {% end %} + + {% if defined(member_status) %} and member_status IN {{ Array(member_status, "'undefined', 'free', 'paid'", description="Member status to filter on", required=False) }} {% end %} + {% if defined(device) %} and device = {{ String(device, description="Device to filter on", required=False) }} {% end %} + {% if defined(browser) %} and browser = {{ String(browser, description="Browser to filter on", required=False) }} {% end %} + {% if defined(source) %} and source = {{ String(source, description="Source to filter on", required=False) }} {% end %} + {% if defined(location) %} and location = {{ String(location, description="Location to filter on", required=False) }} {% end %} + {% if defined(pathname) %} and pathname = {{ String(pathname, description="Pathname to filter on", required=False) }} {% end %} group by browser order by visits desc limit {{ Int32(skip, 0) }},{{ Int32(limit, 50) }} diff --git a/ghost/tinybird/pipes/top_devices.pipe b/ghost/tinybird/pipes/top_devices.pipe index 7ab3e3c5188..d7bccb165bb 100644 --- a/ghost/tinybird/pipes/top_devices.pipe +++ b/ghost/tinybird/pipes/top_devices.pipe @@ -9,17 +9,15 @@ TOKEN "stats page" READ NODE endpoint DESCRIPTION > - Group by device and calculate hits and visits + Group by device and calculate views and visits SQL > % - select device, uniqMerge(visits) as visits, countMerge(hits) as hits - from analytics_sources_mv + select device, uniq(session_id) as visits, countMerge(pageviews) as pageviews + from analytics_sessions_mv where site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}} - {% if defined(member_status) %} - and member_status IN {{ Array(member_status,'String') }} - {% end %} + {% if defined(date_from) %} and date >= @@ -33,6 +31,14 @@ SQL > {{ Date(date_to, description="Finishing day for filtering a date range", required=False) }} {% else %} and date <= today() {% end %} + + {% if defined(member_status) %} and member_status IN {{ Array(member_status, "'undefined', 'free', 'paid'", description="Member status to filter on", required=False) }} {% end %} + {% if defined(device) %} and device = {{ String(device, description="Device to filter on", required=False) }} {% end %} + {% if defined(browser) %} and browser = {{ String(browser, description="Browser to filter on", required=False) }} {% end %} + {% if defined(source) %} and source = {{ String(source, description="Source to filter on", required=False) }} {% end %} + {% if defined(location) %} and location = {{ String(location, description="Location to filter on", required=False) }} {% end %} + {% if defined(pathname) %} and pathname = {{ String(pathname, description="Pathname to filter on", required=False) }} {% end %} + group by device order by visits desc limit {{ Int32(skip, 0) }},{{ Int32(limit, 50) }} diff --git a/ghost/tinybird/pipes/top_locations.pipe b/ghost/tinybird/pipes/top_locations.pipe index a33067d6d28..d79803e6322 100644 --- a/ghost/tinybird/pipes/top_locations.pipe +++ b/ghost/tinybird/pipes/top_locations.pipe @@ -8,17 +8,15 @@ TOKEN "stats page" READ NODE endpoint DESCRIPTION > - Group by pagepath and calculate hits and visits + Group by pagepath and calculate views and visits SQL > % - select location, uniqMerge(visits) as visits, countMerge(hits) as hits + select location, uniqMerge(visits) as visits, countMerge(pageviews) as pageviews from analytics_pages_mv where site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}} - {% if defined(member_status) %} - and member_status IN {{ Array(member_status,'String') }} - {% end %} + {% if defined(date_from) %} and date >= @@ -32,6 +30,14 @@ SQL > {{ Date(date_to, description="Finishing day for filtering a date range", required=False) }} {% else %} and date <= today() {% end %} + + {% if defined(member_status) %} and member_status IN {{ Array(member_status, "'undefined', 'free', 'paid'", description="Member status to filter on", required=False) }} {% end %} + {% if defined(device) %} and device = {{ String(device, description="Device to filter on", required=False) }} {% end %} + {% if defined(browser) %} and browser = {{ String(browser, description="Browser to filter on", required=False) }} {% end %} + {% if defined(source) %} and source = {{ String(source, description="Source to filter on", required=False) }} {% end %} + {% if defined(location) %} and location = {{ String(location, description="Location to filter on", required=False) }} {% end %} + {% if defined(pathname) %} and pathname = {{ String(pathname, description="Pathname to filter on", required=False) }} {% end %} + group by location order by visits desc limit {{ Int32(skip, 0) }},{{ Int32(limit, 50) }} diff --git a/ghost/tinybird/pipes/top_pages.pipe b/ghost/tinybird/pipes/top_pages.pipe index 8db3d6a21f0..935a91091c0 100644 --- a/ghost/tinybird/pipes/top_pages.pipe +++ b/ghost/tinybird/pipes/top_pages.pipe @@ -8,20 +8,18 @@ TOKEN "stats page" READ NODE endpoint DESCRIPTION > - Group by pagepath and calculate hits and visits + Group by pathname and calculate views and visits SQL > % select pathname, uniqMerge(visits) as visits, - countMerge(hits) as hits + countMerge(pageviews) as pageviews from analytics_pages_mv where site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}} - {% if defined(member_status) %} - and member_status IN {{ Array(member_status,'String') }} - {% end %} + {% if defined(date_from) %} and date >= @@ -36,6 +34,13 @@ SQL > {% else %} and date <= today() {% end %} + {% if defined(member_status) %} and member_status IN {{ Array(member_status, "'undefined', 'free', 'paid'", description="Member status to filter on", required=False) }} {% end %} + {% if defined(device) %} and device = {{ String(device, description="Device to filter on", required=False) }} {% end %} + {% if defined(browser) %} and browser = {{ String(browser, description="Browser to filter on", required=False) }} {% end %} + {% if defined(source) %} and source = {{ String(source, description="Source to filter on", required=False) }} {% end %} + {% if defined(location) %} and location = {{ String(location, description="Location to filter on", required=False) }} {% end %} + {% if defined(pathname) %} and pathname = {{ String(pathname, description="Pathname to filter on", required=False) }} {% end %} + group by pathname order by visits desc limit {{ Int32(skip, 0) }},{{ Int32(limit, 50) }} diff --git a/ghost/tinybird/pipes/top_sources.pipe b/ghost/tinybird/pipes/top_sources.pipe index b892bb4e2e5..32caa3f6a1d 100644 --- a/ghost/tinybird/pipes/top_sources.pipe +++ b/ghost/tinybird/pipes/top_sources.pipe @@ -9,17 +9,15 @@ TOKEN "stats page" READ NODE endpoint DESCRIPTION > - Group by referral and calculate hits and visits + Group by source and calculate views and visits SQL > % - select domainWithoutWWW(referrer) as referrer, uniqMerge(visits) as visits, countMerge(hits) as hits + select source, uniqMerge(visits) as visits, countMerge(pageviews) as pageviews from analytics_sources_mv where site_uuid = {{String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True)}} - {% if defined(member_status) %} - and member_status IN {{ Array(member_status,'String') }} - {% end %} + {% if defined(date_from) %} and date >= @@ -33,6 +31,13 @@ SQL > {{ Date(date_to, description="Finishing day for filtering a date range", required=False) }} {% else %} and date <= today() {% end %} - group by referrer + + {% if defined(member_status) %} and member_status IN {{ Array(member_status, "'undefined', 'free', 'paid'", description="Member status to filter on", required=False) }} {% end %} + {% if defined(device) %} and device = {{ String(device, description="Device to filter on", required=False) }} {% end %} + {% if defined(browser) %} and browser = {{ String(browser, description="Browser to filter on", required=False) }} {% end %} + {% if defined(source) %} and source = {{ String(source, description="Source to filter on", required=False) }} {% end %} + {% if defined(location) %} and location = {{ String(location, description="Location to filter on", required=False) }} {% end %} + {% if defined(pathname) %} and pathname = {{ String(pathname, description="Pathname to filter on", required=False) }} {% end %} + group by source order by visits desc limit {{ Int32(skip, 0) }},{{ Int32(limit, 50) }} diff --git a/ghost/tinybird/pipes/trend.pipe b/ghost/tinybird/pipes/trend.pipe index fafbfcf591c..e333b461db6 100644 --- a/ghost/tinybird/pipes/trend.pipe +++ b/ghost/tinybird/pipes/trend.pipe @@ -15,7 +15,7 @@ SQL > select addMinutes(toStartOfMinute(start), number) as t from (select arrayJoin(range(1, 31)) as number) -NODE hits +NODE visits DESCRIPTION > Get last 30 minutes metrics gropued by minute @@ -37,4 +37,4 @@ DESCRIPTION > Join and generate timeseries with metrics for the last 30 minutes SQL > - select a.t, b.visits from timeseries a left join hits b on a.t = b.t order by a.t + select a.t, b.visits from timeseries a left join visits b on a.t = b.t order by a.t diff --git a/ghost/tinybird/scripts/branch_and_test.sh b/ghost/tinybird/scripts/branch_and_test.sh new file mode 100755 index 00000000000..ee6cae88f63 --- /dev/null +++ b/ghost/tinybird/scripts/branch_and_test.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash + +# Get the directory where this script is located +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" + +# Create a branch and test it + +# Create a variable with a timestamp to use as a random name for the branch +BRANCH_NAME="TEST_$(date +%s)" + +# Check for -y flag +force_delete=false +for arg in "$@"; do + if [[ "$arg" == "-y" ]]; then + force_delete=true + break + fi +done + +# Attempt to create the branch and check for errors +if ! tb branch create "$BRANCH_NAME"; then + echo "🚨 ERROR: Failed to create branch $BRANCH_NAME. Exiting." + exit 1 +fi + +# Run the scripts using their full paths +"$SCRIPT_DIR/append_fixtures.sh" +"$SCRIPT_DIR/exec_test.sh" + +# Conditional deletion based on the -y flag or user prompt +if [ "$force_delete" = true ]; then + tb branch rm "$BRANCH_NAME" --yes + echo "Branch $BRANCH_NAME removed without prompt." +else + read -p "Do you want to delete the branch $BRANCH_NAME? (y/n): " choice + if [[ "$choice" == "y" || "$choice" == "Y" ]]; then + tb branch rm "$BRANCH_NAME" --yes + echo "Branch $BRANCH_NAME removed." + else + echo "Branch $BRANCH_NAME kept." + fi +fi diff --git a/ghost/tinybird/scripts/exec_test.sh b/ghost/tinybird/scripts/exec_test.sh index 50571d952db..02d52c08d16 100755 --- a/ghost/tinybird/scripts/exec_test.sh +++ b/ghost/tinybird/scripts/exec_test.sh @@ -1,9 +1,48 @@ - #!/usr/bin/env bash -set -euxo pipefail +set -euo pipefail export TB_VERSION_WARNING=0 +# Get the expected count once, outside of any function +ndjson_file="./datasources/fixtures/analytics_events.ndjson" +export expected_count=$(wc -l < "$ndjson_file" || echo "0") + +check_sum() { + local file=$1 + local expected_count=$2 + + # Only perform the check if the file starts with "all_" + if [[ ! $(basename "$file") =~ ^all_ ]]; then + return 0 + fi + + local sum=0 + local column_name="" + + # Determine if the file has a 'pageviews' column + if head -n1 "$file" | grep -q 'pageviews'; then + column_name="pageviews" + else + echo "No 'pageviews' column found in $file" + return 0 # No relevant column found, skip the check + fi + + # Get the column number + local column_num=$(head -n1 "$file" | tr ',' '\n' | grep -n "$column_name" | cut -d: -f1) + + # Sum the values in the column + sum=$(tail -n +2 "$file" | cut -d',' -f"$column_num" | awk '{s+=$1} END {print s}') + + # Check if the sum equals the number of lines in the NDJSON file + if [ "$sum" -eq "$expected_count" ]; then + echo "✅ Sanity check passed: Sum of $column_name is $sum (matches NDJSON line count)" + return 0 + else + echo "⚠️ WARNING: Sanity check failed: Sum of $column_name is $sum, expected $expected_count (NDJSON line count)" + return 1 # Return 1 to indicate a warning, but not a failure + fi +} + run_test() { t=$1 echo "** Running $t **" @@ -15,15 +54,15 @@ run_test() { # When appending fixtures, we need to retry in case of the data is not replicated in time while [ $retries -lt $TOTAL_RETRIES ]; do # Run the test and store the output in a temporary file - bash $t $2 >$tmpfile + bash "$t" >"$tmpfile" exit_code=$? if [ "$exit_code" -eq 0 ]; then # If the test passed, break the loop - if diff -B ${t}.result $tmpfile >/dev/null 2>&1; then + if diff -B "${t}.result" "$tmpfile" >/dev/null 2>&1; then break # If the test failed, increment the retries counter and try again else - retries=$((retries+1)) + retries=$((retries + 1)) fi # If the bash command failed, print an error message and break the loop else @@ -31,28 +70,47 @@ run_test() { fi done - if diff -B ${t}.result $tmpfile >/dev/null 2>&1; then + if diff -B "${t}.result" "$tmpfile" >/dev/null 2>&1; then echo "✅ Test $t passed" - rm $tmpfile + check_sum "${t}.result" "$expected_count" || echo "⚠️ Warning: Sanity check did not pass." + rm "$tmpfile" return 0 elif [ $retries -eq $TOTAL_RETRIES ]; then - echo "🚨 ERROR: Test $t failed, diff:"; - diff -B ${t}.result $tmpfile - rm $tmpfile + echo "🚨 ERROR: Test $t failed, showing differences:" + diff -B -u --color -U3 "${t}.result" "$tmpfile" # Use unified diff format with 3 lines of context + rm "$tmpfile" return 1 else echo "🚨 ERROR: Test $t failed with bash command exit code $?" - cat $tmpfile - rm $tmpfile + cat "$tmpfile" + rm "$tmpfile" return 1 fi echo "" } + export -f run_test +export -f check_sum fail=0 -find ./tests -name "*.test" -print0 | xargs -0 -I {} -P 4 bash -c 'run_test "$@"' _ {} || fail=1 + +# Check if a test name was provided as an argument +if [ $# -eq 1 ]; then + test_name=$1 + # Find the test file that matches the provided name + test_file=$(find ./tests -name "${test_name}*.test") + if [ -n "$test_file" ]; then + run_test "$test_file" || fail=1 + else + echo "🚨 ERROR: No test found matching name: $test_name" + fail=1 + fi +else + # If no test name provided, run all tests + find ./tests -name "*.test" -print0 | xargs -0 -I {} bash -c 'run_test "$@"' _ {} || fail=1 +fi if [ $fail == 1 ]; then - exit -1; + echo "🚨 ERROR: Some tests failed" + exit 1 fi diff --git a/ghost/tinybird/scripts/gen_test_results.sh b/ghost/tinybird/scripts/gen_test_results.sh new file mode 100755 index 00000000000..836d4a3fd5f --- /dev/null +++ b/ghost/tinybird/scripts/gen_test_results.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Directory containing the test files +TEST_DIR="./tests" + +# Function to execute a test and update its result file +execute_and_update() { + local test_file="$1" + local result_file="${test_file%.test}.test.result" + + echo "Executing test: $test_file" + + # Execute the test command and capture the output + output=$(bash "$test_file") + + # Write the output to the result file, overwriting existing content + echo "$output" > "$result_file" + + echo "Updated result file: $result_file" + echo "------------------------" +} + +# Main execution +echo "Starting test result regeneration..." + +# Find all .test files and process them +find "$TEST_DIR" -name "*.test" -type f | while read -r test_file; do + execute_and_update "$test_file" +done + +echo "Test result regeneration complete." diff --git a/ghost/tinybird/scripts/unsafe_redeploy.sh b/ghost/tinybird/scripts/unsafe_redeploy.sh new file mode 100755 index 00000000000..6e490dab9a2 --- /dev/null +++ b/ghost/tinybird/scripts/unsafe_redeploy.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Check if --force flag is provided +force=false +if [[ "$@" == *"--force"* ]]; then + force=true +fi + +# Get current branch info +branch_info=$(tb branch current) + +# Check if 'main' and 'production_stats' are both present in the output +if echo "$branch_info" | grep -q "main" && echo "$branch_info" | grep -q "production_stats"; then + if [ "$force" = false ]; then + echo "🚨 ERROR: Attempting to run unsafe_redeploy on main branch in production_stats workspace." + echo "If you're sure you want to do this, run the script with the --force flag." + exit 1 + else + echo "⚠️ WARNING: Running unsafe_redeploy on main branch in production_stats workspace with --force flag." + fi +fi + +echo "Proceeding with unsafe redeploy..." + +# Remove our materialized views and their pipes +tb datasource rm analytics_pages_mv --yes +tb datasource rm analytics_sessions_mv --yes +tb datasource rm analytics_sources_mv --yes +tb pipe rm analytics_pages --yes +tb pipe rm analytics_sessions --yes +tb pipe rm analytics_sources --yes +tb pipe rm analytics_hits --yes + +# Remove all the endpoints +tb pipe rm pipes/kpis.pipe --yes +tb pipe rm pipes/top_browsers.pipe --yes +tb pipe rm pipes/top_devices.pipe --yes +tb pipe rm pipes/top_locations.pipe --yes +tb pipe rm pipes/top_pages.pipe --yes +tb pipe rm pipes/top_sources.pipe --yes +tb pipe rm pipes/trend.pipe --yes + +# Push all the changes +tb push --force --populate diff --git a/ghost/tinybird/tests/all_analytics_hits.test b/ghost/tinybird/tests/all_analytics_hits.test new file mode 100644 index 00000000000..0117414fa24 --- /dev/null +++ b/ghost/tinybird/tests/all_analytics_hits.test @@ -0,0 +1 @@ +tb pipe data analytics_hits --format CSV diff --git a/ghost/tinybird/tests/all_analytics_hits.test.result b/ghost/tinybird/tests/all_analytics_hits.test.result new file mode 100644 index 00000000000..2924012eb8d --- /dev/null +++ b/ghost/tinybird/tests/all_analytics_hits.test.result @@ -0,0 +1,32 @@ +"site_uuid","timestamp","action","version","session_id","member_uuid","member_status","post_uuid","location","source","pathname","href","device","browser" +"mock_site_uuid","2100-01-01 00:06:15","page_hit","1","e5c37e25-ed9e-4940-a2be-bc49149d991a","undefined","undefined","6b8635fb-292f-4422-9fe4-d76cfab2ba31","GB","petty-queen.com","/blog/hello-world/","https://my-ghost-site.com/blog/hello-world/","bot","Unknown" +"mock_site_uuid","2100-01-01 01:21:17","page_hit","1","1267b782-e5a1-4334-8cf6-771d72bbc28e","d4678fdf-824c-4d5f-a5fe-c713d409faac","free","undefined","ES","","/","https://my-ghost-site.com/","desktop","chrome" +"mock_site_uuid","2100-01-01 01:39:48","page_hit","1","1267b782-e5a1-4334-8cf6-771d72bbc28e","d4678fdf-824c-4d5f-a5fe-c713d409faac","free","undefined","ES","my-ghost-site.com","/","https://my-ghost-site.com/","desktop","chrome" +"mock_site_uuid","2100-01-01 02:21:13","page_hit","1","2a31286e-53b4-41da-a7fd-89d966072af5","df8343d2-e89d-45b7-ba12-988734efcc56","free","06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","GB","bing.com","/about/","https://my-ghost-site.com/about/","desktop","ie" +"mock_site_uuid","2100-01-01 02:31:43","page_hit","1","2a31286e-53b4-41da-a7fd-89d966072af5","df8343d2-e89d-45b7-ba12-988734efcc56","free","undefined","GB","my-ghost-site.com","/","https://my-ghost-site.com/","desktop","ie" +"mock_site_uuid","2100-01-02 00:59:45","page_hit","1","f253b9b7-0a1a-4168-8fcf-b20a1668ce4d","65bacac2-8122-4ed0-a11f-ac52aa82beb0","paid","06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","GB","google.com","/about/","https://my-ghost-site.com/about/","desktop","firefox" +"mock_site_uuid","2100-01-02 01:12:56","page_hit","1","f253b9b7-0a1a-4168-8fcf-b20a1668ce4d","65bacac2-8122-4ed0-a11f-ac52aa82beb0","paid","undefined","GB","my-ghost-site.com","/","https://my-ghost-site.com/","desktop","firefox" +"mock_site_uuid","2100-01-02 01:16:52","page_hit","1","f253b9b7-0a1a-4168-8fcf-b20a1668ce4d","65bacac2-8122-4ed0-a11f-ac52aa82beb0","paid","undefined","GB","my-ghost-site.com","/","https://my-ghost-site.com/","desktop","firefox" +"mock_site_uuid","2100-01-03 00:01:24","page_hit","1","9c15f99e-c8b1-4145-a073-e7f8649d2fa4","4c14393f-d792-403e-bbdc-aa5af3abbdd9","free","undefined","US","duckduckgo.com","/","https://my-ghost-site.com/","desktop","firefox" +"mock_site_uuid","2100-01-03 01:28:09","page_hit","1","9c15f99e-c8b1-4145-a073-e7f8649d2fa4","4c14393f-d792-403e-bbdc-aa5af3abbdd9","free","6b8635fb-292f-4422-9fe4-d76cfab2ba31","US","my-ghost-site.com","/blog/hello-world/","https://my-ghost-site.com/blog/hello-world/","desktop","firefox" +"mock_site_uuid","2100-01-03 01:41:44","page_hit","1","8a2461a8-91cd-4f01-b066-3de6dc946995","f4c738bc-7327-440c-8007-6a0b306c05e3","free","06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","DE","bing.com","/about/","https://my-ghost-site.com/about/","desktop","chrome" +"mock_site_uuid","2100-01-03 01:53:31","page_hit","1","8a2461a8-91cd-4f01-b066-3de6dc946995","f4c738bc-7327-440c-8007-6a0b306c05e3","free","6b8635fb-292f-4422-9fe4-d76cfab2ba31","DE","my-ghost-site.com","/blog/hello-world/","https://my-ghost-site.com/blog/hello-world/","desktop","chrome" +"mock_site_uuid","2100-01-03 02:00:19","page_hit","1","8a2461a8-91cd-4f01-b066-3de6dc946995","f4c738bc-7327-440c-8007-6a0b306c05e3","free","06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","DE","my-ghost-site.com","/about/","https://my-ghost-site.com/about/","desktop","chrome" +"mock_site_uuid","2100-01-03 02:51:20","page_hit","1","50785df1-3232-4ff7-8495-d93e06d63f5c","3675e750-09bf-44c9-bc3f-b9aebac37c5d","paid","undefined","FR","search.yahoo.com","/","https://my-ghost-site.com/","desktop","firefox" +"mock_site_uuid","2100-01-03 03:52:39","page_hit","1","50785df1-3232-4ff7-8495-d93e06d63f5c","3675e750-09bf-44c9-bc3f-b9aebac37c5d","paid","undefined","FR","my-ghost-site.com","/","https://my-ghost-site.com/","desktop","firefox" +"mock_site_uuid","2100-01-04 00:25:39","page_hit","1","59478d87-ce95-40fd-a081-65d1e497bcfc","97c79891-2ae9-4eb2-ada8-89d2a998747d","paid","6b8635fb-292f-4422-9fe4-d76cfab2ba31","GB","","/blog/hello-world/","https://my-ghost-site.com/blog/hello-world/","desktop","chrome" +"mock_site_uuid","2100-01-04 01:10:48","page_hit","1","a6b6c4e6-19e3-47a9-afc6-d9870592652e","undefined","undefined","6b8635fb-292f-4422-9fe4-d76cfab2ba31","GB","","/blog/hello-world/","https://my-ghost-site.com/blog/hello-world/","desktop","chrome" +"mock_site_uuid","2100-01-04 01:16:10","page_hit","1","a6b6c4e6-19e3-47a9-afc6-d9870592652e","undefined","undefined","06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","GB","my-ghost-site.com","/about/","https://my-ghost-site.com/about/","desktop","chrome" +"mock_site_uuid","2100-01-04 01:20:15","page_hit","1","a6b6c4e6-19e3-47a9-afc6-d9870592652e","undefined","undefined","06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","GB","my-ghost-site.com","/about/","https://my-ghost-site.com/about/","desktop","chrome" +"mock_site_uuid","2100-01-04 01:35:41","page_hit","1","e22a7f6f-28da-4715-a199-6f0338b593d4","5369031a-a5cd-4176-83d8-d6ffcb3bcfb8","free","6b8635fb-292f-4422-9fe4-d76cfab2ba31","GB","","/blog/hello-world/","https://my-ghost-site.com/blog/hello-world/","desktop","chrome" +"mock_site_uuid","2100-01-04 01:36:33","page_hit","1","e22a7f6f-28da-4715-a199-6f0338b593d4","5369031a-a5cd-4176-83d8-d6ffcb3bcfb8","free","undefined","GB","my-ghost-site.com","/","https://my-ghost-site.com/","desktop","chrome" +"mock_site_uuid","2100-01-04 01:54:50","page_hit","1","e22a7f6f-28da-4715-a199-6f0338b593d4","5369031a-a5cd-4176-83d8-d6ffcb3bcfb8","free","06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","GB","my-ghost-site.com","/about/","https://my-ghost-site.com/about/","desktop","chrome" +"mock_site_uuid","2100-01-05 00:29:59","page_hit","1","490475f1-1fb7-4672-9edd-daa1b411b5f9","undefined","undefined","6b8635fb-292f-4422-9fe4-d76cfab2ba31","GB","baidu.com","/blog/hello-world/","https://my-ghost-site.com/blog/hello-world/","desktop","chrome" +"mock_site_uuid","2100-01-05 00:37:42","page_hit","1","490475f1-1fb7-4672-9edd-daa1b411b5f9","undefined","undefined","undefined","GB","my-ghost-site.com","/","https://my-ghost-site.com/","desktop","chrome" +"mock_site_uuid","2100-01-05 00:38:12","page_hit","1","490475f1-1fb7-4672-9edd-daa1b411b5f9","undefined","undefined","6b8635fb-292f-4422-9fe4-d76cfab2ba31","GB","my-ghost-site.com","/blog/hello-world/","https://my-ghost-site.com/blog/hello-world/","desktop","chrome" +"mock_site_uuid","2100-01-05 01:51:00","page_hit","1","d8e4622f-95cc-4fba-b31b-f38ff72e0975","75a190eb-62da-46d2-972d-a9763c954f42","paid","06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","ES","","/about/","https://my-ghost-site.com/about/","desktop","ie" +"mock_site_uuid","2100-01-05 01:53:03","page_hit","1","d8e4622f-95cc-4fba-b31b-f38ff72e0975","75a190eb-62da-46d2-972d-a9763c954f42","paid","6b8635fb-292f-4422-9fe4-d76cfab2ba31","ES","my-ghost-site.com","/blog/hello-world/","https://my-ghost-site.com/blog/hello-world/","desktop","ie" +"mock_site_uuid","2100-01-06 00:51:26","page_hit","1","8d975128-2027-40c6-834a-972cc0293d21","b7e0fca6-27ce-46c0-af57-c591f20dcd51","free","06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","FR","","/about/","https://my-ghost-site.com/about/","desktop","safari" +"mock_site_uuid","2100-01-06 01:28:38","page_hit","1","61a2896b-7cf8-4853-86a6-a0e4f87c1e21","undefined","undefined","6b8635fb-292f-4422-9fe4-d76cfab2ba31","GB","search.yahoo.com","/blog/hello-world/","https://my-ghost-site.com/blog/hello-world/","desktop","chrome" +"mock_site_uuid","2100-01-07 01:44:10","page_hit","1","7f1e88e1-da8e-46df-bc69-d04fb29d603d","undefined","undefined","06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","US","wilted-tick.com","/about/","https://my-ghost-site.com/about/","desktop","firefox" +"mock_site_uuid","2100-01-07 02:23:19","page_hit","1","98159299-8111-4dc8-9156-bb339fe9508c","undefined","undefined","06b1b0c9-fb53-4a15-a060-3db3fde7b1dd","US","my-ghost-site.com","/blog/hello-world/","https://my-ghost-site.com/blog/hello-world/","desktop","firefox" diff --git a/ghost/tinybird/tests/all_kpis.test b/ghost/tinybird/tests/all_kpis.test new file mode 100644 index 00000000000..ed8400dca91 --- /dev/null +++ b/ghost/tinybird/tests/all_kpis.test @@ -0,0 +1 @@ +tb pipe data kpis --date_from 2100-01-01 --date_to 2100-01-07 --site_uuid mock_site_uuid --format CSV diff --git a/ghost/tinybird/tests/all_kpis.test.result b/ghost/tinybird/tests/all_kpis.test.result new file mode 100644 index 00000000000..5d070bb9060 --- /dev/null +++ b/ghost/tinybird/tests/all_kpis.test.result @@ -0,0 +1,8 @@ +"date","visits","pageviews","bounce_rate","avg_session_sec" +"2100-01-01",3,5,0.3333333333333333,1741 +"2100-01-02",1,3,0,1027 +"2100-01-03",3,7,0,9999 +"2100-01-04",3,7,0.3333333333333333,1716 +"2100-01-05",2,5,0,616 +"2100-01-06",2,2,1,0 +"2100-01-07",2,2,1,0 diff --git a/ghost/tinybird/tests/all_top_browsers.test b/ghost/tinybird/tests/all_top_browsers.test new file mode 100644 index 00000000000..2790787c864 --- /dev/null +++ b/ghost/tinybird/tests/all_top_browsers.test @@ -0,0 +1 @@ + tb pipe data top_browsers --date_from 2100-01-01 --date_to 2100-01-07 --site_uuid mock_site_uuid --format CSV diff --git a/ghost/tinybird/tests/all_top_browsers.test.result b/ghost/tinybird/tests/all_top_browsers.test.result new file mode 100644 index 00000000000..a3d06413d1f --- /dev/null +++ b/ghost/tinybird/tests/all_top_browsers.test.result @@ -0,0 +1,6 @@ +"browser","visits","pageviews" +"chrome",7,16 +"firefox",5,9 +"ie",2,4 +"safari",1,1 +"Unknown",1,1 diff --git a/ghost/tinybird/tests/all_top_devices.test b/ghost/tinybird/tests/all_top_devices.test new file mode 100644 index 00000000000..0741ac06598 --- /dev/null +++ b/ghost/tinybird/tests/all_top_devices.test @@ -0,0 +1,2 @@ + + tb pipe data top_devices --date_from 2100-01-01 --date_to 2100-01-07 --site_uuid mock_site_uuid --format CSV diff --git a/ghost/tinybird/tests/all_top_devices.test.result b/ghost/tinybird/tests/all_top_devices.test.result new file mode 100644 index 00000000000..1eb5964e737 --- /dev/null +++ b/ghost/tinybird/tests/all_top_devices.test.result @@ -0,0 +1,3 @@ +"device","visits","pageviews" +"desktop",15,30 +"bot",1,1 diff --git a/ghost/tinybird/tests/all_top_locations.test b/ghost/tinybird/tests/all_top_locations.test new file mode 100644 index 00000000000..ba0667d3037 --- /dev/null +++ b/ghost/tinybird/tests/all_top_locations.test @@ -0,0 +1,2 @@ + + tb pipe data top_locations --date_from 2100-01-01 --date_to 2100-01-07 --site_uuid mock_site_uuid --format CSV diff --git a/ghost/tinybird/tests/all_top_locations.test.result b/ghost/tinybird/tests/all_top_locations.test.result new file mode 100644 index 00000000000..de9e641f9c4 --- /dev/null +++ b/ghost/tinybird/tests/all_top_locations.test.result @@ -0,0 +1,6 @@ +"location","visits","pageviews" +"GB",8,17 +"US",3,4 +"FR",2,3 +"ES",2,4 +"DE",1,3 diff --git a/ghost/tinybird/tests/all_top_pages.test b/ghost/tinybird/tests/all_top_pages.test new file mode 100644 index 00000000000..2fbd8b270e2 --- /dev/null +++ b/ghost/tinybird/tests/all_top_pages.test @@ -0,0 +1 @@ +tb pipe data top_pages --date_from 2100-01-01 --date_to 2100-01-07 --site_uuid mock_site_uuid --format CSV diff --git a/ghost/tinybird/tests/all_top_pages.test.result b/ghost/tinybird/tests/all_top_pages.test.result new file mode 100644 index 00000000000..f4f53cf1f1d --- /dev/null +++ b/ghost/tinybird/tests/all_top_pages.test.result @@ -0,0 +1,4 @@ +"pathname","visits","pageviews" +"/blog/hello-world/",10,11 +"/about/",8,10 +"/",7,10 diff --git a/ghost/tinybird/tests/all_top_sources.test b/ghost/tinybird/tests/all_top_sources.test new file mode 100644 index 00000000000..bc2f422fa68 --- /dev/null +++ b/ghost/tinybird/tests/all_top_sources.test @@ -0,0 +1 @@ +tb pipe data top_sources --date_from 2100-01-01 --date_to 2100-01-07 --site_uuid mock_site_uuid --format CSV diff --git a/ghost/tinybird/tests/all_top_sources.test.result b/ghost/tinybird/tests/all_top_sources.test.result new file mode 100644 index 00000000000..b05e68fc2f8 --- /dev/null +++ b/ghost/tinybird/tests/all_top_sources.test.result @@ -0,0 +1,9 @@ +"source","visits","pageviews" +"",6,12 +"bing.com",2,5 +"search.yahoo.com",2,3 +"google.com",1,3 +"baidu.com",1,3 +"wilted-tick.com",1,1 +"duckduckgo.com",1,2 +"petty-queen.com",1,1 diff --git a/ghost/tinybird/tests/filter_browser_chrome_kpis.test b/ghost/tinybird/tests/filter_browser_chrome_kpis.test new file mode 100644 index 00000000000..24a90ac25b9 --- /dev/null +++ b/ghost/tinybird/tests/filter_browser_chrome_kpis.test @@ -0,0 +1 @@ +tb pipe data kpis --date_from 2100-01-01 --date_to 2100-01-07 --site_uuid mock_site_uuid --format CSV --browser chrome diff --git a/ghost/tinybird/tests/filter_browser_chrome_kpis.test.result b/ghost/tinybird/tests/filter_browser_chrome_kpis.test.result new file mode 100644 index 00000000000..8f60138e233 --- /dev/null +++ b/ghost/tinybird/tests/filter_browser_chrome_kpis.test.result @@ -0,0 +1,7 @@ +"date","visits","pageviews","bounce_rate","avg_session_sec" +"2100-01-01",1,2,0,1111 +"2100-01-02",0,0,0,0 +"2100-01-03",1,3,0,1115 +"2100-01-04",3,7,0.3333333333333333,1716 +"2100-01-05",1,3,0,493 +"2100-01-06",1,1,1,0 diff --git a/ghost/tinybird/tests/filter_browser_chrome_top_browsers.test b/ghost/tinybird/tests/filter_browser_chrome_top_browsers.test new file mode 100644 index 00000000000..7ee4499fe7f --- /dev/null +++ b/ghost/tinybird/tests/filter_browser_chrome_top_browsers.test @@ -0,0 +1 @@ +tb pipe data top_browsers --date_from 2100-01-01 --date_to 2100-01-07 --site_uuid mock_site_uuid --format CSV --browser chrome diff --git a/ghost/tinybird/tests/filter_browser_chrome_top_browsers.test.result b/ghost/tinybird/tests/filter_browser_chrome_top_browsers.test.result new file mode 100644 index 00000000000..13c1d7db0d2 --- /dev/null +++ b/ghost/tinybird/tests/filter_browser_chrome_top_browsers.test.result @@ -0,0 +1,2 @@ +"browser","visits","pageviews" +"chrome",7,16 diff --git a/ghost/tinybird/tests/filter_browser_chrome_top_pages.test b/ghost/tinybird/tests/filter_browser_chrome_top_pages.test new file mode 100644 index 00000000000..ea53c29c55c --- /dev/null +++ b/ghost/tinybird/tests/filter_browser_chrome_top_pages.test @@ -0,0 +1 @@ +tb pipe data top_pages --date_from 2100-01-01 --date_to 2100-01-07 --site_uuid mock_site_uuid --format CSV --browser chrome diff --git a/ghost/tinybird/tests/filter_browser_chrome_top_pages.test.result b/ghost/tinybird/tests/filter_browser_chrome_top_pages.test.result new file mode 100644 index 00000000000..56b11a7f0e5 --- /dev/null +++ b/ghost/tinybird/tests/filter_browser_chrome_top_pages.test.result @@ -0,0 +1,4 @@ +"pathname","visits","pageviews" +"/blog/hello-world/",6,7 +"/about/",3,5 +"/",3,4 diff --git a/ghost/tinybird/tests/filter_browser_chrome_top_sources.test b/ghost/tinybird/tests/filter_browser_chrome_top_sources.test new file mode 100644 index 00000000000..791f4c108a9 --- /dev/null +++ b/ghost/tinybird/tests/filter_browser_chrome_top_sources.test @@ -0,0 +1 @@ +tb pipe data top_sources --date_from 2100-01-01 --date_to 2100-01-07 --site_uuid mock_site_uuid --format CSV --browser chrome diff --git a/ghost/tinybird/tests/filter_browser_chrome_top_sources.test.result b/ghost/tinybird/tests/filter_browser_chrome_top_sources.test.result new file mode 100644 index 00000000000..943b6e97cd7 --- /dev/null +++ b/ghost/tinybird/tests/filter_browser_chrome_top_sources.test.result @@ -0,0 +1,5 @@ +"source","visits","pageviews" +"",4,9 +"bing.com",1,3 +"search.yahoo.com",1,1 +"baidu.com",1,3 diff --git a/ghost/tinybird/tests/filter_source_bing_kpis.test b/ghost/tinybird/tests/filter_source_bing_kpis.test new file mode 100644 index 00000000000..823fcdc1cac --- /dev/null +++ b/ghost/tinybird/tests/filter_source_bing_kpis.test @@ -0,0 +1 @@ +tb pipe data kpis --date_from 2100-01-01 --date_to 2100-01-07 --site_uuid mock_site_uuid --format CSV --source bing.com diff --git a/ghost/tinybird/tests/filter_source_bing_kpis.test.result b/ghost/tinybird/tests/filter_source_bing_kpis.test.result new file mode 100644 index 00000000000..1cf76c977a0 --- /dev/null +++ b/ghost/tinybird/tests/filter_source_bing_kpis.test.result @@ -0,0 +1,4 @@ +"date","visits","pageviews","bounce_rate","avg_session_sec" +"2100-01-01",1,2,0,630 +"2100-01-02",0,0,0,0 +"2100-01-03",1,3,0,1115 diff --git a/ghost/tinybird/tests/filter_source_bing_top_browsers.test b/ghost/tinybird/tests/filter_source_bing_top_browsers.test new file mode 100644 index 00000000000..4761e986dd5 --- /dev/null +++ b/ghost/tinybird/tests/filter_source_bing_top_browsers.test @@ -0,0 +1 @@ +tb pipe data top_browsers --date_from 2100-01-01 --date_to 2100-01-07 --site_uuid mock_site_uuid --format CSV --source bing.com diff --git a/ghost/tinybird/tests/filter_source_bing_top_browsers.test.result b/ghost/tinybird/tests/filter_source_bing_top_browsers.test.result new file mode 100644 index 00000000000..1c640403f72 --- /dev/null +++ b/ghost/tinybird/tests/filter_source_bing_top_browsers.test.result @@ -0,0 +1,3 @@ +"browser","visits","pageviews" +"chrome",1,3 +"ie",1,2 diff --git a/ghost/tinybird/tests/filter_source_bing_top_pages.test b/ghost/tinybird/tests/filter_source_bing_top_pages.test new file mode 100644 index 00000000000..434a2b2f5e2 --- /dev/null +++ b/ghost/tinybird/tests/filter_source_bing_top_pages.test @@ -0,0 +1 @@ +tb pipe data top_pages --date_from 2100-01-01 --date_to 2100-01-07 --site_uuid mock_site_uuid --format CSV --source bing.com diff --git a/ghost/tinybird/tests/filter_source_bing_top_pages.test.result b/ghost/tinybird/tests/filter_source_bing_top_pages.test.result new file mode 100644 index 00000000000..c72c245a2ad --- /dev/null +++ b/ghost/tinybird/tests/filter_source_bing_top_pages.test.result @@ -0,0 +1,2 @@ +"pathname","visits","pageviews" +"/about/",2,2 diff --git a/ghost/tinybird/tests/filter_source_bing_top_sources.test b/ghost/tinybird/tests/filter_source_bing_top_sources.test new file mode 100644 index 00000000000..9bd58bade57 --- /dev/null +++ b/ghost/tinybird/tests/filter_source_bing_top_sources.test @@ -0,0 +1 @@ +tb pipe data top_sources --date_from 2100-01-01 --date_to 2100-01-07 --site_uuid mock_site_uuid --format CSV --source bing.com diff --git a/ghost/tinybird/tests/filter_source_bing_top_sources.test.result b/ghost/tinybird/tests/filter_source_bing_top_sources.test.result new file mode 100644 index 00000000000..ec66e37fe51 --- /dev/null +++ b/ghost/tinybird/tests/filter_source_bing_top_sources.test.result @@ -0,0 +1,2 @@ +"source","visits","pageviews" +"bing.com",2,5 diff --git a/nx.json b/nx.json index 80110ed5b9a..3e700d12e36 100644 --- a/nx.json +++ b/nx.json @@ -1,33 +1,16 @@ { - "tasksRunnerOptions": { - "default": { - "runner": "nx/tasks-runners/default", - "options": { - "cacheableOperations": [ - "build", - "build:ts", - "lint", - "test", - "test:unit" - ], - "useDaemonProcess": false, - "cacheDirectory": ".nxcache" - } - } - }, + "$schema": "./node_modules/nx/schemas/nx-schema.json", "namedInputs": { - "default": [ - "{projectRoot}/**/*", - "{workspaceRoot}/ghost/tsconfig.json" - ] + "default": ["{projectRoot}/**/*", "{workspaceRoot}/ghost/tsconfig.json"] }, + "parallel": 4, "targetDefaults": { "build": { - "dependsOn": [ - "^build" - ], + "dependsOn": ["^build"], "inputs": [ - { "env": "GHOST_CDN_URL" }, + { + "env": "GHOST_CDN_URL" + }, "default", "^default" ], @@ -36,20 +19,24 @@ "{projectRoot}/es", "{projectRoot}/types", "{projectRoot}/umd" - ] + ], + "cache": true }, "build:ts": { - "dependsOn": [ - "^build:ts" - ], - "inputs": [ - "default", - "^default" - ], - "outputs": [ - "{projectRoot}/build" - ] + "dependsOn": ["^build:ts"], + "inputs": ["default", "^default"], + "outputs": ["{projectRoot}/build"], + "cache": true + }, + "lint": { + "cache": true + }, + "test": { + "cache": true + }, + "test:unit": { + "cache": true } }, - "$schema": "./node_modules/nx/schemas/nx-schema.json" + "cacheDirectory": ".nxcache" } diff --git a/package.json b/package.json index b20c4e289ee..84aa352c51b 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,8 @@ "main:monorepo": "git checkout main && git pull ${GHOST_UPSTREAM:-origin} main && yarn", "main:submodules": "git submodule sync && git submodule update && git submodule foreach \"git checkout main && git pull ${GHOST_UPSTREAM:-origin} main\"", "prepare": "husky install .github/hooks", - "tb": "docker run --rm -v $(pwd):/ghost -w /ghost/ghost/tinybird -it tinybirdco/tinybird-cli-docker" + "tb": "docker run --rm -v $(pwd):/ghost -w /ghost/ghost/tinybird -it tinybirdco/tinybird-cli-docker", + "tb:update": "docker pull tinybirdco/tinybird-cli-docker" }, "resolutions": { "@tryghost/errors": "1.3.5", @@ -111,17 +112,18 @@ "*.js": "eslint" }, "devDependencies": { + "@actions/core": "1.10.1", "chalk": "4.1.2", "concurrently": "8.2.2", "eslint": "8.44.0", "eslint-plugin-ghost": "3.4.0", "eslint-plugin-react": "7.33.0", "husky": "8.0.3", + "inquirer": "8.2.6", "lint-staged": "15.2.10", - "nx": "16.8.1", + "nx": "19.8.3", "rimraf": "5.0.10", "ts-node": "10.9.2", - "typescript": "5.4.5", - "inquirer": "8.2.6" + "typescript": "5.4.5" } } diff --git a/yarn.lock b/yarn.lock index 2e57f447586..4ad2176514f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2694,6 +2694,28 @@ broccoli-funnel "^3.0.5" ember-cli-babel "^7.23.1" +"@emnapi/core@^1.1.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.2.0.tgz#7b738e5033738132bf6af0b8fae7b05249bdcbd7" + integrity sha512-E7Vgw78I93we4ZWdYCb4DGAwRROGkMIXk7/y87UmANR+J6qsWusmC3gLt0H+O0KOt5e6O38U8oJamgbudrES/w== + dependencies: + "@emnapi/wasi-threads" "1.0.1" + tslib "^2.4.0" + +"@emnapi/runtime@^1.1.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.2.0.tgz#71d018546c3a91f3b51106530edbc056b9f2f2e3" + integrity sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ== + dependencies: + tslib "^2.4.0" + +"@emnapi/wasi-threads@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@emnapi/wasi-threads/-/wasi-threads-1.0.1.tgz#d7ae71fd2166b1c916c6cd2d0df2ef565a2e1a5b" + integrity sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw== + dependencies: + tslib "^2.4.0" + "@emotion/babel-plugin@^11.11.0": version "11.11.0" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c" @@ -3281,24 +3303,6 @@ "@glimmer/interfaces" "^0.42.2" "@glimmer/util" "^0.42.2" -"@grpc/grpc-js@^1.7.1": - version "1.10.7" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.10.7.tgz#1abce1a8c4c90b79dbbe57d7e4310f3b0ce72899" - integrity sha512-ZMBVjSeDAz3tFSehyO6Pd08xZT1HfIwq3opbeM4cDlBh52gmwp0wVIPcQur53NN0ac68HMZ/7SF2rGRD5KmVmg== - dependencies: - "@grpc/proto-loader" "^0.7.13" - "@js-sdsl/ordered-map" "^4.4.2" - -"@grpc/proto-loader@^0.7.13": - version "0.7.13" - resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.7.13.tgz#f6a44b2b7c9f7b609f5748c6eac2d420e37670cf" - integrity sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw== - dependencies: - lodash.camelcase "^4.3.0" - long "^5.0.0" - protobufjs "^7.2.5" - yargs "^17.7.2" - "@gulpjs/to-absolute-glob@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz#1fc2460d3953e1d9b9f2dfdb4bcc99da4710c021" @@ -3706,11 +3710,6 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@js-sdsl/ordered-map@^4.4.2": - version "4.4.2" - resolved "https://registry.yarnpkg.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz#9299f82874bab9e4c7f9c48d865becbfe8d6907c" - integrity sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw== - "@juggle/resize-observer@^3.3.1": version "3.4.0" resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60" @@ -3911,6 +3910,15 @@ resolved "https://registry.yarnpkg.com/@miragejs/pretender-node-polyfill/-/pretender-node-polyfill-0.1.2.tgz#d26b6b7483fb70cd62189d05c95d2f67153e43f2" integrity sha512-M/BexG/p05C5lFfMunxo/QcgIJnMT2vDVCd00wNqK2ImZONIlEETZwWJu1QtLxtmYlSHlCFl3JNzp0tLe7OJ5g== +"@napi-rs/wasm-runtime@0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.4.tgz#d27788176f250d86e498081e3c5ff48a17606918" + integrity sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ== + dependencies: + "@emnapi/core" "^1.1.0" + "@emnapi/runtime" "^1.1.0" + "@tybys/wasm-util" "^0.9.0" + "@ndelangen/get-tarball@^3.0.7": version "3.0.7" resolved "https://registry.yarnpkg.com/@ndelangen/get-tarball/-/get-tarball-3.0.7.tgz#87c7aef2df4ff4fbdbab6ac9ed32cee142c4b1a3" @@ -4021,12 +4029,12 @@ mkdirp "^1.0.4" rimraf "^3.0.2" -"@nrwl/tao@16.8.1": - version "16.8.1" - resolved "https://registry.yarnpkg.com/@nrwl/tao/-/tao-16.8.1.tgz#640522eef8905f358ce087e1f6a8489c69e3ebfb" - integrity sha512-hgGFLyEgONSofxnJsXN9NlUx4J8/YSLUkfZKdR8Qa97+JGZT8FEuk7NLFJOWdYYqROoCzXLHK0d+twFFNPS5BQ== +"@nrwl/tao@19.8.3": + version "19.8.3" + resolved "https://registry.yarnpkg.com/@nrwl/tao/-/tao-19.8.3.tgz#2e5ee5121705585cbeda43989d4c35253b90a05d" + integrity sha512-byjBtOXx+xGjMu1wKopJSJbrR3gKqTsCEgp1+YSZ45+iFKxFdXLJrGsyhVqBovCKVBM+5/KtGuEkZoUPlP8JWg== dependencies: - nx "16.8.1" + nx "19.8.3" tslib "^2.3.0" "@nuxtjs/opencollective@0.3.2": @@ -4038,276 +4046,61 @@ consola "^2.15.0" node-fetch "^2.6.1" -"@nx/nx-darwin-arm64@16.8.1": - version "16.8.1" - resolved "https://registry.yarnpkg.com/@nx/nx-darwin-arm64/-/nx-darwin-arm64-16.8.1.tgz#fd85ed007d63d232700272cd07138ecac046525d" - integrity sha512-xOflqyIVcyLPzdJOZcucI+5ClwnTgK8zIvpjbxHokrO9McJJglhfUyP0bbTHpEpWqzA+GaPA/6/Qdu0ATzqQBQ== - -"@nx/nx-darwin-x64@16.8.1": - version "16.8.1" - resolved "https://registry.yarnpkg.com/@nx/nx-darwin-x64/-/nx-darwin-x64-16.8.1.tgz#de75b5052cb7ec93e238af632f0ea0f2d8822e66" - integrity sha512-JJGrlOvEpDMWnM6YKaA1WOnzHgiw5vRKEowX9ba+jxhmCvtdjbLSxi228kv92JtQPPQ91zvtsNM+BFY0EbPOlA== - -"@nx/nx-freebsd-x64@16.8.1": - version "16.8.1" - resolved "https://registry.yarnpkg.com/@nx/nx-freebsd-x64/-/nx-freebsd-x64-16.8.1.tgz#733dbe731af814b87a1429d7c087c4879192536c" - integrity sha512-aZdJQ7cIQfXOmfk4vRXvVYxuV68xz8YyhNZ0IvBfJ16uZQ+YNl4BpklRLEIdaloSbwz9M1NNewmL+AgklEBxlA== - -"@nx/nx-linux-arm-gnueabihf@16.8.1": - version "16.8.1" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-16.8.1.tgz#3d1e2130d26ecc335df21bf8a8afa566bd6b4ed5" - integrity sha512-JzjrTf7FFgikoVUbRs0hKvwHRR6SyqT4yIdk/YyiCt2mWY9w4m5DWtHM/9kJzhckkH9MY66m+X/zG6+NKsEMvg== - -"@nx/nx-linux-arm64-gnu@16.8.1": - version "16.8.1" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-16.8.1.tgz#f0f96dc4be17cac8a387367eaafe71a6b1948fc3" - integrity sha512-CF0s981myBWusW7iW2+fKPa7ceYYe+NO5EdKe9l27fpHDkcA71KZU3q7U823QpO/7tYvVdBevJp3CCn2/GBURQ== - -"@nx/nx-linux-arm64-musl@16.8.1": - version "16.8.1" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-16.8.1.tgz#65ed581e702ef882afd9d7f25b660e34e4c13690" - integrity sha512-X4TobxRt1dALvoeKC3/t1CqZCMUqtEhGG+KQLT/51sG54HdxmTAWRFlvj8PvLH0QSBk4e+uRZAo45qpt3iSnBg== - -"@nx/nx-linux-x64-gnu@16.8.1": - version "16.8.1" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-16.8.1.tgz#cc5c782a67e5b17f4e395d358d87ea5076606dba" - integrity sha512-lHvv2FD14Lpxh7muMLStH2tC1opQOaepO4nXwb1LaaoIpMym7kBgCK8AQuI98/oNQiMDXMNDKWQZCjxnJGDIPw== - -"@nx/nx-linux-x64-musl@16.8.1": - version "16.8.1" - resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-16.8.1.tgz#2837fb7d6590b5fe9f2eb42603d0e064771a8ded" - integrity sha512-c4gQvNgIjggD1A5sYhftQEC1PtAhV3sEnv60X00v9wmjl57Wj4Ty0TgyzpYglLysVRiko/B58S8NYS0jKvMmeA== - -"@nx/nx-win32-arm64-msvc@16.8.1": - version "16.8.1" - resolved "https://registry.yarnpkg.com/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-16.8.1.tgz#d45d8abdd99f3b0dda83a673592299ffdc819895" - integrity sha512-GKHPy/MyGFoV9cdKgcWLZZK2vDdxt5bQ53ss0k+BDKRP+YwLKm7tJl23eeM7JdB4GLCBntEQPC+dBqxOA8Ze/w== - -"@nx/nx-win32-x64-msvc@16.8.1": - version "16.8.1" - resolved "https://registry.yarnpkg.com/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-16.8.1.tgz#6ec1930aaf4d9dea19149d6b3d100b2c7e69d582" - integrity sha512-yHZ5FAcx54rVc31R0yIpniepkHMPwaxG23l8E/ZYbL1iPwE/Wc1HeUzUvxUuSXtguRp7ihcRhaUEPkcSl2EAVw== - -"@opentelemetry/api-logs@0.52.1": - version "0.52.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz#52906375da4d64c206b0c4cb8ffa209214654ecc" - integrity sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A== - dependencies: - "@opentelemetry/api" "^1.0.0" - -"@opentelemetry/api@1.9.0", "@opentelemetry/api@^1.0.0": +"@nx/nx-darwin-arm64@19.8.3": + version "19.8.3" + resolved "https://registry.yarnpkg.com/@nx/nx-darwin-arm64/-/nx-darwin-arm64-19.8.3.tgz#3c1ba34fa043fe30ab7d471afe9d3f8b87bfc337" + integrity sha512-ORHFFWMZcvFi0xcpCaXccXVEhFwAevSHOIKfW359+12H9w7VW2O42B+2NcVMK1mrDTOjlXTd+0AmAu7P4NzWFA== + +"@nx/nx-darwin-x64@19.8.3": + version "19.8.3" + resolved "https://registry.yarnpkg.com/@nx/nx-darwin-x64/-/nx-darwin-x64-19.8.3.tgz#3213e4defdf309b5a550903dc20847e548ac2f7c" + integrity sha512-Ji9DPA0tuzygMcypD/FHRDQSPipcRqMNmSaNKxVpcCbozVTWHvqXFk0rloDIUnxnE0+zvE9LN71H2sS4ZHdTQA== + +"@nx/nx-freebsd-x64@19.8.3": + version "19.8.3" + resolved "https://registry.yarnpkg.com/@nx/nx-freebsd-x64/-/nx-freebsd-x64-19.8.3.tgz#5b9ef272b9c47c471b2002dff945cd240f76deb0" + integrity sha512-Ys+PqtBZCS+QBNs7he3fnxVhMWz/lSSaBVUlVHoQcV1Y4clEpP2TWNQSsbaVnnpcB7pdmKN5ymWdaCaAQuqCMw== + +"@nx/nx-linux-arm-gnueabihf@19.8.3": + version "19.8.3" + resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm-gnueabihf/-/nx-linux-arm-gnueabihf-19.8.3.tgz#867717f09d25e3679af2ec88ca980f533bc83a77" + integrity sha512-hGOlML60ELXkgkqLHB/w/sXbTbXFhOQGSXC72CjaP5G0u1gj8eTQKJ7WEsqPAFMk5SLFFxqM7eid0LmAYYuZWQ== + +"@nx/nx-linux-arm64-gnu@19.8.3": + version "19.8.3" + resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-gnu/-/nx-linux-arm64-gnu-19.8.3.tgz#7b58aa4d57eb0c3131038e268145c7eac581bd4d" + integrity sha512-K/5iVbLbhsx28YtZHvveJgF41rbr2kMdabooZeFqy6VReN7U/zGJMjpV1FzDlf3TNr9jyjPDZgVQRS+qXau2qA== + +"@nx/nx-linux-arm64-musl@19.8.3": + version "19.8.3" + resolved "https://registry.yarnpkg.com/@nx/nx-linux-arm64-musl/-/nx-linux-arm64-musl-19.8.3.tgz#896cb10d65dc3f0ccbedea640bbe2a7038c160cc" + integrity sha512-zqzWjFniZDXiI/3MYxbJ0yIenUKr56apLy70oABTBHx++dsUA3/DxLMNypMA82a8KQtsbePWUi3Pgtr+JIMNXw== + +"@nx/nx-linux-x64-gnu@19.8.3": + version "19.8.3" + resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-gnu/-/nx-linux-x64-gnu-19.8.3.tgz#2e3d132e333721816b879fb77f9733c3f6b1e53e" + integrity sha512-W1RRCqsQvpur4BxP5g5cQwjZB6jhxYLSSXi3QQDaU5ITkaV5Pdj/L7D/G6YgRB8lzKZrXc57aLJ5UKY/Z+di7w== + +"@nx/nx-linux-x64-musl@19.8.3": + version "19.8.3" + resolved "https://registry.yarnpkg.com/@nx/nx-linux-x64-musl/-/nx-linux-x64-musl-19.8.3.tgz#6127d9d0ed5331ca935c2e2c4c2a1d4ce335016d" + integrity sha512-waTo0zBBGnmU7fS87IpOnVGx7EHa0umzSMlGG0LUoU6swOeNODezsBn1Vbvaw1o7sStWBzdEBlxLxHOQXRAidg== + +"@nx/nx-win32-arm64-msvc@19.8.3": + version "19.8.3" + resolved "https://registry.yarnpkg.com/@nx/nx-win32-arm64-msvc/-/nx-win32-arm64-msvc-19.8.3.tgz#90f6330316f8762c08b8361c90d170794a07b0e8" + integrity sha512-lio7ulblEMs1otMtVIrdfdMTBqKRZEHim57AcMHSVnwmtl2ENP6TR3YIgyigjfLlkPanNU7i0QQ4h6Nk2I/FRw== + +"@nx/nx-win32-x64-msvc@19.8.3": + version "19.8.3" + resolved "https://registry.yarnpkg.com/@nx/nx-win32-x64-msvc/-/nx-win32-x64-msvc-19.8.3.tgz#49e89ce492cddbe5b8441c79b1f158d2c2452a30" + integrity sha512-RU11iXJzdrw5CmogT2AwsjxK7g8vWf6Oy23NlrvsQFODtavjqAWoD5qpUY/H16s9lVDwrpzCbGbAXph0lbgLKA== + +"@opentelemetry/api@^1.4.0": version "1.9.0" resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.9.0.tgz#d03eba68273dc0f7509e2a3d5cba21eae10379fe" integrity sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg== -"@opentelemetry/context-async-hooks@1.25.1": - version "1.25.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/context-async-hooks/-/context-async-hooks-1.25.1.tgz#810bff2fcab84ec51f4684aff2d21f6c057d9e73" - integrity sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ== - -"@opentelemetry/core@1.25.1": - version "1.25.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.25.1.tgz#ff667d939d128adfc7c793edae2f6bca177f829d" - integrity sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ== - dependencies: - "@opentelemetry/semantic-conventions" "1.25.1" - -"@opentelemetry/exporter-prometheus@0.52.1": - version "0.52.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.52.1.tgz#615021fffd511d4424edf9347b119d814cc94e89" - integrity sha512-hwK0QnjtqAxGpQAXMNUY+kTT5CnHyz1I0lBA8SFySvaFtExZm7yQg/Ua/i+RBqgun7WkUbkUVJzEi3lKpJ7WdA== - dependencies: - "@opentelemetry/core" "1.25.1" - "@opentelemetry/resources" "1.25.1" - "@opentelemetry/sdk-metrics" "1.25.1" - -"@opentelemetry/exporter-trace-otlp-grpc@0.52.1": - version "0.52.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.52.1.tgz#8b59c93a5833484ba19a7f424632c6ced5ea1d3b" - integrity sha512-pVkSH20crBwMTqB3nIN4jpQKUEoB0Z94drIHpYyEqs7UBr+I0cpYyOR3bqjA/UasQUMROb3GX8ZX4/9cVRqGBQ== - dependencies: - "@grpc/grpc-js" "^1.7.1" - "@opentelemetry/core" "1.25.1" - "@opentelemetry/otlp-grpc-exporter-base" "0.52.1" - "@opentelemetry/otlp-transformer" "0.52.1" - "@opentelemetry/resources" "1.25.1" - "@opentelemetry/sdk-trace-base" "1.25.1" - -"@opentelemetry/exporter-trace-otlp-http@0.52.1": - version "0.52.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.52.1.tgz#99549e05f581050d0df2c1c684d1a819c480ebc6" - integrity sha512-05HcNizx0BxcFKKnS5rwOV+2GevLTVIRA0tRgWYyw4yCgR53Ic/xk83toYKts7kbzcI+dswInUg/4s8oyA+tqg== - dependencies: - "@opentelemetry/core" "1.25.1" - "@opentelemetry/otlp-exporter-base" "0.52.1" - "@opentelemetry/otlp-transformer" "0.52.1" - "@opentelemetry/resources" "1.25.1" - "@opentelemetry/sdk-trace-base" "1.25.1" - -"@opentelemetry/exporter-trace-otlp-proto@0.52.1": - version "0.52.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.52.1.tgz#7b68268cd4d46b7d89ee7c97720031ca80919fd6" - integrity sha512-pt6uX0noTQReHXNeEslQv7x311/F1gJzMnp1HD2qgypLRPbXDeMzzeTngRTUaUbP6hqWNtPxuLr4DEoZG+TcEQ== - dependencies: - "@opentelemetry/core" "1.25.1" - "@opentelemetry/otlp-exporter-base" "0.52.1" - "@opentelemetry/otlp-transformer" "0.52.1" - "@opentelemetry/resources" "1.25.1" - "@opentelemetry/sdk-trace-base" "1.25.1" - -"@opentelemetry/exporter-zipkin@1.25.1": - version "1.25.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.25.1.tgz#81bb3b3aa16500676277c2fd6d50159eaf6c081a" - integrity sha512-RmOwSvkimg7ETwJbUOPTMhJm9A9bG1U8s7Zo3ajDh4zM7eYcycQ0dM7FbLD6NXWbI2yj7UY4q8BKinKYBQksyw== - dependencies: - "@opentelemetry/core" "1.25.1" - "@opentelemetry/resources" "1.25.1" - "@opentelemetry/sdk-trace-base" "1.25.1" - "@opentelemetry/semantic-conventions" "1.25.1" - -"@opentelemetry/instrumentation-runtime-node@0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation-runtime-node/-/instrumentation-runtime-node-0.6.0.tgz#6d547f42222c54987411404cae06912a08d94575" - integrity sha512-sbDGvUFL3pGwiaSazQ9v259uVHu3WtApMeo8vMMEKHb+z1IVN6/prJffCBs4OA+4skmhf3iFTV531mVwtwWbTA== - dependencies: - "@opentelemetry/instrumentation" "^0.52.0" - -"@opentelemetry/instrumentation@0.52.1", "@opentelemetry/instrumentation@^0.52.0": - version "0.52.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz#2e7e46a38bd7afbf03cf688c862b0b43418b7f48" - integrity sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw== - dependencies: - "@opentelemetry/api-logs" "0.52.1" - "@types/shimmer" "^1.0.2" - import-in-the-middle "^1.8.1" - require-in-the-middle "^7.1.1" - semver "^7.5.2" - shimmer "^1.2.1" - -"@opentelemetry/otlp-exporter-base@0.52.1": - version "0.52.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.52.1.tgz#657d9b27c55bd42ab6a8bb181006b57f9c84b91d" - integrity sha512-z175NXOtX5ihdlshtYBe5RpGeBoTXVCKPPLiQlD6FHvpM4Ch+p2B0yWKYSrBfLH24H9zjJiBdTrtD+hLlfnXEQ== - dependencies: - "@opentelemetry/core" "1.25.1" - "@opentelemetry/otlp-transformer" "0.52.1" - -"@opentelemetry/otlp-grpc-exporter-base@0.52.1": - version "0.52.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.52.1.tgz#e1fdfd979289a87faec1c7cf303100c75e49a284" - integrity sha512-zo/YrSDmKMjG+vPeA9aBBrsQM9Q/f2zo6N04WMB3yNldJRsgpRBeLLwvAt/Ba7dpehDLOEFBd1i2JCoaFtpCoQ== - dependencies: - "@grpc/grpc-js" "^1.7.1" - "@opentelemetry/core" "1.25.1" - "@opentelemetry/otlp-exporter-base" "0.52.1" - "@opentelemetry/otlp-transformer" "0.52.1" - -"@opentelemetry/otlp-transformer@0.52.1": - version "0.52.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-transformer/-/otlp-transformer-0.52.1.tgz#779b7ebf0e3791eebeaa64caff06914fe3577948" - integrity sha512-I88uCZSZZtVa0XniRqQWKbjAUm73I8tpEy/uJYPPYw5d7BRdVk0RfTBQw8kSUl01oVWEuqxLDa802222MYyWHg== - dependencies: - "@opentelemetry/api-logs" "0.52.1" - "@opentelemetry/core" "1.25.1" - "@opentelemetry/resources" "1.25.1" - "@opentelemetry/sdk-logs" "0.52.1" - "@opentelemetry/sdk-metrics" "1.25.1" - "@opentelemetry/sdk-trace-base" "1.25.1" - protobufjs "^7.3.0" - -"@opentelemetry/propagator-b3@1.25.1": - version "1.25.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-b3/-/propagator-b3-1.25.1.tgz#653ee5f3f0f223c000907c1559c89c0a208819f7" - integrity sha512-p6HFscpjrv7//kE+7L+3Vn00VEDUJB0n6ZrjkTYHrJ58QZ8B3ajSJhRbCcY6guQ3PDjTbxWklyvIN2ojVbIb1A== - dependencies: - "@opentelemetry/core" "1.25.1" - -"@opentelemetry/propagator-jaeger@1.25.1": - version "1.25.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.25.1.tgz#7eae165921e65dce6f8d87339379880125dab765" - integrity sha512-nBprRf0+jlgxks78G/xq72PipVK+4or9Ypntw0gVZYNTCSK8rg5SeaGV19tV920CMqBD/9UIOiFr23Li/Q8tiA== - dependencies: - "@opentelemetry/core" "1.25.1" - -"@opentelemetry/resources@1.25.1": - version "1.25.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.25.1.tgz#bb9a674af25a1a6c30840b755bc69da2796fefbb" - integrity sha512-pkZT+iFYIZsVn6+GzM0kSX+u3MSLCY9md+lIJOoKl/P+gJFfxJte/60Usdp8Ce4rOs8GduUpSPNe1ddGyDT1sQ== - dependencies: - "@opentelemetry/core" "1.25.1" - "@opentelemetry/semantic-conventions" "1.25.1" - -"@opentelemetry/sdk-logs@0.52.1": - version "0.52.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-logs/-/sdk-logs-0.52.1.tgz#5653faa2d81cae574729bdeb4298b95dc10ae736" - integrity sha512-MBYh+WcPPsN8YpRHRmK1Hsca9pVlyyKd4BxOC4SsgHACnl/bPp4Cri9hWhVm5+2tiQ9Zf4qSc1Jshw9tOLGWQA== - dependencies: - "@opentelemetry/api-logs" "0.52.1" - "@opentelemetry/core" "1.25.1" - "@opentelemetry/resources" "1.25.1" - -"@opentelemetry/sdk-metrics@1.25.1": - version "1.25.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-1.25.1.tgz#50c985ec15557a9654334e7fa1018dc47a8a56b7" - integrity sha512-9Mb7q5ioFL4E4dDrc4wC/A3NTHDat44v4I3p2pLPSxRvqUbDIQyMVr9uK+EU69+HWhlET1VaSrRzwdckWqY15Q== - dependencies: - "@opentelemetry/core" "1.25.1" - "@opentelemetry/resources" "1.25.1" - lodash.merge "^4.6.2" - -"@opentelemetry/sdk-node@0.52.1": - version "0.52.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-node/-/sdk-node-0.52.1.tgz#984f8a679a966b065504ccfe4b811359e417dd30" - integrity sha512-uEG+gtEr6eKd8CVWeKMhH2olcCHM9dEK68pe0qE0be32BcCRsvYURhHaD1Srngh1SQcnQzZ4TP324euxqtBOJA== - dependencies: - "@opentelemetry/api-logs" "0.52.1" - "@opentelemetry/core" "1.25.1" - "@opentelemetry/exporter-trace-otlp-grpc" "0.52.1" - "@opentelemetry/exporter-trace-otlp-http" "0.52.1" - "@opentelemetry/exporter-trace-otlp-proto" "0.52.1" - "@opentelemetry/exporter-zipkin" "1.25.1" - "@opentelemetry/instrumentation" "0.52.1" - "@opentelemetry/resources" "1.25.1" - "@opentelemetry/sdk-logs" "0.52.1" - "@opentelemetry/sdk-metrics" "1.25.1" - "@opentelemetry/sdk-trace-base" "1.25.1" - "@opentelemetry/sdk-trace-node" "1.25.1" - "@opentelemetry/semantic-conventions" "1.25.1" - -"@opentelemetry/sdk-trace-base@1.25.1": - version "1.25.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.1.tgz#cbc1e60af255655d2020aa14cde17b37bd13df37" - integrity sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw== - dependencies: - "@opentelemetry/core" "1.25.1" - "@opentelemetry/resources" "1.25.1" - "@opentelemetry/semantic-conventions" "1.25.1" - -"@opentelemetry/sdk-trace-node@1.25.1": - version "1.25.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.25.1.tgz#856063bef1167ae74139199338c24fb958838ff3" - integrity sha512-nMcjFIKxnFqoez4gUmihdBrbpsEnAX/Xj16sGvZm+guceYE0NE00vLhpDVK6f3q8Q4VFI5xG8JjlXKMB/SkTTQ== - dependencies: - "@opentelemetry/context-async-hooks" "1.25.1" - "@opentelemetry/core" "1.25.1" - "@opentelemetry/propagator-b3" "1.25.1" - "@opentelemetry/propagator-jaeger" "1.25.1" - "@opentelemetry/sdk-trace-base" "1.25.1" - semver "^7.5.2" - -"@opentelemetry/semantic-conventions@1.25.1": - version "1.25.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz#0deecb386197c5e9c2c28f2f89f51fb8ae9f145e" - integrity sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ== - -"@parcel/watcher@2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.4.tgz#f300fef4cc38008ff4b8c29d92588eced3ce014b" - integrity sha512-cTDi+FUDBIUOBKEtj+nhiJ71AZVlkAsQFuGQTun5tV9mwQBQgZvhCzG+URPQc8myeN32yRVZEfVAPCs1RW+Jvg== - dependencies: - node-addon-api "^3.2.1" - node-gyp-build "^4.3.0" - "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -4349,59 +4142,6 @@ dependencies: "@probe.gl/env" "4.0.9" -"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" - integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== - -"@protobufjs/base64@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" - integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== - -"@protobufjs/codegen@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" - integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== - -"@protobufjs/eventemitter@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" - integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== - -"@protobufjs/fetch@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" - integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== - dependencies: - "@protobufjs/aspromise" "^1.1.1" - "@protobufjs/inquire" "^1.1.0" - -"@protobufjs/float@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" - integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== - -"@protobufjs/inquire@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" - integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== - -"@protobufjs/path@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" - integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== - -"@protobufjs/pool@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" - integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== - -"@protobufjs/utf8@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" - integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== - "@radix-ui/number@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.0.1.tgz#644161a3557f46ed38a042acf4a770e826021674" @@ -5011,10 +4751,10 @@ resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.0.tgz#f817d1d3265ac5415dadc67edab30ae196696438" integrity sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg== -"@remirror/core-constants@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-2.0.2.tgz#f05eccdc69e3a65e7d524b52548f567904a11a1a" - integrity sha512-dyHY+sMF0ihPus3O27ODd4+agdHMEmuRdyiZJ2CCWjPV5UFmn17ZbElvk6WOGVE4rdCJKZQCrPV2BcikOMLUGQ== +"@remirror/core-constants@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@remirror/core-constants/-/core-constants-3.0.0.tgz#96fdb89d25c62e7b6a5d08caf0ce5114370e3b8f" + integrity sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg== "@rollup/pluginutils@^5.0.2", "@rollup/pluginutils@^5.0.4": version "5.0.4" @@ -5052,15 +4792,6 @@ "@sentry/types" "7.119.0" "@sentry/utils" "7.119.0" -"@sentry-internal/tracing@7.114.0": - version "7.114.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.114.0.tgz#bdcd364f511e2de45db6e3004faf5685ca2e0f86" - integrity sha512-dOuvfJN7G+3YqLlUY4HIjyWHaRP8vbOgF+OsE5w2l7ZEn1rMAaUbPntAR8AF9GBA6j2zWNoSo8e7GjbJxVofSg== - dependencies: - "@sentry/core" "7.114.0" - "@sentry/types" "7.114.0" - "@sentry/utils" "7.114.0" - "@sentry-internal/tracing@7.116.0": version "7.116.0" resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.116.0.tgz#af3e4e264c440aa5525b5877a10b9a0f870b40e3" @@ -5202,13 +4933,6 @@ "@sentry/types" "7.119.0" "@sentry/utils" "7.119.0" -"@sentry/tracing@7.114.0": - version "7.114.0" - resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.114.0.tgz#32a3438b0f2d02fb7b7359fe7712c5a349a2a329" - integrity sha512-eldEYGADReZ4jWdN5u35yxLUSTOvjsiZAYd4KBEpf+Ii65n7g/kYOKAjNl7tHbrEG1EsMW4nDPWStUMk1w+tfg== - dependencies: - "@sentry-internal/tracing" "7.114.0" - "@sentry/types@7.114.0": version "7.114.0" resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.114.0.tgz#ab8009d5f6df23b7342121083bed34ee2452e856" @@ -5348,6 +5072,13 @@ dependencies: "@sinonjs/commons" "^3.0.0" +"@sinonjs/fake-timers@^13.0.1", "@sinonjs/fake-timers@^13.0.2": + version "13.0.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-13.0.2.tgz#3ffe88abb062067a580fdfba706ad00435a0f2a6" + integrity sha512-4Bb+oqXZTSTZ1q27Izly9lv8B9dlV61CROxPiVtywwzv5SnytJqhvYe6FclHYuXml4cd1VHPo1zd5PmTeJozvA== + dependencies: + "@sinonjs/commons" "^3.0.1" + "@sinonjs/fake-timers@^6.0.0", "@sinonjs/fake-timers@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz#293674fccb3262ac782c7aadfdeca86b10c75c40" @@ -5364,19 +5095,19 @@ lodash.get "^4.4.2" type-detect "^4.0.8" -"@sinonjs/samsam@^8.0.0": - version "8.0.0" - resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-8.0.0.tgz#0d488c91efb3fa1442e26abea81759dfc8b5ac60" - integrity sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew== +"@sinonjs/samsam@^8.0.0", "@sinonjs/samsam@^8.0.1": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-8.0.2.tgz#e4386bf668ff36c95949e55a38dc5f5892fc2689" + integrity sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw== dependencies: - "@sinonjs/commons" "^2.0.0" + "@sinonjs/commons" "^3.0.1" lodash.get "^4.4.2" - type-detect "^4.0.8" + type-detect "^4.1.0" -"@sinonjs/text-encoding@^0.7.1": - version "0.7.2" - resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918" - integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ== +"@sinonjs/text-encoding@^0.7.1", "@sinonjs/text-encoding@^0.7.3": + version "0.7.3" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz#282046f03e886e352b2d5f5da5eb755e01457f3f" + integrity sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA== "@slack/types@^2.9.0": version "2.14.0" @@ -7326,78 +7057,78 @@ resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-14.5.2.tgz#db7257d727c891905947bd1c1a99da20e03c2ebd" integrity sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ== -"@tinybirdco/charts@0.2.0-beta.2": - version "0.2.0-beta.2" - resolved "https://registry.yarnpkg.com/@tinybirdco/charts/-/charts-0.2.0-beta.2.tgz#0b3ed104d23496bbd4809ee49852f522a3636dfe" - integrity sha512-05gLrdiM3kc4QJpDvideX0BjP++SfQ+K6XhxglE8wT/84TkzWpX1ouexyFc5FdsHTnGqeiVx+lhMMwSkN8G6Vg== +"@tinybirdco/charts@0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@tinybirdco/charts/-/charts-0.2.1.tgz#60894995f2901ece3d7cd11d329a571ebd591acd" + integrity sha512-kTzQU3yB9eFATw2DUZR2R+mDYeCO/IkwncgOTcf8Un0v1aSKnG2B9N0vZ9B1zTe3DE7rNd9ChLOyfRtsUAkRGw== dependencies: echarts "^5.5.0" swr "^2.2.5" -"@tiptap/core@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.6.0.tgz#c222f53043abfa69d8d41a081bb3dfc1181fc841" - integrity sha512-MG2OXwpMVaiacGuipqZ3VXi36gMvtV3i3ncrtXufsMducWvniENMi8fEonj/Q7nkeVi59OcoW8vD6kqdKkh2Gw== +"@tiptap/core@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@tiptap/core/-/core-2.8.0.tgz#4b5707d3ac1d61fcbb840371fc04990c1cb466b8" + integrity sha512-xsqDI4BNzYRWRtBq7+/38ThhqEr7uG9Njip1x+9/wgR3vWPBFnBkYJTz6jSxS35NRE6BSnERm4/B/vrLuY1Hdw== -"@tiptap/extension-blockquote@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.6.0.tgz#3f0a80ebd82e1f5d174c24f9794e22ac3184c072" - integrity sha512-/rYTYnn7fk/LBixLv7R1fEee3JDrDo3VHEJ+gBGuyLXcNaYtzWuRfqdeIraNSOuhzHffWQ5dg1oxpVFrVrd6AQ== +"@tiptap/extension-blockquote@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-blockquote/-/extension-blockquote-2.8.0.tgz#331603a27587b03382c061ef72aa5a274287e1f0" + integrity sha512-m3CKrOIvV7fY1Ak2gYf5LkKiz6AHxHpg6wxfVaJvdBqXgLyVtHo552N+A4oSHOSRbB4AG9EBQ2NeBM8cdEQ4MA== -"@tiptap/extension-bubble-menu@^2.6.1": - version "2.6.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.6.1.tgz#8a6777000cdf74d5319a66dc5c283bdac94e00b4" - integrity sha512-lIXilRlvQ2P4rnJ3zp5VM5JB3UG4w/SlSMkXHmOAf8S67aGqH9DHqDI3p2Twp+5ylsg/f0Q3evY/rftD51mNSw== +"@tiptap/extension-bubble-menu@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.8.0.tgz#41fe2ccd525c4d3a7e6e75a795f730ee53bd8cae" + integrity sha512-swg+myJPN60LduQvLMF4hVBqP5LOIN01INZBzBI8egz8QufqtSyRCgXl7Xcma0RT5xIXnZSG9XOqNFf2rtkjKA== dependencies: tippy.js "^6.3.7" -"@tiptap/extension-document@2.6.1": - version "2.6.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.6.1.tgz#ff5797960248e32c24e1070b9bdb86349a201a37" - integrity sha512-BKuyzzmxzs9Rm/LpwriluMjBQvZhM0kcvtWsIGchzmBdoyPQwOtZxItnqSJeGzgJzqdp5otVeLb6PAP/PlPs3g== +"@tiptap/extension-document@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-document/-/extension-document-2.8.0.tgz#7dc5d2622168ad5b81134a92fccf49d7be53f141" + integrity sha512-mp7Isx1sVc/ifeW4uW/PexGQ9exN3NRUOebSpnLfqXeWYk4y1RS1PA/3+IHkOPVetbnapgPjFx/DswlCP3XLjA== -"@tiptap/extension-floating-menu@^2.6.1": - version "2.6.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.6.1.tgz#af5178fe1c180328ec10ff93434f5edbea62d073" - integrity sha512-HVTK9tpFM5umb8J1YbhwJr/Cf7UPSLqZO3dA+MulVxBt9fD4Z+zNbLkhm57rbRZ68DIA0RL57FgFvdxlFCZA4A== +"@tiptap/extension-floating-menu@^2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-floating-menu/-/extension-floating-menu-2.8.0.tgz#06f4cea1aae9d45cf8878498a957180ee58ae148" + integrity sha512-H4QT61CrkLqisnGGC7zgiYmsl2jXPHl89yQCbdlkQN7aw11H7PltcJS2PJguL0OrRVJS/Mv/VTTUiMslmsEV5g== dependencies: tippy.js "^6.3.7" -"@tiptap/extension-hard-break@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.6.0.tgz#6a475ca5422a53c2739c0b54eaf488098972e6cc" - integrity sha512-RX5Xmj2svS7T3QkDs0X8aTekkcDxpnJ3hqHarzR1gK0GdVsrpowVKYhuEAc1zAAUirVa/IBimKXAIkXjDvuvNA== +"@tiptap/extension-hard-break@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-hard-break/-/extension-hard-break-2.8.0.tgz#bb450fcb7ab15b846c2bb556fbdb36a336c1a51a" + integrity sha512-vqiIfviNiCmy/pJTHuDSCAGL2O4QDEdDmAvGJu8oRmElUrnlg8DbJUfKvn6DWQHNSQwRb+LDrwWlzAYj1K9u6A== -"@tiptap/extension-link@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.6.0.tgz#2a3f65742fcb2776973541e9f2cea3169ce4a204" - integrity sha512-16ckFDnwqt0tYtQRgnVhooAUTLSk7liaqcGgc3NJ35gqWZho8hSnaTOl+72dlyJqUjqwt8D24NHFJsSqB9PSxA== +"@tiptap/extension-link@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-link/-/extension-link-2.8.0.tgz#bc2ca4af87881210f41451ea2c722254b31b0d81" + integrity sha512-p67hCG/pYCiOK/oCTPZnlkw9Ei7KJ7kCKFaluTcAmr5j8IBdYfDqSMDNCT4vGXBvKFh4X6xD7S7QvOqcH0Gn9A== dependencies: linkifyjs "^4.1.0" -"@tiptap/extension-paragraph@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.6.0.tgz#721921477e3785c0a0abf132ea109e67bcea9fae" - integrity sha512-ztmgfCSaCGFCR4VudA91WJDEQrpXH2FirQrJSaoOOwk3OfrNSSvcbqAhTmxJHl8e7aFwuR5eH8wbIxKw9In3Kg== +"@tiptap/extension-paragraph@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-paragraph/-/extension-paragraph-2.8.0.tgz#6f3d673d7f1143a64da3c1db2d2128c835a58c41" + integrity sha512-XgxxNNbuBF48rAGwv7/s6as92/xjm/lTZIGTq9aG13ClUKFtgdel7C33SpUCcxg3cO2WkEyllXVyKUiauFZw/A== -"@tiptap/extension-placeholder@2.6.1": - version "2.6.1" - resolved "https://registry.yarnpkg.com/@tiptap/extension-placeholder/-/extension-placeholder-2.6.1.tgz#01f6c83fe0560b5d5ab1e64dc238904e87317d46" - integrity sha512-YwgpKaPoZgYZ/56Yig6PylGHJwfcY+IhraifOx4sYphi/ZamUAgSGhsOYz4l9T/0zzIRLvLYlMsHNgtuN6Iupg== +"@tiptap/extension-placeholder@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-placeholder/-/extension-placeholder-2.8.0.tgz#2a7feffaba01167e4e890bc308852044241124f9" + integrity sha512-BMqv/C9Tcjd7L1/OphUAJTZhWfpWs0rTQJ0bs3RRGsC8L+K20Fg+li45vw7M0teojpfrw57zwJogJd/m23Zr1Q== -"@tiptap/extension-text@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.6.0.tgz#395cd44456059c11fb5f3e2215093a5c32ba59a3" - integrity sha512-Bg/FXGzj6WGvqgCVvFKzTWOpgg8Nl4X2a/bUT7nEmBafpUNAiN6enomMWnniylGTFEqZrywr5KRcvbgl8ubeDQ== +"@tiptap/extension-text@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-text/-/extension-text-2.8.0.tgz#d24d9e627a595aa87585d885f8ee3cd2a959d787" + integrity sha512-EDAdFFzWOvQfVy7j3qkKhBpOeE5thkJaBemSWfXI93/gMVc0ZCdLi24mDvNNgUHlT+RjlIoQq908jZaaxLKN2A== -"@tiptap/pm@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.6.0.tgz#b2ef8aba2f5a04ac3f5051e3d891ffe26f37967b" - integrity sha512-Oj912Jfowuq/K+oEkqx+pADjuVwBCXroC2hdrRMo4w18bb2gI2Nhse6St1KQ9kWWGF36O81RpDbpLe7L1YQhzQ== +"@tiptap/pm@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@tiptap/pm/-/pm-2.8.0.tgz#c79ad5e0f4a00cf306f15ac4497bf2fb0a40c784" + integrity sha512-eMGpRooUMvKz/vOpnKKppApMSoNM325HxTdAJvTlVAmuHp5bOY5kyY1kfUlePRiVx1t1UlFcXs3kecFwkkBD3Q== dependencies: prosemirror-changeset "^2.2.1" prosemirror-collab "^1.3.1" - prosemirror-commands "^1.5.2" + prosemirror-commands "^1.6.0" prosemirror-dropcursor "^1.8.1" prosemirror-gapcursor "^1.3.2" prosemirror-history "^1.4.1" @@ -7405,23 +7136,24 @@ prosemirror-keymap "^1.2.2" prosemirror-markdown "^1.13.0" prosemirror-menu "^1.2.4" - prosemirror-model "^1.22.2" + prosemirror-model "^1.22.3" prosemirror-schema-basic "^1.2.3" prosemirror-schema-list "^1.4.1" prosemirror-state "^1.4.3" prosemirror-tables "^1.4.0" - prosemirror-trailing-node "^2.0.9" - prosemirror-transform "^1.9.0" - prosemirror-view "^1.33.9" + prosemirror-trailing-node "^3.0.0" + prosemirror-transform "^1.10.0" + prosemirror-view "^1.33.10" -"@tiptap/react@2.6.1": - version "2.6.1" - resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.6.1.tgz#77d47730fa0054aeba78488b96e8a486eea2fed2" - integrity sha512-Mq0zhPiR3YZ3IlCtGhNsrXkyK12df71qgvlbWi3KMtcQFU+JWV18qpOyDMK4PxPpPgicKTYlAn57k7HBqtTZ6w== +"@tiptap/react@2.8.0": + version "2.8.0" + resolved "https://registry.yarnpkg.com/@tiptap/react/-/react-2.8.0.tgz#cd91281eab13da0371358fc69d765d89c5f258e0" + integrity sha512-o/aSCjO5Nu4MsNpTF+N1SzYzVQvvBiclmTOZX2E6usZ8jre5zmKfXHDSZnjGSRTK6z6kw5KW8wpjRQha03f9mg== dependencies: - "@tiptap/extension-bubble-menu" "^2.6.1" - "@tiptap/extension-floating-menu" "^2.6.1" + "@tiptap/extension-bubble-menu" "^2.8.0" + "@tiptap/extension-floating-menu" "^2.8.0" "@types/use-sync-external-store" "^0.0.6" + fast-deep-equal "^3" use-sync-external-store "^1.2.2" "@tokenizer/token@^0.3.0": @@ -8098,6 +7830,13 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== +"@tybys/wasm-util@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.9.0.tgz#3e75eb00604c8d6db470bf18c37b7d984a0e3355" + integrity sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw== + dependencies: + tslib "^2.4.0" + "@types/acorn@^4.0.3": version "4.0.6" resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-4.0.6.tgz#d61ca5480300ac41a7d973dd5b84d0a591154a22" @@ -8485,7 +8224,7 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=13.7.0", "@types/node@>=18.0.0", "@types/node@>=8.1.0": +"@types/node@*", "@types/node@>=10.0.0", "@types/node@>=18.0.0", "@types/node@>=8.1.0": version "22.5.5" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.5.tgz#52f939dd0f65fc552a4ad0b392f3c466cc5d7a44" integrity sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA== @@ -8634,11 +8373,6 @@ "@types/mime" "*" "@types/node" "*" -"@types/shimmer@^1.0.2": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@types/shimmer/-/shimmer-1.0.5.tgz#491d8984d4510e550bfeb02d518791d7f59d2b88" - integrity sha512-9Hp0ObzwwO57DpLFF0InUjUm/II8GmKAvzbefxQTihCb7KI6yc9yzf0nLc4mVdby5N4DRCgQM2wCup9KTieeww== - "@types/sinon@10.0.16": version "10.0.16" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.16.tgz#4bf10313bd9aa8eef1e50ec9f4decd3dd455b4d3" @@ -8663,6 +8397,13 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/stoppable@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/stoppable/-/stoppable-1.1.0.tgz#a5fa6a48120b109ca9233eed05c67c50bc4f3b91" + integrity sha512-BRR23Q9CJduH7AM6mk4JRttd8XyFkb4qIPZu4mdLF+VoP+wcjIxIWIKiBbN78NBbEuynrAyMPtzOHnIp2B/JPQ== + dependencies: + "@types/node" "*" + "@types/superagent@^8.1.0": version "8.1.7" resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-8.1.7.tgz#1153819ed4db34427409a1cc58f3e2f13eeec862" @@ -8823,10 +8564,10 @@ "@typescript-eslint/types" "6.9.1" eslint-visitor-keys "^3.4.1" -"@uiw/codemirror-extensions-basic-setup@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.3.tgz#f2dec646a6a1a6072b8b73e67372cb9cd178537e" - integrity sha512-nEMjgbCyeLx+UQgOGAAoUWYFE34z5TlyaKNszuig/BddYFDb0WKcgmC37bDFxR2dZssf3K/lwGWLpXnGKXePbA== +"@uiw/codemirror-extensions-basic-setup@4.23.5": + version "4.23.5" + resolved "https://registry.yarnpkg.com/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.5.tgz#02ebe9c44f76609f15295e1ff9c83e770140c369" + integrity sha512-eTMfT8TejVN/D5vvuz9Lab+MIoRYdtqa2ftZZmU3JpcDIXf9KaExPo+G2Rl9HqySzaasgGXOOG164MAnj3MSIw== dependencies: "@codemirror/autocomplete" "^6.0.0" "@codemirror/commands" "^6.0.0" @@ -8836,16 +8577,16 @@ "@codemirror/state" "^6.0.0" "@codemirror/view" "^6.0.0" -"@uiw/react-codemirror@4.23.3": - version "4.23.3" - resolved "https://registry.yarnpkg.com/@uiw/react-codemirror/-/react-codemirror-4.23.3.tgz#2febc96478e206d704a8810306b9beab1ee76af1" - integrity sha512-TBBLUbeqXmfQSfO+f3rPNOAb+QXbSm7KPB64FHQWLGg2WJNbpOhjLOWMyL+C4ZP3aSCNc2Y5aftEK1vp3wCKTA== +"@uiw/react-codemirror@4.23.5": + version "4.23.5" + resolved "https://registry.yarnpkg.com/@uiw/react-codemirror/-/react-codemirror-4.23.5.tgz#6a16c23062067732cba105ac33ad69cf8e5c2111" + integrity sha512-2zzGpx61L4mq9zDG/hfsO4wAH209TBE8VVsoj/qrccRe6KfcneCwKgRxtQjxBCCnO0Q5S+IP+uwCx5bXRzgQFQ== dependencies: "@babel/runtime" "^7.18.6" "@codemirror/commands" "^6.1.0" "@codemirror/state" "^6.1.1" "@codemirror/theme-one-dark" "^6.0.0" - "@uiw/codemirror-extensions-basic-setup" "4.23.3" + "@uiw/codemirror-extensions-basic-setup" "4.23.5" codemirror "^6.0.0" "@vitejs/plugin-react@4.2.1": @@ -9260,10 +9001,10 @@ js-yaml "^3.10.0" tslib "^2.4.0" -"@zkochan/js-yaml@0.0.6": - version "0.0.6" - resolved "https://registry.yarnpkg.com/@zkochan/js-yaml/-/js-yaml-0.0.6.tgz#975f0b306e705e28b8068a07737fa46d3fc04826" - integrity sha512-nzvgl3VfhcELQ8LyVrYOru+UtAy1nrygk2+AGbTm8a5YcO6o8lSjAT+pfg3vJWxIoZKOUhrK6UU7xW/+00kQrg== +"@zkochan/js-yaml@0.0.7": + version "0.0.7" + resolved "https://registry.yarnpkg.com/@zkochan/js-yaml/-/js-yaml-0.0.7.tgz#4b0cb785220d7c28ce0ec4d0804deb5d821eae89" + integrity sha512-nrUSn7hzt7J6JWgWGz78ZYI8wj+gdIJdk0Ynjpp8l+trkn58Uqsf6RYrYkEK+3X18EX+TNdtJI0WxAtc+L84SQ== dependencies: argparse "^2.0.1" @@ -9502,7 +9243,7 @@ ansi-colors@4.1.1: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== -ansi-colors@^4.1.1: +ansi-colors@^4.1.1, ansi-colors@^4.1.3: version "4.1.3" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== @@ -11021,6 +10762,11 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" +bintrees@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.2.tgz#49f896d6e858a4a499df85c38fb399b9aff840f8" + integrity sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw== + bl@^4.0.3, bl@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" @@ -11084,7 +10830,7 @@ body-parser@1.20.2: type-is "~1.6.18" unpipe "1.0.0" -body-parser@1.20.3, body-parser@^1.19.0: +body-parser@1.20.3, body-parser@^1.19.0, body-parser@^1.20.1: version "1.20.3" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.3.tgz#1953431221c6fb5cd63c4b36d53fab0928e548c6" integrity sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g== @@ -11899,7 +11645,7 @@ browser-process-hrtime@^1.0.0: resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== -browser-stdout@1.3.1: +browser-stdout@1.3.1, browser-stdout@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== @@ -12158,6 +11904,23 @@ bytes@3.1.2: resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +c8@10.1.2: + version "10.1.2" + resolved "https://registry.yarnpkg.com/c8/-/c8-10.1.2.tgz#7fe04ced150316e2a623612ab78378289f7e6a9f" + integrity sha512-Qr6rj76eSshu5CgRYvktW0uM0CFY0yi4Fd5D0duDXO6sYinyopmftUiJVuzBQxQcwQLor7JWDVRP+dUfCmzgJw== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@istanbuljs/schema" "^0.1.3" + find-up "^5.0.0" + foreground-child "^3.1.1" + istanbul-lib-coverage "^3.2.0" + istanbul-lib-report "^3.0.1" + istanbul-reports "^3.1.6" + test-exclude "^7.0.1" + v8-to-istanbul "^9.0.0" + yargs "^17.7.2" + yargs-parser "^21.1.1" + c8@7.14.0: version "7.14.0" resolved "https://registry.yarnpkg.com/c8/-/c8-7.14.0.tgz#f368184c73b125a80565e9ab2396ff0be4d732f3" @@ -12719,7 +12482,7 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: inherits "^2.0.1" safe-buffer "^5.0.1" -cjs-module-lexer@^1.0.0, cjs-module-lexer@^1.2.2: +cjs-module-lexer@^1.0.0: version "1.3.1" resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz#c485341ae8fd999ca4ee5af2d7a1c9ae01e0099c" integrity sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q== @@ -14060,12 +13823,12 @@ debug@3.2.7, debug@^3.0.1, debug@^3.1.0, debug@^3.2.6, debug@^3.2.7: dependencies: ms "^2.1.1" -debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2, debug@~4.3.6: - version "4.3.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" - integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== +debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@^4.3.5, debug@~4.3.1, debug@~4.3.2, debug@~4.3.6: + version "4.3.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.7.tgz#87945b4151a011d76d95a198d7111c865c360a52" + integrity sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ== dependencies: - ms "2.1.2" + ms "^2.1.3" debug@4.1.1: version "4.1.1" @@ -14386,6 +14149,11 @@ dezalgo@^1.0.4: asap "^2.0.0" wrappy "1" +diacritics@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/diacritics/-/diacritics-1.3.0.tgz#3efa87323ebb863e6696cebb0082d48ff3d6f7a1" + integrity sha512-wlwEkqcsaxvPJML+rDh/2iS824jbREk6DUMUKkEaSlxdYHeS43cClJtsWglvw2RfeXGm6ohKDqsXteJ5sP5enA== + dicer@0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" @@ -14424,10 +14192,15 @@ diff@^4.0.1, diff@^4.0.2: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -diff@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" - integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw== +diff@^5.1.0, diff@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" + integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== + +diff@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a" + integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw== diffie-hellman@^5.0.0: version "5.0.3" @@ -14615,15 +14388,22 @@ dot-prop@^5.2.0: dependencies: is-obj "^2.0.0" -dotenv-expand@^10.0.0, dotenv-expand@~10.0.0: +dotenv-expand@^10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-10.0.0.tgz#12605d00fb0af6d0a592e6558585784032e4ef37" integrity sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A== -dotenv@^16.0.0, dotenv@~16.3.1: - version "16.3.1" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" - integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== +dotenv-expand@~11.0.6: + version "11.0.6" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-11.0.6.tgz#f2c840fd924d7c77a94eff98f153331d876882d3" + integrity sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g== + dependencies: + dotenv "^16.4.4" + +dotenv@^16.0.0, dotenv@^16.4.4, dotenv@~16.4.5: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== downsize@0.0.8: version "0.0.8" @@ -17451,7 +17231,7 @@ fast-deep-equal@^2.0.1: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" integrity sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w== -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: +fast-deep-equal@^3, fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== @@ -17461,17 +17241,6 @@ fast-fifo@^1.1.0, fast-fifo@^1.2.0: resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== -fast-glob@3.2.7: - version "3.2.7" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" - integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - fast-glob@^3.0.3, fast-glob@^3.2.12, fast-glob@^3.2.5, fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.1, fast-glob@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" @@ -17924,11 +17693,6 @@ flatten@^1.0.2: resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b" integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg== -flexsearch@0.7.21: - version "0.7.21" - resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.7.21.tgz#0f5ede3f2aae67ddc351efbe3b24b69d29e9d48b" - integrity sha512-W7cHV7Hrwjid6lWmy0IhsWDFQboWSng25U3VVywpHOTJnnAZNPScog67G+cVpeX9f7yDD21ih0WDrMMT+JoaYg== - flexsearch@0.7.43: version "0.7.43" resolved "https://registry.yarnpkg.com/flexsearch/-/flexsearch-0.7.43.tgz#34f89b36278a466ce379c5bf6fb341965ed3f16c" @@ -17991,10 +17755,10 @@ foreground-child@^2.0.0: cross-spawn "^7.0.0" signal-exit "^3.0.2" -foreground-child@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" - integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== +foreground-child@^3.1.0, foreground-child@^3.1.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== dependencies: cross-spawn "^7.0.0" signal-exit "^4.0.1" @@ -18099,6 +17863,13 @@ from2@^2.1.0: inherits "^2.0.1" readable-stream "^2.0.0" +front-matter@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/front-matter/-/front-matter-4.0.2.tgz#b14e54dc745cfd7293484f3210d15ea4edd7f4d5" + integrity sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg== + dependencies: + js-yaml "^3.13.1" + fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -18591,18 +18362,6 @@ glob@3.2.11: inherits "2" minimatch "0.3" -glob@7.1.4: - version "7.1.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" - integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - glob@7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -18638,16 +18397,17 @@ glob@8.1.0, glob@^8.0.3, glob@^8.1.0: minimatch "^5.0.1" once "^1.3.0" -glob@^10.0.0, glob@^10.2.2, glob@^10.3.10, glob@^10.3.7: - version "10.3.10" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" - integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== +glob@^10.0.0, glob@^10.2.2, glob@^10.3.10, glob@^10.3.7, glob@^10.4.1: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== dependencies: foreground-child "^3.1.0" - jackspeak "^2.3.5" - minimatch "^9.0.1" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry "^1.10.1" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" glob@^5.0.10: version "5.0.15" @@ -19283,10 +19043,10 @@ html-to-text@8.2.1: minimist "^1.2.6" selderee "^0.6.0" -html-validate@8.23.0: - version "8.23.0" - resolved "https://registry.yarnpkg.com/html-validate/-/html-validate-8.23.0.tgz#10d69aba5629618ab03cf55594261c2efe3a9fbc" - integrity sha512-j4BZVXGiQVVO46bh/gKlFGXcuV+LzP3m//VmrrbCWiATWaGWurl5DhMr3im9zoxhkIb99CffxdJ3HRI3vLSM4w== +html-validate@8.24.1: + version "8.24.1" + resolved "https://registry.yarnpkg.com/html-validate/-/html-validate-8.24.1.tgz#09525b66574c1135889a9cc74e08e2c6b345105d" + integrity sha512-WDV/JI4sVdX0QW4cupodsuMfI8vDygw3dMXD76OHKNMf7qhKxUsLDnNVKG7GLeAkRP0e0Y6H8AOrlaatob+g4Q== dependencies: "@html-validate/stylish" "^4.1.0" "@sidvind/better-ajv-errors" "3.0.1" @@ -19536,6 +19296,13 @@ husky@8.0.3: resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184" integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg== +i18n-iso-countries@7.12.0: + version "7.12.0" + resolved "https://registry.yarnpkg.com/i18n-iso-countries/-/i18n-iso-countries-7.12.0.tgz#e189d85a505ee025f0f48b5ccc35fa66c436e99c" + integrity sha512-NDFf5j/raA5JrcPT/NcHP3RUMH7TkdkxQKAKdvDlgb+MS296WJzzqvV0Y5uwavSm7A6oYvBeSV0AxoHdDiHIiw== + dependencies: + diacritics "1.3.0" + i18next-parser@8.13.0: version "8.13.0" resolved "https://registry.yarnpkg.com/i18next-parser/-/i18next-parser-8.13.0.tgz#3d0774f1659d691b451c105cc1eaf8c444e11740" @@ -19560,10 +19327,10 @@ i18next-parser@8.13.0: vinyl-fs "^4.0.0" vue-template-compiler "^2.6.11" -i18next@23.15.1, i18next@^23.5.1: - version "23.15.1" - resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.15.1.tgz#c50de337bf12ca5195e697cc0fbe5f32304871d9" - integrity sha512-wB4abZ3uK7EWodYisHl/asf8UYEhrI/vj/8aoSsrj/ZDxj4/UXPOa1KvFt1Fq5hkUHquNqwFlDprmjZ8iySgYA== +i18next@23.15.2, i18next@^23.5.1: + version "23.15.2" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.15.2.tgz#8a54f877ccbbc46696eacb5bd5b31d84f9ade7cb" + integrity sha512-zcPSWzCvw6uKnuYHIqs4W7hTuB9e3AFcSdZgvCWoPXIZsBjBd4djN2/2uOHIB+1DFFkQnMBXvhNg7J3WyCuywQ== dependencies: "@babel/runtime" "^7.23.2" @@ -19651,16 +19418,6 @@ import-fresh@^3.0.0, import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" -import-in-the-middle@^1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/import-in-the-middle/-/import-in-the-middle-1.8.1.tgz#8b51c2cc631b64e53e958d7048d2d9463ce628f8" - integrity sha512-yhRwoHtiLGvmSozNOALgjRPFI6uYsds60EoMqqnXyyv+JOIW/BrrLejuTGBt+bq0T5tLzOHrN0T7xYTm4Qt/ng== - dependencies: - acorn "^8.8.2" - acorn-import-attributes "^1.9.5" - cjs-module-lexer "^1.2.2" - module-details-from-path "^1.0.3" - import-lazy@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-4.0.0.tgz#e8eb627483a0a43da3c03f3e35548be5cb0cc153" @@ -20693,7 +20450,7 @@ iterare@1.2.1: resolved "https://registry.yarnpkg.com/iterare/-/iterare-1.2.1.tgz#139c400ff7363690e33abffa33cbba8920f00042" integrity sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q== -jackspeak@2.1.1, jackspeak@^2.3.5: +jackspeak@2.1.1, jackspeak@^3.1.2: version "2.1.1" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.1.1.tgz#2a42db4cfbb7e55433c28b6f75d8b796af9669cd" integrity sha512-juf9stUEwUaILepraGOWIJTLwg48bUnBmRqd2ln2Os1sW987zeoj/hzhbvRB95oMuS2ZTpjULmdwHNX4rzZIZw== @@ -20810,7 +20567,7 @@ jest-diff@^28.1.3: jest-get-type "^28.0.2" pretty-format "^28.1.3" -jest-diff@^29.7.0: +jest-diff@^29.4.1, jest-diff@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== @@ -21565,6 +21322,11 @@ just-extend@^4.0.2: resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg== +just-extend@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-6.2.0.tgz#b816abfb3d67ee860482e7401564672558163947" + integrity sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw== + jwa@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" @@ -21960,16 +21722,16 @@ line-column@^1.0.2: isarray "^1.0.0" isobject "^2.0.0" +lines-and-columns@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-2.0.3.tgz#b2f0badedb556b747020ab8ea7f0373e22efac1b" + integrity sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w== + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -lines-and-columns@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-2.0.3.tgz#b2f0badedb556b747020ab8ea7f0373e22efac1b" - integrity sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w== - linkify-it@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-2.2.0.tgz#e3b54697e78bf915c70a38acd78fd09e0058b1cf" @@ -22258,7 +22020,7 @@ lodash.bind@^4.1.4: resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35" integrity sha512-lxdsn7xxlCymgLYo1gGvVrfHmkjDiyqVv62FAeF2i5ta72BipE1SLxw8hPEPLhD4/247Ijw07UQH7Hq/chT5LA== -lodash.camelcase@4.3.0, lodash.camelcase@^4.1.1, lodash.camelcase@^4.3.0: +lodash.camelcase@4.3.0, lodash.camelcase@^4.1.1: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== @@ -22480,7 +22242,7 @@ lodash@4.17.21, lodash@^4.0.0, lodash@^4.14.2, lodash@^4.17.10, lodash@^4.17.11, resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-symbols@4.1.0, log-symbols@^4.1.0: +log-symbols@4.1.0, log-symbols@^4.0.0, log-symbols@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== @@ -22521,7 +22283,7 @@ long-timeout@^0.1.1, long-timeout@~0.1.1: resolved "https://registry.yarnpkg.com/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514" integrity sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w== -long@^5.0.0, long@^5.2.0, long@^5.2.1: +long@^5.2.0, long@^5.2.1: version "5.2.3" resolved "https://registry.yarnpkg.com/long/-/long-5.2.3.tgz#a3ba97f3877cf1d778eccbcb048525ebb77499e1" integrity sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q== @@ -22577,10 +22339,10 @@ lru-cache@2: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" integrity sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ== -lru-cache@^10.0.1, "lru-cache@^9.1.1 || ^10.0.0": - version "10.1.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484" - integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag== +lru-cache@^10.0.1, lru-cache@^10.2.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== lru-cache@^4.1.5: version "4.1.5" @@ -22630,6 +22392,11 @@ lru-memoizer@^2.2.0: lodash.clonedeep "^4.5.0" lru-cache "~4.0.0" +lru.min@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/lru.min/-/lru.min-1.1.1.tgz#146e01e3a183fa7ba51049175de04667d5701f0e" + integrity sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q== + ltgt@^2.1.2: version "2.2.1" resolved "https://registry.yarnpkg.com/ltgt/-/ltgt-2.2.1.tgz#f35ca91c493f7b73da0e07495304f17b31f87ee5" @@ -23349,13 +23116,6 @@ minimatch@0.3: dependencies: brace-expansion "^1.1.7" -minimatch@3.0.5: - version "3.0.5" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.5.tgz#4da8f1290ee0f0f8e83d60ca69f8f134068604a3" - integrity sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw== - dependencies: - brace-expansion "^1.1.7" - minimatch@5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" @@ -23363,20 +23123,27 @@ minimatch@5.0.1: dependencies: brace-expansion "^2.0.1" -minimatch@^5.0.1, minimatch@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" - integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^9.0.1: +minimatch@9.0.3: version "9.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== dependencies: brace-expansion "^2.0.1" +minimatch@^5.0.1, minimatch@^5.1.0, minimatch@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + minimist-options@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -23483,10 +23250,10 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.2, minipass@^7.0.3: - version "7.0.4" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" - integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.2, minipass@^7.0.3, minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== minizlib@^2.0.0, minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" @@ -23631,6 +23398,32 @@ mocha@10.2.0: yargs-parser "20.2.4" yargs-unparser "2.0.0" +mocha@10.7.3: + version "10.7.3" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.7.3.tgz#ae32003cabbd52b59aece17846056a68eb4b0752" + integrity sha512-uQWxAu44wwiACGqjbPYmjo7Lg8sFrS3dQe7PP2FQI+woptP4vZXSMcfMyFL/e1yFEeEpV4RtyTpZROOKmxis+A== + dependencies: + ansi-colors "^4.1.3" + browser-stdout "^1.3.1" + chokidar "^3.5.3" + debug "^4.3.5" + diff "^5.2.0" + escape-string-regexp "^4.0.0" + find-up "^5.0.0" + glob "^8.1.0" + he "^1.2.0" + js-yaml "^4.1.0" + log-symbols "^4.1.0" + minimatch "^5.1.6" + ms "^2.1.3" + serialize-javascript "^6.0.2" + strip-json-comments "^3.1.1" + supports-color "^8.1.1" + workerpool "^6.5.1" + yargs "^16.2.0" + yargs-parser "^20.2.9" + yargs-unparser "^2.0.0" + mocha@^2.5.3: version "2.5.3" resolved "https://registry.yarnpkg.com/mocha/-/mocha-2.5.3.tgz#161be5bdeb496771eb9b35745050b622b5aefc58" @@ -23655,11 +23448,6 @@ mock-knex@TryGhost/mock-knex#d8b93b1c20d4820323477f2c60db016ab3e73192: lodash "^4.14.2" semver "^5.3.0" -module-details-from-path@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/module-details-from-path/-/module-details-from-path-1.0.3.tgz#114c949673e2a8a35e9d35788527aa37b679da2b" - integrity sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A== - moment-timezone@0.5.34, moment-timezone@0.5.45, moment-timezone@^0.5.23, moment-timezone@^0.5.31, moment-timezone@^0.5.33: version "0.5.45" resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.45.tgz#cb685acd56bac10e69d93c536366eb65aa6bcf5c" @@ -23786,17 +23574,17 @@ mv@~2: ncp "~2.0.0" rimraf "~2.4.0" -mysql2@3.11.0: - version "3.11.0" - resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.11.0.tgz#2a7bd7c615ab43f8167ed9922063b968f3e48f33" - integrity sha512-J9phbsXGvTOcRVPR95YedzVSxJecpW5A5+cQ57rhHIFXteTP10HCs+VBjS7DHIKfEaI1zQ5tlVrquCd64A6YvA== +mysql2@3.11.3: + version "3.11.3" + resolved "https://registry.yarnpkg.com/mysql2/-/mysql2-3.11.3.tgz#8291e6069a0784310846f6437b8527050dfc10c4" + integrity sha512-Qpu2ADfbKzyLdwC/5d4W7+5Yz7yBzCU05YWt5npWzACST37wJsB23wgOSo00qi043urkiRwXtEvJc9UnuLX/MQ== dependencies: aws-ssl-profiles "^1.1.1" denque "^2.1.0" generate-function "^2.3.1" iconv-lite "^0.6.3" long "^5.2.1" - lru-cache "^8.0.0" + lru.min "^1.0.0" named-placeholders "^1.1.3" seq-queue "^0.0.5" sqlstring "^2.3.2" @@ -23963,6 +23751,17 @@ nise@^5.1.4, nise@^5.1.5: just-extend "^4.0.2" path-to-regexp "^1.7.0" +nise@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/nise/-/nise-6.1.1.tgz#78ea93cc49be122e44cb7c8fdf597b0e8778b64a" + integrity sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g== + dependencies: + "@sinonjs/commons" "^3.0.1" + "@sinonjs/fake-timers" "^13.0.1" + "@sinonjs/text-encoding" "^0.7.3" + just-extend "^6.2.0" + path-to-regexp "^8.1.0" + no-case@^2.2.0: version "2.3.2" resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" @@ -24003,11 +23802,6 @@ node-abi@^3.3.0, node-abi@^3.61.0: dependencies: semver "^7.3.5" -node-addon-api@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" - integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== - node-addon-api@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-6.1.0.tgz#ac8470034e58e67d0c6f1204a18ae6995d9c0d76" @@ -24042,11 +23836,6 @@ node-forge@^1.2.1: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== -node-gyp-build@^4.3.0: - version "4.6.1" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.1.tgz#24b6d075e5e391b8d5539d98c7fc5c210cac8a3e" - integrity sha512-24vnklJmyRS8ViBNI8KbtK/r/DmXQMRiOMXTNz2nrTnAYUwjmEEbnnpB/+kt+yWRv73bPsSPRFddrcIbAxSiMQ== - node-gyp@8.x: version "8.4.1" resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.1.tgz#3d49308fc31f768180957d6b5746845fbd429937" @@ -24396,58 +24185,56 @@ nwsapi@^2.2.0, nwsapi@^2.2.12: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.12.tgz#fb6af5c0ec35b27b4581eb3bbad34ec9e5c696f8" integrity sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w== -nx@16.8.1: - version "16.8.1" - resolved "https://registry.yarnpkg.com/nx/-/nx-16.8.1.tgz#b3b084da5f880c638debbefbf33eeccb96633595" - integrity sha512-K5KrwNdPz0eEe6SY5wrnhZcigjfIJkttPrIJRXNBQTE50NGcOfz1TjMXPdTWBxBCCua5PAealO3OrE8jpv+QnQ== +nx@19.8.3: + version "19.8.3" + resolved "https://registry.yarnpkg.com/nx/-/nx-19.8.3.tgz#4ba06036ff2d963884eaba1c11a91e9e1062555c" + integrity sha512-/3FF4tgwPGRu4bV6O+aHqhTnOGHKF0/HNVkApUwjimSC+YzOX9VH1uBx2eReb4XC1scxDWkIzVi9gkFSXSQDjQ== dependencies: - "@nrwl/tao" "16.8.1" - "@parcel/watcher" "2.0.4" + "@napi-rs/wasm-runtime" "0.2.4" + "@nrwl/tao" "19.8.3" "@yarnpkg/lockfile" "^1.1.0" "@yarnpkg/parsers" "3.0.0-rc.46" - "@zkochan/js-yaml" "0.0.6" - axios "^1.0.0" + "@zkochan/js-yaml" "0.0.7" + axios "^1.7.4" chalk "^4.1.0" cli-cursor "3.1.0" cli-spinners "2.6.1" - cliui "^7.0.2" - dotenv "~16.3.1" - dotenv-expand "~10.0.0" + cliui "^8.0.1" + dotenv "~16.4.5" + dotenv-expand "~11.0.6" enquirer "~2.3.6" - fast-glob "3.2.7" figures "3.2.0" flat "^5.0.2" - fs-extra "^11.1.0" - glob "7.1.4" + front-matter "^4.0.2" ignore "^5.0.4" - js-yaml "4.1.0" + jest-diff "^29.4.1" jsonc-parser "3.2.0" - lines-and-columns "~2.0.3" - minimatch "3.0.5" + lines-and-columns "2.0.3" + minimatch "9.0.3" node-machine-id "1.1.12" npm-run-path "^4.0.1" open "^8.4.0" - semver "7.5.3" + ora "5.3.0" + semver "^7.5.3" string-width "^4.2.3" strong-log-transformer "^2.1.0" tar-stream "~2.2.0" tmp "~0.2.1" tsconfig-paths "^4.1.2" tslib "^2.3.0" - v8-compile-cache "2.3.0" yargs "^17.6.2" yargs-parser "21.1.1" optionalDependencies: - "@nx/nx-darwin-arm64" "16.8.1" - "@nx/nx-darwin-x64" "16.8.1" - "@nx/nx-freebsd-x64" "16.8.1" - "@nx/nx-linux-arm-gnueabihf" "16.8.1" - "@nx/nx-linux-arm64-gnu" "16.8.1" - "@nx/nx-linux-arm64-musl" "16.8.1" - "@nx/nx-linux-x64-gnu" "16.8.1" - "@nx/nx-linux-x64-musl" "16.8.1" - "@nx/nx-win32-arm64-msvc" "16.8.1" - "@nx/nx-win32-x64-msvc" "16.8.1" + "@nx/nx-darwin-arm64" "19.8.3" + "@nx/nx-darwin-x64" "19.8.3" + "@nx/nx-freebsd-x64" "19.8.3" + "@nx/nx-linux-arm-gnueabihf" "19.8.3" + "@nx/nx-linux-arm64-gnu" "19.8.3" + "@nx/nx-linux-arm64-musl" "19.8.3" + "@nx/nx-linux-x64-gnu" "19.8.3" + "@nx/nx-linux-x64-musl" "19.8.3" + "@nx/nx-win32-arm64-msvc" "19.8.3" + "@nx/nx-win32-x64-msvc" "19.8.3" oauth-sign@~0.9.0: version "0.9.0" @@ -24689,6 +24476,20 @@ optionator@^0.9.1, optionator@^0.9.3: prelude-ls "^1.2.1" type-check "^0.4.0" +ora@5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.3.0.tgz#fb832899d3a1372fe71c8b2c534bbfe74961bb6f" + integrity sha512-zAKMgGXUim0Jyd6CXK9lraBnD3H5yPGBPPOkC23a2BG6hsm4Zu6OQSjQuEtV0BHDf4aKHcUFvJiGRrFuW3MG8g== + dependencies: + bl "^4.0.3" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + log-symbols "^4.0.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + ora@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/ora/-/ora-3.4.0.tgz#bf0752491059a3ef3ed4c85097531de9fdbcd318" @@ -24885,6 +24686,11 @@ p-wait-for@3.2.0: dependencies: p-timeout "^3.0.0" +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + pako@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/pako/-/pako-2.0.4.tgz#6cebc4bbb0b6c73b0d5b8d7e8476e2b2fbea576d" @@ -24988,6 +24794,13 @@ parse-passwd@^1.0.0: resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" integrity sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q== +parse-prometheus-text-format@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/parse-prometheus-text-format/-/parse-prometheus-text-format-1.1.1.tgz#498f68ebc391ffada81391ee81cf88325f857806" + integrity sha512-dBlhYVACjRdSqLMFe4/Q1l/Gd3UmXm8ruvsTi7J6ul3ih45AkzkVpI5XHV4aZ37juGZW5+3dGU5lwk+QLM9XJA== + dependencies: + shallow-equal "^1.2.0" + parse-srcset@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" @@ -25121,12 +24934,12 @@ path-root@^0.1.1: dependencies: path-root-regex "^0.1.0" -path-scurry@^1.10.1: - version "1.10.1" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" - integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== dependencies: - lru-cache "^9.1.1 || ^10.0.0" + lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-to-regexp@0.1.10: @@ -25151,6 +24964,11 @@ path-to-regexp@^1.0.0, path-to-regexp@^1.7.0: dependencies: isarray "0.0.1" +path-to-regexp@^8.1.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4" + integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ== + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -26432,6 +26250,14 @@ progress@^2.0.0, progress@^2.0.1: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +prom-client@15.1.3: + version "15.1.3" + resolved "https://registry.yarnpkg.com/prom-client/-/prom-client-15.1.3.tgz#69fa8de93a88bc9783173db5f758dc1c69fa8fc2" + integrity sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g== + dependencies: + "@opentelemetry/api" "^1.4.0" + tdigest "^0.1.1" + promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" @@ -26512,10 +26338,10 @@ prosemirror-collab@^1.3.1: dependencies: prosemirror-state "^1.0.0" -prosemirror-commands@^1.0.0, prosemirror-commands@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.5.2.tgz#e94aeea52286f658cd984270de9b4c3fff580852" - integrity sha512-hgLcPaakxH8tu6YvVAaILV2tXYsW3rAdDR8WNkeKGcgeMVQg3/TMhPdVoh7iAmfgVjZGtcOSjKiQaoeKjzd2mQ== +prosemirror-commands@^1.0.0, prosemirror-commands@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/prosemirror-commands/-/prosemirror-commands-1.6.0.tgz#b79f034ed371576e7bf83ddd4ede689c8ccbd9ab" + integrity sha512-xn1U/g36OqXn2tn5nGmvnnimAj/g1pUx2ypJJIe8WkVX83WyJVC5LTARaxZa2AtQRwntu9Jc5zXs9gL9svp/mg== dependencies: prosemirror-model "^1.0.0" prosemirror-state "^1.0.0" @@ -26584,10 +26410,10 @@ prosemirror-menu@^1.2.4: prosemirror-history "^1.0.0" prosemirror-state "^1.0.0" -prosemirror-model@^1.0.0, prosemirror-model@^1.19.0, prosemirror-model@^1.20.0, prosemirror-model@^1.21.0, prosemirror-model@^1.22.2, prosemirror-model@^1.8.1: - version "1.22.2" - resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.22.2.tgz#9bae923c4c4149eee59ff61a5b390c26011a26b4" - integrity sha512-I4lS7HHIW47D0Xv/gWmi4iUWcQIDYaJKd8Hk4+lcSps+553FlQrhmxtItpEvTr75iAruhzVShVp6WUwsT6Boww== +prosemirror-model@^1.0.0, prosemirror-model@^1.19.0, prosemirror-model@^1.20.0, prosemirror-model@^1.21.0, prosemirror-model@^1.22.3, prosemirror-model@^1.8.1: + version "1.22.3" + resolved "https://registry.yarnpkg.com/prosemirror-model/-/prosemirror-model-1.22.3.tgz#52fdf5897f348b0f07f64bea89156d90afdf645a" + integrity sha512-V4XCysitErI+i0rKFILGt/xClnFJaohe/wrrlT2NSZ+zk8ggQfDH4x2wNK7Gm0Hp4CIoWizvXFP7L9KMaCuI0Q== dependencies: orderedmap "^2.0.0" @@ -26627,25 +26453,25 @@ prosemirror-tables@^1.4.0: prosemirror-transform "^1.2.1" prosemirror-view "^1.13.3" -prosemirror-trailing-node@^2.0.9: - version "2.0.9" - resolved "https://registry.yarnpkg.com/prosemirror-trailing-node/-/prosemirror-trailing-node-2.0.9.tgz#a087e6d1372e888cd3e57c977507b6b85dc658e4" - integrity sha512-YvyIn3/UaLFlFKrlJB6cObvUhmwFNZVhy1Q8OpW/avoTbD/Y7H5EcjK4AZFKhmuS6/N6WkGgt7gWtBWDnmFvHg== +prosemirror-trailing-node@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz#5bc223d4fc1e8d9145e4079ec77a932b54e19e04" + integrity sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ== dependencies: - "@remirror/core-constants" "^2.0.2" + "@remirror/core-constants" "3.0.0" escape-string-regexp "^4.0.0" -prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.2.1, prosemirror-transform@^1.7.3, prosemirror-transform@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.9.0.tgz#81fd1fbd887929a95369e6dd3d240c23c19313f8" - integrity sha512-5UXkr1LIRx3jmpXXNKDhv8OyAOeLTGuXNwdVfg8x27uASna/wQkr9p6fD3eupGOi4PLJfbezxTyi/7fSJypXHg== +prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transform@^1.10.0, prosemirror-transform@^1.2.1, prosemirror-transform@^1.7.3: + version "1.10.0" + resolved "https://registry.yarnpkg.com/prosemirror-transform/-/prosemirror-transform-1.10.0.tgz#2211ddcb13f12e4e530de97c077f82ab46177b0c" + integrity sha512-9UOgFSgN6Gj2ekQH5CTDJ8Rp/fnKR2IkYfGdzzp5zQMFsS4zDllLVx/+jGcX86YlACpG7UR5fwAXiWzxqWtBTg== dependencies: prosemirror-model "^1.21.0" -prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.33.9: - version "1.33.9" - resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.33.9.tgz#0ed61ae42405cfc9799bde4db86badbb1ad99b08" - integrity sha512-xV1A0Vz9cIcEnwmMhKKFAOkfIp8XmJRnaZoPqNXrPS7EK5n11Ov8V76KhR0RsfQd/SIzmWY+bg+M44A2Lx/Nnw== +prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.27.0, prosemirror-view@^1.31.0, prosemirror-view@^1.33.10: + version "1.34.3" + resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.34.3.tgz#24b5d2f9196580c23bbe04e9e7a6797cd3a049f6" + integrity sha512-mKZ54PrX19sSaQye+sef+YjBbNu2voNwLS1ivb6aD2IRmxRGW64HU9B644+7OfJStGLyxvOreKqEgfvXa91WIA== dependencies: prosemirror-model "^1.20.0" prosemirror-state "^1.0.0" @@ -26656,24 +26482,6 @@ proto-list@~1.2.1: resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== -protobufjs@^7.2.5, protobufjs@^7.3.0: - version "7.3.2" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.3.2.tgz#60f3b7624968868f6f739430cfbc8c9370e26df4" - integrity sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg== - dependencies: - "@protobufjs/aspromise" "^1.1.2" - "@protobufjs/base64" "^1.1.2" - "@protobufjs/codegen" "^2.0.4" - "@protobufjs/eventemitter" "^1.1.0" - "@protobufjs/fetch" "^1.1.0" - "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.0" - "@protobufjs/path" "^1.1.2" - "@protobufjs/pool" "^1.1.0" - "@protobufjs/utf8" "^1.1.0" - "@types/node" ">=13.7.0" - long "^5.0.0" - proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -27641,15 +27449,6 @@ require-from-string@^2.0.2: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== -require-in-the-middle@^7.1.1: - version "7.3.0" - resolved "https://registry.yarnpkg.com/require-in-the-middle/-/require-in-the-middle-7.3.0.tgz#ce64a1083647dc07b3273b348357efac8a9945c9" - integrity sha512-nQFEv9gRw6SJAwWD2LrL0NmQvAcO7FBwJbwmr2ttPAacfy0xuiOjE5zt+zM4xDyuyvUaxBi/9gb2SoCyNEVJcw== - dependencies: - debug "^4.1.1" - module-details-from-path "^1.0.3" - resolve "^1.22.1" - require-relative@^0.8.7: version "0.8.7" resolved "https://registry.yarnpkg.com/require-relative/-/require-relative-0.8.7.tgz#7999539fc9e047a37928fa196f8e1563dabd36de" @@ -28134,10 +27933,10 @@ sane@^4.0.0, sane@^4.1.0: minimist "^1.1.1" walker "~1.0.5" -sanitize-html@2.13.0: - version "2.13.0" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.13.0.tgz#71aedcdb777897985a4ea1877bf4f895a1170dae" - integrity sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA== +sanitize-html@2.13.1: + version "2.13.1" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.13.1.tgz#b4639b0a09574ab62b1b353cb99b1b87af742834" + integrity sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg== dependencies: deepmerge "^4.2.2" escape-string-regexp "^4.0.0" @@ -28258,14 +28057,7 @@ selderee@^0.6.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@7.5.3: - version "7.5.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e" - integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ== - dependencies: - lru-cache "^6.0.0" - -semver@7.6.3, semver@^7.0.0, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4, semver@^7.6.2: +semver@7.6.3, semver@^7.0.0, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.2: version "7.6.3" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== @@ -28318,6 +28110,14 @@ send@0.19.0: range-parser "~1.2.1" statuses "2.0.1" +sentry-testkit@5.0.9: + version "5.0.9" + resolved "https://registry.yarnpkg.com/sentry-testkit/-/sentry-testkit-5.0.9.tgz#28356612116aff9c7f3398913cef63d677ac1b4a" + integrity sha512-b9oa3juBM3EXKDU/v5aL4HEC41yGxQZY4pluFN+nvRJLyvHc+JQWPWxxkOPAAr1vGY/4zH+V9NGS+ojIQkc88w== + dependencies: + body-parser "^1.20.1" + express "^4.18.2" + seq-queue@^0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/seq-queue/-/seq-queue-0.0.5.tgz#d56812e1c017a6e4e7c3e3a37a1da6d78dd3c93e" @@ -28337,10 +28137,10 @@ serialize-javascript@^4.0.0: dependencies: randombytes "^2.1.0" -serialize-javascript@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" - integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== +serialize-javascript@^6.0.1, serialize-javascript@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== dependencies: randombytes "^2.1.0" @@ -28421,6 +28221,11 @@ shallow-clone@^3.0.0: dependencies: kind-of "^6.0.2" +shallow-equal@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da" + integrity sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA== + sharp@^0.32.0: version "0.32.6" resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.32.6.tgz#6ad30c0b7cd910df65d5f355f774aa4fce45732a" @@ -28469,11 +28274,6 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== -shimmer@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/shimmer/-/shimmer-1.2.1.tgz#610859f7de327b587efebf501fb43117f9aff337" - integrity sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw== - should-equal@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3" @@ -28592,6 +28392,11 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +sinon-chai@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/sinon-chai/-/sinon-chai-4.0.0.tgz#77d59d9f4a833f0d3a88249b4637acc72656fdfa" + integrity sha512-cWqO7O2I4XfJDWyWElAQ9D/dtdh5Mo0RHndsfiiYyjWnlPzBJdIvjCVURO4EjyYaC3BjV+ISNXCfTXPXTEIEWA== + sinon@15.2.0: version "15.2.0" resolved "https://registry.yarnpkg.com/sinon/-/sinon-15.2.0.tgz#5e44d4bc5a9b5d993871137fd3560bebfac27565" @@ -28616,6 +28421,18 @@ sinon@17.0.0: nise "^5.1.5" supports-color "^7.2.0" +sinon@19.0.2: + version "19.0.2" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-19.0.2.tgz#944cf771d22236aa84fc1ab70ce5bffc3a215dad" + integrity sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g== + dependencies: + "@sinonjs/commons" "^3.0.1" + "@sinonjs/fake-timers" "^13.0.2" + "@sinonjs/samsam" "^8.0.1" + diff "^7.0.0" + nise "^6.1.1" + supports-color "^7.2.0" + sinon@^17.0.1: version "17.0.1" resolved "https://registry.yarnpkg.com/sinon/-/sinon-17.0.1.tgz#26b8ef719261bf8df43f925924cccc96748e407a" @@ -29640,7 +29457,7 @@ supertest@6.3.3: methods "^1.1.2" superagent "^8.0.5" -supertest@^7.0.0: +supertest@7.0.0, supertest@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/supertest/-/supertest-7.0.0.tgz#cac53b3d6872a0b317980b2b0cfa820f09cd7634" integrity sha512-qlsr7fIC0lSddmA3tzojvzubYxvlGtzumcdHgPwbFWMISQwL22MhM2Y3LNt+6w9Yyx7559VW5ab70dgphm8qQA== @@ -29819,10 +29636,10 @@ table@^6.0.9, table@^6.8.1: string-width "^4.2.3" strip-ansi "^6.0.1" -tailwindcss@3.4.12: - version "3.4.12" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.12.tgz#fd3b67c6d2c04d9d7bfa13e3fc70ccef9fef0455" - integrity sha512-Htf/gHj2+soPb9UayUNci/Ja3d8pTmu9ONTfh4QY8r3MATTZOzmv6UYWF7ZwikEIC8okpfqmGqrmDehua8mF8w== +tailwindcss@3.4.13: + version "3.4.13" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.13.tgz#3d11e5510660f99df4f1bfb2d78434666cb8f831" + integrity sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw== dependencies: "@alloc/quick-lru" "^5.2.0" arg "^5.0.2" @@ -29927,6 +29744,13 @@ tarn@^3.0.2: resolved "https://registry.yarnpkg.com/tarn/-/tarn-3.0.2.tgz#73b6140fbb881b71559c4f8bfde3d9a4b3d27693" integrity sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ== +tdigest@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.2.tgz#96c64bac4ff10746b910b0e23b515794e12faced" + integrity sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA== + dependencies: + bintrees "1.0.2" + teex@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/teex/-/teex-1.0.1.tgz#b8fa7245ef8e8effa8078281946c85ab780a0b12" @@ -29998,10 +29822,10 @@ terser-webpack-plugin@^5.3.10: serialize-javascript "^6.0.1" terser "^5.26.0" -terser@5.33.0, terser@^5.26.0, terser@^5.7.0: - version "5.33.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.33.0.tgz#8f9149538c7468ffcb1246cfec603c16720d2db1" - integrity sha512-JuPVaB7s1gdFKPKTelwUyRq5Sid2A3Gko2S0PncwdBq7kN9Ti9HPWDQ06MPsEDGsZeVESjKEnyGy68quBk1w6g== +terser@5.34.1, terser@^5.26.0, terser@^5.7.0: + version "5.34.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.34.1.tgz#af40386bdbe54af0d063e0670afd55c3105abeb6" + integrity sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -30026,6 +29850,15 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +test-exclude@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-7.0.1.tgz#20b3ba4906ac20994e275bbcafd68d510264c2a2" + integrity sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^10.4.1" + minimatch "^9.0.4" + testem@3.15.1, testem@^3.2.0: version "3.15.1" resolved "https://registry.yarnpkg.com/testem/-/testem-3.15.1.tgz#bb39b41b6928bf347fbb9ea1bfe1e085b15ecb7e" @@ -30540,11 +30373,16 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8: +type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-detect@^4.0.0, type-detect@^4.0.5, type-detect@^4.0.8, type-detect@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c" + integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw== + type-fest@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" @@ -30629,11 +30467,16 @@ typescript@5.2.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== -typescript@5.4.5, typescript@^5.0.4: +typescript@5.4.5: version "5.4.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== +typescript@5.6.2, typescript@^5.0.4: + version "5.6.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0" + integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw== + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" @@ -31023,6 +30866,11 @@ use-callback-ref@^1.3.0: dependencies: tslib "^2.0.0" +use-debounce@10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-10.0.3.tgz#636094a37f7aa2bcc77b26b961481a0b571bf7ea" + integrity sha512-DxQSI9ZKso689WM1mjgGU3ozcxU1TJElBJ3X6S4SMzMNcm2lVH0AHmyXB+K7ewjz2BSUKJTDqTcwtSMRfB89dg== + use-isomorphic-layout-effect@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" @@ -31128,7 +30976,7 @@ v8-compile-cache-lib@^3.0.1: resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== -v8-compile-cache@2.3.0, v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0: +v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== @@ -31562,10 +31410,10 @@ webpack-virtual-modules@^0.5.0: resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz#362f14738a56dae107937ab98ea7062e8bdd3b6c" integrity sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw== -webpack@5.94.0: - version "5.94.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.94.0.tgz#77a6089c716e7ab90c1c67574a28da518a20970f" - integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg== +webpack@5.95.0: + version "5.95.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.95.0.tgz#8fd8c454fa60dad186fbe36c400a55848307b4c0" + integrity sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q== dependencies: "@types/estree" "^1.0.5" "@webassemblyjs/ast" "^1.12.1" @@ -31808,7 +31656,7 @@ workerpool@^3.1.1: object-assign "4.1.1" rsvp "^4.8.4" -workerpool@^6.0.2, workerpool@^6.0.3, workerpool@^6.1.5, workerpool@^6.4.0: +workerpool@^6.0.2, workerpool@^6.0.3, workerpool@^6.1.5, workerpool@^6.4.0, workerpool@^6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.5.1.tgz#060f73b39d0caf97c6db64da004cd01b4c099544" integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA== @@ -32037,7 +31885,7 @@ yargs-parser@^20.2.2, yargs-parser@^20.2.9: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-unparser@2.0.0: +yargs-unparser@2.0.0, yargs-unparser@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==