Compare commits

..

38 Commits

Author SHA1 Message Date
schlagmichdoch ccb2170287 Increase version to v1.10.3 2024-01-03 17:18:04 +01:00
schlagmichdoch 3454eebf37 Merge branch 'fix-small-issues' 2024-01-03 17:15:20 +01:00
schlagmichdoch b6203288bf Merge pull request #234 from schlagmichdoch/dependabot/npm_and_yarn/ws-8.16.0
Bump ws from 8.15.0 to 8.16.0
2024-01-03 17:14:47 +01:00
schlagmichdoch bea0fa5b9c Fix color of URLs on receive text dialog 2024-01-03 17:11:38 +01:00
schlagmichdoch 48090ec41c Fix x-paper width (fixed #233) 2024-01-03 16:54:43 +01:00
schlagmichdoch 229084fab3 Properly style indented text via css 2024-01-03 16:54:01 +01:00
schlagmichdoch d58f380565 Prevent executing _onCopy() when text is selected on receive text dialog 2024-01-03 16:53:09 +01:00
schlagmichdoch 676c68b6e7 Clear text field when closing receive text dialog 2024-01-03 16:52:26 +01:00
schlagmichdoch dd0dc21db5 Fix replacement of sent URLs with actual links (fixes #231) 2024-01-03 16:51:44 +01:00
dependabot[bot] 4e72339479 Bump ws from 8.15.0 to 8.16.0
Bumps [ws](https://github.com/websockets/ws) from 8.15.0 to 8.16.0.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.15.0...8.16.0)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-01 04:45:23 +00:00
schlagmichdoch c3e92d7d4c Increase version to v1.10.2 2023-12-15 23:54:20 +01:00
schlagmichdoch b90924af68 Merge branch 'master' into translate 2023-12-15 23:51:11 +01:00
schlagmichdoch 4f80ab4401 Merge pull request #226 from schlagmichdoch/parallelize_asset_loading
Parallelize and speed up loading of deferred assets, refactor code, and fix `RTC_CONFIG` env var
2023-12-15 23:49:54 +01:00
schlagmichdoch f299c90f47 Merge pull request #224 from weblate/weblate-pairdrop-pairdrop-spa
Translations update from Hosted Weblate
2023-12-15 23:47:25 +01:00
schlagmichdoch 6737dcacf7 Defer scripts and prevent deferred stylesheets from being render blocking 2023-12-15 23:40:30 +01:00
Hosted Weblate 5d39bf4a76 Translated using Weblate (Kannada)
Currently translated at 100.0% (161 of 161 strings)

Co-authored-by: Chethan <76928501+ch3thanhs@users.noreply.github.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/kn/
Translation: PairDrop/pairdrop-spa
2023-12-15 21:08:45 +00:00
Hosted Weblate 5c70c873ab Translated using Weblate (Spanish)
Currently translated at 100.0% (166 of 166 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/es/
Translation: PairDrop/pairdrop-spa
2023-12-15 21:08:45 +00:00
Hosted Weblate b336c75b72 Translated using Weblate (German)
Currently translated at 100.0% (166 of 166 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: schlagmichdoch <sonnig-02.hieven@icloud.com>
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/de/
Translation: PairDrop/pairdrop-spa
2023-12-15 21:08:44 +00:00
Hosted Weblate b3f5619f2d Translated using Weblate (Turkish)
Currently translated at 100.0% (161 of 161 strings)

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Æthereal <fr.izgi.kn@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/tr/
Translation: PairDrop/pairdrop-spa
2023-12-15 21:08:44 +00:00
schlagmichdoch 939ca3d35d Enable Kannada translation 2023-12-15 22:08:25 +01:00
schlagmichdoch c2ee459231 Fix error if env var RTC_CONFIG=false (as in default docker-compose.yml and docs) 2023-12-15 21:22:33 +01:00
schlagmichdoch c08b324d6a Refactor localization.js 2023-12-15 21:20:35 +01:00
schlagmichdoch d3a623d352 Refactor for loops to specify imagesOnly to Array.prototype.every() 2023-12-15 21:19:56 +01:00
schlagmichdoch d8f9532039 Parallelize asset loading 2023-12-15 19:39:26 +01:00
schlagmichdoch 9847feeb52 Fix SIGNALING_SERVER documentation 2023-12-13 18:50:33 +01:00
schlagmichdoch ae75cdbc62 Increase version to v1.10.1 2023-12-13 18:15:09 +01:00
schlagmichdoch e2299f1d0f Add documentation for the customizable about pairdrop buttons 2023-12-13 18:10:27 +01:00
schlagmichdoch e06fa47c96 Implement customizable buttons via env vars (fixes #214) 2023-12-13 17:40:48 +01:00
schlagmichdoch 09451caf86 Fix missing English translation for the title of the header expand button 2023-12-13 17:38:19 +01:00
schlagmichdoch 16921cb855 Merge branch 'master' into translate 2023-12-13 17:33:09 +01:00
schlagmichdoch 9b8d824bfc Enable Catalan translation (fixes #217) 2023-12-13 17:33:02 +01:00
schlagmichdoch 211328c2f7 Add Brazilian Portuguese to file list to be cached in service-worker.js 2023-12-13 17:26:21 +01:00
schlagmichdoch 224e6f0db9 Merge pull request #222 from weblate/weblate-pairdrop-pairdrop-spa
Translations update from Hosted Weblate
2023-12-13 17:22:51 +01:00
Hosted Weblate a3690994b8 Translated using Weblate (Catalan)
Currently translated at 100.0% (161 of 161 strings)

Co-authored-by: gmassipg <gerardmassipgil@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/ca/
Translation: PairDrop/pairdrop-spa
2023-12-13 17:22:09 +01:00
Hosted Weblate 866d8a0e16 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (161 of 161 strings)

Co-authored-by: Marcelo Mendonçca Miranda <marcelommira@live.com>
Translate-URL: https://hosted.weblate.org/projects/pairdrop/pairdrop-spa/pt_BR/
Translation: PairDrop/pairdrop-spa
2023-12-13 17:22:09 +01:00
schlagmichdoch 132b2ffa65 Rename deferred style sheet 2023-12-13 17:21:21 +01:00
schlagmichdoch b084a9b83b Preload font and add font-display: swap; to increase loading speed 2023-12-13 17:20:46 +01:00
schlagmichdoch 77f2866893 add write permission to workflow zip-release 2023-12-12 19:48:39 +01:00
22 changed files with 623 additions and 170 deletions
+2 -2
View File
@@ -36,7 +36,7 @@ If applicable, add screenshots to help explain your problem.
**Bug occurs on official PairDrop instance https://pairdrop.net/** **Bug occurs on official PairDrop instance https://pairdrop.net/**
No | Yes No | Yes
Version: v1.10.0 Version: v1.10.3
**Bug occurs on self-hosted PairDrop instance** **Bug occurs on self-hosted PairDrop instance**
No | Yes No | Yes
@@ -44,7 +44,7 @@ No | Yes
**Self-Hosted Setup** **Self-Hosted Setup**
Proxy: Nginx | Apache2 Proxy: Nginx | Apache2
Deployment: docker run | docker compose | npm run start:prod Deployment: docker run | docker compose | npm run start:prod
Version: v1.10.0 Version: v1.10.3
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.
+2
View File
@@ -19,6 +19,8 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
steps: steps:
- uses: actions/checkout@master - uses: actions/checkout@master
- name: Archive Release - name: Archive Release
+42 -3
View File
@@ -399,7 +399,8 @@ RTC_CONFIG="rtc_config.json"
You can host an instance that uses another signaling server You can host an instance that uses another signaling server
This can be useful if you don't want to trust the client files that are hosted on another instance but still want to connect to devices that use https://pairdrop.net. This can be useful if you don't want to trust the client files that are hosted on another instance but still want to connect to devices that use https://pairdrop.net.
### Host Websocket Server (for VPN)
### Specify Signaling Server
```bash ```bash
SIGNALING_SERVER="pairdrop.net" SIGNALING_SERVER="pairdrop.net"
@@ -415,10 +416,48 @@ SIGNALING_SERVER="pairdrop.net"
> E.g. host your own client files under *pairdrop.your-domain.com* but use the official signaling server under *pairdrop.net* > E.g. host your own client files under *pairdrop.your-domain.com* but use the official signaling server under *pairdrop.net*
> This way devices connecting to *pairdrop.your-domain.com* and *pairdrop.net* can discover each other. > This way devices connecting to *pairdrop.your-domain.com* and *pairdrop.net* can discover each other.
> >
> Beware that the version of your PairDrop server is compatible with the version of the signaling server. > Beware that the version of your PairDrop server must be compatible with the version of the signaling server.
> >
> `WS_SERVER` must be a valid url without the protocol prefix. > `SIGNALING_SERVER` must be a valid url without the protocol prefix.
> Examples of valid values: `pairdrop.net`, `pairdrop.your-domain.com:3000`, `your-domain.com/pairdrop` > Examples of valid values: `pairdrop.net`, `pairdrop.your-domain.com:3000`, `your-domain.com/pairdrop`
<br>
### Customizable buttons for the _About PairDrop_ page
```bash
DONATION_BUTTON_ACTIVE=true
DONATION_BUTTON_LINK="https://www.buymeacoffee.com/pairdrop"
DONATION_BUTTON_TITLE="Buy me a coffee"
TWITTER_BUTTON_ACTIVE=true
TWITTER_BUTTON_LINK="https://twitter.com/account"
TWITTER_BUTTON_TITLE="Find me on Twitter"
MASTODON_BUTTON_ACTIVE=true
MASTODON_BUTTON_LINK="https://mastodon.social/account"
MASTODON_BUTTON_TITLE="Find me on Mastodon"
BLUESKY_BUTTON_ACTIVE=true
BLUESKY_BUTTON_LINK="https://bsky.app/profile/account"
BLUESKY_BUTTON_TITLE="Find me on Bluesky"
CUSTOM_BUTTON_ACTIVE=true
CUSTOM_BUTTON_LINK="https://your-custom-social-network.net/account"
CUSTOM_BUTTON_TITLE="Find me on this custom social network"
PRIVACYPOLICY_BUTTON_ACTIVE=true
PRIVACYPOLICY_BUTTON_LINK="https://link-to-your-privacy-policy.net"
PRIVACYPOLICY_BUTTON_TITLE="Open our privacy policy"
```
> Default: unset
>
> By default, clients will show the default button configuration: GitHub, BuyMeACoffee, Twitter, and FAQ on GitHub.
>
> The GitHub and FAQ on GitHub buttons are essential, so they are always shown.
>
> The other buttons can be customized:
>
> * `*_BUTTON_ACTIVE`: set this to `true` to show a natively hidden button or to `false` to hide a normally shown button
> * `*_BUTTON_LINK`: set this to any URL to overwrite the href attribute of the button
> * `*_BUTTON_TITLE`: set this to overwrite the hover title of the button. This will prevent the title from being translated.
<br> <br>
## Healthcheck ## Healthcheck
+9 -9
View File
@@ -1,19 +1,19 @@
{ {
"name": "pairdrop", "name": "pairdrop",
"version": "1.10.0", "version": "1.10.3",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pairdrop", "name": "pairdrop",
"version": "1.10.0", "version": "1.10.3",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"express-rate-limit": "^7.1.5", "express-rate-limit": "^7.1.5",
"ua-parser-js": "^1.0.37", "ua-parser-js": "^1.0.37",
"unique-names-generator": "^4.3.0", "unique-names-generator": "^4.3.0",
"ws": "^8.15.0" "ws": "^8.16.0"
}, },
"engines": { "engines": {
"node": ">=15" "node": ">=15"
@@ -640,9 +640,9 @@
} }
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.15.0", "version": "8.16.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.15.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
"integrity": "sha512-H/Z3H55mrcrgjFwI+5jKavgXvwQLtfPCUEp6pi35VhoB0pfcHnSoyuTzkBEZpzq49g1193CUEwIvmsjcotenYw==", "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
}, },
@@ -1102,9 +1102,9 @@
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
}, },
"ws": { "ws": {
"version": "8.15.0", "version": "8.16.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.15.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz",
"integrity": "sha512-H/Z3H55mrcrgjFwI+5jKavgXvwQLtfPCUEp6pi35VhoB0pfcHnSoyuTzkBEZpzq49g1193CUEwIvmsjcotenYw==", "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==",
"requires": {} "requires": {}
} }
} }
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "pairdrop", "name": "pairdrop",
"version": "1.10.0", "version": "1.10.3",
"type": "module", "type": "module",
"description": "", "description": "",
"main": "server/index.js", "main": "server/index.js",
@@ -15,7 +15,7 @@
"express-rate-limit": "^7.1.5", "express-rate-limit": "^7.1.5",
"ua-parser-js": "^1.0.37", "ua-parser-js": "^1.0.37",
"unique-names-generator": "^4.3.0", "unique-names-generator": "^4.3.0",
"ws": "^8.15.0" "ws": "^8.16.0"
}, },
"engines": { "engines": {
"node": ">=15" "node": ">=15"
+58 -11
View File
@@ -35,6 +35,7 @@
<meta property="og:image" content="images/logo_transparent_512x512.png"> <meta property="og:image" content="images/logo_transparent_512x512.png">
<!-- Resources --> <!-- Resources -->
<link rel="preload" href="lang/en.json" as="fetch"> <link rel="preload" href="lang/en.json" as="fetch">
<link rel="preload" href="fonts/OpenSans/static/OpenSans-Medium.ttf" as="font" type="font/ttf" crossorigin>
<link rel="stylesheet" type="text/css" href="styles/styles-main.css"> <link rel="stylesheet" type="text/css" href="styles/styles-main.css">
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
</head> </head>
@@ -191,6 +192,11 @@
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<span>(Arabic)</span> <span>(Arabic)</span>
</button> </button>
<button class="btn fw wrap" value="ca">
<span>Català</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<span>(Catalan)</span>
</button>
<button class="btn fw wrap" value="de"> <button class="btn fw wrap" value="de">
<span>Deutsch</span> <span>Deutsch</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
@@ -219,6 +225,11 @@
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<span>(Italian)</span> <span>(Italian)</span>
</button> </button>
<button class="btn fw wrap" value="kn">
<span>ಕನ್ನಡ</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
<span>(Kannada)</span>
</button>
<button class="btn fw wrap" value="nl"> <button class="btn fw wrap" value="nl">
<span>Nederlands</span> <span>Nederlands</span>
<span>&nbsp;&nbsp;-&nbsp;&nbsp;</span> <span>&nbsp;&nbsp;-&nbsp;&nbsp;</span>
@@ -571,7 +582,7 @@
</svg> </svg>
<div class="title-wrapper" dir="ltr"> <div class="title-wrapper" dir="ltr">
<h1>PairDrop</h1> <h1>PairDrop</h1>
<div class="font-subheading">v1.10.0</div> <div class="font-subheading">v1.10.3</div>
</div> </div>
<div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text"></div> <div class="font-subheading" data-i18n-key="about.claim" data-i18n-attrs="text"></div>
<div class="row"> <div class="row">
@@ -580,16 +591,36 @@
<use xlink:href="#github"></use> <use xlink:href="#github"></use>
</svg> </svg>
</a> </a>
<a class="icon-button" target="_blank" href="https://www.buymeacoffee.com/pairdrop" rel="noreferrer" data-i18n-key="about.buy-me-a-coffee" data-i18n-attrs="title"> <a class="icon-button" id="donation-btn" target="_blank" href="https://www.buymeacoffee.com/pairdrop" rel="noreferrer" data-i18n-key="about.buy-me-a-coffee" data-i18n-attrs="title">
<svg class="icon"> <svg class="icon">
<use xlink:href="#monetarization"></use> <use xlink:href="#donation"></use>
</svg> </svg>
</a> </a>
<a class="icon-button" target="_blank" href="https://twitter.com/intent/tweet?text=https%3A%2F%2Fpairdrop.net%20by%20https%3A%2F%2Fgithub.com%2Fschlagmichdoch%2F&amp;" rel="noreferrer" data-i18n-key="about.tweet" data-i18n-attrs="title"> <a class="icon-button" id="twitter-btn" target="_blank" href="https://twitter.com/intent/tweet?text=https%3A%2F%2Fpairdrop.net%20by%20https%3A%2F%2Fgithub.com%2Fschlagmichdoch%2F&amp;" rel="noreferrer" data-i18n-key="about.tweet" data-i18n-attrs="title">
<svg class="icon"> <svg class="icon">
<use xlink:href="#twitter"></use> <use xlink:href="#twitter"></use>
</svg> </svg>
</a> </a>
<a class="icon-button" id="mastodon-btn" target="_blank" rel="noreferrer" data-i18n-key="about.mastodon" data-i18n-attrs="title" hidden>
<svg class="icon">
<use xlink:href="#mastodon"></use>
</svg>
</a>
<a class="icon-button" id="bluesky-btn" target="_blank" rel="noreferrer" data-i18n-key="about.bluesky" data-i18n-attrs="title" hidden>
<svg class="icon">
<use xlink:href="#bluesky"></use>
</svg>
</a>
<a class="icon-button" id="custom-btn" target="_blank" rel="noreferrer" data-i18n-key="about.custom" data-i18n-attrs="title" hidden>
<svg class="icon">
<use xlink:href="#custom"></use>
</svg>
</a>
<a class="icon-button" id="privacypolicy-btn" target="_blank" rel="noreferrer" data-i18n-key="about.privacypolicy" data-i18n-attrs="title" hidden>
<svg class="icon">
<use xlink:href="#privacypolicy"></use>
</svg>
</a>
<a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop/blob/master/docs/faq.md" rel="noreferrer" data-i18n-key="about.faq" data-i18n-attrs="title"> <a class="icon-button" target="_blank" href="https://github.com/schlagmichdoch/pairdrop/blob/master/docs/faq.md" rel="noreferrer" data-i18n-key="about.faq" data-i18n-attrs="title">
<svg class="icon"> <svg class="icon">
<use xlink:href="#help-outline"></use> <use xlink:href="#help-outline"></use>
@@ -637,7 +668,7 @@
<path d="M18 1.01L8 1c-1.1 0-2 .9-2 2v3h2V5h10v14H8v-1H6v3c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM10 15h2V8H5v2h3.59L3 15.59 4.41 17 10 11.41z"></path> <path d="M18 1.01L8 1c-1.1 0-2 .9-2 2v3h2V5h10v14H8v-1H6v3c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V3c0-1.1-.9-1.99-2-1.99zM10 15h2V8H5v2h3.59L3 15.59 4.41 17 10 11.41z"></path>
<path fill="none" d="M0 0h24v24H0V0z"></path> <path fill="none" d="M0 0h24v24H0V0z"></path>
</symbol> </symbol>
<symbol id="monetarization"> <symbol id="donation">
<path d="M0 0h24v24H0z" fill="none"></path> <path d="M0 0h24v24H0z" fill="none"></path>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z"></path> <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z"></path>
</symbol> </symbol>
@@ -687,15 +718,31 @@
<path d="M320 464c8.8 0 16-7.2 16-16V160H256c-17.7 0-32-14.3-32-32V48H64c-8.8 0-16 7.2-16 16V448c0 8.8 7.2 16 16 16H320zM0 64C0 28.7 28.7 0 64 0H229.5c17 0 33.3 6.7 45.3 18.7l90.5 90.5c12 12 18.7 28.3 18.7 45.3V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V64z"></path> <path d="M320 464c8.8 0 16-7.2 16-16V160H256c-17.7 0-32-14.3-32-32V48H64c-8.8 0-16 7.2-16 16V448c0 8.8 7.2 16 16 16H320zM0 64C0 28.7 28.7 0 64 0H229.5c17 0 33.3 6.7 45.3 18.7l90.5 90.5c12 12 18.7 28.3 18.7 45.3V448c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V64z"></path>
</symbol> </symbol>
<symbol id="caret" viewBox="0 0 320 512"> <symbol id="caret" viewBox="0 0 320 512">
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M137.4 374.6c12.5 12.5 32.8 12.5 45.3 0l128-128c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8L32 192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l128 128z"></path> <!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->
<path d="M137.4 374.6c12.5 12.5 32.8 12.5 45.3 0l128-128c9.2-9.2 11.9-22.9 6.9-34.9s-16.6-19.8-29.6-19.8L32 192c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9l128 128z"></path>
</symbol>
<symbol id="mastodon" viewBox="0 0 448 512">
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->
<path d="M433 179.1c0-97.2-63.7-125.7-63.7-125.7-62.5-28.7-228.6-28.4-290.5 0 0 0-63.7 28.5-63.7 125.7 0 115.7-6.6 259.4 105.6 289.1 40.5 10.7 75.3 13 103.3 11.4 50.8-2.8 79.3-18.1 79.3-18.1l-1.7-36.9s-36.3 11.4-77.1 10.1c-40.4-1.4-83-4.4-89.6-54a102.5 102.5 0 0 1 -.9-13.9c85.6 20.9 158.7 9.1 178.8 6.7 56.1-6.7 105-41.3 111.2-72.9 9.8-49.8 9-121.5 9-121.5zm-75.1 125.2h-46.6v-114.2c0-49.7-64-51.6-64 6.9v62.5h-46.3V197c0-58.5-64-56.6-64-6.9v114.2H90.2c0-122.1-5.2-147.9 18.4-175 25.9-28.9 79.8-30.8 103.8 6.1l11.6 19.5 11.6-19.5c24.1-37.1 78.1-34.8 103.8-6.1 23.7 27.3 18.4 53 18.4 175z"></path>
</symbol>
<symbol id="bluesky" viewBox="0 0 448 512">
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->
<path d="M0 96C0 60.7 28.7 32 64 32H384c35.3 0 64 28.7 64 64V416c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V96z"></path>
</symbol>
<symbol id="custom" viewBox="0 0 512 512">
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->
<path d="M418.4 157.9c35.3-8.3 61.6-40 61.6-77.9c0-44.2-35.8-80-80-80c-43.4 0-78.7 34.5-80 77.5L136.2 151.1C121.7 136.8 101.9 128 80 128c-44.2 0-80 35.8-80 80s35.8 80 80 80c12.2 0 23.8-2.7 34.1-7.6L259.7 407.8c-2.4 7.6-3.7 15.8-3.7 24.2c0 44.2 35.8 80 80 80s80-35.8 80-80c0-27.7-14-52.1-35.4-66.4l37.8-207.7zM156.3 232.2c2.2-6.9 3.5-14.2 3.7-21.7l183.8-73.5c3.6 3.5 7.4 6.7 11.6 9.5L317.6 354.1c-5.5 1.3-10.8 3.1-15.8 5.5L156.3 232.2z"></path>
</symbol>
<symbol id="privacypolicy" viewBox="0 0 512 512">
<!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.-->
<path d="M256 0c4.6 0 9.2 1 13.4 2.9L457.7 82.8c22 9.3 38.4 31 38.3 57.2c-.5 99.2-41.3 280.7-213.6 363.2c-16.7 8-36.1 8-52.8 0C57.3 420.7 16.5 239.2 16 140c-.1-26.2 16.3-47.9 38.3-57.2L242.7 2.9C246.8 1 251.4 0 256 0zm0 66.8V444.8C394 378 431.1 230.1 432 141.4L256 66.8l0 0z"></path>
</symbol> </symbol>
</svg> </svg>
<!-- Scripts --> <!-- Scripts -->
<script src="scripts/localization.js"></script> <script src="scripts/localization.js" defer></script>
<script src="scripts/persistent-storage.js"></script> <script src="scripts/persistent-storage.js" defer></script>
<script src="scripts/ui-main.js"></script> <script src="scripts/ui-main.js" defer></script>
<script src="scripts/main.js"></script> <script src="scripts/main.js" defer></script>
<!-- Sounds --> <!-- Sounds -->
<audio id="blop" autobuffer="true"> <audio id="blop" autobuffer="true">
<source src="sounds/blop.mp3" type="audio/mpeg"> <source src="sounds/blop.mp3" type="audio/mpeg">
+154 -1
View File
@@ -6,7 +6,16 @@
"no-peers-title": "Obre PairDrop a altres dispositius per enviar arxius", "no-peers-title": "Obre PairDrop a altres dispositius per enviar arxius",
"x-instructions_data-drop-peer": "Allibera per enviar a un company", "x-instructions_data-drop-peer": "Allibera per enviar a un company",
"x-instructions_data-drop-bg": "Allibera per seleccionar recipient", "x-instructions_data-drop-bg": "Allibera per seleccionar recipient",
"no-peers_data-drop-bg": "Allibera per seleccionar destinatari" "no-peers_data-drop-bg": "Allibera per seleccionar destinatari",
"activate-share-mode-base": "Obre PairDrop a altres dispositius per enviar",
"activate-share-mode-shared-files-plural": "{{count}} fitxers compartits",
"x-instructions-share-mode_desktop": "Clica per enviar {{descriptor}}",
"activate-share-mode-shared-file": "fitxer compartit",
"activate-share-mode-and-other-file": "i 1 altre fitxer",
"x-instructions-share-mode_mobile": "Toca per enviar {{descriptor}}",
"activate-share-mode-and-other-files-plural": "i {{count}} fitxers més",
"activate-share-mode-shared-text": "text compartit",
"webrtc-requirement": "Per utilitzar aquesta instància de PairDrop cal habilitar WebRTC!"
}, },
"header": { "header": {
"theme-auto_title": "Adapta el tema al del sistema automàticament", "theme-auto_title": "Adapta el tema al del sistema automàticament",
@@ -22,5 +31,149 @@
"about_title": "Sobre PairDrop", "about_title": "Sobre PairDrop",
"about_aria-label": "Obre Sobre PairDrop", "about_aria-label": "Obre Sobre PairDrop",
"theme-light_title": "Utilitza sempre el mode clar" "theme-light_title": "Utilitza sempre el mode clar"
},
"dialogs": {
"message_placeholder": "Text",
"base64-paste-to-send": "Enganxa el contingut del porta-retalls aquí per compartir {{type}}",
"auto-accept-instructions-2": "per acceptar automàticament tots els fitxers enviats des d'aquell dispositiu.",
"receive-text-title": "Missatge Rebut",
"edit-paired-devices-title": "Edita els Dispositius Vinculats",
"cancel": "Cancel·lar",
"auto-accept-instructions-1": "Activar",
"pair-devices-title": "Vincula Dispositius Permanentment",
"download": "Descarregar",
"title-file": "Fitxer",
"close-toast_title": "Tanca notificació",
"base64-processing": "Processant…",
"decline": "Rebutjar",
"receive-title": "{{descriptor}} Rebut",
"share-text-checkbox": "Mostra sempre aquesta finestra de diàleg en compartir text",
"leave": "Marxar",
"message_title": "Introdueix el missatge a enviar",
"join": "Unir-se",
"title-image-plural": "Imatges",
"send": "Enviar",
"base64-title-files": "Compartir Fitxers",
"base64-tap-to-paste": "Toca aquí per compartir {{type}}",
"base64-text": "text",
"copy": "Copiar",
"file-other-description-image": "i 1 altra imatge",
"pair-devices-qr-code_title": "Clica per copiar l'enllaç per vincular aquest dispositiu",
"approve": "aprovar",
"temporary-public-room-title": "Sala Pública Temporal",
"base64-files": "fitxers",
"paired-device-removed": "Dispositiu vinculat eliminat.",
"has-sent": "ha enviat:",
"share-text-title": "Compartir Missatge de Text",
"file-other-description-file": "i 1 altre fitxer",
"public-room-qr-code_title": "Clica per copiar l'enllaç a la sala pública",
"close": "Tancar",
"system-language": "Idioma del Sistema",
"share-text-subtitle": "Edita el missatge abans d'enviar:",
"unpair": "Desvincular",
"title-image": "Imatge",
"file-other-description-file-plural": "i {{count}} altres fitxers",
"would-like-to-share": "voldria compartir",
"base64-title-text": "Compartir Text",
"send-message-to": "Per a:",
"language-selector-title": "Establir idioma",
"pair": "Vincular",
"hr-or": "O",
"scan-qr-code": "o escaneja el codi QR.",
"input-key-on-this-device": "Introdueix aquesta clau a un altre dispositiu",
"download-again": "Descarregar de nou",
"accept": "Acceptar",
"paired-devices-wrapper_data-empty": "No hi ha dispositius vinculats.",
"enter-key-from-another-device": "Introdueix la clau d'un altre dispositiu aquí.",
"share": "Compartir",
"auto-accept": "auto-acceptar",
"title-file-plural": "Fitxers",
"send-message-title": "Enviar Missatge",
"input-room-id-on-another-device": "Introdueix aquest ID de sala a un altre dispositiu",
"file-other-description-image-plural": "i {{count}} altres imatges",
"enter-room-id-from-another-device": "Introdueix l'ID de sala d'un altre dispositiu per unir-t'hi."
},
"footer": {
"webrtc": "si WebRTC no està disponible.",
"public-room-devices_title": "Pots ser descobert per dispositius en aquesta sala pública independentment de la xarxa.",
"display-name_data-placeholder": "Carregant…",
"display-name_title": "Edita el nom del teu dispositiu permanentment",
"traffic": "El trànsit és",
"paired-devices_title": "Pots ser descobert per dispositius emparellats en qualsevol moment, independentment de la xarxa.",
"public-room-devices": "a la sala {{roomId}}",
"paired-devices": "per dispositius vinculats",
"on-this-network": "En aquesta xarxa",
"routed": "encaminat a través del servidor",
"discovery": "Pots ser descobert:",
"on-this-network_title": "Pots ser descobert per qualsevol usuari en aquesta xarxa.",
"known-as": "Ets conegut com a:"
},
"notifications": {
"request-title": "{{name}} voldria transferir {{count}} {{descriptor}}",
"unfinished-transfers-warning": "Hi ha transferències pendents. Estàs segur que vols tancar PairDrop?",
"message-received": "Missatge rebut per {{name}} - Fes clic per copiar",
"notifications-permissions-error": "El permís per notificacions ha estat bloquejat, ja que l'usuari ha refusat la sol·licitud diverses vegades. Això es pot restablir a Informació de Pàgina, a on es pot accedir clicant la icona amb el cadenat que hi ha al costat de la barra de l'URL.",
"rate-limit-join-key": "S'ha arribat al límit de ràtio. Espera 10 segons i torna-ho a intentar.",
"pair-url-copied-to-clipboard": "Enllaç per vincular aquest dispositiu copiat al porta-retalls",
"connecting": "Connectant…",
"pairing-key-invalidated": "Clau {{key}} invalidada",
"pairing-key-invalid": "Clau no vàlida",
"connected": "Connectat",
"pairing-not-persistent": "Els dispositius vinculats no són persistents",
"text-content-incorrect": "El contingut del text és incorrecte",
"message-transfer-completed": "Transferència de missatge completada",
"file-transfer-completed": "Transferència de fitxers completada",
"file-content-incorrect": "El contingut del fitxer és incorrecte",
"files-incorrect": "Els fitxers són incorrectes",
"selected-peer-left": "L'usuari seleccionat ha marxat",
"link-received": "Enllaç rebut per {{name}} - Fes clic per obrir",
"online": "Tornes a estar en línia",
"public-room-left": "Has sortit de la sala pública {{publicRoomId}}",
"copied-text": "Text copiat al porta-retalls",
"display-name-random-again": "El nom d'usuari ha estat generat novament generat aleatòriament",
"display-name-changed-permanently": "El nom d'usuari està canviat permanentment",
"copied-to-clipboard-error": "Còpia impossible. Copiar manualment.",
"pairing-success": "Dispositius vinculats",
"clipboard-content-incorrect": "El contingut del porta-retalls és incorrecte",
"display-name-changed-temporarily": "El nom d'usuari està canviat només per aquesta sessió",
"copied-to-clipboard": "Copiat al porta-retalls",
"offline": "No estàs en línia",
"pairing-tabs-error": "No es poden vincular dues pestanyes d'un navegador",
"public-room-id-invalid": "ID de sala no vàlid",
"click-to-download": "Clica per descarregar",
"pairing-cleared": "Tots els dispositius desvinculats",
"notifications-enabled": "Notificacions habilitades",
"online-requirement-pairing": "Has d'estar en línia per vincular dispositius",
"ios-memory-limit": "Tan sols és possible enviar fitxers de fins a 200 MB a iOS",
"online-requirement-public-room": "Cal que estiguis en línia per poder crear una sala pública",
"room-url-copied-to-clipboard": "Enllaç a la sala pública copiat al porta-retalls",
"copied-text-error": "L'escriptura al porta-retalls ha fallat. Copiar manualment!",
"download-successful": "{{descriptor}} descarregat",
"click-to-show": "Clica per mostrar"
},
"peer-ui": {
"processing": "Processant…",
"click-to-send-share-mode": "Clica per enviar {{descriptor}}",
"click-to-send": "Fes clic per enviar fitxers o fes clic dret per enviar un missatge",
"waiting": "Esperant…",
"connection-hash": "Per verificar la seguretat del xifratge de punta a punta, compara aquest número de seguretat en ambdós dispositius",
"preparing": "Preparant…",
"transferring": "Transferint…"
},
"about": {
"claim": "La manera més fàcil de compartir fitxers entre dispositius",
"tweet_title": "Tuiteja sobre PairDrop",
"close-about_aria-label": "Tanca Sobre PairDrop",
"buy-me-a-coffee_title": "Convida'm a un cafè!",
"github_title": "PairDrop a GitHub",
"faq_title": "Preguntes freqüents"
},
"document-titles": {
"file-transfer-requested": "Transferència de Fitxers Sol·licitada",
"image-transfer-requested": "Transferència d'Imatges Sol·licitada",
"message-received-plural": "{{count}} Missatges Rebuts",
"message-received": "Missatge Rebut",
"file-received": "Fitxer Rebut",
"file-received-plural": "{{count}} Fitxers Rebuts"
} }
} }
+7 -2
View File
@@ -12,7 +12,8 @@
"cancel-share-mode": "Fertig", "cancel-share-mode": "Fertig",
"language-selector_title": "Sprache Wählen", "language-selector_title": "Sprache Wählen",
"join-public-room_title": "Öffentlichen Raum temporär betreten", "join-public-room_title": "Öffentlichen Raum temporär betreten",
"edit-share-mode": "Bearbeiten" "edit-share-mode": "Bearbeiten",
"expand_title": "Schaltflächenzeile ausklappen"
}, },
"dialogs": { "dialogs": {
"share": "Teilen", "share": "Teilen",
@@ -81,7 +82,11 @@
"close-about_aria-label": "Schließe Über PairDrop", "close-about_aria-label": "Schließe Über PairDrop",
"github_title": "PairDrop auf GitHub", "github_title": "PairDrop auf GitHub",
"buy-me-a-coffee_title": "Kauf mir einen Kaffee!", "buy-me-a-coffee_title": "Kauf mir einen Kaffee!",
"claim": "Der einfachste Weg, Dateien zwischen Geräten zu übertragen" "claim": "Der einfachste Weg, Dateien zwischen Geräten zu übertragen",
"bluesky_title": "Folge uns auf BlueSky",
"privacypolicy_title": "Öffne unsere Datenschutzerklärung",
"mastodon_title": "Schreibe über PairDrop auf Mastodon",
"custom_title": "Folge uns"
}, },
"footer": { "footer": {
"known-as": "Du wirst angezeigt als:", "known-as": "Du wirst angezeigt als:",
+6 -1
View File
@@ -12,7 +12,8 @@
"edit-paired-devices_title": "Edit paired devices", "edit-paired-devices_title": "Edit paired devices",
"join-public-room_title": "Join public room temporarily", "join-public-room_title": "Join public room temporarily",
"cancel-share-mode": "Cancel", "cancel-share-mode": "Cancel",
"edit-share-mode": "Edit" "edit-share-mode": "Edit",
"expand_title": "Expand header button row"
}, },
"instructions": { "instructions": {
"no-peers_data-drop-bg": "Release to select recipient", "no-peers_data-drop-bg": "Release to select recipient",
@@ -114,6 +115,10 @@
"github_title": "PairDrop on GitHub", "github_title": "PairDrop on GitHub",
"buy-me-a-coffee_title": "Buy me a coffee!", "buy-me-a-coffee_title": "Buy me a coffee!",
"tweet_title": "Tweet about PairDrop", "tweet_title": "Tweet about PairDrop",
"mastodon_title": "Write about PairDrop on Mastodon",
"bluesky_title": "Follow us on BlueSky",
"custom_title": "Follow us",
"privacypolicy_title": "Open our privacy policy",
"faq_title": "Frequently asked questions" "faq_title": "Frequently asked questions"
}, },
"notifications": { "notifications": {
+25 -8
View File
@@ -11,7 +11,9 @@
"join-public-room_title": "Unirse a una sala pública temporalmente", "join-public-room_title": "Unirse a una sala pública temporalmente",
"notification_title": "Activar notificaciones", "notification_title": "Activar notificaciones",
"edit-paired-devices_title": "Editar dispositivos emparejados", "edit-paired-devices_title": "Editar dispositivos emparejados",
"theme-light_title": "Siempre usar tema claro" "theme-light_title": "Siempre usar tema claro",
"expand_title": "Ampliar la fila de botones de la cabecera",
"edit-share-mode": "Editar"
}, },
"footer": { "footer": {
"webrtc": "si WebRTC no está disponible.", "webrtc": "si WebRTC no está disponible.",
@@ -73,9 +75,9 @@
}, },
"instructions": { "instructions": {
"x-instructions_mobile": "Toque para enviar archivos o toque prologádamente para enviar un mensaje", "x-instructions_mobile": "Toque para enviar archivos o toque prologádamente para enviar un mensaje",
"x-instructions-share-mode_desktop": "Haga clic para enviar", "x-instructions-share-mode_desktop": "Haga clic para enviar {{descriptor}}",
"activate-share-mode-and-other-files-plural": "y {{count}} archivos diferentes", "activate-share-mode-and-other-files-plural": "y {{count}} archivos diferentes",
"x-instructions-share-mode_mobile": "Toca para enviar", "x-instructions-share-mode_mobile": "Toque para enviar {{descriptor}}",
"activate-share-mode-base": "Abra PairDrop en otros dispositivos para enviar", "activate-share-mode-base": "Abra PairDrop en otros dispositivos para enviar",
"no-peers-subtitle": "Empareje dispositivos o ingrese a una sala pública para que lo puedan encontrar en otras redes", "no-peers-subtitle": "Empareje dispositivos o ingrese a una sala pública para que lo puedan encontrar en otras redes",
"activate-share-mode-shared-text": "texto compartido", "activate-share-mode-shared-text": "texto compartido",
@@ -84,7 +86,10 @@
"x-instructions_data-drop-peer": "Liberar para enviar a un par", "x-instructions_data-drop-peer": "Liberar para enviar a un par",
"x-instructions_data-drop-bg": "Liberar para seleccionar destinatario", "x-instructions_data-drop-bg": "Liberar para seleccionar destinatario",
"no-peers_data-drop-bg": "Liberar para seleccionar destinatario", "no-peers_data-drop-bg": "Liberar para seleccionar destinatario",
"webrtc-requirement": "Para utilizar esta instancia de PairDrop, ¡WebRTC debe estar activado!" "webrtc-requirement": "Para utilizar esta instancia de PairDrop, ¡WebRTC debe estar activado!",
"activate-share-mode-shared-files-plural": "{{count}} archivos compartidos",
"activate-share-mode-shared-file": "archivo compartido",
"activate-share-mode-and-other-file": "y 1 archivo más"
}, },
"peer-ui": { "peer-ui": {
"processing": "Procesando…", "processing": "Procesando…",
@@ -96,7 +101,7 @@
"transferring": "Transferiendo…" "transferring": "Transferiendo…"
}, },
"dialogs": { "dialogs": {
"base64-paste-to-send": "Pegar aquí para enviar {{type}}", "base64-paste-to-send": "Pegar el portapapeles aquí para compartir {{type}}",
"auto-accept-instructions-2": "para aceptar automáticamente todos los archivos enviados desde ese dispositivo.", "auto-accept-instructions-2": "para aceptar automáticamente todos los archivos enviados desde ese dispositivo.",
"receive-text-title": "Mensaje Recibido", "receive-text-title": "Mensaje Recibido",
"edit-paired-devices-title": "Editar Dispositivos Emparejados", "edit-paired-devices-title": "Editar Dispositivos Emparejados",
@@ -112,7 +117,7 @@
"join": "Unirse", "join": "Unirse",
"title-image-plural": "Imágenes", "title-image-plural": "Imágenes",
"send": "Enviar", "send": "Enviar",
"base64-tap-to-paste": "Toca aquí para pegar {{type}}", "base64-tap-to-paste": "Pulse aquí para compartir {{type}}",
"base64-text": "texto", "base64-text": "texto",
"copy": "Copiar", "copy": "Copiar",
"file-other-description-image": "y una imagen mas", "file-other-description-image": "y una imagen mas",
@@ -146,7 +151,15 @@
"message_title": "Insertar el mensaje a enviar", "message_title": "Insertar el mensaje a enviar",
"pair-devices-qr-code_title": "Haz clic para copiar el enlace para emparejar este dispositivo", "pair-devices-qr-code_title": "Haz clic para copiar el enlace para emparejar este dispositivo",
"public-room-qr-code_title": "Haz clic para copiar el enlace a la sala pública", "public-room-qr-code_title": "Haz clic para copiar el enlace a la sala pública",
"message_placeholder": "Texto" "message_placeholder": "Texto",
"close-toast_title": "Cerrar la notificación",
"share-text-checkbox": "Mostrar siempre este cuadro de diálogo al compartir texto",
"base64-title-files": "Compartir archivos",
"approve": "aprobar",
"paired-device-removed": "Se ha eliminado el dispositivo emparejado.",
"share-text-title": "Compartir un mensaje de texto",
"share-text-subtitle": "Edita el mensaje antes de enviarlo:",
"base64-title-text": "Compartir el texto"
}, },
"about": { "about": {
"claim": "La forma más sencilla de transferir archivos entre dispositivos", "claim": "La forma más sencilla de transferir archivos entre dispositivos",
@@ -154,7 +167,11 @@
"close-about_aria-label": "Cerrar Sobre PairDrop", "close-about_aria-label": "Cerrar Sobre PairDrop",
"buy-me-a-coffee_title": "¡Cómprame un café!", "buy-me-a-coffee_title": "¡Cómprame un café!",
"github_title": "PairDrop en GitHub", "github_title": "PairDrop en GitHub",
"faq_title": "Preguntas frecuentes" "faq_title": "Preguntas frecuentes",
"bluesky_title": "Síganos en BlueSky",
"privacypolicy_title": "Abrir nuestra política de privacidad",
"mastodon_title": "Escriba sobre PairDrop en Mastodon",
"custom_title": "Síguenos en"
}, },
"document-titles": { "document-titles": {
"file-transfer-requested": "Transferencia de archivos solicitada", "file-transfer-requested": "Transferencia de archivos solicitada",
+25 -6
View File
@@ -11,11 +11,13 @@
"edit-paired-devices_title": "ಜೋಡಿಯಾಗಿರುವ ಸಾಧನಗಳನ್ನು ಎಡಿಟ್ ಮಾಡಿ", "edit-paired-devices_title": "ಜೋಡಿಯಾಗಿರುವ ಸಾಧನಗಳನ್ನು ಎಡಿಟ್ ಮಾಡಿ",
"language-selector_title": "ಭಾಷೆಯನ್ನು ಆಯ್ಕೆ ಮಾಡಿ", "language-selector_title": "ಭಾಷೆಯನ್ನು ಆಯ್ಕೆ ಮಾಡಿ",
"about_aria-label": "PairDrop ಕುರಿತು ಪುಟವನ್ನು ತೆರೆಯಿರಿ", "about_aria-label": "PairDrop ಕುರಿತು ಪುಟವನ್ನು ತೆರೆಯಿರಿ",
"theme-light_title": "ಯಾವಾಗಲೂ ಲೈಟ್ ಥೀಮ್ ಅನ್ನು ಬಳಸಿ" "theme-light_title": "ಯಾವಾಗಲೂ ಲೈಟ್ ಥೀಮ್ ಅನ್ನು ಬಳಸಿ",
"edit-share-mode": "ಎಡಿಟ್ ಮಾಡಿ",
"cancel-share-mode": "ರದ್ದುಗೊಳಿಸಿ"
}, },
"dialogs": { "dialogs": {
"message_placeholder": "ಪಠ್ಯ", "message_placeholder": "ಪಠ್ಯ",
"base64-paste-to-send": "{{type}} ಕಳುಹಿಸಲು ಇಲ್ಲಿ ಅಂಟಿಸಿ", "base64-paste-to-send": "{{type}} ಅನ್ನು ಹಂಚಿಕೊಳ್ಳಲು ಕ್ಲಿಪ್‌ಬೋರ್ಡ್‌ ಅನ್ನು ಇಲ್ಲಿ ಅಂಟಿಸಿ",
"auto-accept-instructions-2": "ಆ ಸಾಧನದಿಂದ ಕಳುಹಿಸಲಾದ ಎಲ್ಲಾ ಫೈಲ್‌ಗಳನ್ನು ಸ್ವಯಂಚಾಲಿತವಾಗಿ ಸ್ವೀಕರಿಸಲು.", "auto-accept-instructions-2": "ಆ ಸಾಧನದಿಂದ ಕಳುಹಿಸಲಾದ ಎಲ್ಲಾ ಫೈಲ್‌ಗಳನ್ನು ಸ್ವಯಂಚಾಲಿತವಾಗಿ ಸ್ವೀಕರಿಸಲು.",
"receive-text-title": "ಸಂದೇಶವನ್ನು ಸ್ವೀಕರಿಸಲಾಗಿದೆ", "receive-text-title": "ಸಂದೇಶವನ್ನು ಸ್ವೀಕರಿಸಲಾಗಿದೆ",
"edit-paired-devices-title": "ಜೋಡಿಯಾಗಿರುವ ಸಾಧನಗಳನ್ನು ಎಡಿಟ್ ಮಾಡಿ", "edit-paired-devices-title": "ಜೋಡಿಯಾಗಿರುವ ಸಾಧನಗಳನ್ನು ಎಡಿಟ್ ಮಾಡಿ",
@@ -32,7 +34,7 @@
"join": "ಸೇರಿಕೊಳ್ಳಿ", "join": "ಸೇರಿಕೊಳ್ಳಿ",
"title-image-plural": "ಚಿತ್ರಗಳು", "title-image-plural": "ಚಿತ್ರಗಳು",
"send": "ಕಳುಹಿಸಿ", "send": "ಕಳುಹಿಸಿ",
"base64-tap-to-paste": "{{type}} ಅಂಟಿಸಲು ಇಲ್ಲಿ ಟ್ಯಾಪ್ ಮಾಡಿ", "base64-tap-to-paste": "{{type}} ಹಂಚಿಕೊಳ್ಳಲು ಇಲ್ಲಿ ಟ್ಯಾಪ್ ಮಾಡಿ",
"base64-text": "ಪಠ್ಯ", "base64-text": "ಪಠ್ಯ",
"copy": "ನಕಲು ಮಾಡಿ", "copy": "ನಕಲು ಮಾಡಿ",
"file-other-description-image": "ಮತ್ತು ಇನ್ನೊಂದು ಚಿತ್ರ", "file-other-description-image": "ಮತ್ತು ಇನ್ನೊಂದು ಚಿತ್ರ",
@@ -64,7 +66,15 @@
"send-message-title": "ಸಂದೇಶ ಕಳುಹಿಸಿ", "send-message-title": "ಸಂದೇಶ ಕಳುಹಿಸಿ",
"input-room-id-on-another-device": "ಇನ್ನೊಂದು ಸಾಧನದಲ್ಲಿ ಈ ರೂಮ್ ಐಡಿಯನ್ನು ನಮೂದಿಸಿ", "input-room-id-on-another-device": "ಇನ್ನೊಂದು ಸಾಧನದಲ್ಲಿ ಈ ರೂಮ್ ಐಡಿಯನ್ನು ನಮೂದಿಸಿ",
"file-other-description-image-plural": "ಮತ್ತು {{count}} ಇತರ ಚಿತ್ರಗಳು", "file-other-description-image-plural": "ಮತ್ತು {{count}} ಇತರ ಚಿತ್ರಗಳು",
"enter-room-id-from-another-device": "ಕೊಠಡಿ ಸೇರಲು ಇನ್ನೊಂದು ಸಾಧನದಿಂದ ರೂಮ್ ಐಡಿ ನಮೂದಿಸಿ." "enter-room-id-from-another-device": "ಕೊಠಡಿ ಸೇರಲು ಇನ್ನೊಂದು ಸಾಧನದಿಂದ ರೂಮ್ ಐಡಿ ನಮೂದಿಸಿ.",
"close-toast_title": "ಅಧಿಸೂಚನೆಯನ್ನು ಮುಚ್ಚಿರಿ",
"share-text-checkbox": "ಪಠ್ಯವನ್ನು ಹಂಚಿಕೊಳ್ಳುವಾಗ ಯಾವಾಗಲೂ ಈ ಡೈಲಾಗ್ ಅನ್ನು ತೋರಿಸಿ",
"base64-title-files": "ಫೈಲ್‌ಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಿ",
"approve": "ಅನುಮೋದಿಸಿ",
"paired-device-removed": "ಜೋಡಿಸಲಾದ ಸಾಧನವನ್ನು ತೆಗೆದುಹಾಕಲಾಗಿದೆ.",
"share-text-title": "ಪಠ್ಯ ಸಂದೇಶವನ್ನು ಹಂಚಿಕೊಳ್ಳಿ",
"share-text-subtitle": "ಸಂದೇಶವನ್ನು ಕಳುಹಿಸುವ ಮೊದಲು ಎಡಿಟ್ ಮಾಡಿ:",
"base64-title-text": "ಪಠ್ಯವನ್ನು ಹಂಚಿಕೊಳ್ಳಿ"
}, },
"footer": { "footer": {
"webrtc": "WebRTC ಲಭ್ಯವಿಲ್ಲದಿದ್ದರೆ.", "webrtc": "WebRTC ಲಭ್ಯವಿಲ್ಲದಿದ್ದರೆ.",
@@ -137,7 +147,15 @@
"x-instructions_data-drop-peer": "ಪೀರ್‌ಗೆ ಕಳುಹಿಸಲು ಬಿಡುಗಡೆ ಮಾಡಿ", "x-instructions_data-drop-peer": "ಪೀರ್‌ಗೆ ಕಳುಹಿಸಲು ಬಿಡುಗಡೆ ಮಾಡಿ",
"x-instructions_data-drop-bg": "ಸ್ವೀಕರಿಸುವವರನ್ನು ಆಯ್ಕೆ ಮಾಡಲು ಬಿಡುಗಡೆ ಮಾಡಿ", "x-instructions_data-drop-bg": "ಸ್ವೀಕರಿಸುವವರನ್ನು ಆಯ್ಕೆ ಮಾಡಲು ಬಿಡುಗಡೆ ಮಾಡಿ",
"no-peers_data-drop-bg": "ಸ್ವೀಕರಿಸುವವರನ್ನು ಆಯ್ಕೆ ಮಾಡಲು ಬಿಡುಗಡೆ ಮಾಡಿ", "no-peers_data-drop-bg": "ಸ್ವೀಕರಿಸುವವರನ್ನು ಆಯ್ಕೆ ಮಾಡಲು ಬಿಡುಗಡೆ ಮಾಡಿ",
"webrtc-requirement": "ಈ PairDrop ನಿದರ್ಶನವನ್ನು ಬಳಸಲು, WebRTC ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಬೇಕು!" "webrtc-requirement": "ಈ PairDrop ನಿದರ್ಶನವನ್ನು ಬಳಸಲು, WebRTC ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಬೇಕು!",
"activate-share-mode-base": "ಕಳುಹಿಸಲು ಇತರ ಸಾಧನಗಳಲ್ಲಿ PairDrop ತೆರೆಯಿರಿ",
"activate-share-mode-shared-files-plural": "{{count}} ಹಂಚಿದ ಫೈಲ್‌ಗಳು",
"x-instructions-share-mode_desktop": "{{descriptor}} ಕಳುಹಿಸಲು ಕ್ಲಿಕ್ ಮಾಡಿ",
"activate-share-mode-shared-file": "ಹಂಚಿದ ಫೈಲ್",
"activate-share-mode-and-other-file": "ಮತ್ತು ಇತರ 1 ಫೈಲ್",
"x-instructions-share-mode_mobile": "{{descriptor}} ಕಳುಹಿಸಲು ಟ್ಯಾಪ್ ಮಾಡಿ",
"activate-share-mode-and-other-files-plural": "ಮತ್ತು ಇತರ {{count}} ಫೈಲ್‌ಗಳು",
"activate-share-mode-shared-text": "ಹಂಚಿದ ಪಠ್ಯ"
}, },
"peer-ui": { "peer-ui": {
"processing": "ಪ್ರಕ್ರಿಯೆಗೊಳಿಸಲಾಗುತ್ತಿದೆ…", "processing": "ಪ್ರಕ್ರಿಯೆಗೊಳಿಸಲಾಗುತ್ತಿದೆ…",
@@ -146,7 +164,8 @@
"waiting": "ನಿರೀಕ್ಷಿಸಲಾಗುತ್ತಿದೆ…", "waiting": "ನಿರೀಕ್ಷಿಸಲಾಗುತ್ತಿದೆ…",
"connection-hash": "ಎಂಡ್-ಟು-ಎಂಡ್ ಎನ್‌ಕ್ರಿಪ್ಶನ್‌ನ ಭದ್ರತೆಯನ್ನು ಪರಿಶೀಲಿಸಲು, ಎರಡೂ ಸಾಧನಗಳಲ್ಲಿ ಈ ಭದ್ರತಾ ಸಂಖ್ಯೆಯನ್ನು ಹೋಲಿಸಿ", "connection-hash": "ಎಂಡ್-ಟು-ಎಂಡ್ ಎನ್‌ಕ್ರಿಪ್ಶನ್‌ನ ಭದ್ರತೆಯನ್ನು ಪರಿಶೀಲಿಸಲು, ಎರಡೂ ಸಾಧನಗಳಲ್ಲಿ ಈ ಭದ್ರತಾ ಸಂಖ್ಯೆಯನ್ನು ಹೋಲಿಸಿ",
"preparing": "ಸಿದ್ಧಪಡಿಸಲಾಗುತ್ತಿದೆ…", "preparing": "ಸಿದ್ಧಪಡಿಸಲಾಗುತ್ತಿದೆ…",
"transferring": "ವರ್ಗಾಯಿಸಲಾಗುತ್ತಿದೆ…" "transferring": "ವರ್ಗಾಯಿಸಲಾಗುತ್ತಿದೆ…",
"click-to-send-share-mode": "{{descriptor}} ಕಳುಹಿಸಲು ಕ್ಲಿಕ್ ಮಾಡಿ"
}, },
"about": { "about": {
"claim": "ಸಾಧನಗಳಾದ್ಯಂತ ಫೈಲ್‌ಗಳನ್ನು ವರ್ಗಾಯಿಸಲು ಸುಲಭವಾದ ಮಾರ್ಗ", "claim": "ಸಾಧನಗಳಾದ್ಯಂತ ಫೈಲ್‌ಗಳನ್ನು ವರ್ಗಾಯಿಸಲು ಸುಲಭವಾದ ಮಾರ್ಗ",
+20 -6
View File
@@ -11,7 +11,8 @@
"pair-device_title": "Emparelhar seus dispositivos permanentemente", "pair-device_title": "Emparelhar seus dispositivos permanentemente",
"edit-paired-devices_title": "Editar dispositivos emparelhados", "edit-paired-devices_title": "Editar dispositivos emparelhados",
"join-public-room_title": "Entrar em uma sala pública temporariamente", "join-public-room_title": "Entrar em uma sala pública temporariamente",
"cancel-share-mode": "Concluído" "cancel-share-mode": "Concluído",
"edit-share-mode": "Editar"
}, },
"instructions": { "instructions": {
"no-peers_data-drop-bg": "Solte para selecionar o destinatário", "no-peers_data-drop-bg": "Solte para selecionar o destinatário",
@@ -25,7 +26,11 @@
"x-instructions-share-mode_mobile": "Toque para enviar", "x-instructions-share-mode_mobile": "Toque para enviar",
"activate-share-mode-base": "Abra o PairDrop em outros dispositivos para enviar", "activate-share-mode-base": "Abra o PairDrop em outros dispositivos para enviar",
"activate-share-mode-and-other-files-plural": "e {{count}} outros arquivos", "activate-share-mode-and-other-files-plural": "e {{count}} outros arquivos",
"activate-share-mode-shared-text": "texto compartilhado" "activate-share-mode-shared-text": "texto compartilhado",
"activate-share-mode-shared-files-plural": "{{count}} arquivos compartilhados",
"activate-share-mode-shared-file": "arquivo compartilhado",
"activate-share-mode-and-other-file": "e 1 outro arquivo",
"webrtc-requirement": "Para usar essa instância do PairDrop, o WebRTC deve estar habilitado!"
}, },
"footer": { "footer": {
"known-as": "Você é conhecido como:", "known-as": "Você é conhecido como:",
@@ -69,14 +74,14 @@
"share": "Compartilhar", "share": "Compartilhar",
"download": "Baixar", "download": "Baixar",
"send-message-title": "Enviar Mensagem", "send-message-title": "Enviar Mensagem",
"send-message-to": "Enviar uma Mensagem para", "send-message-to": "Para:",
"message_title": "Insira a mensagem a ser enviada", "message_title": "Insira a mensagem a ser enviada",
"send": "Enviar", "send": "Enviar",
"receive-text-title": "Mensagem Recebida", "receive-text-title": "Mensagem Recebida",
"copy": "Copiar", "copy": "Copiar",
"base64-processing": "Processando…", "base64-processing": "Processando…",
"base64-tap-to-paste": "Toque aqui para colar {{type}}", "base64-tap-to-paste": "Toque aqui para compartilhar {{type}}",
"base64-paste-to-send": "Cole aqui para enviar {{type}}", "base64-paste-to-send": "Cole da área de transferência aqui para compartilhar {{type}}",
"base64-text": "texto", "base64-text": "texto",
"base64-files": "arquivos", "base64-files": "arquivos",
"file-other-description-image": "e mais 1 imagem", "file-other-description-image": "e mais 1 imagem",
@@ -92,7 +97,16 @@
"language-selector-title": "Definir idioma", "language-selector-title": "Definir idioma",
"system-language": "Idioma do sistema", "system-language": "Idioma do sistema",
"public-room-qr-code_title": "Clique para copiar o link da sala pública", "public-room-qr-code_title": "Clique para copiar o link da sala pública",
"pair-devices-qr-code_title": "Clique para copiar o link para emparelhar este dispositivo" "pair-devices-qr-code_title": "Clique para copiar o link para emparelhar este dispositivo",
"message_placeholder": "Texto",
"close-toast_title": "Fechar notificação",
"share-text-checkbox": "Sempre exibir essa mensagem ao compartilhar texto",
"base64-title-files": "Compartilhar arquivos",
"approve": "aprovar",
"paired-device-removed": "Dispositivo pareado foi removido.",
"share-text-title": "Compartilhar Mensagem de Texto",
"share-text-subtitle": "Editar mensagem antes de enviar:",
"base64-title-text": "Compartilhar texto"
}, },
"about": { "about": {
"close-about_aria-label": "Fechar Sobre o PairDrop", "close-about_aria-label": "Fechar Sobre o PairDrop",
+7 -7
View File
@@ -1,7 +1,7 @@
{ {
"header": { "header": {
"about_title": "PairDrop Hakkında", "about_title": "PairDrop Hakkında",
"about_aria-label": "PairDrop Hakkında Aç", "about_aria-label": "PairDrop Hakkındayı Aç",
"theme-auto_title": "Temayı sisteme uyarla", "theme-auto_title": "Temayı sisteme uyarla",
"theme-light_title": "Daima açık tema kullan", "theme-light_title": "Daima açık tema kullan",
"theme-dark_title": "Daima koyu tema kullan", "theme-dark_title": "Daima koyu tema kullan",
@@ -17,13 +17,13 @@
"instructions": { "instructions": {
"no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın", "no-peers_data-drop-bg": "Alıcıyı seçmek için bırakın",
"x-instructions_mobile": "Dosya göndermek için dokun veya mesaj göndermek için uzun dokun", "x-instructions_mobile": "Dosya göndermek için dokun veya mesaj göndermek için uzun dokun",
"x-instructions-share-mode_desktop": "Göndermek için tıkla {{descriptor}}", "x-instructions-share-mode_desktop": "{{descriptor}} kişisine göndermek için tıkla",
"activate-share-mode-and-other-files-plural": "ve {{count}} diğer dosya", "activate-share-mode-and-other-files-plural": "ve {{count}} diğer dosya",
"x-instructions-share-mode_mobile": "Göndermek için dokun {{descriptor}}", "x-instructions-share-mode_mobile": "{{descriptor}} kişisine göndermek için dokun",
"activate-share-mode-base": "Göndermek için diğer cihazlarda PairDrop'u açın", "activate-share-mode-base": "Göndermek için diğer cihazlarda PairDrop'u açın",
"no-peers-subtitle": "Diğer ağlarda keşfedilebilir olmak için cihazları eşleştirin veya ortak bir odaya girin", "no-peers-subtitle": "Diğer ağlarda keşfedilebilir olmak için cihazları eşleştirin veya ortak bir odaya girin",
"activate-share-mode-shared-text": "paylaşılan metin", "activate-share-mode-shared-text": "paylaşılan metin",
"x-instructions_desktop": "Dosya göndermek için tıkla ya da mesaj göndermek için sağ tıkla", "x-instructions_desktop": "Dosya göndermek için tıkla veya mesaj göndermek için sağ tıkla",
"no-peers-title": "Dosya göndermek için diğer cihazlarda PairDrop'u açın", "no-peers-title": "Dosya göndermek için diğer cihazlarda PairDrop'u açın",
"x-instructions_data-drop-peer": "Göndermek için serbest bırak", "x-instructions_data-drop-peer": "Göndermek için serbest bırak",
"x-instructions_data-drop-bg": "Alıcıyı seçmek için bırakın", "x-instructions_data-drop-bg": "Alıcıyı seçmek için bırakın",
@@ -131,11 +131,11 @@
"public-room-left": "{{publicRoomId}} genel odasından ayrıldın", "public-room-left": "{{publicRoomId}} genel odasından ayrıldın",
"copied-text": "Metin panoya kopyalandı", "copied-text": "Metin panoya kopyalandı",
"display-name-random-again": "Mevcut adın tekrardan rastgele oluşturuldu", "display-name-random-again": "Mevcut adın tekrardan rastgele oluşturuldu",
"display-name-changed-permanently": "Mevcut adın kalıcı olarak değiştirilir", "display-name-changed-permanently": "Mevcut adın kalıcı olarak değiştirildi",
"copied-to-clipboard-error": "Kopyalama mümkün değil. Manuel olarak kopyalayın.", "copied-to-clipboard-error": "Kopyalama mümkün değil. Manuel olarak kopyalayın.",
"pairing-success": "Cihazlar eşleştirildi", "pairing-success": "Cihazlar eşleştirildi",
"clipboard-content-incorrect": "Pano içeriği yanlış", "clipboard-content-incorrect": "Pano içeriği yanlış",
"display-name-changed-temporarily": "Mevcut adın yalnızca bu oturum için değiştirilir", "display-name-changed-temporarily": "Mevcut adın yalnızca bu oturum için değiştirildi",
"copied-to-clipboard": "Panoya kopyalandı", "copied-to-clipboard": "Panoya kopyalandı",
"offline": "Çevrimdışısın", "offline": "Çevrimdışısın",
"pairing-tabs-error": "İki web tarayıcı sekmesini eşleştirmek mümkün değildir", "pairing-tabs-error": "İki web tarayıcı sekmesini eşleştirmek mümkün değildir",
@@ -149,7 +149,7 @@
"room-url-copied-to-clipboard": "Genel oda bağlantı linki panoya kopyalandı", "room-url-copied-to-clipboard": "Genel oda bağlantı linki panoya kopyalandı",
"copied-text-error": "Panoya kopyalanamadı. Lütfen manuel olarak kopyalayın!", "copied-text-error": "Panoya kopyalanamadı. Lütfen manuel olarak kopyalayın!",
"download-successful": "{{descriptor}} indirildi", "download-successful": "{{descriptor}} indirildi",
"click-to-show": "Göstermek için tıkla" "click-to-show": "Görmek için tıkla"
}, },
"peer-ui": { "peer-ui": {
"processing": "İşleniyor…", "processing": "İşleniyor…",
+91 -51
View File
@@ -1,40 +1,49 @@
class Localization { class Localization {
constructor() { constructor() {
Localization.$htmlRoot = document.querySelector('html');
Localization.defaultLocale = "en"; Localization.defaultLocale = "en";
Localization.supportedLocales = ["ar", "de", "en", "es", "fr", "id", "it", "ja", "nb", "nl", "ro", "ru", "tr", "zh-CN","pt-BR"]; Localization.supportedLocales = ["ar", "ca", "de", "en", "es", "fr", "id", "it", "ja", "kn", "nb", "nl", "pt-BR", "ro", "ru", "tr", "zh-CN"];
Localization.supportedLocalesRtl = ["ar"]; Localization.supportedLocalesRtl = ["ar"];
Localization.translations = {}; Localization.translations = {};
Localization.defaultTranslations = {}; Localization.translationsDefaultLocale = {};
Localization.systemLocale = Localization.getSupportedOrDefault(navigator.languages); Localization.systemLocale = Localization.getSupportedOrDefaultLocales(navigator.languages);
let storedLanguageCode = localStorage.getItem('language_code'); let storedLanguageCode = localStorage.getItem('language_code');
Localization.initialLocale = storedLanguageCode && Localization.isSupported(storedLanguageCode) Localization.initialLocale = storedLanguageCode && Localization.localeIsSupported(storedLanguageCode)
? storedLanguageCode ? storedLanguageCode
: Localization.systemLocale; : Localization.systemLocale;
} }
static isSupported(locale) { static localeIsSupported(locale) {
return Localization.supportedLocales.indexOf(locale) > -1; return Localization.supportedLocales.indexOf(locale) > -1;
} }
static isRtlLanguage(locale) { static localeIsRtl(locale) {
return Localization.supportedLocalesRtl.indexOf(locale) > -1; return Localization.supportedLocalesRtl.indexOf(locale) > -1;
} }
static isCurrentLocaleRtl() { static currentLocaleIsRtl() {
return Localization.isRtlLanguage(Localization.locale); return Localization.localeIsRtl(Localization.locale);
} }
static getSupportedOrDefault(locales) { static currentLocaleIsDefault() {
return Localization.locale === Localization.defaultLocale
}
static getSupportedOrDefaultLocales(locales) {
// get generic locales not included in locales
// ["en-us", "de-CH", "fr"] --> ["en", "de"]
let localesGeneric = locales let localesGeneric = locales
.map(locale => locale.split("-")[0]) .map(locale => locale.split("-")[0])
.filter(locale => locales.indexOf(locale) === -1); .filter(locale => locales.indexOf(locale) === -1);
return locales.find(Localization.isSupported) // If there is no perfect match for browser locales, try generic locales first before resorting to the default locale
|| localesGeneric.find(Localization.isSupported) return locales.find(Localization.localeIsSupported)
|| localesGeneric.find(Localization.localeIsSupported)
|| Localization.defaultLocale; || Localization.defaultLocale;
} }
@@ -48,16 +57,14 @@ class Localization {
await Localization.setLocale(locale) await Localization.setLocale(locale)
await Localization.translatePage(); await Localization.translatePage();
const htmlRootNode = document.querySelector('html'); if (Localization.localeIsRtl(locale)) {
Localization.$htmlRoot.setAttribute('dir', 'rtl');
if (Localization.isRtlLanguage(locale)) {
htmlRootNode.setAttribute('dir', 'rtl');
} }
else { else {
htmlRootNode.removeAttribute('dir'); Localization.$htmlRoot.removeAttribute('dir');
} }
htmlRootNode.setAttribute('lang', locale); Localization.$htmlRoot.setAttribute('lang', locale);
console.log("Page successfully translated", console.log("Page successfully translated",
@@ -111,75 +118,108 @@ class Localization {
const key = element.getAttribute("data-i18n-key"); const key = element.getAttribute("data-i18n-key");
const attrs = element.getAttribute("data-i18n-attrs").split(" "); const attrs = element.getAttribute("data-i18n-attrs").split(" ");
for (let i in attrs) { attrs.forEach(attr => {
let attr = attrs[i];
if (attr === "text") { if (attr === "text") {
element.innerText = Localization.getTranslation(key); element.innerText = Localization.getTranslation(key);
} }
else { else {
element.setAttribute(attr, Localization.getTranslation(key, attr)); element.setAttribute(attr, Localization.getTranslation(key, attr));
} }
} })
} }
static getTranslation(key, attr = null, data = {}, useDefault = false) { static getTranslationFromTranslationsObj(translationObj, key, attr) {
const keys = key.split(".");
let translationCandidates = useDefault
? Localization.defaultTranslations
: Localization.translations;
let translation; let translation;
try { try {
const keys = key.split(".");
for (let i = 0; i < keys.length - 1; i++) { for (let i = 0; i < keys.length - 1; i++) {
translationCandidates = translationCandidates[keys[i]] // iterate into translation object until last layer
translationObj = translationObj[keys[i]]
} }
let lastKey = keys[keys.length - 1]; let lastKey = keys[keys.length - 1];
if (attr) lastKey += "_" + attr; if (attr) lastKey += "_" + attr;
translation = translationCandidates[lastKey]; translation = translationObj[lastKey];
for (let j in data) {
if (translation.includes(`{{${j}}}`)) {
translation = translation.replace(`{{${j}}}`, data[j]);
} else {
console.warn(`Translation for your language ${Localization.locale.toUpperCase()} misses at least one data placeholder:`, key, attr, data);
Localization.logHelpCallKey(key);
Localization.logHelpCall();
translation = "";
break;
}
}
} catch (e) { } catch (e) {
console.error(e); console.error(e);
translation = "";
} }
if (!translation) { if (!translation) {
if (!useDefault) { throw new Error(`Translation misses entry. Key: ${key} Attribute: ${attr}`);
console.warn(`Missing translation entry for your language ${Localization.locale.toUpperCase()}. Using ${Localization.defaultLocale.toUpperCase()} instead.`, key, attr); }
Localization.logHelpCallKey(key);
Localization.logHelpCall(); return translation;
translation = this.getTranslation(key, attr, data, true); }
static addDataToTranslation(translation, data) {
for (let j in data) {
if (!translation.includes(`{{${j}}}`)) {
throw new Error(`Translation misses data placeholder: ${j}`);
}
// Add data to translation
translation = translation.replace(`{{${j}}}`, data[j]);
}
return translation;
}
static getTranslation(key, attr = null, data = {}, useDefault = false) {
let translationObj = useDefault
? Localization.translationsDefaultLocale
: Localization.translations;
let translation;
try {
translation = Localization.getTranslationFromTranslationsObj(translationObj, key, attr);
translation = Localization.addDataToTranslation(translation, data);
}
catch (e) {
// Log warnings and help calls
console.warn(e);
Localization.logTranslationMissingOrBroken(key, attr, data, useDefault);
Localization.logHelpCallKey(key, attr);
Localization.logHelpCall();
if (useDefault || Localization.currentLocaleIsDefault()) {
// Is default locale already
// Use empty string as translation
translation = ""
} }
else { else {
console.warn("Missing translation in default language:", key, attr); // Is not default locale yet
Localization.logHelpCall(); // Get translation for default language with same arguments
console.log(`Using default language ${Localization.defaultLocale.toUpperCase()} instead.`);
translation = this.getTranslation(key, attr, data, true);
} }
} }
return Localization.escapeHTML(translation); return Localization.escapeHTML(translation);
} }
static logTranslationMissingOrBroken(key, attr, data, useDefault) {
let usedLocale = useDefault
? Localization.defaultLocale.toUpperCase()
: Localization.locale.toUpperCase();
console.warn(`Missing or broken translation for language ${usedLocale}.\n`, 'key:', key, 'attr:', attr, 'data:', data);
}
static logHelpCall() { static logHelpCall() {
console.log("Help translating PairDrop: https://hosted.weblate.org/engage/pairdrop/"); console.log("Help translating PairDrop: https://hosted.weblate.org/engage/pairdrop/");
} }
static logHelpCallKey(key) { static logHelpCallKey(key, attr) {
console.warn(`Translate this string here: https://hosted.weblate.org/browse/pairdrop/pairdrop-spa/${Localization.locale.toLowerCase()}/?q=${key}`); let locale = Localization.locale.toLowerCase();
let keyComplete = !attr || attr === "text"
? key
: `${key}_${attr}`;
console.warn(`Translate this string here: https://hosted.weblate.org/browse/pairdrop/pairdrop-spa/${locale}/?q=${keyComplete}`);
} }
static escapeHTML(unsafeText) { static escapeHTML(unsafeText) {
+39 -26
View File
@@ -7,7 +7,7 @@ class PairDrop {
this.$headerInstallBtn = $('install'); this.$headerInstallBtn = $('install');
this.deferredStyles = [ this.deferredStyles = [
"styles/deferred-styles.css" "styles/styles-deferred.css"
]; ];
this.deferredScripts = [ this.deferredScripts = [
"scripts/browser-tabs-connector.js", "scripts/browser-tabs-connector.js",
@@ -56,13 +56,16 @@ class PairDrop {
await this.backgroundCanvas.fadeIn(); await this.backgroundCanvas.fadeIn();
// Load deferred assets // Load deferred assets
console.log("Load deferred assets...");
await this.loadDeferredAssets(); await this.loadDeferredAssets();
console.log("Loading of deferred assets completed."); console.log("Loading of deferred assets completed.");
console.log("Hydrate UI...");
await this.hydrate(); await this.hydrate();
console.log("UI hydrated."); console.log("UI hydrated.");
// Evaluate url params as soon as ws is connected // Evaluate url params as soon as ws is connected
console.log("Evaluate URL params as soon as websocket connection is established.");
Events.on('ws-connected', _ => this.evaluateUrlParams(), {once: true}); Events.on('ws-connected', _ => this.evaluateUrlParams(), {once: true});
} }
@@ -102,36 +105,40 @@ class PairDrop {
} }
} }
async loadDeferredAssets() { loadDeferredAssets() {
console.log("Load deferred assets"); const stylePromises = this.deferredStyles.map(url => this.loadAndApplyStylesheet(url));
for (const url of this.deferredStyles) { const scriptPromises = this.deferredScripts.map(url => this.loadAndApplyScript(url));
await this.loadAndApplyStylesheet(url);
} return Promise.all([...stylePromises, ...scriptPromises]);
for (const url of this.deferredScripts) {
await this.loadAndApplyScript(url);
}
} }
loadStyleSheet(url) { loadStyleSheet(url) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let stylesheet = document.createElement('link'); let stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet'; stylesheet.rel = 'preload';
stylesheet.as = 'style';
stylesheet.href = url; stylesheet.href = url;
stylesheet.type = 'text/css'; stylesheet.onload = _ => {
stylesheet.onload = resolve; stylesheet.onload = null;
stylesheet.rel = 'stylesheet';
resolve();
};
stylesheet.onerror = reject; stylesheet.onerror = reject;
document.head.appendChild(stylesheet); document.head.appendChild(stylesheet);
}); });
} }
async loadAndApplyStylesheet(url) { loadAndApplyStylesheet(url) {
try { return new Promise( async (resolve) => {
await this.loadStyleSheet(url); try {
console.log(`Stylesheet loaded successfully: ${url}`); await this.loadStyleSheet(url);
} catch (error) { console.log(`Stylesheet loaded successfully: ${url}`);
console.error('Error loading stylesheet:', error); resolve();
} } catch (error) {
console.error('Error loading stylesheet:', error);
}
});
} }
loadScript(url) { loadScript(url) {
@@ -145,16 +152,20 @@ class PairDrop {
}); });
} }
async loadAndApplyScript(url) { loadAndApplyScript(url) {
try { return new Promise( async (resolve) => {
await this.loadScript(url); try {
console.log(`Script loaded successfully: ${url}`); await this.loadScript(url);
} catch (error) { console.log(`Script loaded successfully: ${url}`);
console.error('Error loading script:', error); resolve();
} } catch (error) {
console.error('Error loading script:', error);
}
});
} }
async hydrate() { async hydrate() {
this.aboutUI = new AboutUI();
this.peersUI = new PeersUI(); this.peersUI = new PeersUI();
this.languageSelectDialog = new LanguageSelectDialog(); this.languageSelectDialog = new LanguageSelectDialog();
this.receiveFileDialog = new ReceiveFileDialog(); this.receiveFileDialog = new ReceiveFileDialog();
@@ -222,6 +233,8 @@ class PairDrop {
// remove url params from url // remove url params from url
const urlWithoutParams = getUrlWithoutArguments(); const urlWithoutParams = getUrlWithoutArguments();
window.history.replaceState({}, "Rewrite URL", urlWithoutParams); window.history.replaceState({}, "Rewrite URL", urlWithoutParams);
console.log("URL params evaluated.");
} }
} }
+1 -1
View File
@@ -132,7 +132,7 @@ class HeaderUI {
this.$header.classList.remove('overflow-expanded'); this.$header.classList.remove('overflow-expanded');
const rtlLocale = Localization.isCurrentLocaleRtl(); const rtlLocale = Localization.currentLocaleIsRtl();
let icon; let icon;
const $headerIconsShown = document.querySelectorAll('body > header:first-of-type > *:not([hidden])'); const $headerIconsShown = document.querySelectorAll('body > header:first-of-type > *:not([hidden])');
+73 -23
View File
@@ -1978,12 +1978,15 @@ class ReceiveTextDialog extends Dialog {
this._receiveTextQueue = []; this._receiveTextQueue = [];
} }
selectionEmpty() {
return !window.getSelection().toString()
}
async _onKeyDown(e) { async _onKeyDown(e) {
if (!this.isShown()) return if (!this.isShown()) return
if (e.code === "KeyC" && (e.ctrlKey || e.metaKey)) { if (e.code === "KeyC" && (e.ctrlKey || e.metaKey) && this.selectionEmpty()) {
await this._onCopy() await this._onCopy()
this.hide();
} }
else if (e.code === "Escape") { else if (e.code === "Escape") {
this.hide(); this.hide();
@@ -2014,10 +2017,19 @@ class ReceiveTextDialog extends Dialog {
// Beautify text if text is short // Beautify text if text is short
if (text.length < 2000) { if (text.length < 2000) {
// replace urls with actual links // replace URLs with actual links
this.$text.innerHTML = this.$text.innerHTML.replace(/((https?:\/\/|www)[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\-._~:\/?#\[\]@!$&'()*+,;=]+)/g, url => { this.$text.innerHTML = this.$text.innerHTML
return `<a href="${url}" target="_blank">${url}</a>`; .replace(/(^|(?<=(<br>|\s)))(https?:\/\/|www.)(([a-z]|[A-Z]|[0-9]|[\-_~:\/?#\[\]@!$&'()*+,;=%]){2,}\.)(([a-z]|[A-Z]|[0-9]|[\-_~:\/?#\[\]@!$&'()*+,;=%.]){2,})/g,
}); (url) => {
let link = url;
// prefix www.example.com with http protocol to prevent it from being a relative link
if (link.startsWith('www')) {
link = "http://" + link
}
return `<a href="${link}" target="_blank">${url}</a>`;
});
} }
this._evaluateOverflowing(this.$text); this._evaluateOverflowing(this.$text);
@@ -2049,7 +2061,10 @@ class ReceiveTextDialog extends Dialog {
hide() { hide() {
super.hide(); super.hide();
setTimeout(() => this._dequeueRequests(), 500); setTimeout(() => {
this._dequeueRequests();
this.$text.innerHTML = "";
}, 500);
} }
} }
@@ -2275,6 +2290,53 @@ class Base64Dialog extends Dialog {
} }
} }
class AboutUI {
constructor() {
this.$donationBtn = $('donation-btn');
this.$twitterBtn = $('twitter-btn');
this.$mastodonBtn = $('mastodon-btn');
this.$blueskyBtn = $('bluesky-btn');
this.$customBtn = $('custom-btn');
this.$privacypolicyBtn = $('privacypolicy-btn');
Events.on('config', e => this._onConfig(e.detail.buttons));
}
async _onConfig(btnConfig) {
await this._evaluateBtnConfig(this.$donationBtn, btnConfig.donation_button);
await this._evaluateBtnConfig(this.$twitterBtn, btnConfig.twitter_button);
await this._evaluateBtnConfig(this.$mastodonBtn, btnConfig.mastodon_button);
await this._evaluateBtnConfig(this.$blueskyBtn, btnConfig.bluesky_button);
await this._evaluateBtnConfig(this.$customBtn, btnConfig.custom_button);
await this._evaluateBtnConfig(this.$privacypolicyBtn, btnConfig.privacypolicy_button);
}
async _evaluateBtnConfig($btn, config) {
// if config is not set leave everything as default
if (!Object.keys(config).length) return;
if (config.active === "false") {
$btn.setAttribute('hidden', true);
} else {
if (config.link) {
$btn.setAttribute('href', config.link);
}
if (config.title) {
$btn.setAttribute('title', config.title);
// prevent overwriting of custom title when setting different language
$btn.removeAttribute('data-i18n-key');
$btn.removeAttribute('data-i18n-attrs');
}
if (config.icon) {
$btn.setAttribute('title', config.title);
// prevent overwriting of custom title when setting different language
$btn.removeAttribute('data-i18n-key');
$btn.removeAttribute('data-i18n-attrs');
}
$btn.removeAttribute('hidden');
}
}
}
class Toast extends Dialog { class Toast extends Dialog {
constructor() { constructor() {
super('toast'); super('toast');
@@ -2373,14 +2435,9 @@ class Notifications {
_downloadNotification(files) { _downloadNotification(files) {
if (document.visibilityState !== 'visible') { if (document.visibilityState !== 'visible') {
let imagesOnly = true; let imagesOnly = files.every(file => file.type.split('/')[0] === 'image');
for(let i=0; i<files.length; i++) {
if (files[i].type.split('/')[0] !== 'image') {
imagesOnly = false;
break;
}
}
let title; let title;
if (files.length === 1) { if (files.length === 1) {
title = `${files[0].name}`; title = `${files[0].name}`;
} }
@@ -2405,15 +2462,8 @@ class Notifications {
_requestNotification(request, peerId) { _requestNotification(request, peerId) {
if (document.visibilityState !== 'visible') { if (document.visibilityState !== 'visible') {
let imagesOnly = true; let imagesOnly = request.header.every(header => header.mime.split('/')[0] === 'image');
for(let i=0; i<request.header.length; i++) { let displayName = $(peerId).querySelector('.name').textContent;
if (request.header[i].mime.split('/')[0] !== 'image') {
imagesOnly = false;
break;
}
}
let displayName = $(peerId).querySelector('.name').textContent
let descriptor; let descriptor;
if (request.header.length === 1) { if (request.header.length === 1) {
+6 -3
View File
@@ -1,4 +1,4 @@
const cacheVersion = 'v1.10.0'; const cacheVersion = 'v1.10.3';
const cacheTitle = `pairdrop-cache-${cacheVersion}`; const cacheTitle = `pairdrop-cache-${cacheVersion}`;
const forceFetch = false; // FOR DEVELOPMENT: Set to true to always update assets instead of using cached versions const forceFetch = false; // FOR DEVELOPMENT: Set to true to always update assets instead of using cached versions
const relativePathsToCache = [ const relativePathsToCache = [
@@ -6,7 +6,7 @@ const relativePathsToCache = [
'index.html', 'index.html',
'manifest.json', 'manifest.json',
'styles/styles-main.css', 'styles/styles-main.css',
'styles/deferred-styles.css', 'styles/styles-deferred.css',
'scripts/localization.js', 'scripts/localization.js',
'scripts/main.js', 'scripts/main.js',
'scripts/network.js', 'scripts/network.js',
@@ -27,6 +27,7 @@ const relativePathsToCache = [
'images/android-chrome-512x512-maskable.png', 'images/android-chrome-512x512-maskable.png',
'images/apple-touch-icon.png', 'images/apple-touch-icon.png',
'lang/ar.json', 'lang/ar.json',
'lang/ca.json',
'lang/de.json', 'lang/de.json',
'lang/en.json', 'lang/en.json',
'lang/es.json', 'lang/es.json',
@@ -34,11 +35,13 @@ const relativePathsToCache = [
'lang/id.json', 'lang/id.json',
'lang/it.json', 'lang/it.json',
'lang/ja.json', 'lang/ja.json',
'lang/kn.json',
'lang/nb.json', 'lang/nb.json',
'lang/nl.json', 'lang/nl.json',
'lang/tr.json', 'lang/pt-BR.json',
'lang/ro.json', 'lang/ro.json',
'lang/ru.json', 'lang/ru.json',
'lang/tr.json',
'lang/zh-CN.json' 'lang/zh-CN.json'
]; ];
const relativePathsNotToCache = [ const relativePathsNotToCache = [
@@ -16,6 +16,7 @@
max-height: 350px; max-height: 350px;
word-break: break-word; word-break: break-word;
word-wrap: anywhere; word-wrap: anywhere;
white-space: pre-wrap;
} }
.textarea:before { .textarea:before {
@@ -335,6 +336,7 @@ x-dialog x-paper {
display: flex; display: flex;
margin: auto; margin: auto;
flex-direction: column; flex-direction: column;
width: 100%;
max-width: 450px; max-width: 450px;
z-index: 3; z-index: 3;
border-radius: 30px; border-radius: 30px;
@@ -382,10 +384,6 @@ x-dialog:not([show]) x-paper {
transform: scale(0.1); transform: scale(0.1);
} }
x-dialog a {
color: var(--primary-color);
}
/* Pair Devices Dialog & Public Room Dialog */ /* Pair Devices Dialog & Public Room Dialog */
.input-key-container { .input-key-container {
@@ -784,7 +782,7 @@ x-dialog x-paper {
background-color: var(--bg-color-secondary) !important; background-color: var(--bg-color-secondary) !important;
} }
.textarea * { .textarea *:not(a) {
margin: 0 !important; margin: 0 !important;
padding: 0 !important; padding: 0 !important;
color: unset !important; color: unset !important;
@@ -797,6 +795,10 @@ x-dialog x-paper {
font-weight: unset !important; font-weight: unset !important;
} }
x-dialog a {
color: var(--primary-color);
}
/* Image/Video/Audio Preview */ /* Image/Video/Audio Preview */
.file-preview { .file-preview {
margin-bottom: 15px; margin-bottom: 15px;
+1
View File
@@ -252,6 +252,7 @@ html[dir="rtl"] #expand.flipped > .icon {
@font-face { @font-face {
font-family: "Open Sans"; font-family: "Open Sans";
src: url('../fonts/OpenSans/static/OpenSans-Medium.ttf') format('truetype'); src: url('../fonts/OpenSans/static/OpenSans-Medium.ttf') format('truetype');
font-display: swap;
} }
body { body {
+44 -2
View File
@@ -32,10 +32,14 @@ process.on('unhandledRejection', (reason, promise) => {
// Evaluate arguments for deployment with Docker and Node.js // Evaluate arguments for deployment with Docker and Node.js
let conf = {}; let conf = {};
conf.debugMode = process.env.DEBUG_MODE === "true"; conf.debugMode = process.env.DEBUG_MODE === "true";
conf.port = process.env.PORT || 3000; conf.port = process.env.PORT || 3000;
conf.wsFallback = process.argv.includes('--include-ws-fallback') || process.env.WS_FALLBACK === "true"; conf.wsFallback = process.argv.includes('--include-ws-fallback') || process.env.WS_FALLBACK === "true";
conf.rtcConfig = process.env.RTC_CONFIG
conf.rtcConfig = process.env.RTC_CONFIG && process.env.RTC_CONFIG !== "false"
? JSON.parse(fs.readFileSync(process.env.RTC_CONFIG, 'utf8')) ? JSON.parse(fs.readFileSync(process.env.RTC_CONFIG, 'utf8'))
: { : {
"sdpSemantics": "unified-plan", "sdpSemantics": "unified-plan",
@@ -47,7 +51,10 @@ conf.rtcConfig = process.env.RTC_CONFIG
}; };
conf.signalingServer = process.env.SIGNALING_SERVER || false; conf.signalingServer = process.env.SIGNALING_SERVER && process.env.SIGNALING_SERVER !== "false"
? process.env.SIGNALING_SERVER
: false;
conf.ipv6Localize = parseInt(process.env.IPV6_LOCALIZE) || false; conf.ipv6Localize = parseInt(process.env.IPV6_LOCALIZE) || false;
let rateLimit = false; let rateLimit = false;
@@ -62,10 +69,45 @@ else {
} }
conf.rateLimit = rateLimit; conf.rateLimit = rateLimit;
conf.buttons = {
"donation_button": {
"active": process.env.DONATION_BUTTON_ACTIVE,
"link": process.env.DONATION_BUTTON_LINK,
"title": process.env.DONATION_BUTTON_TITLE
},
"twitter_button": {
"active": process.env.TWITTER_BUTTON_ACTIVE,
"link": process.env.TWITTER_BUTTON_LINK,
"title": process.env.TWITTER_BUTTON_TITLE
},
"mastodon_button": {
"active": process.env.MASTODON_BUTTON_ACTIVE,
"link": process.env.MASTODON_BUTTON_LINK,
"title": process.env.MASTODON_BUTTON_TITLE
},
"bluesky_button": {
"active": process.env.BLUESKY_BUTTON_ACTIVE,
"link": process.env.BLUESKY_BUTTON_LINK,
"title": process.env.BLUESKY_BUTTON_TITLE
},
"custom_button": {
"active": process.env.CUSTOM_BUTTON_ACTIVE,
"link": process.env.CUSTOM_BUTTON_LINK,
"title": process.env.CUSTOM_BUTTON_TITLE
},
"privacypolicy_button": {
"active": process.env.PRIVACYPOLICY_BUTTON_ACTIVE,
"link": process.env.PRIVACYPOLICY_BUTTON_LINK,
"title": process.env.PRIVACYPOLICY_BUTTON_TITLE
}
};
// Evaluate arguments for deployment with Node.js only // Evaluate arguments for deployment with Node.js only
conf.autoStart = process.argv.includes('--auto-restart'); conf.autoStart = process.argv.includes('--auto-restart');
conf.localhostOnly = process.argv.includes('--localhost-only'); conf.localhostOnly = process.argv.includes('--localhost-only');
// Validate configuration // Validate configuration
if (conf.ipv6Localize) { if (conf.ipv6Localize) {
if (!(0 < conf.ipv6Localize && conf.ipv6Localize < 8)) { if (!(0 < conf.ipv6Localize && conf.ipv6Localize < 8)) {
+2 -1
View File
@@ -49,7 +49,8 @@ export default class PairDropServer {
// By using `WS_SERVER`, you can host an instance that uses another signaling server. // By using `WS_SERVER`, you can host an instance that uses another signaling server.
app.get('/config', (req, res) => { app.get('/config', (req, res) => {
res.send({ res.send({
signalingServer: conf.signalingServer signalingServer: conf.signalingServer,
buttons: conf.buttons
}); });
}); });